summaryrefslogtreecommitdiff
path: root/spec/ruby/core/string/dump_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/ruby/core/string/dump_spec.rb')
-rw-r--r--spec/ruby/core/string/dump_spec.rb396
1 files changed, 396 insertions, 0 deletions
diff --git a/spec/ruby/core/string/dump_spec.rb b/spec/ruby/core/string/dump_spec.rb
new file mode 100644
index 0000000000..176be79db2
--- /dev/null
+++ b/spec/ruby/core/string/dump_spec.rb
@@ -0,0 +1,396 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#dump" do
+ it "does not take into account if a string is frozen" do
+ "foo".freeze.dump.should_not.frozen?
+ end
+
+ it "returns a String instance" do
+ StringSpecs::MyString.new.dump.should.instance_of?(String)
+ end
+
+ it "wraps string with \"" do
+ "foo".dump.should == '"foo"'
+ end
+
+ it "returns a string with special characters replaced with \\<char> notation" do
+ [ ["\a", '"\\a"'],
+ ["\b", '"\\b"'],
+ ["\t", '"\\t"'],
+ ["\n", '"\\n"'],
+ ["\v", '"\\v"'],
+ ["\f", '"\\f"'],
+ ["\r", '"\\r"'],
+ ["\e", '"\\e"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with \" and \\ escaped with a backslash" do
+ [ ["\"", '"\\""'],
+ ["\\", '"\\\\"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with \\#<char> when # is followed by $, @, @@, {" do
+ [ ["\#$PATH", '"\\#$PATH"'],
+ ["\#@a", '"\\#@a"'],
+ ["\#@@a", '"\\#@@a"'],
+ ["\#{a}", '"\\#{a}"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with # not escaped when followed by any other character" do
+ [ ["#", '"#"'],
+ ["#1", '"#1"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with printable non-alphanumeric characters unescaped" do
+ [ [" ", '" "'],
+ ["!", '"!"'],
+ ["$", '"$"'],
+ ["%", '"%"'],
+ ["&", '"&"'],
+ ["'", '"\'"'],
+ ["(", '"("'],
+ [")", '")"'],
+ ["*", '"*"'],
+ ["+", '"+"'],
+ [",", '","'],
+ ["-", '"-"'],
+ [".", '"."'],
+ ["/", '"/"'],
+ [":", '":"'],
+ [";", '";"'],
+ ["<", '"<"'],
+ ["=", '"="'],
+ [">", '">"'],
+ ["?", '"?"'],
+ ["@", '"@"'],
+ ["[", '"["'],
+ ["]", '"]"'],
+ ["^", '"^"'],
+ ["_", '"_"'],
+ ["`", '"`"'],
+ ["{", '"{"'],
+ ["|", '"|"'],
+ ["}", '"}"'],
+ ["~", '"~"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with numeric characters unescaped" do
+ [ ["0", '"0"'],
+ ["1", '"1"'],
+ ["2", '"2"'],
+ ["3", '"3"'],
+ ["4", '"4"'],
+ ["5", '"5"'],
+ ["6", '"6"'],
+ ["7", '"7"'],
+ ["8", '"8"'],
+ ["9", '"9"'],
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with upper-case alpha characters unescaped" do
+ [ ["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"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with lower-case alpha characters unescaped" do
+ [ ["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"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with non-printing ASCII characters replaced by \\x notation" do
+ # Avoid the file encoding by computing the string with #chr.
+ [ [0000.chr, '"\\x00"'],
+ [0001.chr, '"\\x01"'],
+ [0002.chr, '"\\x02"'],
+ [0003.chr, '"\\x03"'],
+ [0004.chr, '"\\x04"'],
+ [0005.chr, '"\\x05"'],
+ [0006.chr, '"\\x06"'],
+ [0016.chr, '"\\x0E"'],
+ [0017.chr, '"\\x0F"'],
+ [0020.chr, '"\\x10"'],
+ [0021.chr, '"\\x11"'],
+ [0022.chr, '"\\x12"'],
+ [0023.chr, '"\\x13"'],
+ [0024.chr, '"\\x14"'],
+ [0025.chr, '"\\x15"'],
+ [0026.chr, '"\\x16"'],
+ [0027.chr, '"\\x17"'],
+ [0030.chr, '"\\x18"'],
+ [0031.chr, '"\\x19"'],
+ [0032.chr, '"\\x1A"'],
+ [0034.chr, '"\\x1C"'],
+ [0035.chr, '"\\x1D"'],
+ [0036.chr, '"\\x1E"'],
+ [0037.chr, '"\\x1F"'],
+ [0177.chr, '"\\x7F"'],
+ [0200.chr, '"\\x80"'],
+ [0201.chr, '"\\x81"'],
+ [0202.chr, '"\\x82"'],
+ [0203.chr, '"\\x83"'],
+ [0204.chr, '"\\x84"'],
+ [0205.chr, '"\\x85"'],
+ [0206.chr, '"\\x86"'],
+ [0207.chr, '"\\x87"'],
+ [0210.chr, '"\\x88"'],
+ [0211.chr, '"\\x89"'],
+ [0212.chr, '"\\x8A"'],
+ [0213.chr, '"\\x8B"'],
+ [0214.chr, '"\\x8C"'],
+ [0215.chr, '"\\x8D"'],
+ [0216.chr, '"\\x8E"'],
+ [0217.chr, '"\\x8F"'],
+ [0220.chr, '"\\x90"'],
+ [0221.chr, '"\\x91"'],
+ [0222.chr, '"\\x92"'],
+ [0223.chr, '"\\x93"'],
+ [0224.chr, '"\\x94"'],
+ [0225.chr, '"\\x95"'],
+ [0226.chr, '"\\x96"'],
+ [0227.chr, '"\\x97"'],
+ [0230.chr, '"\\x98"'],
+ [0231.chr, '"\\x99"'],
+ [0232.chr, '"\\x9A"'],
+ [0233.chr, '"\\x9B"'],
+ [0234.chr, '"\\x9C"'],
+ [0235.chr, '"\\x9D"'],
+ [0236.chr, '"\\x9E"'],
+ [0237.chr, '"\\x9F"'],
+ [0240.chr, '"\\xA0"'],
+ [0241.chr, '"\\xA1"'],
+ [0242.chr, '"\\xA2"'],
+ [0243.chr, '"\\xA3"'],
+ [0244.chr, '"\\xA4"'],
+ [0245.chr, '"\\xA5"'],
+ [0246.chr, '"\\xA6"'],
+ [0247.chr, '"\\xA7"'],
+ [0250.chr, '"\\xA8"'],
+ [0251.chr, '"\\xA9"'],
+ [0252.chr, '"\\xAA"'],
+ [0253.chr, '"\\xAB"'],
+ [0254.chr, '"\\xAC"'],
+ [0255.chr, '"\\xAD"'],
+ [0256.chr, '"\\xAE"'],
+ [0257.chr, '"\\xAF"'],
+ [0260.chr, '"\\xB0"'],
+ [0261.chr, '"\\xB1"'],
+ [0262.chr, '"\\xB2"'],
+ [0263.chr, '"\\xB3"'],
+ [0264.chr, '"\\xB4"'],
+ [0265.chr, '"\\xB5"'],
+ [0266.chr, '"\\xB6"'],
+ [0267.chr, '"\\xB7"'],
+ [0270.chr, '"\\xB8"'],
+ [0271.chr, '"\\xB9"'],
+ [0272.chr, '"\\xBA"'],
+ [0273.chr, '"\\xBB"'],
+ [0274.chr, '"\\xBC"'],
+ [0275.chr, '"\\xBD"'],
+ [0276.chr, '"\\xBE"'],
+ [0277.chr, '"\\xBF"'],
+ [0300.chr, '"\\xC0"'],
+ [0301.chr, '"\\xC1"'],
+ [0302.chr, '"\\xC2"'],
+ [0303.chr, '"\\xC3"'],
+ [0304.chr, '"\\xC4"'],
+ [0305.chr, '"\\xC5"'],
+ [0306.chr, '"\\xC6"'],
+ [0307.chr, '"\\xC7"'],
+ [0310.chr, '"\\xC8"'],
+ [0311.chr, '"\\xC9"'],
+ [0312.chr, '"\\xCA"'],
+ [0313.chr, '"\\xCB"'],
+ [0314.chr, '"\\xCC"'],
+ [0315.chr, '"\\xCD"'],
+ [0316.chr, '"\\xCE"'],
+ [0317.chr, '"\\xCF"'],
+ [0320.chr, '"\\xD0"'],
+ [0321.chr, '"\\xD1"'],
+ [0322.chr, '"\\xD2"'],
+ [0323.chr, '"\\xD3"'],
+ [0324.chr, '"\\xD4"'],
+ [0325.chr, '"\\xD5"'],
+ [0326.chr, '"\\xD6"'],
+ [0327.chr, '"\\xD7"'],
+ [0330.chr, '"\\xD8"'],
+ [0331.chr, '"\\xD9"'],
+ [0332.chr, '"\\xDA"'],
+ [0333.chr, '"\\xDB"'],
+ [0334.chr, '"\\xDC"'],
+ [0335.chr, '"\\xDD"'],
+ [0336.chr, '"\\xDE"'],
+ [0337.chr, '"\\xDF"'],
+ [0340.chr, '"\\xE0"'],
+ [0341.chr, '"\\xE1"'],
+ [0342.chr, '"\\xE2"'],
+ [0343.chr, '"\\xE3"'],
+ [0344.chr, '"\\xE4"'],
+ [0345.chr, '"\\xE5"'],
+ [0346.chr, '"\\xE6"'],
+ [0347.chr, '"\\xE7"'],
+ [0350.chr, '"\\xE8"'],
+ [0351.chr, '"\\xE9"'],
+ [0352.chr, '"\\xEA"'],
+ [0353.chr, '"\\xEB"'],
+ [0354.chr, '"\\xEC"'],
+ [0355.chr, '"\\xED"'],
+ [0356.chr, '"\\xEE"'],
+ [0357.chr, '"\\xEF"'],
+ [0360.chr, '"\\xF0"'],
+ [0361.chr, '"\\xF1"'],
+ [0362.chr, '"\\xF2"'],
+ [0363.chr, '"\\xF3"'],
+ [0364.chr, '"\\xF4"'],
+ [0365.chr, '"\\xF5"'],
+ [0366.chr, '"\\xF6"'],
+ [0367.chr, '"\\xF7"'],
+ [0370.chr, '"\\xF8"'],
+ [0371.chr, '"\\xF9"'],
+ [0372.chr, '"\\xFA"'],
+ [0373.chr, '"\\xFB"'],
+ [0374.chr, '"\\xFC"'],
+ [0375.chr, '"\\xFD"'],
+ [0376.chr, '"\\xFE"'],
+ [0377.chr, '"\\xFF"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with non-printing single-byte UTF-8 characters replaced by \\x notation" do
+ [ [0000.chr('utf-8'), '"\x00"'],
+ [0001.chr('utf-8'), '"\x01"'],
+ [0002.chr('utf-8'), '"\x02"'],
+ [0003.chr('utf-8'), '"\x03"'],
+ [0004.chr('utf-8'), '"\x04"'],
+ [0005.chr('utf-8'), '"\x05"'],
+ [0006.chr('utf-8'), '"\x06"'],
+ [0016.chr('utf-8'), '"\x0E"'],
+ [0017.chr('utf-8'), '"\x0F"'],
+ [0020.chr('utf-8'), '"\x10"'],
+ [0021.chr('utf-8'), '"\x11"'],
+ [0022.chr('utf-8'), '"\x12"'],
+ [0023.chr('utf-8'), '"\x13"'],
+ [0024.chr('utf-8'), '"\x14"'],
+ [0025.chr('utf-8'), '"\x15"'],
+ [0026.chr('utf-8'), '"\x16"'],
+ [0027.chr('utf-8'), '"\x17"'],
+ [0030.chr('utf-8'), '"\x18"'],
+ [0031.chr('utf-8'), '"\x19"'],
+ [0032.chr('utf-8'), '"\x1A"'],
+ [0034.chr('utf-8'), '"\x1C"'],
+ [0035.chr('utf-8'), '"\x1D"'],
+ [0036.chr('utf-8'), '"\x1E"'],
+ [0037.chr('utf-8'), '"\x1F"'],
+ [0177.chr('utf-8'), '"\x7F"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with multi-byte UTF-8 characters less than or equal 0xFFFF replaced by \\uXXXX notation with upper-case hex digits" do
+ [ [0200.chr('utf-8'), '"\u0080"'],
+ [0201.chr('utf-8'), '"\u0081"'],
+ [0202.chr('utf-8'), '"\u0082"'],
+ [0203.chr('utf-8'), '"\u0083"'],
+ [0204.chr('utf-8'), '"\u0084"'],
+ [0206.chr('utf-8'), '"\u0086"'],
+ [0207.chr('utf-8'), '"\u0087"'],
+ [0210.chr('utf-8'), '"\u0088"'],
+ [0211.chr('utf-8'), '"\u0089"'],
+ [0212.chr('utf-8'), '"\u008A"'],
+ [0213.chr('utf-8'), '"\u008B"'],
+ [0214.chr('utf-8'), '"\u008C"'],
+ [0215.chr('utf-8'), '"\u008D"'],
+ [0216.chr('utf-8'), '"\u008E"'],
+ [0217.chr('utf-8'), '"\u008F"'],
+ [0220.chr('utf-8'), '"\u0090"'],
+ [0221.chr('utf-8'), '"\u0091"'],
+ [0222.chr('utf-8'), '"\u0092"'],
+ [0223.chr('utf-8'), '"\u0093"'],
+ [0224.chr('utf-8'), '"\u0094"'],
+ [0225.chr('utf-8'), '"\u0095"'],
+ [0226.chr('utf-8'), '"\u0096"'],
+ [0227.chr('utf-8'), '"\u0097"'],
+ [0230.chr('utf-8'), '"\u0098"'],
+ [0231.chr('utf-8'), '"\u0099"'],
+ [0232.chr('utf-8'), '"\u009A"'],
+ [0233.chr('utf-8'), '"\u009B"'],
+ [0234.chr('utf-8'), '"\u009C"'],
+ [0235.chr('utf-8'), '"\u009D"'],
+ [0236.chr('utf-8'), '"\u009E"'],
+ [0237.chr('utf-8'), '"\u009F"'],
+ [0177777.chr('utf-8'), '"\uFFFF"'],
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with multi-byte UTF-8 characters greater than 0xFFFF replaced by \\u{XXXXXX} notation with upper-case hex digits" do
+ 0x10000.chr('utf-8').dump.should == '"\u{10000}"'
+ 0x10FFFF.chr('utf-8').dump.should == '"\u{10FFFF}"'
+ end
+
+ it "includes .force_encoding(name) if the encoding isn't ASCII compatible" do
+ "\u{876}".encode('utf-16be').dump.should.end_with?(".force_encoding(\"UTF-16BE\")")
+ "\u{876}".encode('utf-16le').dump.should.end_with?(".force_encoding(\"UTF-16LE\")")
+ end
+
+ it "returns a String in the same encoding as self" do
+ "foo".encode("ISO-8859-1").dump.encoding.should == Encoding::ISO_8859_1
+ "foo".encode('windows-1251').dump.encoding.should == Encoding::Windows_1251
+ 1.chr.dump.encoding.should == Encoding::US_ASCII
+ end
+end
s='graph'>
-rw-r--r--lib/bundler/digest.rb71
-rw-r--r--lib/bundler/dsl.rb698
-rw-r--r--lib/bundler/endpoint_specification.rb184
-rw-r--r--lib/bundler/env.rb129
-rw-r--r--lib/bundler/environment_preserver.rb69
-rw-r--r--lib/bundler/errors.rb306
-rw-r--r--lib/bundler/feature_flag.rb20
-rw-r--r--lib/bundler/fetcher.rb361
-rw-r--r--lib/bundler/fetcher/base.rb52
-rw-r--r--lib/bundler/fetcher/compact_index.rb120
-rw-r--r--lib/bundler/fetcher/dependency.rb85
-rw-r--r--lib/bundler/fetcher/downloader.rb116
-rw-r--r--lib/bundler/fetcher/gem_remote_fetcher.rb22
-rw-r--r--lib/bundler/fetcher/index.rb25
-rw-r--r--lib/bundler/force_platform.rb16
-rw-r--r--lib/bundler/friendly_errors.rb127
-rw-r--r--lib/bundler/gem_helper.rb237
-rw-r--r--lib/bundler/gem_tasks.rb7
-rw-r--r--lib/bundler/gem_version_promoter.rb147
-rw-r--r--lib/bundler/index.rb203
-rw-r--r--lib/bundler/injector.rb284
-rw-r--r--lib/bundler/inline.rb146
-rw-r--r--lib/bundler/installer.rb236
-rw-r--r--lib/bundler/installer/gem_installer.rb88
-rw-r--r--lib/bundler/installer/parallel_installer.rb241
-rw-r--r--lib/bundler/installer/standalone.rb113
-rw-r--r--lib/bundler/lazy_specification.rb272
-rw-r--r--lib/bundler/lockfile_generator.rb119
-rw-r--r--lib/bundler/lockfile_parser.rb328
-rw-r--r--lib/bundler/man/.document1
-rw-r--r--lib/bundler/man/bundle-add.182
-rw-r--r--lib/bundler/man/bundle-add.1.ronn95
-rw-r--r--lib/bundler/man/bundle-binstubs.130
-rw-r--r--lib/bundler/man/bundle-binstubs.1.ronn42
-rw-r--r--lib/bundler/man/bundle-cache.156
-rw-r--r--lib/bundler/man/bundle-cache.1.ronn95
-rw-r--r--lib/bundler/man/bundle-check.121
-rw-r--r--lib/bundler/man/bundle-check.1.ronn26
-rw-r--r--lib/bundler/man/bundle-clean.117
-rw-r--r--lib/bundler/man/bundle-clean.1.ronn18
-rw-r--r--lib/bundler/man/bundle-config.1343
-rw-r--r--lib/bundler/man/bundle-config.1.ronn463
-rw-r--r--lib/bundler/man/bundle-console.133
-rw-r--r--lib/bundler/man/bundle-console.1.ronn39
-rw-r--r--lib/bundler/man/bundle-doctor.169
-rw-r--r--lib/bundler/man/bundle-doctor.1.ronn77
-rw-r--r--lib/bundler/man/bundle-env.19
-rw-r--r--lib/bundler/man/bundle-env.1.ronn10
-rw-r--r--lib/bundler/man/bundle-exec.1104
-rw-r--r--lib/bundler/man/bundle-exec.1.ronn150
-rw-r--r--lib/bundler/man/bundle-fund.122
-rw-r--r--lib/bundler/man/bundle-fund.1.ronn25
-rw-r--r--lib/bundler/man/bundle-gem.1107
-rw-r--r--lib/bundler/man/bundle-gem.1.ronn150
-rw-r--r--lib/bundler/man/bundle-help.19
-rw-r--r--lib/bundler/man/bundle-help.1.ronn12
-rw-r--r--lib/bundler/man/bundle-info.117
-rw-r--r--lib/bundler/man/bundle-info.1.ronn21
-rw-r--r--lib/bundler/man/bundle-init.120
-rw-r--r--lib/bundler/man/bundle-init.1.ronn32
-rw-r--r--lib/bundler/man/bundle-install.1178
-rw-r--r--lib/bundler/man/bundle-install.1.ronn314
-rw-r--r--lib/bundler/man/bundle-issue.145
-rw-r--r--lib/bundler/man/bundle-issue.1.ronn37
-rw-r--r--lib/bundler/man/bundle-licenses.19
-rw-r--r--lib/bundler/man/bundle-licenses.1.ronn10
-rw-r--r--lib/bundler/man/bundle-list.140
-rw-r--r--lib/bundler/man/bundle-list.1.ronn41
-rw-r--r--lib/bundler/man/bundle-lock.175
-rw-r--r--lib/bundler/man/bundle-lock.1.ronn115
-rw-r--r--lib/bundler/man/bundle-open.132
-rw-r--r--lib/bundler/man/bundle-open.1.ronn28
-rw-r--r--lib/bundler/man/bundle-outdated.1106
-rw-r--r--lib/bundler/man/bundle-outdated.1.ronn117
-rw-r--r--lib/bundler/man/bundle-platform.149
-rw-r--r--lib/bundler/man/bundle-platform.1.ronn49
-rw-r--r--lib/bundler/man/bundle-plugin.176
-rw-r--r--lib/bundler/man/bundle-plugin.1.ronn84
-rw-r--r--lib/bundler/man/bundle-pristine.123
-rw-r--r--lib/bundler/man/bundle-pristine.1.ronn34
-rw-r--r--lib/bundler/man/bundle-remove.115
-rw-r--r--lib/bundler/man/bundle-remove.1.ronn16
-rw-r--r--lib/bundler/man/bundle-show.116
-rw-r--r--lib/bundler/man/bundle-show.1.ronn21
-rw-r--r--lib/bundler/man/bundle-update.1284
-rw-r--r--lib/bundler/man/bundle-update.1.ronn367
-rw-r--r--lib/bundler/man/bundle-version.122
-rw-r--r--lib/bundler/man/bundle-version.1.ronn24
-rw-r--r--lib/bundler/man/bundle.193
-rw-r--r--lib/bundler/man/bundle.1.ronn107
-rw-r--r--lib/bundler/man/gemfile.5547
-rw-r--r--lib/bundler/man/gemfile.5.ronn639
-rw-r--r--lib/bundler/man/index.txt31
-rw-r--r--lib/bundler/match_metadata.rb50
-rw-r--r--lib/bundler/match_platform.rb42
-rw-r--r--lib/bundler/match_remote_metadata.rb44
-rw-r--r--lib/bundler/materialization.rb59
-rw-r--r--lib/bundler/mirror.rb221
-rw-r--r--lib/bundler/override.rb69
-rw-r--r--lib/bundler/plugin.rb381
-rw-r--r--lib/bundler/plugin/api.rb81
-rw-r--r--lib/bundler/plugin/api/source.rb330
-rw-r--r--lib/bundler/plugin/dsl.rb53
-rw-r--r--lib/bundler/plugin/events.rb127
-rw-r--r--lib/bundler/plugin/index.rb241
-rw-r--r--lib/bundler/plugin/installer.rb123
-rw-r--r--lib/bundler/plugin/installer/git.rb34
-rw-r--r--lib/bundler/plugin/installer/path.rb26
-rw-r--r--lib/bundler/plugin/installer/rubygems.rb19
-rw-r--r--lib/bundler/plugin/source_list.rb31
-rw-r--r--lib/bundler/process_lock.rb20
-rw-r--r--lib/bundler/remote_specification.rb126
-rw-r--r--lib/bundler/resolver.rb645
-rw-r--r--lib/bundler/resolver/base.rb119
-rw-r--r--lib/bundler/resolver/candidate.rb85
-rw-r--r--lib/bundler/resolver/incompatibility.rb15
-rw-r--r--lib/bundler/resolver/package.rb95
-rw-r--r--lib/bundler/resolver/root.rb25
-rw-r--r--lib/bundler/resolver/spec_group.rb74
-rw-r--r--lib/bundler/resolver/strategy.rb43
-rw-r--r--lib/bundler/retry.rb92
-rw-r--r--lib/bundler/ruby_dsl.rb67
-rw-r--r--lib/bundler/ruby_version.rb133
-rw-r--r--lib/bundler/rubygems_ext.rb503
-rw-r--r--lib/bundler/rubygems_gem_installer.rb196
-rw-r--r--lib/bundler/rubygems_integration.rb456
-rw-r--r--lib/bundler/runtime.rb331
-rw-r--r--lib/bundler/safe_marshal.rb31
-rw-r--r--lib/bundler/self_manager.rb197
-rw-r--r--lib/bundler/settings.rb587
-rw-r--r--lib/bundler/settings/validator.rb86
-rw-r--r--lib/bundler/setup.rb39
-rw-r--r--lib/bundler/shared_helpers.rb393
-rw-r--r--lib/bundler/source.rb120
-rw-r--r--lib/bundler/source/gemspec.rb19
-rw-r--r--lib/bundler/source/git.rb456
-rw-r--r--lib/bundler/source/git/git_proxy.rb503
-rw-r--r--lib/bundler/source/metadata.rb67
-rw-r--r--lib/bundler/source/path.rb256
-rw-r--r--lib/bundler/source/path/installer.rb53
-rw-r--r--lib/bundler/source/rubygems.rb598
-rw-r--r--lib/bundler/source/rubygems/remote.rb86
-rw-r--r--lib/bundler/source/rubygems_aggregate.rb71
-rw-r--r--lib/bundler/source_list.rb232
-rw-r--r--lib/bundler/source_map.rb72
-rw-r--r--lib/bundler/spec_set.rb402
-rw-r--r--lib/bundler/stub_specification.rb147
-rw-r--r--lib/bundler/templates/.document1
-rw-r--r--lib/bundler/templates/Executable16
-rw-r--r--lib/bundler/templates/Executable.standalone14
-rw-r--r--lib/bundler/templates/Gemfile5
-rw-r--r--lib/bundler/templates/newgem/CHANGELOG.md.tt5
-rw-r--r--lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt10
-rw-r--r--lib/bundler/templates/newgem/Cargo.toml.tt13
-rw-r--r--lib/bundler/templates/newgem/Gemfile.tt24
-rw-r--r--lib/bundler/templates/newgem/LICENSE.txt.tt21
-rw-r--r--lib/bundler/templates/newgem/README.md.tt49
-rw-r--r--lib/bundler/templates/newgem/Rakefile.tt72
-rw-r--r--lib/bundler/templates/newgem/bin/console.tt11
-rw-r--r--lib/bundler/templates/newgem/bin/setup.tt8
-rw-r--r--lib/bundler/templates/newgem/circleci/config.yml.tt37
-rw-r--r--lib/bundler/templates/newgem/exe/newgem.tt3
-rw-r--r--lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt22
-rw-r--r--lib/bundler/templates/newgem/ext/newgem/build.rs.tt5
-rw-r--r--lib/bundler/templates/newgem/ext/newgem/extconf-c.rb.tt10
-rw-r--r--lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt11
-rw-r--r--lib/bundler/templates/newgem/ext/newgem/extconf-rust.rb.tt6
-rw-r--r--lib/bundler/templates/newgem/ext/newgem/go.mod.tt5
-rw-r--r--lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt2
-rw-r--r--lib/bundler/templates/newgem/ext/newgem/newgem.c.tt9
-rw-r--r--lib/bundler/templates/newgem/ext/newgem/newgem.go.tt31
-rw-r--r--lib/bundler/templates/newgem/ext/newgem/newgem.h.tt6
-rw-r--r--lib/bundler/templates/newgem/ext/newgem/src/lib.rs.tt23
-rw-r--r--lib/bundler/templates/newgem/github/workflows/build-gems.yml.tt69
-rw-r--r--lib/bundler/templates/newgem/github/workflows/main.yml.tt48
-rw-r--r--lib/bundler/templates/newgem/gitignore.tt23
-rw-r--r--lib/bundler/templates/newgem/gitlab-ci.yml.tt27
-rw-r--r--lib/bundler/templates/newgem/lib/newgem.rb.tt15
-rw-r--r--lib/bundler/templates/newgem/lib/newgem/version.rb.tt9
-rw-r--r--lib/bundler/templates/newgem/newgem.gemspec.tt58
-rw-r--r--lib/bundler/templates/newgem/rspec.tt3
-rw-r--r--lib/bundler/templates/newgem/rubocop.yml.tt8
-rw-r--r--lib/bundler/templates/newgem/sig/newgem.rbs.tt8
-rw-r--r--lib/bundler/templates/newgem/spec/newgem_spec.rb.tt19
-rw-r--r--lib/bundler/templates/newgem/spec/spec_helper.rb.tt15
-rw-r--r--lib/bundler/templates/newgem/standard.yml.tt3
-rw-r--r--lib/bundler/templates/newgem/test/minitest/test_helper.rb.tt6
-rw-r--r--lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt19
-rw-r--r--lib/bundler/templates/newgem/test/test-unit/newgem_test.rb.tt15
-rw-r--r--lib/bundler/templates/newgem/test/test-unit/test_helper.rb.tt6
-rw-r--r--lib/bundler/ui.rb9
-rw-r--r--lib/bundler/ui/rg_proxy.rb19
-rw-r--r--lib/bundler/ui/shell.rb191
-rw-r--r--lib/bundler/ui/silent.rb96
-rw-r--r--lib/bundler/uri_credentials_filter.rb43
-rw-r--r--lib/bundler/uri_normalizer.rb23
-rw-r--r--lib/bundler/vendor/.document1
-rw-r--r--lib/bundler/vendor/connection_pool/lib/connection_pool.rb233
-rw-r--r--lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb237
-rw-r--r--lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb3
-rw-r--r--lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb56
-rw-r--r--lib/bundler/vendor/fileutils/lib/fileutils.rb2701
-rw-r--r--lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb1153
-rw-r--r--lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb41
-rw-r--r--lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb65
-rw-r--r--lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb80
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub.rb31
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/assignment.rb20
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/basic_package_source.rb169
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/failure_writer.rb182
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/incompatibility.rb150
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/package.rb43
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/partial_solution.rb121
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/rubygems.rb45
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/solve_failure.rb19
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/static_package_source.rb61
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/strategy.rb42
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/term.rb105
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/version.rb3
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/version_constraint.rb129
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb423
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb236
-rw-r--r--lib/bundler/vendor/pub_grub/lib/pub_grub/version_union.rb178
-rw-r--r--lib/bundler/vendor/securerandom/lib/securerandom.rb102
-rw-r--r--lib/bundler/vendor/thor/lib/thor.rb674
-rw-r--r--lib/bundler/vendor/thor/lib/thor/actions.rb340
-rw-r--r--lib/bundler/vendor/thor/lib/thor/actions/create_file.rb105
-rw-r--r--lib/bundler/vendor/thor/lib/thor/actions/create_link.rb61
-rw-r--r--lib/bundler/vendor/thor/lib/thor/actions/directory.rb108
-rw-r--r--lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb143
-rw-r--r--lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb407
-rw-r--r--lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb130
-rw-r--r--lib/bundler/vendor/thor/lib/thor/base.rb825
-rw-r--r--lib/bundler/vendor/thor/lib/thor/command.rb151
-rw-r--r--lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb107
-rw-r--r--lib/bundler/vendor/thor/lib/thor/error.rb106
-rw-r--r--lib/bundler/vendor/thor/lib/thor/group.rb292
-rw-r--r--lib/bundler/vendor/thor/lib/thor/invocation.rb178
-rw-r--r--lib/bundler/vendor/thor/lib/thor/line_editor.rb17
-rw-r--r--lib/bundler/vendor/thor/lib/thor/line_editor/basic.rb37
-rw-r--r--lib/bundler/vendor/thor/lib/thor/line_editor/readline.rb88
-rw-r--r--lib/bundler/vendor/thor/lib/thor/nested_context.rb29
-rw-r--r--lib/bundler/vendor/thor/lib/thor/parser.rb4
-rw-r--r--lib/bundler/vendor/thor/lib/thor/parser/argument.rb86
-rw-r--r--lib/bundler/vendor/thor/lib/thor/parser/arguments.rb195
-rw-r--r--lib/bundler/vendor/thor/lib/thor/parser/option.rb178
-rw-r--r--lib/bundler/vendor/thor/lib/thor/parser/options.rb294
-rw-r--r--lib/bundler/vendor/thor/lib/thor/rake_compat.rb72
-rw-r--r--lib/bundler/vendor/thor/lib/thor/runner.rb335
-rw-r--r--lib/bundler/vendor/thor/lib/thor/shell.rb81
-rw-r--r--lib/bundler/vendor/thor/lib/thor/shell/basic.rb384
-rw-r--r--lib/bundler/vendor/thor/lib/thor/shell/color.rb112
-rw-r--r--lib/bundler/vendor/thor/lib/thor/shell/column_printer.rb29
-rw-r--r--lib/bundler/vendor/thor/lib/thor/shell/html.rb81
-rw-r--r--lib/bundler/vendor/thor/lib/thor/shell/table_printer.rb118
-rw-r--r--lib/bundler/vendor/thor/lib/thor/shell/terminal.rb42
-rw-r--r--lib/bundler/vendor/thor/lib/thor/shell/wrapped_printer.rb38
-rw-r--r--lib/bundler/vendor/thor/lib/thor/util.rb285
-rw-r--r--lib/bundler/vendor/thor/lib/thor/version.rb3
-rw-r--r--lib/bundler/vendor/tsort/lib/tsort.rb455
-rw-r--r--lib/bundler/vendor/uri/lib/uri.rb104
-rw-r--r--lib/bundler/vendor/uri/lib/uri/common.rb922
-rw-r--r--lib/bundler/vendor/uri/lib/uri/file.rb100
-rw-r--r--lib/bundler/vendor/uri/lib/uri/ftp.rb267
-rw-r--r--lib/bundler/vendor/uri/lib/uri/generic.rb1592
-rw-r--r--lib/bundler/vendor/uri/lib/uri/http.rb137
-rw-r--r--lib/bundler/vendor/uri/lib/uri/https.rb23
-rw-r--r--lib/bundler/vendor/uri/lib/uri/ldap.rb261
-rw-r--r--lib/bundler/vendor/uri/lib/uri/ldaps.rb22
-rw-r--r--lib/bundler/vendor/uri/lib/uri/mailto.rb293
-rw-r--r--lib/bundler/vendor/uri/lib/uri/rfc2396_parser.rb547
-rw-r--r--lib/bundler/vendor/uri/lib/uri/rfc3986_parser.rb206
-rw-r--r--lib/bundler/vendor/uri/lib/uri/version.rb6
-rw-r--r--lib/bundler/vendor/uri/lib/uri/ws.rb83
-rw-r--r--lib/bundler/vendor/uri/lib/uri/wss.rb23
-rw-r--r--lib/bundler/vendored_fileutils.rb4
-rw-r--r--lib/bundler/vendored_net_http.rb23
-rw-r--r--lib/bundler/vendored_persistent.rb11
-rw-r--r--lib/bundler/vendored_pub_grub.rb4
-rw-r--r--lib/bundler/vendored_securerandom.rb12
-rw-r--r--lib/bundler/vendored_thor.rb8
-rw-r--r--lib/bundler/vendored_timeout.rb12
-rw-r--r--lib/bundler/vendored_tsort.rb4
-rw-r--r--lib/bundler/vendored_uri.rb21
-rw-r--r--lib/bundler/version.rb21
-rw-r--r--lib/bundler/vlad.rb4
-rw-r--r--lib/bundler/worker.rb125
-rw-r--r--lib/bundler/yaml_serializer.rb98
-rw-r--r--lib/cgi.rb279
-rw-r--r--lib/cgi/.document1
-rw-r--r--lib/cgi/cookie.rb137
-rw-r--r--lib/cgi/core.rb784
-rw-r--r--lib/cgi/escape.rb232
-rw-r--r--lib/cgi/html.rb1021
-rw-r--r--lib/cgi/session.rb537
-rw-r--r--lib/cgi/session/pstore.rb111
-rw-r--r--lib/cgi/util.rb186
-rw-r--r--lib/cmath.rb233
-rw-r--r--lib/complex.rb24
-rw-r--r--lib/csv.rb2311
-rw-r--r--lib/date.rb1834
-rw-r--r--lib/date/format.rb1313
-rw-r--r--lib/debug.rb907
-rw-r--r--lib/delegate.gemspec29
-rw-r--r--lib/delegate.rb538
-rw-r--r--lib/did_you_mean.rb131
-rw-r--r--lib/did_you_mean/core_ext/name_error.rb57
-rw-r--r--lib/did_you_mean/did_you_mean.gemspec25
-rw-r--r--lib/did_you_mean/experimental.rb2
-rw-r--r--lib/did_you_mean/formatter.rb44
-rw-r--r--lib/did_you_mean/formatters/plain_formatter.rb4
-rw-r--r--lib/did_you_mean/formatters/verbose_formatter.rb10
-rw-r--r--lib/did_you_mean/jaro_winkler.rb84
-rw-r--r--lib/did_you_mean/levenshtein.rb57
-rw-r--r--lib/did_you_mean/spell_checker.rb46
-rw-r--r--lib/did_you_mean/spell_checkers/key_error_checker.rb28
-rw-r--r--lib/did_you_mean/spell_checkers/method_name_checker.rb79
-rw-r--r--lib/did_you_mean/spell_checkers/name_error_checkers.rb20
-rw-r--r--lib/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb49
-rw-r--r--lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb85
-rw-r--r--lib/did_you_mean/spell_checkers/null_checker.rb6
-rw-r--r--lib/did_you_mean/spell_checkers/pattern_key_name_checker.rb28
-rw-r--r--lib/did_you_mean/spell_checkers/require_path_checker.rb39
-rw-r--r--lib/did_you_mean/tree_spell_checker.rb109
-rw-r--r--lib/did_you_mean/verbose.rb2
-rw-r--r--lib/did_you_mean/version.rb3
-rw-r--r--lib/drb.rb2
-rw-r--r--lib/drb/acl.rb146
-rw-r--r--lib/drb/drb.rb1783
-rw-r--r--lib/drb/eq.rb16
-rw-r--r--lib/drb/extserv.rb71
-rw-r--r--lib/drb/extservm.rb89
-rw-r--r--lib/drb/gw.rb122
-rw-r--r--lib/drb/invokemethod.rb34
-rw-r--r--lib/drb/observer.rb22
-rw-r--r--lib/drb/ssl.rb190
-rw-r--r--lib/drb/timeridconv.rb91
-rw-r--r--lib/drb/unix.rb108
-rw-r--r--lib/e2mmap.rb172
-rw-r--r--lib/erb.rb1961
-rw-r--r--lib/erb/compiler.rb487
-rw-r--r--lib/erb/def_method.rb47
-rw-r--r--lib/erb/erb.gemspec37
-rw-r--r--lib/erb/util.rb77
-rw-r--r--lib/erb/version.rb5
-rw-r--r--lib/error_highlight.rb2
-rw-r--r--lib/error_highlight/base.rb938
-rw-r--r--lib/error_highlight/core_ext.rb76
-rw-r--r--lib/error_highlight/error_highlight.gemspec27
-rw-r--r--lib/error_highlight/formatter.rb74
-rw-r--r--lib/error_highlight/version.rb3
-rw-r--r--lib/fileutils.gemspec31
-rw-r--r--lib/fileutils.rb2641
-rw-r--r--lib/find.gemspec29
-rw-r--r--lib/find.rb146
-rw-r--r--lib/forwardable.rb323
-rw-r--r--lib/forwardable/forwardable.gemspec26
-rw-r--r--lib/getoptlong.rb610
-rw-r--r--lib/gserver.rb253
-rw-r--r--lib/ipaddr.gemspec36
-rw-r--r--lib/ipaddr.rb852
-rw-r--r--lib/irb.rb346
-rw-r--r--lib/irb/cmd/chws.rb32
-rw-r--r--lib/irb/cmd/fork.rb38
-rw-r--r--lib/irb/cmd/help.rb34
-rw-r--r--lib/irb/cmd/load.rb66
-rw-r--r--lib/irb/cmd/nop.rb38
-rw-r--r--lib/irb/cmd/pushws.rb38
-rw-r--r--lib/irb/cmd/subirb.rb42
-rw-r--r--lib/irb/completion.rb207
-rw-r--r--lib/irb/context.rb255
-rw-r--r--lib/irb/ext/change-ws.rb61
-rw-r--r--lib/irb/ext/history.rb109
-rw-r--r--lib/irb/ext/loader.rb119
-rw-r--r--lib/irb/ext/math-mode.rb36
-rw-r--r--lib/irb/ext/multi-irb.rb240
-rw-r--r--lib/irb/ext/save-history.rb86
-rw-r--r--lib/irb/ext/tracer.rb60
-rw-r--r--lib/irb/ext/use-loader.rb64
-rw-r--r--lib/irb/ext/workspaces.rb55
-rw-r--r--lib/irb/extend-command.rb268
-rw-r--r--lib/irb/frame.rb66
-rw-r--r--lib/irb/help.rb35
-rw-r--r--lib/irb/init.rb286
-rw-r--r--lib/irb/input-method.rb142
-rw-r--r--lib/irb/lc/error.rb29
-rw-r--r--lib/irb/lc/help-message38
-rw-r--r--lib/irb/lc/ja/encoding_aliases.rb8
-rw-r--r--lib/irb/lc/ja/error.rb27
-rw-r--r--lib/irb/lc/ja/help-message39
-rw-r--r--lib/irb/locale.rb195
-rw-r--r--lib/irb/magic-file.rb36
-rw-r--r--lib/irb/notifier.rb144
-rw-r--r--lib/irb/output-method.rb69
-rw-r--r--lib/irb/ruby-lex.rb1155
-rw-r--r--lib/irb/ruby-token.rb270
-rw-r--r--lib/irb/slex.rb282
-rw-r--r--lib/irb/src_encoding.rb4
-rw-r--r--lib/irb/version.rb15
-rw-r--r--lib/irb/workspace.rb108
-rw-r--r--lib/irb/ws-for-case-2.rb14
-rw-r--r--lib/irb/xmp.rb97
-rw-r--r--lib/logger.rb732
-rw-r--r--lib/mathn.rb206
-rw-r--r--lib/matrix.rb1382
-rw-r--r--lib/minitest/autorun.rb9
-rw-r--r--lib/minitest/mock.rb37
-rw-r--r--lib/minitest/spec.rb89
-rw-r--r--lib/minitest/unit.rb497
-rw-r--r--lib/mkmf.rb4273
-rw-r--r--lib/monitor.rb331
-rw-r--r--lib/mutex_m.rb91
-rw-r--r--lib/net/.document8
-rw-r--r--lib/net/ftp.rb981
-rw-r--r--lib/net/http.rb4062
-rw-r--r--lib/net/http/exceptions.rb35
-rw-r--r--lib/net/http/generic_request.rb429
-rw-r--r--lib/net/http/header.rb985
-rw-r--r--lib/net/http/net-http.gemspec39
-rw-r--r--lib/net/http/proxy_delta.rb17
-rw-r--r--lib/net/http/request.rb88
-rw-r--r--lib/net/http/requests.rb444
-rw-r--r--lib/net/http/response.rb739
-rw-r--r--lib/net/http/responses.rb1242
-rw-r--r--lib/net/http/status.rb84
-rw-r--r--lib/net/https.rb131
-rw-r--r--lib/net/imap.rb3449
-rw-r--r--lib/net/net-protocol.gemspec33
-rw-r--r--lib/net/pop.rb1000
-rw-r--r--lib/net/protocol.rb261
-rw-r--r--lib/net/smtp.rb1014
-rw-r--r--lib/net/telnet.rb759
-rw-r--r--lib/observer.rb193
-rw-r--r--lib/open-uri.gemspec32
-rw-r--r--lib/open-uri.rb478
-rw-r--r--lib/open3.rb1424
-rw-r--r--lib/open3/open3.gemspec33
-rw-r--r--lib/open3/version.rb4
-rw-r--r--lib/optionparser.rb2
-rw-r--r--lib/optparse.rb1327
-rw-r--r--lib/optparse/ac.rb70
-rw-r--r--lib/optparse/date.rb3
-rw-r--r--lib/optparse/kwargs.rb27
-rw-r--r--lib/optparse/optparse.gemspec34
-rw-r--r--lib/optparse/shellwords.rb3
-rw-r--r--lib/optparse/time.rb3
-rw-r--r--lib/optparse/uri.rb3
-rw-r--r--lib/optparse/version.rb12
-rw-r--r--lib/ostruct.rb145
-rw-r--r--lib/pathname.rb1167
-rw-r--r--lib/pp.gemspec35
-rw-r--r--lib/pp.rb644
-rw-r--r--lib/prettyprint.gemspec29
-rw-r--r--lib/prettyprint.rb791
-rw-r--r--lib/prime.rb461
-rw-r--r--lib/prism.rb145
-rw-r--r--lib/prism/desugar_compiler.rb463
-rw-r--r--lib/prism/ffi.rb611
-rw-r--r--lib/prism/lex_compat.rb906
-rw-r--r--lib/prism/node_ext.rb388
-rw-r--r--lib/prism/node_find.rb185
-rw-r--r--lib/prism/parse_result.rb1211
-rw-r--r--lib/prism/parse_result/comments.rb219
-rw-r--r--lib/prism/parse_result/errors.rb72
-rw-r--r--lib/prism/parse_result/newlines.rb204
-rw-r--r--lib/prism/pattern.rb314
-rw-r--r--lib/prism/polyfill/append_as_bytes.rb15
-rw-r--r--lib/prism/polyfill/byteindex.rb13
-rw-r--r--lib/prism/polyfill/scan_byte.rb14
-rw-r--r--lib/prism/polyfill/unpack1.rb14
-rw-r--r--lib/prism/polyfill/warn.rb36
-rw-r--r--lib/prism/prism.gemspec232
-rw-r--r--lib/prism/relocation.rb665
-rw-r--r--lib/prism/string_query.rb46
-rw-r--r--lib/prism/translation.rb20
-rw-r--r--lib/prism/translation/parser.rb376
-rw-r--r--lib/prism/translation/parser/builder.rb70
-rw-r--r--lib/prism/translation/parser/compiler.rb2219
-rw-r--r--lib/prism/translation/parser/lexer.rb819
-rw-r--r--lib/prism/translation/parser_current.rb26
-rw-r--r--lib/prism/translation/parser_versions.rb36
-rw-r--r--lib/prism/translation/ripper.rb4266
-rw-r--r--lib/prism/translation/ripper/filter.rb53
-rw-r--r--lib/prism/translation/ripper/lexer.rb133
-rw-r--r--lib/prism/translation/ripper/sexp.rb118
-rw-r--r--lib/prism/translation/ripper/shim.rb7
-rw-r--r--lib/prism/translation/ruby_parser.rb1676
-rw-r--r--lib/profile.rb10
-rw-r--r--lib/profiler.rb59
-rw-r--r--lib/pstore.rb543
-rw-r--r--lib/racc/parser.rb441
-rwxr-xr-xlib/rake.rb2465
-rw-r--r--lib/rake/classic_namespace.rb8
-rw-r--r--lib/rake/clean.rb33
-rw-r--r--lib/rake/gempackagetask.rb97
-rw-r--r--lib/rake/loaders/makefile.rb35
-rw-r--r--lib/rake/packagetask.rb185
-rw-r--r--lib/rake/rake_test_loader.rb5
-rw-r--r--lib/rake/rdoctask.rb147
-rw-r--r--lib/rake/runtest.rb23
-rw-r--r--lib/rake/tasklib.rb23
-rw-r--r--lib/rake/testtask.rb161
-rw-r--r--lib/rake/win32.rb54
-rw-r--r--lib/random/formatter.rb372
-rw-r--r--lib/rational.rb19
-rw-r--r--lib/rbconfig/datadir.rb24
-rw-r--r--lib/rdoc.rb395
-rw-r--r--lib/rdoc/README232
-rw-r--r--lib/rdoc/code_objects.rb1061
-rw-r--r--lib/rdoc/diagram.rb340
-rw-r--r--lib/rdoc/dot.rb249
-rw-r--r--lib/rdoc/generator.rb1082
-rw-r--r--lib/rdoc/generator/chm.rb113
-rw-r--r--lib/rdoc/generator/chm/chm.rb100
-rw-r--r--lib/rdoc/generator/html.rb445
-rw-r--r--lib/rdoc/generator/html/common.rb24
-rw-r--r--lib/rdoc/generator/html/frameless.rb92
-rw-r--r--lib/rdoc/generator/html/hefss.rb150
-rw-r--r--lib/rdoc/generator/html/html.rb769
-rw-r--r--lib/rdoc/generator/html/kilmer.rb151
-rw-r--r--lib/rdoc/generator/html/kilmerfactory.rb427
-rw-r--r--lib/rdoc/generator/html/one_page_html.rb122
-rw-r--r--lib/rdoc/generator/ri.rb226
-rw-r--r--lib/rdoc/generator/texinfo.rb81
-rw-r--r--lib/rdoc/generator/texinfo/class.texinfo.erb44
-rw-r--r--lib/rdoc/generator/texinfo/file.texinfo.erb6
-rw-r--r--lib/rdoc/generator/texinfo/method.texinfo.erb6
-rw-r--r--lib/rdoc/generator/texinfo/texinfo.erb28
-rw-r--r--lib/rdoc/generator/xml.rb117
-rw-r--r--lib/rdoc/generator/xml/rdf.rb113
-rw-r--r--lib/rdoc/generator/xml/xml.rb123
-rw-r--r--lib/rdoc/known_classes.rb68
-rw-r--r--lib/rdoc/markup.rb378
-rw-r--r--lib/rdoc/markup/attribute_manager.rb265
-rw-r--r--lib/rdoc/markup/formatter.rb14
-rw-r--r--lib/rdoc/markup/fragments.rb337
-rw-r--r--lib/rdoc/markup/inline.rb101
-rw-r--r--lib/rdoc/markup/lines.rb152
-rw-r--r--lib/rdoc/markup/preprocess.rb75
-rw-r--r--lib/rdoc/markup/to_flow.rb185
-rw-r--r--lib/rdoc/markup/to_html.rb403
-rw-r--r--lib/rdoc/markup/to_html_crossref.rb148
-rw-r--r--lib/rdoc/markup/to_latex.rb328
-rw-r--r--lib/rdoc/markup/to_test.rb50
-rw-r--r--lib/rdoc/markup/to_texinfo.rb69
-rw-r--r--lib/rdoc/options.rb638
-rw-r--r--lib/rdoc/parser.rb142
-rw-r--r--lib/rdoc/parser/c.rb661
-rw-r--r--lib/rdoc/parser/f95.rb1835
-rw-r--r--lib/rdoc/parser/perl.rb165
-rw-r--r--lib/rdoc/parser/ruby.rb2829
-rw-r--r--lib/rdoc/parser/simple.rb38
-rw-r--r--lib/rdoc/rdoc.rb293
-rw-r--r--lib/rdoc/ri.rb8
-rw-r--r--lib/rdoc/ri/cache.rb187
-rw-r--r--lib/rdoc/ri/descriptions.rb156
-rw-r--r--lib/rdoc/ri/display.rb392
-rw-r--r--lib/rdoc/ri/driver.rb669
-rw-r--r--lib/rdoc/ri/formatter.rb616
-rw-r--r--lib/rdoc/ri/paths.rb92
-rw-r--r--lib/rdoc/ri/reader.rb106
-rw-r--r--lib/rdoc/ri/util.rb79
-rw-r--r--lib/rdoc/ri/writer.rb68
-rw-r--r--lib/rdoc/stats.rb115
-rw-r--r--lib/rdoc/template.rb64
-rw-r--r--lib/rdoc/tokenstream.rb33
-rw-r--r--lib/resolv-replace.rb63
-rw-r--r--lib/resolv.gemspec29
-rw-r--r--lib/resolv.rb1669
-rw-r--r--lib/rexml/attlistdecl.rb62
-rw-r--r--lib/rexml/attribute.rb188
-rw-r--r--lib/rexml/cdata.rb67
-rw-r--r--lib/rexml/child.rb96
-rw-r--r--lib/rexml/comment.rb80
-rw-r--r--lib/rexml/doctype.rb270
-rw-r--r--lib/rexml/document.rb231
-rw-r--r--lib/rexml/dtd/attlistdecl.rb10
-rw-r--r--lib/rexml/dtd/dtd.rb51
-rw-r--r--lib/rexml/dtd/elementdecl.rb17
-rw-r--r--lib/rexml/dtd/entitydecl.rb56
-rw-r--r--lib/rexml/dtd/notationdecl.rb39
-rw-r--r--lib/rexml/element.rb1246
-rw-r--r--lib/rexml/encoding.rb71
-rw-r--r--lib/rexml/encodings/CP-1252.rb103
-rw-r--r--lib/rexml/encodings/EUC-JP.rb35
-rw-r--r--lib/rexml/encodings/ICONV.rb22
-rw-r--r--lib/rexml/encodings/ISO-8859-1.rb7
-rw-r--r--lib/rexml/encodings/ISO-8859-15.rb72
-rw-r--r--lib/rexml/encodings/SHIFT-JIS.rb37
-rw-r--r--lib/rexml/encodings/SHIFT_JIS.rb1
-rw-r--r--lib/rexml/encodings/UNILE.rb34
-rw-r--r--lib/rexml/encodings/US-ASCII.rb30
-rw-r--r--lib/rexml/encodings/UTF-16.rb35
-rw-r--r--lib/rexml/encodings/UTF-8.rb18
-rw-r--r--lib/rexml/entity.rb166
-rw-r--r--lib/rexml/formatters/default.rb109
-rw-r--r--lib/rexml/formatters/pretty.rb139
-rw-r--r--lib/rexml/formatters/transitive.rb58
-rw-r--r--lib/rexml/functions.rb388
-rw-r--r--lib/rexml/instruction.rb70
-rw-r--r--lib/rexml/light/node.rb196
-rw-r--r--lib/rexml/namespace.rb47
-rw-r--r--lib/rexml/node.rb75
-rw-r--r--lib/rexml/output.rb24
-rw-r--r--lib/rexml/parent.rb166
-rw-r--r--lib/rexml/parseexception.rb51
-rw-r--r--lib/rexml/parsers/baseparser.rb530
-rw-r--r--lib/rexml/parsers/lightparser.rb58
-rw-r--r--lib/rexml/parsers/pullparser.rb196
-rw-r--r--lib/rexml/parsers/sax2parser.rb247
-rw-r--r--lib/rexml/parsers/streamparser.rb46
-rw-r--r--lib/rexml/parsers/treeparser.rb100
-rw-r--r--lib/rexml/parsers/ultralightparser.rb56
-rw-r--r--lib/rexml/parsers/xpathparser.rb698
-rw-r--r--lib/rexml/quickpath.rb263
-rw-r--r--lib/rexml/rexml.rb31
-rw-r--r--lib/rexml/sax2listener.rb97
-rw-r--r--lib/rexml/source.rb258
-rw-r--r--lib/rexml/streamlistener.rb92
-rw-r--r--lib/rexml/syncenumerator.rb32
-rw-r--r--lib/rexml/text.rb404
-rw-r--r--lib/rexml/undefinednamespaceexception.rb8
-rw-r--r--lib/rexml/validation/relaxng.rb559
-rw-r--r--lib/rexml/validation/validation.rb155
-rw-r--r--lib/rexml/validation/validationexception.rb9
-rw-r--r--lib/rexml/xmldecl.rb119
-rw-r--r--lib/rexml/xmltokens.rb18
-rw-r--r--lib/rexml/xpath.rb77
-rw-r--r--lib/rexml/xpath_parser.rb792
-rw-r--r--lib/rinda/.document3
-rw-r--r--lib/rinda/rinda.rb283
-rw-r--r--lib/rinda/ring.rb271
-rw-r--r--lib/rinda/tuplespace.rb642
-rw-r--r--lib/rss.rb19
-rw-r--r--lib/rss/0.9.rb428
-rw-r--r--lib/rss/1.0.rb452
-rw-r--r--lib/rss/2.0.rb111
-rw-r--r--lib/rss/atom.rb748
-rw-r--r--lib/rss/content.rb31
-rw-r--r--lib/rss/content/1.0.rb10
-rw-r--r--lib/rss/content/2.0.rb12
-rw-r--r--lib/rss/converter.rb170
-rw-r--r--lib/rss/dublincore.rb161
-rw-r--r--lib/rss/dublincore/1.0.rb13
-rw-r--r--lib/rss/dublincore/2.0.rb13
-rw-r--r--lib/rss/dublincore/atom.rb17
-rw-r--r--lib/rss/image.rb193
-rw-r--r--lib/rss/itunes.rb410
-rw-r--r--lib/rss/maker.rb44
-rw-r--r--lib/rss/maker/0.9.rb467
-rw-r--r--lib/rss/maker/1.0.rb434
-rw-r--r--lib/rss/maker/2.0.rb223
-rw-r--r--lib/rss/maker/atom.rb172
-rw-r--r--lib/rss/maker/base.rb880
-rw-r--r--lib/rss/maker/content.rb21
-rw-r--r--lib/rss/maker/dublincore.rb124
-rw-r--r--lib/rss/maker/entry.rb163
-rw-r--r--lib/rss/maker/feed.rb429
-rw-r--r--lib/rss/maker/image.rb111
-rw-r--r--lib/rss/maker/itunes.rb242
-rw-r--r--lib/rss/maker/slash.rb33
-rw-r--r--lib/rss/maker/syndication.rb18
-rw-r--r--lib/rss/maker/taxonomy.rb118
-rw-r--r--lib/rss/maker/trackback.rb61
-rw-r--r--lib/rss/parser.rb551
-rw-r--r--lib/rss/rexmlparser.rb54
-rw-r--r--lib/rss/rss.rb1313
-rw-r--r--lib/rss/slash.rb49
-rw-r--r--lib/rss/syndication.rb67
-rw-r--r--lib/rss/taxonomy.rb145
-rw-r--r--lib/rss/trackback.rb288
-rw-r--r--lib/rss/utils.rb111
-rw-r--r--lib/rss/xml-stylesheet.rb105
-rw-r--r--lib/rss/xml.rb71
-rw-r--r--lib/rss/xmlparser.rb93
-rw-r--r--lib/rss/xmlscanner.rb121
-rw-r--r--lib/ruby2_keywords.gemspec23
-rw-r--r--lib/rubygems.rb1597
-rw-r--r--lib/rubygems/available_set.rb165
-rw-r--r--lib/rubygems/basic_specification.rb384
-rw-r--r--lib/rubygems/builder.rb88
-rw-r--r--lib/rubygems/bundler_version_finder.rb135
-rw-r--r--lib/rubygems/ci_detector.rb75
-rw-r--r--lib/rubygems/command.rb882
-rw-r--r--lib/rubygems/command_manager.rb354
-rw-r--r--lib/rubygems/commands/build_command.rb129
-rw-r--r--lib/rubygems/commands/cert_command.rb391
-rw-r--r--lib/rubygems/commands/check_command.rb126
-rw-r--r--lib/rubygems/commands/cleanup_command.rb187
-rw-r--r--lib/rubygems/commands/contents_command.rb186
-rw-r--r--lib/rubygems/commands/dependency_command.rb236
-rw-r--r--lib/rubygems/commands/environment_command.rb180
-rw-r--r--lib/rubygems/commands/exec_command.rb259
-rw-r--r--lib/rubygems/commands/fetch_command.rb91
-rw-r--r--lib/rubygems/commands/generate_index_command.rb98
-rw-r--r--lib/rubygems/commands/help_command.rb343
-rw-r--r--lib/rubygems/commands/info_command.rb38
-rw-r--r--lib/rubygems/commands/install_command.rb286
-rw-r--r--lib/rubygems/commands/list_command.rb39
-rw-r--r--lib/rubygems/commands/lock_command.rb29
-rw-r--r--lib/rubygems/commands/mirror_command.rb125
-rw-r--r--lib/rubygems/commands/open_command.rb83
-rw-r--r--lib/rubygems/commands/outdated_command.rb34
-rw-r--r--lib/rubygems/commands/owner_command.rb125
-rw-r--r--lib/rubygems/commands/pristine_command.rb232
-rw-r--r--lib/rubygems/commands/push_command.rb185
-rw-r--r--lib/rubygems/commands/query_command.rb233
-rw-r--r--lib/rubygems/commands/rdoc_command.rb156
-rw-r--r--lib/rubygems/commands/rebuild_command.rb261
-rw-r--r--lib/rubygems/commands/search_command.rb74
-rw-r--r--lib/rubygems/commands/server_command.rb60
-rw-r--r--lib/rubygems/commands/setup_command.rb667
-rw-r--r--lib/rubygems/commands/signin_command.rb34
-rw-r--r--lib/rubygems/commands/signout_command.rb32
-rw-r--r--lib/rubygems/commands/sources_command.rb382
-rw-r--r--lib/rubygems/commands/specification_command.rb149
-rw-r--r--lib/rubygems/commands/stale_command.rb25
-rw-r--r--lib/rubygems/commands/uninstall_command.rb239
-rw-r--r--lib/rubygems/commands/unpack_command.rb171
-rw-r--r--lib/rubygems/commands/update_command.rb355
-rw-r--r--lib/rubygems/commands/which_command.rb65
-rw-r--r--lib/rubygems/commands/yank_command.rb99
-rw-r--r--lib/rubygems/config_file.rb608
-rw-r--r--lib/rubygems/core_ext/kernel_gem.rb68
-rw-r--r--lib/rubygems/core_ext/kernel_require.rb152
-rw-r--r--lib/rubygems/core_ext/kernel_warn.rb45
-rw-r--r--lib/rubygems/core_ext/tcpsocket_init.rb54
-rwxr-xr-xlib/rubygems/custom_require.rb46
-rw-r--r--lib/rubygems/defaults.rb285
-rw-r--r--lib/rubygems/dependency.rb339
-rw-r--r--lib/rubygems/dependency_installer.rb378
-rw-r--r--lib/rubygems/dependency_list.rb193
-rw-r--r--lib/rubygems/deprecate.rb171
-rwxr-xr-xlib/rubygems/digest/digest_adapter.rb40
-rwxr-xr-xlib/rubygems/digest/md5.rb23
-rwxr-xr-xlib/rubygems/digest/sha1.rb17
-rwxr-xr-xlib/rubygems/digest/sha2.rb17
-rw-r--r--lib/rubygems/doc_manager.rb214
-rw-r--r--lib/rubygems/doctor.rb132
-rw-r--r--lib/rubygems/errors.rb177
-rw-r--r--lib/rubygems/exceptions.rb181
-rw-r--r--lib/rubygems/ext.rb16
-rw-r--r--lib/rubygems/ext/build_error.rb9
-rw-r--r--lib/rubygems/ext/builder.rb265
-rw-r--r--lib/rubygems/ext/cargo_builder.rb349
-rw-r--r--lib/rubygems/ext/cargo_builder/link_flag_converter.rb27
-rw-r--r--lib/rubygems/ext/cmake_builder.rb110
-rw-r--r--lib/rubygems/ext/configure_builder.rb20
-rw-r--r--lib/rubygems/ext/ext_conf_builder.rb74
-rw-r--r--lib/rubygems/ext/rake_builder.rb34
-rw-r--r--lib/rubygems/format.rb87
-rw-r--r--lib/rubygems/gem_openssl.rb83
-rw-r--r--lib/rubygems/gem_path_searcher.rb100
-rw-r--r--lib/rubygems/gem_runner.rb112
-rw-r--r--lib/rubygems/gemcutter_utilities.rb398
-rw-r--r--lib/rubygems/gemcutter_utilities/webauthn_listener.rb112
-rw-r--r--lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb163
-rw-r--r--lib/rubygems/gemcutter_utilities/webauthn_poller.rb80
-rw-r--r--lib/rubygems/gemspec_helpers.rb19
-rw-r--r--lib/rubygems/indexer.rb370
-rw-r--r--lib/rubygems/indexer/abstract_index_builder.rb88
-rw-r--r--lib/rubygems/indexer/latest_index_builder.rb35
-rw-r--r--lib/rubygems/indexer/marshal_index_builder.rb17
-rw-r--r--lib/rubygems/indexer/master_index_builder.rb54
-rw-r--r--lib/rubygems/indexer/quick_index_builder.rb50
-rw-r--r--lib/rubygems/install_message.rb13
-rw-r--r--lib/rubygems/install_update_options.rb233
-rw-r--r--lib/rubygems/installer.rb1114
-rw-r--r--lib/rubygems/installer_uninstaller_utils.rb27
-rw-r--r--lib/rubygems/local_remote_options.rb84
-rw-r--r--lib/rubygems/name_tuple.rb125
-rw-r--r--lib/rubygems/old_format.rb148
-rw-r--r--lib/rubygems/openssl.rb7
-rw-r--r--lib/rubygems/package.rb830
-rw-r--r--lib/rubygems/package/digest_io.rb63
-rw-r--r--lib/rubygems/package/f_sync_dir.rb24
-rw-r--r--lib/rubygems/package/file_source.rb32
-rw-r--r--lib/rubygems/package/io_source.rb48
-rw-r--r--lib/rubygems/package/old.rb169
-rw-r--r--lib/rubygems/package/source.rb4
-rw-r--r--lib/rubygems/package/tar_header.rb306
-rw-r--r--lib/rubygems/package/tar_input.rb219
-rw-r--r--lib/rubygems/package/tar_output.rb143
-rw-r--r--lib/rubygems/package/tar_reader.rb89
-rw-r--r--lib/rubygems/package/tar_reader/entry.rb189
-rw-r--r--lib/rubygems/package/tar_writer.rb238
-rw-r--r--lib/rubygems/package_task.rb123
-rw-r--r--lib/rubygems/path_support.rb85
-rw-r--r--lib/rubygems/platform.rb370
-rw-r--r--lib/rubygems/psych_tree.rb37
-rw-r--r--lib/rubygems/query_utils.rb349
-rw-r--r--lib/rubygems/rdoc.rb26
-rw-r--r--lib/rubygems/remote_fetcher.rb418
-rw-r--r--lib/rubygems/request.rb299
-rw-r--r--lib/rubygems/request/connection_pools.rb96
-rw-r--r--lib/rubygems/request/http_pool.rb54
-rw-r--r--lib/rubygems/request/https_pool.rb10
-rw-r--r--lib/rubygems/request_set.rb514
-rw-r--r--lib/rubygems/request_set/gem_dependency_api.rb841
-rw-r--r--lib/rubygems/request_set/lockfile.rb233
-rw-r--r--lib/rubygems/require_paths_builder.rb15
-rw-r--r--lib/rubygems/requirement.rb329
-rw-r--r--lib/rubygems/resolver.rb565
-rw-r--r--lib/rubygems/resolver/activation_request.rb159
-rw-r--r--lib/rubygems/resolver/api_set.rb139
-rw-r--r--lib/rubygems/resolver/api_set/gem_parser.rb21
-rw-r--r--lib/rubygems/resolver/api_specification.rb105
-rw-r--r--lib/rubygems/resolver/best_set.rb49
-rw-r--r--lib/rubygems/resolver/composed_set.rb65
-rw-r--r--lib/rubygems/resolver/current_set.rb12
-rw-r--r--lib/rubygems/resolver/dependency_request.rb119
-rw-r--r--lib/rubygems/resolver/git_set.rb120
-rw-r--r--lib/rubygems/resolver/git_specification.rb57
-rw-r--r--lib/rubygems/resolver/incompatibility.rb10
-rw-r--r--lib/rubygems/resolver/index_set.rb79
-rw-r--r--lib/rubygems/resolver/index_specification.rb101
-rw-r--r--lib/rubygems/resolver/installed_specification.rb57
-rw-r--r--lib/rubygems/resolver/installer_set.rb271
-rw-r--r--lib/rubygems/resolver/local_specification.rb40
-rw-r--r--lib/rubygems/resolver/lock_set.rb81
-rw-r--r--lib/rubygems/resolver/lock_specification.rb86
-rw-r--r--lib/rubygems/resolver/requirement_list.rb82
-rw-r--r--lib/rubygems/resolver/set.rb55
-rw-r--r--lib/rubygems/resolver/source_set.rb47
-rw-r--r--lib/rubygems/resolver/spec_specification.rb76
-rw-r--r--lib/rubygems/resolver/specification.rb126
-rw-r--r--lib/rubygems/resolver/strategy.rb44
-rw-r--r--lib/rubygems/resolver/vendor_set.rb86
-rw-r--r--lib/rubygems/resolver/vendor_specification.rb23
-rw-r--r--lib/rubygems/rubygems_version.rb6
-rw-r--r--lib/rubygems/s3_uri_signer.rb226
-rw-r--r--lib/rubygems/safe_marshal.rb75
-rw-r--r--lib/rubygems/safe_marshal/elements.rb146
-rw-r--r--lib/rubygems/safe_marshal/reader.rb325
-rw-r--r--lib/rubygems/safe_marshal/visitors/stream_printer.rb31
-rw-r--r--lib/rubygems/safe_marshal/visitors/to_ruby.rb428
-rw-r--r--lib/rubygems/safe_marshal/visitors/visitor.rb74
-rw-r--r--lib/rubygems/safe_yaml.rb55
-rw-r--r--lib/rubygems/security.rb965
-rw-r--r--lib/rubygems/security/policies.rb114
-rw-r--r--lib/rubygems/security/policy.rb288
-rw-r--r--lib/rubygems/security/signer.rb212
-rw-r--r--lib/rubygems/security/trust_dir.rb117
-rw-r--r--lib/rubygems/security_option.rb43
-rw-r--r--lib/rubygems/server.rb629
-rw-r--r--lib/rubygems/source.rb253
-rw-r--r--lib/rubygems/source/git.rb244
-rw-r--r--lib/rubygems/source/installed.rb39
-rw-r--r--lib/rubygems/source/local.rb135
-rw-r--r--lib/rubygems/source/lock.rb49
-rw-r--r--lib/rubygems/source/specific_file.rb73
-rw-r--r--lib/rubygems/source/vendor.rb24
-rw-r--r--lib/rubygems/source_index.rb559
-rw-r--r--lib/rubygems/source_info_cache.rb393
-rw-r--r--lib/rubygems/source_info_cache_entry.rb56
-rw-r--r--lib/rubygems/source_list.rb182
-rw-r--r--lib/rubygems/spec_fetcher.rb365
-rw-r--r--lib/rubygems/specification.rb3200
-rw-r--r--lib/rubygems/specification_policy.rb557
-rw-r--r--lib/rubygems/specification_record.rb225
-rw-r--r--lib/rubygems/ssl_certs/.document1
-rw-r--r--lib/rubygems/ssl_certs/rubygems.org/GlobalSign.pem21
-rw-r--r--lib/rubygems/stub_specification.rb236
-rw-r--r--lib/rubygems/target_rbconfig.rb50
-rw-r--r--lib/rubygems/test_utilities.rb131
-rw-r--r--lib/rubygems/text.rb94
-rw-r--r--lib/rubygems/timer.rb25
-rw-r--r--lib/rubygems/uninstaller.rb428
-rw-r--r--lib/rubygems/unknown_command_spell_checker.rb21
-rw-r--r--lib/rubygems/update_suggestion.rb56
-rw-r--r--lib/rubygems/uri.rb126
-rw-r--r--lib/rubygems/uri_formatter.rb48
-rw-r--r--lib/rubygems/user_interaction.rb769
-rw-r--r--lib/rubygems/util.rb96
-rw-r--r--lib/rubygems/util/atomic_file_writer.rb76
-rw-r--r--lib/rubygems/util/licenses.rb888
-rw-r--r--lib/rubygems/validator.rb227
-rw-r--r--lib/rubygems/vendor/.document1
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http.rb2608
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb35
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb429
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/header.rb985
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/proxy_delta.rb17
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/request.rb88
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/requests.rb444
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/response.rb739
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/responses.rb1242
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/status.rb84
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/https.rb23
-rw-r--r--lib/rubygems/vendor/net-protocol/lib/net/protocol.rb544
-rw-r--r--lib/rubygems/vendor/optparse/lib/optionparser.rb2
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse.rb2467
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/ac.rb70
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/date.rb18
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb27
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb7
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/time.rb11
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/uri.rb7
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/version.rb80
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub.rb53
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb20
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb169
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb182
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb150
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb43
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb121
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb45
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb19
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb61
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb42
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb105
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb3
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb129
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb423
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb236
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb178
-rw-r--r--lib/rubygems/vendor/resolv/lib/resolv.rb3499
-rw-r--r--lib/rubygems/vendor/securerandom/lib/securerandom.rb102
-rw-r--r--lib/rubygems/vendor/timeout/lib/timeout.rb201
-rw-r--r--lib/rubygems/vendor/tsort/lib/tsort.rb455
-rw-r--r--lib/rubygems/vendor/uri/lib/uri.rb104
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/common.rb922
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/file.rb100
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/ftp.rb267
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/generic.rb1592
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/http.rb137
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/https.rb23
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/ldap.rb261
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/ldaps.rb22
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/mailto.rb293
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb547
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/rfc3986_parser.rb206
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/version.rb6
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/ws.rb83
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/wss.rb23
-rw-r--r--lib/rubygems/vendored_net_http.rb5
-rw-r--r--lib/rubygems/vendored_optparse.rb3
-rw-r--r--lib/rubygems/vendored_pub_grub.rb3
-rw-r--r--lib/rubygems/vendored_securerandom.rb3
-rw-r--r--lib/rubygems/vendored_timeout.rb5
-rw-r--r--lib/rubygems/vendored_tsort.rb3
-rw-r--r--lib/rubygems/version.rb483
-rw-r--r--lib/rubygems/version_option.rb62
-rw-r--r--lib/rubygems/win_platform.rb30
-rw-r--r--lib/rubygems/yaml_serializer.rb845
-rw-r--r--lib/scanf.rb703
-rw-r--r--lib/securerandom.gemspec35
-rw-r--r--lib/securerandom.rb224
-rw-r--r--lib/set.rb1274
-rw-r--r--lib/set/subclass_compatible.rb347
-rw-r--r--lib/shell.rb300
-rw-r--r--lib/shell/builtin-command.rb160
-rw-r--r--lib/shell/command-processor.rb593
-rw-r--r--lib/shell/error.rb25
-rw-r--r--lib/shell/filter.rb109
-rw-r--r--lib/shell/process-controller.rb319
-rw-r--r--lib/shell/system-command.rb159
-rw-r--r--lib/shell/version.rb15
-rw-r--r--lib/shellwords.gemspec29
-rw-r--r--lib/shellwords.rb194
-rw-r--r--lib/singleton.gemspec30
-rw-r--r--lib/singleton.rb389
-rw-r--r--lib/sync.rb307
-rw-r--r--lib/syntax_suggest.rb3
-rw-r--r--lib/syntax_suggest/api.rb200
-rw-r--r--lib/syntax_suggest/around_block_scan.rb232
-rw-r--r--lib/syntax_suggest/block_expand.rb165
-rw-r--r--lib/syntax_suggest/capture/before_after_keyword_ends.rb85
-rw-r--r--lib/syntax_suggest/capture/falling_indent_lines.rb71
-rw-r--r--lib/syntax_suggest/capture_code_context.rb245
-rw-r--r--lib/syntax_suggest/clean_document.rb223
-rw-r--r--lib/syntax_suggest/cli.rb130
-rw-r--r--lib/syntax_suggest/code_block.rb100
-rw-r--r--lib/syntax_suggest/code_frontier.rb178
-rw-r--r--lib/syntax_suggest/code_line.rb222
-rw-r--r--lib/syntax_suggest/code_search.rb139
-rw-r--r--lib/syntax_suggest/core_ext.rb47
-rw-r--r--lib/syntax_suggest/display_code_with_line_numbers.rb70
-rw-r--r--lib/syntax_suggest/display_invalid_blocks.rb83
-rw-r--r--lib/syntax_suggest/explain_syntax.rb109
-rw-r--r--lib/syntax_suggest/left_right_token_count.rb162
-rw-r--r--lib/syntax_suggest/mini_stringio.rb30
-rw-r--r--lib/syntax_suggest/parse_blocks_from_indent_line.rb60
-rw-r--r--lib/syntax_suggest/pathname_from_message.rb59
-rw-r--r--lib/syntax_suggest/priority_engulf_queue.rb63
-rw-r--r--lib/syntax_suggest/priority_queue.rb105
-rw-r--r--lib/syntax_suggest/scan_history.rb134
-rw-r--r--lib/syntax_suggest/syntax_suggest.gemspec32
-rw-r--r--lib/syntax_suggest/token.rb49
-rw-r--r--lib/syntax_suggest/unvisited_lines.rb36
-rw-r--r--lib/syntax_suggest/version.rb5
-rw-r--r--lib/syntax_suggest/visitor.rb80
-rw-r--r--lib/tempfile.gemspec33
-rw-r--r--lib/tempfile.rb711
-rw-r--r--lib/test/unit.rb66
-rw-r--r--lib/test/unit/assertions.rb122
-rw-r--r--lib/test/unit/testcase.rb12
-rw-r--r--lib/thread.rb367
-rw-r--r--lib/thwait.rb168
-rw-r--r--lib/time.gemspec36
-rw-r--r--lib/time.rb945
-rw-r--r--lib/timeout.gemspec33
-rw-r--r--lib/timeout.rb372
-rw-r--r--lib/tmpdir.gemspec30
-rw-r--r--lib/tmpdir.rb192
-rw-r--r--lib/tracer.rb166
-rw-r--r--lib/tsort.rb290
-rw-r--r--lib/ubygems.rb10
-rw-r--r--lib/un.gemspec31
-rw-r--r--lib/un.rb261
-rw-r--r--lib/unicode_normalize/normalize.rb187
-rw-r--r--lib/unicode_normalize/tables.rb9439
-rw-r--r--lib/uri.rb113
-rw-r--r--lib/uri/.document7
-rw-r--r--lib/uri/common.rb1363
-rw-r--r--lib/uri/file.rb100
-rw-r--r--lib/uri/ftp.rb145
-rw-r--r--lib/uri/generic.rb1162
-rw-r--r--lib/uri/http.rb119
-rw-r--r--lib/uri/https.rb11
-rw-r--r--lib/uri/ldap.rb89
-rw-r--r--lib/uri/ldaps.rb14
-rw-r--r--lib/uri/mailto.rb211
-rw-r--r--lib/uri/rfc2396_parser.rb547
-rw-r--r--lib/uri/rfc3986_parser.rb206
-rw-r--r--lib/uri/uri.gemspec42
-rw-r--r--lib/uri/version.rb6
-rw-r--r--lib/uri/ws.rb83
-rw-r--r--lib/uri/wss.rb23
-rw-r--r--lib/weakref.gemspec32
-rw-r--r--lib/weakref.rb101
-rw-r--r--lib/webrick.rb29
-rw-r--r--lib/webrick/accesslog.rb67
-rw-r--r--lib/webrick/cgi.rb260
-rw-r--r--lib/webrick/compat.rb15
-rw-r--r--lib/webrick/config.rb100
-rw-r--r--lib/webrick/cookie.rb110
-rw-r--r--lib/webrick/htmlutils.rb25
-rw-r--r--lib/webrick/httpauth.rb45
-rw-r--r--lib/webrick/httpauth/authenticator.rb79
-rw-r--r--lib/webrick/httpauth/basicauth.rb65
-rw-r--r--lib/webrick/httpauth/digestauth.rb344
-rw-r--r--lib/webrick/httpauth/htdigest.rb91
-rw-r--r--lib/webrick/httpauth/htgroup.rb61
-rw-r--r--lib/webrick/httpauth/htpasswd.rb83
-rw-r--r--lib/webrick/httpauth/userdb.rb29
-rw-r--r--lib/webrick/httpproxy.rb288
-rw-r--r--lib/webrick/httprequest.rb406
-rw-r--r--lib/webrick/httpresponse.rb326
-rw-r--r--lib/webrick/https.rb63
-rw-r--r--lib/webrick/httpserver.rb217
-rw-r--r--lib/webrick/httpservlet.rb22
-rw-r--r--lib/webrick/httpservlet/abstract.rb71
-rw-r--r--lib/webrick/httpservlet/cgi_runner.rb47
-rw-r--r--lib/webrick/httpservlet/cgihandler.rb110
-rw-r--r--lib/webrick/httpservlet/erbhandler.rb54
-rw-r--r--lib/webrick/httpservlet/filehandler.rb435
-rw-r--r--lib/webrick/httpservlet/prochandler.rb33
-rw-r--r--lib/webrick/httpstatus.rb126
-rw-r--r--lib/webrick/httputils.rb392
-rw-r--r--lib/webrick/httpversion.rb49
-rw-r--r--lib/webrick/log.rb88
-rw-r--r--lib/webrick/server.rb210
-rw-r--r--lib/webrick/ssl.rb126
-rw-r--r--lib/webrick/utils.rb175
-rw-r--r--lib/webrick/version.rb13
-rw-r--r--lib/xmlrpc/.document1
-rw-r--r--lib/xmlrpc/README.rdoc300
-rw-r--r--lib/xmlrpc/README.txt31
-rw-r--r--lib/xmlrpc/base64.rb81
-rw-r--r--lib/xmlrpc/client.rb625
-rw-r--r--lib/xmlrpc/config.rb40
-rw-r--r--lib/xmlrpc/create.rb290
-rw-r--r--lib/xmlrpc/datetime.rb142
-rw-r--r--lib/xmlrpc/httpserver.rb178
-rw-r--r--lib/xmlrpc/marshal.rb76
-rw-r--r--lib/xmlrpc/parser.rb813
-rw-r--r--lib/xmlrpc/server.rb782
-rw-r--r--lib/xmlrpc/utils.rb165
-rw-r--r--lib/yaml.rb481
-rw-r--r--lib/yaml/baseemitter.rb242
-rw-r--r--lib/yaml/basenode.rb216
-rw-r--r--lib/yaml/constants.rb45
-rw-r--r--lib/yaml/dbm.rb220
-rw-r--r--lib/yaml/encoding.rb33
-rw-r--r--lib/yaml/error.rb34
-rw-r--r--lib/yaml/loader.rb14
-rw-r--r--lib/yaml/rubytypes.rb446
-rw-r--r--lib/yaml/store.rb81
-rw-r--r--lib/yaml/stream.rb40
-rw-r--r--lib/yaml/stringio.rb83
-rw-r--r--lib/yaml/syck.rb19
-rw-r--r--lib/yaml/tag.rb91
-rw-r--r--lib/yaml/types.rb192
-rw-r--r--lib/yaml/yaml.gemspec30
-rw-r--r--lib/yaml/yamlnode.rb54
-rw-r--r--lib/yaml/ypath.rb52
1152 files changed, 155126 insertions, 109996 deletions
diff --git a/lib/.document b/lib/.document
deleted file mode 100644
index d3c4a1369b..0000000000
--- a/lib/.document
+++ /dev/null
@@ -1,107 +0,0 @@
-# We only run RDoc on the top-level files in here: we skip
-# all the helper stuff in sub-directories
-
-# Eventually, we hope to see...
-# *.rb
-
-# But for now
-
-English.rb
-Env.rb
-README
-abbrev.rb
-base64.rb
-benchmark.rb
-cgi
-cgi.rb
-complex.rb
-csv.rb
-date
-date.rb
-date2.rb
-debug.rb
-delegate.rb
-drb
-drb.rb
-erb.rb
-eregex.rb
-fileutils.rb
-finalize.rb
-find.rb
-forwardable.rb
-ftools.rb
-generator.rb
-getoptlong.rb
-getopts.rb
-gserver.rb
-importenv.rb
-ipaddr.rb
-irb
-irb.rb
-jcode.rb
-logger.rb
-mailread.rb
-mathn.rb
-matrix.rb
-mkmf.rb
-monitor.rb
-mutex_m.rb
-net
-observer.rb
-open-uri.rb
-open3.rb
-optparse
-optparse.rb
-ostruct.rb
-parsearg.rb
-parsedate.rb
-pathname.rb
-ping.rb
-pp.rb
-prettyprint.rb
-prime.rb
-profile.rb
-profiler.rb
-pstore.rb
-racc
-rational.rb
-rdoc.rb
-rdoc
-readbytes.rb
-resolv-replace.rb
-resolv.rb
-rexml
-rinda
-rss
-rss.rb
-rubyunit.rb
-runit
-scanf.rb
-set.rb
-shell
-shell.rb
-shellwords.rb
-# TODO: YARV cause error. why ...?
-# singleton.rb
-soap
-sync.rb
-tempfile.rb
-test
-thread.rb
-thwait.rb
-time.rb
-timeout.rb
-tmpdir.rb
-tracer.rb
-tsort.rb
-un.rb
-uri
-uri.rb
-weakref.rb
-webrick
-webrick.rb
-wsdl
-xmlrpc
-xsd
-yaml
-yaml.rb
diff --git a/lib/English.gemspec b/lib/English.gemspec
new file mode 100644
index 0000000000..9c09555ca1
--- /dev/null
+++ b/lib/English.gemspec
@@ -0,0 +1,27 @@
+Gem::Specification.new do |spec|
+ spec.name = "english"
+ spec.version = "0.8.1"
+ spec.authors = ["Yukihiro Matsumoto"]
+ spec.email = ["matz@ruby-lang.org"]
+
+ spec.summary = %q{Require 'English.rb' to reference global variables with less cryptic names.}
+ spec.description = %q{Require 'English.rb' to reference global variables with less cryptic names.}
+ spec.homepage = "https://github.com/ruby/English"
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ excludes = %W[
+ :^/test :^/spec :^/feature :^/bin
+ :^/Rakefile :^/Gemfile\* :^/.git*
+ :^/#{File.basename(__FILE__)}
+ ]
+ spec.files = IO.popen(%W[git ls-files -z --] + excludes, err: IO::NULL) do |f|
+ f.readlines("\x0", chomp: true)
+ end
+ spec.require_paths = ["lib"]
+end
diff --git a/lib/English.rb b/lib/English.rb
index 1a0e11de74..bf7896dcd6 100644
--- a/lib/English.rb
+++ b/lib/English.rb
@@ -1,113 +1,127 @@
+# frozen_string_literal: true
# Include the English library file in a Ruby script, and you can
-# reference the global variables such as \VAR{\$\_} using less
-# cryptic names, listed in the following table.% \vref{tab:english}.
+# reference the global variables such as <code>$_</code> using less
+# cryptic names, listed below.
#
# Without 'English':
#
# $\ = ' -- '
# "waterbuffalo" =~ /buff/
-# print $", $', $$, "\n"
+# print $', $$, "\n"
#
-# With English:
+# With 'English':
#
# require "English"
-#
+#
# $OUTPUT_FIELD_SEPARATOR = ' -- '
# "waterbuffalo" =~ /buff/
-# print $LOADED_FEATURES, $POSTMATCH, $PID, "\n"
-
+# print $POSTMATCH, $PID, "\n"
+#
+# Below is a full list of descriptive aliases and their associated global
+# variable:
+#
+# <tt>$ERROR_INFO</tt>:: <tt>$!</tt>
+# <tt>$ERROR_POSITION</tt>:: <tt>$@</tt>
+# <tt>$FS</tt>:: <tt>$;</tt>
+# <tt>$FIELD_SEPARATOR</tt>:: <tt>$;</tt>
+# <tt>$OFS</tt>:: <tt>$,</tt>
+# <tt>$OUTPUT_FIELD_SEPARATOR</tt>:: <tt>$,</tt>
+# <tt>$RS</tt>:: <tt>$/</tt>
+# <tt>$INPUT_RECORD_SEPARATOR</tt>:: <tt>$/</tt>
+# <tt>$ORS</tt>:: <tt>$\</tt>
+# <tt>$OUTPUT_RECORD_SEPARATOR</tt>:: <tt>$\</tt>
+# <tt>$NR</tt>:: <tt>$.</tt>
+# <tt>$INPUT_LINE_NUMBER</tt>:: <tt>$.</tt>
+# <tt>$LAST_READ_LINE</tt>:: <tt>$_</tt>
+# <tt>$DEFAULT_OUTPUT</tt>:: <tt>$></tt>
+# <tt>$DEFAULT_INPUT</tt>:: <tt>$<</tt>
+# <tt>$PID</tt>:: <tt>$$</tt>
+# <tt>$PROCESS_ID</tt>:: <tt>$$</tt>
+# <tt>$CHILD_STATUS</tt>:: <tt>$?</tt>
+# <tt>$LAST_MATCH_INFO</tt>:: <tt>$~</tt>
+# <tt>$ARGV</tt>:: <tt>$*</tt>
+# <tt>$MATCH</tt>:: <tt>$&</tt>
+# <tt>$PREMATCH</tt>:: <tt>$`</tt>
+# <tt>$POSTMATCH</tt>:: <tt>$'</tt>
+# <tt>$LAST_PAREN_MATCH</tt>:: <tt>$+</tt>
+#
+module English end if false
# The exception object passed to +raise+.
alias $ERROR_INFO $!
# The stack backtrace generated by the last
-# exception. <tt>See Kernel.caller</tt> for details. Thread local.
+# exception. See Kernel#caller for details. Thread local.
alias $ERROR_POSITION $@
-# The default separator pattern used by <tt>String.split</tt>. May be
-# set from the command line using the <tt>-F</tt> flag.
+# The default separator pattern used by String#split. May be set from
+# the command line using the <code>-F</code> flag.
alias $FS $;
-
-# The default separator pattern used by <tt>String.split</tt>. May be
-# set from the command line using the <tt>-F</tt> flag.
alias $FIELD_SEPARATOR $;
# The separator string output between the parameters to methods such
-# as <tt>Kernel.print</tt> and <tt>Array.join</tt>. Defaults to +nil+,
-# which adds no text.
-alias $OFS $,
+# as Kernel#print and Array#join. Defaults to +nil+, which adds no
+# text.
# The separator string output between the parameters to methods such
-# as <tt>Kernel.print</tt> and <tt>Array.join</tt>. Defaults to +nil+,
-# which adds no text.
+# as Kernel#print and Array#join. Defaults to +nil+, which adds no
+# text.
+alias $OFS $,
alias $OUTPUT_FIELD_SEPARATOR $,
# The input record separator (newline by default). This is the value
-# that routines such as <tt>Kernel.gets</tt> use to determine record
+# that routines such as Kernel#gets use to determine record
# boundaries. If set to +nil+, +gets+ will read the entire file.
alias $RS $/
-
-# The input record separator (newline by default). This is the value
-# that routines such as <tt>Kernel.gets</tt> use to determine record
-# boundaries. If set to +nil+, +gets+ will read the entire file.
alias $INPUT_RECORD_SEPARATOR $/
# The string appended to the output of every call to methods such as
-# <tt>Kernel.print</tt> and <tt>IO.write</tt>. The default value is
-# +nil+.
+# Kernel#print and IO#write. The default value is +nil+.
alias $ORS $\
-
-# The string appended to the output of every call to methods such as
-# <tt>Kernel.print</tt> and <tt>IO.write</tt>. The default value is
-# +nil+.
alias $OUTPUT_RECORD_SEPARATOR $\
# The number of the last line read from the current input file.
-alias $INPUT_LINE_NUMBER $.
-
-# The number of the last line read from the current input file.
alias $NR $.
+alias $INPUT_LINE_NUMBER $.
-# The last line read by <tt>Kernel.gets</tt> or
-# <tt>Kernel.readline</tt>. Many string-related functions in the
-# +Kernel+ module operate on <tt>$_</tt> by default. The variable is
+# The last line read by Kernel#gets or
+# Kernel#readline. Many string-related functions in the
+# Kernel module operate on <code>$_</code> by default. The variable is
# local to the current scope. Thread local.
alias $LAST_READ_LINE $_
-# The destination of output for <tt>Kernel.print</tt>
-# and <tt>Kernel.printf</tt>. The default value is
-# <tt>$stdout</tt>.
+# The destination of output for Kernel#print
+# and Kernel#printf. The default value is
+# <code>$stdout</code>.
alias $DEFAULT_OUTPUT $>
# An object that provides access to the concatenation
# of the contents of all the files
-# given as command-line arguments, or <tt>$stdin</tt>
+# given as command-line arguments, or <code>$stdin</code>
# (in the case where there are no
-# arguments). <tt>$<</tt> supports methods similar to a
-# +File+ object:
+# arguments). <code>$<</code> supports methods similar to a
+# File object:
# +inmode+, +close+,
-# <tt>closed?</tt>, +each+,
-# <tt>each_byte</tt>, <tt>each_line</tt>,
-# +eof+, <tt>eof?</tt>, +file+,
+# <code>closed?</code>, +each+,
+# <code>each_byte</code>, <code>each_line</code>,
+# +eof+, <code>eof?</code>, +file+,
# +filename+, +fileno+,
# +getc+, +gets+, +lineno+,
-# <tt>lineno=</tt>, +path+,
-# +pos+, <tt>pos=</tt>,
+# <code>lineno=</code>, +path+,
+# +pos+, <code>pos=</code>,
# +read+, +readchar+,
# +readline+, +readlines+,
# +rewind+, +seek+, +skip+,
-# +tell+, <tt>to_a</tt>, <tt>to_i</tt>,
-# <tt>to_io</tt>, <tt>to_s</tt>, along with the
-# methods in +Enumerable+. The method +file+
-# returns a +File+ object for the file currently
-# being read. This may change as <tt>$<</tt> reads
+# +tell+, <code>to_a</code>, <code>to_i</code>,
+# <code>to_io</code>, <code>to_s</code>, along with the
+# methods in Enumerable. The method +file+
+# returns a File object for the file currently
+# being read. This may change as <code>$<</code> reads
# through the files on the command line. Read only.
alias $DEFAULT_INPUT $<
# The process number of the program being executed. Read only.
alias $PID $$
-
-# The process number of the program being executed. Read only.
alias $PROCESS_ID $$
# The exit status of the last child process to terminate. Read
@@ -115,18 +129,13 @@ alias $PROCESS_ID $$
alias $CHILD_STATUS $?
# A +MatchData+ object that encapsulates the results of a successful
-# pattern match. The variables <tt>$&</tt>, <tt>$`</tt>, <tt>$'</tt>,
-# and <tt>$1</tt> to <tt>$9</tt> are all derived from
-# <tt>$~</tt>. Assigning to <tt>$~</tt> changes the values of these
+# pattern match. The variables <code>$&</code>, <code>$`</code>, <code>$'</code>,
+# and <code>$1</code> to <code>$9</code> are all derived from
+# <code>$~</code>. Assigning to <code>$~</code> changes the values of these
# derived variables. This variable is local to the current
-# scope. Thread local.
+# scope.
alias $LAST_MATCH_INFO $~
-# If set to any value apart from +nil+ or +false+, all pattern matches
-# will be case insensitive, string comparisons will ignore case, and
-# string hash values will be case insensitive. Deprecated
-alias $IGNORECASE $=
-
# An array of strings containing the command-line
# options from the invocation of the program. Options
# used by the Ruby interpreter will have been
@@ -135,21 +144,21 @@ alias $ARGV $*
# The string matched by the last successful pattern
# match. This variable is local to the current
-# scope. Read only. Thread local.
+# scope. Read only.
alias $MATCH $&
# The string preceding the match in the last
-# successful pattern match. This variable is local to
-# the current scope. Read only. Thread local.
+# successful pattern match. This variable is local to
+# the current scope. Read only.
alias $PREMATCH $`
# The string following the match in the last
-# successful pattern match. This variable is local to
-# the current scope. Read only. Thread local.
+# successful pattern match. This variable is local to
+# the current scope. Read only.
alias $POSTMATCH $'
# The contents of the highest-numbered group matched in the last
-# successful pattern match. Thus, in <tt>"cat" =~ /(c|a)(t|z)/</tt>,
-# <tt>$+</tt> will be set to "t". This variable is local to the
-# current scope. Read only. Thread local.
+# successful pattern match. Thus, in <code>"cat" =~ /(c|a)(t|z)/</code>,
+# <code>$+</code> will be set to "t". This variable is local to the
+# current scope. Read only.
alias $LAST_PAREN_MATCH $+
diff --git a/lib/README b/lib/README
deleted file mode 100644
index 1132755cf5..0000000000
--- a/lib/README
+++ /dev/null
@@ -1,80 +0,0 @@
-English.rb lets Perl'ish global variables have English names
-README this file
-benchmark.rb a benchmark utility
-cgi.rb CGI support library
-cgi/session.rb CGI session class
-complex.rb complex number support
-csv.rb CSV parser/generator
-date.rb date object
-date/format.rb date parsing and formatting
-debug.rb ruby debugger
-delegate.rb delegates messages to other object
-drb.rb distributed Ruby
-e2mmap.rb exception utilities
-erb.rb tiny eRuby library
-fileutils.rb file utilities
-finalize.rb adds finalizer to the object
-find.rb traverses directory tree
-forwardable.rb explicit delegation library
-getoptlong.rb GNU getoptlong compatible
-gserver.rb general TCP server
-ipaddr.rb defines the IPAddr class
-irb.rb interactive ruby
-logger.rb simple logging utility
-mathn.rb extended math operation
-matrix.rb matrix calculation library
-mkmf.rb Makefile maker
-monitor.rb exclusive region monitor for thread
-mutex_m.rb mutex mixin
-net/ftp.rb ftp access
-net/http.rb HTTP access
-net/imap.rb IMAP4 access
-net/pop.rb POP3 access
-net/protocol.rb abstract class for net library (DO NOT USE)
-net/smtp.rb SMTP access
-net/telnet.rb telnet library
-observer.rb observer desing pattern library (provides Observable)
-open-uri.rb easy-to-use network interface using URI and Net
-open3.rb opens subprocess connection stdin/stdout/stderr
-optparse.rb command line option analysis
-ostruct.rb python style object
-parsedate.rb parses date string (obsolete)
-pathname.rb Object-Oriented Pathname Class
-pp.rb pretty print objects
-prettyprint.rb pretty printing algorithm
-prime.rb prime numbers and factorization
-profile.rb runs ruby profiler
-profiler.rb ruby profiler module
-pstore.rb persistent object strage using marshal
-racc/parser.rb racc (Ruby yACC) runtime
-rational.rb rational number support
-rdoc source-code documentation tool
-resolv-replace.rb replace Socket DNS by resolve.rb
-resolv.rb DNS resolver in Ruby
-rexml an XML parser for Ruby, in Ruby
-scanf.rb scanf for Ruby
-set.rb defines the Set class
-shell.rb runs commands and does pipeline operations like shell
-shellwords.rb split into words like shell
-singleton.rb singleton design pattern library
-sync.rb 2 phase lock
-tempfile.rb temporary file with automatic removal
-test/unit Ruby Unit Testing Framework
-thread.rb thread support
-thwait.rb thread syncronization class
-time.rb RFC2822, RFC2616, ISO8601 style time formatting/parsing
-timeout.rb provides timeout
-tmpdir.rb retrieve temporary directory path
-tracer.rb execution tracer
-tsort.rb topological sorting
-un.rb Utilities to replace common UNIX commands in Makefiles etc
-uri.rb URI support
-uri/ftp.rb ftp scheme support
-uri/http.rb http scheme support
-uri/https.rb https scheme support
-uri/ldap.rb ldap scheme support
-uri/mailto.rb mailto scheme support
-weakref.rb weak reference class
-webrick.rb WEB server toolkit
-xmlrpc XML-RPC implementation
-yaml.rb YAML implementation
diff --git a/lib/abbrev.rb b/lib/abbrev.rb
deleted file mode 100644
index 6530679681..0000000000
--- a/lib/abbrev.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-#!/usr/bin/env ruby
-=begin
-#
-# Copyright (c) 2001,2003 Akinori MUSHA <knu@iDaemons.org>
-#
-# All rights reserved. You can redistribute and/or modify it under
-# the same terms as Ruby.
-#
-# $Idaemons: /home/cvs/rb/abbrev.rb,v 1.2 2001/05/30 09:37:45 knu Exp $
-# $RoughId: abbrev.rb,v 1.4 2003/10/14 19:45:42 knu Exp $
-# $Id$
-=end
-
-# Calculate the set of unique abbreviations for a given set of strings.
-#
-# require 'abbrev'
-# require 'pp'
-#
-# pp Abbrev::abbrev(['ruby', 'rules']).sort
-#
-# <i>Generates:</i>
-#
-# [["rub", "ruby"],
-# ["ruby", "ruby"],
-# ["rul", "rules"],
-# ["rule", "rules"],
-# ["rules", "rules"]]
-#
-# Also adds an +abbrev+ method to class +Array+.
-
-module Abbrev
-
- # Given a set of strings, calculate the set of unambiguous
- # abbreviations for those strings, and return a hash where the keys
- # are all the possible abbreviations and the values are the full
- # strings. Thus, given input of "car" and "cone", the keys pointing
- # to "car" would be "ca" and "car", while those pointing to "cone"
- # would be "co", "con", and "cone".
- #
- # The optional +pattern+ parameter is a pattern or a string. Only
- # those input strings matching the pattern, or begging the string,
- # are considered for inclusion in the output hash
-
- def abbrev(words, pattern = nil)
- table = {}
- seen = Hash.new(0)
-
- if pattern.is_a?(String)
- pattern = /^#{Regexp.quote(pattern)}/ # regard as a prefix
- end
-
- words.each do |word|
- next if (abbrev = word).empty?
- while (len = abbrev.rindex(/[\w\W]\z/)) > 0
- abbrev = word[0,len]
-
- next if pattern && pattern !~ abbrev
-
- case seen[abbrev] += 1
- when 1
- table[abbrev] = word
- when 2
- table.delete(abbrev)
- else
- break
- end
- end
- end
-
- words.each do |word|
- next if pattern && pattern !~ word
-
- table[word] = word
- end
-
- table
- end
-
- module_function :abbrev
-end
-
-class Array
- # Calculates the set of unambiguous abbreviations for the strings in
- # +self+. If passed a pattern or a string, only the strings matching
- # the pattern or starting with the string are considered.
- #
- # %w{ car cone }.abbrev #=> { "ca" => "car", "car" => "car",
- # "co" => "cone", "con" => cone",
- # "cone" => "cone" }
- def abbrev(pattern = nil)
- Abbrev::abbrev(self, pattern)
- end
-end
-
-if $0 == __FILE__
- while line = gets
- hash = line.split.abbrev
-
- hash.sort.each do |k, v|
- puts "#{k} => #{v}"
- end
- end
-end
diff --git a/lib/base64.rb b/lib/base64.rb
deleted file mode 100644
index ebd796eccd..0000000000
--- a/lib/base64.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-#
-# = base64.rb: methods for base64-encoding and -decoding stings
-#
-
-# The Base64 module provides for the encoding (#encode64, #strict_encode64,
-# #urlsafe_encode64) and decoding (#decode64, #strict_decode64,
-# #urlsafe_decode64) of binary data using a Base64 representation.
-#
-# == Example
-#
-# A simple encoding and decoding.
-#
-# require "base64"
-#
-# enc = Base64.encode64('Send reinforcements')
-# # -> "U2VuZCByZWluZm9yY2VtZW50cw==\n"
-# plain = Base64.decode64(enc)
-# # -> "Send reinforcements"
-#
-# The purpose of using base64 to encode data is that it translates any
-# binary data into purely printable characters.
-
-module Base64
- module_function
-
- # Returns the Base64-encoded version of +bin+.
- # This method complies with RFC 2045.
- # Line feeds are added to every 60 encoded charactors.
- #
- # require 'base64'
- # Base64.encode64("Now is the time for all good coders\nto learn Ruby")
- #
- # <i>Generates:</i>
- #
- # Tm93IGlzIHRoZSB0aW1lIGZvciBhbGwgZ29vZCBjb2RlcnMKdG8gbGVhcm4g
- # UnVieQ==
- def encode64(bin)
- [bin].pack("m")
- end
-
- # Returns the Base64-decoded version of +str+.
- # This method complies with RFC 2045.
- # Characters outside the base alphabet are ignored.
- #
- # require 'base64'
- # str = 'VGhpcyBpcyBsaW5lIG9uZQpUaGlzIG' +
- # 'lzIGxpbmUgdHdvClRoaXMgaXMgbGlu' +
- # 'ZSB0aHJlZQpBbmQgc28gb24uLi4K'
- # puts Base64.decode64(str)
- #
- # <i>Generates:</i>
- #
- # This is line one
- # This is line two
- # This is line three
- # And so on...
- def decode64(str)
- str.unpack("m").first
- end
-
- # Returns the Base64-encoded version of +bin+.
- # This method complies with RFC 4648.
- # No line feeds are added.
- def strict_encode64(bin)
- [bin].pack("m0")
- end
-
- # Returns the Base64-decoded version of +str+.
- # This method complies with RFC 4648.
- # ArgumentError is raised if +str+ is incorrectly padded or contains
- # non-alphabet characters. Note that CR or LF are also rejected.
- def strict_decode64(str)
- str.unpack("m0").first
- end
-
- # Returns the Base64-encoded version of +bin+.
- # This method complies with ``Base 64 Encoding with URL and Filename Safe
- # Alphabet'' in RFC 4648.
- # The alphabet uses '-' instead of '+' and '_' instead of '/'.
- def urlsafe_encode64(bin)
- strict_encode64(bin).tr("+/", "-_")
- end
-
- # Returns the Base64-decoded version of +str+.
- # This method complies with ``Base 64 Encoding with URL and Filename Safe
- # Alphabet'' in RFC 4648.
- # The alphabet uses '-' instead of '+' and '_' instead of '/'.
- def urlsafe_decode64(str)
- strict_decode64(str.tr("-_", "+/"))
- end
-end
diff --git a/lib/benchmark.rb b/lib/benchmark.rb
deleted file mode 100644
index a54700a1c2..0000000000
--- a/lib/benchmark.rb
+++ /dev/null
@@ -1,572 +0,0 @@
-=begin
-#
-# benchmark.rb - a performance benchmarking library
-#
-# $Id$
-#
-# Created by Gotoken (gotoken@notwork.org).
-#
-# Documentation by Gotoken (original RD), Lyle Johnson (RDoc conversion), and
-# Gavin Sinclair (editing).
-#
-=end
-
-# == Overview
-#
-# The Benchmark module provides methods for benchmarking Ruby code, giving
-# detailed reports on the time taken for each task.
-#
-
-# The Benchmark module provides methods to measure and report the time
-# used to execute Ruby code.
-#
-# * Measure the time to construct the string given by the expression
-# <tt>"a"*1_000_000</tt>:
-#
-# require 'benchmark'
-#
-# puts Benchmark.measure { "a"*1_000_000 }
-#
-# On my machine (FreeBSD 3.2 on P5, 100MHz) this generates:
-#
-# 1.166667 0.050000 1.216667 ( 0.571355)
-#
-# This report shows the user CPU time, system CPU time, the sum of
-# the user and system CPU times, and the elapsed real time. The unit
-# of time is seconds.
-#
-# * Do some experiments sequentially using the #bm method:
-#
-# require 'benchmark'
-#
-# n = 50000
-# Benchmark.bm do |x|
-# x.report { for i in 1..n; a = "1"; end }
-# x.report { n.times do ; a = "1"; end }
-# x.report { 1.upto(n) do ; a = "1"; end }
-# end
-#
-# The result:
-#
-# user system total real
-# 1.033333 0.016667 1.016667 ( 0.492106)
-# 1.483333 0.000000 1.483333 ( 0.694605)
-# 1.516667 0.000000 1.516667 ( 0.711077)
-#
-# * Continuing the previous example, put a label in each report:
-#
-# require 'benchmark'
-#
-# n = 50000
-# Benchmark.bm(7) do |x|
-# x.report("for:") { for i in 1..n; a = "1"; end }
-# x.report("times:") { n.times do ; a = "1"; end }
-# x.report("upto:") { 1.upto(n) do ; a = "1"; end }
-# end
-#
-# The result:
-#
-# user system total real
-# for: 1.050000 0.000000 1.050000 ( 0.503462)
-# times: 1.533333 0.016667 1.550000 ( 0.735473)
-# upto: 1.500000 0.016667 1.516667 ( 0.711239)
-#
-#
-# * The times for some benchmarks depend on the order in which items
-# are run. These differences are due to the cost of memory
-# allocation and garbage collection. To avoid these discrepancies,
-# the #bmbm method is provided. For example, to compare ways to
-# sort an array of floats:
-#
-# require 'benchmark'
-#
-# array = (1..1000000).map { rand }
-#
-# Benchmark.bmbm do |x|
-# x.report("sort!") { array.dup.sort! }
-# x.report("sort") { array.dup.sort }
-# end
-#
-# The result:
-#
-# Rehearsal -----------------------------------------
-# sort! 11.928000 0.010000 11.938000 ( 12.756000)
-# sort 13.048000 0.020000 13.068000 ( 13.857000)
-# ------------------------------- total: 25.006000sec
-#
-# user system total real
-# sort! 12.959000 0.010000 12.969000 ( 13.793000)
-# sort 12.007000 0.000000 12.007000 ( 12.791000)
-#
-#
-# * Report statistics of sequential experiments with unique labels,
-# using the #benchmark method:
-#
-# require 'benchmark'
-#
-# n = 50000
-# Benchmark.benchmark(" "*7 + CAPTION, 7, FMTSTR, ">total:", ">avg:") do |x|
-# tf = x.report("for:") { for i in 1..n; a = "1"; end }
-# tt = x.report("times:") { n.times do ; a = "1"; end }
-# tu = x.report("upto:") { 1.upto(n) do ; a = "1"; end }
-# [tf+tt+tu, (tf+tt+tu)/3]
-# end
-#
-# The result:
-#
-# user system total real
-# for: 1.016667 0.016667 1.033333 ( 0.485749)
-# times: 1.450000 0.016667 1.466667 ( 0.681367)
-# upto: 1.533333 0.000000 1.533333 ( 0.722166)
-# >total: 4.000000 0.033333 4.033333 ( 1.889282)
-# >avg: 1.333333 0.011111 1.344444 ( 0.629761)
-
-module Benchmark
-
- BENCHMARK_VERSION = "2002-04-25" #:nodoc"
-
- def Benchmark::times() # :nodoc:
- Process::times()
- end
-
-
- # Invokes the block with a <tt>Benchmark::Report</tt> object, which
- # may be used to collect and report on the results of individual
- # benchmark tests. Reserves <i>label_width</i> leading spaces for
- # labels on each line. Prints _caption_ at the top of the
- # report, and uses _fmt_ to format each line.
- # If the block returns an array of
- # <tt>Benchmark::Tms</tt> objects, these will be used to format
- # additional lines of output. If _label_ parameters are
- # given, these are used to label these extra lines.
- #
- # _Note_: Other methods provide a simpler interface to this one, and are
- # suitable for nearly all benchmarking requirements. See the examples in
- # Benchmark, and the #bm and #bmbm methods.
- #
- # Example:
- #
- # require 'benchmark'
- # include Benchmark # we need the CAPTION and FMTSTR constants
- #
- # n = 50000
- # Benchmark.benchmark(" "*7 + CAPTION, 7, FMTSTR, ">total:", ">avg:") do |x|
- # tf = x.report("for:") { for i in 1..n; a = "1"; end }
- # tt = x.report("times:") { n.times do ; a = "1"; end }
- # tu = x.report("upto:") { 1.upto(n) do ; a = "1"; end }
- # [tf+tt+tu, (tf+tt+tu)/3]
- # end
- #
- # <i>Generates:</i>
- #
- # user system total real
- # for: 1.016667 0.016667 1.033333 ( 0.485749)
- # times: 1.450000 0.016667 1.466667 ( 0.681367)
- # upto: 1.533333 0.000000 1.533333 ( 0.722166)
- # >total: 4.000000 0.033333 4.033333 ( 1.889282)
- # >avg: 1.333333 0.011111 1.344444 ( 0.629761)
- #
-
- def benchmark(caption = "", label_width = nil, fmtstr = nil, *labels) # :yield: report
- sync = STDOUT.sync
- STDOUT.sync = true
- label_width ||= 0
- fmtstr ||= FMTSTR
- raise ArgumentError, "no block" unless iterator?
- print caption
- results = yield(Report.new(label_width, fmtstr))
- Array === results and results.grep(Tms).each {|t|
- print((labels.shift || t.label || "").ljust(label_width),
- t.format(fmtstr))
- }
- STDOUT.sync = sync
- end
-
-
- # A simple interface to the #benchmark method, #bm is generates sequential reports
- # with labels. The parameters have the same meaning as for #benchmark.
- #
- # require 'benchmark'
- #
- # n = 50000
- # Benchmark.bm(7) do |x|
- # x.report("for:") { for i in 1..n; a = "1"; end }
- # x.report("times:") { n.times do ; a = "1"; end }
- # x.report("upto:") { 1.upto(n) do ; a = "1"; end }
- # end
- #
- # <i>Generates:</i>
- #
- # user system total real
- # for: 1.050000 0.000000 1.050000 ( 0.503462)
- # times: 1.533333 0.016667 1.550000 ( 0.735473)
- # upto: 1.500000 0.016667 1.516667 ( 0.711239)
- #
-
- def bm(label_width = 0, *labels, &blk) # :yield: report
- benchmark(" "*label_width + CAPTION, label_width, FMTSTR, *labels, &blk)
- end
-
-
- # Sometimes benchmark results are skewed because code executed
- # earlier encounters different garbage collection overheads than
- # that run later. #bmbm attempts to minimize this effect by running
- # the tests twice, the first time as a rehearsal in order to get the
- # runtime environment stable, the second time for
- # real. <tt>GC.start</tt> is executed before the start of each of
- # the real timings; the cost of this is not included in the
- # timings. In reality, though, there's only so much that #bmbm can
- # do, and the results are not guaranteed to be isolated from garbage
- # collection and other effects.
- #
- # Because #bmbm takes two passes through the tests, it can
- # calculate the required label width.
- #
- # require 'benchmark'
- #
- # array = (1..1000000).map { rand }
- #
- # Benchmark.bmbm do |x|
- # x.report("sort!") { array.dup.sort! }
- # x.report("sort") { array.dup.sort }
- # end
- #
- # <i>Generates:</i>
- #
- # Rehearsal -----------------------------------------
- # sort! 11.928000 0.010000 11.938000 ( 12.756000)
- # sort 13.048000 0.020000 13.068000 ( 13.857000)
- # ------------------------------- total: 25.006000sec
- #
- # user system total real
- # sort! 12.959000 0.010000 12.969000 ( 13.793000)
- # sort 12.007000 0.000000 12.007000 ( 12.791000)
- #
- # #bmbm yields a Benchmark::Job object and returns an array of
- # Benchmark::Tms objects.
- #
- def bmbm(width = 0, &blk) # :yield: job
- job = Job.new(width)
- yield(job)
- width = job.width
- sync = STDOUT.sync
- STDOUT.sync = true
-
- # rehearsal
- print "Rehearsal "
- puts '-'*(width+CAPTION.length - "Rehearsal ".length)
- list = []
- job.list.each{|label,item|
- print(label.ljust(width))
- res = Benchmark::measure(&item)
- print res.format()
- list.push res
- }
- sum = Tms.new; list.each{|i| sum += i}
- ets = sum.format("total: %tsec")
- printf("%s %s\n\n",
- "-"*(width+CAPTION.length-ets.length-1), ets)
-
- # take
- print ' '*width, CAPTION
- list = []
- ary = []
- job.list.each{|label,item|
- GC::start
- print label.ljust(width)
- res = Benchmark::measure(&item)
- print res.format()
- ary.push res
- list.push [label, res]
- }
-
- STDOUT.sync = sync
- ary
- end
-
- #
- # Returns the time used to execute the given block as a
- # Benchmark::Tms object.
- #
- def measure(label = "") # :yield:
- t0, r0 = Benchmark.times, Time.now
- yield
- t1, r1 = Benchmark.times, Time.now
- Benchmark::Tms.new(t1.utime - t0.utime,
- t1.stime - t0.stime,
- t1.cutime - t0.cutime,
- t1.cstime - t0.cstime,
- r1.to_f - r0.to_f,
- label)
- end
-
- #
- # Returns the elapsed real time used to execute the given block.
- #
- def realtime(&blk) # :yield:
- r0 = Time.now
- yield
- r1 = Time.now
- r1.to_f - r0.to_f
- end
-
-
-
- #
- # A Job is a sequence of labelled blocks to be processed by the
- # Benchmark.bmbm method. It is of little direct interest to the user.
- #
- class Job # :nodoc:
- #
- # Returns an initialized Job instance.
- # Usually, one doesn't call this method directly, as new
- # Job objects are created by the #bmbm method.
- # _width_ is a initial value for the label offset used in formatting;
- # the #bmbm method passes its _width_ argument to this constructor.
- #
- def initialize(width)
- @width = width
- @list = []
- end
-
- #
- # Registers the given label and block pair in the job list.
- #
- def item(label = "", &blk) # :yield:
- raise ArgumentError, "no block" unless block_given?
- label += ' '
- w = label.length
- @width = w if @width < w
- @list.push [label, blk]
- self
- end
-
- alias report item
-
- # An array of 2-element arrays, consisting of label and block pairs.
- attr_reader :list
-
- # Length of the widest label in the #list, plus one.
- attr_reader :width
- end
-
- module_function :benchmark, :measure, :realtime, :bm, :bmbm
-
-
-
- #
- # This class is used by the Benchmark.benchmark and Benchmark.bm methods.
- # It is of little direct interest to the user.
- #
- class Report # :nodoc:
- #
- # Returns an initialized Report instance.
- # Usually, one doesn't call this method directly, as new
- # Report objects are created by the #benchmark and #bm methods.
- # _width_ and _fmtstr_ are the label offset and
- # format string used by Tms#format.
- #
- def initialize(width = 0, fmtstr = nil)
- @width, @fmtstr = width, fmtstr
- end
-
- #
- # Prints the _label_ and measured time for the block,
- # formatted by _fmt_. See Tms#format for the
- # formatting rules.
- #
- def item(label = "", *fmt, &blk) # :yield:
- print label.ljust(@width)
- res = Benchmark::measure(&blk)
- print res.format(@fmtstr, *fmt)
- res
- end
-
- alias report item
- end
-
-
-
- #
- # A data object, representing the times associated with a benchmark
- # measurement.
- #
- class Tms
- CAPTION = " user system total real\n"
- FMTSTR = "%10.6u %10.6y %10.6t %10.6r\n"
-
- # User CPU time
- attr_reader :utime
-
- # System CPU time
- attr_reader :stime
-
- # User CPU time of children
- attr_reader :cutime
-
- # System CPU time of children
- attr_reader :cstime
-
- # Elapsed real time
- attr_reader :real
-
- # Total time, that is _utime_ + _stime_ + _cutime_ + _cstime_
- attr_reader :total
-
- # Label
- attr_reader :label
-
- #
- # Returns an initialized Tms object which has
- # _u_ as the user CPU time, _s_ as the system CPU time,
- # _cu_ as the children's user CPU time, _cs_ as the children's
- # system CPU time, _real_ as the elapsed real time and _l_
- # as the label.
- #
- def initialize(u = 0.0, s = 0.0, cu = 0.0, cs = 0.0, real = 0.0, l = nil)
- @utime, @stime, @cutime, @cstime, @real, @label = u, s, cu, cs, real, l
- @total = @utime + @stime + @cutime + @cstime
- end
-
- #
- # Returns a new Tms object whose times are the sum of the times for this
- # Tms object, plus the time required to execute the code block (_blk_).
- #
- def add(&blk) # :yield:
- self + Benchmark::measure(&blk)
- end
-
- #
- # An in-place version of #add.
- #
- def add!
- t = Benchmark::measure(&blk)
- @utime = utime + t.utime
- @stime = stime + t.stime
- @cutime = cutime + t.cutime
- @cstime = cstime + t.cstime
- @real = real + t.real
- self
- end
-
- #
- # Returns a new Tms object obtained by memberwise summation
- # of the individual times for this Tms object with those of the other
- # Tms object.
- # This method and #/() are useful for taking statistics.
- #
- def +(other); memberwise(:+, other) end
-
- #
- # Returns a new Tms object obtained by memberwise subtraction
- # of the individual times for the other Tms object from those of this
- # Tms object.
- #
- def -(other); memberwise(:-, other) end
-
- #
- # Returns a new Tms object obtained by memberwise multiplication
- # of the individual times for this Tms object by _x_.
- #
- def *(x); memberwise(:*, x) end
-
- #
- # Returns a new Tms object obtained by memberwise division
- # of the individual times for this Tms object by _x_.
- # This method and #+() are useful for taking statistics.
- #
- def /(x); memberwise(:/, x) end
-
- #
- # Returns the contents of this Tms object as
- # a formatted string, according to a format string
- # like that passed to Kernel.format. In addition, #format
- # accepts the following extensions:
- #
- # <tt>%u</tt>:: Replaced by the user CPU time, as reported by Tms#utime.
- # <tt>%y</tt>:: Replaced by the system CPU time, as reported by #stime (Mnemonic: y of "s*y*stem")
- # <tt>%U</tt>:: Replaced by the children's user CPU time, as reported by Tms#cutime
- # <tt>%Y</tt>:: Replaced by the children's system CPU time, as reported by Tms#cstime
- # <tt>%t</tt>:: Replaced by the total CPU time, as reported by Tms#total
- # <tt>%r</tt>:: Replaced by the elapsed real time, as reported by Tms#real
- # <tt>%n</tt>:: Replaced by the label string, as reported by Tms#label (Mnemonic: n of "*n*ame")
- #
- # If _fmtstr_ is not given, FMTSTR is used as default value, detailing the
- # user, system and real elapsed time.
- #
- def format(arg0 = nil, *args)
- fmtstr = (arg0 || FMTSTR).dup
- fmtstr.gsub!(/(%[-+\.\d]*)n/){"#{$1}s" % label}
- fmtstr.gsub!(/(%[-+\.\d]*)u/){"#{$1}f" % utime}
- fmtstr.gsub!(/(%[-+\.\d]*)y/){"#{$1}f" % stime}
- fmtstr.gsub!(/(%[-+\.\d]*)U/){"#{$1}f" % cutime}
- fmtstr.gsub!(/(%[-+\.\d]*)Y/){"#{$1}f" % cstime}
- fmtstr.gsub!(/(%[-+\.\d]*)t/){"#{$1}f" % total}
- fmtstr.gsub!(/(%[-+\.\d]*)r/){"(#{$1}f)" % real}
- arg0 ? Kernel::format(fmtstr, *args) : fmtstr
- end
-
- #
- # Same as #format.
- #
- def to_s
- format
- end
-
- #
- # Returns a new 6-element array, consisting of the
- # label, user CPU time, system CPU time, children's
- # user CPU time, children's system CPU time and elapsed
- # real time.
- #
- def to_a
- [@label, @utime, @stime, @cutime, @cstime, @real]
- end
-
- protected
- def memberwise(op, x)
- case x
- when Benchmark::Tms
- Benchmark::Tms.new(utime.__send__(op, x.utime),
- stime.__send__(op, x.stime),
- cutime.__send__(op, x.cutime),
- cstime.__send__(op, x.cstime),
- real.__send__(op, x.real)
- )
- else
- Benchmark::Tms.new(utime.__send__(op, x),
- stime.__send__(op, x),
- cutime.__send__(op, x),
- cstime.__send__(op, x),
- real.__send__(op, x)
- )
- end
- end
- end
-
- # The default caption string (heading above the output times).
- CAPTION = Benchmark::Tms::CAPTION
-
- # The default format string used to display times. See also Benchmark::Tms#format.
- FMTSTR = Benchmark::Tms::FMTSTR
-end
-
-if __FILE__ == $0
- include Benchmark
-
- n = ARGV[0].to_i.nonzero? || 50000
- puts %Q([#{n} times iterations of `a = "1"'])
- benchmark(" " + CAPTION, 7, FMTSTR) do |x|
- x.report("for:") {for i in 1..n; a = "1"; end} # Benchmark::measure
- x.report("times:") {n.times do ; a = "1"; end}
- x.report("upto:") {1.upto(n) do ; a = "1"; end}
- end
-
- benchmark do
- [
- measure{for i in 1..n; a = "1"; end}, # Benchmark::measure
- measure{n.times do ; a = "1"; end},
- measure{1.upto(n) do ; a = "1"; end}
- ]
- end
-end
diff --git a/lib/bundled_gems.rb b/lib/bundled_gems.rb
new file mode 100644
index 0000000000..a287c49a34
--- /dev/null
+++ b/lib/bundled_gems.rb
@@ -0,0 +1,275 @@
+# -*- frozen-string-literal: true -*-
+
+module Gem # :nodoc:
+ # TODO: the nodoc above is a workaround for RDoc's Prism parser handling stopdoc differently, we may want to use
+ # stopdoc/startdoc pair like before
+end
+
+module Gem::BUNDLED_GEMS # :nodoc:
+ SINCE = {
+ "racc" => "3.3.0",
+ "abbrev" => "3.4.0",
+ "base64" => "3.4.0",
+ "bigdecimal" => "3.4.0",
+ "csv" => "3.4.0",
+ "drb" => "3.4.0",
+ "getoptlong" => "3.4.0",
+ "mutex_m" => "3.4.0",
+ "nkf" => "3.4.0",
+ "observer" => "3.4.0",
+ "resolv-replace" => "3.4.0",
+ "rinda" => "3.4.0",
+ "syslog" => "3.4.0",
+ "ostruct" => "4.0.0",
+ "pstore" => "4.0.0",
+ "rdoc" => "4.0.0",
+ "win32ole" => "4.0.0",
+ "fiddle" => "4.0.0",
+ "logger" => "4.0.0",
+ "benchmark" => "4.0.0",
+ "irb" => "4.0.0",
+ "reline" => "4.0.0",
+ # "readline" => "4.0.0", # This is wrapper for reline. We don't warn for this.
+ "tsort" => "4.1.0",
+ }.freeze
+
+ EXACT = {
+ "kconv" => "nkf",
+ }.freeze
+
+ WARNED = {} # unfrozen
+
+ conf = ::RbConfig::CONFIG
+ LIBDIR = (conf["rubylibdir"] + "/").freeze
+ ARCHDIR = (conf["rubyarchdir"] + "/").freeze
+ dlext = [conf["DLEXT"], "so"].uniq
+ DLEXT = /\.#{Regexp.union(dlext)}\z/
+ LIBEXT = /\.#{Regexp.union("rb", *dlext)}\z/
+
+ def self.replace_require(specs)
+ return if [::Kernel.singleton_class, ::Kernel].any? {|klass| klass.respond_to?(:no_warning_require) }
+
+ spec_names = specs.to_a.each_with_object({}) {|spec, h| h[spec.name] = true }
+
+ [::Kernel.singleton_class, ::Kernel].each do |kernel_class|
+ kernel_class.send(:alias_method, :no_warning_require, :require)
+ kernel_class.send(:define_method, :require) do |name|
+ if message = ::Gem::BUNDLED_GEMS.warning?(name, specs: spec_names)
+ Kernel.warn message, uplevel: ::Gem::BUNDLED_GEMS.uplevel
+ end
+ kernel_class.send(:no_warning_require, name)
+ end
+ if kernel_class == ::Kernel
+ kernel_class.send(:private, :require)
+ else
+ kernel_class.send(:public, :require)
+ end
+ end
+ end
+
+ def self.uplevel
+ frame_count = 0
+ require_labels = ["replace_require", "require"]
+ uplevel = 0
+ require_found = false
+ Thread.each_caller_location do |cl|
+ frame_count += 1
+
+ if require_found
+ unless require_labels.include?(cl.base_label)
+ return uplevel
+ end
+ else
+ if require_labels.include?(cl.base_label)
+ require_found = true
+ end
+ end
+ uplevel += 1
+ # Don't show script name when bundle exec and call ruby script directly.
+ if cl.path.end_with?("bundle")
+ return
+ end
+ end
+ require_found ? 1 : (frame_count - 1).nonzero?
+ end
+
+ def self.warning?(name, specs: nil)
+ # name can be a feature name or a file path with String or Pathname
+ feature = File.path(name).sub(LIBEXT, "")
+
+ # The actual checks needed to properly identify the gem being required
+ # are costly (see [Bug #20641]), so we first do a much cheaper check
+ # to exclude the vast majority of candidates.
+ subfeature = if feature.include?("/")
+ # bootsnap expands `require "csv"` to `require "#{LIBDIR}/csv.rb"`,
+ # and `require "syslog"` to `require "#{ARCHDIR}/syslog.so"`.
+ feature.delete_prefix!(ARCHDIR)
+ feature.delete_prefix!(LIBDIR)
+ # 1. A segment for the EXACT mapping and SINCE check
+ # 2. A segment for the SINCE check for dashed names
+ # 3. A segment to check if there's a subfeature
+ segments = feature.split("/", 3)
+ name = segments.shift
+ name = EXACT[name] || name
+ if !SINCE[name]
+ name = "#{name}-#{segments.shift}"
+ return unless SINCE[name]
+ end
+ segments.any?
+ else
+ name = EXACT[feature] || feature
+ return unless SINCE[name]
+ false
+ end
+
+ if suppress_list = Thread.current[:__bundled_gems_warning_suppression]
+ return if suppress_list.include?(name) || suppress_list.include?(feature)
+ end
+
+ return if specs.include?(name)
+
+ # Don't warn if a hyphenated gem provides this feature
+ # (e.g., benchmark-ips provides benchmark/ips, benchmark/timing, etc.)
+ if subfeature
+ prefix = feature.split("/").first + "-"
+ return if specs.any? { |spec, _| spec.start_with?(prefix) }
+
+ # Don't warn if the feature is found outside the standard library
+ # (e.g., benchmark-ips's lib dir is on $LOAD_PATH but not in specs)
+ resolved = $LOAD_PATH.resolve_feature_path(feature) rescue nil
+ if resolved && !resolved[1].start_with?(LIBDIR, ARCHDIR)
+ return
+ end
+ end
+
+ return if WARNED[name]
+ WARNED[name] = true
+
+ level = RUBY_VERSION < SINCE[name] ? :warning : :error
+
+ if subfeature
+ "#{feature} is found in #{name}, which"
+ else
+ "#{feature} #{level == :warning ? "was loaded" : "used to be loaded"} from the standard library, but"
+ end + build_message(name, level)
+ end
+
+ def self.build_message(name, level)
+ msg = if level == :warning
+ " will no longer be part of the default gems starting from Ruby #{SINCE[name]}"
+ else
+ " is not part of the default gems since Ruby #{SINCE[name]}."
+ end
+
+ if defined?(Bundler)
+ motivation = level == :warning ? "silence this warning" : "fix this error"
+ msg += "\nYou can add #{name} to your Gemfile or gemspec to #{motivation}."
+
+ # We detect the gem name from caller_locations. First we walk until we find `require`
+ # then take the first frame that's not from `require`.
+ #
+ # Additionally, we need to skip Bootsnap and Zeitwerk if present, these
+ # gems decorate Kernel#require, so they are not really the ones issuing
+ # the require call users should be warned about. Those are upwards.
+ frames_to_skip = 3
+ location = nil
+ require_found = false
+ Thread.each_caller_location do |cl|
+ if frames_to_skip >= 1
+ frames_to_skip -= 1
+ next
+ end
+
+ if require_found
+ if cl.base_label != "require"
+ location = cl.path
+ break
+ end
+ else
+ if cl.base_label == "require"
+ require_found = true
+ end
+ end
+ end
+
+ if location && File.file?(location) && !location.start_with?(Gem::BUNDLED_GEMS::LIBDIR)
+ caller_gem = nil
+ Gem.path.each do |path|
+ if location =~ %r{#{path}/gems/([\w\-\.]+)}
+ caller_gem = $1
+ break
+ end
+ end
+ if caller_gem
+ msg += "\nAlso please contact the author of #{caller_gem} to request adding #{name} into its gemspec."
+ end
+ end
+ else
+ msg += " Install #{name} from RubyGems."
+ end
+
+ msg
+ end
+
+ def self.force_activate(gem)
+ require "bundler"
+ Bundler.reset!
+
+ # Build and activate a temporary definition containing the original gems + the requested gem
+ builder = Bundler::Dsl.new
+
+ lockfile = nil
+ if Bundler::SharedHelpers.in_bundle? && Bundler.definition.gemfiles.size > 0
+ Bundler.definition.gemfiles.each {|gemfile| builder.eval_gemfile(gemfile) }
+ lockfile = begin
+ Bundler.default_lockfile
+ rescue Bundler::GemfileNotFound
+ nil
+ end
+ else
+ # Fake BUNDLE_GEMFILE and BUNDLE_LOCKFILE to let checks pass
+ orig_gemfile = ENV["BUNDLE_GEMFILE"]
+ orig_lockfile = ENV["BUNDLE_LOCKFILE"]
+ Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", "Gemfile"
+ Bundler::SharedHelpers.set_env "BUNDLE_LOCKFILE", "Gemfile.lock"
+ end
+
+ builder.gem gem
+
+ definition = builder.to_definition(lockfile, nil)
+ definition.validate_runtime!
+
+ begin
+ orig_ui = Bundler.ui
+ orig_no_lock = Bundler::Definition.no_lock
+
+ ui = Bundler::UI::Shell.new
+ ui.level = "silent"
+ Bundler.ui = ui
+ Bundler::Definition.no_lock = true
+
+ Bundler::Runtime.new(nil, definition).setup
+ rescue Bundler::GemNotFound
+ warn "Failed to activate #{gem}, please install it with 'gem install #{gem}'"
+ ensure
+ ENV['BUNDLE_GEMFILE'] = orig_gemfile if orig_gemfile
+ ENV['BUNDLE_LOCKFILE'] = orig_lockfile if orig_lockfile
+ Bundler.ui = orig_ui
+ Bundler::Definition.no_lock = orig_no_lock
+ end
+ end
+end
+
+# for RubyGems without Bundler environment.
+# If loading library is not part of the default gems and the bundled gems, warn it.
+class LoadError
+ def message # :nodoc:
+ return super unless path
+
+ name = path.tr("/", "-")
+ if !defined?(Bundler) && Gem::BUNDLED_GEMS::SINCE[name] && !Gem::BUNDLED_GEMS::WARNED[name]
+ warn name + Gem::BUNDLED_GEMS.build_message(name, :error), uplevel: Gem::BUNDLED_GEMS.uplevel
+ end
+ super
+ end
+end
diff --git a/lib/bundler.rb b/lib/bundler.rb
new file mode 100644
index 0000000000..12dde90fc5
--- /dev/null
+++ b/lib/bundler.rb
@@ -0,0 +1,692 @@
+# frozen_string_literal: true
+
+require_relative "bundler/rubygems_ext"
+require_relative "bundler/vendored_fileutils"
+autoload :Pathname, "pathname" unless defined?(Pathname)
+require "rbconfig"
+
+require_relative "bundler/errors"
+require_relative "bundler/environment_preserver"
+require_relative "bundler/plugin"
+require_relative "bundler/rubygems_integration"
+require_relative "bundler/version"
+require_relative "bundler/current_ruby"
+require_relative "bundler/build_metadata"
+
+# Bundler provides a consistent environment for Ruby projects by
+# tracking and installing the exact gems and versions that are needed.
+#
+# Bundler is a part of Ruby's standard library.
+#
+# Bundler is used by creating _gemfiles_ listing all the project dependencies
+# and (optionally) their versions and then using
+#
+# require 'bundler/setup'
+#
+# or Bundler.setup to setup environment where only specified gems and their
+# specified versions could be used.
+#
+# See {Bundler website}[https://bundler.io/docs.html] for extensive documentation
+# on gemfiles creation and Bundler usage.
+#
+# As a standard library inside project, Bundler could be used for introspection
+# of loaded and required modules.
+#
+module Bundler
+ environment_preserver = EnvironmentPreserver.from_env
+ ORIGINAL_ENV = environment_preserver.restore
+ environment_preserver.replace_with_backup
+ SUDO_MUTEX = Thread::Mutex.new
+
+ autoload :Checksum, File.expand_path("bundler/checksum", __dir__)
+ autoload :CLI, File.expand_path("bundler/cli", __dir__)
+ autoload :CIDetector, File.expand_path("bundler/ci_detector", __dir__)
+ autoload :CompactIndexClient, File.expand_path("bundler/compact_index_client", __dir__)
+ autoload :Definition, File.expand_path("bundler/definition", __dir__)
+ autoload :Dependency, File.expand_path("bundler/dependency", __dir__)
+ autoload :Deprecate, File.expand_path("bundler/deprecate", __dir__)
+ autoload :Digest, File.expand_path("bundler/digest", __dir__)
+ autoload :Dsl, File.expand_path("bundler/dsl", __dir__)
+ autoload :EndpointSpecification, File.expand_path("bundler/endpoint_specification", __dir__)
+ autoload :Env, File.expand_path("bundler/env", __dir__)
+ autoload :Fetcher, File.expand_path("bundler/fetcher", __dir__)
+ autoload :FeatureFlag, File.expand_path("bundler/feature_flag", __dir__)
+ autoload :FREEBSD, File.expand_path("bundler/constants", __dir__)
+ autoload :GemHelper, File.expand_path("bundler/gem_helper", __dir__)
+ autoload :GemVersionPromoter, File.expand_path("bundler/gem_version_promoter", __dir__)
+ autoload :Index, File.expand_path("bundler/index", __dir__)
+ autoload :Injector, File.expand_path("bundler/injector", __dir__)
+ autoload :Installer, File.expand_path("bundler/installer", __dir__)
+ autoload :LazySpecification, File.expand_path("bundler/lazy_specification", __dir__)
+ autoload :LockfileParser, File.expand_path("bundler/lockfile_parser", __dir__)
+ autoload :MatchRemoteMetadata, File.expand_path("bundler/match_remote_metadata", __dir__)
+ autoload :Materialization, File.expand_path("bundler/materialization", __dir__)
+ autoload :NULL, File.expand_path("bundler/constants", __dir__)
+ autoload :Override, File.expand_path("bundler/override", __dir__)
+ autoload :ProcessLock, File.expand_path("bundler/process_lock", __dir__)
+ autoload :RemoteSpecification, File.expand_path("bundler/remote_specification", __dir__)
+ autoload :Resolver, File.expand_path("bundler/resolver", __dir__)
+ autoload :Retry, File.expand_path("bundler/retry", __dir__)
+ autoload :RubyDsl, File.expand_path("bundler/ruby_dsl", __dir__)
+ autoload :RubyVersion, File.expand_path("bundler/ruby_version", __dir__)
+ autoload :Runtime, File.expand_path("bundler/runtime", __dir__)
+ autoload :SelfManager, File.expand_path("bundler/self_manager", __dir__)
+ autoload :Settings, File.expand_path("bundler/settings", __dir__)
+ autoload :SharedHelpers, File.expand_path("bundler/shared_helpers", __dir__)
+ autoload :Source, File.expand_path("bundler/source", __dir__)
+ autoload :SourceList, File.expand_path("bundler/source_list", __dir__)
+ autoload :SourceMap, File.expand_path("bundler/source_map", __dir__)
+ autoload :SpecSet, File.expand_path("bundler/spec_set", __dir__)
+ autoload :StubSpecification, File.expand_path("bundler/stub_specification", __dir__)
+ autoload :UI, File.expand_path("bundler/ui", __dir__)
+ autoload :URICredentialsFilter, File.expand_path("bundler/uri_credentials_filter", __dir__)
+ autoload :URINormalizer, File.expand_path("bundler/uri_normalizer", __dir__)
+ autoload :WINDOWS, File.expand_path("bundler/constants", __dir__)
+ autoload :SafeMarshal, File.expand_path("bundler/safe_marshal", __dir__)
+
+ class << self
+ def configure
+ @configure ||= configure_gem_home_and_path
+ end
+
+ def ui
+ (defined?(@ui) && @ui) || (self.ui = UI::Shell.new)
+ end
+
+ def ui=(ui)
+ Bundler.rubygems.ui = UI::RGProxy.new(ui)
+ @ui = ui
+ end
+
+ # Returns absolute path of where gems are installed on the filesystem.
+ def bundle_path
+ @bundle_path ||= Pathname.new(configured_bundle_path.path).expand_path(root)
+ end
+
+ def create_bundle_path
+ mkdir_p(bundle_path) unless bundle_path.exist?
+
+ @bundle_path = bundle_path.realpath
+ rescue Errno::EEXIST
+ raise PathError, "Could not install to path `#{bundle_path}` " \
+ "because a file already exists at that path. Either remove or rename the file so the directory can be created."
+ end
+
+ def configured_bundle_path
+ @configured_bundle_path ||= Bundler.settings.path.tap(&:validate!)
+ end
+
+ # Returns absolute location of where binstubs are installed to.
+ def bin_path
+ @bin_path ||= begin
+ path = Bundler.settings[:bin] || "bin"
+ path = Pathname.new(path).expand_path(root).expand_path
+ mkdir_p(path)
+ path
+ end
+ end
+
+ # Turns on the Bundler runtime. After +Bundler.setup+ call, all +load+ or
+ # +require+ of the gems would be allowed only if they are part of
+ # the Gemfile or Ruby's standard library. If the versions specified
+ # in Gemfile, only those versions would be loaded.
+ #
+ # Assuming Gemfile
+ #
+ # gem 'first_gem', '= 1.0'
+ # group :test do
+ # gem 'second_gem', '= 1.0'
+ # end
+ #
+ # The code using Bundler.setup works as follows:
+ #
+ # require 'third_gem' # allowed, required from global gems
+ # require 'first_gem' # allowed, loads the last installed version
+ # Bundler.setup
+ # require 'fourth_gem' # fails with LoadError
+ # require 'second_gem' # loads exactly version 1.0
+ #
+ # +Bundler.setup+ can be called only once, all subsequent calls are no-op.
+ #
+ # If _groups_ list is provided, only gems from specified groups would
+ # be allowed (gems specified outside groups belong to special +:default+ group).
+ #
+ # To require all gems from Gemfile (or only some groups), see Bundler.require.
+ #
+ def setup(*groups)
+ # Return if all groups are already loaded
+ return @setup if defined?(@setup) && @setup
+
+ configure_custom_gemfile
+ definition.validate_runtime!
+
+ SharedHelpers.print_major_deprecations!
+
+ if groups.empty?
+ # Load all groups, but only once
+ @setup = load.setup
+ else
+ load.setup(*groups)
+ end
+ end
+
+ def auto_switch
+ self_manager.restart_with_locked_bundler_if_needed
+ end
+
+ # Automatically install dependencies if <tt>settings[:auto_install]</tt> exists.
+ # This is set through config cmd `bundle config set --global auto_install 1`.
+ #
+ # Note that this method `nil`s out the global Definition object, so it
+ # should be called first, before you instantiate anything like an
+ # `Installer` that'll keep a reference to the old one instead.
+ def auto_install
+ return unless Bundler.settings[:auto_install]
+
+ begin
+ definition.specs
+ rescue GemNotFound, GitError
+ ui.info "Automatically installing missing gems."
+ reset!
+ CLI::Install.new({}).run
+ reset!
+ end
+ end
+
+ # Setups Bundler environment (see Bundler.setup) if it is not already set,
+ # and loads all gems from groups specified. Unlike ::setup, can be called
+ # multiple times with different groups (if they were allowed by setup).
+ #
+ # Assuming Gemfile
+ #
+ # gem 'first_gem', '= 1.0'
+ # group :test do
+ # gem 'second_gem', '= 1.0'
+ # end
+ #
+ # The code will work as follows:
+ #
+ # Bundler.setup # allow all groups
+ # Bundler.require(:default) # requires only first_gem
+ # # ...later
+ # Bundler.require(:test) # requires second_gem
+ #
+ def require(*groups)
+ setup(*groups).require(*groups)
+ end
+
+ def load
+ @load ||= Runtime.new(root, definition)
+ end
+
+ def environment
+ SharedHelpers.feature_removed! "Bundler.environment has been removed in favor of Bundler.load"
+ end
+
+ # Returns an instance of Bundler::Definition for given Gemfile and lockfile
+ #
+ # @param unlock [Hash, Boolean, nil] Gems that have been requested
+ # to be updated or true if all gems should be updated
+ # @param lockfile [Pathname] Path to Gemfile.lock
+ # @return [Bundler::Definition]
+ def definition(unlock = nil, lockfile = default_lockfile)
+ @definition = nil if unlock
+ @definition ||= begin
+ configure
+ Definition.build(default_gemfile, lockfile, unlock)
+ end
+ end
+
+ def frozen_bundle?
+ frozen = Bundler.settings[:frozen]
+ return frozen unless frozen.nil?
+
+ Bundler.settings[:deployment]
+ end
+
+ def locked_gems
+ @locked_gems ||=
+ if defined?(@definition) && @definition
+ definition.locked_gems
+ elsif Bundler.default_lockfile.file?
+ lock = Bundler.read_file(Bundler.default_lockfile)
+ LockfileParser.new(lock)
+ end
+ end
+
+ def ruby_scope
+ "#{Bundler.rubygems.ruby_engine}/#{RbConfig::CONFIG["ruby_version"]}"
+ end
+
+ def user_home
+ @user_home ||= begin
+ home = Bundler.rubygems.user_home
+ bundle_home = home ? File.join(home, ".bundle") : nil
+
+ warning = if home.nil?
+ "Your home directory is not set."
+ elsif !File.directory?(home)
+ "`#{home}` is not a directory."
+ elsif !File.writable?(home) && (!File.directory?(bundle_home) || !File.writable?(bundle_home))
+ "`#{home}` is not writable."
+ end
+
+ if warning
+ Bundler.ui.warn "#{warning}\n"
+ user_home = tmp_home_path
+ Bundler.ui.warn "Bundler will use `#{user_home}' as your home directory temporarily.\n"
+ user_home
+ else
+ Pathname.new(home)
+ end
+ end
+ end
+
+ def user_bundle_path(dir = "home")
+ env_var, fallback = case dir
+ when "home"
+ ["BUNDLE_USER_HOME", proc { Pathname.new(user_home).join(".bundle") }]
+ when "cache"
+ ["BUNDLE_USER_CACHE", proc { user_bundle_path.join("cache") }]
+ when "config"
+ ["BUNDLE_USER_CONFIG", proc { user_bundle_path.join("config") }]
+ when "plugin"
+ ["BUNDLE_USER_PLUGIN", proc { user_bundle_path.join("plugin") }]
+ else
+ raise BundlerError, "Unknown user path requested: #{dir}"
+ end
+ # `fallback` will already be a Pathname, but Pathname.new() is
+ # idempotent so it's OK
+ Pathname.new(ENV.fetch(env_var, &fallback))
+ end
+
+ def user_cache
+ user_bundle_path("cache")
+ end
+
+ def home
+ bundle_path.join("bundler")
+ end
+
+ def install_path
+ home.join("gems")
+ end
+
+ def specs_path
+ bundle_path.join("specifications")
+ end
+
+ def root
+ @root ||= begin
+ SharedHelpers.root
+ rescue GemfileNotFound
+ bundle_dir = default_bundle_dir
+ raise GemfileNotFound, "Could not locate Gemfile or .bundle/ directory" unless bundle_dir
+ Pathname.new(File.expand_path("..", bundle_dir))
+ end
+ end
+
+ def app_config_path
+ if app_config = ENV["BUNDLE_APP_CONFIG"]
+ app_config_pathname = Pathname.new(app_config)
+
+ if app_config_pathname.absolute?
+ app_config_pathname
+ else
+ app_config_pathname.expand_path(root)
+ end
+ else
+ root.join(".bundle")
+ end
+ end
+
+ def app_cache(custom_path = nil)
+ path = custom_path || root
+ Pathname.new(path).join(Bundler.settings.app_cache_path)
+ end
+
+ def tmp(name = Process.pid.to_s)
+ Kernel.send(:require, "tmpdir")
+ Pathname.new(Dir.mktmpdir(["bundler", name]))
+ end
+
+ def rm_rf(path)
+ FileUtils.remove_entry_secure(path) if path && File.exist?(path)
+ end
+
+ def settings
+ @settings ||= Settings.new(app_config_path)
+ rescue GemfileNotFound
+ @settings = Settings.new
+ end
+
+ # @return [Hash] Environment present before Bundler was activated
+ def original_env
+ ORIGINAL_ENV.clone
+ end
+
+ def clean_env
+ removed_message =
+ "`Bundler.clean_env` has been removed in favor of `Bundler.unbundled_env`. " \
+ "If you instead want the environment before bundler was originally loaded, use `Bundler.original_env`"
+ Bundler::SharedHelpers.feature_removed!(removed_message)
+ end
+
+ # @return [Hash] Environment with all bundler-related variables removed
+ def unbundled_env
+ unbundle_env(original_env)
+ end
+
+ # Remove all bundler-related variables from ENV
+ def unbundle_env!
+ ENV.replace(unbundle_env(ENV))
+ end
+
+ # Run block with environment present before Bundler was activated
+ def with_original_env
+ with_env(original_env) { yield }
+ end
+
+ def with_clean_env
+ removed_message =
+ "`Bundler.with_clean_env` has been removed in favor of `Bundler.with_unbundled_env`. " \
+ "If you instead want the environment before bundler was originally loaded, use `Bundler.with_original_env`"
+ Bundler::SharedHelpers.feature_removed!(removed_message)
+ end
+
+ # Run block with all bundler-related variables removed
+ def with_unbundled_env
+ with_env(unbundled_env) { yield }
+ end
+
+ # Run subcommand with the environment present before Bundler was activated
+ def original_system(*args)
+ with_original_env { Kernel.system(*args) }
+ end
+
+ def clean_system(*args)
+ removed_message =
+ "`Bundler.clean_system` has been removed in favor of `Bundler.unbundled_system`. " \
+ "If you instead want to run the command in the environment before bundler was originally loaded, use `Bundler.original_system`"
+ Bundler::SharedHelpers.feature_removed!(removed_message)
+ end
+
+ # Run subcommand in an environment with all bundler related variables removed
+ def unbundled_system(*args)
+ with_unbundled_env { Kernel.system(*args) }
+ end
+
+ # Run a `Kernel.exec` to a subcommand with the environment present before Bundler was activated
+ def original_exec(*args)
+ with_original_env { Kernel.exec(*args) }
+ end
+
+ def clean_exec(*args)
+ removed_message =
+ "`Bundler.clean_exec` has been removed in favor of `Bundler.unbundled_exec`. " \
+ "If you instead want to exec to a command in the environment before bundler was originally loaded, use `Bundler.original_exec`"
+ Bundler::SharedHelpers.feature_removed!(removed_message)
+ end
+
+ # Run a `Kernel.exec` to a subcommand in an environment with all bundler related variables removed
+ def unbundled_exec(*args)
+ with_env(unbundled_env) { Kernel.exec(*args) }
+ end
+
+ def local_platform
+ return Gem::Platform::RUBY if Bundler.settings[:force_ruby_platform]
+ Gem::Platform.local
+ end
+
+ def generic_local_platform
+ Gem::Platform.generic(local_platform)
+ end
+
+ def default_gemfile
+ SharedHelpers.default_gemfile
+ end
+
+ def default_lockfile
+ SharedHelpers.default_lockfile
+ end
+
+ def default_bundle_dir
+ SharedHelpers.default_bundle_dir
+ end
+
+ def system_bindir
+ # Gem.bindir doesn't always return the location that RubyGems will install
+ # system binaries. If you put '-n foo' in your .gemrc, RubyGems will
+ # install binstubs there instead. Unfortunately, RubyGems doesn't expose
+ # that directory at all, so rather than parse .gemrc ourselves, we allow
+ # the directory to be set as well, via `bundle config set --local bindir foo`.
+ Bundler.settings[:system_bindir] || Bundler.rubygems.gem_bindir
+ end
+
+ def preferred_gemfile_name
+ Bundler.settings[:init_gems_rb] ? "gems.rb" : "Gemfile"
+ end
+
+ def use_system_gems?
+ configured_bundle_path.use_system_gems?
+ end
+
+ def mkdir_p(path)
+ SharedHelpers.filesystem_access(path, :create) do |p|
+ FileUtils.mkdir_p(p)
+ end
+ end
+
+ def which(executable)
+ executable_path = find_executable(executable)
+ return executable_path if executable_path
+
+ if (paths = ENV["PATH"])
+ quote = '"'
+ paths.split(File::PATH_SEPARATOR).find do |path|
+ path = path[1..-2] if path.start_with?(quote) && path.end_with?(quote)
+ executable_path = find_executable(File.expand_path(executable, path))
+ return executable_path if executable_path
+ end
+ end
+ end
+
+ def find_executable(path)
+ extensions = RbConfig::CONFIG["EXECUTABLE_EXTS"]&.split
+ extensions = [RbConfig::CONFIG["EXEEXT"]] unless extensions&.any?
+ candidates = extensions.map {|ext| "#{path}#{ext}" }
+
+ candidates.find {|candidate| File.file?(candidate) && File.executable?(candidate) }
+ end
+
+ def read_file(file)
+ SharedHelpers.filesystem_access(file, :read) do
+ File.open(file, "r:UTF-8", &:read)
+ end
+ end
+
+ def safe_load_marshal(data)
+ if Gem.respond_to?(:load_safe_marshal)
+ Gem.load_safe_marshal
+ begin
+ Gem::SafeMarshal.safe_load(data)
+ rescue Gem::SafeMarshal::Reader::Error, Gem::SafeMarshal::Visitors::ToRuby::Error => e
+ raise MarshalError, "#{e.class}: #{e.message}"
+ end
+ else
+ load_marshal(data, marshal_proc: SafeMarshal.proc)
+ end
+ end
+
+ def load_gemspec(file, validate = false)
+ @gemspec_cache ||= {}
+ key = File.expand_path(file)
+ @gemspec_cache[key] ||= load_gemspec_uncached(file, validate)
+ # Protect against caching side-effected gemspecs by returning a
+ # new instance each time.
+ @gemspec_cache[key]&.dup
+ end
+
+ def load_gemspec_uncached(file, validate = false)
+ path = Pathname.new(file)
+ contents = read_file(file)
+ spec = eval_gemspec(path, contents)
+ return unless spec
+ spec.loaded_from = path.expand_path.to_s
+ Bundler.rubygems.validate(spec) if validate
+ spec
+ end
+
+ def clear_gemspec_cache
+ @gemspec_cache = {}
+ end
+
+ def git_present?
+ return @git_present if defined?(@git_present)
+ @git_present = Bundler.which("git")
+ end
+
+ def feature_flag
+ @feature_flag ||= FeatureFlag.new(Bundler.settings[:simulate_version] || VERSION)
+ end
+
+ def reset!
+ reset_paths!
+ Plugin.reset!
+ reset_rubygems!
+ end
+
+ def reset_settings_and_root!
+ @settings = nil
+ @root = nil
+ end
+
+ def reset_paths!
+ @bin_path = nil
+ @bundle_path = nil
+ @configure = nil
+ @configured_bundle_path = nil
+ @definition = nil
+ @load = nil
+ @locked_gems = nil
+ @root = nil
+ @settings = nil
+ @setup = nil
+ @user_home = nil
+ end
+
+ def reset_rubygems!
+ return unless defined?(@rubygems) && @rubygems
+ rubygems.undo_replacements
+ rubygems.reset
+ @rubygems = nil
+ end
+
+ def configure_gem_home_and_path(path = bundle_path)
+ configure_gem_path
+ configure_gem_home(path)
+ Bundler.rubygems.clear_paths
+ end
+
+ def configure_custom_gemfile(custom_gemfile = nil)
+ custom_gemfile ||= Bundler.settings[:gemfile]
+
+ if custom_gemfile && !custom_gemfile.empty?
+ Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", File.expand_path(custom_gemfile)
+ reset_settings_and_root!
+ end
+ end
+
+ def self_manager
+ @self_manager ||= begin
+ require_relative "bundler/self_manager"
+ Bundler::SelfManager.new
+ end
+ end
+
+ private
+
+ def unbundle_env(env)
+ if env.key?("BUNDLER_ORIG_MANPATH")
+ env["MANPATH"] = env["BUNDLER_ORIG_MANPATH"]
+ end
+
+ env.delete_if {|k, _| k[0, 7] == "BUNDLE_" }
+ env.delete("BUNDLER_SETUP")
+
+ if env.key?("RUBYOPT")
+ rubyopt = env["RUBYOPT"].split(" ")
+ rubyopt.delete("-r#{File.expand_path("bundler/setup", __dir__)}")
+ rubyopt.delete("-rbundler/setup")
+ env["RUBYOPT"] = rubyopt.join(" ")
+ end
+
+ if env.key?("RUBYLIB")
+ rubylib = env["RUBYLIB"].split(File::PATH_SEPARATOR)
+ rubylib.delete(__dir__)
+ env["RUBYLIB"] = rubylib.join(File::PATH_SEPARATOR)
+ end
+
+ env
+ end
+
+ def load_marshal(data, marshal_proc: nil)
+ Marshal.load(data, marshal_proc)
+ rescue TypeError => e
+ raise MarshalError, "#{e.class}: #{e.message}"
+ end
+
+ def eval_yaml_gemspec(path, contents)
+ Kernel.require "psych"
+
+ Gem::Specification.from_yaml(contents)
+ end
+
+ def eval_gemspec(path, contents)
+ if contents.start_with?("---") # YAML header
+ eval_yaml_gemspec(path, contents)
+ else
+ # Eval the gemspec from its parent directory, because some gemspecs
+ # depend on "./" relative paths.
+ SharedHelpers.chdir(path.dirname.to_s) do
+ eval(contents, TOPLEVEL_BINDING.dup, path.expand_path.to_s)
+ end
+ end
+ rescue ScriptError, StandardError => e
+ msg = "There was an error while loading `#{path.basename}`: #{e.message}"
+
+ raise GemspecError, Dsl::DSLError.new(msg, path.to_s, e.backtrace, contents)
+ end
+
+ def configure_gem_path
+ unless use_system_gems?
+ # this needs to be empty string to cause
+ # PathSupport.split_gem_path to only load up the
+ # Bundler --path setting as the GEM_PATH.
+ Bundler::SharedHelpers.set_env "GEM_PATH", ""
+ end
+ end
+
+ def configure_gem_home(path)
+ Bundler::SharedHelpers.set_env "GEM_HOME", path.to_s
+ end
+
+ def tmp_home_path
+ Kernel.send(:require, "tmpdir")
+ SharedHelpers.filesystem_access(Dir.tmpdir) do
+ path = Bundler.tmp
+ at_exit { Bundler.rm_rf(path) }
+ path
+ end
+ end
+
+ # @param env [Hash]
+ def with_env(env)
+ backup = ENV.to_hash
+ ENV.replace(env)
+ yield
+ ensure
+ ENV.replace(backup)
+ end
+ end
+end
diff --git a/lib/bundler/.document b/lib/bundler/.document
new file mode 100644
index 0000000000..238bbd8705
--- /dev/null
+++ b/lib/bundler/.document
@@ -0,0 +1 @@
+# not in RDoc
diff --git a/lib/bundler/build_metadata.rb b/lib/bundler/build_metadata.rb
new file mode 100644
index 0000000000..49d2518078
--- /dev/null
+++ b/lib/bundler/build_metadata.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Bundler
+ # Represents metadata from when the Bundler gem was built.
+ module BuildMetadata
+ # begin ivars
+ @built_at = nil
+ # end ivars
+
+ # A hash representation of the build metadata.
+ def self.to_h
+ {
+ "Timestamp" => timestamp,
+ "Git SHA" => git_commit_sha,
+ }
+ end
+
+ # A timestamp representing the date the bundler gem was built, or the
+ # current time if never built
+ def self.timestamp
+ @timestamp ||= @built_at || Time.now.utc.strftime("%Y-%m-%d").freeze
+ end
+
+ # A string representing the date the bundler gem was built.
+ def self.built_at
+ @built_at
+ end
+
+ # The SHA for the git commit the bundler gem was built from.
+ def self.git_commit_sha
+ return @git_commit_sha if instance_variable_defined? :@git_commit_sha
+
+ # If Bundler has been installed without its .git directory and without a
+ # commit instance variable then we can't determine its commits SHA.
+ git_dir = File.expand_path("../../../.git", __dir__)
+ if File.directory?(git_dir)
+ return @git_commit_sha = IO.popen(%w[git rev-parse --short HEAD], { chdir: git_dir }, &:read).strip.freeze
+ end
+
+ @git_commit_sha ||= "unknown"
+ end
+ end
+end
diff --git a/lib/bundler/bundler.gemspec b/lib/bundler/bundler.gemspec
new file mode 100644
index 0000000000..49319e81b4
--- /dev/null
+++ b/lib/bundler/bundler.gemspec
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+begin
+ require_relative "lib/bundler/version"
+rescue LoadError
+ # for Ruby core repository
+ require_relative "version"
+end
+
+Gem::Specification.new do |s|
+ s.name = "bundler"
+ s.version = Bundler::VERSION
+ s.license = "MIT"
+ s.authors = [
+ "André Arko", "Samuel Giddins", "Colby Swandale", "Hiroshi Shibata",
+ "David Rodríguez", "Grey Baker", "Stephanie Morillo", "Chris Morris", "James Wen", "Tim Moore",
+ "André Medeiros", "Jessica Lynn Suttles", "Terence Lee", "Carl Lerche",
+ "Yehuda Katz"
+ ]
+ s.email = ["team@bundler.io"]
+ s.homepage = "https://bundler.io"
+ s.summary = "The best way to manage your application's dependencies"
+ s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably"
+
+ s.metadata = {
+ "bug_tracker_uri" => "https://github.com/ruby/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3ABundler",
+ "changelog_uri" => "https://github.com/ruby/rubygems/blob/master/bundler/CHANGELOG.md",
+ "homepage_uri" => "https://bundler.io/",
+ "source_code_uri" => "https://github.com/ruby/rubygems/tree/master/bundler",
+ }
+
+ s.required_ruby_version = ">= 3.2.0"
+
+ # It should match the RubyGems version shipped with `required_ruby_version` above
+ s.required_rubygems_version = ">= 3.4.1"
+
+ s.files = Dir.glob("lib/bundler{.rb,/**/*}", File::FNM_DOTMATCH).reject {|f| File.directory?(f) }
+
+ # include the gemspec itself because warbler breaks w/o it
+ s.files += %w[lib/bundler/bundler.gemspec]
+
+ s.bindir = "exe"
+ s.executables = %w[bundle bundler]
+ s.require_paths = ["lib"]
+end
diff --git a/lib/bundler/capistrano.rb b/lib/bundler/capistrano.rb
new file mode 100644
index 0000000000..6d2437d895
--- /dev/null
+++ b/lib/bundler/capistrano.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+require_relative "shared_helpers"
+Bundler::SharedHelpers.feature_removed! "The Bundler task for Capistrano. Please use https://github.com/capistrano/bundler"
diff --git a/lib/bundler/checksum.rb b/lib/bundler/checksum.rb
new file mode 100644
index 0000000000..ce05818bb0
--- /dev/null
+++ b/lib/bundler/checksum.rb
@@ -0,0 +1,270 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Checksum
+ ALGO_SEPARATOR = "="
+ DEFAULT_ALGORITHM = "sha256"
+ private_constant :DEFAULT_ALGORITHM
+ DEFAULT_BLOCK_SIZE = 16_384
+ private_constant :DEFAULT_BLOCK_SIZE
+
+ class << self
+ def from_gem_package(gem_package, algo = DEFAULT_ALGORITHM)
+ return if Bundler.settings[:disable_checksum_validation]
+ return unless source = gem_package.instance_variable_get(:@gem)
+ return unless source.respond_to?(:with_read_io)
+
+ source.with_read_io do |io|
+ from_gem(io, source.path)
+ ensure
+ io.rewind
+ end
+ end
+
+ def from_gem(io, pathname, algo = DEFAULT_ALGORITHM)
+ digest = Bundler::SharedHelpers.digest(algo.upcase).new
+ buf = String.new(capacity: DEFAULT_BLOCK_SIZE)
+ digest << io.readpartial(DEFAULT_BLOCK_SIZE, buf) until io.eof?
+ Checksum.new(algo, digest.hexdigest!, Source.new(:gem, pathname))
+ end
+
+ def from_api(digest, source_uri, algo = DEFAULT_ALGORITHM)
+ return if Bundler.settings[:disable_checksum_validation]
+
+ Checksum.new(algo, to_hexdigest(digest, algo), Source.new(:api, source_uri))
+ end
+
+ def from_lock(lock_checksum, lockfile_location)
+ algo, digest = lock_checksum.strip.split(ALGO_SEPARATOR, 2)
+ Checksum.new(algo, to_hexdigest(digest, algo), Source.new(:lock, lockfile_location))
+ end
+
+ def to_hexdigest(digest, algo = DEFAULT_ALGORITHM)
+ return digest unless algo == DEFAULT_ALGORITHM
+ return digest if digest.match?(/\A[0-9a-f]{64}\z/i)
+
+ if digest.match?(%r{\A[-0-9a-z_+/]{43}={0,2}\z}i)
+ digest = digest.tr("-_", "+/") # fix urlsafe base64
+ digest.unpack1("m0").unpack1("H*")
+ else
+ raise ArgumentError, "#{digest.inspect} is not a valid SHA256 hex or base64 digest"
+ end
+ end
+ end
+
+ attr_reader :algo, :digest, :sources
+
+ def initialize(algo, digest, source)
+ @algo = algo
+ @digest = digest
+ @sources = [source]
+ end
+
+ def ==(other)
+ match?(other) && other.sources == sources
+ end
+
+ alias_method :eql?, :==
+
+ def same_source?(other)
+ sources.include?(other.sources.first)
+ end
+
+ def match?(other)
+ other.is_a?(self.class) && other.digest == digest && other.algo == algo
+ end
+
+ def hash
+ digest.hash
+ end
+
+ def to_s
+ "#{to_lock} (from #{sources.first}#{", ..." if sources.size > 1})"
+ end
+
+ def to_lock
+ "#{algo}#{ALGO_SEPARATOR}#{digest}"
+ end
+
+ def merge!(other)
+ return nil unless match?(other)
+
+ @sources.concat(other.sources).uniq!
+ self
+ end
+
+ def formatted_sources
+ sources.join("\n and ").concat("\n")
+ end
+
+ def removable?
+ sources.all?(&:removable?)
+ end
+
+ def removal_instructions
+ msg = +""
+ i = 1
+ sources.each do |source|
+ msg << " #{i}. #{source.removal}\n"
+ i += 1
+ end
+ msg << " #{i}. run `bundle install`\n"
+ end
+
+ def inspect
+ abbr = "#{algo}#{ALGO_SEPARATOR}#{digest[0, 8]}"
+ from = "from #{sources.join(" and ")}"
+ "#<#{self.class}:#{object_id} #{abbr} #{from}>"
+ end
+
+ class Source
+ attr_reader :type, :location
+
+ def initialize(type, location)
+ @type = type
+ @location = location
+ end
+
+ def removable?
+ [:lock, :gem].include?(type)
+ end
+
+ def ==(other)
+ other.is_a?(self.class) && other.type == type && other.location == location
+ end
+
+ # phrased so that the usual string format is grammatically correct
+ # rake (10.3.2) sha256=abc123 from #{to_s}
+ def to_s
+ case type
+ when :lock
+ "the lockfile CHECKSUMS at #{location}"
+ when :gem
+ "the gem at #{location}"
+ when :api
+ "the API at #{location}"
+ else
+ "#{location} (#{type})"
+ end
+ end
+
+ # A full sentence describing how to remove the checksum
+ def removal
+ case type
+ when :lock
+ "remove the matching checksum in #{location}"
+ when :gem
+ "remove the gem at #{location}"
+ when :api
+ "checksums from #{location} cannot be locally modified, you may need to update your sources"
+ else
+ "remove #{location} (#{type})"
+ end
+ end
+ end
+
+ class Store
+ attr_reader :store
+ protected :store
+
+ def initialize
+ @store = {}
+ @store_mutex = Mutex.new
+ end
+
+ def inspect
+ "#<#{self.class}:#{object_id} size=#{store.size}>"
+ end
+
+ # Replace when the new checksum is from the same source.
+ # The primary purpose is registering checksums from gems where there are
+ # duplicates of the same gem (according to full_name) in the index.
+ #
+ # In particular, this is when 2 gems have two similar platforms, e.g.
+ # "darwin20" and "darwin-20", both of which resolve to darwin-20.
+ # In the Index, the later gem replaces the former, so we do that here.
+ #
+ # However, if the new checksum is from a different source, we register like normal.
+ # This ensures a mismatch error where there are multiple top level sources
+ # that contain the same gem with different checksums.
+ def replace(spec, checksum)
+ return unless checksum
+
+ lock_name = spec.lock_name
+ @store_mutex.synchronize do
+ existing = fetch_checksum(lock_name, checksum.algo)
+ if !existing || existing.same_source?(checksum)
+ store_checksum(lock_name, checksum)
+ else
+ merge_checksum(lock_name, checksum, existing)
+ end
+ end
+ end
+
+ def missing?(spec)
+ @store[spec.lock_name].nil?
+ end
+
+ def empty?(spec)
+ return false unless spec.source.is_a?(Bundler::Source::Rubygems)
+
+ @store[spec.lock_name].empty?
+ end
+
+ def register(spec, checksum)
+ register_checksum(spec.lock_name, checksum)
+ end
+
+ def merge!(other)
+ other.store.each do |lock_name, checksums|
+ checksums.each do |_algo, checksum|
+ register_checksum(lock_name, checksum)
+ end
+ end
+ end
+
+ def to_lock(spec)
+ lock_name = spec.lock_name
+ checksums = @store[lock_name]
+ if checksums&.any?
+ "#{lock_name} #{checksums.values.map(&:to_lock).sort.join(",")}"
+ else
+ lock_name
+ end
+ end
+
+ private
+
+ def register_checksum(lock_name, checksum)
+ @store_mutex.synchronize do
+ if checksum
+ existing = fetch_checksum(lock_name, checksum.algo)
+ if existing
+ merge_checksum(lock_name, checksum, existing)
+ else
+ store_checksum(lock_name, checksum)
+ end
+ else
+ init_checksum(lock_name)
+ end
+ end
+ end
+
+ def merge_checksum(lock_name, checksum, existing)
+ existing.merge!(checksum) || raise(ChecksumMismatchError.new(lock_name, existing, checksum))
+ end
+
+ def store_checksum(lock_name, checksum)
+ init_checksum(lock_name)[checksum.algo] = checksum
+ end
+
+ def init_checksum(lock_name)
+ @store[lock_name] ||= {}
+ end
+
+ def fetch_checksum(lock_name, algo)
+ @store[lock_name]&.fetch(algo, nil)
+ end
+ end
+ end
+end
diff --git a/lib/bundler/ci_detector.rb b/lib/bundler/ci_detector.rb
new file mode 100644
index 0000000000..e5fedbdea8
--- /dev/null
+++ b/lib/bundler/ci_detector.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Bundler
+ module CIDetector
+ # NOTE: Any changes made here will need to be made to both lib/rubygems/ci_detector.rb and
+ # bundler/lib/bundler/ci_detector.rb (which are enforced duplicates).
+ # TODO: Drop that duplication once bundler drops support for RubyGems 3.4
+ #
+ # ## Recognized CI providers, their signifiers, and the relevant docs ##
+ #
+ # Travis CI - CI, TRAVIS https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
+ # Cirrus CI - CI, CIRRUS_CI https://cirrus-ci.org/guide/writing-tasks/#environment-variables
+ # Circle CI - CI, CIRCLECI https://circleci.com/docs/variables/#built-in-environment-variables
+ # Gitlab CI - CI, GITLAB_CI https://docs.gitlab.com/ee/ci/variables/
+ # AppVeyor - CI, APPVEYOR https://www.appveyor.com/docs/environment-variables/
+ # CodeShip - CI_NAME https://docs.cloudbees.com/docs/cloudbees-codeship/latest/pro-builds-and-configuration/environment-variables#_default_environment_variables
+ # dsari - CI, DSARI https://github.com/rfinnie/dsari#running
+ # Jenkins - BUILD_NUMBER https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
+ # TeamCity - TEAMCITY_VERSION https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html#Predefined+Server+Build+Parameters
+ # Appflow - CI_BUILD_ID https://ionic.io/docs/appflow/automation/environments#predefined-environments
+ # TaskCluster - TASKCLUSTER_ROOT_URL https://docs.taskcluster.net/docs/manual/design/env-vars
+ # Semaphore - CI, SEMAPHORE https://docs.semaphoreci.com/ci-cd-environment/environment-variables/
+ # BuildKite - CI, BUILDKITE https://buildkite.com/docs/pipelines/environment-variables
+ # GoCD - GO_SERVER_URL https://docs.gocd.org/current/faq/dev_use_current_revision_in_build.html
+ # GH Actions - CI, GITHUB_ACTIONS https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
+ #
+ # ### Some "standard" ENVs that multiple providers may set ###
+ #
+ # * CI - this is set by _most_ (but not all) CI providers now; it's approaching a standard.
+ # * CI_NAME - Not as frequently used, but some providers set this to specify their own name
+
+ # Any of these being set is a reasonably reliable indicator that we are
+ # executing in a CI environment.
+ ENV_INDICATORS = [
+ "CI",
+ "CI_NAME",
+ "CONTINUOUS_INTEGRATION",
+ "BUILD_NUMBER",
+ "CI_APP_ID",
+ "CI_BUILD_ID",
+ "CI_BUILD_NUMBER",
+ "RUN_ID",
+ "TASKCLUSTER_ROOT_URL",
+ ].freeze
+
+ # For each CI, this env suffices to indicate that we're on _that_ CI's
+ # containers. (A few of them only supply a CI_NAME variable, which is also
+ # nice). And if they set "CI" but we can't tell which one they are, we also
+ # want to know that - a bare "ci" without another token tells us as much.
+ ENV_DESCRIPTORS = {
+ "TRAVIS" => "travis",
+ "CIRCLECI" => "circle",
+ "CIRRUS_CI" => "cirrus",
+ "DSARI" => "dsari",
+ "SEMAPHORE" => "semaphore",
+ "JENKINS_URL" => "jenkins",
+ "BUILDKITE" => "buildkite",
+ "GO_SERVER_URL" => "go",
+ "GITLAB_CI" => "gitlab",
+ "GITHUB_ACTIONS" => "github",
+ "TASKCLUSTER_ROOT_URL" => "taskcluster",
+ "CI" => "ci",
+ }.freeze
+
+ def self.ci?
+ ENV_INDICATORS.any? {|var| ENV.include?(var) }
+ end
+
+ def self.ci_strings
+ matching_names = ENV_DESCRIPTORS.select {|env, _| ENV[env] }.values
+ matching_names << ENV["CI_NAME"].downcase if ENV["CI_NAME"]
+ matching_names.reject(&:empty?).sort.uniq
+ end
+ end
+end
diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb
new file mode 100644
index 0000000000..9d8a68fff9
--- /dev/null
+++ b/lib/bundler/cli.rb
@@ -0,0 +1,830 @@
+# frozen_string_literal: true
+
+require_relative "vendored_thor"
+
+module Bundler
+ class CLI < Thor
+ require_relative "cli/common"
+ require_relative "cli/install"
+
+ package_name "Bundler"
+
+ AUTO_INSTALL_CMDS = %w[show binstubs outdated exec open console licenses clean].freeze
+ PARSEABLE_COMMANDS = %w[check config help exec platform show version].freeze
+ EXTENSIONS = ["c", "rust", "go"].freeze
+
+ COMMAND_ALIASES = {
+ "check" => "c",
+ "install" => "i",
+ "plugin" => "",
+ "list" => "ls",
+ "exec" => ["e", "ex", "exe"],
+ "cache" => ["package", "pack"],
+ "version" => ["-v", "--version"],
+ }.freeze
+
+ def self.start(*)
+ check_invalid_ext_option(ARGV) if ARGV.include?("--ext")
+
+ super
+ ensure
+ Bundler::SharedHelpers.print_major_deprecations!
+ end
+
+ def self.dispatch(*)
+ super do |i|
+ i.send(:print_command)
+ i.send(:warn_on_outdated_bundler)
+ end
+ end
+
+ def self.all_aliases
+ @all_aliases ||= begin
+ command_aliases = {}
+
+ COMMAND_ALIASES.each do |name, aliases|
+ Array(aliases).each do |one_alias|
+ command_aliases[one_alias] = name
+ end
+ end
+
+ command_aliases
+ end
+ end
+
+ def self.aliases_for(command_name)
+ COMMAND_ALIASES.select {|k, _| k == command_name }.invert
+ end
+
+ def initialize(*args)
+ super
+
+ current_cmd = args.last[:current_command].name
+
+ # `bundle config` manages stored settings, so avoid promoting settings
+ # like `gemfile` or `lockfile` to environment variables before it runs.
+ unless current_cmd == "config"
+ Bundler.configure_custom_gemfile(options[:gemfile])
+
+ # lock --lockfile works differently than install --lockfile
+ unless current_cmd == "lock"
+ custom_lockfile = options[:lockfile] || ENV["BUNDLE_LOCKFILE"] || Bundler.settings[:lockfile]
+ if custom_lockfile && !custom_lockfile.empty?
+ Bundler::SharedHelpers.set_env "BUNDLE_LOCKFILE", File.expand_path(custom_lockfile)
+ reset_settings = true
+ end
+ end
+ end
+
+ Bundler.reset_settings_and_root! if reset_settings
+
+ Bundler.auto_switch
+
+ Bundler.settings.set_command_option_if_given :retry, options[:retry]
+
+ Bundler.auto_install if AUTO_INSTALL_CMDS.include?(current_cmd)
+ rescue UnknownArgumentError => e
+ raise InvalidOption, e.message
+ ensure
+ self.options ||= {}
+ unprinted_warnings = Bundler.ui.unprinted_warnings
+ Bundler.ui = UI::Shell.new(options)
+ Bundler.ui.level = "debug" if options[:verbose] || Bundler.settings[:verbose]
+ unprinted_warnings.each {|w| Bundler.ui.warn(w) }
+ end
+
+ check_unknown_options!(except: [:config, :exec])
+ stop_on_unknown_option! :exec
+
+ desc "cli_help", "Prints a summary of bundler commands", hide: true
+ def cli_help
+ version
+ Bundler.ui.info "\n"
+
+ primary_commands = ["install", "update", "cache", "exec", "config", "help"]
+
+ list = self.class.printable_commands(true)
+ by_name = list.group_by {|name, _message| name.match(/^bundler? (\w+)/)[1] }
+ utilities = by_name.keys.sort - primary_commands
+ primary_commands.map! {|name| (by_name[name] || raise("no primary command #{name}")).first }
+ utilities.map! {|name| by_name[name].first }
+
+ shell.say "Bundler commands:\n\n"
+
+ shell.say " Primary commands:\n"
+ shell.print_table(primary_commands, indent: 4, truncate: true)
+ shell.say
+ shell.say " Utilities:\n"
+ shell.print_table(utilities, indent: 4, truncate: true)
+ shell.say
+ self.class.send(:class_options_help, shell)
+ end
+
+ desc "install_or_cli_help", "Deprecated alias of install", hide: true
+ def install_or_cli_help
+ Bundler.ui.warn <<~MSG
+ `bundle install_or_cli_help` is a deprecated alias of `bundle install`.
+ It might be called due to the 'default_cli_command' being set to 'install_or_cli_help',
+ if so fix that by running `bundle config set default_cli_command install --global`.
+ MSG
+ invoke_other_command("install")
+ end
+
+ def self.default_command(meth = nil)
+ return super if meth
+
+ unless Bundler.settings[:default_cli_command]
+ Bundler.ui.info <<~MSG
+ In a future version of Bundler, running `bundle` without argument will no longer run `bundle install`.
+ Instead, the `cli_help` command will be displayed. Please use `bundle install` explicitly for scripts like CI/CD.
+ You can use the future behavior now with `bundle config set default_cli_command cli_help --global`,
+ or you can continue to use the current behavior with `bundle config set default_cli_command install --global`.
+ This message will be removed after a default_cli_command value is set.
+
+ MSG
+ end
+
+ Bundler.settings[:default_cli_command] || "install"
+ end
+
+ class_option "no-color", type: :boolean, desc: "Disable colorization in output"
+ class_option "retry", type: :numeric, aliases: "-r", banner: "NUM",
+ desc: "Specify the number of times you wish to attempt network commands"
+ class_option "verbose", type: :boolean, desc: "Enable verbose output mode", aliases: "-V"
+
+ def help(cli = nil)
+ cli = self.class.all_aliases[cli] if self.class.all_aliases[cli]
+
+ if Bundler.settings[:plugins] && Bundler::Plugin.command?(cli) && !self.class.all_commands.key?(cli)
+ return Bundler::Plugin.exec_command(cli, ["--help"])
+ end
+
+ case cli
+ when "gemfile" then command = "gemfile"
+ when nil then command = "bundle"
+ else command = "bundle-#{cli}"
+ end
+
+ man_path = File.expand_path("man", __dir__)
+ man_pages = Hash[Dir.glob(File.join(man_path, "**", "*")).grep(/.*\.\d*\Z/).collect do |f|
+ [File.basename(f, ".*"), f]
+ end]
+
+ if man_pages.include?(command)
+ man_page = man_pages[command]
+ if Bundler.which("man") && !man_path.match?(%r{^(?:file:/.+!|uri:classloader:)/META-INF/jruby.home/.+})
+ Kernel.exec("man", man_page)
+ else
+ puts File.read("#{man_path}/#{File.basename(man_page)}.ronn")
+ end
+ elsif command_path = Bundler.which("bundler-#{cli}")
+ Kernel.exec(command_path, "--help")
+ else
+ super
+ end
+ end
+
+ def self.handle_no_command_error(command, has_namespace = $thor_runner)
+ if Bundler.settings[:plugins] && Bundler::Plugin.command?(command)
+ return Bundler::Plugin.exec_command(command, ARGV[1..-1])
+ end
+
+ return super unless command_path = Bundler.which("bundler-#{command}")
+
+ Kernel.exec(command_path, *ARGV[1..-1])
+ end
+
+ desc "init [OPTIONS]", "Generates a Gemfile into the current working directory"
+ long_desc <<-D
+ Init generates a default Gemfile in the current working directory. When adding a
+ Gemfile to a gem with a gemspec, the --gemspec option will automatically add each
+ dependency listed in the gemspec file to the newly created Gemfile.
+ D
+ method_option "gemspec", type: :string, banner: "Use the specified .gemspec to create the Gemfile"
+ method_option "gemfile", type: :string, banner: "Use the specified name for the gemfile instead of 'Gemfile'"
+ def init
+ require_relative "cli/init"
+ Init.new(options.dup).run
+ end
+
+ desc "check [OPTIONS]", "Checks if the dependencies listed in Gemfile are satisfied by currently installed gems"
+ long_desc <<-D
+ Check searches the local machine for each of the gems requested in the Gemfile. If
+ all gems are found, Bundler prints a success message and exits with a status of 0.
+ If not, the first missing gem is listed and Bundler exits status 1.
+ D
+ method_option "dry-run", type: :boolean, default: false, banner: "Lock the Gemfile"
+ method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
+ method_option "path", type: :string, banner: "Specify a different path than the system default, namely, $BUNDLE_PATH or $GEM_HOME (removed)"
+ def check
+ remembered_flag_deprecation("path")
+
+ require_relative "cli/check"
+ Check.new(options).run
+ end
+
+ map aliases_for("check")
+
+ desc "remove [GEM [GEM ...]]", "Removes gems from the Gemfile"
+ long_desc <<-D
+ Removes the given gems from the Gemfile while ensuring that the resulting Gemfile is still valid. If the gem is not found, Bundler prints a error message and if gem could not be removed due to any reason Bundler will display a warning.
+ D
+ method_option "install", type: :boolean, banner: "Runs 'bundle install' after removing the gems from the Gemfile (removed)"
+ def remove(*gems)
+ if ARGV.include?("--install")
+ removed_message = "The `--install` flag has been removed. `bundle install` is triggered by default."
+ raise InvalidOption, removed_message
+ end
+
+ require_relative "cli/remove"
+ Remove.new(gems, options).run
+ end
+
+ desc "install [OPTIONS]", "Install the current environment to the system"
+ long_desc <<-D
+ Install will install all of the gems in the current bundle, making them available
+ for use. In a freshly checked out repository, this command will give you the same
+ gem versions as the last person who updated the Gemfile and ran `bundle update`.
+
+ Passing [DIR] to install (e.g. vendor) will cause the unpacked gems to be installed
+ into the [DIR] directory rather than into system gems.
+
+ If the bundle has already been installed, bundler will tell you so and then exit.
+ D
+ method_option "binstubs", type: :string, lazy_default: "bin", banner: "Generate bin stubs for bundled gems to ./bin (removed)"
+ method_option "clean", type: :boolean, banner: "Run bundle clean automatically after install (removed)"
+ method_option "deployment", type: :boolean, banner: "Install using defaults tuned for deployment environments (removed)"
+ method_option "frozen", type: :boolean, banner: "Do not allow the Gemfile.lock to be updated after this install (removed)"
+ method_option "full-index", type: :boolean, banner: "Fall back to using the single-file index of all gems"
+ method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
+ method_option "jobs", aliases: "-j", type: :numeric, banner: "Specify the number of jobs to run in parallel"
+ method_option "local", type: :boolean, banner: "Do not attempt to fetch gems remotely and use the gem cache instead"
+ method_option "lockfile", type: :string, banner: "Use the specified lockfile instead of the default."
+ method_option "prefer-local", type: :boolean, banner: "Only attempt to fetch gems remotely if not present locally, even if newer versions are available remotely"
+ method_option "no-cache", type: :boolean, banner: "Don't update the existing gem cache."
+ method_option "no-lock", type: :boolean, banner: "Don't create a lockfile."
+ method_option "force", type: :boolean, aliases: "--redownload", banner: "Force reinstalling every gem, even if already installed"
+ method_option "no-prune", type: :boolean, banner: "Don't remove stale gems from the cache (removed)."
+ method_option "path", type: :string, banner: "Specify a different path than the system default, namely, $BUNDLE_PATH or $GEM_HOME (removed)."
+ method_option "quiet", type: :boolean, banner: "Only output warnings and errors."
+ method_option "shebang", type: :string, banner: "Specify a different shebang executable name than the default, usually 'ruby' (removed)"
+ method_option "standalone", type: :array, lazy_default: [], banner: "Make a bundle that can work without the Bundler runtime"
+ method_option "system", type: :boolean, banner: "Install to the system location ($BUNDLE_PATH or $GEM_HOME) even if the bundle was previously installed somewhere else for this application (removed)"
+ method_option "trust-policy", alias: "P", type: :string, banner: "Gem trust policy (like gem install -P). Must be one of #{Bundler.rubygems.security_policy_keys.join("|")}"
+ method_option "target-rbconfig", type: :string, banner: "Path to rbconfig.rb for the deployment target platform"
+ method_option "without", type: :array, banner: "Exclude gems that are part of the specified named group (removed)."
+ method_option "with", type: :array, banner: "Include gems that are part of the specified named group (removed)."
+ method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
+ def install
+ %w[clean deployment frozen no-prune path shebang without with].each do |option|
+ remembered_flag_deprecation(option)
+ end
+
+ print_remembered_flag_deprecation("--system", "path.system", "true") if ARGV.include?("--system")
+
+ remembered_flag_deprecation("deployment", negative: true)
+
+ if ARGV.include?("--binstubs")
+ removed_message = "The --binstubs option has been removed in favor of `bundle binstubs --all`"
+ raise InvalidOption, removed_message
+ end
+
+ require_relative "cli/install"
+ options = self.options.dup
+ options["lockfile"] ||= ENV["BUNDLE_LOCKFILE"]
+ Bundler.settings.temporary(no_install: false) do
+ Install.new(options).run
+ end
+ rescue GemfileNotFound => error
+ invoke_other_command("cli_help")
+ raise error # re-raise to show the error and get a failing exit status
+ end
+
+ map aliases_for("install")
+
+ desc "update [OPTIONS]", "Update the current environment"
+ long_desc <<-D
+ Update will install the newest versions of the gems listed in the Gemfile. Use
+ update when you have changed the Gemfile, or if you want to get the newest
+ possible versions of the gems in the bundle.
+ D
+ method_option "full-index", type: :boolean, banner: "Fall back to using the single-file index of all gems"
+ method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
+ method_option "group", aliases: "-g", type: :array, banner: "Update a specific group"
+ method_option "jobs", aliases: "-j", type: :numeric, banner: "Specify the number of jobs to run in parallel"
+ method_option "local", type: :boolean, banner: "Do not attempt to fetch gems remotely and use the gem cache instead"
+ method_option "quiet", type: :boolean, banner: "Only output warnings and errors."
+ method_option "source", type: :array, banner: "Update a specific source (and all gems associated with it)"
+ method_option "force", type: :boolean, aliases: "--redownload", banner: "Force reinstalling every gem, even if already installed"
+ method_option "ruby", type: :boolean, banner: "Update ruby specified in Gemfile.lock"
+ method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler"
+ method_option "patch", type: :boolean, banner: "Prefer updating only to next patch version"
+ method_option "minor", type: :boolean, banner: "Prefer updating only to next minor version"
+ method_option "major", type: :boolean, banner: "Prefer updating to next major version (default)"
+ method_option "pre", type: :boolean, banner: "Always choose the highest allowed version when updating gems, regardless of prerelease status"
+ method_option "strict", type: :boolean, banner: "Do not allow any gem to be updated past latest --patch | --minor | --major"
+ method_option "conservative", type: :boolean, banner: "Use bundle install conservative update behavior and do not allow shared dependencies to be updated."
+ method_option "all", type: :boolean, banner: "Update everything."
+ method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
+ def update(*gems)
+ require_relative "cli/update"
+ Bundler.settings.temporary(no_install: false) do
+ Update.new(options, gems).run
+ end
+ end
+
+ desc "show GEM [OPTIONS]", "Shows all gems that are part of the bundle, or the path to a given gem"
+ long_desc <<-D
+ Show lists the names and versions of all gems that are required by your Gemfile.
+ Calling show with [GEM] will list the exact location of that gem on your machine.
+ D
+ method_option "paths", type: :boolean, banner: "List the paths of all gems that are required by your Gemfile."
+ method_option "outdated", type: :boolean, banner: "Show verbose output including whether gems are outdated (removed)."
+ def show(gem_name = nil)
+ if ARGV.include?("--outdated")
+ removed_message = "the `--outdated` flag to `bundle show` has been removed in favor of `bundle show --verbose`"
+ raise InvalidOption, removed_message
+ end
+ require_relative "cli/show"
+ Show.new(options, gem_name).run
+ end
+
+ desc "list", "List all gems in the bundle"
+ method_option "name-only", type: :boolean, banner: "print only the gem names"
+ method_option "only-group", type: :array, default: [], banner: "print gems from a given set of groups"
+ method_option "without-group", type: :array, default: [], banner: "print all gems except from a given set of groups"
+ method_option "format", type: :string, banner: "format output ('json' is the only supported format)"
+ method_option "paths", type: :boolean, banner: "print the path to each gem in the bundle"
+ def list
+ require_relative "cli/list"
+ List.new(options).run
+ end
+
+ map aliases_for("list")
+
+ desc "info GEM [OPTIONS]", "Show information for the given gem"
+ method_option "path", type: :boolean, banner: "Print full path to gem"
+ method_option "version", type: :boolean, banner: "Print gem version"
+ def info(gem_name)
+ require_relative "cli/info"
+ Info.new(options, gem_name).run
+ end
+
+ desc "binstubs GEM [OPTIONS]", "Install the binstubs of the listed gem"
+ long_desc <<-D
+ Generate binstubs for executables in [GEM]. Binstubs are put into bin,
+ or the --binstubs directory if one has been set. Calling binstubs with [GEM [GEM]]
+ will create binstubs for all given gems.
+ D
+ method_option "force", type: :boolean, default: false, banner: "Overwrite existing binstubs if they exist"
+ method_option "path", type: :string, lazy_default: "bin", banner: "Binstub destination directory, `bin` by default (removed)"
+ method_option "shebang", type: :string, banner: "Specify a different shebang executable name than the default (usually 'ruby')"
+ method_option "standalone", type: :boolean, banner: "Make binstubs that can work without the Bundler runtime"
+ method_option "all", type: :boolean, banner: "Install binstubs for all gems"
+ method_option "all-platforms", type: :boolean, default: false, banner: "Install binstubs for all platforms"
+ def binstubs(*gems)
+ remembered_flag_deprecation("path", option_name: "bin")
+
+ require_relative "cli/binstubs"
+ Binstubs.new(options, gems).run
+ end
+
+ desc "add GEM VERSION", "Add gem to Gemfile and run bundle install"
+ long_desc <<-D
+ Adds the specified gem to Gemfile (if valid) and run 'bundle install' in one step.
+ D
+ method_option "version", aliases: "-v", type: :string
+ method_option "group", aliases: "-g", type: :string
+ method_option "source", aliases: "-s", type: :string
+ method_option "require", aliases: "-r", type: :string, banner: "Adds require path to gem. Provide false, or a path as a string."
+ method_option "path", type: :string
+ method_option "git", type: :string
+ method_option "github", type: :string
+ method_option "branch", type: :string
+ method_option "ref", type: :string
+ method_option "glob", type: :string, banner: "The location of a dependency's .gemspec, expanded within Ruby (single quotes recommended)"
+ method_option "quiet", type: :boolean, banner: "Only output warnings and errors."
+ method_option "skip-install", type: :boolean, banner: "Adds gem to the Gemfile but does not install it"
+ method_option "optimistic", type: :boolean, banner: "Ignored (now default behavior)"
+ method_option "pessimistic", type: :boolean, banner: "Adds pessimistic declaration of version to gem"
+ method_option "strict", type: :boolean, banner: "Adds strict declaration of version to gem"
+ method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
+ def add(*gems)
+ require_relative "cli/add"
+ Add.new(options.dup, gems).run
+ end
+
+ desc "outdated GEM [OPTIONS]", "List installed gems with newer versions available"
+ long_desc <<-D
+ Outdated lists the names and versions of gems that have a newer version available
+ in the given source. Calling outdated with [GEM [GEM]] will only check for newer
+ versions of the given gems. Prerelease gems are ignored by default. If your gems
+ are up to date, Bundler will exit with a status of 0. Otherwise, it will exit 1.
+
+ For more information on patch level options (--major, --minor, --patch,
+ --strict) see documentation on the same options on the update command.
+ D
+ method_option "group", type: :string, banner: "List gems from a specific group"
+ method_option "groups", type: :boolean, banner: "List gems organized by groups"
+ method_option "local", type: :boolean, banner: "Do not attempt to fetch gems remotely and use the gem cache instead"
+ method_option "pre", type: :boolean, banner: "Check for newer pre-release gems"
+ method_option "source", type: :array, banner: "Check against a specific source"
+ method_option "filter-strict", type: :boolean, aliases: "--strict", banner: "Only list newer versions allowed by your Gemfile requirements"
+ method_option "update-strict", type: :boolean, banner: "Strict conservative resolution, do not allow any gem to be updated past latest --patch | --minor | --major"
+ method_option "minor", type: :boolean, banner: "Prefer updating only to next minor version"
+ method_option "major", type: :boolean, banner: "Prefer updating to next major version (default)"
+ method_option "patch", type: :boolean, banner: "Prefer updating only to next patch version"
+ method_option "filter-major", type: :boolean, banner: "Only list major newer versions"
+ method_option "filter-minor", type: :boolean, banner: "Only list minor newer versions"
+ method_option "filter-patch", type: :boolean, banner: "Only list patch newer versions"
+ method_option "parseable", aliases: "--porcelain", type: :boolean, banner: "Use minimal formatting for more parseable output"
+ method_option "only-explicit", type: :boolean, banner: "Only list gems specified in your Gemfile, not their dependencies"
+ method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
+ def outdated(*gems)
+ require_relative "cli/outdated"
+ Outdated.new(options, gems).run
+ end
+
+ desc "fund [OPTIONS]", "Lists information about gems seeking funding assistance"
+ method_option "group", aliases: "-g", type: :array, banner: "Fetch funding information for a specific group"
+ def fund
+ require_relative "cli/fund"
+ Fund.new(options).run
+ end
+
+ desc "cache [OPTIONS]", "Locks and then caches all of the gems into vendor/cache"
+ method_option "all", type: :boolean, default: Bundler.settings[:cache_all], banner: "Include all sources (including path and git) (removed)."
+ method_option "all-platforms", type: :boolean, banner: "Include gems for all platforms present in the lockfile, not only the current one"
+ method_option "cache-path", type: :string, banner: "Specify a different cache path than the default (vendor/cache)."
+ method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
+ method_option "no-install", type: :boolean, banner: "Don't install the gems, only update the cache."
+ method_option "no-prune", type: :boolean, banner: "Don't remove stale gems from the cache (removed)."
+ method_option "path", type: :string, banner: "Specify a different path than the system default, namely, $BUNDLE_PATH or $GEM_HOME (removed)."
+ method_option "quiet", type: :boolean, banner: "Only output warnings and errors."
+ method_option "frozen", type: :boolean, banner: "Do not allow the Gemfile.lock to be updated after this bundle cache operation's install (removed)"
+ long_desc <<-D
+ The cache command will copy the .gem files for every gem in the bundle into the
+ directory ./vendor/cache. If you then check that directory into your source
+ control repository, others who check out your source will be able to install the
+ bundle without having to download any additional gems.
+ D
+ def cache
+ print_remembered_flag_deprecation("--all", "cache_all", "true") if ARGV.include?("--all")
+ print_remembered_flag_deprecation("--no-all", "cache_all", "false") if ARGV.include?("--no-all")
+
+ %w[frozen no-prune].each do |option|
+ remembered_flag_deprecation(option)
+ end
+
+ if flag_passed?("--path")
+ removed_message =
+ "The `--path` flag has been removed because its semantics were unclear. " \
+ "Use `bundle config cache_path` to configure the path of your cache of gems, " \
+ "and `bundle config path` to configure the path where your gems are installed, " \
+ "and stop using this flag"
+ raise InvalidOption, removed_message
+ end
+
+ require_relative "cli/cache"
+ Cache.new(options).run
+ end
+
+ map aliases_for("cache")
+
+ desc "exec [OPTIONS]", "Run the command in context of the bundle"
+ method_option :keep_file_descriptors, type: :boolean, default: true, banner: "Passes all file descriptors to the new processes. Default is true, and setting it to false is not permitted (removed)."
+ method_option :gemfile, type: :string, required: false, banner: "Use the specified gemfile instead of Gemfile"
+ long_desc <<-D
+ Exec runs a command, providing it access to the gems in the bundle. While using
+ bundle exec you can require and call the bundled gems as if they were installed
+ into the system wide RubyGems repository.
+ D
+ def exec(*args)
+ if ARGV.include?("--no-keep-file-descriptors")
+ removed_message = "The `--no-keep-file-descriptors` has been removed. `bundle exec` no longer mess with your file descriptors. Close them in the exec'd script if you need to"
+ raise InvalidOption, removed_message
+ end
+
+ require_relative "cli/exec"
+ Exec.new(options, args).run
+ end
+
+ map aliases_for("exec")
+
+ desc "config NAME [VALUE]", "Retrieve or set a configuration value"
+ long_desc <<-D
+ Retrieves or sets a configuration value. If only one parameter is provided, retrieve the value. If two parameters are provided, replace the
+ existing value with the newly provided one.
+
+ By default, setting a configuration value sets it for all projects
+ on the machine.
+
+ If a global setting is superseded by local configuration, this command
+ will show the current value, as well as any superseded values and
+ where they were specified.
+ D
+ require_relative "cli/config"
+ subcommand "config", Config
+
+ desc "open GEM", "Opens the source directory of the given bundled gem"
+ method_option "path", type: :string, lazy_default: "", banner: "Open relative path of the gem source."
+ def open(name)
+ require_relative "cli/open"
+ Open.new(options, name).run
+ end
+
+ desc "console [GROUP]", "Opens an IRB session with the bundle pre-loaded"
+ def console(group = nil)
+ require_relative "cli/console"
+ Console.new(options, group).run
+ end
+
+ desc "version", "Prints Bundler version information"
+ def version
+ cli_help = current_command.name == "cli_help"
+ if cli_help || ARGV.include?("version")
+ build_info = " (#{BuildMetadata.timestamp} commit #{BuildMetadata.git_commit_sha})"
+ end
+
+ if !cli_help
+ Bundler.ui.info "#{Bundler.verbose_version}#{build_info}"
+ else
+ Bundler.ui.info "Bundler version #{Bundler.verbose_version}#{build_info}"
+ end
+ end
+
+ map aliases_for("version")
+
+ desc "licenses", "Prints the license of all gems in the bundle"
+ def licenses
+ Bundler.load.specs.sort_by {|s| s.license.to_s }.reverse_each do |s|
+ gem_name = s.name
+ license = s.license || s.licenses
+
+ if license.empty?
+ Bundler.ui.warn "#{gem_name}: Unknown"
+ else
+ Bundler.ui.info "#{gem_name}: #{license}"
+ end
+ end
+ end
+
+ desc "viz [OPTIONS]", "Generates a visual dependency graph", hide: true
+ def viz
+ SharedHelpers.feature_removed! "The `viz` command has been renamed to `graph` and moved to a plugin. See https://github.com/rubygems/bundler-graph"
+ end
+
+ desc "gem NAME [OPTIONS]", "Creates a skeleton for creating a rubygem"
+ method_option :exe, type: :boolean, default: false, aliases: ["--bin", "-b"], banner: "Generate a binary executable for your library."
+ method_option :coc, type: :boolean, banner: "Generate a code of conduct file. Set a default with `bundle config set --global gem.coc true`."
+ method_option :edit, type: :string, aliases: "-e", required: false, lazy_default: [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? }, banner: "Open generated gemspec in the specified editor (defaults to $EDITOR or $BUNDLER_EDITOR)"
+ method_option :ext, type: :string, banner: "Generate the boilerplate for C extension code.", enum: EXTENSIONS
+ method_option :git, type: :boolean, default: true, banner: "Initialize a git repo inside your library."
+ method_option :mit, type: :boolean, banner: "Generate an MIT license file. Set a default with `bundle config set --global gem.mit true`."
+ method_option :rubocop, type: :boolean, banner: "Add rubocop to the generated Rakefile and gemspec. Set a default with `bundle config set --global gem.rubocop true` (removed)."
+ method_option :changelog, type: :boolean, banner: "Generate changelog file. Set a default with `bundle config set --global gem.changelog true`."
+ method_option :test, type: :string, lazy_default: Bundler.settings["gem.test"] || "", aliases: "-t", banner: "Use the specified test framework for your library", enum: %w[rspec minitest test-unit], desc: "Generate a test directory for your library, either rspec, minitest or test-unit. Set a default with `bundle config set --global gem.test (rspec|minitest|test-unit)`."
+ method_option :ci, type: :string, lazy_default: Bundler.settings["gem.ci"] || "", enum: %w[github gitlab circle], banner: "Generate CI configuration, either GitHub Actions, GitLab CI or CircleCI. Set a default with `bundle config set --global gem.ci (github|gitlab|circle)`"
+ method_option :linter, type: :string, lazy_default: Bundler.settings["gem.linter"] || "", enum: %w[rubocop standard], banner: "Add a linter and code formatter, either RuboCop or Standard. Set a default with `bundle config set --global gem.linter (rubocop|standard)`"
+ method_option :github_username, type: :string, default: Bundler.settings["gem.github_username"], banner: "Set your username on GitHub", desc: "Fill in GitHub username on README so that you don't have to do it manually. Set a default with `bundle config set --global gem.github_username <your_username>`."
+ method_option :bundle, type: :boolean, default: Bundler.settings["gem.bundle"], banner: "Automatically run `bundle install` after creation. Set a default with `bundle config set --global gem.bundle true`"
+
+ def gem(name)
+ require_relative "cli/gem"
+
+ raise InvalidOption, "--rubocop has been removed, use --linter=rubocop" if ARGV.include?("--rubocop")
+ raise InvalidOption, "--no-rubocop has been removed, use --no-linter" if ARGV.include?("--no-rubocop")
+
+ cmd_args = args + [self]
+ cmd_args.unshift(options)
+
+ Gem.new(*cmd_args).run
+ end
+
+ def self.source_root
+ File.expand_path("templates", __dir__)
+ end
+
+ desc "clean [OPTIONS]", "Cleans up unused gems in your bundler directory"
+ method_option "dry-run", type: :boolean, default: false, banner: "Only print out changes, do not clean gems"
+ method_option "force", type: :boolean, default: false, banner: "Forces cleaning up unused gems even if Bundler is configured to use globally installed gems. As a consequence, removes all system gems except for the ones in the current application."
+ def clean
+ require_relative "cli/clean"
+ Clean.new(options.dup).run
+ end
+
+ desc "platform [OPTIONS]", "Displays platform compatibility information"
+ method_option "ruby", type: :boolean, default: false, banner: "only display ruby related platform information"
+ def platform
+ require_relative "cli/platform"
+ Platform.new(options).run
+ end
+
+ desc "inject GEM VERSION", "Add the named gem, with version requirements, to the resolved Gemfile", hide: true
+ def inject(*)
+ SharedHelpers.feature_removed! "The `inject` command has been replaced by the `add` command"
+ end
+
+ desc "lock", "Creates a lockfile without installing"
+ method_option "update", type: :array, lazy_default: true, banner: "ignore the existing lockfile, update all gems by default, or update list of given gems"
+ method_option "local", type: :boolean, default: false, banner: "do not attempt to fetch remote gemspecs and use the local gem cache only"
+ method_option "print", type: :boolean, default: false, banner: "print the lockfile to STDOUT instead of writing to the file system"
+ method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
+ method_option "lockfile", type: :string, default: nil, banner: "the path the lockfile should be written to"
+ method_option "full-index", type: :boolean, default: false, banner: "Fall back to using the single-file index of all gems"
+ method_option "add-checksums", type: :boolean, default: false, banner: "Adds checksums to the lockfile"
+ method_option "add-platform", type: :array, default: [], banner: "Add a new platform to the lockfile"
+ method_option "remove-platform", type: :array, default: [], banner: "Remove a platform from the lockfile"
+ method_option "normalize-platforms", type: :boolean, default: false, banner: "Normalize lockfile platforms"
+ method_option "patch", type: :boolean, banner: "If updating, prefer updating only to next patch version"
+ method_option "minor", type: :boolean, banner: "If updating, prefer updating only to next minor version"
+ method_option "major", type: :boolean, banner: "If updating, prefer updating to next major version (default)"
+ method_option "pre", type: :boolean, banner: "If updating, always choose the highest allowed version, regardless of prerelease status"
+ method_option "strict", type: :boolean, banner: "If updating, do not allow any gem to be updated past latest --patch | --minor | --major"
+ method_option "conservative", type: :boolean, banner: "If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated"
+ method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler"
+ def lock
+ require_relative "cli/lock"
+ Lock.new(options).run
+ end
+
+ desc "env", "Print information about the environment Bundler is running under"
+ def env
+ Env.write($stdout)
+ end
+
+ desc "doctor [OPTIONS]", "Checks the bundle for common problems"
+ require_relative "cli/doctor"
+ subcommand("doctor", Doctor)
+
+ desc "issue", "Learn how to report an issue in Bundler"
+ def issue
+ require_relative "cli/issue"
+ Issue.new.run
+ end
+
+ desc "pristine [GEMS...]", "Restores installed gems to pristine condition"
+ long_desc <<-D
+ Restores installed gems to pristine condition from files located in the
+ gem cache. Gems installed from a git repository will be issued `git
+ checkout --force`.
+ D
+ def pristine(*gems)
+ require_relative "cli/pristine"
+ Bundler.settings.temporary(no_install: false) do
+ Pristine.new(gems).run
+ end
+ end
+
+ if Bundler.settings[:plugins]
+ require_relative "cli/plugin"
+ desc "plugin", "Manage the bundler plugins"
+ subcommand "plugin", Plugin
+ end
+
+ # Reformat the arguments passed to bundle that include a --help flag
+ # into the corresponding `bundle help #{command}` call
+ def self.reformatted_help_args(args)
+ bundler_commands = (COMMAND_ALIASES.keys + COMMAND_ALIASES.values).flatten
+
+ help_flags = %w[--help -h]
+ exec_commands = ["exec"] + COMMAND_ALIASES["exec"]
+
+ help_used = args.index {|a| help_flags.include? a }
+ exec_used = args.index {|a| exec_commands.include? a }
+
+ command = args.find {|a| bundler_commands.include? a }
+
+ if exec_used && help_used
+ if exec_used + help_used == 1
+ %w[help exec]
+ else
+ args
+ end
+ elsif help_used
+ args = args.dup
+ args.delete_at(help_used)
+ ["help", command || args].flatten.compact
+ else
+ args
+ end
+ end
+
+ def self.check_invalid_ext_option(arguments)
+ # when invalid version of `--ext` is called
+ if invalid_ext_value?(arguments)
+ removed_message = "Extensions can now be generated using C or Rust, so `--ext` with no arguments has been removed. Please select a language, e.g. `--ext=rust` to generate a Rust extension."
+ raise InvalidOption, removed_message
+ end
+ end
+
+ def self.invalid_ext_value?(arguments)
+ index = arguments.index("--ext")
+ next_argument = arguments[index + 1]
+
+ # it is ok when --ext is followed with valid extension value
+ # for example `bundle gem hello --ext c`
+ return false if EXTENSIONS.include?(next_argument)
+
+ # invalid call when --ext is called with no value in last position
+ # for example `bundle gem hello_gem --ext`
+ return true if next_argument.nil?
+
+ # invalid call when --ext is followed by other parameter
+ # for example `bundle gem --ext --no-ci hello_gem`
+ return true if next_argument.start_with?("-")
+
+ # invalid call when --ext is followed by gem name
+ # for example `bundle gem --ext hello_gem`
+ return true if next_argument
+
+ false
+ end
+
+ private
+
+ def current_command
+ _, _, config = @_initializer
+ config[:current_command]
+ end
+
+ def invoke_other_command(name)
+ _, _, config = @_initializer
+ original_command = config[:current_command]
+ command = self.class.all_commands[name]
+ config[:current_command] = command
+ send(name)
+ ensure
+ config[:current_command] = original_command
+ end
+
+ def current_command=(command)
+ end
+
+ def print_command
+ return unless Bundler.ui.debug?
+ cmd = current_command
+ command_name = cmd.name
+ return if PARSEABLE_COMMANDS.include?(command_name)
+ command = ["bundle", command_name] + args
+ options_to_print = options.dup
+ options_to_print.delete_if do |k, v|
+ next unless o = cmd.options[k]
+ o.default == v
+ end
+ command << Thor::Options.to_switches(options_to_print.sort_by(&:first)).strip
+ command.reject!(&:empty?)
+ Bundler.ui.info "Running `#{command * " "}` with bundler #{Bundler.verbose_version}"
+ end
+
+ def warn_on_outdated_bundler
+ return if Bundler.settings[:disable_version_check]
+
+ command_name = current_command.name
+ return if PARSEABLE_COMMANDS.include?(command_name)
+
+ return unless SharedHelpers.md5_available?
+
+ require_relative "vendored_uri"
+ remote = Source::Rubygems::Remote.new(Gem::URI("https://rubygems.org"))
+ cache_path = Bundler.user_cache.join("compact_index", remote.cache_slug)
+ latest = Bundler::CompactIndexClient.new(cache_path).latest_version("bundler")
+ return unless latest
+
+ current = Gem::Version.new(VERSION)
+ return if current >= latest
+
+ Bundler.ui.warn \
+ "The latest bundler is #{latest}, but you are currently running #{current}.\n" \
+ "To update to the most recent version, run `bundle update --bundler`"
+ rescue RuntimeError
+ nil
+ end
+
+ def remembered_flag_deprecation(name, negative: false, option_name: nil)
+ option = current_command.options[name]
+ flag_name = option.switch_name
+ flag_name = "--no-" + flag_name.gsub(/\A--/, "") if negative
+ return unless flag_passed?(flag_name)
+
+ value = options[name]
+ value = value.join(" ").to_s if option.type == :array
+ value = "'#{value}'" unless option.type == :boolean
+
+ print_remembered_flag_deprecation(flag_name, option_name || name.tr("-", "_"), value)
+ end
+
+ def print_remembered_flag_deprecation(flag_name, option_name, option_value)
+ removed_message =
+ "The `#{flag_name}` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer does. " \
+ "Instead please use `bundle config set #{option_name} #{option_value}`, " \
+ "and stop using this flag"
+ raise InvalidOption, removed_message
+ end
+
+ def flag_passed?(name)
+ ARGV.any? {|arg| name == arg.split("=")[0] }
+ end
+ end
+end
diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb
new file mode 100644
index 0000000000..20f76b59d1
--- /dev/null
+++ b/lib/bundler/cli/add.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Add
+ attr_reader :gems, :options, :version
+
+ def initialize(options, gems)
+ @gems = gems
+ @options = options
+ @options[:group] = options[:group].split(",").map(&:strip) unless options[:group].nil?
+ @version = options[:version].split(",").map(&:strip) unless options[:version].nil?
+ end
+
+ def run
+ Bundler.ui.level = "warn" if options[:quiet]
+
+ Bundler::CLI::Common.validate_cooldown!(options[:cooldown])
+ Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown]
+
+ validate_options!
+ inject_dependencies
+ perform_bundle_install unless options["skip-install"]
+ end
+
+ private
+
+ def perform_bundle_install
+ Installer.install(Bundler.root, Bundler.definition)
+ Bundler.load.cache if Bundler.app_cache.exist?
+ end
+
+ def inject_dependencies
+ dependencies = gems.map {|g| Bundler::Dependency.new(g, version, options) }
+
+ Injector.inject(dependencies,
+ conservative_versioning: options[:version].nil?, # Perform conservative versioning only when version is not specified
+ pessimistic: options[:pessimistic],
+ strict: options[:strict])
+ end
+
+ def validate_options!
+ raise InvalidOption, "You cannot specify `--git` and `--github` at the same time." if options["git"] && options["github"]
+
+ unless options["git"] || options["github"]
+ raise InvalidOption, "You cannot specify `--branch` unless `--git` or `--github` is specified." if options["branch"]
+
+ raise InvalidOption, "You cannot specify `--ref` unless `--git` or `--github` is specified." if options["ref"]
+ end
+
+ raise InvalidOption, "You cannot specify `--branch` and `--ref` at the same time." if options["branch"] && options["ref"]
+
+ raise InvalidOption, "You cannot specify `--strict` and `--pessimistic` at the same time." if options[:strict] && options[:pessimistic]
+
+ # raise error when no gems are specified
+ raise InvalidOption, "Please specify gems to add." if gems.empty?
+
+ version.to_a.each do |v|
+ raise InvalidOption, "Invalid gem requirement pattern '#{v}'" unless Gem::Requirement::PATTERN.match?(v.to_s)
+ end
+ end
+ end
+end
diff --git a/lib/bundler/cli/binstubs.rb b/lib/bundler/cli/binstubs.rb
new file mode 100644
index 0000000000..8ce138df96
--- /dev/null
+++ b/lib/bundler/cli/binstubs.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Binstubs
+ attr_reader :options, :gems
+ def initialize(options, gems)
+ @options = options
+ @gems = gems
+ end
+
+ def run
+ Bundler.definition.validate_runtime!
+ path_option = options["path"]
+ path_option = nil if path_option&.empty?
+ Bundler.settings.set_command_option :bin, path_option if options["path"]
+ Bundler.settings.set_command_option_if_given :shebang, options["shebang"]
+ installer = Installer.new(Bundler.root, Bundler.definition)
+
+ installer_opts = {
+ force: options[:force],
+ binstubs_cmd: true,
+ all_platforms: options["all-platforms"],
+ }
+
+ if options[:all]
+ raise InvalidOption, "Cannot specify --all with specific gems" unless gems.empty?
+ @gems = Bundler.definition.specs.map(&:name)
+ installer_opts.delete(:binstubs_cmd)
+ elsif gems.empty?
+ Bundler.ui.error "`bundle binstubs` needs at least one gem to run."
+ exit 1
+ end
+
+ gems.each do |gem_name|
+ spec = Bundler.definition.specs.find {|s| s.name == gem_name }
+ unless spec
+ raise GemNotFound, Bundler::CLI::Common.gem_not_found_message(
+ gem_name, Bundler.definition.specs
+ )
+ end
+
+ if options[:standalone]
+ if gem_name == "bundler"
+ Bundler.ui.warn("Sorry, Bundler can only be run via RubyGems.") unless options[:all]
+ next
+ end
+
+ Bundler.settings.temporary(path: Bundler.settings[:path] || Bundler.root) do
+ installer.generate_standalone_bundler_executable_stubs(spec, installer_opts)
+ end
+ else
+ installer.generate_bundler_executable_stubs(spec, installer_opts)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/cli/cache.rb b/lib/bundler/cli/cache.rb
new file mode 100644
index 0000000000..59605df847
--- /dev/null
+++ b/lib/bundler/cli/cache.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Cache
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def run
+ Bundler.ui.level = "warn" if options[:quiet]
+ Bundler.settings.set_command_option_if_given :cache_path, options["cache-path"]
+
+ install
+
+ Bundler.settings.temporary(cache_all_platforms: options["all-platforms"]) do
+ Bundler.load.cache
+ end
+ end
+
+ private
+
+ def install
+ require_relative "install"
+ options = self.options.dup
+ options["local"] = false if Bundler.settings[:cache_all_platforms]
+ options["no-cache"] = true
+ Bundler::CLI::Install.new(options).run
+ end
+ end
+end
diff --git a/lib/bundler/cli/check.rb b/lib/bundler/cli/check.rb
new file mode 100644
index 0000000000..493eb3ec6a
--- /dev/null
+++ b/lib/bundler/cli/check.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Check
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def run
+ Bundler.settings.set_command_option_if_given :path, options[:path]
+
+ definition = Bundler.definition
+ definition.validate_runtime!
+
+ begin
+ definition.check!
+ not_installed = definition.missing_specs
+ rescue GemNotFound, GitError, SolveFailure
+ Bundler.ui.error "Bundler can't satisfy your Gemfile's dependencies."
+ Bundler.ui.warn "Install missing gems with `bundle install`."
+ exit 1
+ end
+
+ if not_installed.any?
+ Bundler.ui.error "The following gems are missing"
+ not_installed.each {|s| Bundler.ui.error " * #{s.name} (#{s.version})" }
+ Bundler.ui.warn "Install missing gems with `bundle install`"
+ exit 1
+ elsif !Bundler.default_lockfile.file? && Bundler.frozen_bundle?
+ Bundler.ui.error "This bundle has been frozen, but there is no #{SharedHelpers.relative_lockfile_path} present"
+ exit 1
+ else
+ definition.lock(true) unless options[:"dry-run"]
+ Bundler.ui.info "The Gemfile's dependencies are satisfied"
+ end
+ end
+ end
+end
diff --git a/lib/bundler/cli/clean.rb b/lib/bundler/cli/clean.rb
new file mode 100644
index 0000000000..c6b0968e3e
--- /dev/null
+++ b/lib/bundler/cli/clean.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Clean
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def run
+ require_path_or_force unless options[:"dry-run"]
+ Bundler.load.clean(options[:"dry-run"])
+ end
+
+ protected
+
+ def require_path_or_force
+ return unless Bundler.use_system_gems? && !options[:force]
+ raise InvalidOption, "Cleaning all the gems on your system is dangerous! " \
+ "If you're sure you want to remove every system gem not in this " \
+ "bundle, run `bundle clean --force`."
+ end
+ end
+end
diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb
new file mode 100644
index 0000000000..b44fbc3096
--- /dev/null
+++ b/lib/bundler/cli/common.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+module Bundler
+ module CLI::Common
+ def self.validate_cooldown!(value)
+ return if value.nil?
+ return if value.is_a?(Integer) && value >= 0
+ raise InvalidOption, "Expected `--cooldown` to be a non-negative integer, got #{value.inspect}"
+ end
+
+ def self.output_post_install_messages(messages)
+ return if Bundler.settings["ignore_messages"]
+ messages.to_a.each do |name, msg|
+ print_post_install_message(name, msg) unless Bundler.settings["ignore_messages.#{name}"]
+ end
+ end
+
+ def self.print_post_install_message(name, msg)
+ Bundler.ui.confirm "Post-install message from #{name}:"
+ Bundler.ui.info msg
+ end
+
+ def self.output_fund_metadata_summary
+ return if Bundler.settings["ignore_funding_requests"]
+ definition = Bundler.definition
+ current_dependencies = definition.requested_dependencies
+ current_specs = definition.specs
+
+ count = current_dependencies.count {|dep| current_specs[dep.name].first.metadata.key?("funding_uri") }
+
+ return if count.zero?
+
+ intro = count > 1 ? "#{count} installed gems you directly depend on are" : "#{count} installed gem you directly depend on is"
+ message = "#{intro} looking for funding.\n Run `bundle fund` for details"
+ Bundler.ui.info message
+ end
+
+ def self.output_without_groups_message(command)
+ return if Bundler.settings[:without].empty?
+ Bundler.ui.confirm without_groups_message(command)
+ end
+
+ def self.without_groups_message(command)
+ command_in_past_tense = command == :install ? "installed" : "updated"
+ groups = Bundler.settings[:without]
+ "Gems in the #{verbalize_groups(groups)} were not #{command_in_past_tense}."
+ end
+
+ def self.verbalize_groups(groups)
+ groups.map! {|g| "'#{g}'" }
+ group_list = [groups[0...-1].join(", "), groups[-1..-1]].
+ reject {|s| s.to_s.empty? }.join(" and ")
+ group_str = groups.size == 1 ? "group" : "groups"
+ "#{group_str} #{group_list}"
+ end
+
+ def self.select_spec(name, regex_match = nil)
+ specs = []
+ regexp = Regexp.new(name) if regex_match
+
+ Bundler.definition.specs.each do |spec|
+ return spec if spec.name == name
+ specs << spec if regexp && spec.name.match?(regexp)
+ end
+
+ default_spec = default_gem_spec(name)
+ specs << default_spec if default_spec
+
+ case specs.count
+ when 0
+ dep_in_other_group = Bundler.definition.current_dependencies.find {|dep|dep.name == name }
+
+ if dep_in_other_group
+ raise GemNotFound, "Could not find gem '#{name}', because it's in the #{verbalize_groups(dep_in_other_group.groups)}, configured to be ignored."
+ else
+ raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies)
+ end
+ when 1
+ specs.first
+ else
+ ask_for_spec_from(specs)
+ end
+ rescue RegexpError
+ raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies)
+ end
+
+ def self.default_gem_spec(name)
+ gem_spec = Gem::Specification.find_all_by_name(name).last
+ gem_spec if gem_spec&.default_gem?
+ end
+
+ def self.ask_for_spec_from(specs)
+ specs.each_with_index do |spec, index|
+ Bundler.ui.info "#{index.succ} : #{spec.name}", true
+ end
+ Bundler.ui.info "0 : - exit -", true
+
+ num = Bundler.ui.ask("> ").to_i
+ num > 0 ? specs[num - 1] : nil
+ end
+
+ def self.gem_not_found_message(missing_gem_name, alternatives)
+ message = "Could not find gem '#{missing_gem_name}'."
+ alternate_names = alternatives.map {|a| a.respond_to?(:name) ? a.name : a }
+ if alternate_names.include?(missing_gem_name.downcase)
+ message += "\nDid you mean '#{missing_gem_name.downcase}'?"
+ elsif defined?(DidYouMean::SpellChecker)
+ suggestions = DidYouMean::SpellChecker.new(dictionary: alternate_names).correct(missing_gem_name)
+ message += "\nDid you mean #{word_list(suggestions)}?" unless suggestions.empty?
+ end
+ message
+ end
+
+ def self.ensure_all_gems_in_lockfile!(names, locked_gems = Bundler.locked_gems)
+ return unless locked_gems
+
+ locked_names = locked_gems.specs.map(&:name).uniq
+ names.-(locked_names).each do |g|
+ raise GemNotFound, gem_not_found_message(g, locked_names)
+ end
+ end
+
+ def self.configure_gem_version_promoter(definition, options)
+ patch_level = patch_level_options(options)
+ patch_level << :patch if patch_level.empty? && Bundler.settings[:prefer_patch]
+ raise InvalidOption, "Provide only one of the following options: #{patch_level.join(", ")}" unless patch_level.length <= 1
+
+ definition.gem_version_promoter.tap do |gvp|
+ gvp.level = patch_level.first || :major
+ gvp.strict = options[:strict] || options["filter-strict"]
+ gvp.pre = options[:pre]
+ end
+ end
+
+ def self.patch_level_options(options)
+ [:major, :minor, :patch].select {|v| options.keys.include?(v.to_s) }
+ end
+
+ def self.clean_after_install?
+ clean = Bundler.settings[:clean]
+ return clean unless clean.nil?
+ clean ||= Bundler.feature_flag.bundler_5_mode? && Bundler.settings[:path].nil?
+ clean &&= !Bundler.use_system_gems?
+ clean
+ end
+
+ def self.word_list(words)
+ if words.empty?
+ return ""
+ end
+
+ words = words.map {|word| "'#{word}'" }
+
+ if words.length == 1
+ return words[0]
+ end
+
+ [words[0..-2].join(", "), words[-1]].join(" or ")
+ end
+ end
+end
diff --git a/lib/bundler/cli/config.rb b/lib/bundler/cli/config.rb
new file mode 100644
index 0000000000..976cda7484
--- /dev/null
+++ b/lib/bundler/cli/config.rb
@@ -0,0 +1,208 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Config < Thor
+ class_option :parseable, type: :boolean, banner: "Use minimal formatting for more parseable output"
+
+ def self.scope_options
+ method_option :global, type: :boolean, banner: "Only change the global config"
+ method_option :local, type: :boolean, banner: "Only change the local config"
+ end
+ private_class_method :scope_options
+
+ desc "base NAME [VALUE]", "The Bundler 1 config interface", hide: true
+ scope_options
+ method_option :delete, type: :boolean, banner: "delete"
+ def base(name = nil, *value)
+ new_args =
+ if ARGV.size == 1
+ ["config", "list"]
+ elsif ARGV.include?("--delete")
+ ARGV.map {|arg| arg == "--delete" ? "unset" : arg }
+ elsif ARGV.include?("--global") || ARGV.include?("--local") || ARGV.size == 3
+ ["config", "set", *ARGV[1..-1]]
+ else
+ ["config", "get", ARGV[1]]
+ end
+
+ message = "Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle #{new_args.join(" ")}` instead."
+ SharedHelpers.feature_deprecated! message
+
+ Base.new(options, name, value, self).run
+ end
+
+ desc "list", "List out all configured settings"
+ def list
+ Base.new(options, nil, nil, self).run
+ end
+
+ desc "get NAME", "Returns the value for the given key"
+ def get(name)
+ Base.new(options, name, nil, self).run
+ end
+
+ desc "set NAME VALUE", "Sets the given value for the given key"
+ scope_options
+ def set(name, value, *value_)
+ Base.new(options, name, value_.unshift(value), self).run
+ end
+
+ desc "unset NAME", "Unsets the value for the given key"
+ scope_options
+ def unset(name)
+ options[:delete] = true
+ Base.new(options, name, nil, self).run
+ end
+
+ default_task :base
+
+ class Base
+ attr_reader :name, :value, :options, :scope, :thor
+
+ def initialize(options, name, value, thor)
+ @options = options
+ @name = name
+ value = Array(value)
+ @value = value.empty? ? nil : value.join(" ")
+ @thor = thor
+ validate_scope!
+ end
+
+ def run
+ unless name
+ warn_unused_scope "Ignoring --#{scope}"
+ confirm_all
+ return
+ end
+
+ if options[:delete]
+ if !explicit_scope? || scope != "global"
+ Bundler.settings.set_local(name, nil)
+ end
+ if !explicit_scope? || scope != "local"
+ Bundler.settings.set_global(name, nil)
+ end
+ return
+ end
+
+ if value.nil?
+ warn_unused_scope "Ignoring --#{scope} since no value to set was given"
+ current_value = Bundler.settings[name]
+
+ if options[:parseable]
+ if value = Bundler.settings[name]
+ Bundler.ui.info("#{name}=#{value}")
+ end
+ else
+ confirm(name)
+ end
+
+ if current_value.nil?
+ exit 1
+ else
+ return
+ end
+ end
+
+ Bundler.ui.info(message) if message
+ Bundler.settings.send("set_#{scope}", name, new_value)
+ end
+
+ def confirm_all
+ if @options[:parseable]
+ thor.with_padding do
+ Bundler.settings.all.each do |setting|
+ val = Bundler.settings[setting]
+ Bundler.ui.info "#{setting}=#{val}"
+ end
+ end
+ else
+ Bundler.ui.confirm "Settings are listed in order of priority. The top value will be used.\n"
+ Bundler.settings.all.each do |setting|
+ Bundler.ui.confirm setting
+ show_pretty_values_for(setting)
+ Bundler.ui.confirm ""
+ end
+ end
+ end
+
+ def confirm(name)
+ Bundler.ui.confirm "Settings for `#{name}` in order of priority. The top value will be used"
+ show_pretty_values_for(name)
+ end
+
+ def new_value
+ pathname = Pathname.new(value)
+ if name.start_with?("local.") && pathname.directory?
+ pathname.expand_path.to_s
+ else
+ value
+ end
+ end
+
+ def message
+ locations = Bundler.settings.locations(name)
+ if @options[:parseable]
+ "#{name}=#{new_value}" if new_value
+ elsif scope == "global"
+ if !locations[:local].nil?
+ "Your application has set #{name} to #{locations[:local].inspect}. " \
+ "This will override the global value you are currently setting"
+ elsif locations[:env]
+ "You have a bundler environment variable for #{name} set to " \
+ "#{locations[:env].inspect}. This will take precedence over the global value you are setting"
+ elsif !locations[:global].nil? && locations[:global] != value
+ "You are replacing the current global value of #{name}, which is currently " \
+ "#{locations[:global].inspect}"
+ end
+ elsif scope == "local" && !locations[:local].nil? && locations[:local] != value
+ "You are replacing the current local value of #{name}, which is currently " \
+ "#{locations[:local].inspect}"
+ end
+ end
+
+ def show_pretty_values_for(setting)
+ thor.with_padding do
+ Bundler.settings.pretty_values_for(setting).each do |line|
+ Bundler.ui.info line
+ end
+ end
+ end
+
+ def explicit_scope?
+ @explicit_scope
+ end
+
+ def warn_unused_scope(msg)
+ return unless explicit_scope?
+ return if options[:parseable]
+
+ Bundler.ui.warn(msg)
+ end
+
+ def validate_scope!
+ @explicit_scope = true
+ scopes = %w[global local].select {|s| options[s] }
+ case scopes.size
+ when 0
+ @scope = inside_app? ? "local" : "global"
+ @explicit_scope = false
+ when 1
+ @scope = scopes.first
+ else
+ raise InvalidOption,
+ "The options #{scopes.join " and "} were specified. Please only use one of the switches at a time."
+ end
+ end
+
+ private
+
+ def inside_app?
+ Bundler.root
+ true
+ rescue GemfileNotFound
+ false
+ end
+ end
+ end
+end
diff --git a/lib/bundler/cli/console.rb b/lib/bundler/cli/console.rb
new file mode 100644
index 0000000000..2d1a2ce458
--- /dev/null
+++ b/lib/bundler/cli/console.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Console
+ attr_reader :options, :group
+ def initialize(options, group)
+ @options = options
+ @group = group
+ end
+
+ def run
+ group ? Bundler.require(:default, *group.split(" ").map!(&:to_sym)) : Bundler.require
+ ARGV.clear
+
+ console = get_console(Bundler.settings[:console] || "irb")
+ console.start
+ end
+
+ def get_console(name)
+ require name
+ get_constant(name)
+ rescue LoadError
+ if name == "irb"
+ if defined?(Gem::BUNDLED_GEMS) && Gem::BUNDLED_GEMS.respond_to?(:force_activate)
+ Gem::BUNDLED_GEMS.force_activate "irb"
+ require name
+ return get_constant(name)
+ end
+ Bundler.ui.error "#{name} is not available"
+ exit 1
+ else
+ Bundler.ui.error "Couldn't load console #{name}, falling back to irb"
+ name = "irb"
+ retry
+ end
+ end
+
+ def get_constant(name)
+ const_name = {
+ "pry" => :Pry,
+ "ripl" => :Ripl,
+ "irb" => :IRB,
+ }[name]
+ Object.const_get(const_name)
+ end
+ end
+end
diff --git a/lib/bundler/cli/doctor.rb b/lib/bundler/cli/doctor.rb
new file mode 100644
index 0000000000..5fd6a73d91
--- /dev/null
+++ b/lib/bundler/cli/doctor.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Doctor < Thor
+ default_command(:diagnose)
+
+ desc "diagnose [OPTIONS]", "Checks the bundle for common problems"
+ long_desc <<-D
+ Doctor scans the OS dependencies of each of the gems requested in the Gemfile. If
+ missing dependencies are detected, Bundler prints them and exits status 1.
+ Otherwise, Bundler prints a success message and exits with a status of 0.
+ D
+ method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
+ method_option "quiet", type: :boolean, banner: "Only output warnings and errors."
+ method_option "ssl", type: :boolean, default: false, banner: "Diagnose SSL problems."
+ def diagnose
+ require_relative "doctor/diagnose"
+ Diagnose.new(options).run
+ end
+
+ desc "ssl [OPTIONS]", "Diagnose SSL problems"
+ long_desc <<-D
+ Diagnose SSL problems, especially related to certificates or TLS version while connecting to https://rubygems.org.
+ D
+ method_option "host", type: :string, banner: "The host to diagnose."
+ method_option "tls-version", type: :string, banner: "Specify the SSL/TLS version when running the diagnostic. Accepts either <1.1> or <1.2>"
+ method_option "verify-mode", type: :string, banner: "Specify the mode used for certification verification. Accepts either <peer> or <none>"
+ def ssl
+ require_relative "doctor/ssl"
+ SSL.new(options).run
+ end
+ end
+end
diff --git a/lib/bundler/cli/doctor/diagnose.rb b/lib/bundler/cli/doctor/diagnose.rb
new file mode 100644
index 0000000000..a878025dda
--- /dev/null
+++ b/lib/bundler/cli/doctor/diagnose.rb
@@ -0,0 +1,167 @@
+# frozen_string_literal: true
+
+require "rbconfig"
+require "shellwords"
+
+module Bundler
+ class CLI::Doctor::Diagnose
+ DARWIN_REGEX = /\s+(.+) \(compatibility /
+ LDD_REGEX = /\t\S+ => (\S+) \(\S+\)/
+
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def otool_available?
+ Bundler.which("otool")
+ end
+
+ def ldd_available?
+ Bundler.which("ldd")
+ end
+
+ def dylibs_darwin(path)
+ output = `/usr/bin/otool -L #{path.shellescape}`.chomp
+ dylibs = output.split("\n")[1..-1].filter_map {|l| l.match(DARWIN_REGEX)&.match(1) }.uniq
+ # ignore @rpath and friends
+ dylibs.reject {|dylib| dylib.start_with? "@" }
+ end
+
+ def dylibs_ldd(path)
+ output = `/usr/bin/ldd #{path.shellescape}`.chomp
+ output.split("\n").filter_map do |l|
+ match = l.match(LDD_REGEX)
+ next if match.nil?
+ match.captures[0]
+ end
+ end
+
+ def dylibs(path)
+ case RbConfig::CONFIG["host_os"]
+ when /darwin/
+ return [] unless otool_available?
+ dylibs_darwin(path)
+ when /(linux|solaris|bsd)/
+ return [] unless ldd_available?
+ dylibs_ldd(path)
+ else # Windows, etc.
+ Bundler.ui.warn("Dynamic library check not supported on this platform.")
+ []
+ end
+ end
+
+ def bundles_for_gem(spec)
+ Dir.glob("#{spec.full_gem_path}/**/*.bundle")
+ end
+
+ def lookup_with_fiddle(path)
+ require "fiddle"
+ Fiddle.dlopen(path)
+ false
+ rescue Fiddle::DLError
+ true
+ end
+
+ def check!
+ require_relative "../check"
+ Bundler::CLI::Check.new({}).run
+ end
+
+ def diagnose_ssl
+ require_relative "ssl"
+ Bundler::CLI::Doctor::SSL.new({}).run
+ end
+
+ def run
+ Bundler.ui.level = "warn" if options[:quiet]
+ Bundler.settings.validate!
+ check!
+ diagnose_ssl if options[:ssl]
+
+ definition = Bundler.definition
+ broken_links = {}
+
+ definition.specs.each do |spec|
+ bundles_for_gem(spec).each do |bundle|
+ bad_paths = dylibs(bundle).select do |f|
+ lookup_with_fiddle(f)
+ end
+ if bad_paths.any?
+ broken_links[spec] ||= []
+ broken_links[spec].concat(bad_paths)
+ end
+ end
+ end
+
+ permissions_valid = check_home_permissions
+
+ if broken_links.any?
+ message = "The following gems are missing OS dependencies:"
+ broken_links.flat_map do |spec, paths|
+ paths.uniq.map do |path|
+ "\n * #{spec.name}: #{path}"
+ end
+ end.sort.each {|m| message += m }
+ raise ProductionError, message
+ elsif permissions_valid
+ Bundler.ui.info "No issues found with the installed bundle"
+ end
+ end
+
+ private
+
+ def check_home_permissions
+ require "find"
+ files_not_readable = []
+ files_not_readable_and_owned_by_different_user = []
+ files_not_owned_by_current_user_but_still_readable = []
+ broken_symlinks = []
+ Find.find(Bundler.bundle_path.to_s).each do |f|
+ if !File.exist?(f)
+ broken_symlinks << f
+ elsif !File.readable?(f)
+ if File.stat(f).uid != Process.uid
+ files_not_readable_and_owned_by_different_user << f
+ else
+ files_not_readable << f
+ end
+ elsif File.stat(f).uid != Process.uid
+ files_not_owned_by_current_user_but_still_readable << f
+ end
+ end
+
+ ok = true
+
+ if broken_symlinks.any?
+ Bundler.ui.warn "Broken links exist in the Bundler home. Please report them to the offending gem's upstream repo. These files are:\n - #{broken_symlinks.join("\n - ")}"
+
+ ok = false
+ end
+
+ if files_not_owned_by_current_user_but_still_readable.any?
+ Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \
+ "user, but are still readable. These files are:\n - #{files_not_owned_by_current_user_but_still_readable.join("\n - ")}"
+
+ ok = false
+ end
+
+ if files_not_readable_and_owned_by_different_user.any?
+ Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \
+ "user, and are not readable. These files are:\n - #{files_not_readable_and_owned_by_different_user.join("\n - ")}"
+
+ ok = false
+ end
+
+ if files_not_readable.any?
+ Bundler.ui.warn "Files exist in the Bundler home that are not " \
+ "readable by the current user. These files are:\n - #{files_not_readable.join("\n - ")}"
+
+ ok = false
+ end
+
+ ok
+ end
+ end
+end
diff --git a/lib/bundler/cli/doctor/ssl.rb b/lib/bundler/cli/doctor/ssl.rb
new file mode 100644
index 0000000000..21fc4edf2d
--- /dev/null
+++ b/lib/bundler/cli/doctor/ssl.rb
@@ -0,0 +1,249 @@
+# frozen_string_literal: true
+
+require "rubygems/remote_fetcher"
+require "uri"
+
+module Bundler
+ class CLI::Doctor::SSL
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def run
+ return unless openssl_installed?
+
+ output_ssl_environment
+ bundler_success = bundler_connection_successful?
+ rubygem_success = rubygem_connection_successful?
+
+ return unless net_http_connection_successful?
+
+ Explanation.summarize(bundler_success, rubygem_success, host)
+ end
+
+ private
+
+ def host
+ @options[:host] || "rubygems.org"
+ end
+
+ def tls_version
+ @options[:"tls-version"].then do |version|
+ "TLS#{version.sub(".", "_")}".to_sym if version
+ end
+ end
+
+ def verify_mode
+ mode = @options[:"verify-mode"] || :peer
+
+ @verify_mode ||= mode.then {|mod| OpenSSL::SSL.const_get("verify_#{mod}".upcase) }
+ end
+
+ def uri
+ @uri ||= URI("https://#{host}")
+ end
+
+ def openssl_installed?
+ require "openssl"
+
+ true
+ rescue LoadError
+ Bundler.ui.warn(<<~MSG)
+ Oh no! Your Ruby doesn't have OpenSSL, so it can't connect to #{host}.
+ You'll need to recompile or reinstall Ruby with OpenSSL support and try again.
+ MSG
+
+ false
+ end
+
+ def output_ssl_environment
+ Bundler.ui.info(<<~MESSAGE)
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+ MESSAGE
+ end
+
+ def bundler_connection_successful?
+ Bundler.ui.info("\nTrying connections to #{uri}:\n")
+
+ bundler_uri = Gem::URI(uri.to_s)
+ Bundler::Fetcher.new(
+ Bundler::Source::Rubygems::Remote.new(bundler_uri)
+ ).send(:connection).request(bundler_uri)
+
+ Bundler.ui.info("Bundler: success")
+
+ true
+ rescue StandardError => error
+ Bundler.ui.warn("Bundler: failed (#{Explanation.explain_bundler_or_rubygems_error(error)})")
+
+ false
+ end
+
+ def rubygem_connection_successful?
+ Gem::RemoteFetcher.fetcher.fetch_path(uri)
+ Bundler.ui.info("RubyGems: success")
+
+ true
+ rescue StandardError => error
+ Bundler.ui.warn("RubyGems: failed (#{Explanation.explain_bundler_or_rubygems_error(error)})")
+
+ false
+ end
+
+ def net_http_connection_successful?
+ ::Gem::Net::HTTP.new(uri.host, uri.port).tap do |http|
+ http.use_ssl = true
+ http.min_version = tls_version
+ http.max_version = tls_version
+ http.verify_mode = verify_mode
+ end.start
+
+ Bundler.ui.info("Ruby net/http: success")
+ warn_on_unsupported_tls12
+
+ true
+ rescue StandardError => error
+ Bundler.ui.warn(<<~MSG)
+ Ruby net/http: failed
+
+ Unfortunately, this Ruby can't connect to #{host}.
+
+ #{Explanation.explain_net_http_error(error, host, tls_version)}
+ MSG
+
+ false
+ end
+
+ def warn_on_unsupported_tls12
+ ctx = OpenSSL::SSL::SSLContext.new
+ supported = true
+
+ if ctx.respond_to?(:min_version=)
+ begin
+ ctx.min_version = ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
+ rescue OpenSSL::SSL::SSLError, NameError
+ supported = false
+ end
+ else
+ supported = OpenSSL::SSL::SSLContext::METHODS.include?(:TLSv1_2) # rubocop:disable Naming/VariableNumber
+ end
+
+ Bundler.ui.warn(<<~EOM) unless supported
+
+ WARNING: Although your Ruby can connect to #{host} today, your OpenSSL is very old!
+ WARNING: You will need to upgrade OpenSSL to use #{host}.
+
+ EOM
+ end
+
+ module Explanation
+ extend self
+
+ def explain_bundler_or_rubygems_error(error)
+ case error.message
+ when /certificate verify failed/
+ "certificate verification"
+ when /read server hello A/
+ "SSL/TLS protocol version mismatch"
+ when /tlsv1 alert protocol version/
+ "requested TLS version is too old"
+ else
+ error.message
+ end
+ end
+
+ def explain_net_http_error(error, host, tls_version)
+ case error.message
+ # Check for certificate errors
+ when /certificate verify failed/
+ <<~MSG
+ #{show_ssl_certs}
+ Your Ruby can't connect to #{host} because you are missing the certificate files OpenSSL needs to verify you are connecting to the genuine #{host} servers.
+ MSG
+ # Check for TLS version errors
+ when /read server hello A/, /tlsv1 alert protocol version/
+ if tls_version.to_s == "TLS1_3"
+ "Your Ruby can't connect to #{host} because #{tls_version} isn't supported yet.\n"
+ else
+ <<~MSG
+ Your Ruby can't connect to #{host} because your version of OpenSSL is too old.
+ You'll need to upgrade your OpenSSL install and/or recompile Ruby to use a newer OpenSSL.
+ MSG
+ end
+ # OpenSSL doesn't support TLS version specified by argument
+ when /unknown SSL method/
+ "Your Ruby can't connect because #{tls_version} isn't supported by your version of OpenSSL."
+ else
+ <<~MSG
+ Even worse, we're not sure why.
+
+ Here's the full error information:
+ #{error.class}: #{error.message}
+ #{error.backtrace.join("\n ")}
+
+ You might have more luck using Mislav's SSL doctor.rb script. You can get it here:
+ https://github.com/mislav/ssl-tools/blob/8b3dec4/doctor.rb
+
+ Read more about the script and how to use it in this blog post:
+ https://mislav.net/2013/07/ruby-openssl/
+ MSG
+ end
+ end
+
+ def summarize(bundler_success, rubygems_success, host)
+ guide_url = "http://ruby.to/ssl-check-failed"
+
+ message = if bundler_success && rubygems_success
+ <<~MSG
+ Hooray! This Ruby can connect to #{host}.
+ You are all set to use Bundler and RubyGems.
+
+ MSG
+ elsif !bundler_success && !rubygems_success
+ <<~MSG
+ For some reason, your Ruby installation can connect to #{host}, but neither RubyGems nor Bundler can.
+ The most likely fix is to manually upgrade RubyGems by following the instructions at #{guide_url}.
+ After you've done that, run `gem install bundler` to upgrade Bundler, and then run this script again to make sure everything worked. â£
+
+ MSG
+ elsif !bundler_success
+ <<~MSG
+ Although your Ruby installation and RubyGems can both connect to #{host}, Bundler is having trouble.
+ The most likely way to fix this is to upgrade Bundler by running `gem install bundler`.
+ Run this script again after doing that to make sure everything is all set.
+ If you're still having trouble, check out the troubleshooting guide at #{guide_url}.
+
+ MSG
+ else
+ <<~MSG
+ It looks like Ruby and Bundler can connect to #{host}, but RubyGems itself cannot.
+ You can likely solve this by manually downloading and installing a RubyGems update.
+ Visit #{guide_url} for instructions on how to manually upgrade RubyGems.
+
+ MSG
+ end
+
+ Bundler.ui.info("\n#{message}")
+ end
+
+ private
+
+ def show_ssl_certs
+ ssl_cert_file = ENV["SSL_CERT_FILE"] || OpenSSL::X509::DEFAULT_CERT_FILE
+ ssl_cert_dir = ENV["SSL_CERT_DIR"] || OpenSSL::X509::DEFAULT_CERT_DIR
+
+ <<~MSG
+ Below affect only Ruby net/http connections:
+ SSL_CERT_FILE: #{File.exist?(ssl_cert_file) ? "exists #{ssl_cert_file}" : "is missing #{ssl_cert_file}"}
+ SSL_CERT_DIR: #{Dir.exist?(ssl_cert_dir) ? "exists #{ssl_cert_dir}" : "is missing #{ssl_cert_dir}"}
+ MSG
+ end
+ end
+ end
+end
diff --git a/lib/bundler/cli/exec.rb b/lib/bundler/cli/exec.rb
new file mode 100644
index 0000000000..2fdc416286
--- /dev/null
+++ b/lib/bundler/cli/exec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require_relative "../current_ruby"
+
+module Bundler
+ class CLI::Exec
+ attr_reader :options, :args, :cmd
+
+ TRAPPED_SIGNALS = %w[INT].freeze
+
+ def initialize(options, args)
+ @options = options
+ @cmd = args.shift
+ @args = args
+ @args << { close_others: !options.keep_file_descriptors? } unless Bundler.current_ruby.jruby?
+ end
+
+ def run
+ validate_cmd!
+ SharedHelpers.set_bundle_environment
+ if bin_path = Bundler.which(cmd)
+ if !Bundler.settings[:disable_exec_load] && directly_loadable?(bin_path)
+ bin_path.delete_suffix!(".bat") if Gem.win_platform?
+ kernel_load(bin_path, *args)
+ else
+ bin_path = "./" + bin_path unless File.absolute_path?(bin_path)
+ kernel_exec(bin_path, *args)
+ end
+ else
+ # exec using the given command
+ kernel_exec(cmd, *args)
+ end
+ end
+
+ private
+
+ def validate_cmd!
+ return unless cmd.nil?
+ Bundler.ui.error "bundler: exec needs a command to run"
+ exit 128
+ end
+
+ def kernel_exec(*args)
+ Kernel.exec(*args)
+ rescue Errno::EACCES, Errno::ENOEXEC
+ Bundler.ui.error "bundler: not executable: #{cmd}"
+ exit 126
+ rescue Errno::ENOENT
+ Bundler.ui.error "bundler: command not found: #{cmd}"
+ Bundler.ui.warn "Install missing gem executables with `bundle install`"
+ exit 127
+ end
+
+ def kernel_load(file, *args)
+ args.pop if args.last.is_a?(Hash)
+ ARGV.replace(args)
+ $0 = file
+ Process.setproctitle(process_title(file, args)) if Process.respond_to?(:setproctitle)
+ require_relative "../setup"
+ TRAPPED_SIGNALS.each {|s| trap(s, "DEFAULT") }
+ Kernel.load(file)
+ rescue SystemExit, SignalException
+ raise
+ rescue Exception # rubocop:disable Lint/RescueException
+ Bundler.ui.error "bundler: failed to load command: #{cmd} (#{file})"
+ Bundler::FriendlyErrors.disable!
+ raise
+ end
+
+ def process_title(file, args)
+ "#{file} #{args.join(" ")}".strip
+ end
+
+ def directly_loadable?(file)
+ if Gem.win_platform?
+ script_wrapper?(file)
+ else
+ ruby_shebang?(file)
+ end
+ end
+
+ def script_wrapper?(file)
+ script_file = file.delete_suffix(".bat")
+ return false unless File.exist?(script_file)
+
+ if File.zero?(script_file)
+ Bundler.ui.warn "#{script_file} is empty"
+ return false
+ end
+
+ header = File.open(file, "r") {|f| f.read(32) }
+ ruby_exe = "#{RbConfig::CONFIG["RUBY_INSTALL_NAME"]}#{RbConfig::CONFIG["EXEEXT"]}"
+ ruby_exe = "ruby.exe" if ruby_exe.empty?
+ header.include?(ruby_exe)
+ end
+
+ def ruby_shebang?(file)
+ possibilities = [
+ "#!/usr/bin/env ruby\n",
+ "#!/usr/bin/env jruby\n",
+ "#!/usr/bin/env truffleruby\n",
+ "#!#{Gem.ruby}\n",
+ ]
+
+ if File.zero?(file)
+ Bundler.ui.warn "#{file} is empty"
+ return false
+ end
+
+ first_line = File.open(file, "rb") {|f| f.read(possibilities.map(&:size).max) }
+ possibilities.any? {|shebang| first_line.start_with?(shebang) }
+ end
+ end
+end
diff --git a/lib/bundler/cli/fund.rb b/lib/bundler/cli/fund.rb
new file mode 100644
index 0000000000..ad7f31f3d6
--- /dev/null
+++ b/lib/bundler/cli/fund.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Fund
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def run
+ Bundler.definition.validate_runtime!
+
+ groups = Array(options[:group]).map(&:to_sym)
+
+ deps = if groups.any?
+ Bundler.definition.dependencies_for(groups)
+ else
+ Bundler.definition.requested_dependencies
+ end
+
+ fund_info = deps.each_with_object([]) do |dep, arr|
+ spec = Bundler.definition.specs[dep.name].first
+ if spec.metadata.key?("funding_uri")
+ arr << "* #{spec.name} (#{spec.version})\n Funding: #{spec.metadata["funding_uri"]}"
+ end
+ end
+
+ if fund_info.empty?
+ Bundler.ui.info "None of the installed gems you directly depend on are looking for funding."
+ else
+ Bundler.ui.info fund_info.join("\n")
+ end
+ end
+ end
+end
diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb
new file mode 100644
index 0000000000..c8c24c8e66
--- /dev/null
+++ b/lib/bundler/cli/gem.rb
@@ -0,0 +1,476 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI
+ Bundler.require_thor_actions
+ include Thor::Actions
+ end
+
+ class CLI::Gem
+ DEFAULT_GITHUB_USERNAME = "[USERNAME]"
+
+ attr_reader :options, :gem_name, :thor, :name, :target, :extension
+
+ def initialize(options, gem_name, thor)
+ @options = options
+ @gem_name = resolve_name(gem_name)
+
+ @thor = thor
+ thor.behavior = :invoke
+ thor.destination_root = nil
+
+ @name = @gem_name
+ @target = Pathname.new(SharedHelpers.pwd).join(gem_name)
+
+ @extension = options[:ext]
+
+ validate_ext_name if @extension
+ end
+
+ def run
+ Bundler.ui.confirm "Creating gem '#{name}'..."
+
+ underscored_name = name.tr("-", "_")
+ namespaced_path = name.tr("-", "/")
+ constant_name = name.gsub(/-[_-]*(?![_-]|$)/) { "::" }.gsub(/([_-]+|(::)|^)(.|$)/) { $2.to_s + $3.upcase }
+ constant_array = constant_name.split("::")
+ minitest_constant_name = constant_array.clone.tap {|a| a[-1] = "Test#{a[-1]}" }.join("::") # Foo::Bar => Foo::TestBar
+
+ use_git = Bundler.git_present? && options[:git]
+
+ git_author_name = use_git ? `git config user.name`.chomp : ""
+ git_username = use_git ? `git config github.user`.chomp : ""
+ git_user_email = use_git ? `git config user.email`.chomp : ""
+ github_username = github_username(git_username)
+
+ if github_username.empty?
+ homepage_uri = "TODO: Put your gem's website or public repo URL here."
+ source_code_uri = "TODO: Put your gem's public repo URL here."
+ changelog_uri = "TODO: Put your gem's CHANGELOG.md URL here."
+ else
+ homepage_uri = "https://github.com/#{github_username}/#{name}"
+ source_code_uri = "https://github.com/#{github_username}/#{name}"
+ changelog_uri = "https://github.com/#{github_username}/#{name}/blob/main/CHANGELOG.md"
+ end
+
+ config = {
+ name: name,
+ underscored_name: underscored_name,
+ namespaced_path: namespaced_path,
+ makefile_path: "#{underscored_name}/#{underscored_name}",
+ constant_name: constant_name,
+ constant_array: constant_array,
+ author: git_author_name.empty? ? "TODO: Write your name" : git_author_name,
+ email: git_user_email.empty? ? "TODO: Write your email address" : git_user_email,
+ test: options[:test],
+ ext: extension,
+ exe: options[:exe],
+ bundle: options[:bundle],
+ bundler_version: bundler_dependency_version,
+ git: use_git,
+ github_username: github_username.empty? ? DEFAULT_GITHUB_USERNAME : github_username,
+ required_ruby_version: required_ruby_version,
+ rust_builder_required_rubygems_version: rust_builder_required_rubygems_version,
+ minitest_constant_name: minitest_constant_name,
+ ignore_paths: %w[bin/],
+ homepage_uri: homepage_uri,
+ source_code_uri: source_code_uri,
+ changelog_uri: changelog_uri,
+ }
+ ensure_safe_gem_name(name, constant_array)
+
+ templates = {
+ "Gemfile.tt" => Bundler.preferred_gemfile_name,
+ "lib/newgem.rb.tt" => "lib/#{namespaced_path}.rb",
+ "lib/newgem/version.rb.tt" => "lib/#{namespaced_path}/version.rb",
+ "sig/newgem.rbs.tt" => "sig/#{namespaced_path}.rbs",
+ "newgem.gemspec.tt" => "#{name}.gemspec",
+ "Rakefile.tt" => "Rakefile",
+ "README.md.tt" => "README.md",
+ "bin/console.tt" => "bin/console",
+ "bin/setup.tt" => "bin/setup",
+ }
+
+ executables = %w[
+ bin/console
+ bin/setup
+ ]
+
+ case Bundler.preferred_gemfile_name
+ when "Gemfile"
+ config[:ignore_paths] << "Gemfile"
+ when "gems.rb"
+ config[:ignore_paths] << "gems.rb"
+ config[:ignore_paths] << "gems.locked"
+ end
+
+ if use_git
+ templates.merge!("gitignore.tt" => ".gitignore")
+ config[:ignore_paths] << ".gitignore"
+ end
+
+ if test_framework = ask_and_set_test_framework
+ config[:test] = test_framework
+
+ case test_framework
+ when "rspec"
+ templates.merge!(
+ "rspec.tt" => ".rspec",
+ "spec/spec_helper.rb.tt" => "spec/spec_helper.rb",
+ "spec/newgem_spec.rb.tt" => "spec/#{namespaced_path}_spec.rb"
+ )
+ config[:test_task] = :spec
+ config[:ignore_paths] << ".rspec"
+ config[:ignore_paths] << "spec/"
+ when "minitest"
+ # Generate path for minitest target file (FileList["test/**/test_*.rb"])
+ # foo => test/test_foo.rb
+ # foo-bar => test/foo/test_bar.rb
+ # foo_bar => test/test_foo_bar.rb
+ paths = namespaced_path.rpartition("/")
+ paths[2] = "test_#{paths[2]}"
+ minitest_namespaced_path = paths.join("")
+
+ templates.merge!(
+ "test/minitest/test_helper.rb.tt" => "test/test_helper.rb",
+ "test/minitest/test_newgem.rb.tt" => "test/#{minitest_namespaced_path}.rb"
+ )
+ config[:test_task] = :test
+ config[:ignore_paths] << "test/"
+ when "test-unit"
+ templates.merge!(
+ "test/test-unit/test_helper.rb.tt" => "test/test_helper.rb",
+ "test/test-unit/newgem_test.rb.tt" => "test/#{namespaced_path}_test.rb"
+ )
+ config[:test_task] = :test
+ config[:ignore_paths] << "test/"
+ end
+ end
+
+ config[:ci] = ask_and_set_ci
+ case config[:ci]
+ when "github"
+ templates.merge!("github/workflows/main.yml.tt" => ".github/workflows/main.yml")
+ if extension == "rust"
+ templates.merge!("github/workflows/build-gems.yml.tt" => ".github/workflows/build-gems.yml")
+ end
+ config[:ignore_paths] << ".github/"
+ when "gitlab"
+ templates.merge!("gitlab-ci.yml.tt" => ".gitlab-ci.yml")
+ config[:ignore_paths] << ".gitlab-ci.yml"
+ when "circle"
+ templates.merge!("circleci/config.yml.tt" => ".circleci/config.yml")
+ config[:ignore_paths] << ".circleci/"
+ end
+
+ if ask_and_set(:mit, "Do you want to license your code permissively under the MIT license?",
+ "Using a MIT license means that any other developer or company will be legally allowed " \
+ "to use your code for free as long as they admit you created it. You can read more about " \
+ "the MIT license at https://choosealicense.com/licenses/mit.")
+ config[:mit] = true
+ Bundler.ui.info "MIT License enabled in config"
+ templates.merge!("LICENSE.txt.tt" => "LICENSE.txt")
+ end
+
+ if ask_and_set(:coc, "Do you want to include a code of conduct in gems you generate?",
+ "Codes of conduct can increase contributions to your project by contributors who " \
+ "prefer safe, respectful, productive, and collaborative spaces. \n" \
+ "See https://github.com/ruby/rubygems/blob/master/CODE_OF_CONDUCT.md")
+ config[:coc] = true
+ Bundler.ui.info "Code of conduct enabled in config"
+ templates.merge!("CODE_OF_CONDUCT.md.tt" => "CODE_OF_CONDUCT.md")
+ end
+
+ if ask_and_set(:changelog, "Do you want to include a changelog?",
+ "A changelog is a file which contains a curated, chronologically ordered list of notable " \
+ "changes for each version of a project. To make it easier for users and contributors to" \
+ " see precisely what notable changes have been made between each release (or version) of" \
+ " the project. Whether consumers or developers, the end users of software are" \
+ " human beings who care about what's in the software. When the software changes, people " \
+ "want to know why and how. see https://keepachangelog.com")
+ config[:changelog] = true
+ Bundler.ui.info "Changelog enabled in config"
+ templates.merge!("CHANGELOG.md.tt" => "CHANGELOG.md")
+ end
+
+ config[:linter] = ask_and_set_linter
+ case config[:linter]
+ when "rubocop"
+ Bundler.ui.info "RuboCop enabled in config"
+ templates.merge!("rubocop.yml.tt" => ".rubocop.yml")
+ config[:ignore_paths] << ".rubocop.yml"
+ when "standard"
+ Bundler.ui.info "Standard enabled in config"
+ templates.merge!("standard.yml.tt" => ".standard.yml")
+ config[:ignore_paths] << ".standard.yml"
+ end
+
+ if config[:exe]
+ templates.merge!("exe/newgem.tt" => "exe/#{name}")
+ executables.push("exe/#{name}")
+ end
+
+ if extension == "c"
+ templates.merge!(
+ "ext/newgem/extconf-c.rb.tt" => "ext/#{name}/extconf.rb",
+ "ext/newgem/newgem.h.tt" => "ext/#{name}/#{underscored_name}.h",
+ "ext/newgem/newgem.c.tt" => "ext/#{name}/#{underscored_name}.c"
+ )
+ end
+
+ if extension == "rust"
+ templates.merge!(
+ "Cargo.toml.tt" => "Cargo.toml",
+ "ext/newgem/Cargo.toml.tt" => "ext/#{name}/Cargo.toml",
+ "ext/newgem/build.rs.tt" => "ext/#{name}/build.rs",
+ "ext/newgem/extconf-rust.rb.tt" => "ext/#{name}/extconf.rb",
+ "ext/newgem/src/lib.rs.tt" => "ext/#{name}/src/lib.rs",
+ )
+ end
+
+ if extension == "go"
+ templates.merge!(
+ "ext/newgem/go.mod.tt" => "ext/#{name}/go.mod",
+ "ext/newgem/extconf-go.rb.tt" => "ext/#{name}/extconf.rb",
+ "ext/newgem/newgem.h.tt" => "ext/#{name}/#{underscored_name}.h",
+ "ext/newgem/newgem.go.tt" => "ext/#{name}/#{underscored_name}.go",
+ "ext/newgem/newgem-go.c.tt" => "ext/#{name}/#{underscored_name}.c",
+ )
+
+ config[:go_module_username] = config[:github_username] == DEFAULT_GITHUB_USERNAME ? "username" : config[:github_username]
+ end
+
+ if target.exist? && !target.directory?
+ Bundler.ui.error "Couldn't create a new gem named `#{gem_name}` because there's an existing file named `#{gem_name}`."
+ exit Bundler::BundlerError.all_errors[Bundler::GenericSystemCallError]
+ end
+
+ if use_git
+ Bundler.ui.info "\nInitializing git repo in #{target}"
+ require "shellwords"
+ `git init #{target.to_s.shellescape}`
+
+ config[:git_default_branch] = File.read("#{target}/.git/HEAD").split("/").last.chomp
+ end
+
+ templates.each do |src, dst|
+ destination = target.join(dst)
+ thor.template("newgem/#{src}", destination, config)
+ end
+
+ executables.each do |file|
+ path = target.join(file)
+ executable = (path.stat.mode | 0o111)
+ path.chmod(executable)
+ end
+
+ if use_git
+ IO.popen(%w[git add .], { chdir: target }, &:read)
+ end
+
+ if config[:bundle]
+ Bundler.ui.info "Running bundle install in the new gem directory."
+ Dir.chdir(target) do
+ system("bundle install")
+ end
+ end
+
+ # Open gemspec in editor
+ open_editor(options["edit"], target.join("#{name}.gemspec")) if options[:edit]
+
+ Bundler.ui.info "\nGem '#{name}' was successfully created. " \
+ "For more information on making a RubyGem visit https://guides.rubygems.org/make-your-own-gem/"
+ end
+
+ private
+
+ def resolve_name(name)
+ Pathname.new(SharedHelpers.pwd).join(name).basename.to_s
+ end
+
+ def ask_and_set(key, prompt, explanation)
+ choice = options[key]
+ choice = Bundler.settings["gem.#{key}"] if choice.nil?
+
+ if choice.nil?
+ Bundler.ui.info "\n#{explanation}"
+ choice = Bundler.ui.yes? "#{prompt} y/(n):"
+ Bundler.settings.set_global("gem.#{key}", choice)
+ end
+
+ choice
+ end
+
+ def validate_ext_name
+ return unless gem_name.index("-")
+
+ Bundler.ui.error "You have specified a gem name which does not conform to the \n" \
+ "naming guidelines for C extensions. For more information, \n" \
+ "see the 'Extension Naming' section at the following URL:\n" \
+ "https://guides.rubygems.org/gems-with-extensions/\n"
+ exit 1
+ end
+
+ def ask_and_set_test_framework
+ return if skip?(:test)
+ test_framework = options[:test] || Bundler.settings["gem.test"]
+
+ if test_framework.to_s.empty?
+ Bundler.ui.info "\nDo you want to generate tests with your gem?"
+ Bundler.ui.info hint_text("test")
+
+ result = Bundler.ui.ask "Enter a test framework. rspec/minitest/test-unit/(none):"
+ if /rspec|minitest|test-unit/.match?(result)
+ test_framework = result
+ else
+ test_framework = false
+ end
+ end
+
+ if Bundler.settings["gem.test"].nil?
+ Bundler.settings.set_global("gem.test", test_framework)
+ end
+
+ if options[:test] == Bundler.settings["gem.test"]
+ Bundler.ui.info "#{options[:test]} is already configured, ignoring --test flag."
+ end
+
+ test_framework
+ end
+
+ def skip?(option)
+ options.key?(option) && options[option].nil?
+ end
+
+ def hint_text(setting)
+ if Bundler.settings["gem.#{setting}"] == false
+ "Your choice will only be applied to this gem."
+ else
+ "Future `bundle gem` calls will use your choice. " \
+ "This setting can be changed anytime with `bundle config gem.#{setting}`."
+ end
+ end
+
+ def ask_and_set_ci
+ return if skip?(:ci)
+ ci_template = options[:ci] || Bundler.settings["gem.ci"]
+
+ if ci_template.to_s.empty?
+ Bundler.ui.info "\nDo you want to set up continuous integration for your gem? " \
+ "Supported services:\n" \
+ "* CircleCI: https://circleci.com/\n" \
+ "* GitHub Actions: https://github.com/features/actions\n" \
+ "* GitLab CI: https://docs.gitlab.com/ee/ci/\n"
+ Bundler.ui.info hint_text("ci")
+
+ result = Bundler.ui.ask "Enter a CI service. github/gitlab/circle/(none):"
+ if /github|gitlab|circle/.match?(result)
+ ci_template = result
+ else
+ ci_template = false
+ end
+ end
+
+ if Bundler.settings["gem.ci"].nil?
+ Bundler.settings.set_global("gem.ci", ci_template)
+ end
+
+ if options[:ci] == Bundler.settings["gem.ci"]
+ Bundler.ui.info "#{options[:ci]} is already configured, ignoring --ci flag."
+ end
+
+ ci_template
+ end
+
+ def ask_and_set_linter
+ return if skip?(:linter)
+ linter_template = options[:linter] || Bundler.settings["gem.linter"]
+
+ if linter_template.to_s.empty?
+ Bundler.ui.info "\nDo you want to add a code linter and formatter to your gem? " \
+ "Supported Linters:\n" \
+ "* RuboCop: https://rubocop.org\n" \
+ "* Standard: https://github.com/standardrb/standard\n"
+ Bundler.ui.info hint_text("linter")
+
+ result = Bundler.ui.ask "Enter a linter. rubocop/standard/(none):"
+ if /rubocop|standard/.match?(result)
+ linter_template = result
+ else
+ linter_template = false
+ end
+ end
+
+ if Bundler.settings["gem.linter"].nil?
+ Bundler.settings.set_global("gem.linter", linter_template)
+ end
+
+ # Once gem.linter safely set, unset the deprecated gem.rubocop
+ unless Bundler.settings["gem.rubocop"].nil?
+ Bundler.settings.set_global("gem.rubocop", nil)
+ end
+
+ if options[:linter] == Bundler.settings["gem.linter"]
+ Bundler.ui.info "#{options[:linter]} is already configured, ignoring --linter flag."
+ end
+
+ linter_template
+ end
+
+ def bundler_dependency_version
+ v = Gem::Version.new(Bundler::VERSION)
+ req = v.segments[0..1]
+ req << "a" if v.prerelease?
+ req.join(".")
+ end
+
+ def ensure_safe_gem_name(name, constant_array)
+ if /^\d/.match?(name)
+ Bundler.ui.error "Invalid gem name #{name} Please give a name which does not start with numbers."
+ exit 1
+ end
+
+ if /[A-Z]/.match?(name)
+ Bundler.ui.warn "Gem names with capital letters are not recommended. Please use only lowercase letters, numbers, and hyphens."
+ end
+
+ constant_name = constant_array.join("::")
+
+ existing_constant = constant_array.inject(Object) do |c, s|
+ defined = begin
+ c.const_defined?(s)
+ rescue NameError
+ Bundler.ui.error "Invalid gem name #{name} -- `#{constant_name}` is an invalid constant name"
+ exit 1
+ end
+ (defined && c.const_get(s)) || break
+ end
+
+ return unless existing_constant
+ Bundler.ui.error "Invalid gem name #{name} constant #{constant_name} is already in use. Please choose another gem name."
+ exit 1
+ end
+
+ def open_editor(editor, file)
+ thor.run(%(#{editor} "#{file}"))
+ end
+
+ def rust_builder_required_rubygems_version
+ "3.3.11"
+ end
+
+ def required_ruby_version
+ "3.2.0"
+ end
+
+ def github_username(git_username)
+ if options[:github_username].nil?
+ git_username
+ elsif options[:github_username] == false
+ ""
+ else
+ options[:github_username]
+ end
+ end
+ end
+end
diff --git a/lib/bundler/cli/info.rb b/lib/bundler/cli/info.rb
new file mode 100644
index 0000000000..cd01d4949b
--- /dev/null
+++ b/lib/bundler/cli/info.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Info
+ attr_reader :gem_name, :options
+ def initialize(options, gem_name)
+ @options = options
+ @gem_name = gem_name
+ end
+
+ def run
+ Bundler.ui.silence do
+ Bundler.definition.validate_runtime!
+ Bundler.load.lock
+ end
+
+ spec = spec_for_gem(gem_name)
+
+ if spec
+ return print_gem_path(spec) if @options[:path]
+ return print_gem_version(spec) if @options[:version]
+ print_gem_info(spec)
+ end
+ end
+
+ private
+
+ def spec_for_gem(name)
+ Bundler::CLI::Common.select_spec(name, :regex_match)
+ end
+
+ def print_gem_version(spec)
+ Bundler.ui.info spec.version.to_s
+ end
+
+ def print_gem_path(spec)
+ name = spec.name
+ if name == "bundler"
+ path = File.expand_path("../../..", __dir__)
+ else
+ path = spec.full_gem_path
+ if spec.installation_missing?
+ return Bundler.ui.warn "The gem #{name} is missing. It should be installed at #{path}, but was not found"
+ end
+ end
+
+ Bundler.ui.info path
+ end
+
+ def print_gem_info(spec)
+ metadata = spec.metadata
+ name = spec.name
+ gem_info = String.new
+ gem_info << " * #{name} (#{spec.version}#{spec.git_version})\n"
+ gem_info << "\tSummary: #{spec.summary}\n" if spec.summary
+ gem_info << "\tHomepage: #{spec.homepage}\n" if spec.homepage
+ gem_info << "\tDocumentation: #{metadata["documentation_uri"]}\n" if metadata.key?("documentation_uri")
+ gem_info << "\tSource Code: #{metadata["source_code_uri"]}\n" if metadata.key?("source_code_uri")
+ gem_info << "\tFunding: #{metadata["funding_uri"]}\n" if metadata.key?("funding_uri")
+ gem_info << "\tWiki: #{metadata["wiki_uri"]}\n" if metadata.key?("wiki_uri")
+ gem_info << "\tChangelog: #{metadata["changelog_uri"]}\n" if metadata.key?("changelog_uri")
+ gem_info << "\tBug Tracker: #{metadata["bug_tracker_uri"]}\n" if metadata.key?("bug_tracker_uri")
+ gem_info << "\tMailing List: #{metadata["mailing_list_uri"]}\n" if metadata.key?("mailing_list_uri")
+ gem_info << "\tPath: #{spec.full_gem_path}\n"
+ gem_info << "\tDefault Gem: yes\n" if spec.respond_to?(:default_gem?) && spec.default_gem?
+ gem_info << "\tReverse Dependencies: \n\t\t#{gem_dependencies.join("\n\t\t")}" if gem_dependencies.any?
+
+ if name != "bundler" && spec.installation_missing?
+ return Bundler.ui.warn "The gem #{name} is missing. Gemspec information is still available though:\n#{gem_info}"
+ end
+
+ Bundler.ui.info gem_info
+ end
+
+ def gem_dependencies
+ @gem_dependencies ||= Bundler.definition.specs.filter_map do |spec|
+ dependency = spec.dependencies.find {|dep| dep.name == gem_name }
+ next unless dependency
+ "#{spec.name} (#{spec.version}) depends on #{gem_name} (#{dependency.requirements_list.join(", ")})"
+ end.sort
+ end
+ end
+end
diff --git a/lib/bundler/cli/init.rb b/lib/bundler/cli/init.rb
new file mode 100644
index 0000000000..246b9d6460
--- /dev/null
+++ b/lib/bundler/cli/init.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Init
+ attr_reader :options
+ def initialize(options)
+ @options = options
+ end
+
+ def run
+ if File.exist?(gemfile)
+ Bundler.ui.error "#{gemfile} already exists at #{File.expand_path(gemfile)}"
+ exit 1
+ end
+
+ unless File.writable?(Dir.pwd)
+ Bundler.ui.error "Can not create #{gemfile} as the current directory is not writable."
+ exit 1
+ end
+
+ if options[:gemspec]
+ gemspec = File.expand_path(options[:gemspec])
+ unless File.exist?(gemspec)
+ Bundler.ui.error "Gem specification #{gemspec} doesn't exist"
+ exit 1
+ end
+
+ spec = Bundler.load_gemspec_uncached(gemspec)
+
+ File.open(gemfile, "wb") do |file|
+ file << "# Generated from #{gemspec}\n"
+ file << spec.to_gemfile
+ end
+ else
+ File.open(File.expand_path("../templates/Gemfile", __dir__), "r") do |template|
+ File.open(gemfile, "wb") do |destination|
+ IO.copy_stream(template, destination)
+ end
+ end
+ end
+
+ puts "Writing new #{gemfile} to #{SharedHelpers.pwd}/#{gemfile}"
+ end
+
+ private
+
+ def gemfile
+ @gemfile ||= options[:gemfile] || Bundler.preferred_gemfile_name
+ end
+ end
+end
diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb
new file mode 100644
index 0000000000..69affd1a10
--- /dev/null
+++ b/lib/bundler/cli/install.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Install
+ attr_reader :options
+ def initialize(options)
+ @options = options
+ end
+
+ def run
+ Bundler.ui.level = "warn" if options[:quiet]
+
+ warn_if_root
+
+ if options[:local]
+ Bundler.self_manager.restart_with_locked_bundler_if_needed
+ else
+ Bundler.self_manager.install_locked_bundler_and_restart_with_it_if_needed
+ end
+
+ Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Gem.freebsd_platform?
+
+ if target_rbconfig_path = options[:"target-rbconfig"]
+ Bundler.rubygems.set_target_rbconfig(target_rbconfig_path)
+ end
+
+ check_trust_policy
+
+ if Bundler.frozen_bundle? && !Bundler.default_lockfile.exist?
+ flag = "deployment setting" if Bundler.settings[:deployment]
+ flag = "frozen setting" if Bundler.settings[:frozen]
+ raise ProductionError, "The #{flag} requires a lockfile. Please make " \
+ "sure you have checked your #{SharedHelpers.relative_lockfile_path} into version control " \
+ "before deploying."
+ end
+
+ normalize_settings
+
+ Bundler::Fetcher.disable_endpoint = options["full-index"]
+
+ Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins]
+
+ # For install we want to enable strict validation
+ # (rather than some optimizations we perform at app runtime).
+ definition = Bundler.definition(strict: true)
+ definition.validate_runtime!
+ definition.lockfile = options["lockfile"] if options["lockfile"]
+ definition.lockfile = false if options["no-lock"]
+
+ installer = Installer.install(Bundler.root, definition, options)
+
+ Bundler.settings.temporary(cache_all_platforms: options[:local] ? false : Bundler.settings[:cache_all_platforms]) do
+ Bundler.load.cache(nil, options[:local]) if Bundler.app_cache.exist? && !options["no-cache"] && !Bundler.frozen_bundle?
+ end
+
+ Bundler.ui.confirm "Bundle complete! #{dependencies_count_for(definition)}, #{gems_installed_for(definition)}."
+ Bundler::CLI::Common.output_without_groups_message(:install)
+
+ if Bundler.use_system_gems?
+ Bundler.ui.confirm "Use `bundle info [gemname]` to see where a bundled gem is installed."
+ else
+ relative_path = Bundler.configured_bundle_path.base_path_relative_to_pwd
+ Bundler.ui.confirm "Bundled gems are installed into `#{relative_path}`"
+ end
+
+ Bundler::CLI::Common.output_post_install_messages installer.post_install_messages
+
+ if CLI::Common.clean_after_install?
+ require_relative "clean"
+ Bundler::CLI::Clean.new(options).run
+ end
+
+ Bundler::CLI::Common.output_fund_metadata_summary
+ rescue Gem::InvalidSpecificationException
+ Bundler.ui.warn "You have one or more invalid gemspecs that need to be fixed."
+ raise
+ end
+
+ private
+
+ def warn_if_root
+ return if Bundler.settings[:silence_root_warning] || Gem.win_platform? || !Process.uid.zero?
+ Bundler.ui.warn "Don't run Bundler as root. Installing your bundle as root " \
+ "will break this application for all non-root users on this machine.", wrap: true
+ end
+
+ def dependencies_count_for(definition)
+ count = definition.dependencies.count
+ "#{count} Gemfile #{count == 1 ? "dependency" : "dependencies"}"
+ end
+
+ def gems_installed_for(definition)
+ count = definition.specs.count {|spec| spec.name != "bundler" }
+ "#{count} #{count == 1 ? "gem" : "gems"} now installed"
+ end
+
+ def check_trust_policy
+ trust_policy = options["trust-policy"]
+ unless Bundler.rubygems.security_policies.keys.unshift(nil).include?(trust_policy)
+ raise InvalidOption, "RubyGems doesn't know about trust policy '#{trust_policy}'. " \
+ "The known policies are: #{Bundler.rubygems.security_policies.keys.join(", ")}."
+ end
+ Bundler.settings.set_command_option_if_given :"trust-policy", trust_policy
+ end
+
+ def normalize_settings
+ if options["standalone"] && Bundler.settings[:path].nil? && !options["local"]
+ Bundler.settings.set_command_option :path, "bundle"
+ end
+
+ Bundler.settings.set_command_option_if_given :shebang, options["shebang"]
+
+ Bundler.settings.set_command_option_if_given :jobs, options["jobs"]
+
+ Bundler::CLI::Common.validate_cooldown!(options["cooldown"])
+ Bundler.settings.set_command_option_if_given :cooldown, options["cooldown"]
+
+ Bundler.settings.set_command_option_if_given :no_prune, options["no-prune"]
+
+ Bundler.settings.set_command_option_if_given :no_install, options["no-install"]
+
+ Bundler.settings.set_command_option_if_given :clean, options["clean"]
+
+ options[:force] = options[:redownload] if options[:redownload]
+ end
+ end
+end
diff --git a/lib/bundler/cli/issue.rb b/lib/bundler/cli/issue.rb
new file mode 100644
index 0000000000..cbfb7da2d8
--- /dev/null
+++ b/lib/bundler/cli/issue.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require "rbconfig"
+
+module Bundler
+ class CLI::Issue
+ def run
+ Bundler.ui.info <<~EOS
+ Did you find an issue with Bundler? Before filing a new issue,
+ be sure to check out these resources:
+
+ 1. Check out our troubleshooting guide for quick fixes to common issues:
+ https://github.com/ruby/rubygems/blob/master/doc/bundler/TROUBLESHOOTING.md
+
+ 2. Instructions for common Bundler uses can be found on the documentation
+ site: https://bundler.io/
+
+ 3. Information about each Bundler command can be found in the Bundler
+ man pages: https://bundler.io/man/bundle.1.html
+
+ Hopefully the troubleshooting steps above resolved your problem! If things
+ still aren't working the way you expect them to, please let us know so
+ that we can diagnose and help fix the problem you're having, by filling
+ in the new issue form located at
+ https://github.com/ruby/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md,
+ and copy and pasting the information below.
+
+ EOS
+
+ Bundler.ui.info Bundler::Env.report
+
+ Bundler.ui.info "\n## Bundle Doctor"
+ doctor
+ end
+
+ def doctor
+ require_relative "doctor/diagnose"
+ Bundler::CLI::Doctor::Diagnose.new({}).run
+ end
+ end
+end
diff --git a/lib/bundler/cli/list.rb b/lib/bundler/cli/list.rb
new file mode 100644
index 0000000000..6a467f45a9
--- /dev/null
+++ b/lib/bundler/cli/list.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require "json"
+
+module Bundler
+ class CLI::List
+ def initialize(options)
+ @options = options
+ @without_group = options["without-group"].map(&:to_sym)
+ @only_group = options["only-group"].map(&:to_sym)
+ @format = options["format"]
+ end
+
+ def run
+ raise InvalidOption, "The `--only-group` and `--without-group` options cannot be used together" if @only_group.any? && @without_group.any?
+
+ raise InvalidOption, "The `--name-only` and `--paths` options cannot be used together" if @options["name-only"] && @options[:paths]
+
+ specs = if @only_group.any? || @without_group.any?
+ filtered_specs_by_groups
+ else
+ begin
+ Bundler.load.specs
+ rescue GemNotFound => e
+ Bundler.ui.error e.message
+ Bundler.ui.warn "Install missing gems with `bundle install`."
+ exit 1
+ end
+ end.reject {|s| s.name == "bundler" }.sort_by(&:name)
+
+ case @format
+ when "json"
+ print_json(specs: specs)
+ when nil
+ print_human(specs: specs)
+ else
+ raise InvalidOption, "Unknown option`--format=#{@format}`. Supported formats: `json`"
+ end
+ end
+
+ private
+
+ def print_json(specs:)
+ gems = if @options["name-only"]
+ specs.map {|s| { name: s.name } }
+ else
+ specs.map do |s|
+ {
+ name: s.name,
+ version: s.version.to_s,
+ git_version: s.git_version&.strip,
+ }.tap do |h|
+ h[:path] = s.full_gem_path if @options["paths"]
+ end
+ end
+ end
+ Bundler.ui.info({ gems: gems }.to_json)
+ end
+
+ def print_human(specs:)
+ return Bundler.ui.info "No gems in the Gemfile" if specs.empty?
+
+ return specs.each {|s| Bundler.ui.info s.name } if @options["name-only"]
+ return specs.each {|s| Bundler.ui.info s.full_gem_path } if @options["paths"]
+
+ Bundler.ui.info "Gems included by the bundle:"
+
+ specs.each {|s| Bundler.ui.info " * #{s.name} (#{s.version}#{s.git_version})" }
+
+ Bundler.ui.info "Use `bundle info` to print more detailed information about a gem"
+ end
+
+ def verify_group_exists(groups)
+ (@without_group + @only_group).each do |group|
+ raise InvalidOption, "`#{group}` group could not be found." unless groups.include?(group)
+ end
+ end
+
+ def filtered_specs_by_groups
+ definition = Bundler.definition
+ groups = definition.groups
+
+ verify_group_exists(groups)
+
+ show_groups =
+ if @without_group.any?
+ groups.reject {|g| @without_group.include?(g) }
+ elsif @only_group.any?
+ groups.select {|g| @only_group.include?(g) }
+ else
+ groups
+ end.map(&:to_sym)
+
+ definition.specs_for(show_groups)
+ end
+ end
+end
diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb
new file mode 100644
index 0000000000..2f78868936
--- /dev/null
+++ b/lib/bundler/cli/lock.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Lock
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def run
+ unless Bundler.default_gemfile
+ Bundler.ui.error "Unable to find a Gemfile to lock"
+ exit 1
+ end
+
+ check_for_conflicting_options
+
+ print = options[:print]
+ previous_output_stream = Bundler.ui.output_stream
+ Bundler.ui.output_stream = :stderr if print
+
+ Bundler::Fetcher.disable_endpoint = options["full-index"]
+
+ update = options[:update]
+ conservative = options[:conservative]
+ bundler = options[:bundler]
+
+ if update.is_a?(Array) # unlocking specific gems
+ Bundler::CLI::Common.ensure_all_gems_in_lockfile!(update)
+ update = { gems: update, conservative: conservative }
+ elsif update && conservative
+ update = { conservative: conservative }
+ elsif update && bundler
+ update = { bundler: bundler }
+ end
+
+ Bundler.settings.temporary(frozen: false) do
+ definition = Bundler.definition(update, Bundler.default_lockfile)
+ definition.add_checksums if options["add-checksums"]
+
+ Bundler::CLI::Common.configure_gem_version_promoter(definition, options) if options[:update]
+
+ options["remove-platform"].each do |platform_string|
+ platform = Gem::Platform.new(platform_string)
+ definition.remove_platform(platform)
+ end
+
+ options["add-platform"].each do |platform_string|
+ platform = Gem::Platform.new(platform_string)
+ if platform.to_s == "unknown"
+ Bundler.ui.error "The platform `#{platform_string}` is unknown to RubyGems and can't be added to the lockfile."
+ exit 1
+ end
+ definition.add_platform(platform)
+ end
+
+ if definition.platforms.empty?
+ raise InvalidOption, "Removing all platforms from the bundle is not allowed"
+ end
+
+ definition.remotely! unless options[:local]
+
+ if options["normalize-platforms"]
+ definition.normalize_platforms
+ end
+
+ if print
+ puts definition.to_lock
+ else
+ file = options[:lockfile]
+ file = file ? Pathname.new(file).expand_path : Bundler.default_lockfile
+
+ puts "Writing lockfile to #{file}"
+ definition.write_lock(file, false)
+ end
+ end
+
+ Bundler.ui.output_stream = previous_output_stream
+ end
+
+ private
+
+ def check_for_conflicting_options
+ if options["normalize-platforms"] && options["add-platform"].any?
+ raise InvalidOption, "--normalize-platforms can't be used with --add-platform"
+ end
+
+ if options["normalize-platforms"] && options["remove-platform"].any?
+ raise InvalidOption, "--normalize-platforms can't be used with --remove-platform"
+ end
+ end
+ end
+end
diff --git a/lib/bundler/cli/open.rb b/lib/bundler/cli/open.rb
new file mode 100644
index 0000000000..f24693b843
--- /dev/null
+++ b/lib/bundler/cli/open.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Open
+ attr_reader :options, :name, :path
+ def initialize(options, name)
+ @options = options
+ @name = name
+ @path = options[:path] unless options[:path].nil?
+ end
+
+ def run
+ raise InvalidOption, "Cannot specify `--path` option without a value" if !@path.nil? && @path.empty?
+ editor = [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? }
+ return Bundler.ui.info("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") unless editor
+ return unless spec = Bundler::CLI::Common.select_spec(name, :regex_match)
+ if spec.default_gem?
+ Bundler.ui.info "Unable to open #{name} because it's a default gem, so the directory it would normally be installed to does not exist."
+ else
+ root_path = spec.full_gem_path
+ require "shellwords"
+ command = Shellwords.split(editor) << File.join([root_path, path].compact)
+ Bundler.with_original_env do
+ system(*command, { chdir: root_path })
+ end || Bundler.ui.info("Could not run '#{command.join(" ")}'")
+ end
+ end
+ end
+end
diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb
new file mode 100644
index 0000000000..465e56ada2
--- /dev/null
+++ b/lib/bundler/cli/outdated.rb
@@ -0,0 +1,337 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Outdated
+ attr_reader :options, :gems, :options_include_groups, :filter_options_patch, :sources, :strict
+ attr_accessor :outdated_gems
+
+ def initialize(options, gems)
+ @options = options
+ @gems = gems
+ @sources = Array(options[:source])
+
+ @filter_options_patch = options.keys & %w[filter-major filter-minor filter-patch]
+
+ @outdated_gems = []
+
+ @options_include_groups = [:group, :groups].any? do |v|
+ options.keys.include?(v.to_s)
+ end
+
+ # the patch level options imply strict is also true. It wouldn't make
+ # sense otherwise.
+ @strict = options["filter-strict"] || Bundler::CLI::Common.patch_level_options(options).any?
+ end
+
+ def run
+ check_for_deployment_mode!
+
+ Bundler::CLI::Common.validate_cooldown!(options[:cooldown])
+ Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown]
+
+ Bundler.definition.validate_runtime!
+ current_specs = Bundler.ui.silence { Bundler.definition.resolve }
+
+ gems.each do |gem_name|
+ if current_specs[gem_name].empty?
+ raise GemNotFound, "Could not find gem '#{gem_name}'."
+ end
+ end
+
+ current_dependencies = Bundler.ui.silence do
+ Bundler.load.dependencies.map {|dep| [dep.name, dep] }.to_h
+ end
+
+ definition = if gems.empty? && sources.empty?
+ # We're doing a full update
+ Bundler.definition(true)
+ else
+ Bundler.definition(gems: gems, sources: sources)
+ end
+
+ Bundler::CLI::Common.configure_gem_version_promoter(
+ Bundler.definition,
+ options.merge(strict: @strict)
+ )
+
+ definition_resolution = proc do
+ options[:local] ? definition.resolve_with_cache! : definition.resolve_remotely!
+ end
+
+ if options[:parseable]
+ Bundler.ui.progress(&definition_resolution)
+ else
+ definition_resolution.call
+ end
+
+ Bundler.ui.info ""
+
+ # Loop through the current specs
+ gemfile_specs, dependency_specs = current_specs.partition do |spec|
+ current_dependencies.key? spec.name
+ end
+
+ specs = if options["only-explicit"]
+ gemfile_specs
+ else
+ gemfile_specs + dependency_specs
+ end
+
+ specs.sort_by(&:name).uniq(&:name).each do |current_spec|
+ next unless gems.empty? || gems.include?(current_spec.name)
+
+ active_spec = retrieve_active_spec(definition, current_spec)
+ next unless active_spec
+
+ next unless filter_options_patch.empty? || update_present_via_semver_portions(current_spec, active_spec, options)
+
+ gem_outdated = Gem::Version.new(active_spec.version) > Gem::Version.new(current_spec.version)
+ next unless gem_outdated || (current_spec.git_version != active_spec.git_version)
+
+ dependency = current_dependencies[current_spec.name]
+ groups = ""
+ if dependency && !options[:parseable]
+ groups = dependency.groups.join(", ")
+ end
+
+ outdated_gems << {
+ active_spec: active_spec,
+ current_spec: current_spec,
+ dependency: dependency,
+ groups: groups,
+ }
+ end
+
+ relevant_outdated_gems = if options_include_groups
+ outdated_gems.group_by {|g| g[:groups] }.sort.flat_map do |groups, gems|
+ contains_group = groups.split(", ").include?(options[:group])
+ next unless options[:groups] || contains_group
+
+ gems
+ end.compact
+ else
+ outdated_gems
+ end
+
+ if relevant_outdated_gems.empty?
+ unless options[:parseable]
+ Bundler.ui.info(nothing_outdated_message)
+ end
+ else
+ if options[:parseable]
+ print_gems(relevant_outdated_gems)
+ else
+ print_gems_table(relevant_outdated_gems)
+ end
+
+ exit 1
+ end
+ end
+
+ private
+
+ def loaded_from_for(spec)
+ return unless spec.respond_to?(:loaded_from)
+
+ spec.loaded_from
+ end
+
+ def groups_text(group_text, groups)
+ "#{group_text}#{groups.split(",").size > 1 ? "s" : ""} \"#{groups}\""
+ end
+
+ def nothing_outdated_message
+ if filter_options_patch.any?
+ display = filter_options_patch.map do |o|
+ o.sub("filter-", "")
+ end.join(" or ")
+
+ "No #{display} updates to display.\n"
+ else
+ "Bundle up to date!\n"
+ end
+ end
+
+ def retrieve_active_spec(definition, current_spec)
+ active_spec = definition.resolve.find_by_name_and_platform(current_spec.name, current_spec.platform)
+ return unless active_spec
+
+ return active_spec if strict
+
+ active_specs = active_spec.source.specs.search(current_spec.name).select {|spec| spec.installable_on_platform?(current_spec.platform) }.sort_by(&:version)
+ if !current_spec.version.prerelease? && !options[:pre] && active_specs.size > 1
+ active_specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? }
+ end
+ active_specs.last
+ end
+
+ def print_gems(gems_list)
+ gems_list.each do |gem|
+ print_gem(
+ gem[:current_spec],
+ gem[:active_spec],
+ gem[:dependency],
+ gem[:groups],
+ )
+ end
+ end
+
+ def print_gems_table(gems_list)
+ data = gems_list.map do |gem|
+ gem_column_for(
+ gem[:current_spec],
+ gem[:active_spec],
+ gem[:dependency],
+ gem[:groups],
+ )
+ end
+
+ print_indented([table_header] + data)
+ end
+
+ def print_gem(current_spec, active_spec, dependency, groups)
+ spec_version = "#{active_spec.version}#{active_spec.git_version}"
+ if Bundler.ui.debug?
+ loaded_from = loaded_from_for(active_spec)
+ spec_version += " (from #{loaded_from})" if loaded_from
+ end
+ current_version = "#{current_spec.version}#{current_spec.git_version}"
+
+ if dependency&.specific?
+ dependency_version = %(, requested #{dependency.requirement})
+ end
+
+ spec_outdated_info = "#{active_spec.name} (newest #{spec_version}, " \
+ "installed #{current_version}#{dependency_version}"
+
+ release_date = release_date_for(active_spec)
+ spec_outdated_info += ", released #{release_date}" unless release_date.empty?
+
+ remaining = cooldown_days_remaining(active_spec)
+ spec_outdated_info += ", in cooldown for #{remaining} more day#{"s" if remaining > 1}" if remaining
+
+ spec_outdated_info += ")"
+
+ output_message = if options[:parseable]
+ spec_outdated_info.to_s
+ elsif options_include_groups || groups.empty?
+ " * #{spec_outdated_info}"
+ else
+ " * #{spec_outdated_info} in #{groups_text("group", groups)}"
+ end
+
+ Bundler.ui.info output_message.rstrip
+ end
+
+ def gem_column_for(current_spec, active_spec, dependency, groups)
+ current_version = "#{current_spec.version}#{current_spec.git_version}"
+ spec_version = "#{active_spec.version}#{active_spec.git_version}"
+ remaining = cooldown_days_remaining(active_spec)
+ spec_version += " (cooldown #{remaining}d)" if remaining
+ dependency = dependency.requirement if dependency
+
+ ret_val = [active_spec.name, current_version, spec_version, dependency.to_s, groups.to_s]
+ ret_val << release_date_for(active_spec)
+ ret_val << loaded_from_for(active_spec).to_s if Bundler.ui.debug?
+ ret_val
+ end
+
+ def cooldown_days_remaining(spec, now = Time.now)
+ return nil unless spec.respond_to?(:created_at) && spec.created_at
+ return nil unless spec.respond_to?(:remote) && spec.remote
+ days = spec.remote.effective_cooldown
+ return nil if days.nil? || days <= 0
+ remaining = days - ((now - spec.created_at) / 86_400.0)
+ remaining > 0 ? remaining.ceil : nil
+ end
+
+ def check_for_deployment_mode!
+ return unless Bundler.frozen_bundle?
+ suggested_command = if Bundler.settings.locations("frozen").keys.&([:global, :local]).any?
+ "bundle config unset frozen"
+ elsif Bundler.settings.locations("deployment").keys.&([:global, :local]).any?
+ "bundle config unset deployment"
+ end
+ raise ProductionError, "You are trying to check outdated gems in " \
+ "deployment mode. Run `bundle outdated` elsewhere.\n" \
+ "\nIf this is a development machine, remove the " \
+ "#{Bundler.default_gemfile} freeze" \
+ "\nby running `#{suggested_command}`."
+ end
+
+ def update_present_via_semver_portions(current_spec, active_spec, options)
+ current_major = current_spec.version.segments.first
+ active_major = active_spec.version.segments.first
+
+ update_present = false
+ update_present = active_major > current_major if options["filter-major"]
+
+ if !update_present && (options["filter-minor"] || options["filter-patch"]) && current_major == active_major
+ current_minor = get_version_semver_portion_value(current_spec, 1)
+ active_minor = get_version_semver_portion_value(active_spec, 1)
+
+ update_present = active_minor > current_minor if options["filter-minor"]
+
+ if !update_present && options["filter-patch"] && current_minor == active_minor
+ current_patch = get_version_semver_portion_value(current_spec, 2)
+ active_patch = get_version_semver_portion_value(active_spec, 2)
+
+ update_present = active_patch > current_patch
+ end
+ end
+
+ update_present
+ end
+
+ def get_version_semver_portion_value(spec, version_portion_index)
+ version_section = spec.version.segments[version_portion_index, 1]
+ version_section.to_a[0].to_i
+ end
+
+ def print_indented(matrix)
+ header = matrix[0]
+ data = matrix[1..-1]
+
+ column_sizes = Array.new(header.size) do |index|
+ matrix.max_by {|row| row[index].length }[index].length
+ end
+
+ Bundler.ui.info justify(header, column_sizes)
+
+ data.sort_by! {|row| row[0] }
+
+ data.each do |row|
+ Bundler.ui.info justify(row, column_sizes)
+ end
+ end
+
+ def table_header
+ header = ["Gem", "Current", "Latest", "Requested", "Groups", "Release Date"]
+ header << "Path" if Bundler.ui.debug?
+ header
+ end
+
+ def release_date_for(spec)
+ return "" unless spec.respond_to?(:date)
+
+ date = spec.date
+ return "" unless date
+
+ return "" unless Gem.const_defined?(:DEFAULT_SOURCE_DATE_EPOCH)
+ default_date = Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc
+ default_date = Time.utc(default_date.year, default_date.month, default_date.day)
+
+ date = date.utc if date.respond_to?(:utc)
+
+ return "" if date == default_date
+
+ date.strftime("%Y-%m-%d")
+ end
+
+ def justify(row, sizes)
+ row.each_with_index.map do |element, index|
+ element.ljust(sizes[index])
+ end.join(" ").strip + "\n"
+ end
+ end
+end
diff --git a/lib/bundler/cli/platform.rb b/lib/bundler/cli/platform.rb
new file mode 100644
index 0000000000..32d68abbb1
--- /dev/null
+++ b/lib/bundler/cli/platform.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Platform
+ attr_reader :options
+ def initialize(options)
+ @options = options
+ end
+
+ def run
+ ruby_version = if Bundler.locked_gems
+ Bundler.locked_gems.ruby_version&.gsub(/p\d+\Z/, "")
+ else
+ Bundler.definition.ruby_version&.single_version_string
+ end
+
+ output = []
+
+ if options[:ruby]
+ if ruby_version
+ output << ruby_version
+ else
+ output << "No ruby version specified"
+ end
+ else
+ platforms = Bundler.definition.platforms.map {|p| "* #{p}" }
+
+ output << "Your platform is: #{Gem::Platform.local}"
+ output << "Your app has gems that work on these platforms:\n#{platforms.join("\n")}"
+
+ if ruby_version
+ output << "Your Gemfile specifies a Ruby version requirement:\n* #{ruby_version}"
+
+ begin
+ Bundler.definition.validate_runtime!
+ output << "Your current platform satisfies the Ruby version requirement."
+ rescue RubyVersionMismatch => e
+ output << e.message
+ end
+ else
+ output << "Your Gemfile does not specify a Ruby version requirement."
+ end
+ end
+
+ Bundler.ui.info output.join("\n\n")
+ end
+ end
+end
diff --git a/lib/bundler/cli/plugin.rb b/lib/bundler/cli/plugin.rb
new file mode 100644
index 0000000000..32fa660fe0
--- /dev/null
+++ b/lib/bundler/cli/plugin.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require_relative "../vendored_thor"
+module Bundler
+ class CLI::Plugin < Thor
+ desc "install PLUGINS", "Install the plugin from the source"
+ long_desc <<-D
+ Install plugins either from the rubygems source provided (with --source option), from a git source provided with --git, or a local path provided with --path. If no sources are provided, it uses Gem.sources
+ D
+ method_option "source", type: :string, default: nil, banner: "URL of the RubyGems source to fetch the plugin from"
+ method_option "version", type: :string, default: nil, banner: "The version of the plugin to fetch"
+ method_option "git", type: :string, default: nil, banner: "URL of the git repo to fetch from"
+ method_option "local_git", type: :string, default: nil, banner: "Path of the local git repo to fetch from (removed)"
+ method_option "branch", type: :string, default: nil, banner: "The git branch to checkout"
+ method_option "ref", type: :string, default: nil, banner: "The git revision to check out"
+ method_option "path", type: :string, default: nil, banner: "Path of a local gem to directly use"
+ def install(*plugins)
+ if options.key?(:local_git)
+ raise InvalidOption, "--local_git has been removed, use --git"
+ end
+
+ Bundler::Plugin.install(plugins, options)
+ end
+
+ desc "uninstall PLUGINS", "Uninstall the plugins"
+ long_desc <<-D
+ Uninstall given list of plugins. To uninstall all the plugins, use -all option.
+ D
+ method_option "all", type: :boolean, default: nil, banner: "Uninstall all the installed plugins. If no plugin is installed, then it does nothing."
+ def uninstall(*plugins)
+ Bundler::Plugin.uninstall(plugins, options)
+ end
+
+ desc "list", "List the installed plugins and available commands"
+ def list
+ Bundler::Plugin.list
+ end
+ end
+end
diff --git a/lib/bundler/cli/pristine.rb b/lib/bundler/cli/pristine.rb
new file mode 100644
index 0000000000..f463f0bce8
--- /dev/null
+++ b/lib/bundler/cli/pristine.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Pristine
+ def initialize(gems)
+ @gems = gems
+ end
+
+ def run
+ CLI::Common.ensure_all_gems_in_lockfile!(@gems)
+ definition = Bundler.definition
+ definition.validate_runtime!
+ installer = Bundler::Installer.new(Bundler.root, definition)
+ git_sources = []
+
+ ProcessLock.lock do
+ installed_specs = definition.specs.reject do |spec|
+ next if spec.name == "bundler" # Source::Rubygems doesn't install bundler
+ next if !@gems.empty? && !@gems.include?(spec.name)
+
+ gem_name = "#{spec.name} (#{spec.version}#{spec.git_version})"
+ gem_name += " (#{spec.platform})" if !spec.platform.nil? && spec.platform != Gem::Platform::RUBY
+
+ case source = spec.source
+ when Source::Rubygems
+ cached_gem = spec.cache_file
+ unless File.exist?(cached_gem)
+ Bundler.ui.error("Failed to pristine #{gem_name}. Cached gem #{cached_gem} does not exist.")
+ next
+ end
+
+ FileUtils.rm_rf spec.full_gem_path
+ when Source::Git
+ if source.local?
+ Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is locally overridden.")
+ next
+ end
+
+ source.remote!
+ if extension_cache_path = source.extension_cache_path(spec)
+ FileUtils.rm_rf extension_cache_path
+ end
+ FileUtils.rm_rf spec.extension_dir
+ FileUtils.rm_rf spec.full_gem_path
+
+ next if git_sources.include?(source)
+ git_sources << source
+ else
+ Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is sourced from local path.")
+ next
+ end
+
+ true
+ end.map(&:name)
+
+ jobs = Bundler.settings.installation_parallelization
+ pristine_count = definition.specs.count - installed_specs.count
+ # allow a pristining a single gem to skip the parallel worker
+ jobs = [jobs, pristine_count].min
+ ParallelInstaller.call(installer, definition.specs, jobs, false, true, skip: installed_specs)
+ end
+ end
+ end
+end
diff --git a/lib/bundler/cli/remove.rb b/lib/bundler/cli/remove.rb
new file mode 100644
index 0000000000..44a4d891dd
--- /dev/null
+++ b/lib/bundler/cli/remove.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Remove
+ def initialize(gems, options)
+ @gems = gems
+ @options = options
+ end
+
+ def run
+ raise InvalidOption, "Please specify gems to remove." if @gems.empty?
+
+ Injector.remove(@gems, {})
+ Installer.install(Bundler.root, Bundler.definition)
+ end
+ end
+end
diff --git a/lib/bundler/cli/show.rb b/lib/bundler/cli/show.rb
new file mode 100644
index 0000000000..67fdcc797e
--- /dev/null
+++ b/lib/bundler/cli/show.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Show
+ attr_reader :options, :gem_name, :latest_specs
+ def initialize(options, gem_name)
+ @options = options
+ @gem_name = gem_name
+ @verbose = options[:verbose]
+ @latest_specs = fetch_latest_specs if @verbose
+ end
+
+ def run
+ Bundler.ui.silence do
+ Bundler.definition.validate_runtime!
+ Bundler.load.lock
+ end
+
+ if gem_name
+ if gem_name == "bundler"
+ path = File.expand_path("../../..", __dir__)
+ else
+ spec = Bundler::CLI::Common.select_spec(gem_name, :regex_match)
+ return unless spec
+ path = spec.full_gem_path
+ unless File.directory?(path)
+ return Bundler.ui.warn "The gem #{gem_name} is missing. It should be installed at #{path}, but was not found"
+ end
+ end
+ return Bundler.ui.info(path)
+ end
+
+ if options[:paths]
+ Bundler.load.specs.sort_by(&:name).map do |s|
+ Bundler.ui.info s.full_gem_path
+ end
+ else
+ Bundler.ui.info "Gems included by the bundle:"
+ Bundler.load.specs.sort_by(&:name).each do |s|
+ desc = " * #{s.name} (#{s.version}#{s.git_version})"
+ if @verbose
+ latest = latest_specs.find {|l| l.name == s.name }
+ Bundler.ui.info <<~END
+ #{desc.lstrip}
+ \tSummary: #{s.summary || "No description available."}
+ \tHomepage: #{s.homepage || "No website available."}
+ \tStatus: #{outdated?(s, latest) ? "Outdated - #{s.version} < #{latest.version}" : "Up to date"}
+ END
+ else
+ Bundler.ui.info desc
+ end
+ end
+ end
+ end
+
+ private
+
+ def fetch_latest_specs
+ definition = Bundler.definition(true)
+ Bundler.ui.info "Fetching remote specs for outdated check...\n\n"
+ Bundler.ui.silence { definition.remotely! }
+ Bundler.reset!
+ definition.specs
+ end
+
+ def outdated?(current, latest)
+ return false unless latest
+ Gem::Version.new(current.version) < Gem::Version.new(latest.version)
+ end
+ end
+end
diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb
new file mode 100644
index 0000000000..d92ffd995f
--- /dev/null
+++ b/lib/bundler/cli/update.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Update
+ attr_reader :options, :gems
+ def initialize(options, gems)
+ @options = options
+ @gems = gems
+ end
+
+ def run
+ Bundler.ui.level = "warn" if options[:quiet]
+
+ update_bundler = options[:bundler]
+
+ Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler) if update_bundler
+
+ Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins]
+
+ sources = Array(options[:source])
+ groups = Array(options[:group]).map(&:to_sym)
+
+ full_update = gems.empty? && sources.empty? && groups.empty? && !options[:ruby] && !update_bundler
+
+ if full_update && !options[:all]
+ if Bundler.settings[:update_requires_all_flag]
+ raise InvalidOption, "To update everything, pass the `--all` flag."
+ end
+ SharedHelpers.feature_deprecated! "Pass --all to `bundle update` to update everything"
+ elsif !full_update && options[:all]
+ raise InvalidOption, "Cannot specify --all along with specific options."
+ end
+
+ conservative = options[:conservative]
+
+ if full_update
+ if conservative
+ Bundler.definition(conservative: conservative)
+ else
+ Bundler.definition(true)
+ end
+ else
+ unless Bundler.default_lockfile.exist?
+ raise GemfileLockNotFound, "This Bundle hasn't been installed yet. " \
+ "Run `bundle install` to update and install the bundled gems."
+ end
+ Bundler::CLI::Common.ensure_all_gems_in_lockfile!(gems)
+
+ if groups.any?
+ deps = Bundler.definition.dependencies.select {|d| (d.groups & groups).any? }
+ gems.concat(deps.map(&:name))
+ end
+
+ Bundler.definition(gems: gems, sources: sources, ruby: options[:ruby],
+ conservative: conservative,
+ bundler: update_bundler)
+ end
+
+ Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options)
+
+ Bundler::Fetcher.disable_endpoint = options["full-index"]
+
+ opts = options.dup
+ opts["update"] = true
+ opts["local"] = options[:local]
+ opts["force"] = options[:redownload] if options[:redownload]
+
+ Bundler.settings.set_command_option_if_given :jobs, opts["jobs"]
+ Bundler::CLI::Common.validate_cooldown!(options[:cooldown])
+ Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown]
+
+ Bundler.definition.validate_runtime!
+
+ if locked_gems = Bundler.definition.locked_gems
+ previous_locked_info = locked_gems.specs.reduce({}) do |h, s|
+ h[s.name] = { spec: s, version: s.version, source: s.source.identifier }
+ h
+ end
+ end
+
+ installer = Installer.install Bundler.root, Bundler.definition, opts
+ Bundler.load.cache if Bundler.app_cache.exist?
+
+ if CLI::Common.clean_after_install?
+ require_relative "clean"
+ Bundler::CLI::Clean.new(options).run
+ end
+
+ if locked_gems
+ gems.each do |name|
+ locked_info = previous_locked_info[name]
+ next unless locked_info
+
+ locked_spec = locked_info[:spec]
+ new_spec = Bundler.definition.specs[name].first
+ unless new_spec
+ unless locked_spec.installable_on_platform?(Bundler.local_platform)
+ Bundler.ui.warn "Bundler attempted to update #{name} but it was not considered because it is for a different platform from the current one"
+ end
+
+ next
+ end
+
+ locked_source = locked_info[:source]
+ new_source = new_spec.source.identifier
+ next if locked_source != new_source
+
+ new_version = new_spec.version
+ locked_version = locked_info[:version]
+ if new_version < locked_version
+ Bundler.ui.warn "Note: #{name} version regressed from #{locked_version} to #{new_version}"
+ elsif new_version == locked_version
+ Bundler.ui.warn "Bundler attempted to update #{name} but its version stayed the same"
+ end
+ end
+ end
+
+ Bundler.ui.confirm "Bundle updated!"
+ Bundler::CLI::Common.output_without_groups_message(:update)
+ Bundler::CLI::Common.output_post_install_messages installer.post_install_messages
+
+ Bundler::CLI::Common.output_fund_metadata_summary
+ end
+ end
+end
diff --git a/lib/bundler/compact_index_client.rb b/lib/bundler/compact_index_client.rb
new file mode 100644
index 0000000000..6865e30dbc
--- /dev/null
+++ b/lib/bundler/compact_index_client.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require "set"
+
+module Bundler
+ # The CompactIndexClient is responsible for fetching and parsing the compact index.
+ #
+ # The compact index is a set of caching optimized files that are used to fetch gem information.
+ # The files are:
+ # - names: a list of all gem names
+ # - versions: a list of all gem versions
+ # - info/[gem]: a list of all versions of a gem
+ #
+ # The client is instantiated with:
+ # - `directory`: the root directory where the cache files are stored.
+ # - `fetcher`: (optional) an object that responds to #call(uri_path, headers) and returns an http response.
+ # If the `fetcher` is not provided, the client will only read cached files from disk.
+ #
+ # The client is organized into:
+ # - `Updater`: updates the cached files on disk using the fetcher.
+ # - `Cache`: calls the updater, caches files, read and return them from disk
+ # - `Parser`: parses the compact index file data
+ # - `CacheFile`: a concurrency safe file reader/writer that verifies checksums
+ #
+ # The client is intended to optimize memory usage and performance.
+ # It is called 100s or 1000s of times, parsing files with hundreds of thousands of lines.
+ # It may be called concurrently without global interpreter lock in some Rubies.
+ # As a result, some methods may look more complex than necessary to save memory or time.
+ class CompactIndexClient
+ SUPPORTED_DIGESTS = { "sha-256" => :SHA256 }.freeze
+ DEBUG_MUTEX = Thread::Mutex.new
+
+ # info returns an Array of INFO Arrays. Each INFO Array has the following indices:
+ INFO_NAME = 0
+ INFO_VERSION = 1
+ INFO_PLATFORM = 2
+ INFO_DEPS = 3
+ INFO_REQS = 4
+
+ def self.debug
+ return unless ENV["DEBUG_COMPACT_INDEX"]
+ DEBUG_MUTEX.synchronize { warn("[#{self}] #{yield}") }
+ end
+
+ class Error < StandardError; end
+
+ require_relative "compact_index_client/cache"
+ require_relative "compact_index_client/cache_file"
+ require_relative "compact_index_client/parser"
+ require_relative "compact_index_client/updater"
+
+ def initialize(directory, fetcher = nil)
+ @cache = Cache.new(directory, fetcher)
+ @parser = Parser.new(@cache)
+ end
+
+ def names
+ Bundler::CompactIndexClient.debug { "names" }
+ @parser.names
+ end
+
+ def versions
+ Bundler::CompactIndexClient.debug { "versions" }
+ @parser.versions
+ end
+
+ def dependencies(names)
+ Bundler::CompactIndexClient.debug { "dependencies(#{names})" }
+ names.map {|name| info(name) }
+ end
+
+ def info(name)
+ Bundler::CompactIndexClient.debug { "info(#{name})" }
+ @parser.info(name)
+ end
+
+ def latest_version(name)
+ Bundler::CompactIndexClient.debug { "latest_version(#{name})" }
+ @parser.info(name).map {|d| Gem::Version.new(d[INFO_VERSION]) }.max
+ end
+
+ def available?
+ Bundler::CompactIndexClient.debug { "available?" }
+ @parser.available?
+ end
+
+ def reset!
+ Bundler::CompactIndexClient.debug { "reset!" }
+ @cache.reset!
+ end
+ end
+end
diff --git a/lib/bundler/compact_index_client/cache.rb b/lib/bundler/compact_index_client/cache.rb
new file mode 100644
index 0000000000..3bae6c9efd
--- /dev/null
+++ b/lib/bundler/compact_index_client/cache.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require "rubygems/resolver/api_set/gem_parser"
+
+module Bundler
+ class CompactIndexClient
+ class Cache
+ attr_reader :directory
+
+ def initialize(directory, fetcher = nil)
+ @directory = Pathname.new(directory).expand_path
+ @updater = Updater.new(fetcher) if fetcher
+ @mutex = Thread::Mutex.new
+ @endpoints = Set.new
+
+ @info_root = mkdir("info")
+ @special_characters_info_root = mkdir("info-special-characters")
+ @info_etag_root = mkdir("info-etags")
+ end
+
+ def names
+ fetch("names", names_path, names_etag_path)
+ end
+
+ def versions
+ fetch("versions", versions_path, versions_etag_path)
+ end
+
+ def info(name, remote_checksum = nil)
+ path = info_path(name)
+
+ if remote_checksum && remote_checksum != SharedHelpers.checksum_for_file(path, :MD5)
+ fetch("info/#{name}", path, info_etag_path(name))
+ else
+ Bundler::CompactIndexClient.debug { "update skipped info/#{name} (#{remote_checksum ? "versions index checksum is nil" : "versions index checksum matches local"})" }
+ read(path)
+ end
+ end
+
+ def reset!
+ @mutex.synchronize { @endpoints.clear }
+ end
+
+ private
+
+ def names_path = directory.join("names")
+ def names_etag_path = directory.join("names.etag")
+ def versions_path = directory.join("versions")
+ def versions_etag_path = directory.join("versions.etag")
+
+ def info_path(name)
+ name = name.to_s
+ # TODO: converge this into the info_root by hashing all filenames like info_etag_path
+ if /[^a-z0-9_-]/.match?(name)
+ name += "-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}"
+ @special_characters_info_root.join(name)
+ else
+ @info_root.join(name)
+ end
+ end
+
+ def info_etag_path(name)
+ name = name.to_s
+ @info_etag_root.join("#{name}-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}")
+ end
+
+ def mkdir(name)
+ directory.join(name).tap do |dir|
+ SharedHelpers.filesystem_access(dir) do
+ FileUtils.mkdir_p(dir)
+ end
+ end
+ end
+
+ def fetch(remote_path, path, etag_path)
+ if already_fetched?(remote_path)
+ Bundler::CompactIndexClient.debug { "already fetched #{remote_path}" }
+ else
+ Bundler::CompactIndexClient.debug { "fetching #{remote_path}" }
+ @updater&.update(remote_path, path, etag_path)
+ end
+
+ read(path)
+ end
+
+ def already_fetched?(remote_path)
+ @mutex.synchronize { !@endpoints.add?(remote_path) }
+ end
+
+ def read(path)
+ return unless path.file?
+ SharedHelpers.filesystem_access(path, :read, &:read)
+ end
+ end
+ end
+end
diff --git a/lib/bundler/compact_index_client/cache_file.rb b/lib/bundler/compact_index_client/cache_file.rb
new file mode 100644
index 0000000000..299d683438
--- /dev/null
+++ b/lib/bundler/compact_index_client/cache_file.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+require_relative "../vendored_fileutils"
+require "rubygems/package"
+
+module Bundler
+ class CompactIndexClient
+ # write cache files in a way that is robust to concurrent modifications
+ # if digests are given, the checksums will be verified
+ class CacheFile
+ DEFAULT_FILE_MODE = 0o644
+ private_constant :DEFAULT_FILE_MODE
+
+ class Error < RuntimeError; end
+ class ClosedError < Error; end
+
+ class DigestMismatchError < Error
+ def initialize(digests, expected_digests)
+ super "Calculated checksums #{digests.inspect} did not match expected #{expected_digests.inspect}."
+ end
+ end
+
+ # Initialize with a copy of the original file, then yield the instance.
+ def self.copy(path, &block)
+ new(path) do |file|
+ file.initialize_digests
+
+ SharedHelpers.filesystem_access(path, :read) do
+ path.open("rb") do |s|
+ file.open {|f| IO.copy_stream(s, f) }
+ end
+ end
+
+ yield file
+ end
+ end
+
+ # Write data to a temp file, then replace the original file with it verifying the digests if given.
+ def self.write(path, data, digests = nil)
+ return unless data
+ new(path) do |file|
+ file.digests = digests
+ file.write(data)
+ end
+ end
+
+ attr_reader :original_path, :path
+
+ def initialize(original_path, &block)
+ @original_path = original_path
+ @perm = original_path.file? ? original_path.stat.mode : DEFAULT_FILE_MODE
+ @path = original_path.sub(/$/, ".#{$$}.tmp")
+ return unless block_given?
+ begin
+ yield self
+ ensure
+ close
+ end
+ end
+
+ def size
+ path.size
+ end
+
+ # initialize the digests using CompactIndexClient::SUPPORTED_DIGESTS, or a subset based on keys.
+ def initialize_digests(keys = nil)
+ @digests = keys ? SUPPORTED_DIGESTS.slice(*keys) : SUPPORTED_DIGESTS.dup
+ @digests.transform_values! {|algo_class| SharedHelpers.digest(algo_class).new }
+ end
+
+ # reset the digests so they don't contain any previously read data
+ def reset_digests
+ @digests&.each_value(&:reset)
+ end
+
+ # set the digests that will be verified at the end
+ def digests=(expected_digests)
+ @expected_digests = expected_digests
+
+ if @expected_digests.nil?
+ @digests = nil
+ elsif @digests
+ @digests = @digests.slice(*@expected_digests.keys)
+ else
+ initialize_digests(@expected_digests.keys)
+ end
+ end
+
+ def digests?
+ @digests&.any?
+ end
+
+ # Open the temp file for writing, reusing original permissions, yielding the IO object.
+ def open(write_mode = "wb", perm = @perm, &block)
+ raise ClosedError, "Cannot reopen closed file" if @closed
+ SharedHelpers.filesystem_access(path, :write) do
+ path.open(write_mode, perm) do |f|
+ yield digests? ? Gem::Package::DigestIO.new(f, @digests) : f
+ end
+ end
+ end
+
+ # Returns false without appending when no digests since appending is too error prone to do without digests.
+ def append(data)
+ return false unless digests?
+ open("a") {|f| f.write data }
+ verify && commit
+ end
+
+ def write(data)
+ reset_digests
+ open {|f| f.write data }
+ commit!
+ end
+
+ def commit!
+ verify || raise(DigestMismatchError.new(@base64digests, @expected_digests))
+ commit
+ end
+
+ # Verify the digests, returning true on match, false on mismatch.
+ def verify
+ return true unless @expected_digests && digests?
+ @base64digests = @digests.transform_values!(&:base64digest)
+ @digests = nil
+ @base64digests.all? {|algo, digest| @expected_digests[algo] == digest }
+ end
+
+ # Replace the original file with the temp file without verifying digests.
+ # The file is permanently closed.
+ def commit
+ raise ClosedError, "Cannot commit closed file" if @closed
+ SharedHelpers.filesystem_access(original_path, :write) do
+ FileUtils.mv(path, original_path)
+ end
+ @closed = true
+ end
+
+ # Remove the temp file without replacing the original file.
+ # The file is permanently closed.
+ def close
+ return if @closed
+ FileUtils.remove_file(path) if @path&.file?
+ @closed = true
+ end
+ end
+ end
+end
diff --git a/lib/bundler/compact_index_client/parser.rb b/lib/bundler/compact_index_client/parser.rb
new file mode 100644
index 0000000000..ad0d17ed4a
--- /dev/null
+++ b/lib/bundler/compact_index_client/parser.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CompactIndexClient
+ class Parser
+ # `compact_index` - an object responding to #names, #versions, #info(name, checksum),
+ # returning the file contents as a string
+ def initialize(compact_index)
+ @compact_index = compact_index
+ @info_checksums = nil
+ @versions_by_name = nil
+ @available = nil
+ @gem_parser = nil
+ end
+
+ def names
+ lines(@compact_index.names)
+ end
+
+ def versions
+ @versions_by_name ||= Hash.new {|hash, key| hash[key] = [] }
+ @info_checksums = {}
+
+ lines(@compact_index.versions).each do |line|
+ name, versions_string, checksum = line.split(" ", 3)
+ @info_checksums[name] = checksum || ""
+ versions_string.split(",") do |version|
+ delete = version.delete_prefix!("-")
+ version = version.split("-", 2).unshift(name)
+ if delete
+ @versions_by_name[name].delete(version)
+ else
+ @versions_by_name[name] << version
+ end
+ end
+ end
+
+ @versions_by_name
+ end
+
+ def info(name)
+ data = @compact_index.info(name, info_checksums[name])
+ lines(data).map {|line| gem_parser.parse(line).unshift(name) }
+ end
+
+ def available?
+ return @available unless @available.nil?
+ @available = !info_checksums.empty?
+ end
+
+ private
+
+ def info_checksums
+ @info_checksums ||= lines(@compact_index.versions).each_with_object({}) do |line, checksums|
+ parse_version_checksum(line, checksums)
+ end
+ end
+
+ def lines(data)
+ return [] if data.nil? || data.empty?
+ lines = data.split("\n")
+ header = lines.index("---")
+ header ? lines[header + 1..-1] : lines
+ end
+
+ def gem_parser
+ @gem_parser ||= Gem::Resolver::APISet::GemParser.new
+ end
+
+ # This is mostly the same as `split(" ", 3)` but it avoids allocating extra objects.
+ # This method gets called at least once for every gem when parsing versions.
+ def parse_version_checksum(line, checksums)
+ return unless (name_end = line.index(" ")) # Artifactory bug causes blank lines in artifactor index files
+ checksum_start = line.index(" ", name_end + 1)
+ return unless checksum_start
+ checksum_start += 1
+
+ checksum_end = line.size - checksum_start
+
+ line.freeze # allows slicing into the string to not allocate a copy of the line
+ name = line[0, name_end]
+ checksum = line[checksum_start, checksum_end]
+ checksums[name.freeze] = checksum # freeze name since it is used as a hash key
+ end
+ end
+ end
+end
diff --git a/lib/bundler/compact_index_client/updater.rb b/lib/bundler/compact_index_client/updater.rb
new file mode 100644
index 0000000000..6066fdc7c4
--- /dev/null
+++ b/lib/bundler/compact_index_client/updater.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CompactIndexClient
+ class Updater
+ class MismatchedChecksumError < Error
+ def initialize(path, message)
+ super "The checksum of /#{path} does not match the checksum provided by the server! Something is wrong. #{message}"
+ end
+ end
+
+ def initialize(fetcher)
+ @fetcher = fetcher
+ end
+
+ def update(remote_path, local_path, etag_path)
+ append(remote_path, local_path, etag_path) || replace(remote_path, local_path, etag_path)
+ rescue CacheFile::DigestMismatchError => e
+ raise MismatchedChecksumError.new(remote_path, e.message)
+ rescue Zlib::GzipFile::Error
+ raise Bundler::HTTPError
+ end
+
+ private
+
+ def append(remote_path, local_path, etag_path)
+ return false unless local_path.file? && local_path.size.nonzero?
+
+ CacheFile.copy(local_path) do |file|
+ etag = etag_path.read.tap(&:chomp!) if etag_path.file?
+
+ # Subtract a byte to ensure the range won't be empty.
+ # Avoids 416 (Range Not Satisfiable) responses.
+ response = @fetcher.call(remote_path, request_headers(etag, file.size - 1))
+ break true if response.is_a?(Gem::Net::HTTPNotModified)
+
+ file.digests = parse_digests(response)
+ # server may ignore Range and return the full response
+ if response.is_a?(Gem::Net::HTTPPartialContent)
+ tail = response.body.byteslice(1..-1)
+ break false unless tail && file.append(tail)
+ else
+ file.write(response.body)
+ end
+ CacheFile.write(etag_path, etag_from_response(response))
+ true
+ end
+ end
+
+ # request without range header to get the full file or a 304 Not Modified
+ def replace(remote_path, local_path, etag_path)
+ etag = etag_path.read.tap(&:chomp!) if etag_path.file?
+ response = @fetcher.call(remote_path, request_headers(etag))
+ return true if response.is_a?(Gem::Net::HTTPNotModified)
+ CacheFile.write(local_path, response.body, parse_digests(response))
+ CacheFile.write(etag_path, etag_from_response(response))
+ end
+
+ def request_headers(etag, range_start = nil)
+ headers = {}
+ headers["Range"] = "bytes=#{range_start}-" if range_start
+ headers["If-None-Match"] = %("#{etag}") if etag
+ headers
+ end
+
+ def etag_for_request(etag_path)
+ etag_path.read.tap(&:chomp!) if etag_path.file?
+ end
+
+ def etag_from_response(response)
+ return unless response["ETag"]
+ etag = response["ETag"].delete_prefix("W/")
+ return if etag.delete_prefix!('"') && !etag.delete_suffix!('"')
+ etag
+ end
+
+ # Unwraps and returns a Hash of digest algorithms and base64 values
+ # according to RFC 8941 Structured Field Values for HTTP.
+ # https://www.rfc-editor.org/rfc/rfc8941#name-parsing-a-byte-sequence
+ # Ignores unsupported algorithms.
+ def parse_digests(response)
+ return unless header = response["Repr-Digest"] || response["Digest"]
+ digests = {}
+ header.split(",") do |param|
+ algorithm, value = param.split("=", 2)
+ algorithm.strip!
+ algorithm.downcase!
+ next unless SUPPORTED_DIGESTS.key?(algorithm)
+ next unless value = byte_sequence(value)
+ digests[algorithm] = value
+ end
+ digests.empty? ? nil : digests
+ end
+
+ # Unwrap surrounding colons (byte sequence)
+ # The wrapping characters must be matched or we return nil.
+ # Also handles quotes because right now rubygems.org sends them.
+ def byte_sequence(value)
+ return if value.delete_prefix!(":") && !value.delete_suffix!(":")
+ return if value.delete_prefix!('"') && !value.delete_suffix!('"')
+ value
+ end
+ end
+ end
+end
diff --git a/lib/bundler/constants.rb b/lib/bundler/constants.rb
new file mode 100644
index 0000000000..9564771e78
--- /dev/null
+++ b/lib/bundler/constants.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require "rbconfig"
+
+module Bundler
+ WINDOWS = RbConfig::CONFIG["host_os"] =~ /(msdos|mswin|djgpp|mingw)/
+ deprecate_constant :WINDOWS
+
+ FREEBSD = RbConfig::CONFIG["host_os"].to_s.include?("bsd")
+ deprecate_constant :FREEBSD
+
+ NULL = File::NULL
+ deprecate_constant :NULL
+end
diff --git a/lib/bundler/current_ruby.rb b/lib/bundler/current_ruby.rb
new file mode 100644
index 0000000000..17c7655adb
--- /dev/null
+++ b/lib/bundler/current_ruby.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require_relative "rubygems_ext"
+
+module Bundler
+ # Returns current version of Ruby
+ #
+ # @return [CurrentRuby] Current version of Ruby
+ def self.current_ruby
+ @current_ruby ||= CurrentRuby.new
+ end
+
+ class CurrentRuby
+ ALL_RUBY_VERSIONS = [*18..27, *30..34, *40..41].freeze
+ KNOWN_MINOR_VERSIONS = ALL_RUBY_VERSIONS.map {|v| v.digits.reverse.join(".") }.freeze
+ KNOWN_MAJOR_VERSIONS = ALL_RUBY_VERSIONS.map {|v| v.digits.last.to_s }.uniq.freeze
+ PLATFORM_MAP = {
+ ruby: [Gem::Platform::RUBY, CurrentRuby::ALL_RUBY_VERSIONS],
+ mri: [Gem::Platform::RUBY, CurrentRuby::ALL_RUBY_VERSIONS],
+ rbx: [Gem::Platform::RUBY],
+ truffleruby: [Gem::Platform::RUBY],
+ jruby: [Gem::Platform::JAVA, [18, 19]],
+ windows: [Gem::Platform::WINDOWS, CurrentRuby::ALL_RUBY_VERSIONS],
+ # deprecated
+ mswin: [Gem::Platform::MSWIN, CurrentRuby::ALL_RUBY_VERSIONS],
+ mswin64: [Gem::Platform::MSWIN64, CurrentRuby::ALL_RUBY_VERSIONS - [18]],
+ mingw: [Gem::Platform::UNIVERSAL_MINGW, CurrentRuby::ALL_RUBY_VERSIONS],
+ x64_mingw: [Gem::Platform::UNIVERSAL_MINGW, CurrentRuby::ALL_RUBY_VERSIONS - [18, 19]],
+ }.each_with_object({}) do |(platform, spec), hash|
+ hash[platform] = spec[0]
+ spec[1]&.each {|version| hash[:"#{platform}_#{version}"] = spec[0] }
+ end.freeze
+
+ def ruby?
+ return true if Bundler::MatchPlatform.generic_local_platform_is_ruby?
+
+ !windows? && (RUBY_ENGINE == "ruby" || RUBY_ENGINE == "rbx" || RUBY_ENGINE == "maglev" || RUBY_ENGINE == "truffleruby")
+ end
+
+ def mri?
+ !windows? && RUBY_ENGINE == "ruby"
+ end
+
+ def rbx?
+ ruby? && RUBY_ENGINE == "rbx"
+ end
+
+ def jruby?
+ RUBY_ENGINE == "jruby"
+ end
+
+ def maglev?
+ removed_message =
+ "`CurrentRuby#maglev?` was removed with no replacement. Please use the " \
+ "built-in Ruby `RUBY_ENGINE` constant to check the Ruby implementation you are running on."
+ SharedHelpers.feature_removed!(removed_message)
+ end
+
+ def truffleruby?
+ RUBY_ENGINE == "truffleruby"
+ end
+
+ def windows?
+ Gem.win_platform?
+ end
+ alias_method :mswin?, :windows?
+ alias_method :mswin64?, :windows?
+ alias_method :mingw?, :windows?
+ alias_method :x64_mingw?, :windows?
+
+ (KNOWN_MINOR_VERSIONS + KNOWN_MAJOR_VERSIONS).each do |version|
+ trimmed_version = version.tr(".", "")
+ define_method(:"on_#{trimmed_version}?") do
+ RUBY_VERSION.start_with?("#{version}.")
+ end
+
+ PLATFORM_MAP.keys.each do |platform|
+ define_method(:"#{platform}_#{trimmed_version}?") do
+ send(:"#{platform}?") && send(:"on_#{trimmed_version}?")
+ end
+ end
+
+ define_method(:"maglev_#{trimmed_version}?") do
+ removed_message =
+ "`CurrentRuby##{__method__}` was removed with no replacement. Please use the " \
+ "built-in Ruby `RUBY_ENGINE` and `RUBY_VERSION` constants to perform a similar check."
+
+ SharedHelpers.feature_removed!(removed_message)
+
+ send(:"maglev?") && send(:"on_#{trimmed_version}?")
+ end
+ end
+ end
+end
diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb
new file mode 100644
index 0000000000..7a95671471
--- /dev/null
+++ b/lib/bundler/definition.rb
@@ -0,0 +1,1329 @@
+# frozen_string_literal: true
+
+require_relative "lockfile_parser"
+require_relative "worker"
+
+module Bundler
+ class Definition
+ class << self
+ # Do not create or modify a lockfile (Makes #lock a noop)
+ attr_accessor :no_lock
+ end
+
+ attr_writer :lockfile, :overrides
+
+ attr_reader(
+ :dependencies,
+ :locked_checksums,
+ :locked_deps,
+ :locked_gems,
+ :overrides,
+ :platforms,
+ :ruby_version,
+ :lockfile,
+ :gemfiles,
+ :sources
+ )
+
+ # Given a gemfile and lockfile creates a Bundler definition
+ #
+ # @param gemfile [Pathname] Path to Gemfile
+ # @param lockfile [Pathname,nil] Path to Gemfile.lock
+ # @param unlock [Hash, Boolean, nil] Gems that have been requested
+ # to be updated or true if all gems should be updated
+ # @return [Bundler::Definition]
+ def self.build(gemfile, lockfile, unlock)
+ unlock ||= {}
+ gemfile = Pathname.new(gemfile).expand_path
+
+ raise GemfileNotFound, "#{gemfile} not found" unless gemfile.file?
+
+ Plugin.hook(Plugin::Events::GEM_BEFORE_EVAL, gemfile, lockfile)
+ Dsl.evaluate(gemfile, lockfile, unlock).tap do |definition|
+ Plugin.hook(Plugin::Events::GEM_AFTER_EVAL, definition)
+ end
+ end
+
+ #
+ # How does the new system work?
+ #
+ # * Load information from Gemfile and Lockfile
+ # * Invalidate stale locked specs
+ # * All specs from stale source are stale
+ # * All specs that are reachable only through a stale
+ # dependency are stale.
+ # * If all fresh dependencies are satisfied by the locked
+ # specs, then we can try to resolve locally.
+ #
+ # @param lockfile [Pathname] Path to Gemfile.lock
+ # @param dependencies [Array(Bundler::Dependency)] array of dependencies from Gemfile
+ # @param sources [Bundler::SourceList]
+ # @param unlock [Hash, Boolean, nil] Gems that have been requested
+ # to be updated or true if all gems should be updated
+ # @param ruby_version [Bundler::RubyVersion, nil] Requested Ruby Version
+ # @param optional_groups [Array(String)] A list of optional groups
+ def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [], gemfiles = [], overrides = [])
+ unlock ||= {}
+
+ if unlock == true
+ @unlocking_all = true
+ strict = false
+ @unlocking_bundler = false
+ @unlocking = unlock
+ @sources_to_unlock = []
+ @unlocking_ruby = false
+ @explicit_unlocks = []
+ conservative = false
+ else
+ @unlocking_all = false
+ strict = unlock.delete(:strict)
+ @unlocking_bundler = unlock.delete(:bundler)
+ @unlocking = unlock.any? {|_k, v| !Array(v).empty? }
+ @sources_to_unlock = unlock.delete(:sources) || []
+ @unlocking_ruby = unlock.delete(:ruby)
+ @explicit_unlocks = unlock.delete(:gems) || []
+ conservative = unlock.delete(:conservative)
+ end
+
+ @dependencies = dependencies
+ @sources = sources
+ @optional_groups = optional_groups
+ @prefer_local = false
+ @specs = nil
+ @ruby_version = ruby_version
+ @gemfiles = gemfiles
+ @overrides = overrides
+
+ @lockfile = lockfile
+ @lockfile_contents = String.new
+
+ @locked_bundler_version = nil
+ @resolved_bundler_version = nil
+
+ @locked_ruby_version = nil
+ @new_platforms = []
+ @removed_platforms = []
+ @originally_invalid_platforms = []
+
+ if lockfile_exists?
+ @lockfile_contents = Bundler.read_file(lockfile)
+ @locked_gems = LockfileParser.new(@lockfile_contents, strict: strict)
+ @locked_platforms = @locked_gems.platforms
+ @most_specific_locked_platform = @locked_gems.most_specific_locked_platform
+ @platforms = @locked_platforms.dup
+ @locked_bundler_version = @locked_gems.bundler_version
+ @locked_ruby_version = @locked_gems.ruby_version
+ @locked_deps = @locked_gems.dependencies
+ Override.attach(@locked_gems.specs, @overrides)
+ @originally_locked_specs = SpecSet.new(@locked_gems.specs)
+ @originally_locked_sources = @locked_gems.sources
+ @locked_checksums = @locked_gems.checksums
+
+ if @unlocking_all
+ @locked_specs = SpecSet.new([])
+ @locked_sources = []
+ else
+ @locked_specs = @originally_locked_specs
+ @locked_sources = @originally_locked_sources
+ end
+
+ locked_gem_sources = @originally_locked_sources.select {|s| s.is_a?(Source::Rubygems) }
+ multisource_lockfile = locked_gem_sources.size == 1 && locked_gem_sources.first.multiple_remotes?
+
+ if multisource_lockfile
+ msg = "Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. Make sure you run `bundle install` in non frozen mode and commit the result to make your lockfile secure."
+
+ Bundler::SharedHelpers.feature_removed! msg
+ end
+ else
+ @locked_gems = nil
+ @locked_platforms = []
+ @most_specific_locked_platform = nil
+ @platforms = []
+ @locked_deps = {}
+ @locked_specs = SpecSet.new([])
+ @locked_sources = []
+ @originally_locked_specs = @locked_specs
+ @originally_locked_sources = @locked_sources
+ @locked_checksums = Bundler.settings[:lockfile_checksums]
+ end
+
+ @unlocking_ruby ||= if @ruby_version && locked_ruby_version_object
+ @ruby_version.diff(locked_ruby_version_object)
+ end
+ @unlocking ||= @unlocking_ruby ||= (!@locked_ruby_version ^ !@ruby_version)
+
+ @current_platform_missing = add_current_platform unless Bundler.frozen_bundle?
+
+ @source_changes = converge_sources
+ @path_changes = converge_paths
+
+ if conservative
+ @gems_to_unlock = @explicit_unlocks.any? ? @explicit_unlocks : @dependencies.map(&:name)
+ else
+ eager_unlock = @explicit_unlocks.map {|name| Dependency.new(name, ">= 0") }
+ @gems_to_unlock = @locked_specs.for(eager_unlock, platforms).map(&:name).uniq
+ end
+
+ @dependency_changes = converge_dependencies
+ @local_changes = converge_locals
+
+ check_lockfile
+ end
+
+ def gem_version_promoter
+ @gem_version_promoter ||= GemVersionPromoter.new
+ end
+
+ def check!
+ # If dependencies have changed, we need to resolve remotely. Otherwise,
+ # since we'll be resolving with a single local source, we may end up
+ # locking gems under the wrong source in the lockfile, and missing lockfile
+ # checksums
+ resolve_remotely! if @dependency_changes
+
+ # Now do a local only resolve, to verify if any gems are missing locally
+ sources.local_only!
+ resolve
+ end
+
+ #
+ # Setup sources according to the given options and the state of the
+ # definition.
+ #
+ # @return [Boolean] Whether fetching remote information will be necessary or not
+ #
+ def setup_domain!(options = {})
+ prefer_local! if options[:"prefer-local"]
+
+ sources.cached!
+
+ if options[:add_checksums] || (!options[:local] && install_needed?)
+ sources.remote!
+ true
+ else
+ Bundler.settings.set_command_option(:jobs, 1) unless install_needed? # to avoid the overhead of Bundler::Worker
+ sources.local!
+ false
+ end
+ end
+
+ def resolve_with_cache!
+ with_cache!
+
+ resolve
+ end
+
+ def with_cache!
+ sources.local!
+ sources.cached!
+ end
+
+ def resolve_remotely!
+ remotely!
+
+ resolve
+ end
+
+ def remotely!
+ sources.cached!
+ sources.remote!
+ end
+
+ def prefer_local!
+ @prefer_local = true
+
+ sources.prefer_local!
+ end
+
+ # For given dependency list returns a SpecSet with Gemspec of all the required
+ # dependencies.
+ # 1. The method first resolves the dependencies specified in Gemfile
+ # 2. After that it tries and fetches gemspec of resolved dependencies
+ #
+ # @return [Bundler::SpecSet]
+ def specs
+ @specs ||= materialize(requested_dependencies)
+ end
+
+ def new_specs
+ specs - @locked_specs
+ end
+
+ def removed_specs
+ @locked_specs - specs
+ end
+
+ def missing_specs
+ preload_git_sources
+ resolve.missing_specs_for(requested_dependencies)
+ end
+
+ def missing_specs?
+ missing = missing_specs
+ return false if missing.empty?
+ Bundler.ui.debug "The definition is missing #{missing.map(&:full_name)}"
+ true
+ rescue BundlerError => e
+ @resolve = nil
+ @resolver = nil
+ @resolution_base = nil
+ @source_requirements = nil
+ @specs = nil
+
+ Bundler.ui.debug "The definition is missing dependencies, failed to resolve & materialize locally (#{e})"
+ true
+ end
+
+ def requested_specs
+ specs_for(requested_groups)
+ end
+
+ def requested_dependencies
+ dependencies_for(requested_groups)
+ end
+
+ def current_dependencies
+ filter_relevant(dependencies)
+ end
+
+ def current_locked_dependencies
+ filter_relevant(locked_dependencies)
+ end
+
+ def filter_relevant(dependencies)
+ dependencies.select do |d|
+ relevant_deps?(d)
+ end
+ end
+
+ def relevant_deps?(dep)
+ platforms_array = [Bundler.generic_local_platform].freeze
+
+ dep.should_include? && !dep.gem_platforms(platforms_array).empty?
+ end
+
+ def locked_dependencies
+ @locked_deps.values
+ end
+
+ def new_deps
+ @new_deps ||= @dependencies - locked_dependencies
+ end
+
+ def deleted_deps
+ @deleted_deps ||= locked_dependencies - @dependencies
+ end
+
+ def specs_for(groups)
+ return specs if groups.empty?
+ deps = dependencies_for(groups)
+ materialize(deps)
+ end
+
+ def dependencies_for(groups)
+ groups.map!(&:to_sym)
+ deps = current_dependencies # always returns a new array
+ deps.select! do |d|
+ d.groups.intersect?(groups)
+ end
+ deps
+ end
+
+ # Resolve all the dependencies specified in Gemfile. It ensures that
+ # dependencies that have been already resolved via locked file and are fresh
+ # are reused when resolving dependencies
+ #
+ # @return [SpecSet] resolved dependencies
+ def resolve
+ @resolve ||= if Bundler.frozen_bundle?
+ Bundler.ui.debug "Frozen, using resolution from the lockfile"
+ @locked_specs
+ elsif no_resolve_needed?
+ if deleted_deps.any?
+ Bundler.ui.debug "Some dependencies were deleted, using a subset of the resolution from the lockfile"
+ SpecSet.new(filter_specs(@locked_specs, @dependencies - deleted_deps))
+ else
+ Bundler.ui.debug "Found no changes, using resolution from the lockfile"
+ if @removed_platforms.any? || @locked_gems.may_include_redundant_platform_specific_gems?
+ SpecSet.new(filter_specs(@locked_specs, @dependencies))
+ else
+ @locked_specs
+ end
+ end
+ else
+ Bundler.ui.debug resolve_needed_reason
+
+ start_resolution
+ end
+ end
+
+ def spec_git_paths
+ sources.git_sources.filter_map {|s| File.realpath(s.path) if File.exist?(s.path) }
+ end
+
+ def groups
+ dependencies.flat_map(&:groups).uniq
+ end
+
+ def lock(file_or_preserve_unknown_sections = false, preserve_unknown_sections_or_unused = false)
+ if [true, false, nil].include?(file_or_preserve_unknown_sections)
+ target_lockfile = lockfile
+ preserve_unknown_sections = file_or_preserve_unknown_sections
+ else
+ target_lockfile = file_or_preserve_unknown_sections
+ preserve_unknown_sections = preserve_unknown_sections_or_unused
+
+ suggestion = if target_lockfile == lockfile
+ "To fix this warning, remove it from the `Definition#lock` call."
+ else
+ "Instead, instantiate a new definition passing `#{target_lockfile}`, and call `lock` without a file argument on that definition"
+ end
+
+ msg = "`Definition#lock` was passed a target file argument. #{suggestion}"
+
+ Bundler::SharedHelpers.feature_removed! msg
+ end
+
+ write_lock(target_lockfile, preserve_unknown_sections)
+ end
+
+ def write_lock(file, preserve_unknown_sections)
+ return if Definition.no_lock || !lockfile || file.nil?
+
+ contents = to_lock
+
+ # Convert to \r\n if the existing lock has them
+ # i.e., Windows with `git config core.autocrlf=true`
+ contents.gsub!(/\n/, "\r\n") if @lockfile_contents.match?("\r\n")
+
+ if @locked_bundler_version
+ locked_major = @locked_bundler_version.segments.first
+ current_major = bundler_version_to_lock.segments.first
+
+ updating_major = locked_major < current_major
+ end
+
+ preserve_unknown_sections ||= !updating_major && (Bundler.frozen_bundle? || !(unlocking? || @unlocking_bundler))
+
+ if File.exist?(file) && lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections)
+ return if Bundler.frozen_bundle?
+ SharedHelpers.filesystem_access(file) { FileUtils.touch(file) }
+ return
+ end
+
+ if Bundler.frozen_bundle?
+ Bundler.ui.error "Cannot write a changed lockfile while frozen."
+ return
+ end
+
+ begin
+ SharedHelpers.filesystem_access(file) do |p|
+ File.open(p, "wb") {|f| f.puts(contents) }
+ end
+ rescue ReadOnlyFileSystemError
+ raise ProductionError, lockfile_changes_summary("file system is read-only")
+ end
+ end
+
+ def locked_ruby_version
+ return unless ruby_version
+ if @unlocking_ruby || !@locked_ruby_version
+ Bundler::RubyVersion.system
+ else
+ @locked_ruby_version
+ end
+ end
+
+ def locked_ruby_version_object
+ return unless @locked_ruby_version
+ @locked_ruby_version_object ||= begin
+ unless version = RubyVersion.from_string(@locked_ruby_version)
+ raise LockfileError, "The Ruby version #{@locked_ruby_version} from " \
+ "#{@lockfile} could not be parsed. " \
+ "Try running bundle update --ruby to resolve this."
+ end
+ version
+ end
+ end
+
+ def bundler_version_to_lock
+ @resolved_bundler_version || Bundler.gem_version
+ end
+
+ def to_lock
+ require_relative "lockfile_generator"
+ LockfileGenerator.generate(self)
+ end
+
+ def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false)
+ return unless Bundler.frozen_bundle?
+
+ raise ProductionError, "Frozen mode is set, but there's no lockfile" unless lockfile_exists?
+
+ msg = lockfile_changes_summary("frozen mode is set")
+ return unless msg
+
+ unless explicit_flag
+ suggested_command = unless Bundler.settings.locations("frozen").keys.include?(:env)
+ "bundle config set frozen false"
+ end
+ msg << "\n\nIf this is a development machine, remove the #{SharedHelpers.relative_lockfile_path} " \
+ "freeze by running `#{suggested_command}`." if suggested_command
+ end
+
+ raise ProductionError, msg
+ end
+
+ def validate_runtime!
+ validate_ruby!
+ validate_platforms!
+ end
+
+ def validate_ruby!
+ return unless ruby_version
+
+ if diff = ruby_version.diff(Bundler::RubyVersion.system)
+ problem, expected, actual = diff
+
+ msg = case problem
+ when :engine
+ "Your Ruby engine is #{actual}, but your Gemfile specified #{expected}"
+ when :version
+ "Your Ruby version is #{actual}, but your Gemfile specified #{expected}"
+ when :engine_version
+ "Your #{Bundler::RubyVersion.system.engine} version is #{actual}, but your Gemfile specified #{ruby_version.engine} #{expected}"
+ end
+
+ raise RubyVersionMismatch, msg
+ end
+ end
+
+ def validate_platforms!
+ return if current_platform_locked? || @platforms.include?(Gem::Platform::RUBY)
+
+ raise ProductionError, "Your bundle only supports platforms #{@platforms.map(&:to_s)} " \
+ "but your local platform is #{Bundler.local_platform}. " \
+ "Add the current platform to the lockfile with\n`bundle lock --add-platform #{Bundler.local_platform}` and try again."
+ end
+
+ def normalize_platforms
+ resolve.normalize_platforms!(current_dependencies, platforms)
+
+ @resolve = SpecSet.new(resolve.for(current_dependencies, @platforms))
+ end
+
+ def add_platform(platform)
+ return if @platforms.include?(platform)
+
+ @new_platforms << platform
+ @platforms << platform
+ end
+
+ def remove_platform(platform)
+ raise InvalidOption, "Unable to remove the platform `#{platform}` since the only platforms are #{@platforms.join ", "}" unless @platforms.include?(platform)
+
+ @removed_platforms << platform
+ @platforms.delete(platform)
+ end
+
+ def nothing_changed?
+ !something_changed?
+ end
+
+ def no_resolve_needed?
+ !resolve_needed?
+ end
+
+ def unlocking?
+ @unlocking
+ end
+
+ def add_checksums
+ require "rubygems/package"
+
+ @locked_checksums = true
+
+ setup_domain!(add_checksums: true)
+
+ # force materialization to real specifications, so that checksums are fetched
+ specs.each do |spec|
+ next unless spec.source.is_a?(Bundler::Source::Rubygems)
+ # Checksum was fetched from the compact index API.
+ next if !spec.source.checksum_store.missing?(spec) && !spec.source.checksum_store.empty?(spec)
+ # The gem isn't installed, can't compute the checksum.
+ next unless spec.loaded_from
+
+ package = Gem::Package.new(spec.source.cached_built_in_gem(spec))
+ checksum = Checksum.from_gem_package(package)
+ spec.source.checksum_store.register(spec, checksum)
+ end
+ end
+
+ private
+
+ def lockfile_changes_summary(update_refused_reason)
+ added = []
+ deleted = []
+ changed = []
+
+ added.concat @new_platforms.map {|p| "* platform: #{p}" }
+ deleted.concat @removed_platforms.map {|p| "* platform: #{p}" }
+
+ added.concat new_deps.map {|d| "* #{pretty_dep(d)}" } if new_deps.any?
+ deleted.concat deleted_deps.map {|d| "* #{pretty_dep(d)}" } if deleted_deps.any?
+
+ both_sources = Hash.new {|h, k| h[k] = [] }
+ current_dependencies.each {|d| both_sources[d.name][0] = d }
+ current_locked_dependencies.each {|d| both_sources[d.name][1] = d }
+
+ both_sources.each do |name, (dep, lock_dep)|
+ next if dep.nil? || lock_dep.nil?
+
+ gemfile_source = dep.source || default_source
+ lock_source = lock_dep.source || default_source
+ next if lock_source.include?(gemfile_source)
+
+ gemfile_source_name = dep.source ? gemfile_source.to_gemfile : "no specified source"
+ lockfile_source_name = lock_dep.source ? lock_source.to_gemfile : "no specified source"
+ changed << "* #{name} from `#{lockfile_source_name}` to `#{gemfile_source_name}`"
+ end
+
+ return unless added.any? || deleted.any? || changed.any? || resolve_needed?
+
+ msg = String.new("#{change_reason[0].upcase}#{change_reason[1..-1].strip}, but ")
+ msg << "the lockfile " unless msg.start_with?("Your lockfile")
+ msg << "can't be updated because #{update_refused_reason}"
+ msg << "\n\nYou have added to the Gemfile:\n" << added.join("\n") if added.any?
+ msg << "\n\nYou have deleted from the Gemfile:\n" << deleted.join("\n") if deleted.any?
+ msg << "\n\nYou have changed in the Gemfile:\n" << changed.join("\n") if changed.any?
+ msg << "\n\nRun `bundle install` elsewhere and add the updated #{SharedHelpers.relative_lockfile_path} to version control.\n" unless unlocking?
+ msg
+ end
+
+ def install_needed?
+ resolve_needed? || missing_specs?
+ end
+
+ def something_changed?
+ return true unless lockfile_exists?
+
+ @source_changes ||
+ @dependency_changes ||
+ @current_platform_missing ||
+ @new_platforms.any? ||
+ @path_changes ||
+ @local_changes ||
+ @missing_lockfile_dep ||
+ @unlocking_bundler ||
+ @locked_spec_with_missing_checksums ||
+ @locked_spec_with_empty_checksums ||
+ @locked_spec_with_missing_deps ||
+ @locked_spec_with_invalid_deps
+ end
+
+ def resolve_needed?
+ unlocking? || something_changed?
+ end
+
+ def should_add_extra_platforms?
+ !lockfile_exists? && Bundler::MatchPlatform.generic_local_platform_is_ruby? && !Bundler.settings[:force_ruby_platform]
+ end
+
+ def lockfile_exists?
+ lockfile && File.exist?(lockfile)
+ end
+
+ def resolver
+ @resolver ||= new_resolver(resolution_base)
+ end
+
+ def expanded_dependencies
+ apply_overrides_to(dependencies_with_bundler) + metadata_dependencies
+ end
+
+ def apply_overrides_to(deps)
+ return deps if @overrides.empty?
+ deps.map {|dep| apply_override_to(dep) }
+ end
+
+ def apply_override_to(dep)
+ override = Override.find_for(@overrides, dep.name, :version)
+ return dep unless override
+ new_dep = dep.dup
+ new_dep.instance_variable_set(:@requirement, override.apply_to(dep.requirement))
+ new_dep
+ end
+
+ def dependencies_with_bundler
+ return dependencies unless @unlocking_bundler
+ return dependencies if dependencies.any? {|d| d.name == "bundler" }
+
+ [Dependency.new("bundler", @unlocking_bundler)] + dependencies
+ end
+
+ def resolution_base
+ @resolution_base ||= begin
+ last_resolve = converge_locked_specs
+ remove_invalid_platforms!
+ base = new_resolution_base(last_resolve: last_resolve, unlock: @unlocking_all || @gems_to_unlock)
+ base = additional_base_requirements_to_prevent_downgrades(base)
+ base = additional_base_requirements_to_force_updates(base)
+ base
+ end
+ end
+
+ def filter_specs(specs, deps, skips: [])
+ SpecSet.new(specs).for(deps, platforms, skips: skips)
+ end
+
+ def materialize(dependencies)
+ specs = begin
+ resolve.materialize(dependencies)
+ rescue IncorrectLockfileDependencies => e
+ raise if Bundler.frozen_bundle?
+
+ reresolve_without([e.spec])
+ retry
+ end
+
+ missing_specs = resolve.missing_specs
+
+ if missing_specs.any?
+ missing_specs.each do |s|
+ locked_gem = @locked_specs[s.name].last
+ next if locked_gem.nil? || locked_gem.version != s.version || sources.local_mode?
+
+ message = if sources.implicit_global_source?
+ "Because your Gemfile specifies no global remote source, your bundle is locked to " \
+ "#{locked_gem} from #{locked_gem.source}. However, #{locked_gem} is not installed. You'll " \
+ "need to either add a global remote source to your Gemfile or make sure #{locked_gem} is " \
+ "available locally before rerunning Bundler."
+ else
+ "Your bundle is locked to #{locked_gem} from #{locked_gem.source}, but that version can " \
+ "no longer be found in that source. That means the author of #{locked_gem} has removed it. " \
+ "You'll need to update your bundle to a version other than #{locked_gem} that hasn't been " \
+ "removed in order to install."
+ end
+
+ raise GemNotFound, message
+ end
+
+ missing_specs_list = missing_specs.group_by(&:source).map do |source, missing_specs_for_source|
+ "#{missing_specs_for_source.map(&:full_name).join(", ")} in #{source}"
+ end
+
+ raise GemNotFound, "Could not find #{missing_specs_list.join(" nor ")}"
+ end
+
+ partially_missing_specs = resolve.partially_missing_specs
+
+ if partially_missing_specs.any? && !sources.local_mode?
+ Bundler.ui.warn "Some locked specs have possibly been yanked (#{partially_missing_specs.map(&:full_name).join(", ")}). Ignoring them..."
+
+ resolve.delete(partially_missing_specs)
+ end
+
+ incomplete_specs = resolve.incomplete_specs
+ loop do
+ break if incomplete_specs.empty?
+
+ Bundler.ui.debug("The lockfile does not have all gems needed for the current platform though, Bundler will still re-resolve dependencies")
+ sources.remote!
+ reresolve_without(incomplete_specs)
+ specs = resolve.materialize(dependencies)
+
+ still_incomplete_specs = resolve.incomplete_specs
+
+ if still_incomplete_specs == incomplete_specs
+ resolver.raise_incomplete! incomplete_specs
+ end
+
+ incomplete_specs = still_incomplete_specs
+ end
+
+ insecurely_materialized_specs = resolve.insecurely_materialized_specs
+
+ if insecurely_materialized_specs.any?
+ Bundler.ui.warn "The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version:\n" \
+ " * #{insecurely_materialized_specs.map(&:full_name).join("\n * ")}\n" \
+ "Please run `bundle lock --normalize-platforms` and commit the resulting lockfile.\n" \
+ "Alternatively, you may run `bundle lock --add-platform <list-of-platforms-that-you-want-to-support>`"
+ end
+
+ bundler = sources.metadata_source.specs.search(["bundler", Bundler.gem_version]).last
+ specs["bundler"] = bundler
+
+ specs
+ end
+
+ def reresolve_without(incomplete_specs)
+ resolution_base.delete(incomplete_specs)
+ @resolve = start_resolution
+ end
+
+ def start_resolution
+ local_platform_needed_for_resolvability = @most_specific_non_local_locked_platform && !@platforms.include?(Bundler.local_platform)
+ @platforms << Bundler.local_platform if local_platform_needed_for_resolvability
+
+ result = SpecSet.new(resolver.start)
+
+ @resolved_bundler_version = result.find {|spec| spec.name == "bundler" }&.version
+
+ @new_platforms.each do |platform|
+ incomplete_specs = result.incomplete_specs_for_platform(current_dependencies, platform)
+
+ if incomplete_specs.any?
+ resolver.raise_incomplete! incomplete_specs
+ end
+ end
+
+ if @most_specific_non_local_locked_platform
+ if result.incomplete_for_platform?(current_dependencies, @most_specific_non_local_locked_platform)
+ @platforms.delete(@most_specific_non_local_locked_platform)
+ elsif local_platform_needed_for_resolvability
+ @platforms.delete(Bundler.local_platform)
+ end
+ end
+
+ if should_add_extra_platforms?
+ result.add_extra_platforms!(platforms)
+ elsif @originally_invalid_platforms.any?
+ result.add_originally_invalid_platforms!(platforms, @originally_invalid_platforms)
+ end
+
+ SpecSet.new(result.for(dependencies, @platforms | [Gem::Platform::RUBY]))
+ end
+
+ def precompute_source_requirements_for_indirect_dependencies?
+ if sources.non_global_rubygems_sources.all?(&:dependency_api_available?)
+ true
+ else
+ non_dependency_api_warning
+ false
+ end
+ end
+
+ def non_dependency_api_warning
+ non_api_sources = sources.non_global_rubygems_sources.reject(&:dependency_api_available?)
+ non_api_source_names = non_api_sources.map {|d| " * #{d}" }.join("\n")
+
+ msg = String.new
+ msg << "Your Gemfile contains scoped sources that don't implement a dependency API, namely:\n\n"
+ msg << non_api_source_names
+ msg << "\n\nUsing the above gem servers may result in installing unexpected gems. " \
+ "To resolve this warning, make sure you use gem servers that implement dependency APIs, " \
+ "such as gemstash or geminabox gem servers."
+ Bundler.ui.warn msg
+ end
+
+ def current_platform_locked?
+ @platforms.any? do |bundle_platform|
+ Bundler.generic_local_platform == bundle_platform || Bundler.local_platform === bundle_platform
+ end
+ end
+
+ def add_current_platform
+ return if @platforms.include?(Bundler.local_platform)
+
+ @most_specific_non_local_locked_platform = find_most_specific_locked_platform
+ return if @most_specific_non_local_locked_platform
+
+ @platforms << Bundler.local_platform
+ true
+ end
+
+ def find_most_specific_locked_platform
+ return unless current_platform_locked?
+
+ @most_specific_locked_platform
+ end
+
+ def resolve_needed_reason
+ if lockfile_exists?
+ if unlocking?
+ "Re-resolving dependencies because #{unlocking_reason}"
+ else
+ "Found changes from the lockfile, re-resolving dependencies because #{lockfile_changed_reason}"
+ end
+ else
+ "Resolving dependencies because there's no lockfile"
+ end
+ end
+
+ def change_reason
+ if resolve_needed?
+ if unlocking?
+ unlocking_reason
+ else
+ lockfile_changed_reason
+ end
+ else
+ "some dependencies were deleted from your gemfile"
+ end
+ end
+
+ def unlocking_reason
+ unlock_targets = if @gems_to_unlock.any?
+ ["gems", @gems_to_unlock]
+ elsif @sources_to_unlock.any?
+ ["sources", @sources_to_unlock]
+ end
+
+ unlock_reason = if unlock_targets
+ "#{unlock_targets.first}: (#{unlock_targets.last.join(", ")})"
+ else
+ @unlocking_ruby ? "ruby" : ""
+ end
+
+ "bundler is unlocking #{unlock_reason}"
+ end
+
+ def lockfile_changed_reason
+ [
+ [@source_changes, "the list of sources changed"],
+ [@dependency_changes, "the dependencies in your gemfile changed"],
+ [@current_platform_missing, "your lockfile is missing the current platform"],
+ [@new_platforms.any?, "you are adding a new platform to your lockfile"],
+ [@path_changes, "the gemspecs for path gems changed"],
+ [@local_changes, "the gemspecs for git local gems changed"],
+ [@missing_lockfile_dep, "your lockfile is missing \"#{@missing_lockfile_dep}\""],
+ [@unlocking_bundler, "an update to the version of Bundler itself was requested"],
+ [@locked_spec_with_missing_checksums, "your lockfile is missing a CHECKSUMS entry for \"#{@locked_spec_with_missing_checksums}\""],
+ [@locked_spec_with_empty_checksums, "your lockfile has an empty CHECKSUMS entry for \"#{@locked_spec_with_empty_checksums}\""],
+ [@locked_spec_with_missing_deps, "your lockfile includes \"#{@locked_spec_with_missing_deps}\" but not some of its dependencies"],
+ [@locked_spec_with_invalid_deps, "your lockfile does not satisfy dependencies of \"#{@locked_spec_with_invalid_deps}\""],
+ ].select(&:first).map(&:last).join(", ")
+ end
+
+ def pretty_dep(dep)
+ SharedHelpers.pretty_dependency(dep)
+ end
+
+ # Check if the specs of the given source changed
+ # according to the locked source.
+ def specs_changed?(source)
+ locked = @locked_sources.find {|s| s == source }
+
+ !locked || dependencies_for_source_changed?(source, locked) || specs_for_source_changed?(source)
+ end
+
+ def dependencies_for_source_changed?(source, locked_source)
+ deps_for_source = @dependencies.select {|dep| dep.source == source }
+ locked_deps_for_source = locked_dependencies.select {|dep| dep.source == locked_source }
+
+ deps_for_source.uniq.sort != locked_deps_for_source.sort
+ end
+
+ def specs_for_source_changed?(source)
+ locked_index = Index.new
+ locked_index.use(@locked_specs.select {|s| s.replace_source_with!(source) })
+
+ !locked_index.subset?(source.specs)
+ rescue PathError, GitError => e
+ Bundler.ui.debug "Assuming that #{source} has not changed since fetching its specs errored (#{e})"
+ false
+ end
+
+ # Get all locals and override their matching sources.
+ # Return true if any of the locals changed (for example,
+ # they point to a new revision) or depend on new specs.
+ def converge_locals
+ locals = []
+
+ Bundler.settings.local_overrides.map do |k, v|
+ spec = @dependencies.find {|s| s.name == k }
+ source = spec&.source
+ if source&.respond_to?(:local_override!)
+ source.unlock! if @gems_to_unlock.include?(spec.name)
+ locals << [source, source.local_override!(v)]
+ end
+ end
+
+ sources_with_changes = locals.select do |source, changed|
+ changed || specs_changed?(source)
+ end.map(&:first)
+ !sources_with_changes.each {|source| @sources_to_unlock << source.name }.empty?
+ end
+
+ def check_lockfile
+ @locked_spec_with_invalid_deps = nil
+ @locked_spec_with_missing_deps = nil
+ @locked_spec_with_missing_checksums = nil
+ @locked_spec_with_empty_checksums = nil
+
+ missing_deps = []
+ missing_checksums = []
+ empty_checksums = []
+ invalid = []
+
+ @locked_specs.each do |s|
+ if @locked_checksums
+ checksum_store = s.source.checksum_store
+
+ if checksum_store.missing?(s)
+ missing_checksums << s
+ elsif checksum_store.empty?(s)
+ empty_checksums << s
+ end
+ end
+
+ validation = @locked_specs.validate_deps(s)
+
+ missing_deps << s if validation == :missing
+ invalid << s if validation == :invalid
+ end
+
+ @locked_spec_with_missing_checksums = missing_checksums.first.name if missing_checksums.any?
+ @locked_spec_with_empty_checksums = empty_checksums.first.name if empty_checksums.any?
+
+ if missing_deps.any?
+ @locked_specs.delete(missing_deps)
+
+ @locked_spec_with_missing_deps = missing_deps.first.name
+ end
+
+ if invalid.any?
+ @locked_specs.delete(invalid)
+
+ @locked_spec_with_invalid_deps = invalid.first.name
+ end
+ end
+
+ def converge_paths
+ sources.path_sources.any? do |source|
+ specs_changed?(source)
+ end
+ end
+
+ def converge_sources
+ # Replace the sources from the Gemfile with the sources from the Gemfile.lock,
+ # if they exist in the Gemfile.lock and are `==`. If you can't find an equivalent
+ # source in the Gemfile.lock, use the one from the Gemfile.
+ changes = sources.replace_sources!(@locked_sources)
+
+ sources.all_sources.each do |source|
+ # has to be done separately, because we want to keep the locked checksum
+ # store for a source, even when doing a full update
+ if @locked_checksums && @locked_gems && locked_source = @originally_locked_sources.find {|s| s == source && !s.equal?(source) }
+ source.checksum_store.merge!(locked_source.checksum_store)
+ end
+ # If the source is unlockable and the current command allows an unlock of
+ # the source (for example, you are doing a `bundle update <foo>` of a git-pinned
+ # gem), unlock it. For git sources, this means to unlock the revision, which
+ # will cause the `ref` used to be the most recent for the branch (or master) if
+ # an explicit `ref` is not used.
+ if source.respond_to?(:unlock!) && @sources_to_unlock.include?(source.name)
+ source.unlock!
+ changes = true
+ end
+ end
+
+ sources.metadata_source.checksum_store.merge!(@locked_gems.metadata_source.checksum_store) if @locked_gems
+
+ changes
+ end
+
+ def converge_dependencies
+ @missing_lockfile_dep = nil
+ @changed_dependencies = []
+
+ @dependencies.each do |dep|
+ if dep.source
+ dep.source = sources.get(dep.source)
+ end
+ next unless relevant_deps?(dep)
+
+ name = dep.name
+
+ dep_changed = @locked_deps[name].nil?
+
+ unless name == "bundler"
+ locked_specs = @originally_locked_specs[name]
+
+ if locked_specs.empty?
+ @missing_lockfile_dep = name if dep_changed == false
+ else
+ if locked_specs.map(&:source).uniq.size > 1
+ @locked_specs.delete(locked_specs.select {|s| s.source != dep.source })
+ end
+
+ unless apply_override_to(dep).matches_spec?(locked_specs.first)
+ @gems_to_unlock << name
+ dep_changed = true
+ end
+ end
+ end
+
+ @changed_dependencies << name if dep_changed
+ end
+
+ converge_overrides_outside_dependencies
+
+ @changed_dependencies.any?
+ end
+
+ def converge_overrides_outside_dependencies
+ @overrides.each do |override|
+ # :all overrides are intentionally not pre-unlocked. They take effect on
+ # fresh resolution (no lockfile) or when the user runs `bundle update`.
+ # Forcing a full re-resolve from a single :all directive would surprise
+ # users with unrelated dependency churn.
+ next unless override.target.is_a?(String)
+
+ name = override.target
+ next if @changed_dependencies.include?(name)
+ next if @originally_locked_specs[name].empty?
+ # version: overrides on direct deps are detected in the per-dep
+ # converge_dependencies loop via apply_override_to + matches_spec?.
+ # Other fields are not visible there, so they always reach here.
+ next if override.field == :version && @dependencies.any? {|d| d.name == name }
+
+ @gems_to_unlock << name
+ @changed_dependencies << name
+ end
+ end
+
+ # Remove elements from the locked specs that are expired. This will most
+ # commonly happen if the Gemfile has changed since the lockfile was last
+ # generated
+ def converge_locked_specs
+ converged = converge_specs(@locked_specs)
+
+ resolve = SpecSet.new(converged)
+
+ diff = nil
+
+ # Now, we unlock any sources that do not have anymore gems pinned to it
+ sources.all_sources.each do |source|
+ next unless source.respond_to?(:unlock!)
+
+ unless resolve.any? {|s| s.source == source }
+ diff ||= @locked_specs.to_a - resolve.to_a
+ source.unlock! if diff.any? {|s| s.source == source }
+ end
+ end
+
+ resolve
+ end
+
+ def converge_specs(specs)
+ converged = []
+ deps = []
+
+ specs.each do |s|
+ name = s.name
+ next if @gems_to_unlock.include?(name)
+
+ dep = @dependencies.find {|d| s.satisfies?(d) }
+ lockfile_source = s.source
+
+ if dep
+ replacement_source = dep.source
+
+ deps << dep if !replacement_source || lockfile_source.include?(replacement_source) || new_deps.include?(dep)
+ else
+ parent_dep = @dependencies.find do |d|
+ next unless d.source && d.source != lockfile_source
+ next if d.source.is_a?(Source::Gemspec)
+
+ parent_locked_specs = @originally_locked_specs[d.name]
+
+ parent_locked_specs.any? do |parent_spec|
+ parent_spec.runtime_dependencies.any? {|rd| rd.name == s.name }
+ end
+ end
+
+ if parent_dep && parent_dep.source.is_a?(Source::Path) && parent_dep.source.specs[s]&.any?
+ replacement_source = parent_dep.source
+ else
+ replacement_source = sources.get(lockfile_source)
+ end
+ end
+
+ # Replace the locked dependency's source with the equivalent source from the Gemfile
+ s.source = replacement_source || default_source
+ next if s.source_changed?
+
+ source = s.source
+ next if @sources_to_unlock.include?(source.name)
+
+ # Path sources have special logic
+ if source.is_a?(Source::Path)
+ new_spec = source.specs[s].first
+ if new_spec
+ s.runtime_dependencies.replace(new_spec.runtime_dependencies)
+ else
+ # If the spec is no longer in the path source, unlock it. This
+ # commonly happens if the version changed in the gemspec
+ @gems_to_unlock << name
+ end
+ end
+
+ converged << s
+ end
+
+ filter_specs(converged, deps, skips: @gems_to_unlock)
+ end
+
+ def metadata_dependencies
+ @metadata_dependencies ||= [
+ Dependency.new("Ruby\0", Bundler::RubyVersion.system.gem_version),
+ Dependency.new("RubyGems\0", Gem::VERSION),
+ ]
+ end
+
+ def source_requirements
+ @source_requirements ||= find_source_requirements
+ end
+
+ def preload_git_source_worker
+ workers = Bundler.settings.installation_parallelization
+
+ @preload_git_source_worker ||= Bundler::Worker.new(workers, "Git source preloading", ->(source, _) { source.specs })
+ end
+
+ def preload_git_sources
+ if Gem.ruby_version < Gem::Version.new("3.3")
+ # Ruby 3.2 has a bug that incorrectly triggers a circular dependency warning. This version will continue to
+ # fetch git repositories one by one.
+ return
+ end
+
+ begin
+ needed_git_sources.each {|source| preload_git_source_worker.enq(source) }
+ ensure
+ preload_git_source_worker.stop
+ end
+ end
+
+ # Git sources needed for the requested groups (excludes sources only used by --without groups)
+ def needed_git_sources
+ needed_deps = dependencies_for(requested_groups)
+ sources.git_sources.select do |source|
+ needed_deps.any? {|d| d.source == source }
+ end
+ end
+
+ # Git sources that should be excluded (only used by --without groups)
+ def excluded_git_sources
+ sources.git_sources - needed_git_sources
+ end
+
+ def find_source_requirements
+ preload_git_sources
+
+ # Only safe to exclude when locked_requirements (merged below) backfills the gap.
+ nothing_changed = nothing_changed?
+ excluded = nothing_changed ? excluded_git_sources : []
+
+ # Record the specs available in each gem's source, so that those
+ # specs will be available later when the resolver knows where to
+ # look for that gemspec (or its dependencies)
+ source_requirements = if precompute_source_requirements_for_indirect_dependencies?
+ all_requirements = source_map.all_requirements(excluded)
+ { default: default_source }.merge(all_requirements)
+ else
+ { default: Source::RubygemsAggregate.new(sources, source_map, excluded) }.merge(source_map.direct_requirements)
+ end
+ source_requirements.merge!(source_map.locked_requirements) if nothing_changed
+ metadata_dependencies.each do |dep|
+ source_requirements[dep.name] = sources.metadata_source
+ end
+
+ default_bundler_source = source_requirements["bundler"] || default_source
+
+ if @unlocking_bundler
+ default_bundler_source.add_dependency_names("bundler")
+ else
+ source_requirements[:default_bundler] = default_bundler_source
+ source_requirements["bundler"] = sources.metadata_source # needs to come last to override
+ end
+
+ source_requirements
+ end
+
+ def default_source
+ sources.default_source
+ end
+
+ def requested_groups
+ values = groups - Bundler.settings[:without] - @optional_groups + Bundler.settings[:with]
+ values &= Bundler.settings[:only] unless Bundler.settings[:only].empty?
+ values
+ end
+
+ def lockfiles_equal?(current, proposed, preserve_unknown_sections)
+ if preserve_unknown_sections
+ sections_to_ignore = LockfileParser.sections_to_ignore(@locked_bundler_version)
+ sections_to_ignore += LockfileParser.unknown_sections_in_lockfile(current)
+ sections_to_ignore << LockfileParser::RUBY
+ sections_to_ignore << LockfileParser::BUNDLED unless @unlocking_bundler
+ pattern = /#{Regexp.union(sections_to_ignore)}\n(\s{2,}.*\n)+/
+ whitespace_cleanup = /\n{2,}/
+ current = current.gsub(pattern, "\n").gsub(whitespace_cleanup, "\n\n").strip
+ proposed = proposed.gsub(pattern, "\n").gsub(whitespace_cleanup, "\n\n").strip
+ end
+ current == proposed
+ end
+
+ def additional_base_requirements_to_prevent_downgrades(resolution_base)
+ return resolution_base unless @locked_gems
+ @originally_locked_specs.each do |locked_spec|
+ next if locked_spec.source.is_a?(Source::Path) || locked_spec.source_changed?
+
+ name = locked_spec.name
+ next if @changed_dependencies.include?(name)
+
+ resolution_base.base_requirements[name] = Gem::Requirement.new(">= #{locked_spec.version}")
+ end
+ resolution_base
+ end
+
+ def additional_base_requirements_to_force_updates(resolution_base)
+ return resolution_base if @explicit_unlocks.empty?
+ full_update = SpecSet.new(new_resolver_for_full_update.start)
+ @explicit_unlocks.each do |name|
+ version = full_update.version_for(name)
+ resolution_base.base_requirements[name] = Gem::Requirement.new("= #{version}") if version
+ end
+ resolution_base
+ end
+
+ def remove_invalid_platforms!
+ return if Bundler.frozen_bundle?
+
+ skips = (@new_platforms + [Bundler.local_platform]).uniq
+
+ # We should probably avoid removing non-ruby platforms, since that means
+ # lockfile will no longer install on those platforms, so a error to give
+ # heads up to the user may be better. However, we have tests expecting
+ # non ruby platform autoremoval to work, so leaving that in place for
+ # now.
+ skips |= platforms - [Gem::Platform::RUBY] if @dependency_changes
+
+ @originally_invalid_platforms = @originally_locked_specs.remove_invalid_platforms!(current_dependencies, platforms, skips: skips)
+ end
+
+ def source_map
+ @source_map ||= SourceMap.new(sources, dependencies, @locked_specs)
+ end
+
+ def new_resolver_for_full_update
+ new_resolver(unlocked_resolution_base)
+ end
+
+ def unlocked_resolution_base
+ new_resolution_base(last_resolve: SpecSet.new([]), unlock: true)
+ end
+
+ def new_resolution_base(last_resolve:, unlock:)
+ new_resolution_platforms = @current_platform_missing ? @new_platforms + [Bundler.local_platform] : @new_platforms
+ Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local, new_platforms: new_resolution_platforms, overrides: @overrides)
+ end
+
+ def new_resolver(base)
+ Resolver.new(base, gem_version_promoter, @most_specific_locked_platform)
+ end
+ end
+end
diff --git a/lib/bundler/dependency.rb b/lib/bundler/dependency.rb
new file mode 100644
index 0000000000..cb9c7a76ea
--- /dev/null
+++ b/lib/bundler/dependency.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+require "rubygems/dependency"
+require_relative "shared_helpers"
+
+module Bundler
+ class Dependency < Gem::Dependency
+ def initialize(name, version, options = {}, &blk)
+ type = options["type"] || :runtime
+ super(name, version, type)
+
+ @options = options
+ end
+
+ def groups
+ @groups ||= Array(@options["group"] || :default).map(&:to_sym)
+ end
+
+ def source
+ return @source if defined?(@source)
+
+ @source = @options["source"]
+ end
+
+ def path
+ return @path if defined?(@path)
+
+ @path = @options["path"]
+ end
+
+ def git
+ return @git if defined?(@git)
+
+ @git = @options["git"]
+ end
+
+ def github
+ return @github if defined?(@github)
+
+ @github = @options["github"]
+ end
+
+ def branch
+ return @branch if defined?(@branch)
+
+ @branch = @options["branch"]
+ end
+
+ def ref
+ return @ref if defined?(@ref)
+
+ @ref = @options["ref"]
+ end
+
+ def glob
+ return @glob if defined?(@glob)
+
+ @glob = @options["glob"]
+ end
+
+ def platforms
+ @platforms ||= Array(@options["platforms"])
+ end
+
+ def env
+ return @env if defined?(@env)
+
+ @env = @options["env"]
+ end
+
+ def should_include
+ @should_include ||= @options.fetch("should_include", true)
+ end
+
+ def gemfile
+ return @gemfile if defined?(@gemfile)
+
+ @gemfile = @options["gemfile"]
+ end
+
+ def force_ruby_platform
+ return @force_ruby_platform if defined?(@force_ruby_platform)
+
+ @force_ruby_platform = @options["force_ruby_platform"]
+ end
+
+ def autorequire
+ return @autorequire if defined?(@autorequire)
+
+ @autorequire = Array(@options["require"] || []) if @options.key?("require")
+ end
+
+ RUBY_PLATFORM_ARRAY = [Gem::Platform::RUBY].freeze
+ private_constant :RUBY_PLATFORM_ARRAY
+
+ # Returns the platforms this dependency is valid for, in the same order as
+ # passed in the `valid_platforms` parameter
+ def gem_platforms(valid_platforms)
+ return RUBY_PLATFORM_ARRAY if force_ruby_platform
+ return valid_platforms if platforms.empty?
+
+ valid_platforms.select {|p| expanded_platforms.include?(Gem::Platform.generic(p)) }
+ end
+
+ def expanded_platforms
+ @expanded_platforms ||= platforms.filter_map {|pl| CurrentRuby::PLATFORM_MAP[pl] }.flatten.uniq
+ end
+
+ def should_include?
+ should_include && current_env? && current_platform?
+ end
+
+ def gemspec_dev_dep?
+ @gemspec_dev_dep ||= @options.fetch("gemspec_dev_dep", false)
+ end
+
+ def gemfile_dep?
+ !gemspec_dev_dep?
+ end
+
+ def current_env?
+ return true unless env
+ if env.is_a?(Hash)
+ env.all? do |key, val|
+ ENV[key.to_s] && (val.is_a?(String) ? ENV[key.to_s] == val : ENV[key.to_s] =~ val)
+ end
+ else
+ ENV[env.to_s]
+ end
+ end
+
+ def current_platform?
+ return true if platforms.empty?
+ platforms.any? do |p|
+ Bundler.current_ruby.send("#{p}?")
+ end
+ end
+
+ def to_lock
+ out = super
+ out << "!" if source
+ out
+ end
+
+ def specific?
+ super
+ rescue NoMethodError
+ requirement != ">= 0"
+ end
+ end
+end
diff --git a/lib/bundler/deployment.rb b/lib/bundler/deployment.rb
new file mode 100644
index 0000000000..3344449e82
--- /dev/null
+++ b/lib/bundler/deployment.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "shared_helpers"
+Bundler::SharedHelpers.feature_removed! "Bundler no longer integrates with " \
+ "Capistrano, but Capistrano provides its own integration with " \
+ "Bundler via the capistrano-bundler gem. Use it instead."
diff --git a/lib/bundler/deprecate.rb b/lib/bundler/deprecate.rb
new file mode 100644
index 0000000000..f59533630e
--- /dev/null
+++ b/lib/bundler/deprecate.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+begin
+ require "rubygems/deprecate"
+rescue LoadError
+ # it's fine if it doesn't exist on the current RubyGems...
+ nil
+end
+
+module Bundler
+ # If Bundler::Deprecate is an autoload constant, we need to define it
+ if defined?(Bundler::Deprecate) && !autoload?(:Deprecate)
+ # nothing to do!
+ elsif defined? ::Deprecate
+ Deprecate = ::Deprecate
+ elsif defined? Gem::Deprecate
+ Deprecate = Gem::Deprecate
+ else
+ class Deprecate
+ end
+ end
+
+ unless Deprecate.respond_to?(:skip_during)
+ def Deprecate.skip_during
+ original = skip
+ self.skip = true
+ yield
+ ensure
+ self.skip = original
+ end
+ end
+
+ unless Deprecate.respond_to?(:skip)
+ def Deprecate.skip
+ @skip ||= false
+ end
+ end
+
+ unless Deprecate.respond_to?(:skip=)
+ def Deprecate.skip=(skip)
+ @skip = skip
+ end
+ end
+end
diff --git a/lib/bundler/digest.rb b/lib/bundler/digest.rb
new file mode 100644
index 0000000000..158803033d
--- /dev/null
+++ b/lib/bundler/digest.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+# This code was extracted from https://github.com/Solistra/ruby-digest which is under public domain
+module Bundler
+ module Digest
+ # The initial constant values for the 32-bit constant words A, B, C, D, and
+ # E, respectively.
+ SHA1_WORDS = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0].freeze
+
+ # The 8-bit field used for bitwise `AND` masking. Defaults to `0xFFFFFFFF`.
+ SHA1_MASK = 0xFFFFFFFF
+
+ class << self
+ def sha1(string)
+ unless string.is_a?(String)
+ raise TypeError, "can't convert #{string.class.inspect} into String"
+ end
+
+ buffer = string.b
+
+ words = SHA1_WORDS.dup
+ generate_split_buffer(buffer) do |chunk|
+ w = []
+ chunk.each_slice(4) do |a, b, c, d|
+ w << (((a << 8 | b) << 8 | c) << 8 | d)
+ end
+ a, b, c, d, e = *words
+ (16..79).each do |i|
+ w[i] = SHA1_MASK & rotate(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1)
+ end
+ 0.upto(79) do |i|
+ case i
+ when 0..19
+ f = ((b & c) | (~b & d))
+ k = 0x5A827999
+ when 20..39
+ f = (b ^ c ^ d)
+ k = 0x6ED9EBA1
+ when 40..59
+ f = ((b & c) | (b & d) | (c & d))
+ k = 0x8F1BBCDC
+ when 60..79
+ f = (b ^ c ^ d)
+ k = 0xCA62C1D6
+ end
+ t = SHA1_MASK & rotate(a, 5) + f + e + k + w[i]
+ a, b, c, d, e = t, a, SHA1_MASK & rotate(b, 30), c, d # rubocop:disable Style/ParallelAssignment
+ end
+ mutated = [a, b, c, d, e]
+ words.map!.with_index {|word, index| SHA1_MASK & (word + mutated[index]) }
+ end
+
+ words.pack("N*").unpack1("H*")
+ end
+
+ private
+
+ def generate_split_buffer(string, &block)
+ size = string.bytesize * 8
+ buffer = string.bytes << 128
+ buffer << 0 while buffer.size % 64 != 56
+ buffer.concat([size].pack("Q>").bytes)
+ buffer.each_slice(64, &block)
+ end
+
+ def rotate(value, spaces)
+ value << spaces | value >> (32 - spaces)
+ end
+ end
+ end
+end
diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb
new file mode 100644
index 0000000000..6e2638a8be
--- /dev/null
+++ b/lib/bundler/dsl.rb
@@ -0,0 +1,698 @@
+# frozen_string_literal: true
+
+require_relative "dependency"
+require_relative "ruby_dsl"
+
+module Bundler
+ class Dsl
+ include RubyDsl
+
+ def self.evaluate(gemfile, lockfile, unlock)
+ builder = new
+ builder.lockfile(lockfile)
+ builder.eval_gemfile(gemfile)
+ builder.to_definition(builder.lockfile_path, unlock)
+ end
+
+ VALID_PLATFORMS = Bundler::CurrentRuby::PLATFORM_MAP.keys.freeze
+
+ VALID_KEYS = %w[group groups git path glob name branch ref tag require submodules
+ platform platforms source install_if force_ruby_platform].freeze
+
+ GITHUB_PULL_REQUEST_URL = %r{\Ahttps://github\.com/([A-Za-z0-9_\-\.]+/[A-Za-z0-9_\-\.]+)/pull/(\d+)\z}
+ GITLAB_MERGE_REQUEST_URL = %r{\Ahttps://gitlab\.com/([A-Za-z0-9_\-\./]+)/-/merge_requests/(\d+)\z}
+
+ attr_reader :gemspecs, :gemfile, :overrides
+ attr_accessor :dependencies
+
+ def initialize
+ @source = nil
+ @sources = SourceList.new
+ @git_sources = {}
+ @dependencies = []
+ @groups = []
+ @install_conditionals = []
+ @optional_groups = []
+ @platforms = []
+ @env = nil
+ @ruby_version = nil
+ @gemspecs = []
+ @gemfile = nil
+ @gemfiles = []
+ @lockfile = nil
+ @overrides = []
+ add_git_sources
+ end
+
+ def eval_gemfile(gemfile, contents = nil)
+ with_gemfile(gemfile) do |current_gemfile|
+ contents ||= Bundler.read_file(current_gemfile)
+ instance_eval(contents, current_gemfile, 1)
+ rescue GemfileEvalError => e
+ message = "There was an error evaluating `#{File.basename current_gemfile}`: #{e.message}"
+ raise DSLError.new(message, current_gemfile, e.backtrace, contents)
+ rescue GemfileError, InvalidArgumentError, InvalidOption, DeprecatedError, ScriptError => e
+ message = "There was an error parsing `#{File.basename current_gemfile}`: #{e.message}"
+ raise DSLError.new(message, current_gemfile, e.backtrace, contents)
+ rescue StandardError => e
+ raise unless e.backtrace_locations.first.path == current_gemfile
+ message = "There was an error parsing `#{File.basename current_gemfile}`: #{e.message}"
+ raise DSLError.new(message, current_gemfile, e.backtrace, contents)
+ end
+ end
+
+ def gemspec(opts = nil)
+ opts ||= {}
+ path = opts[:path] || "."
+ glob = opts[:glob]
+ name = opts[:name]
+ development_group = opts[:development_group] || :development
+ expanded_path = gemfile_root.join(path)
+
+ gemspecs = Gem::Util.glob_files_in_dir("{,*}.gemspec", expanded_path).filter_map {|g| Bundler.load_gemspec(g) }
+ gemspecs.reject! {|s| s.name != name } if name
+ specs_by_name_and_version = gemspecs.group_by {|s| [s.name, s.version] }
+
+ case specs_by_name_and_version.size
+ when 1
+ specs = specs_by_name_and_version.values.first
+ spec = specs.find {|s| s.installable_on_platform?(Bundler.local_platform) } || specs.first
+
+ @gemspecs << spec
+
+ path path, "glob" => glob, "name" => spec.name, "gemspec" => spec do
+ add_dependency spec.name
+ end
+
+ spec.development_dependencies.each do |dep|
+ add_dependency dep.name, dep.requirement.as_list, "gemspec_dev_dep" => true, "group" => development_group
+ end
+ when 0
+ raise InvalidOption, "There are no gemspecs at #{expanded_path}"
+ else
+ raise InvalidOption, "There are multiple gemspecs at #{expanded_path}. " \
+ "Please use the :name option to specify which one should be used"
+ end
+ end
+
+ def gem(name, *args)
+ options = args.last.is_a?(Hash) ? args.pop.dup : {}
+ version = args || [">= 0"]
+
+ normalize_options(name, version, options)
+
+ add_dependency(name, version, options)
+ end
+
+ # For usage in Dsl.evaluate, since lockfile is used as part of the Gemfile.
+ def lockfile_path
+ @lockfile
+ end
+
+ def lockfile(file)
+ @lockfile = file
+ end
+
+ def source(source, *args, &blk)
+ options = args.last.is_a?(Hash) ? args.pop.dup : {}
+ options = normalize_hash(options)
+ source = normalize_source(source)
+ cooldown = options["cooldown"]
+ if cooldown && !(cooldown.is_a?(Integer) && cooldown >= 0)
+ raise InvalidOption, "Expected `cooldown` to be a non-negative integer, got #{cooldown.inspect}"
+ end
+
+ if options.key?("type")
+ options["type"] = options["type"].to_s
+ unless Plugin.source?(options["type"])
+ raise InvalidOption, "No plugin sources available for #{options["type"]}"
+ end
+
+ unless block_given?
+ raise InvalidOption, "You need to pass a block to #source with :type option"
+ end
+
+ source_opts = options.merge("uri" => source)
+ with_source(@sources.add_plugin_source(options["type"], source_opts), &blk)
+ elsif block_given?
+ with_source(@sources.add_rubygems_source("remotes" => source, "cooldown" => cooldown), &blk)
+ else
+ @sources.add_global_rubygems_remote(source, cooldown: cooldown)
+ end
+ end
+
+ def git_source(name, &block)
+ unless block_given?
+ raise InvalidOption, "You need to pass a block to #git_source"
+ end
+
+ if valid_keys.include?(name.to_s)
+ raise InvalidOption, "You cannot use #{name} as a git source. It " \
+ "is a reserved key. Reserved keys are: #{valid_keys.join(", ")}"
+ end
+
+ @git_sources[name.to_s] = block
+ end
+
+ def path(path, options = {}, &blk)
+ source_options = normalize_hash(options).merge(
+ "path" => Pathname.new(path),
+ "root_path" => gemfile_root
+ )
+
+ source_options["global"] = true unless block_given?
+
+ source = @sources.add_path_source(source_options)
+ with_source(source, &blk)
+ end
+
+ def git(uri, options = {}, &blk)
+ unless block_given?
+ msg = "You can no longer specify a git source by itself. Instead, \n" \
+ "either use the :git option on a gem, or specify the gems that \n" \
+ "bundler should find in the git source by passing a block to \n" \
+ "the git method, like: \n\n" \
+ " git 'git://github.com/rails/rails.git' do\n" \
+ " gem 'rails'\n" \
+ " end"
+ raise DeprecatedError, msg
+ end
+
+ with_source(@sources.add_git_source(normalize_hash(options).merge("uri" => uri)), &blk)
+ end
+
+ def github(repo, options = {})
+ raise InvalidArgumentError, "GitHub sources require a block" unless block_given?
+ github_uri = @git_sources["github"].call(repo)
+ git_options = normalize_hash(options).merge("uri" => github_uri)
+ git_source = @sources.add_git_source(git_options)
+ with_source(git_source) { yield }
+ end
+
+ SUPPORTED_OVERRIDE_FIELDS = [:version, :required_ruby_version, :required_rubygems_version].freeze
+ SUPPORTED_OVERRIDE_SYMBOL_OPERATIONS = [:ignore_upper].freeze
+
+ def override(target, **operations)
+ validate_override_target!(target)
+
+ if target == :all && operations.key?(:version)
+ raise ArgumentError, "`override :all, version:` is not allowed; version requirements are per-gem"
+ end
+
+ operations.each do |field, operation|
+ validate_override_field!(field)
+ validate_override_operation!(operation)
+ validate_override_uniqueness!(target, field)
+ end
+
+ source_location = caller_locations(1, 1)&.first
+ operations.each do |field, operation|
+ @overrides << Override.new(target, field, operation, source_location: source_location)
+ end
+ end
+
+ def to_definition(lockfile, unlock)
+ check_primary_source_safety
+ lockfile = @lockfile unless @lockfile.nil?
+ Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles, @overrides)
+ end
+
+ def group(*args, &blk)
+ options = args.last.is_a?(Hash) ? args.pop.dup : {}
+ normalize_group_options(options, args)
+
+ @groups.concat args
+
+ if options["optional"]
+ optional_groups = args - @optional_groups
+ @optional_groups.concat optional_groups
+ end
+
+ yield
+ ensure
+ args.each { @groups.pop }
+ end
+
+ def install_if(*args)
+ @install_conditionals.concat args
+ yield
+ ensure
+ args.each { @install_conditionals.pop }
+ end
+
+ def platforms(*platforms)
+ @platforms.concat platforms
+ yield
+ ensure
+ platforms.each { @platforms.pop }
+ end
+ alias_method :platform, :platforms
+
+ def env(name)
+ old = @env
+ @env = name
+ yield
+ ensure
+ @env = old
+ end
+
+ def plugin(*args)
+ # Pass on
+ end
+
+ def method_missing(name, *args)
+ raise GemfileError, "Undefined local variable or method `#{name}' for Gemfile"
+ end
+
+ def check_primary_source_safety
+ check_path_source_safety
+ check_rubygems_source_safety
+ end
+
+ private
+
+ def validate_override_target!(target)
+ return if target == :all
+ return if target.is_a?(String)
+ raise ArgumentError, "override target must be :all or a gem name string, got #{target.inspect}"
+ end
+
+ def validate_override_field!(field)
+ return if SUPPORTED_OVERRIDE_FIELDS.include?(field)
+ supported = SUPPORTED_OVERRIDE_FIELDS.map {|f| "`#{f}:`" }.join(", ")
+ raise ArgumentError, "unsupported override field `#{field}:`; supported fields: #{supported}"
+ end
+
+ def validate_override_operation!(operation)
+ case operation
+ when String
+ Gem::Requirement.new(operation)
+ when nil
+ # ok
+ when Symbol
+ return if SUPPORTED_OVERRIDE_SYMBOL_OPERATIONS.include?(operation)
+ raise ArgumentError, "unsupported override operation: #{operation.inspect}"
+ else
+ raise ArgumentError, "override operation must be a String, Symbol, or nil, got #{operation.inspect}"
+ end
+ rescue Gem::Requirement::BadRequirementError => e
+ raise ArgumentError, "invalid override version requirement #{operation.inspect}: #{e.message}"
+ end
+
+ def validate_override_uniqueness!(target, field)
+ return unless @overrides.any? {|o| o.target == target && o.field == field }
+ raise ArgumentError, "duplicate override for #{target.inspect} `#{field}:`"
+ end
+
+ def add_dependency(name, version = nil, options = {})
+ options["gemfile"] = @gemfile
+ options["source"] ||= @source
+ options["env"] ||= @env
+
+ dep = Dependency.new(name, version, options)
+
+ # if there's already a dependency with this name we try to prefer one
+ if current = @dependencies.find {|d| d.name == name }
+ if current.requirement != dep.requirement
+ current_requirement_open = current.requirements_list.include?(">= 0")
+
+ gemspec_dep = [dep, current].find(&:gemspec_dev_dep?)
+ if gemspec_dep
+ require_relative "vendor/pub_grub/lib/pub_grub/version_range"
+ require_relative "vendor/pub_grub/lib/pub_grub/version_constraint"
+ require_relative "vendor/pub_grub/lib/pub_grub/version_union"
+ require_relative "vendor/pub_grub/lib/pub_grub/rubygems"
+
+ current_gemspec_range = PubGrub::RubyGems.requirement_to_range(current.requirement)
+ next_gemspec_range = PubGrub::RubyGems.requirement_to_range(dep.requirement)
+
+ if current_gemspec_range.intersects?(next_gemspec_range)
+ dep = Dependency.new(name, current.requirement.as_list + dep.requirement.as_list, options)
+ else
+ gemfile_dep = [dep, current].find(&:gemfile_dep?)
+
+ if gemfile_dep
+ raise GemfileError, "The #{name} dependency has conflicting requirements in Gemfile (#{gemfile_dep.requirement}) and gemspec (#{gemspec_dep.requirement})"
+ else
+ raise GemfileError, "Two gemspec development dependencies have conflicting requirements on the same gem: #{dep} and #{current}"
+ end
+ end
+ else
+ update_prompt = ""
+
+ if File.basename(@gemfile) == Injector::INJECTED_GEMS
+ if dep.requirements_list.include?(">= 0") && !current_requirement_open
+ update_prompt = ". Gem already added"
+ else
+ update_prompt = ". If you want to update the gem version, run `bundle update #{name}`"
+
+ update_prompt += ". You may also need to change the version requirement specified in the Gemfile if it's too restrictive." unless current_requirement_open
+ end
+ end
+
+ raise GemfileError, "You cannot specify the same gem twice with different version requirements.\n" \
+ "You specified: #{name} (#{current.requirement}) and #{name} (#{dep.requirement})" \
+ "#{update_prompt}"
+ end
+ end
+
+ unless current.gemspec_dev_dep? && dep.gemspec_dev_dep?
+ # Always prefer the dependency from the Gemfile
+ if current.gemspec_dev_dep?
+ @dependencies.delete(current)
+ elsif dep.gemspec_dev_dep?
+ return
+ elsif current.source.to_s != dep.source.to_s
+ raise GemfileError, "You cannot specify the same gem twice coming from different sources.\n" \
+ "You specified that #{name} (#{dep.requirement}) should come from " \
+ "#{current.source || "an unspecified source"} and #{dep.source}\n"
+ else
+ Bundler.ui.warn "Your Gemfile lists the gem #{name} (#{current.requirement}) more than once.\n" \
+ "You should probably keep only one of them.\n" \
+ "Remove any duplicate entries and specify the gem only once.\n" \
+ "While it's not a problem now, it could cause errors if you change the version of one of them later."
+ end
+ end
+ end
+
+ @dependencies << dep
+ end
+
+ def with_gemfile(gemfile)
+ expanded_gemfile_path = Pathname.new(gemfile).expand_path(@gemfile&.parent)
+ original_gemfile = @gemfile
+ @gemfile = expanded_gemfile_path
+ @gemfiles << expanded_gemfile_path
+ yield @gemfile.to_s
+ ensure
+ @gemfile = original_gemfile
+ end
+
+ def add_git_sources
+ git_source(:github) do |repo_name|
+ if repo_name =~ GITHUB_PULL_REQUEST_URL
+ {
+ "git" => "https://github.com/#{$1}.git",
+ "branch" => nil,
+ "ref" => "refs/pull/#{$2}/head",
+ "tag" => nil,
+ }
+ else
+ repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
+ "https://github.com/#{repo_name}.git"
+ end
+ end
+
+ git_source(:gist) do |repo_name|
+ "https://gist.github.com/#{repo_name}.git"
+ end
+
+ git_source(:bitbucket) do |repo_name|
+ user_name, repo_name = repo_name.split("/")
+ repo_name ||= user_name
+ "https://#{user_name}@bitbucket.org/#{user_name}/#{repo_name}.git"
+ end
+
+ git_source(:gitlab) do |repo_name|
+ if repo_name =~ GITLAB_MERGE_REQUEST_URL
+ {
+ "git" => "https://gitlab.com/#{$1}.git",
+ "branch" => nil,
+ "ref" => "refs/merge-requests/#{$2}/head",
+ "tag" => nil,
+ }
+ else
+ repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
+ "https://gitlab.com/#{repo_name}.git"
+ end
+ end
+ end
+
+ def with_source(source)
+ old_source = @source
+ if block_given?
+ @source = source
+ yield
+ end
+ source
+ ensure
+ @source = old_source
+ end
+
+ def normalize_hash(opts)
+ opts.keys.each do |k|
+ opts[k.to_s] = opts.delete(k) unless k.is_a?(String)
+ end
+ opts
+ end
+
+ def valid_keys
+ @valid_keys ||= VALID_KEYS
+ end
+
+ def normalize_options(name, version, opts)
+ if name.is_a?(Symbol)
+ raise GemfileError, %(You need to specify gem names as Strings. Use 'gem "#{name}"' instead)
+ end
+ if /\s/.match?(name)
+ raise GemfileError, %('#{name}' is not a valid gem name because it contains whitespace)
+ end
+ raise GemfileError, %(an empty gem name is not valid) if name.empty?
+
+ normalize_hash(opts)
+
+ git_names = @git_sources.keys.map(&:to_s)
+ validate_keys("gem '#{name}'", opts, valid_keys + git_names)
+
+ groups = @groups.dup
+ opts["group"] = opts.delete("groups") || opts["group"]
+ groups.concat Array(opts.delete("group"))
+ groups = [:default] if groups.empty?
+
+ install_if = @install_conditionals.dup
+ install_if.concat Array(opts.delete("install_if"))
+ install_if = install_if.reduce(true) do |memo, val|
+ memo && (val.respond_to?(:call) ? val.call : val)
+ end
+
+ platforms = @platforms.dup
+ opts["platforms"] = opts["platform"] || opts["platforms"]
+ platforms.concat Array(opts.delete("platforms"))
+ platforms.map!(&:to_sym)
+ platforms.each do |p|
+ next if VALID_PLATFORMS.include?(p)
+ raise GemfileError, "`#{p}` is not a valid platform. The available options are: #{VALID_PLATFORMS.inspect}"
+ end
+
+ windows_platforms = platforms.select {|pl| pl.to_s.match?(/mingw|mswin/) }
+ if windows_platforms.any?
+ windows_platforms = windows_platforms.map! {|pl| ":#{pl}" }.join(", ")
+ deprecated_message = "Platform #{windows_platforms} will be removed in the future. Please use platform :windows instead."
+ Bundler::SharedHelpers.feature_deprecated! deprecated_message
+ end
+
+ # Save sources passed in a key
+ if opts.key?("source")
+ source = normalize_source(opts["source"])
+ opts["source"] = @sources.add_rubygems_source("remotes" => source)
+ end
+
+ git_name = (git_names & opts.keys).last
+ if @git_sources[git_name]
+ git_opts = @git_sources[git_name].call(opts[git_name])
+ git_opts = { "git" => git_opts } if git_opts.is_a?(String)
+ opts.merge!(git_opts) do |key, _gemfile_value, _git_source_value|
+ raise GemfileError, %(The :#{key} option can't be used with `#{git_name}: #{opts[git_name].inspect}`)
+ end
+ end
+
+ %w[git path].each do |type|
+ next unless param = opts[type]
+ if version.first && version.first =~ /^\s*=?\s*(\d[^\s]*)\s*$/
+ options = opts.merge("name" => name, "version" => $1)
+ else
+ options = opts.dup
+ end
+ source = send(type, param, options) {}
+ opts["source"] = source
+ end
+
+ opts["platforms"] = platforms.dup
+ opts["group"] = groups
+ opts["should_include"] = install_if
+ end
+
+ def normalize_group_options(opts, groups)
+ normalize_hash(opts)
+
+ groups = groups.map {|group| ":#{group}" }.join(", ")
+ validate_keys("group #{groups}", opts, %w[optional])
+
+ opts["optional"] ||= false
+ end
+
+ def validate_keys(command, opts, valid_keys)
+ if opts["branch"] && !(opts["git"] || opts["github"] || (opts.keys & @git_sources.keys.map(&:to_s)).any?)
+ raise GemfileError, %(The `branch` option for `#{command}` is not allowed. Only gems with a git source can specify a branch)
+ end
+
+ invalid_keys = opts.keys - valid_keys
+ return true unless invalid_keys.any?
+
+ message = String.new
+ message << "You passed #{invalid_keys.map {|k| ":" + k }.join(", ")} "
+ message << if invalid_keys.size > 1
+ "as options for #{command}, but they are invalid."
+ else
+ "as an option for #{command}, but it is invalid."
+ end
+
+ message << " Valid options are: #{valid_keys.join(", ")}."
+ message << " You may be able to resolve this by upgrading Bundler to the newest version."
+ raise InvalidOption, message
+ end
+
+ def normalize_source(source)
+ case source
+ when :gemcutter, :rubygems, :rubyforge
+ removed_message =
+ "The source :#{source} is disallowed because HTTP requests are insecure.\n" \
+ "Please change your source to 'https://rubygems.org' if possible, or 'http://rubygems.org' if not."
+ Bundler::SharedHelpers.feature_removed! removed_message
+ when String
+ source
+ else
+ raise GemfileError, "Unknown source '#{source}'"
+ end
+ end
+
+ def check_path_source_safety
+ return if @sources.global_path_source.nil?
+
+ msg = "You can no longer specify a path source by itself. Instead, \n" \
+ "either use the :path option on a gem, or specify the gems that \n" \
+ "bundler should find in the path source by passing a block to \n" \
+ "the path method, like: \n\n" \
+ " path 'dir/containing/rails' do\n" \
+ " gem 'rails'\n" \
+ " end\n\n"
+
+ SharedHelpers.feature_removed! msg.strip
+ end
+
+ def check_rubygems_source_safety
+ multiple_global_source_warning if @sources.aggregate_global_source?
+ end
+
+ def multiple_global_source_warning
+ msg = "This Gemfile contains multiple global sources. " \
+ "Each source after the first must include a block to indicate which gems " \
+ "should come from that source"
+ raise GemfileEvalError, msg
+ end
+
+ class DSLError < GemfileError
+ # @return [String] the description that should be presented to the user.
+ #
+ attr_reader :description
+
+ # @return [String] the path of the dsl file that raised the exception.
+ #
+ attr_reader :dsl_path
+
+ # @return [Exception] the backtrace of the exception raised by the
+ # evaluation of the dsl file.
+ #
+ attr_reader :backtrace
+
+ # @param [Exception] backtrace @see backtrace
+ # @param [String] dsl_path @see dsl_path
+ #
+ def initialize(description, dsl_path, backtrace, contents = nil)
+ @status_code = $!.respond_to?(:status_code) && $!.status_code
+
+ @description = description
+ @dsl_path = dsl_path
+ @backtrace = backtrace
+ @contents = contents
+ end
+
+ def status_code
+ @status_code || super
+ end
+
+ # @return [String] the contents of the DSL that cause the exception to
+ # be raised.
+ #
+ def contents
+ @contents ||= dsl_path && File.exist?(dsl_path) && File.read(dsl_path)
+ end
+
+ # The message of the exception reports the content of podspec for the
+ # line that generated the original exception.
+ #
+ # @example Output
+ #
+ # Invalid podspec at `RestKit.podspec` - undefined method
+ # `exclude_header_search_paths=' for #<Pod::Specification for
+ # `RestKit/Network (0.9.3)`>
+ #
+ # from spec-repos/master/RestKit/0.9.3/RestKit.podspec:36
+ # -------------------------------------------
+ # # because it would break: #import <CoreData/CoreData.h>
+ # > ns.exclude_header_search_paths = 'Code/RestKit.h'
+ # end
+ # -------------------------------------------
+ #
+ # @return [String] the message of the exception.
+ #
+ def to_s
+ @to_s ||= begin
+ trace_line, description = parse_line_number_from_description
+
+ m = String.new("\n[!] ")
+ m << description
+ m << ". Bundler cannot continue.\n"
+
+ return m unless backtrace && dsl_path && contents
+
+ trace_line = backtrace.find {|l| l.include?(dsl_path) } || trace_line
+ return m unless trace_line
+ line_number = trace_line.split(":")[1].to_i - 1
+ return m unless line_number
+
+ lines = contents.lines.to_a
+ indent = " # "
+ indicator = indent.tr("#", ">")
+ first_line = line_number.zero?
+ last_line = (line_number == (lines.count - 1))
+
+ m << "\n"
+ m << "#{indent}from #{trace_line.gsub(/:in.*$/, "")}\n"
+ m << "#{indent}-------------------------------------------\n"
+ m << "#{indent}#{lines[line_number - 1]}" unless first_line
+ m << "#{indicator}#{lines[line_number]}"
+ m << "#{indent}#{lines[line_number + 1]}" unless last_line
+ m << "\n" unless m.end_with?("\n")
+ m << "#{indent}-------------------------------------------\n"
+ end
+ end
+
+ private
+
+ def parse_line_number_from_description
+ description = self.description
+ if dsl_path && description =~ /((#{Regexp.quote File.expand_path(dsl_path)}|#{Regexp.quote dsl_path}):\d+)/
+ trace_line = Regexp.last_match[1]
+ description = description.sub(/\n.*\n(\.\.\.)? *\^~+$/, "").sub(/#{Regexp.quote trace_line}:\s*/, "").sub("\n", " - ")
+ end
+ [trace_line, description]
+ end
+ end
+
+ def gemfile_root
+ @gemfile ||= Bundler.default_gemfile
+ @gemfile.dirname
+ end
+ end
+end
diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb
new file mode 100644
index 0000000000..7c7ce107e2
--- /dev/null
+++ b/lib/bundler/endpoint_specification.rb
@@ -0,0 +1,184 @@
+# frozen_string_literal: true
+
+module Bundler
+ # used for Creating Specifications from the Gemcutter Endpoint
+ class EndpointSpecification < Gem::Specification
+ include MatchRemoteMetadata
+
+ attr_reader :name, :version, :platform, :checksum, :created_at
+ attr_writer :dependencies
+ attr_accessor :remote, :locked_platform
+
+ def initialize(name, version, platform, spec_fetcher, dependencies, metadata = nil)
+ super()
+ @name = name
+ @version = Gem::Version.create version
+ @platform = Gem::Platform.new(platform)
+ @spec_fetcher = spec_fetcher
+ @dependencies = nil
+ @unbuilt_dependencies = dependencies
+
+ @loaded_from = nil
+ @remote_specification = nil
+ @locked_platform = nil
+
+ parse_metadata(metadata)
+ end
+
+ def insecurely_materialized?
+ @locked_platform.to_s != @platform.to_s
+ end
+
+ def fetch_platform
+ @platform
+ end
+
+ def dependencies
+ @dependencies ||= @unbuilt_dependencies.map! {|dep, reqs| build_dependency(dep, reqs) }
+ end
+ alias_method :runtime_dependencies, :dependencies
+
+ # needed for standalone, load required_paths from local gemspec
+ # after the gem is installed
+ def require_paths
+ if @remote_specification
+ @remote_specification.require_paths
+ elsif _local_specification
+ _local_specification.require_paths
+ else
+ super
+ end
+ end
+
+ # needed for inline
+ def load_paths
+ # remote specs aren't installed, and can't have load_paths
+ if _local_specification
+ _local_specification.load_paths
+ else
+ super
+ end
+ end
+
+ # needed for binstubs
+ def executables
+ if @remote_specification
+ @remote_specification.executables
+ elsif _local_specification
+ _local_specification.executables
+ else
+ super
+ end
+ end
+
+ # needed for bundle clean
+ def bindir
+ if @remote_specification
+ @remote_specification.bindir
+ elsif _local_specification
+ _local_specification.bindir
+ else
+ super
+ end
+ end
+
+ # needed for post_install_messages during install
+ def post_install_message
+ if @remote_specification
+ @remote_specification.post_install_message
+ elsif _local_specification
+ _local_specification.post_install_message
+ else
+ super
+ end
+ end
+
+ # needed for "with native extensions" during install
+ def extensions
+ if @remote_specification
+ @remote_specification.extensions
+ elsif _local_specification
+ _local_specification.extensions
+ else
+ super
+ end
+ end
+
+ # needed for `bundle fund`
+ def metadata
+ if @remote_specification
+ @remote_specification.metadata
+ elsif _local_specification
+ _local_specification.metadata
+ else
+ super
+ end
+ end
+
+ def _local_specification
+ return unless @loaded_from && File.exist?(local_specification_path)
+ eval(File.read(local_specification_path), nil, local_specification_path).tap do |spec|
+ spec.loaded_from = @loaded_from
+ end
+ end
+
+ def __swap__(spec)
+ SharedHelpers.ensure_same_dependencies(self, dependencies, spec.dependencies)
+ @remote_specification = spec
+ end
+
+ def inspect
+ "#<#{self.class} @name=\"#{name}\" (#{full_name.delete_prefix("#{name}-")})>"
+ end
+
+ private
+
+ def _remote_specification
+ @_remote_specification ||= @spec_fetcher.fetch_spec([@name, @version, @platform])
+ end
+
+ def local_specification_path
+ "#{base_dir}/specifications/#{full_name}.gemspec"
+ end
+
+ def parse_metadata(data)
+ unless data
+ @required_ruby_version = nil
+ @required_rubygems_version = nil
+ @created_at = nil
+ return
+ end
+
+ data.each do |k, v|
+ next unless v
+ case k.to_s
+ when "checksum"
+ begin
+ @checksum = Checksum.from_api(v.last, @spec_fetcher.uri)
+ rescue ArgumentError => e
+ raise ArgumentError, "Invalid checksum for #{full_name}: #{e.message}"
+ end
+ when "rubygems"
+ @required_rubygems_version = Gem::Requirement.new(v)
+ when "ruby"
+ @required_ruby_version = Gem::Requirement.new(v)
+ when "created_at"
+ value = v.is_a?(Array) ? v.last : v
+ if value.is_a?(String)
+ @created_at = begin
+ Time.new(value)
+ rescue ArgumentError
+ nil
+ end
+ end
+ end
+ end
+ rescue StandardError => e
+ raise GemspecError, "There was an error parsing the metadata for the gem #{name} (#{version}): #{e.class}\n#{e}\nThe metadata was #{data.inspect}"
+ end
+
+ def build_dependency(name, requirements)
+ Dependency.new(name, requirements)
+ end
+ end
+end
diff --git a/lib/bundler/env.rb b/lib/bundler/env.rb
new file mode 100644
index 0000000000..2b29705060
--- /dev/null
+++ b/lib/bundler/env.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+require_relative "rubygems_integration"
+require_relative "source/git/git_proxy"
+
+module Bundler
+ class Env
+ def self.write(io)
+ io.write report
+ end
+
+ def self.report(options = {})
+ print_gemfile = options.delete(:print_gemfile) { true }
+ print_gemspecs = options.delete(:print_gemspecs) { true }
+
+ out = String.new
+ append_formatted_table("Environment", environment, out)
+ append_formatted_table("Bundler Build Metadata", BuildMetadata.to_h, out)
+
+ unless Bundler.settings.all.empty?
+ out << "\n## Bundler settings\n\n```\n"
+ Bundler.settings.all.each do |setting|
+ out << setting << "\n"
+ Bundler.settings.pretty_values_for(setting).each do |line|
+ out << " " << line << "\n"
+ end
+ end
+ out << "```\n"
+ end
+
+ return out unless SharedHelpers.in_bundle?
+
+ if print_gemfile
+ gemfiles = [Bundler.default_gemfile]
+ begin
+ gemfiles = Bundler.definition.gemfiles
+ rescue GemfileNotFound
+ nil
+ end
+
+ out << "\n## Gemfile\n"
+ gemfiles.each do |gemfile|
+ out << "\n### #{SharedHelpers.relative_path_to(gemfile)}\n\n"
+ out << "```ruby\n" << read_file(gemfile).chomp << "\n```\n"
+ end
+
+ out << "\n### #{SharedHelpers.relative_path_to(Bundler.default_lockfile)}\n\n"
+ out << "```\n" << read_file(Bundler.default_lockfile).chomp << "\n```\n"
+ end
+
+ if print_gemspecs
+ dsl = Dsl.new.tap {|d| d.eval_gemfile(Bundler.default_gemfile) }
+ out << "\n## Gemspecs\n" unless dsl.gemspecs.empty?
+ dsl.gemspecs.each do |gs|
+ out << "\n### #{File.basename(gs.loaded_from)}"
+ out << "\n\n```ruby\n" << read_file(gs.loaded_from).chomp << "\n```\n"
+ end
+ end
+
+ out
+ end
+
+ def self.read_file(filename)
+ Bundler.read_file(filename.to_s).strip
+ rescue Errno::ENOENT
+ "<No #{filename} found>"
+ rescue RuntimeError => e
+ "#{e.class}: #{e.message}"
+ end
+
+ def self.ruby_version
+ "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE} revision #{RUBY_REVISION}) [#{Gem::Platform.local}]"
+ end
+
+ def self.git_version
+ Bundler::Source::Git::GitProxy.new(nil, nil).full_version
+ rescue Bundler::Source::Git::GitNotInstalledError
+ "not installed"
+ end
+
+ def self.environment
+ out = []
+
+ out << ["Bundler", Bundler::VERSION]
+ out << [" Platforms", Gem.platforms.join(", ")]
+ out << ["Ruby", ruby_version]
+ out << [" Full Path", Gem.ruby]
+ out << [" Config Dir", Pathname.new(Gem::ConfigFile::SYSTEM_WIDE_CONFIG_FILE).dirname]
+ out << ["RubyGems", Gem::VERSION]
+ out << [" Gem Home", Gem.dir]
+ out << [" Gem Path", Gem.path.join(File::PATH_SEPARATOR)]
+ out << [" User Home", Gem.user_home]
+ out << [" User Path", Gem.user_dir]
+ out << [" Bin Dir", Gem.bindir]
+ if defined?(OpenSSL::SSL)
+ out << ["OpenSSL"]
+ out << [" Compiled", OpenSSL::OPENSSL_VERSION] if defined?(OpenSSL::OPENSSL_VERSION)
+ out << [" Loaded", OpenSSL::OPENSSL_LIBRARY_VERSION] if defined?(OpenSSL::OPENSSL_LIBRARY_VERSION)
+ out << [" Cert File", OpenSSL::X509::DEFAULT_CERT_FILE] if defined?(OpenSSL::X509::DEFAULT_CERT_FILE)
+ out << [" Cert Dir", OpenSSL::X509::DEFAULT_CERT_DIR] if defined?(OpenSSL::X509::DEFAULT_CERT_DIR)
+ end
+ out << ["Git", git_version]
+
+ if (exe = caller_locations.last.absolute_path)&.match? %r{(exe|bin)/bundler?\z}
+ shebang = File.read(exe).lines.first
+ shebang.sub!(/^#!\s*/, "")
+ unless shebang.start_with?(Gem.ruby, "/usr/bin/env ruby")
+ out << ["Gem.ruby", Gem.ruby]
+ out << ["bundle #!", shebang]
+ end
+ end
+
+ out
+ end
+
+ def self.append_formatted_table(title, pairs, out)
+ return if pairs.empty?
+ out << "\n" unless out.empty?
+ out << "## #{title}\n\n```\n"
+ ljust = pairs.map {|k, _v| k.to_s.length }.max
+ pairs.each do |k, v|
+ out << "#{k.to_s.ljust(ljust)} #{v}\n"
+ end
+ out << "```\n"
+ end
+
+ private_class_method :read_file, :ruby_version, :git_version, :append_formatted_table
+ end
+end
diff --git a/lib/bundler/environment_preserver.rb b/lib/bundler/environment_preserver.rb
new file mode 100644
index 0000000000..bf9478a299
--- /dev/null
+++ b/lib/bundler/environment_preserver.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Bundler
+ class EnvironmentPreserver
+ INTENTIONALLY_NIL = "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL"
+ BUNDLER_KEYS = %w[
+ BUNDLE_BIN_PATH
+ BUNDLE_GEMFILE
+ BUNDLE_LOCKFILE
+ BUNDLER_VERSION
+ BUNDLER_SETUP
+ GEM_HOME
+ GEM_PATH
+ MANPATH
+ PATH
+ RB_USER_INSTALL
+ RUBYLIB
+ RUBYOPT
+ ].map(&:freeze).freeze
+ BUNDLER_PREFIX = "BUNDLER_ORIG_"
+
+ def self.from_env
+ new(ENV.to_hash, BUNDLER_KEYS)
+ end
+
+ # @param env [Hash]
+ # @param keys [Array<String>]
+ def initialize(env, keys)
+ @original = env
+ @keys = keys
+ @prefix = BUNDLER_PREFIX
+ end
+
+ # Replaces `ENV` with the bundler environment variables backed up
+ def replace_with_backup
+ ENV.replace(backup)
+ end
+
+ # @return [Hash]
+ def backup
+ env = @original.clone
+ @keys.each do |key|
+ value = env[key]
+ if !value.nil?
+ env[@prefix + key] ||= value
+ else
+ env[@prefix + key] ||= INTENTIONALLY_NIL
+ end
+ end
+ env
+ end
+
+ # @return [Hash]
+ def restore
+ env = @original.clone
+ @keys.each do |key|
+ value_original = env[@prefix + key]
+ next if value_original.nil?
+ if value_original == INTENTIONALLY_NIL
+ env.delete(key)
+ else
+ env[key] = value_original
+ end
+ env.delete(@prefix + key)
+ end
+ env
+ end
+ end
+end
diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb
new file mode 100644
index 0000000000..dff5d93128
--- /dev/null
+++ b/lib/bundler/errors.rb
@@ -0,0 +1,306 @@
+# frozen_string_literal: true
+
+module Bundler
+ class BundlerError < StandardError
+ def self.status_code(code)
+ define_method(:status_code) { code }
+ if match = BundlerError.all_errors.find {|_k, v| v == code }
+ error, _ = match
+ raise ArgumentError,
+ "Trying to register #{self} for status code #{code} but #{error} is already registered"
+ end
+ BundlerError.all_errors[self] = code
+ end
+
+ def self.all_errors
+ @all_errors ||= {}
+ end
+ end
+
+ class GemfileError < BundlerError; status_code(4); end
+ class InstallError < BundlerError; status_code(5); end
+
+ # Internal error, should be rescued
+ class SolveFailure < BundlerError; status_code(6); end
+
+ class GemNotFound < BundlerError; status_code(7); end
+ class InstallHookError < BundlerError; status_code(8); end
+ class RemovedError < BundlerError; status_code(9); end
+ class GemfileNotFound < BundlerError; status_code(10); end
+ class GitError < BundlerError; status_code(11); end
+ class DeprecatedError < BundlerError; status_code(12); end
+ class PathError < BundlerError; status_code(13); end
+ class GemspecError < BundlerError; status_code(14); end
+ class InvalidOption < BundlerError; status_code(15); end
+ class ProductionError < BundlerError; status_code(16); end
+
+ class HTTPError < BundlerError
+ status_code(17)
+ def filter_uri(uri)
+ URICredentialsFilter.credential_filtered_uri(uri)
+ end
+ end
+
+ class RubyVersionMismatch < BundlerError; status_code(18); end
+ class SecurityError < BundlerError; status_code(19); end
+ class LockfileError < BundlerError; status_code(20); end
+ class CyclicDependencyError < BundlerError; status_code(21); end
+ class GemfileLockNotFound < BundlerError; status_code(22); end
+ class PluginError < BundlerError; status_code(29); end
+ class ThreadCreationError < BundlerError; status_code(33); end
+ class APIResponseMismatchError < BundlerError; status_code(34); end
+ class APIResponseInvalidDependenciesError < BundlerError; status_code(35); end
+ class GemfileEvalError < GemfileError; end
+ class MarshalError < StandardError; end
+
+ class ChecksumMismatchError < SecurityError
+ def initialize(lock_name, existing, checksum)
+ @lock_name = lock_name
+ @existing = existing
+ @checksum = checksum
+ end
+
+ def message
+ <<~MESSAGE
+ Bundler found mismatched checksums. This is a potential security risk.
+ #{@lock_name} #{@existing.to_lock}
+ from #{@existing.sources.join("\n and ")}
+ #{@lock_name} #{@checksum.to_lock}
+ from #{@checksum.sources.join("\n and ")}
+
+ #{mismatch_resolution_instructions}
+ To ignore checksum security warnings, disable checksum validation with
+ `bundle config set --local disable_checksum_validation true`
+ MESSAGE
+ end
+
+ def mismatch_resolution_instructions
+ removable, remote = [@existing, @checksum].partition(&:removable?)
+ case removable.size
+ when 1
+ msg = +"If you trust #{remote.first.sources.first}, to resolve this issue you can:\n"
+ msg << removable.first.removal_instructions
+ when 2
+ msg = +"To resolve this issue you can either:\n"
+ msg << @checksum.removal_instructions
+ msg << "or if you are sure that the new checksum from #{@checksum.sources.first} is correct:\n"
+ msg << @existing.removal_instructions
+ end
+ end
+
+ status_code(37)
+ end
+
+ class PermissionError < BundlerError
+ def initialize(path, permission_type = :write)
+ @path = path
+ @permission_type = permission_type
+ end
+
+ def action
+ case @permission_type
+ when :read then "read from"
+ when :write then "write to"
+ when :executable, :exec then "execute"
+ else @permission_type.to_s
+ end
+ end
+
+ def permission_type
+ case @permission_type
+ when :create
+ "executable permissions for all parent directories and write permissions for `#{parent_folder}`"
+ else
+ "#{@permission_type} permissions for that path"
+ end
+ end
+
+ def parent_folder
+ File.dirname(@path)
+ end
+
+ def message
+ "There was an error while trying to #{action} `#{@path}`. " \
+ "It is likely that you need to grant #{permission_type}."
+ end
+
+ status_code(23)
+ end
+
+ class GemRequireError < BundlerError
+ attr_reader :orig_exception
+
+ def initialize(orig_exception, msg)
+ full_message = msg + "\nGem Load Error is:
+ #{orig_exception.full_message(highlight: false)}\n"\
+ "Backtrace for gem load error is:\n"\
+ "#{orig_exception.backtrace.join("\n")}\n"\
+ "Bundler Error Backtrace:\n"
+ super(full_message)
+ @orig_exception = orig_exception
+ end
+
+ status_code(24)
+ end
+
+ class YamlSyntaxError < BundlerError
+ attr_reader :orig_exception
+
+ def initialize(orig_exception, msg)
+ super(msg)
+ @orig_exception = orig_exception
+ end
+
+ status_code(25)
+ end
+
+ class TemporaryResourceError < PermissionError
+ def message
+ "There was an error while trying to #{action} `#{@path}`. " \
+ "Some resource was temporarily unavailable. It's suggested that you try" \
+ "the operation again."
+ end
+
+ status_code(26)
+ end
+
+ class VirtualProtocolError < BundlerError
+ def message
+ "There was an error relating to virtualization and file access. " \
+ "It is likely that you need to grant access to or mount some file system correctly."
+ end
+
+ status_code(27)
+ end
+
+ class OperationNotSupportedError < PermissionError
+ def message
+ "Attempting to #{action} `#{@path}` is unsupported by your OS."
+ end
+
+ status_code(28)
+ end
+
+ class NoSpaceOnDeviceError < PermissionError
+ def message
+ "There was an error while trying to #{action} `#{@path}`. " \
+ "There was insufficient space remaining on the device."
+ end
+
+ status_code(31)
+ end
+
+ class ReadOnlyFileSystemError < PermissionError
+ def message
+ "There was an error while trying to #{action} `#{@path}`. " \
+ "File system is read-only."
+ end
+
+ status_code(42)
+ end
+
+ class OperationNotPermittedError < PermissionError
+ def message
+ "There was an error while trying to #{action} `#{@path}`. " \
+ "Underlying OS system call raised an EPERM error."
+ end
+
+ status_code(43)
+ end
+
+ class GenericSystemCallError < BundlerError
+ attr_reader :underlying_error
+
+ def initialize(underlying_error, message)
+ @underlying_error = underlying_error
+ super("#{message}\nThe underlying system error is #{@underlying_error.class}: #{@underlying_error}")
+ end
+
+ status_code(32)
+ end
+
+ class DirectoryRemovalError < BundlerError
+ def initialize(orig_exception, msg)
+ full_message = "#{msg}.\n" \
+ "The underlying error was #{orig_exception.class}:
+ #{orig_exception.full_message(highlight: false)},
+ with backtrace:\n" \
+ " #{orig_exception.backtrace.join("\n ")}\n\n" \
+ "Bundler Error Backtrace:"
+ super(full_message)
+ end
+
+ status_code(36)
+ end
+
+ class InsecureInstallPathError < BundlerError
+ def initialize(name, path)
+ @name = name
+ @path = path
+ end
+
+ def message
+ "Bundler cannot reinstall #{@name} because there's a previous installation of it at #{@path} that is unsafe to remove.\n" \
+ "The parent of #{@path} is world-writable and does not have the sticky bit set, making it insecure to remove due to potential vulnerabilities.\n" \
+ "Please change the permissions of #{File.dirname(@path)} or choose a different install path."
+ end
+
+ status_code(38)
+ end
+
+ class CorruptBundlerInstallError < BundlerError
+ def initialize(loaded_spec)
+ @loaded_spec = loaded_spec
+ end
+
+ def message
+ "The running version of Bundler (#{Bundler::VERSION}) does not match the version of the specification installed for it (#{@loaded_spec.version}). " \
+ "This can be caused by reinstalling Ruby without removing previous installation, leaving around an upgraded default version of Bundler. " \
+ "Reinstalling Ruby from scratch should fix the problem."
+ end
+
+ status_code(39)
+ end
+
+ class InvalidArgumentError < BundlerError; status_code(40); end
+
+ class IncorrectLockfileDependencies < BundlerError
+ attr_reader :spec, :actual_dependencies, :lockfile_dependencies
+
+ def initialize(spec, actual_dependencies = nil, lockfile_dependencies = nil)
+ @spec = spec
+ @actual_dependencies = actual_dependencies
+ @lockfile_dependencies = lockfile_dependencies
+ end
+
+ def message
+ lines = ["Bundler found incorrect dependencies in the lockfile for #{spec.full_name}", ""]
+
+ if @actual_dependencies && @lockfile_dependencies
+ actual_by_name = @actual_dependencies.each_with_object({}) {|d, h| h[d.name] = d }
+ lockfile_by_name = @lockfile_dependencies.each_with_object({}) {|d, h| h[d.name] = d }
+
+ (actual_by_name.keys | lockfile_by_name.keys).sort.each do |name|
+ actual = actual_by_name[name]
+ lockfile = lockfile_by_name[name]
+ next if actual && lockfile && actual.requirement == lockfile.requirement
+
+ if actual && lockfile
+ lines << " #{name}: gemspec specifies #{actual.requirement}, lockfile has #{lockfile.requirement}"
+ elsif actual
+ lines << " #{name}: gemspec specifies #{actual.requirement}, not in lockfile"
+ else
+ lines << " #{name}: not in gemspec, lockfile has #{lockfile.requirement}"
+ end
+ end
+
+ lines << ""
+ end
+
+ lines << "Please run `bundle install` to regenerate the lockfile."
+ lines.join("\n")
+ end
+
+ status_code(41)
+ end
+end
diff --git a/lib/bundler/feature_flag.rb b/lib/bundler/feature_flag.rb
new file mode 100644
index 0000000000..dea8abedba
--- /dev/null
+++ b/lib/bundler/feature_flag.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Bundler
+ class FeatureFlag
+ (1..10).each {|v| define_method("bundler_#{v}_mode?") { @major_version >= v } }
+
+ def removed_major?(target_major_version)
+ @major_version > target_major_version
+ end
+
+ def deprecated_major?(target_major_version)
+ @major_version >= target_major_version
+ end
+
+ def initialize(bundler_version)
+ @bundler_version = Gem::Version.create(bundler_version)
+ @major_version = @bundler_version.segments.first
+ end
+ end
+end
diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb
new file mode 100644
index 0000000000..0b6ced6f39
--- /dev/null
+++ b/lib/bundler/fetcher.rb
@@ -0,0 +1,361 @@
+# frozen_string_literal: true
+
+require_relative "vendored_persistent"
+require_relative "vendored_timeout"
+require_relative "vendored_securerandom"
+require "zlib"
+
+module Bundler
+ # Handles all the fetching with the rubygems server
+ class Fetcher
+ autoload :Base, File.expand_path("fetcher/base", __dir__)
+ autoload :CompactIndex, File.expand_path("fetcher/compact_index", __dir__)
+ autoload :Downloader, File.expand_path("fetcher/downloader", __dir__)
+ autoload :Dependency, File.expand_path("fetcher/dependency", __dir__)
+ autoload :Index, File.expand_path("fetcher/index", __dir__)
+
+ # This error is raised when it looks like the network is down
+ class NetworkDownError < HTTPError; end
+ # This error is raised if we should rate limit our requests to the API
+ class TooManyRequestsError < HTTPError; end
+ # This error is raised if the API returns a 413 (only printed in verbose)
+ class FallbackError < HTTPError; end
+
+ # This is the error raised if OpenSSL fails the cert verification
+ class CertificateFailureError < HTTPError
+ def initialize(remote_uri)
+ remote_uri = filter_uri(remote_uri)
+ super "Could not verify the SSL certificate for #{remote_uri}.\nThere" \
+ " is a chance you are experiencing a man-in-the-middle attack, but" \
+ " most likely your system doesn't have the CA certificates needed" \
+ " for verification. For information about OpenSSL certificates, see" \
+ " https://railsapps.github.io/openssl-certificate-verify-failed.html."
+ end
+ end
+
+ # This is the error raised when a source is HTTPS and OpenSSL didn't load
+ class SSLError < HTTPError
+ def initialize(msg = nil)
+ super "Could not load OpenSSL.\n" \
+ "You must recompile Ruby with OpenSSL support.\n" \
+ "original error: #{msg}\n"
+ end
+ end
+
+ # This error is raised if HTTP authentication is required, but not provided.
+ class AuthenticationRequiredError < HTTPError
+ def initialize(remote_uri)
+ remote_uri = filter_uri(remote_uri)
+ super "Authentication is required for #{remote_uri}.\n" \
+ "Please supply credentials for this source. You can do this by running:\n" \
+ "`bundle config set --global #{remote_uri} username:password`\n" \
+ "or by storing the credentials in the `#{Settings.key_for(remote_uri)}` environment variable"
+ end
+ end
+
+ # This error is raised if HTTP authentication is provided, but incorrect.
+ class BadAuthenticationError < HTTPError
+ def initialize(remote_uri)
+ remote_uri = filter_uri(remote_uri)
+ super "Bad username or password for #{remote_uri}.\n" \
+ "Please double-check your credentials and correct them."
+ end
+ end
+
+ # This error is raised if HTTP authentication is correct, but lacks
+ # necessary permissions.
+ class AuthenticationForbiddenError < HTTPError
+ def initialize(remote_uri)
+ remote_uri = filter_uri(remote_uri)
+ super "Access token could not be authenticated for #{remote_uri}.\n" \
+ "Make sure it's valid and has the necessary scopes configured."
+ end
+ end
+
+ HTTP_ERRORS = (Downloader::HTTP_RETRYABLE_ERRORS + Downloader::HTTP_NON_RETRYABLE_ERRORS).freeze
+ deprecate_constant :HTTP_ERRORS
+
+ NET_ERRORS = [
+ :HTTPBadGateway,
+ :HTTPBadRequest,
+ :HTTPFailedDependency,
+ :HTTPForbidden,
+ :HTTPInsufficientStorage,
+ :HTTPMethodNotAllowed,
+ :HTTPMovedPermanently,
+ :HTTPNoContent,
+ :HTTPNotFound,
+ :HTTPNotImplemented,
+ :HTTPPreconditionFailed,
+ :HTTPRequestEntityTooLarge,
+ :HTTPRequestURITooLong,
+ :HTTPUnauthorized,
+ :HTTPUnprocessableEntity,
+ :HTTPUnsupportedMediaType,
+ :HTTPVersionNotSupported,
+ ].freeze
+ deprecate_constant :NET_ERRORS
+
+ # Exceptions classes that should bypass retry attempts. If your password didn't work the
+ # first time, it's not going to the third time.
+ FAIL_ERRORS = [
+ AuthenticationRequiredError,
+ BadAuthenticationError,
+ AuthenticationForbiddenError,
+ FallbackError,
+ SecurityError,
+ Gem::Requirement::BadRequirementError,
+ Gem::Net::HTTPBadGateway,
+ Gem::Net::HTTPBadRequest,
+ Gem::Net::HTTPFailedDependency,
+ Gem::Net::HTTPForbidden,
+ Gem::Net::HTTPInsufficientStorage,
+ Gem::Net::HTTPMethodNotAllowed,
+ Gem::Net::HTTPMovedPermanently,
+ Gem::Net::HTTPNoContent,
+ Gem::Net::HTTPNotFound,
+ Gem::Net::HTTPNotImplemented,
+ Gem::Net::HTTPPreconditionFailed,
+ Gem::Net::HTTPRequestEntityTooLarge,
+ Gem::Net::HTTPRequestURITooLong,
+ Gem::Net::HTTPUnauthorized,
+ Gem::Net::HTTPUnprocessableEntity,
+ Gem::Net::HTTPUnsupportedMediaType,
+ Gem::Net::HTTPVersionNotSupported,
+ ].freeze
+
+ class << self
+ attr_accessor :disable_endpoint, :api_timeout, :redirect_limit, :max_retries
+ end
+
+ self.redirect_limit = Bundler.settings[:redirect] # How many redirects to allow in one request
+ self.api_timeout = Bundler.settings[:timeout] # How long to wait for each API call
+ self.max_retries = Bundler.settings[:retry] # How many retries for the API call
+
+ def initialize(remote)
+ @cis = nil
+ @remote = remote
+
+ Socket.do_not_reverse_lookup = true
+ connection # create persistent connection
+ end
+
+ def uri
+ @remote.anonymized_uri
+ end
+
+ # fetch a gem specification
+ def fetch_spec(spec)
+ spec -= [nil, "ruby", ""]
+ spec_file_name = "#{spec.join "-"}.gemspec"
+
+ uri = Gem::URI.parse("#{remote_uri}#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}.rz")
+ spec = if uri.scheme == "file"
+ path = Gem::Util.correct_for_windows_path(uri.path)
+ Bundler.safe_load_marshal Bundler.rubygems.inflate(Gem.read_binary(path))
+ elsif cached_spec_path = gemspec_cached_path(spec_file_name)
+ Bundler.load_gemspec(cached_spec_path)
+ else
+ Bundler.safe_load_marshal Bundler.rubygems.inflate(downloader.fetch(uri).body)
+ end
+ raise MarshalError, "is #{spec.inspect}" unless spec.is_a?(Gem::Specification)
+ spec
+ rescue MarshalError
+ raise HTTPError, "Gemspec #{spec} contained invalid data.\n" \
+ "Your network or your gem server is probably having issues right now."
+ end
+
+ # return the specs in the bundler format as an index with retries
+ def specs_with_retry(gem_names, source)
+ Bundler::Retry.new("fetcher", FAIL_ERRORS).attempts do
+ specs(gem_names, source)
+ end
+ end
+
+ # return the specs in the bundler format as an index
+ def specs(gem_names, source)
+ index = Bundler::Index.new
+
+ fetch_specs(gem_names).each do |name, version, platform, dependencies, metadata|
+ spec = if dependencies
+ EndpointSpecification.new(name, version, platform, self, dependencies, metadata).tap do |es|
+ source.checksum_store.replace(es, es.checksum)
+ end
+ else
+ RemoteSpecification.new(name, version, platform, self)
+ end
+ spec.source = source
+ spec.remote = @remote
+ index << spec
+ end
+
+ index
+ rescue CertificateFailureError
+ Bundler.ui.info "" if gem_names && api_fetcher? # newline after dots
+ raise
+ end
+
+ def user_agent
+ @user_agent ||= begin
+ ruby = Bundler::RubyVersion.system
+
+ agent = String.new("bundler/#{Bundler::VERSION}")
+ agent << " rubygems/#{Gem::VERSION}"
+ agent << " ruby/#{ruby.versions_string(ruby.versions)}"
+ agent << " (#{ruby.host})"
+ agent << " command/#{ARGV.first}"
+
+ if ruby.engine != "ruby"
+ # engine_version raises on unknown engines
+ engine_version = begin
+ ruby.engine_versions
+ rescue RuntimeError
+ "???"
+ end
+ agent << " #{ruby.engine}/#{ruby.versions_string(engine_version)}"
+ end
+
+ agent << " options/#{Bundler.settings.all.join(",")}"
+
+ agent << " ci/#{cis.join(",")}" if cis.any?
+
+ # add a random ID so we can consolidate runs server-side
+ agent << " " << Gem::SecureRandom.hex(8)
+
+ # add any user agent strings set in the config
+ extra_ua = Bundler.settings[:user_agent]
+ agent << " " << extra_ua if extra_ua
+
+ agent
+ end
+ end
+
+ def http_proxy
+ return unless uri = connection.proxy_uri
+ uri.to_s
+ end
+
+ def inspect
+ "#<#{self.class}:0x#{object_id} uri=#{uri}>"
+ end
+
+ def api_fetcher?
+ fetchers.first.api_fetcher?
+ end
+
+ def gem_remote_fetcher
+ @gem_remote_fetcher ||= begin
+ require_relative "fetcher/gem_remote_fetcher"
+ fetcher = GemRemoteFetcher.new Gem.configuration[:http_proxy]
+ fetcher.headers["User-Agent"] = user_agent
+ fetcher.headers["X-Gemfile-Source"] = @remote.original_uri.to_s if @remote.original_uri
+ fetcher
+ end
+ end
+
+ private
+
+ def available_fetchers
+ if Bundler::Fetcher.disable_endpoint
+ [Index]
+ elsif remote_uri.scheme == "file"
+ Bundler.ui.debug("Using a local server, bundler won't use the CompactIndex API")
+ [Index]
+ else
+ [CompactIndex, Dependency, Index]
+ end
+ end
+
+ def fetchers
+ @fetchers ||= available_fetchers.map {|f| f.new(downloader, @remote, uri, gem_remote_fetcher) }.drop_while {|f| !f.available? }
+ end
+
+ def fetch_specs(gem_names)
+ fetchers.reject!(&:api_fetcher?) unless gem_names
+ fetchers.reject! do |f|
+ specs = f.specs(gem_names)
+ return specs if specs
+ true
+ end
+ []
+ end
+
+ def cis
+ @cis ||= Bundler::CIDetector.ci_strings
+ end
+
+ def connection
+ @connection ||= begin
+ needs_ssl = remote_uri.scheme == "https" ||
+ Bundler.settings[:ssl_verify_mode] ||
+ Bundler.settings[:ssl_client_cert]
+ if needs_ssl
+ begin
+ require "openssl"
+ rescue StandardError, LoadError => e
+ raise SSLError.new(e.message)
+ end
+ end
+
+ con = Gem::Net::HTTP::Persistent.new name: "bundler", proxy: :ENV
+ if gem_proxy = Gem.configuration[:http_proxy]
+ con.proxy = Gem::URI.parse(gem_proxy) if gem_proxy != :no_proxy
+ end
+
+ if remote_uri.scheme == "https"
+ con.verify_mode = (Bundler.settings[:ssl_verify_mode] ||
+ OpenSSL::SSL::VERIFY_PEER)
+ con.cert_store = bundler_cert_store
+ end
+
+ ssl_client_cert = Bundler.settings[:ssl_client_cert] ||
+ (Gem.configuration.ssl_client_cert if
+ Gem.configuration.respond_to?(:ssl_client_cert))
+ if ssl_client_cert
+ pem = File.read(ssl_client_cert)
+ con.cert = OpenSSL::X509::Certificate.new(pem)
+ con.key = OpenSSL::PKey::RSA.new(pem)
+ end
+
+ con.read_timeout = Fetcher.api_timeout
+ con.open_timeout = Fetcher.api_timeout
+ con.override_headers["User-Agent"] = user_agent
+ con.override_headers["X-Gemfile-Source"] = @remote.original_uri.to_s if @remote.original_uri
+ con
+ end
+ end
+
+ # cached gem specification path, if one exists
+ def gemspec_cached_path(spec_file_name)
+ paths = Bundler.rubygems.spec_cache_dirs.map {|dir| File.join(dir, spec_file_name) }
+ paths.find {|path| File.file? path }
+ end
+
+ def bundler_cert_store
+ store = OpenSSL::X509::Store.new
+ ssl_ca_cert = Bundler.settings[:ssl_ca_cert] ||
+ (Gem.configuration.ssl_ca_cert if
+ Gem.configuration.respond_to?(:ssl_ca_cert))
+ if ssl_ca_cert
+ if File.directory? ssl_ca_cert
+ store.add_path ssl_ca_cert
+ else
+ store.add_file ssl_ca_cert
+ end
+ else
+ store.set_default_paths
+ require "rubygems/request"
+ Gem::Request.get_cert_files.each {|c| store.add_file c }
+ end
+ store
+ end
+
+ def remote_uri
+ @remote.uri
+ end
+
+ def downloader
+ @downloader ||= Downloader.new(connection, self.class.redirect_limit)
+ end
+ end
+end
diff --git a/lib/bundler/fetcher/base.rb b/lib/bundler/fetcher/base.rb
new file mode 100644
index 0000000000..cfec2f8e94
--- /dev/null
+++ b/lib/bundler/fetcher/base.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Fetcher
+ class Base
+ attr_reader :downloader
+ attr_reader :display_uri
+ attr_reader :remote
+ attr_reader :gem_remote_fetcher
+
+ def initialize(downloader, remote, display_uri, gem_remote_fetcher)
+ raise "Abstract class" if self.class == Base
+ @downloader = downloader
+ @remote = remote
+ @display_uri = display_uri
+ @gem_remote_fetcher = gem_remote_fetcher
+ end
+
+ def remote_uri
+ @remote.uri
+ end
+
+ def fetch_uri
+ @fetch_uri ||= if remote_uri.host == "rubygems.org"
+ uri = remote_uri.dup
+ uri.host = "index.rubygems.org"
+ uri
+ else
+ remote_uri
+ end
+ end
+
+ def available?
+ true
+ end
+
+ def api_fetcher?
+ false
+ end
+
+ private
+
+ def log_specs(&block)
+ if Bundler.ui.debug?
+ Bundler.ui.debug yield
+ else
+ Bundler.ui.info ".", false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/fetcher/compact_index.rb b/lib/bundler/fetcher/compact_index.rb
new file mode 100644
index 0000000000..52168111fe
--- /dev/null
+++ b/lib/bundler/fetcher/compact_index.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require_relative "base"
+require_relative "../worker"
+
+module Bundler
+ class Fetcher
+ class CompactIndex < Base
+ def self.compact_index_request(method_name)
+ method = instance_method(method_name)
+ undef_method(method_name)
+ define_method(method_name) do |*args, &blk|
+ method.bind_call(self, *args, &blk)
+ rescue NetworkDownError, CompactIndexClient::Updater::MismatchedChecksumError => e
+ raise HTTPError, e.message
+ rescue AuthenticationRequiredError, BadAuthenticationError
+ # Fail since we got a 401 from the server.
+ raise
+ rescue HTTPError => e
+ Bundler.ui.trace(e)
+ nil
+ end
+ end
+
+ def specs(gem_names)
+ specs_for_names(gem_names)
+ end
+ compact_index_request :specs
+
+ def specs_for_names(gem_names)
+ gem_info = []
+ complete_gems = []
+ remaining_gems = gem_names.dup
+
+ until remaining_gems.empty?
+ log_specs { "Looking up gems #{remaining_gems.inspect}" }
+ deps = fetch_gem_infos(remaining_gems).flatten(1)
+ next_gems = deps.flat_map {|d| d[CompactIndexClient::INFO_DEPS].flat_map(&:first) }.uniq
+ deps.each {|dep| gem_info << dep }
+ complete_gems.concat(deps.map(&:first)).uniq!
+ remaining_gems = next_gems - complete_gems
+ end
+ @bundle_worker&.stop
+ @bundle_worker = nil # reset it. Not sure if necessary
+
+ gem_info
+ end
+
+ def available?
+ unless SharedHelpers.md5_available?
+ Bundler.ui.debug("FIPS mode is enabled, bundler can't use the CompactIndex API")
+ return nil
+ end
+ # Read info file checksums out of /versions, so we can know if gems are up to date
+ compact_index_client.available?
+ rescue CompactIndexClient::Updater::MismatchedChecksumError => e
+ Bundler.ui.debug(e.message)
+ nil
+ end
+ compact_index_request :available?
+
+ def api_fetcher?
+ true
+ end
+
+ private
+
+ def compact_index_client
+ @compact_index_client ||=
+ SharedHelpers.filesystem_access(cache_path) do
+ CompactIndexClient.new(cache_path, client_fetcher)
+ end
+ end
+
+ def fetch_gem_infos(names)
+ in_parallel(names) {|name| compact_index_client.info(name) }
+ rescue TooManyRequestsError # rubygems.org is rate limiting us, slow down.
+ @bundle_worker&.stop
+ @bundle_worker = nil # reset it. Not sure if necessary
+ compact_index_client.reset!
+ names.map {|name| compact_index_client.info(name) }
+ end
+
+ def in_parallel(inputs, &blk)
+ func = lambda {|object, _index| blk.call(object) }
+ worker = bundle_worker(func)
+ inputs.each {|input| worker.enq(input) }
+ inputs.map { worker.deq }
+ end
+
+ def bundle_worker(func = nil)
+ @bundle_worker ||= begin
+ worker_name = "Compact Index (#{display_uri.host})"
+ Bundler::Worker.new(Bundler.settings.processor_count, worker_name, func)
+ end
+ @bundle_worker.tap do |worker|
+ worker.instance_variable_set(:@func, func) if func
+ end
+ end
+
+ def cache_path
+ Bundler.user_cache.join("compact_index", remote.cache_slug)
+ end
+
+ def client_fetcher
+ ClientFetcher.new(self, Bundler.ui)
+ end
+
+ ClientFetcher = Struct.new(:fetcher, :ui) do
+ def call(path, headers)
+ fetcher.downloader.fetch(fetcher.fetch_uri + path, headers)
+ rescue NetworkDownError => e
+ raise unless headers["If-None-Match"]
+ ui.warn "Using the cached data for the new index because of a network error: #{e}"
+ Gem::Net::HTTPNotModified.new(nil, nil, nil)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/fetcher/dependency.rb b/lib/bundler/fetcher/dependency.rb
new file mode 100644
index 0000000000..4f2414e33d
--- /dev/null
+++ b/lib/bundler/fetcher/dependency.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require_relative "base"
+require "cgi/escape"
+require "cgi/util" unless defined?(CGI::EscapeExt)
+
+module Bundler
+ class Fetcher
+ class Dependency < Base
+ def available?
+ @available ||= fetch_uri.scheme != "file" && downloader.fetch(dependency_api_uri)
+ rescue NetworkDownError => e
+ raise HTTPError, e.message
+ rescue AuthenticationRequiredError
+ # Fail since we got a 401 from the server.
+ raise
+ rescue HTTPError
+ false
+ end
+
+ def api_fetcher?
+ true
+ end
+
+ def specs(gem_names, full_dependency_list = [], last_spec_list = [])
+ query_list = gem_names.uniq - full_dependency_list
+
+ log_specs { "Query List: #{query_list.inspect}" }
+
+ return last_spec_list if query_list.empty?
+
+ spec_list, deps_list = Bundler::Retry.new("dependency api", FAIL_ERRORS).attempts do
+ dependency_specs(query_list)
+ end
+
+ returned_gems = spec_list.map(&:first).uniq
+ specs(deps_list, full_dependency_list + returned_gems, spec_list + last_spec_list)
+ rescue MarshalError, HTTPError, GemspecError
+ Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over
+ Bundler.ui.debug "could not fetch from the dependency API, trying the full index"
+ nil
+ end
+
+ def dependency_specs(gem_names)
+ Bundler.ui.debug "Query Gemcutter Dependency Endpoint API: #{gem_names.join(",")}"
+
+ gem_list = unmarshalled_dep_gems(gem_names)
+ get_formatted_specs_and_deps(gem_list)
+ end
+
+ def unmarshalled_dep_gems(gem_names)
+ gem_list = []
+ gem_names.each_slice(api_request_size) do |names|
+ marshalled_deps = downloader.fetch(dependency_api_uri(names)).body
+ gem_list.concat(Bundler.safe_load_marshal(marshalled_deps))
+ end
+ gem_list
+ end
+
+ def get_formatted_specs_and_deps(gem_list)
+ deps_list = []
+ spec_list = []
+
+ gem_list.each do |s|
+ deps_list.concat(s[:dependencies].map(&:first))
+ deps = s[:dependencies].map {|n, d| [n, d.split(", ")] }
+ spec_list.push([s[:name], s[:number], s[:platform], deps])
+ end
+ [spec_list, deps_list]
+ end
+
+ def dependency_api_uri(gem_names = [])
+ uri = fetch_uri + "api/v1/dependencies"
+ uri.query = "gems=#{CGI.escape(gem_names.sort.join(","))}" if gem_names.any?
+ uri
+ end
+
+ private
+
+ def api_request_size
+ Bundler.settings[:api_request_size]&.to_i || Source::Rubygems::API_REQUEST_SIZE
+ end
+ end
+ end
+end
diff --git a/lib/bundler/fetcher/downloader.rb b/lib/bundler/fetcher/downloader.rb
new file mode 100644
index 0000000000..179eed8340
--- /dev/null
+++ b/lib/bundler/fetcher/downloader.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Fetcher
+ class Downloader
+ HTTP_NON_RETRYABLE_ERRORS = [
+ SocketError,
+ Errno::EADDRNOTAVAIL,
+ Errno::ENETDOWN,
+ Errno::ENETUNREACH,
+ Gem::Net::HTTP::Persistent::Error,
+ Errno::EHOSTUNREACH,
+ ].freeze
+
+ HTTP_RETRYABLE_ERRORS = [
+ Gem::Timeout::Error,
+ EOFError,
+ Errno::EINVAL,
+ Errno::ECONNRESET,
+ Errno::ETIMEDOUT,
+ Errno::EAGAIN,
+ Gem::Net::HTTPBadResponse,
+ Gem::Net::HTTPHeaderSyntaxError,
+ Gem::Net::ProtocolError,
+ Zlib::BufError,
+ ].freeze
+
+ attr_reader :connection
+ attr_reader :redirect_limit
+
+ def initialize(connection, redirect_limit)
+ @connection = connection
+ @redirect_limit = redirect_limit
+ end
+
+ def fetch(uri, headers = {}, counter = 0)
+ raise HTTPError, "Too many redirects" if counter >= redirect_limit
+
+ filtered_uri = URICredentialsFilter.credential_filtered_uri(uri)
+
+ response = request(uri, headers)
+ Bundler.ui.debug("HTTP #{response.code} #{response.message} #{filtered_uri}")
+
+ case response
+ when Gem::Net::HTTPSuccess, Gem::Net::HTTPNotModified
+ response
+ when Gem::Net::HTTPRedirection
+ new_uri = Gem::URI.parse(response["location"])
+ if new_uri.host == uri.host
+ new_uri.user = uri.user
+ new_uri.password = uri.password
+ end
+ fetch(new_uri, headers, counter + 1)
+ when Gem::Net::HTTPRequestedRangeNotSatisfiable
+ new_headers = headers.dup
+ new_headers.delete("Range")
+ fetch(uri, new_headers)
+ when Gem::Net::HTTPRequestEntityTooLarge
+ raise FallbackError, response.body
+ when Gem::Net::HTTPTooManyRequests
+ raise TooManyRequestsError, response.body
+ when Gem::Net::HTTPUnauthorized
+ raise BadAuthenticationError, uri.host if uri.userinfo
+ raise AuthenticationRequiredError, uri.host
+ when Gem::Net::HTTPForbidden
+ raise AuthenticationForbiddenError, uri.host
+ when Gem::Net::HTTPNotFound
+ raise FallbackError, "Gem::Net::HTTPNotFound: #{filtered_uri}"
+ else
+ message = "Gem::#{response.class.name.gsub(/\AGem::/, "")}"
+ message += ": #{response.body}" unless response.body.empty?
+ raise HTTPError, message
+ end
+ end
+
+ def request(uri, headers)
+ validate_uri_scheme!(uri)
+
+ filtered_uri = URICredentialsFilter.credential_filtered_uri(uri)
+
+ Bundler.ui.debug "HTTP GET #{filtered_uri}"
+ req = Gem::Net::HTTP::Get.new uri.request_uri, headers
+ if uri.user
+ user = CGI.unescape(uri.user)
+ password = uri.password ? CGI.unescape(uri.password) : nil
+ req.basic_auth(user, password)
+ end
+ connection.request(uri, req)
+ rescue OpenSSL::SSL::SSLError
+ raise CertificateFailureError.new(uri)
+ rescue *HTTP_NON_RETRYABLE_ERRORS => e
+ Bundler.ui.trace e
+
+ host = uri.host
+ host_port = "#{host}:#{uri.port}"
+ host = host_port if filtered_uri.to_s.include?(host_port)
+ raise NetworkDownError, "Could not reach host #{host}. Check your network " \
+ "connection and try again."
+ rescue *HTTP_RETRYABLE_ERRORS => e
+ Bundler.ui.trace e
+
+ raise HTTPError, "Network error while fetching #{filtered_uri}" \
+ " (#{e})"
+ end
+
+ private
+
+ def validate_uri_scheme!(uri)
+ return if /\Ahttps?\z/.match?(uri.scheme)
+ raise InvalidOption,
+ "The request uri `#{uri}` has an invalid scheme (`#{uri.scheme}`). " \
+ "Did you mean `http` or `https`?"
+ end
+ end
+ end
+end
diff --git a/lib/bundler/fetcher/gem_remote_fetcher.rb b/lib/bundler/fetcher/gem_remote_fetcher.rb
new file mode 100644
index 0000000000..3159e05688
--- /dev/null
+++ b/lib/bundler/fetcher/gem_remote_fetcher.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "rubygems/remote_fetcher"
+
+module Bundler
+ class Fetcher
+ class GemRemoteFetcher < Gem::RemoteFetcher
+ def initialize(*)
+ super
+
+ @pool_size = Bundler.settings.installation_parallelization
+ end
+
+ def request(*args)
+ super do |req|
+ req.delete("User-Agent") if headers["User-Agent"]
+ yield req if block_given?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/fetcher/index.rb b/lib/bundler/fetcher/index.rb
new file mode 100644
index 0000000000..6e37e1e5d1
--- /dev/null
+++ b/lib/bundler/fetcher/index.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require_relative "base"
+
+module Bundler
+ class Fetcher
+ class Index < Base
+ def specs(_gem_names)
+ Bundler.rubygems.fetch_all_remote_specs(remote, gem_remote_fetcher)
+ rescue Gem::RemoteFetcher::FetchError => e
+ case e.message
+ when /certificate verify failed/
+ raise CertificateFailureError.new(display_uri)
+ when /401/
+ raise BadAuthenticationError, remote_uri if remote_uri.userinfo
+ raise AuthenticationRequiredError, remote_uri
+ when /403/
+ raise AuthenticationForbiddenError, remote_uri
+ else
+ raise HTTPError, "Could not fetch specs from #{display_uri} due to underlying error <#{e.message}>"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/force_platform.rb b/lib/bundler/force_platform.rb
new file mode 100644
index 0000000000..7af33218cb
--- /dev/null
+++ b/lib/bundler/force_platform.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Bundler
+ module ForcePlatform
+ # The `:force_ruby_platform` value used by dependencies for resolution, and
+ # by locked specifications for materialization is `false` by default, except
+ # for TruffleRuby. TruffleRuby generally needs to force the RUBY platform
+ # variant unless the name is explicitly allowlisted.
+
+ def default_force_ruby_platform
+ return false unless RUBY_ENGINE == "truffleruby"
+
+ !Gem::Platform::REUSE_AS_BINARY_ON_TRUFFLERUBY.include?(name)
+ end
+ end
+end
diff --git a/lib/bundler/friendly_errors.rb b/lib/bundler/friendly_errors.rb
new file mode 100644
index 0000000000..5e8eaee6bb
--- /dev/null
+++ b/lib/bundler/friendly_errors.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+require_relative "vendored_thor"
+
+module Bundler
+ module FriendlyErrors
+ module_function
+
+ def enable!
+ @disabled = false
+ end
+
+ def disabled?
+ @disabled
+ end
+
+ def disable!
+ @disabled = true
+ end
+
+ def log_error(error)
+ case error
+ when YamlSyntaxError
+ Bundler.ui.error error.message
+ Bundler.ui.trace error.orig_exception
+ when Dsl::DSLError, GemspecError
+ Bundler.ui.error error.message
+ when GemRequireError
+ Bundler.ui.error error.message
+ Bundler.ui.trace error.orig_exception
+ when BundlerError
+ if Bundler.ui.debug?
+ Bundler.ui.trace error
+ else
+ Bundler.ui.error error.message, wrap: true
+ end
+ when Thor::Error
+ Bundler.ui.error error.message
+ when Interrupt
+ Bundler.ui.error "\nQuitting..."
+ Bundler.ui.trace error
+ when Gem::InvalidSpecificationException
+ Bundler.ui.error error.message, wrap: true
+ when SystemExit
+ when *[defined?(Java::JavaLang::OutOfMemoryError) && Java::JavaLang::OutOfMemoryError].compact
+ Bundler.ui.error "\nYour JVM has run out of memory, and Bundler cannot continue. " \
+ "You can decrease the amount of memory Bundler needs by removing gems from your Gemfile, " \
+ "especially large gems. (Gems can be as large as hundreds of megabytes, and Bundler has to read those files!). " \
+ "Alternatively, you can increase the amount of memory the JVM is able to use by running Bundler with jruby -J-Xmx1024m -S bundle (JRuby defaults to 500MB)."
+ else request_issue_report_for(error)
+ end
+ end
+
+ def exit_status(error)
+ case error
+ when BundlerError then error.status_code
+ when Thor::Error then 15
+ when SystemExit then error.status
+ else 1
+ end
+ end
+
+ def request_issue_report_for(e)
+ Bundler.ui.error <<~EOS, nil, nil
+ --- ERROR REPORT TEMPLATE -------------------------------------------------------
+
+ ```
+ #{exception_message(e)}
+ ```
+
+ #{Bundler::Env.report}
+ --- TEMPLATE END ----------------------------------------------------------------
+
+ EOS
+
+ Bundler.ui.error "Unfortunately, an unexpected error occurred, and Bundler cannot continue."
+
+ Bundler.ui.error <<~EOS, nil, :yellow
+
+ First, try this link to see if there are any existing issue reports for this error:
+ #{issues_url(e)}
+
+ If there aren't any reports for this error yet, please fill in the new issue form located at #{new_issue_url}. Make sure to copy and paste the full output of this command under the "What happened instead?" section.
+ EOS
+ end
+
+ def exception_message(error)
+ message = serialized_exception_for(error)
+ cause = error.cause
+ return message unless cause
+
+ message + serialized_exception_for(cause)
+ end
+
+ def serialized_exception_for(e)
+ <<~EOS
+ #{e.class}: #{e.message}
+ #{e.backtrace&.join("\n ")&.chomp}
+ EOS
+ end
+
+ def issues_url(exception)
+ message = exception.message.lines.first.tr(":", " ").chomp
+ message = message.split("-").first if exception.is_a?(Errno)
+ require "cgi/escape"
+ require "cgi/util" unless defined?(CGI::EscapeExt)
+ "https://github.com/ruby/rubygems/search?q=" \
+ "#{CGI.escape(message)}&type=Issues"
+ end
+
+ def new_issue_url
+ "https://github.com/ruby/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md"
+ end
+ end
+
+ def self.with_friendly_errors
+ FriendlyErrors.enable!
+ yield
+ rescue SignalException
+ raise
+ rescue Exception => e # rubocop:disable Lint/RescueException
+ raise if FriendlyErrors.disabled?
+
+ FriendlyErrors.log_error(e)
+ exit FriendlyErrors.exit_status(e)
+ end
+end
diff --git a/lib/bundler/gem_helper.rb b/lib/bundler/gem_helper.rb
new file mode 100644
index 0000000000..5ce0ef6280
--- /dev/null
+++ b/lib/bundler/gem_helper.rb
@@ -0,0 +1,237 @@
+# frozen_string_literal: true
+
+require_relative "../bundler"
+require "shellwords"
+
+module Bundler
+ class GemHelper
+ include Rake::DSL if defined? Rake::DSL
+
+ class << self
+ # set when install'd.
+ attr_accessor :instance
+
+ def install_tasks(opts = {})
+ new(opts[:dir], opts[:name]).install
+ end
+
+ def tag_prefix=(prefix)
+ instance.tag_prefix = prefix
+ end
+
+ def gemspec(&block)
+ gemspec = instance.gemspec
+ block&.call(gemspec)
+ gemspec
+ end
+ end
+
+ attr_reader :spec_path, :base, :gemspec
+
+ attr_writer :tag_prefix
+
+ def initialize(base = nil, name = nil)
+ @base = File.expand_path(base || SharedHelpers.pwd)
+ gemspecs = name ? [File.join(@base, "#{name}.gemspec")] : Gem::Util.glob_files_in_dir("{,*}.gemspec", @base)
+ raise "Unable to determine name from existing gemspec. Use :name => 'gemname' in #install_tasks to manually set it." unless gemspecs.size == 1
+ @spec_path = gemspecs.first
+ @gemspec = Bundler.load_gemspec(@spec_path)
+ @tag_prefix = ""
+ end
+
+ def install
+ built_gem_path = nil
+
+ desc "Build #{name}-#{version}.gem into the pkg directory."
+ task "build" do
+ built_gem_path = build_gem
+ end
+
+ desc "Generate SHA512 checksum of #{name}-#{version}.gem into the checksums directory."
+ task "build:checksum" => "build" do
+ build_checksum(built_gem_path)
+ end
+
+ desc "Build and install #{name}-#{version}.gem into system gems."
+ task "install" => "build" do
+ install_gem(built_gem_path)
+ end
+
+ desc "Build and install #{name}-#{version}.gem into system gems without network access."
+ task "install:local" => "build" do
+ install_gem(built_gem_path, :local)
+ end
+
+ desc "Create tag #{version_tag} and build and push #{name}-#{version}.gem to #{gem_push_host}\n" \
+ "To prevent publishing in RubyGems use `gem_push=no rake release`"
+ task "release", [:remote] => ["build", "release:guard_clean",
+ "release:source_control_push", "release:rubygem_push"] do
+ end
+
+ task "release:guard_clean" do
+ guard_clean
+ end
+
+ task "release:source_control_push", [:remote] do |_, args|
+ tag_version { git_push(args[:remote]) } unless already_tagged?
+ end
+
+ task "release:rubygem_push" => "build" do
+ rubygem_push(built_gem_path) if gem_push?
+ end
+
+ GemHelper.instance = self
+ end
+
+ def build_gem
+ file_name = nil
+ sh([*gem_command, "build", "-V", spec_path]) do
+ file_name = File.basename(built_gem_path)
+ SharedHelpers.filesystem_access(File.join(base, "pkg")) {|p| FileUtils.mkdir_p(p) }
+ FileUtils.mv(built_gem_path, "pkg")
+ Bundler.ui.confirm "#{name} #{version} built to pkg/#{file_name}."
+ end
+ File.join(base, "pkg", file_name)
+ end
+
+ def install_gem(built_gem_path = nil, local = false)
+ built_gem_path ||= build_gem
+ cmd = [*gem_command, "install", built_gem_path.to_s]
+ cmd << "--local" if local
+ sh(cmd)
+ Bundler.ui.confirm "#{name} (#{version}) installed."
+ end
+
+ def build_checksum(built_gem_path = nil)
+ built_gem_path ||= build_gem
+ SharedHelpers.filesystem_access(File.join(base, "checksums")) {|p| FileUtils.mkdir_p(p) }
+ file_name = "#{File.basename(built_gem_path)}.sha512"
+ require "digest/sha2"
+ checksum = ::Digest::SHA512.file(built_gem_path).hexdigest
+ target = File.join(base, "checksums", file_name)
+ File.write(target, checksum + "\n")
+ Bundler.ui.confirm "#{name} #{version} checksum written to checksums/#{file_name}."
+ end
+
+ protected
+
+ def rubygem_push(path)
+ cmd = [*gem_command, "push", path]
+ cmd << "--key" << gem_key if gem_key
+ cmd << "--host" << allowed_push_host if allowed_push_host
+ sh_with_input(cmd)
+ Bundler.ui.confirm "Pushed #{name} #{version} to #{gem_push_host}"
+ end
+
+ def built_gem_path
+ Gem::Util.glob_files_in_dir("#{name}-*.gem", base).sort_by {|f| File.mtime(f) }.last
+ end
+
+ def git_push(remote = nil)
+ remote ||= default_remote
+ sh("git push #{remote} refs/heads/#{current_branch}".shellsplit)
+ sh("git push #{remote} refs/tags/#{version_tag}".shellsplit)
+ Bundler.ui.confirm "Pushed git commits and release tag."
+ end
+
+ def default_remote
+ remote_for_branch, status = sh_with_status(%W[git config --get branch.#{current_branch}.remote])
+ return "origin" unless status.success?
+
+ remote_for_branch.strip
+ end
+
+ def current_branch
+ # We can replace this with `git branch --show-current` once we drop support for git < 2.22.0
+ sh(%w[git rev-parse --abbrev-ref HEAD]).gsub(%r{\Aheads/}, "").strip
+ end
+
+ def allowed_push_host
+ @gemspec.metadata["allowed_push_host"] if @gemspec.respond_to?(:metadata)
+ end
+
+ def gem_push_host
+ env_rubygems_host = ENV["RUBYGEMS_HOST"]
+ env_rubygems_host = nil if env_rubygems_host&.empty?
+
+ allowed_push_host || env_rubygems_host || "rubygems.org"
+ end
+
+ def already_tagged?
+ return false unless sh(%w[git tag]).split(/\n/).include?(version_tag)
+ Bundler.ui.confirm "Tag #{version_tag} has already been created."
+ true
+ end
+
+ def guard_clean
+ clean? && committed? || raise("There are files that need to be committed first.")
+ end
+
+ def clean?
+ sh_with_status(%w[git diff --exit-code])[1].success?
+ end
+
+ def committed?
+ sh_with_status(%w[git diff-index --quiet --cached HEAD])[1].success?
+ end
+
+ def tag_version
+ sh %W[git tag -m Version\ #{version} #{version_tag}]
+ Bundler.ui.confirm "Tagged #{version_tag}."
+ yield if block_given?
+ rescue RuntimeError
+ Bundler.ui.error "Untagging #{version_tag} due to error."
+ sh_with_status %W[git tag -d #{version_tag}]
+ raise
+ end
+
+ def version
+ gemspec.version
+ end
+
+ def version_tag
+ "#{@tag_prefix}v#{version}"
+ end
+
+ def name
+ gemspec.name
+ end
+
+ def sh_with_input(cmd)
+ Bundler.ui.debug(cmd)
+ SharedHelpers.chdir(base) do
+ abort unless Kernel.system(*cmd)
+ end
+ end
+
+ def sh(cmd, &block)
+ out, status = sh_with_status(cmd, &block)
+ unless status.success?
+ raise("Running `#{cmd.shelljoin}` failed with the following output:\n\n#{out}\n")
+ end
+ out
+ end
+
+ def sh_with_status(cmd, &block)
+ Bundler.ui.debug(cmd)
+ SharedHelpers.chdir(base) do
+ outbuf = IO.popen(cmd, err: [:child, :out], &:read)
+ status = $?
+ block&.call(outbuf) if status.success?
+ [outbuf, status]
+ end
+ end
+
+ def gem_key
+ Bundler.settings["gem.push_key"].to_s.downcase if Bundler.settings["gem.push_key"]
+ end
+
+ def gem_push?
+ !%w[n no nil false off 0].include?(ENV["gem_push"].to_s.downcase)
+ end
+
+ def gem_command
+ ENV["GEM_COMMAND"]&.shellsplit || ["gem"]
+ end
+ end
+end
diff --git a/lib/bundler/gem_tasks.rb b/lib/bundler/gem_tasks.rb
new file mode 100644
index 0000000000..bc725d3602
--- /dev/null
+++ b/lib/bundler/gem_tasks.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require "rake/clean"
+CLOBBER.include "pkg"
+
+require_relative "gem_helper"
+Bundler::GemHelper.install_tasks
diff --git a/lib/bundler/gem_version_promoter.rb b/lib/bundler/gem_version_promoter.rb
new file mode 100644
index 0000000000..d64dbacfdb
--- /dev/null
+++ b/lib/bundler/gem_version_promoter.rb
@@ -0,0 +1,147 @@
+# frozen_string_literal: true
+
+module Bundler
+ # This class contains all of the logic for determining the next version of a
+ # Gem to update to based on the requested level (patch, minor, major).
+ # Primarily designed to work with Resolver which will provide it the list of
+ # available dependency versions as found in its index, before returning it to
+ # to the resolution engine to select the best version.
+ class GemVersionPromoter
+ attr_reader :level
+ attr_accessor :pre
+
+ # By default, strict is false, meaning every available version of a gem
+ # is returned from sort_versions. The order gives preference to the
+ # requested level (:patch, :minor, :major) but in complicated requirement
+ # cases some gems will by necessity be promoted past the requested level,
+ # or even reverted to older versions.
+ #
+ # If strict is set to true, the results from sort_versions will be
+ # truncated, eliminating any version outside the current level scope.
+ # This can lead to unexpected outcomes or even VersionConflict exceptions
+ # that report a version of a gem not existing for versions that indeed do
+ # existing in the referenced source.
+ attr_accessor :strict
+
+ # Creates a GemVersionPromoter instance.
+ #
+ # @return [GemVersionPromoter]
+ def initialize
+ @level = :major
+ @strict = false
+ @pre = false
+ end
+
+ # @param value [Symbol] One of three Symbols: :major, :minor or :patch.
+ def level=(value)
+ v = case value
+ when String, Symbol
+ value.to_sym
+ end
+
+ raise ArgumentError, "Unexpected level #{v}. Must be :major, :minor or :patch" unless [:major, :minor, :patch].include?(v)
+ @level = v
+ end
+
+ # Given a Resolver::Package and an Array of Specifications of available
+ # versions for a gem, this method will return the Array of Specifications
+ # sorted in an order to give preference to the current level (:major, :minor
+ # or :patch) when resolution is deciding what versions best resolve all
+ # dependencies in the bundle.
+ # @param package [Resolver::Package] The package being resolved.
+ # @param specs [Specification] An array of Specifications for the package.
+ # @return [Specification] A new instance of the Specification Array sorted.
+ def sort_versions(package, specs)
+ locked_version = package.locked_version
+
+ result = specs.sort do |a, b|
+ unless package.prerelease_specified? || pre?
+ a_pre = a.prerelease?
+ b_pre = b.prerelease?
+
+ next 1 if a_pre && !b_pre
+ next -1 if b_pre && !a_pre
+ end
+
+ if major? || locked_version.nil?
+ b <=> a
+ elsif either_version_older_than_locked?(a, b, locked_version)
+ b <=> a
+ elsif segments_do_not_match?(a, b, :major)
+ a <=> b
+ elsif !minor? && segments_do_not_match?(a, b, :minor)
+ a <=> b
+ else
+ b <=> a
+ end
+ end
+ post_sort(result, package.unlock?, locked_version)
+ end
+
+ # @return [bool] Convenience method for testing value of level variable.
+ def major?
+ level == :major
+ end
+
+ # @return [bool] Convenience method for testing value of level variable.
+ def minor?
+ level == :minor
+ end
+
+ # @return [bool] Convenience method for testing value of pre variable.
+ def pre?
+ pre == true
+ end
+
+ # Given a Resolver::Package and an Array of Specifications of available
+ # versions for a gem, this method will truncate the Array if strict
+ # is true. That means filtering out downgrades from the version currently
+ # locked, and filtering out upgrades that go past the selected level (major,
+ # minor, or patch).
+ # @param package [Resolver::Package] The package being resolved.
+ # @param specs [Specification] An array of Specifications for the package.
+ # @return [Specification] A new instance of the Specification Array
+ # truncated.
+ def filter_versions(package, specs)
+ return specs unless strict
+
+ locked_version = package.locked_version
+ return specs if locked_version.nil? || major?
+
+ specs.select do |spec|
+ gsv = spec.version
+
+ must_match = minor? ? [0] : [0, 1]
+
+ all_match = must_match.all? {|idx| gsv.segments[idx] == locked_version.segments[idx] }
+ all_match && gsv >= locked_version
+ end
+ end
+
+ private
+
+ def either_version_older_than_locked?(a, b, locked_version)
+ a.version < locked_version || b.version < locked_version
+ end
+
+ def segments_do_not_match?(a, b, level)
+ index = [:major, :minor].index(level)
+ a.segments[index] != b.segments[index]
+ end
+
+ # Specific version moves can't always reliably be done during sorting
+ # as not all elements are compared against each other.
+ def post_sort(result, unlock, locked_version)
+ if unlock || locked_version.nil?
+ result
+ else
+ move_version_to_beginning(result, locked_version)
+ end
+ end
+
+ def move_version_to_beginning(result, version)
+ move, keep = result.partition {|s| s.version.to_s == version.to_s }
+ move.concat(keep)
+ end
+ end
+end
diff --git a/lib/bundler/index.rb b/lib/bundler/index.rb
new file mode 100644
index 0000000000..9aef2dfa12
--- /dev/null
+++ b/lib/bundler/index.rb
@@ -0,0 +1,203 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Index
+ include Enumerable
+
+ def self.build
+ i = new
+ yield i
+ i
+ end
+
+ attr_reader :specs, :duplicates, :sources
+ protected :specs, :duplicates
+
+ RUBY = "ruby"
+ NULL = "\0"
+
+ def initialize
+ @sources = []
+ @cache = {}
+ @specs = {}
+ @duplicates = {}
+ end
+
+ def initialize_copy(o)
+ @sources = o.sources.dup
+ @cache = {}
+ @specs = {}
+ @duplicates = {}
+
+ o.specs.each do |name, hash|
+ @specs[name] = hash.dup
+ end
+ o.duplicates.each do |name, array|
+ @duplicates[name] = array.dup
+ end
+ end
+
+ def inspect
+ "#<#{self.class}:0x#{object_id} sources=#{sources.map(&:inspect)} specs.size=#{specs.size}>"
+ end
+
+ def empty?
+ each { return false }
+ true
+ end
+
+ # Search this index's specs, and any source indexes that this index knows
+ # about, returning all of the results.
+ def search(query)
+ results = local_search(query)
+ return results unless @sources.any?
+
+ @sources.each do |source|
+ results = safe_concat(results, source.search(query))
+ end
+ results.uniq!(&:full_name) unless results.empty? # avoid modifying frozen EMPTY_SEARCH
+ results
+ end
+
+ alias_method :[], :search
+
+ def local_search(query)
+ case query
+ when Gem::Specification, RemoteSpecification, LazySpecification, EndpointSpecification then search_by_spec(query)
+ when String then specs_by_name(query)
+ when Array then specs_by_name_and_version(*query)
+ else
+ raise "You can't search for a #{query.inspect}."
+ end
+ end
+
+ def add(spec)
+ (@specs[spec.name] ||= {}).store(spec.full_name, spec)
+ end
+ alias_method :<<, :add
+
+ def each(&blk)
+ return enum_for(:each) unless blk
+ specs.values.each do |spec_sets|
+ spec_sets.values.each(&blk)
+ end
+ sources.each {|s| s.each(&blk) }
+ self
+ end
+
+ def spec_names
+ names = specs.keys + sources.map(&:spec_names)
+ names.uniq!
+ names
+ end
+
+ def unmet_dependency_names
+ dependency_names.select do |name|
+ search(name).empty?
+ end
+ end
+
+ def dependency_names
+ names = []
+ each do |spec|
+ spec.dependencies.each do |dep|
+ next if dep.type == :development
+ names << dep.name
+ end
+ end
+ names.uniq
+ end
+
+ # Combines indexes proritizing existing specs, like `Hash#reverse_merge!`
+ # Duplicate specs found in `other` are stored in `@duplicates`.
+ def use(other)
+ return unless other
+ other.each do |spec|
+ exist?(spec) ? add_duplicate(spec) : add(spec)
+ end
+ self
+ end
+
+ # Combines indexes proritizing specs from `other`, like `Hash#merge!`
+ # Duplicate specs found in `self` are saved in `@duplicates`.
+ def merge!(other)
+ return unless other
+ other.each do |spec|
+ if existing = find_by_spec(spec)
+ unless dependencies_eql?(existing, spec)
+ Bundler.ui.warn "Local specification for #{spec.full_name} has different dependencies than the remote gem, ignoring it"
+ next
+ end
+
+ add_duplicate(existing)
+ end
+ add spec
+ end
+ self
+ end
+
+ def size
+ @sources.inject(@specs.size) do |size, source|
+ size += source.size
+ end
+ end
+
+ # Whether all the specs in self are in other
+ def subset?(other)
+ all? do |spec|
+ other_spec = other[spec].first
+ other_spec && dependencies_eql?(spec, other_spec) && spec.source == other_spec.source
+ end
+ end
+
+ def dependencies_eql?(spec, other_spec)
+ deps = spec.runtime_dependencies
+ other_deps = other_spec.runtime_dependencies
+ deps.sort == other_deps.sort
+ end
+
+ def add_source(index)
+ raise ArgumentError, "Source must be an index, not #{index.class}" unless index.is_a?(Index)
+ @sources << index
+ @sources.uniq! # need to use uniq! here instead of checking for the item before adding
+ end
+
+ private
+
+ def safe_concat(a, b)
+ return a if b.empty?
+ return b if a.empty?
+ a.concat(b)
+ end
+
+ def add_duplicate(spec)
+ (@duplicates[spec.name] ||= []) << spec
+ end
+
+ def specs_by_name_and_version(name, version)
+ results = @specs[name]&.values
+ return EMPTY_SEARCH unless results
+ results.select! {|spec| spec.version == version }
+ results
+ end
+
+ def specs_by_name(name)
+ @specs[name]&.values || EMPTY_SEARCH
+ end
+
+ EMPTY_SEARCH = [].freeze
+
+ def search_by_spec(spec)
+ spec = find_by_spec(spec)
+ spec ? [spec] : EMPTY_SEARCH
+ end
+
+ def find_by_spec(spec)
+ @specs[spec.name]&.fetch(spec.full_name, nil)
+ end
+
+ def exist?(spec)
+ @specs[spec.name]&.key?(spec.full_name)
+ end
+ end
+end
diff --git a/lib/bundler/injector.rb b/lib/bundler/injector.rb
new file mode 100644
index 0000000000..6aa9179024
--- /dev/null
+++ b/lib/bundler/injector.rb
@@ -0,0 +1,284 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Injector
+ INJECTED_GEMS = "injected gems"
+
+ def self.inject(new_deps, options = {})
+ injector = new(new_deps, options)
+ injector.inject(Bundler.default_gemfile, Bundler.default_lockfile)
+ end
+
+ def self.remove(gems, options = {})
+ injector = new(gems, options)
+ injector.remove(Bundler.default_gemfile, Bundler.default_lockfile)
+ end
+
+ def initialize(deps, options = {})
+ @deps = deps
+ @options = options
+ end
+
+ # @param [Pathname] gemfile_path The Gemfile in which to inject the new dependency.
+ # @param [Pathname] lockfile_path The lockfile in which to inject the new dependency.
+ # @return [Array]
+ def inject(gemfile_path, lockfile_path)
+ Bundler.definition.ensure_equivalent_gemfile_and_lockfile(true)
+
+ # temporarily unfreeze
+ Bundler.settings.temporary(deployment: false, frozen: false) do
+ # evaluate the Gemfile we have now
+ builder = Dsl.new
+ builder.eval_gemfile(gemfile_path)
+
+ # don't inject any gems that are already in the Gemfile
+ @deps -= builder.dependencies
+
+ # add new deps to the end of the in-memory Gemfile
+ # Set conservative versioning to false because
+ # we want to let the resolver resolve the version first
+ builder.eval_gemfile(INJECTED_GEMS, build_gem_lines(false)) if @deps.any?
+
+ # resolve to see if the new deps broke anything
+ @definition = builder.to_definition(lockfile_path, {})
+ @definition.remotely!
+
+ # since nothing broke, we can add those gems to the gemfile
+ append_to(gemfile_path, build_gem_lines(@options[:conservative_versioning])) if @deps.any?
+
+ # since we resolved successfully, write out the lockfile
+ @definition.lock
+
+ # invalidate the cached Bundler.definition
+ Bundler.reset_paths!
+
+ # return an array of the deps that we added
+ @deps
+ end
+ end
+
+ # @param [Pathname] gemfile_path The Gemfile from which to remove dependencies.
+ # @param [Pathname] lockfile_path The lockfile from which to remove dependencies.
+ # @return [Array]
+ def remove(gemfile_path, lockfile_path)
+ # remove gems from each gemfiles we have
+ Bundler.definition.gemfiles.each do |path|
+ deps = remove_deps(path)
+
+ show_warning("No gems were removed from the gemfile.") if deps.empty?
+
+ deps.each {|dep| Bundler.ui.confirm "#{SharedHelpers.pretty_dependency(dep)} was removed." }
+ end
+
+ # Invalidate the cached Bundler.definition.
+ # This prevents e.g. `bundle remove ...` from using outdated information.
+ Bundler.reset_paths!
+ end
+
+ private
+
+ def conservative_version(spec)
+ version = spec.version
+ return ">= 0" if version.nil?
+ seg_end_index = version >= Gem::Version.new("1.0") ? 1 : 2
+
+ prerelease_suffix = version.to_s.delete_prefix(version.release.to_s) if version.prerelease?
+ "#{version_prefix}#{version.segments[0..seg_end_index].join(".")}#{prerelease_suffix}"
+ end
+
+ def version_prefix
+ if @options[:strict]
+ "= "
+ elsif @options[:pessimistic]
+ "~> "
+ else
+ ">= "
+ end
+ end
+
+ def build_gem_lines(conservative_versioning)
+ @deps.map do |d|
+ name = d.name.dump
+
+ requirement = if conservative_versioning
+ ", \"#{conservative_version(@definition.specs[d.name][0])}\""
+ else
+ ", #{d.requirement.as_list.map(&:dump).join(", ")}"
+ end
+
+ if d.groups != Array(:default)
+ group = d.groups.size == 1 ? ", group: #{d.groups.first.inspect}" : ", groups: #{d.groups.inspect}"
+ end
+
+ source = ", source: \"#{d.source}\"" unless d.source.nil?
+ path = ", path: \"#{d.path}\"" unless d.path.nil?
+ git = ", git: \"#{d.git}\"" unless d.git.nil?
+ github = ", github: \"#{d.github}\"" unless d.github.nil?
+ branch = ", branch: \"#{d.branch}\"" unless d.branch.nil?
+ ref = ", ref: \"#{d.ref}\"" unless d.ref.nil?
+ glob = ", glob: \"#{d.glob}\"" unless d.glob.nil?
+ require_path = ", require: #{convert_autorequire(d.autorequire)}" unless d.autorequire.nil?
+
+ %(gem #{name}#{requirement}#{group}#{source}#{path}#{git}#{github}#{branch}#{ref}#{glob}#{require_path})
+ end.join("\n")
+ end
+
+ def append_to(gemfile_path, new_gem_lines)
+ gemfile_path.open("a") do |f|
+ f.puts
+ f.puts new_gem_lines
+ end
+ end
+
+ # evaluates a gemfile to remove the specified gem
+ # from it.
+ def remove_deps(gemfile_path)
+ initial_gemfile = File.readlines(gemfile_path)
+
+ Bundler.ui.info "Removing gems from #{gemfile_path}"
+
+ # evaluate the Gemfile we have
+ builder = Dsl.new
+ builder.eval_gemfile(gemfile_path)
+
+ removed_deps = remove_gems_from_dependencies(builder, @deps, gemfile_path)
+
+ # abort the operation if no gems were removed
+ # no need to operate on gemfile further
+ return [] if removed_deps.empty?
+
+ cleaned_gemfile = remove_gems_from_gemfile(@deps, gemfile_path)
+
+ SharedHelpers.write_to_gemfile(gemfile_path, cleaned_gemfile)
+
+ # check for errors
+ # including extra gems being removed
+ # or some gems not being removed
+ # and return the actual removed deps
+ cross_check_for_errors(gemfile_path, builder.dependencies, removed_deps, initial_gemfile)
+ end
+
+ # @param [Dsl] builder Dsl object of current Gemfile.
+ # @param [Array] gems Array of names of gems to be removed.
+ # @param [Pathname] gemfile_path Path of the Gemfile.
+ # @return [Array] Array of removed dependencies.
+ def remove_gems_from_dependencies(builder, gems, gemfile_path)
+ removed_deps = []
+
+ gems.each do |gem_name|
+ deleted_dep = builder.dependencies.find {|d| d.name == gem_name }
+
+ if deleted_dep.nil?
+ raise GemfileError, "`#{gem_name}` is not specified in #{gemfile_path} so it could not be removed."
+ end
+
+ builder.dependencies.delete(deleted_dep)
+
+ removed_deps << deleted_dep
+ end
+
+ removed_deps
+ end
+
+ # @param [Array] gems Array of names of gems to be removed.
+ # @param [Pathname] gemfile_path The Gemfile from which to remove dependencies.
+ def remove_gems_from_gemfile(gems, gemfile_path)
+ patterns = /gem\s+(['"])#{Regexp.union(gems)}\1|gem\s*\((['"])#{Regexp.union(gems)}\2.*\)/
+ new_gemfile = []
+ multiline_removal = false
+ File.readlines(gemfile_path).each do |line|
+ match_data = line.match(patterns)
+ if match_data && is_not_within_comment?(line, match_data)
+ multiline_removal = line.rstrip.end_with?(",")
+ # skip lines which match the regex
+ next
+ end
+
+ # skip followup lines until line does not end with ','
+ new_gemfile << line unless multiline_removal
+ multiline_removal = line.rstrip.end_with?(",") if multiline_removal
+ end
+
+ # remove line \n and append them with other strings
+ new_gemfile.each_with_index do |_line, index|
+ if new_gemfile[index + 1] == "\n"
+ new_gemfile[index] += new_gemfile[index + 1]
+ new_gemfile.delete_at(index + 1)
+ end
+ end
+
+ %w[group source env install_if].each {|block| remove_nested_blocks(new_gemfile, block) }
+
+ new_gemfile.join.chomp
+ end
+
+ # @param [String] line Individual line of gemfile content.
+ # @param [MatchData] match_data Data about Regex match.
+ def is_not_within_comment?(line, match_data)
+ match_start_index = match_data.offset(0).first
+ !line[0..match_start_index].include?("#")
+ end
+
+ # @param [Array] gemfile Array of gemfile contents.
+ # @param [String] block_name Name of block name to look for.
+ def remove_nested_blocks(gemfile, block_name)
+ nested_blocks = 0
+
+ # count number of nested blocks
+ gemfile.each_with_index {|line, index| nested_blocks += 1 if !gemfile[index + 1].nil? && gemfile[index + 1].include?(block_name) && line.include?(block_name) }
+
+ while nested_blocks >= 0
+ nested_blocks -= 1
+
+ gemfile.each_with_index do |line, index|
+ next unless !line.nil? && line.strip.start_with?(block_name)
+ if /^\s*end\s*$/.match?(gemfile[index + 1])
+ gemfile[index] = nil
+ gemfile[index + 1] = nil
+ end
+ end
+
+ gemfile.compact!
+ end
+ end
+
+ # @param [Pathname] gemfile_path The Gemfile from which to remove dependencies.
+ # @param [Array] original_deps Array of original dependencies.
+ # @param [Array] removed_deps Array of removed dependencies.
+ # @param [Array] initial_gemfile Contents of original Gemfile before any operation.
+ def cross_check_for_errors(gemfile_path, original_deps, removed_deps, initial_gemfile)
+ # evaluate the new gemfile to look for any failure cases
+ builder = Dsl.new
+ builder.eval_gemfile(gemfile_path)
+
+ # record gems which were removed but not requested
+ extra_removed_gems = original_deps - builder.dependencies
+
+ # if some extra gems were removed then raise error
+ # and revert Gemfile to original
+ unless extra_removed_gems.empty?
+ SharedHelpers.write_to_gemfile(gemfile_path, initial_gemfile.join)
+
+ raise InvalidOption, "Gems could not be removed. #{extra_removed_gems.join(", ")} would also have been removed. Bundler cannot continue."
+ end
+
+ # record gems which could not be removed due to some reasons
+ errored_deps = builder.dependencies.select {|d| d.gemfile == gemfile_path } & removed_deps.select {|d| d.gemfile == gemfile_path }
+
+ show_warning "#{errored_deps.map(&:name).join(", ")} could not be removed." unless errored_deps.empty?
+
+ # return actual removed dependencies
+ removed_deps - errored_deps
+ end
+
+ def show_warning(message)
+ Bundler.ui.info Bundler.ui.add_color(message, :yellow)
+ end
+
+ def convert_autorequire(autorequire)
+ autorequire = autorequire.first
+ return autorequire if autorequire == "false"
+ autorequire.inspect
+ end
+ end
+end
diff --git a/lib/bundler/inline.rb b/lib/bundler/inline.rb
new file mode 100644
index 0000000000..a1b8e0475e
--- /dev/null
+++ b/lib/bundler/inline.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+# Allows for declaring a Gemfile inline in a ruby script, installing any gems
+# that aren't already installed on the user's system.
+#
+# @note Every gem that is specified in this 'Gemfile' will be `require`d, as if
+# the user had manually called `Bundler.require`. To avoid a requested gem
+# being automatically required, add the `:require => false` option to the
+# `gem` dependency declaration.
+#
+# @param force_latest_compatible [Boolean] Force installing the *latest*
+# compatible versions of the gems,
+# even if compatible versions are
+# already installed locally.
+# This also logs output if the
+# `:quiet` option is not set.
+# Defaults to `false`.
+#
+# @param gemfile [Proc] a block that is evaluated as a `Gemfile`.
+#
+# @example Using an inline Gemfile
+#
+# #!/usr/bin/env ruby
+#
+# require 'bundler/inline'
+#
+# gemfile do
+# source 'https://rubygems.org'
+# gem 'json', require: false
+# gem 'nap', require: 'rest'
+# gem 'cocoapods', '~> 0.34.1'
+# end
+#
+# puts Pod::VERSION # => "0.34.4"
+#
+def gemfile(force_latest_compatible = false, options = {}, &gemfile)
+ require_relative "../bundler"
+ Bundler.reset!
+
+ opts = options.dup
+ ui = opts.delete(:ui) { Bundler::UI::Shell.new }
+ ui.level = "silent" if opts.delete(:quiet) || !force_latest_compatible
+ Bundler.ui = ui
+ raise ArgumentError, "Unknown options: #{opts.keys.join(", ")}" unless opts.empty?
+
+ old_gemfile = ENV["BUNDLE_GEMFILE"]
+ old_lockfile = ENV["BUNDLE_LOCKFILE"]
+
+ Bundler.unbundle_env!
+
+ begin
+ Bundler.instance_variable_set(:@bundle_path, Pathname.new(Gem.dir))
+ Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", "Gemfile"
+ Bundler::SharedHelpers.set_env "BUNDLE_LOCKFILE", "Gemfile.lock"
+
+ Bundler::Plugin.gemfile_install(&gemfile) if Bundler.settings[:plugins]
+ builder = Bundler::Dsl.new
+ builder.instance_eval(&gemfile)
+
+ Bundler.settings.temporary(deployment: false, frozen: false) do
+ definition = builder.to_definition(nil, true)
+ definition.validate_runtime!
+
+ if force_latest_compatible || definition.missing_specs?
+ do_install = -> do
+ Bundler.settings.temporary(inline: true, no_install: false) do
+ installer = Bundler::Installer.install(Bundler.root, definition, system: true)
+ installer.post_install_messages.each do |name, message|
+ Bundler.ui.info "Post-install message from #{name}:\n#{message}"
+ end
+ end
+ end
+
+ # When possible we do the install in a subprocess because to install
+ # gems we need to require some default gems like `securerandom` etc
+ # which may later conflict with the Gemfile requirements.
+ installed_in_fork = false
+ if Process.respond_to?(:fork)
+ Gem.load_yaml # Avoid NameError on Ruby 3.2's safe_yaml.rb after Gem::Specification.reset
+ _, status = Process.waitpid2(Process.fork do
+ $VERBOSE = nil
+ do_install.call end)
+ exit(status.exitstatus || status.to_i) unless status.success?
+
+ installed_in_fork = true
+
+ Bundler.reset!
+ Gem::Specification.reset
+ Bundler.instance_variable_set(:@bundle_path, Pathname.new(Gem.dir))
+
+ builder = Bundler::Dsl.new
+ builder.instance_eval(&gemfile)
+ builder.check_primary_source_safety
+
+ definition = builder.to_definition(nil, true)
+ definition.validate_runtime!
+ else
+ do_install.call
+ end
+ end
+
+ configure_forked_definition = ->(d) do
+ d.sources.rubygems_sources.each(&:remote!)
+ d.sources.git_sources.each do |source|
+ source.cached!
+ source.instance_variable_set(:@copied, true)
+ end
+ def d.lock(*); end
+ end
+ configure_forked_definition.call(definition) if installed_in_fork
+
+ begin
+ runtime = Bundler::Runtime.new(nil, definition).setup
+ rescue Gem::LoadError => e
+ name = e.name
+ version = e.requirement.requirements.first[1]
+ activated_version = Gem.loaded_specs[name].version
+
+ Bundler.ui.info \
+ "The #{name} gem was resolved to #{version}, but #{activated_version} was activated by Bundler while installing it, causing a conflict. " \
+ "Bundler will now retry resolving with #{activated_version} instead."
+
+ builder.dependencies.delete_if {|d| d.name == name }
+ builder.instance_eval { gem name, activated_version }
+ definition = builder.to_definition(nil, true)
+ configure_forked_definition.call(definition) if installed_in_fork
+
+ retry
+ end
+
+ runtime.require
+ end
+ ensure
+ if old_gemfile
+ ENV["BUNDLE_GEMFILE"] = old_gemfile
+ else
+ ENV["BUNDLE_GEMFILE"] = ""
+ end
+
+ if old_lockfile
+ ENV["BUNDLE_LOCKFILE"] = old_lockfile
+ else
+ ENV["BUNDLE_LOCKFILE"] = ""
+ end
+ end
+end
diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb
new file mode 100644
index 0000000000..87d9a75627
--- /dev/null
+++ b/lib/bundler/installer.rb
@@ -0,0 +1,236 @@
+# frozen_string_literal: true
+
+require_relative "worker"
+require_relative "installer/parallel_installer"
+require_relative "installer/standalone"
+require_relative "installer/gem_installer"
+
+module Bundler
+ class Installer
+ attr_reader :post_install_messages, :definition
+
+ # Begins the installation process for Bundler.
+ # For more information see the #run method on this class.
+ def self.install(root, definition, options = {})
+ installer = new(root, definition)
+ Plugin.hook(Plugin::Events::GEM_BEFORE_INSTALL_ALL, definition.dependencies)
+ installer.run(options)
+ Plugin.hook(Plugin::Events::GEM_AFTER_INSTALL_ALL, definition.dependencies)
+ installer
+ end
+
+ def initialize(root, definition)
+ @root = root
+ @definition = definition
+ @post_install_messages = {}
+ end
+
+ # Runs the install procedures for a specific Gemfile.
+ #
+ # Firstly, this method will check to see if `Bundler.bundle_path` exists
+ # and if not then Bundler will create the directory. This is usually the same
+ # location as RubyGems which typically is the `~/.gem` directory
+ # unless other specified.
+ #
+ # Secondly, it checks if Bundler has been configured to be "frozen".
+ # Frozen ensures that the Gemfile and the Gemfile.lock file are matching.
+ # This stops a situation where a developer may update the Gemfile but may not run
+ # `bundle install`, which leads to the Gemfile.lock file not being correctly updated.
+ # If this file is not correctly updated then any other developer running
+ # `bundle install` will potentially not install the correct gems.
+ #
+ # Thirdly, Bundler checks if there are any dependencies specified in the Gemfile.
+ # If there are no dependencies specified then Bundler returns a warning message stating
+ # so and this method returns.
+ #
+ # Fourthly, Bundler checks if the Gemfile.lock exists, and if so
+ # then proceeds to set up a definition based on the Gemfile and the Gemfile.lock.
+ # During this step Bundler will also download information about any new gems
+ # that are not in the Gemfile.lock and resolve any dependencies if needed.
+ #
+ # Fifthly, Bundler resolves the dependencies either through a cache of gems or by remote.
+ # This then leads into the gems being installed, along with stubs for their executables,
+ # but only if the --binstubs option has been passed or Bundler.options[:bin] has been set
+ # earlier.
+ #
+ # Sixthly, a new Gemfile.lock is created from the installed gems to ensure that the next time
+ # that a user runs `bundle install` they will receive any updates from this process.
+ #
+ # Finally, if the user has specified the standalone flag, Bundler will generate the needed
+ # require paths and save them in a `setup.rb` file. See `bundle standalone --help` for more
+ # information.
+ def run(options)
+ Bundler.create_bundle_path
+
+ ProcessLock.lock do
+ # Invalidate any stale gem specification cache from before we acquired the lock.
+ # Another process may have installed gems while we were waiting.
+ Gem::Specification.reset
+ @definition.sources.clear_cache
+
+ @definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment])
+
+ if @definition.dependencies.empty?
+ Bundler.ui.warn "The Gemfile specifies no dependencies"
+ lock
+ return
+ end
+
+ if @definition.setup_domain!(options)
+ ensure_specs_are_compatible!
+ load_plugins
+ end
+ install(options)
+
+ Gem::Specification.reset # invalidate gem specification cache so that installed gems are immediately available
+
+ lock
+ Standalone.new(options[:standalone], @definition).generate if options[:standalone]
+ end
+ end
+
+ def generate_bundler_executable_stubs(spec, options = {})
+ if spec.name == "bundler"
+ Bundler.ui.warn "Bundler itself does not use binstubs because its version is selected by RubyGems"
+ return
+ end
+
+ if options[:binstubs_cmd] && spec.executables.empty?
+ options = {}
+ spec.runtime_dependencies.each do |dep|
+ bins = @definition.specs[dep].first.executables
+ options[dep.name] = bins unless bins.empty?
+ end
+ if options.any?
+ Bundler.ui.warn "#{spec.name} has no executables, but you may want " \
+ "one from a gem it depends on."
+ options.each {|name, bins| Bundler.ui.warn " #{name} has: #{bins.join(", ")}" }
+ else
+ Bundler.ui.warn "There are no executables for the gem #{spec.name}."
+ end
+ return
+ end
+
+ # double-assignment to avoid warnings about variables that will be used by ERB
+ bin_path = Bundler.bin_path
+ bin_path = bin_path
+ relative_gemfile_path = Bundler.default_gemfile.relative_path_from(bin_path)
+ relative_gemfile_path = relative_gemfile_path
+ ruby_command = Thor::Util.ruby_command
+ ruby_command = ruby_command
+ template_path = File.expand_path("templates/Executable", __dir__)
+ template = File.read(template_path)
+
+ exists = []
+ spec.executables.each do |executable|
+ binstub_path = "#{bin_path}/#{executable}"
+ if File.exist?(binstub_path) && !options[:force]
+ exists << executable
+ next
+ end
+
+ mode = Gem.win_platform? ? "wb:UTF-8" : "w"
+ require "erb"
+ content = ERB.new(template, trim_mode: "-").result(binding)
+
+ File.write(binstub_path, content, mode: mode, perm: 0o777 & ~File.umask)
+ if Gem.win_platform? || options[:all_platforms]
+ prefix = "@ruby -x \"%~f0\" %*\n@exit /b %ERRORLEVEL%\n\n"
+ File.write("#{binstub_path}.cmd", prefix + content, mode: mode)
+ end
+ end
+
+ if options[:binstubs_cmd] && exists.any?
+ case exists.size
+ when 1
+ Bundler.ui.warn "Skipped #{exists[0]} since it already exists."
+ when 2
+ Bundler.ui.warn "Skipped #{exists.join(" and ")} since they already exist."
+ else
+ items = exists[0...-1].empty? ? nil : exists[0...-1].join(", ")
+ skipped = [items, exists[-1]].compact.join(" and ")
+ Bundler.ui.warn "Skipped #{skipped} since they already exist."
+ end
+ Bundler.ui.warn "If you want to overwrite skipped stubs, use --force."
+ end
+ end
+
+ def generate_standalone_bundler_executable_stubs(spec, options = {})
+ # double-assignment to avoid warnings about variables that will be used by ERB
+ bin_path = Bundler.bin_path
+ unless path = Bundler.settings[:path]
+ raise "Can't standalone without an explicit path set"
+ end
+ standalone_path = Bundler.root.join(path).relative_path_from(bin_path)
+ standalone_path = standalone_path
+ template = File.read(File.expand_path("templates/Executable.standalone", __dir__))
+ ruby_command = Thor::Util.ruby_command
+ ruby_command = ruby_command
+
+ spec.executables.each do |executable|
+ next if executable == "bundle"
+ executable_path = Pathname(spec.full_gem_path).join(spec.bindir, executable).relative_path_from(bin_path)
+ executable_path = executable_path
+
+ mode = Gem.win_platform? ? "wb:UTF-8" : "w"
+ require "erb"
+ content = ERB.new(template, trim_mode: "-").result(binding)
+
+ File.write("#{bin_path}/#{executable}", content, mode: mode, perm: 0o755)
+ if Gem.win_platform? || options[:all_platforms]
+ prefix = "@ruby -x \"%~f0\" %*\n@exit /b %ERRORLEVEL%\n\n"
+ File.write("#{bin_path}/#{executable}.cmd", prefix + content, mode: mode)
+ end
+ end
+ end
+
+ private
+
+ # the order that the resolver provides is significant, since
+ # dependencies might affect the installation of a gem.
+ # that said, it's a rare situation (other than rake), and parallel
+ # installation is SO MUCH FASTER. so we let people opt in.
+ def install(options)
+ standalone = options[:standalone]
+ force = options[:force]
+ local = options[:local] || options[:"prefer-local"]
+ jobs = Bundler.settings.installation_parallelization
+ spec_installations = ParallelInstaller.call(self, @definition.specs, jobs, standalone, force, local: local)
+ spec_installations.each do |installation|
+ post_install_messages[installation.name] = installation.post_install_message if installation.has_post_install_message?
+ end
+ end
+
+ def load_plugins
+ Gem.load_plugins
+
+ requested_path_gems = @definition.specs.select {|s| s.source.is_a?(Source::Path) }
+ path_plugin_files = requested_path_gems.flat_map do |spec|
+ spec.matches_for_glob("rubygems_plugin#{Bundler.rubygems.suffix_pattern}")
+ rescue TypeError
+ error_message = "#{spec.name} #{spec.version} has an invalid gemspec"
+ raise Gem::InvalidSpecificationException, error_message
+ end
+ Gem.load_plugin_files(path_plugin_files)
+ Gem.load_env_plugins
+ end
+
+ def ensure_specs_are_compatible!
+ overrides = @definition.overrides
+ @definition.specs.each do |spec|
+ unless spec.matches_current_ruby_with_overrides?(overrides)
+ raise InstallError, "#{spec.full_name} requires ruby version #{spec.required_ruby_version}, " \
+ "which is incompatible with the current version, #{Gem.ruby_version}"
+ end
+ unless spec.matches_current_rubygems_with_overrides?(overrides)
+ raise InstallError, "#{spec.full_name} requires rubygems version #{spec.required_rubygems_version}, " \
+ "which is incompatible with the current version, #{Gem.rubygems_version}"
+ end
+ end
+ end
+
+ def lock
+ @definition.lock
+ end
+ end
+end
diff --git a/lib/bundler/installer/gem_installer.rb b/lib/bundler/installer/gem_installer.rb
new file mode 100644
index 0000000000..f3b43c31ee
--- /dev/null
+++ b/lib/bundler/installer/gem_installer.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Bundler
+ class GemInstaller
+ attr_reader :spec, :standalone, :worker, :force, :local, :installer
+
+ def initialize(spec, installer, standalone = false, worker = 0, force = false, local = false)
+ @spec = spec
+ @installer = installer
+ @standalone = standalone
+ @worker = worker
+ @force = force
+ @local = local
+ end
+
+ def install_from_spec
+ post_install_message = install
+ Bundler.ui.debug "#{worker}: #{spec.name} (#{spec.version}) from #{spec.loaded_from}"
+ [true, post_install_message]
+ rescue Bundler::InstallHookError, Bundler::SecurityError, Bundler::APIResponseMismatchError, Bundler::InsecureInstallPathError
+ raise
+ rescue Errno::ENOSPC
+ [false, out_of_space_message]
+ rescue Bundler::BundlerError, Gem::InstallError => e
+ [false, specific_failure_message(e)]
+ end
+
+ def download
+ spec.source.download(
+ spec,
+ force: force,
+ local: local,
+ build_args: Array(spec_settings),
+ previous_spec: previous_spec,
+ )
+
+ [true, nil]
+ rescue Bundler::BundlerError => e
+ [false, specific_failure_message(e)]
+ end
+
+ private
+
+ def specific_failure_message(e)
+ message = "#{e.class}: #{e.message}\n"
+ message += " " + e.backtrace.join("\n ") + "\n\n"
+ message = message.lines.first + Bundler.ui.add_color(message.lines.drop(1).join, :clear)
+ message + Bundler.ui.add_color(failure_message, :red)
+ end
+
+ def failure_message
+ install_error_message
+ end
+
+ def install_error_message
+ "An error occurred while installing #{spec.name} (#{spec.version}), and Bundler cannot continue."
+ end
+
+ def spec_settings
+ # Fetch the build settings, if there are any
+ if settings = Bundler.settings["build.#{spec.name}"]
+ require "shellwords"
+ Shellwords.shellsplit(settings)
+ end
+ end
+
+ def install
+ spec.source.install(
+ spec,
+ force: force,
+ local: local,
+ build_args: Array(spec_settings),
+ previous_spec: previous_spec,
+ )
+ end
+
+ def previous_spec
+ locked_gems = installer.definition.locked_gems
+ return unless locked_gems
+
+ locked_gems.specs.find {|s| s.name == spec.name }
+ end
+
+ def out_of_space_message
+ "#{install_error_message}\nYour disk is out of space. Free some space to be able to install your bundle."
+ end
+ end
+end
diff --git a/lib/bundler/installer/parallel_installer.rb b/lib/bundler/installer/parallel_installer.rb
new file mode 100644
index 0000000000..fef326ed0a
--- /dev/null
+++ b/lib/bundler/installer/parallel_installer.rb
@@ -0,0 +1,241 @@
+# frozen_string_literal: true
+
+require_relative "../worker"
+require_relative "gem_installer"
+
+module Bundler
+ class ParallelInstaller
+ class SpecInstallation
+ attr_accessor :spec, :name, :full_name, :post_install_message, :state, :error, :dependencies
+ def initialize(spec)
+ @spec = spec
+ @name = spec.name
+ @full_name = spec.full_name
+ @state = :none
+ @post_install_message = ""
+ @error = nil
+ end
+
+ def installed?
+ state == :installed
+ end
+
+ def enqueued?
+ state == :enqueued
+ end
+
+ def enqueue_with_priority?
+ state == :installable && spec.extensions.any?
+ end
+
+ def failed?
+ state == :failed
+ end
+
+ def ready_to_enqueue?
+ state == :none
+ end
+
+ def ready_to_install?(installed_specs)
+ return false unless state == :downloaded
+
+ spec.extensions.none? || dependencies_installed?(installed_specs)
+ end
+
+ def has_post_install_message?
+ !post_install_message.empty?
+ end
+
+ # Recursively checks that all dependencies (direct and transitive) have been installed.
+ def dependencies_installed?(installed_specs)
+ dependencies.all? do |dep|
+ installed_specs.include?(dep.name) && dep.dependencies_installed?(installed_specs)
+ end
+ end
+
+ def to_s
+ "#<#{self.class} #{full_name} (#{state})>"
+ end
+ end
+
+ def self.call(*args, **kwargs)
+ new(*args, **kwargs).call
+ end
+
+ attr_reader :size
+
+ def initialize(installer, all_specs, size, standalone, force, local: false, skip: nil)
+ @installer = installer
+ @size = size
+ @standalone = standalone
+ @force = force
+ @local = local
+ @specs = all_specs.map {|s| SpecInstallation.new(s) }
+ specs_by_name = @specs.to_h {|s| [s.name, s] }
+ @specs.each do |spec_install|
+ spec_install.dependencies = spec_install.spec.dependencies.filter_map do |dep|
+ specs_by_name[dep.name] unless dep.type == :development || dep.name == spec_install.name
+ end
+ end
+ @specs.each do |spec_install|
+ spec_install.state = :installed if skip.include?(spec_install.name)
+ end if skip
+ @spec_set = all_specs
+ @rake = @specs.find {|s| s.name == "rake" unless s.installed? }
+ end
+
+ def call
+ if @rake
+ do_download(@rake, 0)
+ do_install(@rake, 0)
+ Gem::Specification.reset
+ end
+
+ if @size > 1
+ install_with_worker
+ else
+ install_serially
+ end
+
+ handle_error if failed_specs.any?
+ @specs
+ ensure
+ worker_pool&.stop
+ end
+
+ private
+
+ def failed_specs
+ @specs.select(&:failed?)
+ end
+
+ def install_with_worker
+ installed_specs = {}
+ enqueue_specs(installed_specs)
+
+ process_specs(installed_specs) until finished_installing?
+ end
+
+ def install_serially
+ until finished_installing?
+ raise "failed to find a spec to enqueue while installing serially" unless spec_install = @specs.find(&:ready_to_enqueue?)
+ spec_install.state = :enqueued
+ do_download(spec_install, 0)
+ do_install(spec_install, 0)
+ end
+ end
+
+ def worker_pool
+ @worker_pool ||= Bundler::Worker.new @size, "Parallel Installer", lambda {|spec_install, worker_num|
+ case spec_install.state
+ when :enqueued
+ do_download(spec_install, worker_num)
+ when :installable
+ do_install(spec_install, worker_num)
+ else
+ spec_install
+ end
+ }
+ end
+
+ def do_download(spec_install, worker_num)
+ Plugin.hook(Plugin::Events::GEM_BEFORE_INSTALL, spec_install)
+
+ gem_installer = Bundler::GemInstaller.new(
+ spec_install.spec, @installer, @standalone, worker_num, @force, @local
+ )
+
+ success, message = gem_installer.download
+
+ if success
+ spec_install.state = :downloaded
+ else
+ spec_install.error = "#{message}\n\n#{require_tree_for_spec(spec_install.spec)}"
+ spec_install.state = :failed
+ end
+
+ spec_install
+ end
+
+ def do_install(spec_install, worker_num)
+ gem_installer = Bundler::GemInstaller.new(
+ spec_install.spec, @installer, @standalone, worker_num, @force, @local
+ )
+ success, message = gem_installer.install_from_spec
+ if success
+ spec_install.state = :installed
+ spec_install.post_install_message = message unless message.nil?
+ else
+ spec_install.error = "#{message}\n\n#{require_tree_for_spec(spec_install.spec)}"
+ spec_install.state = :failed
+ end
+ Plugin.hook(Plugin::Events::GEM_AFTER_INSTALL, spec_install)
+ spec_install
+ end
+
+ # Dequeue a spec and save its post-install message and then enqueue the
+ # remaining specs.
+ # Some specs might've had to wait til this spec was installed to be
+ # processed so the call to `enqueue_specs` is important after every
+ # dequeue.
+ def process_specs(installed_specs)
+ spec = worker_pool.deq
+
+ if spec.installed?
+ installed_specs[spec.name] = true
+ return
+ elsif spec.failed?
+ return
+ elsif spec.ready_to_install?(installed_specs)
+ spec.state = :installable
+ end
+
+ worker_pool.enq(spec, priority: spec.enqueue_with_priority?)
+ end
+
+ def finished_installing?
+ @specs.all? do |spec|
+ return true if spec.failed?
+ spec.installed?
+ end
+ end
+
+ def handle_error
+ errors = failed_specs.map(&:error)
+ if exception = errors.find {|e| e.is_a?(Bundler::BundlerError) }
+ raise exception
+ end
+ raise Bundler::InstallError, errors.join("\n\n")
+ end
+
+ def require_tree_for_spec(spec)
+ tree = @spec_set.what_required(spec)
+ t = String.new("In #{File.basename(SharedHelpers.default_gemfile)}:\n")
+ tree.each_with_index do |s, depth|
+ t << " " * depth.succ << s.name
+ unless tree.last == s
+ t << %( was resolved to #{s.version}, which depends on)
+ end
+ t << %(\n)
+ end
+ t
+ end
+
+ # Keys in the remains hash represent uninstalled gems specs.
+ # We enqueue all gem specs that do not have any dependencies.
+ # Later we call this lambda again to install specs that depended on
+ # previously installed specifications. We continue until all specs
+ # are installed.
+ def enqueue_specs(installed_specs)
+ @specs.each do |spec|
+ if spec.installed?
+ installed_specs[spec.name] = true
+ next
+ end
+
+ spec.state = :enqueued
+ worker_pool.enq spec
+ end
+ end
+ end
+end
diff --git a/lib/bundler/installer/standalone.rb b/lib/bundler/installer/standalone.rb
new file mode 100644
index 0000000000..8b4de64df5
--- /dev/null
+++ b/lib/bundler/installer/standalone.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Standalone
+ def initialize(groups, definition)
+ @specs = definition.specs_for(groups)
+ end
+
+ def generate
+ SharedHelpers.filesystem_access(bundler_path) do |p|
+ FileUtils.mkdir_p(p)
+ end
+ File.open File.join(bundler_path, "setup.rb"), "w" do |file|
+ file.puts "require 'rbconfig'"
+ file.puts prevent_gem_activation
+ file.puts define_path_helpers
+ file.puts reverse_rubygems_kernel_mixin
+ paths.each do |path|
+ if Pathname.new(path).absolute?
+ file.puts %($:.unshift "#{path}")
+ else
+ file.puts %($:.unshift File.expand_path("\#{__dir__}/#{path}"))
+ end
+ end
+ end
+ end
+
+ private
+
+ def paths
+ @specs.flat_map do |spec|
+ next if spec.name == "bundler"
+ Array(spec.require_paths).map do |path|
+ gem_path(path, spec).
+ sub(version_dir, '#{RUBY_ENGINE}/#{Gem.ruby_api_version}').
+ sub(extensions_dir, 'extensions/\k<platform>/#{Gem.extension_api_version}')
+ # This is a static string intentionally. It's interpolated at a later time.
+ end
+ end.compact
+ end
+
+ def version_dir
+ "#{RUBY_ENGINE}/#{Gem.ruby_api_version}"
+ end
+
+ def extensions_dir
+ %r{extensions/(?<platform>[^/]+)/#{Regexp.escape(Gem.extension_api_version)}}
+ end
+
+ def bundler_path
+ Bundler.root.join(Bundler.settings[:path].to_s, "bundler")
+ end
+
+ def gem_path(path, spec)
+ full_path = Pathname.new(path).absolute? ? path : File.join(spec.full_gem_path, path)
+ if spec.source.instance_of?(Source::Path) && spec.source.path.absolute?
+ full_path
+ else
+ SharedHelpers.relative_path_to(full_path, from: Bundler.root.join(bundler_path))
+ end
+ end
+
+ def prevent_gem_activation
+ <<~'END'
+ module Kernel
+ remove_method(:gem) if private_method_defined?(:gem)
+
+ def gem(*)
+ end
+
+ private :gem
+ end
+ END
+ end
+
+ def define_path_helpers
+ <<~'END'
+ unless defined?(Gem)
+ module Gem
+ def self.ruby_api_version
+ RbConfig::CONFIG["ruby_version"]
+ end
+
+ def self.extension_api_version
+ if 'no' == RbConfig::CONFIG['ENABLE_SHARED']
+ "#{ruby_api_version}-static"
+ else
+ ruby_api_version
+ end
+ end
+ end
+ end
+ END
+ end
+
+ def reverse_rubygems_kernel_mixin
+ <<~END
+ if Gem.respond_to?(:discover_gems_on_require=)
+ Gem.discover_gems_on_require = false
+ else
+ [::Kernel.singleton_class, ::Kernel].each do |k|
+ if k.private_method_defined?(:gem_original_require)
+ private_require = k.private_method_defined?(:require)
+ k.send(:remove_method, :require)
+ k.send(:define_method, :require, k.instance_method(:gem_original_require))
+ k.send(:private, :require) if private_require
+ end
+ end
+ end
+ END
+ end
+ end
+end
diff --git a/lib/bundler/lazy_specification.rb b/lib/bundler/lazy_specification.rb
new file mode 100644
index 0000000000..0da621d21f
--- /dev/null
+++ b/lib/bundler/lazy_specification.rb
@@ -0,0 +1,272 @@
+# frozen_string_literal: true
+
+require_relative "force_platform"
+
+module Bundler
+ class LazySpecification
+ include MatchMetadata
+ include MatchPlatform
+ include ForcePlatform
+
+ attr_reader :name, :version, :platform, :materialization
+ attr_accessor :source, :remote, :force_ruby_platform, :dependencies, :required_ruby_version, :required_rubygems_version, :overrides
+
+ #
+ # For backwards compatibility with existing lockfiles, if the most specific
+ # locked platform is not a specific platform like x86_64-linux or
+ # universal-java-11, then we keep the previous behaviour of resolving the
+ # best platform variant at materiliazation time. For previous bundler
+ # versions (before 2.2.0) this was always the case (except when the lockfile
+ # only included non-ruby platforms), but we're also keeping this behaviour
+ # on newer bundlers unless users generate the lockfile from scratch or
+ # explicitly add a more specific platform.
+ #
+ attr_accessor :most_specific_locked_platform
+
+ alias_method :runtime_dependencies, :dependencies
+
+ def self.from_spec(s)
+ lazy_spec = new(s.name, s.version, s.platform, s.source)
+ lazy_spec.dependencies = s.runtime_dependencies
+ lazy_spec.required_ruby_version = s.required_ruby_version
+ lazy_spec.required_rubygems_version = s.required_rubygems_version
+ lazy_spec.overrides = s.overrides if s.is_a?(LazySpecification)
+ lazy_spec
+ end
+
+ def initialize(name, version, platform, source = nil, **materialization_options)
+ @name = name
+ @version = version
+ @dependencies = []
+ @required_ruby_version = Gem::Requirement.default
+ @required_rubygems_version = Gem::Requirement.default
+ @platform = platform || Gem::Platform::RUBY
+
+ @original_source = source
+ @source = source
+ @materialization_options = materialization_options
+
+ @force_ruby_platform = default_force_ruby_platform
+ @most_specific_locked_platform = nil
+ @materialization = nil
+ end
+
+ def missing?
+ @materialization == self
+ end
+
+ def incomplete?
+ @materialization.nil?
+ end
+
+ def source_changed?
+ @original_source != source
+ end
+
+ def full_name
+ @full_name ||= if platform == Gem::Platform::RUBY
+ "#{@name}-#{@version}"
+ else
+ "#{@name}-#{@version}-#{platform}"
+ end
+ end
+
+ def lock_name
+ @lock_name ||= name_tuple.lock_name
+ end
+
+ def name_tuple
+ Gem::NameTuple.new(@name, @version, @platform)
+ end
+
+ def ==(other)
+ full_name == other.full_name
+ end
+
+ def eql?(other)
+ full_name.eql?(other.full_name)
+ end
+
+ def hash
+ full_name.hash
+ end
+
+ ##
+ # Does this locked specification satisfy +dependency+?
+ #
+ # NOTE: Rubygems default requirement is ">= 0", which doesn't match
+ # prereleases of 0 versions, like "0.0.0.dev" or "0.0.0.SNAPSHOT". However,
+ # bundler users expect those to work. We need to make sure that Gemfile
+ # dependencies without explicit requirements (which use ">= 0" under the
+ # hood by default) are still valid for locked specs using this kind of
+ # versions. The method implements an ad-hoc fix for that. A better solution
+ # might be to change default rubygems requirement of dependencies to be ">=
+ # 0.A" but that's a major refactoring likely to break things. Hopefully we
+ # can attempt it in the future.
+ #
+
+ def satisfies?(dependency)
+ effective_requirement = dependency.requirement == Gem::Requirement.default ? Gem::Requirement.new(">= 0.A") : dependency.requirement
+
+ @name == dependency.name && effective_requirement.satisfied_by?(Gem::Version.new(@version))
+ end
+
+ def to_lock
+ out = String.new
+ out << " #{lock_name}\n"
+
+ dependencies.sort_by(&:to_s).uniq.each do |dep|
+ next if dep.type == :development
+ out << " #{dep.to_lock}\n"
+ end
+
+ out
+ end
+
+ def materialize_for_cache
+ source.remote!
+
+ materialize(self, &:first)
+ end
+
+ def materialized_for_installation
+ @materialization = materialize_for_installation
+
+ self unless incomplete?
+ end
+
+ def materialize_for_installation
+ source.local!
+
+ if use_exact_resolved_specifications?
+ spec = materialize(self) {|specs| choose_compatible(specs, fallback_to_non_installable: false) }
+ return spec if spec
+
+ # Exact spec is incompatible; in frozen mode, try to find a compatible platform variant
+ # In non-frozen mode, return nil to trigger re-resolution and lockfile update
+ if Bundler.frozen_bundle?
+ materialize([name, version]) {|specs| resolve_best_platform(specs) }
+ end
+ else
+ materialize([name, version]) {|specs| resolve_best_platform(specs) }
+ end
+ end
+
+ def inspect
+ "#<#{self.class} @name=\"#{name}\" (#{full_name.delete_prefix("#{name}-")})>"
+ end
+
+ def to_s
+ lock_name
+ end
+
+ def git_version
+ return unless source.is_a?(Bundler::Source::Git)
+ " #{source.revision[0..6]}"
+ end
+
+ def force_ruby_platform!
+ @force_ruby_platform = true
+ end
+
+ def replace_source_with!(gemfile_source)
+ return unless gemfile_source.can_lock?(self)
+
+ @source = gemfile_source
+
+ true
+ end
+
+ private
+
+ def use_exact_resolved_specifications?
+ !source.is_a?(Source::Path) && ruby_platform_materializes_to_ruby_platform?
+ end
+
+ # Try platforms in order of preference until finding a compatible spec.
+ # Used for legacy lockfiles and as a fallback when the exact locked spec
+ # is incompatible. Falls back to frozen bundle behavior if none match.
+ def resolve_best_platform(specs)
+ find_compatible_platform_spec(specs) || frozen_bundle_fallback(specs)
+ end
+
+ def find_compatible_platform_spec(specs)
+ candidate_platforms.each do |plat|
+ candidates = MatchPlatform.select_best_platform_match(specs, plat)
+ spec = choose_compatible(candidates, fallback_to_non_installable: false)
+ return spec if spec
+ end
+ nil
+ end
+
+ # Platforms to try in order of preference. Ruby platform is last since it
+ # requires compilation, but works when precompiled gems are incompatible.
+ def candidate_platforms
+ target = source.is_a?(Source::Path) ? platform : Bundler.local_platform
+ [target, platform, Gem::Platform::RUBY].uniq
+ end
+
+ # In frozen mode, accept any candidate. Will error at install time.
+ # When target differs from locked platform, prefer locked platform's candidates
+ # to preserve lockfile integrity.
+ def frozen_bundle_fallback(specs)
+ target = source.is_a?(Source::Path) ? platform : Bundler.local_platform
+ fallback_platform = target == platform ? target : platform
+ candidates = MatchPlatform.select_best_platform_match(specs, fallback_platform)
+ choose_compatible(candidates)
+ end
+
+ def ruby_platform_materializes_to_ruby_platform?
+ generic_platform = Bundler.generic_local_platform == Gem::Platform::JAVA ? Gem::Platform::JAVA : Gem::Platform::RUBY
+
+ (most_specific_locked_platform != generic_platform) || force_ruby_platform || Bundler.settings[:force_ruby_platform]
+ end
+
+ def materialize(query)
+ matching_specs = source.specs.search(query)
+ return self if matching_specs.empty?
+
+ yield matching_specs
+ end
+
+ # If in frozen mode, we fallback to a non-installable candidate because by
+ # doing this we avoid re-resolving and potentially end up changing the
+ # lockfile, which is not allowed. In that case, we will give a proper error
+ # about the mismatch higher up the stack, right before trying to install the
+ # bad gem.
+ def choose_compatible(candidates, fallback_to_non_installable: Bundler.frozen_bundle?)
+ override_list = overrides || []
+ search = candidates.reverse.find do |spec|
+ spec.is_a?(StubSpecification) || spec.matches_current_metadata_with_overrides?(override_list)
+ end
+ if search.nil? && fallback_to_non_installable
+ search = candidates.last
+ end
+
+ if search
+ validate_dependencies(search) if search.platform == platform
+
+ search.locked_platform = platform if search.instance_of?(RemoteSpecification) || search.instance_of?(EndpointSpecification)
+ end
+ search
+ end
+
+ # Validate dependencies of this locked spec are consistent with dependencies
+ # of the actual spec that was materialized.
+ #
+ # Note that unless we are in strict mode (which we set during installation)
+ # we don't validate dependencies of locally installed gems but
+ # accept what's in the lockfile instead for performance, since loading
+ # dependencies of locally installed gems would mean evaluating all gemspecs,
+ # which would affect `bundler/setup` performance.
+ def validate_dependencies(spec)
+ if !@materialization_options[:strict] && spec.is_a?(StubSpecification)
+ spec.dependencies = dependencies
+ else
+ if !source.is_a?(Source::Path) && spec.runtime_dependencies.sort != dependencies.sort
+ raise IncorrectLockfileDependencies.new(self, spec.runtime_dependencies, dependencies)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/lockfile_generator.rb b/lib/bundler/lockfile_generator.rb
new file mode 100644
index 0000000000..2a3ad22480
--- /dev/null
+++ b/lib/bundler/lockfile_generator.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+module Bundler
+ class LockfileGenerator
+ attr_reader :definition
+ attr_reader :out
+
+ # @private
+ def initialize(definition)
+ @definition = definition
+ @out = String.new
+ end
+
+ def self.generate(definition)
+ new(definition).generate!
+ end
+
+ def generate!
+ add_sources
+ add_platforms
+ add_dependencies
+ add_checksums
+ add_locked_ruby_version
+ add_bundled_with
+
+ out
+ end
+
+ private
+
+ def add_sources
+ definition.sources.lock_sources.each_with_index do |source, idx|
+ out << "\n" unless idx.zero?
+
+ # Add the source header
+ out << source.to_lock
+
+ # Find all specs for this source
+ specs = definition.resolve.select {|s| source.can_lock?(s) }
+ add_specs(specs)
+ end
+ end
+
+ def add_specs(specs)
+ # This needs to be sorted by full name so that
+ # gems with the same name, but different platform
+ # are ordered consistently
+ specs.sort_by(&:full_name).each do |spec|
+ next if spec.name == "bundler"
+ out << spec.to_lock
+ end
+ end
+
+ def add_platforms
+ add_section("PLATFORMS", definition.platforms)
+ end
+
+ def add_dependencies
+ out << "\nDEPENDENCIES\n"
+
+ handled = []
+ definition.dependencies.sort_by(&:to_s).each do |dep|
+ next if handled.include?(dep.name)
+ out << dep.to_lock << "\n"
+ handled << dep.name
+ end
+ end
+
+ def add_checksums
+ return unless definition.locked_checksums
+ checksums = definition.resolve.map do |spec|
+ spec.source.checksum_store.to_lock(spec)
+ end
+
+ add_section("CHECKSUMS", checksums + bundler_checksum)
+ end
+
+ def add_locked_ruby_version
+ return unless locked_ruby_version = definition.locked_ruby_version
+ add_section("RUBY VERSION", locked_ruby_version.to_s)
+ end
+
+ def add_bundled_with
+ add_section("BUNDLED WITH", definition.bundler_version_to_lock.to_s)
+ end
+
+ def add_section(name, value)
+ out << "\n#{name}\n"
+ case value
+ when Array
+ value.map(&:to_s).sort.each do |val|
+ out << " #{val}\n"
+ end
+ when Hash
+ value.to_a.sort_by {|k, _| k.to_s }.each do |key, val|
+ out << " #{key}: #{val}\n"
+ end
+ when String
+ out << " #{value}\n"
+ else
+ raise ArgumentError, "#{value.inspect} can't be serialized in a lockfile"
+ end
+ end
+
+ def bundler_checksum
+ return [] if Bundler.gem_version.to_s.end_with?(".dev") || ENV["SKIP_BUNDLER_CHECKSUM"]
+
+ bundler_spec = definition.sources.metadata_source.specs.search(["bundler", Bundler.gem_version]).last
+ return [] unless File.exist?(bundler_spec.cache_file)
+
+ require "rubygems/package"
+
+ package = Gem::Package.new(bundler_spec.cache_file)
+ definition.sources.metadata_source.checksum_store.register(bundler_spec, Checksum.from_gem_package(package))
+
+ [definition.sources.metadata_source.checksum_store.to_lock(bundler_spec)]
+ end
+ end
+end
diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb
new file mode 100644
index 0000000000..852fc631f3
--- /dev/null
+++ b/lib/bundler/lockfile_parser.rb
@@ -0,0 +1,328 @@
+# frozen_string_literal: true
+
+require_relative "shared_helpers"
+
+module Bundler
+ class LockfileParser
+ class Position
+ attr_reader :line, :column
+ def initialize(line, column)
+ @line = line
+ @column = column
+ end
+
+ def advance!(string)
+ lines = string.count("\n")
+ if lines > 0
+ @line += lines
+ @column = string.length - string.rindex("\n")
+ else
+ @column += string.length
+ end
+ end
+
+ def to_s
+ "#{line}:#{column}"
+ end
+ end
+
+ attr_reader(
+ :sources,
+ :metadata_source,
+ :dependencies,
+ :specs,
+ :platforms,
+ :most_specific_locked_platform,
+ :bundler_version,
+ :ruby_version,
+ :checksums,
+ )
+
+ BUNDLED = "BUNDLED WITH"
+ DEPENDENCIES = "DEPENDENCIES"
+ CHECKSUMS = "CHECKSUMS"
+ PLATFORMS = "PLATFORMS"
+ RUBY = "RUBY VERSION"
+ GIT = "GIT"
+ GEM = "GEM"
+ PATH = "PATH"
+ PLUGIN = "PLUGIN SOURCE"
+ SPECS = " specs:"
+ OPTIONS = /^ ([a-z]+): (.*)$/i
+ SOURCE = [GIT, GEM, PATH, PLUGIN].freeze
+
+ SECTIONS_BY_VERSION_INTRODUCED = {
+ Gem::Version.create("1.0") => [DEPENDENCIES, PLATFORMS, GIT, GEM, PATH].freeze,
+ Gem::Version.create("1.10") => [BUNDLED].freeze,
+ Gem::Version.create("1.12") => [RUBY].freeze,
+ Gem::Version.create("1.13") => [PLUGIN].freeze,
+ Gem::Version.create("2.5.0") => [CHECKSUMS].freeze,
+ }.freeze
+
+ KNOWN_SECTIONS = SECTIONS_BY_VERSION_INTRODUCED.values.flatten!.freeze
+
+ ENVIRONMENT_VERSION_SECTIONS = [BUNDLED, RUBY].freeze
+ deprecate_constant(:ENVIRONMENT_VERSION_SECTIONS)
+
+ def self.sections_in_lockfile(lockfile_contents)
+ sections = lockfile_contents.scan(/^\w[\w ]*$/)
+ sections.uniq!
+ sections
+ end
+
+ def self.unknown_sections_in_lockfile(lockfile_contents)
+ sections_in_lockfile(lockfile_contents) - KNOWN_SECTIONS
+ end
+
+ def self.sections_to_ignore(base_version = nil)
+ base_version &&= base_version.release
+ base_version ||= Gem::Version.create("1.0")
+ attributes = []
+ SECTIONS_BY_VERSION_INTRODUCED.each do |version, introduced|
+ next if version <= base_version
+ attributes += introduced
+ end
+ attributes
+ end
+
+ def self.bundled_with
+ lockfile = Bundler.default_lockfile
+ return unless lockfile.file?
+
+ lockfile_contents = Bundler.read_file(lockfile)
+ return unless lockfile_contents.include?(BUNDLED)
+
+ lockfile_contents.split(BUNDLED).last.strip
+ end
+
+ def initialize(lockfile, strict: false, lockfile_path: nil)
+ @platforms = []
+ @sources = []
+ @metadata_source = Source::Metadata.new
+ @dependencies = {}
+ @parse_method = nil
+ @specs = {}
+ @lockfile_path = lockfile_path || begin
+ SharedHelpers.relative_lockfile_path
+ rescue GemfileNotFound
+ "Gemfile.lock"
+ end
+ @pos = Position.new(1, 1)
+ @strict = strict
+
+ if lockfile.match?(/<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|/)
+ raise LockfileError, "Your #{@lockfile_path} contains merge conflicts.\n" \
+ "Run `git checkout HEAD -- #{@lockfile_path}` first to get a clean lock."
+ end
+
+ @valid = lockfile.strip.empty? ||
+ lockfile.split(/(?:\r?\n)+/).any? {|l| KNOWN_SECTIONS.include?(l) }
+
+ unless @valid
+ SharedHelpers.feature_deprecated!(
+ "Your #{@lockfile_path} does not appear to be a valid lockfile. " \
+ "Run `rm #{@lockfile_path}` and then `bundle install` to generate a new lockfile. " \
+ "This will raise a LockfileError in a future version of Bundler."
+ )
+ end
+
+ lockfile.split(/((?:\r?\n)+)/) do |line|
+ # split alternates between the line and the following whitespace
+ next @pos.advance!(line) if line.match?(/^\s*$/)
+
+ if SOURCE.include?(line)
+ @parse_method = :parse_source
+ parse_source(line)
+ elsif line == DEPENDENCIES
+ @parse_method = :parse_dependency
+ elsif line == CHECKSUMS
+ # This is a temporary solution to make this feature disabled by default
+ # for all gemfiles that don't already explicitly include the feature.
+ @checksums = true
+ @parse_method = :parse_checksum
+ elsif line == PLATFORMS
+ @parse_method = :parse_platform
+ elsif line == RUBY
+ @parse_method = :parse_ruby
+ elsif line == BUNDLED
+ @parse_method = :parse_bundled_with
+ elsif /^[^\s]/.match?(line)
+ @parse_method = nil
+ elsif @parse_method
+ send(@parse_method, line)
+ end
+ @pos.advance!(line)
+ end
+
+ if @platforms.include?(Gem::Platform::X64_MINGW_LEGACY)
+ SharedHelpers.feature_deprecated!("Found x64-mingw32 in lockfile, which is deprecated and will be removed in the future.")
+ end
+
+ @most_specific_locked_platform = @platforms.min_by do |bundle_platform|
+ Gem::Platform.platform_specificity_match(bundle_platform, Bundler.local_platform)
+ end
+ @specs = @specs.values.sort_by!(&:full_name).each do |spec|
+ spec.most_specific_locked_platform = @most_specific_locked_platform
+ end
+ rescue ArgumentError => e
+ Bundler.ui.debug(e)
+ raise LockfileError, "Your lockfile is unreadable. Run `rm #{@lockfile_path}` " \
+ "and then `bundle install` to generate a new lockfile. The error occurred while " \
+ "evaluating #{@lockfile_path}:#{@pos}"
+ end
+
+ def may_include_redundant_platform_specific_gems?
+ bundler_version.nil? || bundler_version < Gem::Version.new("1.16.2")
+ end
+
+ def valid?
+ @valid
+ end
+
+ private
+
+ TYPES = {
+ GIT => Bundler::Source::Git,
+ GEM => Bundler::Source::Rubygems,
+ PATH => Bundler::Source::Path,
+ PLUGIN => Bundler::Plugin,
+ }.freeze
+
+ def parse_source(line)
+ case line
+ when SPECS
+ return unless TYPES.key?(@type)
+ @current_source = TYPES[@type].from_lock(@opts)
+ @sources << @current_source
+ when OPTIONS
+ value = $2
+ value = true if value == "true"
+ value = false if value == "false"
+
+ key = $1
+
+ if @opts[key]
+ @opts[key] = Array(@opts[key])
+ @opts[key] << value
+ else
+ @opts[key] = value
+ end
+ when *SOURCE
+ @current_source = nil
+ @opts = {}
+ @type = line
+ else
+ parse_spec(line)
+ end
+ end
+
+ space = / /
+ NAME_VERSION = /
+ ^(#{space}{2}|#{space}{4}|#{space}{6})(?!#{space}) # Exactly 2, 4, or 6 spaces at the start of the line
+ (.*?) # Name
+ (?:#{space}\(([^-]*) # Space, followed by version
+ (?:-(.*))?\))? # Optional platform
+ (!)? # Optional pinned marker
+ (?:#{space}([^ ]+))? # Optional checksum
+ $ # Line end
+ /xo
+
+ def parse_dependency(line)
+ return unless line =~ NAME_VERSION
+ spaces = $1
+ return unless spaces.size == 2
+ name = -$2
+ version = $3
+ pinned = $5
+
+ version = version.split(",").each(&:strip!) if version
+
+ dep = Bundler::Dependency.new(name, version)
+
+ if pinned && dep.name != "bundler"
+ spec = @specs.find {|_, v| v.name == dep.name }
+ dep.source = spec.last.source if spec
+
+ # Path sources need to know what the default name / version
+ # to use in the case that there are no gemspecs present. A fake
+ # gemspec is created based on the version set on the dependency
+ # TODO: Use the version from the spec instead of from the dependency
+ if version && version.size == 1 && version.first =~ /^\s*= (.+)\s*$/ && dep.source.is_a?(Bundler::Source::Path)
+ dep.source.name = name
+ dep.source.version = $1
+ end
+ end
+
+ @dependencies[dep.name] = dep
+ end
+
+ def parse_checksum(line)
+ return unless line =~ NAME_VERSION
+
+ spaces = $1
+ return unless spaces.size == 2
+ checksums = $6
+ name = $2
+ version = $3
+ platform = $4
+
+ version = Gem::Version.new(version)
+ platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY
+ full_name = Gem::NameTuple.new(name, version, platform).full_name
+ spec = @specs[full_name]
+
+ if name == "bundler"
+ spec ||= LazySpecification.new(name, version, platform, @metadata_source)
+ end
+ return unless spec
+
+ if checksums
+ checksums.split(",") do |lock_checksum|
+ column = line.index(lock_checksum) + 1
+ checksum = Checksum.from_lock(lock_checksum, "#{@lockfile_path}:#{@pos.line}:#{column}")
+ spec.source.checksum_store.register(spec, checksum)
+ end
+ else
+ spec.source.checksum_store.register(spec, nil)
+ end
+ end
+
+ def parse_spec(line)
+ return unless line =~ NAME_VERSION
+ spaces = $1
+ name = -$2
+ version = $3
+
+ if spaces.size == 4
+ # only load platform for non-dependency (spec) line
+ platform = $4
+
+ version = Gem::Version.new(version)
+ platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY
+ @current_spec = LazySpecification.new(name, version, platform, @current_source, strict: @strict)
+ @current_source.add_dependency_names(name)
+
+ @specs[@current_spec.full_name] = @current_spec
+ elsif spaces.size == 6
+ version = version.split(",").each(&:strip!) if version
+ dep = Gem::Dependency.new(name, version)
+ @current_spec.dependencies << dep
+ end
+ end
+
+ def parse_platform(line)
+ @platforms << Gem::Platform.new($1.strip) if line =~ /^ (.*)$/
+ end
+
+ def parse_bundled_with(line)
+ line.strip!
+ return unless Gem::Version.correct?(line)
+ @bundler_version = Gem::Version.create(line)
+ end
+
+ def parse_ruby(line)
+ line.strip!
+ @ruby_version = line
+ end
+ end
+end
diff --git a/lib/bundler/man/.document b/lib/bundler/man/.document
new file mode 100644
index 0000000000..fb66f13c33
--- /dev/null
+++ b/lib/bundler/man/.document
@@ -0,0 +1 @@
+# Ignore all files in this directory
diff --git a/lib/bundler/man/bundle-add.1 b/lib/bundler/man/bundle-add.1
new file mode 100644
index 0000000000..0956aa83f1
--- /dev/null
+++ b/lib/bundler/man/bundle-add.1
@@ -0,0 +1,82 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-ADD" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-add\fR \- Add gem to the Gemfile and run bundle install
+.SH "SYNOPSIS"
+\fBbundle add\fR \fIGEM_NAME\fR [\-\-group=GROUP] [\-\-version=VERSION] [\-\-source=SOURCE] [\-\-path=PATH] [\-\-git=GIT|\-\-github=GITHUB] [\-\-branch=BRANCH] [\-\-ref=REF] [\-\-cooldown=NUMBER] [\-\-quiet] [\-\-skip\-install] [\-\-strict|\-\-optimistic]
+.SH "DESCRIPTION"
+Adds the named gem to the [\fBGemfile(5)\fR][Gemfile(5)] and run \fBbundle install\fR\. \fBbundle install\fR can be avoided by using the flag \fB\-\-skip\-install\fR\.
+.SH "OPTIONS"
+.TP
+\fB\-\-version=VERSION\fR, \fB\-v=VERSION\fR
+Specify version requirements(s) for the added gem\.
+.TP
+\fB\-\-group=GROUP\fR, \fB\-g=GROUP\fR
+Specify the group(s) for the added gem\. Multiple groups should be separated by commas\.
+.TP
+\fB\-\-source=SOURCE\fR, \fB\-s=SOURCE\fR
+Specify the source for the added gem\.
+.TP
+\fB\-\-require=REQUIRE\fR, \fB\-r=REQUIRE\fR
+Adds require path to gem\. Provide false, or a path as a string\.
+.TP
+\fB\-\-path=PATH\fR
+Specify the file system path for the added gem\.
+.TP
+\fB\-\-git=GIT\fR
+Specify the git source for the added gem\.
+.TP
+\fB\-\-github=GITHUB\fR
+Specify the github source for the added gem\.
+.TP
+\fB\-\-branch=BRANCH\fR
+Specify the git branch for the added gem\.
+.TP
+\fB\-\-ref=REF\fR
+Specify the git ref for the added gem\.
+.TP
+\fB\-\-glob=GLOB\fR
+Specify the location of a dependency's \.gemspec, expanded within Ruby (single quotes recommended)\.
+.TP
+\fB\-\-quiet\fR
+Do not print progress information to the standard output\.
+.TP
+\fB\-\-skip\-install\fR
+Adds the gem to the Gemfile but does not install it\.
+.TP
+\fB\-\-optimistic\fR
+Ignored (now default behavior)
+.TP
+\fB\-\-pessimistic\fR
+Adds pessimistic declaration of version\.
+.TP
+\fB\-\-strict\fR
+Adds strict declaration of version\.
+.TP
+\fB\-\-cooldown=<number>\fR
+Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run\. See \fBcooldown\fR in bundle\-config(1) for precedence rules\.
+.SH "EXAMPLES"
+.IP "1." 4
+You can add the \fBrails\fR gem to the Gemfile without any version restriction\. The source of the gem will be the global source\.
+.IP
+\fBbundle add rails\fR
+.IP "2." 4
+You can add the \fBrails\fR gem with version greater than 1\.1 (not including 1\.1) and less than 3\.0\.
+.IP
+\fBbundle add rails \-\-version "> 1\.1, < 3\.0"\fR
+.IP "3." 4
+You can use the \fBhttps://gems\.example\.com\fR custom source and assign the gem to a group\.
+.IP
+\fBbundle add rails \-\-version ">= 5\.0\.0" \-\-source "https://gems\.example\.com" \-\-group "development"\fR
+.IP "4." 4
+The following adds the \fBgem\fR entry to the Gemfile without installing the gem\. You can install gems later via \fBbundle install\fR\.
+.IP
+\fBbundle add rails \-\-skip\-install\fR
+.IP "5." 4
+You can assign the gem to more than one group\.
+.IP
+\fBbundle add rails \-\-group "development, test"\fR
+.IP "" 0
+.SH "SEE ALSO"
+Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR, bundle\-remove(1) \fIbundle\-remove\.1\.html\fR
diff --git a/lib/bundler/man/bundle-add.1.ronn b/lib/bundler/man/bundle-add.1.ronn
new file mode 100644
index 0000000000..8c65af0cc0
--- /dev/null
+++ b/lib/bundler/man/bundle-add.1.ronn
@@ -0,0 +1,95 @@
+bundle-add(1) -- Add gem to the Gemfile and run bundle install
+==============================================================
+
+## SYNOPSIS
+
+`bundle add` <GEM_NAME> [--group=GROUP] [--version=VERSION] [--source=SOURCE]
+ [--path=PATH] [--git=GIT|--github=GITHUB] [--branch=BRANCH] [--ref=REF]
+ [--cooldown=NUMBER] [--quiet] [--skip-install] [--strict|--optimistic]
+
+## DESCRIPTION
+
+Adds the named gem to the [`Gemfile(5)`][Gemfile(5)] and run `bundle install`.
+`bundle install` can be avoided by using the flag `--skip-install`.
+
+## OPTIONS
+
+* `--version=VERSION`, `-v=VERSION`:
+ Specify version requirements(s) for the added gem.
+
+* `--group=GROUP`, `-g=GROUP`:
+ Specify the group(s) for the added gem. Multiple groups should be separated by commas.
+
+* `--source=SOURCE`, `-s=SOURCE`:
+ Specify the source for the added gem.
+
+* `--require=REQUIRE`, `-r=REQUIRE`:
+ Adds require path to gem. Provide false, or a path as a string.
+
+* `--path=PATH`:
+ Specify the file system path for the added gem.
+
+* `--git=GIT`:
+ Specify the git source for the added gem.
+
+* `--github=GITHUB`:
+ Specify the github source for the added gem.
+
+* `--branch=BRANCH`:
+ Specify the git branch for the added gem.
+
+* `--ref=REF`:
+ Specify the git ref for the added gem.
+
+* `--glob=GLOB`:
+ Specify the location of a dependency's .gemspec, expanded within Ruby (single quotes recommended).
+
+* `--quiet`:
+ Do not print progress information to the standard output.
+
+* `--skip-install`:
+ Adds the gem to the Gemfile but does not install it.
+
+* `--optimistic`:
+ Ignored (now default behavior)
+
+* `--pessimistic`:
+ Adds pessimistic declaration of version.
+
+* `--strict`:
+ Adds strict declaration of version.
+
+* `--cooldown=<number>`:
+ Only consider gem versions published at least <number> days ago when
+ resolving. Pass `0` to disable cooldown for this run. See `cooldown`
+ in bundle-config(1) for precedence rules.
+
+## EXAMPLES
+
+1. You can add the `rails` gem to the Gemfile without any version restriction.
+ The source of the gem will be the global source.
+
+ `bundle add rails`
+
+2. You can add the `rails` gem with version greater than 1.1 (not including 1.1) and less than 3.0.
+
+ `bundle add rails --version "> 1.1, < 3.0"`
+
+3. You can use the `https://gems.example.com` custom source and assign the gem
+ to a group.
+
+ `bundle add rails --version ">= 5.0.0" --source "https://gems.example.com" --group "development"`
+
+4. The following adds the `gem` entry to the Gemfile without installing the
+ gem. You can install gems later via `bundle install`.
+
+ `bundle add rails --skip-install`
+
+5. You can assign the gem to more than one group.
+
+ `bundle add rails --group "development, test"`
+
+## SEE ALSO
+
+[Gemfile(5)](https://bundler.io/man/gemfile.5.html),
+[bundle-remove(1)](bundle-remove.1.html)
diff --git a/lib/bundler/man/bundle-binstubs.1 b/lib/bundler/man/bundle-binstubs.1
new file mode 100644
index 0000000000..246daeae53
--- /dev/null
+++ b/lib/bundler/man/bundle-binstubs.1
@@ -0,0 +1,30 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-BINSTUBS" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-binstubs\fR \- Install the binstubs of the listed gems
+.SH "SYNOPSIS"
+\fBbundle binstubs\fR \fIGEM_NAME\fR [\-\-force] [\-\-standalone] [\-\-all\-platforms]
+.SH "DESCRIPTION"
+Binstubs are scripts that wrap around executables\. Bundler creates a small Ruby file (a binstub) that loads Bundler, runs the command, and puts it into \fBbin/\fR\. Binstubs are a shortcut\-or alternative\- to always using \fBbundle exec\fR\. This gives you a file that can be run directly, and one that will always run the correct gem version used by the application\.
+.P
+For example, if you run \fBbundle binstubs rspec\-core\fR, Bundler will create the file \fBbin/rspec\fR\. That file will contain enough code to load Bundler, tell it to load the bundled gems, and then run rspec\.
+.P
+This command generates binstubs for executables in \fBGEM_NAME\fR\. Binstubs are put into \fBbin\fR, or the directory specified by \fBbin\fR setting if it has been configured\. Calling binstubs with [GEM [GEM]] will create binstubs for all given gems\.
+.SH "OPTIONS"
+.TP
+\fB\-\-force\fR
+Overwrite existing binstubs if they exist\.
+.TP
+\fB\-\-standalone\fR
+Makes binstubs that can work without depending on Rubygems or Bundler at runtime\.
+.TP
+\fB\-\-shebang=SHEBANG\fR
+Specify a different shebang executable name than the default (default 'ruby')
+.TP
+\fB\-\-all\fR
+Create binstubs for all gems in the bundle\.
+.TP
+\fB\-\-all\-platforms\fR
+Install binstubs for all platforms\.
+
diff --git a/lib/bundler/man/bundle-binstubs.1.ronn b/lib/bundler/man/bundle-binstubs.1.ronn
new file mode 100644
index 0000000000..cbe5983f4d
--- /dev/null
+++ b/lib/bundler/man/bundle-binstubs.1.ronn
@@ -0,0 +1,42 @@
+bundle-binstubs(1) -- Install the binstubs of the listed gems
+=============================================================
+
+## SYNOPSIS
+
+`bundle binstubs` <GEM_NAME> [--force] [--standalone] [--all-platforms]
+
+## DESCRIPTION
+
+Binstubs are scripts that wrap around executables. Bundler creates a
+small Ruby file (a binstub) that loads Bundler, runs the command,
+and puts it into `bin/`. Binstubs are a shortcut-or alternative-
+to always using `bundle exec`. This gives you a file that can be run
+directly, and one that will always run the correct gem version
+used by the application.
+
+For example, if you run `bundle binstubs rspec-core`, Bundler will create
+the file `bin/rspec`. That file will contain enough code to load Bundler,
+tell it to load the bundled gems, and then run rspec.
+
+This command generates binstubs for executables in `GEM_NAME`.
+Binstubs are put into `bin`, or the directory specified by `bin` setting if it
+has been configured. Calling binstubs with [GEM [GEM]] will create binstubs for
+all given gems.
+
+## OPTIONS
+
+* `--force`:
+ Overwrite existing binstubs if they exist.
+
+* `--standalone`:
+ Makes binstubs that can work without depending on Rubygems or Bundler at
+ runtime.
+
+* `--shebang=SHEBANG`:
+ Specify a different shebang executable name than the default (default 'ruby')
+
+* `--all`:
+ Create binstubs for all gems in the bundle.
+
+* `--all-platforms`:
+ Install binstubs for all platforms.
diff --git a/lib/bundler/man/bundle-cache.1 b/lib/bundler/man/bundle-cache.1
new file mode 100644
index 0000000000..38ea047961
--- /dev/null
+++ b/lib/bundler/man/bundle-cache.1
@@ -0,0 +1,56 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-CACHE" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-cache\fR \- Package your needed \fB\.gem\fR files into your application
+.SH "SYNOPSIS"
+\fBbundle cache\fR [\fIOPTIONS\fR]
+.P
+alias: \fBpackage\fR, \fBpack\fR
+.SH "DESCRIPTION"
+Copy all of the \fB\.gem\fR files needed to run the application into the \fBvendor/cache\fR directory\. In the future, when running \fBbundle install(1)\fR \fIbundle\-install\.1\.html\fR, use the gems in the cache in preference to the ones on \fBrubygems\.org\fR\.
+.SH "OPTIONS"
+.TP
+\fB\-\-all\-platforms\fR
+Include gems for all platforms present in the lockfile, not only the current one\.
+.TP
+\fB\-\-cache\-path=CACHE\-PATH\fR
+Specify a different cache path than the default (vendor/cache)\.
+.TP
+\fB\-\-gemfile=GEMFILE\fR
+Use the specified gemfile instead of Gemfile\.
+.TP
+\fB\-\-no\-install\fR
+Don't install the gems, only update the cache\.
+.TP
+\fB\-\-quiet\fR
+Only output warnings and errors\.
+.SH "GIT AND PATH GEMS"
+The \fBbundle cache\fR command can also package \fB:git\fR and \fB:path\fR dependencies besides \.gem files\. This can be disabled setting \fBcache_all\fR to false\.
+.SH "SUPPORT FOR MULTIPLE PLATFORMS"
+When using gems that have different packages for different platforms, Bundler supports caching of gems for other platforms where the Gemfile has been resolved (i\.e\. present in the lockfile) in \fBvendor/cache\fR\. This needs to be enabled via the \fB\-\-all\-platforms\fR option\. This setting will be remembered in your local bundler configuration\.
+.SH "REMOTE FETCHING"
+By default, if you run \fBbundle install(1)\fR \fIbundle\-install\.1\.html\fR after running bundle cache(1) \fIbundle\-cache\.1\.html\fR, bundler will still connect to \fBrubygems\.org\fR to check whether a platform\-specific gem exists for any of the gems in \fBvendor/cache\fR\.
+.P
+For instance, consider this Gemfile(5):
+.IP "" 4
+.nf
+source "https://rubygems\.org"
+
+gem "nokogiri"
+.fi
+.IP "" 0
+.P
+If you run \fBbundle cache\fR under C Ruby, bundler will retrieve the version of \fBnokogiri\fR for the \fB"ruby"\fR platform\. If you deploy to JRuby and run \fBbundle install\fR, bundler is forced to check to see whether a \fB"java"\fR platformed \fBnokogiri\fR exists\.
+.P
+Even though the \fBnokogiri\fR gem for the Ruby platform is \fItechnically\fR acceptable on JRuby, it has a C extension that does not run on JRuby\. As a result, bundler will, by default, still connect to \fBrubygems\.org\fR to check whether it has a version of one of your gems more specific to your platform\.
+.P
+This problem is also not limited to the \fB"java"\fR platform\. A similar (common) problem can happen when developing on Windows and deploying to Linux, or even when developing on OSX and deploying to Linux\.
+.P
+If you know for sure that the gems packaged in \fBvendor/cache\fR are appropriate for the platform you are on, you can run \fBbundle install \-\-local\fR to skip checking for more appropriate gems, and use the ones in \fBvendor/cache\fR\.
+.P
+One way to be sure that you have the right platformed versions of all your gems is to run \fBbundle cache\fR on an identical machine and check in the gems\. For instance, you can run \fBbundle cache\fR on an identical staging box during your staging process, and check in the \fBvendor/cache\fR before deploying to production\.
+.P
+By default, bundle cache(1) \fIbundle\-cache\.1\.html\fR fetches and also installs the gems to the default location\. To package the dependencies to \fBvendor/cache\fR without installing them to the local install location, you can run \fBbundle cache \-\-no\-install\fR\.
+.SH "HISTORY"
+In Bundler 2\.1, \fBcache\fR took in the functionalities of \fBpackage\fR and now \fBpackage\fR and \fBpack\fR are aliases of \fBcache\fR\.
diff --git a/lib/bundler/man/bundle-cache.1.ronn b/lib/bundler/man/bundle-cache.1.ronn
new file mode 100644
index 0000000000..51846c96b4
--- /dev/null
+++ b/lib/bundler/man/bundle-cache.1.ronn
@@ -0,0 +1,95 @@
+bundle-cache(1) -- Package your needed `.gem` files into your application
+=========================================================================
+
+## SYNOPSIS
+
+`bundle cache` [*OPTIONS*]
+
+alias: `package`, `pack`
+
+## DESCRIPTION
+
+Copy all of the `.gem` files needed to run the application into the
+`vendor/cache` directory. In the future, when running [`bundle install(1)`](bundle-install.1.html),
+use the gems in the cache in preference to the ones on `rubygems.org`.
+
+## OPTIONS
+
+* `--all-platforms`:
+ Include gems for all platforms present in the lockfile, not only the current one.
+
+* `--cache-path=CACHE-PATH`:
+ Specify a different cache path than the default (vendor/cache).
+
+* `--gemfile=GEMFILE`:
+ Use the specified gemfile instead of Gemfile.
+
+* `--no-install`:
+ Don't install the gems, only update the cache.
+
+* `--quiet`:
+ Only output warnings and errors.
+
+## GIT AND PATH GEMS
+
+The `bundle cache` command can also package `:git` and `:path` dependencies
+besides .gem files. This can be disabled setting `cache_all` to false.
+
+## SUPPORT FOR MULTIPLE PLATFORMS
+
+When using gems that have different packages for different platforms, Bundler
+supports caching of gems for other platforms where the Gemfile has been resolved
+(i.e. present in the lockfile) in `vendor/cache`. This needs to be enabled via
+the `--all-platforms` option. This setting will be remembered in your local
+bundler configuration.
+
+## REMOTE FETCHING
+
+By default, if you run [`bundle install(1)`](bundle-install.1.html) after running
+[bundle cache(1)](bundle-cache.1.html), bundler will still connect to `rubygems.org`
+to check whether a platform-specific gem exists for any of the gems
+in `vendor/cache`.
+
+For instance, consider this Gemfile(5):
+
+ source "https://rubygems.org"
+
+ gem "nokogiri"
+
+If you run `bundle cache` under C Ruby, bundler will retrieve
+the version of `nokogiri` for the `"ruby"` platform. If you deploy
+to JRuby and run `bundle install`, bundler is forced to check to
+see whether a `"java"` platformed `nokogiri` exists.
+
+Even though the `nokogiri` gem for the Ruby platform is
+_technically_ acceptable on JRuby, it has a C extension
+that does not run on JRuby. As a result, bundler will, by default,
+still connect to `rubygems.org` to check whether it has a version
+of one of your gems more specific to your platform.
+
+This problem is also not limited to the `"java"` platform.
+A similar (common) problem can happen when developing on Windows
+and deploying to Linux, or even when developing on OSX and
+deploying to Linux.
+
+If you know for sure that the gems packaged in `vendor/cache`
+are appropriate for the platform you are on, you can run
+`bundle install --local` to skip checking for more appropriate
+gems, and use the ones in `vendor/cache`.
+
+One way to be sure that you have the right platformed versions
+of all your gems is to run `bundle cache` on an identical
+machine and check in the gems. For instance, you can run
+`bundle cache` on an identical staging box during your
+staging process, and check in the `vendor/cache` before
+deploying to production.
+
+By default, [bundle cache(1)](bundle-cache.1.html) fetches and also
+installs the gems to the default location. To package the
+dependencies to `vendor/cache` without installing them to the
+local install location, you can run `bundle cache --no-install`.
+
+## HISTORY
+
+In Bundler 2.1, `cache` took in the functionalities of `package` and now
+`package` and `pack` are aliases of `cache`.
diff --git a/lib/bundler/man/bundle-check.1 b/lib/bundler/man/bundle-check.1
new file mode 100644
index 0000000000..6cd474d90a
--- /dev/null
+++ b/lib/bundler/man/bundle-check.1
@@ -0,0 +1,21 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-CHECK" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-check\fR \- Verifies if dependencies are satisfied by installed gems
+.SH "SYNOPSIS"
+\fBbundle check\fR [\-\-dry\-run] [\-\-gemfile=FILE]
+.SH "DESCRIPTION"
+\fBcheck\fR searches the local machine for each of the gems requested in the Gemfile\. If all gems are found, Bundler prints a success message and exits with a status of 0\.
+.P
+If not, the first missing gem is listed and Bundler exits status 1\.
+.P
+If the lockfile needs to be updated then it will be resolved using the gems installed on the local machine, if they satisfy the requirements\.
+.SH "OPTIONS"
+.TP
+\fB\-\-dry\-run\fR
+Locks the [\fBGemfile(5)\fR][Gemfile(5)] before running the command\.
+.TP
+\fB\-\-gemfile=GEMFILE\fR
+Use the specified gemfile instead of the [\fBGemfile(5)\fR][Gemfile(5)]\.
+
diff --git a/lib/bundler/man/bundle-check.1.ronn b/lib/bundler/man/bundle-check.1.ronn
new file mode 100644
index 0000000000..92589159c9
--- /dev/null
+++ b/lib/bundler/man/bundle-check.1.ronn
@@ -0,0 +1,26 @@
+bundle-check(1) -- Verifies if dependencies are satisfied by installed gems
+===========================================================================
+
+## SYNOPSIS
+
+`bundle check` [--dry-run]
+ [--gemfile=FILE]
+
+## DESCRIPTION
+
+`check` searches the local machine for each of the gems requested in the
+Gemfile. If all gems are found, Bundler prints a success message and exits with
+a status of 0.
+
+If not, the first missing gem is listed and Bundler exits status 1.
+
+If the lockfile needs to be updated then it will be resolved using the gems
+installed on the local machine, if they satisfy the requirements.
+
+## OPTIONS
+
+* `--dry-run`:
+ Locks the [`Gemfile(5)`][Gemfile(5)] before running the command.
+
+* `--gemfile=GEMFILE`:
+ Use the specified gemfile instead of the [`Gemfile(5)`][Gemfile(5)].
diff --git a/lib/bundler/man/bundle-clean.1 b/lib/bundler/man/bundle-clean.1
new file mode 100644
index 0000000000..eb90636c17
--- /dev/null
+++ b/lib/bundler/man/bundle-clean.1
@@ -0,0 +1,17 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-CLEAN" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-clean\fR \- Cleans up unused gems in your bundler directory
+.SH "SYNOPSIS"
+\fBbundle clean\fR [\-\-dry\-run] [\-\-force]
+.SH "DESCRIPTION"
+This command will remove all unused gems in your bundler directory\. This is useful when you have made many changes to your gem dependencies\.
+.SH "OPTIONS"
+.TP
+\fB\-\-dry\-run\fR
+Print the changes, but do not clean the unused gems\.
+.TP
+\fB\-\-force\fR
+Forces cleaning up unused gems even if Bundler is configured to use globally installed gems\. As a consequence, removes all system gems except for the ones in the current application\.
+
diff --git a/lib/bundler/man/bundle-clean.1.ronn b/lib/bundler/man/bundle-clean.1.ronn
new file mode 100644
index 0000000000..dae27c21ee
--- /dev/null
+++ b/lib/bundler/man/bundle-clean.1.ronn
@@ -0,0 +1,18 @@
+bundle-clean(1) -- Cleans up unused gems in your bundler directory
+==================================================================
+
+## SYNOPSIS
+
+`bundle clean` [--dry-run] [--force]
+
+## DESCRIPTION
+
+This command will remove all unused gems in your bundler directory. This is
+useful when you have made many changes to your gem dependencies.
+
+## OPTIONS
+
+* `--dry-run`:
+ Print the changes, but do not clean the unused gems.
+* `--force`:
+ Forces cleaning up unused gems even if Bundler is configured to use globally installed gems. As a consequence, removes all system gems except for the ones in the current application.
diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1
new file mode 100644
index 0000000000..c055c8a415
--- /dev/null
+++ b/lib/bundler/man/bundle-config.1
@@ -0,0 +1,343 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-CONFIG" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-config\fR \- Set bundler configuration options
+.SH "SYNOPSIS"
+\fBbundle config\fR [list]
+.br
+\fBbundle config\fR [get [\-\-local|\-\-global]] NAME
+.br
+\fBbundle config\fR [set [\-\-local|\-\-global]] NAME VALUE
+.br
+\fBbundle config\fR unset [\-\-local|\-\-global] NAME
+.SH "DESCRIPTION"
+This command allows you to interact with Bundler's configuration system\.
+.P
+Bundler loads configuration settings in this order:
+.IP "1." 4
+Local config (\fB<project_root>/\.bundle/config\fR or \fB$BUNDLE_APP_CONFIG/config\fR)
+.IP "2." 4
+Environmental variables (\fBENV\fR)
+.IP "3." 4
+Global config (\fB~/\.bundle/config\fR)
+.IP "4." 4
+Bundler default config
+.IP "" 0
+.P
+Executing \fBbundle\fR with the \fBBUNDLE_IGNORE_CONFIG\fR environment variable set will cause it to ignore all configuration\.
+.SH "SUB\-COMMANDS"
+.SS "list (default command)"
+Executing \fBbundle config list\fR will print a list of all bundler configuration for the current bundle, and where that configuration was set\.
+.SS "get"
+Executing \fBbundle config get <name>\fR will print the value of that configuration setting, and all locations where it was set\.
+.P
+\fBOPTIONS\fR
+.TP
+\fB\-\-local\fR
+Get configuration from configuration file for the local application, namely, \fB<project_root>/\.bundle/config\fR, or \fB$BUNDLE_APP_CONFIG/config\fR if \fBBUNDLE_APP_CONFIG\fR is set\.
+.TP
+\fB\-\-global\fR
+Get configuration from configuration file global to all bundles executed as the current user, namely, from \fB~/\.bundle/config\fR\.
+.SS "set"
+Executing \fBbundle config set <name> <value>\fR defaults to setting \fBlocal\fR configuration if executing from within a local application, otherwise it will set \fBglobal\fR configuration\.
+.P
+\fBOPTIONS\fR
+.TP
+\fB\-\-local\fR
+Executing \fBbundle config set \-\-local <name> <value>\fR will set that configuration in the directory for the local application\. The configuration will be stored in \fB<project_root>/\.bundle/config\fR\. If \fBBUNDLE_APP_CONFIG\fR is set, the configuration will be stored in \fB$BUNDLE_APP_CONFIG/config\fR\.
+.TP
+\fB\-\-global\fR
+Executing \fBbundle config set \-\-global <name> <value>\fR will set that configuration to the value specified for all bundles executed as the current user\. The configuration will be stored in \fB~/\.bundle/config\fR\. If \fIname\fR already is set, \fIname\fR will be overridden and user will be warned\.
+.SS "unset"
+Executing \fBbundle config unset <name>\fR will delete the configuration in both local and global sources\.
+.P
+\fBOPTIONS\fR
+.TP
+\fB\-\-local\fR
+Executing \fBbundle config unset \-\-local <name>\fR will delete the configuration only from the local application\.
+.TP
+\fB\-\-global\fR
+Executing \fBbundle config unset \-\-global <name>\fR will delete the configuration only from the user configuration\.
+.SH "CONFIGURATION KEYS"
+Configuration keys in bundler have two forms: the canonical form and the environment variable form\.
+.P
+For instance, passing the \fB\-\-without\fR flag to bundle install(1) \fIbundle\-install\.1\.html\fR prevents Bundler from installing certain groups specified in the Gemfile(5)\. Bundler persists this value in \fBapp/\.bundle/config\fR so that calls to \fBBundler\.setup\fR do not try to find gems from the \fBGemfile\fR that you didn't install\. Additionally, subsequent calls to bundle install(1) \fIbundle\-install\.1\.html\fR remember this setting and skip those groups\.
+.P
+The canonical form of this configuration is \fB"without"\fR\. To convert the canonical form to the environment variable form, capitalize it, and prepend \fBBUNDLE_\fR\. The environment variable form of \fB"without"\fR is \fBBUNDLE_WITHOUT\fR\.
+.P
+Any periods in the configuration keys must be replaced with two underscores when setting it via environment variables\. The configuration key \fBlocal\.rack\fR becomes the environment variable \fBBUNDLE_LOCAL__RACK\fR\.
+.SH "LIST OF AVAILABLE KEYS"
+The following is a list of all configuration keys and their purpose\. You can learn more about their operation in bundle install(1) \fIbundle\-install\.1\.html\fR\.
+.IP "\(bu" 4
+\fBapi_request_size\fR (\fBBUNDLE_API_REQUEST_SIZE\fR): Configure how many dependencies to fetch when resolving the specifications\. This configuration is only used when fetching specifications from RubyGems servers that didn't implement the Compact Index API\. Defaults to 100\.
+.IP "\(bu" 4
+\fBauto_install\fR (\fBBUNDLE_AUTO_INSTALL\fR): Automatically run \fBbundle install\fR when gems are missing\.
+.IP "\(bu" 4
+\fBbin\fR (\fBBUNDLE_BIN\fR): If configured, \fBbundle binstubs\fR will install executables from gems in the bundle to the specified directory\. Otherwise it will create them in a \fBbin\fR directory relative to the Gemfile directory\. These executables run in Bundler's context\. If used, you might add this directory to your environment's \fBPATH\fR variable\. For instance, if the \fBrails\fR gem comes with a \fBrails\fR executable, \fBbundle binstubs\fR will create a \fBbin/rails\fR executable that ensures that all referred dependencies will be resolved using the bundled gems\.
+.IP "\(bu" 4
+\fBcache_all\fR (\fBBUNDLE_CACHE_ALL\fR): Cache all gems, including path and git gems\. This needs to be explicitly before bundler 4, but will be the default on bundler 4\.
+.IP "\(bu" 4
+\fBcache_all_platforms\fR (\fBBUNDLE_CACHE_ALL_PLATFORMS\fR): Cache gems for all platforms\.
+.IP "\(bu" 4
+\fBcache_path\fR (\fBBUNDLE_CACHE_PATH\fR): The directory that bundler will place cached gems in when running \fBbundle package\fR, and that bundler will look in when installing gems\. Defaults to \fBvendor/cache\fR\.
+.IP "\(bu" 4
+\fBclean\fR (\fBBUNDLE_CLEAN\fR): Whether Bundler should run \fBbundle clean\fR automatically after \fBbundle install\fR\. Defaults to \fBtrue\fR in Bundler 4, as long as \fBpath\fR is not explicitly configured\.
+.IP "\(bu" 4
+\fBconsole\fR (\fBBUNDLE_CONSOLE\fR): The console that \fBbundle console\fR starts\. Defaults to \fBirb\fR\.
+.IP "\(bu" 4
+\fBcooldown\fR (\fBBUNDLE_COOLDOWN\fR): Number of days a published gem version must age before bundler will resolve to it\. Defaults to unset (no cooldown)\. Pass \fB0\fR to disable cooldown for an individual run\.
+.IP
+The effective cooldown for any given gem is resolved from three layers, highest precedence first:
+.IP "1." 4
+CLI flag \fB\-\-cooldown N\fR on \fBinstall\fR, \fBupdate\fR, \fBadd\fR, and \fBoutdated\fR\.
+.IP "2." 4
+This setting (\fBbundle config set cooldown N\fR or \fBBUNDLE_COOLDOWN=N\fR)\.
+.IP "3." 4
+The per\-source \fBcooldown:\fR keyword in the Gemfile, such as \fBsource "https://rubygems\.org", cooldown: 7\fR\.
+.IP "" 0
+.IP
+The CLI flag and this setting apply uniformly to every source, including ones declared with their own \fBcooldown:\fR value\. To keep a private registry permanently exempt while still cooling down public gems, declare \fBsource "https://internal", cooldown: 0\fR in the Gemfile; remember that \fB\-\-cooldown N\fR on the command line will still override it for that single run\.
+.IP
+Cooldown filtering depends on the gem server providing a per\-version \fBcreated_at\fR timestamp in the v2 compact\-index format\. Versions without that metadata \- older gem servers, historical entries that predate the v2 cutover on \fBrubygems\.org\fR, or private registries that still emit the v1 format \- are treated as outside the cooldown window and remain resolvable\. If you rely on cooldown for supply\-chain protection, confirm that the gem server emits \fBcreated_at\fR in its \fB/info/<gem>\fR responses\.
+.IP "\(bu" 4
+\fBdefault_cli_command\fR (\fBBUNDLE_DEFAULT_CLI_COMMAND\fR): The command that running \fBbundle\fR without arguments should run\. Defaults to \fBcli_help\fR since Bundler 4, but can also be \fBinstall\fR which was the previous default\.
+.IP "\(bu" 4
+\fBdeployment\fR (\fBBUNDLE_DEPLOYMENT\fR): Equivalent to setting \fBfrozen\fR to \fBtrue\fR and \fBpath\fR to \fBvendor/bundle\fR\.
+.IP "\(bu" 4
+\fBdisable_checksum_validation\fR (\fBBUNDLE_DISABLE_CHECKSUM_VALIDATION\fR): Allow installing gems even if they do not match the checksum provided by RubyGems\.
+.IP "\(bu" 4
+\fBdisable_exec_load\fR (\fBBUNDLE_DISABLE_EXEC_LOAD\fR): Stop Bundler from using \fBload\fR to launch an executable in\-process in \fBbundle exec\fR\.
+.IP "\(bu" 4
+\fBdisable_local_branch_check\fR (\fBBUNDLE_DISABLE_LOCAL_BRANCH_CHECK\fR): Allow Bundler to use a local git override without a branch specified in the Gemfile\.
+.IP "\(bu" 4
+\fBdisable_local_revision_check\fR (\fBBUNDLE_DISABLE_LOCAL_REVISION_CHECK\fR): Allow Bundler to use a local git override without checking if the revision present in the lockfile is present in the repository\.
+.IP "\(bu" 4
+\fBdisable_shared_gems\fR (\fBBUNDLE_DISABLE_SHARED_GEMS\fR): Stop Bundler from accessing gems installed to RubyGems' normal location\.
+.IP "\(bu" 4
+\fBdisable_version_check\fR (\fBBUNDLE_DISABLE_VERSION_CHECK\fR): Stop Bundler from checking if a newer Bundler version is available on rubygems\.org\.
+.IP "\(bu" 4
+\fBforce_ruby_platform\fR (\fBBUNDLE_FORCE_RUBY_PLATFORM\fR): Ignore the current machine's platform and install only \fBruby\fR platform gems\. As a result, gems with native extensions will be compiled from source\.
+.IP "\(bu" 4
+\fBfrozen\fR (\fBBUNDLE_FROZEN\fR): Disallow any automatic changes to \fBGemfile\.lock\fR\. Bundler commands will be blocked unless the lockfile can be installed exactly as written\. Usually this will happen when changing the \fBGemfile\fR manually and forgetting to update the lockfile through \fBbundle lock\fR or \fBbundle install\fR\.
+.IP "\(bu" 4
+\fBgem\.github_username\fR (\fBBUNDLE_GEM__GITHUB_USERNAME\fR): Sets a GitHub username or organization to be used in the \fBREADME\fR and \fB\.gemspec\fR files when you create a new gem via \fBbundle gem\fR command\. It can be overridden by passing an explicit \fB\-\-github\-username\fR flag to \fBbundle gem\fR\.
+.IP "\(bu" 4
+\fBgem\.push_key\fR (\fBBUNDLE_GEM__PUSH_KEY\fR): Sets the \fB\-\-key\fR parameter for \fBgem push\fR when using the \fBrake release\fR command with a private gemstash server\.
+.IP "\(bu" 4
+\fBgemfile\fR (\fBBUNDLE_GEMFILE\fR): The name of the file that bundler should use as the \fBGemfile\fR\. This location of this file also sets the root of the project, which is used to resolve relative paths in the \fBGemfile\fR, among other things\. By default, bundler will search up from the current working directory until it finds a \fBGemfile\fR\.
+.IP "\(bu" 4
+\fBglobal_gem_cache\fR (\fBBUNDLE_GLOBAL_GEM_CACHE\fR): Whether Bundler should cache all gems and compiled extensions globally, rather than locally to the configured installation path\.
+.IP "\(bu" 4
+\fBignore_funding_requests\fR (\fBBUNDLE_IGNORE_FUNDING_REQUESTS\fR): When set, no funding requests will be printed\.
+.IP "\(bu" 4
+\fBignore_messages\fR (\fBBUNDLE_IGNORE_MESSAGES\fR): When set, no post install messages will be printed\. To silence a single gem, use dot notation like \fBignore_messages\.httparty true\fR\.
+.IP "\(bu" 4
+\fBinit_gems_rb\fR (\fBBUNDLE_INIT_GEMS_RB\fR): Generate a \fBgems\.rb\fR instead of a \fBGemfile\fR when running \fBbundle init\fR\.
+.IP "\(bu" 4
+\fBjobs\fR (\fBBUNDLE_JOBS\fR): The number of gems Bundler can download and install in parallel\. Defaults to the number of available processors\.
+.IP "\(bu" 4
+\fBlockfile\fR (\fBBUNDLE_LOCKFILE\fR): The path to the lockfile that bundler should use\. By default, Bundler adds \fB\.lock\fR to the end of the \fBgemfile\fR entry\. Can be set to \fBfalse\fR in the Gemfile to disable lockfile creation entirely (see gemfile(5))\.
+.IP "\(bu" 4
+\fBlockfile_checksums\fR (\fBBUNDLE_LOCKFILE_CHECKSUMS\fR): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. Defaults to true\.
+.IP "\(bu" 4
+\fBno_build_extension\fR (\fBBUNDLE_NO_BUILD_EXTENSION\fR): Whether Bundler should skip building native extensions during installation\. When set, gems are installed without compiling their C extensions\. To build extensions later, unset this setting and run \fBbundle pristine <gem>\fR\.
+.IP "\(bu" 4
+\fBno_install\fR (\fBBUNDLE_NO_INSTALL\fR): Whether \fBbundle package\fR should skip installing gems\.
+.IP "\(bu" 4
+\fBno_install_plugin\fR (\fBBUNDLE_NO_INSTALL_PLUGIN\fR): Whether Bundler should skip installing RubyGems plugins during installation\. When set, plugin files are not written to the plugins directory\. To install plugins later, unset this setting and run \fBbundle pristine <gem>\fR\.
+.IP "\(bu" 4
+\fBno_prune\fR (\fBBUNDLE_NO_PRUNE\fR): Whether Bundler should leave outdated gems unpruned when caching\.
+.IP "\(bu" 4
+\fBonly\fR (\fBBUNDLE_ONLY\fR): A space\-separated list of groups to install only gems of the specified groups\. Please check carefully if you want to install also gems without a group, because they get put inside \fBdefault\fR group\. For example \fBonly test:default\fR will install all gems specified in test group and without one\.
+.IP "\(bu" 4
+\fBpath\fR (\fBBUNDLE_PATH\fR): The location on disk where all gems in your bundle will be located regardless of \fB$GEM_HOME\fR or \fB$GEM_PATH\fR values\. Bundle gems not found in this location will be installed by \fBbundle install\fR\. When not set, Bundler install by default to a \fB\.bundle\fR directory relative to repository root in Bundler 4, and to the default system path (\fBGem\.dir\fR) before Bundler 4\. That means that before Bundler 4, Bundler shares this location with Rubygems, and \fBgem install \|\.\|\.\|\.\fR will have gems installed in the same location and therefore, gems installed without \fBpath\fR set will show up by calling \fBgem list\fR\. This will not be the case in Bundler 4\.
+.IP "\(bu" 4
+\fBpath\.system\fR (\fBBUNDLE_PATH__SYSTEM\fR): Whether Bundler will install gems into the default system path (\fBGem\.dir\fR)\.
+.IP "\(bu" 4
+\fBplugins\fR (\fBBUNDLE_PLUGINS\fR): Enable Bundler's experimental plugin system\.
+.IP "\(bu" 4
+\fBprefer_patch\fR (\fBBUNDLE_PREFER_PATCH\fR): Prefer updating only to next patch version during updates\. Makes \fBbundle update\fR calls equivalent to \fBbundler update \-\-patch\fR\.
+.IP "\(bu" 4
+\fBredirect\fR (\fBBUNDLE_REDIRECT\fR): The number of redirects allowed for network requests\. Defaults to \fB5\fR\.
+.IP "\(bu" 4
+\fBretry\fR (\fBBUNDLE_RETRY\fR): The number of times to retry failed network requests\. Defaults to \fB3\fR\.
+.IP "\(bu" 4
+\fBshebang\fR (\fBBUNDLE_SHEBANG\fR): The program name that should be invoked for generated binstubs\. Defaults to the ruby install name used to generate the binstub\.
+.IP "\(bu" 4
+\fBsilence_deprecations\fR (\fBBUNDLE_SILENCE_DEPRECATIONS\fR): Whether Bundler should silence deprecation warnings for behavior that will be changed in the next major version\.
+.IP "\(bu" 4
+\fBsilence_root_warning\fR (\fBBUNDLE_SILENCE_ROOT_WARNING\fR): Silence the warning Bundler prints when installing gems as root\.
+.IP "\(bu" 4
+\fBsimulate_version\fR (\fBBUNDLE_SIMULATE_VERSION\fR): The virtual version Bundler should use for activating feature flags\. Can be used to simulate all the new functionality that will be enabled in a future major version\.
+.IP "\(bu" 4
+\fBssl_ca_cert\fR (\fBBUNDLE_SSL_CA_CERT\fR): Path to a designated CA certificate file or folder containing multiple certificates for trusted CAs in PEM format\.
+.IP "\(bu" 4
+\fBssl_client_cert\fR (\fBBUNDLE_SSL_CLIENT_CERT\fR): Path to a designated file containing a X\.509 client certificate and key in PEM format\.
+.IP "\(bu" 4
+\fBssl_verify_mode\fR (\fBBUNDLE_SSL_VERIFY_MODE\fR): The SSL verification mode Bundler uses when making HTTPS requests\. Defaults to verify peer\.
+.IP "\(bu" 4
+\fBsystem_bindir\fR (\fBBUNDLE_SYSTEM_BINDIR\fR): The location where RubyGems installs binstubs\. Defaults to \fBGem\.bindir\fR\.
+.IP "\(bu" 4
+\fBtimeout\fR (\fBBUNDLE_TIMEOUT\fR): The seconds allowed before timing out for network requests\. Defaults to \fB10\fR\.
+.IP "\(bu" 4
+\fBupdate_requires_all_flag\fR (\fBBUNDLE_UPDATE_REQUIRES_ALL_FLAG\fR): Require passing \fB\-\-all\fR to \fBbundle update\fR when everything should be updated, and disallow passing no options to \fBbundle update\fR\.
+.IP "\(bu" 4
+\fBuser_agent\fR (\fBBUNDLE_USER_AGENT\fR): The custom user agent fragment Bundler includes in API requests\.
+.IP "\(bu" 4
+\fBverbose\fR (\fBBUNDLE_VERBOSE\fR): Whether Bundler should print verbose output\. Defaults to \fBfalse\fR, unless the \fB\-\-verbose\fR CLI flag is used\.
+.IP "\(bu" 4
+\fBversion\fR (\fBBUNDLE_VERSION\fR): The version of Bundler to use when running under Bundler environment\. Defaults to \fBlockfile\fR\. You can also specify \fBsystem\fR or \fBx\.y\.z\fR\. \fBlockfile\fR will use the Bundler version specified in the \fBGemfile\.lock\fR, \fBsystem\fR will use the system version of Bundler, and \fBx\.y\.z\fR will use the specified version of Bundler\.
+.IP "\(bu" 4
+\fBwith\fR (\fBBUNDLE_WITH\fR): A space\-separated or \fB:\fR\-separated list of groups whose gems bundler should install\.
+.IP "\(bu" 4
+\fBwithout\fR (\fBBUNDLE_WITHOUT\fR): A space\-separated or \fB:\fR\-separated list of groups whose gems bundler should not install\.
+.IP "" 0
+.SH "BUILD OPTIONS"
+You can use \fBbundle config\fR to give Bundler the flags to pass to the gem installer every time bundler tries to install a particular gem\.
+.P
+A very common example, the \fBmysql\fR gem, requires Snow Leopard users to pass configuration flags to \fBgem install\fR to specify where to find the \fBmysql_config\fR executable\.
+.IP "" 4
+.nf
+gem install mysql \-\- \-\-with\-mysql\-config=/usr/local/mysql/bin/mysql_config
+.fi
+.IP "" 0
+.P
+Since the specific location of that executable can change from machine to machine, you can specify these flags on a per\-machine basis\.
+.IP "" 4
+.nf
+bundle config set \-\-global build\.mysql \-\-with\-mysql\-config=/usr/local/mysql/bin/mysql_config
+.fi
+.IP "" 0
+.P
+After running this command, every time bundler needs to install the \fBmysql\fR gem, it will pass along the flags you specified\.
+.SH "LOCAL GIT REPOS"
+Bundler also allows you to work against a git repository locally instead of using the remote version\. This can be achieved by setting up a local override:
+.IP "" 4
+.nf
+bundle config set \-\-local local\.GEM_NAME /path/to/local/git/repository
+.fi
+.IP "" 0
+.P
+Important: This feature only works for gems that are specified with a git source in your Gemfile\. It does not work for gems installed from RubyGems or other sources\. The gem must be defined with \fBgit:\fR option pointing to a remote repository\.
+.P
+For example, if your Gemfile contains:
+.IP "" 4
+.nf
+gem "rack", git: "https://github\.com/rack/rack\.git", branch: "main"
+.fi
+.IP "" 0
+.P
+Then you can use a local Rack repository by running:
+.IP "" 4
+.nf
+bundle config set \-\-local local\.rack ~/Work/git/rack
+.fi
+.IP "" 0
+.P
+Now instead of checking out the remote git repository, the local override will be used\. Similar to a path source, every time the local git repository change, changes will be automatically picked up by Bundler\. This means a commit in the local git repo will update the revision in the \fBGemfile\.lock\fR to the local git repo revision\. This requires the same attention as git submodules\. Before pushing to the remote, you need to ensure the local override was pushed, otherwise you may point to a commit that only exists in your local machine\. You'll also need to CGI escape your usernames and passwords as well\.
+.P
+Bundler does many checks to ensure a developer won't work with invalid references\. Particularly, we force a developer to specify a branch in the \fBGemfile\fR in order to use this feature\. If the branch specified in the \fBGemfile\fR and the current branch in the local git repository do not match, Bundler will abort\. This ensures that a developer is always working against the correct branches, and prevents accidental locking to a different branch\.
+.P
+Finally, Bundler also ensures that the current revision in the \fBGemfile\.lock\fR exists in the local git repository\. By doing this, Bundler forces you to fetch the latest changes in the remotes\.
+.P
+If you need to temporarily use a local version of a gem that is normally installed from RubyGems (not from git), use a path source instead:
+.IP "" 4
+.nf
+gem "rack", path: "~/Work/git/rack"
+.fi
+.IP "" 0
+.SH "MIRRORS OF GEM SOURCES"
+Bundler supports overriding gem sources with mirrors\. This allows you to configure rubygems\.org as the gem source in your Gemfile while still using your mirror to fetch gems\.
+.IP "" 4
+.nf
+bundle config set \-\-global mirror\.SOURCE_URL MIRROR_URL
+.fi
+.IP "" 0
+.P
+For example, to use a mirror of https://rubygems\.org hosted at https://example\.org:
+.IP "" 4
+.nf
+bundle config set \-\-global mirror\.https://rubygems\.org https://example\.org
+.fi
+.IP "" 0
+.P
+Each mirror also provides a fallback timeout setting\. If the mirror does not respond within the fallback timeout, Bundler will try to use the original server instead of the mirror\.
+.IP "" 4
+.nf
+bundle config set \-\-global mirror\.SOURCE_URL\.fallback_timeout TIMEOUT
+.fi
+.IP "" 0
+.P
+For example, to fall back to rubygems\.org after 3 seconds:
+.IP "" 4
+.nf
+bundle config set \-\-global mirror\.https://rubygems\.org\.fallback_timeout 3
+.fi
+.IP "" 0
+.P
+The default fallback timeout is 0\.1 seconds, but the setting can currently only accept whole seconds (for example, 1, 15, or 30)\.
+.SH "CREDENTIALS FOR GEM SOURCES"
+Bundler allows you to configure credentials for any gem source, which allows you to avoid putting secrets into your Gemfile\.
+.IP "" 4
+.nf
+bundle config set \-\-global SOURCE_HOSTNAME USERNAME:PASSWORD
+.fi
+.IP "" 0
+.P
+For example, to save the credentials of user \fBclaudette\fR for the gem source at \fBgems\.longerous\.com\fR, you would run:
+.IP "" 4
+.nf
+bundle config set \-\-global gems\.longerous\.com claudette:s00pers3krit
+.fi
+.IP "" 0
+.P
+Or you can set the credentials as an environment variable like this:
+.IP "" 4
+.nf
+export BUNDLE_GEMS__LONGEROUS__COM="claudette:s00pers3krit"
+.fi
+.IP "" 0
+.P
+For gems with a git source with HTTP(S) URL you can specify credentials like so:
+.IP "" 4
+.nf
+bundle config set \-\-global https://github\.com/ruby/rubygems\.git username:password
+.fi
+.IP "" 0
+.P
+Or you can set the credentials as an environment variable like so:
+.IP "" 4
+.nf
+export BUNDLE_GITHUB__COM=username:password
+.fi
+.IP "" 0
+.P
+This is especially useful for private repositories on hosts such as GitHub, where you can use personal OAuth tokens:
+.IP "" 4
+.nf
+export BUNDLE_GITHUB__COM=abcd0123generatedtoken:x\-oauth\-basic
+.fi
+.IP "" 0
+.P
+Note that any configured credentials will be redacted by informative commands such as \fBbundle config list\fR or \fBbundle config get\fR, unless you use the \fB\-\-parseable\fR flag\. This is to avoid unintentionally leaking credentials when copy\-pasting bundler output\.
+.P
+Also note that to guarantee a sane mapping between valid environment variable names and valid host names, bundler makes the following transformations:
+.IP "\(bu" 4
+Any \fB\-\fR characters in a host name are mapped to a triple underscore (\fB___\fR) in the corresponding environment variable\.
+.IP "\(bu" 4
+Any \fB\.\fR characters in a host name are mapped to a double underscore (\fB__\fR) in the corresponding environment variable\.
+.IP "" 0
+.P
+This means that if you have a gem server named \fBmy\.gem\-host\.com\fR, you'll need to use the \fBBUNDLE_MY__GEM___HOST__COM\fR variable to configure credentials for it through ENV\.
+.SH "CONFIGURE BUNDLER DIRECTORIES"
+Bundler's home, cache and plugin directories and config file can be configured through environment variables\. The default location for Bundler's home directory is \fB~/\.bundle\fR, which all directories inherit from by default\. The following outlines the available environment variables and their default values
+.IP "" 4
+.nf
+BUNDLE_USER_HOME : $HOME/\.bundle
+BUNDLE_USER_CACHE : $BUNDLE_USER_HOME/cache
+BUNDLE_USER_CONFIG : $BUNDLE_USER_HOME/config
+BUNDLE_USER_PLUGIN : $BUNDLE_USER_HOME/plugin
+.fi
+.IP "" 0
+
diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn
new file mode 100644
index 0000000000..72f891b428
--- /dev/null
+++ b/lib/bundler/man/bundle-config.1.ronn
@@ -0,0 +1,463 @@
+bundle-config(1) -- Set bundler configuration options
+=====================================================
+
+## SYNOPSIS
+
+`bundle config` [list]<br>
+`bundle config` [get [--local|--global]] NAME<br>
+`bundle config` [set [--local|--global]] NAME VALUE<br>
+`bundle config` unset [--local|--global] NAME
+
+## DESCRIPTION
+
+This command allows you to interact with Bundler's configuration system.
+
+Bundler loads configuration settings in this order:
+
+1. Local config (`<project_root>/.bundle/config` or `$BUNDLE_APP_CONFIG/config`)
+2. Environmental variables (`ENV`)
+3. Global config (`~/.bundle/config`)
+4. Bundler default config
+
+Executing `bundle` with the `BUNDLE_IGNORE_CONFIG` environment variable set will
+cause it to ignore all configuration.
+
+## SUB-COMMANDS
+
+### list (default command)
+
+Executing `bundle config list` will print a list of all bundler
+configuration for the current bundle, and where that configuration
+was set.
+
+### get
+
+Executing `bundle config get <name>` will print the value of that configuration
+setting, and all locations where it was set.
+
+**OPTIONS**
+
+* `--local`:
+ Get configuration from configuration file for the local application, namely,
+ `<project_root>/.bundle/config`, or `$BUNDLE_APP_CONFIG/config` if
+ `BUNDLE_APP_CONFIG` is set.
+
+* `--global`:
+ Get configuration from configuration file global to all bundles executed as
+ the current user, namely, from `~/.bundle/config`.
+
+### set
+
+Executing `bundle config set <name> <value>` defaults to setting `local`
+configuration if executing from within a local application, otherwise it will
+set `global` configuration.
+
+**OPTIONS**
+
+* `--local`:
+ Executing `bundle config set --local <name> <value>` will set that configuration
+ in the directory for the local application. The configuration will be stored in
+ `<project_root>/.bundle/config`. If `BUNDLE_APP_CONFIG` is set, the configuration
+ will be stored in `$BUNDLE_APP_CONFIG/config`.
+
+* `--global`:
+ Executing `bundle config set --global <name> <value>` will set that
+ configuration to the value specified for all bundles executed as the current
+ user. The configuration will be stored in `~/.bundle/config`. If <name> already
+ is set, <name> will be overridden and user will be warned.
+
+### unset
+
+Executing `bundle config unset <name>` will delete the configuration in both
+local and global sources.
+
+**OPTIONS**
+
+* `--local`:
+ Executing `bundle config unset --local <name>` will delete the configuration
+ only from the local application.
+
+* `--global`:
+ Executing `bundle config unset --global <name>` will delete the configuration
+ only from the user configuration.
+
+## CONFIGURATION KEYS
+
+Configuration keys in bundler have two forms: the canonical form and the
+environment variable form.
+
+For instance, passing the `--without` flag to [bundle install(1)](bundle-install.1.html)
+prevents Bundler from installing certain groups specified in the Gemfile(5). Bundler
+persists this value in `app/.bundle/config` so that calls to `Bundler.setup`
+do not try to find gems from the `Gemfile` that you didn't install. Additionally,
+subsequent calls to [bundle install(1)](bundle-install.1.html) remember this setting
+and skip those groups.
+
+The canonical form of this configuration is `"without"`. To convert the canonical
+form to the environment variable form, capitalize it, and prepend `BUNDLE_`. The
+environment variable form of `"without"` is `BUNDLE_WITHOUT`.
+
+Any periods in the configuration keys must be replaced with two underscores when
+setting it via environment variables. The configuration key `local.rack` becomes
+the environment variable `BUNDLE_LOCAL__RACK`.
+
+## LIST OF AVAILABLE KEYS
+
+The following is a list of all configuration keys and their purpose. You can
+learn more about their operation in [bundle install(1)](bundle-install.1.html).
+
+* `api_request_size` (`BUNDLE_API_REQUEST_SIZE`):
+ Configure how many dependencies to fetch when resolving the specifications.
+ This configuration is only used when fetching specifications from RubyGems
+ servers that didn't implement the Compact Index API.
+ Defaults to 100.
+* `auto_install` (`BUNDLE_AUTO_INSTALL`):
+ Automatically run `bundle install` when gems are missing.
+* `bin` (`BUNDLE_BIN`):
+ If configured, `bundle binstubs` will install executables from gems in the
+ bundle to the specified directory. Otherwise it will create them in a `bin`
+ directory relative to the Gemfile directory. These executables run in
+ Bundler's context. If used, you might add this directory to your
+ environment's `PATH` variable. For instance, if the `rails` gem comes with a
+ `rails` executable, `bundle binstubs` will create a `bin/rails` executable
+ that ensures that all referred dependencies will be resolved using the
+ bundled gems.
+* `cache_all` (`BUNDLE_CACHE_ALL`):
+ Cache all gems, including path and git gems. This needs to be explicitly
+ before bundler 4, but will be the default on bundler 4.
+* `cache_all_platforms` (`BUNDLE_CACHE_ALL_PLATFORMS`):
+ Cache gems for all platforms.
+* `cache_path` (`BUNDLE_CACHE_PATH`):
+ The directory that bundler will place cached gems in when running
+ <code>bundle package</code>, and that bundler will look in when installing gems.
+ Defaults to `vendor/cache`.
+* `clean` (`BUNDLE_CLEAN`):
+ Whether Bundler should run `bundle clean` automatically after
+ `bundle install`. Defaults to `true` in Bundler 4, as long as `path` is not
+ explicitly configured.
+* `console` (`BUNDLE_CONSOLE`):
+ The console that `bundle console` starts. Defaults to `irb`.
+* `cooldown` (`BUNDLE_COOLDOWN`):
+ Number of days a published gem version must age before bundler will
+ resolve to it. Defaults to unset (no cooldown). Pass `0` to disable
+ cooldown for an individual run.
+
+ The effective cooldown for any given gem is resolved from three
+ layers, highest precedence first:
+
+ 1. CLI flag `--cooldown N` on `install`, `update`, `add`, and
+ `outdated`.
+ 2. This setting (`bundle config set cooldown N` or
+ `BUNDLE_COOLDOWN=N`).
+ 3. The per-source `cooldown:` keyword in the Gemfile, such as
+ `source "https://rubygems.org", cooldown: 7`.
+
+ The CLI flag and this setting apply uniformly to every source,
+ including ones declared with their own `cooldown:` value. To keep a
+ private registry permanently exempt while still cooling down public
+ gems, declare `source "https://internal", cooldown: 0` in the
+ Gemfile; remember that `--cooldown N` on the command line will
+ still override it for that single run.
+
+ Cooldown filtering depends on the gem server providing a per-version
+ `created_at` timestamp in the v2 compact-index format. Versions
+ without that metadata - older gem servers, historical entries that
+ predate the v2 cutover on `rubygems.org`, or private registries that
+ still emit the v1 format - are treated as outside the cooldown
+ window and remain resolvable. If you rely on cooldown for
+ supply-chain protection, confirm that the gem server emits
+ `created_at` in its `/info/<gem>` responses.
+* `default_cli_command` (`BUNDLE_DEFAULT_CLI_COMMAND`):
+ The command that running `bundle` without arguments should run. Defaults to
+ `cli_help` since Bundler 4, but can also be `install` which was the previous
+ default.
+* `deployment` (`BUNDLE_DEPLOYMENT`):
+ Equivalent to setting `frozen` to `true` and `path` to `vendor/bundle`.
+* `disable_checksum_validation` (`BUNDLE_DISABLE_CHECKSUM_VALIDATION`):
+ Allow installing gems even if they do not match the checksum provided by
+ RubyGems.
+* `disable_exec_load` (`BUNDLE_DISABLE_EXEC_LOAD`):
+ Stop Bundler from using `load` to launch an executable in-process in
+ `bundle exec`.
+* `disable_local_branch_check` (`BUNDLE_DISABLE_LOCAL_BRANCH_CHECK`):
+ Allow Bundler to use a local git override without a branch specified in the
+ Gemfile.
+* `disable_local_revision_check` (`BUNDLE_DISABLE_LOCAL_REVISION_CHECK`):
+ Allow Bundler to use a local git override without checking if the revision
+ present in the lockfile is present in the repository.
+* `disable_shared_gems` (`BUNDLE_DISABLE_SHARED_GEMS`):
+ Stop Bundler from accessing gems installed to RubyGems' normal location.
+* `disable_version_check` (`BUNDLE_DISABLE_VERSION_CHECK`):
+ Stop Bundler from checking if a newer Bundler version is available on
+ rubygems.org.
+* `force_ruby_platform` (`BUNDLE_FORCE_RUBY_PLATFORM`):
+ Ignore the current machine's platform and install only `ruby` platform gems.
+ As a result, gems with native extensions will be compiled from source.
+* `frozen` (`BUNDLE_FROZEN`):
+ Disallow any automatic changes to `Gemfile.lock`. Bundler commands will
+ be blocked unless the lockfile can be installed exactly as written.
+ Usually this will happen when changing the `Gemfile` manually and forgetting
+ to update the lockfile through `bundle lock` or `bundle install`.
+* `gem.github_username` (`BUNDLE_GEM__GITHUB_USERNAME`):
+ Sets a GitHub username or organization to be used in the `README` and `.gemspec` files
+ when you create a new gem via `bundle gem` command. It can be overridden by passing an
+ explicit `--github-username` flag to `bundle gem`.
+* `gem.push_key` (`BUNDLE_GEM__PUSH_KEY`):
+ Sets the `--key` parameter for `gem push` when using the `rake release`
+ command with a private gemstash server.
+* `gemfile` (`BUNDLE_GEMFILE`):
+ The name of the file that bundler should use as the `Gemfile`. This location
+ of this file also sets the root of the project, which is used to resolve
+ relative paths in the `Gemfile`, among other things. By default, bundler
+ will search up from the current working directory until it finds a
+ `Gemfile`.
+* `global_gem_cache` (`BUNDLE_GLOBAL_GEM_CACHE`):
+ Whether Bundler should cache all gems and compiled extensions globally,
+ rather than locally to the configured installation path.
+* `ignore_funding_requests` (`BUNDLE_IGNORE_FUNDING_REQUESTS`):
+ When set, no funding requests will be printed.
+* `ignore_messages` (`BUNDLE_IGNORE_MESSAGES`):
+ When set, no post install messages will be printed. To silence a single gem,
+ use dot notation like `ignore_messages.httparty true`.
+* `init_gems_rb` (`BUNDLE_INIT_GEMS_RB`):
+ Generate a `gems.rb` instead of a `Gemfile` when running `bundle init`.
+* `jobs` (`BUNDLE_JOBS`):
+ The number of gems Bundler can download and install in parallel.
+ Defaults to the number of available processors.
+* `lockfile` (`BUNDLE_LOCKFILE`):
+ The path to the lockfile that bundler should use. By default, Bundler adds
+ `.lock` to the end of the `gemfile` entry. Can be set to `false` in the
+ Gemfile to disable lockfile creation entirely (see gemfile(5)).
+* `lockfile_checksums` (`BUNDLE_LOCKFILE_CHECKSUMS`):
+ Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources. Defaults to true.
+* `no_build_extension` (`BUNDLE_NO_BUILD_EXTENSION`):
+ Whether Bundler should skip building native extensions during installation.
+ When set, gems are installed without compiling their C extensions.
+ To build extensions later, unset this setting and run `bundle pristine <gem>`.
+* `no_install` (`BUNDLE_NO_INSTALL`):
+ Whether `bundle package` should skip installing gems.
+* `no_install_plugin` (`BUNDLE_NO_INSTALL_PLUGIN`):
+ Whether Bundler should skip installing RubyGems plugins during installation.
+ When set, plugin files are not written to the plugins directory.
+ To install plugins later, unset this setting and run `bundle pristine <gem>`.
+* `no_prune` (`BUNDLE_NO_PRUNE`):
+ Whether Bundler should leave outdated gems unpruned when caching.
+* `only` (`BUNDLE_ONLY`):
+ A space-separated list of groups to install only gems of the specified groups.
+ Please check carefully if you want to install also gems without a group, because
+ they get put inside `default` group. For example `only test:default` will install
+ all gems specified in test group and without one.
+* `path` (`BUNDLE_PATH`):
+ The location on disk where all gems in your bundle will be located regardless
+ of `$GEM_HOME` or `$GEM_PATH` values. Bundle gems not found in this location
+ will be installed by `bundle install`. When not set, Bundler install by
+ default to a `.bundle` directory relative to repository root in Bundler 4,
+ and to the default system path (`Gem.dir`) before Bundler 4. That means that
+ before Bundler 4, Bundler shares this location with Rubygems, and `gem
+ install ...` will have gems installed in the same location and therefore,
+ gems installed without `path` set will show up by calling `gem list`. This
+ will not be the case in Bundler 4.
+* `path.system` (`BUNDLE_PATH__SYSTEM`):
+ Whether Bundler will install gems into the default system path (`Gem.dir`).
+* `plugins` (`BUNDLE_PLUGINS`):
+ Enable Bundler's experimental plugin system.
+* `prefer_patch` (`BUNDLE_PREFER_PATCH`):
+ Prefer updating only to next patch version during updates. Makes `bundle update` calls equivalent to `bundler update --patch`.
+* `redirect` (`BUNDLE_REDIRECT`):
+ The number of redirects allowed for network requests. Defaults to `5`.
+* `retry` (`BUNDLE_RETRY`):
+ The number of times to retry failed network requests. Defaults to `3`.
+* `shebang` (`BUNDLE_SHEBANG`):
+ The program name that should be invoked for generated binstubs. Defaults to
+ the ruby install name used to generate the binstub.
+* `silence_deprecations` (`BUNDLE_SILENCE_DEPRECATIONS`):
+ Whether Bundler should silence deprecation warnings for behavior that will
+ be changed in the next major version.
+* `silence_root_warning` (`BUNDLE_SILENCE_ROOT_WARNING`):
+ Silence the warning Bundler prints when installing gems as root.
+* `simulate_version` (`BUNDLE_SIMULATE_VERSION`):
+ The virtual version Bundler should use for activating feature flags. Can be
+ used to simulate all the new functionality that will be enabled in a future
+ major version.
+* `ssl_ca_cert` (`BUNDLE_SSL_CA_CERT`):
+ Path to a designated CA certificate file or folder containing multiple
+ certificates for trusted CAs in PEM format.
+* `ssl_client_cert` (`BUNDLE_SSL_CLIENT_CERT`):
+ Path to a designated file containing a X.509 client certificate
+ and key in PEM format.
+* `ssl_verify_mode` (`BUNDLE_SSL_VERIFY_MODE`):
+ The SSL verification mode Bundler uses when making HTTPS requests.
+ Defaults to verify peer.
+* `system_bindir` (`BUNDLE_SYSTEM_BINDIR`):
+ The location where RubyGems installs binstubs. Defaults to `Gem.bindir`.
+* `timeout` (`BUNDLE_TIMEOUT`):
+ The seconds allowed before timing out for network requests. Defaults to `10`.
+* `update_requires_all_flag` (`BUNDLE_UPDATE_REQUIRES_ALL_FLAG`):
+ Require passing `--all` to `bundle update` when everything should be updated,
+ and disallow passing no options to `bundle update`.
+* `user_agent` (`BUNDLE_USER_AGENT`):
+ The custom user agent fragment Bundler includes in API requests.
+* `verbose` (`BUNDLE_VERBOSE`):
+ Whether Bundler should print verbose output. Defaults to `false`, unless the
+ `--verbose` CLI flag is used.
+* `version` (`BUNDLE_VERSION`):
+ The version of Bundler to use when running under Bundler environment.
+ Defaults to `lockfile`. You can also specify `system` or `x.y.z`.
+ `lockfile` will use the Bundler version specified in the `Gemfile.lock`,
+ `system` will use the system version of Bundler, and `x.y.z` will use
+ the specified version of Bundler.
+* `with` (`BUNDLE_WITH`):
+ A space-separated or `:`-separated list of groups whose gems bundler should install.
+* `without` (`BUNDLE_WITHOUT`):
+ A space-separated or `:`-separated list of groups whose gems bundler should not install.
+
+## BUILD OPTIONS
+
+You can use `bundle config` to give Bundler the flags to pass to the gem
+installer every time bundler tries to install a particular gem.
+
+A very common example, the `mysql` gem, requires Snow Leopard users to
+pass configuration flags to `gem install` to specify where to find the
+`mysql_config` executable.
+
+ gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
+
+Since the specific location of that executable can change from machine
+to machine, you can specify these flags on a per-machine basis.
+
+ bundle config set --global build.mysql --with-mysql-config=/usr/local/mysql/bin/mysql_config
+
+After running this command, every time bundler needs to install the
+`mysql` gem, it will pass along the flags you specified.
+
+## LOCAL GIT REPOS
+
+Bundler also allows you to work against a git repository locally
+instead of using the remote version. This can be achieved by setting
+up a local override:
+
+ bundle config set --local local.GEM_NAME /path/to/local/git/repository
+
+Important: This feature only works for gems that are specified with a git
+source in your Gemfile. It does not work for gems installed from RubyGems
+or other sources. The gem must be defined with `git:` option pointing to a
+remote repository.
+
+For example, if your Gemfile contains:
+
+ gem "rack", git: "https://github.com/rack/rack.git", branch: "main"
+
+Then you can use a local Rack repository by running:
+
+ bundle config set --local local.rack ~/Work/git/rack
+
+Now instead of checking out the remote git repository, the local
+override will be used. Similar to a path source, every time the local
+git repository change, changes will be automatically picked up by
+Bundler. This means a commit in the local git repo will update the
+revision in the `Gemfile.lock` to the local git repo revision. This
+requires the same attention as git submodules. Before pushing to
+the remote, you need to ensure the local override was pushed, otherwise
+you may point to a commit that only exists in your local machine.
+You'll also need to CGI escape your usernames and passwords as well.
+
+Bundler does many checks to ensure a developer won't work with
+invalid references. Particularly, we force a developer to specify
+a branch in the `Gemfile` in order to use this feature. If the branch
+specified in the `Gemfile` and the current branch in the local git
+repository do not match, Bundler will abort. This ensures that
+a developer is always working against the correct branches, and prevents
+accidental locking to a different branch.
+
+Finally, Bundler also ensures that the current revision in the
+`Gemfile.lock` exists in the local git repository. By doing this, Bundler
+forces you to fetch the latest changes in the remotes.
+
+If you need to temporarily use a local version of a gem that is normally
+installed from RubyGems (not from git), use a path source instead:
+
+ gem "rack", path: "~/Work/git/rack"
+
+## MIRRORS OF GEM SOURCES
+
+Bundler supports overriding gem sources with mirrors. This allows you to
+configure rubygems.org as the gem source in your Gemfile while still using your
+mirror to fetch gems.
+
+ bundle config set --global mirror.SOURCE_URL MIRROR_URL
+
+For example, to use a mirror of https://rubygems.org hosted at https://example.org:
+
+ bundle config set --global mirror.https://rubygems.org https://example.org
+
+Each mirror also provides a fallback timeout setting. If the mirror does not
+respond within the fallback timeout, Bundler will try to use the original
+server instead of the mirror.
+
+ bundle config set --global mirror.SOURCE_URL.fallback_timeout TIMEOUT
+
+For example, to fall back to rubygems.org after 3 seconds:
+
+ bundle config set --global mirror.https://rubygems.org.fallback_timeout 3
+
+The default fallback timeout is 0.1 seconds, but the setting can currently
+only accept whole seconds (for example, 1, 15, or 30).
+
+## CREDENTIALS FOR GEM SOURCES
+
+Bundler allows you to configure credentials for any gem source, which allows
+you to avoid putting secrets into your Gemfile.
+
+ bundle config set --global SOURCE_HOSTNAME USERNAME:PASSWORD
+
+For example, to save the credentials of user `claudette` for the gem source at
+`gems.longerous.com`, you would run:
+
+ bundle config set --global gems.longerous.com claudette:s00pers3krit
+
+Or you can set the credentials as an environment variable like this:
+
+ export BUNDLE_GEMS__LONGEROUS__COM="claudette:s00pers3krit"
+
+For gems with a git source with HTTP(S) URL you can specify credentials like so:
+
+ bundle config set --global https://github.com/ruby/rubygems.git username:password
+
+Or you can set the credentials as an environment variable like so:
+
+ export BUNDLE_GITHUB__COM=username:password
+
+This is especially useful for private repositories on hosts such as GitHub,
+where you can use personal OAuth tokens:
+
+ export BUNDLE_GITHUB__COM=abcd0123generatedtoken:x-oauth-basic
+
+Note that any configured credentials will be redacted by informative commands
+such as `bundle config list` or `bundle config get`, unless you use the
+`--parseable` flag. This is to avoid unintentionally leaking credentials when
+copy-pasting bundler output.
+
+Also note that to guarantee a sane mapping between valid environment variable
+names and valid host names, bundler makes the following transformations:
+
+* Any `-` characters in a host name are mapped to a triple underscore (`___`) in the
+ corresponding environment variable.
+
+* Any `.` characters in a host name are mapped to a double underscore (`__`) in the
+ corresponding environment variable.
+
+This means that if you have a gem server named `my.gem-host.com`, you'll need to
+use the `BUNDLE_MY__GEM___HOST__COM` variable to configure credentials for it
+through ENV.
+
+## CONFIGURE BUNDLER DIRECTORIES
+
+Bundler's home, cache and plugin directories and config file can be configured
+through environment variables. The default location for Bundler's home directory is
+`~/.bundle`, which all directories inherit from by default. The following
+outlines the available environment variables and their default values
+
+ BUNDLE_USER_HOME : $HOME/.bundle
+ BUNDLE_USER_CACHE : $BUNDLE_USER_HOME/cache
+ BUNDLE_USER_CONFIG : $BUNDLE_USER_HOME/config
+ BUNDLE_USER_PLUGIN : $BUNDLE_USER_HOME/plugin
diff --git a/lib/bundler/man/bundle-console.1 b/lib/bundler/man/bundle-console.1
new file mode 100644
index 0000000000..5d3f65365f
--- /dev/null
+++ b/lib/bundler/man/bundle-console.1
@@ -0,0 +1,33 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-CONSOLE" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-console\fR \- Open an IRB session with the bundle pre\-loaded
+.SH "SYNOPSIS"
+\fBbundle console\fR [GROUP]
+.SH "DESCRIPTION"
+Starts an interactive Ruby console session in the context of the current bundle\.
+.P
+If no \fBGROUP\fR is specified, all gems in the \fBdefault\fR group in the Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR are preliminarily loaded\.
+.P
+If \fBGROUP\fR is specified, all gems in the given group in the Gemfile in addition to the gems in \fBdefault\fR group are loaded\. Even if the given group does not exist in the Gemfile, IRB console starts without any warning or error\.
+.P
+The environment variable \fBBUNDLE_CONSOLE\fR or \fBbundle config set console\fR can be used to change the shell from the following:
+.IP "\(bu" 4
+\fBirb\fR (default)
+.IP "\(bu" 4
+\fBpry\fR (https://github\.com/pry/pry)
+.IP "\(bu" 4
+\fBripl\fR (https://github\.com/cldwalker/ripl)
+.IP "" 0
+.P
+\fBbundle console\fR uses irb by default\. An alternative Pry or Ripl can be used with \fBbundle console\fR by adjusting the \fBconsole\fR Bundler setting\. Also make sure that \fBpry\fR or \fBripl\fR is in your Gemfile\.
+.SH "EXAMPLE"
+.nf
+$ bundle config set console pry
+$ bundle console
+Resolving dependencies\|\.\|\.\|\.
+[1] pry(main)>
+.fi
+.SH "SEE ALSO"
+Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR
diff --git a/lib/bundler/man/bundle-console.1.ronn b/lib/bundler/man/bundle-console.1.ronn
new file mode 100644
index 0000000000..ed842ae1c3
--- /dev/null
+++ b/lib/bundler/man/bundle-console.1.ronn
@@ -0,0 +1,39 @@
+bundle-console(1) -- Open an IRB session with the bundle pre-loaded
+===================================================================
+
+## SYNOPSIS
+
+`bundle console` [GROUP]
+
+## DESCRIPTION
+
+Starts an interactive Ruby console session in the context of the current bundle.
+
+If no `GROUP` is specified, all gems in the `default` group in the [Gemfile(5)](https://bundler.io/man/gemfile.5.html) are
+preliminarily loaded.
+
+If `GROUP` is specified, all gems in the given group in the Gemfile in addition
+to the gems in `default` group are loaded. Even if the given group does not
+exist in the Gemfile, IRB console starts without any warning or error.
+
+The environment variable `BUNDLE_CONSOLE` or `bundle config set console` can be used to change
+the shell from the following:
+
+* `irb` (default)
+* `pry` (https://github.com/pry/pry)
+* `ripl` (https://github.com/cldwalker/ripl)
+
+`bundle console` uses irb by default. An alternative Pry or Ripl can be used with
+`bundle console` by adjusting the `console` Bundler setting. Also make sure that
+`pry` or `ripl` is in your Gemfile.
+
+## EXAMPLE
+
+ $ bundle config set console pry
+ $ bundle console
+ Resolving dependencies...
+ [1] pry(main)>
+
+## SEE ALSO
+
+[Gemfile(5)](https://bundler.io/man/gemfile.5.html)
diff --git a/lib/bundler/man/bundle-doctor.1 b/lib/bundler/man/bundle-doctor.1
new file mode 100644
index 0000000000..4c59871b66
--- /dev/null
+++ b/lib/bundler/man/bundle-doctor.1
@@ -0,0 +1,69 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-DOCTOR" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-doctor\fR \- Checks the bundle for common problems
+.SH "SYNOPSIS"
+\fBbundle doctor [diagnose]\fR [\-\-quiet] [\-\-gemfile=GEMFILE] [\-\-ssl]
+.br
+\fBbundle doctor ssl\fR [\-\-host=HOST] [\-\-tls\-version=TLS\-VERSION] [\-\-verify\-mode=VERIFY\-MODE]
+.br
+\fBbundle doctor\fR help [COMMAND]
+.SH "DESCRIPTION"
+You can diagnose common Bundler problems with this command such as checking gem environment or SSL/TLS issue\.
+.SH "SUB\-COMMANDS"
+.SS "diagnose (default command)"
+Checks your Gemfile and gem environment for common problems\. If issues are detected, Bundler prints them and exits status 1\. Otherwise, Bundler prints a success message and exits status 0\.
+.P
+Examples of common problems caught include:
+.IP "\(bu" 4
+Invalid Bundler settings
+.IP "\(bu" 4
+Mismatched Ruby versions
+.IP "\(bu" 4
+Mismatched platforms
+.IP "\(bu" 4
+Uninstalled gems
+.IP "\(bu" 4
+Missing dependencies
+.IP "" 0
+.P
+\fBOPTIONS\fR
+.TP
+\fB\-\-quiet\fR
+Only output warnings and errors\.
+.TP
+\fB\-\-gemfile=GEMFILE\fR
+The location of the Gemfile(5) which Bundler should use\. This defaults to a Gemfile(5) in the current working directory\. In general, Bundler will assume that the location of the Gemfile(5) is also the project's root and will try to find \fBGemfile\.lock\fR and \fBvendor/cache\fR relative to this location\.
+.TP
+\fB\-\-ssl\fR
+Diagnose common SSL problems when connecting to https://rubygems\.org\.
+.IP
+This flag runs the \fBbundle doctor ssl\fR subcommand with default values underneath\.
+.SS "ssl"
+If you've experienced issues related to SSL certificates and/or TLS versions while connecting to https://rubygems\.org, this command can help troubleshoot common problems\. The diagnostic will perform a few checks such as:
+.IP "\(bu" 4
+Verify the Ruby OpenSSL version installed on your system\.
+.IP "\(bu" 4
+Check the OpenSSL library version used for compilation\.
+.IP "\(bu" 4
+Ensure CA certificates are correctly setup on your machine\.
+.IP "\(bu" 4
+Open a TLS connection and verify the outcome\.
+.IP "" 0
+.P
+\fBOPTIONS\fR
+.TP
+\fB\-\-host=HOST\fR
+Perform the diagnostic on HOST\. Defaults to \fBrubygems\.org\fR\.
+.TP
+\fB\-\-tls\-version=TLS\-VERSION\fR
+Specify the TLS version when opening the connection to HOST\.
+.IP
+Accepted values are: \fB1\.1\fR or \fB1\.2\fR\.
+.TP
+\fB\-\-verify\-mode=VERIFY\-MODE\fR
+Specify the TLS verify mode when opening the connection to HOST\. Defaults to \fBSSL_VERIFY_PEER\fR\.
+.IP
+Accepted values are: \fBCLIENT_ONCE\fR, \fBFAIL_IF_NO_PEER_CERT\fR, \fBNONE\fR, \fBPEER\fR\.
+
diff --git a/lib/bundler/man/bundle-doctor.1.ronn b/lib/bundler/man/bundle-doctor.1.ronn
new file mode 100644
index 0000000000..7495099ff5
--- /dev/null
+++ b/lib/bundler/man/bundle-doctor.1.ronn
@@ -0,0 +1,77 @@
+bundle-doctor(1) -- Checks the bundle for common problems
+=========================================================
+
+## SYNOPSIS
+
+`bundle doctor [diagnose]` [--quiet]
+ [--gemfile=GEMFILE]
+ [--ssl]<br>
+`bundle doctor ssl` [--host=HOST]
+ [--tls-version=TLS-VERSION]
+ [--verify-mode=VERIFY-MODE]<br>
+`bundle doctor` help [COMMAND]
+
+## DESCRIPTION
+
+You can diagnose common Bundler problems with this command such as checking gem environment or SSL/TLS issue.
+
+## SUB-COMMANDS
+
+### diagnose (default command)
+
+Checks your Gemfile and gem environment for common problems. If issues
+are detected, Bundler prints them and exits status 1. Otherwise,
+Bundler prints a success message and exits status 0.
+
+Examples of common problems caught include:
+
+* Invalid Bundler settings
+* Mismatched Ruby versions
+* Mismatched platforms
+* Uninstalled gems
+* Missing dependencies
+
+**OPTIONS**
+
+* `--quiet`:
+ Only output warnings and errors.
+
+* `--gemfile=GEMFILE`:
+ The location of the Gemfile(5) which Bundler should use. This defaults
+ to a Gemfile(5) in the current working directory. In general, Bundler
+ will assume that the location of the Gemfile(5) is also the project's
+ root and will try to find `Gemfile.lock` and `vendor/cache` relative
+ to this location.
+
+* `--ssl`:
+ Diagnose common SSL problems when connecting to https://rubygems.org.
+
+ This flag runs the `bundle doctor ssl` subcommand with default values
+ underneath.
+
+### ssl
+
+If you've experienced issues related to SSL certificates and/or TLS versions while connecting
+to https://rubygems.org, this command can help troubleshoot common problems.
+The diagnostic will perform a few checks such as:
+
+* Verify the Ruby OpenSSL version installed on your system.
+* Check the OpenSSL library version used for compilation.
+* Ensure CA certificates are correctly setup on your machine.
+* Open a TLS connection and verify the outcome.
+
+**OPTIONS**
+
+* `--host=HOST`:
+ Perform the diagnostic on HOST. Defaults to `rubygems.org`.
+
+* `--tls-version=TLS-VERSION`:
+ Specify the TLS version when opening the connection to HOST.
+
+ Accepted values are: `1.1` or `1.2`.
+
+* `--verify-mode=VERIFY-MODE`:
+ Specify the TLS verify mode when opening the connection to HOST.
+ Defaults to `SSL_VERIFY_PEER`.
+
+ Accepted values are: `CLIENT_ONCE`, `FAIL_IF_NO_PEER_CERT`, `NONE`, `PEER`.
diff --git a/lib/bundler/man/bundle-env.1 b/lib/bundler/man/bundle-env.1
new file mode 100644
index 0000000000..25fcb64891
--- /dev/null
+++ b/lib/bundler/man/bundle-env.1
@@ -0,0 +1,9 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-ENV" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-env\fR \- Print information about the environment Bundler is running under
+.SH "SYNOPSIS"
+\fBbundle env\fR
+.SH "DESCRIPTION"
+Prints information about the environment Bundler is running under\.
diff --git a/lib/bundler/man/bundle-env.1.ronn b/lib/bundler/man/bundle-env.1.ronn
new file mode 100644
index 0000000000..c2df9c29c2
--- /dev/null
+++ b/lib/bundler/man/bundle-env.1.ronn
@@ -0,0 +1,10 @@
+bundle-env(1) -- Print information about the environment Bundler is running under
+=================================================================================
+
+## SYNOPSIS
+
+`bundle env`
+
+## DESCRIPTION
+
+Prints information about the environment Bundler is running under.
diff --git a/lib/bundler/man/bundle-exec.1 b/lib/bundler/man/bundle-exec.1
new file mode 100644
index 0000000000..c3a6a09d57
--- /dev/null
+++ b/lib/bundler/man/bundle-exec.1
@@ -0,0 +1,104 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-EXEC" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-exec\fR \- Execute a command in the context of the bundle
+.SH "SYNOPSIS"
+\fBbundle exec\fR [\-\-gemfile=GEMFILE] \fIcommand\fR
+.SH "DESCRIPTION"
+This command executes the command, making all gems specified in the [\fBGemfile(5)\fR][Gemfile(5)] available to \fBrequire\fR in Ruby programs\.
+.P
+Essentially, if you would normally have run something like \fBrspec spec/my_spec\.rb\fR, and you want to use the gems specified in the [\fBGemfile(5)\fR][Gemfile(5)] and installed via bundle install(1) \fIbundle\-install\.1\.html\fR, you should run \fBbundle exec rspec spec/my_spec\.rb\fR\.
+.P
+Note that \fBbundle exec\fR does not require that an executable is available on your shell's \fB$PATH\fR\.
+.SH "OPTIONS"
+.TP
+\fB\-\-gemfile=GEMFILE\fR
+Use the specified gemfile instead of [\fBGemfile(5)\fR][Gemfile(5)]\.
+.SH "BUNDLE INSTALL \-\-BINSTUBS"
+If you use the \fB\-\-binstubs\fR flag in bundle install(1) \fIbundle\-install\.1\.html\fR, Bundler will automatically create a directory (which defaults to \fBapp_root/bin\fR) containing all of the executables available from gems in the bundle\.
+.P
+After using \fB\-\-binstubs\fR, \fBbin/rspec spec/my_spec\.rb\fR is identical to \fBbundle exec rspec spec/my_spec\.rb\fR\.
+.SH "ENVIRONMENT MODIFICATIONS"
+\fBbundle exec\fR makes a number of changes to the shell environment, then executes the command you specify in full\.
+.IP "\(bu" 4
+make sure that it's still possible to shell out to \fBbundle\fR from inside a command invoked by \fBbundle exec\fR (using \fB$BUNDLE_BIN_PATH\fR)
+.IP "\(bu" 4
+put the directory containing executables (like \fBrails\fR, \fBrspec\fR, \fBrackup\fR) for your bundle on \fB$PATH\fR
+.IP "\(bu" 4
+make sure that if bundler is invoked in the subshell, it uses the same \fBGemfile\fR (by setting \fBBUNDLE_GEMFILE\fR)
+.IP "\(bu" 4
+add \fB\-rbundler/setup\fR to \fB$RUBYOPT\fR, which makes sure that Ruby programs invoked in the subshell can see the gems in the bundle
+.IP "" 0
+.P
+It also modifies Rubygems:
+.IP "\(bu" 4
+disallow loading additional gems not in the bundle
+.IP "\(bu" 4
+modify the \fBgem\fR method to be a no\-op if a gem matching the requirements is in the bundle, and to raise a \fBGem::LoadError\fR if it's not
+.IP "\(bu" 4
+Define \fBGem\.refresh\fR to be a no\-op, since the source index is always frozen when using bundler, and to prevent gems from the system leaking into the environment
+.IP "\(bu" 4
+Override \fBGem\.bin_path\fR to use the gems in the bundle, making system executables work
+.IP "\(bu" 4
+Add all gems in the bundle into Gem\.loaded_specs
+.IP "" 0
+.P
+Finally, \fBbundle exec\fR also implicitly modifies \fBGemfile\.lock\fR if the lockfile and the Gemfile do not match\. Bundler needs the Gemfile to determine things such as a gem's groups, \fBautorequire\fR, and platforms, etc\., and that information isn't stored in the lockfile\. The Gemfile and lockfile must be synced in order to \fBbundle exec\fR successfully, so \fBbundle exec\fR updates the lockfile beforehand\.
+.SS "Loading"
+By default, when attempting to \fBbundle exec\fR to a file with a ruby shebang, Bundler will \fBKernel\.load\fR that file instead of using \fBKernel\.exec\fR\. For the vast majority of cases, this is a performance improvement\. In a rare few cases, this could cause some subtle side\-effects (such as dependence on the exact contents of \fB$0\fR or \fB__FILE__\fR) and the optimization can be disabled by enabling the \fBdisable_exec_load\fR setting\.
+.SS "Shelling out"
+Any Ruby code that opens a subshell (like \fBsystem\fR, backticks, or \fB%x{}\fR) will automatically use the current Bundler environment\. If you need to shell out to a Ruby command that is not part of your current bundle, use the \fBwith_unbundled_env\fR method with a block\. Any subshells created inside the block will be given the environment present before Bundler was activated\. For example, Homebrew commands run Ruby, but don't work inside a bundle:
+.IP "" 4
+.nf
+Bundler\.with_unbundled_env do
+ `brew install wget`
+end
+.fi
+.IP "" 0
+.P
+Using \fBwith_unbundled_env\fR is also necessary if you are shelling out to a different bundle\. Any Bundler commands run in a subshell will inherit the current Gemfile, so commands that need to run in the context of a different bundle also need to use \fBwith_unbundled_env\fR\.
+.IP "" 4
+.nf
+Bundler\.with_unbundled_env do
+ Dir\.chdir "/other/bundler/project" do
+ `bundle exec \./script`
+ end
+end
+.fi
+.IP "" 0
+.P
+Bundler provides convenience helpers that wrap \fBsystem\fR and \fBexec\fR, and they can be used like this:
+.IP "" 4
+.nf
+Bundler\.unbundled_system('brew install wget')
+Bundler\.unbundled_exec('brew install wget')
+.fi
+.IP "" 0
+.SH "RUBYGEMS PLUGINS"
+At present, the Rubygems plugin system requires all files named \fBrubygems_plugin\.rb\fR on the load path of \fIany\fR installed gem when any Ruby code requires \fBrubygems\.rb\fR\. This includes executables installed into the system, like \fBrails\fR, \fBrackup\fR, and \fBrspec\fR\.
+.P
+Since Rubygems plugins can contain arbitrary Ruby code, they commonly end up activating themselves or their dependencies\.
+.P
+For instance, the \fBgemcutter 0\.5\fR gem depended on \fBjson_pure\fR\. If you had that version of gemcutter installed (even if you \fIalso\fR had a newer version without this problem), Rubygems would activate \fBgemcutter 0\.5\fR and \fBjson_pure <latest>\fR\.
+.P
+If your Gemfile(5) also contained \fBjson_pure\fR (or a gem with a dependency on \fBjson_pure\fR), the latest version on your system might conflict with the version in your Gemfile(5), or the snapshot version in your \fBGemfile\.lock\fR\.
+.P
+If this happens, bundler will say:
+.IP "" 4
+.nf
+You have already activated json_pure 1\.4\.6 but your Gemfile
+requires json_pure 1\.4\.3\. Consider using bundle exec\.
+.fi
+.IP "" 0
+.P
+In this situation, you almost certainly want to remove the underlying gem with the problematic gem plugin\. In general, the authors of these plugins (in this case, the \fBgemcutter\fR gem) have released newer versions that are more careful in their plugins\.
+.P
+You can find a list of all the gems containing gem plugins by running
+.IP "" 4
+.nf
+ruby \-e "puts Gem\.find_files('rubygems_plugin\.rb')"
+.fi
+.IP "" 0
+.P
+At the very least, you should remove all but the newest version of each gem plugin, and also remove all gem plugins that you aren't using (\fBgem uninstall gem_name\fR)\.
diff --git a/lib/bundler/man/bundle-exec.1.ronn b/lib/bundler/man/bundle-exec.1.ronn
new file mode 100644
index 0000000000..e51a66a084
--- /dev/null
+++ b/lib/bundler/man/bundle-exec.1.ronn
@@ -0,0 +1,150 @@
+bundle-exec(1) -- Execute a command in the context of the bundle
+================================================================
+
+## SYNOPSIS
+
+`bundle exec` [--gemfile=GEMFILE] <command>
+
+## DESCRIPTION
+
+This command executes the command, making all gems specified in the
+[`Gemfile(5)`][Gemfile(5)] available to `require` in Ruby programs.
+
+Essentially, if you would normally have run something like
+`rspec spec/my_spec.rb`, and you want to use the gems specified
+in the [`Gemfile(5)`][Gemfile(5)] and installed via [bundle install(1)](bundle-install.1.html), you
+should run `bundle exec rspec spec/my_spec.rb`.
+
+Note that `bundle exec` does not require that an executable is
+available on your shell's `$PATH`.
+
+## OPTIONS
+
+* `--gemfile=GEMFILE`:
+ Use the specified gemfile instead of [`Gemfile(5)`][Gemfile(5)].
+
+## BUNDLE INSTALL --BINSTUBS
+
+If you use the `--binstubs` flag in [bundle install(1)](bundle-install.1.html), Bundler will
+automatically create a directory (which defaults to `app_root/bin`)
+containing all of the executables available from gems in the bundle.
+
+After using `--binstubs`, `bin/rspec spec/my_spec.rb` is identical
+to `bundle exec rspec spec/my_spec.rb`.
+
+## ENVIRONMENT MODIFICATIONS
+
+`bundle exec` makes a number of changes to the shell environment,
+then executes the command you specify in full.
+
+* make sure that it's still possible to shell out to `bundle`
+ from inside a command invoked by `bundle exec` (using
+ `$BUNDLE_BIN_PATH`)
+* put the directory containing executables (like `rails`, `rspec`,
+ `rackup`) for your bundle on `$PATH`
+* make sure that if bundler is invoked in the subshell, it uses
+ the same `Gemfile` (by setting `BUNDLE_GEMFILE`)
+* add `-rbundler/setup` to `$RUBYOPT`, which makes sure that
+ Ruby programs invoked in the subshell can see the gems in
+ the bundle
+
+It also modifies Rubygems:
+
+* disallow loading additional gems not in the bundle
+* modify the `gem` method to be a no-op if a gem matching
+ the requirements is in the bundle, and to raise a
+ `Gem::LoadError` if it's not
+* Define `Gem.refresh` to be a no-op, since the source
+ index is always frozen when using bundler, and to
+ prevent gems from the system leaking into the environment
+* Override `Gem.bin_path` to use the gems in the bundle,
+ making system executables work
+* Add all gems in the bundle into Gem.loaded_specs
+
+Finally, `bundle exec` also implicitly modifies `Gemfile.lock` if the lockfile
+and the Gemfile do not match. Bundler needs the Gemfile to determine things
+such as a gem's groups, `autorequire`, and platforms, etc., and that
+information isn't stored in the lockfile. The Gemfile and lockfile must be
+synced in order to `bundle exec` successfully, so `bundle exec`
+updates the lockfile beforehand.
+
+### Loading
+
+By default, when attempting to `bundle exec` to a file with a ruby shebang,
+Bundler will `Kernel.load` that file instead of using `Kernel.exec`. For the
+vast majority of cases, this is a performance improvement. In a rare few cases,
+this could cause some subtle side-effects (such as dependence on the exact
+contents of `$0` or `__FILE__`) and the optimization can be disabled by enabling
+the `disable_exec_load` setting.
+
+### Shelling out
+
+Any Ruby code that opens a subshell (like `system`, backticks, or `%x{}`) will
+automatically use the current Bundler environment. If you need to shell out to
+a Ruby command that is not part of your current bundle, use the
+`with_unbundled_env` method with a block. Any subshells created inside the block
+will be given the environment present before Bundler was activated. For
+example, Homebrew commands run Ruby, but don't work inside a bundle:
+
+ Bundler.with_unbundled_env do
+ `brew install wget`
+ end
+
+Using `with_unbundled_env` is also necessary if you are shelling out to a different
+bundle. Any Bundler commands run in a subshell will inherit the current
+Gemfile, so commands that need to run in the context of a different bundle also
+need to use `with_unbundled_env`.
+
+ Bundler.with_unbundled_env do
+ Dir.chdir "/other/bundler/project" do
+ `bundle exec ./script`
+ end
+ end
+
+Bundler provides convenience helpers that wrap `system` and `exec`, and they
+can be used like this:
+
+ Bundler.unbundled_system('brew install wget')
+ Bundler.unbundled_exec('brew install wget')
+
+
+## RUBYGEMS PLUGINS
+
+At present, the Rubygems plugin system requires all files
+named `rubygems_plugin.rb` on the load path of _any_ installed
+gem when any Ruby code requires `rubygems.rb`. This includes
+executables installed into the system, like `rails`, `rackup`,
+and `rspec`.
+
+Since Rubygems plugins can contain arbitrary Ruby code, they
+commonly end up activating themselves or their dependencies.
+
+For instance, the `gemcutter 0.5` gem depended on `json_pure`.
+If you had that version of gemcutter installed (even if
+you _also_ had a newer version without this problem), Rubygems
+would activate `gemcutter 0.5` and `json_pure <latest>`.
+
+If your Gemfile(5) also contained `json_pure` (or a gem
+with a dependency on `json_pure`), the latest version on
+your system might conflict with the version in your
+Gemfile(5), or the snapshot version in your `Gemfile.lock`.
+
+If this happens, bundler will say:
+
+ You have already activated json_pure 1.4.6 but your Gemfile
+ requires json_pure 1.4.3. Consider using bundle exec.
+
+In this situation, you almost certainly want to remove the
+underlying gem with the problematic gem plugin. In general,
+the authors of these plugins (in this case, the `gemcutter`
+gem) have released newer versions that are more careful in
+their plugins.
+
+You can find a list of all the gems containing gem plugins
+by running
+
+ ruby -e "puts Gem.find_files('rubygems_plugin.rb')"
+
+At the very least, you should remove all but the newest
+version of each gem plugin, and also remove all gem plugins
+that you aren't using (`gem uninstall gem_name`).
diff --git a/lib/bundler/man/bundle-fund.1 b/lib/bundler/man/bundle-fund.1
new file mode 100644
index 0000000000..caee1f81dd
--- /dev/null
+++ b/lib/bundler/man/bundle-fund.1
@@ -0,0 +1,22 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-FUND" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-fund\fR \- Lists information about gems seeking funding assistance
+.SH "SYNOPSIS"
+\fBbundle fund\fR [\fIOPTIONS\fR]
+.SH "DESCRIPTION"
+\fBbundle fund\fR lists information about gems seeking funding assistance\.
+.SH "OPTIONS"
+.TP
+\fB\-\-group=<list>\fR, \fB\-g=<list>\fR
+Fetch funding information for a specific group\.
+.SH "EXAMPLES"
+.nf
+# Lists funding information for all gems
+bundle fund
+
+# Lists funding information for a specific group
+bundle fund \-\-group=security
+.fi
+
diff --git a/lib/bundler/man/bundle-fund.1.ronn b/lib/bundler/man/bundle-fund.1.ronn
new file mode 100644
index 0000000000..faf8b9c4a7
--- /dev/null
+++ b/lib/bundler/man/bundle-fund.1.ronn
@@ -0,0 +1,25 @@
+bundle-fund(1) -- Lists information about gems seeking funding assistance
+=========================================================================
+
+## SYNOPSIS
+
+`bundle fund` [*OPTIONS*]
+
+## DESCRIPTION
+
+**bundle fund** lists information about gems seeking funding assistance.
+
+## OPTIONS
+
+* `--group=<list>`, `-g=<list>`:
+ Fetch funding information for a specific group.
+
+## EXAMPLES
+
+```
+# Lists funding information for all gems
+bundle fund
+
+# Lists funding information for a specific group
+bundle fund --group=security
+```
diff --git a/lib/bundler/man/bundle-gem.1 b/lib/bundler/man/bundle-gem.1
new file mode 100644
index 0000000000..87d7568246
--- /dev/null
+++ b/lib/bundler/man/bundle-gem.1
@@ -0,0 +1,107 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-GEM" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-gem\fR \- Generate a project skeleton for creating a rubygem
+.SH "SYNOPSIS"
+\fBbundle gem\fR \fIGEM_NAME\fR \fIOPTIONS\fR
+.SH "DESCRIPTION"
+Generates a directory named \fBGEM_NAME\fR with a \fBRakefile\fR, \fBGEM_NAME\.gemspec\fR, and other supporting files and directories that can be used to develop a rubygem with that name\.
+.P
+Run \fBrake \-T\fR in the resulting project for a list of Rake tasks that can be used to test and publish the gem to rubygems\.org\.
+.P
+The generated project skeleton can be customized with OPTIONS, as explained below\. Note that these options can also be specified via Bundler's global configuration file using the following names:
+.IP "\(bu" 4
+\fBgem\.coc\fR
+.IP "\(bu" 4
+\fBgem\.mit\fR
+.IP "\(bu" 4
+\fBgem\.test\fR
+.IP "" 0
+.SH "OPTIONS"
+.TP
+\fB\-\-exe\fR, \fB\-\-bin\fR, \fB\-b\fR
+Specify that Bundler should create a binary executable (as \fBexe/GEM_NAME\fR) in the generated rubygem project\. This binary will also be added to the \fBGEM_NAME\.gemspec\fR manifest\. This behavior is disabled by default\.
+.TP
+\fB\-\-no\-exe\fR
+Do not create a binary (overrides \fB\-\-exe\fR specified in the global config)\.
+.TP
+\fB\-\-coc\fR
+Add a \fBCODE_OF_CONDUCT\.md\fR file to the root of the generated project\. If this option is unspecified, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\.
+.TP
+\fB\-\-no\-coc\fR
+Do not create a \fBCODE_OF_CONDUCT\.md\fR (overrides \fB\-\-coc\fR specified in the global config)\.
+.TP
+\fB\-\-changelog\fR
+Add a \fBCHANGELOG\.md\fR file to the root of the generated project\. If this option is unspecified, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\. Update the default with \fBbundle config set \-\-global gem\.changelog <true|false>\fR\.
+.TP
+\fB\-\-no\-changelog\fR
+Do not create a \fBCHANGELOG\.md\fR (overrides \fB\-\-changelog\fR specified in the global config)\.
+.TP
+\fB\-\-ext=c\fR, \fB\-\-ext=go\fR, \fB\-\-ext=rust\fR
+Add boilerplate for C, Go (currently go\-gem\-wrapper \fIhttps://github\.com/ruby\-go\-gem/go\-gem\-wrapper\fR based) or Rust (currently magnus \fIhttps://docs\.rs/magnus\fR based) extension code to the generated project\. This behavior is disabled by default\.
+.TP
+\fB\-\-no\-ext\fR
+Do not add extension code (overrides \fB\-\-ext\fR specified in the global config)\.
+.TP
+\fB\-\-git\fR
+Initialize a git repo inside your library\.
+.TP
+\fB\-\-github\-username=GITHUB_USERNAME\fR
+Fill in GitHub username on README so that you don't have to do it manually\. Set a default with \fBbundle config set \-\-global gem\.github_username <your_username>\fR\.
+.TP
+\fB\-\-mit\fR
+Add an MIT license to a \fBLICENSE\.txt\fR file in the root of the generated project\. Your name from the global git config is used for the copyright statement\. If this option is unspecified, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\.
+.TP
+\fB\-\-no\-mit\fR
+Do not create a \fBLICENSE\.txt\fR (overrides \fB\-\-mit\fR specified in the global config)\.
+.TP
+\fB\-t\fR, \fB\-\-test=minitest\fR, \fB\-\-test=rspec\fR, \fB\-\-test=test\-unit\fR
+Specify the test framework that Bundler should use when generating the project\. Acceptable values are \fBminitest\fR, \fBrspec\fR and \fBtest\-unit\fR\. The \fBGEM_NAME\.gemspec\fR will be configured and a skeleton test/spec directory will be created based on this option\. Given no option is specified:
+.IP
+When Bundler is configured to generate tests, this defaults to Bundler's global config setting \fBgem\.test\fR\.
+.IP
+When Bundler is configured to not generate tests, an interactive prompt will be displayed and the answer will be used for the current rubygem project\.
+.IP
+When Bundler is unconfigured, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\.
+.TP
+\fB\-\-no\-test\fR
+Do not use a test framework (overrides \fB\-\-test\fR specified in the global config)\.
+.TP
+\fB\-\-ci\fR, \fB\-\-ci=circle\fR, \fB\-\-ci=github\fR, \fB\-\-ci=gitlab\fR
+Specify the continuous integration service that Bundler should use when generating the project\. Acceptable values are \fBgithub\fR, \fBgitlab\fR and \fBcircle\fR\. A configuration file will be generated in the project directory\. Given no option is specified:
+.IP
+When Bundler is configured to generate CI files, this defaults to Bundler's global config setting \fBgem\.ci\fR\.
+.IP
+When Bundler is configured to not generate CI files, an interactive prompt will be displayed and the answer will be used for the current rubygem project\.
+.IP
+When Bundler is unconfigured, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\.
+.TP
+\fB\-\-no\-ci\fR
+Do not use a continuous integration service (overrides \fB\-\-ci\fR specified in the global config)\.
+.TP
+\fB\-\-linter\fR, \fB\-\-linter=rubocop\fR, \fB\-\-linter=standard\fR
+Specify the linter and code formatter that Bundler should add to the project's development dependencies\. Acceptable values are \fBrubocop\fR and \fBstandard\fR\. A configuration file will be generated in the project directory\. Given no option is specified:
+.IP
+When Bundler is configured to add a linter, this defaults to Bundler's global config setting \fBgem\.linter\fR\.
+.IP
+When Bundler is configured not to add a linter, an interactive prompt will be displayed and the answer will be used for the current rubygem project\.
+.IP
+When Bundler is unconfigured, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\.
+.TP
+\fB\-\-no\-linter\fR
+Do not add a linter (overrides \fB\-\-linter\fR specified in the global config)\.
+.TP
+\fB\-\-edit=EDIT\fR, \fB\-e=EDIT\fR
+Open the resulting GEM_NAME\.gemspec in EDIT, or the default editor if not specified\. The default is \fB$BUNDLER_EDITOR\fR, \fB$VISUAL\fR, or \fB$EDITOR\fR\.
+.TP
+\fB\-\-bundle\fR
+Run \fBbundle install\fR after creating the gem\.
+.TP
+\fB\-\-no\-bundle\fR
+Do not run \fBbundle install\fR after creating the gem\.
+.SH "SEE ALSO"
+.IP "\(bu" 4
+bundle config(1) \fIbundle\-config\.1\.html\fR
+.IP "" 0
+
diff --git a/lib/bundler/man/bundle-gem.1.ronn b/lib/bundler/man/bundle-gem.1.ronn
new file mode 100644
index 0000000000..488c8113e4
--- /dev/null
+++ b/lib/bundler/man/bundle-gem.1.ronn
@@ -0,0 +1,150 @@
+bundle-gem(1) -- Generate a project skeleton for creating a rubygem
+===================================================================
+
+## SYNOPSIS
+
+`bundle gem` <GEM_NAME> [OPTIONS]
+
+## DESCRIPTION
+
+Generates a directory named `GEM_NAME` with a `Rakefile`, `GEM_NAME.gemspec`,
+and other supporting files and directories that can be used to develop a
+rubygem with that name.
+
+Run `rake -T` in the resulting project for a list of Rake tasks that can be used
+to test and publish the gem to rubygems.org.
+
+The generated project skeleton can be customized with OPTIONS, as explained
+below. Note that these options can also be specified via Bundler's global
+configuration file using the following names:
+
+* `gem.coc`
+* `gem.mit`
+* `gem.test`
+
+## OPTIONS
+
+* `--exe`, `--bin`, `-b`:
+ Specify that Bundler should create a binary executable (as `exe/GEM_NAME`)
+ in the generated rubygem project. This binary will also be added to the
+ `GEM_NAME.gemspec` manifest. This behavior is disabled by default.
+
+* `--no-exe`:
+ Do not create a binary (overrides `--exe` specified in the global config).
+
+* `--coc`:
+ Add a `CODE_OF_CONDUCT.md` file to the root of the generated project. If
+ this option is unspecified, an interactive prompt will be displayed and the
+ answer will be saved in Bundler's global config for future `bundle gem` use.
+
+* `--no-coc`:
+ Do not create a `CODE_OF_CONDUCT.md` (overrides `--coc` specified in the
+ global config).
+
+* `--changelog`:
+ Add a `CHANGELOG.md` file to the root of the generated project. If
+ this option is unspecified, an interactive prompt will be displayed and the
+ answer will be saved in Bundler's global config for future `bundle gem` use.
+ Update the default with `bundle config set --global gem.changelog <true|false>`.
+
+* `--no-changelog`:
+ Do not create a `CHANGELOG.md` (overrides `--changelog` specified in the
+ global config).
+
+* `--ext=c`, `--ext=go`, `--ext=rust`:
+ Add boilerplate for C, Go (currently [go-gem-wrapper](https://github.com/ruby-go-gem/go-gem-wrapper) based) or Rust (currently [magnus](https://docs.rs/magnus) based) extension code to the generated project. This behavior
+ is disabled by default.
+
+* `--no-ext`:
+ Do not add extension code (overrides `--ext` specified in the global
+ config).
+
+* `--git`:
+ Initialize a git repo inside your library.
+
+* `--github-username=GITHUB_USERNAME`:
+ Fill in GitHub username on README so that you don't have to do it manually. Set a default with `bundle config set --global gem.github_username <your_username>`.
+
+* `--mit`:
+ Add an MIT license to a `LICENSE.txt` file in the root of the generated
+ project. Your name from the global git config is used for the copyright
+ statement. If this option is unspecified, an interactive prompt will be
+ displayed and the answer will be saved in Bundler's global config for future
+ `bundle gem` use.
+
+* `--no-mit`:
+ Do not create a `LICENSE.txt` (overrides `--mit` specified in the global
+ config).
+
+* `-t`, `--test=minitest`, `--test=rspec`, `--test=test-unit`:
+ Specify the test framework that Bundler should use when generating the
+ project. Acceptable values are `minitest`, `rspec` and `test-unit`. The
+ `GEM_NAME.gemspec` will be configured and a skeleton test/spec directory will
+ be created based on this option. Given no option is specified:
+
+ When Bundler is configured to generate tests, this defaults to Bundler's
+ global config setting `gem.test`.
+
+ When Bundler is configured to not generate tests, an interactive prompt will
+ be displayed and the answer will be used for the current rubygem project.
+
+ When Bundler is unconfigured, an interactive prompt will be displayed and
+ the answer will be saved in Bundler's global config for future `bundle gem`
+ use.
+
+* `--no-test`:
+ Do not use a test framework (overrides `--test` specified in the global
+ config).
+
+* `--ci`, `--ci=circle`, `--ci=github`, `--ci=gitlab`:
+ Specify the continuous integration service that Bundler should use when
+ generating the project. Acceptable values are `github`, `gitlab`
+ and `circle`. A configuration file will be generated in the project directory.
+ Given no option is specified:
+
+ When Bundler is configured to generate CI files, this defaults to Bundler's
+ global config setting `gem.ci`.
+
+ When Bundler is configured to not generate CI files, an interactive prompt
+ will be displayed and the answer will be used for the current rubygem project.
+
+ When Bundler is unconfigured, an interactive prompt will be displayed and
+ the answer will be saved in Bundler's global config for future `bundle gem`
+ use.
+
+* `--no-ci`:
+ Do not use a continuous integration service (overrides `--ci` specified in
+ the global config).
+
+* `--linter`, `--linter=rubocop`, `--linter=standard`:
+ Specify the linter and code formatter that Bundler should add to the
+ project's development dependencies. Acceptable values are `rubocop` and
+ `standard`. A configuration file will be generated in the project directory.
+ Given no option is specified:
+
+ When Bundler is configured to add a linter, this defaults to Bundler's
+ global config setting `gem.linter`.
+
+ When Bundler is configured not to add a linter, an interactive prompt
+ will be displayed and the answer will be used for the current rubygem project.
+
+ When Bundler is unconfigured, an interactive prompt will be displayed and
+ the answer will be saved in Bundler's global config for future `bundle gem`
+ use.
+
+* `--no-linter`:
+ Do not add a linter (overrides `--linter` specified in the global config).
+
+* `--edit=EDIT`, `-e=EDIT`:
+ Open the resulting GEM_NAME.gemspec in EDIT, or the default editor if not
+ specified. The default is `$BUNDLER_EDITOR`, `$VISUAL`, or `$EDITOR`.
+
+* `--bundle`:
+ Run `bundle install` after creating the gem.
+
+* `--no-bundle`:
+ Do not run `bundle install` after creating the gem.
+
+## SEE ALSO
+
+* [bundle config(1)](bundle-config.1.html)
diff --git a/lib/bundler/man/bundle-help.1 b/lib/bundler/man/bundle-help.1
new file mode 100644
index 0000000000..3bcfd047e5
--- /dev/null
+++ b/lib/bundler/man/bundle-help.1
@@ -0,0 +1,9 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-HELP" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-help\fR \- Displays detailed help for each subcommand
+.SH "SYNOPSIS"
+\fBbundle help\fR [COMMAND]
+.SH "DESCRIPTION"
+Displays detailed help for the given subcommand\. You can specify a single \fBCOMMAND\fR at the same time\. When \fBCOMMAND\fR is omitted, help for \fBhelp\fR command will be displayed\.
diff --git a/lib/bundler/man/bundle-help.1.ronn b/lib/bundler/man/bundle-help.1.ronn
new file mode 100644
index 0000000000..0e144aead7
--- /dev/null
+++ b/lib/bundler/man/bundle-help.1.ronn
@@ -0,0 +1,12 @@
+bundle-help(1) -- Displays detailed help for each subcommand
+============================================================
+
+## SYNOPSIS
+
+`bundle help` [COMMAND]
+
+## DESCRIPTION
+
+Displays detailed help for the given subcommand.
+You can specify a single `COMMAND` at the same time.
+When `COMMAND` is omitted, help for `help` command will be displayed.
diff --git a/lib/bundler/man/bundle-info.1 b/lib/bundler/man/bundle-info.1
new file mode 100644
index 0000000000..49c2295f8c
--- /dev/null
+++ b/lib/bundler/man/bundle-info.1
@@ -0,0 +1,17 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-INFO" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-info\fR \- Show information for the given gem in your bundle
+.SH "SYNOPSIS"
+\fBbundle info\fR [GEM_NAME] [\-\-path] [\-\-version]
+.SH "DESCRIPTION"
+Given a gem name present in your bundle, print the basic information about it such as homepage, version, path and summary\.
+.SH "OPTIONS"
+.TP
+\fB\-\-path\fR
+Print the path of the given gem
+.TP
+\fB\-\-version\fR
+Print gem version
+
diff --git a/lib/bundler/man/bundle-info.1.ronn b/lib/bundler/man/bundle-info.1.ronn
new file mode 100644
index 0000000000..e99db8c614
--- /dev/null
+++ b/lib/bundler/man/bundle-info.1.ronn
@@ -0,0 +1,21 @@
+bundle-info(1) -- Show information for the given gem in your bundle
+===================================================================
+
+## SYNOPSIS
+
+`bundle info` [GEM_NAME]
+ [--path]
+ [--version]
+
+## DESCRIPTION
+
+Given a gem name present in your bundle, print the basic information about it
+ such as homepage, version, path and summary.
+
+## OPTIONS
+
+* `--path`:
+ Print the path of the given gem
+
+* `--version`:
+ Print gem version
diff --git a/lib/bundler/man/bundle-init.1 b/lib/bundler/man/bundle-init.1
new file mode 100644
index 0000000000..63e2376c3f
--- /dev/null
+++ b/lib/bundler/man/bundle-init.1
@@ -0,0 +1,20 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-INIT" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-init\fR \- Generates a Gemfile into the current working directory
+.SH "SYNOPSIS"
+\fBbundle init\fR [\-\-gemspec=FILE]
+.SH "DESCRIPTION"
+Init generates a default [\fBGemfile(5)\fR][Gemfile(5)] in the current working directory\. When adding a [\fBGemfile(5)\fR][Gemfile(5)] to a gem with a gemspec, the \fB\-\-gemspec\fR option will automatically add each dependency listed in the gemspec file to the newly created [\fBGemfile(5)\fR][Gemfile(5)]\.
+.SH "OPTIONS"
+.TP
+\fB\-\-gemspec=GEMSPEC\fR
+Use the specified \.gemspec to create the [\fBGemfile(5)\fR][Gemfile(5)]
+.TP
+\fB\-\-gemfile=GEMFILE\fR
+Use the specified name for the gemfile instead of \fBGemfile\fR
+.SH "FILES"
+Included in the default [\fBGemfile(5)\fR][Gemfile(5)] generated is the line \fB# frozen_string_literal: true\fR\. This is a magic comment supported for the first time in Ruby 2\.3\. The presence of this line results in all string literals in the file being implicitly frozen\.
+.SH "SEE ALSO"
+Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR
diff --git a/lib/bundler/man/bundle-init.1.ronn b/lib/bundler/man/bundle-init.1.ronn
new file mode 100644
index 0000000000..ab3c427b52
--- /dev/null
+++ b/lib/bundler/man/bundle-init.1.ronn
@@ -0,0 +1,32 @@
+bundle-init(1) -- Generates a Gemfile into the current working directory
+========================================================================
+
+## SYNOPSIS
+
+`bundle init` [--gemspec=FILE]
+
+## DESCRIPTION
+
+Init generates a default [`Gemfile(5)`][Gemfile(5)] in the current working directory. When
+adding a [`Gemfile(5)`][Gemfile(5)] to a gem with a gemspec, the `--gemspec` option will
+automatically add each dependency listed in the gemspec file to the newly
+created [`Gemfile(5)`][Gemfile(5)].
+
+## OPTIONS
+
+* `--gemspec=GEMSPEC`:
+ Use the specified .gemspec to create the [`Gemfile(5)`][Gemfile(5)]
+
+* `--gemfile=GEMFILE`:
+ Use the specified name for the gemfile instead of `Gemfile`
+
+## FILES
+
+Included in the default [`Gemfile(5)`][Gemfile(5)]
+generated is the line `# frozen_string_literal: true`. This is a magic comment
+supported for the first time in Ruby 2.3. The presence of this line
+results in all string literals in the file being implicitly frozen.
+
+## SEE ALSO
+
+[Gemfile(5)](https://bundler.io/man/gemfile.5.html)
diff --git a/lib/bundler/man/bundle-install.1 b/lib/bundler/man/bundle-install.1
new file mode 100644
index 0000000000..801768c7ec
--- /dev/null
+++ b/lib/bundler/man/bundle-install.1
@@ -0,0 +1,178 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-INSTALL" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-install\fR \- Install the dependencies specified in your Gemfile
+.SH "SYNOPSIS"
+\fBbundle install\fR [\-\-cooldown=NUMBER] [\-\-force] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-local] [\-\-lockfile=LOCKFILE] [\-\-no\-cache] [\-\-no\-lock] [\-\-prefer\-local] [\-\-quiet] [\-\-retry=NUMBER] [\-\-standalone[=GROUP[ GROUP\|\.\|\.\|\.]]] [\-\-trust\-policy=TRUST\-POLICY] [\-\-target\-rbconfig=TARGET\-RBCONFIG]
+.SH "DESCRIPTION"
+Install the gems specified in your Gemfile(5)\. If this is the first time you run bundle install (and a \fBGemfile\.lock\fR does not exist), Bundler will fetch all remote sources, resolve dependencies and install all needed gems\.
+.P
+If a \fBGemfile\.lock\fR does exist, and you have not updated your Gemfile(5), Bundler will fetch all remote sources, but use the dependencies specified in the \fBGemfile\.lock\fR instead of resolving dependencies\.
+.P
+If a \fBGemfile\.lock\fR does exist, and you have updated your Gemfile(5), Bundler will use the dependencies in the \fBGemfile\.lock\fR for all gems that you did not update, but will re\-resolve the dependencies of gems that you did update\. You can find more information about this update process below under \fICONSERVATIVE UPDATING\fR\.
+.SH "OPTIONS"
+.TP
+\fB\-\-cooldown=<number>\fR
+Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. See \fBcooldown\fR in bundle\-config(1) for details on the precedence between the CLI flag, Bundler config, and Gemfile per\-source settings\.
+.TP
+\fB\-\-force\fR, \fB\-\-redownload\fR
+Force reinstalling every gem, even if already installed\.
+.TP
+\fB\-\-full\-index\fR
+Bundler will not call Rubygems' API endpoint (default) but download and cache a (currently big) index file of all gems\. Performance can be improved for large bundles that seldom change by enabling this option\.
+.TP
+\fB\-\-gemfile=GEMFILE\fR
+The location of the Gemfile(5) which Bundler should use\. This defaults to a Gemfile(5) in the current working directory\. In general, Bundler will assume that the location of the Gemfile(5) is also the project's root and will try to find \fBGemfile\.lock\fR and \fBvendor/cache\fR relative to this location\.
+.TP
+\fB\-\-jobs=<number>\fR, \fB\-j=<number>\fR
+The maximum number of parallel download and install jobs\. The default is the number of available processors\.
+.TP
+\fB\-\-local\fR
+Do not attempt to connect to \fBrubygems\.org\fR\. Instead, Bundler will use the gems already present in Rubygems' cache or in \fBvendor/cache\fR\. Note that if an appropriate platform\-specific gem exists on \fBrubygems\.org\fR it will not be found\.
+.TP
+\fB\-\-lockfile=LOCKFILE\fR
+The location of the lockfile which Bundler should use\. This defaults to the Gemfile location with \fB\.lock\fR appended\.
+.TP
+\fB\-\-prefer\-local\fR
+Force using locally installed gems, or gems already present in Rubygems' cache or in \fBvendor/cache\fR, when resolving, even if newer versions are available remotely\. Only attempt to connect to \fBrubygems\.org\fR for gems that are not present locally\.
+.TP
+\fB\-\-no\-cache\fR
+Do not update the cache in \fBvendor/cache\fR with the newly bundled gems\. This does not remove any gems in the cache but keeps the newly bundled gems from being cached during the install\.
+.TP
+\fB\-\-no\-lock\fR
+Do not create a lockfile\. Useful if you want to install dependencies but not lock versions of gems\. Recommended for library development, and other situations where the code is expected to work with a range of dependency versions\.
+.IP
+This has the same effect as using \fBlockfile false\fR in the Gemfile\. See gemfile(5) for more information\.
+.TP
+\fB\-\-quiet\fR
+Do not print progress information to the standard output\.
+.TP
+\fB\-\-retry=[<number>]\fR
+Retry failed network or git requests for \fInumber\fR times\.
+.TP
+\fB\-\-standalone[=<list>]\fR
+Makes a bundle that can work without depending on Rubygems or Bundler at runtime\. A space separated list of groups to install can be specified\. Bundler creates a directory named \fBbundle\fR and installs the bundle there\. It also generates a \fBbundle/bundler/setup\.rb\fR file to replace Bundler's own setup in the manner required\.
+.TP
+\fB\-\-trust\-policy=TRUST\-POLICY\fR
+Apply the Rubygems security policy \fIpolicy\fR, where policy is one of \fBHighSecurity\fR, \fBMediumSecurity\fR, \fBLowSecurity\fR, \fBAlmostNoSecurity\fR, or \fBNoSecurity\fR\. For more details, please see the Rubygems signing documentation linked below in \fISEE ALSO\fR\.
+.TP
+\fB\-\-target\-rbconfig=TARGET\-RBCONFIG\fR
+Path to rbconfig\.rb for the deployment target platform\.
+.SH "DEPLOYMENT MODE"
+Bundler's defaults are optimized for development\. To switch to defaults optimized for deployment and for CI, use the \fBdeployment\fR setting\. Do not activate deployment mode on development machines, as it will cause an error when the Gemfile(5) is modified\.
+.IP "1." 4
+A \fBGemfile\.lock\fR is required\.
+.IP
+To ensure that the same versions of the gems you developed with and tested with are also used in deployments, a \fBGemfile\.lock\fR is required\.
+.IP
+This is mainly to ensure that you remember to check your \fBGemfile\.lock\fR into version control\.
+.IP "2." 4
+The \fBGemfile\.lock\fR must be up to date
+.IP
+In development, you can modify your Gemfile(5) and re\-run \fBbundle install\fR to \fIconservatively update\fR your \fBGemfile\.lock\fR snapshot\.
+.IP
+In deployment, your \fBGemfile\.lock\fR should be up\-to\-date with changes made in your Gemfile(5)\.
+.IP "3." 4
+Gems are installed to \fBvendor/bundle\fR not your default system location
+.IP
+In development, it's convenient to share the gems used in your application with other applications and other scripts that run on the system\.
+.IP
+In deployment, isolation is a more important default\. In addition, the user deploying the application may not have permission to install gems to the system, or the web server may not have permission to read them\.
+.IP
+As a result, when \fBdeployment\fR is configured, \fBbundle install\fR installs gems to the \fBvendor/bundle\fR directory in the application\. This may be overridden using the \fBpath\fR setting\.
+.IP "" 0
+.SH "INSTALLING GROUPS"
+By default, \fBbundle install\fR will install all gems in all groups in your Gemfile(5), except those declared for a different platform\.
+.P
+However, you can explicitly tell Bundler to skip installing certain groups with the \fBwithout\fR setting\. This setting takes a space\-separated list of groups\.
+.P
+While the \fBwithout\fR setting will skip \fIinstalling\fR the gems in the specified groups, \fBbundle install\fR will still \fIdownload\fR those gems and use them to resolve the dependencies of every gem in your Gemfile(5)\.
+.P
+This is so that installing a different set of groups on another machine (such as a production server) will not change the gems and versions that you have already developed and tested against\.
+.P
+\fBBundler offers a rock\-solid guarantee that the third\-party code you are running in development and testing is also the third\-party code you are running in production\. You can choose to exclude some of that code in different environments, but you will never be caught flat\-footed by different versions of third\-party code being used in different environments\.\fR
+.P
+For a simple illustration, consider the following Gemfile(5):
+.IP "" 4
+.nf
+source 'https://rubygems\.org'
+
+gem 'sinatra'
+
+group :production do
+ gem 'rack\-perftools\-profiler'
+end
+.fi
+.IP "" 0
+.P
+In this case, \fBsinatra\fR depends on any version of Rack (\fB>= 1\.0\fR), while \fBrack\-perftools\-profiler\fR depends on 1\.x (\fB~> 1\.0\fR)\.
+.P
+When you configure \fBbundle config without production\fR in development, we look at the dependencies of \fBrack\-perftools\-profiler\fR as well\. That way, you do not spend all your time developing against Rack 2\.0, using new APIs unavailable in Rack 1\.x, only to have Bundler switch to Rack 1\.2 when the \fBproduction\fR group \fIis\fR used\.
+.P
+This should not cause any problems in practice, because we do not attempt to \fBinstall\fR the gems in the excluded groups, and only evaluate as part of the dependency resolution process\.
+.P
+This also means that you cannot include different versions of the same gem in different groups, because doing so would result in different sets of dependencies used in development and production\. Because of the vagaries of the dependency resolution process, this usually affects more than the gems you list in your Gemfile(5), and can (surprisingly) radically change the gems you are using\.
+.SH "THE GEMFILE\.LOCK"
+When you run \fBbundle install\fR, Bundler will persist the full names and versions of all gems that you used (including dependencies of the gems specified in the Gemfile(5)) into a file called \fBGemfile\.lock\fR\.
+.P
+Bundler uses this file in all subsequent calls to \fBbundle install\fR, which guarantees that you always use the same exact code, even as your application moves across machines\.
+.P
+Because of the way dependency resolution works, even a seemingly small change (for instance, an update to a point\-release of a dependency of a gem in your Gemfile(5)) can result in radically different gems being needed to satisfy all dependencies\.
+.P
+As a result, you \fBSHOULD\fR check your \fBGemfile\.lock\fR into version control, in both applications and gems\. If you do not, every machine that checks out your repository (including your production server) will resolve all dependencies again, which will result in different versions of third\-party code being used if \fBany\fR of the gems in the Gemfile(5) or any of their dependencies have been updated\.
+.P
+When Bundler first shipped, the \fBGemfile\.lock\fR was included in the \fB\.gitignore\fR file included with generated gems\. Over time, however, it became clear that this practice forces the pain of broken dependencies onto new contributors, while leaving existing contributors potentially unaware of the problem\. Since \fBbundle install\fR is usually the first step towards a contribution, the pain of broken dependencies would discourage new contributors from contributing\. As a result, we have revised our guidance for gem authors to now recommend checking in the lock for gems\.
+.SH "CONSERVATIVE UPDATING"
+When you make a change to the Gemfile(5) and then run \fBbundle install\fR, Bundler will update only the gems that you modified\.
+.P
+In other words, if a gem that you \fBdid not modify\fR worked before you called \fBbundle install\fR, it will continue to use the exact same versions of all dependencies as it used before the update\.
+.P
+Let's take a look at an example\. Here's your original Gemfile(5):
+.IP "" 4
+.nf
+source 'https://rubygems\.org'
+
+gem 'actionpack', '2\.3\.8'
+gem 'activemerchant'
+.fi
+.IP "" 0
+.P
+In this case, both \fBactionpack\fR and \fBactivemerchant\fR depend on \fBactivesupport\fR\. The \fBactionpack\fR gem depends on \fBactivesupport 2\.3\.8\fR and \fBrack ~> 1\.1\.0\fR, while the \fBactivemerchant\fR gem depends on \fBactivesupport >= 2\.3\.2\fR, \fBbraintree >= 2\.0\.0\fR, and \fBbuilder >= 2\.0\.0\fR\.
+.P
+When the dependencies are first resolved, Bundler will select \fBactivesupport 2\.3\.8\fR, which satisfies the requirements of both gems in your Gemfile(5)\.
+.P
+Next, you modify your Gemfile(5) to:
+.IP "" 4
+.nf
+source 'https://rubygems\.org'
+
+gem 'actionpack', '3\.0\.0\.rc'
+gem 'activemerchant'
+.fi
+.IP "" 0
+.P
+The \fBactionpack 3\.0\.0\.rc\fR gem has a number of new dependencies, and updates the \fBactivesupport\fR dependency to \fB= 3\.0\.0\.rc\fR and the \fBrack\fR dependency to \fB~> 1\.2\.1\fR\.
+.P
+When you run \fBbundle install\fR, Bundler notices that you changed the \fBactionpack\fR gem, but not the \fBactivemerchant\fR gem\. It evaluates the gems currently being used to satisfy its requirements:
+.TP
+\fBactivesupport 2\.3\.8\fR
+also used to satisfy a dependency in \fBactivemerchant\fR, which is not being updated
+.TP
+\fBrack ~> 1\.1\.0\fR
+not currently being used to satisfy another dependency
+.P
+Because you did not explicitly ask to update \fBactivemerchant\fR, you would not expect it to suddenly stop working after updating \fBactionpack\fR\. However, satisfying the new \fBactivesupport 3\.0\.0\.rc\fR dependency of actionpack requires updating one of its dependencies\.
+.P
+Even though \fBactivemerchant\fR declares a very loose dependency that theoretically matches \fBactivesupport 3\.0\.0\.rc\fR, Bundler treats gems in your Gemfile(5) that have not changed as an atomic unit together with their dependencies\. In this case, the \fBactivemerchant\fR dependency is treated as \fBactivemerchant 1\.7\.1 + activesupport 2\.3\.8\fR, so \fBbundle install\fR will report that it cannot update \fBactionpack\fR\.
+.P
+To explicitly update \fBactionpack\fR, including its dependencies which other gems in the Gemfile(5) still depend on, run \fBbundle update actionpack\fR (see \fBbundle update(1)\fR)\.
+.P
+\fBSummary\fR: In general, after making a change to the Gemfile(5) , you should first try to run \fBbundle install\fR, which will guarantee that no other gem in the Gemfile(5) is impacted by the change\. If that does not work, run bundle update(1) \fIbundle\-update\.1\.html\fR\.
+.SH "SEE ALSO"
+.IP "\(bu" 4
+Gem install docs \fIhttps://guides\.rubygems\.org/rubygems\-basics/#installing\-gems\fR
+.IP "\(bu" 4
+Rubygems signing docs \fIhttps://guides\.rubygems\.org/security/\fR
+.IP "" 0
+
diff --git a/lib/bundler/man/bundle-install.1.ronn b/lib/bundler/man/bundle-install.1.ronn
new file mode 100644
index 0000000000..56fd8bdf42
--- /dev/null
+++ b/lib/bundler/man/bundle-install.1.ronn
@@ -0,0 +1,314 @@
+bundle-install(1) -- Install the dependencies specified in your Gemfile
+=======================================================================
+
+## SYNOPSIS
+
+`bundle install` [--cooldown=NUMBER]
+ [--force]
+ [--full-index]
+ [--gemfile=GEMFILE]
+ [--jobs=NUMBER]
+ [--local]
+ [--lockfile=LOCKFILE]
+ [--no-cache]
+ [--no-lock]
+ [--prefer-local]
+ [--quiet]
+ [--retry=NUMBER]
+ [--standalone[=GROUP[ GROUP...]]]
+ [--trust-policy=TRUST-POLICY]
+ [--target-rbconfig=TARGET-RBCONFIG]
+
+## DESCRIPTION
+
+Install the gems specified in your Gemfile(5). If this is the first
+time you run bundle install (and a `Gemfile.lock` does not exist),
+Bundler will fetch all remote sources, resolve dependencies and
+install all needed gems.
+
+If a `Gemfile.lock` does exist, and you have not updated your Gemfile(5),
+Bundler will fetch all remote sources, but use the dependencies
+specified in the `Gemfile.lock` instead of resolving dependencies.
+
+If a `Gemfile.lock` does exist, and you have updated your Gemfile(5),
+Bundler will use the dependencies in the `Gemfile.lock` for all gems
+that you did not update, but will re-resolve the dependencies of
+gems that you did update. You can find more information about this
+update process below under [CONSERVATIVE UPDATING][].
+
+## OPTIONS
+
+* `--cooldown=<number>`:
+ Only consider gem versions published at least <number> days ago when
+ resolving. Pass `0` to disable cooldown for this run, overriding any
+ per-source or global configuration. See `cooldown` in bundle-config(1)
+ for details on the precedence between the CLI flag, Bundler config,
+ and Gemfile per-source settings.
+
+* `--force`, `--redownload`:
+ Force reinstalling every gem, even if already installed.
+
+* `--full-index`:
+ Bundler will not call Rubygems' API endpoint (default) but download and cache
+ a (currently big) index file of all gems. Performance can be improved for
+ large bundles that seldom change by enabling this option.
+
+* `--gemfile=GEMFILE`:
+ The location of the Gemfile(5) which Bundler should use. This defaults
+ to a Gemfile(5) in the current working directory. In general, Bundler
+ will assume that the location of the Gemfile(5) is also the project's
+ root and will try to find `Gemfile.lock` and `vendor/cache` relative
+ to this location.
+
+* `--jobs=<number>`, `-j=<number>`:
+ The maximum number of parallel download and install jobs. The default is the
+ number of available processors.
+
+* `--local`:
+ Do not attempt to connect to `rubygems.org`. Instead, Bundler will use the
+ gems already present in Rubygems' cache or in `vendor/cache`. Note that if an
+ appropriate platform-specific gem exists on `rubygems.org` it will not be
+ found.
+
+* `--lockfile=LOCKFILE`:
+ The location of the lockfile which Bundler should use. This defaults
+ to the Gemfile location with `.lock` appended.
+
+* `--prefer-local`:
+ Force using locally installed gems, or gems already present in Rubygems' cache
+ or in `vendor/cache`, when resolving, even if newer versions are available
+ remotely. Only attempt to connect to `rubygems.org` for gems that are not
+ present locally.
+
+* `--no-cache`:
+ Do not update the cache in `vendor/cache` with the newly bundled gems. This
+ does not remove any gems in the cache but keeps the newly bundled gems from
+ being cached during the install.
+
+* `--no-lock`:
+ Do not create a lockfile. Useful if you want to install dependencies but not
+ lock versions of gems. Recommended for library development, and other
+ situations where the code is expected to work with a range of dependency
+ versions.
+
+ This has the same effect as using `lockfile false` in the Gemfile.
+ See gemfile(5) for more information.
+
+* `--quiet`:
+ Do not print progress information to the standard output.
+
+* `--retry=[<number>]`:
+ Retry failed network or git requests for <number> times.
+
+* `--standalone[=<list>]`:
+ Makes a bundle that can work without depending on Rubygems or Bundler at
+ runtime. A space separated list of groups to install can be specified.
+ Bundler creates a directory named `bundle` and installs the bundle there. It
+ also generates a `bundle/bundler/setup.rb` file to replace Bundler's own setup
+ in the manner required.
+
+* `--trust-policy=TRUST-POLICY`:
+ Apply the Rubygems security policy <policy>, where policy is one of
+ `HighSecurity`, `MediumSecurity`, `LowSecurity`, `AlmostNoSecurity`, or
+ `NoSecurity`. For more details, please see the Rubygems signing documentation
+ linked below in [SEE ALSO][].
+
+* `--target-rbconfig=TARGET-RBCONFIG`:
+ Path to rbconfig.rb for the deployment target platform.
+
+## DEPLOYMENT MODE
+
+Bundler's defaults are optimized for development. To switch to
+defaults optimized for deployment and for CI, use the `deployment`
+setting. Do not activate deployment mode on development machines, as it
+will cause an error when the Gemfile(5) is modified.
+
+1. A `Gemfile.lock` is required.
+
+ To ensure that the same versions of the gems you developed with
+ and tested with are also used in deployments, a `Gemfile.lock`
+ is required.
+
+ This is mainly to ensure that you remember to check your
+ `Gemfile.lock` into version control.
+
+2. The `Gemfile.lock` must be up to date
+
+ In development, you can modify your Gemfile(5) and re-run
+ `bundle install` to [conservatively update][CONSERVATIVE UPDATING]
+ your `Gemfile.lock` snapshot.
+
+ In deployment, your `Gemfile.lock` should be up-to-date with
+ changes made in your Gemfile(5).
+
+3. Gems are installed to `vendor/bundle` not your default system location
+
+ In development, it's convenient to share the gems used in your
+ application with other applications and other scripts that run on
+ the system.
+
+ In deployment, isolation is a more important default. In addition,
+ the user deploying the application may not have permission to install
+ gems to the system, or the web server may not have permission to
+ read them.
+
+ As a result, when `deployment` is configured, `bundle install` installs gems
+ to the `vendor/bundle` directory in the application. This may be
+ overridden using the `path` setting.
+
+## INSTALLING GROUPS
+
+By default, `bundle install` will install all gems in all groups
+in your Gemfile(5), except those declared for a different platform.
+
+However, you can explicitly tell Bundler to skip installing
+certain groups with the `without` setting. This setting takes
+a space-separated list of groups.
+
+While the `without` setting will skip _installing_ the gems in the
+specified groups, `bundle install` will still _download_ those gems and use them
+to resolve the dependencies of every gem in your Gemfile(5).
+
+This is so that installing a different set of groups on another
+ machine (such as a production server) will not change the
+gems and versions that you have already developed and tested against.
+
+`Bundler offers a rock-solid guarantee that the third-party
+code you are running in development and testing is also the
+third-party code you are running in production. You can choose
+to exclude some of that code in different environments, but you
+will never be caught flat-footed by different versions of
+third-party code being used in different environments.`
+
+For a simple illustration, consider the following Gemfile(5):
+
+ source 'https://rubygems.org'
+
+ gem 'sinatra'
+
+ group :production do
+ gem 'rack-perftools-profiler'
+ end
+
+In this case, `sinatra` depends on any version of Rack (`>= 1.0`), while
+`rack-perftools-profiler` depends on 1.x (`~> 1.0`).
+
+When you configure `bundle config without production` in development, we
+look at the dependencies of `rack-perftools-profiler` as well. That way,
+you do not spend all your time developing against Rack 2.0, using new
+APIs unavailable in Rack 1.x, only to have Bundler switch to Rack 1.2
+when the `production` group _is_ used.
+
+This should not cause any problems in practice, because we do not
+attempt to `install` the gems in the excluded groups, and only evaluate
+as part of the dependency resolution process.
+
+This also means that you cannot include different versions of the same
+gem in different groups, because doing so would result in different
+sets of dependencies used in development and production. Because of
+the vagaries of the dependency resolution process, this usually
+affects more than the gems you list in your Gemfile(5), and can
+(surprisingly) radically change the gems you are using.
+
+## THE GEMFILE.LOCK
+
+When you run `bundle install`, Bundler will persist the full names
+and versions of all gems that you used (including dependencies of
+the gems specified in the Gemfile(5)) into a file called `Gemfile.lock`.
+
+Bundler uses this file in all subsequent calls to `bundle install`,
+which guarantees that you always use the same exact code, even
+as your application moves across machines.
+
+Because of the way dependency resolution works, even a
+seemingly small change (for instance, an update to a point-release
+of a dependency of a gem in your Gemfile(5)) can result in radically
+different gems being needed to satisfy all dependencies.
+
+As a result, you `SHOULD` check your `Gemfile.lock` into version
+control, in both applications and gems. If you do not, every machine that
+checks out your repository (including your production server) will resolve all
+dependencies again, which will result in different versions of
+third-party code being used if `any` of the gems in the Gemfile(5)
+or any of their dependencies have been updated.
+
+When Bundler first shipped, the `Gemfile.lock` was included in the `.gitignore`
+file included with generated gems. Over time, however, it became clear that
+this practice forces the pain of broken dependencies onto new contributors,
+while leaving existing contributors potentially unaware of the problem. Since
+`bundle install` is usually the first step towards a contribution, the pain of
+broken dependencies would discourage new contributors from contributing. As a
+result, we have revised our guidance for gem authors to now recommend checking
+in the lock for gems.
+
+## CONSERVATIVE UPDATING
+
+When you make a change to the Gemfile(5) and then run `bundle install`,
+Bundler will update only the gems that you modified.
+
+In other words, if a gem that you `did not modify` worked before
+you called `bundle install`, it will continue to use the exact
+same versions of all dependencies as it used before the update.
+
+Let's take a look at an example. Here's your original Gemfile(5):
+
+ source 'https://rubygems.org'
+
+ gem 'actionpack', '2.3.8'
+ gem 'activemerchant'
+
+In this case, both `actionpack` and `activemerchant` depend on
+`activesupport`. The `actionpack` gem depends on `activesupport 2.3.8`
+and `rack ~> 1.1.0`, while the `activemerchant` gem depends on
+`activesupport >= 2.3.2`, `braintree >= 2.0.0`, and `builder >= 2.0.0`.
+
+When the dependencies are first resolved, Bundler will select
+`activesupport 2.3.8`, which satisfies the requirements of both
+gems in your Gemfile(5).
+
+Next, you modify your Gemfile(5) to:
+
+ source 'https://rubygems.org'
+
+ gem 'actionpack', '3.0.0.rc'
+ gem 'activemerchant'
+
+The `actionpack 3.0.0.rc` gem has a number of new dependencies,
+and updates the `activesupport` dependency to `= 3.0.0.rc` and
+the `rack` dependency to `~> 1.2.1`.
+
+When you run `bundle install`, Bundler notices that you changed
+the `actionpack` gem, but not the `activemerchant` gem. It
+evaluates the gems currently being used to satisfy its requirements:
+
+ * `activesupport 2.3.8`:
+ also used to satisfy a dependency in `activemerchant`,
+ which is not being updated
+ * `rack ~> 1.1.0`:
+ not currently being used to satisfy another dependency
+
+Because you did not explicitly ask to update `activemerchant`,
+you would not expect it to suddenly stop working after updating
+`actionpack`. However, satisfying the new `activesupport 3.0.0.rc`
+dependency of actionpack requires updating one of its dependencies.
+
+Even though `activemerchant` declares a very loose dependency
+that theoretically matches `activesupport 3.0.0.rc`, Bundler treats
+gems in your Gemfile(5) that have not changed as an atomic unit
+together with their dependencies. In this case, the `activemerchant`
+dependency is treated as `activemerchant 1.7.1 + activesupport 2.3.8`,
+so `bundle install` will report that it cannot update `actionpack`.
+
+To explicitly update `actionpack`, including its dependencies
+which other gems in the Gemfile(5) still depend on, run
+`bundle update actionpack` (see `bundle update(1)`).
+
+`Summary`: In general, after making a change to the Gemfile(5) , you
+should first try to run `bundle install`, which will guarantee that no
+other gem in the Gemfile(5) is impacted by the change. If that
+does not work, run [bundle update(1)](bundle-update.1.html).
+
+## SEE ALSO
+
+* [Gem install docs](https://guides.rubygems.org/rubygems-basics/#installing-gems)
+* [Rubygems signing docs](https://guides.rubygems.org/security/)
diff --git a/lib/bundler/man/bundle-issue.1 b/lib/bundler/man/bundle-issue.1
new file mode 100644
index 0000000000..3af277ef86
--- /dev/null
+++ b/lib/bundler/man/bundle-issue.1
@@ -0,0 +1,45 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-ISSUE" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-issue\fR \- Get help reporting Bundler issues
+.SH "SYNOPSIS"
+\fBbundle issue\fR
+.SH "DESCRIPTION"
+Provides guidance on reporting Bundler issues and outputs detailed system information that should be included when filing a bug report\. This command:
+.IP "1." 4
+Displays links to troubleshooting resources
+.IP "2." 4
+Shows instructions for reporting issues
+.IP "3." 4
+Outputs comprehensive environment information needed for debugging
+.IP "" 0
+.P
+The command helps ensure that bug reports include all necessary system details for effective troubleshooting\.
+.SH "OUTPUT"
+The command outputs several sections:
+.IP "\(bu" 4
+Troubleshooting links and resources
+.IP "\(bu" 4
+Link to the GitHub issue template
+.IP "\(bu" 4
+Environment information including: Bundler version and platforms, Ruby version and configuration, RubyGems version and paths, Development tool versions (Git, RVM, rbenv, chruby)
+.IP "\(bu" 4
+Bundler build metadata
+.IP "\(bu" 4
+Current Bundler settings
+.IP "\(bu" 4
+Bundle Doctor output
+.IP "" 0
+.SH "EXAMPLES"
+Get issue reporting information:
+.IP "" 4
+.nf
+$ bundle issue
+.fi
+.IP "" 0
+.SH "SEE ALSO"
+.IP "\(bu" 4
+bundle\-doctor(1)
+.IP "" 0
+
diff --git a/lib/bundler/man/bundle-issue.1.ronn b/lib/bundler/man/bundle-issue.1.ronn
new file mode 100644
index 0000000000..37f676a354
--- /dev/null
+++ b/lib/bundler/man/bundle-issue.1.ronn
@@ -0,0 +1,37 @@
+bundle-issue(1) -- Get help reporting Bundler issues
+====================================================
+
+## SYNOPSIS
+
+`bundle issue`
+
+## DESCRIPTION
+
+Provides guidance on reporting Bundler issues and outputs detailed system information that should be included when filing a bug report. This command:
+
+1. Displays links to troubleshooting resources
+2. Shows instructions for reporting issues
+3. Outputs comprehensive environment information needed for debugging
+
+The command helps ensure that bug reports include all necessary system details for effective troubleshooting.
+
+## OUTPUT
+
+The command outputs several sections:
+
+* Troubleshooting links and resources
+* Link to the GitHub issue template
+* Environment information including: Bundler version and platforms, Ruby version and configuration, RubyGems version and paths, Development tool versions (Git, RVM, rbenv, chruby)
+* Bundler build metadata
+* Current Bundler settings
+* Bundle Doctor output
+
+## EXAMPLES
+
+Get issue reporting information:
+
+ $ bundle issue
+
+## SEE ALSO
+
+* bundle-doctor(1)
diff --git a/lib/bundler/man/bundle-licenses.1 b/lib/bundler/man/bundle-licenses.1
new file mode 100644
index 0000000000..ab5996d2be
--- /dev/null
+++ b/lib/bundler/man/bundle-licenses.1
@@ -0,0 +1,9 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-LICENSES" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-licenses\fR \- Print the license of all gems in the bundle
+.SH "SYNOPSIS"
+\fBbundle licenses\fR
+.SH "DESCRIPTION"
+Prints the license of all gems in the bundle\.
diff --git a/lib/bundler/man/bundle-licenses.1.ronn b/lib/bundler/man/bundle-licenses.1.ronn
new file mode 100644
index 0000000000..91caba6c2a
--- /dev/null
+++ b/lib/bundler/man/bundle-licenses.1.ronn
@@ -0,0 +1,10 @@
+bundle-licenses(1) -- Print the license of all gems in the bundle
+=================================================================
+
+## SYNOPSIS
+
+`bundle licenses`
+
+## DESCRIPTION
+
+Prints the license of all gems in the bundle.
diff --git a/lib/bundler/man/bundle-list.1 b/lib/bundler/man/bundle-list.1
new file mode 100644
index 0000000000..e759e0d449
--- /dev/null
+++ b/lib/bundler/man/bundle-list.1
@@ -0,0 +1,40 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-LIST" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-list\fR \- List all the gems in the bundle
+.SH "SYNOPSIS"
+\fBbundle list\fR [\-\-name\-only] [\-\-paths] [\-\-without\-group=GROUP[ GROUP\|\.\|\.\|\.]] [\-\-only\-group=GROUP[ GROUP\|\.\|\.\|\.]]
+.SH "DESCRIPTION"
+Prints a list of all the gems in the bundle including their version\.
+.P
+Example:
+.P
+bundle list \-\-name\-only
+.P
+bundle list \-\-paths
+.P
+bundle list \-\-without\-group test
+.P
+bundle list \-\-only\-group dev
+.P
+bundle list \-\-only\-group dev test \-\-paths
+.P
+bundle list \-\-format json
+.SH "OPTIONS"
+.TP
+\fB\-\-name\-only\fR
+Print only the name of each gem\.
+.TP
+\fB\-\-paths\fR
+Print the path to each gem in the bundle\.
+.TP
+\fB\-\-without\-group=<list>\fR
+A space\-separated list of groups of gems to skip during printing\.
+.TP
+\fB\-\-only\-group=<list>\fR
+A space\-separated list of groups of gems to print\.
+.TP
+\fB\-\-format=FORMAT\fR
+Format output ('json' is the only supported format)
+
diff --git a/lib/bundler/man/bundle-list.1.ronn b/lib/bundler/man/bundle-list.1.ronn
new file mode 100644
index 0000000000..9ec2b13282
--- /dev/null
+++ b/lib/bundler/man/bundle-list.1.ronn
@@ -0,0 +1,41 @@
+bundle-list(1) -- List all the gems in the bundle
+=================================================
+
+## SYNOPSIS
+
+`bundle list` [--name-only] [--paths] [--without-group=GROUP[ GROUP...]] [--only-group=GROUP[ GROUP...]]
+
+## DESCRIPTION
+
+Prints a list of all the gems in the bundle including their version.
+
+Example:
+
+bundle list --name-only
+
+bundle list --paths
+
+bundle list --without-group test
+
+bundle list --only-group dev
+
+bundle list --only-group dev test --paths
+
+bundle list --format json
+
+## OPTIONS
+
+* `--name-only`:
+ Print only the name of each gem.
+
+* `--paths`:
+ Print the path to each gem in the bundle.
+
+* `--without-group=<list>`:
+ A space-separated list of groups of gems to skip during printing.
+
+* `--only-group=<list>`:
+ A space-separated list of groups of gems to print.
+
+* `--format=FORMAT`:
+ Format output ('json' is the only supported format)
diff --git a/lib/bundler/man/bundle-lock.1 b/lib/bundler/man/bundle-lock.1
new file mode 100644
index 0000000000..396c8ff6ca
--- /dev/null
+++ b/lib/bundler/man/bundle-lock.1
@@ -0,0 +1,75 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-LOCK" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-lock\fR \- Creates / Updates a lockfile without installing
+.SH "SYNOPSIS"
+\fBbundle lock\fR [\-\-update] [\-\-bundler[=BUNDLER]] [\-\-local] [\-\-print] [\-\-lockfile=PATH] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-add\-checksums] [\-\-add\-platform] [\-\-remove\-platform] [\-\-normalize\-platforms] [\-\-patch] [\-\-minor] [\-\-major] [\-\-pre] [\-\-strict] [\-\-conservative]
+.SH "DESCRIPTION"
+Lock the gems specified in Gemfile\.
+.SH "OPTIONS"
+.TP
+\fB\-\-update[=<list>]\fR
+Ignores the existing lockfile\. Resolve then updates lockfile\. Taking a list of gems or updating all gems if no list is given\.
+.TP
+\fB\-\-bundler[=BUNDLER]\fR
+Update the locked version of bundler to the given version or the latest version if no version is given\.
+.TP
+\fB\-\-local\fR
+Do not attempt to connect to \fBrubygems\.org\fR\. Instead, Bundler will use the gems already present in Rubygems' cache or in \fBvendor/cache\fR\. Note that if a appropriate platform\-specific gem exists on \fBrubygems\.org\fR it will not be found\.
+.TP
+\fB\-\-print\fR
+Prints the lockfile to STDOUT instead of writing to the file system\.
+.TP
+\fB\-\-lockfile=LOCKFILE\fR
+The path where the lockfile should be written to\.
+.TP
+\fB\-\-full\-index\fR
+Fall back to using the single\-file index of all gems\.
+.TP
+\fB\-\-gemfile=GEMFILE\fR
+Use the specified gemfile instead of [\fBGemfile(5)\fR][Gemfile(5)]\.
+.TP
+\fB\-\-add\-checksums\fR
+Add checksums to the lockfile\.
+.TP
+\fB\-\-add\-platform=<list>\fR
+Add a new platform to the lockfile, re\-resolving for the addition of that platform\.
+.TP
+\fB\-\-remove\-platform=<list>\fR
+Remove a platform from the lockfile\.
+.TP
+\fB\-\-normalize\-platforms\fR
+Normalize lockfile platforms\.
+.TP
+\fB\-\-patch\fR
+If updating, prefer updating only to next patch version\.
+.TP
+\fB\-\-minor\fR
+If updating, prefer updating only to next minor version\.
+.TP
+\fB\-\-major\fR
+If updating, prefer updating to next major version (default)\.
+.TP
+\fB\-\-pre\fR
+If updating, always choose the highest allowed version, regardless of prerelease status\.
+.TP
+\fB\-\-strict\fR
+If updating, do not allow any gem to be updated past latest \-\-patch | \-\-minor | \-\-major\.
+.TP
+\fB\-\-conservative\fR
+If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated\.
+.SH "UPDATING ALL GEMS"
+If you run \fBbundle lock\fR with \fB\-\-update\fR option without list of gems, bundler will ignore any previously installed gems and resolve all dependencies again based on the latest versions of all gems available in the sources\.
+.SH "UPDATING A LIST OF GEMS"
+Sometimes, you want to update a single gem in the Gemfile(5), and leave the rest of the gems that you specified locked to the versions in the \fBGemfile\.lock\fR\.
+.P
+For instance, you only want to update \fBnokogiri\fR, run \fBbundle lock \-\-update nokogiri\fR\.
+.P
+Bundler will update \fBnokogiri\fR and any of its dependencies, but leave the rest of the gems that you specified locked to the versions in the \fBGemfile\.lock\fR\.
+.SH "SUPPORTING OTHER PLATFORMS"
+If you want your bundle to support platforms other than the one you're running locally, you can run \fBbundle lock \-\-add\-platform PLATFORM\fR to add PLATFORM to the lockfile, force bundler to re\-resolve and consider the new platform when picking gems, all without needing to have a machine that matches PLATFORM handy to install those platform\-specific gems on\.
+.P
+For a full explanation of gem platforms, see \fBgem help platform\fR\.
+.SH "PATCH LEVEL OPTIONS"
+See bundle update(1) \fIbundle\-update\.1\.html\fR for details\.
diff --git a/lib/bundler/man/bundle-lock.1.ronn b/lib/bundler/man/bundle-lock.1.ronn
new file mode 100644
index 0000000000..6d3e63c982
--- /dev/null
+++ b/lib/bundler/man/bundle-lock.1.ronn
@@ -0,0 +1,115 @@
+bundle-lock(1) -- Creates / Updates a lockfile without installing
+=================================================================
+
+## SYNOPSIS
+
+`bundle lock` [--update]
+ [--bundler[=BUNDLER]]
+ [--local]
+ [--print]
+ [--lockfile=PATH]
+ [--full-index]
+ [--gemfile=GEMFILE]
+ [--add-checksums]
+ [--add-platform]
+ [--remove-platform]
+ [--normalize-platforms]
+ [--patch]
+ [--minor]
+ [--major]
+ [--pre]
+ [--strict]
+ [--conservative]
+
+## DESCRIPTION
+
+Lock the gems specified in Gemfile.
+
+## OPTIONS
+
+* `--update[=<list>]`:
+ Ignores the existing lockfile. Resolve then updates lockfile. Taking a list
+ of gems or updating all gems if no list is given.
+
+* `--bundler[=BUNDLER]`:
+ Update the locked version of bundler to the given version or the latest
+ version if no version is given.
+
+* `--local`:
+ Do not attempt to connect to `rubygems.org`. Instead, Bundler will use the
+ gems already present in Rubygems' cache or in `vendor/cache`. Note that if a
+ appropriate platform-specific gem exists on `rubygems.org` it will not be
+ found.
+
+* `--print`:
+ Prints the lockfile to STDOUT instead of writing to the file system.
+
+* `--lockfile=LOCKFILE`:
+ The path where the lockfile should be written to.
+
+* `--full-index`:
+ Fall back to using the single-file index of all gems.
+
+* `--gemfile=GEMFILE`:
+ Use the specified gemfile instead of [`Gemfile(5)`][Gemfile(5)].
+
+* `--add-checksums`:
+ Add checksums to the lockfile.
+
+* `--add-platform=<list>`:
+ Add a new platform to the lockfile, re-resolving for the addition of that
+ platform.
+
+* `--remove-platform=<list>`:
+ Remove a platform from the lockfile.
+
+* `--normalize-platforms`:
+ Normalize lockfile platforms.
+
+* `--patch`:
+ If updating, prefer updating only to next patch version.
+
+* `--minor`:
+ If updating, prefer updating only to next minor version.
+
+* `--major`:
+ If updating, prefer updating to next major version (default).
+
+* `--pre`:
+ If updating, always choose the highest allowed version, regardless of prerelease status.
+
+* `--strict`:
+ If updating, do not allow any gem to be updated past latest --patch | --minor | --major.
+
+* `--conservative`:
+ If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated.
+
+## UPDATING ALL GEMS
+
+If you run `bundle lock` with `--update` option without list of gems, bundler will
+ignore any previously installed gems and resolve all dependencies again based
+on the latest versions of all gems available in the sources.
+
+## UPDATING A LIST OF GEMS
+
+Sometimes, you want to update a single gem in the Gemfile(5), and leave the rest of
+the gems that you specified locked to the versions in the `Gemfile.lock`.
+
+For instance, you only want to update `nokogiri`, run `bundle lock --update nokogiri`.
+
+Bundler will update `nokogiri` and any of its dependencies, but leave the rest of the
+gems that you specified locked to the versions in the `Gemfile.lock`.
+
+## SUPPORTING OTHER PLATFORMS
+
+If you want your bundle to support platforms other than the one you're running
+locally, you can run `bundle lock --add-platform PLATFORM` to add PLATFORM to
+the lockfile, force bundler to re-resolve and consider the new platform when
+picking gems, all without needing to have a machine that matches PLATFORM handy
+to install those platform-specific gems on.
+
+For a full explanation of gem platforms, see `gem help platform`.
+
+## PATCH LEVEL OPTIONS
+
+See [bundle update(1)](bundle-update.1.html) for details.
diff --git a/lib/bundler/man/bundle-open.1 b/lib/bundler/man/bundle-open.1
new file mode 100644
index 0000000000..2aab59f14b
--- /dev/null
+++ b/lib/bundler/man/bundle-open.1
@@ -0,0 +1,32 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-OPEN" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-open\fR \- Opens the source directory for a gem in your bundle
+.SH "SYNOPSIS"
+\fBbundle open\fR [GEM] [\-\-path=PATH]
+.SH "DESCRIPTION"
+Opens the source directory of the provided GEM in your editor\.
+.P
+For this to work the \fBEDITOR\fR or \fBBUNDLER_EDITOR\fR environment variable has to be set\.
+.P
+Example:
+.IP "" 4
+.nf
+bundle open 'rack'
+.fi
+.IP "" 0
+.P
+Will open the source directory for the 'rack' gem in your bundle\.
+.IP "" 4
+.nf
+bundle open 'rack' \-\-path 'README\.md'
+.fi
+.IP "" 0
+.P
+Will open the README\.md file of the 'rack' gem source in your bundle\.
+.SH "OPTIONS"
+.TP
+\fB\-\-path[=PATH]\fR
+Specify GEM source relative path to open\.
+
diff --git a/lib/bundler/man/bundle-open.1.ronn b/lib/bundler/man/bundle-open.1.ronn
new file mode 100644
index 0000000000..24dbe97e44
--- /dev/null
+++ b/lib/bundler/man/bundle-open.1.ronn
@@ -0,0 +1,28 @@
+bundle-open(1) -- Opens the source directory for a gem in your bundle
+=====================================================================
+
+## SYNOPSIS
+
+`bundle open` [GEM] [--path=PATH]
+
+## DESCRIPTION
+
+Opens the source directory of the provided GEM in your editor.
+
+For this to work the `EDITOR` or `BUNDLER_EDITOR` environment variable has to
+be set.
+
+Example:
+
+ bundle open 'rack'
+
+Will open the source directory for the 'rack' gem in your bundle.
+
+ bundle open 'rack' --path 'README.md'
+
+Will open the README.md file of the 'rack' gem source in your bundle.
+
+## OPTIONS
+
+* `--path[=PATH]`:
+ Specify GEM source relative path to open.
diff --git a/lib/bundler/man/bundle-outdated.1 b/lib/bundler/man/bundle-outdated.1
new file mode 100644
index 0000000000..c2f8086e24
--- /dev/null
+++ b/lib/bundler/man/bundle-outdated.1
@@ -0,0 +1,106 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-OUTDATED" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-outdated\fR \- List installed gems with newer versions available
+.SH "SYNOPSIS"
+\fBbundle outdated\fR [GEM] [\-\-local] [\-\-pre] [\-\-source] [\-\-filter\-strict | \-\-strict] [\-\-update\-strict] [\-\-parseable | \-\-porcelain] [\-\-group=GROUP] [\-\-groups] [\-\-patch|\-\-minor|\-\-major] [\-\-filter\-major] [\-\-filter\-minor] [\-\-filter\-patch] [\-\-only\-explicit] [\-\-cooldown=NUMBER]
+.SH "DESCRIPTION"
+Outdated lists the names and versions of gems that have a newer version available in the given source\. Calling outdated with [GEM [GEM]] will only check for newer versions of the given gems\. Prerelease gems are ignored by default\. If your gems are up to date, Bundler will exit with a status of 0\. Otherwise, it will exit 1\.
+.SH "OPTIONS"
+.TP
+\fB\-\-local\fR
+Do not attempt to fetch gems remotely and use the gem cache instead\.
+.TP
+\fB\-\-pre\fR
+Check for newer pre\-release gems\.
+.TP
+\fB\-\-source=<list>\fR
+Check against a specific source\.
+.TP
+\fB\-\-filter\-strict\fR, \fB\-\-strict\fR
+Only list newer versions allowed by your Gemfile requirements, also respecting conservative update flags (\-\-patch, \-\-minor, \-\-major)\.
+.TP
+\fB\-\-update\-strict\fR
+Strict conservative resolution, do not allow any gem to be updated past latest \-\-patch | \-\-minor | \-\-major\.
+.TP
+\fB\-\-parseable\fR, \fB\-\-porcelain\fR
+Use minimal formatting for more parseable output\.
+.TP
+\fB\-\-group=GROUP\fR
+List gems from a specific group\.
+.TP
+\fB\-\-groups\fR
+List gems organized by groups\.
+.TP
+\fB\-\-minor\fR
+Prefer updating only to next minor version\.
+.TP
+\fB\-\-major\fR
+Prefer updating to next major version (default)\.
+.TP
+\fB\-\-patch\fR
+Prefer updating only to next patch version\.
+.TP
+\fB\-\-filter\-major\fR
+Only list major newer versions\.
+.TP
+\fB\-\-filter\-minor\fR
+Only list minor newer versions\.
+.TP
+\fB\-\-filter\-patch\fR
+Only list patch newer versions\.
+.TP
+\fB\-\-only\-explicit\fR
+Only list gems specified in your Gemfile, not their dependencies\.
+.TP
+\fB\-\-cooldown=<number>\fR
+Annotate (rather than hide) versions that are still inside the cooldown window of \fInumber\fR days\. The prose output appends "in cooldown for Nd more days" and the table form adds "(cooldown Nd)" to the Latest column\. See \fBcooldown\fR in bundle\-config(1)\.
+.SH "PATCH LEVEL OPTIONS"
+See bundle update(1) \fIbundle\-update\.1\.html\fR for details\.
+.SH "FILTERING OUTPUT"
+The 3 filtering options do not affect the resolution of versions, merely what versions are shown in the output\.
+.P
+If the regular output shows the following:
+.IP "" 4
+.nf
+* Gem Current Latest Requested Groups Release Date
+* faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test 2024\-02\-05
+* hashie 1\.2\.0 3\.4\.6 = 1\.2\.0 default 2023\-11\-10
+* headless 2\.2\.3 2\.3\.1 = 2\.2\.3 test 2022\-08\-19
+.fi
+.IP "" 0
+.P
+\fB\-\-filter\-major\fR would only show:
+.IP "" 4
+.nf
+* Gem Current Latest Requested Groups Release Date
+* hashie 1\.2\.0 3\.4\.6 = 1\.2\.0 default 2023\-11\-10
+.fi
+.IP "" 0
+.P
+\fB\-\-filter\-minor\fR would only show:
+.IP "" 4
+.nf
+* Gem Current Latest Requested Groups Release Date
+* headless 2\.2\.3 2\.3\.1 = 2\.2\.3 test 2022\-08\-19
+.fi
+.IP "" 0
+.P
+\fB\-\-filter\-patch\fR would only show:
+.IP "" 4
+.nf
+* Gem Current Latest Requested Groups Release Date
+* faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test 2024\-02\-05
+.fi
+.IP "" 0
+.P
+Filter options can be combined\. \fB\-\-filter\-minor\fR and \fB\-\-filter\-patch\fR would show:
+.IP "" 4
+.nf
+* Gem Current Latest Requested Groups Release Date
+* faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test 2024\-02\-05
+.fi
+.IP "" 0
+.P
+Combining all three \fBfilter\fR options would be the same result as providing none of them\.
diff --git a/lib/bundler/man/bundle-outdated.1.ronn b/lib/bundler/man/bundle-outdated.1.ronn
new file mode 100644
index 0000000000..e5badac2e9
--- /dev/null
+++ b/lib/bundler/man/bundle-outdated.1.ronn
@@ -0,0 +1,117 @@
+bundle-outdated(1) -- List installed gems with newer versions available
+=======================================================================
+
+## SYNOPSIS
+
+`bundle outdated` [GEM] [--local]
+ [--pre]
+ [--source]
+ [--filter-strict | --strict]
+ [--update-strict]
+ [--parseable | --porcelain]
+ [--group=GROUP]
+ [--groups]
+ [--patch|--minor|--major]
+ [--filter-major]
+ [--filter-minor]
+ [--filter-patch]
+ [--only-explicit]
+ [--cooldown=NUMBER]
+
+## DESCRIPTION
+
+Outdated lists the names and versions of gems that have a newer version available
+in the given source. Calling outdated with [GEM [GEM]] will only check for newer
+versions of the given gems. Prerelease gems are ignored by default. If your gems
+are up to date, Bundler will exit with a status of 0. Otherwise, it will exit 1.
+
+## OPTIONS
+
+* `--local`:
+ Do not attempt to fetch gems remotely and use the gem cache instead.
+
+* `--pre`:
+ Check for newer pre-release gems.
+
+* `--source=<list>`:
+ Check against a specific source.
+
+* `--filter-strict`, `--strict`:
+ Only list newer versions allowed by your Gemfile requirements, also respecting conservative update flags (--patch, --minor, --major).
+
+* `--update-strict`:
+ Strict conservative resolution, do not allow any gem to be updated past latest --patch | --minor | --major.
+
+* `--parseable`, `--porcelain`:
+ Use minimal formatting for more parseable output.
+
+* `--group=GROUP`:
+ List gems from a specific group.
+
+* `--groups`:
+ List gems organized by groups.
+
+* `--minor`:
+ Prefer updating only to next minor version.
+
+* `--major`:
+ Prefer updating to next major version (default).
+
+* `--patch`:
+ Prefer updating only to next patch version.
+
+* `--filter-major`:
+ Only list major newer versions.
+
+* `--filter-minor`:
+ Only list minor newer versions.
+
+* `--filter-patch`:
+ Only list patch newer versions.
+
+* `--only-explicit`:
+ Only list gems specified in your Gemfile, not their dependencies.
+
+* `--cooldown=<number>`:
+ Annotate (rather than hide) versions that are still inside the
+ cooldown window of <number> days. The prose output appends "in
+ cooldown for Nd more days" and the table form adds "(cooldown Nd)" to
+ the Latest column. See `cooldown` in bundle-config(1).
+
+## PATCH LEVEL OPTIONS
+
+See [bundle update(1)](bundle-update.1.html) for details.
+
+## FILTERING OUTPUT
+
+The 3 filtering options do not affect the resolution of versions, merely what versions are shown
+in the output.
+
+If the regular output shows the following:
+
+ * Gem Current Latest Requested Groups Release Date
+ * faker 1.6.5 1.6.6 ~> 1.4 development, test 2024-02-05
+ * hashie 1.2.0 3.4.6 = 1.2.0 default 2023-11-10
+ * headless 2.2.3 2.3.1 = 2.2.3 test 2022-08-19
+
+`--filter-major` would only show:
+
+ * Gem Current Latest Requested Groups Release Date
+ * hashie 1.2.0 3.4.6 = 1.2.0 default 2023-11-10
+
+`--filter-minor` would only show:
+
+ * Gem Current Latest Requested Groups Release Date
+ * headless 2.2.3 2.3.1 = 2.2.3 test 2022-08-19
+
+`--filter-patch` would only show:
+
+ * Gem Current Latest Requested Groups Release Date
+ * faker 1.6.5 1.6.6 ~> 1.4 development, test 2024-02-05
+
+Filter options can be combined. `--filter-minor` and `--filter-patch` would show:
+
+ * Gem Current Latest Requested Groups Release Date
+ * faker 1.6.5 1.6.6 ~> 1.4 development, test 2024-02-05
+
+Combining all three `filter` options would be the same result as providing none of them.
diff --git a/lib/bundler/man/bundle-platform.1 b/lib/bundler/man/bundle-platform.1
new file mode 100644
index 0000000000..39b7111263
--- /dev/null
+++ b/lib/bundler/man/bundle-platform.1
@@ -0,0 +1,49 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-PLATFORM" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-platform\fR \- Displays platform compatibility information
+.SH "SYNOPSIS"
+\fBbundle platform\fR [\-\-ruby]
+.SH "DESCRIPTION"
+\fBplatform\fR displays information from your Gemfile, Gemfile\.lock, and Ruby VM about your platform\.
+.P
+For instance, using this Gemfile(5):
+.IP "" 4
+.nf
+source "https://rubygems\.org"
+
+ruby "3\.1\.2"
+
+gem "rack"
+.fi
+.IP "" 0
+.P
+If you run \fBbundle platform\fR on Ruby 3\.1\.2, it displays the following output:
+.IP "" 4
+.nf
+Your platform is: x86_64\-linux
+
+Your app has gems that work on these platforms:
+* arm64\-darwin\-21
+* ruby
+* x64\-mingw\-ucrt
+* x86_64\-linux
+
+Your Gemfile specifies a Ruby version requirement:
+* ruby 3\.1\.2
+
+Your current platform satisfies the Ruby version requirement\.
+.fi
+.IP "" 0
+.P
+\fBplatform\fR lists all the platforms in your \fBGemfile\.lock\fR as well as the \fBruby\fR directive if applicable from your Gemfile(5)\. It also lets you know if the \fBruby\fR directive requirement has been met\. If \fBruby\fR directive doesn't match the running Ruby VM, it tells you what part does not\.
+.SH "OPTIONS"
+.TP
+\fB\-\-ruby\fR
+It will display the ruby directive information, so you don't have to parse it from the Gemfile(5)\.
+.SH "SEE ALSO"
+.IP "\(bu" 4
+bundle\-lock(1) \fIbundle\-lock\.1\.html\fR
+.IP "" 0
+
diff --git a/lib/bundler/man/bundle-platform.1.ronn b/lib/bundler/man/bundle-platform.1.ronn
new file mode 100644
index 0000000000..744acd1b43
--- /dev/null
+++ b/lib/bundler/man/bundle-platform.1.ronn
@@ -0,0 +1,49 @@
+bundle-platform(1) -- Displays platform compatibility information
+=================================================================
+
+## SYNOPSIS
+
+`bundle platform` [--ruby]
+
+## DESCRIPTION
+
+`platform` displays information from your Gemfile, Gemfile.lock, and Ruby
+VM about your platform.
+
+For instance, using this Gemfile(5):
+
+ source "https://rubygems.org"
+
+ ruby "3.1.2"
+
+ gem "rack"
+
+If you run `bundle platform` on Ruby 3.1.2, it displays the following output:
+
+ Your platform is: x86_64-linux
+
+ Your app has gems that work on these platforms:
+ * arm64-darwin-21
+ * ruby
+ * x64-mingw-ucrt
+ * x86_64-linux
+
+ Your Gemfile specifies a Ruby version requirement:
+ * ruby 3.1.2
+
+ Your current platform satisfies the Ruby version requirement.
+
+`platform` lists all the platforms in your `Gemfile.lock` as well as the
+`ruby` directive if applicable from your Gemfile(5). It also lets you know
+if the `ruby` directive requirement has been met. If `ruby` directive doesn't
+match the running Ruby VM, it tells you what part does not.
+
+## OPTIONS
+
+* `--ruby`:
+ It will display the ruby directive information, so you don't have to
+ parse it from the Gemfile(5).
+
+## SEE ALSO
+
+* [bundle-lock(1)](bundle-lock.1.html)
diff --git a/lib/bundler/man/bundle-plugin.1 b/lib/bundler/man/bundle-plugin.1
new file mode 100644
index 0000000000..d182c7789b
--- /dev/null
+++ b/lib/bundler/man/bundle-plugin.1
@@ -0,0 +1,76 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-PLUGIN" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-plugin\fR \- Manage Bundler plugins
+.SH "SYNOPSIS"
+\fBbundle plugin\fR install PLUGINS [\-\-source=SOURCE] [\-\-version=VERSION] [\-\-git=GIT] [\-\-branch=BRANCH|\-\-ref=REF] [\-\-path=PATH]
+.br
+\fBbundle plugin\fR uninstall PLUGINS [\-\-all]
+.br
+\fBbundle plugin\fR list
+.br
+\fBbundle plugin\fR help [COMMAND]
+.SH "DESCRIPTION"
+You can install, uninstall, and list plugin(s) with this command to extend functionalities of Bundler\.
+.SH "SUB\-COMMANDS"
+.SS "install"
+Install the given plugin(s)\.
+.P
+For example, \fBbundle plugin install bundler\-graph\fR will install bundler\-graph gem from globally configured sources (defaults to RubyGems\.org)\. Note that the global source specified in Gemfile is ignored\.
+.P
+\fBOPTIONS\fR
+.TP
+\fB\-\-source=SOURCE\fR
+Install the plugin gem from a specific source, rather than from globally configured sources\.
+.IP
+Example: \fBbundle plugin install bundler\-graph \-\-source https://example\.com\fR
+.TP
+\fB\-\-version=VERSION\fR
+Specify a version of the plugin gem to install via \fB\-\-version\fR\.
+.IP
+Example: \fBbundle plugin install bundler\-graph \-\-version 0\.2\.1\fR
+.TP
+\fB\-\-git=GIT\fR
+Install the plugin gem from a Git repository\. You can use standard Git URLs like:
+.IP
+\fBssh://[user@]host\.xz[:port]/path/to/repo\.git\fR
+.br
+\fBhttp[s]://host\.xz[:port]/path/to/repo\.git\fR
+.br
+\fB/path/to/repo\fR
+.br
+\fBfile:///path/to/repo\fR
+.IP
+Example: \fBbundle plugin install bundler\-graph \-\-git https://github\.com/rubygems/bundler\-graph\fR
+.TP
+\fB\-\-branch=BRANCH\fR
+When you specify \fB\-\-git\fR, you can use \fB\-\-branch\fR to use\.
+.TP
+\fB\-\-ref=REF\fR
+When you specify \fB\-\-git\fR, you can use \fB\-\-ref\fR to specify any tag, or commit hash (revision) to use\.
+.TP
+\fB\-\-path=PATH\fR
+Install the plugin gem from a local path\.
+.IP
+Example: \fBbundle plugin install bundler\-graph \-\-path \.\./bundler\-graph\fR
+.SS "uninstall"
+Uninstall the plugin(s) specified in PLUGINS\.
+.P
+\fBOPTIONS\fR
+.TP
+\fB\-\-all\fR
+Uninstall all the installed plugins\. If no plugin is installed, then it does nothing\.
+.SS "list"
+List the installed plugins and available commands\.
+.P
+No options\.
+.SS "help"
+Describe subcommands or one specific subcommand\.
+.P
+No options\.
+.SH "SEE ALSO"
+.IP "\(bu" 4
+How to write a Bundler plugin \fIhttps://bundler\.io/guides/bundler_plugins\.html\fR
+.IP "" 0
+
diff --git a/lib/bundler/man/bundle-plugin.1.ronn b/lib/bundler/man/bundle-plugin.1.ronn
new file mode 100644
index 0000000000..b54e0c08b4
--- /dev/null
+++ b/lib/bundler/man/bundle-plugin.1.ronn
@@ -0,0 +1,84 @@
+bundle-plugin(1) -- Manage Bundler plugins
+==========================================
+
+## SYNOPSIS
+
+`bundle plugin` install PLUGINS [--source=SOURCE] [--version=VERSION]
+ [--git=GIT] [--branch=BRANCH|--ref=REF]
+ [--path=PATH]<br>
+`bundle plugin` uninstall PLUGINS [--all]<br>
+`bundle plugin` list<br>
+`bundle plugin` help [COMMAND]
+
+## DESCRIPTION
+
+You can install, uninstall, and list plugin(s) with this command to extend functionalities of Bundler.
+
+## SUB-COMMANDS
+
+### install
+
+Install the given plugin(s).
+
+For example, `bundle plugin install bundler-graph` will install bundler-graph
+gem from globally configured sources (defaults to RubyGems.org). Note that the
+global source specified in Gemfile is ignored.
+
+**OPTIONS**
+
+* `--source=SOURCE`:
+ Install the plugin gem from a specific source, rather than from globally configured sources.
+
+ Example: `bundle plugin install bundler-graph --source https://example.com`
+
+* `--version=VERSION`:
+ Specify a version of the plugin gem to install via `--version`.
+
+ Example: `bundle plugin install bundler-graph --version 0.2.1`
+
+* `--git=GIT`:
+ Install the plugin gem from a Git repository. You can use standard Git URLs like:
+
+ `ssh://[user@]host.xz[:port]/path/to/repo.git`<br>
+ `http[s]://host.xz[:port]/path/to/repo.git`<br>
+ `/path/to/repo`<br>
+ `file:///path/to/repo`
+
+ Example: `bundle plugin install bundler-graph --git https://github.com/rubygems/bundler-graph`
+
+* `--branch=BRANCH`:
+ When you specify `--git`, you can use `--branch` to use.
+
+* `--ref=REF`:
+ When you specify `--git`, you can use `--ref` to specify any tag, or commit
+ hash (revision) to use.
+
+* `--path=PATH`:
+ Install the plugin gem from a local path.
+
+ Example: `bundle plugin install bundler-graph --path ../bundler-graph`
+
+### uninstall
+
+Uninstall the plugin(s) specified in PLUGINS.
+
+**OPTIONS**
+
+* `--all`:
+ Uninstall all the installed plugins. If no plugin is installed, then it does nothing.
+
+### list
+
+List the installed plugins and available commands.
+
+No options.
+
+### help
+
+Describe subcommands or one specific subcommand.
+
+No options.
+
+## SEE ALSO
+
+* [How to write a Bundler plugin](https://bundler.io/guides/bundler_plugins.html)
diff --git a/lib/bundler/man/bundle-pristine.1 b/lib/bundler/man/bundle-pristine.1
new file mode 100644
index 0000000000..f6cc066571
--- /dev/null
+++ b/lib/bundler/man/bundle-pristine.1
@@ -0,0 +1,23 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-PRISTINE" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-pristine\fR \- Restores installed gems to their pristine condition
+.SH "SYNOPSIS"
+\fBbundle pristine\fR
+.SH "DESCRIPTION"
+\fBpristine\fR restores the installed gems in the bundle to their pristine condition using the local gem cache from RubyGems\. For git gems, a forced checkout will be performed\.
+.P
+For further explanation, \fBbundle pristine\fR ignores unpacked files on disk\. In other words, this command utilizes the local \fB\.gem\fR cache or the gem's git repository as if one were installing from scratch\.
+.P
+Note: the Bundler gem cannot be restored to its original state with \fBpristine\fR\. One also cannot use \fBbundle pristine\fR on gems with a 'path' option in the Gemfile, because bundler has no original copy it can restore from\.
+.P
+When is it practical to use \fBbundle pristine\fR?
+.P
+It comes in handy when a developer is debugging a gem\. \fBbundle pristine\fR is a great way to get rid of experimental changes to a gem that one may not want\.
+.P
+Why use \fBbundle pristine\fR over \fBgem pristine \-\-all\fR?
+.P
+Both commands are very similar\. For context: \fBbundle pristine\fR, without arguments, cleans all gems from the lockfile\. Meanwhile, \fBgem pristine \-\-all\fR cleans all installed gems for that Ruby version\.
+.P
+If a developer forgets which gems in their project they might have been debugging, the Rubygems \fBgem pristine [GEMNAME]\fR command may be inconvenient\. One can avoid waiting for \fBgem pristine \-\-all\fR, and instead run \fBbundle pristine\fR\.
diff --git a/lib/bundler/man/bundle-pristine.1.ronn b/lib/bundler/man/bundle-pristine.1.ronn
new file mode 100644
index 0000000000..984debeb3d
--- /dev/null
+++ b/lib/bundler/man/bundle-pristine.1.ronn
@@ -0,0 +1,34 @@
+bundle-pristine(1) -- Restores installed gems to their pristine condition
+=========================================================================
+
+## SYNOPSIS
+
+`bundle pristine`
+
+## DESCRIPTION
+
+`pristine` restores the installed gems in the bundle to their pristine condition
+using the local gem cache from RubyGems. For git gems, a forced checkout will be performed.
+
+For further explanation, `bundle pristine` ignores unpacked files on disk. In other
+words, this command utilizes the local `.gem` cache or the gem's git repository
+as if one were installing from scratch.
+
+Note: the Bundler gem cannot be restored to its original state with `pristine`.
+One also cannot use `bundle pristine` on gems with a 'path' option in the Gemfile,
+because bundler has no original copy it can restore from.
+
+When is it practical to use `bundle pristine`?
+
+It comes in handy when a developer is debugging a gem. `bundle pristine` is a
+great way to get rid of experimental changes to a gem that one may not want.
+
+Why use `bundle pristine` over `gem pristine --all`?
+
+Both commands are very similar.
+For context: `bundle pristine`, without arguments, cleans all gems from the lockfile.
+Meanwhile, `gem pristine --all` cleans all installed gems for that Ruby version.
+
+If a developer forgets which gems in their project they might
+have been debugging, the Rubygems `gem pristine [GEMNAME]` command may be inconvenient.
+One can avoid waiting for `gem pristine --all`, and instead run `bundle pristine`.
diff --git a/lib/bundler/man/bundle-remove.1 b/lib/bundler/man/bundle-remove.1
new file mode 100644
index 0000000000..2ca40e74db
--- /dev/null
+++ b/lib/bundler/man/bundle-remove.1
@@ -0,0 +1,15 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-REMOVE" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-remove\fR \- Removes gems from the Gemfile
+.SH "SYNOPSIS"
+`bundle remove [GEM [GEM \|\.\|\.\|\.]]
+.SH "DESCRIPTION"
+Removes the given gems from the Gemfile while ensuring that the resulting Gemfile is still valid\. If a gem cannot be removed, a warning is printed\. If a gem is already absent from the Gemfile, and error is raised\.
+.P
+Example:
+.P
+bundle remove rails
+.P
+bundle remove rails rack
diff --git a/lib/bundler/man/bundle-remove.1.ronn b/lib/bundler/man/bundle-remove.1.ronn
new file mode 100644
index 0000000000..49cb4dc1fd
--- /dev/null
+++ b/lib/bundler/man/bundle-remove.1.ronn
@@ -0,0 +1,16 @@
+bundle-remove(1) -- Removes gems from the Gemfile
+=================================================
+
+## SYNOPSIS
+
+`bundle remove [GEM [GEM ...]]
+
+## DESCRIPTION
+
+Removes the given gems from the Gemfile while ensuring that the resulting Gemfile is still valid. If a gem cannot be removed, a warning is printed. If a gem is already absent from the Gemfile, and error is raised.
+
+Example:
+
+bundle remove rails
+
+bundle remove rails rack
diff --git a/lib/bundler/man/bundle-show.1 b/lib/bundler/man/bundle-show.1
new file mode 100644
index 0000000000..a2142694b8
--- /dev/null
+++ b/lib/bundler/man/bundle-show.1
@@ -0,0 +1,16 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-SHOW" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-show\fR \- Shows all the gems in your bundle, or the path to a gem
+.SH "SYNOPSIS"
+\fBbundle show\fR [GEM] [\-\-paths]
+.SH "DESCRIPTION"
+Without the [GEM] option, \fBshow\fR will print a list of the names and versions of all gems that are required by your [\fBGemfile(5)\fR][Gemfile(5)], sorted by name\.
+.P
+Calling show with [GEM] will list the exact location of that gem on your machine\.
+.SH "OPTIONS"
+.TP
+\fB\-\-paths\fR
+List the paths of all gems that are required by your [\fBGemfile(5)\fR][Gemfile(5)], sorted by gem name\.
+
diff --git a/lib/bundler/man/bundle-show.1.ronn b/lib/bundler/man/bundle-show.1.ronn
new file mode 100644
index 0000000000..a6a59a1445
--- /dev/null
+++ b/lib/bundler/man/bundle-show.1.ronn
@@ -0,0 +1,21 @@
+bundle-show(1) -- Shows all the gems in your bundle, or the path to a gem
+=========================================================================
+
+## SYNOPSIS
+
+`bundle show` [GEM]
+ [--paths]
+
+## DESCRIPTION
+
+Without the [GEM] option, `show` will print a list of the names and versions of
+all gems that are required by your [`Gemfile(5)`][Gemfile(5)], sorted by name.
+
+Calling show with [GEM] will list the exact location of that gem on your
+machine.
+
+## OPTIONS
+
+* `--paths`:
+ List the paths of all gems that are required by your [`Gemfile(5)`][Gemfile(5)],
+ sorted by gem name.
diff --git a/lib/bundler/man/bundle-update.1 b/lib/bundler/man/bundle-update.1
new file mode 100644
index 0000000000..94161083fc
--- /dev/null
+++ b/lib/bundler/man/bundle-update.1
@@ -0,0 +1,284 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-UPDATE" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-update\fR \- Update your gems to the latest available versions
+.SH "SYNOPSIS"
+\fBbundle update\fR \fI*gems\fR [\-\-all] [\-\-group=NAME] [\-\-source=NAME] [\-\-local] [\-\-ruby] [\-\-bundler[=VERSION]] [\-\-cooldown=NUMBER] [\-\-force] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-quiet] [\-\-patch|\-\-minor|\-\-major] [\-\-pre] [\-\-strict] [\-\-conservative]
+.SH "DESCRIPTION"
+Update the gems specified (all gems, if \fB\-\-all\fR flag is used), ignoring the previously installed gems specified in the \fBGemfile\.lock\fR\. In general, you should use bundle install(1) \fIbundle\-install\.1\.html\fR to install the same exact gems and versions across machines\.
+.P
+You would use \fBbundle update\fR to explicitly update the version of a gem\.
+.SH "OPTIONS"
+.TP
+\fB\-\-all\fR
+Update all gems specified in Gemfile\.
+.TP
+\fB\-\-group=<list>\fR, \fB\-g=<list>\fR
+Only update the gems in the specified group\. For instance, you can update all gems in the development group with \fBbundle update \-\-group development\fR\. You can also call \fBbundle update rails \-\-group test\fR to update the rails gem and all gems in the test group, for example\.
+.TP
+\fB\-\-source=<list>\fR
+The name of a \fB:git\fR or \fB:path\fR source used in the Gemfile(5)\. For instance, with a \fB:git\fR source of \fBhttp://github\.com/rails/rails\.git\fR, you would call \fBbundle update \-\-source rails\fR
+.TP
+\fB\-\-local\fR
+Do not attempt to fetch gems remotely and use the gem cache instead\.
+.TP
+\fB\-\-ruby\fR
+Update the locked version of Ruby to the current version of Ruby\.
+.TP
+\fB\-\-bundler[=BUNDLER]\fR
+Update the locked version of bundler to the invoked bundler version\.
+.TP
+\fB\-\-force\fR, \fB\-\-redownload\fR
+Force reinstalling every gem, even if already installed\.
+.TP
+\fB\-\-full\-index\fR
+Fall back to using the single\-file index of all gems\.
+.TP
+\fB\-\-gemfile=GEMFILE\fR
+Use the specified gemfile instead of [\fBGemfile(5)\fR][Gemfile(5)]\.
+.TP
+\fB\-\-jobs=<number>\fR, \fB\-j=<number>\fR
+Specify the number of jobs to run in parallel\. The default is the number of available processors\.
+.TP
+\fB\-\-retry=[<number>]\fR
+Retry failed network or git requests for \fInumber\fR times\.
+.TP
+\fB\-\-quiet\fR
+Only output warnings and errors\.
+.TP
+\fB\-\-patch\fR
+Prefer updating only to next patch version\.
+.TP
+\fB\-\-minor\fR
+Prefer updating only to next minor version\.
+.TP
+\fB\-\-major\fR
+Prefer updating to next major version (default)\.
+.TP
+\fB\-\-pre\fR
+Always choose the highest allowed version, regardless of prerelease status\.
+.TP
+\fB\-\-strict\fR
+Do not allow any gem to be updated past latest \fB\-\-patch\fR | \fB\-\-minor\fR | \fB\-\-major\fR\.
+.TP
+\fB\-\-conservative\fR
+Use bundle install conservative update behavior and do not allow indirect dependencies to be updated\.
+.TP
+\fB\-\-cooldown=<number>\fR
+Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. Combine with \fB\-\-conservative\fR to minimize transitive churn when bypassing cooldown for an urgent update\. See \fBcooldown\fR in bundle\-config(1)\.
+.SH "UPDATING ALL GEMS"
+If you run \fBbundle update \-\-all\fR, bundler will ignore any previously installed gems and resolve all dependencies again based on the latest versions of all gems available in the sources\.
+.P
+Consider the following Gemfile(5):
+.IP "" 4
+.nf
+source "https://rubygems\.org"
+
+gem "rails", "3\.0\.0\.rc"
+gem "nokogiri"
+.fi
+.IP "" 0
+.P
+When you run bundle install(1) \fIbundle\-install\.1\.html\fR the first time, bundler will resolve all of the dependencies, all the way down, and install what you need:
+.IP "" 4
+.nf
+Fetching gem metadata from https://rubygems\.org/\|\.\|\.\|\.\|\.\|\.\|\.\|\.\|\.\|\.
+Resolving dependencies\|\.\|\.\|\.
+Installing builder 2\.1\.2
+Installing abstract 1\.0\.0
+Installing rack 1\.2\.8
+Using bundler 1\.7\.6
+Installing rake 10\.4\.0
+Installing polyglot 0\.3\.5
+Installing mime\-types 1\.25\.1
+Installing i18n 0\.4\.2
+Installing mini_portile 0\.6\.1
+Installing tzinfo 0\.3\.42
+Installing rack\-mount 0\.6\.14
+Installing rack\-test 0\.5\.7
+Installing treetop 1\.4\.15
+Installing thor 0\.14\.6
+Installing activesupport 3\.0\.0\.rc
+Installing erubis 2\.6\.6
+Installing activemodel 3\.0\.0\.rc
+Installing arel 0\.4\.0
+Installing mail 2\.2\.20
+Installing activeresource 3\.0\.0\.rc
+Installing actionpack 3\.0\.0\.rc
+Installing activerecord 3\.0\.0\.rc
+Installing actionmailer 3\.0\.0\.rc
+Installing railties 3\.0\.0\.rc
+Installing rails 3\.0\.0\.rc
+Installing nokogiri 1\.6\.5
+
+Bundle complete! 2 Gemfile dependencies, 26 gems total\.
+Use `bundle show [gemname]` to see where a bundled gem is installed\.
+.fi
+.IP "" 0
+.P
+As you can see, even though you have two gems in the Gemfile(5), your application needs 26 different gems in order to run\. Bundler remembers the exact versions it installed in \fBGemfile\.lock\fR\. The next time you run bundle install(1) \fIbundle\-install\.1\.html\fR, bundler skips the dependency resolution and installs the same gems as it installed last time\.
+.P
+After checking in the \fBGemfile\.lock\fR into version control and cloning it on another machine, running bundle install(1) \fIbundle\-install\.1\.html\fR will \fIstill\fR install the gems that you installed last time\. You don't need to worry that a new release of \fBerubis\fR or \fBmail\fR changes the gems you use\.
+.P
+However, from time to time, you might want to update the gems you are using to the newest versions that still match the gems in your Gemfile(5)\.
+.P
+To do this, run \fBbundle update \-\-all\fR, which will ignore the \fBGemfile\.lock\fR, and resolve all the dependencies again\. Keep in mind that this process can result in a significantly different set of the 25 gems, based on the requirements of new gems that the gem authors released since the last time you ran \fBbundle update \-\-all\fR\.
+.SH "UPDATING A LIST OF GEMS"
+Sometimes, you want to update a single gem in the Gemfile(5), and leave the rest of the gems that you specified locked to the versions in the \fBGemfile\.lock\fR\.
+.P
+For instance, in the scenario above, imagine that \fBnokogiri\fR releases version \fB1\.4\.4\fR, and you want to update it \fIwithout\fR updating Rails and all of its dependencies\. To do this, run \fBbundle update nokogiri\fR\.
+.P
+Bundler will update \fBnokogiri\fR and any of its dependencies, but leave alone Rails and its dependencies\.
+.SH "OVERLAPPING DEPENDENCIES"
+Sometimes, multiple gems declared in your Gemfile(5) are satisfied by the same second\-level dependency\. For instance, consider the case of \fBthin\fR and \fBrack\-perftools\-profiler\fR\.
+.IP "" 4
+.nf
+source "https://rubygems\.org"
+
+gem "thin"
+gem "rack\-perftools\-profiler"
+.fi
+.IP "" 0
+.P
+The \fBthin\fR gem depends on \fBrack >= 1\.0\fR, while \fBrack\-perftools\-profiler\fR depends on \fBrack ~> 1\.0\fR\. If you run bundle install, you get:
+.IP "" 4
+.nf
+Fetching source index for https://rubygems\.org/
+Installing daemons (1\.1\.0)
+Installing eventmachine (0\.12\.10) with native extensions
+Installing open4 (1\.0\.1)
+Installing perftools\.rb (0\.4\.7) with native extensions
+Installing rack (1\.2\.1)
+Installing rack\-perftools_profiler (0\.0\.2)
+Installing thin (1\.2\.7) with native extensions
+Using bundler (1\.0\.0\.rc\.3)
+.fi
+.IP "" 0
+.P
+In this case, the two gems have their own set of dependencies, but they share \fBrack\fR in common\. If you run \fBbundle update thin\fR, bundler will update \fBdaemons\fR, \fBeventmachine\fR and \fBrack\fR, which are dependencies of \fBthin\fR, but not \fBopen4\fR or \fBperftools\.rb\fR, which are dependencies of \fBrack\-perftools_profiler\fR\. Note that \fBbundle update thin\fR will update \fBrack\fR even though it's \fIalso\fR a dependency of \fBrack\-perftools_profiler\fR\.
+.P
+In short, by default, when you update a gem using \fBbundle update\fR, bundler will update all dependencies of that gem, including those that are also dependencies of another gem\.
+.P
+To prevent updating indirect dependencies, prior to version 1\.14 the only option was the \fBCONSERVATIVE UPDATING\fR behavior in bundle install(1) \fIbundle\-install\.1\.html\fR:
+.P
+In this scenario, updating the \fBthin\fR version manually in the Gemfile(5), and then running bundle install(1) \fIbundle\-install\.1\.html\fR will only update \fBdaemons\fR and \fBeventmachine\fR, but not \fBrack\fR\. For more information, see the \fBCONSERVATIVE UPDATING\fR section of bundle install(1) \fIbundle\-install\.1\.html\fR\.
+.P
+Starting with 1\.14, specifying the \fB\-\-conservative\fR option will also prevent indirect dependencies from being updated\.
+.SH "PATCH LEVEL OPTIONS"
+Version 1\.14 introduced 4 patch\-level options that will influence how gem versions are resolved\. One of the following options can be used: \fB\-\-patch\fR, \fB\-\-minor\fR or \fB\-\-major\fR\. \fB\-\-strict\fR can be added to further influence resolution\.
+.TP
+\fB\-\-patch\fR
+Prefer updating only to next patch version\.
+.TP
+\fB\-\-minor\fR
+Prefer updating only to next minor version\.
+.TP
+\fB\-\-major\fR
+Prefer updating to next major version (default)\.
+.TP
+\fB\-\-strict\fR
+Do not allow any gem to be updated past latest \fB\-\-patch\fR | \fB\-\-minor\fR | \fB\-\-major\fR\.
+.P
+When Bundler is resolving what versions to use to satisfy declared requirements in the Gemfile or in parent gems, it looks up all available versions, filters out any versions that don't satisfy the requirement, and then, by default, sorts them from newest to oldest, considering them in that order\.
+.P
+Providing one of the patch level options (e\.g\. \fB\-\-patch\fR) changes the sort order of the satisfying versions, causing Bundler to consider the latest \fB\-\-patch\fR or \fB\-\-minor\fR version available before other versions\. Note that versions outside the stated patch level could still be resolved to if necessary to find a suitable dependency graph\.
+.P
+For example, if gem 'foo' is locked at 1\.0\.2, with no gem requirement defined in the Gemfile, and versions 1\.0\.3, 1\.0\.4, 1\.1\.0, 1\.1\.1, 2\.0\.0 all exist, the default order of preference by default (\fB\-\-major\fR) will be "2\.0\.0, 1\.1\.1, 1\.1\.0, 1\.0\.4, 1\.0\.3, 1\.0\.2"\.
+.P
+If the \fB\-\-patch\fR option is used, the order of preference will change to "1\.0\.4, 1\.0\.3, 1\.0\.2, 1\.1\.1, 1\.1\.0, 2\.0\.0"\.
+.P
+If the \fB\-\-minor\fR option is used, the order of preference will change to "1\.1\.1, 1\.1\.0, 1\.0\.4, 1\.0\.3, 1\.0\.2, 2\.0\.0"\.
+.P
+Combining the \fB\-\-strict\fR option with any of the patch level options will remove any versions beyond the scope of the patch level option, to ensure that no gem is updated that far\.
+.P
+To continue the previous example, if both \fB\-\-patch\fR and \fB\-\-strict\fR options are used, the available versions for resolution would be "1\.0\.4, 1\.0\.3, 1\.0\.2"\. If \fB\-\-minor\fR and \fB\-\-strict\fR are used, it would be "1\.1\.1, 1\.1\.0, 1\.0\.4, 1\.0\.3, 1\.0\.2"\.
+.P
+Gem requirements as defined in the Gemfile will still be the first determining factor for what versions are available\. If the gem requirement for \fBfoo\fR in the Gemfile is '~> 1\.0', that will accomplish the same thing as providing the \fB\-\-minor\fR and \fB\-\-strict\fR options\.
+.SH "PATCH LEVEL EXAMPLES"
+Given the following gem specifications:
+.IP "" 4
+.nf
+foo 1\.4\.3, requires: ~> bar 2\.0
+foo 1\.4\.4, requires: ~> bar 2\.0
+foo 1\.4\.5, requires: ~> bar 2\.1
+foo 1\.5\.0, requires: ~> bar 2\.1
+foo 1\.5\.1, requires: ~> bar 3\.0
+bar with versions 2\.0\.3, 2\.0\.4, 2\.1\.0, 2\.1\.1, 3\.0\.0
+.fi
+.IP "" 0
+.P
+Gemfile:
+.IP "" 4
+.nf
+gem 'foo'
+.fi
+.IP "" 0
+.P
+Gemfile\.lock:
+.IP "" 4
+.nf
+foo (1\.4\.3)
+ bar (~> 2\.0)
+bar (2\.0\.3)
+.fi
+.IP "" 0
+.P
+Cases:
+.IP "" 4
+.nf
+# Command Line Result
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-
+1 bundle update \-\-patch 'foo 1\.4\.5', 'bar 2\.1\.1'
+2 bundle update \-\-patch foo 'foo 1\.4\.5', 'bar 2\.1\.1'
+3 bundle update \-\-minor 'foo 1\.5\.1', 'bar 3\.0\.0'
+4 bundle update \-\-minor \-\-strict 'foo 1\.5\.0', 'bar 2\.1\.1'
+5 bundle update \-\-patch \-\-strict 'foo 1\.4\.4', 'bar 2\.0\.4'
+.fi
+.IP "" 0
+.P
+In case 1, bar is upgraded to 2\.1\.1, a minor version increase, because the dependency from foo 1\.4\.5 required it\.
+.P
+In case 2, only foo is requested to be unlocked, but bar is also allowed to move because it's not a declared dependency in the Gemfile\.
+.P
+In case 3, bar goes up a whole major release, because a minor increase is preferred now for foo, and when it goes to 1\.5\.1, it requires 3\.0\.0 of bar\.
+.P
+In case 4, foo is preferred up to a minor version, but 1\.5\.1 won't work because the \-\-strict flag removes bar 3\.0\.0 from consideration since it's a major increment\.
+.P
+In case 5, both foo and bar have any minor or major increments removed from consideration because of the \-\-strict flag, so the most they can move is up to 1\.4\.4 and 2\.0\.4\.
+.SH "RECOMMENDED WORKFLOW"
+In general, when working with an application managed with bundler, you should use the following workflow:
+.IP "\(bu" 4
+After you create your Gemfile(5) for the first time, run
+.IP
+$ bundle install
+.IP "\(bu" 4
+Check the resulting \fBGemfile\.lock\fR into version control
+.IP
+$ git add Gemfile\.lock
+.IP "\(bu" 4
+When checking out this repository on another development machine, run
+.IP
+$ bundle install
+.IP "\(bu" 4
+When checking out this repository on a deployment machine, run
+.IP
+$ bundle install \-\-deployment
+.IP "\(bu" 4
+After changing the Gemfile(5) to reflect a new or update dependency, run
+.IP
+$ bundle install
+.IP "\(bu" 4
+Make sure to check the updated \fBGemfile\.lock\fR into version control
+.IP
+$ git add Gemfile\.lock
+.IP "\(bu" 4
+If bundle install(1) \fIbundle\-install\.1\.html\fR reports a conflict, manually update the specific gems that you changed in the Gemfile(5)
+.IP
+$ bundle update rails thin
+.IP "\(bu" 4
+If you want to update all the gems to the latest possible versions that still match the gems listed in the Gemfile(5), run
+.IP
+$ bundle update \-\-all
+.IP "" 0
+
diff --git a/lib/bundler/man/bundle-update.1.ronn b/lib/bundler/man/bundle-update.1.ronn
new file mode 100644
index 0000000000..72fbf054d1
--- /dev/null
+++ b/lib/bundler/man/bundle-update.1.ronn
@@ -0,0 +1,367 @@
+bundle-update(1) -- Update your gems to the latest available versions
+=====================================================================
+
+## SYNOPSIS
+
+`bundle update` <*gems> [--all]
+ [--group=NAME]
+ [--source=NAME]
+ [--local]
+ [--ruby]
+ [--bundler[=VERSION]]
+ [--cooldown=NUMBER]
+ [--force]
+ [--full-index]
+ [--gemfile=GEMFILE]
+ [--jobs=NUMBER]
+ [--quiet]
+ [--patch|--minor|--major]
+ [--pre]
+ [--strict]
+ [--conservative]
+
+## DESCRIPTION
+
+Update the gems specified (all gems, if `--all` flag is used), ignoring
+the previously installed gems specified in the `Gemfile.lock`. In
+general, you should use [bundle install(1)](bundle-install.1.html) to install the same exact
+gems and versions across machines.
+
+You would use `bundle update` to explicitly update the version of a
+gem.
+
+## OPTIONS
+
+* `--all`:
+ Update all gems specified in Gemfile.
+
+* `--group=<list>`, `-g=<list>`:
+ Only update the gems in the specified group. For instance, you can update all gems
+ in the development group with `bundle update --group development`. You can also
+ call `bundle update rails --group test` to update the rails gem and all gems in
+ the test group, for example.
+
+* `--source=<list>`:
+ The name of a `:git` or `:path` source used in the Gemfile(5). For
+ instance, with a `:git` source of `http://github.com/rails/rails.git`,
+ you would call `bundle update --source rails`
+
+* `--local`:
+ Do not attempt to fetch gems remotely and use the gem cache instead.
+
+* `--ruby`:
+ Update the locked version of Ruby to the current version of Ruby.
+
+* `--bundler[=BUNDLER]`:
+ Update the locked version of bundler to the invoked bundler version.
+
+* `--force`, `--redownload`:
+ Force reinstalling every gem, even if already installed.
+
+* `--full-index`:
+ Fall back to using the single-file index of all gems.
+
+* `--gemfile=GEMFILE`:
+ Use the specified gemfile instead of [`Gemfile(5)`][Gemfile(5)].
+
+* `--jobs=<number>`, `-j=<number>`:
+ Specify the number of jobs to run in parallel. The default is the number of
+ available processors.
+
+* `--retry=[<number>]`:
+ Retry failed network or git requests for <number> times.
+
+* `--quiet`:
+ Only output warnings and errors.
+
+* `--patch`:
+ Prefer updating only to next patch version.
+
+* `--minor`:
+ Prefer updating only to next minor version.
+
+* `--major`:
+ Prefer updating to next major version (default).
+
+* `--pre`:
+ Always choose the highest allowed version, regardless of prerelease status.
+
+* `--strict`:
+ Do not allow any gem to be updated past latest `--patch` | `--minor` | `--major`.
+
+* `--conservative`:
+ Use bundle install conservative update behavior and do not allow indirect dependencies to be updated.
+
+* `--cooldown=<number>`:
+ Only consider gem versions published at least <number> days ago when
+ resolving. Pass `0` to disable cooldown for this run, overriding any
+ per-source or global configuration. Combine with `--conservative` to
+ minimize transitive churn when bypassing cooldown for an urgent
+ update. See `cooldown` in bundle-config(1).
+
+## UPDATING ALL GEMS
+
+If you run `bundle update --all`, bundler will ignore
+any previously installed gems and resolve all dependencies again
+based on the latest versions of all gems available in the sources.
+
+Consider the following Gemfile(5):
+
+ source "https://rubygems.org"
+
+ gem "rails", "3.0.0.rc"
+ gem "nokogiri"
+
+When you run [bundle install(1)](bundle-install.1.html) the first time, bundler will resolve
+all of the dependencies, all the way down, and install what you need:
+
+ Fetching gem metadata from https://rubygems.org/.........
+ Resolving dependencies...
+ Installing builder 2.1.2
+ Installing abstract 1.0.0
+ Installing rack 1.2.8
+ Using bundler 1.7.6
+ Installing rake 10.4.0
+ Installing polyglot 0.3.5
+ Installing mime-types 1.25.1
+ Installing i18n 0.4.2
+ Installing mini_portile 0.6.1
+ Installing tzinfo 0.3.42
+ Installing rack-mount 0.6.14
+ Installing rack-test 0.5.7
+ Installing treetop 1.4.15
+ Installing thor 0.14.6
+ Installing activesupport 3.0.0.rc
+ Installing erubis 2.6.6
+ Installing activemodel 3.0.0.rc
+ Installing arel 0.4.0
+ Installing mail 2.2.20
+ Installing activeresource 3.0.0.rc
+ Installing actionpack 3.0.0.rc
+ Installing activerecord 3.0.0.rc
+ Installing actionmailer 3.0.0.rc
+ Installing railties 3.0.0.rc
+ Installing rails 3.0.0.rc
+ Installing nokogiri 1.6.5
+
+ Bundle complete! 2 Gemfile dependencies, 26 gems total.
+ Use `bundle show [gemname]` to see where a bundled gem is installed.
+
+As you can see, even though you have two gems in the Gemfile(5), your application
+needs 26 different gems in order to run. Bundler remembers the exact versions
+it installed in `Gemfile.lock`. The next time you run [bundle install(1)](bundle-install.1.html), bundler skips
+the dependency resolution and installs the same gems as it installed last time.
+
+After checking in the `Gemfile.lock` into version control and cloning it on another
+machine, running [bundle install(1)](bundle-install.1.html) will _still_ install the gems that you installed
+last time. You don't need to worry that a new release of `erubis` or `mail` changes
+the gems you use.
+
+However, from time to time, you might want to update the gems you are using to the
+newest versions that still match the gems in your Gemfile(5).
+
+To do this, run `bundle update --all`, which will ignore the `Gemfile.lock`, and resolve
+all the dependencies again. Keep in mind that this process can result in a significantly
+different set of the 25 gems, based on the requirements of new gems that the gem
+authors released since the last time you ran `bundle update --all`.
+
+## UPDATING A LIST OF GEMS
+
+Sometimes, you want to update a single gem in the Gemfile(5), and leave the rest of the
+gems that you specified locked to the versions in the `Gemfile.lock`.
+
+For instance, in the scenario above, imagine that `nokogiri` releases version `1.4.4`, and
+you want to update it _without_ updating Rails and all of its dependencies. To do this,
+run `bundle update nokogiri`.
+
+Bundler will update `nokogiri` and any of its dependencies, but leave alone Rails and
+its dependencies.
+
+## OVERLAPPING DEPENDENCIES
+
+Sometimes, multiple gems declared in your Gemfile(5) are satisfied by the same
+second-level dependency. For instance, consider the case of `thin` and
+`rack-perftools-profiler`.
+
+ source "https://rubygems.org"
+
+ gem "thin"
+ gem "rack-perftools-profiler"
+
+The `thin` gem depends on `rack >= 1.0`, while `rack-perftools-profiler` depends
+on `rack ~> 1.0`. If you run bundle install, you get:
+
+ Fetching source index for https://rubygems.org/
+ Installing daemons (1.1.0)
+ Installing eventmachine (0.12.10) with native extensions
+ Installing open4 (1.0.1)
+ Installing perftools.rb (0.4.7) with native extensions
+ Installing rack (1.2.1)
+ Installing rack-perftools_profiler (0.0.2)
+ Installing thin (1.2.7) with native extensions
+ Using bundler (1.0.0.rc.3)
+
+In this case, the two gems have their own set of dependencies, but they share
+`rack` in common. If you run `bundle update thin`, bundler will update `daemons`,
+`eventmachine` and `rack`, which are dependencies of `thin`, but not `open4` or
+`perftools.rb`, which are dependencies of `rack-perftools_profiler`. Note that
+`bundle update thin` will update `rack` even though it's _also_ a dependency of
+`rack-perftools_profiler`.
+
+In short, by default, when you update a gem using `bundle update`, bundler will
+update all dependencies of that gem, including those that are also dependencies
+of another gem.
+
+To prevent updating indirect dependencies, prior to version 1.14 the only option
+was the `CONSERVATIVE UPDATING` behavior in [bundle install(1)](bundle-install.1.html):
+
+In this scenario, updating the `thin` version manually in the Gemfile(5),
+and then running [bundle install(1)](bundle-install.1.html) will only update `daemons` and `eventmachine`,
+but not `rack`. For more information, see the `CONSERVATIVE UPDATING` section
+of [bundle install(1)](bundle-install.1.html).
+
+Starting with 1.14, specifying the `--conservative` option will also prevent indirect
+dependencies from being updated.
+
+## PATCH LEVEL OPTIONS
+
+Version 1.14 introduced 4 patch-level options that will influence how gem
+versions are resolved. One of the following options can be used: `--patch`,
+`--minor` or `--major`. `--strict` can be added to further influence resolution.
+
+* `--patch`:
+ Prefer updating only to next patch version.
+
+* `--minor`:
+ Prefer updating only to next minor version.
+
+* `--major`:
+ Prefer updating to next major version (default).
+
+* `--strict`:
+ Do not allow any gem to be updated past latest `--patch` | `--minor` | `--major`.
+
+When Bundler is resolving what versions to use to satisfy declared
+requirements in the Gemfile or in parent gems, it looks up all
+available versions, filters out any versions that don't satisfy
+the requirement, and then, by default, sorts them from newest to
+oldest, considering them in that order.
+
+Providing one of the patch level options (e.g. `--patch`) changes the
+sort order of the satisfying versions, causing Bundler to consider the
+latest `--patch` or `--minor` version available before other versions.
+Note that versions outside the stated patch level could still be
+resolved to if necessary to find a suitable dependency graph.
+
+For example, if gem 'foo' is locked at 1.0.2, with no gem requirement
+defined in the Gemfile, and versions 1.0.3, 1.0.4, 1.1.0, 1.1.1, 2.0.0
+all exist, the default order of preference by default (`--major`) will
+be "2.0.0, 1.1.1, 1.1.0, 1.0.4, 1.0.3, 1.0.2".
+
+If the `--patch` option is used, the order of preference will change to
+"1.0.4, 1.0.3, 1.0.2, 1.1.1, 1.1.0, 2.0.0".
+
+If the `--minor` option is used, the order of preference will change to
+"1.1.1, 1.1.0, 1.0.4, 1.0.3, 1.0.2, 2.0.0".
+
+Combining the `--strict` option with any of the patch level options
+will remove any versions beyond the scope of the patch level option,
+to ensure that no gem is updated that far.
+
+To continue the previous example, if both `--patch` and `--strict`
+options are used, the available versions for resolution would be
+"1.0.4, 1.0.3, 1.0.2". If `--minor` and `--strict` are used, it would
+be "1.1.1, 1.1.0, 1.0.4, 1.0.3, 1.0.2".
+
+Gem requirements as defined in the Gemfile will still be the first
+determining factor for what versions are available. If the gem
+requirement for `foo` in the Gemfile is '~> 1.0', that will accomplish
+the same thing as providing the `--minor` and `--strict` options.
+
+## PATCH LEVEL EXAMPLES
+
+Given the following gem specifications:
+
+ foo 1.4.3, requires: ~> bar 2.0
+ foo 1.4.4, requires: ~> bar 2.0
+ foo 1.4.5, requires: ~> bar 2.1
+ foo 1.5.0, requires: ~> bar 2.1
+ foo 1.5.1, requires: ~> bar 3.0
+ bar with versions 2.0.3, 2.0.4, 2.1.0, 2.1.1, 3.0.0
+
+Gemfile:
+
+ gem 'foo'
+
+Gemfile.lock:
+
+ foo (1.4.3)
+ bar (~> 2.0)
+ bar (2.0.3)
+
+Cases:
+
+ # Command Line Result
+ ------------------------------------------------------------
+ 1 bundle update --patch 'foo 1.4.5', 'bar 2.1.1'
+ 2 bundle update --patch foo 'foo 1.4.5', 'bar 2.1.1'
+ 3 bundle update --minor 'foo 1.5.1', 'bar 3.0.0'
+ 4 bundle update --minor --strict 'foo 1.5.0', 'bar 2.1.1'
+ 5 bundle update --patch --strict 'foo 1.4.4', 'bar 2.0.4'
+
+In case 1, bar is upgraded to 2.1.1, a minor version increase, because
+the dependency from foo 1.4.5 required it.
+
+In case 2, only foo is requested to be unlocked, but bar is also
+allowed to move because it's not a declared dependency in the Gemfile.
+
+In case 3, bar goes up a whole major release, because a minor increase
+is preferred now for foo, and when it goes to 1.5.1, it requires 3.0.0
+of bar.
+
+In case 4, foo is preferred up to a minor version, but 1.5.1 won't work
+because the --strict flag removes bar 3.0.0 from consideration since
+it's a major increment.
+
+In case 5, both foo and bar have any minor or major increments removed
+from consideration because of the --strict flag, so the most they can
+move is up to 1.4.4 and 2.0.4.
+
+## RECOMMENDED WORKFLOW
+
+In general, when working with an application managed with bundler, you should
+use the following workflow:
+
+* After you create your Gemfile(5) for the first time, run
+
+ $ bundle install
+
+* Check the resulting `Gemfile.lock` into version control
+
+ $ git add Gemfile.lock
+
+* When checking out this repository on another development machine, run
+
+ $ bundle install
+
+* When checking out this repository on a deployment machine, run
+
+ $ bundle install --deployment
+
+* After changing the Gemfile(5) to reflect a new or update dependency, run
+
+ $ bundle install
+
+* Make sure to check the updated `Gemfile.lock` into version control
+
+ $ git add Gemfile.lock
+
+* If [bundle install(1)](bundle-install.1.html) reports a conflict, manually update the specific
+ gems that you changed in the Gemfile(5)
+
+ $ bundle update rails thin
+
+* If you want to update all the gems to the latest possible versions that
+ still match the gems listed in the Gemfile(5), run
+
+ $ bundle update --all
diff --git a/lib/bundler/man/bundle-version.1 b/lib/bundler/man/bundle-version.1
new file mode 100644
index 0000000000..751a408312
--- /dev/null
+++ b/lib/bundler/man/bundle-version.1
@@ -0,0 +1,22 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE\-VERSION" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\-version\fR \- Prints Bundler version information
+.SH "SYNOPSIS"
+\fBbundle version\fR
+.SH "DESCRIPTION"
+Prints Bundler version information\.
+.SH "OPTIONS"
+No options\.
+.SH "EXAMPLE"
+Print the version of Bundler with build date and commit hash of the in the Git source\.
+.IP "" 4
+.nf
+bundle version
+.fi
+.IP "" 0
+.P
+shows \fBBundler version 2\.3\.21 (2022\-08\-24 commit d54be5fdd8)\fR for example\.
+.P
+cf\. \fBbundle \-\-version\fR shows \fBBundler version 2\.3\.21\fR\.
diff --git a/lib/bundler/man/bundle-version.1.ronn b/lib/bundler/man/bundle-version.1.ronn
new file mode 100644
index 0000000000..46c6f0b30a
--- /dev/null
+++ b/lib/bundler/man/bundle-version.1.ronn
@@ -0,0 +1,24 @@
+bundle-version(1) -- Prints Bundler version information
+=======================================================
+
+## SYNOPSIS
+
+`bundle version`
+
+## DESCRIPTION
+
+Prints Bundler version information.
+
+## OPTIONS
+
+No options.
+
+## EXAMPLE
+
+Print the version of Bundler with build date and commit hash of the in the Git source.
+
+ bundle version
+
+shows `Bundler version 2.3.21 (2022-08-24 commit d54be5fdd8)` for example.
+
+cf. `bundle --version` shows `Bundler version 2.3.21`.
diff --git a/lib/bundler/man/bundle.1 b/lib/bundler/man/bundle.1
new file mode 100644
index 0000000000..167815631a
--- /dev/null
+++ b/lib/bundler/man/bundle.1
@@ -0,0 +1,93 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "BUNDLE" "1" "May 2026" ""
+.SH "NAME"
+\fBbundle\fR \- Ruby Dependency Management
+.SH "SYNOPSIS"
+\fBbundle\fR COMMAND [\-\-no\-color] [\-\-verbose] [ARGS]
+.SH "DESCRIPTION"
+Bundler manages an \fBapplication's dependencies\fR through its entire life across many machines systematically and repeatably\.
+.P
+See the bundler website \fIhttps://bundler\.io\fR for information on getting started, and Gemfile(5) for more information on the \fBGemfile\fR format\.
+.SH "OPTIONS"
+.TP
+\fB\-\-no\-color\fR
+Print all output without color
+.TP
+\fB\-\-retry\fR, \fB\-r\fR
+Specify the number of times you wish to attempt network commands
+.TP
+\fB\-\-verbose\fR, \fB\-V\fR
+Print out additional logging information
+.SH "BUNDLE COMMANDS"
+We divide \fBbundle\fR subcommands into primary commands and utilities:
+.SH "PRIMARY COMMANDS"
+.TP
+\fBbundle install(1)\fR \fIbundle\-install\.1\.html\fR
+Install the gems specified by the \fBGemfile\fR or \fBGemfile\.lock\fR
+.TP
+\fBbundle update(1)\fR \fIbundle\-update\.1\.html\fR
+Update dependencies to their latest versions
+.TP
+\fBbundle cache(1)\fR \fIbundle\-cache\.1\.html\fR
+Package the \.gem files required by your application into the \fBvendor/cache\fR directory (aliases: \fBbundle package\fR, \fBbundle pack\fR)
+.TP
+\fBbundle exec(1)\fR \fIbundle\-exec\.1\.html\fR
+Execute a script in the current bundle
+.TP
+\fBbundle config(1)\fR \fIbundle\-config\.1\.html\fR
+Specify and read configuration options for Bundler
+.TP
+\fBbundle help(1)\fR \fIbundle\-help\.1\.html\fR
+Display detailed help for each subcommand
+.SH "UTILITIES"
+.TP
+\fBbundle add(1)\fR \fIbundle\-add\.1\.html\fR
+Add the named gem to the Gemfile and run \fBbundle install\fR
+.TP
+\fBbundle binstubs(1)\fR \fIbundle\-binstubs\.1\.html\fR
+Generate binstubs for executables in a gem
+.TP
+\fBbundle check(1)\fR \fIbundle\-check\.1\.html\fR
+Determine whether the requirements for your application are installed and available to Bundler
+.TP
+\fBbundle show(1)\fR \fIbundle\-show\.1\.html\fR
+Show the source location of a particular gem in the bundle
+.TP
+\fBbundle outdated(1)\fR \fIbundle\-outdated\.1\.html\fR
+Show all of the outdated gems in the current bundle
+.TP
+\fBbundle console(1)\fR (deprecated)
+Start an IRB session in the current bundle
+.TP
+\fBbundle open(1)\fR \fIbundle\-open\.1\.html\fR
+Open an installed gem in the editor
+.TP
+\fBbundle lock(1)\fR \fIbundle\-lock\.1\.html\fR
+Generate a lockfile for your dependencies
+.TP
+\fBbundle init(1)\fR \fIbundle\-init\.1\.html\fR
+Generate a simple \fBGemfile\fR, placed in the current directory
+.TP
+\fBbundle gem(1)\fR \fIbundle\-gem\.1\.html\fR
+Create a simple gem, suitable for development with Bundler
+.TP
+\fBbundle platform(1)\fR \fIbundle\-platform\.1\.html\fR
+Display platform compatibility information
+.TP
+\fBbundle clean(1)\fR \fIbundle\-clean\.1\.html\fR
+Clean up unused gems in your Bundler directory
+.TP
+\fBbundle doctor(1)\fR \fIbundle\-doctor\.1\.html\fR
+Display warnings about common problems
+.TP
+\fBbundle remove(1)\fR \fIbundle\-remove\.1\.html\fR
+Removes gems from the Gemfile
+.TP
+\fBbundle plugin(1)\fR \fIbundle\-plugin\.1\.html\fR
+Manage Bundler plugins
+.TP
+\fBbundle version(1)\fR \fIbundle\-version\.1\.html\fR
+Prints Bundler version information
+.SH "PLUGINS"
+When running a command that isn't listed in PRIMARY COMMANDS or UTILITIES, Bundler will try to find an executable on your path named \fBbundler\-<command>\fR and execute it, passing down any extra arguments to it\.
diff --git a/lib/bundler/man/bundle.1.ronn b/lib/bundler/man/bundle.1.ronn
new file mode 100644
index 0000000000..1c2b3df7af
--- /dev/null
+++ b/lib/bundler/man/bundle.1.ronn
@@ -0,0 +1,107 @@
+bundle(1) -- Ruby Dependency Management
+=======================================
+
+## SYNOPSIS
+
+`bundle` COMMAND [--no-color] [--verbose] [ARGS]
+
+## DESCRIPTION
+
+Bundler manages an `application's dependencies` through its entire life
+across many machines systematically and repeatably.
+
+See [the bundler website](https://bundler.io) for information on getting
+started, and Gemfile(5) for more information on the `Gemfile` format.
+
+## OPTIONS
+
+* `--no-color`:
+ Print all output without color
+
+* `--retry`, `-r`:
+ Specify the number of times you wish to attempt network commands
+
+* `--verbose`, `-V`:
+ Print out additional logging information
+
+## BUNDLE COMMANDS
+
+We divide `bundle` subcommands into primary commands and utilities:
+
+## PRIMARY COMMANDS
+
+* [`bundle install(1)`](bundle-install.1.html):
+ Install the gems specified by the `Gemfile` or `Gemfile.lock`
+
+* [`bundle update(1)`](bundle-update.1.html):
+ Update dependencies to their latest versions
+
+* [`bundle cache(1)`](bundle-cache.1.html):
+ Package the .gem files required by your application into the
+ `vendor/cache` directory (aliases: `bundle package`, `bundle pack`)
+
+* [`bundle exec(1)`](bundle-exec.1.html):
+ Execute a script in the current bundle
+
+* [`bundle config(1)`](bundle-config.1.html):
+ Specify and read configuration options for Bundler
+
+* [`bundle help(1)`](bundle-help.1.html):
+ Display detailed help for each subcommand
+
+## UTILITIES
+
+* [`bundle add(1)`](bundle-add.1.html):
+ Add the named gem to the Gemfile and run `bundle install`
+
+* [`bundle binstubs(1)`](bundle-binstubs.1.html):
+ Generate binstubs for executables in a gem
+
+* [`bundle check(1)`](bundle-check.1.html):
+ Determine whether the requirements for your application are installed
+ and available to Bundler
+
+* [`bundle show(1)`](bundle-show.1.html):
+ Show the source location of a particular gem in the bundle
+
+* [`bundle outdated(1)`](bundle-outdated.1.html):
+ Show all of the outdated gems in the current bundle
+
+* `bundle console(1)` (deprecated):
+ Start an IRB session in the current bundle
+
+* [`bundle open(1)`](bundle-open.1.html):
+ Open an installed gem in the editor
+
+* [`bundle lock(1)`](bundle-lock.1.html):
+ Generate a lockfile for your dependencies
+
+* [`bundle init(1)`](bundle-init.1.html):
+ Generate a simple `Gemfile`, placed in the current directory
+
+* [`bundle gem(1)`](bundle-gem.1.html):
+ Create a simple gem, suitable for development with Bundler
+
+* [`bundle platform(1)`](bundle-platform.1.html):
+ Display platform compatibility information
+
+* [`bundle clean(1)`](bundle-clean.1.html):
+ Clean up unused gems in your Bundler directory
+
+* [`bundle doctor(1)`](bundle-doctor.1.html):
+ Display warnings about common problems
+
+* [`bundle remove(1)`](bundle-remove.1.html):
+ Removes gems from the Gemfile
+
+* [`bundle plugin(1)`](bundle-plugin.1.html):
+ Manage Bundler plugins
+
+* [`bundle version(1)`](bundle-version.1.html):
+ Prints Bundler version information
+
+## PLUGINS
+
+When running a command that isn't listed in PRIMARY COMMANDS or UTILITIES,
+Bundler will try to find an executable on your path named `bundler-<command>`
+and execute it, passing down any extra arguments to it.
diff --git a/lib/bundler/man/gemfile.5 b/lib/bundler/man/gemfile.5
new file mode 100644
index 0000000000..64e93c4b15
--- /dev/null
+++ b/lib/bundler/man/gemfile.5
@@ -0,0 +1,547 @@
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "GEMFILE" "5" "May 2026" ""
+.SH "NAME"
+\fBGemfile\fR \- A format for describing gem dependencies for Ruby programs
+.SH "SYNOPSIS"
+A \fBGemfile\fR describes the gem dependencies required to execute associated Ruby code\.
+.P
+Place the \fBGemfile\fR in the root of the directory containing the associated code\. For instance, in a Rails application, place the \fBGemfile\fR in the same directory as the \fBRakefile\fR\.
+.SH "SYNTAX"
+A \fBGemfile\fR is evaluated as Ruby code, in a context which makes available a number of methods used to describe the gem requirements\.
+.SH "GLOBAL SOURCE"
+At the top of the \fBGemfile\fR, add a single line for the \fBRubyGems\fR source that contains the gems listed in the \fBGemfile\fR\.
+.IP "" 4
+.nf
+source "https://rubygems\.org"
+.fi
+.IP "" 0
+.P
+You can add only one global source\. In Bundler 1\.13, adding multiple global sources was deprecated\. The \fBsource\fR \fBMUST\fR be a valid RubyGems repository\.
+.P
+To use more than one source of RubyGems, you should use \fI\fBsource\fR block\fR\.
+.P
+A source is checked for gems following the heuristics described in \fISOURCE PRIORITY\fR\.
+.P
+\fBNote about a behavior of the feature deprecated in Bundler 1\.13\fR: If a gem is found in more than one global source, Bundler will print a warning after installing the gem indicating which source was used, and listing the other sources where the gem is available\. A specific source can be selected for gems that need to use a non\-standard repository, suppressing this warning, by using the \fI\fB:source\fR option\fR or \fBsource\fR block\.
+.SS "CREDENTIALS"
+Some gem sources require a username and password\. Use bundle config(1) \fIbundle\-config\.1\.html\fR to set the username and password for any of the sources that need it\. The command must be run once on each computer that will install the Gemfile, but this keeps the credentials from being stored in plain text in version control\.
+.IP "" 4
+.nf
+bundle config gems\.example\.com user:password
+.fi
+.IP "" 0
+.P
+For some sources, like a company Gemfury account, it may be easier to include the credentials in the Gemfile as part of the source URL\.
+.IP "" 4
+.nf
+source "https://user:password@gems\.example\.com"
+.fi
+.IP "" 0
+.P
+Credentials in the source URL will take precedence over credentials set using \fBconfig\fR\.
+.SH "RUBY"
+If your application requires a specific Ruby version or engine, specify your requirements using the \fBruby\fR method, with the following arguments\. All parameters are \fBOPTIONAL\fR unless otherwise specified\.
+.SS "VERSION (required)"
+The version of Ruby that your application requires\. If your application requires an alternate Ruby engine, such as JRuby, TruffleRuby, etc\., this should be the Ruby version that the engine is compatible with\.
+.IP "" 4
+.nf
+ruby "3\.1\.2"
+.fi
+.IP "" 0
+.P
+If you wish to derive your Ruby version from a version file (ie \.ruby\-version), you can use the \fBfile\fR option instead\.
+.IP "" 4
+.nf
+ruby file: "\.ruby\-version"
+.fi
+.IP "" 0
+.P
+The version file should conform to any of the following formats:
+.IP "\(bu" 4
+\fB3\.1\.2\fR (\.ruby\-version)
+.IP "\(bu" 4
+\fBruby 3\.1\.2\fR (\.tool\-versions, read: https://asdf\-vm\.com/manage/configuration\.html#tool\-versions)
+.IP "" 0
+.SS "ENGINE"
+Each application \fImay\fR specify a Ruby engine\. If an engine is specified, an engine version \fImust\fR also be specified\.
+.P
+What exactly is an Engine?
+.IP "\(bu" 4
+A Ruby engine is an implementation of the Ruby language\.
+.IP "\(bu" 4
+For background: the reference or original implementation of the Ruby programming language is called Matz's Ruby Interpreter \fIhttps://en\.wikipedia\.org/wiki/Ruby_MRI\fR, or MRI for short\. This is named after Ruby creator Yukihiro Matsumoto, also known as Matz\. MRI is also known as CRuby, because it is written in C\. MRI is the most widely used Ruby engine\.
+.IP "\(bu" 4
+Other implementations \fIhttps://www\.ruby\-lang\.org/en/about/\fR of Ruby exist\. Some of the more well\-known implementations include JRuby \fIhttps://www\.jruby\.org/\fR and TruffleRuby \fIhttps://www\.graalvm\.org/ruby/\fR\. Rubinius is an alternative implementation of Ruby written in Ruby\. JRuby is an implementation of Ruby on the JVM, short for Java Virtual Machine\. TruffleRuby is a Ruby implementation on the GraalVM, a language toolkit built on the JVM\.
+.IP "" 0
+.SS "ENGINE VERSION"
+Each application \fImay\fR specify a Ruby engine version\. If an engine version is specified, an engine \fImust\fR also be specified\. If the engine is "ruby" the engine version specified \fImust\fR match the Ruby version\.
+.IP "" 4
+.nf
+ruby "2\.6\.8", engine: "jruby", engine_version: "9\.3\.8\.0"
+.fi
+.IP "" 0
+.SS "PATCHLEVEL"
+Each application \fImay\fR specify a Ruby patchlevel\. Specifying the patchlevel has been meaningless since Ruby 2\.1\.0 was released as the patchlevel is now uniquely determined by a combination of major, minor, and teeny version numbers\.
+.P
+This option was implemented in Bundler 1\.4\.0 for Ruby 2\.0 or earlier\.
+.IP "" 4
+.nf
+ruby "3\.1\.2", patchlevel: "20"
+.fi
+.IP "" 0
+.SH "GEMS"
+Specify gem requirements using the \fBgem\fR method, with the following arguments\. All parameters are \fBOPTIONAL\fR unless otherwise specified\.
+.SS "NAME (required)"
+For each gem requirement, list a single \fIgem\fR line\.
+.IP "" 4
+.nf
+gem "nokogiri"
+.fi
+.IP "" 0
+.SS "VERSION"
+Each \fIgem\fR \fBMAY\fR have one or more version specifiers\.
+.IP "" 4
+.nf
+gem "nokogiri", ">= 1\.4\.2"
+gem "RedCloth", ">= 4\.1\.0", "< 4\.2\.0"
+.fi
+.IP "" 0
+.SS "REQUIRE AS"
+Each \fIgem\fR \fBMAY\fR specify files that should be used when autorequiring via \fBBundler\.require\fR\. You may pass an array with multiple files or \fBtrue\fR if the file you want \fBrequired\fR has the same name as \fIgem\fR or \fBfalse\fR to prevent any file from being autorequired\.
+.IP "" 4
+.nf
+gem "redis", require: ["redis/connection/hiredis", "redis"]
+gem "webmock", require: false
+gem "byebug", require: true
+.fi
+.IP "" 0
+.P
+The argument defaults to the name of the gem\. For example, these are identical:
+.IP "" 4
+.nf
+gem "nokogiri"
+gem "nokogiri", require: "nokogiri"
+gem "nokogiri", require: true
+.fi
+.IP "" 0
+.SS "GROUPS"
+Each \fIgem\fR \fBMAY\fR specify membership in one or more groups\. Any \fIgem\fR that does not specify membership in any group is placed in the \fBdefault\fR group\.
+.IP "" 4
+.nf
+gem "rspec", group: :test
+gem "wirble", groups: [:development, :test]
+.fi
+.IP "" 0
+.P
+The Bundler runtime allows its two main methods, \fBBundler\.setup\fR and \fBBundler\.require\fR, to limit their impact to particular groups\.
+.IP "" 4
+.nf
+# setup adds gems to Ruby's load path
+Bundler\.setup # defaults to all groups
+require "bundler/setup" # same as Bundler\.setup
+Bundler\.setup(:default) # only set up the _default_ group
+Bundler\.setup(:test) # only set up the _test_ group (but `not` _default_)
+Bundler\.setup(:default, :test) # set up the _default_ and _test_ groups, but no others
+
+# require requires all of the gems in the specified groups
+Bundler\.require # defaults to the _default_ group
+Bundler\.require(:default) # identical
+Bundler\.require(:default, :test) # requires the _default_ and _test_ groups
+Bundler\.require(:test) # requires the _test_ group
+.fi
+.IP "" 0
+.P
+The Bundler CLI allows you to specify a list of groups whose gems \fBbundle install\fR should not install with the \fBwithout\fR configuration\.
+.P
+To specify multiple groups to ignore, specify a list of groups separated by spaces\.
+.IP "" 4
+.nf
+bundle config set \-\-local without test
+bundle config set \-\-local without development test
+.fi
+.IP "" 0
+.P
+Also, calling \fBBundler\.setup\fR with no parameters, or calling \fBrequire "bundler/setup"\fR will setup all groups except for the ones you excluded via \fB\-\-without\fR (since they are not available)\.
+.P
+Note that on \fBbundle install\fR, bundler downloads and evaluates all gems, in order to create a single canonical list of all of the required gems and their dependencies\. This means that you cannot list different versions of the same gems in different groups\. For more details, see Understanding Bundler \fIhttps://bundler\.io/rationale\.html\fR\.
+.SS "PLATFORMS"
+If a gem should only be used in a particular platform or set of platforms, you can specify them\. Platforms are essentially identical to groups, except that you do not need to use the \fB\-\-without\fR install\-time flag to exclude groups of gems for other platforms\.
+.P
+There are a number of \fBGemfile\fR platforms:
+.TP
+\fBruby\fR
+C Ruby (MRI), Rubinius, or TruffleRuby, but not Windows
+.TP
+\fBmri\fR
+C Ruby (MRI) only, but not Windows
+.TP
+\fBwindows\fR
+Windows C Ruby (MRI), including RubyInstaller 32\-bit and 64\-bit versions
+.TP
+\fBmswin\fR
+Windows C Ruby (MRI), including RubyInstaller 32\-bit versions
+.TP
+\fBmswin64\fR
+Windows C Ruby (MRI), including RubyInstaller 64\-bit versions
+.TP
+\fBrbx\fR
+Rubinius
+.TP
+\fBjruby\fR
+JRuby
+.TP
+\fBtruffleruby\fR
+TruffleRuby
+.P
+On platforms \fBruby\fR, \fBmri\fR, \fBmswin\fR, \fBmswin64\fR, and \fBwindows\fR, you may additionally specify a version by appending the major and minor version numbers without a delimiter\. For example, to specify that a gem should only be used on platform \fBruby\fR version 3\.1, use:
+.IP "" 4
+.nf
+ruby_31
+.fi
+.IP "" 0
+.P
+As with groups (above), you may specify one or more platforms:
+.IP "" 4
+.nf
+gem "weakling", platforms: :jruby
+gem "ruby\-debug", platforms: :mri_31
+gem "nokogiri", platforms: [:windows_31, :jruby]
+.fi
+.IP "" 0
+.P
+All operations involving groups (\fBbundle install\fR \fIbundle\-install\.1\.html\fR, \fBBundler\.setup\fR, \fBBundler\.require\fR) behave exactly the same as if any groups not matching the current platform were explicitly excluded\.
+.P
+The following platform values are deprecated and should be replaced with \fBwindows\fR:
+.IP "\(bu" 4
+\fBmswin\fR, \fBmswin64\fR, \fBmingw32\fR, \fBx64_mingw\fR
+.IP "" 0
+.P
+Note that, while unfortunately using the same terminology, the values of this option are different from the values that \fBbundle lock \-\-add\-platform\fR can take\. The values of this option are more closer to "Ruby Implementation" while the values that \fBbundle lock \-\-add\-platform\fR understands are more related to OS and architecture of the different systems where your lockfile will be used\.
+.SS "FORCE_RUBY_PLATFORM"
+If you always want the pure ruby variant of a gem to be chosen over platform specific variants, you can use the \fBforce_ruby_platform\fR option:
+.IP "" 4
+.nf
+gem "ffi", force_ruby_platform: true
+.fi
+.IP "" 0
+.P
+This can be handy (assuming the pure ruby variant works fine) when:
+.IP "\(bu" 4
+You're having issues with the platform specific variant\.
+.IP "\(bu" 4
+The platform specific variant does not yet support a newer ruby (and thus has a \fBrequired_ruby_version\fR upper bound), but you still want your Gemfile{\.lock} files to resolve under that ruby\.
+.IP "" 0
+.SS "SOURCE"
+You can select an alternate RubyGems repository for a gem using the ':source' option\.
+.IP "" 4
+.nf
+gem "some_internal_gem", source: "https://gems\.example\.com"
+.fi
+.IP "" 0
+.P
+This forces the gem to be loaded from this source and ignores the global source declared at the top level of the file\. If the gem does not exist in this source, it will not be installed\.
+.P
+Bundler will search for child dependencies of this gem by first looking in the source selected for the parent, but if they are not found there, it will fall back on the global source\.
+.P
+\fBNote about a behavior of the feature deprecated in Bundler 1\.13\fR: Selecting a specific source repository this way also suppresses the ambiguous gem warning described above in \fIGLOBAL SOURCE\fR\.
+.P
+Using the \fB:source\fR option for an individual gem will also make that source available as a possible global source for any other gems which do not specify explicit sources\. Thus, when adding gems with explicit sources, it is recommended that you also ensure all other gems in the Gemfile are using explicit sources\.
+.SS "GIT"
+If necessary, you can specify that a gem is located at a particular git repository using the \fB:git\fR parameter\. The repository can be accessed via several protocols:
+.TP
+\fBHTTP(S)\fR
+gem "rails", git: "https://github\.com/rails/rails\.git"
+.TP
+\fBSSH\fR
+gem "rails", git: "git@github\.com:rails/rails\.git"
+.TP
+\fBgit\fR
+gem "rails", git: "git://github\.com/rails/rails\.git"
+.P
+If using SSH, the user that you use to run \fBbundle install\fR \fBMUST\fR have the appropriate keys available in their \fB$HOME/\.ssh\fR\.
+.P
+\fBNOTE\fR: \fBhttp://\fR and \fBgit://\fR URLs should be avoided if at all possible\. These protocols are unauthenticated, so a man\-in\-the\-middle attacker can deliver malicious code and compromise your system\. HTTPS and SSH are strongly preferred\.
+.P
+The \fBgroup\fR, \fBplatforms\fR, and \fBrequire\fR options are available and behave exactly the same as they would for a normal gem\.
+.P
+A git repository \fBSHOULD\fR have at least one file, at the root of the directory containing the gem, with the extension \fB\.gemspec\fR\. This file \fBMUST\fR contain a valid gem specification, as expected by the \fBgem build\fR command\.
+.P
+If a git repository does not have a \fB\.gemspec\fR, bundler will attempt to create one, but it will not contain any dependencies, executables, or C extension compilation instructions\. As a result, it may fail to properly integrate into your application\.
+.P
+If a git repository does have a \fB\.gemspec\fR for the gem you attached it to, a version specifier, if provided, means that the git repository is only valid if the \fB\.gemspec\fR specifies a version matching the version specifier\. If not, bundler will print a warning\.
+.IP "" 4
+.nf
+gem "rails", "2\.3\.8", git: "https://github\.com/rails/rails\.git"
+# bundle install will fail, because the \.gemspec in the rails
+# repository's master branch specifies version 3\.0\.0
+.fi
+.IP "" 0
+.P
+If a git repository does \fBnot\fR have a \fB\.gemspec\fR for the gem you attached it to, a version specifier \fBMUST\fR be provided\. Bundler will use this version in the simple \fB\.gemspec\fR it creates\.
+.P
+Git repositories support a number of additional options\.
+.TP
+\fBbranch\fR, \fBtag\fR, and \fBref\fR
+You \fBMUST\fR only specify at most one of these options\. The default is \fBbranch: "master"\fR\. For example:
+.IP
+gem "rails", git: "https://github\.com/rails/rails\.git", branch: "5\-0\-stable"
+.IP
+gem "rails", git: "https://github\.com/rails/rails\.git", tag: "v5\.0\.0"
+.IP
+gem "rails", git: "https://github\.com/rails/rails\.git", ref: "4aded"
+.TP
+\fBsubmodules\fR
+For reference, a git submodule \fIhttps://git\-scm\.com/book/en/v2/Git\-Tools\-Submodules\fR lets you have another git repository within a subfolder of your repository\. Specify \fBsubmodules: true\fR to cause bundler to expand any submodules included in the git repository
+.P
+If a git repository contains multiple \fB\.gemspecs\fR, each \fB\.gemspec\fR represents a gem located at the same place in the file system as the \fB\.gemspec\fR\.
+.IP "" 4
+.nf
+|~rails [git root]
+| |\-rails\.gemspec [rails gem located here]
+|~actionpack
+| |\-actionpack\.gemspec [actionpack gem located here]
+|~activesupport
+| |\-activesupport\.gemspec [activesupport gem located here]
+|\|\.\|\.\|\.
+.fi
+.IP "" 0
+.P
+To install a gem located in a git repository, bundler changes to the directory containing the gemspec, runs \fBgem build name\.gemspec\fR and then installs the resulting gem\. The \fBgem build\fR command, which comes standard with Rubygems, evaluates the \fB\.gemspec\fR in the context of the directory in which it is located\.
+.SS "GIT SOURCE"
+A custom git source can be defined via the \fBgit_source\fR method\. Provide the source's name as an argument, and a block which receives a single argument and interpolates it into a string to return the full repo address:
+.IP "" 4
+.nf
+git_source(:stash){ |repo_name| "https://stash\.corp\.acme\.pl/#{repo_name}\.git" }
+gem 'rails', stash: 'forks/rails'
+.fi
+.IP "" 0
+.P
+In addition, if you wish to choose a specific branch:
+.IP "" 4
+.nf
+gem "rails", stash: "forks/rails", branch: "branch_name"
+.fi
+.IP "" 0
+.SS "GITHUB"
+\fBNOTE\fR: This shorthand should be avoided until Bundler 2\.0, since it currently expands to an insecure \fBgit://\fR URL\. This allows a man\-in\-the\-middle attacker to compromise your system\.
+.P
+If the git repository you want to use is hosted on GitHub and is public, you can use the :github shorthand to specify the github username and repository name (without the trailing "\.git"), separated by a slash\. If both the username and repository name are the same, you can omit one\.
+.IP "" 4
+.nf
+gem "rails", github: "rails/rails"
+gem "rails", github: "rails"
+.fi
+.IP "" 0
+.P
+Are both equivalent to
+.IP "" 4
+.nf
+gem "rails", git: "https://github\.com/rails/rails\.git"
+.fi
+.IP "" 0
+.P
+Since the \fBgithub\fR method is a specialization of \fBgit_source\fR, it accepts a \fB:branch\fR named argument\.
+.P
+You can also directly pass a pull request URL:
+.IP "" 4
+.nf
+gem "rails", github: "https://github\.com/rails/rails/pull/43753"
+.fi
+.IP "" 0
+.P
+Which is equivalent to:
+.IP "" 4
+.nf
+gem "rails", github: "rails/rails", branch: "refs/pull/43753/head"
+.fi
+.IP "" 0
+.SS "GIST"
+If the git repository you want to use is hosted as a GitHub Gist and is public, you can use the :gist shorthand to specify the gist identifier (without the trailing "\.git")\.
+.IP "" 4
+.nf
+gem "the_hatch", gist: "4815162342"
+.fi
+.IP "" 0
+.P
+Is equivalent to:
+.IP "" 4
+.nf
+gem "the_hatch", git: "https://gist\.github\.com/4815162342\.git"
+.fi
+.IP "" 0
+.P
+Since the \fBgist\fR method is a specialization of \fBgit_source\fR, it accepts a \fB:branch\fR named argument\.
+.SS "BITBUCKET"
+If the git repository you want to use is hosted on Bitbucket and is public, you can use the :bitbucket shorthand to specify the bitbucket username and repository name (without the trailing "\.git"), separated by a slash\. If both the username and repository name are the same, you can omit one\.
+.IP "" 4
+.nf
+gem "rails", bitbucket: "rails/rails"
+gem "rails", bitbucket: "rails"
+.fi
+.IP "" 0
+.P
+Are both equivalent to
+.IP "" 4
+.nf
+gem "rails", git: "https://rails@bitbucket\.org/rails/rails\.git"
+.fi
+.IP "" 0
+.P
+Since the \fBbitbucket\fR method is a specialization of \fBgit_source\fR, it accepts a \fB:branch\fR named argument\.
+.SS "PATH"
+You can specify that a gem is located in a particular location on the file system\. Relative paths are resolved relative to the directory containing the \fBGemfile\fR\.
+.P
+Similar to the semantics of the \fB:git\fR option, the \fB:path\fR option requires that the directory in question either contains a \fB\.gemspec\fR for the gem, or that you specify an explicit version that bundler should use\.
+.P
+Unlike \fB:git\fR, bundler does not compile C extensions for gems specified as paths\.
+.IP "" 4
+.nf
+gem "rails", path: "vendor/rails"
+.fi
+.IP "" 0
+.P
+If you would like to use multiple local gems directly from the filesystem, you can set a global \fBpath\fR option to the path containing the gem's files\. This will automatically load gemspec files from subdirectories\.
+.IP "" 4
+.nf
+path 'components' do
+ gem 'admin_ui'
+ gem 'public_ui'
+end
+.fi
+.IP "" 0
+.SH "BLOCK FORM OF SOURCE, GIT, PATH, GROUP and PLATFORMS"
+The \fB:source\fR, \fB:git\fR, \fB:path\fR, \fB:group\fR, and \fB:platforms\fR options may be applied to a group of gems by using block form\.
+.IP "" 4
+.nf
+source "https://gems\.example\.com" do
+ gem "some_internal_gem"
+ gem "another_internal_gem"
+end
+
+git "https://github\.com/rails/rails\.git" do
+ gem "activesupport"
+ gem "actionpack"
+end
+
+platforms :ruby do
+ gem "ruby\-debug"
+ gem "sqlite3"
+end
+
+group :development, optional: true do
+ gem "wirble"
+ gem "faker"
+end
+.fi
+.IP "" 0
+.P
+In the case of the group block form the :optional option can be given to prevent a group from being installed unless listed in the \fB\-\-with\fR option given to the \fBbundle install\fR command\.
+.P
+In the case of the \fBgit\fR block form, the \fB:ref\fR, \fB:branch\fR, \fB:tag\fR, and \fB:submodules\fR options may be passed to the \fBgit\fR method, and all gems in the block will inherit those options\.
+.P
+The presence of a \fBsource\fR block in a Gemfile also makes that source available as a possible global source for any other gems which do not specify explicit sources\. Thus, when defining source blocks, it is recommended that you also ensure all other gems in the Gemfile are using explicit sources, either via source blocks or \fB:source\fR directives on individual gems\.
+.SH "INSTALL_IF"
+The \fBinstall_if\fR method allows gems to be installed based on a proc or lambda\. This is especially useful for optional gems that can only be used if certain software is installed or some other conditions are met\.
+.IP "" 4
+.nf
+install_if \-> { RUBY_PLATFORM =~ /darwin/ } do
+ gem "pasteboard"
+end
+.fi
+.IP "" 0
+.SH "GEMSPEC"
+The \fB\.gemspec\fR \fIhttps://guides\.rubygems\.org/specification\-reference/\fR file is where you provide metadata about your gem to Rubygems\. Some required Gemspec attributes include the name, description, and homepage of your gem\. This is also where you specify the dependencies your gem needs to run\.
+.P
+If you wish to use Bundler to help install dependencies for a gem while it is being developed, use the \fBgemspec\fR method to pull in the dependencies listed in the \fB\.gemspec\fR file\.
+.P
+The \fBgemspec\fR method adds any runtime dependencies as gem requirements in the default group\. It also adds development dependencies as gem requirements in the \fBdevelopment\fR group\. Finally, it adds a gem requirement on your project (\fBpath: '\.'\fR)\. In conjunction with \fBBundler\.setup\fR, this allows you to require project files in your test code as you would if the project were installed as a gem; you need not manipulate the load path manually or require project files via relative paths\.
+.P
+The \fBgemspec\fR method supports optional \fB:path\fR, \fB:glob\fR, \fB:name\fR, and \fB:development_group\fR options, which control where bundler looks for the \fB\.gemspec\fR, the glob it uses to look for the gemspec (defaults to: \fB{,*,*/*}\.gemspec\fR), what named \fB\.gemspec\fR it uses (if more than one is present), and which group development dependencies are included in\.
+.P
+When a \fBgemspec\fR dependency encounters version conflicts during resolution, the local version under development will always be selected \-\- even if there are remote versions that better match other requirements for the \fBgemspec\fR gem\.
+.SH "OVERRIDE"
+The \fBoverride\fR directive rewrites a constraint on another gem before resolution runs\. It targets the common case where an upstream gem's published metadata is too narrow on the current project's machine \-\- a stale upper bound, an unwanted floor, or a transitive pin that has to be lifted\.
+.IP "" 4
+.nf
+override <target>, <field>: <operation>
+.fi
+.IP "" 0
+.P
+\fB<target>\fR is a gem name string or \fB:all\fR\. \fB<field>\fR is one of \fBversion:\fR, \fBrequired_ruby_version:\fR, or \fBrequired_rubygems_version:\fR\. \fB<operation>\fR is one of:
+.IP "\(bu" 4
+a version requirement string (e\.g\. \fB">= 8\.0"\fR), which \fBreplaces\fR the target's requirement absolutely\. The original requirement, both direct and transitive, is discarded in favour of the override\.
+.IP "\(bu" 4
+\fB:ignore_upper\fR, which removes upper\-bound operators (\fB<\fR and \fB<=\fR) from the existing requirement and folds \fB~>\fR into its lower bound (\fB~> 1\.5\fR becomes \fB>= 1\.5\fR)\. Other operators, including \fB!=\fR, are preserved\.
+.IP "\(bu" 4
+\fBnil\fR, which collapses the requirement to \fB>= 0\fR (no constraint at all)\.
+.IP "" 0
+.P
+\fB:all\fR only applies to \fBrequired_ruby_version:\fR and \fBrequired_rubygems_version:\fR\. A per\-gem override on the same field takes precedence over an \fB:all\fR override\. \fB:all + version:\fR is rejected: version requirements only make sense per gem\.
+.P
+Multiple \fBoverride\fR calls for distinct targets are allowed; declaring the same \fBtarget\fR and \fBfield\fR twice is an error\.
+.IP "" 4
+.nf
+source "https://rubygems\.org"
+
+# Force every reference to "rails" \-\- direct or transitive \-\- to >= 8\.0\.
+override "rails", version: ">= 8\.0"
+
+# Strip the upper bound on nokogiri\.
+override "nokogiri", version: :ignore_upper
+
+# Drop the version pin on legacy entirely\.
+override "legacy", version: nil
+
+# Loosen every gem's required_ruby_version upper bound\.
+override :all, required_ruby_version: :ignore_upper
+
+# Override one specific gem's required_rubygems_version\.
+override "tricky", required_rubygems_version: nil
+
+gem "rails", "~> 7\.0"
+.fi
+.IP "" 0
+.P
+The override only affects resolution and the install\-time Ruby/RubyGems compatibility checks; \fBGemfile\.lock\fR continues to reflect the resolved versions, not the rewritten requirements\. When resolution still fails, Bundler appends the active overrides (with their Gemfile location) to the error message so it is clear which override shaped the constraint set\.
+.SH "SOURCE PRIORITY"
+When attempting to locate a gem to satisfy a gem requirement, bundler uses the following priority order:
+.IP "1." 4
+The source explicitly attached to the gem (using \fB:source\fR, \fB:path\fR, or \fB:git\fR)
+.IP "2." 4
+For implicit gems (dependencies of explicit gems), any source, git, or path repository declared on the parent\. This results in bundler prioritizing the ActiveSupport gem from the Rails git repository over ones from \fBrubygems\.org\fR
+.IP "3." 4
+If neither of the above conditions are met, the global source will be used\. If multiple global sources are specified, they will be prioritized from last to first, but this is deprecated since Bundler 1\.13, so Bundler prints a warning and will abort with an error in the future\.
+.IP "" 0
+.SH "LOCKFILE"
+By default, Bundler will create a lockfile by adding \fB\.lock\fR to the end of the Gemfile name\. To change this, use the \fBlockfile\fR method:
+.IP "" 4
+.nf
+lockfile "/path/to/lockfile\.lock"
+.fi
+.IP "" 0
+.P
+This is useful when you want to use different lockfiles per ruby version or platform\.
+.P
+To avoid writing a lock file, use \fBfalse\fR as the argument:
+.IP "" 4
+.nf
+lockfile false
+.fi
+.IP "" 0
+.P
+This is useful for library development and other situations where the code is expected to work with a range of dependency versions\.
+.SS "LOCKFILE PRECEDENCE"
+When determining path to the lockfile or whether to create a lockfile, the following precedence is used:
+.IP "1." 4
+The \fBbundle install\fR \fB\-\-no\-lock\fR option (which disables lockfile creation)\.
+.IP "2." 4
+The \fBbundle install\fR \fB\-\-lockfile\fR option\.
+.IP "3." 4
+The \fBBUNDLE_LOCKFILE\fR environment variable\.
+.IP "4." 4
+The \fBlockfile\fR method in the Gemfile\.
+.IP "5." 4
+The default behavior of adding \fB\.lock\fR to the end of the Gemfile name\.
+.IP "" 0
+
diff --git a/lib/bundler/man/gemfile.5.ronn b/lib/bundler/man/gemfile.5.ronn
new file mode 100644
index 0000000000..69fef90654
--- /dev/null
+++ b/lib/bundler/man/gemfile.5.ronn
@@ -0,0 +1,639 @@
+Gemfile(5) -- A format for describing gem dependencies for Ruby programs
+========================================================================
+
+## SYNOPSIS
+
+A `Gemfile` describes the gem dependencies required to execute associated
+Ruby code.
+
+Place the `Gemfile` in the root of the directory containing the associated
+code. For instance, in a Rails application, place the `Gemfile` in the same
+directory as the `Rakefile`.
+
+## SYNTAX
+
+A `Gemfile` is evaluated as Ruby code, in a context which makes available
+a number of methods used to describe the gem requirements.
+
+## GLOBAL SOURCE
+
+At the top of the `Gemfile`, add a single line for the `RubyGems` source that
+contains the gems listed in the `Gemfile`.
+
+ source "https://rubygems.org"
+
+You can add only one global source. In Bundler 1.13, adding multiple global
+sources was deprecated. The `source` `MUST` be a valid RubyGems repository.
+
+To use more than one source of RubyGems, you should use [`source` block
+](#BLOCK-FORM-OF-SOURCE-GIT-PATH-GROUP-and-PLATFORMS).
+
+A source is checked for gems following the heuristics described in
+[SOURCE PRIORITY][].
+
+**Note about a behavior of the feature deprecated in Bundler 1.13**:
+If a gem is found in more than one global source, Bundler
+will print a warning after installing the gem indicating which source was used,
+and listing the other sources where the gem is available. A specific source can
+be selected for gems that need to use a non-standard repository, suppressing
+this warning, by using the [`:source` option](#SOURCE) or `source` block.
+
+### CREDENTIALS
+
+Some gem sources require a username and password. Use [bundle config(1)](bundle-config.1.html) to set
+the username and password for any of the sources that need it. The command must
+be run once on each computer that will install the Gemfile, but this keeps the
+credentials from being stored in plain text in version control.
+
+ bundle config gems.example.com user:password
+
+For some sources, like a company Gemfury account, it may be easier to
+include the credentials in the Gemfile as part of the source URL.
+
+ source "https://user:password@gems.example.com"
+
+Credentials in the source URL will take precedence over credentials set using
+`config`.
+
+## RUBY
+
+If your application requires a specific Ruby version or engine, specify your
+requirements using the `ruby` method, with the following arguments.
+All parameters are `OPTIONAL` unless otherwise specified.
+
+### VERSION (required)
+
+The version of Ruby that your application requires. If your application
+requires an alternate Ruby engine, such as JRuby, TruffleRuby, etc., this
+should be the Ruby version that the engine is compatible with.
+
+ ruby "3.1.2"
+
+If you wish to derive your Ruby version from a version file (ie .ruby-version),
+you can use the `file` option instead.
+
+ ruby file: ".ruby-version"
+
+The version file should conform to any of the following formats:
+
+ - `3.1.2` (.ruby-version)
+ - `ruby 3.1.2` (.tool-versions, read: https://asdf-vm.com/manage/configuration.html#tool-versions)
+
+### ENGINE
+
+Each application _may_ specify a Ruby engine. If an engine is specified, an
+engine version _must_ also be specified.
+
+What exactly is an Engine?
+ - A Ruby engine is an implementation of the Ruby language.
+
+ - For background: the reference or original implementation of the Ruby
+ programming language is called
+ [Matz's Ruby Interpreter](https://en.wikipedia.org/wiki/Ruby_MRI), or MRI
+ for short. This is named after Ruby creator Yukihiro Matsumoto,
+ also known as Matz. MRI is also known as CRuby, because it is written in C.
+ MRI is the most widely used Ruby engine.
+
+ - [Other implementations](https://www.ruby-lang.org/en/about/) of Ruby exist.
+ Some of the more well-known implementations include
+ [JRuby](https://www.jruby.org/) and [TruffleRuby](https://www.graalvm.org/ruby/).
+ Rubinius is an alternative implementation of Ruby written in Ruby.
+ JRuby is an implementation of Ruby on the JVM, short for Java Virtual Machine.
+ TruffleRuby is a Ruby implementation on the GraalVM, a language toolkit built on the JVM.
+
+### ENGINE VERSION
+
+Each application _may_ specify a Ruby engine version. If an engine version is
+specified, an engine _must_ also be specified. If the engine is "ruby" the
+engine version specified _must_ match the Ruby version.
+
+ ruby "2.6.8", engine: "jruby", engine_version: "9.3.8.0"
+
+### PATCHLEVEL
+
+Each application _may_ specify a Ruby patchlevel. Specifying the patchlevel has
+been meaningless since Ruby 2.1.0 was released as the patchlevel is now
+uniquely determined by a combination of major, minor, and teeny version numbers.
+
+This option was implemented in Bundler 1.4.0 for Ruby 2.0 or earlier.
+
+ ruby "3.1.2", patchlevel: "20"
+
+## GEMS
+
+Specify gem requirements using the `gem` method, with the following arguments.
+All parameters are `OPTIONAL` unless otherwise specified.
+
+### NAME (required)
+
+For each gem requirement, list a single _gem_ line.
+
+ gem "nokogiri"
+
+### VERSION
+
+Each _gem_ `MAY` have one or more version specifiers.
+
+ gem "nokogiri", ">= 1.4.2"
+ gem "RedCloth", ">= 4.1.0", "< 4.2.0"
+
+### REQUIRE AS
+
+Each _gem_ `MAY` specify files that should be used when autorequiring via
+`Bundler.require`. You may pass an array with multiple files or `true` if the file
+you want `required` has the same name as _gem_ or `false` to
+prevent any file from being autorequired.
+
+ gem "redis", require: ["redis/connection/hiredis", "redis"]
+ gem "webmock", require: false
+ gem "byebug", require: true
+
+The argument defaults to the name of the gem. For example, these are identical:
+
+ gem "nokogiri"
+ gem "nokogiri", require: "nokogiri"
+ gem "nokogiri", require: true
+
+### GROUPS
+
+Each _gem_ `MAY` specify membership in one or more groups. Any _gem_ that does
+not specify membership in any group is placed in the `default` group.
+
+ gem "rspec", group: :test
+ gem "wirble", groups: [:development, :test]
+
+The Bundler runtime allows its two main methods, `Bundler.setup` and
+`Bundler.require`, to limit their impact to particular groups.
+
+ # setup adds gems to Ruby's load path
+ Bundler.setup # defaults to all groups
+ require "bundler/setup" # same as Bundler.setup
+ Bundler.setup(:default) # only set up the _default_ group
+ Bundler.setup(:test) # only set up the _test_ group (but `not` _default_)
+ Bundler.setup(:default, :test) # set up the _default_ and _test_ groups, but no others
+
+ # require requires all of the gems in the specified groups
+ Bundler.require # defaults to the _default_ group
+ Bundler.require(:default) # identical
+ Bundler.require(:default, :test) # requires the _default_ and _test_ groups
+ Bundler.require(:test) # requires the _test_ group
+
+The Bundler CLI allows you to specify a list of groups whose gems `bundle install` should
+not install with the `without` configuration.
+
+To specify multiple groups to ignore, specify a list of groups separated by spaces.
+
+ bundle config set --local without test
+ bundle config set --local without development test
+
+Also, calling `Bundler.setup` with no parameters, or calling `require "bundler/setup"`
+will setup all groups except for the ones you excluded via `--without` (since they
+are not available).
+
+Note that on `bundle install`, bundler downloads and evaluates all gems, in order to
+create a single canonical list of all of the required gems and their dependencies.
+This means that you cannot list different versions of the same gems in different
+groups. For more details, see [Understanding Bundler](https://bundler.io/rationale.html).
+
+### PLATFORMS
+
+If a gem should only be used in a particular platform or set of platforms, you can
+specify them. Platforms are essentially identical to groups, except that you do not
+need to use the `--without` install-time flag to exclude groups of gems for other
+platforms.
+
+There are a number of `Gemfile` platforms:
+
+ * `ruby`:
+ C Ruby (MRI), Rubinius, or TruffleRuby, but not Windows
+ * `mri`:
+ C Ruby (MRI) only, but not Windows
+ * `windows`:
+ Windows C Ruby (MRI), including RubyInstaller 32-bit and 64-bit versions
+ * `mswin`:
+ Windows C Ruby (MRI), including RubyInstaller 32-bit versions
+ * `mswin64`:
+ Windows C Ruby (MRI), including RubyInstaller 64-bit versions
+ * `rbx`:
+ Rubinius
+ * `jruby`:
+ JRuby
+ * `truffleruby`:
+ TruffleRuby
+
+On platforms `ruby`, `mri`, `mswin`, `mswin64`, and `windows`, you may
+additionally specify a version by appending the major and minor version numbers
+without a delimiter. For example, to specify that a gem should only be used on
+platform `ruby` version 3.1, use:
+
+ ruby_31
+
+As with groups (above), you may specify one or more platforms:
+
+ gem "weakling", platforms: :jruby
+ gem "ruby-debug", platforms: :mri_31
+ gem "nokogiri", platforms: [:windows_31, :jruby]
+
+All operations involving groups ([`bundle install`](bundle-install.1.html), `Bundler.setup`,
+`Bundler.require`) behave exactly the same as if any groups not
+matching the current platform were explicitly excluded.
+
+The following platform values are deprecated and should be replaced with `windows`:
+
+ * `mswin`, `mswin64`, `mingw32`, `x64_mingw`
+
+Note that, while unfortunately using the same terminology, the values of this
+option are different from the values that `bundle lock --add-platform` can take.
+The values of this option are more closer to "Ruby Implementation" while the
+values that `bundle lock --add-platform` understands are more related to OS and
+architecture of the different systems where your lockfile will be used.
+
+### FORCE_RUBY_PLATFORM
+
+If you always want the pure ruby variant of a gem to be chosen over platform
+specific variants, you can use the `force_ruby_platform` option:
+
+ gem "ffi", force_ruby_platform: true
+
+This can be handy (assuming the pure ruby variant works fine) when:
+
+* You're having issues with the platform specific variant.
+* The platform specific variant does not yet support a newer ruby (and thus has
+ a `required_ruby_version` upper bound), but you still want your Gemfile{.lock}
+ files to resolve under that ruby.
+
+### SOURCE
+
+You can select an alternate RubyGems repository for a gem using the ':source'
+option.
+
+ gem "some_internal_gem", source: "https://gems.example.com"
+
+This forces the gem to be loaded from this source and ignores the global source
+declared at the top level of the file. If the gem does not exist in this source,
+it will not be installed.
+
+Bundler will search for child dependencies of this gem by first looking in the
+source selected for the parent, but if they are not found there, it will fall
+back on the global source.
+
+**Note about a behavior of the feature deprecated in Bundler 1.13**:
+Selecting a specific source repository this way also suppresses the ambiguous
+gem warning described above in [GLOBAL SOURCE](#GLOBAL-SOURCE).
+
+Using the `:source` option for an individual gem will also make that source
+available as a possible global source for any other gems which do not specify
+explicit sources. Thus, when adding gems with explicit sources, it is
+recommended that you also ensure all other gems in the Gemfile are using
+explicit sources.
+
+### GIT
+
+If necessary, you can specify that a gem is located at a particular
+git repository using the `:git` parameter. The repository can be accessed via
+several protocols:
+
+ * `HTTP(S)`:
+ gem "rails", git: "https://github.com/rails/rails.git"
+ * `SSH`:
+ gem "rails", git: "git@github.com:rails/rails.git"
+ * `git`:
+ gem "rails", git: "git://github.com/rails/rails.git"
+
+If using SSH, the user that you use to run `bundle install` `MUST` have the
+appropriate keys available in their `$HOME/.ssh`.
+
+`NOTE`: `http://` and `git://` URLs should be avoided if at all possible. These
+protocols are unauthenticated, so a man-in-the-middle attacker can deliver
+malicious code and compromise your system. HTTPS and SSH are strongly
+preferred.
+
+The `group`, `platforms`, and `require` options are available and behave
+exactly the same as they would for a normal gem.
+
+A git repository `SHOULD` have at least one file, at the root of the
+directory containing the gem, with the extension `.gemspec`. This file
+`MUST` contain a valid gem specification, as expected by the `gem build`
+command.
+
+If a git repository does not have a `.gemspec`, bundler will attempt to
+create one, but it will not contain any dependencies, executables, or
+C extension compilation instructions. As a result, it may fail to properly
+integrate into your application.
+
+If a git repository does have a `.gemspec` for the gem you attached it
+to, a version specifier, if provided, means that the git repository is
+only valid if the `.gemspec` specifies a version matching the version
+specifier. If not, bundler will print a warning.
+
+ gem "rails", "2.3.8", git: "https://github.com/rails/rails.git"
+ # bundle install will fail, because the .gemspec in the rails
+ # repository's master branch specifies version 3.0.0
+
+If a git repository does `not` have a `.gemspec` for the gem you attached
+it to, a version specifier `MUST` be provided. Bundler will use this
+version in the simple `.gemspec` it creates.
+
+Git repositories support a number of additional options.
+
+ * `branch`, `tag`, and `ref`:
+ You `MUST` only specify at most one of these options. The default
+ is `branch: "master"`. For example:
+
+ gem "rails", git: "https://github.com/rails/rails.git", branch: "5-0-stable"
+
+ gem "rails", git: "https://github.com/rails/rails.git", tag: "v5.0.0"
+
+ gem "rails", git: "https://github.com/rails/rails.git", ref: "4aded"
+
+ * `submodules`:
+ For reference, a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules)
+ lets you have another git repository within a subfolder of your repository.
+ Specify `submodules: true` to cause bundler to expand any
+ submodules included in the git repository
+
+If a git repository contains multiple `.gemspecs`, each `.gemspec`
+represents a gem located at the same place in the file system as
+the `.gemspec`.
+
+ |~rails [git root]
+ | |-rails.gemspec [rails gem located here]
+ |~actionpack
+ | |-actionpack.gemspec [actionpack gem located here]
+ |~activesupport
+ | |-activesupport.gemspec [activesupport gem located here]
+ |...
+
+To install a gem located in a git repository, bundler changes to
+the directory containing the gemspec, runs `gem build name.gemspec`
+and then installs the resulting gem. The `gem build` command,
+which comes standard with Rubygems, evaluates the `.gemspec` in
+the context of the directory in which it is located.
+
+### GIT SOURCE
+
+A custom git source can be defined via the `git_source` method. Provide the source's name
+as an argument, and a block which receives a single argument and interpolates it into a
+string to return the full repo address:
+
+ git_source(:stash){ |repo_name| "https://stash.corp.acme.pl/#{repo_name}.git" }
+ gem 'rails', stash: 'forks/rails'
+
+In addition, if you wish to choose a specific branch:
+
+ gem "rails", stash: "forks/rails", branch: "branch_name"
+
+### GITHUB
+
+`NOTE`: This shorthand should be avoided until Bundler 2.0, since it
+currently expands to an insecure `git://` URL. This allows a
+man-in-the-middle attacker to compromise your system.
+
+If the git repository you want to use is hosted on GitHub and is public, you can use the
+:github shorthand to specify the github username and repository name (without the
+trailing ".git"), separated by a slash. If both the username and repository name are the
+same, you can omit one.
+
+ gem "rails", github: "rails/rails"
+ gem "rails", github: "rails"
+
+Are both equivalent to
+
+ gem "rails", git: "https://github.com/rails/rails.git"
+
+Since the `github` method is a specialization of `git_source`, it accepts a `:branch` named argument.
+
+You can also directly pass a pull request URL:
+
+ gem "rails", github: "https://github.com/rails/rails/pull/43753"
+
+Which is equivalent to:
+
+ gem "rails", github: "rails/rails", branch: "refs/pull/43753/head"
+
+### GIST
+
+If the git repository you want to use is hosted as a GitHub Gist and is public, you can use
+the :gist shorthand to specify the gist identifier (without the trailing ".git").
+
+ gem "the_hatch", gist: "4815162342"
+
+Is equivalent to:
+
+ gem "the_hatch", git: "https://gist.github.com/4815162342.git"
+
+Since the `gist` method is a specialization of `git_source`, it accepts a `:branch` named argument.
+
+### BITBUCKET
+
+If the git repository you want to use is hosted on Bitbucket and is public, you can use the
+:bitbucket shorthand to specify the bitbucket username and repository name (without the
+trailing ".git"), separated by a slash. If both the username and repository name are the
+same, you can omit one.
+
+ gem "rails", bitbucket: "rails/rails"
+ gem "rails", bitbucket: "rails"
+
+Are both equivalent to
+
+ gem "rails", git: "https://rails@bitbucket.org/rails/rails.git"
+
+Since the `bitbucket` method is a specialization of `git_source`, it accepts a `:branch` named argument.
+
+### PATH
+
+You can specify that a gem is located in a particular location
+on the file system. Relative paths are resolved relative to the
+directory containing the `Gemfile`.
+
+Similar to the semantics of the `:git` option, the `:path`
+option requires that the directory in question either contains
+a `.gemspec` for the gem, or that you specify an explicit
+version that bundler should use.
+
+Unlike `:git`, bundler does not compile C extensions for
+gems specified as paths.
+
+ gem "rails", path: "vendor/rails"
+
+If you would like to use multiple local gems directly from the filesystem, you can set a global `path` option to the path containing the gem's files. This will automatically load gemspec files from subdirectories.
+
+ path 'components' do
+ gem 'admin_ui'
+ gem 'public_ui'
+ end
+
+## BLOCK FORM OF SOURCE, GIT, PATH, GROUP and PLATFORMS
+
+The `:source`, `:git`, `:path`, `:group`, and `:platforms` options may be
+applied to a group of gems by using block form.
+
+ source "https://gems.example.com" do
+ gem "some_internal_gem"
+ gem "another_internal_gem"
+ end
+
+ git "https://github.com/rails/rails.git" do
+ gem "activesupport"
+ gem "actionpack"
+ end
+
+ platforms :ruby do
+ gem "ruby-debug"
+ gem "sqlite3"
+ end
+
+ group :development, optional: true do
+ gem "wirble"
+ gem "faker"
+ end
+
+In the case of the group block form the :optional option can be given
+to prevent a group from being installed unless listed in the `--with`
+option given to the `bundle install` command.
+
+In the case of the `git` block form, the `:ref`, `:branch`, `:tag`,
+and `:submodules` options may be passed to the `git` method, and
+all gems in the block will inherit those options.
+
+The presence of a `source` block in a Gemfile also makes that source
+available as a possible global source for any other gems which do not specify
+explicit sources. Thus, when defining source blocks, it is
+recommended that you also ensure all other gems in the Gemfile are using
+explicit sources, either via source blocks or `:source` directives on
+individual gems.
+
+## INSTALL_IF
+
+The `install_if` method allows gems to be installed based on a proc or lambda.
+This is especially useful for optional gems that can only be used if certain
+software is installed or some other conditions are met.
+
+ install_if -> { RUBY_PLATFORM =~ /darwin/ } do
+ gem "pasteboard"
+ end
+
+## GEMSPEC
+
+The [`.gemspec`](https://guides.rubygems.org/specification-reference/) file is where
+ you provide metadata about your gem to Rubygems. Some required Gemspec
+ attributes include the name, description, and homepage of your gem. This is
+ also where you specify the dependencies your gem needs to run.
+
+If you wish to use Bundler to help install dependencies for a gem while it is
+being developed, use the `gemspec` method to pull in the dependencies listed in
+the `.gemspec` file.
+
+The `gemspec` method adds any runtime dependencies as gem requirements in the
+default group. It also adds development dependencies as gem requirements in the
+`development` group. Finally, it adds a gem requirement on your project (`path:
+'.'`). In conjunction with `Bundler.setup`, this allows you to require project
+files in your test code as you would if the project were installed as a gem; you
+need not manipulate the load path manually or require project files via relative
+paths.
+
+The `gemspec` method supports optional `:path`, `:glob`, `:name`, and `:development_group`
+options, which control where bundler looks for the `.gemspec`, the glob it uses to look
+for the gemspec (defaults to: `{,*,*/*}.gemspec`), what named `.gemspec` it uses
+(if more than one is present), and which group development dependencies are included in.
+
+When a `gemspec` dependency encounters version conflicts during resolution, the
+local version under development will always be selected -- even if there are
+remote versions that better match other requirements for the `gemspec` gem.
+
+## OVERRIDE
+
+The `override` directive rewrites a constraint on another gem before
+resolution runs. It targets the common case where an upstream gem's
+published metadata is too narrow on the current project's machine -- a stale
+upper bound, an unwanted floor, or a transitive pin that has to be lifted.
+
+ override <target>, <field>: <operation>
+
+`<target>` is a gem name string or `:all`. `<field>` is one of `version:`,
+`required_ruby_version:`, or `required_rubygems_version:`. `<operation>` is
+one of:
+
+ * a version requirement string (e.g. `">= 8.0"`), which **replaces** the
+ target's requirement absolutely. The original requirement, both direct
+ and transitive, is discarded in favour of the override.
+ * `:ignore_upper`, which removes upper-bound operators (`<` and `<=`) from
+ the existing requirement and folds `~>` into its lower bound (`~> 1.5`
+ becomes `>= 1.5`). Other operators, including `!=`, are preserved.
+ * `nil`, which collapses the requirement to `>= 0` (no constraint at all).
+
+`:all` only applies to `required_ruby_version:` and `required_rubygems_version:`.
+A per-gem override on the same field takes precedence over an `:all` override.
+`:all + version:` is rejected: version requirements only make sense per gem.
+
+Multiple `override` calls for distinct targets are allowed; declaring the
+same `target` and `field` twice is an error.
+
+ source "https://rubygems.org"
+
+ # Force every reference to "rails" -- direct or transitive -- to >= 8.0.
+ override "rails", version: ">= 8.0"
+
+ # Strip the upper bound on nokogiri.
+ override "nokogiri", version: :ignore_upper
+
+ # Drop the version pin on legacy entirely.
+ override "legacy", version: nil
+
+ # Loosen every gem's required_ruby_version upper bound.
+ override :all, required_ruby_version: :ignore_upper
+
+ # Override one specific gem's required_rubygems_version.
+ override "tricky", required_rubygems_version: nil
+
+ gem "rails", "~> 7.0"
+
+The override only affects resolution and the install-time Ruby/RubyGems
+compatibility checks; `Gemfile.lock` continues to reflect the resolved
+versions, not the rewritten requirements. When resolution still fails,
+Bundler appends the active overrides (with their Gemfile location) to the
+error message so it is clear which override shaped the constraint set.
+
+## SOURCE PRIORITY
+
+When attempting to locate a gem to satisfy a gem requirement,
+bundler uses the following priority order:
+
+ 1. The source explicitly attached to the gem (using `:source`, `:path`, or
+ `:git`)
+ 2. For implicit gems (dependencies of explicit gems), any source, git, or path
+ repository declared on the parent. This results in bundler prioritizing the
+ ActiveSupport gem from the Rails git repository over ones from
+ `rubygems.org`
+ 3. If neither of the above conditions are met, the global source will be used.
+ If multiple global sources are specified, they will be prioritized from
+ last to first, but this is deprecated since Bundler 1.13, so Bundler prints
+ a warning and will abort with an error in the future.
+
+## LOCKFILE
+
+By default, Bundler will create a lockfile by adding `.lock` to the end of the
+Gemfile name. To change this, use the `lockfile` method:
+
+ lockfile "/path/to/lockfile.lock"
+
+This is useful when you want to use different lockfiles per ruby version or
+platform.
+
+To avoid writing a lock file, use `false` as the argument:
+
+ lockfile false
+
+This is useful for library development and other situations where the code is
+expected to work with a range of dependency versions.
+
+### LOCKFILE PRECEDENCE
+
+When determining path to the lockfile or whether to create a lockfile, the
+following precedence is used:
+
+1. The `bundle install` `--no-lock` option (which disables lockfile creation).
+1. The `bundle install` `--lockfile` option.
+1. The `BUNDLE_LOCKFILE` environment variable.
+1. The `lockfile` method in the Gemfile.
+1. The default behavior of adding `.lock` to the end of the Gemfile name.
diff --git a/lib/bundler/man/index.txt b/lib/bundler/man/index.txt
new file mode 100644
index 0000000000..f610ba852a
--- /dev/null
+++ b/lib/bundler/man/index.txt
@@ -0,0 +1,31 @@
+Gemfile(5) gemfile.5
+bundle(1) bundle.1
+bundle-add(1) bundle-add.1
+bundle-binstubs(1) bundle-binstubs.1
+bundle-cache(1) bundle-cache.1
+bundle-check(1) bundle-check.1
+bundle-clean(1) bundle-clean.1
+bundle-config(1) bundle-config.1
+bundle-console(1) bundle-console.1
+bundle-doctor(1) bundle-doctor.1
+bundle-env(1) bundle-env.1
+bundle-exec(1) bundle-exec.1
+bundle-fund(1) bundle-fund.1
+bundle-gem(1) bundle-gem.1
+bundle-help(1) bundle-help.1
+bundle-info(1) bundle-info.1
+bundle-init(1) bundle-init.1
+bundle-install(1) bundle-install.1
+bundle-issue(1) bundle-issue.1
+bundle-licenses(1) bundle-licenses.1
+bundle-list(1) bundle-list.1
+bundle-lock(1) bundle-lock.1
+bundle-open(1) bundle-open.1
+bundle-outdated(1) bundle-outdated.1
+bundle-platform(1) bundle-platform.1
+bundle-plugin(1) bundle-plugin.1
+bundle-pristine(1) bundle-pristine.1
+bundle-remove(1) bundle-remove.1
+bundle-show(1) bundle-show.1
+bundle-update(1) bundle-update.1
+bundle-version(1) bundle-version.1
diff --git a/lib/bundler/match_metadata.rb b/lib/bundler/match_metadata.rb
new file mode 100644
index 0000000000..92aeb2e893
--- /dev/null
+++ b/lib/bundler/match_metadata.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Bundler
+ module MatchMetadata
+ def matches_current_metadata?
+ matches_current_ruby? && matches_current_rubygems?
+ end
+
+ def matches_current_ruby?
+ @required_ruby_version.satisfied_by?(Gem.ruby_version)
+ end
+
+ def matches_current_rubygems?
+ @required_rubygems_version.satisfied_by?(Gem.rubygems_version)
+ end
+
+ def matches_current_metadata_with_overrides?(overrides)
+ matches_current_ruby_with_overrides?(overrides) && matches_current_rubygems_with_overrides?(overrides)
+ end
+
+ def matches_current_ruby_with_overrides?(overrides)
+ effective_required_version(@required_ruby_version, :required_ruby_version, overrides).satisfied_by?(Gem.ruby_version)
+ end
+
+ def matches_current_rubygems_with_overrides?(overrides)
+ effective_required_version(@required_rubygems_version, :required_rubygems_version, overrides).satisfied_by?(Gem.rubygems_version)
+ end
+
+ def expanded_dependencies
+ runtime_dependencies + [
+ metadata_dependency("Ruby", @required_ruby_version),
+ metadata_dependency("RubyGems", @required_rubygems_version),
+ ].compact
+ end
+
+ def metadata_dependency(name, requirement)
+ return if requirement.nil? || requirement.none?
+
+ Gem::Dependency.new("#{name}\0", requirement)
+ end
+
+ private
+
+ def effective_required_version(requirement, field, overrides)
+ return requirement if overrides.nil? || overrides.empty?
+ override = Override.find_for(overrides, name, field)
+ override ? override.apply_to(requirement) : requirement
+ end
+ end
+end
diff --git a/lib/bundler/match_platform.rb b/lib/bundler/match_platform.rb
new file mode 100644
index 0000000000..479818e5ec
--- /dev/null
+++ b/lib/bundler/match_platform.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Bundler
+ module MatchPlatform
+ def installable_on_platform?(target_platform) # :nodoc:
+ return true if [Gem::Platform::RUBY, nil, target_platform].include?(platform)
+ return true if Gem::Platform.new(platform) === target_platform
+
+ false
+ end
+
+ def self.select_best_platform_match(specs, platform, force_ruby: false, prefer_locked: false)
+ matching = select_all_platform_match(specs, platform, force_ruby: force_ruby, prefer_locked: prefer_locked)
+
+ Gem::Platform.sort_and_filter_best_platform_match(matching, platform)
+ end
+
+ def self.select_best_local_platform_match(specs, force_ruby: false)
+ local = Bundler.local_platform
+ matching = select_all_platform_match(specs, local, force_ruby: force_ruby).filter_map(&:materialized_for_installation)
+
+ Gem::Platform.sort_best_platform_match(matching, local)
+ end
+
+ def self.select_all_platform_match(specs, platform, force_ruby: false, prefer_locked: false)
+ matching = specs.select {|spec| spec.installable_on_platform?(force_ruby ? Gem::Platform::RUBY : platform) }
+
+ specs.each(&:force_ruby_platform!) if force_ruby
+
+ if prefer_locked
+ locked_originally = matching.select {|spec| spec.is_a?(::Bundler::LazySpecification) }
+ return locked_originally if locked_originally.any?
+ end
+
+ matching
+ end
+
+ def self.generic_local_platform_is_ruby?
+ Bundler.generic_local_platform == Gem::Platform::RUBY
+ end
+ end
+end
diff --git a/lib/bundler/match_remote_metadata.rb b/lib/bundler/match_remote_metadata.rb
new file mode 100644
index 0000000000..601af7e55d
--- /dev/null
+++ b/lib/bundler/match_remote_metadata.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Bundler
+ module FetchMetadata
+ # A fallback is included because the original version of the specification
+ # API didn't include that field, so some marshalled specs in the index have it
+ # set to +nil+.
+ def matches_current_ruby?
+ ensure_required_ruby_version_loaded
+ super
+ end
+
+ def matches_current_rubygems?
+ ensure_required_rubygems_version_loaded
+ super
+ end
+
+ def matches_current_ruby_with_overrides?(overrides)
+ ensure_required_ruby_version_loaded
+ super
+ end
+
+ def matches_current_rubygems_with_overrides?(overrides)
+ ensure_required_rubygems_version_loaded
+ super
+ end
+
+ private
+
+ def ensure_required_ruby_version_loaded
+ @required_ruby_version ||= _remote_specification.required_ruby_version || Gem::Requirement.default # rubocop:disable Naming/MemoizedInstanceVariableName
+ end
+
+ def ensure_required_rubygems_version_loaded
+ @required_rubygems_version ||= _remote_specification.required_rubygems_version || Gem::Requirement.default # rubocop:disable Naming/MemoizedInstanceVariableName
+ end
+ end
+
+ module MatchRemoteMetadata
+ include MatchMetadata
+
+ prepend FetchMetadata
+ end
+end
diff --git a/lib/bundler/materialization.rb b/lib/bundler/materialization.rb
new file mode 100644
index 0000000000..82e48464a7
--- /dev/null
+++ b/lib/bundler/materialization.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Bundler
+ #
+ # This class materializes a set of resolved specifications (`LazySpecification`)
+ # for a given gem into the most appropriate real specifications
+ # (`StubSepecification`, `EndpointSpecification`, etc), given a dependency and a
+ # target platform.
+ #
+ class Materialization
+ def initialize(dep, platform, candidates:)
+ @dep = dep
+ @platform = platform
+ @candidates = candidates
+ end
+
+ def complete?
+ specs.any?
+ end
+
+ def specs
+ @specs ||= if @candidates.nil?
+ []
+ elsif platform
+ MatchPlatform.select_best_platform_match(@candidates, platform, force_ruby: dep.force_ruby_platform)
+ else
+ MatchPlatform.select_best_local_platform_match(@candidates, force_ruby: dep.force_ruby_platform || dep.default_force_ruby_platform)
+ end
+ end
+
+ def dependencies
+ (materialized_spec || specs.first).runtime_dependencies.map {|d| [d, platform] }
+ end
+
+ def materialized_spec
+ specs.reject(&:missing?).first&.materialization
+ end
+
+ def completely_missing_specs
+ return [] unless specs.all?(&:missing?)
+
+ specs
+ end
+
+ def partially_missing_specs
+ specs.select(&:missing?)
+ end
+
+ def incomplete_specs
+ return [] if complete?
+
+ @candidates || LazySpecification.new(dep.name, nil, nil)
+ end
+
+ private
+
+ attr_reader :dep, :platform
+ end
+end
diff --git a/lib/bundler/mirror.rb b/lib/bundler/mirror.rb
new file mode 100644
index 0000000000..494a6d6aef
--- /dev/null
+++ b/lib/bundler/mirror.rb
@@ -0,0 +1,221 @@
+# frozen_string_literal: true
+
+require "socket"
+
+module Bundler
+ class Settings
+ # Class used to build the mirror set and then find a mirror for a given URI
+ #
+ # @param prober [Prober object, nil] by default a TCPSocketProbe, this object
+ # will be used to probe the mirror address to validate that the mirror replies.
+ class Mirrors
+ def initialize(prober = nil)
+ @all = Mirror.new
+ @prober = prober || TCPSocketProbe.new
+ @mirrors = {}
+ end
+
+ # Returns a mirror for the given uri.
+ #
+ # Depending on the uri having a valid mirror or not, it may be a
+ # mirror that points to the provided uri
+ def for(uri)
+ if @all.validate!(@prober).valid?
+ @all
+ else
+ fetch_valid_mirror_for(Settings.normalize_uri(uri))
+ end
+ end
+
+ def each
+ @mirrors.each do |k, v|
+ yield k, v.uri.to_s
+ end
+ end
+
+ def parse(key, value)
+ config = MirrorConfig.new(key, value)
+ mirror = if config.all?
+ @all
+ else
+ @mirrors[config.uri] ||= Mirror.new
+ end
+ config.update_mirror(mirror)
+ end
+
+ private
+
+ def fetch_valid_mirror_for(uri)
+ downcased = uri.to_s.downcase
+ mirror = @mirrors[downcased] || @mirrors[Gem::URI(downcased).host] || Mirror.new(uri)
+ mirror.validate!(@prober)
+ mirror = Mirror.new(uri) unless mirror.valid?
+ mirror
+ end
+ end
+
+ # A mirror
+ #
+ # Contains both the uri that should be used as a mirror and the
+ # fallback timeout which will be used for probing if the mirror
+ # replies on time or not.
+ class Mirror
+ DEFAULT_FALLBACK_TIMEOUT = 0.1
+
+ attr_reader :uri, :fallback_timeout
+
+ def initialize(uri = nil, fallback_timeout = 0)
+ self.uri = uri
+ self.fallback_timeout = fallback_timeout
+ @valid = nil
+ end
+
+ def uri=(uri)
+ @uri = if uri.nil?
+ nil
+ else
+ Gem::URI(uri.to_s)
+ end
+ @valid = nil
+ end
+
+ def fallback_timeout=(timeout)
+ case timeout
+ when true, "true"
+ @fallback_timeout = DEFAULT_FALLBACK_TIMEOUT
+ when false, "false"
+ @fallback_timeout = 0
+ else
+ @fallback_timeout = timeout.to_i
+ end
+ @valid = nil
+ end
+
+ def ==(other)
+ !other.nil? && uri == other.uri && fallback_timeout == other.fallback_timeout
+ end
+
+ def valid?
+ return false if @uri.nil?
+ return @valid unless @valid.nil?
+ false
+ end
+
+ def validate!(probe = nil)
+ @valid = false if uri.nil?
+ if @valid.nil?
+ @valid = fallback_timeout == 0 || (probe || TCPSocketProbe.new).replies?(self)
+ end
+ self
+ end
+ end
+
+ # Class used to parse one configuration line
+ #
+ # Gets the configuration line and the value.
+ # This object provides a `update_mirror` method
+ # used to setup the given mirror value.
+ class MirrorConfig
+ attr_accessor :uri, :value
+
+ def initialize(config_line, value)
+ uri, fallback =
+ config_line.match(%r{\Amirror\.(all|.+?)(\.fallback_timeout)?\/?\z}).captures
+ @fallback = !fallback.nil?
+ @all = false
+ if uri == "all"
+ @all = true
+ else
+ @uri = Gem::URI(uri).absolute? ? Settings.normalize_uri(uri) : uri
+ end
+ @value = value
+ end
+
+ def all?
+ @all
+ end
+
+ def update_mirror(mirror)
+ if @fallback
+ mirror.fallback_timeout = @value
+ else
+ mirror.uri = Settings.normalize_uri(@value)
+ end
+ end
+ end
+
+ # Class used for probing TCP availability for a given mirror.
+ class TCPSocketProbe
+ def replies?(mirror)
+ MirrorSockets.new(mirror).any? do |socket, address, timeout|
+ socket.connect_nonblock(address)
+ rescue Errno::EINPROGRESS
+ wait_for_writtable_socket(socket, address, timeout)
+ rescue RuntimeError # Connection failed somehow, again
+ false
+ end
+ end
+
+ private
+
+ def wait_for_writtable_socket(socket, address, timeout)
+ if IO.select(nil, [socket], nil, timeout)
+ probe_writtable_socket(socket, address)
+ else # TCP Handshake timed out, or there is something dropping packets
+ false
+ end
+ end
+
+ def probe_writtable_socket(socket, address)
+ socket.connect_nonblock(address)
+ rescue Errno::EISCONN
+ true
+ rescue StandardError # Connection failed
+ false
+ end
+ end
+ end
+
+ # Class used to build the list of sockets that correspond to
+ # a given mirror.
+ #
+ # One mirror may correspond to many different addresses, both
+ # because of it having many dns entries or because
+ # the network interface is both ipv4 and ipv5
+ class MirrorSockets
+ def initialize(mirror)
+ @timeout = mirror.fallback_timeout
+ @addresses = Socket.getaddrinfo(mirror.uri.host, mirror.uri.port).map do |address|
+ SocketAddress.new(address[0], address[3], address[1])
+ end
+ end
+
+ def any?
+ @addresses.any? do |address|
+ socket = Socket.new(Socket.const_get(address.type), Socket::SOCK_STREAM, 0)
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
+ value = yield socket, address.to_socket_address, @timeout
+ socket.close unless socket.closed?
+ value
+ end
+ end
+ end
+
+ # Socket address builder.
+ #
+ # Given a socket type, a host and a port,
+ # provides a method to build sockaddr string
+ class SocketAddress
+ attr_reader :type, :host, :port
+
+ def initialize(type, host, port)
+ @type = type
+ @host = host
+ @port = port
+ end
+
+ def to_socket_address
+ Socket.pack_sockaddr_in(@port, @host)
+ end
+ end
+end
diff --git a/lib/bundler/override.rb b/lib/bundler/override.rb
new file mode 100644
index 0000000000..0e0ec59fd7
--- /dev/null
+++ b/lib/bundler/override.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Override
+ UPPER_BOUND_OPERATORS = ["<", "<="].freeze
+
+ def self.find_for(overrides, name, field)
+ overrides.find {|o| o.target == name && o.field == field } ||
+ overrides.find {|o| o.target == :all && o.field == field }
+ end
+
+ # Attach the given overrides onto every LazySpecification in `specs` so
+ # downstream consumers (LazySpecification#choose_compatible, the install-
+ # time compatibility check, etc.) can read the override list off the spec
+ # itself. Non-LazySpec entries (StubSpecification, Gem::Specification, ...)
+ # are left untouched.
+ def self.attach(specs, overrides)
+ return if overrides.nil? || overrides.empty?
+ specs.each {|s| s.overrides = overrides if s.is_a?(LazySpecification) }
+ end
+
+ attr_reader :target, :field, :operation, :source_location
+
+ def initialize(target, field, operation, source_location: nil)
+ @target = target
+ @field = field
+ @operation = operation
+ @source_location = source_location
+ end
+
+ def source_location_label
+ return nil unless @source_location
+ "#{File.basename(@source_location.path)}:#{@source_location.lineno}"
+ end
+
+ def apply_to(requirement)
+ case operation
+ when nil
+ Gem::Requirement.default
+ when :ignore_upper
+ remove_upper_bounds(requirement)
+ when String
+ Gem::Requirement.new(operation)
+ else
+ raise ArgumentError, "unsupported override operation: #{operation.inspect}"
+ end
+ end
+
+ private
+
+ def remove_upper_bounds(requirement)
+ return Gem::Requirement.default if requirement.nil? || requirement.none?
+
+ preserved = requirement.requirements.filter_map do |op, version|
+ if UPPER_BOUND_OPERATORS.include?(op)
+ nil
+ elsif op == "~>"
+ [">=", version]
+ else
+ [op, version]
+ end
+ end
+
+ return Gem::Requirement.default if preserved.empty?
+
+ Gem::Requirement.new(preserved.map {|op, v| "#{op} #{v}" })
+ end
+ end
+end
diff --git a/lib/bundler/plugin.rb b/lib/bundler/plugin.rb
new file mode 100644
index 0000000000..faca6bea53
--- /dev/null
+++ b/lib/bundler/plugin.rb
@@ -0,0 +1,381 @@
+# frozen_string_literal: true
+
+require_relative "plugin/api"
+
+module Bundler
+ module Plugin
+ autoload :DSL, File.expand_path("plugin/dsl", __dir__)
+ autoload :Events, File.expand_path("plugin/events", __dir__)
+ autoload :Index, File.expand_path("plugin/index", __dir__)
+ autoload :Installer, File.expand_path("plugin/installer", __dir__)
+ autoload :SourceList, File.expand_path("plugin/source_list", __dir__)
+
+ class MalformattedPlugin < PluginError; end
+ class UndefinedCommandError < PluginError; end
+ class UnknownSourceError < PluginError; end
+ class PluginInstallError < PluginError; end
+
+ PLUGIN_FILE_NAME = "plugins.rb"
+
+ module_function
+
+ def reset!
+ instance_variables.each {|i| remove_instance_variable(i) }
+
+ @sources = {}
+ @commands = {}
+ @hooks_by_event = Hash.new {|h, k| h[k] = [] }
+ @loaded_plugin_names = []
+ end
+
+ reset!
+
+ # Installs a new plugin by the given name
+ #
+ # @param [Array<String>] names the name of plugin to be installed
+ # @param [Hash] options various parameters as described in description.
+ # Refer to cli/plugin for available options
+ def install(names, options)
+ raise InvalidOption, "You cannot specify `--branch` and `--ref` at the same time." if options["branch"] && options["ref"]
+
+ specs = Installer.new.install(names, options)
+
+ save_plugins names, specs
+ rescue PluginError
+ specs_to_delete = specs.select {|k, _v| names.include?(k) && !index.commands.values.include?(k) }
+ specs_to_delete.each_value {|spec| Bundler.rm_rf(spec.full_gem_path) }
+
+ raise
+ end
+
+ # Uninstalls plugins by the given names
+ #
+ # @param [Array<String>] names the names of plugins to be uninstalled
+ def uninstall(names, options)
+ if names.empty? && !options[:all]
+ Bundler.ui.error "No plugins to uninstall. Specify at least 1 plugin to uninstall.\n"\
+ "Use --all option to uninstall all the installed plugins."
+ return
+ end
+
+ names = index.installed_plugins if options[:all]
+ if names.any?
+ names.each do |name|
+ if index.installed?(name)
+ path = index.plugin_path(name).to_s
+ Bundler.rm_rf(path) if index.installed_in_plugin_root?(name)
+ index.unregister_plugin(name)
+ Bundler.ui.info "Uninstalled plugin #{name}"
+ else
+ Bundler.ui.error "Plugin #{name} is not installed \n"
+ end
+ end
+ else
+ Bundler.ui.info "No plugins installed"
+ end
+ end
+
+ # List installed plugins and commands
+ #
+ def list
+ installed_plugins = index.installed_plugins
+ if installed_plugins.any?
+ output = String.new
+ installed_plugins.each do |plugin|
+ output << "#{plugin}\n"
+ output << "-----\n"
+ index.plugin_commands(plugin).each do |command|
+ output << " #{command}\n"
+ end
+ output << "\n"
+ end
+ else
+ output = "No plugins installed"
+ end
+ Bundler.ui.info output
+ end
+
+ # Evaluates the Gemfile with a limited DSL and installs the plugins
+ # specified by plugin method
+ #
+ # @param [Pathname] gemfile path
+ # @param [Proc] block that can be evaluated for (inline) Gemfile
+ def gemfile_install(gemfile = nil, &inline)
+ Bundler.settings.temporary(frozen: false, deployment: false) do
+ builder = DSL.new
+ if block_given?
+ builder.instance_eval(&inline)
+ else
+ builder.eval_gemfile(gemfile)
+ end
+ builder.check_primary_source_safety
+ definition = builder.to_definition(nil, true)
+
+ return if definition.dependencies.empty?
+
+ plugins = definition.dependencies.map(&:name)
+ installed_specs = Installer.new.install_definition(definition)
+
+ save_plugins plugins, installed_specs, builder.inferred_plugins
+ end
+ rescue RuntimeError => e
+ unless e.is_a?(GemfileError)
+ Bundler.ui.error "Failed to install plugin: #{e.message}\n #{e.backtrace[0]}"
+ end
+ raise
+ end
+
+ # The index object used to store the details about the plugin
+ def index
+ @index ||= Index.new
+ end
+
+ # The directory root for all plugin related data
+ #
+ # If run in an app, points to local root, in app_config_path
+ # Otherwise, points to global root, in Bundler.user_bundle_path("plugin")
+ def root
+ @root ||= if SharedHelpers.in_bundle?
+ local_root
+ else
+ global_root
+ end
+ end
+
+ def local_root
+ Bundler.app_config_path.join("plugin")
+ end
+
+ # The global directory root for all plugin related data
+ def global_root
+ Bundler.user_bundle_path("plugin")
+ end
+
+ # The cache directory for plugin stuffs
+ def cache
+ @cache ||= root.join("cache")
+ end
+
+ # To be called via the API to register to handle a command
+ def add_command(command, cls)
+ @commands[command] = cls
+ end
+
+ # Checks if any plugin handles the command
+ def command?(command)
+ !index.command_plugin(command).nil?
+ end
+
+ # To be called from Cli class to pass the command and argument to
+ # appropriate plugin class
+ def exec_command(command, args)
+ raise UndefinedCommandError, "Command `#{command}` not found" unless command? command
+
+ load_plugin index.command_plugin(command) unless @commands.key? command
+
+ @commands[command].new.exec(command, args)
+ end
+
+ # To be called via the API to register to handle a source plugin
+ def add_source(source, cls)
+ @sources[source] = cls
+ end
+
+ # Checks if any plugin declares the source
+ def source?(name)
+ !index.source_plugin(name.to_s).nil?
+ end
+
+ # @return [Class] that handles the source. The class includes API::Source
+ def source(name)
+ raise UnknownSourceError, "Source #{name} not found" unless source? name
+
+ load_plugin(index.source_plugin(name)) unless @sources.key? name
+
+ @sources[name]
+ end
+
+ # @param [Hash] The options that are present in the lockfile
+ # @return [API::Source] the instance of the class that handles the source
+ # type passed in locked_opts
+ def from_lock(locked_opts)
+ src = source(locked_opts["type"])
+
+ src.new(locked_opts.merge("uri" => locked_opts["remote"]))
+ end
+
+ # To be called via the API to register a hooks and corresponding block that
+ # will be called to handle the hook
+ def add_hook(event, &block)
+ unless Events.defined_event?(event)
+ raise ArgumentError, "Event '#{event}' not defined in Bundler::Plugin::Events"
+ end
+ @hooks_by_event[event.to_s] << block
+ end
+
+ # Runs all the hooks that are registered for the passed event
+ #
+ # It passes the passed arguments and block to the block registered with
+ # the api.
+ #
+ # @param [String] event
+ def hook(event, *args, &arg_blk)
+ return unless Bundler.settings[:plugins]
+ unless Events.defined_event?(event)
+ raise ArgumentError, "Event '#{event}' not defined in Bundler::Plugin::Events"
+ end
+
+ plugins = index.hook_plugins(event)
+ return unless plugins.any?
+
+ plugins.each {|name| load_plugin(name) }
+
+ @hooks_by_event[event].each {|blk| blk.call(*args, &arg_blk) }
+ end
+
+ # currently only intended for specs
+ #
+ # @return [String, nil] installed path
+ def installed?(plugin)
+ Index.new.installed?(plugin)
+ end
+
+ # @return [true, false] whether the plugin is loaded
+ def loaded?(plugin)
+ @loaded_plugin_names.include?(plugin)
+ end
+
+ # Post installation processing and registering with index
+ #
+ # @param [Array<String>] plugins list to be installed
+ # @param [Hash] specs of plugins mapped to installation path (currently they
+ # contain all the installed specs, including plugins)
+ # @param [Array<String>] names of inferred source plugins that can be ignored
+ def save_plugins(plugins, specs, optional_plugins = [])
+ plugins.each do |name|
+ spec = specs[name]
+
+ # It's possible that the `plugin` found in the Gemfile don't appear in the specs. For instance when
+ # calling `BUNDLE_WITHOUT=default bundle install`, the plugins will not get installed.
+ next if spec.nil?
+ next if index.up_to_date?(spec)
+
+ save_plugin(name, spec, optional_plugins.include?(name))
+ end
+ end
+
+ # Checks if the gem is good to be a plugin
+ #
+ # At present it only checks whether it contains plugins.rb file
+ #
+ # @param [Pathname] plugin_path the path plugin is installed at
+ # @raise [MalformattedPlugin] if plugins.rb file is not found
+ def validate_plugin!(plugin_path)
+ plugin_file = plugin_path.join(PLUGIN_FILE_NAME)
+ raise MalformattedPlugin, "#{PLUGIN_FILE_NAME} was not found in the plugin." unless plugin_file.file?
+ end
+
+ # Validates and registers a plugin.
+ #
+ # @param [String] name the name of the plugin
+ # @param [Specification] spec of installed plugin
+ # @param [Boolean] optional_plugin, removed if there is conflict with any
+ # other plugin (used for default source plugins)
+ #
+ # @raise [PluginInstallError] if validation or registration raises any error
+ def save_plugin(name, spec, optional_plugin = false)
+ validate_plugin! Pathname.new(spec.full_gem_path)
+ installed = register_plugin(name, spec, optional_plugin)
+ Bundler.ui.info "Installed plugin #{name}" if installed
+ rescue PluginError => e
+ raise PluginInstallError, "Failed to install plugin `#{spec.name}`, due to #{e.class} (#{e.message})"
+ end
+
+ # Runs the plugins.rb file in an isolated namespace, records the plugin
+ # actions it registers for and then passes the data to index to be stored.
+ #
+ # @param [String] name the name of the plugin
+ # @param [Specification] spec of installed plugin
+ # @param [Boolean] optional_plugin, removed if there is conflict with any
+ # other plugin (used for default source plugins)
+ #
+ # @raise [MalformattedPlugin] if plugins.rb raises any error
+ def register_plugin(name, spec, optional_plugin = false)
+ commands = @commands
+ sources = @sources
+ hooks = @hooks_by_event
+
+ @commands = {}
+ @sources = {}
+ @hooks_by_event = Hash.new {|h, k| h[k] = [] }
+
+ load_paths = spec.load_paths
+ Gem.add_to_load_path(*load_paths)
+ path = Pathname.new spec.full_gem_path
+
+ begin
+ load path.join(PLUGIN_FILE_NAME), true
+ rescue StandardError => e
+ raise MalformattedPlugin, "#{e.class}: #{e.message}"
+ end
+
+ if optional_plugin && @sources.keys.any? {|s| source? s }
+ Bundler.rm_rf(path)
+ false
+ else
+ index.register_plugin(name, path.to_s, load_paths, @commands.keys,
+ @sources.keys, @hooks_by_event.keys)
+ true
+ end
+ ensure
+ @commands = commands
+ @sources = sources
+ @hooks_by_event = hooks
+ end
+
+ # Executes the plugins.rb file
+ #
+ # @param [String] name of the plugin
+ def load_plugin(name)
+ return unless name && !name.empty?
+ return if loaded?(name)
+
+ # Need to ensure before this that plugin root where the rest of gems
+ # are installed to be on load path to support plugin deps. Currently not
+ # done to avoid conflicts
+ path = index.plugin_path(name)
+
+ paths = index.load_paths(name)
+ invalid_paths = paths.reject {|p| File.directory?(p) }
+
+ if invalid_paths.any?
+ Bundler.ui.warn <<~MESSAGE
+ The following plugin paths don't exist: #{invalid_paths.join(", ")}.
+
+ This can happen if the plugin was installed with a different version of Ruby that has since been uninstalled.
+
+ If you would like to reinstall the plugin, run:
+
+ bundler plugin uninstall #{name} && bundler plugin install #{name}
+
+ Continuing without installing plugin #{name}.
+ MESSAGE
+
+ return
+ end
+
+ Gem.add_to_load_path(*paths)
+
+ load path.join(PLUGIN_FILE_NAME)
+
+ @loaded_plugin_names << name
+ rescue RuntimeError => e
+ Bundler.ui.error "Failed loading plugin #{name}: #{e.message}"
+ raise
+ end
+
+ class << self
+ private :load_plugin, :register_plugin, :save_plugins, :validate_plugin!
+ end
+ end
+end
diff --git a/lib/bundler/plugin/api.rb b/lib/bundler/plugin/api.rb
new file mode 100644
index 0000000000..ee2bffe3ab
--- /dev/null
+++ b/lib/bundler/plugin/api.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module Bundler
+ # This is the interfacing class represents the API that we intend to provide
+ # the plugins to use.
+ #
+ # For plugins to be independent of the Bundler internals they shall limit their
+ # interactions to methods of this class only. This will save them from breaking
+ # when some internal change.
+ #
+ # Currently we are delegating the methods defined in Bundler class to
+ # itself. So, this class acts as a buffer.
+ #
+ # If there is some change in the Bundler class that is incompatible to its
+ # previous behavior or if otherwise desired, we can reimplement(or implement)
+ # the method to preserve compatibility.
+ #
+ # To use this, either the class can inherit this class or use it directly.
+ # For example of both types of use, refer the file `spec/plugins/command.rb`
+ #
+ # To use it without inheriting, you will have to create an object of this
+ # to use the functions (except for declaration functions like command, source,
+ # and hooks).
+ module Plugin
+ class API
+ autoload :Source, File.expand_path("api/source", __dir__)
+
+ # The plugins should declare that they handle a command through this helper.
+ #
+ # @param [String] command being handled by them
+ # @param [Class] (optional) class that handles the command. If not
+ # provided, the `self` class will be used.
+ def self.command(command, cls = self)
+ Plugin.add_command command, cls
+ end
+
+ # The plugins should declare that they provide a installation source
+ # through this helper.
+ #
+ # @param [String] the source type they provide
+ # @param [Class] (optional) class that handles the source. If not
+ # provided, the `self` class will be used.
+ def self.source(source, cls = self)
+ cls.send :include, Bundler::Plugin::API::Source
+ Plugin.add_source source, cls
+ end
+
+ def self.hook(event, &block)
+ Plugin.add_hook(event, &block)
+ end
+
+ # The cache dir to be used by the plugins for storage
+ #
+ # @return [Pathname] path of the cache dir
+ def cache_dir
+ Plugin.cache.join("plugins")
+ end
+
+ # A tmp dir to be used by plugins
+ # Accepts names that get concatenated as suffix
+ #
+ # @return [Pathname] object for the new directory created
+ def tmp(*names)
+ Bundler.tmp(["plugin", *names].join("-"))
+ end
+
+ def method_missing(name, *args, &blk)
+ return Bundler.send(name, *args, &blk) if Bundler.respond_to?(name)
+
+ return SharedHelpers.send(name, *args, &blk) if SharedHelpers.respond_to?(name)
+
+ super
+ end
+
+ def respond_to_missing?(name, include_private = false)
+ SharedHelpers.respond_to?(name, include_private) ||
+ Bundler.respond_to?(name, include_private) || super
+ end
+ end
+ end
+end
diff --git a/lib/bundler/plugin/api/source.rb b/lib/bundler/plugin/api/source.rb
new file mode 100644
index 0000000000..798326673a
--- /dev/null
+++ b/lib/bundler/plugin/api/source.rb
@@ -0,0 +1,330 @@
+# frozen_string_literal: true
+
+module Bundler
+ module Plugin
+ class API
+ # This class provides the base to build source plugins
+ # All the method here are required to build a source plugin (except
+ # `uri_hash`, `gem_install_dir`; they are helpers).
+ #
+ # Defaults for methods, where ever possible are provided which is
+ # expected to work. But, all source plugins have to override
+ # `fetch_gemspec_files` and `install`. Defaults are also not provided for
+ # `remote!`, `cache!` and `unlock!`.
+ #
+ # The defaults shall work for most situations but nevertheless they can
+ # be (preferably should be) overridden as per the plugins' needs safely
+ # (as long as they behave as expected).
+ # On overriding `initialize` you should call super first.
+ #
+ # If required plugin should override `hash`, `==` and `eql?` methods to be
+ # able to match objects representing same sources, but may be created in
+ # different situation (like form gemfile and lockfile). The default ones
+ # checks only for class and uri, but elaborate source plugins may need
+ # more comparisons (e.g. git checking on branch or tag).
+ #
+ # @!attribute [r] uri
+ # @return [String] the remote specified with `source` block in Gemfile
+ #
+ # @!attribute [r] options
+ # @return [String] options passed during initialization (either from
+ # lockfile or Gemfile)
+ #
+ # @!attribute [r] name
+ # @return [String] name that can be used to uniquely identify a source
+ #
+ # @!attribute [rw] dependency_names
+ # @return [Array<String>] Names of dependencies that the source should
+ # try to resolve. It is not necessary to use this list internally. This
+ # is present to be compatible with `Definition` and is used by
+ # rubygems source.
+ module Source
+ attr_reader :uri, :options, :name, :checksum_store
+ attr_accessor :dependency_names
+
+ def initialize(opts)
+ @options = opts
+ @dependency_names = []
+ @uri = opts["uri"]
+ @type = opts["type"]
+ @name = opts["name"] || "#{@type} at #{@uri}"
+ @checksum_store = Checksum::Store.new
+ end
+
+ # This is used by the default `spec` method to constructs the
+ # Specification objects for the gems and versions that can be installed
+ # by this source plugin.
+ #
+ # Note: If the spec method is overridden, this function is not necessary
+ #
+ # @return [Array<String>] paths of the gemspec files for gems that can
+ # be installed
+ def fetch_gemspec_files
+ []
+ end
+
+ # Options to be saved in the lockfile so that the source plugin is able
+ # to check out same version of gem later.
+ #
+ # There options are passed when the source plugin is created from the
+ # lockfile.
+ #
+ # @return [Hash]
+ def options_to_lock
+ {}
+ end
+
+ # Download the gem specified by the spec at appropriate path.
+ #
+ # A source plugin can implement this method to split the download and the
+ # installation of a gem.
+ #
+ # @return [Boolean] Whether the download of the gem succeeded.
+ def download(spec, opts); end
+
+ # Install the gem specified by the spec at appropriate path.
+ # `install_path` provides a sufficient default, if the source can only
+ # satisfy one gem, but is not binding.
+ #
+ # @return [String] post installation message (if any)
+ def install(spec, opts)
+ raise MalformattedPlugin, "Source plugins need to override the install method."
+ end
+
+ # It builds extensions, generates bins and installs them for the spec
+ # provided.
+ #
+ # It depends on `spec.loaded_from` to get full_gem_path. The source
+ # plugins should set that.
+ #
+ # It should be called in `install` after the plugin is done placing the
+ # gem at correct install location.
+ #
+ # It also runs Gem hooks `pre_install`, `post_build` and `post_install`
+ #
+ # Note: Do not override if you don't know what you are doing.
+ def post_install(spec, disable_exts = false)
+ opts = { env_shebang: false, disable_extensions: disable_exts }
+ installer = Bundler::Source::Path::Installer.new(spec, opts)
+ installer.post_install
+ end
+
+ # A default installation path to install a single gem. If the source
+ # servers multiple gems, it's not of much use and the source should one
+ # of its own.
+ def install_path
+ @install_path ||=
+ begin
+ base_name = File.basename(Gem::URI.parse(uri).normalize.path)
+
+ gem_install_dir.join("#{base_name}-#{uri_hash[0..11]}")
+ end
+ end
+
+ # Parses the gemspec files to find the specs for the gems that can be
+ # satisfied by the source.
+ #
+ # Few important points to keep in mind:
+ # - If the gems are not installed then it shall return specs for all
+ # the gems it can satisfy
+ # - If gem is installed (that is to be detected by the plugin itself)
+ # then it shall return at least the specs that are installed.
+ # - The `loaded_from` for each of the specs shall be correct (it is
+ # used to find the load path)
+ #
+ # @return [Bundler::Index] index containing the specs
+ def specs
+ files = fetch_gemspec_files
+
+ Bundler::Index.build do |index|
+ files.each do |file|
+ next unless spec = Bundler.load_gemspec(file)
+ spec.installed_by_version = Gem::VERSION
+
+ spec.source = self
+ Bundler.rubygems.validate(spec)
+
+ index << spec
+ end
+ end
+ end
+
+ # Set internal representation to fetch the gems/specs locally.
+ #
+ # When this is called, the source should try to fetch the specs and
+ # install from the local system.
+ def local!
+ end
+
+ # Set internal representation to fetch the gems/specs from remote.
+ #
+ # When this is called, the source should try to fetch the specs and
+ # install from remote path.
+ def remote!
+ end
+
+ # Set internal representation to fetch the gems/specs from app cache.
+ #
+ # When this is called, the source should try to fetch the specs and
+ # install from the path provided by `app_cache_path`.
+ def cached!
+ end
+
+ # This is called to update the spec and installation.
+ #
+ # If the source plugin is loaded from lockfile or otherwise, it shall
+ # refresh the cache/specs (e.g. git sources can make a fresh clone).
+ def unlock!
+ end
+
+ # Name of directory where plugin the is expected to cache the gems when
+ # #cache is called.
+ #
+ # Also this name is matched against the directories in cache for pruning
+ #
+ # This is used by `app_cache_path`
+ def app_cache_dirname
+ base_name = File.basename(Gem::URI.parse(uri).normalize.path)
+ "#{base_name}-#{uri_hash}"
+ end
+
+ # This method is called while caching to save copy of the gems that the
+ # source can resolve to path provided by `app_cache_app`so that they can
+ # be reinstalled from the cache without querying the remote (i.e. an
+ # alternative to remote)
+ #
+ # This is stored with the app and source plugins should try to provide
+ # specs and install only from this cache when `cached!` is called.
+ #
+ # This cache is different from the internal caching that can be done
+ # at sub paths of `cache_path` (from API). This can be though as caching
+ # by bundler.
+ def cache(spec, custom_path = nil)
+ new_cache_path = app_cache_path(custom_path)
+
+ FileUtils.rm_rf(new_cache_path)
+ FileUtils.cp_r(install_path, new_cache_path)
+ FileUtils.rm_rf(app_cache_path.join(".git"))
+ FileUtils.touch(app_cache_path.join(".bundlecache"))
+ end
+
+ # This shall check if two source object represent the same source.
+ #
+ # The comparison shall take place only on the attribute that can be
+ # inferred from the options passed from Gemfile and not on attributes
+ # that are used to pin down the gem to specific version (e.g. Git
+ # sources should compare on branch and tag but not on commit hash)
+ #
+ # The sources objects are constructed from Gemfile as well as from
+ # lockfile. To converge the sources, it is necessary that they match.
+ #
+ # The same applies for `eql?` and `hash`
+ def ==(other)
+ other.is_a?(self.class) && uri == other.uri
+ end
+
+ # When overriding `eql?` please preserve the behaviour as mentioned in
+ # docstring for `==` method.
+ alias_method :eql?, :==
+
+ # When overriding `hash` please preserve the behaviour as mentioned in
+ # docstring for `==` method, i.e. two methods equal by above comparison
+ # should have same hash.
+ def hash
+ [self.class, uri].hash
+ end
+
+ # A helper method, not necessary if not used internally.
+ def installed?
+ File.directory?(install_path)
+ end
+
+ # The full path where the plugin should cache the gem so that it can be
+ # installed latter.
+ #
+ # Note: Do not override if you don't know what you are doing.
+ def app_cache_path(custom_path = nil)
+ @app_cache_path ||= Bundler.app_cache(custom_path).join(app_cache_dirname)
+ end
+
+ # Used by definition.
+ #
+ # Note: Do not override if you don't know what you are doing.
+ def unmet_deps
+ specs.unmet_dependency_names
+ end
+
+ # Used by definition.
+ #
+ # Note: Do not override if you don't know what you are doing.
+ def spec_names
+ specs.spec_names
+ end
+
+ # Used by definition.
+ #
+ # Note: Do not override if you don't know what you are doing.
+ def add_dependency_names(names)
+ @dependencies |= Array(names)
+ end
+
+ # NOTE: Do not override if you don't know what you are doing.
+ def can_lock?(spec)
+ spec.source == self
+ end
+
+ # Generates the content to be entered into the lockfile.
+ # Saves type and remote and also calls to `options_to_lock`.
+ #
+ # Plugin should use `options_to_lock` to save information in lockfile
+ # and not override this.
+ #
+ # Note: Do not override if you don't know what you are doing.
+ def to_lock
+ out = String.new("#{LockfileParser::PLUGIN}\n")
+ out << " remote: #{@uri}\n"
+ out << " type: #{@type}\n"
+ options_to_lock.each do |opt, value|
+ out << " #{opt}: #{value}\n"
+ end
+ out << " specs:\n"
+ end
+
+ def to_s
+ "plugin source for #{@type} with uri #{@uri}"
+ end
+ alias_method :identifier, :to_s
+
+ # NOTE: Do not override if you don't know what you are doing.
+ def include?(other)
+ other == self
+ end
+
+ def uri_hash
+ SharedHelpers.digest(:SHA1).hexdigest(uri)
+ end
+
+ # NOTE: Do not override if you don't know what you are doing.
+ def gem_install_dir
+ Bundler.install_path
+ end
+
+ # It is used to obtain the full_gem_path.
+ #
+ # spec's loaded_from path is expanded against this to get full_gem_path
+ #
+ # Note: Do not override if you don't know what you are doing.
+ def root
+ Bundler.root
+ end
+
+ # @private
+ # This API on source might not be stable, and for now we expect plugins
+ # to download all specs in `#specs`, so we implement the method for
+ # compatibility purposes and leave it undocumented (and don't support)
+ # overriding it)
+ def double_check_for(*); end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/plugin/dsl.rb b/lib/bundler/plugin/dsl.rb
new file mode 100644
index 0000000000..da751d1774
--- /dev/null
+++ b/lib/bundler/plugin/dsl.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Bundler
+ module Plugin
+ # Dsl to parse the Gemfile looking for plugins to install
+ class DSL < Bundler::Dsl
+ class PluginGemfileError < PluginError; end
+ alias_method :_gem, :gem # To use for plugin installation as gem
+
+ # So that we don't have to override all there methods to dummy ones
+ # explicitly.
+ # They will be handled by method_missing
+ [:gemspec, :gem, :install_if, :platforms, :env].each {|m| undef_method m }
+
+ # This lists the plugins that was added automatically and not specified by
+ # the user.
+ #
+ # When we encounter :type attribute with a source block, we add a plugin
+ # by name bundler-source-<type> to list of plugins to be installed.
+ #
+ # These plugins are optional and are not installed when there is conflict
+ # with any other plugin.
+ attr_reader :inferred_plugins
+
+ def initialize
+ super
+ @sources = Plugin::SourceList.new
+ @inferred_plugins = [] # The source plugins inferred from :type
+ end
+
+ def plugin(name, *args)
+ _gem(name, *args)
+ end
+
+ def method_missing(name, *args)
+ raise PluginGemfileError, "Undefined local variable or method `#{name}' for Gemfile" unless Bundler::Dsl.method_defined? name
+ end
+
+ def source(source, *args, &blk)
+ options = args.last.is_a?(Hash) ? args.pop.dup : {}
+ options = normalize_hash(options)
+ return super unless options.key?("type")
+
+ plugin_name = "bundler-source-#{options["type"]}"
+
+ return if @dependencies.any? {|d| d.name == plugin_name }
+
+ plugin(plugin_name)
+ @inferred_plugins << plugin_name
+ end
+ end
+ end
+end
diff --git a/lib/bundler/plugin/events.rb b/lib/bundler/plugin/events.rb
new file mode 100644
index 0000000000..3fbf60307e
--- /dev/null
+++ b/lib/bundler/plugin/events.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+module Bundler
+ module Plugin
+ module Events
+ def self.define(const, event)
+ const = const.to_sym.freeze
+ if const_defined?(const) && const_get(const) != event
+ raise ArgumentError, "Attempting to reassign #{const} to a different value"
+ end
+ const_set(const, event) unless const_defined?(const)
+ @events ||= {}
+ @events[event] = const
+ end
+ private_class_method :define
+
+ def self.reset
+ @events.each_value do |const|
+ remove_const(const)
+ end
+ @events = nil
+ end
+ private_class_method :reset
+
+ # Check if an event has been defined
+ # @param event [String] An event to check
+ # @return [Boolean] A boolean indicating if the event has been defined
+ def self.defined_event?(event)
+ @events ||= {}
+ @events.key?(event)
+ end
+
+ # @!parse
+ # A hook called before the Gemfile is evaluated
+ # Includes the Gemfile path and the Lockfile path
+ # GEM_BEFORE_EVAL = "before-eval"
+ define :GEM_BEFORE_EVAL, "before-eval"
+
+ # @!parse
+ # A hook called after the Gemfile is evaluated
+ # Includes a Bundler::Definition
+ # GEM_AFTER_EVAL = "after-eval"
+ define :GEM_AFTER_EVAL, "after-eval"
+
+ # @!parse
+ # A hook called before any gems install
+ # Includes an Array of Bundler::Dependency objects
+ # GEM_BEFORE_INSTALL_ALL = "before-install-all"
+ define :GEM_BEFORE_INSTALL_ALL, "before-install-all"
+
+ # @!parse
+ # A hook called before each individual gem is downloaded from a remote source.
+ # Includes a spec-like object responding to the Gem::Specification API
+ # (for example, a Bundler spec proxy such as Bundler::EndpointSpecification
+ # or Bundler::RemoteSpecification). Does not fire when the gem is already
+ # present at the initial download-cache check.
+ # GEM_BEFORE_FETCH = "before-fetch"
+ define :GEM_BEFORE_FETCH, "before-fetch"
+
+ # @!parse
+ # A hook called after each individual gem is downloaded from a remote source.
+ # Includes a spec-like object responding to the Gem::Specification API
+ # (for example, a Bundler spec proxy such as Bundler::EndpointSpecification
+ # or Bundler::RemoteSpecification). Does not fire when the gem is already
+ # present at the initial download-cache check.
+ # GEM_AFTER_FETCH = "after-fetch"
+ define :GEM_AFTER_FETCH, "after-fetch"
+
+ # @!parse
+ # A hook called before a git source is fetched or checked out.
+ # Includes a Bundler::Source::Git reference.
+ # GIT_BEFORE_FETCH = "before-git-fetch"
+ define :GIT_BEFORE_FETCH, "before-git-fetch"
+
+ # @!parse
+ # A hook called after a git source is fetched or checked out.
+ # Includes a Bundler::Source::Git reference.
+ # GIT_AFTER_FETCH = "after-git-fetch"
+ define :GIT_AFTER_FETCH, "after-git-fetch"
+
+ # @!parse
+ # A hook called before each individual gem is installed
+ # Includes a Bundler::ParallelInstaller::SpecInstallation.
+ # No state, error, post_install_message will be present as nothing has installed yet
+ # GEM_BEFORE_INSTALL = "before-install"
+ define :GEM_BEFORE_INSTALL, "before-install"
+
+ # @!parse
+ # A hook called after each individual gem is installed
+ # Includes a Bundler::ParallelInstaller::SpecInstallation.
+ # - If state is failed, an error will be present.
+ # - If state is success, a post_install_message may be present.
+ # GEM_AFTER_INSTALL = "after-install"
+ define :GEM_AFTER_INSTALL, "after-install"
+
+ # @!parse
+ # A hook called after any gems install
+ # Includes an Array of Bundler::Dependency objects
+ # GEM_AFTER_INSTALL_ALL = "after-install-all"
+ define :GEM_AFTER_INSTALL_ALL, "after-install-all"
+
+ # @!parse
+ # A hook called before any gems require
+ # Includes an Array of Bundler::Dependency objects.
+ # GEM_BEFORE_REQUIRE_ALL = "before-require-all"
+ define :GEM_BEFORE_REQUIRE_ALL, "before-require-all"
+
+ # @!parse
+ # A hook called before each individual gem is required
+ # Includes a Bundler::Dependency.
+ # GEM_BEFORE_REQUIRE = "before-require"
+ define :GEM_BEFORE_REQUIRE, "before-require"
+
+ # @!parse
+ # A hook called after each individual gem is required
+ # Includes a Bundler::Dependency.
+ # GEM_AFTER_REQUIRE = "after-require"
+ define :GEM_AFTER_REQUIRE, "after-require"
+
+ # @!parse
+ # A hook called after all gems required
+ # Includes an Array of Bundler::Dependency objects.
+ # GEM_AFTER_REQUIRE_ALL = "after-require-all"
+ define :GEM_AFTER_REQUIRE_ALL, "after-require-all"
+ end
+ end
+end
diff --git a/lib/bundler/plugin/index.rb b/lib/bundler/plugin/index.rb
new file mode 100644
index 0000000000..1dfb061304
--- /dev/null
+++ b/lib/bundler/plugin/index.rb
@@ -0,0 +1,241 @@
+# frozen_string_literal: true
+
+module Bundler
+ # Manages which plugins are installed and their sources. This also is supposed to map
+ # which plugin does what (currently the features are not implemented so this class is
+ # now a stub class).
+ module Plugin
+ class Index
+ class CommandConflict < PluginError
+ def initialize(plugin, commands)
+ msg = "Command(s) `#{commands.join("`, `")}` declared by #{plugin} are already registered."
+ super msg
+ end
+ end
+
+ class SourceConflict < PluginError
+ def initialize(plugin, sources)
+ msg = "Source(s) `#{sources.join("`, `")}` declared by #{plugin} are already registered."
+ super msg
+ end
+ end
+
+ attr_reader :commands
+
+ def initialize
+ @plugin_paths = {}
+ @commands = {}
+ @sources = {}
+ @hooks = {}
+ @load_paths = {}
+
+ begin
+ load_index(global_index_file, true)
+ rescue PermissionError
+ # no need to fail when on a read-only FS, for example
+ nil
+ rescue ArgumentError => e
+ # ruby 3.4 checks writability in Dir.tmpdir
+ raise unless e.message&.include?("could not find a temporary directory")
+ nil
+ end
+ load_index(local_index_file) if SharedHelpers.in_bundle?
+ end
+
+ # This function is to be called when a new plugin is installed. This
+ # function shall add the functions of the plugin to existing maps and also
+ # the name to source location.
+ #
+ # @param [String] name of the plugin to be registered
+ # @param [String] path where the plugin is installed
+ # @param [Array<String>] load_paths for the plugin
+ # @param [Array<String>] commands that are handled by the plugin
+ # @param [Array<String>] sources that are handled by the plugin
+ def register_plugin(name, path, load_paths, commands, sources, hooks)
+ old_commands = @commands.dup
+
+ common = commands & @commands.keys
+ raise CommandConflict.new(name, common) unless common.empty?
+ commands.each {|c| @commands[c] = name }
+
+ common = sources & @sources.keys
+ raise SourceConflict.new(name, common) unless common.empty?
+ sources.each {|k| @sources[k] = name }
+
+ hooks.each do |event|
+ event_hooks = (@hooks[event] ||= []) << name
+ event_hooks.uniq!
+ end
+
+ @plugin_paths[name] = path
+ @load_paths[name] = load_paths
+ save_index
+ rescue StandardError
+ @commands = old_commands
+ raise
+ end
+
+ def unregister_plugin(name)
+ @commands.delete_if {|_, v| v == name }
+ @sources.delete_if {|_, v| v == name }
+ @hooks.each do |hook, names|
+ names.delete(name)
+ @hooks.delete(hook) if names.empty?
+ end
+ @plugin_paths.delete(name)
+ @load_paths.delete(name)
+ save_index
+ end
+
+ # Path of default index file
+ def index_file
+ Plugin.root.join("index")
+ end
+
+ # Path where the global index file is stored
+ def global_index_file
+ Plugin.global_root.join("index")
+ end
+
+ # Path where the local index file is stored
+ def local_index_file
+ Plugin.local_root.join("index")
+ end
+
+ def plugin_path(name)
+ Pathname.new @plugin_paths[name]
+ end
+
+ def load_paths(name)
+ @load_paths[name]
+ end
+
+ # Fetch the name of plugin handling the command
+ def command_plugin(command)
+ @commands[command]
+ end
+
+ def installed?(name)
+ @plugin_paths[name]
+ end
+
+ def up_to_date?(spec)
+ path = installed?(spec.name)
+
+ path == spec.full_gem_path
+ end
+
+ def installed_plugins
+ @plugin_paths.keys
+ end
+
+ def plugin_commands(plugin)
+ @commands.find_all {|_, n| n == plugin }.map(&:first)
+ end
+
+ def source?(source)
+ @sources.key? source
+ end
+
+ def source_plugin(name)
+ @sources[name]
+ end
+
+ # Returns the list of plugin names handling the passed event
+ def hook_plugins(event)
+ @hooks[event] || []
+ end
+
+ # This plugin is installed inside the .bundle/plugin directory,
+ # and thus is managed solely by Bundler
+ def installed_in_plugin_root?(name)
+ return false unless (path = installed?(name))
+
+ path.start_with?("#{Plugin.root}/")
+ end
+
+ private
+
+ # Reads the index file from the directory and initializes the instance
+ # variables.
+ #
+ # It skips the sources if the second param is true
+ # @param [Pathname] index file path
+ # @param [Boolean] is the index file global index
+ def load_index(index_file, global = false)
+ base = base_for_index(global)
+
+ SharedHelpers.filesystem_access(index_file, :read) do |index_f|
+ valid_file = index_f&.exist? && !index_f.size.zero?
+ break unless valid_file
+
+ data = index_f.read
+
+ require_relative "../yaml_serializer"
+ index = YAMLSerializer.load(data)
+
+ @commands.merge!(index["commands"])
+ @hooks.merge!(index["hooks"])
+ @load_paths.merge!(transform_index_paths(index["load_paths"]) {|p| absolutize_path(p, base) })
+ @plugin_paths.merge!(transform_index_paths(index["plugin_paths"]) {|p| absolutize_path(p, base) })
+ @sources.merge!(index["sources"]) unless global
+ end
+ end
+
+ # Should be called when any of the instance variables change. Stores the
+ # instance variables in YAML format. (The instance variables are supposed
+ # to be only String key value pairs)
+ def save_index
+ base = base_for_index(false)
+
+ index = {
+ "commands" => @commands,
+ "hooks" => @hooks,
+ "load_paths" => transform_index_paths(@load_paths) {|p| relativize_path(p, base) },
+ "plugin_paths" => transform_index_paths(@plugin_paths) {|p| relativize_path(p, base) },
+ "sources" => @sources,
+ }
+
+ require_relative "../yaml_serializer"
+ SharedHelpers.filesystem_access(index_file) do |index_f|
+ FileUtils.mkdir_p(index_f.dirname)
+ File.open(index_f, "w") {|f| f.puts YAMLSerializer.dump(index) }
+ end
+ end
+
+ def base_for_index(global)
+ global ? Plugin.global_root : Plugin.root
+ end
+
+ def transform_index_paths(paths)
+ return {} unless paths
+
+ paths.transform_values do |value|
+ if value.is_a?(Array)
+ value.map {|path| yield path }
+ else
+ yield value
+ end
+ end
+ end
+
+ def relativize_path(path, base)
+ pathname = Pathname.new(path)
+ return path unless pathname.absolute?
+
+ base_path = Pathname.new(base)
+ if pathname == base_path || pathname.to_s.start_with?(base_path.to_s + File::SEPARATOR)
+ pathname.relative_path_from(base_path).to_s
+ else
+ path
+ end
+ end
+
+ def absolutize_path(path, base)
+ pathname = Pathname.new(path)
+ pathname = Pathname.new(base).join(pathname) unless pathname.absolute?
+ pathname.to_s
+ end
+ end
+ end
+end
diff --git a/lib/bundler/plugin/installer.rb b/lib/bundler/plugin/installer.rb
new file mode 100644
index 0000000000..9be8b36843
--- /dev/null
+++ b/lib/bundler/plugin/installer.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+module Bundler
+ # Handles the installation of plugin in appropriate directories.
+ #
+ # This class is supposed to be wrapper over the existing gem installation infra
+ # but currently it itself handles everything as the Source's subclasses (e.g. Source::RubyGems)
+ # are heavily dependent on the Gemfile.
+ module Plugin
+ class Installer
+ autoload :Rubygems, File.expand_path("installer/rubygems", __dir__)
+ autoload :Git, File.expand_path("installer/git", __dir__)
+ autoload :Path, File.expand_path("installer/path", __dir__)
+
+ def install(names, options)
+ check_sources_consistency!(options)
+
+ version = options[:version] || [">= 0"]
+
+ if options[:git]
+ install_git(names, version, options)
+ elsif options[:path]
+ install_path(names, version, options[:path])
+ else
+ sources = options[:source] || Gem.sources
+ install_rubygems(names, version, sources)
+ end
+ end
+
+ # Installs the plugin from Definition object created by limited parsing of
+ # Gemfile searching for plugins to be installed
+ #
+ # @param [Definition] definition object
+ # @return [Hash] map of names to their specs they are installed with
+ def install_definition(definition)
+ def definition.lock(*); end
+ definition.remotely!
+ specs = definition.specs
+
+ install_from_specs specs
+ end
+
+ private
+
+ def check_sources_consistency!(options)
+ if (options.keys & [:source, :git, :path]).length > 1
+ raise InvalidOption, "Only one of --source, --git, or --path may be specified"
+ end
+
+ if (options.key?(:branch) || options.key?(:ref)) && !options.key?(:git)
+ raise InvalidOption, "--#{options.key?(:branch) ? "branch" : "ref"} can only be used with git sources"
+ end
+
+ if options.key?(:branch) && options.key?(:ref)
+ raise InvalidOption, "--branch and --ref can't be both specified"
+ end
+ end
+
+ def install_git(names, version, options)
+ source_list = SourceList.new
+ source = source_list.add_git_source({ "uri" => options[:git],
+ "branch" => options[:branch],
+ "ref" => options[:ref] })
+
+ install_all_sources(names, version, source_list, source)
+ end
+
+ def install_path(names, version, path)
+ source_list = SourceList.new
+ source = source_list.add_path_source({ "path" => path, "root_path" => SharedHelpers.pwd })
+
+ install_all_sources(names, version, source_list, source)
+ end
+
+ # Installs the plugin from rubygems source and returns the path where the
+ # plugin was installed
+ #
+ # @param [String] name of the plugin gem to search in the source
+ # @param [Array] version of the gem to install
+ # @param [String, Array<String>] source(s) to resolve the gem
+ #
+ # @return [Hash] map of names to the specs of plugins installed
+ def install_rubygems(names, version, sources)
+ source_list = SourceList.new
+
+ Array(sources).each {|remote| source_list.add_global_rubygems_remote(remote) }
+
+ install_all_sources(names, version, source_list)
+ end
+
+ def install_all_sources(names, version, source_list, source = nil)
+ deps = names.map {|name| Dependency.new(name, version, { "source" => source }) }
+
+ Bundler.configure_gem_home_and_path(Plugin.root)
+
+ Bundler.settings.temporary(deployment: false, frozen: false) do
+ definition = Definition.new(nil, deps, source_list, true)
+
+ install_definition(definition)
+ end
+ end
+
+ # Installs the plugins and deps from the provided specs and returns map of
+ # gems to their paths
+ #
+ # @param specs to install
+ #
+ # @return [Hash] map of names to the specs
+ def install_from_specs(specs)
+ paths = {}
+
+ specs.each do |spec|
+ spec.source.download(spec)
+ spec.source.install(spec)
+
+ paths[spec.name] = spec
+ end
+
+ paths
+ end
+ end
+ end
+end
diff --git a/lib/bundler/plugin/installer/git.rb b/lib/bundler/plugin/installer/git.rb
new file mode 100644
index 0000000000..deec5e99b3
--- /dev/null
+++ b/lib/bundler/plugin/installer/git.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Bundler
+ module Plugin
+ class Installer
+ class Git < Bundler::Source::Git
+ def cache_path
+ @cache_path ||= begin
+ git_scope = "#{base_name}-#{uri_hash}"
+
+ Plugin.cache.join("bundler", "git", git_scope)
+ end
+ end
+
+ def install_path
+ @install_path ||= begin
+ git_scope = "#{base_name}-#{shortref_for_path(revision)}"
+
+ Plugin.root.join("bundler", "gems", git_scope)
+ end
+ end
+
+ def root
+ Plugin.root
+ end
+
+ def generate_bin(spec, disable_extensions = false)
+ # Need to find a way without code duplication
+ # For now, we can ignore this
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/plugin/installer/path.rb b/lib/bundler/plugin/installer/path.rb
new file mode 100644
index 0000000000..58c4924eb0
--- /dev/null
+++ b/lib/bundler/plugin/installer/path.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Bundler
+ module Plugin
+ class Installer
+ class Path < Bundler::Source::Path
+ def root
+ SharedHelpers.in_bundle? ? Bundler.root : Plugin.root
+ end
+
+ def eql?(other)
+ return unless other.class == self.class
+ expanded_original_path == other.expanded_original_path &&
+ version == other.version
+ end
+
+ alias_method :==, :eql?
+
+ def generate_bin(spec, disable_extensions = false)
+ # Need to find a way without code duplication
+ # For now, we can ignore this
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/plugin/installer/rubygems.rb b/lib/bundler/plugin/installer/rubygems.rb
new file mode 100644
index 0000000000..cb5db9c30e
--- /dev/null
+++ b/lib/bundler/plugin/installer/rubygems.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Bundler
+ module Plugin
+ class Installer
+ class Rubygems < Bundler::Source::Rubygems
+ private
+
+ def rubygems_dir
+ Plugin.root
+ end
+
+ def cache_path
+ Plugin.cache
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/plugin/source_list.rb b/lib/bundler/plugin/source_list.rb
new file mode 100644
index 0000000000..d929ade29e
--- /dev/null
+++ b/lib/bundler/plugin/source_list.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Bundler
+ # SourceList object to be used while parsing the Gemfile, setting the
+ # approptiate options to be used with Source classes for plugin installation
+ module Plugin
+ class SourceList < Bundler::SourceList
+ def add_git_source(options = {})
+ add_source_to_list Plugin::Installer::Git.new(options), git_sources
+ end
+
+ def add_path_source(options = {})
+ add_source_to_list Plugin::Installer::Path.new(options), path_sources
+ end
+
+ def add_rubygems_source(options = {})
+ add_source_to_list Plugin::Installer::Rubygems.new(options), @rubygems_sources
+ end
+
+ def all_sources
+ path_sources + git_sources + rubygems_sources + [metadata_source]
+ end
+
+ private
+
+ def source_class
+ Plugin::Installer::Rubygems
+ end
+ end
+ end
+end
diff --git a/lib/bundler/process_lock.rb b/lib/bundler/process_lock.rb
new file mode 100644
index 0000000000..784b17e363
--- /dev/null
+++ b/lib/bundler/process_lock.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Bundler
+ class ProcessLock
+ def self.lock(bundle_path = Bundler.bundle_path, &block)
+ lock_file_path = File.join(bundle_path, "bundler.lock")
+ base_lock_file_path = lock_file_path.delete_suffix(".lock")
+
+ require "fileutils" if Bundler.rubygems.provides?("< 3.6.0")
+
+ begin
+ SharedHelpers.filesystem_access(lock_file_path, :write) do
+ Gem.open_file_with_lock(base_lock_file_path, &block)
+ end
+ rescue PermissionError
+ block.call
+ end
+ end
+ end
+end
diff --git a/lib/bundler/remote_specification.rb b/lib/bundler/remote_specification.rb
new file mode 100644
index 0000000000..dcaaf6af2e
--- /dev/null
+++ b/lib/bundler/remote_specification.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+module Bundler
+ # Represents a lazily loaded gem specification, where the full specification
+ # is on the source server in rubygems' "quick" index. The proxy object is to
+ # be seeded with what we're given from the source's abbreviated index - the
+ # full specification will only be fetched when necessary.
+ class RemoteSpecification
+ include MatchRemoteMetadata
+ include MatchPlatform
+ include Comparable
+
+ attr_reader :name, :version, :platform
+ attr_writer :dependencies
+ attr_accessor :source, :remote, :locked_platform, :created_at
+
+ def initialize(name, version, platform, spec_fetcher)
+ @name = name
+ @version = Gem::Version.create version
+ @original_platform = platform || Gem::Platform::RUBY
+ @platform = Gem::Platform.new(platform)
+ @spec_fetcher = spec_fetcher
+ @dependencies = nil
+ @locked_platform = nil
+ end
+
+ def insecurely_materialized?
+ @locked_platform.to_s != @platform.to_s
+ end
+
+ # Needed before installs, since the arch matters then and quick
+ # specs don't bother to include the arch in the platform string
+ def fetch_platform
+ @platform = _remote_specification.platform
+ end
+
+ def full_name
+ @full_name ||= if @platform == Gem::Platform::RUBY
+ "#{@name}-#{@version}"
+ else
+ "#{@name}-#{@version}-#{@platform}"
+ end
+ end
+
+ # Compare this specification against another object. Using sort_obj
+ # is compatible with Gem::Specification and other Bundler or RubyGems
+ # objects. Otherwise, use the default Object comparison.
+ def <=>(other)
+ if other.respond_to?(:sort_obj)
+ sort_obj <=> other.sort_obj
+ else
+ super
+ end
+ end
+
+ # Because Rubyforge cannot be trusted to provide valid specifications
+ # once the remote gem is downloaded, the backend specification will
+ # be swapped out.
+ def __swap__(spec)
+ raise APIResponseInvalidDependenciesError unless spec.dependencies.all? {|d| d.is_a?(Gem::Dependency) }
+
+ SharedHelpers.ensure_same_dependencies(self, dependencies, spec.dependencies)
+ @_remote_specification = spec
+ end
+
+ # Create a delegate used for sorting. This strategy is copied from
+ # RubyGems 2.23 and ensures that Bundler's specifications can be
+ # compared and sorted with RubyGems' own specifications.
+ #
+ # @see #<=>
+ # @see Gem::Specification#sort_obj
+ #
+ # @return [Array] an object you can use to compare and sort this
+ # specification against other specifications
+ def sort_obj
+ [@name, @version, @platform == Gem::Platform::RUBY ? -1 : 1]
+ end
+
+ def to_s
+ "#<#{self.class} name=#{name} version=#{version} platform=#{platform}>"
+ end
+
+ def dependencies
+ @dependencies ||= begin
+ deps = method_missing(:dependencies)
+
+ # allow us to handle when the specs dependencies are an array of array of string
+ # in order to delay the crash to `#__swap__` where it results in a friendlier error
+ # see https://github.com/rubygems/bundler/issues/5797
+ deps = deps.map {|d| d.is_a?(Gem::Dependency) ? d : Gem::Dependency.new(*d) }
+
+ deps
+ end
+ end
+
+ def runtime_dependencies
+ dependencies.select(&:runtime?)
+ end
+
+ def git_version
+ return unless loaded_from && source.is_a?(Bundler::Source::Git)
+ " #{source.revision[0..6]}"
+ end
+
+ private
+
+ def to_ary
+ nil
+ end
+
+ def _remote_specification
+ @_remote_specification ||= @spec_fetcher.fetch_spec([@name, @version, @original_platform])
+ @_remote_specification || raise(GemspecError, "Gemspec data for #{full_name} was" \
+ " missing from the server!")
+ end
+
+ def method_missing(method, *args, &blk)
+ _remote_specification.send(method, *args, &blk)
+ end
+
+ def respond_to?(method, include_all = false)
+ super || _remote_specification.respond_to?(method, include_all)
+ end
+ public :respond_to?
+ end
+end
diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb
new file mode 100644
index 0000000000..753e9987d5
--- /dev/null
+++ b/lib/bundler/resolver.rb
@@ -0,0 +1,645 @@
+# frozen_string_literal: true
+
+module Bundler
+ #
+ # This class implements the interface needed by PubGrub for resolution. It is
+ # equivalent to the `PubGrub::BasicPackageSource` class provided by PubGrub by
+ # default and used by the most simple PubGrub consumers.
+ #
+ class Resolver
+ require_relative "vendored_pub_grub"
+ require_relative "resolver/base"
+ require_relative "resolver/candidate"
+ require_relative "resolver/incompatibility"
+ require_relative "resolver/root"
+ require_relative "resolver/strategy"
+
+ def initialize(base, gem_version_promoter, most_specific_locked_platform = nil)
+ @source_requirements = base.source_requirements
+ @base = base
+ @gem_version_promoter = gem_version_promoter
+ @most_specific_locked_platform = most_specific_locked_platform
+ end
+
+ def start
+ @requirements = @base.requirements
+ @packages = @base.packages
+
+ root, logger = setup_solver
+
+ Bundler.ui.info "Resolving dependencies...", true
+
+ solve_versions(root: root, logger: logger)
+ end
+
+ def setup_solver
+ root = Resolver::Root.new(name_for_explicit_dependency_source)
+ root_version = Resolver::Candidate.new(0)
+
+ @all_specs = Hash.new do |specs, name|
+ source = source_for(name)
+ matches = source.specs.search(name)
+
+ # Don't bother to check for circular deps when no dependency API are
+ # available, since it's too slow to be usable. That edge case won't work
+ # but resolution other than that should work fine and reasonably fast.
+ if source.respond_to?(:dependency_api_available?) && source.dependency_api_available?
+ matches = filter_invalid_self_dependencies(matches, name)
+ end
+
+ specs[name] = matches.sort_by {|s| [s.version, s.platform.to_s] }
+ end
+
+ @all_versions = Hash.new do |candidates, package|
+ candidates[package] = all_versions_for(package)
+ end
+
+ @sorted_versions = Hash.new do |candidates, package|
+ candidates[package] = filtered_versions_for(package).sort
+ end
+
+ @sorted_versions[root] = [root_version]
+
+ root_dependencies = prepare_dependencies(@requirements, @packages)
+
+ @cached_dependencies = Hash.new do |dependencies, package|
+ dependencies[package] = Hash.new do |versions, version|
+ deps = version.dependencies.reject {|d| d.name == package.name }
+ deps = apply_metadata_overrides(deps, package.name)
+ versions[version] = to_dependency_hash(deps, @packages)
+ end
+ end
+
+ @cached_dependencies[root] = { root_version => root_dependencies }
+
+ logger = Bundler::UI::Shell.new
+ logger.level = debug? ? "debug" : "warn"
+
+ [root, logger]
+ end
+
+ def solve_versions(root:, logger:)
+ solver = PubGrub::VersionSolver.new(source: self, root: root, strategy: Strategy.new(self), logger: logger)
+ result = solver.solve
+ resolved_specs = result.flat_map {|package, version| version.to_specs(package, @most_specific_locked_platform) }
+ Override.attach(resolved_specs, @base.overrides)
+ SpecSet.new(resolved_specs).specs_with_additional_variants_from(@base.locked_specs)
+ rescue PubGrub::SolveFailure => e
+ incompatibility = e.incompatibility
+
+ names_to_unlock, names_to_allow_prereleases_for, names_to_allow_remote_specs_for, extended_explanation = find_names_to_relax(incompatibility)
+
+ names_to_relax = names_to_unlock + names_to_allow_prereleases_for + names_to_allow_remote_specs_for
+
+ if names_to_relax.any?
+ if names_to_unlock.any?
+ Bundler.ui.debug "Found conflicts with locked dependencies. Will retry with #{names_to_unlock.join(", ")} unlocked...", true
+
+ @base.unlock_names(names_to_unlock)
+ end
+
+ if names_to_allow_prereleases_for.any?
+ Bundler.ui.debug "Found conflicts with dependencies with prereleases. Will retry considering prereleases for #{names_to_allow_prereleases_for.join(", ")}...", true
+
+ @base.include_prereleases(names_to_allow_prereleases_for)
+ end
+
+ if names_to_allow_remote_specs_for.any?
+ Bundler.ui.debug "Found conflicts with local versions of #{names_to_allow_remote_specs_for.join(", ")}. Will retry considering remote versions...", true
+
+ @base.include_remote_specs(names_to_allow_remote_specs_for)
+ end
+
+ root, logger = setup_solver
+
+ Bundler.ui.debug "Retrying resolution...", true
+ retry
+ end
+
+ explanation = e.message
+
+ if extended_explanation
+ explanation << "\n\n"
+ explanation << extended_explanation
+ end
+
+ override_summary = override_diagnostic_summary
+ explanation << override_summary if override_summary
+
+ raise SolveFailure.new(explanation)
+ end
+
+ def override_diagnostic_summary
+ return nil if @base.overrides.empty?
+
+ lines = ["Bundler applied the following overrides while resolving:"]
+ @base.overrides.each do |override|
+ target = override.target == :all ? ":all" : override.target.inspect
+ location = override.source_location_label
+ lines << " override #{target}, #{override.field}: #{override.operation.inspect}" \
+ "#{location ? " (declared at #{location})" : ""}"
+ end
+ "\n\n#{lines.join("\n")}"
+ end
+
+ def find_names_to_relax(incompatibility)
+ names_to_unlock = []
+ names_to_allow_prereleases_for = []
+ names_to_allow_remote_specs_for = []
+ extended_explanation = nil
+
+ while incompatibility.conflict?
+ cause = incompatibility.cause
+ incompatibility = cause.incompatibility
+
+ incompatibility.terms.each do |term|
+ package = term.package
+ name = package.name
+
+ if base_requirements[name]
+ names_to_unlock << name
+ elsif package.ignores_prereleases? && @all_specs[name].any? {|s| s.version.prerelease? }
+ names_to_allow_prereleases_for << name
+ elsif package.prefer_local? && @all_specs[name].any? {|s| !s.is_a?(StubSpecification) }
+ names_to_allow_remote_specs_for << name
+ end
+
+ no_versions_incompat = [cause.incompatibility, cause.satisfier].find {|incompat| incompat.cause.is_a?(PubGrub::Incompatibility::NoVersions) }
+ next unless no_versions_incompat
+
+ extended_explanation = no_versions_incompat.extended_explanation
+ end
+ end
+
+ [names_to_unlock.uniq, names_to_allow_prereleases_for.uniq, names_to_allow_remote_specs_for.uniq, extended_explanation]
+ end
+
+ def parse_dependency(package, dependency)
+ range = if repository_for(package).is_a?(Source::Gemspec)
+ PubGrub::VersionRange.any
+ else
+ requirement_to_range(dependency)
+ end
+
+ PubGrub::VersionConstraint.new(package, range: range)
+ end
+
+ def versions_for(package, range = VersionRange.any)
+ range.select_versions(@sorted_versions[package])
+ end
+
+ def no_versions_incompatibility_for(package, unsatisfied_term)
+ cause = PubGrub::Incompatibility::NoVersions.new(unsatisfied_term)
+ name = package.name
+ constraint = unsatisfied_term.constraint
+ constraint_string = constraint.constraint_string
+ requirements = constraint_string.split(" OR ").map {|req| Gem::Requirement.new(req.split(",")) }
+
+ if name == "bundler" && bundler_pinned_to_current_version?
+ custom_explanation = "the current Bundler version (#{Bundler::VERSION}) does not satisfy #{constraint}"
+ extended_explanation = bundler_not_found_message(requirements)
+ else
+ specs_matching_other_platforms = filter_matching_specs(@all_specs[name], requirements)
+
+ platforms_explanation = specs_matching_other_platforms.any? ? " for any resolution platforms (#{package.platforms.join(", ")})" : ""
+ custom_explanation = "#{constraint} could not be found in #{repository_for(package)}#{platforms_explanation}"
+ if hint = cooldown_hint(specs_matching_other_platforms)
+ custom_explanation += " (#{hint})"
+ end
+
+ label = "#{name} (#{constraint_string})"
+ extended_explanation = other_specs_matching_message(specs_matching_other_platforms, label) if specs_matching_other_platforms.any?
+ end
+
+ Incompatibility.new([unsatisfied_term], cause: cause, custom_explanation: custom_explanation, extended_explanation: extended_explanation)
+ end
+
+ def debug?
+ ENV["BUNDLER_DEBUG_RESOLVER"] ||
+ ENV["BUNDLER_DEBUG_RESOLVER_TREE"] ||
+ ENV["DEBUG_RESOLVER"] ||
+ ENV["DEBUG_RESOLVER_TREE"] ||
+ false
+ end
+
+ def incompatibilities_for(package, version)
+ package_deps = @cached_dependencies[package]
+ sorted_versions = @sorted_versions[package]
+ package_deps[version].map do |dep_package, dep_constraint|
+ low = high = sorted_versions.index(version)
+
+ # find version low such that all >= low share the same dep
+ while low > 0 && package_deps[sorted_versions[low - 1]][dep_package] == dep_constraint
+ low -= 1
+ end
+ low =
+ if low == 0
+ nil
+ else
+ sorted_versions[low]
+ end
+
+ # find version high such that all < high share the same dep
+ while high < sorted_versions.length && package_deps[sorted_versions[high]][dep_package] == dep_constraint
+ high += 1
+ end
+ high =
+ if high == sorted_versions.length
+ nil
+ else
+ sorted_versions[high]
+ end
+
+ range = PubGrub::VersionRange.new(min: low, max: high, include_min: !low.nil?)
+
+ self_constraint = PubGrub::VersionConstraint.new(package, range: range)
+
+ dep_term = PubGrub::Term.new(dep_constraint, false)
+ self_term = PubGrub::Term.new(self_constraint, true)
+
+ custom_explanation = if dep_package.meta? && package.root?
+ "current #{dep_package} version is #{dep_constraint.constraint_string}"
+ end
+
+ PubGrub::Incompatibility.new([self_term, dep_term], cause: :dependency, custom_explanation: custom_explanation)
+ end
+ end
+
+ def all_versions_for(package)
+ name = package.name
+ results = (@base[name] + filter_specs(@all_specs[name], package)).uniq {|spec| [spec.version.hash, spec.platform] }
+
+ if name == "bundler" && !bundler_pinned_to_current_version?
+ bundler_spec = Gem.loaded_specs["bundler"]
+ results << bundler_spec if bundler_spec
+ end
+
+ locked_requirement = base_requirements[name]
+ results = filter_matching_specs(results, locked_requirement) if locked_requirement
+
+ results.group_by(&:version).reduce([]) do |groups, (version, specs)|
+ platform_specs = package.platform_specs(specs)
+
+ # If package is a top-level dependency,
+ # candidate is only valid if there are matching versions for all resolution platforms.
+ #
+ # If package is not a top-level deependency,
+ # then it's not necessary that it has matching versions for all platforms, since it may have been introduced only as
+ # a dependency for a platform specific variant, so it will only need to have a valid version for that platform.
+ #
+ if package.top_level?
+ next groups if platform_specs.any?(&:empty?)
+ else
+ next groups if platform_specs.all?(&:empty?)
+ end
+
+ ruby_specs = MatchPlatform.select_best_platform_match(specs, Gem::Platform::RUBY)
+ ruby_group = Resolver::SpecGroup.new(ruby_specs)
+
+ unless ruby_group.empty?
+ platform_specs.each do |s|
+ ruby_group.merge(Resolver::SpecGroup.new(s))
+ end
+
+ groups << Resolver::Candidate.new(version, group: ruby_group, priority: -1)
+ next groups if package.force_ruby_platform?
+ end
+
+ platform_group = Resolver::SpecGroup.new(platform_specs.flatten.uniq)
+ next groups if platform_group == ruby_group
+
+ groups << Resolver::Candidate.new(version, group: platform_group, priority: 1)
+
+ groups
+ end
+ end
+
+ def source_for(name)
+ @source_requirements[name] || @source_requirements[:default]
+ end
+
+ def default_bundler_source
+ @source_requirements[:default_bundler]
+ end
+
+ def bundler_pinned_to_current_version?
+ !default_bundler_source.nil?
+ end
+
+ def name_for_explicit_dependency_source
+ Bundler.default_gemfile.basename.to_s
+ rescue StandardError
+ "Gemfile"
+ end
+
+ def raise_incomplete!(incomplete_specs)
+ raise_not_found!(@base.get_package(incomplete_specs.first.name))
+ end
+
+ def sort_versions_by_preferred(package, versions)
+ @gem_version_promoter.sort_versions(package, versions)
+ end
+
+ private
+
+ def raise_not_found!(package)
+ name = package.name
+ source = source_for(name)
+ specs = @all_specs[name]
+ matching_part = name
+ requirement_label = SharedHelpers.pretty_dependency(package.dependency)
+ cache_message = begin
+ " or in gems cached in #{Bundler.settings.app_cache_path}" if Bundler.app_cache.exist?
+ rescue GemfileNotFound
+ nil
+ end
+ specs_matching_requirement = filter_matching_specs(specs, package.dependency.requirement)
+
+ not_found_message = if specs_matching_requirement.any?
+ specs = specs_matching_requirement
+ matching_part = requirement_label
+ platforms = package.platforms
+
+ if platforms.size == 1
+ "Could not find gem '#{requirement_label}' with platform '#{platforms.first}'"
+ else
+ "Could not find gems matching '#{requirement_label}' valid for all resolution platforms (#{platforms.join(", ")})"
+ end
+ else
+ "Could not find gem '#{requirement_label}'"
+ end
+
+ message = String.new("#{not_found_message} in #{source}#{cache_message}.\n")
+
+ if specs.any?
+ message << "\n#{other_specs_matching_message(specs, matching_part)}"
+ end
+
+ if hint = cooldown_hint(specs_matching_requirement)
+ message << "\n\n#{hint}."
+ end
+
+ if specs_matching_requirement.any? && (hint = platform_mismatch_hint)
+ message << "\n\n#{hint}"
+ end
+
+ raise GemNotFound, message
+ end
+
+ def platform_mismatch_hint
+ locked_platforms = Bundler.locked_gems&.platforms
+ return unless locked_platforms
+
+ local_platform = Bundler.local_platform
+ return if locked_platforms.include?(local_platform)
+ return if locked_platforms.any? {|p| p == Gem::Platform::RUBY }
+
+ "Your current platform (#{local_platform}) is not included in the lockfile's platforms (#{locked_platforms.join(", ")}). " \
+ "Add the current platform to the lockfile with\n`bundle lock --add-platform #{local_platform}` and try again."
+ rescue GemfileNotFound
+ nil
+ end
+
+ def filtered_versions_for(package)
+ @gem_version_promoter.filter_versions(package, @all_versions[package])
+ end
+
+ def raise_all_versions_filtered_out!(package)
+ level = @gem_version_promoter.level
+ name = package.name
+ locked_version = package.locked_version
+ requirement = package.dependency
+
+ raise GemNotFound,
+ "#{name} is locked to #{locked_version}, while Gemfile is requesting #{requirement}. " \
+ "--strict --#{level} was specified, but there are no #{level} level upgrades from #{locked_version} satisfying #{requirement}, so version solving has failed"
+ end
+
+ def filter_matching_specs(specs, requirements)
+ Array(requirements).flat_map do |requirement|
+ specs.select {| spec| requirement_satisfied_by?(requirement, spec) }
+ end
+ end
+
+ def filter_specs(specs, package)
+ filter_remote_specs(filter_cooldown(filter_prereleases(specs, package)), package)
+ end
+
+ def filter_prereleases(specs, package)
+ return specs unless package.ignores_prereleases? && specs.size > 1
+
+ specs.reject {|s| s.version.prerelease? }
+ end
+
+ def filter_cooldown(specs)
+ return specs if specs.empty?
+ excluded_versions = cooldown_excluded_versions(specs)
+ return specs if excluded_versions.empty?
+ specs.reject {|s| excluded_versions.include?([s.name, s.version]) }
+ end
+
+ def cooldown_excluded_versions(specs)
+ excluded = {}
+ specs.each do |spec|
+ next unless cooldown_excluded?(spec)
+ excluded[[spec.name, spec.version]] = true
+ end
+ excluded
+ end
+
+ def cooldown_hint(specs)
+ excluded_versions = cooldown_excluded_versions(specs)
+ return nil if excluded_versions.empty?
+ "#{excluded_versions.size} version#{"s" if excluded_versions.size > 1} excluded by the cooldown setting; pass `--cooldown 0` to bypass"
+ end
+
+ def cooldown_excluded?(spec)
+ return false unless spec.respond_to?(:created_at) && spec.created_at
+ return false unless spec.respond_to?(:remote) && spec.remote
+ return false if pinned_by_lockfile_floor?(spec)
+ days = spec.remote.effective_cooldown
+ return false if days.nil? || days <= 0
+ (cooldown_now - spec.created_at) < (days * 86_400)
+ end
+
+ # A spec sitting exactly at a `>= locked_version` prevent-downgrade floor is
+ # the version the lockfile currently pins. `bundle update` and `bundle
+ # outdated` install that floor so resolution never moves a gem backwards.
+ # Filtering it out for cooldown would then make resolution impossible
+ # whenever the locked version is itself inside the cooldown window, which is
+ # exactly what happens to a lockfile written before cooldown was enabled.
+ # Keep it eligible; gems being explicitly updated carry an exact `=`
+ # requirement instead and stay subject to the cooldown filter.
+ def pinned_by_lockfile_floor?(spec)
+ return false unless defined?(@base) && @base
+ requirement = base_requirements[spec.name]
+ return false unless requirement && !requirement.exact?
+ requirement.requirements.any? {|op, version| op == ">=" && version == spec.version }
+ end
+
+ def cooldown_now
+ @cooldown_now ||= Time.now
+ end
+
+ def filter_remote_specs(specs, package)
+ if package.prefer_local?
+ local_specs = specs.select {|s| s.is_a?(StubSpecification) }
+
+ if local_specs.empty?
+ package.consider_remote_versions!
+ specs
+ else
+ local_specs
+ end
+ else
+ specs
+ end
+ end
+
+ # Ignore versions that depend on themselves incorrectly
+ def filter_invalid_self_dependencies(specs, name)
+ specs.reject do |s|
+ s.dependencies.any? {|d| d.name == name && !d.requirement.satisfied_by?(s.version) }
+ end
+ end
+
+ def requirement_satisfied_by?(requirement, spec)
+ requirement.satisfied_by?(spec.version) || spec.source.is_a?(Source::Gemspec)
+ end
+
+ def repository_for(package)
+ source_for(package.name)
+ end
+
+ def base_requirements
+ @base.base_requirements
+ end
+
+ def prepare_dependencies(requirements, packages)
+ to_dependency_hash(requirements, packages).filter_map do |dep_package, dep_constraint|
+ name = dep_package.name
+
+ next [dep_package, dep_constraint] if name == "bundler"
+
+ dep_range = dep_constraint.range
+ versions = versions_for(dep_package, dep_range)
+ if versions.empty?
+ if dep_package.ignores_prereleases? || dep_package.prefer_local?
+ @all_versions.delete(dep_package)
+ @sorted_versions.delete(dep_package)
+ end
+ dep_package.consider_prereleases! if dep_package.ignores_prereleases?
+ dep_package.consider_remote_versions! if dep_package.prefer_local?
+ versions = versions_for(dep_package, dep_range)
+ end
+
+ if versions.empty? && select_all_versions(dep_package, dep_range).any?
+ raise_all_versions_filtered_out!(dep_package)
+ end
+
+ next [dep_package, dep_constraint] unless versions.empty?
+
+ next unless dep_package.current_platform?
+
+ raise_not_found!(dep_package)
+ end.to_h
+ end
+
+ def select_all_versions(package, range)
+ range.select_versions(@all_versions[package])
+ end
+
+ def other_specs_matching_message(specs, requirement)
+ message = String.new("The source contains the following gems matching '#{requirement}':\n")
+ message << specs.map {|s| " * #{s.full_name}" }.join("\n")
+ message
+ end
+
+ def requirement_to_range(requirement)
+ ranges = requirement.requirements.map do |(op, version)|
+ ver = Resolver::Candidate.new(version, priority: -1)
+ platform_ver = Resolver::Candidate.new(version, priority: 1)
+
+ case op
+ when "~>"
+ name = "~> #{ver}"
+ bump = Resolver::Candidate.new(version.bump.to_s + ".A")
+ PubGrub::VersionRange.new(name: name, min: ver, max: bump, include_min: true)
+ when ">"
+ PubGrub::VersionRange.new(min: platform_ver)
+ when ">="
+ PubGrub::VersionRange.new(min: ver, include_min: true)
+ when "<"
+ PubGrub::VersionRange.new(max: ver)
+ when "<="
+ PubGrub::VersionRange.new(max: platform_ver, include_max: true)
+ when "="
+ PubGrub::VersionRange.new(min: ver, max: platform_ver, include_min: true, include_max: true)
+ when "!="
+ PubGrub::VersionRange.new(min: ver, max: platform_ver, include_min: true, include_max: true).invert
+ else
+ raise "bad version specifier: #{op}"
+ end
+ end
+
+ ranges.inject(&:intersect)
+ end
+
+ def to_dependency_hash(dependencies, packages)
+ apply_overrides(dependencies).inject({}) do |deps, dep|
+ package = packages[dep.name]
+
+ current_req = deps[package]
+ new_req = parse_dependency(package, dep.requirement)
+
+ deps[package] = if current_req
+ current_req.intersect(new_req)
+ else
+ new_req
+ end
+
+ deps
+ end
+ end
+
+ def apply_overrides(dependencies)
+ return dependencies if @base.overrides.empty?
+
+ dependencies.map do |dep|
+ override = Override.find_for(@base.overrides, dep.name, :version)
+ next dep unless override
+ Gem::Dependency.new(dep.name, override.apply_to(dep.requirement))
+ end
+ end
+
+ METADATA_DEP_FIELD = {
+ "Ruby\0" => :required_ruby_version,
+ "RubyGems\0" => :required_rubygems_version,
+ }.freeze
+
+ def apply_metadata_overrides(dependencies, name)
+ return dependencies if @base.overrides.empty?
+
+ dependencies.map do |dep|
+ field = METADATA_DEP_FIELD[dep.name]
+ next dep unless field
+ override = Override.find_for(@base.overrides, name, field)
+ next dep unless override
+ Gem::Dependency.new(dep.name, override.apply_to(dep.requirement))
+ end
+ end
+
+ def bundler_not_found_message(conflict_dependencies)
+ candidate_specs = filter_matching_specs(default_bundler_source.specs.search("bundler"), conflict_dependencies)
+
+ if candidate_specs.any?
+ target_version = candidate_specs.last.version
+ new_command = [File.basename($PROGRAM_NAME), "_#{target_version}_", *ARGV].join(" ")
+ "Your bundle requires a different version of Bundler than the one you're running.\n" \
+ "Install the necessary version with `gem install bundler:#{target_version}` and rerun bundler using `#{new_command}`\n"
+ else
+ "Your bundle requires a different version of Bundler than the one you're running, and that version could not be found.\n"
+ end
+ end
+ end
+end
diff --git a/lib/bundler/resolver/base.rb b/lib/bundler/resolver/base.rb
new file mode 100644
index 0000000000..00bdd08303
--- /dev/null
+++ b/lib/bundler/resolver/base.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require_relative "package"
+
+module Bundler
+ class Resolver
+ class Base
+ attr_reader :packages, :requirements, :source_requirements, :locked_specs, :overrides
+
+ def initialize(source_requirements, dependencies, base, platforms, options)
+ @overrides = options.delete(:overrides) || []
+ @source_requirements = source_requirements
+ @locked_specs = options[:locked_specs]
+
+ @base = base
+
+ @packages = Hash.new do |hash, name|
+ hash[name] = Package.new(name, platforms, **options)
+ end
+
+ @requirements = dependencies.filter_map do |dep|
+ dep_platforms = dep.gem_platforms(platforms)
+
+ # Dependencies scoped to external platforms are ignored
+ next if dep_platforms.empty?
+
+ name = dep.name
+
+ @packages[name] = Package.new(name, dep_platforms, **options.merge(dependency: dep))
+
+ dep
+ end
+ end
+
+ def [](name)
+ @base[name]
+ end
+
+ def delete(specs)
+ @base.delete(specs)
+ end
+
+ def get_package(name)
+ @packages[name]
+ end
+
+ def base_requirements
+ @base_requirements ||= build_base_requirements
+ end
+
+ def unlock_names(names)
+ indirect_pins = indirect_pins(names)
+
+ if indirect_pins.any?
+ loosen_names(indirect_pins)
+ else
+ pins = pins(names)
+
+ if pins.any?
+ loosen_names(pins)
+ else
+ unrestrict_names(names)
+ end
+ end
+ end
+
+ def include_prereleases(names)
+ names.each do |name|
+ get_package(name).consider_prereleases!
+ end
+ end
+
+ def include_remote_specs(names)
+ names.each do |name|
+ get_package(name).consider_remote_versions!
+ end
+ end
+
+ private
+
+ def indirect_pins(names)
+ names.select {|name| @base_requirements[name].exact? && @requirements.none? {|dep| dep.name == name } }
+ end
+
+ def pins(names)
+ names.select {|name| @base_requirements[name].exact? }
+ end
+
+ def loosen_names(names)
+ names.each do |name|
+ version = @base_requirements[name].requirements.first[1]
+
+ @base_requirements[name] = Gem::Requirement.new(">= #{version}")
+
+ @base.delete_by_name(name)
+ end
+ end
+
+ def unrestrict_names(names)
+ names.each do |name|
+ @base_requirements.delete(name)
+ end
+ end
+
+ def build_base_requirements
+ base_requirements = {}
+ @base.each do |ls|
+ if ls.source_changed? && ls.source.specs.search(ls.name).empty?
+ raise GemNotFound, "Could not find gem '#{ls.name}' in #{ls.source}"
+ end
+
+ req = Gem::Requirement.new(ls.version)
+ base_requirements[ls.name] = req
+ end
+ base_requirements
+ end
+ end
+ end
+end
diff --git a/lib/bundler/resolver/candidate.rb b/lib/bundler/resolver/candidate.rb
new file mode 100644
index 0000000000..5298b2530f
--- /dev/null
+++ b/lib/bundler/resolver/candidate.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require_relative "spec_group"
+
+module Bundler
+ class Resolver
+ #
+ # This class is a PubGrub compatible "Version" class that takes Bundler
+ # resolution complexities into account.
+ #
+ # Each Resolver::Candidate has a underlying `Gem::Version` plus a set of
+ # platforms. For example, 1.1.0-x86_64-linux is a different resolution candidate
+ # from 1.1.0 (generic). This is because different platform variants of the
+ # same gem version can bring different dependencies, so they need to be
+ # considered separately.
+ #
+ # Some candidates may also keep some information explicitly about the
+ # package they refer to. These candidates are referred to as "canonical" and
+ # are used when materializing resolution results back into RubyGems
+ # specifications that can be installed, written to lockfiles, and so on.
+ #
+ class Candidate
+ include Comparable
+
+ attr_reader :version
+
+ def initialize(version, group: nil, priority: -1)
+ @spec_group = group || SpecGroup.new([])
+ @version = Gem::Version.new(version)
+ @priority = priority
+ end
+
+ def dependencies
+ @spec_group.dependencies
+ end
+
+ def to_specs(package, most_specific_locked_platform)
+ return [] if package.meta?
+
+ @spec_group.to_specs(package.force_ruby_platform?, most_specific_locked_platform)
+ end
+
+ def prerelease?
+ @version.prerelease?
+ end
+
+ def segments
+ @version.segments
+ end
+
+ def <=>(other)
+ return unless other.is_a?(self.class)
+
+ version_comparison = version <=> other.version
+ return version_comparison unless version_comparison.zero?
+
+ priority <=> other.priority
+ end
+
+ def ==(other)
+ return unless other.is_a?(self.class)
+
+ version == other.version && priority == other.priority
+ end
+
+ def eql?(other)
+ return unless other.is_a?(self.class)
+
+ version.eql?(other.version) && priority.eql?(other.priority)
+ end
+
+ def hash
+ [@version, @priority].hash
+ end
+
+ def to_s
+ @version.to_s
+ end
+
+ protected
+
+ attr_reader :priority
+ end
+ end
+end
diff --git a/lib/bundler/resolver/incompatibility.rb b/lib/bundler/resolver/incompatibility.rb
new file mode 100644
index 0000000000..4ac1b2e1ea
--- /dev/null
+++ b/lib/bundler/resolver/incompatibility.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Resolver
+ class Incompatibility < PubGrub::Incompatibility
+ attr_reader :extended_explanation
+
+ def initialize(terms, cause:, custom_explanation: nil, extended_explanation: nil)
+ @extended_explanation = extended_explanation
+
+ super(terms, cause: cause, custom_explanation: custom_explanation)
+ end
+ end
+ end
+end
diff --git a/lib/bundler/resolver/package.rb b/lib/bundler/resolver/package.rb
new file mode 100644
index 0000000000..3906be3f57
--- /dev/null
+++ b/lib/bundler/resolver/package.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Resolver
+ #
+ # Represents a gem being resolved, in a format PubGrub likes.
+ #
+ # The class holds the following information:
+ #
+ # * Platforms this gem will be resolved on.
+ # * The locked version of this gem resolution should favor (if any).
+ # * Whether the gem should be unlocked to its latest version.
+ # * The dependency explicit set in the Gemfile for this gem (if any).
+ #
+ class Package
+ attr_reader :name, :platforms, :dependency, :locked_version
+
+ def initialize(name, platforms, locked_specs:, unlock:, prerelease: false, prefer_local: false, dependency: nil, new_platforms: [])
+ @name = name
+ @platforms = platforms
+ @locked_version = locked_specs.version_for(name)
+ @unlock = unlock
+ @dependency = dependency || Dependency.new(name, @locked_version)
+ @platforms |= [Gem::Platform::RUBY] if @dependency.default_force_ruby_platform
+ @top_level = !dependency.nil?
+ @prerelease = @dependency.prerelease? || @locked_version&.prerelease? || prerelease ? :consider_first : :ignore
+ @prefer_local = prefer_local
+ @new_platforms = new_platforms
+ end
+
+ def platform_specs(specs)
+ platforms.map do |platform|
+ prefer_locked = @new_platforms.include?(platform) ? false : !unlock?
+ MatchPlatform.select_best_platform_match(specs, platform, prefer_locked: prefer_locked)
+ end
+ end
+
+ def to_s
+ @name.delete("\0")
+ end
+
+ def root?
+ false
+ end
+
+ def top_level?
+ @top_level
+ end
+
+ def meta?
+ @name.end_with?("\0")
+ end
+
+ def ==(other)
+ self.class == other.class && @name == other.name
+ end
+
+ def hash
+ @name.hash
+ end
+
+ def unlock?
+ @unlock == true || @unlock.include?(name)
+ end
+
+ def ignores_prereleases?
+ @prerelease == :ignore
+ end
+
+ def prerelease_specified?
+ @prerelease == :consider_first
+ end
+
+ def consider_prereleases!
+ @prerelease = :consider_last
+ end
+
+ def prefer_local?
+ @prefer_local
+ end
+
+ def consider_remote_versions!
+ @prefer_local = false
+ end
+
+ def force_ruby_platform?
+ @dependency.force_ruby_platform
+ end
+
+ def current_platform?
+ @dependency.current_platform?
+ end
+ end
+ end
+end
diff --git a/lib/bundler/resolver/root.rb b/lib/bundler/resolver/root.rb
new file mode 100644
index 0000000000..e5eb634fb8
--- /dev/null
+++ b/lib/bundler/resolver/root.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require_relative "package"
+
+module Bundler
+ class Resolver
+ #
+ # Represents the Gemfile from the resolver's perspective. It's the root
+ # package and Gemfile entries depend on it.
+ #
+ class Root < Package
+ def initialize(name)
+ @name = name
+ end
+
+ def meta?
+ true
+ end
+
+ def root?
+ true
+ end
+ end
+ end
+end
diff --git a/lib/bundler/resolver/spec_group.rb b/lib/bundler/resolver/spec_group.rb
new file mode 100644
index 0000000000..ac6ba86c4c
--- /dev/null
+++ b/lib/bundler/resolver/spec_group.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Resolver
+ class SpecGroup
+ attr_reader :specs
+
+ def initialize(specs)
+ @specs = specs
+ end
+
+ def empty?
+ @specs.empty?
+ end
+
+ def name
+ @name ||= exemplary_spec.name
+ end
+
+ def version
+ @version ||= exemplary_spec.version
+ end
+
+ def source
+ @source ||= exemplary_spec.source
+ end
+
+ def to_specs(force_ruby_platform, most_specific_locked_platform)
+ @specs.map do |s|
+ lazy_spec = LazySpecification.from_spec(s)
+ lazy_spec.force_ruby_platform = force_ruby_platform
+ lazy_spec.most_specific_locked_platform = most_specific_locked_platform
+ lazy_spec
+ end
+ end
+
+ def to_s
+ sorted_spec_names.join(", ")
+ end
+
+ def dependencies
+ @dependencies ||= @specs.flat_map(&:expanded_dependencies).uniq.sort
+ end
+
+ def ==(other)
+ sorted_spec_names == other.sorted_spec_names
+ end
+
+ def merge(other)
+ return false unless equivalent?(other)
+
+ @specs |= other.specs
+
+ true
+ end
+
+ protected
+
+ def sorted_spec_names
+ @specs.map(&:full_name).sort
+ end
+
+ private
+
+ def equivalent?(other)
+ name == other.name && version == other.version && source == other.source && dependencies == other.dependencies
+ end
+
+ def exemplary_spec
+ @specs.first
+ end
+ end
+ end
+end
diff --git a/lib/bundler/resolver/strategy.rb b/lib/bundler/resolver/strategy.rb
new file mode 100644
index 0000000000..7519d38968
--- /dev/null
+++ b/lib/bundler/resolver/strategy.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Resolver
+ class Strategy
+ def initialize(source)
+ @source = source
+ @package_priority_cache = {}
+ end
+
+ def next_package_and_version(unsatisfied)
+ package, range = next_term_to_try_from(unsatisfied)
+
+ [package, most_preferred_version_of(package, range).first]
+ end
+
+ private
+
+ def next_term_to_try_from(unsatisfied)
+ unsatisfied.min_by do |package, range|
+ @package_priority_cache[[package, range]] ||= begin
+ matching_versions = @source.versions_for(package, range)
+ higher_versions = @source.versions_for(package, range.upper_invert)
+
+ [matching_versions.count <= 1 ? 0 : 1, higher_versions.count]
+ end
+ end
+ end
+
+ def most_preferred_version_of(package, range)
+ versions = @source.versions_for(package, range)
+
+ # Conditional avoids (among other things) calling
+ # sort_versions_by_preferred with the root package
+ if versions.size > 1
+ @source.sort_versions_by_preferred(package, versions)
+ else
+ versions
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/retry.rb b/lib/bundler/retry.rb
new file mode 100644
index 0000000000..49b0f63838
--- /dev/null
+++ b/lib/bundler/retry.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+module Bundler
+ # General purpose class for retrying code that may fail
+ class Retry
+ attr_accessor :name, :total_runs, :current_run
+
+ class << self
+ attr_accessor :default_base_delay
+
+ def default_attempts
+ default_retries + 1
+ end
+ alias_method :attempts, :default_attempts
+
+ def default_retries
+ Bundler.settings[:retry]
+ end
+ end
+
+ # Set default base delay for exponential backoff
+ self.default_base_delay = 1.0
+
+ def initialize(name, exceptions = nil, retries = self.class.default_retries, opts = {})
+ @name = name
+ @retries = retries
+ @exceptions = Array(exceptions) || []
+ @total_runs = @retries + 1 # will run once, then upto attempts.times
+ @base_delay = opts[:base_delay] || self.class.default_base_delay
+ @max_delay = opts[:max_delay] || 60.0
+ @jitter = opts[:jitter] || 0.5
+ end
+
+ def attempt(&block)
+ @current_run = 0
+ @failed = false
+ @error = nil
+ run(&block) while keep_trying?
+ @result
+ end
+ alias_method :attempts, :attempt
+
+ private
+
+ def run(&block)
+ @failed = false
+ @current_run += 1
+ @result = block.call
+ rescue StandardError => e
+ fail_attempt(e)
+ end
+
+ def fail_attempt(e)
+ @failed = true
+ if last_attempt? || @exceptions.any? {|k| e.is_a?(k) }
+ Bundler.ui.info "" unless Bundler.ui.debug?
+ raise e
+ end
+ if name
+ Bundler.ui.info "" unless Bundler.ui.debug? # Add new line in case dots preceded this
+ Bundler.ui.warn "Retrying #{name} due to error (#{current_run.next}/#{total_runs}): #{e.class} #{e.message}", true
+ end
+ backoff_sleep if @base_delay > 0
+ true
+ end
+
+ def backoff_sleep
+ # Exponential backoff: delay = base_delay * 2^(attempt - 1)
+ # Add jitter to prevent thundering herd: random value between 0 and jitter seconds
+ delay = @base_delay * (2**(@current_run - 1))
+ delay = [@max_delay, delay].min
+ jitter_amount = rand * @jitter
+ total_delay = delay + jitter_amount
+ Bundler.ui.debug "Sleeping for #{total_delay.round(2)} seconds before retry"
+ sleep(total_delay)
+ end
+
+ def sleep(duration)
+ Kernel.sleep(duration)
+ end
+
+ def keep_trying?
+ return true if current_run.zero?
+ return false if last_attempt?
+ true if @failed
+ end
+
+ def last_attempt?
+ current_run >= total_runs
+ end
+ end
+end
diff --git a/lib/bundler/ruby_dsl.rb b/lib/bundler/ruby_dsl.rb
new file mode 100644
index 0000000000..5e52f38c8f
--- /dev/null
+++ b/lib/bundler/ruby_dsl.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Bundler
+ module RubyDsl
+ def ruby(*ruby_version)
+ options = ruby_version.pop if ruby_version.last.is_a?(Hash)
+ ruby_version.flatten!
+
+ if options
+ patchlevel = options[:patchlevel]
+ engine = options[:engine]
+ engine_version = options[:engine_version]
+
+ raise GemfileError, "Please define :engine_version" if engine && engine_version.nil?
+ raise GemfileError, "Please define :engine" if engine_version && engine.nil?
+
+ if options[:file]
+ raise GemfileError, "Do not pass version argument when using :file option" unless ruby_version.empty?
+ ruby_version << normalize_ruby_file(options[:file])
+ end
+
+ if engine == "ruby" && engine_version && ruby_version != Array(engine_version)
+ raise GemfileEvalError, "ruby_version must match the :engine_version for MRI"
+ end
+ end
+
+ @ruby_version = RubyVersion.new(ruby_version, patchlevel, engine, engine_version)
+ end
+
+ # Support the various file formats found in .ruby-version files.
+ #
+ # 3.2.2
+ # ruby-3.2.2
+ #
+ # Also supports .tool-versions files for asdf. Lines not starting with "ruby" are ignored.
+ #
+ # ruby 2.5.1 # comment is ignored
+ # ruby 2.5.1# close comment and extra spaces doesn't confuse
+ #
+ # Intentionally does not support `3.2.1@gemset` since rvm recommends using .ruby-gemset instead
+ #
+ # Loads the file relative to the dirname of the Gemfile itself.
+ def normalize_ruby_file(filename)
+ file_content = Bundler.read_file(gemfile.dirname.join(filename))
+ # match "ruby-3.2.2", ruby = "3.2.2", ruby = '3.2.2' or "ruby 3.2.2" capturing version string up to the first space or comment
+ version_match = /^ # Start of line
+ ruby # Literal "ruby"
+ [\s-]* # Optional whitespace or hyphens (for "ruby-3.2.2" format)
+ (?:=\s*)? # Optional equals sign with whitespace (for ruby = "3.2.2" format)
+ (?:
+ "([^"]+)" # Double quoted version
+ |
+ '([^']+)' # Single quoted version
+ |
+ ([^\s#"']+) # Unquoted version
+ )
+ /x.match(file_content)
+ if version_match
+ version_match[1] || version_match[2] || version_match[3]
+ else
+ file_content.strip
+ end
+ rescue Errno::ENOENT
+ raise GemfileError, "Could not find version file #{filename}"
+ end
+ end
+end
diff --git a/lib/bundler/ruby_version.rb b/lib/bundler/ruby_version.rb
new file mode 100644
index 0000000000..aeff07582e
--- /dev/null
+++ b/lib/bundler/ruby_version.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+module Bundler
+ class RubyVersion
+ attr_reader :versions,
+ :patchlevel,
+ :engine,
+ :engine_versions,
+ :gem_version,
+ :engine_gem_version
+
+ def initialize(versions, patchlevel, engine, engine_version)
+ # The parameters to this method must satisfy the
+ # following constraints, which are verified in
+ # the DSL:
+ #
+ # * If an engine is specified, an engine version
+ # must also be specified
+ # * If an engine version is specified, an engine
+ # must also be specified
+ # * If the engine is "ruby", the engine version
+ # must not be specified, or the engine version
+ # specified must match the version.
+
+ @versions = Array(versions).map do |v|
+ normalized_v = normalize_version(v)
+
+ unless Gem::Requirement::PATTERN.match?(normalized_v)
+ raise InvalidArgumentError, "#{v} is not a valid requirement on the Ruby version"
+ end
+
+ op, v = Gem::Requirement.parse(normalized_v)
+ op == "=" ? v.to_s : "#{op} #{v}"
+ end
+
+ @gem_version = Gem::Requirement.create(@versions.first).requirements.first.last
+ @input_engine = engine&.to_s
+ @engine = engine&.to_s || "ruby"
+ @engine_versions = (engine_version && Array(engine_version)) || @versions
+ @engine_gem_version = Gem::Requirement.create(@engine_versions.first).requirements.first.last
+ @patchlevel = patchlevel || (@gem_version.prerelease? ? "-1" : nil)
+ end
+
+ def to_s(versions = self.versions)
+ output = String.new("ruby #{versions_string(versions)}")
+ output << " (#{engine} #{versions_string(engine_versions)})" unless engine == "ruby"
+
+ output
+ end
+
+ # @private
+ PATTERN = /
+ ruby\s
+ (\d+\.\d+\.\d+(?:\.\S+)?) # ruby version
+ (?:p(-?\d+))? # optional patchlevel
+ (?:\s\((\S+)\s(.+)\))? # optional engine info
+ /xo
+
+ # Returns a RubyVersion from the given string.
+ # @param [String] the version string to match.
+ # @return [RubyVersion,Nil] The version if the string is a valid RubyVersion
+ # description, and nil otherwise.
+ def self.from_string(string)
+ new($1, $2, $3, $4) if string =~ PATTERN
+ end
+
+ def single_version_string
+ to_s(gem_version)
+ end
+
+ def ==(other)
+ versions == other.versions &&
+ engine == other.engine &&
+ engine_versions == other.engine_versions
+ end
+
+ def host
+ @host ||= [
+ RbConfig::CONFIG["host_cpu"],
+ RbConfig::CONFIG["host_vendor"],
+ RbConfig::CONFIG["host_os"],
+ ].join("-")
+ end
+
+ # Returns a tuple of these things:
+ # [diff, this, other]
+ # The priority of attributes are
+ # 1. engine
+ # 2. ruby_version
+ # 3. engine_version
+ def diff(other)
+ raise ArgumentError, "Can only diff with a RubyVersion, not a #{other.class}" unless other.is_a?(RubyVersion)
+ if engine != other.engine && @input_engine
+ [:engine, engine, other.engine]
+ elsif versions.empty? || !matches?(versions, other.gem_version)
+ [:version, versions_string(versions), versions_string(other.versions)]
+ elsif @input_engine && !matches?(engine_versions, other.engine_gem_version)
+ [:engine_version, versions_string(engine_versions), versions_string(other.engine_versions)]
+ end
+ end
+
+ def versions_string(versions)
+ Array(versions).join(", ")
+ end
+
+ def self.system
+ ruby_engine = RUBY_ENGINE.dup
+ ruby_version = Gem.ruby_version.to_s
+ ruby_engine_version = RUBY_ENGINE == "ruby" ? ruby_version : RUBY_ENGINE_VERSION.dup
+ patchlevel = RUBY_PATCHLEVEL.to_s
+
+ @system ||= RubyVersion.new(ruby_version, patchlevel, ruby_engine, ruby_engine_version)
+ end
+
+ private
+
+ # Ruby's official preview version format uses a `-`: Example: 3.3.0-preview2
+ # However, RubyGems recognizes preview version format with a `.`: Example: 3.3.0.preview2
+ # Returns version string after replacing `-` with `.`
+ def normalize_version(version)
+ version.tr("-", ".")
+ end
+
+ def matches?(requirements, version)
+ # Handles RUBY_PATCHLEVEL of -1 for instances like ruby-head
+ return requirements == version if requirements.to_s == "-1" || version.to_s == "-1"
+
+ Array(requirements).all? do |requirement|
+ Gem::Requirement.create(requirement).satisfied_by?(Gem::Version.create(version))
+ end
+ end
+ end
+end
diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb
new file mode 100644
index 0000000000..4ad2bdf46f
--- /dev/null
+++ b/lib/bundler/rubygems_ext.rb
@@ -0,0 +1,503 @@
+# frozen_string_literal: true
+
+require "rubygems" unless defined?(Gem)
+
+# We can't let `Gem::Source` be autoloaded in the `Gem::Specification#source`
+# redefinition below, so we need to load it upfront. The reason is that if
+# Bundler monkeypatches are loaded before RubyGems activates an executable (for
+# example, through `ruby -rbundler -S irb`), gem activation might end up calling
+# the redefined `Gem::Specification#source` and triggering the `Gem::Source`
+# autoload. That would result in requiring "rubygems/source" inside another
+# require, which would trigger a monitor error and cause the `autoload` to
+# eventually fail. A better solution is probably to completely avoid autoloading
+# `Gem::Source` from the redefined `Gem::Specification#source`.
+require "rubygems/source"
+
+module Gem
+ # Can be removed once RubyGems 3.5.11 support is dropped
+ unless Gem.respond_to?(:freebsd_platform?)
+ def self.freebsd_platform?
+ RbConfig::CONFIG["host_os"].to_s.include?("bsd")
+ end
+ end
+
+ # Can be removed once RubyGems 3.5.18 support is dropped
+ unless Gem.respond_to?(:open_file_with_lock)
+ class << self
+ remove_method :open_file_with_flock if Gem.respond_to?(:open_file_with_flock)
+
+ def open_file_with_flock(path, &block)
+ # read-write mode is used rather than read-only in order to support NFS
+ mode = IO::RDWR | IO::APPEND | IO::CREAT | IO::BINARY
+ mode |= IO::SHARE_DELETE if IO.const_defined?(:SHARE_DELETE)
+
+ File.open(path, mode) do |io|
+ begin
+ io.flock(File::LOCK_EX)
+ rescue Errno::ENOSYS, Errno::ENOTSUP
+ end
+ yield io
+ end
+ end
+
+ def open_file_with_lock(path, &block)
+ file_lock = "#{path}.lock"
+ open_file_with_flock(file_lock, &block)
+ ensure
+ FileUtils.rm_f file_lock
+ end
+ end
+ end
+
+ require "rubygems/platform"
+
+ class Platform
+ # Can be removed once RubyGems 3.6.9 support is dropped
+ unless respond_to?(:generic)
+ JAVA = Gem::Platform.new("java") # :nodoc:
+ MSWIN = Gem::Platform.new("mswin32") # :nodoc:
+ MSWIN64 = Gem::Platform.new("mswin64") # :nodoc:
+ MINGW = Gem::Platform.new("x86-mingw32") # :nodoc:
+ X64_MINGW_LEGACY = Gem::Platform.new("x64-mingw32") # :nodoc:
+ X64_MINGW = Gem::Platform.new("x64-mingw-ucrt") # :nodoc:
+ UNIVERSAL_MINGW = Gem::Platform.new("universal-mingw") # :nodoc:
+ WINDOWS = [MSWIN, MSWIN64, UNIVERSAL_MINGW].freeze # :nodoc:
+ X64_LINUX = Gem::Platform.new("x86_64-linux") # :nodoc:
+ X64_LINUX_MUSL = Gem::Platform.new("x86_64-linux-musl") # :nodoc:
+
+ GENERICS = [JAVA, *WINDOWS].freeze # :nodoc:
+ private_constant :GENERICS
+
+ GENERIC_CACHE = GENERICS.each_with_object({}) {|g, h| h[g] = g } # :nodoc:
+ private_constant :GENERIC_CACHE
+
+ class << self
+ ##
+ # Returns the generic platform for the given platform.
+
+ def generic(platform)
+ return Gem::Platform::RUBY if platform.nil? || platform == Gem::Platform::RUBY
+
+ GENERIC_CACHE[platform] ||= begin
+ found = GENERICS.find do |match|
+ platform === match
+ end
+ found || Gem::Platform::RUBY
+ end
+ end
+
+ ##
+ # Returns the platform specificity match for the given spec platform and user platform.
+
+ def platform_specificity_match(spec_platform, user_platform)
+ return -1 if spec_platform == user_platform
+ return 1_000_000 if spec_platform.nil? || spec_platform == Gem::Platform::RUBY || user_platform == Gem::Platform::RUBY
+
+ os_match(spec_platform, user_platform) +
+ cpu_match(spec_platform, user_platform) * 10 +
+ version_match(spec_platform, user_platform) * 100
+ end
+
+ ##
+ # Sorts and filters the best platform match for the given matching specs and platform.
+
+ def sort_and_filter_best_platform_match(matching, platform)
+ return matching if matching.one?
+
+ exact = matching.select {|spec| spec.platform == platform }
+ return exact if exact.any?
+
+ sorted_matching = sort_best_platform_match(matching, platform)
+ exemplary_spec = sorted_matching.first
+
+ sorted_matching.take_while {|spec| same_specificity?(platform, spec, exemplary_spec) && same_deps?(spec, exemplary_spec) }
+ end
+
+ ##
+ # Sorts the best platform match for the given matching specs and platform.
+
+ def sort_best_platform_match(matching, platform)
+ matching.sort_by.with_index do |spec, i|
+ [
+ platform_specificity_match(spec.platform, platform),
+ i, # for stable sort
+ ]
+ end
+ end
+
+ private
+
+ def same_specificity?(platform, spec, exemplary_spec)
+ platform_specificity_match(spec.platform, platform) == platform_specificity_match(exemplary_spec.platform, platform)
+ end
+
+ def same_deps?(spec, exemplary_spec)
+ spec.required_ruby_version == exemplary_spec.required_ruby_version &&
+ spec.required_rubygems_version == exemplary_spec.required_rubygems_version &&
+ spec.dependencies.sort == exemplary_spec.dependencies.sort
+ end
+
+ def os_match(spec_platform, user_platform)
+ if spec_platform.os == user_platform.os
+ 0
+ else
+ 1
+ end
+ end
+
+ def cpu_match(spec_platform, user_platform)
+ if spec_platform.cpu == user_platform.cpu
+ 0
+ elsif spec_platform.cpu == "arm" && user_platform.cpu.to_s.start_with?("arm")
+ 0
+ elsif spec_platform.cpu.nil? || spec_platform.cpu == "universal"
+ 1
+ else
+ 2
+ end
+ end
+
+ def version_match(spec_platform, user_platform)
+ if spec_platform.version == user_platform.version
+ 0
+ elsif spec_platform.version.nil?
+ 1
+ else
+ 2
+ end
+ end
+ end
+
+ end
+ end
+
+ require "rubygems/specification"
+
+ # Can be removed once RubyGems 3.5.14 support is dropped
+ VALIDATES_FOR_RESOLUTION = Specification.new.respond_to?(:validate_for_resolution).freeze
+
+ class Specification
+ # Can be removed once RubyGems 3.5.15 support is dropped
+ correct_array_attributes = @@default_value.select {|_k,v| v.is_a?(Array) }.keys
+ unless @@array_attributes == correct_array_attributes
+ @@array_attributes = correct_array_attributes # rubocop:disable Style/ClassVars
+ end
+
+ require_relative "match_metadata"
+ require_relative "match_platform"
+
+ include ::Bundler::MatchMetadata
+
+ attr_accessor :remote, :relative_loaded_from
+
+ module AllowSettingSource
+ attr_writer :source
+
+ def source
+ (defined?(@source) && @source) || super
+ end
+ end
+
+ prepend AllowSettingSource
+
+ alias_method :rg_full_gem_path, :full_gem_path
+ alias_method :rg_loaded_from, :loaded_from
+
+ def full_gem_path
+ if source.respond_to?(:root)
+ File.expand_path(File.dirname(loaded_from), source.root)
+ else
+ rg_full_gem_path
+ end
+ end
+
+ def loaded_from
+ if relative_loaded_from
+ source.path.join(relative_loaded_from).to_s
+ else
+ rg_loaded_from
+ end
+ end
+
+ def load_paths
+ full_require_paths
+ end
+
+ alias_method :rg_extension_dir, :extension_dir
+ def extension_dir
+ # following instance variable is already used in original method
+ # and that is the reason to prefix it with bundler_ and add rubocop exception
+ @bundler_extension_dir ||= if source.respond_to?(:extension_dir_name) # rubocop:disable Naming/MemoizedInstanceVariableName
+ unique_extension_dir = [source.extension_dir_name, File.basename(full_gem_path)].uniq.join("-")
+ File.expand_path(File.join(extensions_dir, unique_extension_dir))
+ else
+ rg_extension_dir
+ end
+ end
+
+ # Can be removed once RubyGems 3.5.21 support is dropped
+ remove_method :gem_dir if method_defined?(:gem_dir, false)
+
+ def gem_dir
+ full_gem_path
+ end
+
+ def insecurely_materialized?
+ false
+ end
+
+ def groups
+ @groups ||= []
+ end
+
+ def git_version
+ return unless loaded_from && source.is_a?(Bundler::Source::Git)
+ " #{source.revision[0..6]}"
+ end
+
+ def to_gemfile(path = nil)
+ gemfile = String.new("source 'https://rubygems.org'\n")
+ gemfile << dependencies_to_gemfile(nondevelopment_dependencies)
+ unless development_dependencies.empty?
+ gemfile << "\n"
+ gemfile << dependencies_to_gemfile(development_dependencies, :development)
+ end
+ gemfile
+ end
+
+ def nondevelopment_dependencies
+ dependencies - development_dependencies
+ end
+
+ def installation_missing?
+ !default_gem? && !File.directory?(full_gem_path)
+ end
+
+ def lock_name
+ @lock_name ||= name_tuple.lock_name
+ end
+
+ unless VALIDATES_FOR_RESOLUTION
+ def validate_for_resolution
+ SpecificationPolicy.new(self).validate_for_resolution
+ end
+ end
+
+ if Gem.rubygems_version < Gem::Version.new("3.5.22")
+ module FixPathSourceMissingExtensions
+ def missing_extensions?
+ return false if %w[Bundler::Source::Path Bundler::Source::Gemspec].include?(source.class.name)
+
+ super
+ end
+ end
+
+ prepend FixPathSourceMissingExtensions
+ end
+
+ private
+
+ def dependencies_to_gemfile(dependencies, group = nil)
+ gemfile = String.new
+ if dependencies.any?
+ gemfile << "group :#{group} do\n" if group
+ dependencies.each do |dependency|
+ gemfile << " " if group
+ gemfile << %(gem "#{dependency.name}")
+ req = dependency.requirements_list.first
+ gemfile << %(, "#{req}") if req
+ gemfile << "\n"
+ end
+ gemfile << "end\n" if group
+ end
+ gemfile
+ end
+ end
+
+ unless VALIDATES_FOR_RESOLUTION
+ class SpecificationPolicy
+ def validate_for_resolution
+ validate_required!
+ end
+ end
+ end
+
+ module BetterPermissionError
+ def data
+ super
+ rescue Errno::EACCES
+ raise Bundler::PermissionError.new(loaded_from, :read)
+ end
+ end
+
+ require "rubygems/stub_specification"
+
+ class StubSpecification
+ prepend BetterPermissionError
+ end
+
+ class Dependency
+ require_relative "force_platform"
+
+ include ::Bundler::ForcePlatform
+
+ attr_reader :force_ruby_platform
+
+ attr_accessor :source, :groups
+
+ alias_method :eql?, :==
+
+ unless method_defined?(:encode_with, false)
+ def encode_with(coder)
+ [:@name, :@requirement, :@type, :@prerelease, :@version_requirements].each do |ivar|
+ coder[ivar.to_s.sub(/^@/, "")] = instance_variable_get(ivar)
+ end
+ end
+ end
+
+ def to_lock
+ out = String.new(" #{name}")
+ unless requirement.none?
+ reqs = requirement.requirements.map {|o, v| "#{o} #{v}" }.sort.reverse
+ out << " (#{reqs.join(", ")})"
+ end
+ out
+ end
+
+ if Gem.rubygems_version < Gem::Version.new("3.5.22")
+ module FilterIgnoredSpecs
+ def matching_specs(platform_only = false)
+ super.reject(&:ignored?)
+ end
+ end
+
+ prepend FilterIgnoredSpecs
+ end
+ end
+
+ # On universal Rubies, resolve the "universal" arch to the real CPU arch, without changing the extension directory.
+ class BasicSpecification
+ if /^universal\.(?<arch>.*?)-/ =~ (CROSS_COMPILING || RUBY_PLATFORM)
+ local_platform = Platform.local
+ if local_platform.cpu == "universal"
+ ORIGINAL_LOCAL_PLATFORM = local_platform.to_s.freeze
+
+ local_platform.cpu = if arch == "arm64e" # arm64e is only permitted for Apple system binaries
+ "arm64"
+ else
+ arch
+ end
+
+ def extensions_dir
+ @extensions_dir ||=
+ Gem.default_ext_dir_for(base_dir) || File.join(base_dir, "extensions", ORIGINAL_LOCAL_PLATFORM, Gem.extension_api_version)
+ end
+ end
+ end
+
+ # Can be removed once RubyGems 3.5.22 support is dropped
+ unless new.respond_to?(:ignored?)
+ def ignored?
+ return @ignored unless @ignored.nil?
+
+ @ignored = missing_extensions?
+ end
+ end
+
+ # Can be removed once RubyGems 3.6.9 support is dropped
+ unless new.respond_to?(:installable_on_platform?)
+ include(::Bundler::MatchPlatform)
+ end
+ end
+
+ require "rubygems/name_tuple"
+
+ class NameTuple
+ # Versions of RubyGems before about 3.5.0 don't to_s the platform.
+ unless Gem::NameTuple.new("a", Gem::Version.new("1"), Gem::Platform.new("x86_64-linux")).platform.is_a?(String)
+ alias_method :initialize_with_platform, :initialize
+
+ def initialize(name, version, platform = Gem::Platform::RUBY)
+ if Gem::Platform === platform
+ initialize_with_platform(name, version, platform.to_s)
+ else
+ initialize_with_platform(name, version, platform)
+ end
+ end
+ end
+
+ def lock_name
+ if platform == Gem::Platform::RUBY
+ "#{name} (#{version})"
+ else
+ "#{name} (#{version}-#{platform})"
+ end
+ end
+ end
+
+ unless Gem.rubygems_version >= Gem::Version.new("3.5.19")
+ class Resolver::ActivationRequest
+ remove_method :installed?
+
+ def installed?
+ case @spec
+ when Gem::Resolver::VendorSpecification then
+ true
+ else
+ this_spec = full_spec
+
+ Gem::Specification.any? do |s|
+ s == this_spec && s.base_dir == this_spec.base_dir
+ end
+ end
+ end
+ end
+ end
+
+ unless Gem.rubygems_version >= Gem::Version.new("3.6.7")
+ module UnfreezeCompactIndexParsedResponse
+ def parse(line)
+ version, platform, dependencies, requirements = super
+ [version, platform, dependencies.frozen? ? dependencies.dup : dependencies, requirements.frozen? ? requirements.dup : requirements]
+ end
+ end
+
+ Resolver::APISet::GemParser.prepend(UnfreezeCompactIndexParsedResponse)
+ end
+
+ # RubyGems before 4.0.13 split compact index dependency/requirement entries
+ # on every colon, which mangles metadata values that contain colons such as
+ # the `created_at` timestamps the cooldown feature relies on. Split only on
+ # the first colon so those values survive on older RubyGems.
+ #
+ # The module is defined unconditionally so it stays testable on any RubyGems,
+ # but only prepended when the host RubyGems still has the buggy behavior.
+ module SplitCompactIndexEntryOnFirstColon
+ private
+
+ def parse_dependency(string)
+ dependency = string.split(":", 2)
+ dependency[-1] = dependency[-1].split("&") if dependency.size > 1
+ dependency[0] = -dependency[0]
+ dependency
+ end
+ end
+
+ unless Gem.rubygems_version >= Gem::Version.new("4.0.13")
+ Resolver::APISet::GemParser.prepend(SplitCompactIndexEntryOnFirstColon)
+ end
+
+ if Gem.rubygems_version < Gem::Version.new("3.6.0")
+ class Package; end
+ require "rubygems/package/tar_reader"
+ require "rubygems/package/tar_reader/entry"
+
+ module FixFullNameEncoding
+ def full_name
+ super.force_encoding(Encoding::UTF_8)
+ end
+ end
+
+ Package::TarReader::Entry.prepend(FixFullNameEncoding)
+ end
+end
diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb
new file mode 100644
index 0000000000..fc019f54d2
--- /dev/null
+++ b/lib/bundler/rubygems_gem_installer.rb
@@ -0,0 +1,196 @@
+# frozen_string_literal: true
+
+require "rubygems/installer"
+
+module Bundler
+ class RubyGemsGemInstaller < Gem::Installer
+ def check_executable_overwrite(filename)
+ # Bundler needs to install gems regardless of binstub overwriting
+ end
+
+ def install
+ pre_install_checks
+
+ run_pre_install_hooks
+
+ spec.loaded_from = spec_file
+
+ # Completely remove any previous gem files
+ strict_rm_rf gem_dir
+ strict_rm_rf spec.extension_dir
+
+ SharedHelpers.filesystem_access(gem_dir, :create) do
+ FileUtils.mkdir_p gem_dir
+ end
+
+ SharedHelpers.filesystem_access(gem_dir, :write) do
+ extract_files
+ end
+
+ if options[:build_extension] == false
+ warn_skipped_extensions
+ elsif spec.extensions.any?
+ build_extensions
+ end
+ write_build_info_file
+ run_post_build_hooks
+
+ SharedHelpers.filesystem_access(bin_dir, :write) do
+ generate_bin
+ end
+
+ if options[:install_plugin] == false
+ remove_stale_plugins
+ warn_skipped_plugins
+ else
+ generate_plugins
+ end
+
+ write_spec
+
+ SharedHelpers.filesystem_access("#{gem_home}/cache", :write) do
+ write_cache_file
+ end
+
+ say spec.post_install_message unless spec.post_install_message.nil?
+
+ run_post_install_hooks
+
+ spec
+ end
+
+ if Bundler.rubygems.provides?("< 3.5")
+ def pre_install_checks
+ super
+ rescue Gem::FilePermissionError
+ # Ignore permission checks in RubyGems. Instead, go on, and try to write
+ # for real. We properly handle permission errors when they happen.
+ nil
+ end
+ end
+
+ def ensure_writable_dir(dir)
+ super
+ rescue Gem::FilePermissionError
+ # Ignore permission checks in RubyGems. Instead, go on, and try to write
+ # for real. We properly handle permission errors when they happen.
+ nil
+ end
+
+ def generate_plugins
+ return unless Gem::Installer.method_defined?(:generate_plugins, false)
+
+ ensure_writable_dir @plugins_dir
+
+ if spec.plugins.empty?
+ remove_plugins_for(spec, @plugins_dir)
+ else
+ regenerate_plugins_for(spec, @plugins_dir)
+ end
+ end
+
+ def warn_skipped_extensions
+ return if spec.extensions.empty?
+
+ Bundler.ui.warn "#{spec.full_name} contains native extensions that were not built.\n" \
+ "To build extensions, unset no_build_extension and run `bundle pristine #{spec.name}`."
+ end
+
+ def warn_skipped_plugins
+ return if spec.plugins.empty?
+
+ Bundler.ui.warn "#{spec.full_name} contains plugins that were not installed.\n" \
+ "To install plugins, unset no_install_plugin and run `bundle pristine #{spec.name}`."
+ end
+
+ if Bundler.rubygems.provides?("< 3.5.19")
+ def generate_bin_script(filename, bindir)
+ bin_script_path = File.join bindir, formatted_program_filename(filename)
+
+ Gem.open_file_with_lock(bin_script_path) do
+ require "fileutils"
+ FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers
+
+ File.open(bin_script_path, "wb", 0o755) do |file|
+ file.write app_script_text(filename)
+ file.chmod(options[:prog_mode] || 0o755)
+ end
+ end
+
+ verbose bin_script_path
+
+ generate_windows_script filename, bindir
+ end
+ end
+
+ def build_jobs
+ Bundler.settings[:jobs] || super
+ end
+
+ def build_extensions
+ extension_cache_path = options[:bundler_extension_cache_path]
+ extension_dir = spec.extension_dir
+ unless extension_cache_path && extension_dir
+ prepare_extension_build(extension_dir)
+ return super
+ end
+
+ build_complete = SharedHelpers.filesystem_access(extension_cache_path.join("gem.build_complete"), :read, &:file?)
+ if build_complete && !options[:force]
+ SharedHelpers.filesystem_access(File.dirname(extension_dir)) do |p|
+ FileUtils.mkpath p
+ end
+ SharedHelpers.filesystem_access(extension_cache_path) do
+ FileUtils.cp_r extension_cache_path, extension_dir
+ end
+ else
+ prepare_extension_build(extension_dir)
+ super
+ SharedHelpers.filesystem_access(extension_cache_path.parent, &:mkpath)
+ SharedHelpers.filesystem_access(extension_cache_path) do
+ FileUtils.cp_r extension_dir, extension_cache_path
+ end
+ end
+ end
+
+ def spec
+ if Bundler.rubygems.provides?("< 3.3.12") # RubyGems implementation rescues and re-raises errors before 3.3.12 and we don't want that
+ @package.spec
+ else
+ super
+ end
+ end
+
+ def gem_checksum
+ Checksum.from_gem_package(@package)
+ end
+
+ private
+
+ def prepare_extension_build(extension_dir)
+ SharedHelpers.filesystem_access(extension_dir, :create) do
+ FileUtils.mkdir_p extension_dir
+ end
+ end
+
+ def strict_rm_rf(dir)
+ return unless File.exist?(dir)
+ return if Dir.empty?(dir)
+
+ parent = File.dirname(dir)
+ parent_st = File.stat(parent)
+
+ if parent_st.world_writable? && !parent_st.sticky?
+ raise InsecureInstallPathError.new(spec.full_name, dir)
+ end
+
+ begin
+ FileUtils.remove_entry_secure(dir)
+ rescue StandardError => e
+ raise unless File.exist?(dir)
+
+ raise DirectoryRemovalError.new(e, "Could not delete previous installation of `#{dir}`")
+ end
+ end
+ end
+end
diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb
new file mode 100644
index 0000000000..e04ef23259
--- /dev/null
+++ b/lib/bundler/rubygems_integration.rb
@@ -0,0 +1,456 @@
+# frozen_string_literal: true
+
+require "rubygems" unless defined?(Gem)
+
+module Bundler
+ class RubygemsIntegration
+ require "monitor"
+
+ EXT_LOCK = Monitor.new
+
+ def initialize
+ @replaced_methods = {}
+ end
+
+ def version
+ @version ||= Gem.rubygems_version
+ end
+
+ def provides?(req_str)
+ Gem::Requirement.new(req_str).satisfied_by?(version)
+ end
+
+ def build_args
+ require "rubygems/command"
+ Gem::Command.build_args
+ end
+
+ def build_args=(args)
+ require "rubygems/command"
+ Gem::Command.build_args = args
+ end
+
+ def set_target_rbconfig(path)
+ Gem.set_target_rbconfig(path)
+ end
+
+ def loaded_specs(name)
+ Gem.loaded_specs[name]
+ end
+
+ def mark_loaded(spec)
+ if spec.respond_to?(:activated=)
+ current = Gem.loaded_specs[spec.name]
+ current.activated = false if current
+ spec.activated = true
+ end
+ Gem.loaded_specs[spec.name] = spec
+ end
+
+ def validate(spec)
+ Bundler.ui.silence { spec.validate_for_resolution }
+ rescue Gem::InvalidSpecificationException => e
+ error_message = "The gemspec at #{spec.loaded_from} is not valid. Please fix this gemspec.\n" \
+ "The validation error was '#{e.message}'\n"
+ raise Gem::InvalidSpecificationException.new(error_message)
+ rescue Errno::ENOENT
+ nil
+ end
+
+ def stub_set_spec(stub, spec)
+ stub.instance_variable_set(:@spec, spec)
+ end
+
+ def path(obj)
+ obj.to_s
+ end
+
+ def ruby_engine
+ Gem.ruby_engine
+ end
+
+ def read_binary(path)
+ Gem.read_binary(path)
+ end
+
+ def inflate(obj)
+ Gem::Util.inflate(obj)
+ end
+
+ def gem_dir
+ Gem.dir
+ end
+
+ def gem_bindir
+ Gem.bindir
+ end
+
+ def user_home
+ Gem.user_home
+ end
+
+ def gem_path
+ Gem.path
+ end
+
+ def reset
+ Gem::Specification.reset
+ end
+
+ def post_reset_hooks
+ Gem.post_reset_hooks
+ end
+
+ def suffix_pattern
+ Gem.suffix_pattern
+ end
+
+ def gem_cache
+ gem_path.map {|p| File.expand_path("cache", p) }
+ end
+
+ def spec_cache_dirs
+ @spec_cache_dirs ||= begin
+ dirs = gem_path.map {|dir| File.join(dir, "specifications") }
+ dirs << Gem.spec_cache_dir
+ dirs.uniq.select {|dir| File.directory? dir }
+ end
+ end
+
+ def marshal_spec_dir
+ Gem::MARSHAL_SPEC_DIR
+ end
+
+ def clear_paths
+ Gem.clear_paths
+ end
+
+ def bin_path(gem, bin, ver)
+ Gem.bin_path(gem, bin, ver)
+ end
+
+ def loaded_gem_paths
+ loaded_gem_paths = Gem.loaded_specs.map {|_, s| s.full_require_paths }
+ loaded_gem_paths.flatten
+ end
+
+ def ui=(obj)
+ Gem::DefaultUserInteraction.ui = obj
+ end
+
+ def ext_lock
+ EXT_LOCK
+ end
+
+ def spec_from_gem(path)
+ require "rubygems/package"
+ Gem::Package.new(path).spec
+ end
+
+ def build_gem(gem_dir, spec)
+ build(spec)
+ end
+
+ def security_policy_keys
+ %w[High Medium Low AlmostNo No].map {|level| "#{level}Security" }
+ end
+
+ def security_policies
+ @security_policies ||= begin
+ require "rubygems/security"
+ Gem::Security::Policies
+ rescue LoadError, NameError
+ {}
+ end
+ end
+
+ def reverse_rubygems_kernel_mixin
+ # Disable rubygems' gem activation system
+ if Gem.respond_to?(:discover_gems_on_require=)
+ Gem.discover_gems_on_require = false
+ else
+ [::Kernel.singleton_class, ::Kernel].each do |k|
+ if k.private_method_defined?(:gem_original_require)
+ redefine_method(k, :require, k.instance_method(:gem_original_require))
+ end
+ end
+ end
+ end
+
+ def replace_gem(specs_by_name)
+ executables = nil
+
+ [::Kernel.singleton_class, ::Kernel].each do |kernel_class|
+ redefine_method(kernel_class, :gem) do |dep, *reqs|
+ if executables&.include?(File.basename(caller_locations(1, 1).first.path))
+ break
+ end
+
+ reqs.pop if reqs.last.is_a?(Hash)
+
+ unless dep.respond_to?(:name) && dep.respond_to?(:requirement)
+ dep = Gem::Dependency.new(dep, reqs)
+ end
+
+ if spec = specs_by_name[dep.name]
+ return true if dep.matches_spec?(spec)
+ end
+
+ message = if spec.nil?
+ target_file = begin
+ Bundler.default_gemfile.basename
+ rescue GemfileNotFound
+ "inline Gemfile"
+ end
+ "#{dep.name} is not part of the bundle." \
+ " Add it to your #{target_file}."
+ else
+ "can't activate #{dep}, already activated #{spec.full_name}. " \
+ "Make sure all dependencies are added to Gemfile."
+ end
+
+ e = Gem::LoadError.new(message)
+ e.name = dep.name
+ e.requirement = dep.requirement
+ raise e
+ end
+ end
+ end
+
+ # Used to give better error messages when activating specs outside of the current bundle
+ def replace_bin_path(specs_by_name)
+ redefine_method(gem_class, :find_spec_for_exe) do |gem_name, *args|
+ exec_name = args.first
+ raise ArgumentError, "you must supply exec_name" unless exec_name
+
+ spec_with_name = specs_by_name[gem_name]
+ matching_specs_by_exec_name = specs_by_name.values.select {|s| s.executables.include?(exec_name) }
+ spec = matching_specs_by_exec_name.delete(spec_with_name)
+
+ unless spec || !matching_specs_by_exec_name.empty?
+ message = "can't find executable #{exec_name} for gem #{gem_name}"
+ if spec_with_name.nil?
+ message += ". #{gem_name} is not currently included in the bundle, " \
+ "perhaps you meant to add it to your #{Bundler.default_gemfile.basename}?"
+ end
+ raise Gem::Exception, message
+ end
+
+ unless spec
+ spec = matching_specs_by_exec_name.shift
+ warn \
+ "Bundler is using a binstub that was created for a different gem (#{spec.name}).\n" \
+ "You should run `bundle binstub #{gem_name}` " \
+ "to work around a system/bundle conflict."
+ end
+
+ unless matching_specs_by_exec_name.empty?
+ conflicting_names = matching_specs_by_exec_name.map(&:name).join(", ")
+ warn \
+ "The `#{exec_name}` executable in the `#{spec.name}` gem is being loaded, but it's also present in other gems (#{conflicting_names}).\n" \
+ "If you meant to run the executable for another gem, make sure you use a project specific binstub (`bundle binstub <gem_name>`).\n" \
+ "If you plan to use multiple conflicting executables, generate binstubs for them and disambiguate their names."
+ end
+
+ spec
+ end
+ end
+
+ # Replace or hook into RubyGems to provide a bundlerized view
+ # of the world.
+ def replace_entrypoints(specs)
+ specs_by_name = add_default_gems_to(specs)
+
+ reverse_rubygems_kernel_mixin
+ begin
+ # bundled_gems only provide with Ruby 3.3 or later
+ require "bundled_gems"
+ rescue LoadError
+ else
+ Gem::BUNDLED_GEMS.replace_require(specs) if Gem::BUNDLED_GEMS.respond_to?(:replace_require)
+ end
+ replace_gem(specs_by_name)
+ stub_rubygems(specs_by_name.values)
+ replace_bin_path(specs_by_name)
+
+ Gem.clear_paths
+ end
+
+ # Add default gems not already present in specs, and return them as a hash.
+ def add_default_gems_to(specs)
+ specs_by_name = specs.reduce({}) do |h, s|
+ h[s.name] = s
+ h
+ end
+
+ Bundler.rubygems.default_stubs.each do |stub|
+ default_spec = stub.to_spec
+ default_spec_name = default_spec.name
+ next if specs_by_name.key?(default_spec_name)
+
+ specs_by_name[default_spec_name] = default_spec
+ end
+
+ specs_by_name
+ end
+
+ def undo_replacements
+ @replaced_methods.each do |(sym, klass), method|
+ redefine_method(klass, sym, method)
+ end
+ post_reset_hooks.reject! {|proc| proc.binding.source_location[0] == __FILE__ }
+ @replaced_methods.clear
+ end
+
+ def redefine_method(klass, method, unbound_method = nil, &block)
+ visibility = method_visibility(klass, method)
+ begin
+ if (instance_method = klass.instance_method(method)) && method != :initialize
+ # doing this to ensure we also get private methods
+ klass.send(:remove_method, method)
+ end
+ rescue NameError
+ # method isn't defined
+ nil
+ end
+ @replaced_methods[[method, klass]] = instance_method
+ if unbound_method
+ klass.send(:define_method, method, unbound_method)
+ klass.send(visibility, method)
+ elsif block
+ klass.send(:define_method, method, &block)
+ klass.send(visibility, method)
+ end
+ end
+
+ def method_visibility(klass, method)
+ if klass.private_method_defined?(method)
+ :private
+ elsif klass.protected_method_defined?(method)
+ :protected
+ else
+ :public
+ end
+ end
+
+ def stub_rubygems(specs)
+ Gem::Specification.all = specs
+
+ Gem.post_reset do
+ Gem::Specification.all = specs
+ end
+
+ redefine_method(gem_class, :finish_resolve) do |*|
+ []
+ end
+
+ redefine_method(gem_class, :load_plugins) do |*|
+ load_plugin_files specs.flat_map(&:plugins)
+ end
+ end
+
+ def plain_specs
+ Gem::Specification._all
+ end
+
+ def plain_specs=(specs)
+ Gem::Specification.all = specs
+ end
+
+ def fetch_specs(remote, name, fetcher)
+ require "rubygems/remote_fetcher"
+ path = remote.uri.to_s + "#{name}.#{Gem.marshal_version}.gz"
+ string = fetcher.fetch_path(path)
+ specs = Bundler.safe_load_marshal(string)
+ raise MarshalError, "Specs #{name} from #{remote} is expected to be an Array but was unexpected class #{specs.class}" unless specs.is_a?(Array)
+ specs
+ rescue Gem::RemoteFetcher::FetchError
+ # it's okay for prerelease to fail
+ raise unless name == "prerelease_specs"
+ end
+
+ def fetch_all_remote_specs(remote, gem_remote_fetcher)
+ specs = fetch_specs(remote, "specs", gem_remote_fetcher)
+ pres = fetch_specs(remote, "prerelease_specs", gem_remote_fetcher) || []
+
+ specs.concat(pres)
+ end
+
+ def download_gem(spec, uri, cache_dir, fetcher)
+ require "rubygems/remote_fetcher"
+ uri = Bundler.settings.mirror_for(uri)
+ redacted_uri = Gem::Uri.redact(uri)
+
+ Bundler::Retry.new("download gem from #{redacted_uri}").attempts do
+ gem_file_name = spec.file_name
+ local_gem_path = File.join cache_dir, gem_file_name
+ return if File.exist? local_gem_path
+
+ begin
+ remote_gem_path = uri + "gems/#{gem_file_name}"
+
+ SharedHelpers.filesystem_access(local_gem_path) do
+ fetcher.cache_update_path remote_gem_path, local_gem_path
+ end
+ rescue Gem::RemoteFetcher::FetchError
+ raise if spec.original_platform == spec.platform
+
+ original_gem_file_name = "#{spec.original_name}.gem"
+ raise if gem_file_name == original_gem_file_name
+
+ gem_file_name = original_gem_file_name
+ retry
+ end
+ end
+ rescue Gem::RemoteFetcher::FetchError => e
+ raise Bundler::HTTPError, "Could not download gem from #{redacted_uri} due to underlying error <#{e.message}>"
+ end
+
+ def build(spec, skip_validation = false)
+ require "rubygems/package"
+ Gem::Package.build(spec, skip_validation)
+ end
+
+ def path_separator
+ Gem.path_separator
+ end
+
+ def all_specs
+ SharedHelpers.feature_removed! "Bundler.rubygems.all_specs has been removed in favor of Bundler.rubygems.installed_specs"
+ end
+
+ def installed_specs
+ Gem::Specification.stubs.reject(&:default_gem?).map do |stub|
+ StubSpecification.from_stub(stub)
+ end
+ end
+
+ def default_specs
+ Gem::Specification.default_stubs.map do |stub|
+ StubSpecification.from_stub(stub)
+ end
+ end
+
+ def find_bundler(version)
+ find_name("bundler").find {|s| s.version.to_s == version.to_s }
+ end
+
+ def find_name(name)
+ Gem::Specification.stubs_for(name).map(&:to_spec)
+ end
+
+ def default_stubs
+ Gem::Specification.default_stubs("*.gemspec")
+ end
+
+ private
+
+ def gem_class
+ class << Gem; self; end
+ end
+ end
+
+ def self.rubygems
+ @rubygems ||= RubygemsIntegration.new
+ end
+end
diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb
new file mode 100644
index 0000000000..5280e72aa2
--- /dev/null
+++ b/lib/bundler/runtime.rb
@@ -0,0 +1,331 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Runtime
+ include SharedHelpers
+
+ def initialize(root, definition)
+ @root = root
+ @definition = definition
+ end
+
+ def setup(*groups)
+ @definition.ensure_equivalent_gemfile_and_lockfile
+
+ # Has to happen first
+ clean_load_path
+
+ specs = @definition.specs_for(groups)
+
+ SharedHelpers.set_bundle_environment
+ Bundler.rubygems.replace_entrypoints(specs)
+
+ # Activate the specs
+ load_paths = specs.map do |spec|
+ check_for_activated_spec!(spec)
+
+ Bundler.rubygems.mark_loaded(spec)
+ spec.load_paths.reject {|path| $LOAD_PATH.include?(path) }
+ end.reverse.flatten
+
+ Gem.add_to_load_path(*load_paths)
+
+ setup_manpath
+
+ lock(preserve_unknown_sections: true)
+
+ self
+ end
+
+ def require(*groups)
+ groups.map!(&:to_sym)
+ groups = [:default] if groups.empty?
+
+ dependencies = @definition.dependencies.select do |dep|
+ # Select the dependency if it is in any of the requested groups, and
+ # for the current platform, and matches the gem constraints.
+ (dep.groups & groups).any? && dep.should_include?
+ end
+
+ Plugin.hook(Plugin::Events::GEM_BEFORE_REQUIRE_ALL, dependencies)
+
+ dependencies.each do |dep|
+ Plugin.hook(Plugin::Events::GEM_BEFORE_REQUIRE, dep)
+
+ # Loop through all the specified autorequires for the
+ # dependency. If there are none, use the dependency's name
+ # as the autorequire.
+ Array(dep.autorequire || dep.name).each do |file|
+ # Allow `require: true` as an alias for `require: <name>`
+ file = dep.name if file == true
+ required_file = file
+ begin
+ Kernel.require required_file
+ rescue LoadError => e
+ if dep.autorequire.nil? && e.path == required_file
+ if required_file.include?("-")
+ required_file = required_file.tr("-", "/")
+ retry
+ end
+ else
+ raise Bundler::GemRequireError.new e,
+ "There was an error while trying to load the gem '#{file}'."
+ end
+ rescue StandardError => e
+ raise Bundler::GemRequireError.new e,
+ "There was an error while trying to load the gem '#{file}'."
+ end
+ end
+
+ Plugin.hook(Plugin::Events::GEM_AFTER_REQUIRE, dep)
+ end
+
+ Plugin.hook(Plugin::Events::GEM_AFTER_REQUIRE_ALL, dependencies)
+
+ dependencies
+ end
+
+ def self.definition_method(meth)
+ define_method(meth) do
+ raise ArgumentError, "no definition when calling Runtime##{meth}" unless @definition
+ @definition.send(meth)
+ end
+ end
+ private_class_method :definition_method
+
+ definition_method :requested_specs
+ definition_method :specs
+ definition_method :dependencies
+ definition_method :current_dependencies
+ definition_method :requires
+
+ def lock(opts = {})
+ return if @definition.no_resolve_needed?
+ @definition.lock(opts[:preserve_unknown_sections])
+ end
+
+ alias_method :gems, :specs
+
+ def cache(custom_path = nil, local = false)
+ cache_path = Bundler.app_cache(custom_path)
+ SharedHelpers.filesystem_access(cache_path) do |p|
+ FileUtils.mkdir_p(p)
+ end unless File.exist?(cache_path)
+
+ Bundler.ui.info "Updating files in #{Bundler.settings.app_cache_path}"
+
+ specs_to_cache = if Bundler.settings[:cache_all_platforms]
+ @definition.resolve.materialized_for_all_platforms
+ else
+ begin
+ specs
+ rescue GemNotFound
+ if local
+ Bundler.ui.warn "Some gems seem to be missing from your #{Bundler.settings.app_cache_path} directory."
+ end
+
+ raise
+ end
+ end
+
+ specs_to_cache.each do |spec|
+ next if spec.name == "bundler"
+
+ source = spec.source
+ next if source.is_a?(Source::Gemspec)
+
+ if source.respond_to?(:migrate_cache)
+ source.migrate_cache(custom_path, local: local)
+ elsif source.respond_to?(:cache)
+ source.cache(spec, custom_path)
+ end
+ end
+
+ Dir[cache_path.join("*/.git")].each do |git_dir|
+ FileUtils.rm_rf(git_dir)
+ FileUtils.touch(File.expand_path("../.bundlecache", git_dir))
+ end
+
+ prune_cache(cache_path) unless Bundler.settings[:no_prune]
+ end
+
+ def prune_cache(cache_path)
+ SharedHelpers.filesystem_access(cache_path) do |p|
+ FileUtils.mkdir_p(p)
+ end unless File.exist?(cache_path)
+ resolve = @definition.resolve
+ prune_gem_cache(resolve, cache_path)
+ prune_git_and_path_cache(resolve, cache_path)
+ end
+
+ def clean(dry_run = false)
+ gem_bins = Dir["#{Gem.dir}/bin/*"]
+ git_dirs = Dir["#{Gem.dir}/bundler/gems/*"]
+ git_cache_dirs = Dir["#{Gem.dir}/cache/bundler/git/*"]
+ gem_dirs = Dir["#{Gem.dir}/gems/*"]
+ gem_files = Dir["#{Gem.dir}/cache/*.gem"]
+ gemspec_files = Dir["#{Gem.dir}/specifications/*.gemspec"]
+ extension_dirs = Dir["#{Gem.dir}/extensions/*/*/*"] + Dir["#{Gem.dir}/bundler/gems/extensions/*/*/*"]
+ spec_gem_paths = []
+ # need to keep git sources around
+ spec_git_paths = @definition.spec_git_paths
+ spec_git_cache_dirs = []
+ spec_gem_executables = []
+ spec_cache_paths = []
+ spec_gemspec_paths = []
+ spec_extension_paths = []
+ specs_to_keep = Bundler.rubygems.add_default_gems_to(specs).values
+
+ current_bundler = Bundler.rubygems.find_bundler(Bundler.gem_version)
+ if current_bundler
+ specs_to_keep << current_bundler
+ end
+
+ specs_to_keep.each do |spec|
+ spec_gem_paths << spec.full_gem_path
+ # need to check here in case gems are nested like for the rails git repo
+ md = %r{(.+bundler/gems/.+-[a-f0-9]{7,12})}.match(spec.full_gem_path)
+ spec_git_paths << md[1] if md
+ spec_gem_executables << spec.executables.collect do |executable|
+ e = "#{Bundler.rubygems.gem_bindir}/#{executable}"
+ [e, "#{e}.bat"]
+ end
+ spec_cache_paths << spec.cache_file
+ spec_gemspec_paths << spec.spec_file
+ spec_extension_paths << spec.extension_dir if spec.respond_to?(:extension_dir)
+ spec_git_cache_dirs << spec.source.cache_path.to_s if spec.source.is_a?(Bundler::Source::Git)
+ end
+ spec_gem_paths.uniq!
+ spec_gem_executables.flatten!
+
+ stale_gem_bins = gem_bins - spec_gem_executables
+ stale_git_dirs = git_dirs - spec_git_paths - ["#{Gem.dir}/bundler/gems/extensions"]
+ stale_git_cache_dirs = git_cache_dirs - spec_git_cache_dirs
+ stale_gem_dirs = gem_dirs - spec_gem_paths
+ stale_gem_files = gem_files - spec_cache_paths
+ stale_gemspec_files = gemspec_files - spec_gemspec_paths
+ stale_extension_dirs = extension_dirs - spec_extension_paths
+
+ removed_stale_gem_dirs = stale_gem_dirs.collect {|dir| remove_dir(dir, dry_run) }
+ removed_stale_git_dirs = stale_git_dirs.collect {|dir| remove_dir(dir, dry_run) }
+ output = removed_stale_gem_dirs + removed_stale_git_dirs
+
+ unless dry_run
+ stale_files = stale_gem_bins + stale_gem_files + stale_gemspec_files
+ stale_files.each do |file|
+ SharedHelpers.filesystem_access(File.dirname(file)) do |_p|
+ FileUtils.rm(file) if File.exist?(file)
+ end
+ end
+
+ stale_dirs = stale_git_cache_dirs + stale_extension_dirs
+ stale_dirs.each do |stale_dir|
+ SharedHelpers.filesystem_access(stale_dir) do |dir|
+ FileUtils.rm_rf(dir) if File.exist?(dir)
+ end
+ end
+ end
+
+ output
+ end
+
+ private
+
+ def prune_gem_cache(resolve, cache_path)
+ cached = Dir["#{cache_path}/*.gem"]
+
+ cached = cached.delete_if do |path|
+ spec = Bundler.rubygems.spec_from_gem path
+
+ resolve.any? do |s|
+ s.name == spec.name && s.version == spec.version && !s.source.is_a?(Bundler::Source::Git)
+ end
+ end
+
+ if cached.any?
+ Bundler.ui.info "Removing outdated .gem files from #{Bundler.settings.app_cache_path}"
+
+ cached.each do |path|
+ Bundler.ui.info " * #{File.basename(path)}"
+
+ begin
+ File.delete(path)
+ rescue Errno::ENOENT
+ end
+ end
+ end
+ end
+
+ def prune_git_and_path_cache(resolve, cache_path)
+ cached = Dir["#{cache_path}/*/.bundlecache"]
+
+ cached = cached.delete_if do |path|
+ name = File.basename(File.dirname(path))
+
+ resolve.any? do |s|
+ source = s.source
+ source.respond_to?(:app_cache_dirname) && source.app_cache_dirname == name
+ end
+ end
+
+ if cached.any?
+ Bundler.ui.info "Removing outdated git and path gems from #{Bundler.settings.app_cache_path}"
+
+ cached.each do |path|
+ path = File.dirname(path)
+ Bundler.ui.info " * #{File.basename(path)}"
+ FileUtils.rm_rf(path)
+ end
+ end
+ end
+
+ def setup_manpath
+ # Add man/ subdirectories from activated bundles to MANPATH for man(1)
+ manuals = $LOAD_PATH.filter_map do |path|
+ man_subdir = path.sub(/lib$/, "man")
+ man_subdir unless Dir[man_subdir + "/man?/"].empty?
+ end
+
+ return if manuals.empty?
+ Bundler::SharedHelpers.set_env "MANPATH", manuals.concat(
+ ENV["MANPATH"] ? ENV["MANPATH"].to_s.split(File::PATH_SEPARATOR) : [""]
+ ).uniq.join(File::PATH_SEPARATOR)
+ end
+
+ def remove_dir(dir, dry_run)
+ full_name = Pathname.new(dir).basename.to_s
+
+ parts = full_name.split("-")
+ name = parts[0..-2].join("-")
+ version = parts.last
+ output = "#{name} (#{version})"
+
+ if dry_run
+ Bundler.ui.info "Would have removed #{output}"
+ else
+ Bundler.ui.info "Removing #{output}"
+ FileUtils.rm_rf(dir)
+ end
+
+ output
+ end
+
+ def check_for_activated_spec!(spec)
+ return unless activated_spec = Bundler.rubygems.loaded_specs(spec.name)
+ return if activated_spec.version == spec.version
+
+ suggestion = if activated_spec.default_gem?
+ "Since #{spec.name} is a default gem, you can either remove your dependency on it" \
+ " or try updating to a newer version of bundler that supports #{spec.name} as a default gem."
+ else
+ "Prepending `bundle exec` to your command may solve this."
+ end
+
+ e = Gem::LoadError.new "You have already activated #{activated_spec.name} #{activated_spec.version}, " \
+ "but your Gemfile requires #{spec.name} #{spec.version}. #{suggestion}"
+ e.name = spec.name
+ e.requirement = Gem::Requirement.new(spec.version.to_s)
+ raise e
+ end
+ end
+end
diff --git a/lib/bundler/safe_marshal.rb b/lib/bundler/safe_marshal.rb
new file mode 100644
index 0000000000..50aa0f60a6
--- /dev/null
+++ b/lib/bundler/safe_marshal.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Bundler
+ module SafeMarshal
+ ALLOWED_CLASSES = [
+ Array,
+ FalseClass,
+ Gem::Specification,
+ Gem::Version,
+ Hash,
+ String,
+ Symbol,
+ Time,
+ TrueClass,
+ ].freeze
+
+ ERROR = "Unexpected class %s present in marshaled data. Only %s are allowed."
+
+ PROC = proc do |object|
+ object.tap do
+ unless ALLOWED_CLASSES.include?(object.class)
+ raise TypeError, format(ERROR, object.class, ALLOWED_CLASSES.join(", "))
+ end
+ end
+ end
+
+ def self.proc
+ PROC
+ end
+ end
+end
diff --git a/lib/bundler/self_manager.rb b/lib/bundler/self_manager.rb
new file mode 100644
index 0000000000..82efbf56a4
--- /dev/null
+++ b/lib/bundler/self_manager.rb
@@ -0,0 +1,197 @@
+# frozen_string_literal: true
+
+module Bundler
+ #
+ # This class handles installing and switching to the version of bundler needed
+ # by an application.
+ #
+ class SelfManager
+ def restart_with_locked_bundler_if_needed
+ restart_version = find_restart_version
+ return unless restart_version && installed?(restart_version)
+
+ restart_with(restart_version)
+ end
+
+ def install_locked_bundler_and_restart_with_it_if_needed
+ restart_version = find_restart_version
+ return unless restart_version
+
+ if restart_version == lockfile_version
+ Bundler.ui.info \
+ "Bundler #{current_version} is running, but your lockfile was generated with #{lockfile_version}. " \
+ "Installing Bundler #{lockfile_version} and restarting using that version."
+ else
+ Bundler.ui.info \
+ "Bundler #{current_version} is running, but your configuration was #{restart_version}. " \
+ "Installing Bundler #{restart_version} and restarting using that version."
+ end
+
+ install_and_restart_with(restart_version)
+ end
+
+ def update_bundler_and_restart_with_it_if_needed(target)
+ spec = resolve_update_version_from(target)
+ return unless spec
+
+ version = spec.version
+
+ Bundler.ui.info "Updating bundler to #{version}."
+
+ install(spec) unless installed?(version)
+
+ restart_with(version)
+ end
+
+ private
+
+ def install_and_restart_with(version)
+ requirement = Gem::Requirement.new(version)
+ spec = find_latest_matching_spec(requirement)
+
+ if spec.nil?
+ Bundler.ui.warn "Your lockfile is locked to a version of bundler (#{lockfile_version}) that doesn't exist at https://rubygems.org/. Going on using #{current_version}"
+ return
+ end
+
+ install(spec)
+ rescue StandardError => e
+ Bundler.ui.trace e
+ Bundler.ui.warn "There was an error installing the locked bundler version (#{lockfile_version}), rerun with the `--verbose` flag for more details. Going on using bundler #{current_version}."
+ else
+ restart_with(version)
+ end
+
+ def install(spec)
+ spec.source.download(spec)
+ spec.source.install(spec)
+ end
+
+ def restart_with(version)
+ configured_gem_home = ENV["GEM_HOME"]
+ configured_orig_gem_home = ENV["BUNDLER_ORIG_GEM_HOME"]
+ configured_gem_path = ENV["GEM_PATH"]
+ configured_orig_gem_path = ENV["BUNDLER_ORIG_GEM_PATH"]
+
+ argv0 = File.exist?($PROGRAM_NAME) ? $PROGRAM_NAME : Process.argv0
+ cmd = [argv0, *ARGV]
+ cmd.unshift(Gem.ruby) unless File.executable?(argv0)
+
+ Bundler.with_original_env do
+ Kernel.exec(
+ {
+ "GEM_HOME" => configured_gem_home,
+ "BUNDLER_ORIG_GEM_HOME" => configured_orig_gem_home,
+ "GEM_PATH" => configured_gem_path,
+ "BUNDLER_ORIG_GEM_PATH" => configured_orig_gem_path,
+ "BUNDLER_VERSION" => version.to_s,
+ },
+ *cmd
+ )
+ end
+ end
+
+ def needs_switching?(restart_version)
+ autoswitching_applies? &&
+ released?(restart_version) &&
+ !running?(restart_version)
+ end
+
+ def autoswitching_applies?
+ (ENV["BUNDLER_VERSION"].nil? || ENV["BUNDLER_VERSION"].empty?) &&
+ ruby_can_restart_with_same_arguments? &&
+ lockfile_version
+ end
+
+ def resolve_update_version_from(target)
+ requirement = Gem::Requirement.new(target)
+ update_candidate = find_latest_matching_spec(requirement)
+
+ if update_candidate.nil?
+ raise InvalidOption, "The `bundle update --bundler` target version (#{target}) does not exist"
+ end
+
+ resolved_version = update_candidate.version
+ needs_update = requirement.specific? ? !running?(resolved_version) : running_older_than?(resolved_version)
+
+ return unless needs_update
+
+ update_candidate
+ end
+
+ def local_specs
+ @local_specs ||= Bundler::Source::Rubygems.new("allow_local" => true).specs.select {|spec| spec.name == "bundler" }
+ end
+
+ def remote_specs
+ @remote_specs ||= begin
+ source = Bundler::Source::Rubygems.new("remotes" => "https://rubygems.org")
+ source.remote!
+ source.add_dependency_names("bundler")
+ source.specs.select(&:matches_current_metadata?)
+ end
+ end
+
+ def find_latest_matching_spec(requirement)
+ Bundler.configure
+ local_result = find_latest_matching_spec_from_collection(local_specs, requirement)
+ return local_result if local_result && requirement.specific?
+
+ remote_result = find_latest_matching_spec_from_collection(remote_specs, requirement)
+ return remote_result if local_result.nil?
+
+ [local_result, remote_result].max
+ end
+
+ def find_latest_matching_spec_from_collection(specs, requirement)
+ specs.sort.reverse_each.find {|spec| requirement.satisfied_by?(spec.version) }
+ end
+
+ def running?(version)
+ version == current_version
+ end
+
+ def running_older_than?(version)
+ current_version < version
+ end
+
+ def released?(version)
+ !version.to_s.end_with?(".dev")
+ end
+
+ def ruby_can_restart_with_same_arguments?
+ $PROGRAM_NAME != "-e"
+ end
+
+ def installed?(restart_version)
+ Bundler.configure
+
+ Bundler.rubygems.find_bundler(restart_version.to_s)
+ end
+
+ def current_version
+ @current_version ||= Bundler.gem_version
+ end
+
+ def lockfile_version
+ return @lockfile_version if defined?(@lockfile_version)
+
+ parsed_version = Bundler::LockfileParser.bundled_with
+ @lockfile_version = parsed_version ? Gem::Version.new(parsed_version) : nil
+ rescue ArgumentError
+ @lockfile_version = nil
+ end
+
+ def find_restart_version
+ return unless SharedHelpers.in_bundle?
+
+ configured_version = Bundler.settings[:version]
+ return if configured_version == "system"
+
+ restart_version = configured_version == "lockfile" ? lockfile_version : Gem::Version.new(configured_version)
+ return unless needs_switching?(restart_version)
+
+ restart_version
+ end
+ end
+end
diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb
new file mode 100644
index 0000000000..fd77c2f7fc
--- /dev/null
+++ b/lib/bundler/settings.rb
@@ -0,0 +1,587 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Settings
+ autoload :Mirror, File.expand_path("mirror", __dir__)
+ autoload :Mirrors, File.expand_path("mirror", __dir__)
+ autoload :Validator, File.expand_path("settings/validator", __dir__)
+
+ BOOL_KEYS = %w[
+ auto_install
+ cache_all
+ cache_all_platforms
+ clean
+ deployment
+ disable_checksum_validation
+ disable_exec_load
+ disable_local_branch_check
+ disable_local_revision_check
+ disable_shared_gems
+ disable_version_check
+ force_ruby_platform
+ frozen
+ gem.changelog
+ gem.coc
+ gem.mit
+ gem.bundle
+ git.allow_insecure
+ global_gem_cache
+ ignore_messages
+ init_gems_rb
+ inline
+ lockfile_checksums
+ no_build_extension
+ no_install
+ no_install_plugin
+ no_prune
+ path.system
+ plugins
+ prefer_patch
+ silence_deprecations
+ silence_root_warning
+ update_requires_all_flag
+ verbose
+ ].freeze
+
+ NUMBER_KEYS = %w[
+ cooldown
+ jobs
+ redirect
+ retry
+ ssl_verify_mode
+ timeout
+ ].freeze
+
+ ARRAY_KEYS = %w[
+ only
+ with
+ without
+ ].freeze
+
+ STRING_KEYS = %w[
+ bin
+ cache_path
+ console
+ default_cli_command
+ gem.ci
+ gem.github_username
+ gem.linter
+ gem.rubocop
+ gem.test
+ gemfile
+ lockfile
+ path
+ shebang
+ simulate_version
+ system_bindir
+ trust-policy
+ version
+ ].freeze
+
+ DEFAULT_CONFIG = {
+ "BUNDLE_SILENCE_DEPRECATIONS" => false,
+ "BUNDLE_DISABLE_VERSION_CHECK" => true,
+ "BUNDLE_PREFER_PATCH" => false,
+ "BUNDLE_REDIRECT" => 5,
+ "BUNDLE_RETRY" => 3,
+ "BUNDLE_TIMEOUT" => 10,
+ "BUNDLE_VERSION" => "lockfile",
+ "BUNDLE_LOCKFILE_CHECKSUMS" => true,
+ "BUNDLE_CACHE_ALL" => true,
+ "BUNDLE_PLUGINS" => true,
+ "BUNDLE_GLOBAL_GEM_CACHE" => false,
+ "BUNDLE_UPDATE_REQUIRES_ALL_FLAG" => false,
+ }.freeze
+
+ def initialize(root = nil)
+ @root = root
+ @local_config = load_config(local_config_file)
+ @local_root = root || Pathname.new(".bundle").expand_path
+
+ @env_config = ENV.to_h
+ @env_config.select! {|key, _value| key.start_with?("BUNDLE_") }
+ @env_config.delete("BUNDLE_")
+
+ @global_config = load_config(global_config_file)
+ @temporary = {}
+
+ @key_cache = {}
+ end
+
+ def [](name)
+ key = key_for(name)
+
+ value = nil
+ configs.each do |_, config|
+ value = config[key]
+ next if value.nil?
+ break
+ end
+
+ converted_value(value, name)
+ end
+
+ def set_command_option(key, value)
+ temporary(key => value)
+ value
+ end
+
+ def set_command_option_if_given(key, value)
+ return if value.nil?
+ set_command_option(key, value)
+ end
+
+ def set_local(key, value)
+ local_config_file = @local_root.join("config")
+
+ set_key(key, value, @local_config, local_config_file)
+ end
+
+ def temporary(update)
+ existing = Hash[update.map {|k, _| [k, @temporary[key_for(k)]] }]
+ update.each do |k, v|
+ set_key(k, v, @temporary, nil)
+ end
+ return unless block_given?
+ begin
+ yield
+ ensure
+ existing.each {|k, v| set_key(k, v, @temporary, nil) }
+ end
+ end
+
+ def set_global(key, value)
+ set_key(key, value, @global_config, global_config_file)
+ end
+
+ def all
+ keys = @temporary.keys.union(@global_config.keys, @local_config.keys, @env_config.keys)
+
+ keys.map! do |key|
+ key = key.delete_prefix("BUNDLE_")
+ key.gsub!("___", "-")
+ key.gsub!("__", ".")
+ key.downcase!
+ key
+ end.sort!
+ keys
+ end
+
+ def local_overrides
+ repos = {}
+ all.each do |k|
+ repos[k.delete_prefix("local.")] = self[k] if k.start_with?("local.")
+ end
+ repos
+ end
+
+ def mirror_for(uri)
+ if uri.is_a?(String)
+ require_relative "vendored_uri"
+ uri = Gem::URI(uri)
+ end
+
+ gem_mirrors.for(uri.to_s).uri
+ end
+
+ def credentials_for(uri)
+ self[uri.to_s] || self[uri.host]
+ end
+
+ def gem_mirrors
+ all.inject(Mirrors.new) do |mirrors, k|
+ mirrors.parse(k, self[k]) if k.start_with?("mirror.")
+ mirrors
+ end
+ end
+
+ def locations(key)
+ key = key_for(key)
+ configs.keys.inject({}) do |partial_locations, level|
+ value_on_level = configs[level][key]
+ partial_locations[level] = value_on_level unless value_on_level.nil?
+ partial_locations
+ end
+ end
+
+ def pretty_values_for(exposed_key)
+ key = key_for(exposed_key)
+
+ locations = []
+
+ if value = @temporary[key]
+ locations << "Set for the current command: #{printable_value(value, exposed_key).inspect}"
+ end
+
+ if value = @local_config[key]
+ locations << "Set for your local app (#{local_config_file}): #{printable_value(value, exposed_key).inspect}"
+ end
+
+ if value = @env_config[key]
+ locations << "Set via #{key}: #{printable_value(value, exposed_key).inspect}"
+ end
+
+ if value = @global_config[key]
+ locations << "Set for the current user (#{global_config_file}): #{printable_value(value, exposed_key).inspect}"
+ end
+
+ return ["You have not configured a value for `#{exposed_key}`"] if locations.empty?
+ locations
+ end
+
+ def processor_count
+ require "etc"
+ Etc.nprocessors
+ rescue StandardError
+ 1
+ end
+
+ # for legacy reasons, in Bundler 2, we do not respect :disable_shared_gems
+ def path
+ configs.each do |_level, settings|
+ path = value_for("path", settings)
+ path_system = value_for("path.system", settings)
+ disabled_shared_gems = value_for("disable_shared_gems", settings)
+ next if path.nil? && path_system.nil? && disabled_shared_gems.nil?
+ system_path = path_system || (disabled_shared_gems == false)
+ return Path.new(path, system_path)
+ end
+
+ path = "vendor/bundle" if self[:deployment]
+
+ Path.new(path, false)
+ end
+
+ Path = Struct.new(:explicit_path, :system_path) do
+ def path
+ path = base_path
+ path = File.join(path, Bundler.ruby_scope) unless use_system_gems?
+ path
+ end
+
+ def use_system_gems?
+ return true if system_path
+ return false if explicit_path
+ !Bundler.feature_flag.bundler_5_mode?
+ end
+
+ def base_path
+ path = explicit_path
+ path ||= ".bundle" unless use_system_gems?
+ path ||= Bundler.rubygems.gem_dir
+ path
+ end
+
+ def base_path_relative_to_pwd
+ base_path = Pathname.new(self.base_path)
+ expanded_base_path = base_path.expand_path(Bundler.root)
+ relative_path = expanded_base_path.relative_path_from(Pathname.pwd)
+ if relative_path.to_s.start_with?("..")
+ relative_path = base_path if base_path.absolute?
+ else
+ relative_path = Pathname.new(File.join(".", relative_path))
+ end
+ relative_path
+ rescue ArgumentError
+ expanded_base_path
+ end
+
+ def validate!
+ return unless explicit_path && system_path
+ path = Bundler.settings.pretty_values_for(:path)
+ path.unshift(nil, "path:") unless path.empty?
+ system_path = Bundler.settings.pretty_values_for("path.system")
+ system_path.unshift(nil, "path.system:") unless system_path.empty?
+ disable_shared_gems = Bundler.settings.pretty_values_for(:disable_shared_gems)
+ disable_shared_gems.unshift(nil, "disable_shared_gems:") unless disable_shared_gems.empty?
+ raise InvalidOption,
+ "Using a custom path while using system gems is unsupported.\n#{path.join("\n")}\n#{system_path.join("\n")}\n#{disable_shared_gems.join("\n")}"
+ end
+ end
+
+ def ignore_config?
+ ENV["BUNDLE_IGNORE_CONFIG"]
+ end
+
+ def app_cache_path
+ @app_cache_path ||= self[:cache_path] || "vendor/cache"
+ end
+
+ def installation_parallelization
+ self[:jobs] || processor_count
+ end
+
+ def validate!
+ all.each do |raw_key|
+ [@local_config, @env_config, @global_config].each do |settings|
+ value = value_for(raw_key, settings)
+ Validator.validate!(raw_key, value, settings.dup)
+ end
+ end
+ end
+
+ def key_for(key)
+ @key_cache[key] ||= self.class.key_for(key)
+ end
+
+ private
+
+ def configs
+ @configs ||= {
+ temporary: @temporary,
+ local: @local_config,
+ env: @env_config,
+ global: @global_config,
+ default: DEFAULT_CONFIG,
+ }
+ end
+
+ def value_for(name, config)
+ converted_value(config[key_for(name)], name)
+ end
+
+ def parent_setting_for(name)
+ split_specific_setting_for(name)[0]
+ end
+
+ def specific_gem_for(name)
+ split_specific_setting_for(name)[1]
+ end
+
+ def split_specific_setting_for(name)
+ name.split(".")
+ end
+
+ def is_bool(name)
+ name = self.class.key_to_s(name)
+ BOOL_KEYS.include?(name) || BOOL_KEYS.include?(parent_setting_for(name))
+ end
+
+ def is_string(name)
+ name = self.class.key_to_s(name)
+ STRING_KEYS.include?(name) || name.start_with?("local.") || name.start_with?("mirror.") || name.start_with?("build.")
+ end
+
+ def to_bool(value)
+ case value
+ when String
+ value.match?(/\A(false|f|no|n|0|)\z/i) ? false : true
+ when nil, false
+ false
+ else
+ true
+ end
+ end
+
+ def is_num(key)
+ NUMBER_KEYS.include?(self.class.key_to_s(key))
+ end
+
+ def is_array(key)
+ ARRAY_KEYS.include?(self.class.key_to_s(key))
+ end
+
+ def is_credential(key)
+ key == "gem.push_key"
+ end
+
+ def is_userinfo(value)
+ value.include?(":")
+ end
+
+ def to_array(value)
+ return [] unless value
+ value.tr(" ", ":").split(":").map(&:to_sym)
+ end
+
+ def array_to_s(array)
+ array = Array(array)
+ return nil if array.empty?
+ array.join(":").tr(" ", ":")
+ end
+
+ def set_key(raw_key, value, hash, file)
+ raw_key = self.class.key_to_s(raw_key)
+ value = array_to_s(value) if is_array(raw_key)
+
+ key = key_for(raw_key)
+
+ return if hash[key] == value
+
+ hash[key] = value
+ hash.delete(key) if value.nil?
+
+ Validator.validate!(raw_key, converted_value(value, raw_key), hash)
+
+ return unless file
+
+ SharedHelpers.filesystem_access(file.dirname, :create) do |p|
+ FileUtils.mkdir_p(p)
+ end
+
+ SharedHelpers.filesystem_access(file) do |p|
+ p.open("w") {|f| f.write(serializer_class.dump(hash)) }
+ end
+ end
+
+ def converted_value(value, key)
+ key = self.class.key_to_s(key)
+
+ if is_array(key)
+ to_array(value)
+ elsif value.nil?
+ nil
+ elsif is_bool(key) || value == "false"
+ to_bool(value)
+ elsif is_num(key)
+ value.to_i
+ else
+ value.to_s
+ end
+ end
+
+ def printable_value(value, key)
+ converted = converted_value(value, key)
+ return converted unless converted.is_a?(String)
+
+ if is_string(key)
+ converted
+ elsif is_credential(key)
+ "[REDACTED]"
+ elsif is_userinfo(converted)
+ username, pass = converted.split(":", 2)
+
+ if pass == "x-oauth-basic"
+ username = "[REDACTED]"
+ else
+ pass = "[REDACTED]"
+ end
+
+ [username, pass].join(":")
+ else
+ converted
+ end
+ end
+
+ def global_config_file
+ if ENV["BUNDLE_CONFIG"] && !ENV["BUNDLE_CONFIG"].empty?
+ Pathname.new(ENV["BUNDLE_CONFIG"])
+ elsif ENV["BUNDLE_USER_CONFIG"] && !ENV["BUNDLE_USER_CONFIG"].empty?
+ Pathname.new(ENV["BUNDLE_USER_CONFIG"])
+ elsif ENV["BUNDLE_USER_HOME"] && !ENV["BUNDLE_USER_HOME"].empty?
+ Pathname.new(ENV["BUNDLE_USER_HOME"]).join("config")
+ elsif Bundler.rubygems.user_home && !Bundler.rubygems.user_home.empty?
+ Pathname.new(Bundler.rubygems.user_home).join(".bundle/config")
+ end
+ end
+
+ def local_config_file
+ Pathname.new(@root).join("config") if @root
+ end
+
+ def load_config(config_file)
+ return {} if !config_file || ignore_config?
+ SharedHelpers.filesystem_access(config_file, :read) do |file|
+ valid_file = file.exist? && !file.size.zero?
+ return {} unless valid_file
+ (serializer_class.load(file.read) || {}).inject({}) do |config, (k, v)|
+ k = k.dup
+ k << "/" if /https?:/i.match?(k) && !k.end_with?("/", "__#{FALLBACK_TIMEOUT_URI_OPTION.upcase}")
+ k.gsub!(".", "__")
+
+ unless k.start_with?("#")
+ if k.include?("-")
+ Bundler.ui.warn "Your #{file} config includes `#{k}`, which contains the dash character (`-`).\n" \
+ "This is deprecated, because configuration through `ENV` should be possible, but `ENV` keys cannot include dashes.\n" \
+ "Please edit #{file} and replace any dashes in configuration keys with a triple underscore (`___`)."
+
+ # string hash keys are frozen
+ k = k.gsub("-", "___")
+ end
+
+ config[k] = v
+ end
+
+ config
+ end
+ end
+ end
+
+ def serializer_class
+ require "rubygems/yaml_serializer"
+ Gem::YAMLSerializer
+ rescue LoadError
+ # TODO: Remove this when RubyGems 3.4 is EOL
+ require_relative "yaml_serializer"
+ YAMLSerializer
+ end
+
+ FALLBACK_TIMEOUT_URI_OPTION = "fallback_timeout"
+
+ NORMALIZE_URI_OPTIONS_PATTERN =
+ /
+ \A
+ (\w+\.)? # optional prefix key
+ (https?.*?) # URI
+ (\.#{FALLBACK_TIMEOUT_URI_OPTION})? # optional suffix key
+ \z
+ /ix
+
+ def self.key_for(key)
+ key = key_to_s(key)
+ key = normalize_uri(key) if key.start_with?("http", "mirror.http")
+ key = key.gsub(".", "__")
+ key.gsub!("-", "___")
+ key.upcase!
+
+ key.gsub(/\A([ #]*)/, '\1BUNDLE_')
+ end
+
+ # TODO: duplicates Rubygems#normalize_uri
+ # TODO: is this the correct place to validate mirror URIs?
+ def self.normalize_uri(uri)
+ uri = uri.to_s
+ if uri =~ NORMALIZE_URI_OPTIONS_PATTERN
+ prefix = $1
+ uri = $2
+ suffix = $3
+ end
+ uri = URINormalizer.normalize_suffix(uri)
+ require_relative "vendored_uri"
+ uri = Gem::URI(uri)
+ unless uri.absolute?
+ raise ArgumentError, format("Gem sources must be absolute. You provided '%s'.", uri)
+ end
+ "#{prefix}#{uri}#{suffix}"
+ end
+
+ # This is a hot method, so avoid respond_to? checks on every invocation
+ if :read.respond_to?(:name)
+ def self.key_to_s(key)
+ case key
+ when String
+ key
+ when Symbol
+ key.name
+ when Gem::URI::HTTP
+ key.to_s
+ else
+ raise ArgumentError, "Invalid key: #{key.inspect}"
+ end
+ end
+ else
+ def self.key_to_s(key)
+ case key
+ when String
+ key
+ when Symbol
+ key.to_s
+ when Gem::URI::HTTP
+ key.to_s
+ else
+ raise ArgumentError, "Invalid key: #{key.inspect}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/settings/validator.rb b/lib/bundler/settings/validator.rb
new file mode 100644
index 0000000000..70a0ca36d4
--- /dev/null
+++ b/lib/bundler/settings/validator.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Settings
+ class Validator
+ class Rule
+ attr_reader :description
+
+ def initialize(keys, description, &validate)
+ @keys = keys
+ @description = description
+ @validate = validate
+ end
+
+ def validate!(key, value, settings)
+ instance_exec(key, value, settings, &@validate)
+ end
+
+ def fail!(key, value, *reasons)
+ reasons.unshift @description
+ raise InvalidOption, "Setting `#{key}` to #{value.inspect} failed:\n#{reasons.map {|r| " - #{r}" }.join("\n")}"
+ end
+
+ def set(settings, key, value, *reasons)
+ hash_key = k(key)
+ return if settings[hash_key] == value
+ reasons.unshift @description
+ Bundler.ui.info "Setting `#{key}` to #{value.inspect}, since #{reasons.join(", ")}"
+ if value.nil?
+ settings.delete(hash_key)
+ else
+ settings[hash_key] = value
+ end
+ end
+
+ def k(key)
+ Bundler.settings.key_for(key)
+ end
+ end
+
+ def self.rules
+ @rules ||= Hash.new {|h, k| h[k] = [] }
+ end
+ private_class_method :rules
+
+ def self.rule(keys, description, &blk)
+ rule = Rule.new(keys, description, &blk)
+ keys.each {|k| rules[k] << rule }
+ end
+ private_class_method :rule
+
+ def self.validate!(key, value, settings)
+ rules_to_validate = rules[key]
+ rules_to_validate.each {|rule| rule.validate!(key, value, settings) }
+ end
+
+ rule %w[path path.system], "path and path.system are mutually exclusive" do |key, value, settings|
+ if key == "path" && value
+ set(settings, "path.system", nil)
+ elsif key == "path.system" && value
+ set(settings, :path, nil)
+ end
+ end
+
+ rule %w[with without], "a group cannot be in both `with` & `without` simultaneously" do |key, value, settings|
+ with = settings.fetch(k(:with), "").split(":").map(&:to_sym)
+ without = settings.fetch(k(:without), "").split(":").map(&:to_sym)
+
+ other_key = key == "with" ? :without : :with
+ other_setting = key == "with" ? without : with
+
+ conflicting = with & without
+ if conflicting.any?
+ fail!(key, value, "`#{other_key}` is current set to #{other_setting.inspect}", "the `#{conflicting.join("`, `")}` groups conflict")
+ end
+ end
+
+ rule %w[default_cli_command], "default_cli_command must be either 'install' or 'cli_help'" do |key, value, _settings|
+ valid_values = %w[install cli_help]
+ if !value.nil? && !valid_values.include?(value.to_s)
+ fail!(key, value, "must be one of: #{valid_values.join(", ")}")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/setup.rb b/lib/bundler/setup.rb
new file mode 100644
index 0000000000..5a0fd8e0e3
--- /dev/null
+++ b/lib/bundler/setup.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require_relative "shared_helpers"
+
+if Bundler::SharedHelpers.in_bundle?
+ require_relative "../bundler"
+
+ # autoswitch to locked Bundler version if available
+ Bundler.auto_switch
+
+ # try to auto_install first before we get to the `Bundler.ui.silence`, so user knows what is happening
+ Bundler.auto_install
+
+ if STDOUT.tty? || ENV["BUNDLER_FORCE_TTY"]
+ begin
+ Bundler.ui.silence { Bundler.setup }
+ rescue Bundler::BundlerError => e
+ Bundler.ui.error e.message
+ Bundler.ui.warn e.backtrace.join("\n") if ENV["DEBUG"]
+ if e.is_a?(Bundler::GemNotFound)
+ default_bundle = Gem.bin_path("bundler", "bundle")
+ current_bundle = Bundler::SharedHelpers.bundle_bin_path
+ suggested_bundle = default_bundle == current_bundle ? "bundle" : current_bundle
+ suggested_cmd = "#{suggested_bundle} install"
+ original_gemfile = Bundler.original_env["BUNDLE_GEMFILE"]
+ suggested_cmd += " --gemfile #{original_gemfile}" if original_gemfile
+ Bundler.ui.warn "Run `#{suggested_cmd}` to install missing gems."
+ end
+ exit e.status_code
+ end
+ else
+ Bundler.ui.silence { Bundler.setup }
+ end
+
+ # We might be in the middle of shelling out to rubygems
+ # (RUBYOPT=-rbundler/setup), so we need to give rubygems the opportunity of
+ # not being silent.
+ Gem::DefaultUserInteraction.ui = nil
+end
diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb
new file mode 100644
index 0000000000..2aa8abe0a0
--- /dev/null
+++ b/lib/bundler/shared_helpers.rb
@@ -0,0 +1,393 @@
+# frozen_string_literal: true
+
+require_relative "version"
+require_relative "rubygems_integration"
+require_relative "current_ruby"
+
+module Bundler
+ autoload :WINDOWS, File.expand_path("constants", __dir__)
+ autoload :FREEBSD, File.expand_path("constants", __dir__)
+ autoload :NULL, File.expand_path("constants", __dir__)
+
+ module SharedHelpers
+ def root
+ gemfile = find_gemfile
+ raise GemfileNotFound, "Could not locate Gemfile" unless gemfile
+ Pathname.new(gemfile).expand_path.parent
+ end
+
+ def default_gemfile
+ gemfile = find_gemfile
+ raise GemfileNotFound, "Could not locate Gemfile" unless gemfile
+ Pathname.new(gemfile).expand_path
+ end
+
+ def default_lockfile
+ given = ENV["BUNDLE_LOCKFILE"]
+ return Pathname.new(given) if given && !given.empty?
+
+ gemfile = default_gemfile
+
+ case gemfile.basename.to_s
+ when "gems.rb" then Pathname.new(gemfile.sub(/.rb$/, ".locked"))
+ else Pathname.new("#{gemfile}.lock")
+ end
+ end
+
+ def default_bundle_dir
+ bundle_dir = find_directory(".bundle")
+ return nil unless bundle_dir
+
+ bundle_dir = Pathname.new(bundle_dir)
+
+ global_bundle_dir = Bundler.user_home.join(".bundle")
+ return nil if bundle_dir == global_bundle_dir
+
+ bundle_dir
+ end
+
+ def in_bundle?
+ find_gemfile
+ end
+
+ def chdir(dir, &blk)
+ Bundler.rubygems.ext_lock.synchronize do
+ Dir.chdir dir, &blk
+ end
+ end
+
+ def pwd
+ Bundler.rubygems.ext_lock.synchronize do
+ Dir.pwd
+ end
+ end
+
+ def with_clean_git_env(&block)
+ keys = %w[GIT_DIR GIT_WORK_TREE]
+ old_env = keys.inject({}) do |h, k|
+ h.update(k => ENV[k])
+ end
+
+ keys.each {|key| ENV.delete(key) }
+
+ block.call
+ ensure
+ keys.each {|key| ENV[key] = old_env[key] }
+ end
+
+ def set_bundle_environment
+ set_bundle_variables
+ set_path
+ set_rubyopt
+ set_rubylib
+ end
+
+ # Rescues permissions errors raised by file system operations
+ # (ie. Errno:EACCESS, Errno::EAGAIN) and raises more friendly errors instead.
+ #
+ # @param path [String] the path that the action will be attempted to
+ # @param action [Symbol, #to_s] the type of operation that will be
+ # performed. For example: :write, :read, :exec
+ #
+ # @yield path
+ #
+ # @raise [Bundler::PermissionError] if Errno:EACCES is raised in the
+ # given block
+ # @raise [Bundler::TemporaryResourceError] if Errno:EAGAIN is raised in the
+ # given block
+ #
+ # @example
+ # filesystem_access("vendor/cache", :create) do
+ # FileUtils.mkdir_p("vendor/cache")
+ # end
+ #
+ # @see {Bundler::PermissionError}
+ def filesystem_access(path, action = :write, &block)
+ yield(path.dup)
+ rescue Errno::EACCES => e
+ path_basename = File.basename(path.to_s)
+ raise unless e.message.include?(path_basename) || action == :create
+
+ raise PermissionError.new(path, action)
+ rescue Errno::EAGAIN
+ raise TemporaryResourceError.new(path, action)
+ rescue Errno::EPROTO
+ raise VirtualProtocolError.new
+ rescue Errno::ENOSPC
+ raise NoSpaceOnDeviceError.new(path, action)
+ rescue Errno::ENOTSUP
+ raise OperationNotSupportedError.new(path, action)
+ rescue Errno::EPERM
+ raise OperationNotPermittedError.new(path, action)
+ rescue Errno::EROFS
+ raise ReadOnlyFileSystemError.new(path, action)
+ rescue Errno::EEXIST, Errno::ENOENT
+ raise
+ rescue SystemCallError => e
+ raise GenericSystemCallError.new(e, "There was an error #{[:create, :write].include?(action) ? "creating" : "accessing"} `#{path}`.")
+ end
+
+ def feature_deprecated!(message)
+ return unless prints_major_deprecations?
+
+ Bundler.ui.warn("[DEPRECATED] #{message}")
+ end
+
+ def feature_removed!(message)
+ require_relative "errors"
+ raise RemovedError, "[REMOVED] #{message}"
+ end
+
+ def print_major_deprecations!
+ multiple_gemfiles = search_up(".") do |dir|
+ gemfiles = gemfile_names.select {|gf| File.file? File.expand_path(gf, dir) }
+ next if gemfiles.empty?
+ break gemfiles.size != 1
+ end
+ return unless multiple_gemfiles
+ message = "Multiple gemfiles (gems.rb and Gemfile) detected. " \
+ "Make sure you remove Gemfile and Gemfile.lock since bundler is ignoring them in favor of gems.rb and gems.locked."
+ Bundler.ui.warn message
+ end
+
+ def ensure_same_dependencies(spec, old_deps, new_deps)
+ new_deps = new_deps.reject {|d| d.type == :development }
+ old_deps = old_deps.reject {|d| d.type == :development }
+
+ without_type = proc {|d| Gem::Dependency.new(d.name, d.requirements_list.sort) }
+ new_deps.map!(&without_type)
+ old_deps.map!(&without_type)
+
+ extra_deps = new_deps - old_deps
+ return if extra_deps.empty?
+
+ Bundler.ui.debug "#{spec.full_name} from #{spec.remote} has corrupted API dependencies" \
+ " (was expecting #{old_deps.map(&:to_s)}, but the real spec has #{new_deps.map(&:to_s)})"
+ raise APIResponseMismatchError,
+ "Downloading #{spec.full_name} revealed dependencies not in the API (#{extra_deps.join(", ")})." \
+ "\nRunning `bundle update #{spec.name}` should fix the problem."
+ end
+
+ def pretty_dependency(dep)
+ msg = String.new(dep.name)
+ msg << " (#{dep.requirement})" unless dep.requirement == Gem::Requirement.default
+
+ if dep.is_a?(Bundler::Dependency)
+ platform_string = dep.platforms.join(", ")
+ msg << " " << platform_string if !platform_string.empty? && platform_string != Gem::Platform::RUBY
+ end
+
+ msg
+ end
+
+ def md5_available?
+ return @md5_available if defined?(@md5_available)
+ @md5_available = begin
+ require "openssl"
+ ::OpenSSL::Digest.digest("MD5", "")
+ true
+ rescue LoadError
+ true
+ rescue ::OpenSSL::Digest::DigestError
+ false
+ end
+ end
+
+ def digest(name)
+ require "digest"
+ Digest(name)
+ end
+
+ def checksum_for_file(path, digest)
+ return unless path.file?
+ # This must use File.read instead of Digest.file().hexdigest
+ # because we need to preserve \n line endings on windows when calculating
+ # the checksum
+ SharedHelpers.filesystem_access(path, :read) do
+ File.open(path, "rb") do |f|
+ digest = SharedHelpers.digest(digest).new
+ buf = String.new(capacity: 16_384, encoding: Encoding::BINARY)
+ digest << buf while f.read(16_384, buf)
+ digest.hexdigest
+ end
+ end
+ end
+
+ def write_to_gemfile(gemfile_path, contents)
+ filesystem_access(gemfile_path) {|g| File.open(g, "w") {|file| file.puts contents } }
+ end
+
+ def relative_gemfile_path
+ relative_path_to(Bundler.default_gemfile)
+ end
+
+ def relative_lockfile_path
+ relative_path_to(Bundler.default_lockfile)
+ end
+
+ def relative_path_to(destination, from: pwd)
+ Pathname.new(destination).relative_path_from(from).to_s
+ rescue ArgumentError
+ # on Windows, if source and destination are on different drivers, there's no relative path from one to the other
+ destination
+ end
+
+ private
+
+ def validate_bundle_path
+ path_separator = Bundler.rubygems.path_separator
+ return unless Bundler.bundle_path.to_s.split(path_separator).size > 1
+ message = "Your bundle path contains text matching #{path_separator.inspect}, " \
+ "which is the path separator for your system. Bundler cannot " \
+ "function correctly when the Bundle path contains the " \
+ "system's PATH separator. Please change your " \
+ "bundle path to not match #{path_separator.inspect}." \
+ "\nYour current bundle path is '#{Bundler.bundle_path}'."
+ raise Bundler::PathError, message
+ end
+
+ def find_gemfile
+ given = ENV["BUNDLE_GEMFILE"]
+ return given if given && !given.empty?
+ find_file(*gemfile_names)
+ end
+
+ def gemfile_names
+ ["gems.rb", "Gemfile"]
+ end
+
+ def find_file(*names)
+ search_up(*names) do |filename|
+ return filename if File.file?(filename)
+ end
+ end
+
+ def find_directory(*names)
+ search_up(*names) do |dirname|
+ return dirname if File.directory?(dirname)
+ end
+ end
+
+ def search_up(*names)
+ previous = nil
+ current = File.expand_path(SharedHelpers.pwd)
+
+ until !File.directory?(current) || current == previous
+ if ENV["BUNDLER_SPEC_RUN"]
+ # avoid stepping above the tmp directory when testing
+ return nil if File.directory?(File.join(current, "tmp"))
+ end
+
+ names.each do |name|
+ filename = File.join(current, name)
+ yield filename
+ end
+ previous = current
+ current = File.expand_path("..", current)
+ end
+ end
+
+ def set_env(key, value)
+ raise ArgumentError, "new key #{key}" unless EnvironmentPreserver::BUNDLER_KEYS.include?(key)
+ orig_key = "#{EnvironmentPreserver::BUNDLER_PREFIX}#{key}"
+ orig = ENV[key]
+ orig ||= EnvironmentPreserver::INTENTIONALLY_NIL
+ ENV[orig_key] ||= orig
+
+ ENV[key] = value
+ end
+ public :set_env
+
+ def set_bundle_variables
+ Bundler::SharedHelpers.set_env "BUNDLE_BIN_PATH", bundle_bin_path
+ Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", find_gemfile.to_s
+ Bundler::SharedHelpers.set_env "BUNDLE_LOCKFILE", default_lockfile.to_s
+ Bundler::SharedHelpers.set_env "BUNDLER_VERSION", Bundler::VERSION
+ Bundler::SharedHelpers.set_env "BUNDLER_SETUP", File.expand_path("setup", __dir__)
+ end
+
+ def bundle_bin_path
+ # bundler exe & lib folders have same root folder, typical gem installation
+ exe_file = File.join(source_root, "exe/bundle")
+
+ # for Ruby core repository testing
+ exe_file = File.join(source_root, "libexec/bundle") unless File.exist?(exe_file)
+
+ # bundler is a default gem, exe path is separate
+ exe_file = Gem.bin_path("bundler", "bundle", VERSION) unless File.exist?(exe_file)
+
+ exe_file
+ end
+ public :bundle_bin_path
+
+ def gemspec_path
+ # inside a gem repository, typical gem installation
+ gemspec_file = File.join(source_root, "../../specifications/bundler-#{VERSION}.gemspec")
+
+ # for Ruby core repository testing
+ gemspec_file = File.expand_path("bundler.gemspec", __dir__) unless File.exist?(gemspec_file)
+
+ # bundler is a default gem
+ gemspec_file = File.join(Gem.default_specifications_dir, "bundler-#{VERSION}.gemspec") unless File.exist?(gemspec_file)
+
+ gemspec_file
+ end
+ public :gemspec_path
+
+ def source_root
+ File.expand_path("../..", __dir__)
+ end
+
+ def set_path
+ validate_bundle_path
+ paths = (ENV["PATH"] || "").split(File::PATH_SEPARATOR)
+ paths.unshift "#{Bundler.bundle_path}/bin"
+ Bundler::SharedHelpers.set_env "PATH", paths.uniq.join(File::PATH_SEPARATOR)
+ end
+
+ def set_rubyopt
+ rubyopt = [ENV["RUBYOPT"]].compact
+ setup_require = "-r#{File.expand_path("setup", __dir__)}"
+ return if !rubyopt.empty? && rubyopt.first.include?(setup_require)
+ rubyopt.unshift setup_require
+ Bundler::SharedHelpers.set_env "RUBYOPT", rubyopt.join(" ")
+ end
+
+ def set_rubylib
+ rubylib = (ENV["RUBYLIB"] || "").split(File::PATH_SEPARATOR)
+ rubylib.unshift bundler_ruby_lib unless RbConfig::CONFIG["rubylibdir"] == bundler_ruby_lib
+ Bundler::SharedHelpers.set_env "RUBYLIB", rubylib.uniq.join(File::PATH_SEPARATOR)
+ end
+
+ def bundler_ruby_lib
+ File.expand_path("..", __dir__)
+ end
+
+ def clean_load_path
+ loaded_gem_paths = Bundler.rubygems.loaded_gem_paths
+
+ $LOAD_PATH.reject! do |p|
+ resolved_path = resolve_path(p)
+ next if $LOADED_FEATURES.any? {|lf| lf.start_with?(resolved_path) }
+ loaded_gem_paths.delete(p)
+ end
+ $LOAD_PATH.uniq!
+ end
+
+ def resolve_path(path)
+ expanded = File.expand_path(path)
+ return expanded unless File.exist?(expanded)
+
+ File.realpath(expanded)
+ end
+
+ def prints_major_deprecations?
+ return false if Bundler.settings[:silence_deprecations]
+ require_relative "deprecate"
+ return false if Bundler::Deprecate.skip
+ true
+ end
+
+ extend self
+ end
+end
diff --git a/lib/bundler/source.rb b/lib/bundler/source.rb
new file mode 100644
index 0000000000..cf71be8801
--- /dev/null
+++ b/lib/bundler/source.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Source
+ autoload :Gemspec, File.expand_path("source/gemspec", __dir__)
+ autoload :Git, File.expand_path("source/git", __dir__)
+ autoload :Metadata, File.expand_path("source/metadata", __dir__)
+ autoload :Path, File.expand_path("source/path", __dir__)
+ autoload :Rubygems, File.expand_path("source/rubygems", __dir__)
+ autoload :RubygemsAggregate, File.expand_path("source/rubygems_aggregate", __dir__)
+
+ attr_accessor :dependency_names
+
+ attr_reader :checksum_store
+
+ def unmet_deps
+ specs.unmet_dependency_names
+ end
+
+ def version_message(spec, locked_spec = nil)
+ message = "#{spec.name} #{spec.version}"
+ message += " (#{spec.platform})" if spec.platform != Gem::Platform::RUBY && !spec.platform.nil?
+
+ if locked_spec
+ locked_spec_version = locked_spec.version
+ if locked_spec_version && spec.version != locked_spec_version
+ message += Bundler.ui.add_color(" (was #{locked_spec_version})", version_color(spec.version, locked_spec_version))
+ end
+ end
+
+ message
+ end
+
+ def download(*); end
+
+ def can_lock?(spec)
+ spec.source == self
+ end
+
+ def prefer_local!; end
+
+ def local!; end
+
+ def local_only!; end
+
+ def cached!; end
+
+ def remote!; end
+
+ def add_dependency_names(names)
+ @dependency_names = Array(dependency_names) | Array(names)
+ end
+
+ # it's possible that gems from one source depend on gems from some
+ # other source, so now we download gemspecs and iterate over those
+ # dependencies, looking for gems we don't have info on yet.
+ def double_check_for(*); end
+
+ def dependency_names_to_double_check
+ specs.dependency_names
+ end
+
+ def spec_names
+ specs.spec_names
+ end
+
+ def include?(other)
+ other == self
+ end
+
+ def inspect
+ "#<#{self.class}:0x#{object_id} #{self}>"
+ end
+
+ def identifier
+ to_s
+ end
+
+ def path?
+ instance_of?(Bundler::Source::Path)
+ end
+
+ def extension_cache_path(spec)
+ return unless Bundler.settings[:global_gem_cache]
+ return unless source_slug = extension_cache_slug(spec)
+ Bundler.user_cache.join(
+ "extensions", Gem::Platform.local.to_s, Bundler.ruby_scope,
+ source_slug, spec.full_name
+ )
+ end
+
+ private
+
+ def version_color(spec_version, locked_spec_version)
+ if Gem::Version.correct?(spec_version) && Gem::Version.correct?(locked_spec_version)
+ # display yellow if there appears to be a regression
+ earlier_version?(spec_version, locked_spec_version) ? :yellow : :green
+ else
+ # default to green if the versions cannot be directly compared
+ :green
+ end
+ end
+
+ def earlier_version?(spec_version, locked_spec_version)
+ Gem::Version.new(spec_version) < Gem::Version.new(locked_spec_version)
+ end
+
+ def print_using_message(message)
+ if !message.include?("(was ")
+ Bundler.ui.debug message
+ else
+ Bundler.ui.info message
+ end
+ end
+
+ def extension_cache_slug(_)
+ nil
+ end
+ end
+end
diff --git a/lib/bundler/source/gemspec.rb b/lib/bundler/source/gemspec.rb
new file mode 100644
index 0000000000..ed766dbe74
--- /dev/null
+++ b/lib/bundler/source/gemspec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Source
+ class Gemspec < Path
+ attr_reader :gemspec
+ attr_writer :checksum_store
+
+ def initialize(options)
+ super
+ @gemspec = options["gemspec"]
+ end
+
+ def to_s
+ "gemspec at `#{@path}`"
+ end
+ end
+ end
+end
diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb
new file mode 100644
index 0000000000..a002a2570a
--- /dev/null
+++ b/lib/bundler/source/git.rb
@@ -0,0 +1,456 @@
+# frozen_string_literal: true
+
+require_relative "../vendored_fileutils"
+
+module Bundler
+ class Source
+ class Git < Path
+ autoload :GitProxy, File.expand_path("git/git_proxy", __dir__)
+
+ attr_reader :uri, :ref, :branch, :options, :glob, :submodules
+
+ def initialize(options)
+ @options = options
+ @checksum_store = Checksum::Store.new
+ @glob = options["glob"] || DEFAULT_GLOB
+
+ @allow_cached = false
+ @allow_remote = false
+
+ # Stringify options that could be set as symbols
+ %w[ref branch tag revision].each {|k| options[k] = options[k].to_s if options[k] }
+
+ @uri = URINormalizer.normalize_suffix(options["uri"] || "", trailing_slash: false)
+ @safe_uri = URICredentialsFilter.credential_filtered_uri(@uri)
+ @branch = options["branch"]
+ @ref = options["ref"] || options["branch"] || options["tag"]
+ @submodules = options["submodules"]
+ @name = options["name"]
+ @version = options["version"].to_s.strip.gsub("-", ".pre.")
+
+ @copied = false
+ @local = false
+ end
+
+ def remote!
+ return if @allow_remote
+
+ @local_specs = nil
+ @allow_remote = true
+ end
+
+ def cached!
+ return if @allow_cached
+
+ @local_specs = nil
+ @allow_cached = true
+ end
+
+ def self.from_lock(options)
+ new(options.merge("uri" => options.delete("remote")))
+ end
+
+ def to_lock
+ out = String.new("GIT\n")
+ out << " remote: #{@uri}\n"
+ out << " revision: #{revision}\n"
+ %w[ref branch tag submodules].each do |opt|
+ out << " #{opt}: #{options[opt]}\n" if options[opt]
+ end
+ out << " glob: #{@glob}\n" unless default_glob?
+ out << " specs:\n"
+ end
+
+ def to_gemfile
+ specifiers = %w[ref branch tag submodules glob].map do |opt|
+ "#{opt}: #{options[opt]}" if options[opt]
+ end
+
+ uri_with_specifiers(specifiers)
+ end
+
+ def hash
+ [self.class, uri, ref, branch, name, glob, submodules].hash
+ end
+
+ def eql?(other)
+ other.is_a?(Git) && uri == other.uri && ref == other.ref &&
+ branch == other.branch && name == other.name &&
+ glob == other.glob &&
+ submodules == other.submodules
+ end
+
+ alias_method :==, :eql?
+
+ def include?(other)
+ other.is_a?(Git) && uri == other.uri &&
+ name == other.name &&
+ glob == other.glob &&
+ submodules == other.submodules
+ end
+
+ def to_s
+ begin
+ at = humanized_ref || current_branch
+
+ rev = "at #{at}@#{shortref_for_display(revision)}"
+ rescue GitError
+ ""
+ end
+
+ uri_with_specifiers([rev, glob_for_display])
+ end
+
+ def identifier
+ uri_with_specifiers([humanized_ref, locked_revision, glob_for_display])
+ end
+
+ def uri_with_specifiers(specifiers)
+ specifiers.compact!
+
+ suffix =
+ if specifiers.any?
+ " (#{specifiers.join(", ")})"
+ else
+ ""
+ end
+
+ "#{@safe_uri}#{suffix}"
+ end
+
+ def name
+ File.basename(@uri, ".git")
+ end
+
+ # This is the path which is going to contain a specific
+ # checkout of the git repository. When using local git
+ # repos, this is set to the local repo.
+ def install_path
+ @install_path ||= begin
+ git_scope = "#{base_name}-#{shortref_for_path(revision)}"
+
+ Bundler.install_path.join(git_scope)
+ end
+ end
+
+ alias_method :path, :install_path
+
+ def extension_dir_name
+ "#{base_name}-#{shortref_for_path(revision)}"
+ end
+
+ def unlock!
+ git_proxy.revision = nil
+ options["revision"] = nil
+
+ @unlocked = true
+ end
+
+ def local_override!(path)
+ return false if local?
+
+ original_path = path
+ path = Pathname.new(path)
+ path = path.expand_path(Bundler.root) unless path.relative?
+
+ unless branch || Bundler.settings[:disable_local_branch_check]
+ raise GitError, "Cannot use local override for #{name} at #{path} because " \
+ ":branch is not specified in Gemfile. Specify a branch or run " \
+ "`bundle config unset local.#{override_for(original_path)}` to remove the local override"
+ end
+
+ unless path.exist?
+ raise GitError, "Cannot use local override for #{name} because #{path} " \
+ "does not exist. Run `bundle config unset local.#{override_for(original_path)}` to remove the local override"
+ end
+
+ @local = true
+ set_paths!(path)
+
+ # Create a new git proxy without the cached revision
+ # so the Gemfile.lock always picks up the new revision.
+ @git_proxy = GitProxy.new(path, uri, options)
+
+ if current_branch != branch && !Bundler.settings[:disable_local_branch_check]
+ raise GitError, "Local override for #{name} at #{path} is using branch " \
+ "#{current_branch} but Gemfile specifies #{branch}"
+ end
+
+ changed = locked_revision && locked_revision != revision
+
+ if !Bundler.settings[:disable_local_revision_check] && changed && !@unlocked && !git_proxy.contains?(locked_revision)
+ raise GitError, "The Gemfile lock is pointing to revision #{shortref_for_display(locked_revision)} " \
+ "but the current branch in your local override for #{name} does not contain such commit. " \
+ "Please make sure your branch is up to date."
+ end
+
+ changed
+ end
+
+ def specs(*)
+ set_cache_path!(app_cache_path) if use_app_cache?
+
+ if requires_checkout? && !@copied
+ Plugin.hook(Plugin::Events::GIT_BEFORE_FETCH, self)
+ begin
+ fetch unless use_app_cache?
+ checkout
+ ensure
+ Plugin.hook(Plugin::Events::GIT_AFTER_FETCH, self)
+ end
+ end
+
+ local_specs
+ end
+
+ def install(spec, options = {})
+ return if Bundler.settings[:no_install]
+ force = options[:force]
+
+ print_using_message "Using #{version_message(spec, options[:previous_spec])} from #{self}"
+
+ if (requires_checkout? && !@copied) || force
+ checkout
+ end
+
+ generate_bin_options = { disable_extensions: !spec.missing_extensions?, build_args: options[:build_args] }
+ generate_bin(spec, generate_bin_options)
+
+ requires_checkout? ? spec.post_install_message : nil
+ end
+
+ def migrate_cache(custom_path = nil, local: false)
+ if local
+ cache_to(custom_path, try_migrate: false)
+ else
+ cache_to(custom_path, try_migrate: true)
+ end
+ end
+
+ def cache(spec, custom_path = nil)
+ cache_to(custom_path, try_migrate: false)
+ end
+
+ def load_spec_files
+ super
+ rescue PathError => e
+ Bundler.ui.trace e
+ raise GitError, "#{self} is not yet checked out. Run `bundle install` first."
+ end
+
+ # This is the path which is going to contain a cache
+ # of the git repository. When using the same git repository
+ # across different projects, this cache will be shared.
+ # When using local git repos, this is set to the local repo.
+ def cache_path
+ @cache_path ||= if Bundler.settings[:global_gem_cache]
+ Bundler.user_cache
+ else
+ Bundler.bundle_path.join("cache", "bundler")
+ end.join("git", git_scope)
+ end
+
+ def app_cache_dirname
+ "#{base_name}-#{shortref_for_path(locked_revision || revision)}"
+ end
+
+ def revision
+ git_proxy.revision
+ end
+
+ def current_branch
+ git_proxy.current_branch
+ end
+
+ def allow_git_ops?
+ @allow_remote || @allow_cached
+ end
+
+ def local?
+ @local
+ end
+
+ private
+
+ def cache_to(custom_path, try_migrate: false)
+ return unless Bundler.settings[:cache_all]
+
+ app_cache_path = app_cache_path(custom_path)
+
+ migrate = try_migrate ? bare_repo?(app_cache_path) : false
+
+ set_cache_path!(nil) if migrate
+
+ return if cache_path == app_cache_path
+
+ cached!
+ FileUtils.rm_rf(app_cache_path)
+ git_proxy.checkout if migrate || requires_checkout?
+ git_proxy.copy_to(app_cache_path, @submodules)
+ serialize_gemspecs_in(app_cache_path)
+ end
+
+ def checkout
+ Bundler.ui.debug " * Checking out revision: #{ref}"
+ if use_app_cache? && !bare_repo?(app_cache_path)
+ SharedHelpers.filesystem_access(install_path.dirname) do |p|
+ FileUtils.mkdir_p(p)
+ end
+ FileUtils.cp_r("#{app_cache_path}/.", install_path)
+ else
+ if use_app_cache? && bare_repo?(app_cache_path)
+ Bundler.ui.warn "Installing from cache in old \"bare repository\" format for compatibility. " \
+ "Please run `bundle cache` and commit the updated cache to migrate to the new format and get rid of this warning."
+ end
+
+ git_proxy.copy_to(install_path, submodules)
+ end
+ serialize_gemspecs_in(install_path)
+ @copied = true
+ end
+
+ def humanized_ref
+ if local?
+ path
+ elsif user_ref = options["ref"]
+ if /\A[a-z0-9]{4,}\z/i.match?(ref)
+ shortref_for_display(user_ref)
+ else
+ user_ref
+ end
+ elsif ref
+ ref
+ end
+ end
+
+ def serialize_gemspecs_in(destination)
+ destination = destination.expand_path(Bundler.root) if destination.relative?
+ Dir["#{destination}/#{@glob}"].each do |spec_path|
+ # Evaluate gemspecs and cache the result. Gemspecs
+ # in git might require git or other dependencies.
+ # The gemspecs we cache should already be evaluated.
+ spec = Bundler.load_gemspec(spec_path)
+ next unless spec
+ spec.installed_by_version = Gem::VERSION
+ Bundler.rubygems.validate(spec)
+ File.open(spec_path, "wb") {|file| file.write(spec.to_ruby) }
+ end
+ end
+
+ def set_paths!(path)
+ set_cache_path!(path)
+ set_install_path!(path)
+ end
+
+ def set_cache_path!(path)
+ @git_proxy = nil
+ @cache_path = path
+ end
+
+ def set_install_path!(path)
+ @local_specs = nil
+ @install_path = path
+ end
+
+ def has_app_cache?
+ locked_revision && super
+ end
+
+ def use_app_cache?
+ has_app_cache? && !local?
+ end
+
+ def requires_checkout?
+ allow_git_ops? && !local? && !locked_revision_checked_out?
+ end
+
+ def locked_revision_checked_out?
+ locked_revision && locked_revision == revision && installed?
+ end
+
+ def installed?
+ git_proxy.installed_to?(install_path)
+ end
+
+ def base_name
+ File.basename(uri.sub(%r{^(\w+://)?([^/:]+:)?(//\w*/)?(\w*/)*}, ""), ".git")
+ end
+
+ def shortref_for_display(ref)
+ ref[0..6]
+ end
+
+ def shortref_for_path(ref)
+ ref[0..11]
+ end
+
+ def glob_for_display
+ default_glob? ? nil : "glob: #{@glob}"
+ end
+
+ def default_glob?
+ @glob == DEFAULT_GLOB
+ end
+
+ def uri_hash
+ if %r{^\w+://(\w+@)?}.match?(uri)
+ # Downcase the domain component of the URI
+ # and strip off a trailing slash, if one is present
+ input = Gem::URI.parse(uri).normalize.to_s.sub(%r{/$}, "")
+ else
+ # If there is no URI scheme, assume it is an ssh/git URI
+ input = uri
+ end
+ # We use SHA1 here for historical reason and to preserve backward compatibility.
+ # But a transition to a simpler mangling algorithm would be welcome.
+ Bundler::Digest.sha1(input)
+ end
+
+ def locked_revision
+ options["revision"]
+ end
+
+ def cached?
+ cache_path.exist?
+ end
+
+ def git_proxy
+ @git_proxy ||= GitProxy.new(cache_path, uri, options, locked_revision, self)
+ end
+
+ def fetch
+ git_proxy.checkout
+ rescue GitError => e
+ Bundler.ui.warn "Using cached git data because of network errors:\n#{e}"
+ end
+
+ # no-op, since we validate when re-serializing the gemspec
+ def validate_spec(_spec); end
+
+ def load_gemspec(file)
+ dirname = Pathname.new(file).dirname
+ SharedHelpers.chdir(dirname.to_s) do
+ stub = Gem::StubSpecification.gemspec_stub(file, install_path.parent, install_path.parent)
+ stub.full_gem_path = dirname.expand_path(root).to_s
+ StubSpecification.from_stub(stub)
+ end
+ end
+
+ def git_scope
+ "#{base_name}-#{uri_hash}"
+ end
+
+ def extension_cache_slug(_)
+ extension_dir_name
+ end
+
+ def override_for(path)
+ Bundler.settings.local_overrides.key(path)
+ end
+
+ def bare_repo?(path)
+ File.exist?(path.join("objects")) && File.exist?(path.join("HEAD"))
+ end
+ end
+ end
+end
diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb
new file mode 100644
index 0000000000..8094dcaa9d
--- /dev/null
+++ b/lib/bundler/source/git/git_proxy.rb
@@ -0,0 +1,503 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Source
+ class Git
+ class GitNotInstalledError < GitError
+ def initialize
+ msg = String.new
+ msg << "You need to install git to be able to use gems from git repositories. "
+ msg << "For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git"
+ super msg
+ end
+ end
+
+ class GitNotAllowedError < GitError
+ def initialize(command)
+ msg = String.new
+ msg << "Bundler is trying to run `#{command}` at runtime. You probably need to run `bundle install`. However, "
+ msg << "this error message could probably be more useful. Please submit a ticket at https://github.com/ruby/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md "
+ msg << "with steps to reproduce as well as the following\n\nCALLER: #{caller.join("\n")}"
+ super msg
+ end
+ end
+
+ class GitCommandError < GitError
+ attr_reader :command
+
+ def initialize(command, path, extra_info = nil)
+ @command = command
+
+ msg = String.new("Git error: command `#{command}`")
+ msg << " in directory #{path}" if path
+ msg << " has failed."
+ msg << "\n#{extra_info}" if extra_info
+ super msg
+ end
+ end
+
+ class MissingGitRevisionError < GitCommandError
+ def initialize(command, destination_path, ref, repo)
+ msg = "Revision #{ref} does not exist in the repository #{repo}. Maybe you misspelled it?"
+ super command, destination_path, msg
+ end
+ end
+
+ class AmbiguousGitReference < GitError
+ def initialize(options)
+ msg = "Specification of branch or ref with tag is ambiguous. You specified #{options.inspect}"
+ super msg
+ end
+ end
+
+ # The GitProxy is responsible to interact with git repositories.
+ # All actions required by the Git source is encapsulated in this
+ # object.
+ class GitProxy
+ attr_accessor :path, :uri, :branch, :tag, :ref, :explicit_ref
+ attr_writer :revision
+
+ def self.version
+ @version ||= full_version[/((\.?\d+)+).*/, 1]
+ end
+
+ def self.full_version
+ @full_version ||= begin
+ raise GitNotInstalledError.new unless Bundler.git_present?
+
+ require "open3"
+ out, err, status = Open3.capture3("git", "--version")
+
+ raise GitCommandError.new("--version", SharedHelpers.pwd, err) unless status.success?
+ Bundler.ui.warn err unless err.empty?
+
+ out.sub(/git version\s*/, "").strip
+ end
+ end
+
+ def self.reset
+ @version = nil
+ @full_version = nil
+ end
+
+ def initialize(path, uri, options = {}, revision = nil, git = nil)
+ @path = path
+ @uri = uri
+ @tag = options["tag"]
+ @branch = options["branch"]
+ @ref = options["ref"]
+ if @tag
+ raise AmbiguousGitReference.new(options) if @branch || @ref
+ @explicit_ref = @tag
+ else
+ @explicit_ref = @ref || @branch
+ end
+ @revision = revision
+ @git = git
+ @commit_ref = nil
+ end
+
+ def revision
+ @revision ||= allowed_with_path { find_local_revision }
+ end
+
+ def current_branch
+ @current_branch ||= with_path do
+ git_local("rev-parse", "--abbrev-ref", "HEAD", dir: path).strip
+ end
+ end
+
+ def contains?(commit)
+ allowed_with_path do
+ result, status = git_null("branch", "--contains", commit, dir: path)
+ status.success? && result.match?(/^\* (.*)$/)
+ end
+ end
+
+ def version
+ self.class.version
+ end
+
+ def full_version
+ self.class.full_version
+ end
+
+ def checkout
+ return if has_revision_cached?
+
+ Bundler.ui.info "Fetching #{credential_filtered_uri}"
+
+ extra_fetch_needed = clone_needs_extra_fetch?
+ unshallow_needed = clone_needs_unshallow?
+ return unless extra_fetch_needed || unshallow_needed
+
+ git_remote_fetch(unshallow_needed ? ["--unshallow"] : depth_args)
+ end
+
+ def copy_to(destination, submodules = false)
+ unless File.exist?(destination.join(".git"))
+ begin
+ SharedHelpers.filesystem_access(destination.dirname) do |p|
+ FileUtils.mkdir_p(p)
+ end
+ SharedHelpers.filesystem_access(destination) do |p|
+ FileUtils.rm_rf(p)
+ end
+ git "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s
+ File.chmod((File.stat(destination).mode | 0o777) & ~File.umask, destination)
+ rescue Errno::EEXIST => e
+ file_path = e.message[%r{.*?((?:[a-zA-Z]:)?/.*)}, 1]
+ raise GitError, "Bundler could not install a gem because it needs to " \
+ "create a directory, but a file exists - #{file_path}. Please delete " \
+ "this file and try again."
+ end
+ end
+
+ ref = @commit_ref || (locked_to_full_sha? && @revision)
+ if ref
+ git "config", "uploadpack.allowAnySHA1InWant", "true", dir: path.to_s if @commit_ref.nil? && needs_allow_any_sha1_in_want?
+
+ git "fetch", "--force", "--quiet", *extra_fetch_args(ref), dir: destination
+ end
+
+ git "reset", "--hard", revision, dir: destination
+
+ if submodules
+ git_retry "submodule", "update", "--init", "--recursive", dir: destination
+ elsif Gem::Version.create(version) >= Gem::Version.create("2.9.0")
+ inner_command = "git -C $toplevel submodule deinit --force $sm_path"
+ git_retry "submodule", "foreach", "--quiet", inner_command, dir: destination
+ end
+ end
+
+ def installed_to?(destination)
+ # if copy_to is interrupted, it may leave a partially installed directory that
+ # contains .git but no other files -- consider this not to be installed
+ Dir.exist?(destination) && (Dir.children(destination) - [".git"]).any?
+ end
+
+ private
+
+ def git_remote_fetch(args)
+ command = fetch_command(args)
+ command_with_no_credentials = check_allowed(command)
+
+ Bundler::Retry.new("`#{command_with_no_credentials}` at #{path}", [MissingGitRevisionError]).attempts do
+ out, err, status = capture(command, path)
+ return out if status.success?
+
+ if err.include?("couldn't find remote ref") || err.include?("not our ref")
+ raise MissingGitRevisionError.new(command_with_no_credentials, path, commit || explicit_ref, credential_filtered_uri)
+ else
+ if shallow?
+ args -= depth_args
+ command = fetch_command(args)
+ command_with_no_credentials = check_allowed(command)
+ end
+ raise GitCommandError.new(command_with_no_credentials, path, err)
+ end
+ end
+ end
+
+ def clone_needs_extra_fetch?
+ return true if path.exist?
+
+ SharedHelpers.filesystem_access(path.dirname) do |p|
+ FileUtils.mkdir_p(p)
+ end
+
+ clone_args = extra_clone_args
+ command = clone_command(clone_args)
+ command_with_no_credentials = check_allowed(command)
+
+ Bundler::Retry.new("`#{command_with_no_credentials}`", [MissingGitRevisionError]).attempts do
+ _, err, status = capture(command, nil)
+ return extra_ref if status.success?
+
+ if err.include?("Could not find remote branch") || # git up to 2.49
+ err.include?("Remote branch #{branch_option} not found") # git 2.49 or higher
+ raise MissingGitRevisionError.new(command_with_no_credentials, nil, explicit_ref, credential_filtered_uri)
+ else
+ if shallow?
+ clone_args -= depth_args
+ command = clone_command(clone_args)
+ command_with_no_credentials = check_allowed(command)
+ end
+ raise GitCommandError.new(command_with_no_credentials, path, err)
+ end
+ end
+ end
+
+ def clone_needs_unshallow?
+ return false unless path.join("shallow").exist?
+ return true unless shallow?
+
+ @revision && @revision != head_revision
+ end
+
+ def extra_ref
+ return false if not_pinned?
+ return true if shallow?
+
+ ref.start_with?("refs/")
+ end
+
+ def depth
+ return @depth if defined?(@depth)
+
+ @depth = if !supports_fetching_unreachable_refs?
+ nil
+ elsif not_pinned? || pinned_to_full_sha?
+ 1
+ elsif ref.include?("~")
+ parsed_depth = ref.split("~").last
+ parsed_depth.to_i + 1
+ end
+ end
+
+ def refspec
+ if commit
+ @commit_ref = "refs/#{commit}-sha"
+ return "#{commit}:#{@commit_ref}"
+ end
+
+ reference = fully_qualified_ref
+
+ reference ||= if ref.include?("~")
+ ref.split("~").first
+ elsif ref.start_with?("refs/")
+ ref
+ else
+ "refs/*"
+ end
+
+ "#{reference}:#{reference}"
+ end
+
+ def commit
+ @commit ||= pinned_to_full_sha? ? ref : @revision
+ end
+
+ def fully_qualified_ref
+ if branch
+ "refs/heads/#{branch}"
+ elsif tag
+ "refs/tags/#{tag}"
+ elsif ref.nil?
+ "refs/heads/#{current_branch}"
+ end
+ end
+
+ def not_pinned?
+ branch_option || ref.nil?
+ end
+
+ def pinned_to_full_sha?
+ full_sha_revision?(ref)
+ end
+
+ def locked_to_full_sha?
+ full_sha_revision?(@revision)
+ end
+
+ def full_sha_revision?(ref)
+ ref&.match?(/\A\h{40}\z/)
+ end
+
+ def git_null(*command, dir: nil)
+ check_allowed(command)
+
+ capture(command, dir, ignore_err: true)
+ end
+
+ def git_retry(*command, dir: nil)
+ command_with_no_credentials = check_allowed(command)
+
+ Bundler::Retry.new("`#{command_with_no_credentials}` at #{dir || SharedHelpers.pwd}").attempts do
+ git(*command, dir: dir)
+ end
+ end
+
+ def git(*command, dir: nil)
+ run_command(*command, dir: dir) do |unredacted_command|
+ check_allowed(unredacted_command)
+ end
+ end
+
+ def git_local(*command, dir: nil)
+ run_command(*command, dir: dir) do |unredacted_command|
+ redact_and_check_presence(unredacted_command)
+ end
+ end
+
+ def has_revision_cached?
+ return unless commit && path.exist?
+ git("cat-file", "-e", commit, dir: path)
+ true
+ rescue GitError
+ false
+ end
+
+ def find_local_revision
+ return head_revision if explicit_ref.nil?
+
+ find_revision_for(explicit_ref)
+ end
+
+ def head_revision
+ verify("HEAD")
+ end
+
+ def find_revision_for(reference)
+ verify(reference)
+ rescue GitCommandError => e
+ raise MissingGitRevisionError.new(e.command, path, reference, credential_filtered_uri)
+ end
+
+ def verify(reference)
+ git("rev-parse", "--verify", reference, dir: path).strip
+ end
+
+ # Adds credentials to the URI
+ def configured_uri
+ if /https?:/.match?(uri)
+ remote = Gem::URI(uri)
+ config_auth = Bundler.settings[remote.to_s] || Bundler.settings[remote.host]
+ remote.userinfo ||= config_auth
+ remote.to_s
+ else
+ uri.to_s
+ end
+ end
+
+ # Removes credentials from the URI
+ def credential_filtered_uri
+ URICredentialsFilter.credential_filtered_uri(uri)
+ end
+
+ def allow?
+ allowed = @git ? @git.allow_git_ops? : true
+
+ raise GitNotInstalledError.new if allowed && !Bundler.git_present?
+
+ allowed
+ end
+
+ def with_path(&blk)
+ checkout unless path.exist?
+ blk.call
+ end
+
+ def allowed_with_path
+ return with_path { yield } if allow?
+ raise GitError, "The git source #{uri} is not yet checked out. Please run `bundle install` before trying to start your application"
+ end
+
+ def check_allowed(command)
+ command_with_no_credentials = redact_and_check_presence(command)
+ raise GitNotAllowedError.new(command_with_no_credentials) unless allow?
+ command_with_no_credentials
+ end
+
+ def redact_and_check_presence(command)
+ raise GitNotInstalledError.new unless Bundler.git_present?
+
+ require "shellwords"
+ URICredentialsFilter.credential_filtered_string("git #{command.shelljoin}", uri)
+ end
+
+ def run_command(*command, dir: nil)
+ command_with_no_credentials = yield(command)
+
+ out, err, status = capture(command, dir)
+
+ raise GitCommandError.new(command_with_no_credentials, dir || SharedHelpers.pwd, err) unless status.success?
+
+ Bundler.ui.warn err unless err.empty?
+
+ out
+ end
+
+ def capture(cmd, dir, ignore_err: false)
+ SharedHelpers.with_clean_git_env do
+ require "open3"
+ out, err, status = Open3.capture3(*capture3_args_for(cmd, dir))
+
+ filtered_out = URICredentialsFilter.credential_filtered_string(out, uri)
+ return [filtered_out, status] if ignore_err
+
+ filtered_err = URICredentialsFilter.credential_filtered_string(err, uri)
+ [filtered_out, filtered_err, status]
+ end
+ end
+
+ def capture3_args_for(cmd, dir)
+ # Disable automatic maintenance so a background commit-graph write in
+ # the source repo can't race the hardlinking local clone and fail with
+ # "hardlink different from source".
+ opts = ["-c", "gc.auto=0", "-c", "maintenance.auto=false"]
+
+ return ["git", *opts, *cmd] unless dir
+
+ ["git", "-C", dir.to_s, *opts, *cmd]
+ end
+
+ def extra_clone_args
+ args = depth_args
+ return [] if args.empty?
+
+ args += ["--single-branch"]
+ args.unshift("--no-tags") if supports_cloning_with_no_tags?
+
+ # If there's a locked revision, no need to clone any specific branch
+ # or tag, since we will end up checking out that locked revision
+ # anyways.
+ return args if @revision
+
+ args += ["--branch", branch_option] if branch_option
+ args
+ end
+
+ def fetch_command(args)
+ ["fetch", "--force", "--quiet", "--no-tags", *args, "--", configured_uri, refspec].compact
+ end
+
+ def clone_command(args)
+ ["clone", "--bare", "--no-hardlinks", "--quiet", *args, "--", configured_uri, path.to_s]
+ end
+
+ def depth_args
+ return [] unless shallow?
+
+ ["--depth", depth.to_s]
+ end
+
+ def extra_fetch_args(ref)
+ extra_args = [path.to_s, *depth_args]
+ extra_args.push(ref)
+ extra_args
+ end
+
+ def branch_option
+ branch || tag
+ end
+
+ def shallow?
+ !depth.nil?
+ end
+
+ def needs_allow_any_sha1_in_want?
+ @needs_allow_any_sha1_in_want ||= Gem::Version.new(version) <= Gem::Version.new("2.13.7")
+ end
+
+ def supports_fetching_unreachable_refs?
+ @supports_fetching_unreachable_refs ||= Gem::Version.new(version) >= Gem::Version.new("2.5.0")
+ end
+
+ def supports_cloning_with_no_tags?
+ @supports_cloning_with_no_tags ||= Gem::Version.new(version) >= Gem::Version.new("2.14.0-rc0")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/source/metadata.rb b/lib/bundler/source/metadata.rb
new file mode 100644
index 0000000000..ecf8895187
--- /dev/null
+++ b/lib/bundler/source/metadata.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Source
+ class Metadata < Source
+ def specs
+ @specs ||= Index.build do |idx|
+ idx << Gem::Specification.new("Ruby\0", Bundler::RubyVersion.system.gem_version)
+ idx << Gem::Specification.new("RubyGems\0", Gem::VERSION) do |s|
+ s.required_rubygems_version = Gem::Requirement.default
+ end
+
+ if local_spec = Gem.loaded_specs["bundler"]
+ raise CorruptBundlerInstallError.new(local_spec) if local_spec.version.to_s != Bundler::VERSION
+
+ idx << local_spec
+ else
+ idx << Gem::Specification.new do |s|
+ s.name = "bundler"
+ s.version = VERSION
+ s.license = "MIT"
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["bundler team"]
+ s.bindir = "exe"
+ s.homepage = "https://bundler.io"
+ s.summary = "The best way to manage your application's dependencies"
+ s.executables = %w[bundle bundler]
+ s.loaded_from = SharedHelpers.gemspec_path
+ end
+ end
+
+ idx.each {|s| s.source = self }
+ end
+ end
+
+ def options
+ {}
+ end
+
+ def install(spec, _opts = {})
+ print_using_message "Using #{version_message(spec)}"
+ nil
+ end
+
+ def to_s
+ "the local ruby installation"
+ end
+
+ def ==(other)
+ self.class == other.class
+ end
+ alias_method :eql?, :==
+
+ def hash
+ self.class.hash
+ end
+
+ def version_message(spec)
+ "#{spec.name} #{spec.version}"
+ end
+
+ def checksum_store
+ @checksum_store ||= Checksum::Store.new
+ end
+ end
+ end
+end
diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb
new file mode 100644
index 0000000000..366a23aea7
--- /dev/null
+++ b/lib/bundler/source/path.rb
@@ -0,0 +1,256 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Source
+ class Path < Source
+ autoload :Installer, File.expand_path("path/installer", __dir__)
+
+ attr_reader :path, :options, :root_path, :original_path
+ attr_writer :name
+ attr_accessor :version
+
+ protected :original_path
+
+ DEFAULT_GLOB = "{,*,*/*}.gemspec"
+
+ def initialize(options)
+ @checksum_store = Checksum::Store.new
+ @options = options.dup
+ @glob = options["glob"] || DEFAULT_GLOB
+
+ @root_path = options["root_path"] || root
+
+ if options["path"]
+ @path = Pathname.new(options["path"])
+ expanded_path = expand(@path)
+ @path = if @path.relative?
+ expanded_path.relative_path_from(File.expand_path(root_path))
+ else
+ expanded_path
+ end
+ end
+
+ @name = options["name"]
+ @version = options["version"]
+
+ # Stores the original path. If at any point we move to the
+ # cached directory, we still have the original path to copy from.
+ @original_path = @path
+ end
+
+ def self.from_lock(options)
+ new(options.merge("path" => options.delete("remote")))
+ end
+
+ def to_lock
+ out = String.new("PATH\n")
+ out << " remote: #{lockfile_path}\n"
+ out << " glob: #{@glob}\n" unless @glob == DEFAULT_GLOB
+ out << " specs:\n"
+ end
+
+ def to_s
+ "source at `#{@path}`"
+ end
+
+ alias_method :identifier, :to_s
+
+ alias_method :to_gemfile, :path
+
+ def hash
+ [self.class, expanded_path, version].hash
+ end
+
+ def eql?(other)
+ [Gemspec, Path].include?(other.class) &&
+ expanded_original_path == other.expanded_original_path &&
+ version == other.version
+ end
+
+ alias_method :==, :eql?
+
+ def name
+ File.basename(expanded_path.to_s)
+ end
+
+ def install(spec, options = {})
+ using_message = "Using #{version_message(spec, options[:previous_spec])} from #{self}"
+ using_message += " and installing its executables" unless spec.executables.empty?
+ print_using_message using_message
+ generate_bin(spec, disable_extensions: true)
+ nil # no post-install message
+ end
+
+ def cache(spec, custom_path = nil)
+ app_cache_path = app_cache_path(custom_path)
+ return unless Bundler.settings[:cache_all]
+ return if expand(@original_path).to_s.index(root_path.to_s + "/") == 0
+
+ unless @original_path.exist?
+ raise GemNotFound, "Can't cache gem #{version_message(spec)} because #{self} is missing!"
+ end
+
+ FileUtils.rm_rf(app_cache_path)
+ FileUtils.cp_r("#{@original_path}/.", app_cache_path)
+ FileUtils.touch(app_cache_path.join(".bundlecache"))
+ end
+
+ def local_specs(*)
+ @local_specs ||= load_spec_files
+ end
+
+ def specs
+ if has_app_cache?
+ @path = app_cache_path
+ @expanded_path = nil # Invalidate
+ end
+ local_specs
+ end
+
+ def app_cache_dirname
+ name
+ end
+
+ def root
+ Bundler.root
+ end
+
+ def expanded_original_path
+ @expanded_original_path ||= expand(original_path)
+ end
+
+ private
+
+ def expanded_path
+ @expanded_path ||= expand(path)
+ end
+
+ def expand(somepath)
+ somepath.expand_path(root_path)
+ rescue ArgumentError => e
+ Bundler.ui.debug(e)
+ raise PathError, "There was an error while trying to use the path " \
+ "`#{somepath}`.\nThe error message was: #{e.message}."
+ end
+
+ def lockfile_path
+ return relative_path(original_path) if original_path.absolute?
+ expand(original_path).relative_path_from(root)
+ end
+
+ def app_cache_path(custom_path = nil)
+ @app_cache_path ||= Bundler.app_cache(custom_path).join(app_cache_dirname)
+ end
+
+ def has_app_cache?
+ SharedHelpers.in_bundle? && app_cache_path.exist?
+ end
+
+ def load_gemspec(file)
+ return unless spec = Bundler.load_gemspec(file)
+ spec.installed_by_version = Gem::VERSION
+ spec
+ end
+
+ def validate_spec(spec)
+ Bundler.rubygems.validate(spec)
+ end
+
+ def load_spec_files
+ index = Index.new
+
+ if File.directory?(expanded_path)
+ # We sort depth-first since `<<` will override the earlier-found specs
+ Gem::Util.glob_files_in_dir(@glob, expanded_path).sort_by {|p| -p.split(File::SEPARATOR).size }.each do |file|
+ next unless spec = load_gemspec(file)
+ spec.source = self
+
+ # The ignore attribute is for ignoring installed gems that don't
+ # have extensions correctly compiled for activation. In the case of
+ # path sources, there's a single version of each gem in the path
+ # source available to Bundler, so we always certainly want to
+ # consider that for activation and never makes sense to ignore it.
+ spec.ignored = false
+
+ # Validation causes extension_dir to be calculated, which depends
+ # on #source, so we validate here instead of load_gemspec
+ validate_spec(spec)
+ index << spec
+ end
+
+ if index.empty? && @name && @version
+ index << Gem::Specification.new do |s|
+ s.name = @name
+ s.source = self
+ s.version = Gem::Version.new(@version)
+ s.platform = Gem::Platform::RUBY
+ s.summary = "Fake gemspec for #{@name}"
+ s.relative_loaded_from = "#{@name}.gemspec"
+ s.authors = ["no one"]
+ if expanded_path.join("bin").exist?
+ executables = expanded_path.join("bin").children
+ executables.reject! {|p| File.directory?(p) }
+ s.executables = executables.map {|c| c.basename.to_s }
+ end
+ end
+ end
+ else
+ message = String.new("The path `#{expanded_path}` ")
+ message << if File.exist?(expanded_path)
+ "is not a directory."
+ else
+ "does not exist."
+ end
+ raise PathError, message
+ end
+
+ index
+ end
+
+ def relative_path(path = self.path)
+ if path.to_s.start_with?(root_path.to_s)
+ return path.relative_path_from(root_path)
+ end
+ path
+ end
+
+ def generate_bin(spec, options = {})
+ gem_dir = Pathname.new(spec.full_gem_path)
+
+ # Some gem authors put absolute paths in their gemspec
+ # and we have to save them from themselves
+ spec.files = spec.files.filter_map do |path|
+ pathname = Pathname.new(path)
+ next path unless pathname.absolute?
+ next if File.directory?(path)
+ begin
+ pathname.relative_path_from(gem_dir).to_s
+ rescue ArgumentError
+ path
+ end
+ end
+
+ installer = Path::Installer.new(
+ spec,
+ env_shebang: false,
+ disable_extensions: options[:disable_extensions],
+ build_args: options[:build_args],
+ bundler_extension_cache_path: extension_cache_path(spec)
+ )
+ installer.post_install
+ rescue Gem::InvalidSpecificationException => e
+ Bundler.ui.warn "\n#{spec.name} at #{spec.full_gem_path} did not have a valid gemspec.\n" \
+ "This prevents bundler from installing bins or native extensions, but " \
+ "that may not affect its functionality."
+
+ if !spec.extensions.empty? && !spec.email.empty?
+ Bundler.ui.warn "If you need to use this package without installing it from a gem " \
+ "repository, please contact #{spec.email} and ask them " \
+ "to modify their .gemspec so it can work with `gem build`."
+ end
+
+ Bundler.ui.warn "The validation message from RubyGems was:\n #{e.message}"
+ end
+ end
+ end
+end
diff --git a/lib/bundler/source/path/installer.rb b/lib/bundler/source/path/installer.rb
new file mode 100644
index 0000000000..39765e5da2
--- /dev/null
+++ b/lib/bundler/source/path/installer.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require_relative "../../rubygems_gem_installer"
+
+module Bundler
+ class Source
+ class Path
+ class Installer < Bundler::RubyGemsGemInstaller
+ attr_reader :spec
+
+ def initialize(spec, options = {})
+ @options = options
+ @spec = spec
+ @gem_dir = Bundler.rubygems.path(spec.full_gem_path)
+ @wrappers = true
+ @env_shebang = true
+ @format_executable = options[:format_executable] || false
+ @build_args = options[:build_args] || Bundler.rubygems.build_args
+ @gem_bin_dir = "#{Bundler.rubygems.gem_dir}/bin"
+ @disable_extensions = options[:disable_extensions]
+ @bin_dir = @gem_bin_dir
+ end
+
+ def post_install
+ run_hooks(:pre_install)
+
+ unless @disable_extensions || Bundler.settings[:no_build_extension]
+ build_extensions
+ run_hooks(:post_build)
+ end
+
+ generate_bin unless spec.executables.empty?
+
+ run_hooks(:post_install)
+ end
+
+ private
+
+ def run_hooks(type)
+ hooks_meth = "#{type}_hooks"
+ return unless Gem.respond_to?(hooks_meth)
+ Gem.send(hooks_meth).each do |hook|
+ result = hook.call(self)
+ next unless result == false
+ location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/
+ message = "#{type} hook#{location} failed for #{spec.full_name}"
+ raise InstallHookError, message
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb
new file mode 100644
index 0000000000..ed864604fe
--- /dev/null
+++ b/lib/bundler/source/rubygems.rb
@@ -0,0 +1,598 @@
+# frozen_string_literal: true
+
+require "rubygems/user_interaction"
+
+module Bundler
+ class Source
+ class Rubygems < Source
+ autoload :Remote, File.expand_path("rubygems/remote", __dir__)
+
+ # Ask for X gems per API request
+ API_REQUEST_SIZE = 100
+ REQUIRE_MUTEX = Mutex.new
+
+ attr_accessor :remotes
+
+ def initialize(options = {})
+ @options = options
+ @remotes = []
+ @remote_cooldowns = {}
+ @dependency_names = []
+ @allow_remote = false
+ @allow_cached = false
+ @allow_local = options["allow_local"] || false
+ @prefer_local = false
+ @checksum_store = Checksum::Store.new
+ @gem_installers = {}
+ @gem_installers_mutex = Mutex.new
+
+ cooldown = options["cooldown"]
+ Array(options["remotes"]).reverse_each {|r| add_remote(r, cooldown: cooldown) }
+
+ @lockfile_remotes = @remotes if options["from_lockfile"]
+ end
+
+ def caches
+ @caches ||= [cache_path, *Bundler.rubygems.gem_cache]
+ end
+
+ def prefer_local!
+ @prefer_local = true
+ end
+
+ def local_only!
+ @specs = nil
+ @allow_local = true
+ @allow_cached = false
+ @allow_remote = false
+ end
+
+ def local_only?
+ @allow_local && !@allow_remote
+ end
+
+ def local!
+ return if @allow_local
+
+ @specs = nil
+ @allow_local = true
+ end
+
+ def remote!
+ return if @allow_remote
+
+ @specs = nil
+ @allow_remote = true
+ end
+
+ def cached!
+ return unless File.exist?(cache_path)
+
+ return if @allow_cached
+
+ @specs = nil
+ @allow_cached = true
+ end
+
+ def hash
+ @remotes.hash
+ end
+
+ def eql?(other)
+ other.is_a?(Rubygems) && other.credless_remotes == credless_remotes
+ end
+
+ alias_method :==, :eql?
+
+ def include?(o)
+ o.is_a?(Rubygems) && (o.credless_remotes - credless_remotes).empty?
+ end
+
+ def multiple_remotes?
+ @remotes.size > 1
+ end
+
+ def no_remotes?
+ @remotes.size == 0
+ end
+
+ def can_lock?(spec)
+ return super unless multiple_remotes?
+ include?(spec.source)
+ end
+
+ def options
+ { "remotes" => @remotes.map(&:to_s) }
+ end
+
+ def self.from_lock(options)
+ options["remotes"] = Array(options.delete("remote")).reverse
+ new(options.merge("from_lockfile" => true))
+ end
+
+ def to_lock
+ out = String.new("GEM\n")
+ lockfile_remotes.reverse_each do |remote|
+ out << " remote: #{remote}\n"
+ end
+ out << " specs:\n"
+ end
+
+ def to_s
+ if remotes.empty?
+ "locally installed gems"
+ elsif @allow_remote && @allow_cached && @allow_local
+ "rubygems repository #{remote_names}, cached gems or installed locally"
+ elsif @allow_remote && @allow_local
+ "rubygems repository #{remote_names} or installed locally"
+ elsif @allow_remote
+ "rubygems repository #{remote_names}"
+ elsif @allow_cached && @allow_local
+ "cached gems or installed locally"
+ else
+ "locally installed gems"
+ end
+ end
+
+ def identifier
+ if remotes.empty?
+ "locally installed gems"
+ else
+ "rubygems repository #{remote_names}"
+ end
+ end
+ alias_method :name, :identifier
+ alias_method :to_gemfile, :identifier
+
+ def specs
+ @specs ||= begin
+ # remote_specs usually generates a way larger Index than the other
+ # sources, and large_idx.merge! small_idx is way faster than
+ # small_idx.merge! large_idx.
+ index = @allow_remote ? remote_specs.dup : Index.new
+
+ # Snapshot per-version `created_at` from the remote info before installed
+ # / cached specs overwrite the EndpointSpecification objects that carry
+ # it. The cooldown filter consults `created_at` on every candidate, so
+ # local stubs need the published date back-filled to participate.
+ remote_created_at = collect_remote_created_at(index)
+
+ index.merge!(cached_specs) if @allow_cached
+ index.merge!(installed_specs) if @allow_local
+
+ if @allow_local
+ if @prefer_local
+ index.merge!(default_specs)
+ else
+ # complete with default specs, only if not already available in the
+ # index through remote, cached, or installed specs
+ index.use(default_specs)
+ end
+ end
+
+ backfill_created_at(index, remote_created_at) unless remote_created_at.empty?
+
+ index
+ end
+ end
+
+ def download(spec, options = {})
+ if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force])
+ return true
+ end
+
+ installer = rubygems_gem_installer(spec, options)
+
+ if spec.remote
+ s = begin
+ installer.spec
+ rescue Gem::Package::FormatError
+ Bundler.rm_rf(installer.gem)
+ raise
+ rescue Gem::Security::Exception => e
+ raise SecurityError,
+ "The gem #{installer.gem} can't be installed because " \
+ "the security policy didn't allow it, with the message: #{e.message}"
+ end
+
+ spec.__swap__(s)
+ end
+
+ spec
+ end
+
+ def install(spec, options = {})
+ if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force])
+ print_using_message "Using #{version_message(spec, options[:previous_spec])}"
+ return nil # no post-install message
+ end
+
+ return if Bundler.settings[:no_install]
+
+ installer = rubygems_gem_installer(spec, options)
+ spec.source.checksum_store.register(spec, installer.gem_checksum)
+
+ message = "Installing #{version_message(spec, options[:previous_spec])}"
+ message += " with native extensions" if spec.extensions.any?
+ Bundler.ui.confirm message
+
+ installed_spec = nil
+
+ Gem.time("Installed #{spec.name} in", 0, true) do
+ installed_spec = installer.install
+ end
+
+ spec.full_gem_path = installed_spec.full_gem_path
+ spec.loaded_from = installed_spec.loaded_from
+ spec.base_dir = installed_spec.base_dir
+
+ spec.post_install_message
+ end
+
+ def cache(spec, custom_path = nil)
+ cached_path = Bundler.settings[:cache_all_platforms] ? fetch_gem_if_possible(spec) : cached_gem(spec)
+ raise GemNotFound, "Missing gem file '#{spec.file_name}'." unless cached_path
+ return if File.dirname(cached_path) == Bundler.app_cache.to_s
+ Bundler.ui.info " * #{File.basename(cached_path)}"
+ FileUtils.cp(cached_path, Bundler.app_cache(custom_path))
+ rescue Errno::EACCES => e
+ Bundler.ui.debug(e)
+ raise InstallError, e.message
+ end
+
+ def cached_built_in_gem(spec, local: false)
+ cached_path = cached_gem(spec)
+ if cached_path.nil? && !local
+ remote_spec = remote_specs.search(spec).first
+ if remote_spec
+ cached_path = fetch_gem(remote_spec)
+ spec.remote = remote_spec.remote
+ else
+ Bundler.ui.warn "#{spec.full_name} is built in to Ruby, and can't be cached because your Gemfile doesn't have any sources that contain it."
+ end
+ end
+ cached_path
+ end
+
+ def add_remote(source, cooldown: nil)
+ uri = normalize_uri(source)
+ @remotes.unshift(uri) unless @remotes.include?(uri)
+ @remote_cooldowns[uri] = cooldown if cooldown
+ end
+
+ def cooldown_for(uri)
+ @remote_cooldowns[uri]
+ end
+
+ def spec_names
+ if dependency_api_available?
+ remote_specs.spec_names
+ else
+ []
+ end
+ end
+
+ def unmet_deps
+ if dependency_api_available?
+ remote_specs.unmet_dependency_names
+ else
+ []
+ end
+ end
+
+ def remote_fetchers
+ @remote_fetchers ||= remotes.to_h do |uri|
+ remote = Source::Rubygems::Remote.new(uri, cooldown: cooldown_for(uri))
+ [remote, Bundler::Fetcher.new(remote)]
+ end.freeze
+ end
+
+ def fetchers
+ @fetchers ||= remote_fetchers.values.freeze
+ end
+
+ def double_check_for(unmet_dependency_names)
+ return unless dependency_api_available?
+
+ unmet_dependency_names = unmet_dependency_names.call
+ unless unmet_dependency_names.nil?
+ if api_fetchers.size <= 1
+ # can't do this when there are multiple fetchers because then we might not fetch from _all_
+ # of them
+ unmet_dependency_names -= remote_specs.spec_names # avoid re-fetching things we've already gotten
+ end
+ return if unmet_dependency_names.empty?
+ end
+
+ Bundler.ui.debug "Double checking for #{unmet_dependency_names || "all specs (due to the size of the request)"} in #{self}"
+
+ fetch_names(api_fetchers, unmet_dependency_names, remote_specs)
+
+ specs.use remote_specs
+ end
+
+ def dependency_names_to_double_check
+ names = []
+ remote_specs.each do |spec|
+ case spec
+ when EndpointSpecification, Gem::Specification, StubSpecification, LazySpecification
+ names.concat(spec.runtime_dependencies.map(&:name))
+ when RemoteSpecification # from the full index
+ return nil
+ else
+ raise "unhandled spec type (#{spec.inspect})"
+ end
+ end
+ names
+ end
+
+ def dependency_api_available?
+ @allow_remote && api_fetchers.any?
+ end
+
+ def clear_cache
+ @specs = nil
+ @installed_specs = nil
+ @default_specs = nil
+ @cached_specs = nil
+ end
+
+ protected
+
+ def remote_names
+ remotes.map(&:to_s).join(", ")
+ end
+
+ def credless_remotes
+ remotes.map(&method(:remove_auth))
+ end
+
+ def cached_gem(spec)
+ global_cache_path = download_cache_path(spec)
+ caches << global_cache_path if global_cache_path
+
+ possibilities = caches.map {|p| package_path(p, spec) }
+ possibilities.find {|p| File.exist?(p) }
+ end
+
+ def package_path(cache_path, spec)
+ "#{cache_path}/#{spec.file_name}"
+ end
+
+ def normalize_uri(uri)
+ uri = URINormalizer.normalize_suffix(uri.to_s)
+ require_relative "../vendored_uri"
+ uri = Gem::URI(uri)
+ raise ArgumentError, "The source must be an absolute URI. For example:\n" \
+ "source 'https://rubygems.org'" if !uri.absolute? || (uri.is_a?(Gem::URI::HTTP) && uri.host.nil?)
+ uri
+ end
+
+ def remove_auth(remote)
+ if remote.user || remote.password
+ remote.dup.tap {|uri| uri.user = uri.password = nil }.to_s
+ else
+ remote.to_s
+ end
+ end
+
+ def installed_specs
+ @installed_specs ||= Index.build do |idx|
+ Bundler.rubygems.installed_specs.reverse_each do |spec|
+ spec.source = self
+ next if spec.ignored?
+ idx << spec
+ end
+ end
+ end
+
+ def default_specs
+ @default_specs ||= Index.build do |idx|
+ Bundler.rubygems.default_specs.each do |spec|
+ spec.source = self
+ idx << spec
+ end
+ end
+ end
+
+ def cached_specs
+ @cached_specs ||= begin
+ idx = Index.new
+
+ Dir["#{cache_path}/*.gem"].each do |gemfile|
+ s ||= Bundler.rubygems.spec_from_gem(gemfile)
+ s.source = self
+ idx << s
+ end
+
+ idx
+ end
+ end
+
+ def api_fetchers
+ fetchers.select(&:api_fetcher?)
+ end
+
+ def remote_specs
+ @remote_specs ||= Index.build do |idx|
+ index_fetchers = fetchers - api_fetchers
+
+ if index_fetchers.empty?
+ fetch_names(api_fetchers, dependency_names, idx)
+ else
+ fetch_names(fetchers, nil, idx)
+ end
+ end
+ end
+
+ def fetch_names(fetchers, dependency_names, index)
+ fetchers.each do |f|
+ if dependency_names
+ Bundler.ui.info "Fetching gem metadata from #{URICredentialsFilter.credential_filtered_uri(f.uri)}", Bundler.ui.debug?
+ index.use f.specs_with_retry(dependency_names, self)
+ Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over
+ else
+ Bundler.ui.info "Fetching source index from #{URICredentialsFilter.credential_filtered_uri(f.uri)}"
+ index.use f.specs_with_retry(nil, self)
+ end
+ end
+ end
+
+ def fetch_gem_if_possible(spec, previous_spec = nil)
+ if spec.remote
+ fetch_gem(spec, previous_spec)
+ else
+ cached_gem(spec)
+ end
+ end
+
+ def fetch_gem(spec, previous_spec = nil)
+ spec.fetch_platform
+
+ cache_path = download_cache_path(spec) || default_cache_path_for(rubygems_dir)
+ gem_path = package_path(cache_path, spec)
+ return gem_path if File.exist?(gem_path)
+
+ SharedHelpers.filesystem_access(cache_path) do |p|
+ FileUtils.mkdir_p(p)
+ end
+ download_gem(spec, cache_path, previous_spec)
+
+ gem_path
+ end
+
+ def installed?(spec)
+ installed_specs[spec].any? && !spec.installation_missing?
+ end
+
+ def rubygems_dir
+ Bundler.bundle_path
+ end
+
+ def default_cache_path_for(dir)
+ "#{dir}/cache"
+ end
+
+ def cache_path
+ Bundler.app_cache
+ end
+
+ private
+
+ def collect_remote_created_at(index)
+ return {} unless @allow_remote
+
+ snapshot = {}
+ index.each do |spec|
+ next unless spec.respond_to?(:created_at) && spec.created_at
+ # Remember the remote that supplied the date too: when a source has
+ # several remotes with different per-URI cooldown settings we must
+ # restore the same one during backfill so `effective_cooldown` agrees.
+ snapshot[[spec.name, spec.version]] = [spec.created_at, spec.remote]
+ end
+ snapshot
+ end
+
+ def backfill_created_at(index, snapshot)
+ index.each do |spec|
+ next unless spec.respond_to?(:created_at=)
+ next if spec.created_at
+ remote_created_at, remote = snapshot[[spec.name, spec.version]]
+ next unless remote_created_at
+ spec.created_at = remote_created_at
+ spec.remote ||= remote if remote && spec.respond_to?(:remote=)
+ end
+ end
+
+ def lockfile_remotes
+ @lockfile_remotes || credless_remotes
+ end
+
+ # Checks if the requested spec exists in the global cache. If it does,
+ # we copy it to the download path, and if it does not, we download it.
+ #
+ # @param [Specification] spec
+ # the spec we want to download or retrieve from the cache.
+ #
+ # @param [String] download_cache_path
+ # the local directory the .gem will end up in.
+ #
+ # @param [Specification] previous_spec
+ # the spec previously locked
+ #
+ def download_gem(spec, download_cache_path, previous_spec = nil)
+ uri = spec.remote.uri
+ Bundler.ui.confirm("Fetching #{version_message(spec, previous_spec)}")
+ gem_remote_fetcher = remote_fetchers.fetch(spec.remote).gem_remote_fetcher
+
+ Plugin.hook(Plugin::Events::GEM_BEFORE_FETCH, spec)
+ begin
+ Gem.time("Downloaded #{spec.name} in", 0, true) do
+ Bundler.rubygems.download_gem(spec, uri, download_cache_path, gem_remote_fetcher)
+ end
+ ensure
+ Plugin.hook(Plugin::Events::GEM_AFTER_FETCH, spec)
+ end
+ end
+
+ # Returns the global cache path of the calling Rubygems::Source object.
+ #
+ # Note that the Source determines the path's subdirectory. We use this
+ # subdirectory in the global cache path so that gems with the same name
+ # -- and possibly different versions -- from different sources are saved
+ # to their respective subdirectories and do not override one another.
+ #
+ # @param [Gem::Specification] specification
+ #
+ # @return [Pathname] The global cache path.
+ #
+ def download_cache_path(spec)
+ return unless Bundler.settings[:global_gem_cache]
+ return unless remote = spec.remote
+ return unless cache_slug = remote.cache_slug
+
+ if Gem.respond_to?(:global_gem_cache_path)
+ Pathname.new(Gem.global_gem_cache_path).join(cache_slug)
+ else
+ # Fall back to old location for older RubyGems versions
+ Bundler.user_cache.join("gems", cache_slug)
+ end
+ end
+
+ def extension_cache_slug(spec)
+ return unless remote = spec.remote
+ remote.cache_slug
+ end
+
+ # We are using a mutex to read and write from/to the hash.
+ # The reason this double synchronization was added is for performance
+ # and to lock the mutex for the shortest possible amount of time. Otherwise,
+ # all threads are fighting over this mutex and when it gets acquired it gets locked
+ # until a thread finishes downloading a gem, leaving the other threads waiting
+ # doing nothing.
+ def rubygems_gem_installer(spec, options)
+ @gem_installers_mutex.synchronize { @gem_installers[spec.name] } || begin
+ path = fetch_gem_if_possible(spec, options[:previous_spec])
+ raise GemNotFound, "Could not find #{spec.file_name} for installation" unless path
+
+ REQUIRE_MUTEX.synchronize { require_relative "../rubygems_gem_installer" }
+
+ installer = Bundler::RubyGemsGemInstaller.at(
+ path,
+ security_policy: Bundler.rubygems.security_policies[Bundler.settings["trust-policy"]],
+ install_dir: rubygems_dir.to_s,
+ bin_dir: Bundler.system_bindir.to_s,
+ ignore_dependencies: true,
+ wrappers: true,
+ env_shebang: true,
+ build_args: options[:build_args],
+ bundler_extension_cache_path: extension_cache_path(spec),
+ build_extension: Bundler.settings[:no_build_extension] ? false : nil,
+ install_plugin: Bundler.settings[:no_install_plugin] ? false : nil
+ )
+ @gem_installers_mutex.synchronize { @gem_installers[spec.name] ||= installer }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/source/rubygems/remote.rb b/lib/bundler/source/rubygems/remote.rb
new file mode 100644
index 0000000000..3d847424b7
--- /dev/null
+++ b/lib/bundler/source/rubygems/remote.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Source
+ class Rubygems
+ class Remote
+ attr_reader :uri, :anonymized_uri, :original_uri, :cooldown
+
+ def initialize(uri, cooldown: nil)
+ orig_uri = uri
+ uri = Bundler.settings.mirror_for(uri)
+ @original_uri = orig_uri if orig_uri != uri
+ fallback_auth = Bundler.settings.credentials_for(uri)
+
+ @uri = apply_auth(uri, fallback_auth).freeze
+ @anonymized_uri = remove_auth(@uri).freeze
+ @cooldown = cooldown
+ end
+
+ # Returns the cooldown days that apply to this remote, resolving the
+ # precedence CLI > config > Gemfile per-source. Returns nil if no
+ # cooldown applies.
+ def effective_cooldown
+ override = Bundler.settings[:cooldown]
+ return override if override
+ @cooldown
+ end
+
+ MAX_CACHE_SLUG_HOST_SIZE = 255 - 1 - 32 # 255 minus dot minus MD5 length
+ private_constant :MAX_CACHE_SLUG_HOST_SIZE
+
+ # @return [String] A slug suitable for use as a cache key for this
+ # remote.
+ #
+ def cache_slug
+ @cache_slug ||= begin
+ return nil unless SharedHelpers.md5_available?
+
+ cache_uri = original_uri || uri
+
+ host = cache_uri.to_s.start_with?("file://") ? nil : cache_uri.host
+
+ uri_parts = [host, cache_uri.user, cache_uri.port, cache_uri.path]
+ uri_parts.compact!
+ uri_digest = SharedHelpers.digest(:MD5).hexdigest(uri_parts.join("."))
+
+ uri_parts.pop
+ host_parts = uri_parts.join(".")
+ return uri_digest if host_parts.empty?
+
+ shortened_host_parts = host_parts[0...MAX_CACHE_SLUG_HOST_SIZE]
+ [shortened_host_parts, uri_digest].join(".")
+ end
+ end
+
+ def to_s
+ "rubygems remote at #{anonymized_uri}"
+ end
+
+ private
+
+ def apply_auth(uri, auth)
+ if auth && uri.userinfo.nil?
+ uri = uri.dup
+ uri.userinfo = auth
+ end
+
+ uri
+ rescue Gem::URI::InvalidComponentError
+ error_message = "Please CGI escape your usernames and passwords before " \
+ "setting them for authentication."
+ raise HTTPError.new(error_message)
+ end
+
+ def remove_auth(uri)
+ if uri.userinfo
+ uri = uri.dup
+ uri.user = uri.password = nil
+ end
+
+ uri
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/source/rubygems_aggregate.rb b/lib/bundler/source/rubygems_aggregate.rb
new file mode 100644
index 0000000000..8aeaa375fa
--- /dev/null
+++ b/lib/bundler/source/rubygems_aggregate.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Source
+ class RubygemsAggregate
+ attr_reader :source_map, :sources
+
+ def initialize(sources, source_map, excluded_sources = [])
+ @sources = sources
+ @source_map = source_map
+ @excluded_sources = excluded_sources
+
+ @index = build_index
+ end
+
+ def specs
+ @index
+ end
+
+ def identifier
+ to_s
+ end
+
+ def to_s
+ "any of the sources"
+ end
+
+ private
+
+ def build_index
+ Index.build do |idx|
+ dependency_names = source_map.pinned_spec_names
+
+ sources.all_sources.each do |source|
+ next if @excluded_sources.include?(source)
+
+ source.dependency_names = dependency_names - source_map.pinned_spec_names(source)
+ idx.add_source source.specs
+ dependency_names.concat(source.unmet_deps).uniq!
+ end
+
+ double_check_for_index(idx, dependency_names)
+ end
+ end
+
+ # Suppose the gem Foo depends on the gem Bar. Foo exists in Source A. Bar has some versions that exist in both
+ # sources A and B. At this point, the API request will have found all the versions of Bar in source A,
+ # but will not have found any versions of Bar from source B, which is a problem if the requested version
+ # of Foo specifically depends on a version of Bar that is only found in source B. This ensures that for
+ # each spec we found, we add all possible versions from all sources to the index.
+ def double_check_for_index(idx, dependency_names)
+ pinned_names = source_map.pinned_spec_names
+
+ names = :names # do this so we only have to traverse to get dependency_names from the index once
+ unmet_dependency_names = lambda do
+ return names unless names == :names
+ new_names = sources.all_sources.map(&:dependency_names_to_double_check)
+ return names = nil if new_names.compact!
+ names = new_names.flatten(1).concat(dependency_names)
+ names.uniq!
+ names -= pinned_names
+ names
+ end
+
+ sources.all_sources.each do |source|
+ source.double_check_for(unmet_dependency_names)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb
new file mode 100644
index 0000000000..ab7002d6e5
--- /dev/null
+++ b/lib/bundler/source_list.rb
@@ -0,0 +1,232 @@
+# frozen_string_literal: true
+
+module Bundler
+ class SourceList
+ attr_reader :path_sources,
+ :git_sources,
+ :plugin_sources,
+ :global_path_source,
+ :metadata_source
+
+ def global_rubygems_source
+ @global_rubygems_source ||= source_class.new("allow_local" => true)
+ end
+
+ def initialize
+ @path_sources = []
+ @git_sources = []
+ @plugin_sources = []
+ @global_rubygems_source = nil
+ @global_path_source = nil
+ @rubygems_sources = []
+ @metadata_source = Source::Metadata.new
+
+ @local_mode = true
+ end
+
+ def aggregate_global_source?
+ global_rubygems_source.multiple_remotes?
+ end
+
+ def implicit_global_source?
+ global_rubygems_source.no_remotes?
+ end
+
+ def add_path_source(options = {})
+ if options["gemspec"]
+ add_source_to_list Source::Gemspec.new(options), path_sources
+ else
+ path_source = add_source_to_list Source::Path.new(options), path_sources
+ @global_path_source ||= path_source if options["global"]
+ path_source
+ end
+ end
+
+ def add_git_source(options = {})
+ add_source_to_list(Source::Git.new(options), git_sources).tap do |source|
+ warn_on_git_protocol(source)
+ end
+ end
+
+ def add_rubygems_source(options = {})
+ new_source = Source::Rubygems.new(options)
+ return @global_rubygems_source if @global_rubygems_source == new_source
+
+ add_source_to_list new_source, @rubygems_sources
+ end
+
+ def add_plugin_source(source, options = {})
+ add_source_to_list Plugin.source(source).new(options), @plugin_sources
+ end
+
+ def add_global_rubygems_remote(uri, cooldown: nil)
+ global_rubygems_source.add_remote(uri, cooldown: cooldown)
+ global_rubygems_source
+ end
+
+ def local_mode?
+ @local_mode
+ end
+
+ def default_source
+ global_path_source || global_rubygems_source
+ end
+
+ def rubygems_sources
+ non_global_rubygems_sources + [global_rubygems_source]
+ end
+
+ def non_global_rubygems_sources
+ @rubygems_sources
+ end
+
+ def all_sources
+ path_sources + git_sources + plugin_sources + rubygems_sources + [metadata_source]
+ end
+
+ def non_default_explicit_sources
+ all_sources - [default_source, metadata_source]
+ end
+
+ def get(source)
+ source_list_for(source).find {|s| s.include?(source) }
+ end
+
+ def lock_sources
+ lock_other_sources + lock_rubygems_sources
+ end
+
+ def lock_other_sources
+ (path_sources + git_sources + plugin_sources).sort_by(&:identifier)
+ end
+
+ def lock_rubygems_sources
+ rubygems_sources.sort_by(&:identifier)
+ end
+
+ # Returns true if there are changes
+ def replace_sources!(replacement_sources)
+ return false if replacement_sources.empty?
+
+ @rubygems_sources, @path_sources, @git_sources, @plugin_sources = map_sources(replacement_sources)
+ @global_rubygems_source = global_replacement_source(replacement_sources)
+
+ !equivalent_sources?(lock_sources, replacement_sources)
+ end
+
+ def prefer_local!
+ all_sources.each(&:prefer_local!)
+ end
+
+ def local_only!
+ all_sources.each(&:local_only!)
+ end
+
+ def local!
+ all_sources.each(&:local!)
+ end
+
+ def cached!
+ all_sources.each(&:cached!)
+ end
+
+ def remote!
+ @local_mode = false
+
+ all_sources.each(&:remote!)
+ end
+
+ def clear_cache
+ rubygems_sources.each(&:clear_cache)
+ end
+
+ private
+
+ def map_sources(replacement_sources)
+ rubygems = @rubygems_sources.map do |source|
+ replace_rubygems_source(replacement_sources, source)
+ end
+
+ git, plugin = [@git_sources, @plugin_sources].map do |sources|
+ sources.map do |source|
+ replace_source(replacement_sources, source)
+ end
+ end
+
+ path = @path_sources.map do |source|
+ replace_path_source(replacement_sources, source)
+ end
+
+ [rubygems, path, git, plugin]
+ end
+
+ def global_replacement_source(replacement_sources)
+ replace_rubygems_source(replacement_sources, global_rubygems_source, &:local!)
+ end
+
+ def replace_rubygems_source(replacement_sources, gemfile_source)
+ replace_source(replacement_sources, gemfile_source) do |replacement_source|
+ # locked sources never include credentials so always prefer remotes from the gemfile
+ replacement_source.remotes = gemfile_source.remotes
+
+ yield replacement_source if block_given?
+
+ replacement_source
+ end
+ end
+
+ def replace_source(replacement_sources, gemfile_source)
+ replacement_source = replacement_sources.find {|s| s == gemfile_source }
+ return gemfile_source unless replacement_source
+
+ replacement_source = yield(replacement_source) if block_given?
+
+ replacement_source
+ end
+
+ def replace_path_source(replacement_sources, gemfile_source)
+ replace_source(replacement_sources, gemfile_source) do |replacement_source|
+ if gemfile_source.is_a?(Source::Gemspec)
+ gemfile_source.checksum_store = replacement_source.checksum_store
+ gemfile_source
+ else
+ replacement_source
+ end
+ end
+ end
+
+ def source_class
+ Source::Rubygems
+ end
+
+ def add_source_to_list(source, list)
+ list.unshift(source).uniq!
+ source
+ end
+
+ def source_list_for(source)
+ case source
+ when Source::Git then git_sources
+ when Source::Path then path_sources
+ when Source::Rubygems then rubygems_sources
+ when Plugin::API::Source then plugin_sources
+ else raise ArgumentError, "Invalid source: #{source.inspect}"
+ end
+ end
+
+ def warn_on_git_protocol(source)
+ return if Bundler.settings["git.allow_insecure"]
+
+ if /^git\:/.match?(source.uri)
+ Bundler.ui.warn "The git source `#{source.uri}` uses the `git` protocol, " \
+ "which transmits data without encryption. Disable this warning with " \
+ "`bundle config set --local git.allow_insecure true`, or switch to the `https` " \
+ "protocol to keep your data secure."
+ end
+ end
+
+ def equivalent_sources?(lock_sources, replacement_sources)
+ lock_sources.sort_by(&:identifier) == replacement_sources.sort_by(&:identifier)
+ end
+ end
+end
diff --git a/lib/bundler/source_map.rb b/lib/bundler/source_map.rb
new file mode 100644
index 0000000000..513eb37f8b
--- /dev/null
+++ b/lib/bundler/source_map.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Bundler
+ class SourceMap
+ attr_reader :sources, :dependencies, :locked_specs
+
+ def initialize(sources, dependencies, locked_specs)
+ @sources = sources
+ @dependencies = dependencies
+ @locked_specs = locked_specs
+ end
+
+ def pinned_spec_names(skip = nil)
+ direct_requirements.reject {|_, source| source == skip }.keys
+ end
+
+ def all_requirements(excluded_sources = [])
+ requirements = direct_requirements.dup
+
+ explicit_sources = sources.non_default_explicit_sources.reject do |source|
+ excluded_sources.include?(source)
+ end
+
+ unmet_deps = explicit_sources.map do |source|
+ (source.spec_names - pinned_spec_names).each do |indirect_dependency_name|
+ previous_source = requirements[indirect_dependency_name]
+ if previous_source.nil?
+ requirements[indirect_dependency_name] = source
+ else
+ msg = ["The gem '#{indirect_dependency_name}' was found in multiple relevant sources."]
+ msg.concat [previous_source, source].map {|s| " * #{s}" }.sort
+ msg << "You must add this gem to the source block for the source you wish it to be installed from."
+ msg = msg.join("\n")
+
+ raise SecurityError, msg
+ end
+ end
+
+ source.unmet_deps
+ end
+
+ sources.default_source.add_dependency_names(unmet_deps.flatten - requirements.keys)
+
+ requirements
+ end
+
+ def direct_requirements
+ @direct_requirements ||= begin
+ requirements = {}
+ default = sources.default_source
+ dependencies.each do |dep|
+ dep_source = dep.source || default
+ dep_source.add_dependency_names(dep.name)
+ requirements[dep.name] = dep_source
+ end
+ requirements
+ end
+ end
+
+ def locked_requirements
+ @locked_requirements ||= begin
+ requirements = {}
+ locked_specs.each do |locked_spec|
+ source = locked_spec.source
+ source.add_dependency_names(locked_spec.name)
+ requirements[locked_spec.name] = source
+ end
+ requirements
+ end
+ end
+ end
+end
diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb
new file mode 100644
index 0000000000..ae5e5cbaa9
--- /dev/null
+++ b/lib/bundler/spec_set.rb
@@ -0,0 +1,402 @@
+# frozen_string_literal: true
+
+require_relative "vendored_tsort"
+
+module Bundler
+ class SpecSet
+ include Enumerable
+ include TSort
+
+ def initialize(specs)
+ @specs = specs
+ end
+
+ def for(dependencies, platforms = [nil], legacy_platforms = [nil], skips: [])
+ if [true, false].include?(platforms)
+ Bundler::SharedHelpers.feature_removed! \
+ "SpecSet#for received a `check` parameter, but that's no longer used and deprecated. " \
+ "SpecSet#for always implicitly performs validation. Please remove this parameter"
+ end
+
+ materialize_dependencies(dependencies, platforms, skips: skips)
+
+ @materializations.flat_map(&:specs).uniq
+ end
+
+ def normalize_platforms!(deps, platforms)
+ remove_invalid_platforms!(deps, platforms)
+ add_extra_platforms!(platforms)
+
+ platforms.map! do |platform|
+ next platform if platform == Gem::Platform::RUBY
+
+ begin
+ Integer(platform.version)
+ rescue ArgumentError, TypeError
+ next platform
+ end
+
+ less_specific_platform = Gem::Platform.new([platform.cpu, platform.os, nil])
+ next platform if incomplete_for_platform?(deps, less_specific_platform)
+
+ less_specific_platform
+ end.uniq!
+ end
+
+ def add_originally_invalid_platforms!(platforms, originally_invalid_platforms)
+ originally_invalid_platforms.each do |originally_invalid_platform|
+ platforms << originally_invalid_platform if complete_platform(originally_invalid_platform)
+ end
+ end
+
+ def remove_invalid_platforms!(deps, platforms, skips: [])
+ invalid_platforms = []
+
+ platforms.reject! do |platform|
+ next false if skips.include?(platform)
+
+ invalid = incomplete_for_platform?(deps, platform)
+ invalid_platforms << platform if invalid
+ invalid
+ end
+
+ invalid_platforms
+ end
+
+ def add_extra_platforms!(platforms)
+ if @specs.empty?
+ platforms.concat([Gem::Platform::RUBY]).uniq
+ return
+ end
+
+ new_platforms = all_platforms.select do |platform|
+ next if platforms.include?(platform)
+ next unless Gem::Platform.generic(platform) == Gem::Platform::RUBY
+
+ complete_platform(platform)
+ end
+ return if new_platforms.empty?
+
+ platforms.concat(new_platforms)
+ return if new_platforms.include?(Bundler.local_platform)
+
+ less_specific_platform = new_platforms.find {|platform| platform != Gem::Platform::RUBY && Bundler.local_platform === platform && platform === Bundler.local_platform }
+ platforms.delete(Bundler.local_platform) if less_specific_platform
+ end
+
+ def validate_deps(s)
+ s.runtime_dependencies.each do |dep|
+ next if dep.name == "bundler"
+
+ return :missing unless names.include?(dep.name)
+ return :invalid if none? {|spec| dep.matches_spec?(spec) }
+ end
+
+ :valid
+ end
+
+ def [](key)
+ key = key.name if key.respond_to?(:name)
+ lookup[key]&.reverse || []
+ end
+
+ def []=(key, value)
+ delete_by_name(key)
+
+ add_spec(value)
+ end
+
+ def delete(specs)
+ Array(specs).each {|spec| remove_spec(spec) }
+ end
+
+ def sort!
+ self
+ end
+
+ def to_a
+ sorted.dup
+ end
+
+ def to_hash
+ lookup.dup
+ end
+
+ def materialize(deps)
+ materialize_dependencies(deps)
+
+ SpecSet.new(materialized_specs)
+ end
+
+ # Materialize for all the specs in the spec set, regardless of what platform they're for
+ # @return [Array<Gem::Specification>]
+ def materialized_for_all_platforms
+ @specs.map do |s|
+ next s unless s.is_a?(LazySpecification)
+ spec = s.materialize_for_cache
+ raise GemNotFound, "Could not find #{s.full_name} in any of the sources" unless spec
+ spec
+ end
+ end
+
+ def incomplete_for_platform?(deps, platform)
+ incomplete_specs_for_platform(deps, platform).any?
+ end
+
+ def incomplete_specs_for_platform(deps, platform)
+ return [] if @specs.empty?
+
+ validation_set = self.class.new(@specs)
+ validation_set.for(deps, [platform])
+ validation_set.incomplete_specs
+ end
+
+ def missing_specs_for(deps)
+ materialize_dependencies(deps)
+
+ missing_specs
+ end
+
+ def missing_specs
+ @materializations.flat_map(&:completely_missing_specs)
+ end
+
+ def partially_missing_specs
+ @materializations.flat_map(&:partially_missing_specs)
+ end
+
+ def incomplete_specs
+ @materializations.flat_map(&:incomplete_specs)
+ end
+
+ def insecurely_materialized_specs
+ materialized_specs.select(&:insecurely_materialized?)
+ end
+
+ def -(other)
+ SharedHelpers.feature_removed! "SpecSet#- has been removed with no replacement"
+ end
+
+ def find_by_name_and_platform(name, platform)
+ lookup[name]&.detect {|spec| spec.installable_on_platform?(platform) }
+ end
+
+ def specs_with_additional_variants_from(other)
+ sorted | additional_variants_from(other)
+ end
+
+ def delete_by_name(name)
+ @specs.reject! {|spec| spec.name == name }
+ @sorted&.reject! {|spec| spec.name == name }
+ return if @lookup.nil?
+
+ @lookup[name] = nil
+ end
+
+ def version_for(name)
+ exemplary_spec(name)&.version
+ end
+
+ def what_required(spec)
+ unless req = find {|s| s.runtime_dependencies.any? {|d| d.name == spec.name } }
+ return [spec]
+ end
+ what_required(req) << spec
+ end
+
+ def <<(spec)
+ SharedHelpers.feature_removed! "SpecSet#<< has been removed with no replacement"
+ end
+
+ def length
+ @specs.length
+ end
+
+ def size
+ @specs.size
+ end
+
+ def empty?
+ @specs.empty?
+ end
+
+ def each(&b)
+ sorted.each(&b)
+ end
+
+ def names
+ lookup.keys
+ end
+
+ def valid?(s)
+ s.matches_current_metadata? && valid_dependencies?(s)
+ end
+
+ def to_s
+ map(&:full_name).to_s
+ end
+
+ private
+
+ def materialize_dependencies(dependencies, platforms = [nil], skips: [])
+ handled = ["bundler"].product(platforms).map {|k| [k, true] }.to_h
+ deps = dependencies.product(platforms)
+ @materializations = []
+
+ loop do
+ break unless dep = deps.shift
+
+ dependency = dep[0]
+ platform = dep[1]
+ name = dependency.name
+
+ key = [name, platform]
+ next if handled.key?(key)
+
+ handled[key] = true
+
+ materialization = Materialization.new(dependency, platform, candidates: lookup[name])
+
+ deps.concat(materialization.dependencies) if materialization.complete?
+
+ @materializations << materialization unless skips.include?(name)
+ end
+
+ @materializations
+ end
+
+ def materialized_specs
+ @materializations.filter_map(&:materialized_spec)
+ end
+
+ def complete_platform(platform)
+ new_specs = []
+
+ valid_platform = lookup.all? do |_, specs|
+ spec = specs.first
+ # The matching candidates returned by source.specs.search are remote
+ # specs that do not carry the override list themselves. Borrow it from
+ # the LazySpec we are validating so platform-variant validation honors
+ # the same overrides the install/resolve path already applies.
+ overrides = spec.is_a?(LazySpecification) ? Array(spec.overrides) : []
+ matching_specs = spec.source.specs.search([spec.name, spec.version])
+ platform_spec = MatchPlatform.select_best_platform_match(matching_specs, platform).find do |s|
+ s.matches_current_metadata_with_overrides?(overrides) && valid_dependencies?(s)
+ end
+
+ if platform_spec
+ unless specs.include?(platform_spec)
+ new_lazy = LazySpecification.from_spec(platform_spec)
+ # Carry the overrides forward so a follow-up complete_platform
+ # call that picks this synthesized variant as its exemplar still
+ # honors the user's override list.
+ new_lazy.overrides = overrides if overrides.any?
+ new_specs << new_lazy
+ end
+ true
+ else
+ false
+ end
+ end
+
+ if valid_platform && new_specs.any?
+ new_specs.each {|spec| add_spec(spec) }
+ end
+
+ valid_platform
+ end
+
+ def all_platforms
+ @specs.flat_map {|spec| spec.source.specs.search([spec.name, spec.version]).map(&:platform) }.uniq
+ end
+
+ def additional_variants_from(other)
+ other.select do |other_spec|
+ spec = exemplary_spec(other_spec.name)
+ next unless spec
+
+ selected = spec.version == other_spec.version && valid_dependencies?(other_spec)
+ other_spec.source = spec.source if selected
+ selected
+ end
+ end
+
+ def valid_dependencies?(s)
+ validate_deps(s) == :valid
+ end
+
+ def sorted
+ @sorted ||= ([lookup["rake"]&.first] + tsort).compact.uniq
+ rescue TSort::Cyclic => error
+ cgems = extract_circular_gems(error)
+ raise CyclicDependencyError, "Your bundle requires gems that depend" \
+ " on each other, creating an infinite loop. Please remove either" \
+ " gem '#{cgems[0]}' or gem '#{cgems[1]}' and try again."
+ end
+
+ def extract_circular_gems(error)
+ error.message.scan(/@name="(.*?)"/).flatten
+ end
+
+ def lookup
+ @lookup ||= begin
+ lookup = {}
+ @specs.each do |s|
+ index_spec(lookup, s.name, s)
+ end
+ lookup
+ end
+ end
+
+ def tsort_each_node
+ # MUST sort by name for backwards compatibility
+ @specs.sort_by(&:name).each {|s| yield s }
+ end
+
+ def tsort_each_child(s)
+ s.dependencies.sort_by(&:name).each do |d|
+ next if d.type == :development
+
+ specs_for_name = lookup[d.name]
+ next unless specs_for_name
+
+ specs_for_name.each {|s2| yield s2 }
+ end
+ end
+
+ def add_spec(spec)
+ @specs << spec
+
+ name = spec.name
+
+ @sorted&.insert(@sorted.bsearch_index {|s| s.name >= name } || @sorted.size, spec)
+ return if @lookup.nil?
+
+ index_spec(@lookup, name, spec)
+ end
+
+ def remove_spec(spec)
+ @specs.delete(spec)
+ @sorted&.delete(spec)
+ return if @lookup.nil?
+
+ indexed_specs = @lookup[spec.name]
+ return unless indexed_specs
+
+ if indexed_specs.size > 1
+ @lookup[spec.name].delete(spec)
+ else
+ @lookup[spec.name] = nil
+ end
+ end
+
+ def index_spec(hash, key, value)
+ hash[key] ||= []
+ hash[key] << value
+ end
+
+ def exemplary_spec(name)
+ self[name].first
+ end
+ end
+end
diff --git a/lib/bundler/stub_specification.rb b/lib/bundler/stub_specification.rb
new file mode 100644
index 0000000000..b353642b40
--- /dev/null
+++ b/lib/bundler/stub_specification.rb
@@ -0,0 +1,147 @@
+# frozen_string_literal: true
+
+module Bundler
+ class StubSpecification < RemoteSpecification
+ def self.from_stub(stub)
+ return stub if stub.is_a?(Bundler::StubSpecification)
+ spec = new(stub.name, stub.version, stub.platform, nil)
+ spec.stub = stub
+ spec
+ end
+
+ def insecurely_materialized?
+ false
+ end
+
+ attr_reader :checksum
+ attr_accessor :stub, :ignored
+
+ def source=(source)
+ super
+ # Stub has no concept of source, which means that extension_dir may be wrong
+ # This is the case for git-based gems. So, instead manually assign the extension dir
+ return unless source.respond_to?(:extension_dir_name)
+ unique_extension_dir = [source.extension_dir_name, File.basename(full_gem_path)].uniq.join("-")
+ path = File.join(stub.extensions_dir, unique_extension_dir)
+ stub.extension_dir = File.expand_path(path)
+ end
+
+ def to_yaml
+ _remote_specification.to_yaml
+ end
+
+ # @!group Stub Delegates
+
+ def ignored?
+ return @ignored unless @ignored.nil?
+
+ @ignored = missing_extensions?
+ return false unless @ignored
+
+ warn "Source #{source} is ignoring #{self} because it is missing extensions"
+
+ true
+ end
+
+ def manually_installed?
+ # This is for manually installed gems which are gems that were fixed in place after a
+ # failed installation. Once the issue was resolved, the user then manually created
+ # the gem specification using the instructions provided by `gem help install`
+ installed_by_version == Gem::Version.new(0)
+ end
+
+ # This is defined directly to avoid having to loading the full spec
+ def missing_extensions?
+ return false if RUBY_ENGINE == "jruby"
+ return false if default_gem?
+ return false if extensions.empty?
+ return false if File.exist? gem_build_complete_path
+ return false if manually_installed?
+
+ true
+ end
+
+ def activated?
+ stub.activated?
+ end
+
+ def activated=(activated)
+ stub.instance_variable_set(:@activated, activated)
+ end
+
+ def extensions
+ stub.extensions
+ end
+
+ def gem_build_complete_path
+ stub.gem_build_complete_path
+ end
+
+ def default_gem?
+ stub.default_gem?
+ end
+
+ def full_gem_path
+ stub.full_gem_path
+ end
+
+ def full_gem_path=(path)
+ stub.full_gem_path = path
+ end
+
+ def full_require_paths
+ stub.full_require_paths
+ end
+
+ def require_paths
+ stub.require_paths
+ end
+
+ def base_dir=(path)
+ stub.base_dir = path
+ end
+
+ def load_paths
+ full_require_paths
+ end
+
+ def loaded_from
+ stub.loaded_from
+ end
+
+ def matches_for_glob(glob)
+ stub.matches_for_glob(glob)
+ end
+
+ def raw_require_paths
+ stub.raw_require_paths
+ end
+
+ def inspect
+ "#<#{self.class} @name=\"#{name}\" (#{full_name.delete_prefix("#{name}-")})>"
+ end
+
+ private
+
+ def _remote_specification
+ @_remote_specification ||= begin
+ rs = stub.to_spec
+ if rs.equal?(self) # happens when to_spec gets the spec from Gem.loaded_specs
+ rs = Gem::Specification.load(loaded_from)
+ Bundler.rubygems.stub_set_spec(stub, rs)
+ end
+
+ unless rs
+ raise GemspecError, "The gemspec for #{full_name} at #{loaded_from}" \
+ " was missing or broken. Try running `gem pristine #{name} -v #{version}`" \
+ " to fix the cached spec."
+ end
+
+ rs.source = source
+ rs.base_dir = stub.base_dir
+
+ rs
+ end
+ end
+ end
+end
diff --git a/lib/bundler/templates/.document b/lib/bundler/templates/.document
new file mode 100644
index 0000000000..fb66f13c33
--- /dev/null
+++ b/lib/bundler/templates/.document
@@ -0,0 +1 @@
+# Ignore all files in this directory
diff --git a/lib/bundler/templates/Executable b/lib/bundler/templates/Executable
new file mode 100644
index 0000000000..b085c24da6
--- /dev/null
+++ b/lib/bundler/templates/Executable
@@ -0,0 +1,16 @@
+#!/usr/bin/env <%= Bundler.settings[:shebang] || RbConfig::CONFIG["ruby_install_name"] %>
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application '<%= executable %>' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("<%= relative_gemfile_path %>", __dir__)
+
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("<%= spec.name %>", "<%= executable %>")
diff --git a/lib/bundler/templates/Executable.standalone b/lib/bundler/templates/Executable.standalone
new file mode 100644
index 0000000000..3117a27e86
--- /dev/null
+++ b/lib/bundler/templates/Executable.standalone
@@ -0,0 +1,14 @@
+#!/usr/bin/env <%= Bundler.settings[:shebang] || RbConfig::CONFIG["ruby_install_name"] %>
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application '<%= executable %>' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+$:.unshift File.expand_path "<%= standalone_path %>", __dir__
+
+require "bundler/setup"
+load File.expand_path "<%= executable_path %>", __dir__
diff --git a/lib/bundler/templates/Gemfile b/lib/bundler/templates/Gemfile
new file mode 100644
index 0000000000..d2403f18b2
--- /dev/null
+++ b/lib/bundler/templates/Gemfile
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+# gem "rails"
diff --git a/lib/bundler/templates/newgem/CHANGELOG.md.tt b/lib/bundler/templates/newgem/CHANGELOG.md.tt
new file mode 100644
index 0000000000..c9ea96d453
--- /dev/null
+++ b/lib/bundler/templates/newgem/CHANGELOG.md.tt
@@ -0,0 +1,5 @@
+## [Unreleased]
+
+## [0.1.0] - <%= Time.now.strftime('%F') %>
+
+- Initial release
diff --git a/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt
new file mode 100644
index 0000000000..633baebdd5
--- /dev/null
+++ b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt
@@ -0,0 +1,10 @@
+# Code of Conduct
+
+<%= config[:name].inspect %> follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
+
+* Participants will be tolerant of opposing views.
+* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
+* When interpreting the words and actions of others, participants should always assume good intentions.
+* Behaviour which can be reasonably considered harassment will not be tolerated.
+
+If you have any concerns about behaviour within this project, please contact us at [<%= config[:email].inspect %>](mailto:<%= config[:email].inspect %>).
diff --git a/lib/bundler/templates/newgem/Cargo.toml.tt b/lib/bundler/templates/newgem/Cargo.toml.tt
new file mode 100644
index 0000000000..cd00f97e5a
--- /dev/null
+++ b/lib/bundler/templates/newgem/Cargo.toml.tt
@@ -0,0 +1,13 @@
+# This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is
+# a Rust project. Your extensions dependencies should be added to the Cargo.toml
+# in the ext/ directory.
+
+[workspace]
+members = ["./ext/<%= config[:name] %>"]
+resolver = "2"
+
+[profile.release]
+# By default, debug symbols are stripped from the final binary which makes it
+# harder to debug if something goes wrong. It's recommended to keep debug
+# symbols in the release build so that you can debug the final binary if needed.
+debug = true
diff --git a/lib/bundler/templates/newgem/Gemfile.tt b/lib/bundler/templates/newgem/Gemfile.tt
new file mode 100644
index 0000000000..85dc593b8f
--- /dev/null
+++ b/lib/bundler/templates/newgem/Gemfile.tt
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+# Specify your gem's dependencies in <%= config[:name] %>.gemspec
+gemspec
+
+gem "irb"
+gem "rake", ">= 13.0"
+<%- if config[:ext] -%>
+
+gem "rake-compiler"
+<%- end -%>
+<%- if config[:test] -%>
+
+gem "<%= config[:test] %>"
+<%- end -%>
+<%- if config[:linter] == "rubocop" -%>
+
+gem "rubocop"
+<%- elsif config[:linter] == "standard" -%>
+
+gem "standard"
+<%- end -%>
diff --git a/lib/bundler/templates/newgem/LICENSE.txt.tt b/lib/bundler/templates/newgem/LICENSE.txt.tt
new file mode 100644
index 0000000000..76ef4b0191
--- /dev/null
+++ b/lib/bundler/templates/newgem/LICENSE.txt.tt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) <%= Time.now.year %> <%= config[:author] %>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/lib/bundler/templates/newgem/README.md.tt b/lib/bundler/templates/newgem/README.md.tt
new file mode 100644
index 0000000000..0ec6a12fa7
--- /dev/null
+++ b/lib/bundler/templates/newgem/README.md.tt
@@ -0,0 +1,49 @@
+# <%= config[:constant_name] %>
+
+TODO: Delete this and the text below, and describe your gem
+
+Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/<%= config[:namespaced_path] %>`. To experiment with that code, run `bin/console` for an interactive prompt.
+
+## Installation
+
+TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
+
+Install the gem and add to the application's Gemfile by executing:
+
+```bash
+bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
+```
+
+If bundler is not being used to manage dependencies, install the gem by executing:
+
+```bash
+gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
+```
+
+## Usage
+
+TODO: Write usage instructions here
+
+## Development
+
+After checking out the repo, run `bin/setup` to install dependencies.<% if config[:test] %> Then, run `rake <%= config[:test_task] %>` to run the tests.<% end %> You can also run `bin/console` for an interactive prompt that will allow you to experiment.<% if config[:bin] %> Run `bundle exec <%= config[:name] %>` to use the gem in this directory, ignoring other installed copies of this gem.<% end %>
+
+To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
+<% if config[:git] -%>
+
+## Contributing
+
+Bug reports and pull requests are welcome on GitHub at https://github.com/<%= config[:github_username] %>/<%= config[:name] %>.<% if config[:coc] %> This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/<%= config[:github_username] %>/<%= config[:name] %>/blob/<%= config[:git_default_branch] %>/CODE_OF_CONDUCT.md).<% end %>
+<% end -%>
+<% if config[:mit] -%>
+
+## License
+
+The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
+<% end -%>
+<% if config[:git] && config[:coc] -%>
+
+## Code of Conduct
+
+Everyone interacting in the <%= config[:constant_name] %> project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/<%= config[:github_username] %>/<%= config[:name] %>/blob/<%= config[:git_default_branch] %>/CODE_OF_CONDUCT.md).
+<% end -%>
diff --git a/lib/bundler/templates/newgem/Rakefile.tt b/lib/bundler/templates/newgem/Rakefile.tt
new file mode 100644
index 0000000000..83f10009c7
--- /dev/null
+++ b/lib/bundler/templates/newgem/Rakefile.tt
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require "bundler/gem_tasks"
+<% default_task_names = [config[:test_task]].compact -%>
+<% case config[:test] -%>
+<% when "minitest" -%>
+require "minitest/test_task"
+
+Minitest::TestTask.create
+
+<% when "test-unit" -%>
+require "rake/testtask"
+
+Rake::TestTask.new(:test) do |t|
+ t.libs << "test"
+ t.libs << "lib"
+ t.test_files = FileList["test/**/*_test.rb"]
+end
+
+<% when "rspec" -%>
+require "rspec/core/rake_task"
+
+RSpec::Core::RakeTask.new(:spec)
+
+<% end -%>
+<% if config[:linter] == "rubocop" -%>
+<% default_task_names << :rubocop -%>
+require "rubocop/rake_task"
+
+RuboCop::RakeTask.new
+
+<% elsif config[:linter] == "standard" -%>
+<% default_task_names << :standard -%>
+require "standard/rake"
+
+<% end -%>
+<% if config[:ext] -%>
+<% default_task_names.unshift(:compile) -%>
+<% default_task_names.unshift(:clobber) unless config[:ext] == 'rust' -%>
+<% if config[:ext] == 'rust' -%>
+require "rb_sys/extensiontask"
+
+task build: :compile
+
+GEMSPEC = Gem::Specification.load("<%= config[:underscored_name] %>.gemspec")
+
+RbSys::ExtensionTask.new(<%= config[:name].inspect %>, GEMSPEC) do |ext|
+ ext.lib_dir = "lib/<%= config[:namespaced_path] %>"
+end
+<% else -%>
+require "rake/extensiontask"
+
+task build: :compile
+
+GEMSPEC = Gem::Specification.load("<%= config[:underscored_name] %>.gemspec")
+
+Rake::ExtensionTask.new("<%= config[:underscored_name] %>", GEMSPEC) do |ext|
+ ext.lib_dir = "lib/<%= config[:namespaced_path] %>"
+end
+<% end -%>
+
+<% if config[:ext] == "go" -%>
+require "go_gem/rake_task"
+
+GoGem::RakeTask.new("<%= config[:underscored_name] %>")
+<% end -%>
+<% end -%>
+<% if default_task_names.size == 1 -%>
+task default: <%= default_task_names.first.inspect %>
+<% else -%>
+task default: %i[<%= default_task_names.join(" ") %>]
+<% end -%>
diff --git a/lib/bundler/templates/newgem/bin/console.tt b/lib/bundler/templates/newgem/bin/console.tt
new file mode 100644
index 0000000000..c91ee65f93
--- /dev/null
+++ b/lib/bundler/templates/newgem/bin/console.tt
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "<%= config[:namespaced_path] %>"
+
+# You can add fixtures and/or initialization code here to make experimenting
+# with your gem easier. You can also use a different console, if you like.
+
+require "irb"
+IRB.start(__FILE__)
diff --git a/lib/bundler/templates/newgem/bin/setup.tt b/lib/bundler/templates/newgem/bin/setup.tt
new file mode 100644
index 0000000000..dce67d860a
--- /dev/null
+++ b/lib/bundler/templates/newgem/bin/setup.tt
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+IFS=$'\n\t'
+set -vx
+
+bundle install
+
+# Do any other automated setup that you need to do here
diff --git a/lib/bundler/templates/newgem/circleci/config.yml.tt b/lib/bundler/templates/newgem/circleci/config.yml.tt
new file mode 100644
index 0000000000..c4dd9d0647
--- /dev/null
+++ b/lib/bundler/templates/newgem/circleci/config.yml.tt
@@ -0,0 +1,37 @@
+version: 2.1
+jobs:
+ build:
+ docker:
+ - image: ruby:<%= RUBY_VERSION %>
+<%- if config[:ext] == 'rust' -%>
+ environment:
+ RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN: 'true'
+<%- end -%>
+<%- if config[:ext] == 'go' -%>
+ environment:
+ GO_VERSION: '1.23.0'
+<%- end -%>
+ steps:
+ - checkout
+<%- if config[:ext] == 'rust' -%>
+ - run:
+ name: Install Rust/Cargo dependencies
+ command: apt-get update && apt-get install -y clang
+ - run:
+ name: Install a RubyGems version that can compile rust extensions
+ command: gem update --system '<%= ::Gem.rubygems_version %>'
+<%- end -%>
+<%- if config[:ext] == 'go' -%>
+ - run:
+ name: Install Go
+ command: |
+ wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz
+ tar -C /usr/local -xzf /tmp/go.tar.gz
+ echo 'export PATH=/usr/local/go/bin:"$PATH"' >> "$BASH_ENV"
+<%- end -%>
+ - run:
+ name: Run the default task
+ command: |
+ gem install bundler -v <%= Bundler::VERSION %>
+ bundle install
+ bundle exec rake
diff --git a/lib/bundler/templates/newgem/exe/newgem.tt b/lib/bundler/templates/newgem/exe/newgem.tt
new file mode 100644
index 0000000000..a8339bb79f
--- /dev/null
+++ b/lib/bundler/templates/newgem/exe/newgem.tt
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+
+require "<%= config[:namespaced_path] %>"
diff --git a/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt b/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt
new file mode 100644
index 0000000000..a06166aee7
--- /dev/null
+++ b/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt
@@ -0,0 +1,22 @@
+[package]
+name = <%= config[:name].inspect %>
+version = "0.1.0"
+edition = "2021"
+authors = ["<%= config[:author] %> <<%= config[:email] %>>"]
+<%- if config[:mit] -%>
+license = "MIT"
+<%- end -%>
+publish = false
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+magnus = { version = "0.8.2" }
+rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
+
+[build-dependencies]
+rb-sys-env = "0.2.2"
+
+[dev-dependencies]
+rb-sys-test-helpers = { version = "0.2.2" }
diff --git a/lib/bundler/templates/newgem/ext/newgem/build.rs.tt b/lib/bundler/templates/newgem/ext/newgem/build.rs.tt
new file mode 100644
index 0000000000..80a7842753
--- /dev/null
+++ b/lib/bundler/templates/newgem/ext/newgem/build.rs.tt
@@ -0,0 +1,5 @@
+pub fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let _ = rb_sys_env::activate()?;
+
+ Ok(())
+}
diff --git a/lib/bundler/templates/newgem/ext/newgem/extconf-c.rb.tt b/lib/bundler/templates/newgem/ext/newgem/extconf-c.rb.tt
new file mode 100644
index 0000000000..0a0c5a3d09
--- /dev/null
+++ b/lib/bundler/templates/newgem/ext/newgem/extconf-c.rb.tt
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require "mkmf"
+
+# Makes all symbols private by default to avoid unintended conflict
+# with other gems. To explicitly export symbols you can use RUBY_FUNC_EXPORTED
+# selectively, or entirely remove this flag.
+append_cflags("-fvisibility=hidden")
+
+create_makefile(<%= config[:makefile_path].inspect %>)
diff --git a/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt b/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt
new file mode 100644
index 0000000000..a689e21ebe
--- /dev/null
+++ b/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require "mkmf"
+require "go_gem/mkmf"
+
+# Makes all symbols private by default to avoid unintended conflict
+# with other gems. To explicitly export symbols you can use RUBY_FUNC_EXPORTED
+# selectively, or entirely remove this flag.
+append_cflags("-fvisibility=hidden")
+
+create_go_makefile(<%= config[:makefile_path].inspect %>)
diff --git a/lib/bundler/templates/newgem/ext/newgem/extconf-rust.rb.tt b/lib/bundler/templates/newgem/ext/newgem/extconf-rust.rb.tt
new file mode 100644
index 0000000000..e24566a17a
--- /dev/null
+++ b/lib/bundler/templates/newgem/ext/newgem/extconf-rust.rb.tt
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require "mkmf"
+require "rb_sys/mkmf"
+
+create_rust_makefile(<%= config[:makefile_path].inspect %>)
diff --git a/lib/bundler/templates/newgem/ext/newgem/go.mod.tt b/lib/bundler/templates/newgem/ext/newgem/go.mod.tt
new file mode 100644
index 0000000000..3f4819d004
--- /dev/null
+++ b/lib/bundler/templates/newgem/ext/newgem/go.mod.tt
@@ -0,0 +1,5 @@
+module github.com/<%= config[:go_module_username] %>/<%= config[:underscored_name] %>
+
+go 1.23
+
+require github.com/ruby-go-gem/go-gem-wrapper latest
diff --git a/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt b/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt
new file mode 100644
index 0000000000..119c0c96ea
--- /dev/null
+++ b/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt
@@ -0,0 +1,2 @@
+#include "<%= config[:underscored_name] %>.h"
+#include "_cgo_export.h"
diff --git a/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt b/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt
new file mode 100644
index 0000000000..bcd5148569
--- /dev/null
+++ b/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt
@@ -0,0 +1,9 @@
+#include "<%= config[:underscored_name] %>.h"
+
+VALUE rb_m<%= config[:constant_array].join %>;
+
+RUBY_FUNC_EXPORTED void
+Init_<%= config[:underscored_name] %>(void)
+{
+ rb_m<%= config[:constant_array].join %> = rb_define_module(<%= config[:constant_name].inspect %>);
+}
diff --git a/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt b/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt
new file mode 100644
index 0000000000..f19b750e58
--- /dev/null
+++ b/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt
@@ -0,0 +1,31 @@
+package main
+
+/*
+#include "<%= config[:underscored_name] %>.h"
+
+VALUE rb_<%= config[:underscored_name] %>_sum(VALUE self, VALUE a, VALUE b);
+*/
+import "C"
+
+import (
+ "github.com/ruby-go-gem/go-gem-wrapper/ruby"
+)
+
+//export rb_<%= config[:underscored_name] %>_sum
+func rb_<%= config[:underscored_name] %>_sum(_ C.VALUE, a C.VALUE, b C.VALUE) C.VALUE {
+ longA := ruby.NUM2LONG(ruby.VALUE(a))
+ longB := ruby.NUM2LONG(ruby.VALUE(b))
+
+ sum := longA + longB
+
+ return C.VALUE(ruby.LONG2NUM(sum))
+}
+
+//export Init_<%= config[:underscored_name] %>
+func Init_<%= config[:underscored_name] %>() {
+ rb_m<%= config[:constant_array].join %> := ruby.RbDefineModule(<%= config[:constant_name].inspect %>)
+ ruby.RbDefineSingletonMethod(rb_m<%= config[:constant_array].join %>, "sum", C.rb_<%= config[:underscored_name] %>_sum, 2)
+}
+
+func main() {
+}
diff --git a/lib/bundler/templates/newgem/ext/newgem/newgem.h.tt b/lib/bundler/templates/newgem/ext/newgem/newgem.h.tt
new file mode 100644
index 0000000000..c6e420b66e
--- /dev/null
+++ b/lib/bundler/templates/newgem/ext/newgem/newgem.h.tt
@@ -0,0 +1,6 @@
+#ifndef <%= config[:underscored_name].upcase %>_H
+#define <%= config[:underscored_name].upcase %>_H 1
+
+#include "ruby.h"
+
+#endif /* <%= config[:underscored_name].upcase %>_H */
diff --git a/lib/bundler/templates/newgem/ext/newgem/src/lib.rs.tt b/lib/bundler/templates/newgem/ext/newgem/src/lib.rs.tt
new file mode 100644
index 0000000000..09ce97682d
--- /dev/null
+++ b/lib/bundler/templates/newgem/ext/newgem/src/lib.rs.tt
@@ -0,0 +1,23 @@
+use magnus::{function, prelude::*, Error, Ruby};
+
+pub fn hello(subject: String) -> String {
+ format!("Hello {subject}, from Rust!")
+}
+
+#[magnus::init]
+fn init(ruby: &Ruby) -> Result<(), Error> {
+ let module = ruby.<%= config[:constant_array].map {|c| "define_module(#{c.dump})?"}.join(".") %>;
+ module.define_singleton_method("hello", function!(hello, 1))?;
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use rb_sys_test_helpers::ruby_test;
+ use super::hello;
+
+ #[ruby_test]
+ fn test_hello() {
+ assert_eq!("Hello world, from Rust!", hello("world".to_string()));
+ }
+}
diff --git a/lib/bundler/templates/newgem/github/workflows/build-gems.yml.tt b/lib/bundler/templates/newgem/github/workflows/build-gems.yml.tt
new file mode 100644
index 0000000000..d49954d2cd
--- /dev/null
+++ b/lib/bundler/templates/newgem/github/workflows/build-gems.yml.tt
@@ -0,0 +1,69 @@
+---
+name: Build gems
+
+on:
+ push:
+ tags:
+ - "v*"
+ - "cross-gem/*"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ packages: write
+
+jobs:
+ ci-data:
+ runs-on: ubuntu-latest
+ outputs:
+ result: ${{ steps.fetch.outputs.result }}
+ steps:
+ - uses: oxidize-rb/actions/fetch-ci-data@v1
+ id: fetch
+ with:
+ supported-ruby-platforms: |
+ exclude: ["arm-linux", "x64-mingw32"]
+ stable-ruby-versions: |
+ exclude: ["head"]
+
+ source-gem:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true
+
+ - name: Build gem
+ run: bundle exec rake build
+
+ - uses: actions/upload-artifact@v7
+ with:
+ name: source-gem
+ path: pkg/*.gem
+
+ cross-gem:
+ name: Compile native gem for ${{ matrix.platform }}
+ runs-on: ubuntu-latest
+ needs: ci-data
+ strategy:
+ matrix:
+ platform: ${{ fromJSON(needs.ci-data.outputs.result).supported-ruby-platforms }}
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true
+
+ - uses: oxidize-rb/actions/cross-gem@v1
+ id: cross-gem
+ with:
+ platform: ${{ matrix.platform }}
+ ruby-versions: ${{ join(fromJSON(needs.ci-data.outputs.result).stable-ruby-versions, ',') }}
+
+ - uses: actions/upload-artifact@v7
+ with:
+ name: cross-gem
+ path: ${{ steps.cross-gem.outputs.gem-path }}
diff --git a/lib/bundler/templates/newgem/github/workflows/main.yml.tt b/lib/bundler/templates/newgem/github/workflows/main.yml.tt
new file mode 100644
index 0000000000..cc8f04dd33
--- /dev/null
+++ b/lib/bundler/templates/newgem/github/workflows/main.yml.tt
@@ -0,0 +1,48 @@
+name: Ruby
+
+on:
+ push:
+ branches:
+ - <%= config[:git_default_branch] %>
+
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ name: Ruby ${{ matrix.ruby }}
+ strategy:
+ matrix:
+ ruby:
+ - '<%= RUBY_VERSION %>'
+
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+<%- if config[:ext] == 'rust' -%>
+ - name: Set up Ruby & Rust
+ uses: oxidize-rb/actions/setup-ruby-and-rust@v1
+ with:
+ ruby-version: ${{ matrix.ruby }}
+ bundler-cache: true
+ cargo-cache: true
+ rubygems: '<%= ::Gem.rubygems_version %>'
+<%- else -%>
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby }}
+ bundler-cache: true
+<%- end -%>
+<%- if config[:ext] == 'go' -%>
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: ext/<%= config[:underscored_name] %>/go.mod
+<%- end -%>
+ - name: Run the default task
+ run: bundle exec rake
diff --git a/lib/bundler/templates/newgem/gitignore.tt b/lib/bundler/templates/newgem/gitignore.tt
new file mode 100644
index 0000000000..9b40ba5a58
--- /dev/null
+++ b/lib/bundler/templates/newgem/gitignore.tt
@@ -0,0 +1,23 @@
+/.bundle/
+/.yardoc
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/spec/reports/
+/tmp/
+<%- if config[:ext] -%>
+*.bundle
+*.so
+*.o
+*.a
+mkmf.log
+<%- if config[:ext] == 'rust' -%>
+target/
+<%- end -%>
+<%- end -%>
+<%- if config[:test] == "rspec" -%>
+
+# rspec failure tracking
+.rspec_status
+<%- end -%>
diff --git a/lib/bundler/templates/newgem/gitlab-ci.yml.tt b/lib/bundler/templates/newgem/gitlab-ci.yml.tt
new file mode 100644
index 0000000000..adbd70cbc0
--- /dev/null
+++ b/lib/bundler/templates/newgem/gitlab-ci.yml.tt
@@ -0,0 +1,27 @@
+default:
+ image: ruby:<%= RUBY_VERSION %>
+
+ before_script:
+<%- if config[:ext] == 'rust' -%>
+ - apt-get update && apt-get install -y clang
+ - gem update --system '<%= ::Gem.rubygems_version %>'
+<%- end -%>
+<%- if config[:ext] == 'go' -%>
+ - wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz
+ - tar -C /usr/local -xzf /tmp/go.tar.gz
+ - export PATH=/usr/local/go/bin:$PATH
+<%- end -%>
+ - gem install bundler -v <%= Bundler::VERSION %>
+ - bundle install
+
+example_job:
+<%- if config[:ext] == 'rust' -%>
+ variables:
+ RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN: 'true'
+<%- end -%>
+<%- if config[:ext] == 'go' -%>
+ variables:
+ GO_VERSION: '1.23.0'
+<%- end -%>
+ script:
+ - bundle exec rake
diff --git a/lib/bundler/templates/newgem/lib/newgem.rb.tt b/lib/bundler/templates/newgem/lib/newgem.rb.tt
new file mode 100644
index 0000000000..3aedee0d25
--- /dev/null
+++ b/lib/bundler/templates/newgem/lib/newgem.rb.tt
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative "<%= File.basename(config[:namespaced_path]) %>/version"
+<%- if config[:ext] -%>
+require "<%= File.basename(config[:namespaced_path]) %>/<%= config[:underscored_name] %>"
+<%- end -%>
+
+<%- config[:constant_array].each_with_index do |c, i| -%>
+<%= " " * i %>module <%= c %>
+<%- end -%>
+<%= " " * config[:constant_array].size %>class Error < StandardError; end
+<%= " " * config[:constant_array].size %># Your code goes here...
+<%- (config[:constant_array].size-1).downto(0) do |i| -%>
+<%= " " * i %>end
+<%- end -%>
diff --git a/lib/bundler/templates/newgem/lib/newgem/version.rb.tt b/lib/bundler/templates/newgem/lib/newgem/version.rb.tt
new file mode 100644
index 0000000000..b5cd4cb232
--- /dev/null
+++ b/lib/bundler/templates/newgem/lib/newgem/version.rb.tt
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+<%- config[:constant_array].each_with_index do |c, i| -%>
+<%= " " * i %>module <%= c %>
+<%- end -%>
+<%= " " * config[:constant_array].size %>VERSION = "0.1.0"
+<%- (config[:constant_array].size-1).downto(0) do |i| -%>
+<%= " " * i %>end
+<%- end -%>
diff --git a/lib/bundler/templates/newgem/newgem.gemspec.tt b/lib/bundler/templates/newgem/newgem.gemspec.tt
new file mode 100644
index 0000000000..1ab1c28f46
--- /dev/null
+++ b/lib/bundler/templates/newgem/newgem.gemspec.tt
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require_relative "lib/<%=config[:namespaced_path]%>/version"
+
+Gem::Specification.new do |spec|
+ spec.name = <%= config[:name].inspect %>
+ spec.version = <%= config[:constant_name] %>::VERSION
+ spec.authors = [<%= config[:author].inspect %>]
+ spec.email = [<%= config[:email].inspect %>]
+
+ spec.summary = "TODO: Write a short summary, because RubyGems requires one."
+ spec.description = "TODO: Write a longer description or delete this line."
+ spec.homepage = "<%= config[:homepage_uri] %>"
+<%- if config[:mit] -%>
+ spec.license = "MIT"
+<%- end -%>
+ spec.required_ruby_version = ">= <%= config[:required_ruby_version] %>"
+ spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = "<%= config[:source_code_uri] %>"
+<%- if config[:changelog] -%>
+ spec.metadata["changelog_uri"] = "<%= config[:changelog_uri] %>"
+<%- end -%>
+
+ # Uncomment the line below to require MFA for gem pushes.
+ # This helps protect your gem from supply chain attacks by ensuring
+ # no one can publish a new version without multi-factor authentication.
+ # See: https://guides.rubygems.org/mfa-requirement-opt-in/
+ # spec.metadata["rubygems_mfa_required"] = "true"
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ gemspec = File.basename(__FILE__)
+ spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
+ ls.readlines("\x0", chomp: true).reject do |f|
+ (f == gemspec) ||
+ f.start_with?(*%w[<%= config[:ignore_paths].join(" ") %>])
+ end
+ end
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+<%- if %w(c rust go).include?(config[:ext]) -%>
+ spec.extensions = ["ext/<%= config[:underscored_name] %>/extconf.rb"]
+<%- end -%>
+
+ # Uncomment to register a new dependency of your gem
+ # spec.add_dependency "example-gem", ">= 1.0"
+<%- if config[:ext] == 'rust' -%>
+ spec.add_dependency "rb_sys", ">= 0.9.128"
+<%- end -%>
+<%- if config[:ext] == 'go' -%>
+ spec.add_dependency "go_gem", ">= 0.2"
+<%- end -%>
+
+ # For more information and examples about making a new gem, check out our
+ # guide at: https://guides.rubygems.org/make-your-own-gem/
+end
diff --git a/lib/bundler/templates/newgem/rspec.tt b/lib/bundler/templates/newgem/rspec.tt
new file mode 100644
index 0000000000..34c5164d9b
--- /dev/null
+++ b/lib/bundler/templates/newgem/rspec.tt
@@ -0,0 +1,3 @@
+--format documentation
+--color
+--require spec_helper
diff --git a/lib/bundler/templates/newgem/rubocop.yml.tt b/lib/bundler/templates/newgem/rubocop.yml.tt
new file mode 100644
index 0000000000..3d1c4ee7b2
--- /dev/null
+++ b/lib/bundler/templates/newgem/rubocop.yml.tt
@@ -0,0 +1,8 @@
+AllCops:
+ TargetRubyVersion: <%= ::Gem::Version.new(config[:required_ruby_version]).segments[0..1].join(".") %>
+
+Style/StringLiterals:
+ EnforcedStyle: double_quotes
+
+Style/StringLiteralsInInterpolation:
+ EnforcedStyle: double_quotes
diff --git a/lib/bundler/templates/newgem/sig/newgem.rbs.tt b/lib/bundler/templates/newgem/sig/newgem.rbs.tt
new file mode 100644
index 0000000000..eb7b380bbb
--- /dev/null
+++ b/lib/bundler/templates/newgem/sig/newgem.rbs.tt
@@ -0,0 +1,8 @@
+<%- config[:constant_array].each_with_index do |c, i| -%>
+<%= " " * i %>module <%= c %>
+<%- end -%>
+<%= " " * config[:constant_array].size %>VERSION: String
+<%= " " * config[:constant_array].size %># See the writing guide of rbs: https://github.com/ruby/rbs#guides
+<%- (config[:constant_array].size-1).downto(0) do |i| -%>
+<%= " " * i %>end
+<%- end -%>
diff --git a/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt b/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt
new file mode 100644
index 0000000000..7c6cde170b
--- /dev/null
+++ b/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.describe <%= config[:constant_name] %> do
+ it "has a version number" do
+ expect(<%= config[:constant_name] %>::VERSION).not_to be nil
+ end
+
+<%- if config[:ext] == 'rust' -%>
+ it "can call into Rust" do
+ result = <%= config[:constant_name] %>.hello("world")
+
+ expect(result).to eq("Hello world, from Rust!")
+ end
+<%- else -%>
+ it "does something useful" do
+ expect(false).to eq(true)
+ end
+<%- end -%>
+end
diff --git a/lib/bundler/templates/newgem/spec/spec_helper.rb.tt b/lib/bundler/templates/newgem/spec/spec_helper.rb.tt
new file mode 100644
index 0000000000..70c6d1fcde
--- /dev/null
+++ b/lib/bundler/templates/newgem/spec/spec_helper.rb.tt
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "<%= config[:namespaced_path] %>"
+
+RSpec.configure do |config|
+ # Enable flags like --only-failures and --next-failure
+ config.example_status_persistence_file_path = ".rspec_status"
+
+ # Disable RSpec exposing methods globally on `Module` and `main`
+ config.disable_monkey_patching!
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end
diff --git a/lib/bundler/templates/newgem/standard.yml.tt b/lib/bundler/templates/newgem/standard.yml.tt
new file mode 100644
index 0000000000..a0696cd2e9
--- /dev/null
+++ b/lib/bundler/templates/newgem/standard.yml.tt
@@ -0,0 +1,3 @@
+# For available configuration options, see:
+# https://github.com/standardrb/standard
+ruby_version: <%= ::Gem::Version.new(config[:required_ruby_version]).segments[0..1].join(".") %>
diff --git a/lib/bundler/templates/newgem/test/minitest/test_helper.rb.tt b/lib/bundler/templates/newgem/test/minitest/test_helper.rb.tt
new file mode 100644
index 0000000000..e05c387bfa
--- /dev/null
+++ b/lib/bundler/templates/newgem/test/minitest/test_helper.rb.tt
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
+require "<%= config[:namespaced_path] %>"
+
+require "minitest/autorun"
diff --git a/lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt b/lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt
new file mode 100644
index 0000000000..844d3aff81
--- /dev/null
+++ b/lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class <%= config[:minitest_constant_name] %> < Minitest::Test
+ def test_that_it_has_a_version_number
+ refute_nil ::<%= config[:constant_name] %>::VERSION
+ end
+
+<%- if config[:ext] == 'rust' -%>
+ def test_hello_world
+ assert_equal "Hello world, from Rust!", <%= config[:constant_name] %>.hello("world")
+ end
+<%- else -%>
+ def test_it_does_something_useful
+ assert false
+ end
+<%- end -%>
+end
diff --git a/lib/bundler/templates/newgem/test/test-unit/newgem_test.rb.tt b/lib/bundler/templates/newgem/test/test-unit/newgem_test.rb.tt
new file mode 100644
index 0000000000..5c61094e62
--- /dev/null
+++ b/lib/bundler/templates/newgem/test/test-unit/newgem_test.rb.tt
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class <%= config[:constant_name] %>Test < Test::Unit::TestCase
+ test "VERSION" do
+ assert do
+ ::<%= config[:constant_name] %>.const_defined?(:VERSION)
+ end
+ end
+
+ test "something useful" do
+ assert_equal("expected", "actual")
+ end
+end
diff --git a/lib/bundler/templates/newgem/test/test-unit/test_helper.rb.tt b/lib/bundler/templates/newgem/test/test-unit/test_helper.rb.tt
new file mode 100644
index 0000000000..6f633c6039
--- /dev/null
+++ b/lib/bundler/templates/newgem/test/test-unit/test_helper.rb.tt
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
+require "<%= config[:namespaced_path] %>"
+
+require "test-unit"
diff --git a/lib/bundler/ui.rb b/lib/bundler/ui.rb
new file mode 100644
index 0000000000..7a4fa03669
--- /dev/null
+++ b/lib/bundler/ui.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Bundler
+ module UI
+ autoload :RGProxy, File.expand_path("ui/rg_proxy", __dir__)
+ autoload :Shell, File.expand_path("ui/shell", __dir__)
+ autoload :Silent, File.expand_path("ui/silent", __dir__)
+ end
+end
diff --git a/lib/bundler/ui/rg_proxy.rb b/lib/bundler/ui/rg_proxy.rb
new file mode 100644
index 0000000000..b17ca65f53
--- /dev/null
+++ b/lib/bundler/ui/rg_proxy.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require_relative "../ui"
+require "rubygems/user_interaction"
+
+module Bundler
+ module UI
+ class RGProxy < ::Gem::SilentUI
+ def initialize(ui)
+ @ui = ui
+ super()
+ end
+
+ def say(message)
+ @ui&.debug(message)
+ end
+ end
+ end
+end
diff --git a/lib/bundler/ui/shell.rb b/lib/bundler/ui/shell.rb
new file mode 100644
index 0000000000..b836208da8
--- /dev/null
+++ b/lib/bundler/ui/shell.rb
@@ -0,0 +1,191 @@
+# frozen_string_literal: true
+
+require_relative "../vendored_thor"
+
+module Bundler
+ module UI
+ class Shell
+ LEVELS = %w[silent error warn confirm info debug].freeze
+ OUTPUT_STREAMS = [:stdout, :stderr].freeze
+
+ attr_writer :shell
+ attr_reader :output_stream
+
+ def initialize(options = {})
+ Thor::Base.shell = options["no-color"] ? Thor::Shell::Basic : nil
+ @shell = Thor::Base.shell.new
+ @level = ENV["DEBUG"] ? "debug" : "info"
+ @warning_history = []
+ @output_stream = :stdout
+ @thread_safe_logger_key = "logger_level_#{object_id}"
+ end
+
+ def add_color(string, *color)
+ @shell.set_color(string, *color)
+ end
+
+ def info(msg = nil, newline = nil)
+ return unless info?
+
+ tell_me(msg || yield, nil, newline)
+ end
+
+ def confirm(msg = nil, newline = nil)
+ return unless confirm?
+
+ tell_me(msg || yield, :green, newline)
+ end
+
+ def warn(msg = nil, newline = nil, color = :yellow)
+ return unless warn?
+ return if @warning_history.include? msg
+ @warning_history << msg
+
+ tell_err(msg || yield, color, newline)
+ end
+
+ def error(msg = nil, newline = nil, color = :red)
+ return unless error?
+
+ tell_err(msg || yield, color, newline)
+ end
+
+ def debug(msg = nil, newline = nil)
+ return unless debug?
+
+ tell_me(msg || yield, nil, newline)
+ end
+
+ def info?
+ level("info")
+ end
+
+ def confirm?
+ level("confirm")
+ end
+
+ def warn?
+ level("warn")
+ end
+
+ def error?
+ level("error")
+ end
+
+ def debug?
+ level("debug")
+ end
+
+ def quiet?
+ level("quiet")
+ end
+
+ def ask(msg)
+ @shell.ask(msg, :green)
+ end
+
+ def yes?(msg)
+ @shell.yes?(msg, :green)
+ end
+
+ def no?(msg)
+ @shell.no?(msg)
+ end
+
+ def level=(level)
+ raise ArgumentError unless LEVELS.include?(level.to_s)
+ @level = level.to_s
+ end
+
+ def level(name = nil)
+ current_level = Thread.current.thread_variable_get(@thread_safe_logger_key) || @level
+ return current_level unless name
+
+ unless index = LEVELS.index(name)
+ raise "#{name.inspect} is not a valid level"
+ end
+ index <= LEVELS.index(current_level)
+ end
+
+ def output_stream=(symbol)
+ raise ArgumentError unless OUTPUT_STREAMS.include?(symbol)
+ @output_stream = symbol
+ end
+
+ def trace(e, newline = nil, force = false)
+ return unless debug? || force
+ msg = "#{e.class}: #{e.message}\n#{e.backtrace.join("\n ")}"
+ tell_err(msg, nil, newline)
+ end
+
+ def silence(&blk)
+ with_level("silent", &blk)
+ end
+
+ def progress(&blk)
+ with_output_stream(:stderr, &blk)
+ end
+
+ def unprinted_warnings
+ []
+ end
+
+ private
+
+ # valimism
+ def tell_me(msg, color = nil, newline = nil)
+ return tell_err(msg, color, newline) if output_stream == :stderr
+
+ msg = word_wrap(msg) if newline.is_a?(Hash) && newline[:wrap]
+ if newline.nil?
+ @shell.say(msg, color)
+ else
+ @shell.say(msg, color, newline)
+ end
+ end
+
+ def tell_err(message, color = nil, newline = nil)
+ return if @shell.send(:stderr).closed?
+
+ newline = !message.to_s.match?(/( |\t)\Z/) if newline.nil?
+ message = word_wrap(message) if newline.is_a?(Hash) && newline[:wrap]
+
+ color = nil if color && !$stderr.tty?
+
+ buffer = @shell.send(:prepare_message, message, *color)
+ buffer << "\n" if newline && !message.to_s.end_with?("\n")
+
+ @shell.send(:stderr).print(buffer)
+ @shell.send(:stderr).flush
+ end
+
+ def strip_leading_spaces(text)
+ spaces = text[/\A\s+/, 0]
+ spaces ? text.gsub(/#{spaces}/, "") : text
+ end
+
+ def word_wrap(text, line_width = Thor::Terminal.terminal_width)
+ strip_leading_spaces(text).split("\n").collect do |line|
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
+ end * "\n"
+ end
+
+ def with_level(desired_level)
+ old_level = level
+ Thread.current.thread_variable_set(@thread_safe_logger_key, desired_level)
+
+ yield
+ ensure
+ Thread.current.thread_variable_set(@thread_safe_logger_key, old_level)
+ end
+
+ def with_output_stream(symbol)
+ original = output_stream
+ self.output_stream = symbol
+ yield
+ ensure
+ @output_stream = original
+ end
+ end
+ end
+end
diff --git a/lib/bundler/ui/silent.rb b/lib/bundler/ui/silent.rb
new file mode 100644
index 0000000000..83d31d4b55
--- /dev/null
+++ b/lib/bundler/ui/silent.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+module Bundler
+ module UI
+ class Silent
+ attr_writer :shell
+
+ def initialize
+ @warnings = []
+ end
+
+ def add_color(string, color)
+ string
+ end
+
+ def info(message = nil, newline = nil)
+ end
+
+ def confirm(message = nil, newline = nil)
+ end
+
+ def warn(message = nil, newline = nil)
+ @warnings |= [message]
+ end
+
+ def error(message = nil, newline = nil)
+ end
+
+ def debug(message = nil, newline = nil)
+ end
+
+ def confirm?
+ false
+ end
+
+ def error?
+ false
+ end
+
+ def debug?
+ false
+ end
+
+ def info?
+ false
+ end
+
+ def quiet?
+ false
+ end
+
+ def warn?
+ false
+ end
+
+ def output_stream=(_symbol)
+ end
+
+ def output_stream
+ nil
+ end
+
+ def ask(message)
+ end
+
+ def yes?(msg)
+ raise "Cannot ask yes? with a silent shell"
+ end
+
+ def no?(msg)
+ raise "Cannot ask no? with a silent shell"
+ end
+
+ def level=(name)
+ end
+
+ def level(name = nil)
+ end
+
+ def trace(message, newline = nil, force = false)
+ end
+
+ def silence
+ yield
+ end
+
+ def progress
+ yield
+ end
+
+ def unprinted_warnings
+ @warnings
+ end
+ end
+ end
+end
diff --git a/lib/bundler/uri_credentials_filter.rb b/lib/bundler/uri_credentials_filter.rb
new file mode 100644
index 0000000000..6804187433
--- /dev/null
+++ b/lib/bundler/uri_credentials_filter.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Bundler
+ module URICredentialsFilter
+ module_function
+
+ def credential_filtered_uri(uri_to_anonymize)
+ return uri_to_anonymize if uri_to_anonymize.nil?
+ uri = uri_to_anonymize.dup
+ if uri.is_a?(String)
+ return uri if File.exist?(uri)
+
+ require_relative "vendored_uri"
+ uri = Gem::URI(uri)
+ end
+
+ if uri.userinfo
+ # oauth authentication
+ if uri.password == "x-oauth-basic" || uri.password == "x" || uri.password.nil?
+ # URI as string does not display with password if no user is set
+ oauth_designation = uri.password
+ uri.user = oauth_designation
+ end
+ uri.password = nil
+ end
+ return uri.to_s if uri_to_anonymize.is_a?(String)
+ uri
+ rescue Gem::URI::InvalidURIError # uri is not canonical uri scheme
+ uri
+ end
+
+ def credential_filtered_string(str_to_filter, uri)
+ return str_to_filter if uri.nil? || str_to_filter.nil?
+ str_with_no_credentials = str_to_filter.dup
+ anonymous_uri_str = credential_filtered_uri(uri).to_s
+ uri_str = uri.to_s
+ if anonymous_uri_str != uri_str
+ str_with_no_credentials = str_with_no_credentials.gsub(uri_str, anonymous_uri_str)
+ end
+ str_with_no_credentials
+ end
+ end
+end
diff --git a/lib/bundler/uri_normalizer.rb b/lib/bundler/uri_normalizer.rb
new file mode 100644
index 0000000000..ad08593256
--- /dev/null
+++ b/lib/bundler/uri_normalizer.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Bundler
+ module URINormalizer
+ module_function
+
+ # Normalizes uri to a consistent version, either with or without trailing
+ # slash.
+ #
+ # TODO: Currently gem sources are locked with a trailing slash, while git
+ # sources are locked without a trailing slash. This should be normalized but
+ # the inconsistency is there for now to avoid changing all lockfiles
+ # including GIT sources. We could normalize this on the next major.
+ #
+ def normalize_suffix(uri, trailing_slash: true)
+ if trailing_slash
+ uri.end_with?("/") ? uri : "#{uri}/"
+ else
+ uri.end_with?("/") ? uri.delete_suffix("/") : uri
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/.document b/lib/bundler/vendor/.document
new file mode 100644
index 0000000000..0c43bbd6b3
--- /dev/null
+++ b/lib/bundler/vendor/.document
@@ -0,0 +1 @@
+# Vendored files do not need to be documented
diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool.rb
new file mode 100644
index 0000000000..e8aaf70016
--- /dev/null
+++ b/lib/bundler/vendor/connection_pool/lib/connection_pool.rb
@@ -0,0 +1,233 @@
+require_relative "../../../vendored_timeout"
+require_relative "connection_pool/version"
+
+class Bundler::ConnectionPool
+ class Error < ::RuntimeError; end
+
+ class PoolShuttingDownError < ::Bundler::ConnectionPool::Error; end
+
+ class TimeoutError < ::Gem::Timeout::Error; end
+end
+
+# Generic connection pool class for sharing a limited number of objects or network connections
+# among many threads. Note: pool elements are lazily created.
+#
+# Example usage with block (faster):
+#
+# @pool = Bundler::ConnectionPool.new { Redis.new }
+# @pool.with do |redis|
+# redis.lpop('my-list') if redis.llen('my-list') > 0
+# end
+#
+# Using optional timeout override (for that single invocation)
+#
+# @pool.with(timeout: 2.0) do |redis|
+# redis.lpop('my-list') if redis.llen('my-list') > 0
+# end
+#
+# Example usage replacing an existing connection (slower):
+#
+# $redis = Bundler::ConnectionPool.wrap { Redis.new }
+#
+# def do_work
+# $redis.lpop('my-list') if $redis.llen('my-list') > 0
+# end
+#
+# Accepts the following options:
+# - :size - number of connections to pool, defaults to 5
+# - :timeout - amount of time to wait for a connection if none currently available, defaults to 5 seconds
+# - :auto_reload_after_fork - automatically drop all connections after fork, defaults to true
+#
+class Bundler::ConnectionPool
+ DEFAULTS = {size: 5, timeout: 5, auto_reload_after_fork: true}.freeze
+
+ def self.wrap(options, &block)
+ Wrapper.new(options, &block)
+ end
+
+ if Process.respond_to?(:fork)
+ INSTANCES = ObjectSpace::WeakMap.new
+ private_constant :INSTANCES
+
+ def self.after_fork
+ INSTANCES.values.each do |pool|
+ next unless pool.auto_reload_after_fork
+
+ # We're on after fork, so we know all other threads are dead.
+ # All we need to do is to ensure the main thread doesn't have a
+ # checked out connection
+ pool.checkin(force: true)
+ pool.reload do |connection|
+ # Unfortunately we don't know what method to call to close the connection,
+ # so we try the most common one.
+ connection.close if connection.respond_to?(:close)
+ end
+ end
+ nil
+ end
+
+ if ::Process.respond_to?(:_fork) # MRI 3.1+
+ module ForkTracker
+ def _fork
+ pid = super
+ if pid == 0
+ Bundler::ConnectionPool.after_fork
+ end
+ pid
+ end
+ end
+ Process.singleton_class.prepend(ForkTracker)
+ end
+ else
+ INSTANCES = nil
+ private_constant :INSTANCES
+
+ def self.after_fork
+ # noop
+ end
+ end
+
+ def initialize(options = {}, &block)
+ raise ArgumentError, "Connection pool requires a block" unless block
+
+ options = DEFAULTS.merge(options)
+
+ @size = Integer(options.fetch(:size))
+ @timeout = options.fetch(:timeout)
+ @auto_reload_after_fork = options.fetch(:auto_reload_after_fork)
+
+ @available = TimedStack.new(@size, &block)
+ @key = :"pool-#{@available.object_id}"
+ @key_count = :"pool-#{@available.object_id}-count"
+ @discard_key = :"pool-#{@available.object_id}-discard"
+ INSTANCES[self] = self if @auto_reload_after_fork && INSTANCES
+ end
+
+ def with(options = {})
+ # We need to manage exception handling manually here in order
+ # to work correctly with `Gem::Timeout.timeout` and `Thread#raise`.
+ # Otherwise an interrupted Thread can leak connections.
+ Thread.handle_interrupt(Exception => :never) do
+ conn = checkout(options)
+ begin
+ Thread.handle_interrupt(Exception => :immediate) do
+ yield conn
+ end
+ ensure
+ checkin
+ end
+ end
+ end
+ alias_method :then, :with
+
+ ##
+ # Marks the current thread's checked-out connection for discard.
+ #
+ # When a connection is marked for discard, it will not be returned to the pool
+ # when checked in. Instead, the connection will be discarded.
+ # This is useful when a connection has become invalid or corrupted
+ # and should not be reused.
+ #
+ # Takes an optional block that will be called with the connection to be discarded.
+ # The block should perform any necessary clean-up on the connection.
+ #
+ # @yield [conn]
+ # @yieldparam conn [Object] The connection to be discarded.
+ # @yieldreturn [void]
+ #
+ #
+ # Note: This only affects the connection currently checked out by the calling thread.
+ # The connection will be discarded when +checkin+ is called.
+ #
+ # @return [void]
+ #
+ # @example
+ # pool.with do |conn|
+ # begin
+ # conn.execute("SELECT 1")
+ # rescue SomeConnectionError
+ # pool.discard_current_connection # Mark connection as bad
+ # raise
+ # end
+ # end
+ def discard_current_connection(&block)
+ ::Thread.current[@discard_key] = block || proc { |conn| conn }
+ end
+
+ def checkout(options = {})
+ if ::Thread.current[@key]
+ ::Thread.current[@key_count] += 1
+ ::Thread.current[@key]
+ else
+ ::Thread.current[@key_count] = 1
+ ::Thread.current[@key] = @available.pop(options[:timeout] || @timeout, options)
+ end
+ end
+
+ def checkin(force: false)
+ if ::Thread.current[@key]
+ if ::Thread.current[@key_count] == 1 || force
+ if ::Thread.current[@discard_key]
+ begin
+ @available.decrement_created
+ ::Thread.current[@discard_key].call(::Thread.current[@key])
+ rescue
+ nil
+ ensure
+ ::Thread.current[@discard_key] = nil
+ end
+ else
+ @available.push(::Thread.current[@key])
+ end
+ ::Thread.current[@key] = nil
+ ::Thread.current[@key_count] = nil
+ else
+ ::Thread.current[@key_count] -= 1
+ end
+ elsif !force
+ raise Bundler::ConnectionPool::Error, "no connections are checked out"
+ end
+
+ nil
+ end
+
+ ##
+ # Shuts down the Bundler::ConnectionPool by passing each connection to +block+ and
+ # then removing it from the pool. Attempting to checkout a connection after
+ # shutdown will raise +Bundler::ConnectionPool::PoolShuttingDownError+.
+ def shutdown(&block)
+ @available.shutdown(&block)
+ end
+
+ ##
+ # Reloads the Bundler::ConnectionPool by passing each connection to +block+ and then
+ # removing it the pool. Subsequent checkouts will create new connections as
+ # needed.
+ def reload(&block)
+ @available.shutdown(reload: true, &block)
+ end
+
+ ## Reaps idle connections that have been idle for over +idle_seconds+.
+ # +idle_seconds+ defaults to 60.
+ def reap(idle_seconds = 60, &block)
+ @available.reap(idle_seconds, &block)
+ end
+
+ # Size of this connection pool
+ attr_reader :size
+ # Automatically drop all connections after fork
+ attr_reader :auto_reload_after_fork
+
+ # Number of pool entries available for checkout at this instant.
+ def available
+ @available.length
+ end
+
+ # Number of pool entries created and idle in the pool.
+ def idle
+ @available.idle
+ end
+end
+
+require_relative "connection_pool/timed_stack"
+require_relative "connection_pool/wrapper"
diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb
new file mode 100644
index 0000000000..026d2c5be2
--- /dev/null
+++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb
@@ -0,0 +1,237 @@
+##
+# The TimedStack manages a pool of homogeneous connections (or any resource
+# you wish to manage). Connections are created lazily up to a given maximum
+# number.
+#
+# Examples:
+#
+# ts = TimedStack.new(1) { MyConnection.new }
+#
+# # fetch a connection
+# conn = ts.pop
+#
+# # return a connection
+# ts.push conn
+#
+# conn = ts.pop
+# ts.pop timeout: 5
+# #=> raises Bundler::ConnectionPool::TimeoutError after 5 seconds
+class Bundler::ConnectionPool::TimedStack
+ attr_reader :max
+
+ ##
+ # Creates a new pool with +size+ connections that are created from the given
+ # +block+.
+ def initialize(size = 0, &block)
+ @create_block = block
+ @created = 0
+ @que = []
+ @max = size
+ @mutex = Thread::Mutex.new
+ @resource = Thread::ConditionVariable.new
+ @shutdown_block = nil
+ end
+
+ ##
+ # Returns +obj+ to the stack. +options+ is ignored in TimedStack but may be
+ # used by subclasses that extend TimedStack.
+ def push(obj, options = {})
+ @mutex.synchronize do
+ if @shutdown_block
+ @created -= 1 unless @created == 0
+ @shutdown_block.call(obj)
+ else
+ store_connection obj, options
+ end
+
+ @resource.broadcast
+ end
+ end
+ alias_method :<<, :push
+
+ ##
+ # Retrieves a connection from the stack. If a connection is available it is
+ # immediately returned. If no connection is available within the given
+ # timeout a Bundler::ConnectionPool::TimeoutError is raised.
+ #
+ # @option options [Float] :timeout (0.5) Wait this many seconds for an available entry
+ # @option options [Class] :exception (Bundler::ConnectionPool::TimeoutError) Exception class to raise
+ # if an entry was not available within the timeout period. Use `exception: false` to return nil.
+ #
+ # The +timeout+ argument will be removed in 3.0.
+ # Other options may be used by subclasses that extend TimedStack.
+ def pop(timeout = 0.5, options = {})
+ options, timeout = timeout, 0.5 if Hash === timeout
+ timeout = options.fetch :timeout, timeout
+
+ deadline = current_time + timeout
+ @mutex.synchronize do
+ loop do
+ raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block
+ if (conn = try_fetch_connection(options))
+ return conn
+ end
+
+ connection = try_create(options)
+ return connection if connection
+
+ to_wait = deadline - current_time
+ if to_wait <= 0
+ exc = options.fetch(:exception, Bundler::ConnectionPool::TimeoutError)
+ if exc
+ raise Bundler::ConnectionPool::TimeoutError, "Waited #{timeout} sec, #{length}/#{@max} available"
+ else
+ return nil
+ end
+ end
+ @resource.wait(@mutex, to_wait)
+ end
+ end
+ end
+
+ ##
+ # Shuts down the TimedStack by passing each connection to +block+ and then
+ # removing it from the pool. Attempting to checkout a connection after
+ # shutdown will raise +Bundler::ConnectionPool::PoolShuttingDownError+ unless
+ # +:reload+ is +true+.
+ def shutdown(reload: false, &block)
+ raise ArgumentError, "shutdown must receive a block" unless block
+
+ @mutex.synchronize do
+ @shutdown_block = block
+ @resource.broadcast
+
+ shutdown_connections
+ @shutdown_block = nil if reload
+ end
+ end
+
+ ##
+ # Reaps connections that were checked in more than +idle_seconds+ ago.
+ def reap(idle_seconds, &block)
+ raise ArgumentError, "reap must receive a block" unless block
+ raise ArgumentError, "idle_seconds must be a number" unless idle_seconds.is_a?(Numeric)
+ raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block
+
+ idle.times do
+ conn =
+ @mutex.synchronize do
+ raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block
+
+ reserve_idle_connection(idle_seconds)
+ end
+ break unless conn
+
+ block.call(conn)
+ end
+ end
+
+ ##
+ # Returns +true+ if there are no available connections.
+ def empty?
+ (@created - @que.length) >= @max
+ end
+
+ ##
+ # The number of connections available on the stack.
+ def length
+ @max - @created + @que.length
+ end
+
+ ##
+ # The number of connections created and available on the stack.
+ def idle
+ @que.length
+ end
+
+ ##
+ # Reduce the created count
+ def decrement_created
+ @created -= 1 unless @created == 0
+ end
+
+ private
+
+ def current_time
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ end
+
+ ##
+ # This is an extension point for TimedStack and is called with a mutex.
+ #
+ # This method must returns a connection from the stack if one exists. Allows
+ # subclasses with expensive match/search algorithms to avoid double-handling
+ # their stack.
+ def try_fetch_connection(options = nil)
+ connection_stored?(options) && fetch_connection(options)
+ end
+
+ ##
+ # This is an extension point for TimedStack and is called with a mutex.
+ #
+ # This method must returns true if a connection is available on the stack.
+ def connection_stored?(options = nil)
+ !@que.empty?
+ end
+
+ ##
+ # This is an extension point for TimedStack and is called with a mutex.
+ #
+ # This method must return a connection from the stack.
+ def fetch_connection(options = nil)
+ @que.pop&.first
+ end
+
+ ##
+ # This is an extension point for TimedStack and is called with a mutex.
+ #
+ # This method must shut down all connections on the stack.
+ def shutdown_connections(options = nil)
+ while (conn = try_fetch_connection(options))
+ @created -= 1 unless @created == 0
+ @shutdown_block.call(conn)
+ end
+ end
+
+ ##
+ # This is an extension point for TimedStack and is called with a mutex.
+ #
+ # This method returns the oldest idle connection if it has been idle for more than idle_seconds.
+ # This requires that the stack is kept in order of checked in time (oldest first).
+ def reserve_idle_connection(idle_seconds)
+ return unless idle_connections?(idle_seconds)
+
+ @created -= 1 unless @created == 0
+
+ @que.shift.first
+ end
+
+ ##
+ # This is an extension point for TimedStack and is called with a mutex.
+ #
+ # Returns true if the first connection in the stack has been idle for more than idle_seconds
+ def idle_connections?(idle_seconds)
+ connection_stored? && (current_time - @que.first.last > idle_seconds)
+ end
+
+ ##
+ # This is an extension point for TimedStack and is called with a mutex.
+ #
+ # This method must return +obj+ to the stack.
+ def store_connection(obj, options = nil)
+ @que.push [obj, current_time]
+ end
+
+ ##
+ # This is an extension point for TimedStack and is called with a mutex.
+ #
+ # This method must create a connection if and only if the total number of
+ # connections allowed has not been met.
+ def try_create(options = nil)
+ unless @created == @max
+ object = @create_block.call
+ @created += 1
+ object
+ end
+ end
+end
diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb
new file mode 100644
index 0000000000..2e9eebdbb6
--- /dev/null
+++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb
@@ -0,0 +1,3 @@
+class Bundler::ConnectionPool
+ VERSION = "2.5.5"
+end
diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb
new file mode 100644
index 0000000000..dd796d1021
--- /dev/null
+++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb
@@ -0,0 +1,56 @@
+class Bundler::ConnectionPool
+ class Wrapper < ::BasicObject
+ METHODS = [:with, :pool_shutdown, :wrapped_pool]
+
+ def initialize(options = {}, &block)
+ @pool = options.fetch(:pool) { ::Bundler::ConnectionPool.new(options, &block) }
+ end
+
+ def wrapped_pool
+ @pool
+ end
+
+ def with(&block)
+ @pool.with(&block)
+ end
+
+ def pool_shutdown(&block)
+ @pool.shutdown(&block)
+ end
+
+ def pool_size
+ @pool.size
+ end
+
+ def pool_available
+ @pool.available
+ end
+
+ def respond_to?(id, *args)
+ METHODS.include?(id) || with { |c| c.respond_to?(id, *args) }
+ end
+
+ # rubocop:disable Style/MissingRespondToMissing
+ if ::RUBY_VERSION >= "3.0.0"
+ def method_missing(name, *args, **kwargs, &block)
+ with do |connection|
+ connection.send(name, *args, **kwargs, &block)
+ end
+ end
+ elsif ::RUBY_VERSION >= "2.7.0"
+ ruby2_keywords def method_missing(name, *args, &block)
+ with do |connection|
+ connection.send(name, *args, &block)
+ end
+ end
+ else
+ def method_missing(name, *args, &block)
+ with do |connection|
+ connection.send(name, *args, &block)
+ end
+ end
+ end
+ # rubocop:enable Style/MethodMissingSuper
+ # rubocop:enable Style/MissingRespondToMissing
+ end
+end
diff --git a/lib/bundler/vendor/fileutils/lib/fileutils.rb b/lib/bundler/vendor/fileutils/lib/fileutils.rb
new file mode 100644
index 0000000000..a11fdc7176
--- /dev/null
+++ b/lib/bundler/vendor/fileutils/lib/fileutils.rb
@@ -0,0 +1,2701 @@
+# frozen_string_literal: true
+
+begin
+ require 'rbconfig'
+rescue LoadError
+ # for make rjit-headers
+end
+
+# Namespace for file utility methods for copying, moving, removing, etc.
+#
+# == What's Here
+#
+# First, what’s elsewhere. \Module \Bundler::FileUtils:
+#
+# - Inherits from {class Object}[rdoc-ref:Object].
+# - Supplements {class File}[rdoc-ref:File]
+# (but is not included or extended there).
+#
+# Here, module \Bundler::FileUtils provides methods that are useful for:
+#
+# - {Creating}[rdoc-ref:FileUtils@Creating].
+# - {Deleting}[rdoc-ref:FileUtils@Deleting].
+# - {Querying}[rdoc-ref:FileUtils@Querying].
+# - {Setting}[rdoc-ref:FileUtils@Setting].
+# - {Comparing}[rdoc-ref:FileUtils@Comparing].
+# - {Copying}[rdoc-ref:FileUtils@Copying].
+# - {Moving}[rdoc-ref:FileUtils@Moving].
+# - {Options}[rdoc-ref:FileUtils@Options].
+#
+# === Creating
+#
+# - ::mkdir: Creates directories.
+# - ::mkdir_p, ::makedirs, ::mkpath: Creates directories,
+# also creating ancestor directories as needed.
+# - ::link_entry: Creates a hard link.
+# - ::ln, ::link: Creates hard links.
+# - ::ln_s, ::symlink: Creates symbolic links.
+# - ::ln_sf: Creates symbolic links, overwriting if necessary.
+# - ::ln_sr: Creates symbolic links relative to targets
+#
+# === Deleting
+#
+# - ::remove_dir: Removes a directory and its descendants.
+# - ::remove_entry: Removes an entry, including its descendants if it is a directory.
+# - ::remove_entry_secure: Like ::remove_entry, but removes securely.
+# - ::remove_file: Removes a file entry.
+# - ::rm, ::remove: Removes entries.
+# - ::rm_f, ::safe_unlink: Like ::rm, but removes forcibly.
+# - ::rm_r: Removes entries and their descendants.
+# - ::rm_rf, ::rmtree: Like ::rm_r, but removes forcibly.
+# - ::rmdir: Removes directories.
+#
+# === Querying
+#
+# - ::pwd, ::getwd: Returns the path to the working directory.
+# - ::uptodate?: Returns whether a given entry is newer than given other entries.
+#
+# === Setting
+#
+# - ::cd, ::chdir: Sets the working directory.
+# - ::chmod: Sets permissions for an entry.
+# - ::chmod_R: Sets permissions for an entry and its descendants.
+# - ::chown: Sets the owner and group for entries.
+# - ::chown_R: Sets the owner and group for entries and their descendants.
+# - ::touch: Sets modification and access times for entries,
+# creating if necessary.
+#
+# === Comparing
+#
+# - ::compare_file, ::cmp, ::identical?: Returns whether two entries are identical.
+# - ::compare_stream: Returns whether two streams are identical.
+#
+# === Copying
+#
+# - ::copy_entry: Recursively copies an entry.
+# - ::copy_file: Copies an entry.
+# - ::copy_stream: Copies a stream.
+# - ::cp, ::copy: Copies files.
+# - ::cp_lr: Recursively creates hard links.
+# - ::cp_r: Recursively copies files, retaining mode, owner, and group.
+# - ::install: Recursively copies files, optionally setting mode,
+# owner, and group.
+#
+# === Moving
+#
+# - ::mv, ::move: Moves entries.
+#
+# === Options
+#
+# - ::collect_method: Returns the names of methods that accept a given option.
+# - ::commands: Returns the names of methods that accept options.
+# - ::have_option?: Returns whether a given method accepts a given option.
+# - ::options: Returns all option names.
+# - ::options_of: Returns the names of the options for a given method.
+#
+# == Path Arguments
+#
+# Some methods in \Bundler::FileUtils accept _path_ arguments,
+# which are interpreted as paths to filesystem entries:
+#
+# - If the argument is a string, that value is the path.
+# - If the argument has method +:to_path+, it is converted via that method.
+# - If the argument has method +:to_str+, it is converted via that method.
+#
+# == About the Examples
+#
+# Some examples here involve trees of file entries.
+# For these, we sometimes display trees using the
+# {tree command-line utility}[https://en.wikipedia.org/wiki/Tree_(command)],
+# which is a recursive directory-listing utility that produces
+# a depth-indented listing of files and directories.
+#
+# We use a helper method to launch the command and control the format:
+#
+# def tree(dirpath = '.')
+# command = "tree --noreport --charset=ascii #{dirpath}"
+# system(command)
+# end
+#
+# To illustrate:
+#
+# tree('src0')
+# # => src0
+# # |-- sub0
+# # | |-- src0.txt
+# # | `-- src1.txt
+# # `-- sub1
+# # |-- src2.txt
+# # `-- src3.txt
+#
+# == Avoiding the TOCTTOU Vulnerability
+#
+# For certain methods that recursively remove entries,
+# there is a potential vulnerability called the
+# {Time-of-check to time-of-use}[https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use],
+# or TOCTTOU, vulnerability that can exist when:
+#
+# - An ancestor directory of the entry at the target path is world writable;
+# such directories include <tt>/tmp</tt>.
+# - The directory tree at the target path includes:
+#
+# - A world-writable descendant directory.
+# - A symbolic link.
+#
+# To avoid that vulnerability, you can use this method to remove entries:
+#
+# - Bundler::FileUtils.remove_entry_secure: removes recursively
+# if the target path points to a directory.
+#
+# Also available are these methods,
+# each of which calls \Bundler::FileUtils.remove_entry_secure:
+#
+# - Bundler::FileUtils.rm_r with keyword argument <tt>secure: true</tt>.
+# - Bundler::FileUtils.rm_rf with keyword argument <tt>secure: true</tt>.
+#
+# Finally, this method for moving entries calls \Bundler::FileUtils.remove_entry_secure
+# if the source and destination are on different file systems
+# (which means that the "move" is really a copy and remove):
+#
+# - Bundler::FileUtils.mv with keyword argument <tt>secure: true</tt>.
+#
+# \Method \Bundler::FileUtils.remove_entry_secure removes securely
+# by applying a special pre-process:
+#
+# - If the target path points to a directory, this method uses methods
+# {File#chown}[rdoc-ref:File#chown]
+# and {File#chmod}[rdoc-ref:File#chmod]
+# in removing directories.
+# - The owner of the target directory should be either the current process
+# or the super user (root).
+#
+# WARNING: You must ensure that *ALL* parent directories cannot be
+# moved by other untrusted users. For example, parent directories
+# should not be owned by untrusted users, and should not be world
+# writable except when the sticky bit is set.
+#
+# For details of this security vulnerability, see Perl cases:
+#
+# - {CVE-2005-0448}[https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-0448].
+# - {CVE-2004-0452}[https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2004-0452].
+#
+module Bundler::FileUtils
+ # The version number.
+ VERSION = "1.8.0"
+
+ def self.private_module_function(name) #:nodoc:
+ module_function name
+ private_class_method name
+ end
+
+ #
+ # Returns a string containing the path to the current directory:
+ #
+ # Bundler::FileUtils.pwd # => "/rdoc/fileutils"
+ #
+ # Related: Bundler::FileUtils.cd.
+ #
+ def pwd
+ Dir.pwd
+ end
+ module_function :pwd
+
+ alias getwd pwd
+ module_function :getwd
+
+ # Changes the working directory to the given +dir+, which
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]:
+ #
+ # With no block given,
+ # changes the current directory to the directory at +dir+; returns zero:
+ #
+ # Bundler::FileUtils.pwd # => "/rdoc/fileutils"
+ # Bundler::FileUtils.cd('..')
+ # Bundler::FileUtils.pwd # => "/rdoc"
+ # Bundler::FileUtils.cd('fileutils')
+ #
+ # With a block given, changes the current directory to the directory
+ # at +dir+, calls the block with argument +dir+,
+ # and restores the original current directory; returns the block's value:
+ #
+ # Bundler::FileUtils.pwd # => "/rdoc/fileutils"
+ # Bundler::FileUtils.cd('..') { |arg| [arg, Bundler::FileUtils.pwd] } # => ["..", "/rdoc"]
+ # Bundler::FileUtils.pwd # => "/rdoc/fileutils"
+ #
+ # Keyword arguments:
+ #
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.cd('..')
+ # Bundler::FileUtils.cd('fileutils')
+ #
+ # Output:
+ #
+ # cd ..
+ # cd fileutils
+ #
+ # Related: Bundler::FileUtils.pwd.
+ #
+ def cd(dir, verbose: nil, &block) # :yield: dir
+ fu_output_message "cd #{dir}" if verbose
+ result = Dir.chdir(dir, &block)
+ fu_output_message 'cd -' if verbose and block
+ result
+ end
+ module_function :cd
+
+ alias chdir cd
+ module_function :chdir
+
+ #
+ # Returns +true+ if the file at path +new+
+ # is newer than all the files at paths in array +old_list+;
+ # +false+ otherwise.
+ #
+ # Argument +new+ and the elements of +old_list+
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]:
+ #
+ # Bundler::FileUtils.uptodate?('Rakefile', ['Gemfile', 'README.md']) # => true
+ # Bundler::FileUtils.uptodate?('Gemfile', ['Rakefile', 'README.md']) # => false
+ #
+ # A non-existent file is considered to be infinitely old.
+ #
+ # Related: Bundler::FileUtils.touch.
+ #
+ def uptodate?(new, old_list)
+ return false unless File.exist?(new)
+ new_time = File.mtime(new)
+ old_list.each do |old|
+ if File.exist?(old)
+ return false unless new_time > File.mtime(old)
+ end
+ end
+ true
+ end
+ module_function :uptodate?
+
+ def remove_trailing_slash(dir) #:nodoc:
+ dir == '/' ? dir : dir.chomp(?/)
+ end
+ private_module_function :remove_trailing_slash
+
+ #
+ # Creates directories at the paths in the given +list+
+ # (a single path or an array of paths);
+ # returns +list+ if it is an array, <tt>[list]</tt> otherwise.
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # With no keyword arguments, creates a directory at each +path+ in +list+
+ # by calling: <tt>Dir.mkdir(path, mode)</tt>;
+ # see {Dir.mkdir}[rdoc-ref:Dir.mkdir]:
+ #
+ # Bundler::FileUtils.mkdir(%w[tmp0 tmp1]) # => ["tmp0", "tmp1"]
+ # Bundler::FileUtils.mkdir('tmp4') # => ["tmp4"]
+ #
+ # Keyword arguments:
+ #
+ # - <tt>mode: <i>mode</i></tt> - also calls <tt>File.chmod(mode, path)</tt>;
+ # see {File.chmod}[rdoc-ref:File.chmod].
+ # - <tt>noop: true</tt> - does not create directories.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.mkdir(%w[tmp0 tmp1], verbose: true)
+ # Bundler::FileUtils.mkdir(%w[tmp2 tmp3], mode: 0700, verbose: true)
+ #
+ # Output:
+ #
+ # mkdir tmp0 tmp1
+ # mkdir -m 700 tmp2 tmp3
+ #
+ # Raises an exception if any path points to an existing
+ # file or directory, or if for any reason a directory cannot be created.
+ #
+ # Related: Bundler::FileUtils.mkdir_p.
+ #
+ def mkdir(list, mode: nil, noop: nil, verbose: nil)
+ list = fu_list(list)
+ fu_output_message "mkdir #{mode ? ('-m %03o ' % mode) : ''}#{list.join ' '}" if verbose
+ return if noop
+
+ list.each do |dir|
+ fu_mkdir dir, mode
+ end
+ end
+ module_function :mkdir
+
+ #
+ # Creates directories at the paths in the given +list+
+ # (a single path or an array of paths),
+ # also creating ancestor directories as needed;
+ # returns +list+ if it is an array, <tt>[list]</tt> otherwise.
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # With no keyword arguments, creates a directory at each +path+ in +list+,
+ # along with any needed ancestor directories,
+ # by calling: <tt>Dir.mkdir(path, mode)</tt>;
+ # see {Dir.mkdir}[rdoc-ref:Dir.mkdir]:
+ #
+ # Bundler::FileUtils.mkdir_p(%w[tmp0/tmp1 tmp2/tmp3]) # => ["tmp0/tmp1", "tmp2/tmp3"]
+ # Bundler::FileUtils.mkdir_p('tmp4/tmp5') # => ["tmp4/tmp5"]
+ #
+ # Keyword arguments:
+ #
+ # - <tt>mode: <i>mode</i></tt> - also calls <tt>File.chmod(mode, path)</tt>;
+ # see {File.chmod}[rdoc-ref:File.chmod].
+ # - <tt>noop: true</tt> - does not create directories.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.mkdir_p(%w[tmp0 tmp1], verbose: true)
+ # Bundler::FileUtils.mkdir_p(%w[tmp2 tmp3], mode: 0700, verbose: true)
+ #
+ # Output:
+ #
+ # mkdir -p tmp0 tmp1
+ # mkdir -p -m 700 tmp2 tmp3
+ #
+ # Raises an exception if for any reason a directory cannot be created.
+ #
+ # Bundler::FileUtils.mkpath and Bundler::FileUtils.makedirs are aliases for Bundler::FileUtils.mkdir_p.
+ #
+ # Related: Bundler::FileUtils.mkdir.
+ #
+ def mkdir_p(list, mode: nil, noop: nil, verbose: nil)
+ list = fu_list(list)
+ fu_output_message "mkdir -p #{mode ? ('-m %03o ' % mode) : ''}#{list.join ' '}" if verbose
+ return *list if noop
+
+ list.each do |item|
+ path = remove_trailing_slash(item)
+
+ stack = []
+ until File.directory?(path) || File.dirname(path) == path
+ stack.push path
+ path = File.dirname(path)
+ end
+ stack.reverse_each do |dir|
+ begin
+ fu_mkdir dir, mode
+ rescue SystemCallError
+ raise unless File.directory?(dir)
+ end
+ end
+ end
+
+ return *list
+ end
+ module_function :mkdir_p
+
+ alias mkpath mkdir_p
+ alias makedirs mkdir_p
+ module_function :mkpath
+ module_function :makedirs
+
+ def fu_mkdir(path, mode) #:nodoc:
+ path = remove_trailing_slash(path)
+ if mode
+ Dir.mkdir path, mode
+ File.chmod mode, path
+ else
+ Dir.mkdir path
+ end
+ end
+ private_module_function :fu_mkdir
+
+ #
+ # Removes directories at the paths in the given +list+
+ # (a single path or an array of paths);
+ # returns +list+, if it is an array, <tt>[list]</tt> otherwise.
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # With no keyword arguments, removes the directory at each +path+ in +list+,
+ # by calling: <tt>Dir.rmdir(path)</tt>;
+ # see {Dir.rmdir}[rdoc-ref:Dir.rmdir]:
+ #
+ # Bundler::FileUtils.rmdir(%w[tmp0/tmp1 tmp2/tmp3]) # => ["tmp0/tmp1", "tmp2/tmp3"]
+ # Bundler::FileUtils.rmdir('tmp4/tmp5') # => ["tmp4/tmp5"]
+ #
+ # Keyword arguments:
+ #
+ # - <tt>parents: true</tt> - removes successive ancestor directories
+ # if empty.
+ # - <tt>noop: true</tt> - does not remove directories.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.rmdir(%w[tmp0/tmp1 tmp2/tmp3], parents: true, verbose: true)
+ # Bundler::FileUtils.rmdir('tmp4/tmp5', parents: true, verbose: true)
+ #
+ # Output:
+ #
+ # rmdir -p tmp0/tmp1 tmp2/tmp3
+ # rmdir -p tmp4/tmp5
+ #
+ # Raises an exception if a directory does not exist
+ # or if for any reason a directory cannot be removed.
+ #
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
+ #
+ def rmdir(list, parents: nil, noop: nil, verbose: nil)
+ list = fu_list(list)
+ fu_output_message "rmdir #{parents ? '-p ' : ''}#{list.join ' '}" if verbose
+ return if noop
+ list.each do |dir|
+ Dir.rmdir(dir = remove_trailing_slash(dir))
+ if parents
+ begin
+ until (parent = File.dirname(dir)) == '.' or parent == dir
+ dir = parent
+ Dir.rmdir(dir)
+ end
+ rescue Errno::ENOTEMPTY, Errno::EEXIST, Errno::ENOENT
+ end
+ end
+ end
+ end
+ module_function :rmdir
+
+ # Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link].
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # When +src+ is the path to an existing file
+ # and +dest+ is the path to a non-existent file,
+ # creates a hard link at +dest+ pointing to +src+; returns zero:
+ #
+ # Dir.children('tmp0/') # => ["t.txt"]
+ # Dir.children('tmp1/') # => []
+ # Bundler::FileUtils.ln('tmp0/t.txt', 'tmp1/t.lnk') # => 0
+ # Dir.children('tmp1/') # => ["t.lnk"]
+ #
+ # When +src+ is the path to an existing file
+ # and +dest+ is the path to an existing directory,
+ # creates a hard link at <tt>dest/src</tt> pointing to +src+; returns zero:
+ #
+ # Dir.children('tmp2') # => ["t.dat"]
+ # Dir.children('tmp3') # => []
+ # Bundler::FileUtils.ln('tmp2/t.dat', 'tmp3') # => 0
+ # Dir.children('tmp3') # => ["t.dat"]
+ #
+ # When +src+ is an array of paths to existing files
+ # and +dest+ is the path to an existing directory,
+ # then for each path +target+ in +src+,
+ # creates a hard link at <tt>dest/target</tt> pointing to +target+;
+ # returns +src+:
+ #
+ # Dir.children('tmp4/') # => []
+ # Bundler::FileUtils.ln(['tmp0/t.txt', 'tmp2/t.dat'], 'tmp4/') # => ["tmp0/t.txt", "tmp2/t.dat"]
+ # Dir.children('tmp4/') # => ["t.dat", "t.txt"]
+ #
+ # Keyword arguments:
+ #
+ # - <tt>force: true</tt> - overwrites +dest+ if it exists.
+ # - <tt>noop: true</tt> - does not create links.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.ln('tmp0/t.txt', 'tmp1/t.lnk', verbose: true)
+ # Bundler::FileUtils.ln('tmp2/t.dat', 'tmp3', verbose: true)
+ # Bundler::FileUtils.ln(['tmp0/t.txt', 'tmp2/t.dat'], 'tmp4/', verbose: true)
+ #
+ # Output:
+ #
+ # ln tmp0/t.txt tmp1/t.lnk
+ # ln tmp2/t.dat tmp3
+ # ln tmp0/t.txt tmp2/t.dat tmp4/
+ #
+ # Raises an exception if +dest+ is the path to an existing file
+ # and keyword argument +force+ is not +true+.
+ #
+ # Related: Bundler::FileUtils.link_entry (has different options).
+ #
+ def ln(src, dest, force: nil, noop: nil, verbose: nil)
+ fu_output_message "ln#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose
+ return if noop
+ fu_each_src_dest0(src, dest) do |s,d|
+ remove_file d, true if force
+ File.link s, d
+ end
+ end
+ module_function :ln
+
+ alias link ln
+ module_function :link
+
+ # Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link].
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # If +src+ is the path to a directory and +dest+ does not exist,
+ # creates links +dest+ and descendents pointing to +src+ and its descendents:
+ #
+ # tree('src0')
+ # # => src0
+ # # |-- sub0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- sub1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # File.exist?('dest0') # => false
+ # Bundler::FileUtils.cp_lr('src0', 'dest0')
+ # tree('dest0')
+ # # => dest0
+ # # |-- sub0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- sub1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ #
+ # If +src+ and +dest+ are both paths to directories,
+ # creates links <tt>dest/src</tt> and descendents
+ # pointing to +src+ and its descendents:
+ #
+ # tree('src1')
+ # # => src1
+ # # |-- sub0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- sub1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # Bundler::FileUtils.mkdir('dest1')
+ # Bundler::FileUtils.cp_lr('src1', 'dest1')
+ # tree('dest1')
+ # # => dest1
+ # # `-- src1
+ # # |-- sub0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- sub1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ #
+ # If +src+ is an array of paths to entries and +dest+ is the path to a directory,
+ # for each path +filepath+ in +src+, creates a link at <tt>dest/filepath</tt>
+ # pointing to that path:
+ #
+ # tree('src2')
+ # # => src2
+ # # |-- sub0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- sub1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # Bundler::FileUtils.mkdir('dest2')
+ # Bundler::FileUtils.cp_lr(['src2/sub0', 'src2/sub1'], 'dest2')
+ # tree('dest2')
+ # # => dest2
+ # # |-- sub0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- sub1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ #
+ # Keyword arguments:
+ #
+ # - <tt>dereference_root: false</tt> - if +src+ is a symbolic link,
+ # does not dereference it.
+ # - <tt>noop: true</tt> - does not create links.
+ # - <tt>remove_destination: true</tt> - removes +dest+ before creating links.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.cp_lr('src0', 'dest0', noop: true, verbose: true)
+ # Bundler::FileUtils.cp_lr('src1', 'dest1', noop: true, verbose: true)
+ # Bundler::FileUtils.cp_lr(['src2/sub0', 'src2/sub1'], 'dest2', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # cp -lr src0 dest0
+ # cp -lr src1 dest1
+ # cp -lr src2/sub0 src2/sub1 dest2
+ #
+ # Raises an exception if +dest+ is the path to an existing file or directory
+ # and keyword argument <tt>remove_destination: true</tt> is not given.
+ #
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
+ #
+ def cp_lr(src, dest, noop: nil, verbose: nil,
+ dereference_root: true, remove_destination: false)
+ fu_output_message "cp -lr#{remove_destination ? ' --remove-destination' : ''} #{[src,dest].flatten.join ' '}" if verbose
+ return if noop
+ fu_each_src_dest(src, dest) do |s, d|
+ link_entry s, d, dereference_root, remove_destination
+ end
+ end
+ module_function :cp_lr
+
+ # Creates {symbolic links}[https://en.wikipedia.org/wiki/Symbolic_link].
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # If +src+ is the path to an existing file:
+ #
+ # - When +dest+ is the path to a non-existent file,
+ # creates a symbolic link at +dest+ pointing to +src+:
+ #
+ # Bundler::FileUtils.touch('src0.txt')
+ # File.exist?('dest0.txt') # => false
+ # Bundler::FileUtils.ln_s('src0.txt', 'dest0.txt')
+ # File.symlink?('dest0.txt') # => true
+ #
+ # - When +dest+ is the path to an existing file,
+ # creates a symbolic link at +dest+ pointing to +src+
+ # if and only if keyword argument <tt>force: true</tt> is given
+ # (raises an exception otherwise):
+ #
+ # Bundler::FileUtils.touch('src1.txt')
+ # Bundler::FileUtils.touch('dest1.txt')
+ # Bundler::FileUtils.ln_s('src1.txt', 'dest1.txt', force: true)
+ # FileTest.symlink?('dest1.txt') # => true
+ #
+ # Bundler::FileUtils.ln_s('src1.txt', 'dest1.txt') # Raises Errno::EEXIST.
+ #
+ # If +dest+ is the path to a directory,
+ # creates a symbolic link at <tt>dest/src</tt> pointing to +src+:
+ #
+ # Bundler::FileUtils.touch('src2.txt')
+ # Bundler::FileUtils.mkdir('destdir2')
+ # Bundler::FileUtils.ln_s('src2.txt', 'destdir2')
+ # File.symlink?('destdir2/src2.txt') # => true
+ #
+ # If +src+ is an array of paths to existing files and +dest+ is a directory,
+ # for each child +child+ in +src+ creates a symbolic link <tt>dest/child</tt>
+ # pointing to +child+:
+ #
+ # Bundler::FileUtils.mkdir('srcdir3')
+ # Bundler::FileUtils.touch('srcdir3/src0.txt')
+ # Bundler::FileUtils.touch('srcdir3/src1.txt')
+ # Bundler::FileUtils.mkdir('destdir3')
+ # Bundler::FileUtils.ln_s(['srcdir3/src0.txt', 'srcdir3/src1.txt'], 'destdir3')
+ # File.symlink?('destdir3/src0.txt') # => true
+ # File.symlink?('destdir3/src1.txt') # => true
+ #
+ # Keyword arguments:
+ #
+ # - <tt>force: true</tt> - overwrites +dest+ if it exists.
+ # - <tt>relative: false</tt> - create links relative to +dest+.
+ # - <tt>noop: true</tt> - does not create links.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.ln_s('src0.txt', 'dest0.txt', noop: true, verbose: true)
+ # Bundler::FileUtils.ln_s('src1.txt', 'destdir1', noop: true, verbose: true)
+ # Bundler::FileUtils.ln_s('src2.txt', 'dest2.txt', force: true, noop: true, verbose: true)
+ # Bundler::FileUtils.ln_s(['srcdir3/src0.txt', 'srcdir3/src1.txt'], 'destdir3', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # ln -s src0.txt dest0.txt
+ # ln -s src1.txt destdir1
+ # ln -sf src2.txt dest2.txt
+ # ln -s srcdir3/src0.txt srcdir3/src1.txt destdir3
+ #
+ # Related: Bundler::FileUtils.ln_sf.
+ #
+ def ln_s(src, dest, force: nil, relative: false, target_directory: true, noop: nil, verbose: nil)
+ if relative
+ return ln_sr(src, dest, force: force, target_directory: target_directory, noop: noop, verbose: verbose)
+ end
+ fu_output_message "ln -s#{force ? 'f' : ''}#{
+ target_directory ? '' : 'T'} #{[src,dest].flatten.join ' '}" if verbose
+ return if noop
+ fu_each_src_dest0(src, dest, target_directory) do |s,d|
+ remove_file d, true if force
+ File.symlink s, d
+ end
+ end
+ module_function :ln_s
+
+ alias symlink ln_s
+ module_function :symlink
+
+ # Like Bundler::FileUtils.ln_s, but always with keyword argument <tt>force: true</tt> given.
+ #
+ def ln_sf(src, dest, noop: nil, verbose: nil)
+ ln_s src, dest, force: true, noop: noop, verbose: verbose
+ end
+ module_function :ln_sf
+
+ # Like Bundler::FileUtils.ln_s, but create links relative to +dest+.
+ #
+ def ln_sr(src, dest, target_directory: true, force: nil, noop: nil, verbose: nil)
+ cmd = "ln -s#{force ? 'f' : ''}#{target_directory ? '' : 'T'}" if verbose
+ fu_each_src_dest0(src, dest, target_directory) do |s,d|
+ if target_directory
+ parent = File.dirname(d)
+ destdirs = fu_split_path(parent)
+ real_ddirs = fu_split_path(File.realpath(parent))
+ else
+ destdirs ||= fu_split_path(dest)
+ real_ddirs ||= fu_split_path(File.realdirpath(dest))
+ end
+ srcdirs = fu_split_path(s)
+ i = fu_common_components(srcdirs, destdirs)
+ n = destdirs.size - i
+ n -= 1 unless target_directory
+ link1 = fu_clean_components(*Array.new([n, 0].max, '..'), *srcdirs[i..-1])
+ begin
+ real_sdirs = fu_split_path(File.realdirpath(s)) rescue nil
+ rescue
+ else
+ i = fu_common_components(real_sdirs, real_ddirs)
+ n = real_ddirs.size - i
+ n -= 1 unless target_directory
+ link2 = fu_clean_components(*Array.new([n, 0].max, '..'), *real_sdirs[i..-1])
+ link1 = link2 if link1.size > link2.size
+ end
+ s = File.join(link1)
+ fu_output_message [cmd, s, d].flatten.join(' ') if verbose
+ next if noop
+ remove_file d, true if force
+ File.symlink s, d
+ end
+ end
+ module_function :ln_sr
+
+ # Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link]; returns +nil+.
+ #
+ # Arguments +src+ and +dest+
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # If +src+ is the path to a file and +dest+ does not exist,
+ # creates a hard link at +dest+ pointing to +src+:
+ #
+ # Bundler::FileUtils.touch('src0.txt')
+ # File.exist?('dest0.txt') # => false
+ # Bundler::FileUtils.link_entry('src0.txt', 'dest0.txt')
+ # File.file?('dest0.txt') # => true
+ #
+ # If +src+ is the path to a directory and +dest+ does not exist,
+ # recursively creates hard links at +dest+ pointing to paths in +src+:
+ #
+ # Bundler::FileUtils.mkdir_p(['src1/dir0', 'src1/dir1'])
+ # src_file_paths = [
+ # 'src1/dir0/t0.txt',
+ # 'src1/dir0/t1.txt',
+ # 'src1/dir1/t2.txt',
+ # 'src1/dir1/t3.txt',
+ # ]
+ # Bundler::FileUtils.touch(src_file_paths)
+ # File.directory?('dest1') # => true
+ # Bundler::FileUtils.link_entry('src1', 'dest1')
+ # File.file?('dest1/dir0/t0.txt') # => true
+ # File.file?('dest1/dir0/t1.txt') # => true
+ # File.file?('dest1/dir1/t2.txt') # => true
+ # File.file?('dest1/dir1/t3.txt') # => true
+ #
+ # Optional arguments:
+ #
+ # - +dereference_root+ - dereferences +src+ if it is a symbolic link (+false+ by default).
+ # - +remove_destination+ - removes +dest+ before creating links (+false+ by default).
+ #
+ # Raises an exception if +dest+ is the path to an existing file or directory
+ # and optional argument +remove_destination+ is not given.
+ #
+ # Related: Bundler::FileUtils.ln (has different options).
+ #
+ def link_entry(src, dest, dereference_root = false, remove_destination = false)
+ Entry_.new(src, nil, dereference_root).traverse do |ent|
+ destent = Entry_.new(dest, ent.rel, false)
+ File.unlink destent.path if remove_destination && File.file?(destent.path)
+ ent.link destent.path
+ end
+ end
+ module_function :link_entry
+
+ # Copies files.
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # If +src+ is the path to a file and +dest+ is not the path to a directory,
+ # copies +src+ to +dest+:
+ #
+ # Bundler::FileUtils.touch('src0.txt')
+ # File.exist?('dest0.txt') # => false
+ # Bundler::FileUtils.cp('src0.txt', 'dest0.txt')
+ # File.file?('dest0.txt') # => true
+ #
+ # If +src+ is the path to a file and +dest+ is the path to a directory,
+ # copies +src+ to <tt>dest/src</tt>:
+ #
+ # Bundler::FileUtils.touch('src1.txt')
+ # Bundler::FileUtils.mkdir('dest1')
+ # Bundler::FileUtils.cp('src1.txt', 'dest1')
+ # File.file?('dest1/src1.txt') # => true
+ #
+ # If +src+ is an array of paths to files and +dest+ is the path to a directory,
+ # copies from each +src+ to +dest+:
+ #
+ # src_file_paths = ['src2.txt', 'src2.dat']
+ # Bundler::FileUtils.touch(src_file_paths)
+ # Bundler::FileUtils.mkdir('dest2')
+ # Bundler::FileUtils.cp(src_file_paths, 'dest2')
+ # File.file?('dest2/src2.txt') # => true
+ # File.file?('dest2/src2.dat') # => true
+ #
+ # Keyword arguments:
+ #
+ # - <tt>preserve: true</tt> - preserves file times.
+ # - <tt>noop: true</tt> - does not copy files.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.cp('src0.txt', 'dest0.txt', noop: true, verbose: true)
+ # Bundler::FileUtils.cp('src1.txt', 'dest1', noop: true, verbose: true)
+ # Bundler::FileUtils.cp(src_file_paths, 'dest2', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # cp src0.txt dest0.txt
+ # cp src1.txt dest1
+ # cp src2.txt src2.dat dest2
+ #
+ # Raises an exception if +src+ is a directory.
+ #
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
+ #
+ def cp(src, dest, preserve: nil, noop: nil, verbose: nil)
+ fu_output_message "cp#{preserve ? ' -p' : ''} #{[src,dest].flatten.join ' '}" if verbose
+ return if noop
+ fu_each_src_dest(src, dest) do |s, d|
+ copy_file s, d, preserve
+ end
+ end
+ module_function :cp
+
+ alias copy cp
+ module_function :copy
+
+ # Recursively copies files.
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # The mode, owner, and group are retained in the copy;
+ # to change those, use Bundler::FileUtils.install instead.
+ #
+ # If +src+ is the path to a file and +dest+ is not the path to a directory,
+ # copies +src+ to +dest+:
+ #
+ # Bundler::FileUtils.touch('src0.txt')
+ # File.exist?('dest0.txt') # => false
+ # Bundler::FileUtils.cp_r('src0.txt', 'dest0.txt')
+ # File.file?('dest0.txt') # => true
+ #
+ # If +src+ is the path to a file and +dest+ is the path to a directory,
+ # copies +src+ to <tt>dest/src</tt>:
+ #
+ # Bundler::FileUtils.touch('src1.txt')
+ # Bundler::FileUtils.mkdir('dest1')
+ # Bundler::FileUtils.cp_r('src1.txt', 'dest1')
+ # File.file?('dest1/src1.txt') # => true
+ #
+ # If +src+ is the path to a directory and +dest+ does not exist,
+ # recursively copies +src+ to +dest+:
+ #
+ # tree('src2')
+ # # => src2
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # Bundler::FileUtils.exist?('dest2') # => false
+ # Bundler::FileUtils.cp_r('src2', 'dest2')
+ # tree('dest2')
+ # # => dest2
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ #
+ # If +src+ and +dest+ are paths to directories,
+ # recursively copies +src+ to <tt>dest/src</tt>:
+ #
+ # tree('src3')
+ # # => src3
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # Bundler::FileUtils.mkdir('dest3')
+ # Bundler::FileUtils.cp_r('src3', 'dest3')
+ # tree('dest3')
+ # # => dest3
+ # # `-- src3
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ #
+ # If +src+ is an array of paths and +dest+ is a directory,
+ # recursively copies from each path in +src+ to +dest+;
+ # the paths in +src+ may point to files and/or directories.
+ #
+ # Keyword arguments:
+ #
+ # - <tt>dereference_root: false</tt> - if +src+ is a symbolic link,
+ # does not dereference it.
+ # - <tt>noop: true</tt> - does not copy files.
+ # - <tt>preserve: true</tt> - preserves file times.
+ # - <tt>remove_destination: true</tt> - removes +dest+ before copying files.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.cp_r('src0.txt', 'dest0.txt', noop: true, verbose: true)
+ # Bundler::FileUtils.cp_r('src1.txt', 'dest1', noop: true, verbose: true)
+ # Bundler::FileUtils.cp_r('src2', 'dest2', noop: true, verbose: true)
+ # Bundler::FileUtils.cp_r('src3', 'dest3', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # cp -r src0.txt dest0.txt
+ # cp -r src1.txt dest1
+ # cp -r src2 dest2
+ # cp -r src3 dest3
+ #
+ # Raises an exception of +src+ is the path to a directory
+ # and +dest+ is the path to a file.
+ #
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
+ #
+ def cp_r(src, dest, preserve: nil, noop: nil, verbose: nil,
+ dereference_root: true, remove_destination: nil)
+ fu_output_message "cp -r#{preserve ? 'p' : ''}#{remove_destination ? ' --remove-destination' : ''} #{[src,dest].flatten.join ' '}" if verbose
+ return if noop
+ fu_each_src_dest(src, dest) do |s, d|
+ copy_entry s, d, preserve, dereference_root, remove_destination
+ end
+ end
+ module_function :cp_r
+
+ # Recursively copies files from +src+ to +dest+.
+ #
+ # Arguments +src+ and +dest+
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # If +src+ is the path to a file, copies +src+ to +dest+:
+ #
+ # Bundler::FileUtils.touch('src0.txt')
+ # File.exist?('dest0.txt') # => false
+ # Bundler::FileUtils.copy_entry('src0.txt', 'dest0.txt')
+ # File.file?('dest0.txt') # => true
+ #
+ # If +src+ is a directory, recursively copies +src+ to +dest+:
+ #
+ # tree('src1')
+ # # => src1
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # Bundler::FileUtils.copy_entry('src1', 'dest1')
+ # tree('dest1')
+ # # => dest1
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ #
+ # The recursive copying preserves file types for regular files,
+ # directories, and symbolic links;
+ # other file types (FIFO streams, device files, etc.) are not supported.
+ #
+ # Optional arguments:
+ #
+ # - +dereference_root+ - if +src+ is a symbolic link,
+ # follows the link (+false+ by default).
+ # - +preserve+ - preserves file times (+false+ by default).
+ # - +remove_destination+ - removes +dest+ before copying files (+false+ by default).
+ #
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
+ #
+ def copy_entry(src, dest, preserve = false, dereference_root = false, remove_destination = false)
+ if dereference_root
+ src = File.realpath(src)
+ end
+
+ Entry_.new(src, nil, false).wrap_traverse(proc do |ent|
+ destent = Entry_.new(dest, ent.rel, false)
+ File.unlink destent.path if remove_destination && (File.file?(destent.path) || File.symlink?(destent.path))
+ ent.copy destent.path
+ end, proc do |ent|
+ destent = Entry_.new(dest, ent.rel, false)
+ ent.copy_metadata destent.path if preserve
+ end)
+ end
+ module_function :copy_entry
+
+ # Copies file from +src+ to +dest+, which should not be directories.
+ #
+ # Arguments +src+ and +dest+
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Examples:
+ #
+ # Bundler::FileUtils.touch('src0.txt')
+ # Bundler::FileUtils.copy_file('src0.txt', 'dest0.txt')
+ # File.file?('dest0.txt') # => true
+ #
+ # Optional arguments:
+ #
+ # - +dereference+ - if +src+ is a symbolic link,
+ # follows the link (+true+ by default).
+ # - +preserve+ - preserves file times (+false+ by default).
+ # - +remove_destination+ - removes +dest+ before copying files (+false+ by default).
+ #
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
+ #
+ def copy_file(src, dest, preserve = false, dereference = true)
+ ent = Entry_.new(src, nil, dereference)
+ ent.copy_file dest
+ ent.copy_metadata dest if preserve
+ end
+ module_function :copy_file
+
+ # Copies \IO stream +src+ to \IO stream +dest+ via
+ # {IO.copy_stream}[rdoc-ref:IO.copy_stream].
+ #
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
+ #
+ def copy_stream(src, dest)
+ IO.copy_stream(src, dest)
+ end
+ module_function :copy_stream
+
+ # Moves entries.
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # If +src+ and +dest+ are on different file systems,
+ # first copies, then removes +src+.
+ #
+ # May cause a local vulnerability if not called with keyword argument
+ # <tt>secure: true</tt>;
+ # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability].
+ #
+ # If +src+ is the path to a single file or directory and +dest+ does not exist,
+ # moves +src+ to +dest+:
+ #
+ # tree('src0')
+ # # => src0
+ # # |-- src0.txt
+ # # `-- src1.txt
+ # File.exist?('dest0') # => false
+ # Bundler::FileUtils.mv('src0', 'dest0')
+ # File.exist?('src0') # => false
+ # tree('dest0')
+ # # => dest0
+ # # |-- src0.txt
+ # # `-- src1.txt
+ #
+ # If +src+ is an array of paths to files and directories
+ # and +dest+ is the path to a directory,
+ # copies from each path in the array to +dest+:
+ #
+ # File.file?('src1.txt') # => true
+ # tree('src1')
+ # # => src1
+ # # |-- src.dat
+ # # `-- src.txt
+ # Dir.empty?('dest1') # => true
+ # Bundler::FileUtils.mv(['src1.txt', 'src1'], 'dest1')
+ # tree('dest1')
+ # # => dest1
+ # # |-- src1
+ # # | |-- src.dat
+ # # | `-- src.txt
+ # # `-- src1.txt
+ #
+ # Keyword arguments:
+ #
+ # - <tt>force: true</tt> - if the move includes removing +src+
+ # (that is, if +src+ and +dest+ are on different file systems),
+ # ignores raised exceptions of StandardError and its descendants.
+ # - <tt>noop: true</tt> - does not move files.
+ # - <tt>secure: true</tt> - removes +src+ securely;
+ # see details at Bundler::FileUtils.remove_entry_secure.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.mv('src0', 'dest0', noop: true, verbose: true)
+ # Bundler::FileUtils.mv(['src1.txt', 'src1'], 'dest1', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # mv src0 dest0
+ # mv src1.txt src1 dest1
+ #
+ def mv(src, dest, force: nil, noop: nil, verbose: nil, secure: nil)
+ fu_output_message "mv#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose
+ return if noop
+ fu_each_src_dest(src, dest) do |s, d|
+ destent = Entry_.new(d, nil, true)
+ begin
+ if destent.exist?
+ if destent.directory?
+ raise Errno::EEXIST, d
+ end
+ end
+ begin
+ File.rename s, d
+ rescue Errno::EXDEV,
+ Errno::EPERM # move from unencrypted to encrypted dir (ext4)
+ copy_entry s, d, true
+ if secure
+ remove_entry_secure s, force
+ else
+ remove_entry s, force
+ end
+ end
+ rescue SystemCallError
+ raise unless force
+ end
+ end
+ end
+ module_function :mv
+
+ alias move mv
+ module_function :move
+
+ # Removes entries at the paths in the given +list+
+ # (a single path or an array of paths)
+ # returns +list+, if it is an array, <tt>[list]</tt> otherwise.
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # With no keyword arguments, removes files at the paths given in +list+:
+ #
+ # Bundler::FileUtils.touch(['src0.txt', 'src0.dat'])
+ # Bundler::FileUtils.rm(['src0.dat', 'src0.txt']) # => ["src0.dat", "src0.txt"]
+ #
+ # Keyword arguments:
+ #
+ # - <tt>force: true</tt> - ignores raised exceptions of StandardError
+ # and its descendants.
+ # - <tt>noop: true</tt> - does not remove files; returns +nil+.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.rm(['src0.dat', 'src0.txt'], noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # rm src0.dat src0.txt
+ #
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
+ #
+ def rm(list, force: nil, noop: nil, verbose: nil)
+ list = fu_list(list)
+ fu_output_message "rm#{force ? ' -f' : ''} #{list.join ' '}" if verbose
+ return if noop
+
+ list.each do |path|
+ remove_file path, force
+ end
+ end
+ module_function :rm
+
+ alias remove rm
+ module_function :remove
+
+ # Equivalent to:
+ #
+ # Bundler::FileUtils.rm(list, force: true, **kwargs)
+ #
+ # Argument +list+ (a single path or an array of paths)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # See Bundler::FileUtils.rm for keyword arguments.
+ #
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
+ #
+ def rm_f(list, noop: nil, verbose: nil)
+ rm list, force: true, noop: noop, verbose: verbose
+ end
+ module_function :rm_f
+
+ alias safe_unlink rm_f
+ module_function :safe_unlink
+
+ # Removes entries at the paths in the given +list+
+ # (a single path or an array of paths);
+ # returns +list+, if it is an array, <tt>[list]</tt> otherwise.
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # May cause a local vulnerability if not called with keyword argument
+ # <tt>secure: true</tt>;
+ # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability].
+ #
+ # For each file path, removes the file at that path:
+ #
+ # Bundler::FileUtils.touch(['src0.txt', 'src0.dat'])
+ # Bundler::FileUtils.rm_r(['src0.dat', 'src0.txt'])
+ # File.exist?('src0.txt') # => false
+ # File.exist?('src0.dat') # => false
+ #
+ # For each directory path, recursively removes files and directories:
+ #
+ # tree('src1')
+ # # => src1
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # Bundler::FileUtils.rm_r('src1')
+ # File.exist?('src1') # => false
+ #
+ # Keyword arguments:
+ #
+ # - <tt>force: true</tt> - ignores raised exceptions of StandardError
+ # and its descendants.
+ # - <tt>noop: true</tt> - does not remove entries; returns +nil+.
+ # - <tt>secure: true</tt> - removes +src+ securely;
+ # see details at Bundler::FileUtils.remove_entry_secure.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.rm_r(['src0.dat', 'src0.txt'], noop: true, verbose: true)
+ # Bundler::FileUtils.rm_r('src1', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # rm -r src0.dat src0.txt
+ # rm -r src1
+ #
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
+ #
+ def rm_r(list, force: nil, noop: nil, verbose: nil, secure: nil)
+ list = fu_list(list)
+ fu_output_message "rm -r#{force ? 'f' : ''} #{list.join ' '}" if verbose
+ return if noop
+ list.each do |path|
+ if secure
+ remove_entry_secure path, force
+ else
+ remove_entry path, force
+ end
+ end
+ end
+ module_function :rm_r
+
+ # Equivalent to:
+ #
+ # Bundler::FileUtils.rm_r(list, force: true, **kwargs)
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # May cause a local vulnerability if not called with keyword argument
+ # <tt>secure: true</tt>;
+ # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability].
+ #
+ # See Bundler::FileUtils.rm_r for keyword arguments.
+ #
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
+ #
+ def rm_rf(list, noop: nil, verbose: nil, secure: nil)
+ rm_r list, force: true, noop: noop, verbose: verbose, secure: secure
+ end
+ module_function :rm_rf
+
+ alias rmtree rm_rf
+ module_function :rmtree
+
+ # Securely removes the entry given by +path+,
+ # which should be the entry for a regular file, a symbolic link,
+ # or a directory.
+ #
+ # Argument +path+
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Avoids a local vulnerability that can exist in certain circumstances;
+ # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability].
+ #
+ # Optional argument +force+ specifies whether to ignore
+ # raised exceptions of StandardError and its descendants.
+ #
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
+ #
+ def remove_entry_secure(path, force = false)
+ unless fu_have_symlink?
+ remove_entry path, force
+ return
+ end
+ fullpath = File.expand_path(path)
+ st = File.lstat(fullpath)
+ unless st.directory?
+ File.unlink fullpath
+ return
+ end
+ # is a directory.
+ parent_st = File.stat(File.dirname(fullpath))
+ unless parent_st.world_writable?
+ remove_entry path, force
+ return
+ end
+ unless parent_st.sticky?
+ raise ArgumentError, "parent directory is world writable, Bundler::FileUtils#remove_entry_secure does not work; abort: #{path.inspect} (parent directory mode #{'%o' % parent_st.mode})"
+ end
+
+ # freeze tree root
+ euid = Process.euid
+ dot_file = fullpath + "/."
+ begin
+ File.open(dot_file) {|f|
+ unless fu_stat_identical_entry?(st, f.stat)
+ # symlink (TOC-to-TOU attack?)
+ File.unlink fullpath
+ return
+ end
+ f.chown euid, -1
+ f.chmod 0700
+ }
+ rescue Errno::EISDIR # JRuby in non-native mode can't open files as dirs
+ File.lstat(dot_file).tap {|fstat|
+ unless fu_stat_identical_entry?(st, fstat)
+ # symlink (TOC-to-TOU attack?)
+ File.unlink fullpath
+ return
+ end
+ File.chown euid, -1, dot_file
+ File.chmod 0700, dot_file
+ }
+ end
+
+ unless fu_stat_identical_entry?(st, File.lstat(fullpath))
+ # TOC-to-TOU attack?
+ File.unlink fullpath
+ return
+ end
+
+ # ---- tree root is frozen ----
+ root = Entry_.new(path)
+ root.preorder_traverse do |ent|
+ if ent.directory?
+ ent.chown euid, -1
+ ent.chmod 0700
+ end
+ end
+ root.postorder_traverse do |ent|
+ begin
+ ent.remove
+ rescue
+ raise unless force
+ end
+ end
+ rescue
+ raise unless force
+ end
+ module_function :remove_entry_secure
+
+ def fu_have_symlink? #:nodoc:
+ File.symlink nil, nil
+ rescue NotImplementedError
+ return false
+ rescue TypeError
+ return true
+ end
+ private_module_function :fu_have_symlink?
+
+ def fu_stat_identical_entry?(a, b) #:nodoc:
+ a.dev == b.dev and a.ino == b.ino
+ end
+ private_module_function :fu_stat_identical_entry?
+
+ # Removes the entry given by +path+,
+ # which should be the entry for a regular file, a symbolic link,
+ # or a directory.
+ #
+ # Argument +path+
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Optional argument +force+ specifies whether to ignore
+ # raised exceptions of StandardError and its descendants.
+ #
+ # Related: Bundler::FileUtils.remove_entry_secure.
+ #
+ def remove_entry(path, force = false)
+ Entry_.new(path).postorder_traverse do |ent|
+ begin
+ ent.remove
+ rescue
+ raise unless force
+ end
+ end
+ rescue
+ raise unless force
+ end
+ module_function :remove_entry
+
+ # Removes the file entry given by +path+,
+ # which should be the entry for a regular file or a symbolic link.
+ #
+ # Argument +path+
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Optional argument +force+ specifies whether to ignore
+ # raised exceptions of StandardError and its descendants.
+ #
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
+ #
+ def remove_file(path, force = false)
+ Entry_.new(path).remove_file
+ rescue
+ raise unless force
+ end
+ module_function :remove_file
+
+ # Recursively removes the directory entry given by +path+,
+ # which should be the entry for a regular file, a symbolic link,
+ # or a directory.
+ #
+ # Argument +path+
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Optional argument +force+ specifies whether to ignore
+ # raised exceptions of StandardError and its descendants.
+ #
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
+ #
+ def remove_dir(path, force = false)
+ raise Errno::ENOTDIR, path unless force or File.directory?(path)
+ remove_entry path, force
+ end
+ module_function :remove_dir
+
+ # Returns +true+ if the contents of files +a+ and +b+ are identical,
+ # +false+ otherwise.
+ #
+ # Arguments +a+ and +b+
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Bundler::FileUtils.identical? and Bundler::FileUtils.cmp are aliases for Bundler::FileUtils.compare_file.
+ #
+ # Related: Bundler::FileUtils.compare_stream.
+ #
+ def compare_file(a, b)
+ return false unless File.size(a) == File.size(b)
+ File.open(a, 'rb') {|fa|
+ File.open(b, 'rb') {|fb|
+ return compare_stream(fa, fb)
+ }
+ }
+ end
+ module_function :compare_file
+
+ alias identical? compare_file
+ alias cmp compare_file
+ module_function :identical?
+ module_function :cmp
+
+ # Returns +true+ if the contents of streams +a+ and +b+ are identical,
+ # +false+ otherwise.
+ #
+ # Arguments +a+ and +b+
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Related: Bundler::FileUtils.compare_file.
+ #
+ def compare_stream(a, b)
+ bsize = fu_stream_blksize(a, b)
+
+ sa = String.new(capacity: bsize)
+ sb = String.new(capacity: bsize)
+
+ begin
+ a.read(bsize, sa)
+ b.read(bsize, sb)
+ return true if sa.empty? && sb.empty?
+ end while sa == sb
+ false
+ end
+ module_function :compare_stream
+
+ # Copies a file entry.
+ # See {install(1)}[https://man7.org/linux/man-pages/man1/install.1.html].
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments];
+ #
+ # If the entry at +dest+ does not exist, copies from +src+ to +dest+:
+ #
+ # File.read('src0.txt') # => "aaa\n"
+ # File.exist?('dest0.txt') # => false
+ # Bundler::FileUtils.install('src0.txt', 'dest0.txt')
+ # File.read('dest0.txt') # => "aaa\n"
+ #
+ # If +dest+ is a file entry, copies from +src+ to +dest+, overwriting:
+ #
+ # File.read('src1.txt') # => "aaa\n"
+ # File.read('dest1.txt') # => "bbb\n"
+ # Bundler::FileUtils.install('src1.txt', 'dest1.txt')
+ # File.read('dest1.txt') # => "aaa\n"
+ #
+ # If +dest+ is a directory entry, copies from +src+ to <tt>dest/src</tt>,
+ # overwriting if necessary:
+ #
+ # File.read('src2.txt') # => "aaa\n"
+ # File.read('dest2/src2.txt') # => "bbb\n"
+ # Bundler::FileUtils.install('src2.txt', 'dest2')
+ # File.read('dest2/src2.txt') # => "aaa\n"
+ #
+ # If +src+ is an array of paths and +dest+ points to a directory,
+ # copies each path +path+ in +src+ to <tt>dest/path</tt>:
+ #
+ # File.file?('src3.txt') # => true
+ # File.file?('src3.dat') # => true
+ # Bundler::FileUtils.mkdir('dest3')
+ # Bundler::FileUtils.install(['src3.txt', 'src3.dat'], 'dest3')
+ # File.file?('dest3/src3.txt') # => true
+ # File.file?('dest3/src3.dat') # => true
+ #
+ # Keyword arguments:
+ #
+ # - <tt>group: <i>group</i></tt> - changes the group if not +nil+,
+ # using {File.chown}[rdoc-ref:File.chown].
+ # - <tt>mode: <i>permissions</i></tt> - changes the permissions.
+ # using {File.chmod}[rdoc-ref:File.chmod].
+ # - <tt>noop: true</tt> - does not copy entries; returns +nil+.
+ # - <tt>owner: <i>owner</i></tt> - changes the owner if not +nil+,
+ # using {File.chown}[rdoc-ref:File.chown].
+ # - <tt>preserve: true</tt> - preserve timestamps
+ # using {File.utime}[rdoc-ref:File.utime].
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.install('src0.txt', 'dest0.txt', noop: true, verbose: true)
+ # Bundler::FileUtils.install('src1.txt', 'dest1.txt', noop: true, verbose: true)
+ # Bundler::FileUtils.install('src2.txt', 'dest2', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # install -c src0.txt dest0.txt
+ # install -c src1.txt dest1.txt
+ # install -c src2.txt dest2
+ #
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
+ #
+ def install(src, dest, mode: nil, owner: nil, group: nil, preserve: nil,
+ noop: nil, verbose: nil)
+ if verbose
+ msg = +"install -c"
+ msg << ' -p' if preserve
+ msg << ' -m ' << mode_to_s(mode) if mode
+ msg << " -o #{owner}" if owner
+ msg << " -g #{group}" if group
+ msg << ' ' << [src,dest].flatten.join(' ')
+ fu_output_message msg
+ end
+ return if noop
+ uid = fu_get_uid(owner)
+ gid = fu_get_gid(group)
+ fu_each_src_dest(src, dest) do |s, d|
+ st = File.stat(s)
+ unless File.exist?(d) and compare_file(s, d)
+ remove_file d, true
+ if d.end_with?('/')
+ mkdir_p d
+ copy_file s, d + File.basename(s)
+ else
+ mkdir_p File.expand_path('..', d)
+ copy_file s, d
+ end
+ File.utime st.atime, st.mtime, d if preserve
+ File.chmod fu_mode(mode, st), d if mode
+ File.chown uid, gid, d if uid or gid
+ end
+ end
+ end
+ module_function :install
+
+ def user_mask(target) #:nodoc:
+ target.each_char.inject(0) do |mask, chr|
+ case chr
+ when "u"
+ mask | 04700
+ when "g"
+ mask | 02070
+ when "o"
+ mask | 01007
+ when "a"
+ mask | 07777
+ else
+ raise ArgumentError, "invalid 'who' symbol in file mode: #{chr}"
+ end
+ end
+ end
+ private_module_function :user_mask
+
+ def apply_mask(mode, user_mask, op, mode_mask) #:nodoc:
+ case op
+ when '='
+ (mode & ~user_mask) | (user_mask & mode_mask)
+ when '+'
+ mode | (user_mask & mode_mask)
+ when '-'
+ mode & ~(user_mask & mode_mask)
+ end
+ end
+ private_module_function :apply_mask
+
+ def symbolic_modes_to_i(mode_sym, path) #:nodoc:
+ path = File.stat(path) unless File::Stat === path
+ mode = path.mode
+ mode_sym.split(/,/).inject(mode & 07777) do |current_mode, clause|
+ target, *actions = clause.split(/([=+-])/)
+ raise ArgumentError, "invalid file mode: #{mode_sym}" if actions.empty?
+ target = 'a' if target.empty?
+ user_mask = user_mask(target)
+ actions.each_slice(2) do |op, perm|
+ need_apply = op == '='
+ mode_mask = (perm || '').each_char.inject(0) do |mask, chr|
+ case chr
+ when "r"
+ mask | 0444
+ when "w"
+ mask | 0222
+ when "x"
+ mask | 0111
+ when "X"
+ if path.directory?
+ mask | 0111
+ else
+ mask
+ end
+ when "s"
+ mask | 06000
+ when "t"
+ mask | 01000
+ when "u", "g", "o"
+ if mask.nonzero?
+ current_mode = apply_mask(current_mode, user_mask, op, mask)
+ end
+ need_apply = false
+ copy_mask = user_mask(chr)
+ (current_mode & copy_mask) / (copy_mask & 0111) * (user_mask & 0111)
+ else
+ raise ArgumentError, "invalid 'perm' symbol in file mode: #{chr}"
+ end
+ end
+
+ if mode_mask.nonzero? || need_apply
+ current_mode = apply_mask(current_mode, user_mask, op, mode_mask)
+ end
+ end
+ current_mode
+ end
+ end
+ private_module_function :symbolic_modes_to_i
+
+ def fu_mode(mode, path) #:nodoc:
+ mode.is_a?(String) ? symbolic_modes_to_i(mode, path) : mode
+ end
+ private_module_function :fu_mode
+
+ def mode_to_s(mode) #:nodoc:
+ mode.is_a?(String) ? mode : "%o" % mode
+ end
+ private_module_function :mode_to_s
+
+ # Changes permissions on the entries at the paths given in +list+
+ # (a single path or an array of paths)
+ # to the permissions given by +mode+;
+ # returns +list+ if it is an array, <tt>[list]</tt> otherwise:
+ #
+ # - Modifies each entry that is a regular file using
+ # {File.chmod}[rdoc-ref:File.chmod].
+ # - Modifies each entry that is a symbolic link using
+ # {File.lchmod}[rdoc-ref:File.lchmod].
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Argument +mode+ may be either an integer or a string:
+ #
+ # - \Integer +mode+: represents the permission bits to be set:
+ #
+ # Bundler::FileUtils.chmod(0755, 'src0.txt')
+ # Bundler::FileUtils.chmod(0644, ['src0.txt', 'src0.dat'])
+ #
+ # - \String +mode+: represents the permissions to be set:
+ #
+ # The string is of the form <tt>[targets][[operator][perms[,perms]]</tt>, where:
+ #
+ # - +targets+ may be any combination of these letters:
+ #
+ # - <tt>'u'</tt>: permissions apply to the file's owner.
+ # - <tt>'g'</tt>: permissions apply to users in the file's group.
+ # - <tt>'o'</tt>: permissions apply to other users not in the file's group.
+ # - <tt>'a'</tt> (the default): permissions apply to all users.
+ #
+ # - +operator+ may be one of these letters:
+ #
+ # - <tt>'+'</tt>: adds permissions.
+ # - <tt>'-'</tt>: removes permissions.
+ # - <tt>'='</tt>: sets (replaces) permissions.
+ #
+ # - +perms+ (may be repeated, with separating commas)
+ # may be any combination of these letters:
+ #
+ # - <tt>'r'</tt>: Read.
+ # - <tt>'w'</tt>: Write.
+ # - <tt>'x'</tt>: Execute (search, for a directory).
+ # - <tt>'X'</tt>: Search (for a directories only;
+ # must be used with <tt>'+'</tt>)
+ # - <tt>'s'</tt>: Uid or gid.
+ # - <tt>'t'</tt>: Sticky bit.
+ #
+ # Examples:
+ #
+ # Bundler::FileUtils.chmod('u=wrx,go=rx', 'src1.txt')
+ # Bundler::FileUtils.chmod('u=wrx,go=rx', '/usr/bin/ruby')
+ #
+ # Keyword arguments:
+ #
+ # - <tt>noop: true</tt> - does not change permissions; returns +nil+.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.chmod(0755, 'src0.txt', noop: true, verbose: true)
+ # Bundler::FileUtils.chmod(0644, ['src0.txt', 'src0.dat'], noop: true, verbose: true)
+ # Bundler::FileUtils.chmod('u=wrx,go=rx', 'src1.txt', noop: true, verbose: true)
+ # Bundler::FileUtils.chmod('u=wrx,go=rx', '/usr/bin/ruby', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # chmod 755 src0.txt
+ # chmod 644 src0.txt src0.dat
+ # chmod u=wrx,go=rx src1.txt
+ # chmod u=wrx,go=rx /usr/bin/ruby
+ #
+ # Related: Bundler::FileUtils.chmod_R.
+ #
+ def chmod(mode, list, noop: nil, verbose: nil)
+ list = fu_list(list)
+ fu_output_message sprintf('chmod %s %s', mode_to_s(mode), list.join(' ')) if verbose
+ return if noop
+ list.each do |path|
+ Entry_.new(path).chmod(fu_mode(mode, path))
+ end
+ end
+ module_function :chmod
+
+ # Like Bundler::FileUtils.chmod, but changes permissions recursively.
+ #
+ def chmod_R(mode, list, noop: nil, verbose: nil, force: nil)
+ list = fu_list(list)
+ fu_output_message sprintf('chmod -R%s %s %s',
+ (force ? 'f' : ''),
+ mode_to_s(mode), list.join(' ')) if verbose
+ return if noop
+ list.each do |root|
+ Entry_.new(root).traverse do |ent|
+ begin
+ ent.chmod(fu_mode(mode, ent.path))
+ rescue
+ raise unless force
+ end
+ end
+ end
+ end
+ module_function :chmod_R
+
+ # Changes the owner and group on the entries at the paths given in +list+
+ # (a single path or an array of paths)
+ # to the given +user+ and +group+;
+ # returns +list+ if it is an array, <tt>[list]</tt> otherwise:
+ #
+ # - Modifies each entry that is a regular file using
+ # {File.chown}[rdoc-ref:File.chown].
+ # - Modifies each entry that is a symbolic link using
+ # {File.lchown}[rdoc-ref:File.lchown].
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # User and group:
+ #
+ # - Argument +user+ may be a user name or a user id;
+ # if +nil+ or +-1+, the user is not changed.
+ # - Argument +group+ may be a group name or a group id;
+ # if +nil+ or +-1+, the group is not changed.
+ # - The user must be a member of the group.
+ #
+ # Examples:
+ #
+ # # One path.
+ # # User and group as string names.
+ # File.stat('src0.txt').uid # => 1004
+ # File.stat('src0.txt').gid # => 1004
+ # Bundler::FileUtils.chown('user2', 'group1', 'src0.txt')
+ # File.stat('src0.txt').uid # => 1006
+ # File.stat('src0.txt').gid # => 1005
+ #
+ # # User and group as uid and gid.
+ # Bundler::FileUtils.chown(1004, 1004, 'src0.txt')
+ # File.stat('src0.txt').uid # => 1004
+ # File.stat('src0.txt').gid # => 1004
+ #
+ # # Array of paths.
+ # Bundler::FileUtils.chown(1006, 1005, ['src0.txt', 'src0.dat'])
+ #
+ # # Directory (not recursive).
+ # Bundler::FileUtils.chown('user2', 'group1', '.')
+ #
+ # Keyword arguments:
+ #
+ # - <tt>noop: true</tt> - does not change permissions; returns +nil+.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.chown('user2', 'group1', 'src0.txt', noop: true, verbose: true)
+ # Bundler::FileUtils.chown(1004, 1004, 'src0.txt', noop: true, verbose: true)
+ # Bundler::FileUtils.chown(1006, 1005, ['src0.txt', 'src0.dat'], noop: true, verbose: true)
+ # Bundler::FileUtils.chown('user2', 'group1', path, noop: true, verbose: true)
+ # Bundler::FileUtils.chown('user2', 'group1', '.', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # chown user2:group1 src0.txt
+ # chown 1004:1004 src0.txt
+ # chown 1006:1005 src0.txt src0.dat
+ # chown user2:group1 src0.txt
+ # chown user2:group1 .
+ #
+ # Related: Bundler::FileUtils.chown_R.
+ #
+ def chown(user, group, list, noop: nil, verbose: nil)
+ list = fu_list(list)
+ fu_output_message sprintf('chown %s %s',
+ (group ? "#{user}:#{group}" : user || ':'),
+ list.join(' ')) if verbose
+ return if noop
+ uid = fu_get_uid(user)
+ gid = fu_get_gid(group)
+ list.each do |path|
+ Entry_.new(path).chown uid, gid
+ end
+ end
+ module_function :chown
+
+ # Like Bundler::FileUtils.chown, but changes owner and group recursively.
+ #
+ def chown_R(user, group, list, noop: nil, verbose: nil, force: nil)
+ list = fu_list(list)
+ fu_output_message sprintf('chown -R%s %s %s',
+ (force ? 'f' : ''),
+ (group ? "#{user}:#{group}" : user || ':'),
+ list.join(' ')) if verbose
+ return if noop
+ uid = fu_get_uid(user)
+ gid = fu_get_gid(group)
+ list.each do |root|
+ Entry_.new(root).traverse do |ent|
+ begin
+ ent.chown uid, gid
+ rescue
+ raise unless force
+ end
+ end
+ end
+ end
+ module_function :chown_R
+
+ def fu_get_uid(user) #:nodoc:
+ return nil unless user
+ case user
+ when Integer
+ user
+ when /\A\d+\z/
+ user.to_i
+ else
+ require 'etc'
+ Etc.getpwnam(user) ? Etc.getpwnam(user).uid : nil
+ end
+ end
+ private_module_function :fu_get_uid
+
+ def fu_get_gid(group) #:nodoc:
+ return nil unless group
+ case group
+ when Integer
+ group
+ when /\A\d+\z/
+ group.to_i
+ else
+ require 'etc'
+ Etc.getgrnam(group) ? Etc.getgrnam(group).gid : nil
+ end
+ end
+ private_module_function :fu_get_gid
+
+ # Updates modification times (mtime) and access times (atime)
+ # of the entries given by the paths in +list+
+ # (a single path or an array of paths);
+ # returns +list+ if it is an array, <tt>[list]</tt> otherwise.
+ #
+ # By default, creates an empty file for any path to a non-existent entry;
+ # use keyword argument +nocreate+ to raise an exception instead.
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Examples:
+ #
+ # # Single path.
+ # f = File.new('src0.txt') # Existing file.
+ # f.atime # => 2022-06-10 11:11:21.200277 -0700
+ # f.mtime # => 2022-06-10 11:11:21.200277 -0700
+ # Bundler::FileUtils.touch('src0.txt')
+ # f = File.new('src0.txt')
+ # f.atime # => 2022-06-11 08:28:09.8185343 -0700
+ # f.mtime # => 2022-06-11 08:28:09.8185343 -0700
+ #
+ # # Array of paths.
+ # Bundler::FileUtils.touch(['src0.txt', 'src0.dat'])
+ #
+ # Keyword arguments:
+ #
+ # - <tt>mtime: <i>time</i></tt> - sets the entry's mtime to the given time,
+ # instead of the current time.
+ # - <tt>nocreate: true</tt> - raises an exception if the entry does not exist.
+ # - <tt>noop: true</tt> - does not touch entries; returns +nil+.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # Bundler::FileUtils.touch('src0.txt', noop: true, verbose: true)
+ # Bundler::FileUtils.touch(['src0.txt', 'src0.dat'], noop: true, verbose: true)
+ # Bundler::FileUtils.touch(path, noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # touch src0.txt
+ # touch src0.txt src0.dat
+ # touch src0.txt
+ #
+ # Related: Bundler::FileUtils.uptodate?.
+ #
+ def touch(list, noop: nil, verbose: nil, mtime: nil, nocreate: nil)
+ list = fu_list(list)
+ t = mtime
+ if verbose
+ fu_output_message "touch #{nocreate ? '-c ' : ''}#{t ? t.strftime('-t %Y%m%d%H%M.%S ') : ''}#{list.join ' '}"
+ end
+ return if noop
+ list.each do |path|
+ created = nocreate
+ begin
+ File.utime(t, t, path)
+ rescue Errno::ENOENT
+ raise if created
+ File.open(path, 'a') {
+ ;
+ }
+ created = true
+ retry if t
+ end
+ end
+ end
+ module_function :touch
+
+ private
+
+ module StreamUtils_ # :nodoc:
+
+ private
+
+ case (defined?(::RbConfig) ? ::RbConfig::CONFIG['host_os'] : ::RUBY_PLATFORM)
+ when /mswin|mingw/
+ def fu_windows?; true end #:nodoc:
+ else
+ def fu_windows?; false end #:nodoc:
+ end
+
+ def fu_copy_stream0(src, dest, blksize = nil) #:nodoc:
+ IO.copy_stream(src, dest)
+ end
+
+ def fu_stream_blksize(*streams) #:nodoc:
+ streams.each do |s|
+ next unless s.respond_to?(:stat)
+ size = fu_blksize(s.stat)
+ return size if size
+ end
+ fu_default_blksize()
+ end
+
+ def fu_blksize(st) #:nodoc:
+ s = st.blksize
+ return nil unless s
+ return nil if s == 0
+ s
+ end
+
+ def fu_default_blksize #:nodoc:
+ 1024
+ end
+ end
+
+ include StreamUtils_
+ extend StreamUtils_
+
+ class Entry_ #:nodoc: internal use only
+ include StreamUtils_
+
+ def initialize(a, b = nil, deref = false)
+ @prefix = @rel = @path = nil
+ if b
+ @prefix = a
+ @rel = b
+ else
+ @path = a
+ end
+ @deref = deref
+ @stat = nil
+ @lstat = nil
+ end
+
+ def inspect
+ "\#<#{self.class} #{path()}>"
+ end
+
+ def path
+ if @path
+ File.path(@path)
+ else
+ join(@prefix, @rel)
+ end
+ end
+
+ def prefix
+ @prefix || @path
+ end
+
+ def rel
+ @rel
+ end
+
+ def dereference?
+ @deref
+ end
+
+ def exist?
+ begin
+ lstat
+ true
+ rescue Errno::ENOENT
+ false
+ end
+ end
+
+ def file?
+ s = lstat!
+ s and s.file?
+ end
+
+ def directory?
+ s = lstat!
+ s and s.directory?
+ end
+
+ def symlink?
+ s = lstat!
+ s and s.symlink?
+ end
+
+ def chardev?
+ s = lstat!
+ s and s.chardev?
+ end
+
+ def blockdev?
+ s = lstat!
+ s and s.blockdev?
+ end
+
+ def socket?
+ s = lstat!
+ s and s.socket?
+ end
+
+ def pipe?
+ s = lstat!
+ s and s.pipe?
+ end
+
+ S_IF_DOOR = 0xD000
+
+ def door?
+ s = lstat!
+ s and (s.mode & 0xF000 == S_IF_DOOR)
+ end
+
+ def entries
+ opts = {}
+ opts[:encoding] = fu_windows? ? ::Encoding::UTF_8 : path.encoding
+
+ files = Dir.children(path, **opts)
+
+ untaint = RUBY_VERSION < '2.7'
+ files.map {|n| Entry_.new(prefix(), join(rel(), untaint ? n.untaint : n)) }
+ end
+
+ def stat
+ return @stat if @stat
+ if lstat() and lstat().symlink?
+ @stat = File.stat(path())
+ else
+ @stat = lstat()
+ end
+ @stat
+ end
+
+ def stat!
+ return @stat if @stat
+ if lstat! and lstat!.symlink?
+ @stat = File.stat(path())
+ else
+ @stat = lstat!
+ end
+ @stat
+ rescue SystemCallError
+ nil
+ end
+
+ def lstat
+ if dereference?
+ @lstat ||= File.stat(path())
+ else
+ @lstat ||= File.lstat(path())
+ end
+ end
+
+ def lstat!
+ lstat()
+ rescue SystemCallError
+ nil
+ end
+
+ def chmod(mode)
+ if symlink?
+ File.lchmod mode, path() if have_lchmod?
+ else
+ File.chmod mode, path()
+ end
+ rescue Errno::EOPNOTSUPP
+ end
+
+ def chown(uid, gid)
+ if symlink?
+ File.lchown uid, gid, path() if have_lchown?
+ else
+ File.chown uid, gid, path()
+ end
+ end
+
+ def link(dest)
+ case
+ when directory?
+ if !File.exist?(dest) and descendant_directory?(dest, path)
+ raise ArgumentError, "cannot link directory %s to itself %s" % [path, dest]
+ end
+ begin
+ Dir.mkdir dest
+ rescue
+ raise unless File.directory?(dest)
+ end
+ else
+ File.link path(), dest
+ end
+ end
+
+ def copy(dest)
+ lstat
+ case
+ when file?
+ copy_file dest
+ when directory?
+ if !File.exist?(dest) and descendant_directory?(dest, path)
+ raise ArgumentError, "cannot copy directory %s to itself %s" % [path, dest]
+ end
+ begin
+ Dir.mkdir dest
+ rescue
+ raise unless File.directory?(dest)
+ end
+ when symlink?
+ File.symlink File.readlink(path()), dest
+ when chardev?, blockdev?
+ raise "cannot handle device file"
+ when socket?
+ begin
+ require 'socket'
+ rescue LoadError
+ raise "cannot handle socket"
+ else
+ raise "cannot handle socket" unless defined?(UNIXServer)
+ end
+ UNIXServer.new(dest).close
+ File.chmod lstat().mode, dest
+ when pipe?
+ raise "cannot handle FIFO" unless File.respond_to?(:mkfifo)
+ File.mkfifo dest, lstat().mode
+ when door?
+ raise "cannot handle door: #{path()}"
+ else
+ raise "unknown file type: #{path()}"
+ end
+ end
+
+ def copy_file(dest)
+ File.open(path()) do |s|
+ File.open(dest, 'wb', s.stat.mode) do |f|
+ IO.copy_stream(s, f)
+ end
+ end
+ end
+
+ def copy_metadata(path)
+ st = lstat()
+ if !st.symlink?
+ File.utime st.atime, st.mtime, path
+ end
+ mode = st.mode
+ begin
+ if st.symlink?
+ begin
+ File.lchown st.uid, st.gid, path
+ rescue NotImplementedError
+ end
+ else
+ File.chown st.uid, st.gid, path
+ end
+ rescue Errno::EPERM, Errno::EACCES
+ # clear setuid/setgid
+ mode &= 01777
+ end
+ if st.symlink?
+ begin
+ File.lchmod mode, path
+ rescue NotImplementedError, Errno::EOPNOTSUPP
+ end
+ else
+ File.chmod mode, path
+ end
+ end
+
+ def remove
+ if directory?
+ remove_dir1
+ else
+ remove_file
+ end
+ end
+
+ def remove_dir1
+ platform_support {
+ Dir.rmdir path().chomp(?/)
+ }
+ end
+
+ def remove_file
+ platform_support {
+ File.unlink path
+ }
+ end
+
+ def platform_support
+ return yield unless fu_windows?
+ first_time_p = true
+ begin
+ yield
+ rescue Errno::ENOENT
+ raise
+ rescue => err
+ if first_time_p
+ first_time_p = false
+ begin
+ File.chmod 0700, path() # Windows does not have symlink
+ retry
+ rescue SystemCallError
+ end
+ end
+ raise err
+ end
+ end
+
+ def preorder_traverse
+ stack = [self]
+ while ent = stack.pop
+ yield ent
+ stack.concat ent.entries.reverse if ent.directory?
+ end
+ end
+
+ alias traverse preorder_traverse
+
+ def postorder_traverse
+ if directory?
+ begin
+ children = entries()
+ rescue Errno::EACCES
+ # Failed to get the list of children.
+ # Assuming there is no children, try to process the parent directory.
+ yield self
+ return
+ end
+
+ children.each do |ent|
+ ent.postorder_traverse do |e|
+ yield e
+ end
+ end
+ end
+ yield self
+ end
+
+ def wrap_traverse(pre, post)
+ pre.call self
+ if directory?
+ entries.each do |ent|
+ ent.wrap_traverse pre, post
+ end
+ end
+ post.call self
+ end
+
+ private
+
+ @@fileutils_rb_have_lchmod = nil
+
+ def have_lchmod?
+ # This is not MT-safe, but it does not matter.
+ if @@fileutils_rb_have_lchmod == nil
+ @@fileutils_rb_have_lchmod = check_have_lchmod?
+ end
+ @@fileutils_rb_have_lchmod
+ end
+
+ def check_have_lchmod?
+ return false unless File.respond_to?(:lchmod)
+ File.lchmod 0
+ return true
+ rescue NotImplementedError
+ return false
+ end
+
+ @@fileutils_rb_have_lchown = nil
+
+ def have_lchown?
+ # This is not MT-safe, but it does not matter.
+ if @@fileutils_rb_have_lchown == nil
+ @@fileutils_rb_have_lchown = check_have_lchown?
+ end
+ @@fileutils_rb_have_lchown
+ end
+
+ def check_have_lchown?
+ return false unless File.respond_to?(:lchown)
+ File.lchown nil, nil
+ return true
+ rescue NotImplementedError
+ return false
+ end
+
+ def join(dir, base)
+ return File.path(dir) if not base or base == '.'
+ return File.path(base) if not dir or dir == '.'
+ begin
+ File.join(dir, base)
+ rescue EncodingError
+ if fu_windows?
+ File.join(dir.encode(::Encoding::UTF_8), base.encode(::Encoding::UTF_8))
+ else
+ raise
+ end
+ end
+ end
+
+ if File::ALT_SEPARATOR
+ DIRECTORY_TERM = "(?=[/#{Regexp.quote(File::ALT_SEPARATOR)}]|\\z)"
+ else
+ DIRECTORY_TERM = "(?=/|\\z)"
+ end
+
+ def descendant_directory?(descendant, ascendant)
+ if File::FNM_SYSCASE.nonzero?
+ File.expand_path(File.dirname(descendant)).casecmp(File.expand_path(ascendant)) == 0
+ else
+ File.expand_path(File.dirname(descendant)) == File.expand_path(ascendant)
+ end
+ end
+ end # class Entry_
+
+ def fu_list(arg) #:nodoc:
+ [arg].flatten.map {|path| File.path(path) }
+ end
+ private_module_function :fu_list
+
+ def fu_each_src_dest(src, dest) #:nodoc:
+ fu_each_src_dest0(src, dest) do |s, d|
+ raise ArgumentError, "same file: #{s} and #{d}" if fu_same?(s, d)
+ yield s, d
+ end
+ end
+ private_module_function :fu_each_src_dest
+
+ def fu_each_src_dest0(src, dest, target_directory = true) #:nodoc:
+ if tmp = Array.try_convert(src)
+ unless target_directory or tmp.size <= 1
+ tmp = tmp.map {|f| File.path(f)} # A workaround for RBS
+ raise ArgumentError, "extra target #{tmp}"
+ end
+ tmp.each do |s|
+ s = File.path(s)
+ yield s, (target_directory ? File.join(dest, File.basename(s)) : dest)
+ end
+ else
+ src = File.path(src)
+ if target_directory and File.directory?(dest)
+ yield src, File.join(dest, File.basename(src))
+ else
+ yield src, File.path(dest)
+ end
+ end
+ end
+ private_module_function :fu_each_src_dest0
+
+ def fu_same?(a, b) #:nodoc:
+ File.identical?(a, b)
+ end
+ private_module_function :fu_same?
+
+ def fu_output_message(msg) #:nodoc:
+ output = @fileutils_output if defined?(@fileutils_output)
+ output ||= $stdout
+ if defined?(@fileutils_label)
+ msg = @fileutils_label + msg
+ end
+ output.puts msg
+ end
+ private_module_function :fu_output_message
+
+ def fu_split_path(path) #:nodoc:
+ path = File.path(path)
+ list = []
+ until (parent, base = File.split(path); parent == path or parent == ".")
+ if base != '..' and list.last == '..' and !(fu_have_symlink? && File.symlink?(path))
+ list.pop
+ else
+ list << base
+ end
+ path = parent
+ end
+ list << path
+ list.reverse!
+ end
+ private_module_function :fu_split_path
+
+ def fu_common_components(target, base) #:nodoc:
+ i = 0
+ while target[i]&.== base[i]
+ i += 1
+ end
+ i
+ end
+ private_module_function :fu_common_components
+
+ def fu_clean_components(*comp) #:nodoc:
+ 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.sub!(%r((?<=\A|/)[^/]+/\z), "")
+ else
+ clean << c
+ path << c << "/"
+ end
+ end
+ clean
+ end
+ private_module_function :fu_clean_components
+
+ if fu_windows?
+ def fu_starting_path?(path) #:nodoc:
+ path&.start_with?(%r(\w:|/))
+ end
+ else
+ def fu_starting_path?(path) #:nodoc:
+ path&.start_with?("/")
+ end
+ end
+ private_module_function :fu_starting_path?
+
+ # This hash table holds command options.
+ OPT_TABLE = {} #:nodoc: internal use only
+ (private_instance_methods & methods(false)).inject(OPT_TABLE) {|tbl, name|
+ (tbl[name.to_s] = instance_method(name).parameters).map! {|t, n| n if t == :key}.compact!
+ tbl
+ }
+
+ public
+
+ # Returns an array of the string names of \Bundler::FileUtils methods
+ # that accept one or more keyword arguments:
+ #
+ # Bundler::FileUtils.commands.sort.take(3) # => ["cd", "chdir", "chmod"]
+ #
+ def self.commands
+ OPT_TABLE.keys
+ end
+
+ # Returns an array of the string keyword names:
+ #
+ # Bundler::FileUtils.options.take(3) # => ["noop", "verbose", "force"]
+ #
+ def self.options
+ OPT_TABLE.values.flatten.uniq.map {|sym| sym.to_s }
+ end
+
+ # Returns +true+ if method +mid+ accepts the given option +opt+, +false+ otherwise;
+ # the arguments may be strings or symbols:
+ #
+ # Bundler::FileUtils.have_option?(:chmod, :noop) # => true
+ # Bundler::FileUtils.have_option?('chmod', 'secure') # => false
+ #
+ def self.have_option?(mid, opt)
+ li = OPT_TABLE[mid.to_s] or raise ArgumentError, "no such method: #{mid}"
+ li.include?(opt)
+ end
+
+ # Returns an array of the string keyword name for method +mid+;
+ # the argument may be a string or a symbol:
+ #
+ # Bundler::FileUtils.options_of(:rm) # => ["force", "noop", "verbose"]
+ # Bundler::FileUtils.options_of('mv') # => ["force", "noop", "verbose", "secure"]
+ #
+ def self.options_of(mid)
+ OPT_TABLE[mid.to_s].map {|sym| sym.to_s }
+ end
+
+ # Returns an array of the string method names of the methods
+ # that accept the given keyword option +opt+;
+ # the argument must be a symbol:
+ #
+ # Bundler::FileUtils.collect_method(:preserve) # => ["cp", "copy", "cp_r", "install"]
+ #
+ def self.collect_method(opt)
+ OPT_TABLE.keys.select {|m| OPT_TABLE[m].include?(opt) }
+ end
+
+ private
+
+ LOW_METHODS = singleton_methods(false) - collect_method(:noop).map(&:intern) # :nodoc:
+ module LowMethods # :nodoc: internal use only
+ private
+ def _do_nothing(*)end
+ ::Bundler::FileUtils::LOW_METHODS.map {|name| alias_method name, :_do_nothing}
+ end
+
+ METHODS = singleton_methods() - [:private_module_function, # :nodoc:
+ :commands, :options, :have_option?, :options_of, :collect_method]
+
+ #
+ # This module has all methods of Bundler::FileUtils module, but it outputs messages
+ # before acting. This equates to passing the <tt>:verbose</tt> flag to
+ # methods in Bundler::FileUtils.
+ #
+ module Verbose
+ include Bundler::FileUtils
+ names = ::Bundler::FileUtils.collect_method(:verbose)
+ names.each do |name|
+ module_eval(<<-EOS, __FILE__, __LINE__ + 1)
+ def #{name}(*args, **options)
+ super(*args, **options, verbose: true)
+ end
+ EOS
+ end
+ private(*names)
+ extend self
+ class << self
+ public(*::Bundler::FileUtils::METHODS)
+ end
+ end
+
+ #
+ # This module has all methods of Bundler::FileUtils module, but never changes
+ # files/directories. This equates to passing the <tt>:noop</tt> flag
+ # to methods in Bundler::FileUtils.
+ #
+ module NoWrite
+ include Bundler::FileUtils
+ include LowMethods
+ names = ::Bundler::FileUtils.collect_method(:noop)
+ names.each do |name|
+ module_eval(<<-EOS, __FILE__, __LINE__ + 1)
+ def #{name}(*args, **options)
+ super(*args, **options, noop: true)
+ end
+ EOS
+ end
+ private(*names)
+ extend self
+ class << self
+ public(*::Bundler::FileUtils::METHODS)
+ end
+ end
+
+ #
+ # This module has all methods of Bundler::FileUtils module, but never changes
+ # files/directories, with printing message before acting.
+ # This equates to passing the <tt>:noop</tt> and <tt>:verbose</tt> flag
+ # to methods in Bundler::FileUtils.
+ #
+ module DryRun
+ include Bundler::FileUtils
+ include LowMethods
+ names = ::Bundler::FileUtils.collect_method(:noop)
+ names.each do |name|
+ module_eval(<<-EOS, __FILE__, __LINE__ + 1)
+ def #{name}(*args, **options)
+ super(*args, **options, noop: true, verbose: true)
+ end
+ EOS
+ end
+ private(*names)
+ extend self
+ class << self
+ public(*::Bundler::FileUtils::METHODS)
+ end
+ end
+
+end
diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb
new file mode 100644
index 0000000000..93e403a5bb
--- /dev/null
+++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb
@@ -0,0 +1,1153 @@
+require_relative '../../../../../vendored_net_http'
+require_relative '../../../../../vendored_uri'
+begin
+ require 'cgi/escape'
+rescue LoadError
+ require 'cgi/util' # for escaping
+end
+require_relative '../../../../connection_pool/lib/connection_pool'
+
+autoload :OpenSSL, 'openssl'
+
+##
+# Persistent connections for Gem::Net::HTTP
+#
+# Gem::Net::HTTP::Persistent maintains persistent connections across all the
+# servers you wish to talk to. For each host:port you communicate with a
+# single persistent connection is created.
+#
+# Connections will be shared across threads through a connection pool to
+# increase reuse of connections.
+#
+# You can shut down any remaining HTTP connections when done by calling
+# #shutdown.
+#
+# Example:
+#
+# require 'bundler/vendor/net-http-persistent/lib/net/http/persistent'
+#
+# uri = Gem::URI 'http://example.com/awesome/web/service'
+#
+# http = Gem::Net::HTTP::Persistent.new
+#
+# # perform a GET
+# response = http.request uri
+#
+# # or
+#
+# get = Gem::Net::HTTP::Get.new uri.request_uri
+# response = http.request get
+#
+# # create a POST
+# post_uri = uri + 'create'
+# post = Gem::Net::HTTP::Post.new post_uri.path
+# post.set_form_data 'some' => 'cool data'
+#
+# # perform the POST, the Gem::URI is always required
+# response http.request post_uri, post
+#
+# âš  Note that for GET, HEAD and other requests that do not have a body,
+# it uses Gem::URI#request_uri as default to send query params
+#
+# == TLS/SSL
+#
+# TLS connections are automatically created depending upon the scheme of the
+# Gem::URI. TLS connections are automatically verified against the default
+# certificate store for your computer. You can override this by changing
+# verify_mode or by specifying an alternate cert_store.
+#
+# Here are the TLS settings, see the individual methods for documentation:
+#
+# #certificate :: This client's certificate
+# #ca_file :: The certificate-authorities
+# #ca_path :: Directory with certificate-authorities
+# #cert_store :: An SSL certificate store
+# #ciphers :: List of SSl ciphers allowed
+# #extra_chain_cert :: Extra certificates to be added to the certificate chain
+# #private_key :: The client's SSL private key
+# #reuse_ssl_sessions :: Reuse a previously opened SSL session for a new
+# connection
+# #ssl_timeout :: Session lifetime
+# #ssl_version :: Which specific SSL version to use
+# #verify_callback :: For server certificate verification
+# #verify_depth :: Depth of certificate verification
+# #verify_mode :: How connections should be verified
+# #verify_hostname :: Use hostname verification for server certificate
+# during the handshake
+#
+# == Proxies
+#
+# A proxy can be set through #proxy= or at initialization time by providing a
+# second argument to ::new. The proxy may be the Gem::URI of the proxy server or
+# <code>:ENV</code> which will consult environment variables.
+#
+# See #proxy= and #proxy_from_env for details.
+#
+# == Headers
+#
+# Headers may be specified for use in every request. #headers are appended to
+# any headers on the request. #override_headers replace existing headers on
+# the request.
+#
+# The difference between the two can be seen in setting the User-Agent. Using
+# <code>http.headers['User-Agent'] = 'MyUserAgent'</code> will send "Ruby,
+# MyUserAgent" while <code>http.override_headers['User-Agent'] =
+# 'MyUserAgent'</code> will send "MyUserAgent".
+#
+# == Tuning
+#
+# === Segregation
+#
+# Each Gem::Net::HTTP::Persistent instance has its own pool of connections. There
+# is no sharing with other instances (as was true in earlier versions).
+#
+# === Idle Timeout
+#
+# If a connection hasn't been used for this number of seconds it will
+# automatically be reset upon the next use to avoid attempting to send to a
+# closed connection. The default value is 5 seconds. nil means no timeout.
+# Set through #idle_timeout.
+#
+# Reducing this value may help avoid the "too many connection resets" error
+# when sending non-idempotent requests while increasing this value will cause
+# fewer round-trips.
+#
+# === Read Timeout
+#
+# The amount of time allowed between reading two chunks from the socket. Set
+# through #read_timeout
+#
+# === Max Requests
+#
+# The number of requests that should be made before opening a new connection.
+# Typically many keep-alive capable servers tune this to 100 or less, so the
+# 101st request will fail with ECONNRESET. If unset (default), this value has
+# no effect, if set, connections will be reset on the request after
+# max_requests.
+#
+# === Open Timeout
+#
+# The amount of time to wait for a connection to be opened. Set through
+# #open_timeout.
+#
+# === Socket Options
+#
+# Socket options may be set on newly-created connections. See #socket_options
+# for details.
+#
+# === Connection Termination
+#
+# If you are done using the Gem::Net::HTTP::Persistent instance you may shut down
+# all the connections in the current thread with #shutdown. This is not
+# recommended for normal use, it should only be used when it will be several
+# minutes before you make another HTTP request.
+#
+# If you are using multiple threads, call #shutdown in each thread when the
+# thread is done making requests. If you don't call shutdown, that's OK.
+# Ruby will automatically garbage collect and shutdown your HTTP connections
+# when the thread terminates.
+
+class Gem::Net::HTTP::Persistent
+
+ ##
+ # The beginning of Time
+
+ EPOCH = Time.at 0 # :nodoc:
+
+ ##
+ # Is OpenSSL available? This test works with autoload
+
+ HAVE_OPENSSL = defined? OpenSSL::SSL # :nodoc:
+
+ ##
+ # The default connection pool size is 1/4 the allowed open files
+ # (<code>ulimit -n</code>) or 256 if your OS does not support file handle
+ # limits (typically windows).
+
+ if Process.const_defined? :RLIMIT_NOFILE
+ open_file_limits = Process.getrlimit(Process::RLIMIT_NOFILE)
+
+ # Under JRuby on Windows Process responds to `getrlimit` but returns something that does not match docs
+ if open_file_limits.respond_to?(:first)
+ DEFAULT_POOL_SIZE = open_file_limits.first / 4
+ else
+ DEFAULT_POOL_SIZE = 256
+ end
+ else
+ DEFAULT_POOL_SIZE = 256
+ end
+
+ ##
+ # The version of Gem::Net::HTTP::Persistent you are using
+
+ VERSION = '4.0.6'
+
+ ##
+ # Error class for errors raised by Gem::Net::HTTP::Persistent. Various
+ # SystemCallErrors are re-raised with a human-readable message under this
+ # class.
+
+ class Error < StandardError; end
+
+ ##
+ # Use this method to detect the idle timeout of the host at +uri+. The
+ # value returned can be used to configure #idle_timeout. +max+ controls the
+ # maximum idle timeout to detect.
+ #
+ # After
+ #
+ # Idle timeout detection is performed by creating a connection then
+ # performing a HEAD request in a loop until the connection terminates
+ # waiting one additional second per loop.
+ #
+ # NOTE: This may not work on ruby > 1.9.
+
+ def self.detect_idle_timeout uri, max = 10
+ uri = Gem::URI uri unless Gem::URI::Generic === uri
+ uri += '/'
+
+ req = Gem::Net::HTTP::Head.new uri.request_uri
+
+ http = new 'net-http-persistent detect_idle_timeout'
+
+ http.connection_for uri do |connection|
+ sleep_time = 0
+
+ http = connection.http
+
+ loop do
+ response = http.request req
+
+ $stderr.puts "HEAD #{uri} => #{response.code}" if $DEBUG
+
+ unless Gem::Net::HTTPOK === response then
+ raise Error, "bad response code #{response.code} detecting idle timeout"
+ end
+
+ break if sleep_time >= max
+
+ sleep_time += 1
+
+ $stderr.puts "sleeping #{sleep_time}" if $DEBUG
+ sleep sleep_time
+ end
+ end
+ rescue
+ # ignore StandardErrors, we've probably found the idle timeout.
+ ensure
+ return sleep_time unless $!
+ end
+
+ ##
+ # This client's OpenSSL::X509::Certificate
+
+ attr_reader :certificate
+
+ ##
+ # For Gem::Net::HTTP parity
+
+ alias cert certificate
+
+ ##
+ # An SSL certificate authority. Setting this will set verify_mode to
+ # VERIFY_PEER.
+
+ attr_reader :ca_file
+
+ ##
+ # A directory of SSL certificates to be used as certificate authorities.
+ # Setting this will set verify_mode to VERIFY_PEER.
+
+ attr_reader :ca_path
+
+ ##
+ # An SSL certificate store. Setting this will override the default
+ # certificate store. See verify_mode for more information.
+
+ attr_reader :cert_store
+
+ ##
+ # The ciphers allowed for SSL connections
+
+ attr_reader :ciphers
+
+ ##
+ # Extra certificates to be added to the certificate chain
+
+ attr_reader :extra_chain_cert
+
+ ##
+ # Sends debug_output to this IO via Gem::Net::HTTP#set_debug_output.
+ #
+ # Never use this method in production code, it causes a serious security
+ # hole.
+
+ attr_accessor :debug_output
+
+ ##
+ # Current connection generation
+
+ attr_reader :generation # :nodoc:
+
+ ##
+ # Headers that are added to every request using Gem::Net::HTTP#add_field
+
+ attr_reader :headers
+
+ ##
+ # Maps host:port to an HTTP version. This allows us to enable version
+ # specific features.
+
+ attr_reader :http_versions
+
+ ##
+ # Maximum time an unused connection can remain idle before being
+ # automatically closed.
+
+ attr_accessor :idle_timeout
+
+ ##
+ # Maximum number of requests on a connection before it is considered expired
+ # and automatically closed.
+
+ attr_accessor :max_requests
+
+ ##
+ # Number of retries to perform if a request fails.
+ #
+ # See also #max_retries=, Gem::Net::HTTP#max_retries=.
+
+ attr_reader :max_retries
+
+ ##
+ # The value sent in the Keep-Alive header. Defaults to 30. Not needed for
+ # HTTP/1.1 servers.
+ #
+ # This may not work correctly for HTTP/1.0 servers
+ #
+ # This method may be removed in a future version as RFC 2616 does not
+ # require this header.
+
+ attr_accessor :keep_alive
+
+ ##
+ # The name for this collection of persistent connections.
+
+ attr_reader :name
+
+ ##
+ # Seconds to wait until a connection is opened. See Gem::Net::HTTP#open_timeout
+
+ attr_accessor :open_timeout
+
+ ##
+ # Headers that are added to every request using Gem::Net::HTTP#[]=
+
+ attr_reader :override_headers
+
+ ##
+ # This client's SSL private key
+
+ attr_reader :private_key
+
+ ##
+ # For Gem::Net::HTTP parity
+
+ alias key private_key
+
+ ##
+ # The URL through which requests will be proxied
+
+ attr_reader :proxy_uri
+
+ ##
+ # List of host suffixes which will not be proxied
+
+ attr_reader :no_proxy
+
+ ##
+ # Test-only accessor for the connection pool
+
+ attr_reader :pool # :nodoc:
+
+ ##
+ # Seconds to wait until reading one block. See Gem::Net::HTTP#read_timeout
+
+ attr_accessor :read_timeout
+
+ ##
+ # Seconds to wait until writing one block. See Gem::Net::HTTP#write_timeout
+
+ attr_accessor :write_timeout
+
+ ##
+ # By default SSL sessions are reused to avoid extra SSL handshakes. Set
+ # this to false if you have problems communicating with an HTTPS server
+ # like:
+ #
+ # SSL_connect [...] read finished A: unexpected message (OpenSSL::SSL::SSLError)
+
+ attr_accessor :reuse_ssl_sessions
+
+ ##
+ # An array of options for Socket#setsockopt.
+ #
+ # By default the TCP_NODELAY option is set on sockets.
+ #
+ # To set additional options append them to this array:
+ #
+ # http.socket_options << [Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1]
+
+ attr_reader :socket_options
+
+ ##
+ # Current SSL connection generation
+
+ attr_reader :ssl_generation # :nodoc:
+
+ ##
+ # SSL session lifetime
+
+ attr_reader :ssl_timeout
+
+ ##
+ # SSL version to use.
+ #
+ # By default, the version will be negotiated automatically between client
+ # and server. Ruby 1.9 and newer only. Deprecated since Ruby 2.5.
+
+ attr_reader :ssl_version
+
+ ##
+ # Minimum SSL version to use, e.g. :TLS1_1
+ #
+ # By default, the version will be negotiated automatically between client
+ # and server. Ruby 2.5 and newer only.
+
+ attr_reader :min_version
+
+ ##
+ # Maximum SSL version to use, e.g. :TLS1_2
+ #
+ # By default, the version will be negotiated automatically between client
+ # and server. Ruby 2.5 and newer only.
+
+ attr_reader :max_version
+
+ ##
+ # Where this instance's last-use times live in the thread local variables
+
+ attr_reader :timeout_key # :nodoc:
+
+ ##
+ # SSL verification callback. Used when ca_file or ca_path is set.
+
+ attr_reader :verify_callback
+
+ ##
+ # Sets the depth of SSL certificate verification
+
+ attr_reader :verify_depth
+
+ ##
+ # HTTPS verify mode. Defaults to OpenSSL::SSL::VERIFY_PEER which verifies
+ # the server certificate.
+ #
+ # If no ca_file, ca_path or cert_store is set the default system certificate
+ # store is used.
+ #
+ # You can use +verify_mode+ to override any default values.
+
+ attr_reader :verify_mode
+
+ ##
+ # HTTPS verify_hostname.
+ #
+ # If a client sets this to true and enables SNI with SSLSocket#hostname=,
+ # the hostname verification on the server certificate is performed
+ # automatically during the handshake using
+ # OpenSSL::SSL.verify_certificate_identity().
+ #
+ # You can set +verify_hostname+ as true to use hostname verification
+ # during the handshake.
+ #
+ # NOTE: This works with Ruby > 3.0.
+
+ attr_reader :verify_hostname
+
+ ##
+ # Creates a new Gem::Net::HTTP::Persistent.
+ #
+ # Set a +name+ for fun. Your library name should be good enough, but this
+ # otherwise has no purpose.
+ #
+ # +proxy+ may be set to a Gem::URI::HTTP or :ENV to pick up proxy options from
+ # the environment. See proxy_from_env for details.
+ #
+ # In order to use a Gem::URI for the proxy you may need to do some extra work
+ # beyond Gem::URI parsing if the proxy requires a password:
+ #
+ # proxy = Gem::URI 'http://proxy.example'
+ # proxy.user = 'AzureDiamond'
+ # proxy.password = 'hunter2'
+ #
+ # Set +pool_size+ to limit the maximum number of connections allowed.
+ # Defaults to 1/4 the number of allowed file handles or 256 if your OS does
+ # not support a limit on allowed file handles. You can have no more than
+ # this many threads with active HTTP transactions.
+
+ def initialize name: nil, proxy: nil, pool_size: DEFAULT_POOL_SIZE
+ @name = name
+
+ @debug_output = nil
+ @proxy_uri = nil
+ @no_proxy = []
+ @headers = {}
+ @override_headers = {}
+ @http_versions = {}
+ @keep_alive = 30
+ @open_timeout = nil
+ @read_timeout = nil
+ @write_timeout = nil
+ @idle_timeout = 5
+ @max_requests = nil
+ @max_retries = 1
+ @socket_options = []
+ @ssl_generation = 0 # incremented when SSL session variables change
+
+ @socket_options << [Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1] if
+ Socket.const_defined? :TCP_NODELAY
+
+ @pool = Gem::Net::HTTP::Persistent::Pool.new size: pool_size do |http_args|
+ Gem::Net::HTTP::Persistent::Connection.new Gem::Net::HTTP, http_args, @ssl_generation
+ end
+
+ @certificate = nil
+ @ca_file = nil
+ @ca_path = nil
+ @ciphers = nil
+ @private_key = nil
+ @ssl_timeout = nil
+ @ssl_version = nil
+ @min_version = nil
+ @max_version = nil
+ @verify_callback = nil
+ @verify_depth = nil
+ @verify_mode = nil
+ @verify_hostname = nil
+ @cert_store = nil
+
+ @generation = 0 # incremented when proxy Gem::URI changes
+
+ if HAVE_OPENSSL then
+ @verify_mode = OpenSSL::SSL::VERIFY_PEER
+ @reuse_ssl_sessions = OpenSSL::SSL.const_defined? :Session
+ end
+
+ self.proxy = proxy if proxy
+ end
+
+ ##
+ # Sets this client's OpenSSL::X509::Certificate
+
+ def certificate= certificate
+ @certificate = certificate
+
+ reconnect_ssl
+ end
+
+ # For Gem::Net::HTTP parity
+ alias cert= certificate=
+
+ ##
+ # Sets the SSL certificate authority file.
+
+ def ca_file= file
+ @ca_file = file
+
+ reconnect_ssl
+ end
+
+ ##
+ # Sets the SSL certificate authority path.
+
+ def ca_path= path
+ @ca_path = path
+
+ reconnect_ssl
+ end
+
+ ##
+ # Overrides the default SSL certificate store used for verifying
+ # connections.
+
+ def cert_store= store
+ @cert_store = store
+
+ reconnect_ssl
+ end
+
+ ##
+ # The ciphers allowed for SSL connections
+
+ def ciphers= ciphers
+ @ciphers = ciphers
+
+ reconnect_ssl
+ end
+
+ if Gem::Net::HTTP.method_defined?(:extra_chain_cert=)
+ ##
+ # Extra certificates to be added to the certificate chain.
+ # It is only supported starting from Gem::Net::HTTP version 0.1.1
+ def extra_chain_cert= extra_chain_cert
+ @extra_chain_cert = extra_chain_cert
+
+ reconnect_ssl
+ end
+ else
+ def extra_chain_cert= _extra_chain_cert
+ raise "extra_chain_cert= is not supported by this version of Gem::Net::HTTP"
+ end
+ end
+
+ ##
+ # Creates a new connection for +uri+
+
+ def connection_for uri
+ use_ssl = uri.scheme.downcase == 'https'
+
+ net_http_args = [uri.hostname, uri.port]
+
+ # I'm unsure if uri.host or uri.hostname should be checked against
+ # the proxy bypass list.
+ if @proxy_uri and not proxy_bypass? uri.host, uri.port then
+ net_http_args.concat @proxy_args
+ else
+ net_http_args.concat [nil, nil, nil, nil]
+ end
+
+ connection = @pool.checkout net_http_args
+
+ begin
+ http = connection.http
+
+ connection.ressl @ssl_generation if
+ connection.ssl_generation != @ssl_generation
+
+ if not http.started? then
+ ssl http if use_ssl
+ start http
+ elsif expired? connection then
+ reset connection
+ end
+
+ http.keep_alive_timeout = @idle_timeout if @idle_timeout
+ http.max_retries = @max_retries if http.respond_to?(:max_retries=)
+ http.read_timeout = @read_timeout if @read_timeout
+ http.write_timeout = @write_timeout if
+ @write_timeout && http.respond_to?(:write_timeout=)
+
+ return yield connection
+ rescue Errno::ECONNREFUSED
+ if http.proxy?
+ address = http.proxy_address
+ port = http.proxy_port
+ else
+ address = http.address
+ port = http.port
+ end
+
+ raise Error, "connection refused: #{address}:#{port}"
+ rescue Errno::EHOSTDOWN
+ if http.proxy?
+ address = http.proxy_address
+ port = http.proxy_port
+ else
+ address = http.address
+ port = http.port
+ end
+
+ raise Error, "host down: #{address}:#{port}"
+ ensure
+ @pool.checkin net_http_args
+ end
+ end
+
+ ##
+ # CGI::escape wrapper
+
+ def escape str
+ CGI.escape str if str
+ end
+
+ ##
+ # CGI::unescape wrapper
+
+ def unescape str
+ CGI.unescape str if str
+ end
+
+
+ ##
+ # Returns true if the connection should be reset due to an idle timeout, or
+ # maximum request count, false otherwise.
+
+ def expired? connection
+ return true if @max_requests && connection.requests >= @max_requests
+ return false unless @idle_timeout
+ return true if @idle_timeout.zero?
+
+ Time.now - connection.last_use > @idle_timeout
+ end
+
+ ##
+ # Starts the Gem::Net::HTTP +connection+
+
+ def start http
+ http.set_debug_output @debug_output if @debug_output
+ http.open_timeout = @open_timeout if @open_timeout
+
+ http.start
+
+ socket = http.instance_variable_get :@socket
+
+ if socket then # for fakeweb
+ @socket_options.each do |option|
+ socket.io.setsockopt(*option)
+ end
+ end
+ end
+
+ ##
+ # Finishes the Gem::Net::HTTP +connection+
+
+ def finish connection
+ connection.finish
+
+ connection.http.instance_variable_set :@last_communicated, nil
+ connection.http.instance_variable_set :@ssl_session, nil unless
+ @reuse_ssl_sessions
+ end
+
+ ##
+ # Returns the HTTP protocol version for +uri+
+
+ def http_version uri
+ @http_versions["#{uri.hostname}:#{uri.port}"]
+ end
+
+ ##
+ # Adds "http://" to the String +uri+ if it is missing.
+
+ def normalize_uri uri
+ (uri =~ /^https?:/) ? uri : "http://#{uri}"
+ end
+
+ ##
+ # Set the maximum number of retries for a request.
+ #
+ # Defaults to one retry.
+ #
+ # Set this to 0 to disable retries.
+
+ def max_retries= retries
+ retries = retries.to_int
+
+ raise ArgumentError, "max_retries must be positive" if retries < 0
+
+ @max_retries = retries
+
+ reconnect
+ end
+
+ ##
+ # Sets this client's SSL private key
+
+ def private_key= key
+ @private_key = key
+
+ reconnect_ssl
+ end
+
+ # For Gem::Net::HTTP parity
+ alias key= private_key=
+
+ ##
+ # Sets the proxy server. The +proxy+ may be the Gem::URI of the proxy server,
+ # the symbol +:ENV+ which will read the proxy from the environment or nil to
+ # disable use of a proxy. See #proxy_from_env for details on setting the
+ # proxy from the environment.
+ #
+ # If the proxy Gem::URI is set after requests have been made, the next request
+ # will shut-down and re-open all connections.
+ #
+ # The +no_proxy+ query parameter can be used to specify hosts which shouldn't
+ # be reached via proxy; if set it should be a comma separated list of
+ # hostname suffixes, optionally with +:port+ appended, for example
+ # <tt>example.com,some.host:8080</tt>.
+
+ def proxy= proxy
+ @proxy_uri = case proxy
+ when :ENV then proxy_from_env
+ when Gem::URI::HTTP then proxy
+ when nil then # ignore
+ else raise ArgumentError, 'proxy must be :ENV or a Gem::URI::HTTP'
+ end
+
+ @no_proxy.clear
+
+ if @proxy_uri then
+ @proxy_args = [
+ @proxy_uri.hostname,
+ @proxy_uri.port,
+ unescape(@proxy_uri.user),
+ unescape(@proxy_uri.password),
+ ]
+
+ @proxy_connection_id = [nil, *@proxy_args].join ':'
+
+ if @proxy_uri.query then
+ @no_proxy = Gem::URI.decode_www_form(@proxy_uri.query).filter_map { |k, v| v if k == 'no_proxy' }.join(',').downcase.split(',').map { |x| x.strip }.reject { |x| x.empty? }
+ end
+ end
+
+ reconnect
+ reconnect_ssl
+ end
+
+ ##
+ # Creates a Gem::URI for an HTTP proxy server from ENV variables.
+ #
+ # If +HTTP_PROXY+ is set a proxy will be returned.
+ #
+ # If +HTTP_PROXY_USER+ or +HTTP_PROXY_PASS+ are set the Gem::URI is given the
+ # indicated user and password unless HTTP_PROXY contains either of these in
+ # the Gem::URI.
+ #
+ # The +NO_PROXY+ ENV variable can be used to specify hosts which shouldn't
+ # be reached via proxy; if set it should be a comma separated list of
+ # hostname suffixes, optionally with +:port+ appended, for example
+ # <tt>example.com,some.host:8080</tt>. When set to <tt>*</tt> no proxy will
+ # be returned.
+ #
+ # For Windows users, lowercase ENV variables are preferred over uppercase ENV
+ # variables.
+
+ def proxy_from_env
+ env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
+
+ return nil if env_proxy.nil? or env_proxy.empty?
+
+ uri = Gem::URI normalize_uri env_proxy
+
+ env_no_proxy = ENV['no_proxy'] || ENV['NO_PROXY']
+
+ # '*' is special case for always bypass
+ return nil if env_no_proxy == '*'
+
+ if env_no_proxy then
+ uri.query = "no_proxy=#{escape(env_no_proxy)}"
+ end
+
+ unless uri.user or uri.password then
+ uri.user = escape ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER']
+ uri.password = escape ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS']
+ end
+
+ uri
+ end
+
+ ##
+ # Returns true when proxy should by bypassed for host.
+
+ def proxy_bypass? host, port
+ host = host.downcase
+ host_port = [host, port].join ':'
+
+ @no_proxy.each do |name|
+ return true if host[-name.length, name.length] == name or
+ host_port[-name.length, name.length] == name
+ end
+
+ false
+ end
+
+ ##
+ # Forces reconnection of all HTTP connections, including TLS/SSL
+ # connections.
+
+ def reconnect
+ @generation += 1
+ end
+
+ ##
+ # Forces reconnection of only TLS/SSL connections.
+
+ def reconnect_ssl
+ @ssl_generation += 1
+ end
+
+ ##
+ # Finishes then restarts the Gem::Net::HTTP +connection+
+
+ def reset connection
+ http = connection.http
+
+ finish connection
+
+ start http
+ rescue Errno::ECONNREFUSED
+ e = Error.new "connection refused: #{http.address}:#{http.port}"
+ e.set_backtrace $@
+ raise e
+ rescue Errno::EHOSTDOWN
+ e = Error.new "host down: #{http.address}:#{http.port}"
+ e.set_backtrace $@
+ raise e
+ end
+
+ ##
+ # Makes a request on +uri+. If +req+ is nil a Gem::Net::HTTP::Get is performed
+ # against +uri+.
+ #
+ # If a block is passed #request behaves like Gem::Net::HTTP#request (the body of
+ # the response will not have been read).
+ #
+ # +req+ must be a Gem::Net::HTTPGenericRequest subclass (see Gem::Net::HTTP for a list).
+
+ def request uri, req = nil, &block
+ uri = Gem::URI uri
+ req = request_setup req || uri
+ response = nil
+
+ connection_for uri do |connection|
+ http = connection.http
+
+ begin
+ connection.requests += 1
+
+ response = http.request req, &block
+
+ if req.connection_close? or
+ (response.http_version <= '1.0' and
+ not response.connection_keep_alive?) or
+ response.connection_close? then
+ finish connection
+ end
+ rescue Exception # make sure to close the connection when it was interrupted
+ finish connection
+
+ raise
+ ensure
+ connection.last_use = Time.now
+ end
+ end
+
+ @http_versions["#{uri.hostname}:#{uri.port}"] ||= response.http_version
+
+ response
+ end
+
+ ##
+ # Creates a GET request if +req_or_uri+ is a Gem::URI and adds headers to the
+ # request.
+ #
+ # Returns the request.
+
+ def request_setup req_or_uri # :nodoc:
+ req = if req_or_uri.respond_to? 'request_uri' then
+ Gem::Net::HTTP::Get.new req_or_uri.request_uri
+ else
+ req_or_uri
+ end
+
+ @headers.each do |pair|
+ req.add_field(*pair)
+ end
+
+ @override_headers.each do |name, value|
+ req[name] = value
+ end
+
+ unless req['Connection'] then
+ req.add_field 'Connection', 'keep-alive'
+ req.add_field 'Keep-Alive', @keep_alive
+ end
+
+ req
+ end
+
+ ##
+ # Shuts down all connections. Attempting to checkout a connection after
+ # shutdown will raise an error.
+ #
+ # *NOTE*: Calling shutdown for can be dangerous!
+ #
+ # If any thread is still using a connection it may cause an error! Call
+ # #shutdown when you are completely done making requests!
+
+ def shutdown
+ @pool.shutdown { |http| http.finish }
+ end
+
+ ##
+ # Discard all existing connections. Subsequent checkouts will create
+ # new connections as needed.
+ #
+ # If any thread is still using a connection it may cause an error! Call
+ # #reload when you are completely done making requests!
+
+ def reload
+ @pool.reload { |http| http.finish }
+ end
+
+ ##
+ # Enables SSL on +connection+
+
+ def ssl connection
+ connection.use_ssl = true
+
+ connection.ciphers = @ciphers if @ciphers
+ connection.ssl_timeout = @ssl_timeout if @ssl_timeout
+ connection.ssl_version = @ssl_version if @ssl_version
+ connection.min_version = @min_version if @min_version
+ connection.max_version = @max_version if @max_version
+
+ connection.verify_depth = @verify_depth
+ connection.verify_mode = @verify_mode
+ connection.verify_hostname = @verify_hostname if
+ @verify_hostname != nil && connection.respond_to?(:verify_hostname=)
+
+ if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE and
+ not Object.const_defined?(:I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG) then
+ warn <<-WARNING
+ !!!SECURITY WARNING!!!
+
+The SSL HTTP connection to:
+
+ #{connection.address}:#{connection.port}
+
+ !!!MAY NOT BE VERIFIED!!!
+
+On your platform your OpenSSL implementation is broken.
+
+There is no difference between the values of VERIFY_NONE and VERIFY_PEER.
+
+This means that attempting to verify the security of SSL connections may not
+work. This exposes you to man-in-the-middle exploits, snooping on the
+contents of your connection and other dangers to the security of your data.
+
+To disable this warning define the following constant at top-level in your
+application:
+
+ I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG = nil
+
+ WARNING
+ end
+
+ connection.ca_file = @ca_file if @ca_file
+ connection.ca_path = @ca_path if @ca_path
+
+ if @ca_file or @ca_path then
+ connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ connection.verify_callback = @verify_callback if @verify_callback
+ end
+
+ if @certificate and @private_key then
+ connection.cert = @certificate
+ connection.key = @private_key
+ end
+
+ if defined?(@extra_chain_cert) and @extra_chain_cert
+ connection.extra_chain_cert = @extra_chain_cert
+ end
+
+ connection.cert_store = if @cert_store then
+ @cert_store
+ else
+ store = OpenSSL::X509::Store.new
+ store.set_default_paths
+ store
+ end
+ end
+
+ ##
+ # SSL session lifetime
+
+ def ssl_timeout= ssl_timeout
+ @ssl_timeout = ssl_timeout
+
+ reconnect_ssl
+ end
+
+ ##
+ # SSL version to use
+
+ def ssl_version= ssl_version
+ @ssl_version = ssl_version
+
+ reconnect_ssl
+ end
+
+ ##
+ # Minimum SSL version to use
+
+ def min_version= min_version
+ @min_version = min_version
+
+ reconnect_ssl
+ end
+
+ ##
+ # maximum SSL version to use
+
+ def max_version= max_version
+ @max_version = max_version
+
+ reconnect_ssl
+ end
+
+ ##
+ # Sets the depth of SSL certificate verification
+
+ def verify_depth= verify_depth
+ @verify_depth = verify_depth
+
+ reconnect_ssl
+ end
+
+ ##
+ # Sets the HTTPS verify mode. Defaults to OpenSSL::SSL::VERIFY_PEER.
+ #
+ # Setting this to VERIFY_NONE is a VERY BAD IDEA and should NEVER be used.
+ # Securely transfer the correct certificate and update the default
+ # certificate store or set the ca file instead.
+
+ def verify_mode= verify_mode
+ @verify_mode = verify_mode
+
+ reconnect_ssl
+ end
+
+ ##
+ # Sets the HTTPS verify_hostname.
+
+ def verify_hostname= verify_hostname
+ @verify_hostname = verify_hostname
+
+ reconnect_ssl
+ end
+
+ ##
+ # SSL verification callback.
+
+ def verify_callback= callback
+ @verify_callback = callback
+
+ reconnect_ssl
+ end
+end
+
+require_relative 'persistent/connection'
+require_relative 'persistent/pool'
diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb
new file mode 100644
index 0000000000..8b9ab5cc78
--- /dev/null
+++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb
@@ -0,0 +1,41 @@
+##
+# A Gem::Net::HTTP connection wrapper that holds extra information for managing the
+# connection's lifetime.
+
+class Gem::Net::HTTP::Persistent::Connection # :nodoc:
+
+ attr_accessor :http
+
+ attr_accessor :last_use
+
+ attr_accessor :requests
+
+ attr_accessor :ssl_generation
+
+ def initialize http_class, http_args, ssl_generation
+ @http = http_class.new(*http_args)
+ @ssl_generation = ssl_generation
+
+ reset
+ end
+
+ def finish
+ @http.finish
+ rescue IOError
+ ensure
+ reset
+ end
+ alias_method :close, :finish
+
+ def reset
+ @last_use = Gem::Net::HTTP::Persistent::EPOCH
+ @requests = 0
+ end
+
+ def ressl ssl_generation
+ @ssl_generation = ssl_generation
+
+ finish
+ end
+
+end
diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb
new file mode 100644
index 0000000000..04a1e754bf
--- /dev/null
+++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb
@@ -0,0 +1,65 @@
+class Gem::Net::HTTP::Persistent::Pool < Bundler::ConnectionPool # :nodoc:
+
+ attr_reader :available # :nodoc:
+ attr_reader :key # :nodoc:
+
+ def initialize(options = {}, &block)
+ super
+
+ @available = Gem::Net::HTTP::Persistent::TimedStackMulti.new(@size, &block)
+ @key = "current-#{@available.object_id}"
+ end
+
+ def checkin net_http_args
+ if net_http_args.is_a?(Hash) && net_http_args.size == 1 && net_http_args[:force]
+ # Bundler::ConnectionPool 2.4+ calls `checkin(force: true)` after fork.
+ # When this happens, we should remove all connections from Thread.current
+ if stacks = Thread.current[@key]
+ stacks.each do |http_args, connections|
+ connections.each do |conn|
+ @available.push conn, connection_args: http_args
+ end
+ connections.clear
+ end
+ end
+ else
+ stack = Thread.current[@key][net_http_args] ||= []
+
+ raise Bundler::ConnectionPool::Error, 'no connections are checked out' if
+ stack.empty?
+
+ conn = stack.pop
+
+ if stack.empty?
+ @available.push conn, connection_args: net_http_args
+
+ Thread.current[@key].delete(net_http_args)
+ Thread.current[@key] = nil if Thread.current[@key].empty?
+ end
+ end
+ nil
+ end
+
+ def checkout net_http_args
+ stacks = Thread.current[@key] ||= {}
+ stack = stacks[net_http_args] ||= []
+
+ if stack.empty? then
+ conn = @available.pop connection_args: net_http_args
+ else
+ conn = stack.last
+ end
+
+ stack.push conn
+
+ conn
+ end
+
+ def shutdown
+ Thread.current[@key] = nil
+ super
+ end
+end
+
+require_relative 'timed_stack_multi'
+
diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb
new file mode 100644
index 0000000000..034fbe39b8
--- /dev/null
+++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb
@@ -0,0 +1,80 @@
+class Gem::Net::HTTP::Persistent::TimedStackMulti < Bundler::ConnectionPool::TimedStack # :nodoc:
+
+ ##
+ # Returns a new hash that has arrays for keys
+ #
+ # Using a class method to limit the bindings referenced by the hash's
+ # default_proc
+
+ def self.hash_of_arrays # :nodoc:
+ Hash.new { |h,k| h[k] = [] }
+ end
+
+ def initialize(size = 0, &block)
+ super
+
+ @enqueued = 0
+ @ques = self.class.hash_of_arrays
+ @lru = {}
+ @key = :"connection_args-#{object_id}"
+ end
+
+ def empty?
+ (@created - @enqueued) >= @max
+ end
+
+ def length
+ @max - @created + @enqueued
+ end
+
+ private
+
+ def connection_stored? options = {} # :nodoc:
+ !@ques[options[:connection_args]].empty?
+ end
+
+ def fetch_connection options = {} # :nodoc:
+ connection_args = options[:connection_args]
+
+ @enqueued -= 1
+ lru_update connection_args
+ @ques[connection_args].pop
+ end
+
+ def lru_update connection_args # :nodoc:
+ @lru.delete connection_args
+ @lru[connection_args] = true
+ end
+
+ def shutdown_connections # :nodoc:
+ @ques.each_key do |key|
+ super connection_args: key
+ end
+ end
+
+ def store_connection obj, options = {} # :nodoc:
+ @ques[options[:connection_args]].push obj
+ @enqueued += 1
+ end
+
+ def try_create options = {} # :nodoc:
+ connection_args = options[:connection_args]
+
+ if @created >= @max && @enqueued >= 1
+ oldest, = @lru.first
+ @lru.delete oldest
+ connection = @ques[oldest].pop
+ connection.close if connection.respond_to?(:close)
+
+ @created -= 1
+ end
+
+ if @created < @max
+ @created += 1
+ lru_update connection_args
+ return @create_block.call(connection_args)
+ end
+ end
+
+end
+
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub.rb
new file mode 100644
index 0000000000..eaaba3fc98
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub.rb
@@ -0,0 +1,31 @@
+require_relative "pub_grub/package"
+require_relative "pub_grub/static_package_source"
+require_relative "pub_grub/term"
+require_relative "pub_grub/version_range"
+require_relative "pub_grub/version_constraint"
+require_relative "pub_grub/version_union"
+require_relative "pub_grub/version_solver"
+require_relative "pub_grub/incompatibility"
+require_relative 'pub_grub/solve_failure'
+require_relative 'pub_grub/failure_writer'
+require_relative 'pub_grub/version'
+
+module Bundler::PubGrub
+ class << self
+ attr_writer :logger
+
+ def logger
+ @logger || default_logger
+ end
+
+ private
+
+ def default_logger
+ require "logger"
+
+ logger = ::Logger.new(STDERR)
+ logger.level = $DEBUG ? ::Logger::DEBUG : ::Logger::WARN
+ @logger = logger
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/assignment.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/assignment.rb
new file mode 100644
index 0000000000..2236a97b5b
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/assignment.rb
@@ -0,0 +1,20 @@
+module Bundler::PubGrub
+ class Assignment
+ attr_reader :term, :cause, :decision_level, :index
+ def initialize(term, cause, decision_level, index)
+ @term = term
+ @cause = cause
+ @decision_level = decision_level
+ @index = index
+ end
+
+ def self.decision(package, version, decision_level, index)
+ term = Term.new(VersionConstraint.exact(package, version), true)
+ new(term, :decision, decision_level, index)
+ end
+
+ def decision?
+ cause == :decision
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/basic_package_source.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/basic_package_source.rb
new file mode 100644
index 0000000000..491151ec0b
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/basic_package_source.rb
@@ -0,0 +1,169 @@
+require_relative 'version_constraint'
+require_relative 'incompatibility'
+
+module Bundler::PubGrub
+ # Types:
+ #
+ # Where possible, Bundler::PubGrub will accept user-defined types, so long as they quack.
+ #
+ # ## "Package":
+ #
+ # This class will be used to represent the various packages being solved for.
+ # .to_s will be called when displaying errors and debugging info, it should
+ # probably return the package's name.
+ # It must also have a reasonable definition of #== and #hash
+ #
+ # Example classes: String ("rails")
+ #
+ #
+ # ## "Version":
+ #
+ # This class will be used to represent a single version number.
+ #
+ # Versions don't need to store their associated package, however they will
+ # only be compared against other versions of the same package.
+ #
+ # It must be Comparible (and implement <=> reasonably)
+ #
+ # Example classes: Gem::Version, Integer
+ #
+ #
+ # ## "Dependency"
+ #
+ # This class represents the requirement one package has on another. It is
+ # returned by dependencies_for(package, version) and will be passed to
+ # parse_dependency to convert it to a format Bundler::PubGrub understands.
+ #
+ # It must also have a reasonable definition of #==
+ #
+ # Example classes: String ("~> 1.0"), Gem::Requirement
+ #
+ class BasicPackageSource
+ # Override me!
+ #
+ # This is called per package to find all possible versions of a package.
+ #
+ # It is called at most once per-package
+ #
+ # Returns: Array of versions for a package, in preferred order of selection
+ def all_versions_for(package)
+ raise NotImplementedError
+ end
+
+ # Override me!
+ #
+ # Returns: Hash in the form of { package => requirement, ... }
+ def dependencies_for(package, version)
+ raise NotImplementedError
+ end
+
+ # Override me!
+ #
+ # Convert a (user-defined) dependency into a format Bundler::PubGrub understands.
+ #
+ # Package is passed to this method but for many implementations is not
+ # needed.
+ #
+ # Returns: either a Bundler::PubGrub::VersionRange, Bundler::PubGrub::VersionUnion, or a
+ # Bundler::PubGrub::VersionConstraint
+ def parse_dependency(package, dependency)
+ raise NotImplementedError
+ end
+
+ # Override me!
+ #
+ # If not overridden, this will call dependencies_for with the root package.
+ #
+ # Returns: Hash in the form of { package => requirement, ... } (see dependencies_for)
+ def root_dependencies
+ dependencies_for(@root_package, @root_version)
+ end
+
+ def initialize
+ @root_package = Package.root
+ @root_version = Package.root_version
+
+ @sorted_versions = Hash.new do |h,k|
+ if k == @root_package
+ h[k] = [@root_version]
+ else
+ h[k] = all_versions_for(k).sort
+ end
+ end
+
+ @cached_dependencies = Hash.new do |packages, package|
+ if package == @root_package
+ packages[package] = {
+ @root_version => root_dependencies
+ }
+ else
+ packages[package] = Hash.new do |versions, version|
+ versions[version] = dependencies_for(package, version)
+ end
+ end
+ end
+ end
+
+ def versions_for(package, range=VersionRange.any)
+ range.select_versions(@sorted_versions[package])
+ end
+
+ def no_versions_incompatibility_for(_package, unsatisfied_term)
+ cause = Incompatibility::NoVersions.new(unsatisfied_term)
+
+ Incompatibility.new([unsatisfied_term], cause: cause)
+ end
+
+ def incompatibilities_for(package, version)
+ package_deps = @cached_dependencies[package]
+ sorted_versions = @sorted_versions[package]
+ package_deps[version].map do |dep_package, dep_constraint_name|
+ low = high = sorted_versions.index(version)
+
+ # find version low such that all >= low share the same dep
+ while low > 0 &&
+ package_deps[sorted_versions[low - 1]][dep_package] == dep_constraint_name
+ low -= 1
+ end
+ low =
+ if low == 0
+ nil
+ else
+ sorted_versions[low]
+ end
+
+ # find version high such that all < high share the same dep
+ while high < sorted_versions.length &&
+ package_deps[sorted_versions[high]][dep_package] == dep_constraint_name
+ high += 1
+ end
+ high =
+ if high == sorted_versions.length
+ nil
+ else
+ sorted_versions[high]
+ end
+
+ range = VersionRange.new(min: low, max: high, include_min: !low.nil?)
+
+ self_constraint = VersionConstraint.new(package, range: range)
+
+ if !@packages.include?(dep_package)
+ # no such package -> this version is invalid
+ end
+
+ dep_constraint = parse_dependency(dep_package, dep_constraint_name)
+ if !dep_constraint
+ # falsey indicates this dependency was invalid
+ cause = Bundler::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint_name)
+ return [Incompatibility.new([Term.new(self_constraint, true)], cause: cause)]
+ elsif !dep_constraint.is_a?(VersionConstraint)
+ # Upgrade range/union to VersionConstraint
+ dep_constraint = VersionConstraint.new(dep_package, range: dep_constraint)
+ end
+
+ Incompatibility.new([Term.new(self_constraint, true), Term.new(dep_constraint, false)], cause: :dependency)
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/failure_writer.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/failure_writer.rb
new file mode 100644
index 0000000000..ee099b23f4
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/failure_writer.rb
@@ -0,0 +1,182 @@
+module Bundler::PubGrub
+ class FailureWriter
+ def initialize(root)
+ @root = root
+
+ # { Incompatibility => Integer }
+ @derivations = {}
+
+ # [ [ String, Integer or nil ] ]
+ @lines = []
+
+ # { Incompatibility => Integer }
+ @line_numbers = {}
+
+ count_derivations(root)
+ end
+
+ def write
+ return @root.to_s unless @root.conflict?
+
+ visit(@root)
+
+ padding = @line_numbers.empty? ? 0 : "(#{@line_numbers.values.last}) ".length
+
+ @lines.map do |message, number|
+ next "" if message.empty?
+
+ lead = number ? "(#{number}) " : ""
+ lead = lead.ljust(padding)
+ message = message.gsub("\n", "\n" + " " * (padding + 2))
+ "#{lead}#{message}"
+ end.join("\n")
+ end
+
+ private
+
+ def write_line(incompatibility, message, numbered:)
+ if numbered
+ number = @line_numbers.length + 1
+ @line_numbers[incompatibility] = number
+ end
+
+ @lines << [message, number]
+ end
+
+ def visit(incompatibility, conclusion: false)
+ raise unless incompatibility.conflict?
+
+ numbered = conclusion || @derivations[incompatibility] > 1;
+ conjunction = conclusion || incompatibility == @root ? "So," : "And"
+
+ cause = incompatibility.cause
+
+ if cause.conflict.conflict? && cause.other.conflict?
+ conflict_line = @line_numbers[cause.conflict]
+ other_line = @line_numbers[cause.other]
+
+ if conflict_line && other_line
+ write_line(
+ incompatibility,
+ "Because #{cause.conflict} (#{conflict_line})\nand #{cause.other} (#{other_line}),\n#{incompatibility}.",
+ numbered: numbered
+ )
+ elsif conflict_line || other_line
+ with_line = conflict_line ? cause.conflict : cause.other
+ without_line = conflict_line ? cause.other : cause.conflict
+ line = @line_numbers[with_line]
+
+ visit(without_line);
+ write_line(
+ incompatibility,
+ "#{conjunction} because #{with_line} (#{line}),\n#{incompatibility}.",
+ numbered: numbered
+ )
+ else
+ single_line_conflict = single_line?(cause.conflict.cause)
+ single_line_other = single_line?(cause.other.cause)
+
+ if single_line_conflict || single_line_other
+ first = single_line_other ? cause.conflict : cause.other
+ second = single_line_other ? cause.other : cause.conflict
+ visit(first)
+ visit(second)
+ write_line(
+ incompatibility,
+ "Thus, #{incompatibility}.",
+ numbered: numbered
+ )
+ else
+ visit(cause.conflict, conclusion: true)
+ @lines << ["", nil]
+ visit(cause.other)
+
+ write_line(
+ incompatibility,
+ "#{conjunction} because #{cause.conflict} (#{@line_numbers[cause.conflict]}),\n#{incompatibility}.",
+ numbered: numbered
+ )
+ end
+ end
+ elsif cause.conflict.conflict? || cause.other.conflict?
+ derived = cause.conflict.conflict? ? cause.conflict : cause.other
+ ext = cause.conflict.conflict? ? cause.other : cause.conflict
+
+ derived_line = @line_numbers[derived]
+ if derived_line
+ write_line(
+ incompatibility,
+ "Because #{ext}\nand #{derived} (#{derived_line}),\n#{incompatibility}.",
+ numbered: numbered
+ )
+ elsif collapsible?(derived)
+ derived_cause = derived.cause
+ if derived_cause.conflict.conflict?
+ collapsed_derived = derived_cause.conflict
+ collapsed_ext = derived_cause.other
+ else
+ collapsed_derived = derived_cause.other
+ collapsed_ext = derived_cause.conflict
+ end
+
+ visit(collapsed_derived)
+
+ write_line(
+ incompatibility,
+ "#{conjunction} because #{collapsed_ext}\nand #{ext},\n#{incompatibility}.",
+ numbered: numbered
+ )
+ else
+ visit(derived)
+ write_line(
+ incompatibility,
+ "#{conjunction} because #{ext},\n#{incompatibility}.",
+ numbered: numbered
+ )
+ end
+ else
+ write_line(
+ incompatibility,
+ "Because #{cause.conflict}\nand #{cause.other},\n#{incompatibility}.",
+ numbered: numbered
+ )
+ end
+ end
+
+ def single_line?(cause)
+ !cause.conflict.conflict? && !cause.other.conflict?
+ end
+
+ def collapsible?(incompatibility)
+ return false if @derivations[incompatibility] > 1
+
+ cause = incompatibility.cause
+ # If incompatibility is derived from two derived incompatibilities,
+ # there are too many transitive causes to display concisely.
+ return false if cause.conflict.conflict? && cause.other.conflict?
+
+ # If incompatibility is derived from two external incompatibilities, it
+ # tends to be confusing to collapse it.
+ return false unless cause.conflict.conflict? || cause.other.conflict?
+
+ # If incompatibility's internal cause is numbered, collapsing it would
+ # get too noisy.
+ complex = cause.conflict.conflict? ? cause.conflict : cause.other
+
+ !@line_numbers.has_key?(complex)
+ end
+
+ def count_derivations(incompatibility)
+ if @derivations.has_key?(incompatibility)
+ @derivations[incompatibility] += 1
+ else
+ @derivations[incompatibility] = 1
+ if incompatibility.conflict?
+ cause = incompatibility.cause
+ count_derivations(cause.conflict)
+ count_derivations(cause.other)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/incompatibility.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/incompatibility.rb
new file mode 100644
index 0000000000..239eaf3401
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/incompatibility.rb
@@ -0,0 +1,150 @@
+module Bundler::PubGrub
+ class Incompatibility
+ ConflictCause = Struct.new(:incompatibility, :satisfier) do
+ alias_method :conflict, :incompatibility
+ alias_method :other, :satisfier
+ end
+
+ InvalidDependency = Struct.new(:package, :constraint) do
+ end
+
+ NoVersions = Struct.new(:constraint) do
+ end
+
+ attr_reader :terms, :cause
+
+ def initialize(terms, cause:, custom_explanation: nil)
+ @cause = cause
+ @terms = cleanup_terms(terms)
+ @custom_explanation = custom_explanation
+
+ if cause == :dependency && @terms.length != 2
+ raise ArgumentError, "a dependency Incompatibility must have exactly two terms. Got #{@terms.inspect}"
+ end
+ end
+
+ def hash
+ cause.hash ^ terms.hash
+ end
+
+ def eql?(other)
+ cause.eql?(other.cause) &&
+ terms.eql?(other.terms)
+ end
+
+ def failure?
+ terms.empty? || (terms.length == 1 && Package.root?(terms[0].package) && terms[0].positive?)
+ end
+
+ def conflict?
+ ConflictCause === cause
+ end
+
+ # Returns all external incompatibilities in this incompatibility's
+ # derivation graph
+ def external_incompatibilities
+ if conflict?
+ [
+ cause.conflict,
+ cause.other
+ ].flat_map(&:external_incompatibilities)
+ else
+ [this]
+ end
+ end
+
+ def to_s
+ return @custom_explanation if @custom_explanation
+
+ case cause
+ when :root
+ "(root dependency)"
+ when :dependency
+ "#{terms[0].to_s(allow_every: true)} depends on #{terms[1].invert}"
+ when Bundler::PubGrub::Incompatibility::InvalidDependency
+ "#{terms[0].to_s(allow_every: true)} depends on unknown package #{cause.package}"
+ when Bundler::PubGrub::Incompatibility::NoVersions
+ "no versions satisfy #{cause.constraint}"
+ when Bundler::PubGrub::Incompatibility::ConflictCause
+ if failure?
+ "version solving has failed"
+ elsif terms.length == 1
+ term = terms[0]
+ if term.positive?
+ if term.constraint.any?
+ "#{term.package} cannot be used"
+ else
+ "#{term.to_s(allow_every: true)} cannot be used"
+ end
+ else
+ "#{term.invert} is required"
+ end
+ else
+ if terms.all?(&:positive?)
+ if terms.length == 2
+ "#{terms[0].to_s(allow_every: true)} is incompatible with #{terms[1]}"
+ else
+ "one of #{terms.map(&:to_s).join(" or ")} must be false"
+ end
+ elsif terms.all?(&:negative?)
+ if terms.length == 2
+ "either #{terms[0].invert} or #{terms[1].invert}"
+ else
+ "one of #{terms.map(&:invert).join(" or ")} must be true";
+ end
+ else
+ positive = terms.select(&:positive?)
+ negative = terms.select(&:negative?).map(&:invert)
+
+ if positive.length == 1
+ "#{positive[0].to_s(allow_every: true)} requires #{negative.join(" or ")}"
+ else
+ "if #{positive.join(" and ")} then #{negative.join(" or ")}"
+ end
+ end
+ end
+ else
+ raise "unhandled cause: #{cause.inspect}"
+ end
+ end
+
+ def inspect
+ "#<#{self.class} #{to_s}>"
+ end
+
+ def pretty_print(q)
+ q.group 2, "#<#{self.class}", ">" do
+ q.breakable
+ q.text to_s
+
+ q.breakable
+ q.text " caused by "
+ q.pp @cause
+ end
+ end
+
+ private
+
+ def cleanup_terms(terms)
+ terms.each do |term|
+ raise "#{term.inspect} must be a term" unless term.is_a?(Term)
+ end
+
+ if terms.length != 1 && ConflictCause === cause
+ terms = terms.reject do |term|
+ term.positive? && Package.root?(term.package)
+ end
+ end
+
+ # Optimized simple cases
+ return terms if terms.length <= 1
+ return terms if terms.length == 2 && terms[0].package != terms[1].package
+
+ terms.group_by(&:package).map do |package, common_terms|
+ common_terms.inject do |acc, term|
+ acc.intersect(term)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/package.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/package.rb
new file mode 100644
index 0000000000..efb9d3da16
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/package.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Bundler::PubGrub
+ class Package
+
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def inspect
+ "#<#{self.class} #{name.inspect}>"
+ end
+
+ def <=>(other)
+ name <=> other.name
+ end
+
+ ROOT = Package.new(:root)
+ ROOT_VERSION = 0
+
+ def self.root
+ ROOT
+ end
+
+ def self.root_version
+ ROOT_VERSION
+ end
+
+ def self.root?(package)
+ if package.respond_to?(:root?)
+ package.root?
+ else
+ package == root
+ end
+ end
+
+ def to_s
+ name.to_s
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/partial_solution.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/partial_solution.rb
new file mode 100644
index 0000000000..4c4b8ca844
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/partial_solution.rb
@@ -0,0 +1,121 @@
+require_relative 'assignment'
+
+module Bundler::PubGrub
+ class PartialSolution
+ attr_reader :assignments, :decisions
+ attr_reader :attempted_solutions
+
+ def initialize
+ reset!
+
+ @attempted_solutions = 1
+ @backtracking = false
+ end
+
+ def decision_level
+ @decisions.length
+ end
+
+ def relation(term)
+ package = term.package
+ return :overlap if !@terms.key?(package)
+
+ @relation_cache[package][term] ||=
+ @terms[package].relation(term)
+ end
+
+ def satisfies?(term)
+ relation(term) == :subset
+ end
+
+ def derive(term, cause)
+ add_assignment(Assignment.new(term, cause, decision_level, assignments.length))
+ end
+
+ def satisfier(term)
+ assignment =
+ @assignments_by[term.package].bsearch do |assignment_by|
+ @cumulative_assignments[assignment_by].satisfies?(term)
+ end
+
+ assignment || raise("#{term} unsatisfied")
+ end
+
+ # A list of unsatisfied terms
+ def unsatisfied
+ @required.keys.reject do |package|
+ @decisions.key?(package)
+ end.map do |package|
+ @terms[package]
+ end
+ end
+
+ def decide(package, version)
+ @attempted_solutions += 1 if @backtracking
+ @backtracking = false;
+
+ decisions[package] = version
+ assignment = Assignment.decision(package, version, decision_level, assignments.length)
+ add_assignment(assignment)
+ end
+
+ def backtrack(previous_level)
+ @backtracking = true
+
+ new_assignments = assignments.select do |assignment|
+ assignment.decision_level <= previous_level
+ end
+
+ new_decisions = Hash[decisions.first(previous_level)]
+
+ reset!
+
+ @decisions = new_decisions
+
+ new_assignments.each do |assignment|
+ add_assignment(assignment)
+ end
+ end
+
+ private
+
+ def reset!
+ # { Array<Assignment> }
+ @assignments = []
+
+ # { Package => Array<Assignment> }
+ @assignments_by = Hash.new { |h,k| h[k] = [] }
+ @cumulative_assignments = {}.compare_by_identity
+
+ # { Package => Package::Version }
+ @decisions = {}
+
+ # { Package => Term }
+ @terms = {}
+ @relation_cache = Hash.new { |h,k| h[k] = {} }
+
+ # { Package => Boolean }
+ @required = {}
+ end
+
+ def add_assignment(assignment)
+ term = assignment.term
+ package = term.package
+
+ @assignments << assignment
+ @assignments_by[package] << assignment
+
+ @required[package] = true if term.positive?
+
+ if @terms.key?(package)
+ old_term = @terms[package]
+ @terms[package] = old_term.intersect(term)
+ else
+ @terms[package] = term
+ end
+ @relation_cache[package].clear
+
+ @cumulative_assignments[assignment] = @terms[package]
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/rubygems.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/rubygems.rb
new file mode 100644
index 0000000000..245c23be22
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/rubygems.rb
@@ -0,0 +1,45 @@
+module Bundler::PubGrub
+ module RubyGems
+ extend self
+
+ def requirement_to_range(requirement)
+ ranges = requirement.requirements.map do |(op, ver)|
+ case op
+ when "~>"
+ name = "~> #{ver}"
+ bump = ver.class.new(ver.bump.to_s + ".A")
+ VersionRange.new(name: name, min: ver, max: bump, include_min: true)
+ when ">"
+ VersionRange.new(min: ver)
+ when ">="
+ VersionRange.new(min: ver, include_min: true)
+ when "<"
+ VersionRange.new(max: ver)
+ when "<="
+ VersionRange.new(max: ver, include_max: true)
+ when "="
+ VersionRange.new(min: ver, max: ver, include_min: true, include_max: true)
+ when "!="
+ VersionRange.new(min: ver, max: ver, include_min: true, include_max: true).invert
+ else
+ raise "bad version specifier: #{op}"
+ end
+ end
+
+ ranges.inject(&:intersect)
+ end
+
+ def requirement_to_constraint(package, requirement)
+ Bundler::PubGrub::VersionConstraint.new(package, range: requirement_to_range(requirement))
+ end
+
+ def parse_range(dep)
+ requirement_to_range(Gem::Requirement.new(dep))
+ end
+
+ def parse_constraint(package, dep)
+ range = parse_range(dep)
+ Bundler::PubGrub::VersionConstraint.new(package, range: range)
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/solve_failure.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/solve_failure.rb
new file mode 100644
index 0000000000..961a7a7c0c
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/solve_failure.rb
@@ -0,0 +1,19 @@
+require_relative 'failure_writer'
+
+module Bundler::PubGrub
+ class SolveFailure < StandardError
+ attr_reader :incompatibility
+
+ def initialize(incompatibility)
+ @incompatibility = incompatibility
+ end
+
+ def to_s
+ "Could not find compatible versions\n\n#{explanation}"
+ end
+
+ def explanation
+ @explanation ||= FailureWriter.new(@incompatibility).write
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/static_package_source.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/static_package_source.rb
new file mode 100644
index 0000000000..36ab06254d
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/static_package_source.rb
@@ -0,0 +1,61 @@
+require_relative 'package'
+require_relative 'rubygems'
+require_relative 'version_constraint'
+require_relative 'incompatibility'
+require_relative 'basic_package_source'
+
+module Bundler::PubGrub
+ class StaticPackageSource < BasicPackageSource
+ class DSL
+ def initialize(packages, root_deps)
+ @packages = packages
+ @root_deps = root_deps
+ end
+
+ def root(deps:)
+ @root_deps.update(deps)
+ end
+
+ def add(name, version, deps: {})
+ version = Gem::Version.new(version)
+ @packages[name] ||= {}
+ raise ArgumentError, "#{name} #{version} declared twice" if @packages[name].key?(version)
+ @packages[name][version] = clean_deps(name, version, deps)
+ end
+
+ private
+
+ # Exclude redundant self-referencing dependencies
+ def clean_deps(name, version, deps)
+ deps.reject {|dep_name, req| name == dep_name && Bundler::PubGrub::RubyGems.parse_range(req).include?(version) }
+ end
+ end
+
+ def initialize
+ @root_deps = {}
+ @packages = {}
+
+ yield DSL.new(@packages, @root_deps)
+
+ super()
+ end
+
+ def all_versions_for(package)
+ @packages[package].keys
+ end
+
+ def root_dependencies
+ @root_deps
+ end
+
+ def dependencies_for(package, version)
+ @packages[package][version]
+ end
+
+ def parse_dependency(package, dependency)
+ return false unless @packages.key?(package)
+
+ Bundler::PubGrub::RubyGems.parse_constraint(package, dependency)
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/strategy.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/strategy.rb
new file mode 100644
index 0000000000..6955655ba4
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/strategy.rb
@@ -0,0 +1,42 @@
+module Bundler::PubGrub
+ class Strategy
+ def initialize(source)
+ @source = source
+
+ @root_package = Package.root
+ @root_version = Package.root_version
+
+ @version_indexes = Hash.new do |h,k|
+ if k == @root_package
+ h[k] = { @root_version => 0 }
+ else
+ h[k] = @source.all_versions_for(k).each.with_index.to_h
+ end
+ end
+ end
+
+ def next_package_and_version(unsatisfied)
+ package, range = next_term_to_try_from(unsatisfied)
+
+ [package, most_preferred_version_of(package, range)]
+ end
+
+ private
+
+ def most_preferred_version_of(package, range)
+ versions = @source.versions_for(package, range)
+
+ indexes = @version_indexes[package]
+ versions.min_by { |version| indexes[version] }
+ end
+
+ def next_term_to_try_from(unsatisfied)
+ unsatisfied.min_by do |package, range|
+ matching_versions = @source.versions_for(package, range)
+ higher_versions = @source.versions_for(package, range.upper_invert)
+
+ [matching_versions.count <= 1 ? 0 : 1, higher_versions.count]
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/term.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/term.rb
new file mode 100644
index 0000000000..1d0f763378
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/term.rb
@@ -0,0 +1,105 @@
+module Bundler::PubGrub
+ class Term
+ attr_reader :package, :constraint, :positive
+
+ def initialize(constraint, positive)
+ @constraint = constraint
+ @package = @constraint.package
+ @positive = positive
+ end
+
+ def to_s(allow_every: false)
+ if positive
+ @constraint.to_s(allow_every: allow_every)
+ else
+ "not #{@constraint}"
+ end
+ end
+
+ def hash
+ constraint.hash ^ positive.hash
+ end
+
+ def eql?(other)
+ positive == other.positive &&
+ constraint.eql?(other.constraint)
+ end
+
+ def invert
+ self.class.new(@constraint, !@positive)
+ end
+ alias_method :inverse, :invert
+
+ def intersect(other)
+ raise ArgumentError, "packages must match" if package != other.package
+
+ if positive? && other.positive?
+ self.class.new(constraint.intersect(other.constraint), true)
+ elsif negative? && other.negative?
+ self.class.new(constraint.union(other.constraint), false)
+ else
+ positive = positive? ? self : other
+ negative = negative? ? self : other
+ self.class.new(positive.constraint.intersect(negative.constraint.invert), true)
+ end
+ end
+
+ def difference(other)
+ intersect(other.invert)
+ end
+
+ def relation(other)
+ if positive? && other.positive?
+ constraint.relation(other.constraint)
+ elsif negative? && other.positive?
+ if constraint.allows_all?(other.constraint)
+ :disjoint
+ else
+ :overlap
+ end
+ elsif positive? && other.negative?
+ if !other.constraint.allows_any?(constraint)
+ :subset
+ elsif other.constraint.allows_all?(constraint)
+ :disjoint
+ else
+ :overlap
+ end
+ elsif negative? && other.negative?
+ if constraint.allows_all?(other.constraint)
+ :subset
+ else
+ :overlap
+ end
+ else
+ raise
+ end
+ end
+
+ def normalized_constraint
+ @normalized_constraint ||= positive ? constraint : constraint.invert
+ end
+
+ def satisfies?(other)
+ raise ArgumentError, "packages must match" unless package == other.package
+
+ relation(other) == :subset
+ end
+
+ def positive?
+ @positive
+ end
+
+ def negative?
+ !positive?
+ end
+
+ def empty?
+ @empty ||= normalized_constraint.empty?
+ end
+
+ def inspect
+ "#<#{self.class} #{self}>"
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/version.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/version.rb
new file mode 100644
index 0000000000..d7984b3863
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/version.rb
@@ -0,0 +1,3 @@
+module Bundler::PubGrub
+ VERSION = "0.5.0"
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/version_constraint.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_constraint.rb
new file mode 100644
index 0000000000..b71f3eaf53
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_constraint.rb
@@ -0,0 +1,129 @@
+require_relative 'version_range'
+
+module Bundler::PubGrub
+ class VersionConstraint
+ attr_reader :package, :range
+
+ # @param package [Bundler::PubGrub::Package]
+ # @param range [Bundler::PubGrub::VersionRange]
+ def initialize(package, range: nil)
+ @package = package
+ @range = range
+ end
+
+ def hash
+ package.hash ^ range.hash
+ end
+
+ def ==(other)
+ package == other.package &&
+ range == other.range
+ end
+
+ def eql?(other)
+ package.eql?(other.package) &&
+ range.eql?(other.range)
+ end
+
+ class << self
+ def exact(package, version)
+ range = VersionRange.new(min: version, max: version, include_min: true, include_max: true)
+ new(package, range: range)
+ end
+
+ def any(package)
+ new(package, range: VersionRange.any)
+ end
+
+ def empty(package)
+ new(package, range: VersionRange.empty)
+ end
+ end
+
+ def intersect(other)
+ unless package == other.package
+ raise ArgumentError, "Can only intersect between VersionConstraint of the same package"
+ end
+
+ self.class.new(package, range: range.intersect(other.range))
+ end
+
+ def union(other)
+ unless package == other.package
+ raise ArgumentError, "Can only intersect between VersionConstraint of the same package"
+ end
+
+ self.class.new(package, range: range.union(other.range))
+ end
+
+ def invert
+ new_range = range.invert
+ self.class.new(package, range: new_range)
+ end
+
+ def difference(other)
+ intersect(other.invert)
+ end
+
+ def allows_all?(other)
+ range.allows_all?(other.range)
+ end
+
+ def allows_any?(other)
+ range.intersects?(other.range)
+ end
+
+ def subset?(other)
+ other.allows_all?(self)
+ end
+
+ def overlap?(other)
+ other.allows_any?(self)
+ end
+
+ def disjoint?(other)
+ !overlap?(other)
+ end
+
+ def relation(other)
+ if subset?(other)
+ :subset
+ elsif overlap?(other)
+ :overlap
+ else
+ :disjoint
+ end
+ end
+
+ def to_s(allow_every: false)
+ if Package.root?(package)
+ package.to_s
+ elsif allow_every && any?
+ "every version of #{package}"
+ else
+ "#{package} #{constraint_string}"
+ end
+ end
+
+ def constraint_string
+ if any?
+ ">= 0"
+ else
+ range.to_s
+ end
+ end
+
+ def empty?
+ range.empty?
+ end
+
+ # Does this match every version of the package
+ def any?
+ range.any?
+ end
+
+ def inspect
+ "#<#{self.class} #{self}>"
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb
new file mode 100644
index 0000000000..49dcf716a3
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb
@@ -0,0 +1,423 @@
+# frozen_string_literal: true
+
+module Bundler::PubGrub
+ class VersionRange
+ attr_reader :min, :max, :include_min, :include_max
+
+ alias_method :include_min?, :include_min
+ alias_method :include_max?, :include_max
+
+ class Empty < VersionRange
+ undef_method :min, :max
+ undef_method :include_min, :include_min?
+ undef_method :include_max, :include_max?
+
+ def initialize
+ end
+
+ def empty?
+ true
+ end
+
+ def eql?(other)
+ other.empty?
+ end
+
+ def hash
+ [].hash
+ end
+
+ def intersects?(_)
+ false
+ end
+
+ def intersect(other)
+ self
+ end
+
+ def allows_all?(other)
+ other.empty?
+ end
+
+ def include?(_)
+ false
+ end
+
+ def any?
+ false
+ end
+
+ def to_s
+ "(no versions)"
+ end
+
+ def ==(other)
+ other.class == self.class
+ end
+
+ def invert
+ VersionRange.any
+ end
+
+ def select_versions(_)
+ []
+ end
+ end
+
+ EMPTY = Empty.new
+ Empty.singleton_class.undef_method(:new)
+
+ def self.empty
+ EMPTY
+ end
+
+ def self.any
+ new
+ end
+
+ def initialize(min: nil, max: nil, include_min: false, include_max: false, name: nil)
+ raise ArgumentError, "Ranges without a lower bound cannot have include_min == true" if !min && include_min == true
+ raise ArgumentError, "Ranges without an upper bound cannot have include_max == true" if !max && include_max == true
+
+ @min = min
+ @max = max
+ @include_min = include_min
+ @include_max = include_max
+ @name = name
+ end
+
+ def hash
+ @hash ||= min.hash ^ max.hash ^ include_min.hash ^ include_max.hash
+ end
+
+ def eql?(other)
+ if other.is_a?(VersionRange)
+ !other.empty? &&
+ min.eql?(other.min) &&
+ max.eql?(other.max) &&
+ include_min.eql?(other.include_min) &&
+ include_max.eql?(other.include_max)
+ else
+ ranges.eql?(other.ranges)
+ end
+ end
+
+ def ranges
+ [self]
+ end
+
+ def include?(version)
+ compare_version(version) == 0
+ end
+
+ # Partitions passed versions into [lower, within, higher]
+ #
+ # versions must be sorted
+ def partition_versions(versions)
+ min_index =
+ if !min || versions.empty?
+ 0
+ elsif include_min?
+ (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= min }
+ else
+ (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > min }
+ end
+
+ lower = versions.slice(0, min_index)
+ versions = versions.slice(min_index, versions.size)
+
+ max_index =
+ if !max || versions.empty?
+ versions.size
+ elsif include_max?
+ (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > max }
+ else
+ (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= max }
+ end
+
+ [
+ lower,
+ versions.slice(0, max_index),
+ versions.slice(max_index, versions.size)
+ ]
+ end
+
+ # Returns versions which are included by this range.
+ #
+ # versions must be sorted
+ def select_versions(versions)
+ return versions if any?
+
+ partition_versions(versions)[1]
+ end
+
+ def compare_version(version)
+ if min
+ case version <=> min
+ when -1
+ return -1
+ when 0
+ return -1 if !include_min
+ when 1
+ end
+ end
+
+ if max
+ case version <=> max
+ when -1
+ when 0
+ return 1 if !include_max
+ when 1
+ return 1
+ end
+ end
+
+ 0
+ end
+
+ def strictly_lower?(other)
+ return false if !max || !other.min
+
+ case max <=> other.min
+ when 0
+ !include_max || !other.include_min
+ when -1
+ true
+ when 1
+ false
+ end
+ end
+
+ def strictly_higher?(other)
+ other.strictly_lower?(self)
+ end
+
+ def intersects?(other)
+ return false if other.empty?
+ return other.intersects?(self) if other.is_a?(VersionUnion)
+ !strictly_lower?(other) && !strictly_higher?(other)
+ end
+ alias_method :allows_any?, :intersects?
+
+ def intersect(other)
+ return other if other.empty?
+ return other.intersect(self) if other.is_a?(VersionUnion)
+
+ min_range =
+ if !min
+ other
+ elsif !other.min
+ self
+ else
+ case min <=> other.min
+ when 0
+ include_min ? other : self
+ when -1
+ other
+ when 1
+ self
+ end
+ end
+
+ max_range =
+ if !max
+ other
+ elsif !other.max
+ self
+ else
+ case max <=> other.max
+ when 0
+ include_max ? other : self
+ when -1
+ self
+ when 1
+ other
+ end
+ end
+
+ if !min_range.equal?(max_range) && min_range.min && max_range.max
+ case min_range.min <=> max_range.max
+ when -1
+ when 0
+ if !min_range.include_min || !max_range.include_max
+ return EMPTY
+ end
+ when 1
+ return EMPTY
+ end
+ end
+
+ VersionRange.new(
+ min: min_range.min,
+ include_min: min_range.include_min,
+ max: max_range.max,
+ include_max: max_range.include_max
+ )
+ end
+
+ # The span covered by two ranges
+ #
+ # If self and other are contiguous, this builds a union of the two ranges.
+ # (if they aren't you are probably calling the wrong method)
+ def span(other)
+ return self if other.empty?
+
+ min_range =
+ if !min
+ self
+ elsif !other.min
+ other
+ else
+ case min <=> other.min
+ when 0
+ include_min ? self : other
+ when -1
+ self
+ when 1
+ other
+ end
+ end
+
+ max_range =
+ if !max
+ self
+ elsif !other.max
+ other
+ else
+ case max <=> other.max
+ when 0
+ include_max ? self : other
+ when -1
+ other
+ when 1
+ self
+ end
+ end
+
+ VersionRange.new(
+ min: min_range.min,
+ include_min: min_range.include_min,
+ max: max_range.max,
+ include_max: max_range.include_max
+ )
+ end
+
+ def union(other)
+ return other.union(self) if other.is_a?(VersionUnion)
+
+ if contiguous_to?(other)
+ span(other)
+ else
+ VersionUnion.union([self, other])
+ end
+ end
+
+ def contiguous_to?(other)
+ return false if other.empty?
+ return true if any?
+
+ intersects?(other) || contiguous_below?(other) || contiguous_above?(other)
+ end
+
+ def contiguous_below?(other)
+ return false if !max || !other.min
+
+ max == other.min && (include_max || other.include_min)
+ end
+
+ def contiguous_above?(other)
+ other.contiguous_below?(self)
+ end
+
+ def allows_all?(other)
+ return true if other.empty?
+
+ if other.is_a?(VersionUnion)
+ return VersionUnion.new([self]).allows_all?(other)
+ end
+
+ return false if max && !other.max
+ return false if min && !other.min
+
+ if min
+ case min <=> other.min
+ when -1
+ when 0
+ return false if !include_min && other.include_min
+ when 1
+ return false
+ end
+ end
+
+ if max
+ case max <=> other.max
+ when -1
+ return false
+ when 0
+ return false if !include_max && other.include_max
+ when 1
+ end
+ end
+
+ true
+ end
+
+ def any?
+ !min && !max
+ end
+
+ def empty?
+ false
+ end
+
+ def to_s
+ @name ||= constraints.join(", ")
+ end
+
+ def inspect
+ "#<#{self.class} #{to_s}>"
+ end
+
+ def upper_invert
+ return self.class.empty unless max
+
+ VersionRange.new(min: max, include_min: !include_max)
+ end
+
+ def invert
+ return self.class.empty if any?
+
+ low = -> { VersionRange.new(max: min, include_max: !include_min) }
+ high = -> { VersionRange.new(min: max, include_min: !include_max) }
+
+ if !min
+ high.call
+ elsif !max
+ low.call
+ else
+ low.call.union(high.call)
+ end
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ min == other.min &&
+ max == other.max &&
+ include_min == other.include_min &&
+ include_max == other.include_max
+ end
+
+ private
+
+ def constraints
+ return ["any"] if any?
+ return ["= #{min}"] if min.to_s == max.to_s
+
+ c = []
+ c << "#{include_min ? ">=" : ">"} #{min}" if min
+ c << "#{include_max ? "<=" : "<"} #{max}" if max
+ c
+ end
+
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb
new file mode 100644
index 0000000000..000923e99a
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb
@@ -0,0 +1,236 @@
+require_relative 'partial_solution'
+require_relative 'term'
+require_relative 'incompatibility'
+require_relative 'solve_failure'
+require_relative 'strategy'
+
+module Bundler::PubGrub
+ class VersionSolver
+ attr_reader :logger
+ attr_reader :source
+ attr_reader :solution
+ attr_reader :strategy
+
+ def initialize(source:, root: Package.root, strategy: Strategy.new(source), logger: Bundler::PubGrub.logger)
+ @logger = logger
+
+ @source = source
+ @strategy = strategy
+
+ # { package => [incompatibility, ...]}
+ @incompatibilities = Hash.new do |h, k|
+ h[k] = []
+ end
+
+ @seen_incompatibilities = {}
+
+ @solution = PartialSolution.new
+
+ add_incompatibility Incompatibility.new([
+ Term.new(VersionConstraint.any(root), false)
+ ], cause: :root)
+
+ propagate(root)
+ end
+
+ def solved?
+ solution.unsatisfied.empty?
+ end
+
+ # Returns true if there is more work to be done, false otherwise
+ def work
+ unsatisfied_terms = solution.unsatisfied
+ if unsatisfied_terms.empty?
+ logger.info { "Solution found after #{solution.attempted_solutions} attempts:" }
+ solution.decisions.each do |package, version|
+ next if Package.root?(package)
+ logger.info { "* #{package} #{version}" }
+ end
+
+ return false
+ end
+
+ next_package = choose_package_version_from(unsatisfied_terms)
+ propagate(next_package)
+
+ true
+ end
+
+ def solve
+ while work; end
+
+ solution.decisions
+ end
+
+ alias_method :result, :solve
+
+ private
+
+ def propagate(initial_package)
+ changed = [initial_package]
+ while package = changed.shift
+ @incompatibilities[package].reverse_each do |incompatibility|
+ result = propagate_incompatibility(incompatibility)
+ if result == :conflict
+ root_cause = resolve_conflict(incompatibility)
+ changed.clear
+ changed << propagate_incompatibility(root_cause)
+ elsif result # should be a Package
+ changed << result
+ end
+ end
+ changed.uniq!
+ end
+ end
+
+ def propagate_incompatibility(incompatibility)
+ unsatisfied = nil
+ incompatibility.terms.each do |term|
+ relation = solution.relation(term)
+ if relation == :disjoint
+ return nil
+ elsif relation == :overlap
+ # If more than one term is inconclusive, we can't deduce anything
+ return nil if unsatisfied
+ unsatisfied = term
+ end
+ end
+
+ if !unsatisfied
+ return :conflict
+ end
+
+ logger.debug { "derived: #{unsatisfied.invert}" }
+
+ solution.derive(unsatisfied.invert, incompatibility)
+
+ unsatisfied.package
+ end
+
+ def choose_package_version_from(unsatisfied_terms)
+ remaining = unsatisfied_terms.map { |t| [t.package, t.constraint.range] }.to_h
+
+ package, version = strategy.next_package_and_version(remaining)
+
+ logger.debug { "attempting #{package} #{version}" }
+
+ if version.nil?
+ unsatisfied_term = unsatisfied_terms.find { |t| t.package == package }
+ add_incompatibility source.no_versions_incompatibility_for(package, unsatisfied_term)
+ return package
+ end
+
+ conflict = false
+
+ source.incompatibilities_for(package, version).each do |incompatibility|
+ if @seen_incompatibilities.include?(incompatibility)
+ logger.debug { "knew: #{incompatibility}" }
+ next
+ end
+ @seen_incompatibilities[incompatibility] = true
+
+ add_incompatibility incompatibility
+
+ conflict ||= incompatibility.terms.all? do |term|
+ term.package == package || solution.satisfies?(term)
+ end
+ end
+
+ unless conflict
+ logger.info { "selected #{package} #{version}" }
+
+ solution.decide(package, version)
+ else
+ logger.info { "conflict: #{conflict.inspect}" }
+ end
+
+ package
+ end
+
+ def resolve_conflict(incompatibility)
+ logger.info { "conflict: #{incompatibility}" }
+
+ new_incompatibility = nil
+
+ while !incompatibility.failure?
+ most_recent_term = nil
+ most_recent_satisfier = nil
+ difference = nil
+
+ previous_level = 1
+
+ incompatibility.terms.each do |term|
+ satisfier = solution.satisfier(term)
+
+ if most_recent_satisfier.nil?
+ most_recent_term = term
+ most_recent_satisfier = satisfier
+ elsif most_recent_satisfier.index < satisfier.index
+ previous_level = [previous_level, most_recent_satisfier.decision_level].max
+ most_recent_term = term
+ most_recent_satisfier = satisfier
+ difference = nil
+ else
+ previous_level = [previous_level, satisfier.decision_level].max
+ end
+
+ if most_recent_term == term
+ difference = most_recent_satisfier.term.difference(most_recent_term)
+ if difference.empty?
+ difference = nil
+ else
+ difference_satisfier = solution.satisfier(difference.inverse)
+ previous_level = [previous_level, difference_satisfier.decision_level].max
+ end
+ end
+ end
+
+ if previous_level < most_recent_satisfier.decision_level ||
+ most_recent_satisfier.decision?
+
+ logger.info { "backtracking to #{previous_level}" }
+ solution.backtrack(previous_level)
+
+ if new_incompatibility
+ add_incompatibility(new_incompatibility)
+ end
+
+ return incompatibility
+ end
+
+ new_terms = []
+ new_terms += incompatibility.terms - [most_recent_term]
+ new_terms += most_recent_satisfier.cause.terms.reject { |term|
+ term.package == most_recent_satisfier.term.package
+ }
+ if difference
+ new_terms << difference.invert
+ end
+
+ new_incompatibility = Incompatibility.new(new_terms, cause: Incompatibility::ConflictCause.new(incompatibility, most_recent_satisfier.cause))
+
+ if incompatibility.to_s == new_incompatibility.to_s
+ logger.info { "!! failed to resolve conflicts, this shouldn't have happened" }
+ break
+ end
+
+ incompatibility = new_incompatibility
+
+ partially = difference ? " partially" : ""
+ logger.info { "! #{most_recent_term} is#{partially} satisfied by #{most_recent_satisfier.term}" }
+ logger.info { "! which is caused by #{most_recent_satisfier.cause}" }
+ logger.info { "! thus #{incompatibility}" }
+ end
+
+ raise SolveFailure.new(incompatibility)
+ end
+
+ def add_incompatibility(incompatibility)
+ logger.debug { "fact: #{incompatibility}" }
+ incompatibility.terms.each do |term|
+ package = term.package
+ @incompatibilities[package] << incompatibility
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/version_union.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_union.rb
new file mode 100644
index 0000000000..bbc10c3804
--- /dev/null
+++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_union.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+module Bundler::PubGrub
+ class VersionUnion
+ attr_reader :ranges
+
+ def self.normalize_ranges(ranges)
+ ranges = ranges.flat_map do |range|
+ range.ranges
+ end
+
+ ranges.reject!(&:empty?)
+
+ return [] if ranges.empty?
+
+ mins, ranges = ranges.partition { |r| !r.min }
+ original_ranges = mins + ranges.sort_by { |r| [r.min, r.include_min ? 0 : 1] }
+ ranges = [original_ranges.shift]
+ original_ranges.each do |range|
+ if ranges.last.contiguous_to?(range)
+ ranges << ranges.pop.span(range)
+ else
+ ranges << range
+ end
+ end
+
+ ranges
+ end
+
+ def self.union(ranges, normalize: true)
+ ranges = normalize_ranges(ranges) if normalize
+
+ if ranges.size == 0
+ VersionRange.empty
+ elsif ranges.size == 1
+ ranges[0]
+ else
+ new(ranges)
+ end
+ end
+
+ def initialize(ranges)
+ raise ArgumentError unless ranges.all? { |r| r.instance_of?(VersionRange) }
+ @ranges = ranges
+ end
+
+ def hash
+ ranges.hash
+ end
+
+ def eql?(other)
+ ranges.eql?(other.ranges)
+ end
+
+ def include?(version)
+ !!ranges.bsearch {|r| r.compare_version(version) }
+ end
+
+ def select_versions(all_versions)
+ versions = []
+ ranges.inject(all_versions) do |acc, range|
+ _, matching, higher = range.partition_versions(acc)
+ versions.concat matching
+ higher
+ end
+ versions
+ end
+
+ def intersects?(other)
+ my_ranges = ranges.dup
+ other_ranges = other.ranges.dup
+
+ my_range = my_ranges.shift
+ other_range = other_ranges.shift
+ while my_range && other_range
+ if my_range.intersects?(other_range)
+ return true
+ end
+
+ if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max)
+ other_range = other_ranges.shift
+ else
+ my_range = my_ranges.shift
+ end
+ end
+ end
+ alias_method :allows_any?, :intersects?
+
+ def allows_all?(other)
+ my_ranges = ranges.dup
+
+ my_range = my_ranges.shift
+
+ other.ranges.all? do |other_range|
+ while my_range
+ break if my_range.allows_all?(other_range)
+ my_range = my_ranges.shift
+ end
+
+ !!my_range
+ end
+ end
+
+ def empty?
+ false
+ end
+
+ def any?
+ false
+ end
+
+ def intersect(other)
+ my_ranges = ranges.dup
+ other_ranges = other.ranges.dup
+ new_ranges = []
+
+ my_range = my_ranges.shift
+ other_range = other_ranges.shift
+ while my_range && other_range
+ new_ranges << my_range.intersect(other_range)
+
+ if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max)
+ other_range = other_ranges.shift
+ else
+ my_range = my_ranges.shift
+ end
+ end
+ new_ranges.reject!(&:empty?)
+ VersionUnion.union(new_ranges, normalize: false)
+ end
+
+ def upper_invert
+ ranges.last.upper_invert
+ end
+
+ def invert
+ ranges.map(&:invert).inject(:intersect)
+ end
+
+ def union(other)
+ VersionUnion.union([self, other])
+ end
+
+ def to_s
+ output = []
+
+ ranges = self.ranges.dup
+ while !ranges.empty?
+ ne = []
+ range = ranges.shift
+ while !ranges.empty? && ranges[0].min.to_s == range.max.to_s
+ ne << range.max
+ range = range.span(ranges.shift)
+ end
+
+ ne.map! {|x| "!= #{x}" }
+ if ne.empty?
+ output << range.to_s
+ elsif range.any?
+ output << ne.join(', ')
+ else
+ output << "#{range}, #{ne.join(', ')}"
+ end
+ end
+
+ output.join(" OR ")
+ end
+
+ def inspect
+ "#<#{self.class} #{to_s}>"
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ self.ranges == other.ranges
+ end
+ end
+end
diff --git a/lib/bundler/vendor/securerandom/lib/securerandom.rb b/lib/bundler/vendor/securerandom/lib/securerandom.rb
new file mode 100644
index 0000000000..01b7fa15a6
--- /dev/null
+++ b/lib/bundler/vendor/securerandom/lib/securerandom.rb
@@ -0,0 +1,102 @@
+# -*- coding: us-ascii -*-
+# frozen_string_literal: true
+
+require 'random/formatter'
+
+# == Secure random number generator interface.
+#
+# This library is an interface to secure random number generators which are
+# suitable for generating session keys in HTTP cookies, etc.
+#
+# You can use this library in your application by requiring it:
+#
+# require 'bundler/vendor/securerandom/lib/securerandom'
+#
+# It supports the following secure random number generators:
+#
+# * openssl
+# * /dev/urandom
+# * Win32
+#
+# Bundler::SecureRandom is extended by the Random::Formatter module which
+# defines the following methods:
+#
+# * alphanumeric
+# * base64
+# * choose
+# * gen_random
+# * hex
+# * rand
+# * random_bytes
+# * random_number
+# * urlsafe_base64
+# * uuid
+#
+# These methods are usable as class methods of Bundler::SecureRandom such as
+# +Bundler::SecureRandom.hex+.
+#
+# If a secure random number generator is not available,
+# +NotImplementedError+ is raised.
+
+module Bundler::SecureRandom
+
+ # The version
+ VERSION = "0.4.1"
+
+ class << self
+ # Returns a random binary string containing +size+ bytes.
+ #
+ # See Random.bytes
+ def bytes(n)
+ return gen_random(n)
+ end
+
+ # Compatibility methods for Ruby 3.2, we can remove this after dropping to support Ruby 3.2
+ def alphanumeric(n = nil, chars: ALPHANUMERIC)
+ n = 16 if n.nil?
+ choose(chars, n)
+ end if RUBY_VERSION < '3.3'
+
+ private
+
+ # :stopdoc:
+
+ # Implementation using OpenSSL
+ def gen_random_openssl(n)
+ return OpenSSL::Random.random_bytes(n)
+ end
+
+ # Implementation using system random device
+ def gen_random_urandom(n)
+ ret = Random.urandom(n)
+ unless ret
+ raise NotImplementedError, "No random device"
+ end
+ unless ret.length == n
+ raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes"
+ end
+ ret
+ end
+
+ begin
+ # Check if Random.urandom is available
+ Random.urandom(1)
+ alias gen_random gen_random_urandom
+ rescue RuntimeError
+ begin
+ require 'openssl'
+ rescue NoMethodError
+ raise NotImplementedError, "No random device"
+ else
+ alias gen_random gen_random_openssl
+ end
+ end
+
+ # :startdoc:
+
+ # Generate random data bytes for Random::Formatter
+ public :gen_random
+ end
+end
+
+Bundler::SecureRandom.extend(Random::Formatter)
diff --git a/lib/bundler/vendor/thor/lib/thor.rb b/lib/bundler/vendor/thor/lib/thor.rb
new file mode 100644
index 0000000000..945bdbd551
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor.rb
@@ -0,0 +1,674 @@
+require_relative "thor/base"
+
+class Bundler::Thor
+ $thor_runner ||= false
+ class << self
+ # Allows for custom "Command" package naming.
+ #
+ # === Parameters
+ # name<String>
+ # options<Hash>
+ #
+ def package_name(name, _ = {})
+ @package_name = name.nil? || name == "" ? nil : name
+ end
+
+ # Sets the default command when thor is executed without an explicit command to be called.
+ #
+ # ==== Parameters
+ # meth<Symbol>:: name of the default command
+ #
+ def default_command(meth = nil)
+ if meth
+ @default_command = meth == :none ? "help" : meth.to_s
+ else
+ @default_command ||= from_superclass(:default_command, "help")
+ end
+ end
+ alias_method :default_task, :default_command
+
+ # Registers another Bundler::Thor subclass as a command.
+ #
+ # ==== Parameters
+ # klass<Class>:: Bundler::Thor subclass to register
+ # command<String>:: Subcommand name to use
+ # usage<String>:: Short usage for the subcommand
+ # description<String>:: Description for the subcommand
+ def register(klass, subcommand_name, usage, description, options = {})
+ if klass <= Bundler::Thor::Group
+ desc usage, description, options
+ define_method(subcommand_name) { |*args| invoke(klass, args) }
+ else
+ desc usage, description, options
+ subcommand subcommand_name, klass
+ end
+ end
+
+ # Defines the usage and the description of the next command.
+ #
+ # ==== Parameters
+ # usage<String>
+ # description<String>
+ # options<String>
+ #
+ def desc(usage, description, options = {})
+ if options[:for]
+ command = find_and_refresh_command(options[:for])
+ command.usage = usage if usage
+ command.description = description if description
+ else
+ @usage = usage
+ @desc = description
+ @hide = options[:hide] || false
+ end
+ end
+
+ # Defines the long description of the next command.
+ #
+ # Long description is by default indented, line-wrapped and repeated whitespace merged.
+ # In order to print long description verbatim, with indentation and spacing exactly
+ # as found in the code, use the +wrap+ option
+ #
+ # long_desc 'your very long description', wrap: false
+ #
+ # ==== Parameters
+ # long description<String>
+ # options<Hash>
+ #
+ def long_desc(long_description, options = {})
+ if options[:for]
+ command = find_and_refresh_command(options[:for])
+ command.long_description = long_description if long_description
+ else
+ @long_desc = long_description
+ @long_desc_wrap = options[:wrap] != false
+ end
+ end
+
+ # Maps an input to a command. If you define:
+ #
+ # map "-T" => "list"
+ #
+ # Running:
+ #
+ # thor -T
+ #
+ # Will invoke the list command.
+ #
+ # ==== Parameters
+ # Hash[String|Array => Symbol]:: Maps the string or the strings in the array to the given command.
+ #
+ def map(mappings = nil, **kw)
+ @map ||= from_superclass(:map, {})
+
+ if mappings && !kw.empty?
+ mappings = kw.merge!(mappings)
+ else
+ mappings ||= kw
+ end
+ if mappings
+ mappings.each do |key, value|
+ if key.respond_to?(:each)
+ key.each { |subkey| @map[subkey] = value }
+ else
+ @map[key] = value
+ end
+ end
+ end
+
+ @map
+ end
+
+ # Declares the options for the next command to be declared.
+ #
+ # ==== Parameters
+ # Hash[Symbol => Object]:: The hash key is the name of the option and the value
+ # is the type of the option. Can be :string, :array, :hash, :boolean, :numeric
+ # or :required (string). If you give a value, the type of the value is used.
+ #
+ def method_options(options = nil)
+ @method_options ||= {}
+ build_options(options, @method_options) if options
+ @method_options
+ end
+
+ alias_method :options, :method_options
+
+ # Adds an option to the set of method options. If :for is given as option,
+ # it allows you to change the options from a previous defined command.
+ #
+ # def previous_command
+ # # magic
+ # end
+ #
+ # method_option :foo, :for => :previous_command
+ #
+ # def next_command
+ # # magic
+ # end
+ #
+ # ==== Parameters
+ # name<Symbol>:: The name of the argument.
+ # options<Hash>:: Described below.
+ #
+ # ==== Options
+ # :desc - Description for the argument.
+ # :required - If the argument is required or not.
+ # :default - Default value for this argument. It cannot be required and have default values.
+ # :aliases - Aliases for this option.
+ # :type - The type of the argument, can be :string, :hash, :array, :numeric or :boolean.
+ # :banner - String to show on usage notes.
+ # :hide - If you want to hide this option from the help.
+ #
+ def method_option(name, options = {})
+ unless [ Symbol, String ].any? { |klass| name.is_a?(klass) }
+ raise ArgumentError, "Expected a Symbol or String, got #{name.inspect}"
+ end
+ scope = if options[:for]
+ find_and_refresh_command(options[:for]).options
+ else
+ method_options
+ end
+
+ build_option(name, options, scope)
+ end
+ alias_method :option, :method_option
+
+ # Adds and declares option group for exclusive options in the
+ # block and arguments. You can declare options as the outside of the block.
+ #
+ # If :for is given as option, it allows you to change the options from
+ # a previous defined command.
+ #
+ # ==== Parameters
+ # Array[Bundler::Thor::Option.name]
+ # options<Hash>:: :for is applied for previous defined command.
+ #
+ # ==== Examples
+ #
+ # exclusive do
+ # option :one
+ # option :two
+ # end
+ #
+ # Or
+ #
+ # option :one
+ # option :two
+ # exclusive :one, :two
+ #
+ # If you give "--one" and "--two" at the same time ExclusiveArgumentsError
+ # will be raised.
+ #
+ def method_exclusive(*args, &block)
+ register_options_relation_for(:method_options,
+ :method_exclusive_option_names, *args, &block)
+ end
+ alias_method :exclusive, :method_exclusive
+
+ # Adds and declares option group for required at least one of options in the
+ # block of arguments. You can declare options as the outside of the block.
+ #
+ # If :for is given as option, it allows you to change the options from
+ # a previous defined command.
+ #
+ # ==== Parameters
+ # Array[Bundler::Thor::Option.name]
+ # options<Hash>:: :for is applied for previous defined command.
+ #
+ # ==== Examples
+ #
+ # at_least_one do
+ # option :one
+ # option :two
+ # end
+ #
+ # Or
+ #
+ # option :one
+ # option :two
+ # at_least_one :one, :two
+ #
+ # If you do not give "--one" and "--two" AtLeastOneRequiredArgumentError
+ # will be raised.
+ #
+ # You can use at_least_one and exclusive at the same time.
+ #
+ # exclusive do
+ # at_least_one do
+ # option :one
+ # option :two
+ # end
+ # end
+ #
+ # Then it is required either only one of "--one" or "--two".
+ #
+ def method_at_least_one(*args, &block)
+ register_options_relation_for(:method_options,
+ :method_at_least_one_option_names, *args, &block)
+ end
+ alias_method :at_least_one, :method_at_least_one
+
+ # Prints help information for the given command.
+ #
+ # ==== Parameters
+ # shell<Bundler::Thor::Shell>
+ # command_name<String>
+ #
+ def command_help(shell, command_name)
+ meth = normalize_command_name(command_name)
+ command = all_commands[meth]
+ handle_no_command_error(meth) unless command
+
+ shell.say "Usage:"
+ shell.say " #{banner(command).split("\n").join("\n ")}"
+ shell.say
+ class_options_help(shell, nil => command.options.values)
+ print_exclusive_options(shell, command)
+ print_at_least_one_required_options(shell, command)
+
+ if command.long_description
+ shell.say "Description:"
+ if command.wrap_long_description
+ shell.print_wrapped(command.long_description, indent: 2)
+ else
+ shell.say command.long_description
+ end
+ else
+ shell.say command.description
+ end
+ end
+ alias_method :task_help, :command_help
+
+ # Prints help information for this class.
+ #
+ # ==== Parameters
+ # shell<Bundler::Thor::Shell>
+ #
+ def help(shell, subcommand = false)
+ list = printable_commands(true, subcommand)
+ Bundler::Thor::Util.thor_classes_in(self).each do |klass|
+ list += klass.printable_commands(false)
+ end
+ sort_commands!(list)
+
+ if defined?(@package_name) && @package_name
+ shell.say "#{@package_name} commands:"
+ else
+ shell.say "Commands:"
+ end
+
+ shell.print_table(list, indent: 2, truncate: true)
+ shell.say
+ class_options_help(shell)
+ print_exclusive_options(shell)
+ print_at_least_one_required_options(shell)
+ end
+
+ # Returns commands ready to be printed.
+ def printable_commands(all = true, subcommand = false)
+ (all ? all_commands : commands).map do |_, command|
+ next if command.hidden?
+ item = []
+ item << banner(command, false, subcommand)
+ item << (command.description ? "# #{command.description.gsub(/\s+/m, ' ')}" : "")
+ item
+ end.compact
+ end
+ alias_method :printable_tasks, :printable_commands
+
+ def subcommands
+ @subcommands ||= from_superclass(:subcommands, [])
+ end
+ alias_method :subtasks, :subcommands
+
+ def subcommand_classes
+ @subcommand_classes ||= {}
+ end
+
+ def subcommand(subcommand, subcommand_class)
+ subcommands << subcommand.to_s
+ subcommand_class.subcommand_help subcommand
+ subcommand_classes[subcommand.to_s] = subcommand_class
+
+ define_method(subcommand) do |*args|
+ args, opts = Bundler::Thor::Arguments.split(args)
+ invoke_args = [args, opts, {invoked_via_subcommand: true, class_options: options}]
+ invoke_args.unshift "help" if opts.delete("--help") || opts.delete("-h")
+ invoke subcommand_class, *invoke_args
+ end
+ subcommand_class.commands.each do |_meth, command|
+ command.ancestor_name = subcommand
+ end
+ end
+ alias_method :subtask, :subcommand
+
+ # Extend check unknown options to accept a hash of conditions.
+ #
+ # === Parameters
+ # options<Hash>: A hash containing :only and/or :except keys
+ def check_unknown_options!(options = {})
+ @check_unknown_options ||= {}
+ options.each do |key, value|
+ if value
+ @check_unknown_options[key] = Array(value)
+ else
+ @check_unknown_options.delete(key)
+ end
+ end
+ @check_unknown_options
+ end
+
+ # Overwrite check_unknown_options? to take subcommands and options into account.
+ def check_unknown_options?(config) #:nodoc:
+ options = check_unknown_options
+ return false unless options
+
+ command = config[:current_command]
+ return true unless command
+
+ name = command.name
+
+ if subcommands.include?(name)
+ false
+ elsif options[:except]
+ !options[:except].include?(name.to_sym)
+ elsif options[:only]
+ options[:only].include?(name.to_sym)
+ else
+ true
+ end
+ end
+
+ # Stop parsing of options as soon as an unknown option or a regular
+ # argument is encountered. All remaining arguments are passed to the command.
+ # This is useful if you have a command that can receive arbitrary additional
+ # options, and where those additional options should not be handled by
+ # Bundler::Thor.
+ #
+ # ==== Example
+ #
+ # To better understand how this is useful, let's consider a command that calls
+ # an external command. A user may want to pass arbitrary options and
+ # arguments to that command. The command itself also accepts some options,
+ # which should be handled by Bundler::Thor.
+ #
+ # class_option "verbose", :type => :boolean
+ # stop_on_unknown_option! :exec
+ # check_unknown_options! :except => :exec
+ #
+ # desc "exec", "Run a shell command"
+ # def exec(*args)
+ # puts "diagnostic output" if options[:verbose]
+ # Kernel.exec(*args)
+ # end
+ #
+ # Here +exec+ can be called with +--verbose+ to get diagnostic output,
+ # e.g.:
+ #
+ # $ thor exec --verbose echo foo
+ # diagnostic output
+ # foo
+ #
+ # But if +--verbose+ is given after +echo+, it is passed to +echo+ instead:
+ #
+ # $ thor exec echo --verbose foo
+ # --verbose foo
+ #
+ # ==== Parameters
+ # Symbol ...:: A list of commands that should be affected.
+ def stop_on_unknown_option!(*command_names)
+ @stop_on_unknown_option = stop_on_unknown_option | command_names
+ end
+
+ def stop_on_unknown_option?(command) #:nodoc:
+ command && stop_on_unknown_option.include?(command.name.to_sym)
+ end
+
+ # Disable the check for required options for the given commands.
+ # This is useful if you have a command that does not need the required options
+ # to work, like help.
+ #
+ # ==== Parameters
+ # Symbol ...:: A list of commands that should be affected.
+ def disable_required_check!(*command_names)
+ @disable_required_check = disable_required_check | command_names
+ end
+
+ def disable_required_check?(command) #:nodoc:
+ command && disable_required_check.include?(command.name.to_sym)
+ end
+
+ # Checks if a specified command exists.
+ #
+ # ==== Parameters
+ # command_name<String>:: The name of the command to check for existence.
+ #
+ # ==== Returns
+ # Boolean:: +true+ if the command exists, +false+ otherwise.
+ def command_exists?(command_name) #:nodoc:
+ commands.keys.include?(normalize_command_name(command_name))
+ end
+
+ protected
+
+ # Returns this class exclusive options array set.
+ #
+ # ==== Returns
+ # Array[Array[Bundler::Thor::Option.name]]
+ #
+ def method_exclusive_option_names #:nodoc:
+ @method_exclusive_option_names ||= []
+ end
+
+ # Returns this class at least one of required options array set.
+ #
+ # ==== Returns
+ # Array[Array[Bundler::Thor::Option.name]]
+ #
+ def method_at_least_one_option_names #:nodoc:
+ @method_at_least_one_option_names ||= []
+ end
+
+ def stop_on_unknown_option #:nodoc:
+ @stop_on_unknown_option ||= []
+ end
+
+ # help command has the required check disabled by default.
+ def disable_required_check #:nodoc:
+ @disable_required_check ||= [:help]
+ end
+
+ def print_exclusive_options(shell, command = nil) # :nodoc:
+ opts = []
+ opts = command.method_exclusive_option_names unless command.nil?
+ opts += class_exclusive_option_names
+ unless opts.empty?
+ shell.say "Exclusive Options:"
+ shell.print_table(opts.map{ |ex| ex.map{ |e| "--#{e}"}}, indent: 2 )
+ shell.say
+ end
+ end
+
+ def print_at_least_one_required_options(shell, command = nil) # :nodoc:
+ opts = []
+ opts = command.method_at_least_one_option_names unless command.nil?
+ opts += class_at_least_one_option_names
+ unless opts.empty?
+ shell.say "Required At Least One:"
+ shell.print_table(opts.map{ |ex| ex.map{ |e| "--#{e}"}}, indent: 2 )
+ shell.say
+ end
+ end
+
+ # The method responsible for dispatching given the args.
+ def dispatch(meth, given_args, given_opts, config) #:nodoc:
+ meth ||= retrieve_command_name(given_args)
+ command = all_commands[normalize_command_name(meth)]
+
+ if !command && config[:invoked_via_subcommand]
+ # We're a subcommand and our first argument didn't match any of our
+ # commands. So we put it back and call our default command.
+ given_args.unshift(meth)
+ command = all_commands[normalize_command_name(default_command)]
+ end
+
+ if command
+ args, opts = Bundler::Thor::Options.split(given_args)
+ if stop_on_unknown_option?(command) && !args.empty?
+ # given_args starts with a non-option, so we treat everything as
+ # ordinary arguments
+ args.concat opts
+ opts.clear
+ end
+ else
+ args = given_args
+ opts = nil
+ command = dynamic_command_class.new(meth)
+ end
+
+ opts = given_opts || opts || []
+ config[:current_command] = command
+ config[:command_options] = command.options
+
+ instance = new(args, opts, config)
+ yield instance if block_given?
+ args = instance.args
+ trailing = args[Range.new(arguments.size, -1)]
+ instance.invoke_command(command, trailing || [])
+ end
+
+ # The banner for this class. You can customize it if you are invoking the
+ # thor class by another ways which is not the Bundler::Thor::Runner. It receives
+ # the command that is going to be invoked and a boolean which indicates if
+ # the namespace should be displayed as arguments.
+ #
+ def banner(command, namespace = nil, subcommand = false)
+ command.formatted_usage(self, $thor_runner, subcommand).split("\n").map do |formatted_usage|
+ "#{basename} #{formatted_usage}"
+ end.join("\n")
+ end
+
+ def baseclass #:nodoc:
+ Bundler::Thor
+ end
+
+ def dynamic_command_class #:nodoc:
+ Bundler::Thor::DynamicCommand
+ end
+
+ def create_command(meth) #:nodoc:
+ @usage ||= nil
+ @desc ||= nil
+ @long_desc ||= nil
+ @long_desc_wrap ||= nil
+ @hide ||= nil
+
+ if @usage && @desc
+ base_class = @hide ? Bundler::Thor::HiddenCommand : Bundler::Thor::Command
+ relations = {exclusive_option_names: method_exclusive_option_names,
+ at_least_one_option_names: method_at_least_one_option_names}
+ commands[meth] = base_class.new(meth, @desc, @long_desc, @long_desc_wrap, @usage, method_options, relations)
+ @usage, @desc, @long_desc, @long_desc_wrap, @method_options, @hide = nil
+ @method_exclusive_option_names, @method_at_least_one_option_names = nil
+ true
+ elsif all_commands[meth] || meth == "method_missing"
+ true
+ else
+ puts "[WARNING] Attempted to create command #{meth.inspect} without usage or description. " \
+ "Call desc if you want this method to be available as command or declare it inside a " \
+ "no_commands{} block. Invoked from #{caller[1].inspect}."
+ false
+ end
+ end
+ alias_method :create_task, :create_command
+
+ def initialize_added #:nodoc:
+ class_options.merge!(method_options)
+ @method_options = nil
+ end
+
+ # Retrieve the command name from given args.
+ def retrieve_command_name(args) #:nodoc:
+ meth = args.first.to_s unless args.empty?
+ args.shift if meth && (map[meth] || meth !~ /^\-/)
+ end
+ alias_method :retrieve_task_name, :retrieve_command_name
+
+ # receives a (possibly nil) command name and returns a name that is in
+ # the commands hash. In addition to normalizing aliases, this logic
+ # will determine if a shortened command is an unambiguous substring of
+ # a command or alias.
+ #
+ # +normalize_command_name+ also converts names like +animal-prison+
+ # into +animal_prison+.
+ def normalize_command_name(meth) #:nodoc:
+ return default_command.to_s.tr("-", "_") unless meth
+
+ possibilities = find_command_possibilities(meth)
+ raise AmbiguousTaskError, "Ambiguous command #{meth} matches [#{possibilities.join(', ')}]" if possibilities.size > 1
+
+ if possibilities.empty?
+ meth ||= default_command
+ elsif map[meth]
+ meth = map[meth]
+ else
+ meth = possibilities.first
+ end
+
+ meth.to_s.tr("-", "_") # treat foo-bar as foo_bar
+ end
+ alias_method :normalize_task_name, :normalize_command_name
+
+ # this is the logic that takes the command name passed in by the user
+ # and determines whether it is an unambiguous substrings of a command or
+ # alias name.
+ def find_command_possibilities(meth)
+ len = meth.to_s.length
+ possibilities = all_commands.reject { |_k, c| c.hidden? }.merge(map).keys.select { |n| meth == n[0, len] }.sort
+ unique_possibilities = possibilities.map { |k| map[k] || k }.uniq
+
+ if possibilities.include?(meth)
+ [meth]
+ elsif unique_possibilities.size == 1
+ unique_possibilities
+ else
+ possibilities
+ end
+ end
+ alias_method :find_task_possibilities, :find_command_possibilities
+
+ def subcommand_help(cmd)
+ desc "help [COMMAND]", "Describe subcommands or one specific subcommand"
+ class_eval "
+ def help(command = nil, subcommand = true); super; end
+"
+ end
+ alias_method :subtask_help, :subcommand_help
+
+ # Sort the commands, lexicographically by default.
+ #
+ # Can be overridden in the subclass to change the display order of the
+ # commands.
+ def sort_commands!(list)
+ list.sort! { |a, b| a[0] <=> b[0] }
+ end
+ end
+
+ include Bundler::Thor::Base
+
+ map HELP_MAPPINGS => :help
+
+ desc "help [COMMAND]", "Describe available commands or one specific command"
+ def help(command = nil, subcommand = false)
+ if command
+ if self.class.subcommands.include? command
+ self.class.subcommand_classes[command].help(shell, true)
+ else
+ self.class.command_help(shell, command)
+ end
+ else
+ self.class.help(shell, subcommand)
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/actions.rb b/lib/bundler/vendor/thor/lib/thor/actions.rb
new file mode 100644
index 0000000000..ca58182691
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/actions.rb
@@ -0,0 +1,340 @@
+require_relative "actions/create_file"
+require_relative "actions/create_link"
+require_relative "actions/directory"
+require_relative "actions/empty_directory"
+require_relative "actions/file_manipulation"
+require_relative "actions/inject_into_file"
+
+class Bundler::Thor
+ module Actions
+ attr_accessor :behavior
+
+ def self.included(base) #:nodoc:
+ super(base)
+ base.extend ClassMethods
+ end
+
+ module ClassMethods
+ # Hold source paths for one Bundler::Thor instance. source_paths_for_search is the
+ # method responsible to gather source_paths from this current class,
+ # inherited paths and the source root.
+ #
+ def source_paths
+ @_source_paths ||= []
+ end
+
+ # Stores and return the source root for this class
+ def source_root(path = nil)
+ @_source_root = path if path
+ @_source_root ||= nil
+ end
+
+ # Returns the source paths in the following order:
+ #
+ # 1) This class source paths
+ # 2) Source root
+ # 3) Parents source paths
+ #
+ def source_paths_for_search
+ paths = []
+ paths += source_paths
+ paths << source_root if source_root
+ paths += from_superclass(:source_paths, [])
+ paths
+ end
+
+ # Add runtime options that help actions execution.
+ #
+ def add_runtime_options!
+ class_option :force, type: :boolean, aliases: "-f", group: :runtime,
+ desc: "Overwrite files that already exist"
+
+ class_option :pretend, type: :boolean, aliases: "-p", group: :runtime,
+ desc: "Run but do not make any changes"
+
+ class_option :quiet, type: :boolean, aliases: "-q", group: :runtime,
+ desc: "Suppress status output"
+
+ class_option :skip, type: :boolean, aliases: "-s", group: :runtime,
+ desc: "Skip files that already exist"
+ end
+ end
+
+ # Extends initializer to add more configuration options.
+ #
+ # ==== Configuration
+ # behavior<Symbol>:: The actions default behavior. Can be :invoke or :revoke.
+ # It also accepts :force, :skip and :pretend to set the behavior
+ # and the respective option.
+ #
+ # destination_root<String>:: The root directory needed for some actions.
+ #
+ def initialize(args = [], options = {}, config = {})
+ self.behavior = case config[:behavior].to_s
+ when "force", "skip"
+ _cleanup_options_and_set(options, config[:behavior])
+ :invoke
+ when "revoke"
+ :revoke
+ else
+ :invoke
+ end
+
+ super
+ self.destination_root = config[:destination_root]
+ end
+
+ # Wraps an action object and call it accordingly to the thor class behavior.
+ #
+ def action(instance) #:nodoc:
+ if behavior == :revoke
+ instance.revoke!
+ else
+ instance.invoke!
+ end
+ end
+
+ # Returns the root for this thor class (also aliased as destination root).
+ #
+ def destination_root
+ @destination_stack.last
+ end
+
+ # Sets the root for this thor class. Relatives path are added to the
+ # directory where the script was invoked and expanded.
+ #
+ def destination_root=(root)
+ @destination_stack ||= []
+ @destination_stack[0] = File.expand_path(root || "")
+ end
+
+ # Returns the given path relative to the absolute root (ie, root where
+ # the script started).
+ #
+ def relative_to_original_destination_root(path, remove_dot = true)
+ root = @destination_stack[0]
+ if path.start_with?(root) && [File::SEPARATOR, File::ALT_SEPARATOR, nil, ""].include?(path[root.size..root.size])
+ path = path.dup
+ path[0...root.size] = "."
+ remove_dot ? (path[2..-1] || "") : path
+ else
+ path
+ end
+ end
+
+ # Holds source paths in instance so they can be manipulated.
+ #
+ def source_paths
+ @source_paths ||= self.class.source_paths_for_search
+ end
+
+ # Receives a file or directory and search for it in the source paths.
+ #
+ def find_in_source_paths(file)
+ possible_files = [file, file + TEMPLATE_EXTNAME]
+ relative_root = relative_to_original_destination_root(destination_root, false)
+
+ source_paths.each do |source|
+ possible_files.each do |f|
+ source_file = File.expand_path(f, File.join(source, relative_root))
+ return source_file if File.exist?(source_file)
+ end
+ end
+
+ message = "Could not find #{file.inspect} in any of your source paths. ".dup
+
+ unless self.class.source_root
+ message << "Please invoke #{self.class.name}.source_root(PATH) with the PATH containing your templates. "
+ end
+
+ message << if source_paths.empty?
+ "Currently you have no source paths."
+ else
+ "Your current source paths are: \n#{source_paths.join("\n")}"
+ end
+
+ raise Error, message
+ end
+
+ # Do something in the root or on a provided subfolder. If a relative path
+ # is given it's referenced from the current root. The full path is yielded
+ # to the block you provide. The path is set back to the previous path when
+ # the method exits.
+ #
+ # Returns the value yielded by the block.
+ #
+ # ==== Parameters
+ # dir<String>:: the directory to move to.
+ # config<Hash>:: give :verbose => true to log and use padding.
+ #
+ def inside(dir = "", config = {}, &block)
+ verbose = config.fetch(:verbose, false)
+ pretend = options[:pretend]
+
+ say_status :inside, dir, verbose
+ shell.padding += 1 if verbose
+ @destination_stack.push File.expand_path(dir, destination_root)
+
+ # If the directory doesn't exist and we're not pretending
+ if !File.exist?(destination_root) && !pretend
+ require "fileutils"
+ FileUtils.mkdir_p(destination_root)
+ end
+
+ result = nil
+ if pretend
+ # In pretend mode, just yield down to the block
+ result = block.arity == 1 ? yield(destination_root) : yield
+ else
+ require "fileutils"
+ FileUtils.cd(destination_root) { result = block.arity == 1 ? yield(destination_root) : yield }
+ end
+
+ @destination_stack.pop
+ shell.padding -= 1 if verbose
+ result
+ end
+
+ # Goes to the root and execute the given block.
+ #
+ def in_root
+ inside(@destination_stack.first) { yield }
+ end
+
+ # Loads an external file and execute it in the instance binding.
+ #
+ # ==== Parameters
+ # path<String>:: The path to the file to execute. Can be a web address or
+ # a relative path from the source root.
+ #
+ # ==== Examples
+ #
+ # apply "http://gist.github.com/103208"
+ #
+ # apply "recipes/jquery.rb"
+ #
+ def apply(path, config = {})
+ verbose = config.fetch(:verbose, true)
+ is_uri = path =~ %r{^https?\://}
+ path = find_in_source_paths(path) unless is_uri
+
+ say_status :apply, path, verbose
+ shell.padding += 1 if verbose
+
+ contents = if is_uri
+ require "open-uri"
+ URI.open(path, "Accept" => "application/x-thor-template", &:read)
+ else
+ File.open(path, &:read)
+ end
+
+ instance_eval(contents, path)
+ shell.padding -= 1 if verbose
+ end
+
+ # Executes a command returning the contents of the command.
+ #
+ # ==== Parameters
+ # command<String>:: the command to be executed.
+ # config<Hash>:: give :verbose => false to not log the status, :capture => true to hide to output. Specify :with
+ # to append an executable to command execution.
+ #
+ # ==== Example
+ #
+ # inside('vendor') do
+ # run('ln -s ~/edge rails')
+ # end
+ #
+ def run(command, config = {})
+ return unless behavior == :invoke
+
+ destination = relative_to_original_destination_root(destination_root, false)
+ desc = "#{command} from #{destination.inspect}"
+
+ if config[:with]
+ desc = "#{File.basename(config[:with].to_s)} #{desc}"
+ command = "#{config[:with]} #{command}"
+ end
+
+ say_status :run, desc, config.fetch(:verbose, true)
+
+ return if options[:pretend]
+
+ env_splat = [config[:env]] if config[:env]
+
+ if config[:capture]
+ require "open3"
+ result, status = Open3.capture2e(*env_splat, command.to_s)
+ success = status.success?
+ else
+ result = system(*env_splat, command.to_s)
+ success = result
+ end
+
+ abort if !success && config.fetch(:abort_on_failure, self.class.exit_on_failure?)
+
+ result
+ end
+
+ # Executes a ruby script (taking into account WIN32 platform quirks).
+ #
+ # ==== Parameters
+ # command<String>:: the command to be executed.
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ def run_ruby_script(command, config = {})
+ return unless behavior == :invoke
+ run command, config.merge(with: Bundler::Thor::Util.ruby_command)
+ end
+
+ # Run a thor command. A hash of options can be given and it's converted to
+ # switches.
+ #
+ # ==== Parameters
+ # command<String>:: the command to be invoked
+ # args<Array>:: arguments to the command
+ # config<Hash>:: give :verbose => false to not log the status, :capture => true to hide to output.
+ # Other options are given as parameter to Bundler::Thor.
+ #
+ #
+ # ==== Examples
+ #
+ # thor :install, "http://gist.github.com/103208"
+ # #=> thor install http://gist.github.com/103208
+ #
+ # thor :list, :all => true, :substring => 'rails'
+ # #=> thor list --all --substring=rails
+ #
+ def thor(command, *args)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+ verbose = config.key?(:verbose) ? config.delete(:verbose) : true
+ pretend = config.key?(:pretend) ? config.delete(:pretend) : false
+ capture = config.key?(:capture) ? config.delete(:capture) : false
+
+ args.unshift(command)
+ args.push Bundler::Thor::Options.to_switches(config)
+ command = args.join(" ").strip
+
+ run command, with: :thor, verbose: verbose, pretend: pretend, capture: capture
+ end
+
+ protected
+
+ # Allow current root to be shared between invocations.
+ #
+ def _shared_configuration #:nodoc:
+ super.merge!(destination_root: destination_root)
+ end
+
+ def _cleanup_options_and_set(options, key) #:nodoc:
+ case options
+ when Array
+ %w(--force -f --skip -s).each { |i| options.delete(i) }
+ options << "--#{key}"
+ when Hash
+ [:force, :skip, "force", "skip"].each { |i| options.delete(i) }
+ options.merge!(key => true)
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb b/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb
new file mode 100644
index 0000000000..6724835b01
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb
@@ -0,0 +1,105 @@
+require_relative "empty_directory"
+
+class Bundler::Thor
+ module Actions
+ # Create a new file relative to the destination root with the given data,
+ # which is the return value of a block or a data string.
+ #
+ # ==== Parameters
+ # destination<String>:: the relative path to the destination root.
+ # data<String|NilClass>:: the data to append to the file.
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ # ==== Examples
+ #
+ # create_file "lib/fun_party.rb" do
+ # hostname = ask("What is the virtual hostname I should use?")
+ # "vhost.name = #{hostname}"
+ # end
+ #
+ # create_file "config/apache.conf", "your apache config"
+ #
+ def create_file(destination, *args, &block)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+ data = args.first
+ action CreateFile.new(self, destination, block || data.to_s, config)
+ end
+ alias_method :add_file, :create_file
+
+ # CreateFile is a subset of Template, which instead of rendering a file with
+ # ERB, it gets the content from the user.
+ #
+ class CreateFile < EmptyDirectory #:nodoc:
+ attr_reader :data
+
+ def initialize(base, destination, data, config = {})
+ @data = data
+ super(base, destination, config)
+ end
+
+ # Checks if the content of the file at the destination is identical to the rendered result.
+ #
+ # ==== Returns
+ # Boolean:: true if it is identical, false otherwise.
+ #
+ def identical?
+ # binread uses ASCII-8BIT, so to avoid false negatives, the string must use the same
+ exists? && File.binread(destination) == String.new(render).force_encoding("ASCII-8BIT")
+ end
+
+ # Holds the content to be added to the file.
+ #
+ def render
+ @render ||= if data.is_a?(Proc)
+ data.call
+ else
+ data
+ end
+ end
+
+ def invoke!
+ invoke_with_conflict_check do
+ require "fileutils"
+ FileUtils.mkdir_p(File.dirname(destination))
+ File.open(destination, "wb", config[:perm]) { |f| f.write render }
+ end
+ given_destination
+ end
+
+ protected
+
+ # Now on conflict we check if the file is identical or not.
+ #
+ def on_conflict_behavior(&block)
+ if identical?
+ say_status :identical, :blue
+ else
+ options = base.options.merge(config)
+ force_or_skip_or_conflict(options[:force], options[:skip], &block)
+ end
+ end
+
+ # If force is true, run the action, otherwise check if it's not being
+ # skipped. If both are false, show the file_collision menu, if the menu
+ # returns true, force it, otherwise skip.
+ #
+ def force_or_skip_or_conflict(force, skip, &block)
+ if force
+ say_status :force, :yellow
+ yield unless pretend?
+ elsif skip
+ say_status :skip, :yellow
+ else
+ say_status :conflict, :red
+ force_or_skip_or_conflict(force_on_collision?, true, &block)
+ end
+ end
+
+ # Shows the file collision menu to the user and gets the result.
+ #
+ def force_on_collision?
+ base.shell.file_collision(destination) { render }
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/actions/create_link.rb b/lib/bundler/vendor/thor/lib/thor/actions/create_link.rb
new file mode 100644
index 0000000000..fb76fcdbe9
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/actions/create_link.rb
@@ -0,0 +1,61 @@
+require_relative "create_file"
+
+class Bundler::Thor
+ module Actions
+ # Create a new file relative to the destination root from the given source.
+ #
+ # ==== Parameters
+ # destination<String>:: the relative path to the destination root.
+ # source<String|NilClass>:: the relative path to the source root.
+ # config<Hash>:: give :verbose => false to not log the status.
+ # :: give :symbolic => false for hard link.
+ #
+ # ==== Examples
+ #
+ # create_link "config/apache.conf", "/etc/apache.conf"
+ #
+ def create_link(destination, *args)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+ source = args.first
+ action CreateLink.new(self, destination, source, config)
+ end
+ alias_method :add_link, :create_link
+
+ # CreateLink is a subset of CreateFile, which instead of taking a block of
+ # data, just takes a source string from the user.
+ #
+ class CreateLink < CreateFile #:nodoc:
+ attr_reader :data
+
+ # Checks if the content of the file at the destination is identical to the rendered result.
+ #
+ # ==== Returns
+ # Boolean:: true if it is identical, false otherwise.
+ #
+ def identical?
+ source = File.expand_path(render, File.dirname(destination))
+ exists? && File.identical?(source, destination)
+ end
+
+ def invoke!
+ invoke_with_conflict_check do
+ require "fileutils"
+ FileUtils.mkdir_p(File.dirname(destination))
+ # Create a symlink by default
+ config[:symbolic] = true if config[:symbolic].nil?
+ File.unlink(destination) if exists?
+ if config[:symbolic]
+ File.symlink(render, destination)
+ else
+ File.link(render, destination)
+ end
+ end
+ given_destination
+ end
+
+ def exists?
+ super || File.symlink?(destination)
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/actions/directory.rb b/lib/bundler/vendor/thor/lib/thor/actions/directory.rb
new file mode 100644
index 0000000000..2f9687c0a5
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/actions/directory.rb
@@ -0,0 +1,108 @@
+require_relative "empty_directory"
+
+class Bundler::Thor
+ module Actions
+ # Copies recursively the files from source directory to root directory.
+ # If any of the files finishes with .tt, it's considered to be a template
+ # and is placed in the destination without the extension .tt. If any
+ # empty directory is found, it's copied and all .empty_directory files are
+ # ignored. If any file name is wrapped within % signs, the text within
+ # the % signs will be executed as a method and replaced with the returned
+ # value. Let's suppose a doc directory with the following files:
+ #
+ # doc/
+ # components/.empty_directory
+ # README
+ # rdoc.rb.tt
+ # %app_name%.rb
+ #
+ # When invoked as:
+ #
+ # directory "doc"
+ #
+ # It will create a doc directory in the destination with the following
+ # files (assuming that the `app_name` method returns the value "blog"):
+ #
+ # doc/
+ # components/
+ # README
+ # rdoc.rb
+ # blog.rb
+ #
+ # <b>Encoded path note:</b> Since Bundler::Thor internals use Object#respond_to? to check if it can
+ # expand %something%, this `something` should be a public method in the class calling
+ # #directory. If a method is private, Bundler::Thor stack raises PrivateMethodEncodedError.
+ #
+ # ==== Parameters
+ # source<String>:: the relative path to the source root.
+ # destination<String>:: the relative path to the destination root.
+ # config<Hash>:: give :verbose => false to not log the status.
+ # If :recursive => false, does not look for paths recursively.
+ # If :mode => :preserve, preserve the file mode from the source.
+ # If :exclude_pattern => /regexp/, prevents copying files that match that regexp.
+ #
+ # ==== Examples
+ #
+ # directory "doc"
+ # directory "doc", "docs", :recursive => false
+ #
+ def directory(source, *args, &block)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+ destination = args.first || source
+ action Directory.new(self, source, destination || source, config, &block)
+ end
+
+ class Directory < EmptyDirectory #:nodoc:
+ attr_reader :source
+
+ def initialize(base, source, destination = nil, config = {}, &block)
+ @source = File.expand_path(Dir[Util.escape_globs(base.find_in_source_paths(source.to_s))].first)
+ @block = block
+ super(base, destination, {recursive: true}.merge(config))
+ end
+
+ def invoke!
+ base.empty_directory given_destination, config
+ execute!
+ end
+
+ def revoke!
+ execute!
+ end
+
+ protected
+
+ def execute!
+ lookup = Util.escape_globs(source)
+ lookup = config[:recursive] ? File.join(lookup, "**") : lookup
+ lookup = file_level_lookup(lookup)
+
+ files(lookup).sort.each do |file_source|
+ next if File.directory?(file_source)
+ next if config[:exclude_pattern] && file_source.match(config[:exclude_pattern])
+ file_destination = File.join(given_destination, file_source.gsub(source, "."))
+ file_destination.gsub!("/./", "/")
+
+ case file_source
+ when /\.empty_directory$/
+ dirname = File.dirname(file_destination).gsub(%r{/\.$}, "")
+ next if dirname == given_destination
+ base.empty_directory(dirname, config)
+ when /#{TEMPLATE_EXTNAME}$/
+ base.template(file_source, file_destination[0..-4], config, &@block)
+ else
+ base.copy_file(file_source, file_destination, config, &@block)
+ end
+ end
+ end
+
+ def file_level_lookup(previous_lookup)
+ File.join(previous_lookup, "*")
+ end
+
+ def files(lookup)
+ Dir.glob(lookup, File::FNM_DOTMATCH)
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb b/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb
new file mode 100644
index 0000000000..c0bca78525
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb
@@ -0,0 +1,143 @@
+class Bundler::Thor
+ module Actions
+ # Creates an empty directory.
+ #
+ # ==== Parameters
+ # destination<String>:: the relative path to the destination root.
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ # ==== Examples
+ #
+ # empty_directory "doc"
+ #
+ def empty_directory(destination, config = {})
+ action EmptyDirectory.new(self, destination, config)
+ end
+
+ # Class which holds create directory logic. This is the base class for
+ # other actions like create_file and directory.
+ #
+ # This implementation is based in Templater actions, created by Jonas Nicklas
+ # and Michael S. Klishin under MIT LICENSE.
+ #
+ class EmptyDirectory #:nodoc:
+ attr_reader :base, :destination, :given_destination, :relative_destination, :config
+
+ # Initializes given the source and destination.
+ #
+ # ==== Parameters
+ # base<Bundler::Thor::Base>:: A Bundler::Thor::Base instance
+ # source<String>:: Relative path to the source of this file
+ # destination<String>:: Relative path to the destination of this file
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ def initialize(base, destination, config = {})
+ @base = base
+ @config = {verbose: true}.merge(config)
+ self.destination = destination
+ end
+
+ # Checks if the destination file already exists.
+ #
+ # ==== Returns
+ # Boolean:: true if the file exists, false otherwise.
+ #
+ def exists?
+ ::File.exist?(destination)
+ end
+
+ def invoke!
+ invoke_with_conflict_check do
+ require "fileutils"
+ ::FileUtils.mkdir_p(destination)
+ end
+ end
+
+ def revoke!
+ say_status :remove, :red
+ require "fileutils"
+ ::FileUtils.rm_rf(destination) if !pretend? && exists?
+ given_destination
+ end
+
+ protected
+
+ # Shortcut for pretend.
+ #
+ def pretend?
+ base.options[:pretend]
+ end
+
+ # Sets the absolute destination value from a relative destination value.
+ # It also stores the given and relative destination. Let's suppose our
+ # script is being executed on "dest", it sets the destination root to
+ # "dest". The destination, given_destination and relative_destination
+ # are related in the following way:
+ #
+ # inside "bar" do
+ # empty_directory "baz"
+ # end
+ #
+ # destination #=> dest/bar/baz
+ # relative_destination #=> bar/baz
+ # given_destination #=> baz
+ #
+ def destination=(destination)
+ return unless destination
+ @given_destination = convert_encoded_instructions(destination.to_s)
+ @destination = ::File.expand_path(@given_destination, base.destination_root)
+ @relative_destination = base.relative_to_original_destination_root(@destination)
+ end
+
+ # Filenames in the encoded form are converted. If you have a file:
+ #
+ # %file_name%.rb
+ #
+ # It calls #file_name from the base and replaces %-string with the
+ # return value (should be String) of #file_name:
+ #
+ # user.rb
+ #
+ # The method referenced can be either public or private.
+ #
+ def convert_encoded_instructions(filename)
+ filename.gsub(/%(.*?)%/) do |initial_string|
+ method = $1.strip
+ base.respond_to?(method, true) ? base.send(method) : initial_string
+ end
+ end
+
+ # Receives a hash of options and just execute the block if some
+ # conditions are met.
+ #
+ def invoke_with_conflict_check(&block)
+ if exists?
+ on_conflict_behavior(&block)
+ else
+ yield unless pretend?
+ say_status :create, :green
+ end
+
+ destination
+ rescue Errno::EISDIR, Errno::EEXIST
+ on_file_clash_behavior
+ end
+
+ def on_file_clash_behavior
+ say_status :file_clash, :red
+ end
+
+ # What to do when the destination file already exists.
+ #
+ def on_conflict_behavior
+ say_status :exist, :blue
+ end
+
+ # Shortcut to say_status shell method.
+ #
+ def say_status(status, color)
+ base.shell.say_status status, relative_destination, color if config[:verbose]
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb b/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb
new file mode 100644
index 0000000000..d8c9863054
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb
@@ -0,0 +1,407 @@
+require "erb"
+
+class Bundler::Thor
+ module Actions
+ # Copies the file from the relative source to the relative destination. If
+ # the destination is not given it's assumed to be equal to the source.
+ #
+ # ==== Parameters
+ # source<String>:: the relative path to the source root.
+ # destination<String>:: the relative path to the destination root.
+ # config<Hash>:: give :verbose => false to not log the status, and
+ # :mode => :preserve, to preserve the file mode from the source.
+ #
+ # ==== Examples
+ #
+ # copy_file "README", "doc/README"
+ #
+ # copy_file "doc/README"
+ #
+ def copy_file(source, *args, &block)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+ destination = args.first || source
+ source = File.expand_path(find_in_source_paths(source.to_s))
+
+ resulting_destination = create_file destination, nil, config do
+ content = File.binread(source)
+ content = yield(content) if block
+ content
+ end
+ if config[:mode] == :preserve
+ mode = File.stat(source).mode
+ chmod(resulting_destination, mode, config)
+ end
+ end
+
+ # Links the file from the relative source to the relative destination. If
+ # the destination is not given it's assumed to be equal to the source.
+ #
+ # ==== Parameters
+ # source<String>:: the relative path to the source root.
+ # destination<String>:: the relative path to the destination root.
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ # ==== Examples
+ #
+ # link_file "README", "doc/README"
+ #
+ # link_file "doc/README"
+ #
+ def link_file(source, *args)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+ destination = args.first || source
+ source = File.expand_path(find_in_source_paths(source.to_s))
+
+ create_link destination, source, config
+ end
+
+ # Gets the content at the given address and places it at the given relative
+ # destination. If a block is given instead of destination, the content of
+ # the url is yielded and used as location.
+ #
+ # +get+ relies on open-uri, so passing application user input would provide
+ # a command injection attack vector.
+ #
+ # ==== Parameters
+ # source<String>:: the address of the given content.
+ # destination<String>:: the relative path to the destination root.
+ # config<Hash>:: give :verbose => false to not log the status, and
+ # :http_headers => <Hash> to add headers to an http request.
+ #
+ # ==== Examples
+ #
+ # get "http://gist.github.com/103208", "doc/README"
+ #
+ # get "http://gist.github.com/103208", "doc/README", :http_headers => {"Content-Type" => "application/json"}
+ #
+ # get "http://gist.github.com/103208" do |content|
+ # content.split("\n").first
+ # end
+ #
+ def get(source, *args, &block)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+ destination = args.first
+
+ render = if source =~ %r{^https?\://}
+ require "open-uri"
+ URI.send(:open, source, config.fetch(:http_headers, {})) { |input| input.binmode.read }
+ else
+ source = File.expand_path(find_in_source_paths(source.to_s))
+ File.open(source) { |input| input.binmode.read }
+ end
+
+ destination ||= if block_given?
+ block.arity == 1 ? yield(render) : yield
+ else
+ File.basename(source)
+ end
+
+ create_file destination, render, config
+ end
+
+ # Gets an ERB template at the relative source, executes it and makes a copy
+ # at the relative destination. If the destination is not given it's assumed
+ # to be equal to the source removing .tt from the filename.
+ #
+ # ==== Parameters
+ # source<String>:: the relative path to the source root.
+ # destination<String>:: the relative path to the destination root.
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ # ==== Examples
+ #
+ # template "README", "doc/README"
+ #
+ # template "doc/README"
+ #
+ def template(source, *args, &block)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+ destination = args.first || source.sub(/#{TEMPLATE_EXTNAME}$/, "")
+
+ source = File.expand_path(find_in_source_paths(source.to_s))
+ context = config.delete(:context) || instance_eval("binding")
+
+ create_file destination, nil, config do
+ capturable_erb = CapturableERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer")
+ content = capturable_erb.tap do |erb|
+ erb.filename = source
+ end.result(context)
+ content = yield(content) if block
+ content
+ end
+ end
+
+ # Changes the mode of the given file or directory.
+ #
+ # ==== Parameters
+ # mode<Integer>:: the file mode
+ # path<String>:: the name of the file to change mode
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ # ==== Example
+ #
+ # chmod "script/server", 0755
+ #
+ def chmod(path, mode, config = {})
+ return unless behavior == :invoke
+ path = File.expand_path(path, destination_root)
+ say_status :chmod, relative_to_original_destination_root(path), config.fetch(:verbose, true)
+ unless options[:pretend]
+ require "fileutils"
+ FileUtils.chmod_R(mode, path)
+ end
+ end
+
+ # Prepend text to a file. Since it depends on insert_into_file, it's reversible.
+ #
+ # ==== Parameters
+ # path<String>:: path of the file to be changed
+ # data<String>:: the data to prepend to the file, can be also given as a block.
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ # ==== Example
+ #
+ # prepend_to_file 'config/environments/test.rb', 'config.gem "rspec"'
+ #
+ # prepend_to_file 'config/environments/test.rb' do
+ # 'config.gem "rspec"'
+ # end
+ #
+ def prepend_to_file(path, *args, &block)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+ config[:after] = /\A/
+ insert_into_file(path, *(args << config), &block)
+ end
+ alias_method :prepend_file, :prepend_to_file
+
+ # Append text to a file. Since it depends on insert_into_file, it's reversible.
+ #
+ # ==== Parameters
+ # path<String>:: path of the file to be changed
+ # data<String>:: the data to append to the file, can be also given as a block.
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ # ==== Example
+ #
+ # append_to_file 'config/environments/test.rb', 'config.gem "rspec"'
+ #
+ # append_to_file 'config/environments/test.rb' do
+ # 'config.gem "rspec"'
+ # end
+ #
+ def append_to_file(path, *args, &block)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+ config[:before] = /\z/
+ insert_into_file(path, *(args << config), &block)
+ end
+ alias_method :append_file, :append_to_file
+
+ # Injects text right after the class definition. Since it depends on
+ # insert_into_file, it's reversible.
+ #
+ # ==== Parameters
+ # path<String>:: path of the file to be changed
+ # klass<String|Class>:: the class to be manipulated
+ # data<String>:: the data to append to the class, can be also given as a block.
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ # ==== Examples
+ #
+ # inject_into_class "app/controllers/application_controller.rb", "ApplicationController", " filter_parameter :password\n"
+ #
+ # inject_into_class "app/controllers/application_controller.rb", "ApplicationController" do
+ # " filter_parameter :password\n"
+ # end
+ #
+ def inject_into_class(path, klass, *args, &block)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+ config[:after] = /class #{klass}\n|class #{klass} .*\n/
+ insert_into_file(path, *(args << config), &block)
+ end
+
+ # Injects text right after the module definition. Since it depends on
+ # insert_into_file, it's reversible.
+ #
+ # ==== Parameters
+ # path<String>:: path of the file to be changed
+ # module_name<String|Class>:: the module to be manipulated
+ # data<String>:: the data to append to the class, can be also given as a block.
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ # ==== Examples
+ #
+ # inject_into_module "app/helpers/application_helper.rb", "ApplicationHelper", " def help; 'help'; end\n"
+ #
+ # inject_into_module "app/helpers/application_helper.rb", "ApplicationHelper" do
+ # " def help; 'help'; end\n"
+ # end
+ #
+ def inject_into_module(path, module_name, *args, &block)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+ config[:after] = /module #{module_name}\n|module #{module_name} .*\n/
+ insert_into_file(path, *(args << config), &block)
+ end
+
+ # Run a regular expression replacement on a file, raising an error if the
+ # contents of the file are not changed.
+ #
+ # ==== Parameters
+ # path<String>:: path of the file to be changed
+ # flag<Regexp|String>:: the regexp or string to be replaced
+ # replacement<String>:: the replacement, can be also given as a block
+ # config<Hash>:: give :verbose => false to not log the status, and
+ # :force => true, to force the replacement regardless of runner behavior.
+ #
+ # ==== Example
+ #
+ # gsub_file! 'app/controllers/application_controller.rb', /#\s*(filter_parameter_logging :password)/, '\1'
+ #
+ # gsub_file! 'README', /rake/, :green do |match|
+ # match << " no more. Use thor!"
+ # end
+ #
+ def gsub_file!(path, flag, *args, &block)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+
+ return unless behavior == :invoke || config.fetch(:force, false)
+
+ path = File.expand_path(path, destination_root)
+ say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
+
+ actually_gsub_file(path, flag, args, true, &block) unless options[:pretend]
+ end
+
+ # Run a regular expression replacement on a file.
+ #
+ # ==== Parameters
+ # path<String>:: path of the file to be changed
+ # flag<Regexp|String>:: the regexp or string to be replaced
+ # replacement<String>:: the replacement, can be also given as a block
+ # config<Hash>:: give :verbose => false to not log the status, and
+ # :force => true, to force the replacement regardless of runner behavior.
+ #
+ # ==== Example
+ #
+ # gsub_file 'app/controllers/application_controller.rb', /#\s*(filter_parameter_logging :password)/, '\1'
+ #
+ # gsub_file 'README', /rake/, :green do |match|
+ # match << " no more. Use thor!"
+ # end
+ #
+ def gsub_file(path, flag, *args, &block)
+ config = args.last.is_a?(Hash) ? args.pop : {}
+
+ return unless behavior == :invoke || config.fetch(:force, false)
+
+ path = File.expand_path(path, destination_root)
+ say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
+
+ actually_gsub_file(path, flag, args, false, &block) unless options[:pretend]
+ end
+
+ # Uncomment all lines matching a given regex. Preserves indentation before
+ # the comment hash and removes the hash and any immediate following space.
+ #
+ # ==== Parameters
+ # path<String>:: path of the file to be changed
+ # flag<Regexp|String>:: the regexp or string used to decide which lines to uncomment
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ # ==== Example
+ #
+ # uncomment_lines 'config/initializers/session_store.rb', /active_record/
+ #
+ def uncomment_lines(path, flag, *args)
+ flag = flag.respond_to?(:source) ? flag.source : flag
+
+ gsub_file(path, /^(\s*)#[[:blank:]]?(.*#{flag})/, '\1\2', *args)
+ end
+
+ # Comment all lines matching a given regex. It will leave the space
+ # which existed before the beginning of the line in tact and will insert
+ # a single space after the comment hash.
+ #
+ # ==== Parameters
+ # path<String>:: path of the file to be changed
+ # flag<Regexp|String>:: the regexp or string used to decide which lines to comment
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ # ==== Example
+ #
+ # comment_lines 'config/initializers/session_store.rb', /cookie_store/
+ #
+ def comment_lines(path, flag, *args)
+ flag = flag.respond_to?(:source) ? flag.source : flag
+
+ gsub_file(path, /^(\s*)([^#\n]*#{flag})/, '\1# \2', *args)
+ end
+
+ # Removes a file at the given location.
+ #
+ # ==== Parameters
+ # path<String>:: path of the file to be changed
+ # config<Hash>:: give :verbose => false to not log the status.
+ #
+ # ==== Example
+ #
+ # remove_file 'README'
+ # remove_file 'app/controllers/application_controller.rb'
+ #
+ def remove_file(path, config = {})
+ return unless behavior == :invoke
+ path = File.expand_path(path, destination_root)
+
+ say_status :remove, relative_to_original_destination_root(path), config.fetch(:verbose, true)
+ if !options[:pretend] && (File.exist?(path) || File.symlink?(path))
+ require "fileutils"
+ ::FileUtils.rm_rf(path)
+ end
+ end
+ alias_method :remove_dir, :remove_file
+
+ attr_accessor :output_buffer
+ private :output_buffer, :output_buffer=
+
+ private
+
+ def concat(string)
+ @output_buffer.concat(string)
+ end
+
+ def capture(*args)
+ with_output_buffer { yield(*args) }
+ end
+
+ def with_output_buffer(buf = "".dup) #:nodoc:
+ raise ArgumentError, "Buffer cannot be a frozen object" if buf.frozen?
+ old_buffer = output_buffer
+ self.output_buffer = buf
+ yield
+ output_buffer
+ ensure
+ self.output_buffer = old_buffer
+ end
+
+ def actually_gsub_file(path, flag, args, error_on_no_change, &block)
+ content = File.binread(path)
+ success = content.gsub!(flag, *args, &block)
+
+ if success.nil? && error_on_no_change
+ raise Bundler::Thor::Error, "The content of #{path} did not change"
+ end
+
+ File.open(path, "wb") { |file| file.write(content) }
+ end
+
+ # Bundler::Thor::Actions#capture depends on what kind of buffer is used in ERB.
+ # Thus CapturableERB fixes ERB to use String buffer.
+ class CapturableERB < ERB
+ def set_eoutvar(compiler, eoutvar = "_erbout")
+ compiler.put_cmd = "#{eoutvar}.concat"
+ compiler.insert_cmd = "#{eoutvar}.concat"
+ compiler.pre_cmd = ["#{eoutvar} = ''.dup"]
+ compiler.post_cmd = [eoutvar]
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb b/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb
new file mode 100644
index 0000000000..70526e615f
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb
@@ -0,0 +1,130 @@
+require_relative "empty_directory"
+
+class Bundler::Thor
+ module Actions
+ # Injects the given content into a file. Different from gsub_file, this
+ # method is reversible.
+ #
+ # ==== Parameters
+ # destination<String>:: Relative path to the destination root
+ # data<String>:: Data to add to the file. Can be given as a block.
+ # config<Hash>:: give :verbose => false to not log the status and the flag
+ # for injection (:after or :before) or :force => true for
+ # insert two or more times the same content.
+ #
+ # ==== Examples
+ #
+ # insert_into_file "config/environment.rb", "config.gem :thor", :after => "Rails::Initializer.run do |config|\n"
+ #
+ # insert_into_file "config/environment.rb", :after => "Rails::Initializer.run do |config|\n" do
+ # gems = ask "Which gems would you like to add?"
+ # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
+ # end
+ #
+ WARNINGS = {unchanged_no_flag: "File unchanged! Either the supplied flag value not found or the content has already been inserted!"}
+
+ def insert_into_file(destination, *args, &block)
+ data = block_given? ? block : args.shift
+
+ config = args.shift || {}
+ config[:after] = /\z/ unless config.key?(:before) || config.key?(:after)
+
+ action InjectIntoFile.new(self, destination, data, config)
+ end
+ alias_method :inject_into_file, :insert_into_file
+
+ class InjectIntoFile < EmptyDirectory #:nodoc:
+ attr_reader :replacement, :flag, :behavior
+
+ def initialize(base, destination, data, config)
+ super(base, destination, {verbose: true}.merge(config))
+
+ @behavior, @flag = if @config.key?(:after)
+ [:after, @config.delete(:after)]
+ else
+ [:before, @config.delete(:before)]
+ end
+
+ @replacement = data.is_a?(Proc) ? data.call : data
+ @flag = Regexp.escape(@flag) unless @flag.is_a?(Regexp)
+ end
+
+ def invoke!
+ content = if @behavior == :after
+ '\0' + replacement
+ else
+ replacement + '\0'
+ end
+
+ if exists?
+ if replace!(/#{flag}/, content, config[:force])
+ say_status(:invoke)
+ elsif replacement_present?
+ say_status(:unchanged, color: :blue)
+ else
+ say_status(:unchanged, warning: WARNINGS[:unchanged_no_flag], color: :red)
+ end
+ else
+ unless pretend?
+ raise Bundler::Thor::Error, "The file #{ destination } does not appear to exist"
+ end
+ end
+ end
+
+ def revoke!
+ say_status :revoke
+
+ regexp = if @behavior == :after
+ content = '\1\2'
+ /(#{flag})(.*)(#{Regexp.escape(replacement)})/m
+ else
+ content = '\2\3'
+ /(#{Regexp.escape(replacement)})(.*)(#{flag})/m
+ end
+
+ replace!(regexp, content, true)
+ end
+
+ protected
+
+ def say_status(behavior, warning: nil, color: nil)
+ status = if behavior == :invoke
+ if flag == /\A/
+ :prepend
+ elsif flag == /\z/
+ :append
+ else
+ :insert
+ end
+ elsif warning
+ warning
+ elsif behavior == :unchanged
+ :unchanged
+ else
+ :subtract
+ end
+
+ super(status, (color || config[:verbose]))
+ end
+
+ def content
+ @content ||= File.read(destination)
+ end
+
+ def replacement_present?
+ content.include?(replacement)
+ end
+
+ # Adds the content to the file.
+ #
+ def replace!(regexp, string, force)
+ if force || !replacement_present?
+ success = content.gsub!(regexp, string)
+
+ File.open(destination, "wb") { |file| file.write(content) } unless pretend?
+ success
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/base.rb b/lib/bundler/vendor/thor/lib/thor/base.rb
new file mode 100644
index 0000000000..b156899c1e
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/base.rb
@@ -0,0 +1,825 @@
+require_relative "command"
+require_relative "core_ext/hash_with_indifferent_access"
+require_relative "error"
+require_relative "invocation"
+require_relative "nested_context"
+require_relative "parser"
+require_relative "shell"
+require_relative "line_editor"
+require_relative "util"
+
+class Bundler::Thor
+ autoload :Actions, File.expand_path("actions", __dir__)
+ autoload :RakeCompat, File.expand_path("rake_compat", __dir__)
+ autoload :Group, File.expand_path("group", __dir__)
+
+ # Shortcuts for help.
+ HELP_MAPPINGS = %w(-h -? --help -D)
+
+ # Bundler::Thor methods that should not be overwritten by the user.
+ THOR_RESERVED_WORDS = %w(invoke shell options behavior root destination_root relative_root
+ action add_file create_file in_root inside run run_ruby_script)
+
+ TEMPLATE_EXTNAME = ".tt"
+
+ class << self
+ def deprecation_warning(message) #:nodoc:
+ unless ENV["THOR_SILENCE_DEPRECATION"]
+ warn "Deprecation warning: #{message}\n" +
+ "You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION."
+ end
+ end
+ end
+
+ module Base
+ attr_accessor :options, :parent_options, :args
+
+ # It receives arguments in an Array and two hashes, one for options and
+ # other for configuration.
+ #
+ # Notice that it does not check if all required arguments were supplied.
+ # It should be done by the parser.
+ #
+ # ==== Parameters
+ # args<Array[Object]>:: An array of objects. The objects are applied to their
+ # respective accessors declared with <tt>argument</tt>.
+ #
+ # options<Hash>:: An options hash that will be available as self.options.
+ # The hash given is converted to a hash with indifferent
+ # access, magic predicates (options.skip?) and then frozen.
+ #
+ # config<Hash>:: Configuration for this Bundler::Thor class.
+ #
+ def initialize(args = [], local_options = {}, config = {})
+ parse_options = self.class.class_options
+
+ # The start method splits inbound arguments at the first argument
+ # that looks like an option (starts with - or --). It then calls
+ # new, passing in the two halves of the arguments Array as the
+ # first two parameters.
+
+ command_options = config.delete(:command_options) # hook for start
+ parse_options = parse_options.merge(command_options) if command_options
+
+ if local_options.is_a?(Array)
+ array_options = local_options
+ hash_options = {}
+ else
+ # Handle the case where the class was explicitly instantiated
+ # with pre-parsed options.
+ array_options = []
+ hash_options = local_options
+ end
+
+ # Let Bundler::Thor::Options parse the options first, so it can remove
+ # declared options from the array. This will leave us with
+ # a list of arguments that weren't declared.
+ current_command = config[:current_command]
+ stop_on_unknown = self.class.stop_on_unknown_option? current_command
+
+ # Give a relation of options.
+ # After parsing, Bundler::Thor::Options check whether right relations are kept
+ relations = if current_command.nil?
+ {exclusive_option_names: [], at_least_one_option_names: []}
+ else
+ current_command.options_relation
+ end
+
+ self.class.class_exclusive_option_names.map { |n| relations[:exclusive_option_names] << n }
+ self.class.class_at_least_one_option_names.map { |n| relations[:at_least_one_option_names] << n }
+
+ disable_required_check = self.class.disable_required_check? current_command
+
+ opts = Bundler::Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check, relations)
+
+ self.options = opts.parse(array_options)
+ self.options = config[:class_options].merge(options) if config[:class_options]
+
+ # If unknown options are disallowed, make sure that none of the
+ # remaining arguments looks like an option.
+ opts.check_unknown! if self.class.check_unknown_options?(config)
+
+ # Add the remaining arguments from the options parser to the
+ # arguments passed in to initialize. Then remove any positional
+ # arguments declared using #argument (this is primarily used
+ # by Bundler::Thor::Group). Tis will leave us with the remaining
+ # positional arguments.
+ to_parse = args
+ to_parse += opts.remaining unless self.class.strict_args_position?(config)
+
+ thor_args = Bundler::Thor::Arguments.new(self.class.arguments)
+ thor_args.parse(to_parse).each { |k, v| __send__("#{k}=", v) }
+ @args = thor_args.remaining
+ end
+
+ class << self
+ def included(base) #:nodoc:
+ super(base)
+ base.extend ClassMethods
+ base.send :include, Invocation
+ base.send :include, Shell
+ end
+
+ # Returns the classes that inherits from Bundler::Thor or Bundler::Thor::Group.
+ #
+ # ==== Returns
+ # Array[Class]
+ #
+ def subclasses
+ @subclasses ||= []
+ end
+
+ # Returns the files where the subclasses are kept.
+ #
+ # ==== Returns
+ # Hash[path<String> => Class]
+ #
+ def subclass_files
+ @subclass_files ||= Hash.new { |h, k| h[k] = [] }
+ end
+
+ # Whenever a class inherits from Bundler::Thor or Bundler::Thor::Group, we should track the
+ # class and the file on Bundler::Thor::Base. This is the method responsible for it.
+ #
+ def register_klass_file(klass) #:nodoc:
+ file = caller[1].match(/(.*):\d+/)[1]
+ Bundler::Thor::Base.subclasses << klass unless Bundler::Thor::Base.subclasses.include?(klass)
+
+ file_subclasses = Bundler::Thor::Base.subclass_files[File.expand_path(file)]
+ file_subclasses << klass unless file_subclasses.include?(klass)
+ end
+ end
+
+ module ClassMethods
+ def attr_reader(*) #:nodoc:
+ no_commands { super }
+ end
+
+ def attr_writer(*) #:nodoc:
+ no_commands { super }
+ end
+
+ def attr_accessor(*) #:nodoc:
+ no_commands { super }
+ end
+
+ # If you want to raise an error for unknown options, call check_unknown_options!
+ # This is disabled by default to allow dynamic invocations.
+ def check_unknown_options!
+ @check_unknown_options = true
+ end
+
+ def check_unknown_options #:nodoc:
+ @check_unknown_options ||= from_superclass(:check_unknown_options, false)
+ end
+
+ def check_unknown_options?(config) #:nodoc:
+ !!check_unknown_options
+ end
+
+ # If you want to raise an error when the default value of an option does not match
+ # the type call check_default_type!
+ # This will be the default; for compatibility a deprecation warning is issued if necessary.
+ def check_default_type!
+ @check_default_type = true
+ end
+
+ # If you want to use defaults that don't match the type of an option,
+ # either specify `check_default_type: false` or call `allow_incompatible_default_type!`
+ def allow_incompatible_default_type!
+ @check_default_type = false
+ end
+
+ def check_default_type #:nodoc:
+ @check_default_type = from_superclass(:check_default_type, nil) unless defined?(@check_default_type)
+ @check_default_type
+ end
+
+ # If true, option parsing is suspended as soon as an unknown option or a
+ # regular argument is encountered. All remaining arguments are passed to
+ # the command as regular arguments.
+ def stop_on_unknown_option?(command_name) #:nodoc:
+ false
+ end
+
+ # If true, option set will not suspend the execution of the command when
+ # a required option is not provided.
+ def disable_required_check?(command_name) #:nodoc:
+ false
+ end
+
+ # If you want only strict string args (useful when cascading thor classes),
+ # call strict_args_position! This is disabled by default to allow dynamic
+ # invocations.
+ def strict_args_position!
+ @strict_args_position = true
+ end
+
+ def strict_args_position #:nodoc:
+ @strict_args_position ||= from_superclass(:strict_args_position, false)
+ end
+
+ def strict_args_position?(config) #:nodoc:
+ !!strict_args_position
+ end
+
+ # Adds an argument to the class and creates an attr_accessor for it.
+ #
+ # Arguments are different from options in several aspects. The first one
+ # is how they are parsed from the command line, arguments are retrieved
+ # from position:
+ #
+ # thor command NAME
+ #
+ # Instead of:
+ #
+ # thor command --name=NAME
+ #
+ # Besides, arguments are used inside your code as an accessor (self.argument),
+ # while options are all kept in a hash (self.options).
+ #
+ # Finally, arguments cannot have type :default or :boolean but can be
+ # optional (supplying :optional => :true or :required => false), although
+ # you cannot have a required argument after a non-required argument. If you
+ # try it, an error is raised.
+ #
+ # ==== Parameters
+ # name<Symbol>:: The name of the argument.
+ # options<Hash>:: Described below.
+ #
+ # ==== Options
+ # :desc - Description for the argument.
+ # :required - If the argument is required or not.
+ # :optional - If the argument is optional or not.
+ # :type - The type of the argument, can be :string, :hash, :array, :numeric.
+ # :default - Default value for this argument. It cannot be required and have default values.
+ # :banner - String to show on usage notes.
+ #
+ # ==== Errors
+ # ArgumentError:: Raised if you supply a required argument after a non required one.
+ #
+ def argument(name, options = {})
+ is_thor_reserved_word?(name, :argument)
+ no_commands { attr_accessor name }
+
+ required = if options.key?(:optional)
+ !options[:optional]
+ elsif options.key?(:required)
+ options[:required]
+ else
+ options[:default].nil?
+ end
+
+ remove_argument name
+
+ if required
+ arguments.each do |argument|
+ next if argument.required?
+ raise ArgumentError, "You cannot have #{name.to_s.inspect} as required argument after " \
+ "the non-required argument #{argument.human_name.inspect}."
+ end
+ end
+
+ options[:required] = required
+
+ arguments << Bundler::Thor::Argument.new(name, options)
+ end
+
+ # Returns this class arguments, looking up in the ancestors chain.
+ #
+ # ==== Returns
+ # Array[Bundler::Thor::Argument]
+ #
+ def arguments
+ @arguments ||= from_superclass(:arguments, [])
+ end
+
+ # Adds a bunch of options to the set of class options.
+ #
+ # class_options :foo => false, :bar => :required, :baz => :string
+ #
+ # If you prefer more detailed declaration, check class_option.
+ #
+ # ==== Parameters
+ # Hash[Symbol => Object]
+ #
+ def class_options(options = nil)
+ @class_options ||= from_superclass(:class_options, {})
+ build_options(options, @class_options) if options
+ @class_options
+ end
+
+ # Adds an option to the set of class options
+ #
+ # ==== Parameters
+ # name<Symbol>:: The name of the argument.
+ # options<Hash>:: Described below.
+ #
+ # ==== Options
+ # :desc:: -- Description for the argument.
+ # :required:: -- If the argument is required or not.
+ # :default:: -- Default value for this argument.
+ # :group:: -- The group for this options. Use by class options to output options in different levels.
+ # :aliases:: -- Aliases for this option. <b>Note:</b> Bundler::Thor follows a convention of one-dash-one-letter options. Thus aliases like "-something" wouldn't be parsed; use either "\--something" or "-s" instead.
+ # :type:: -- The type of the argument, can be :string, :hash, :array, :numeric or :boolean.
+ # :banner:: -- String to show on usage notes.
+ # :hide:: -- If you want to hide this option from the help.
+ #
+ def class_option(name, options = {})
+ unless [ Symbol, String ].any? { |klass| name.is_a?(klass) }
+ raise ArgumentError, "Expected a Symbol or String, got #{name.inspect}"
+ end
+ build_option(name, options, class_options)
+ end
+
+ # Adds and declares option group for exclusive options in the
+ # block and arguments. You can declare options as the outside of the block.
+ #
+ # ==== Parameters
+ # Array[Bundler::Thor::Option.name]
+ #
+ # ==== Examples
+ #
+ # class_exclusive do
+ # class_option :one
+ # class_option :two
+ # end
+ #
+ # Or
+ #
+ # class_option :one
+ # class_option :two
+ # class_exclusive :one, :two
+ #
+ # If you give "--one" and "--two" at the same time ExclusiveArgumentsError
+ # will be raised.
+ #
+ def class_exclusive(*args, &block)
+ register_options_relation_for(:class_options,
+ :class_exclusive_option_names, *args, &block)
+ end
+
+ # Adds and declares option group for required at least one of options in the
+ # block and arguments. You can declare options as the outside of the block.
+ #
+ # ==== Examples
+ #
+ # class_at_least_one do
+ # class_option :one
+ # class_option :two
+ # end
+ #
+ # Or
+ #
+ # class_option :one
+ # class_option :two
+ # class_at_least_one :one, :two
+ #
+ # If you do not give "--one" and "--two" AtLeastOneRequiredArgumentError
+ # will be raised.
+ #
+ # You can use class_at_least_one and class_exclusive at the same time.
+ #
+ # class_exclusive do
+ # class_at_least_one do
+ # class_option :one
+ # class_option :two
+ # end
+ # end
+ #
+ # Then it is required either only one of "--one" or "--two".
+ #
+ def class_at_least_one(*args, &block)
+ register_options_relation_for(:class_options,
+ :class_at_least_one_option_names, *args, &block)
+ end
+
+ # Returns this class exclusive options array set, looking up in the ancestors chain.
+ #
+ # ==== Returns
+ # Array[Array[Bundler::Thor::Option.name]]
+ #
+ def class_exclusive_option_names
+ @class_exclusive_option_names ||= from_superclass(:class_exclusive_option_names, [])
+ end
+
+ # Returns this class at least one of required options array set, looking up in the ancestors chain.
+ #
+ # ==== Returns
+ # Array[Array[Bundler::Thor::Option.name]]
+ #
+ def class_at_least_one_option_names
+ @class_at_least_one_option_names ||= from_superclass(:class_at_least_one_option_names, [])
+ end
+
+ # Removes a previous defined argument. If :undefine is given, undefine
+ # accessors as well.
+ #
+ # ==== Parameters
+ # names<Array>:: Arguments to be removed
+ #
+ # ==== Examples
+ #
+ # remove_argument :foo
+ # remove_argument :foo, :bar, :baz, :undefine => true
+ #
+ def remove_argument(*names)
+ options = names.last.is_a?(Hash) ? names.pop : {}
+
+ names.each do |name|
+ arguments.delete_if { |a| a.name == name.to_s }
+ undef_method name, "#{name}=" if options[:undefine]
+ end
+ end
+
+ # Removes a previous defined class option.
+ #
+ # ==== Parameters
+ # names<Array>:: Class options to be removed
+ #
+ # ==== Examples
+ #
+ # remove_class_option :foo
+ # remove_class_option :foo, :bar, :baz
+ #
+ def remove_class_option(*names)
+ names.each do |name|
+ class_options.delete(name)
+ end
+ end
+
+ # Defines the group. This is used when thor list is invoked so you can specify
+ # that only commands from a pre-defined group will be shown. Defaults to standard.
+ #
+ # ==== Parameters
+ # name<String|Symbol>
+ #
+ def group(name = nil)
+ if name
+ @group = name.to_s
+ else
+ @group ||= from_superclass(:group, "standard")
+ end
+ end
+
+ # Returns the commands for this Bundler::Thor class.
+ #
+ # ==== Returns
+ # Hash:: An ordered hash with commands names as keys and Bundler::Thor::Command
+ # objects as values.
+ #
+ def commands
+ @commands ||= Hash.new
+ end
+ alias_method :tasks, :commands
+
+ # Returns the commands for this Bundler::Thor class and all subclasses.
+ #
+ # ==== Returns
+ # Hash:: An ordered hash with commands names as keys and Bundler::Thor::Command
+ # objects as values.
+ #
+ def all_commands
+ @all_commands ||= from_superclass(:all_commands, Hash.new)
+ @all_commands.merge!(commands)
+ end
+ alias_method :all_tasks, :all_commands
+
+ # Removes a given command from this Bundler::Thor class. This is usually done if you
+ # are inheriting from another class and don't want it to be available
+ # anymore.
+ #
+ # By default it only remove the mapping to the command. But you can supply
+ # :undefine => true to undefine the method from the class as well.
+ #
+ # ==== Parameters
+ # name<Symbol|String>:: The name of the command to be removed
+ # options<Hash>:: You can give :undefine => true if you want commands the method
+ # to be undefined from the class as well.
+ #
+ def remove_command(*names)
+ options = names.last.is_a?(Hash) ? names.pop : {}
+
+ names.each do |name|
+ commands.delete(name.to_s)
+ all_commands.delete(name.to_s)
+ undef_method name if options[:undefine]
+ end
+ end
+ alias_method :remove_task, :remove_command
+
+ # All methods defined inside the given block are not added as commands.
+ #
+ # So you can do:
+ #
+ # class MyScript < Bundler::Thor
+ # no_commands do
+ # def this_is_not_a_command
+ # end
+ # end
+ # end
+ #
+ # You can also add the method and remove it from the command list:
+ #
+ # class MyScript < Bundler::Thor
+ # def this_is_not_a_command
+ # end
+ # remove_command :this_is_not_a_command
+ # end
+ #
+ def no_commands(&block)
+ no_commands_context.enter(&block)
+ end
+
+ alias_method :no_tasks, :no_commands
+
+ def no_commands_context
+ @no_commands_context ||= NestedContext.new
+ end
+
+ def no_commands?
+ no_commands_context.entered?
+ end
+
+ # Sets the namespace for the Bundler::Thor or Bundler::Thor::Group class. By default the
+ # namespace is retrieved from the class name. If your Bundler::Thor class is named
+ # Scripts::MyScript, the help method, for example, will be called as:
+ #
+ # thor scripts:my_script -h
+ #
+ # If you change the namespace:
+ #
+ # namespace :my_scripts
+ #
+ # You change how your commands are invoked:
+ #
+ # thor my_scripts -h
+ #
+ # Finally, if you change your namespace to default:
+ #
+ # namespace :default
+ #
+ # Your commands can be invoked with a shortcut. Instead of:
+ #
+ # thor :my_command
+ #
+ def namespace(name = nil)
+ if name
+ @namespace = name.to_s
+ else
+ @namespace ||= Bundler::Thor::Util.namespace_from_thor_class(self)
+ end
+ end
+
+ # Parses the command and options from the given args, instantiate the class
+ # and invoke the command. This method is used when the arguments must be parsed
+ # from an array. If you are inside Ruby and want to use a Bundler::Thor class, you
+ # can simply initialize it:
+ #
+ # script = MyScript.new(args, options, config)
+ # script.invoke(:command, first_arg, second_arg, third_arg)
+ #
+ def start(given_args = ARGV, config = {})
+ config[:shell] ||= Bundler::Thor::Base.shell.new
+ dispatch(nil, given_args.dup, nil, config)
+ rescue Bundler::Thor::Error => e
+ config[:debug] || ENV["THOR_DEBUG"] == "1" ? (raise e) : config[:shell].error(e.message)
+ exit(false) if exit_on_failure?
+ rescue Errno::EPIPE
+ # This happens if a thor command is piped to something like `head`,
+ # which closes the pipe when it's done reading. This will also
+ # mean that if the pipe is closed, further unnecessary
+ # computation will not occur.
+ exit(true)
+ end
+
+ # Allows to use private methods from parent in child classes as commands.
+ #
+ # ==== Parameters
+ # names<Array>:: Method names to be used as commands
+ #
+ # ==== Examples
+ #
+ # public_command :foo
+ # public_command :foo, :bar, :baz
+ #
+ def public_command(*names)
+ names.each do |name|
+ class_eval "def #{name}(*); super end", __FILE__, __LINE__
+ end
+ end
+ alias_method :public_task, :public_command
+
+ def handle_no_command_error(command, has_namespace = $thor_runner) #:nodoc:
+ raise UndefinedCommandError.new(command, all_commands.keys, (namespace if has_namespace))
+ end
+ alias_method :handle_no_task_error, :handle_no_command_error
+
+ def handle_argument_error(command, error, args, arity) #:nodoc:
+ name = [command.ancestor_name, command.name].compact.join(" ")
+ msg = "ERROR: \"#{basename} #{name}\" was called with ".dup
+ msg << "no arguments" if args.empty?
+ msg << "arguments " << args.inspect unless args.empty?
+ msg << "\nUsage: \"#{banner(command).split("\n").join("\"\n \"")}\""
+ raise InvocationError, msg
+ end
+
+ # A flag that makes the process exit with status 1 if any error happens.
+ def exit_on_failure?
+ Bundler::Thor.deprecation_warning "Bundler::Thor exit with status 0 on errors. To keep this behavior, you must define `exit_on_failure?` in `#{self.name}`"
+ false
+ end
+
+ protected
+
+ # Prints the class options per group. If an option does not belong to
+ # any group, it's printed as Class option.
+ #
+ def class_options_help(shell, groups = {}) #:nodoc:
+ # Group options by group
+ class_options.each do |_, value|
+ groups[value.group] ||= []
+ groups[value.group] << value
+ end
+
+ # Deal with default group
+ global_options = groups.delete(nil) || []
+ print_options(shell, global_options)
+
+ # Print all others
+ groups.each do |group_name, options|
+ print_options(shell, options, group_name)
+ end
+ end
+
+ # Receives a set of options and print them.
+ def print_options(shell, options, group_name = nil)
+ return if options.empty?
+
+ list = []
+ padding = options.map { |o| o.aliases_for_usage.size }.max.to_i
+ options.each do |option|
+ next if option.hide
+ item = [option.usage(padding)]
+ item.push(option.description ? "# #{option.description}" : "")
+
+ list << item
+ list << ["", "# Default: #{option.print_default}"] if option.show_default?
+ list << ["", "# Possible values: #{option.enum_to_s}"] if option.enum
+ end
+
+ shell.say(group_name ? "#{group_name} options:" : "Options:")
+ shell.print_table(list, indent: 2)
+ shell.say ""
+ end
+
+ # Raises an error if the word given is a Bundler::Thor reserved word.
+ def is_thor_reserved_word?(word, type) #:nodoc:
+ return false unless THOR_RESERVED_WORDS.include?(word.to_s)
+ raise "#{word.inspect} is a Bundler::Thor reserved word and cannot be defined as #{type}"
+ end
+
+ # Build an option and adds it to the given scope.
+ #
+ # ==== Parameters
+ # name<Symbol>:: The name of the argument.
+ # options<Hash>:: Described in both class_option and method_option.
+ # scope<Hash>:: Options hash that is being built up
+ def build_option(name, options, scope) #:nodoc:
+ scope[name] = Bundler::Thor::Option.new(name, {check_default_type: check_default_type}.merge!(options))
+ end
+
+ # Receives a hash of options, parse them and add to the scope. This is a
+ # fast way to set a bunch of options:
+ #
+ # build_options :foo => true, :bar => :required, :baz => :string
+ #
+ # ==== Parameters
+ # Hash[Symbol => Object]
+ def build_options(options, scope) #:nodoc:
+ options.each do |key, value|
+ scope[key] = Bundler::Thor::Option.parse(key, value)
+ end
+ end
+
+ # Finds a command with the given name. If the command belongs to the current
+ # class, just return it, otherwise dup it and add the fresh copy to the
+ # current command hash.
+ def find_and_refresh_command(name) #:nodoc:
+ if commands[name.to_s]
+ commands[name.to_s]
+ elsif command = all_commands[name.to_s] # rubocop:disable Lint/AssignmentInCondition
+ commands[name.to_s] = command.clone
+ else
+ raise ArgumentError, "You supplied :for => #{name.inspect}, but the command #{name.inspect} could not be found."
+ end
+ end
+ alias_method :find_and_refresh_task, :find_and_refresh_command
+
+ # Every time someone inherits from a Bundler::Thor class, register the klass
+ # and file into baseclass.
+ def inherited(klass)
+ super(klass)
+ Bundler::Thor::Base.register_klass_file(klass)
+ klass.instance_variable_set(:@no_commands, 0)
+ end
+
+ # Fire this callback whenever a method is added. Added methods are
+ # tracked as commands by invoking the create_command method.
+ def method_added(meth)
+ super(meth)
+ meth = meth.to_s
+
+ if meth == "initialize"
+ initialize_added
+ return
+ end
+
+ # Return if it's not a public instance method
+ return unless public_method_defined?(meth.to_sym)
+
+ return if no_commands? || !create_command(meth)
+
+ is_thor_reserved_word?(meth, :command)
+ Bundler::Thor::Base.register_klass_file(self)
+ end
+
+ # Retrieves a value from superclass. If it reaches the baseclass,
+ # returns default.
+ def from_superclass(method, default = nil)
+ if self == baseclass || !superclass.respond_to?(method, true)
+ default
+ else
+ value = superclass.send(method)
+
+ # Ruby implements `dup` on Object, but raises a `TypeError`
+ # if the method is called on immediates. As a result, we
+ # don't have a good way to check whether dup will succeed
+ # without calling it and rescuing the TypeError.
+ begin
+ value.dup
+ rescue TypeError
+ value
+ end
+
+ end
+ end
+
+ #
+ # The basename of the program invoking the thor class.
+ #
+ def basename
+ File.basename($PROGRAM_NAME).split(" ").first
+ end
+
+ # SIGNATURE: Sets the baseclass. This is where the superclass lookup
+ # finishes.
+ def baseclass #:nodoc:
+ end
+
+ # SIGNATURE: Creates a new command if valid_command? is true. This method is
+ # called when a new method is added to the class.
+ def create_command(meth) #:nodoc:
+ end
+ alias_method :create_task, :create_command
+
+ # SIGNATURE: Defines behavior when the initialize method is added to the
+ # class.
+ def initialize_added #:nodoc:
+ end
+
+ # SIGNATURE: The hook invoked by start.
+ def dispatch(command, given_args, given_opts, config) #:nodoc:
+ raise NotImplementedError
+ end
+
+ # Register a relation of options for target(method_option/class_option)
+ # by args and block.
+ def register_options_relation_for(target, relation, *args, &block) # :nodoc:
+ opt = args.pop if args.last.is_a? Hash
+ opt ||= {}
+ names = args.map{ |arg| arg.to_s }
+ names += built_option_names(target, opt, &block) if block_given?
+ command_scope_member(relation, opt) << names
+ end
+
+ # Get target(method_options or class_options) options
+ # of before and after by block evaluation.
+ def built_option_names(target, opt = {}, &block) # :nodoc:
+ before = command_scope_member(target, opt).map{ |k,v| v.name }
+ instance_eval(&block)
+ after = command_scope_member(target, opt).map{ |k,v| v.name }
+ after - before
+ end
+
+ # Get command scope member by name.
+ def command_scope_member(name, options = {}) # :nodoc:
+ if options[:for]
+ find_and_refresh_command(options[:for]).send(name)
+ else
+ send(name)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/command.rb b/lib/bundler/vendor/thor/lib/thor/command.rb
new file mode 100644
index 0000000000..68c8fffedb
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/command.rb
@@ -0,0 +1,151 @@
+class Bundler::Thor
+ class Command < Struct.new(:name, :description, :long_description, :wrap_long_description, :usage, :options, :options_relation, :ancestor_name)
+ FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/
+
+ def initialize(name, description, long_description, wrap_long_description, usage, options = nil, options_relation = nil)
+ super(name.to_s, description, long_description, wrap_long_description, usage, options || {}, options_relation || {})
+ end
+
+ def initialize_copy(other) #:nodoc:
+ super(other)
+ self.options = other.options.dup if other.options
+ self.options_relation = other.options_relation.dup if other.options_relation
+ end
+
+ def hidden?
+ false
+ end
+
+ # By default, a command invokes a method in the thor class. You can change this
+ # implementation to create custom commands.
+ def run(instance, args = [])
+ arity = nil
+
+ if private_method?(instance)
+ instance.class.handle_no_command_error(name)
+ elsif public_method?(instance)
+ arity = instance.method(name).arity
+ instance.__send__(name, *args)
+ elsif local_method?(instance, :method_missing)
+ instance.__send__(:method_missing, name.to_sym, *args)
+ else
+ instance.class.handle_no_command_error(name)
+ end
+ rescue ArgumentError => e
+ handle_argument_error?(instance, e, caller) ? instance.class.handle_argument_error(self, e, args, arity) : (raise e)
+ rescue NoMethodError => e
+ handle_no_method_error?(instance, e, caller) ? instance.class.handle_no_command_error(name) : (raise e)
+ end
+
+ # Returns the formatted usage by injecting given required arguments
+ # and required options into the given usage.
+ def formatted_usage(klass, namespace = true, subcommand = false)
+ if ancestor_name
+ formatted = "#{ancestor_name} ".dup # add space
+ elsif namespace
+ namespace = klass.namespace
+ formatted = "#{namespace.gsub(/^(default)/, '')}:".dup
+ end
+ formatted ||= "#{klass.namespace.split(':').last} ".dup if subcommand
+
+ formatted ||= "".dup
+
+ Array(usage).map do |specific_usage|
+ formatted_specific_usage = formatted
+
+ formatted_specific_usage += required_arguments_for(klass, specific_usage)
+
+ # Add required options
+ formatted_specific_usage += " #{required_options}"
+
+ # Strip and go!
+ formatted_specific_usage.strip
+ end.join("\n")
+ end
+
+ def method_exclusive_option_names #:nodoc:
+ self.options_relation[:exclusive_option_names] || []
+ end
+
+ def method_at_least_one_option_names #:nodoc:
+ self.options_relation[:at_least_one_option_names] || []
+ end
+
+ protected
+
+ # Add usage with required arguments
+ def required_arguments_for(klass, usage)
+ if klass && !klass.arguments.empty?
+ usage.to_s.gsub(/^#{name}/) do |match|
+ match << " " << klass.arguments.map(&:usage).compact.join(" ")
+ end
+ else
+ usage.to_s
+ end
+ end
+
+ def not_debugging?(instance)
+ !(instance.class.respond_to?(:debugging) && instance.class.debugging)
+ end
+
+ def required_options
+ @required_options ||= options.map { |_, o| o.usage if o.required? }.compact.sort.join(" ")
+ end
+
+ # Given a target, checks if this class name is a public method.
+ def public_method?(instance) #:nodoc:
+ !(instance.public_methods & [name.to_s, name.to_sym]).empty?
+ end
+
+ def private_method?(instance)
+ !(instance.private_methods & [name.to_s, name.to_sym]).empty?
+ end
+
+ def local_method?(instance, name)
+ methods = instance.public_methods(false) + instance.private_methods(false) + instance.protected_methods(false)
+ !(methods & [name.to_s, name.to_sym]).empty?
+ end
+
+ def sans_backtrace(backtrace, caller) #:nodoc:
+ saned = backtrace.reject { |frame| frame =~ FILE_REGEXP || (frame =~ /\.java:/ && RUBY_PLATFORM =~ /java/) || (frame =~ %r{^kernel/} && RUBY_ENGINE =~ /rbx/) }
+ saned - caller
+ end
+
+ def handle_argument_error?(instance, error, caller)
+ not_debugging?(instance) && (error.message =~ /wrong number of arguments/ || error.message =~ /given \d*, expected \d*/) && begin
+ saned = sans_backtrace(error.backtrace, caller)
+ saned.empty? || saned.size == 1
+ end
+ end
+
+ def handle_no_method_error?(instance, error, caller)
+ not_debugging?(instance) &&
+ error.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/
+ end
+ end
+ Task = Command
+
+ # A command that is hidden in help messages but still invocable.
+ class HiddenCommand < Command
+ def hidden?
+ true
+ end
+ end
+ HiddenTask = HiddenCommand
+
+ # A dynamic command that handles method missing scenarios.
+ class DynamicCommand < Command
+ def initialize(name, options = nil)
+ super(name.to_s, "A dynamically-generated command", name.to_s, nil, name.to_s, options)
+ end
+
+ def run(instance, args = [])
+ if (instance.methods & [name.to_s, name.to_sym]).empty?
+ super
+ else
+ instance.class.handle_no_command_error(name)
+ end
+ end
+ end
+ DynamicTask = DynamicCommand
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb b/lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb
new file mode 100644
index 0000000000..b16a98f782
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb
@@ -0,0 +1,107 @@
+class Bundler::Thor
+ module CoreExt #:nodoc:
+ # A hash with indifferent access and magic predicates.
+ #
+ # hash = Bundler::Thor::CoreExt::HashWithIndifferentAccess.new 'foo' => 'bar', 'baz' => 'bee', 'force' => true
+ #
+ # hash[:foo] #=> 'bar'
+ # hash['foo'] #=> 'bar'
+ # hash.foo? #=> true
+ #
+ class HashWithIndifferentAccess < ::Hash #:nodoc:
+ def initialize(hash = {})
+ super()
+ hash.each do |key, value|
+ self[convert_key(key)] = value
+ end
+ end
+
+ def [](key)
+ super(convert_key(key))
+ end
+
+ def []=(key, value)
+ super(convert_key(key), value)
+ end
+
+ def delete(key)
+ super(convert_key(key))
+ end
+
+ def except(*keys)
+ dup.tap do |hash|
+ keys.each { |key| hash.delete(convert_key(key)) }
+ end
+ end
+
+ def fetch(key, *args)
+ super(convert_key(key), *args)
+ end
+
+ def slice(*keys)
+ super(*keys.map{ |key| convert_key(key) })
+ end
+
+ def key?(key)
+ super(convert_key(key))
+ end
+
+ def values_at(*indices)
+ indices.map { |key| self[convert_key(key)] }
+ end
+
+ def merge(other)
+ dup.merge!(other)
+ end
+
+ def merge!(other)
+ other.each do |key, value|
+ self[convert_key(key)] = value
+ end
+ self
+ end
+
+ def reverse_merge(other)
+ self.class.new(other).merge(self)
+ end
+
+ def reverse_merge!(other_hash)
+ replace(reverse_merge(other_hash))
+ end
+
+ def replace(other_hash)
+ super(other_hash)
+ end
+
+ # Convert to a Hash with String keys.
+ def to_hash
+ Hash.new(default).merge!(self)
+ end
+
+ protected
+
+ def convert_key(key)
+ key.is_a?(Symbol) ? key.to_s : key
+ end
+
+ # Magic predicates. For instance:
+ #
+ # options.force? # => !!options['force']
+ # options.shebang # => "/usr/lib/local/ruby"
+ # options.test_framework?(:rspec) # => options[:test_framework] == :rspec
+ #
+ def method_missing(method, *args)
+ method = method.to_s
+ if method =~ /^(\w+)\?$/
+ if args.empty?
+ !!self[$1]
+ else
+ self[$1] == args.first
+ end
+ else
+ self[method]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/error.rb b/lib/bundler/vendor/thor/lib/thor/error.rb
new file mode 100644
index 0000000000..928646e501
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/error.rb
@@ -0,0 +1,106 @@
+class Bundler::Thor
+ Correctable = if defined?(DidYouMean::SpellChecker) && defined?(DidYouMean::Correctable) # rubocop:disable Naming/ConstantName
+ Module.new do
+ def to_s
+ super + DidYouMean.formatter.message_for(corrections)
+ end
+
+ def corrections
+ @corrections ||= self.class.const_get(:SpellChecker).new(self).corrections
+ end
+ end
+ end
+
+ # Bundler::Thor::Error is raised when it's caused by wrong usage of thor classes. Those
+ # errors have their backtrace suppressed and are nicely shown to the user.
+ #
+ # Errors that are caused by the developer, like declaring a method which
+ # overwrites a thor keyword, SHOULD NOT raise a Bundler::Thor::Error. This way, we
+ # ensure that developer errors are shown with full backtrace.
+ class Error < StandardError
+ end
+
+ # Raised when a command was not found.
+ class UndefinedCommandError < Error
+ class SpellChecker
+ attr_reader :error
+
+ def initialize(error)
+ @error = error
+ end
+
+ def corrections
+ @corrections ||= spell_checker.correct(error.command).map(&:inspect)
+ end
+
+ def spell_checker
+ DidYouMean::SpellChecker.new(dictionary: error.all_commands)
+ end
+ end
+
+ attr_reader :command, :all_commands
+
+ def initialize(command, all_commands, namespace)
+ @command = command
+ @all_commands = all_commands
+
+ message = "Could not find command #{command.inspect}"
+ message = namespace ? "#{message} in #{namespace.inspect} namespace." : "#{message}."
+
+ super(message)
+ end
+
+ prepend Correctable if Correctable
+ end
+ UndefinedTaskError = UndefinedCommandError
+
+ class AmbiguousCommandError < Error
+ end
+ AmbiguousTaskError = AmbiguousCommandError
+
+ # Raised when a command was found, but not invoked properly.
+ class InvocationError < Error
+ end
+
+ class UnknownArgumentError < Error
+ class SpellChecker
+ attr_reader :error
+
+ def initialize(error)
+ @error = error
+ end
+
+ def corrections
+ @corrections ||=
+ error.unknown.flat_map { |unknown| spell_checker.correct(unknown) }.uniq.map(&:inspect)
+ end
+
+ def spell_checker
+ @spell_checker ||= DidYouMean::SpellChecker.new(dictionary: error.switches)
+ end
+ end
+
+ attr_reader :switches, :unknown
+
+ def initialize(switches, unknown)
+ @switches = switches
+ @unknown = unknown
+
+ super("Unknown switches #{unknown.map(&:inspect).join(', ')}")
+ end
+
+ prepend Correctable if Correctable
+ end
+
+ class RequiredArgumentMissingError < InvocationError
+ end
+
+ class MalformattedArgumentError < InvocationError
+ end
+
+ class ExclusiveArgumentError < InvocationError
+ end
+
+ class AtLeastOneRequiredArgumentError < InvocationError
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/group.rb b/lib/bundler/vendor/thor/lib/thor/group.rb
new file mode 100644
index 0000000000..30bc311294
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/group.rb
@@ -0,0 +1,292 @@
+require_relative "base"
+
+# Bundler::Thor has a special class called Bundler::Thor::Group. The main difference to Bundler::Thor class
+# is that it invokes all commands at once. It also include some methods that allows
+# invocations to be done at the class method, which are not available to Bundler::Thor
+# commands.
+class Bundler::Thor::Group
+ class << self
+ # The description for this Bundler::Thor::Group. If none is provided, but a source root
+ # exists, tries to find the USAGE one folder above it, otherwise searches
+ # in the superclass.
+ #
+ # ==== Parameters
+ # description<String>:: The description for this Bundler::Thor::Group.
+ #
+ def desc(description = nil)
+ if description
+ @desc = description
+ else
+ @desc ||= from_superclass(:desc, nil)
+ end
+ end
+
+ # Prints help information.
+ #
+ # ==== Options
+ # short:: When true, shows only usage.
+ #
+ def help(shell)
+ shell.say "Usage:"
+ shell.say " #{banner}\n"
+ shell.say
+ class_options_help(shell)
+ shell.say desc if desc
+ end
+
+ # Stores invocations for this class merging with superclass values.
+ #
+ def invocations #:nodoc:
+ @invocations ||= from_superclass(:invocations, {})
+ end
+
+ # Stores invocation blocks used on invoke_from_option.
+ #
+ def invocation_blocks #:nodoc:
+ @invocation_blocks ||= from_superclass(:invocation_blocks, {})
+ end
+
+ # Invoke the given namespace or class given. It adds an instance
+ # method that will invoke the klass and command. You can give a block to
+ # configure how it will be invoked.
+ #
+ # The namespace/class given will have its options showed on the help
+ # usage. Check invoke_from_option for more information.
+ #
+ def invoke(*names, &block)
+ options = names.last.is_a?(Hash) ? names.pop : {}
+ verbose = options.fetch(:verbose, true)
+
+ names.each do |name|
+ invocations[name] = false
+ invocation_blocks[name] = block if block_given?
+
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
+ def _invoke_#{name.to_s.gsub(/\W/, '_')}
+ klass, command = self.class.prepare_for_invocation(nil, #{name.inspect})
+
+ if klass
+ say_status :invoke, #{name.inspect}, #{verbose.inspect}
+ block = self.class.invocation_blocks[#{name.inspect}]
+ _invoke_for_class_method klass, command, &block
+ else
+ say_status :error, %(#{name.inspect} [not found]), :red
+ end
+ end
+ METHOD
+ end
+ end
+
+ # Invoke a thor class based on the value supplied by the user to the
+ # given option named "name". A class option must be created before this
+ # method is invoked for each name given.
+ #
+ # ==== Examples
+ #
+ # class GemGenerator < Bundler::Thor::Group
+ # class_option :test_framework, :type => :string
+ # invoke_from_option :test_framework
+ # end
+ #
+ # ==== Boolean options
+ #
+ # In some cases, you want to invoke a thor class if some option is true or
+ # false. This is automatically handled by invoke_from_option. Then the
+ # option name is used to invoke the generator.
+ #
+ # ==== Preparing for invocation
+ #
+ # In some cases you want to customize how a specified hook is going to be
+ # invoked. You can do that by overwriting the class method
+ # prepare_for_invocation. The class method must necessarily return a klass
+ # and an optional command.
+ #
+ # ==== Custom invocations
+ #
+ # You can also supply a block to customize how the option is going to be
+ # invoked. The block receives two parameters, an instance of the current
+ # class and the klass to be invoked.
+ #
+ def invoke_from_option(*names, &block)
+ options = names.last.is_a?(Hash) ? names.pop : {}
+ verbose = options.fetch(:verbose, :white)
+
+ names.each do |name|
+ unless class_options.key?(name)
+ raise ArgumentError, "You have to define the option #{name.inspect} " \
+ "before setting invoke_from_option."
+ end
+
+ invocations[name] = true
+ invocation_blocks[name] = block if block_given?
+
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
+ def _invoke_from_option_#{name.to_s.gsub(/\W/, '_')}
+ return unless options[#{name.inspect}]
+
+ value = options[#{name.inspect}]
+ value = #{name.inspect} if TrueClass === value
+ klass, command = self.class.prepare_for_invocation(#{name.inspect}, value)
+
+ if klass
+ say_status :invoke, value, #{verbose.inspect}
+ block = self.class.invocation_blocks[#{name.inspect}]
+ _invoke_for_class_method klass, command, &block
+ else
+ say_status :error, %(\#{value} [not found]), :red
+ end
+ end
+ METHOD
+ end
+ end
+
+ # Remove a previously added invocation.
+ #
+ # ==== Examples
+ #
+ # remove_invocation :test_framework
+ #
+ def remove_invocation(*names)
+ names.each do |name|
+ remove_command(name)
+ remove_class_option(name)
+ invocations.delete(name)
+ invocation_blocks.delete(name)
+ end
+ end
+
+ # Overwrite class options help to allow invoked generators options to be
+ # shown recursively when invoking a generator.
+ #
+ def class_options_help(shell, groups = {}) #:nodoc:
+ get_options_from_invocations(groups, class_options) do |klass|
+ klass.send(:get_options_from_invocations, groups, class_options)
+ end
+ super(shell, groups)
+ end
+
+ # Get invocations array and merge options from invocations. Those
+ # options are added to group_options hash. Options that already exists
+ # in base_options are not added twice.
+ #
+ def get_options_from_invocations(group_options, base_options) #:nodoc:
+ invocations.each do |name, from_option|
+ value = if from_option
+ option = class_options[name]
+ option.type == :boolean ? name : option.default
+ else
+ name
+ end
+ next unless value
+
+ klass, _ = prepare_for_invocation(name, value)
+ next unless klass && klass.respond_to?(:class_options)
+
+ value = value.to_s
+ human_name = value.respond_to?(:classify) ? value.classify : value
+
+ group_options[human_name] ||= []
+ group_options[human_name] += klass.class_options.values.select do |class_option|
+ base_options[class_option.name.to_sym].nil? && class_option.group.nil? &&
+ !group_options.values.flatten.any? { |i| i.name == class_option.name }
+ end
+
+ yield klass if block_given?
+ end
+ end
+
+ # Returns commands ready to be printed.
+ def printable_commands(*)
+ item = []
+ item << banner
+ item << (desc ? "# #{desc.gsub(/\s+/m, ' ')}" : "")
+ [item]
+ end
+ alias_method :printable_tasks, :printable_commands
+
+ def handle_argument_error(command, error, _args, arity) #:nodoc:
+ msg = "#{basename} #{command.name} takes #{arity} argument".dup
+ msg << "s" if arity > 1
+ msg << ", but it should not."
+ raise error, msg
+ end
+
+ # Checks if a specified command exists.
+ #
+ # ==== Parameters
+ # command_name<String>:: The name of the command to check for existence.
+ #
+ # ==== Returns
+ # Boolean:: +true+ if the command exists, +false+ otherwise.
+ def command_exists?(command_name) #:nodoc:
+ commands.keys.include?(command_name)
+ end
+
+ protected
+
+ # The method responsible for dispatching given the args.
+ def dispatch(command, given_args, given_opts, config) #:nodoc:
+ if Bundler::Thor::HELP_MAPPINGS.include?(given_args.first)
+ help(config[:shell])
+ return
+ end
+
+ args, opts = Bundler::Thor::Options.split(given_args)
+ opts = given_opts || opts
+
+ instance = new(args, opts, config)
+ yield instance if block_given?
+
+ if command
+ instance.invoke_command(all_commands[command])
+ else
+ instance.invoke_all
+ end
+ end
+
+ # The banner for this class. You can customize it if you are invoking the
+ # thor class by another ways which is not the Bundler::Thor::Runner.
+ def banner
+ "#{basename} #{self_command.formatted_usage(self, false)}"
+ end
+
+ # Represents the whole class as a command.
+ def self_command #:nodoc:
+ Bundler::Thor::DynamicCommand.new(namespace, class_options)
+ end
+ alias_method :self_task, :self_command
+
+ def baseclass #:nodoc:
+ Bundler::Thor::Group
+ end
+
+ def create_command(meth) #:nodoc:
+ commands[meth.to_s] = Bundler::Thor::Command.new(meth, nil, nil, nil, nil)
+ true
+ end
+ alias_method :create_task, :create_command
+ end
+
+ include Bundler::Thor::Base
+
+protected
+
+ # Shortcut to invoke with padding and block handling. Use internally by
+ # invoke and invoke_from_option class methods.
+ def _invoke_for_class_method(klass, command = nil, *args, &block) #:nodoc:
+ with_padding do
+ if block
+ case block.arity
+ when 3
+ yield(self, klass, command)
+ when 2
+ yield(self, klass)
+ when 1
+ instance_exec(klass, &block)
+ end
+ else
+ invoke klass, command, *args
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/invocation.rb b/lib/bundler/vendor/thor/lib/thor/invocation.rb
new file mode 100644
index 0000000000..5ce74710ba
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/invocation.rb
@@ -0,0 +1,178 @@
+class Bundler::Thor
+ module Invocation
+ def self.included(base) #:nodoc:
+ super(base)
+ base.extend ClassMethods
+ end
+
+ module ClassMethods
+ # This method is responsible for receiving a name and find the proper
+ # class and command for it. The key is an optional parameter which is
+ # available only in class methods invocations (i.e. in Bundler::Thor::Group).
+ def prepare_for_invocation(key, name) #:nodoc:
+ case name
+ when Symbol, String
+ Bundler::Thor::Util.find_class_and_command_by_namespace(name.to_s, !key)
+ else
+ name
+ end
+ end
+ end
+
+ # Make initializer aware of invocations and the initialization args.
+ def initialize(args = [], options = {}, config = {}, &block) #:nodoc:
+ @_invocations = config[:invocations] || Hash.new { |h, k| h[k] = [] }
+ @_initializer = [args, options, config]
+ super
+ end
+
+ # Make the current command chain accessible with in a Bundler::Thor-(sub)command
+ def current_command_chain
+ @_invocations.values.flatten.map(&:to_sym)
+ end
+
+ # Receives a name and invokes it. The name can be a string (either "command" or
+ # "namespace:command"), a Bundler::Thor::Command, a Class or a Bundler::Thor instance. If the
+ # command cannot be guessed by name, it can also be supplied as second argument.
+ #
+ # You can also supply the arguments, options and configuration values for
+ # the command to be invoked, if none is given, the same values used to
+ # initialize the invoker are used to initialize the invoked.
+ #
+ # When no name is given, it will invoke the default command of the current class.
+ #
+ # ==== Examples
+ #
+ # class A < Bundler::Thor
+ # def foo
+ # invoke :bar
+ # invoke "b:hello", ["Erik"]
+ # end
+ #
+ # def bar
+ # invoke "b:hello", ["Erik"]
+ # end
+ # end
+ #
+ # class B < Bundler::Thor
+ # def hello(name)
+ # puts "hello #{name}"
+ # end
+ # end
+ #
+ # You can notice that the method "foo" above invokes two commands: "bar",
+ # which belongs to the same class and "hello" which belongs to the class B.
+ #
+ # By using an invocation system you ensure that a command is invoked only once.
+ # In the example above, invoking "foo" will invoke "b:hello" just once, even
+ # if it's invoked later by "bar" method.
+ #
+ # When class A invokes class B, all arguments used on A initialization are
+ # supplied to B. This allows lazy parse of options. Let's suppose you have
+ # some rspec commands:
+ #
+ # class Rspec < Bundler::Thor::Group
+ # class_option :mock_framework, :type => :string, :default => :rr
+ #
+ # def invoke_mock_framework
+ # invoke "rspec:#{options[:mock_framework]}"
+ # end
+ # end
+ #
+ # As you noticed, it invokes the given mock framework, which might have its
+ # own options:
+ #
+ # class Rspec::RR < Bundler::Thor::Group
+ # class_option :style, :type => :string, :default => :mock
+ # end
+ #
+ # Since it's not rspec concern to parse mock framework options, when RR
+ # is invoked all options are parsed again, so RR can extract only the options
+ # that it's going to use.
+ #
+ # If you want Rspec::RR to be initialized with its own set of options, you
+ # have to do that explicitly:
+ #
+ # invoke "rspec:rr", [], :style => :foo
+ #
+ # Besides giving an instance, you can also give a class to invoke:
+ #
+ # invoke Rspec::RR, [], :style => :foo
+ #
+ def invoke(name = nil, *args)
+ if name.nil?
+ warn "[Bundler::Thor] Calling invoke() without argument is deprecated. Please use invoke_all instead.\n#{caller.join("\n")}"
+ return invoke_all
+ end
+
+ args.unshift(nil) if args.first.is_a?(Array) || args.first.nil?
+ command, args, opts, config = args
+
+ klass, command = _retrieve_class_and_command(name, command)
+ raise "Missing Bundler::Thor class for invoke #{name}" unless klass
+ raise "Expected Bundler::Thor class, got #{klass}" unless klass <= Bundler::Thor::Base
+
+ args, opts, config = _parse_initialization_options(args, opts, config)
+ klass.send(:dispatch, command, args, opts, config) do |instance|
+ instance.parent_options = options
+ end
+ end
+
+ # Invoke the given command if the given args.
+ def invoke_command(command, *args) #:nodoc:
+ current = @_invocations[self.class]
+
+ unless current.include?(command.name)
+ current << command.name
+ command.run(self, *args)
+ end
+ end
+ alias_method :invoke_task, :invoke_command
+
+ # Invoke all commands for the current instance.
+ def invoke_all #:nodoc:
+ self.class.all_commands.map { |_, command| invoke_command(command) }
+ end
+
+ # Invokes using shell padding.
+ def invoke_with_padding(*args)
+ with_padding { invoke(*args) }
+ end
+
+ protected
+
+ # Configuration values that are shared between invocations.
+ def _shared_configuration #:nodoc:
+ {invocations: @_invocations}
+ end
+
+ # This method simply retrieves the class and command to be invoked.
+ # If the name is nil or the given name is a command in the current class,
+ # use the given name and return self as class. Otherwise, call
+ # prepare_for_invocation in the current class.
+ def _retrieve_class_and_command(name, sent_command = nil) #:nodoc:
+ if name.nil?
+ [self.class, nil]
+ elsif self.class.all_commands[name.to_s]
+ [self.class, name.to_s]
+ else
+ klass, command = self.class.prepare_for_invocation(nil, name)
+ [klass, command || sent_command]
+ end
+ end
+ alias_method :_retrieve_class_and_task, :_retrieve_class_and_command
+
+ # Initialize klass using values stored in the @_initializer.
+ def _parse_initialization_options(args, opts, config) #:nodoc:
+ stored_args, stored_opts, stored_config = @_initializer
+
+ args ||= stored_args.dup
+ opts ||= stored_opts.dup
+
+ config ||= {}
+ config = stored_config.merge(_shared_configuration).merge!(config)
+
+ [args, opts, config]
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/line_editor.rb b/lib/bundler/vendor/thor/lib/thor/line_editor.rb
new file mode 100644
index 0000000000..5c0c336e7a
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/line_editor.rb
@@ -0,0 +1,17 @@
+require_relative "line_editor/basic"
+require_relative "line_editor/readline"
+
+class Bundler::Thor
+ module LineEditor
+ def self.readline(prompt, options = {})
+ best_available.new(prompt, options).readline
+ end
+
+ def self.best_available
+ [
+ Bundler::Thor::LineEditor::Readline,
+ Bundler::Thor::LineEditor::Basic
+ ].detect(&:available?)
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/line_editor/basic.rb b/lib/bundler/vendor/thor/lib/thor/line_editor/basic.rb
new file mode 100644
index 0000000000..fe3d7c998f
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/line_editor/basic.rb
@@ -0,0 +1,37 @@
+class Bundler::Thor
+ module LineEditor
+ class Basic
+ attr_reader :prompt, :options
+
+ def self.available?
+ true
+ end
+
+ def initialize(prompt, options)
+ @prompt = prompt
+ @options = options
+ end
+
+ def readline
+ $stdout.print(prompt)
+ get_input
+ end
+
+ private
+
+ def get_input
+ if echo?
+ $stdin.gets
+ else
+ # Lazy-load io/console since it is gem-ified as of 2.3
+ require "io/console"
+ $stdin.noecho(&:gets)
+ end
+ end
+
+ def echo?
+ options.fetch(:echo, true)
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/line_editor/readline.rb b/lib/bundler/vendor/thor/lib/thor/line_editor/readline.rb
new file mode 100644
index 0000000000..120eadd06a
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/line_editor/readline.rb
@@ -0,0 +1,88 @@
+class Bundler::Thor
+ module LineEditor
+ class Readline < Basic
+ def self.available?
+ begin
+ require "readline"
+ rescue LoadError
+ end
+
+ Object.const_defined?(:Readline)
+ end
+
+ def readline
+ if echo?
+ ::Readline.completion_append_character = nil
+ # rb-readline does not allow Readline.completion_proc= to receive nil.
+ if complete = completion_proc
+ ::Readline.completion_proc = complete
+ end
+ ::Readline.readline(prompt, add_to_history?)
+ else
+ super
+ end
+ end
+
+ private
+
+ def add_to_history?
+ options.fetch(:add_to_history, true)
+ end
+
+ def completion_proc
+ if use_path_completion?
+ proc { |text| PathCompletion.new(text).matches }
+ elsif completion_options.any?
+ proc do |text|
+ completion_options.select { |option| option.start_with?(text) }
+ end
+ end
+ end
+
+ def completion_options
+ options.fetch(:limited_to, [])
+ end
+
+ def use_path_completion?
+ options.fetch(:path, false)
+ end
+
+ class PathCompletion
+ attr_reader :text
+ private :text
+
+ def initialize(text)
+ @text = text
+ end
+
+ def matches
+ relative_matches
+ end
+
+ private
+
+ def relative_matches
+ absolute_matches.map { |path| path.sub(base_path, "") }
+ end
+
+ def absolute_matches
+ Dir[glob_pattern].map do |path|
+ if File.directory?(path)
+ "#{path}/"
+ else
+ path
+ end
+ end
+ end
+
+ def glob_pattern
+ "#{base_path}#{text}*"
+ end
+
+ def base_path
+ "#{Dir.pwd}/"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/nested_context.rb b/lib/bundler/vendor/thor/lib/thor/nested_context.rb
new file mode 100644
index 0000000000..7d60cb1c12
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/nested_context.rb
@@ -0,0 +1,29 @@
+class Bundler::Thor
+ class NestedContext
+ def initialize
+ @depth = 0
+ end
+
+ def enter
+ push
+
+ yield
+ ensure
+ pop
+ end
+
+ def entered?
+ @depth.positive?
+ end
+
+ private
+
+ def push
+ @depth += 1
+ end
+
+ def pop
+ @depth -= 1
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/parser.rb b/lib/bundler/vendor/thor/lib/thor/parser.rb
new file mode 100644
index 0000000000..45394732ca
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/parser.rb
@@ -0,0 +1,4 @@
+require_relative "parser/argument"
+require_relative "parser/arguments"
+require_relative "parser/option"
+require_relative "parser/options"
diff --git a/lib/bundler/vendor/thor/lib/thor/parser/argument.rb b/lib/bundler/vendor/thor/lib/thor/parser/argument.rb
new file mode 100644
index 0000000000..ee9db4ad8a
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/parser/argument.rb
@@ -0,0 +1,86 @@
+class Bundler::Thor
+ class Argument #:nodoc:
+ VALID_TYPES = [:numeric, :hash, :array, :string]
+
+ attr_reader :name, :description, :enum, :required, :type, :default, :banner
+ alias_method :human_name, :name
+
+ def initialize(name, options = {})
+ class_name = self.class.name.split("::").last
+
+ type = options[:type]
+
+ raise ArgumentError, "#{class_name} name can't be nil." if name.nil?
+ raise ArgumentError, "Type :#{type} is not valid for #{class_name.downcase}s." if type && !valid_type?(type)
+
+ @name = name.to_s
+ @description = options[:desc]
+ @required = options.key?(:required) ? options[:required] : true
+ @type = (type || :string).to_sym
+ @default = options[:default]
+ @banner = options[:banner] || default_banner
+ @enum = options[:enum]
+
+ validate! # Trigger specific validations
+ end
+
+ def print_default
+ if @type == :array and @default.is_a?(Array)
+ @default.map(&:dump).join(" ")
+ else
+ @default
+ end
+ end
+
+ def usage
+ required? ? banner : "[#{banner}]"
+ end
+
+ def required?
+ required
+ end
+
+ def show_default?
+ case default
+ when Array, String, Hash
+ !default.empty?
+ else
+ default
+ end
+ end
+
+ def enum_to_s
+ if enum.respond_to? :join
+ enum.join(", ")
+ else
+ "#{enum.first}..#{enum.last}"
+ end
+ end
+
+ protected
+
+ def validate!
+ raise ArgumentError, "An argument cannot be required and have default value." if required? && !default.nil?
+ raise ArgumentError, "An argument cannot have an enum other than an enumerable." if @enum && !@enum.is_a?(Enumerable)
+ end
+
+ def valid_type?(type)
+ self.class::VALID_TYPES.include?(type.to_sym)
+ end
+
+ def default_banner
+ case type
+ when :boolean
+ nil
+ when :string, :default
+ human_name.upcase
+ when :numeric
+ "N"
+ when :hash
+ "key:value"
+ when :array
+ "one two three"
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/parser/arguments.rb b/lib/bundler/vendor/thor/lib/thor/parser/arguments.rb
new file mode 100644
index 0000000000..b6f9c9a37a
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/parser/arguments.rb
@@ -0,0 +1,195 @@
+class Bundler::Thor
+ class Arguments #:nodoc:
+ NUMERIC = /[-+]?(\d*\.\d+|\d+)/
+
+ # Receives an array of args and returns two arrays, one with arguments
+ # and one with switches.
+ #
+ def self.split(args)
+ arguments = []
+
+ args.each do |item|
+ break if item.is_a?(String) && item =~ /^-/
+ arguments << item
+ end
+
+ [arguments, args[Range.new(arguments.size, -1)]]
+ end
+
+ def self.parse(*args)
+ to_parse = args.pop
+ new(*args).parse(to_parse)
+ end
+
+ # Takes an array of Bundler::Thor::Argument objects.
+ #
+ def initialize(arguments = [])
+ @assigns = {}
+ @non_assigned_required = []
+ @switches = arguments
+
+ arguments.each do |argument|
+ if !argument.default.nil?
+ @assigns[argument.human_name] = argument.default.dup
+ elsif argument.required?
+ @non_assigned_required << argument
+ end
+ end
+ end
+
+ def parse(args)
+ @pile = args.dup
+
+ @switches.each do |argument|
+ break unless peek
+ @non_assigned_required.delete(argument)
+ @assigns[argument.human_name] = send(:"parse_#{argument.type}", argument.human_name)
+ end
+
+ check_requirement!
+ @assigns
+ end
+
+ def remaining
+ @pile
+ end
+
+ private
+
+ def no_or_skip?(arg)
+ arg =~ /^--(no|skip)-([-\w]+)$/
+ $2
+ end
+
+ def last?
+ @pile.empty?
+ end
+
+ def peek
+ @pile.first
+ end
+
+ def shift
+ @pile.shift
+ end
+
+ def unshift(arg)
+ if arg.is_a?(Array)
+ @pile = arg + @pile
+ else
+ @pile.unshift(arg)
+ end
+ end
+
+ def current_is_value?
+ peek && peek.to_s !~ /^-{1,2}\S+/
+ end
+
+ # Runs through the argument array getting strings that contains ":" and
+ # mark it as a hash:
+ #
+ # [ "name:string", "age:integer" ]
+ #
+ # Becomes:
+ #
+ # { "name" => "string", "age" => "integer" }
+ #
+ def parse_hash(name)
+ return shift if peek.is_a?(Hash)
+ hash = {}
+
+ while current_is_value? && peek.include?(":")
+ key, value = shift.split(":", 2)
+ raise MalformattedArgumentError, "You can't specify '#{key}' more than once in option '#{name}'; got #{key}:#{hash[key]} and #{key}:#{value}" if hash.include? key
+ hash[key] = value
+ end
+ hash
+ end
+
+ # Runs through the argument array getting all strings until no string is
+ # found or a switch is found.
+ #
+ # ["a", "b", "c"]
+ #
+ # And returns it as an array:
+ #
+ # ["a", "b", "c"]
+ #
+ def parse_array(name)
+ return shift if peek.is_a?(Array)
+
+ array = []
+
+ while current_is_value?
+ value = shift
+
+ if !value.empty?
+ validate_enum_value!(name, value, "Expected all values of '%s' to be one of %s; got %s")
+ end
+
+ array << value
+ end
+ array
+ end
+
+ # Check if the peek is numeric format and return a Float or Integer.
+ # Check if the peek is included in enum if enum is provided.
+ # Otherwise raises an error.
+ #
+ def parse_numeric(name)
+ return shift if peek.is_a?(Numeric)
+
+ unless peek =~ NUMERIC && $& == peek
+ raise MalformattedArgumentError, "Expected numeric value for '#{name}'; got #{peek.inspect}"
+ end
+
+ value = $&.index(".") ? shift.to_f : shift.to_i
+
+ validate_enum_value!(name, value, "Expected '%s' to be one of %s; got %s")
+
+ value
+ end
+
+ # Parse string:
+ # for --string-arg, just return the current value in the pile
+ # for --no-string-arg, nil
+ # Check if the peek is included in enum if enum is provided. Otherwise raises an error.
+ #
+ def parse_string(name)
+ if no_or_skip?(name)
+ nil
+ else
+ value = shift
+
+ validate_enum_value!(name, value, "Expected '%s' to be one of %s; got %s")
+
+ value
+ end
+ end
+
+ # Raises an error if the switch is an enum and the values aren't included on it.
+ #
+ def validate_enum_value!(name, value, message)
+ return unless @switches.is_a?(Hash)
+
+ switch = @switches[name]
+
+ return unless switch
+
+ if switch.enum && !switch.enum.include?(value)
+ raise MalformattedArgumentError, message % [name, switch.enum_to_s, value]
+ end
+ end
+
+ # Raises an error if @non_assigned_required array is not empty.
+ #
+ def check_requirement!
+ return if @non_assigned_required.empty?
+ names = @non_assigned_required.map do |o|
+ o.respond_to?(:switch_name) ? o.switch_name : o.human_name
+ end.join("', '")
+ class_name = self.class.name.split("::").last.downcase
+ raise RequiredArgumentMissingError, "No value provided for required #{class_name} '#{names}'"
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/parser/option.rb b/lib/bundler/vendor/thor/lib/thor/parser/option.rb
new file mode 100644
index 0000000000..72617c7e34
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/parser/option.rb
@@ -0,0 +1,178 @@
+class Bundler::Thor
+ class Option < Argument #:nodoc:
+ attr_reader :aliases, :group, :lazy_default, :hide, :repeatable
+
+ VALID_TYPES = [:boolean, :numeric, :hash, :array, :string]
+
+ def initialize(name, options = {})
+ @check_default_type = options[:check_default_type]
+ options[:required] = false unless options.key?(:required)
+ @repeatable = options.fetch(:repeatable, false)
+ super
+ @lazy_default = options[:lazy_default]
+ @group = options[:group].to_s.capitalize if options[:group]
+ @aliases = normalize_aliases(options[:aliases])
+ @hide = options[:hide]
+ end
+
+ # This parse quick options given as method_options. It makes several
+ # assumptions, but you can be more specific using the option method.
+ #
+ # parse :foo => "bar"
+ # #=> Option foo with default value bar
+ #
+ # parse [:foo, :baz] => "bar"
+ # #=> Option foo with default value bar and alias :baz
+ #
+ # parse :foo => :required
+ # #=> Required option foo without default value
+ #
+ # parse :foo => 2
+ # #=> Option foo with default value 2 and type numeric
+ #
+ # parse :foo => :numeric
+ # #=> Option foo without default value and type numeric
+ #
+ # parse :foo => true
+ # #=> Option foo with default value true and type boolean
+ #
+ # The valid types are :boolean, :numeric, :hash, :array and :string. If none
+ # is given a default type is assumed. This default type accepts arguments as
+ # string (--foo=value) or booleans (just --foo).
+ #
+ # By default all options are optional, unless :required is given.
+ #
+ def self.parse(key, value)
+ if key.is_a?(Array)
+ name, *aliases = key
+ else
+ name = key
+ aliases = []
+ end
+
+ name = name.to_s
+ default = value
+
+ type = case value
+ when Symbol
+ default = nil
+ if VALID_TYPES.include?(value)
+ value
+ elsif required = (value == :required) # rubocop:disable Lint/AssignmentInCondition
+ :string
+ end
+ when TrueClass, FalseClass
+ :boolean
+ when Numeric
+ :numeric
+ when Hash, Array, String
+ value.class.name.downcase.to_sym
+ end
+
+ new(name.to_s, required: required, type: type, default: default, aliases: aliases)
+ end
+
+ def switch_name
+ @switch_name ||= dasherized? ? name : dasherize(name)
+ end
+
+ def human_name
+ @human_name ||= dasherized? ? undasherize(name) : name
+ end
+
+ def usage(padding = 0)
+ sample = if banner && !banner.to_s.empty?
+ "#{switch_name}=#{banner}".dup
+ else
+ switch_name
+ end
+
+ sample = "[#{sample}]".dup unless required?
+
+ if boolean? && name != "force" && !name.match(/\A(no|skip)[\-_]/)
+ sample << ", [#{dasherize('no-' + human_name)}], [#{dasherize('skip-' + human_name)}]"
+ end
+
+ aliases_for_usage.ljust(padding) + sample
+ end
+
+ def aliases_for_usage
+ if aliases.empty?
+ ""
+ else
+ "#{aliases.join(', ')}, "
+ end
+ end
+
+ def show_default?
+ case default
+ when TrueClass, FalseClass
+ true
+ else
+ super
+ end
+ end
+
+ VALID_TYPES.each do |type|
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{type}?
+ self.type == #{type.inspect}
+ end
+ RUBY
+ end
+
+ protected
+
+ def validate!
+ raise ArgumentError, "An option cannot be boolean and required." if boolean? && required?
+ validate_default_type!
+ end
+
+ def validate_default_type!
+ default_type = case @default
+ when nil
+ return
+ when TrueClass, FalseClass
+ required? ? :string : :boolean
+ when Numeric
+ :numeric
+ when Symbol
+ :string
+ when Hash, Array, String
+ @default.class.name.downcase.to_sym
+ end
+
+ expected_type = (@repeatable && @type != :hash) ? :array : @type
+
+ if default_type != expected_type
+ err = "Expected #{expected_type} default value for '#{switch_name}'; got #{@default.inspect} (#{default_type})"
+
+ if @check_default_type
+ raise ArgumentError, err
+ elsif @check_default_type == nil
+ Bundler::Thor.deprecation_warning "#{err}.\n" +
+ "This will be rejected in the future unless you explicitly pass the options `check_default_type: false`" +
+ " or call `allow_incompatible_default_type!` in your code"
+ end
+ end
+ end
+
+ def dasherized?
+ name.index("-") == 0
+ end
+
+ def undasherize(str)
+ str.sub(/^-{1,2}/, "")
+ end
+
+ def dasherize(str)
+ (str.length > 1 ? "--" : "-") + str.tr("_", "-")
+ end
+
+ private
+
+ def normalize_aliases(aliases)
+ Array(aliases).map { |short| short.to_s.sub(/^(?!\-)/, "-") }
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/parser/options.rb b/lib/bundler/vendor/thor/lib/thor/parser/options.rb
new file mode 100644
index 0000000000..fe22d989e5
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/parser/options.rb
@@ -0,0 +1,294 @@
+class Bundler::Thor
+ class Options < Arguments #:nodoc:
+ LONG_RE = /^(--\w+(?:-\w+)*)$/
+ SHORT_RE = /^(-[a-z])$/i
+ EQ_RE = /^(--\w+(?:-\w+)*|-[a-z])=(.*)$/i
+ SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args
+ SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i
+ OPTS_END = "--".freeze
+
+ # Receives a hash and makes it switches.
+ def self.to_switches(options)
+ options.map do |key, value|
+ case value
+ when true
+ "--#{key}"
+ when Array
+ "--#{key} #{value.map(&:inspect).join(' ')}"
+ when Hash
+ "--#{key} #{value.map { |k, v| "#{k}:#{v}" }.join(' ')}"
+ when nil, false
+ nil
+ else
+ "--#{key} #{value.inspect}"
+ end
+ end.compact.join(" ")
+ end
+
+ # Takes a hash of Bundler::Thor::Option and a hash with defaults.
+ #
+ # If +stop_on_unknown+ is true, #parse will stop as soon as it encounters
+ # an unknown option or a regular argument.
+ def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false, disable_required_check = false, relations = {})
+ @stop_on_unknown = stop_on_unknown
+ @exclusives = (relations[:exclusive_option_names] || []).select{|array| !array.empty?}
+ @at_least_ones = (relations[:at_least_one_option_names] || []).select{|array| !array.empty?}
+ @disable_required_check = disable_required_check
+ options = hash_options.values
+ super(options)
+
+ # Add defaults
+ defaults.each do |key, value|
+ @assigns[key.to_s] = value
+ @non_assigned_required.delete(hash_options[key])
+ end
+
+ @shorts = {}
+ @switches = {}
+ @extra = []
+ @stopped_parsing_after_extra_index = nil
+ @is_treated_as_value = false
+
+ options.each do |option|
+ @switches[option.switch_name] = option
+
+ option.aliases.each do |name|
+ @shorts[name] ||= option.switch_name
+ end
+ end
+ end
+
+ def remaining
+ @extra
+ end
+
+ def peek
+ return super unless @parsing_options
+
+ result = super
+ if result == OPTS_END
+ shift
+ @parsing_options = false
+ @stopped_parsing_after_extra_index ||= @extra.size
+ super
+ else
+ result
+ end
+ end
+
+ def shift
+ @is_treated_as_value = false
+ super
+ end
+
+ def unshift(arg, is_value: false)
+ @is_treated_as_value = is_value
+ super(arg)
+ end
+
+ def parse(args) # rubocop:disable Metrics/MethodLength
+ @pile = args.dup
+ @is_treated_as_value = false
+ @parsing_options = true
+
+ while peek
+ if parsing_options?
+ match, is_switch = current_is_switch?
+ shifted = shift
+
+ if is_switch
+ case shifted
+ when SHORT_SQ_RE
+ unshift($1.split("").map { |f| "-#{f}" })
+ next
+ when EQ_RE
+ unshift($2, is_value: true)
+ switch = $1
+ when SHORT_NUM
+ unshift($2)
+ switch = $1
+ when LONG_RE, SHORT_RE
+ switch = $1
+ end
+
+ switch = normalize_switch(switch)
+ option = switch_option(switch)
+ result = parse_peek(switch, option)
+ assign_result!(option, result)
+ elsif @stop_on_unknown
+ @parsing_options = false
+ @extra << shifted
+ @stopped_parsing_after_extra_index ||= @extra.size
+ @extra << shift while peek
+ break
+ elsif match
+ @extra << shifted
+ @extra << shift while peek && peek !~ /^-/
+ else
+ @extra << shifted
+ end
+ else
+ @extra << shift
+ end
+ end
+
+ check_requirement! unless @disable_required_check
+ check_exclusive!
+ check_at_least_one!
+
+ assigns = Bundler::Thor::CoreExt::HashWithIndifferentAccess.new(@assigns)
+ assigns.freeze
+ assigns
+ end
+
+ def check_exclusive!
+ opts = @assigns.keys
+ # When option A and B are exclusive, if A and B are given at the same time,
+ # the difference of argument array size will decrease.
+ found = @exclusives.find{ |ex| (ex - opts).size < ex.size - 1 }
+ if found
+ names = names_to_switch_names(found & opts).map{|n| "'#{n}'"}
+ class_name = self.class.name.split("::").last.downcase
+ fail ExclusiveArgumentError, "Found exclusive #{class_name} #{names.join(", ")}"
+ end
+ end
+
+ def check_at_least_one!
+ opts = @assigns.keys
+ # When at least one is required of the options A and B,
+ # if the both options were not given, none? would be true.
+ found = @at_least_ones.find{ |one_reqs| one_reqs.none?{ |o| opts.include? o} }
+ if found
+ names = names_to_switch_names(found).map{|n| "'#{n}'"}
+ class_name = self.class.name.split("::").last.downcase
+ fail AtLeastOneRequiredArgumentError, "Not found at least one of required #{class_name} #{names.join(", ")}"
+ end
+ end
+
+ def check_unknown!
+ to_check = @stopped_parsing_after_extra_index ? @extra[0...@stopped_parsing_after_extra_index] : @extra
+
+ # an unknown option starts with - or -- and has no more --'s afterward.
+ unknown = to_check.select { |str| str =~ /^--?(?:(?!--).)*$/ }
+ raise UnknownArgumentError.new(@switches.keys, unknown) unless unknown.empty?
+ end
+
+ protected
+
+ # Option names changes to swith name or human name
+ def names_to_switch_names(names = [])
+ @switches.map do |_, o|
+ if names.include? o.name
+ o.respond_to?(:switch_name) ? o.switch_name : o.human_name
+ else
+ nil
+ end
+ end.compact
+ end
+
+ def assign_result!(option, result)
+ if option.repeatable && option.type == :hash
+ (@assigns[option.human_name] ||= {}).merge!(result)
+ elsif option.repeatable
+ (@assigns[option.human_name] ||= []) << result
+ else
+ @assigns[option.human_name] = result
+ end
+ end
+
+ # Check if the current value in peek is a registered switch.
+ #
+ # Two booleans are returned. The first is true if the current value
+ # starts with a hyphen; the second is true if it is a registered switch.
+ def current_is_switch?
+ return [false, false] if @is_treated_as_value
+ case peek
+ when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
+ [true, switch?($1)]
+ when SHORT_SQ_RE
+ [true, $1.split("").any? { |f| switch?("-#{f}") }]
+ else
+ [false, false]
+ end
+ end
+
+ def current_is_switch_formatted?
+ return false if @is_treated_as_value
+ case peek
+ when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM, SHORT_SQ_RE
+ true
+ else
+ false
+ end
+ end
+
+ def current_is_value?
+ return true if @is_treated_as_value
+ peek && (!parsing_options? || super)
+ end
+
+ def switch?(arg)
+ !switch_option(normalize_switch(arg)).nil?
+ end
+
+ def switch_option(arg)
+ if match = no_or_skip?(arg) # rubocop:disable Lint/AssignmentInCondition
+ @switches[arg] || @switches["--#{match}"]
+ else
+ @switches[arg]
+ end
+ end
+
+ # Check if the given argument is actually a shortcut.
+ #
+ def normalize_switch(arg)
+ (@shorts[arg] || arg).tr("_", "-")
+ end
+
+ def parsing_options?
+ peek
+ @parsing_options
+ end
+
+ # Parse boolean values which can be given as --foo=true or --foo for true values, or
+ # --foo=false, --no-foo or --skip-foo for false values.
+ #
+ def parse_boolean(switch)
+ if current_is_value?
+ if ["true", "TRUE", "t", "T", true].include?(peek)
+ shift
+ true
+ elsif ["false", "FALSE", "f", "F", false].include?(peek)
+ shift
+ false
+ else
+ @switches.key?(switch) || !no_or_skip?(switch)
+ end
+ else
+ @switches.key?(switch) || !no_or_skip?(switch)
+ end
+ end
+
+ # Parse the value at the peek analyzing if it requires an input or not.
+ #
+ def parse_peek(switch, option)
+ if parsing_options? && (current_is_switch_formatted? || last?)
+ if option.boolean?
+ # No problem for boolean types
+ elsif no_or_skip?(switch)
+ return nil # User set value to nil
+ elsif option.string? && !option.required?
+ # Return the default if there is one, else the human name
+ return option.lazy_default || option.default || option.human_name
+ elsif option.lazy_default
+ return option.lazy_default
+ else
+ raise MalformattedArgumentError, "No value provided for option '#{switch}'"
+ end
+ end
+
+ @non_assigned_required.delete(option)
+ send(:"parse_#{option.type}", switch)
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/rake_compat.rb b/lib/bundler/vendor/thor/lib/thor/rake_compat.rb
new file mode 100644
index 0000000000..c6a4653fc1
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/rake_compat.rb
@@ -0,0 +1,72 @@
+require "rake"
+require "rake/dsl_definition"
+
+class Bundler::Thor
+ # Adds a compatibility layer to your Bundler::Thor classes which allows you to use
+ # rake package tasks. For example, to use rspec rake tasks, one can do:
+ #
+ # require 'bundler/vendor/thor/lib/thor/rake_compat'
+ # require 'rspec/core/rake_task'
+ #
+ # class Default < Bundler::Thor
+ # include Bundler::Thor::RakeCompat
+ #
+ # RSpec::Core::RakeTask.new(:spec) do |t|
+ # t.spec_opts = ['--options', './.rspec']
+ # t.spec_files = FileList['spec/**/*_spec.rb']
+ # end
+ # end
+ #
+ module RakeCompat
+ include Rake::DSL if defined?(Rake::DSL)
+
+ def self.rake_classes
+ @rake_classes ||= []
+ end
+
+ def self.included(base)
+ super(base)
+ # Hack. Make rakefile point to invoker, so rdoc task is generated properly.
+ rakefile = File.basename(caller[0].match(/(.*):\d+/)[1])
+ Rake.application.instance_variable_set(:@rakefile, rakefile)
+ rake_classes << base
+ end
+ end
+end
+
+# override task on (main), for compatibility with Rake 0.9
+instance_eval do
+ alias rake_namespace namespace
+
+ def task(*)
+ task = super
+
+ if klass = Bundler::Thor::RakeCompat.rake_classes.last # rubocop:disable Lint/AssignmentInCondition
+ non_namespaced_name = task.name.split(":").last
+
+ description = non_namespaced_name
+ description << task.arg_names.map { |n| n.to_s.upcase }.join(" ")
+ description.strip!
+
+ klass.desc description, Rake.application.last_description || non_namespaced_name
+ Rake.application.last_description = nil
+ klass.send :define_method, non_namespaced_name do |*args|
+ Rake::Task[task.name.to_sym].invoke(*args)
+ end
+ end
+
+ task
+ end
+
+ def namespace(name)
+ if klass = Bundler::Thor::RakeCompat.rake_classes.last # rubocop:disable Lint/AssignmentInCondition
+ const_name = Bundler::Thor::Util.camel_case(name.to_s).to_sym
+ klass.const_set(const_name, Class.new(Bundler::Thor))
+ new_klass = klass.const_get(const_name)
+ Bundler::Thor::RakeCompat.rake_classes << new_klass
+ end
+
+ super
+ Bundler::Thor::RakeCompat.rake_classes.pop
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/runner.rb b/lib/bundler/vendor/thor/lib/thor/runner.rb
new file mode 100644
index 0000000000..f0ce6df96c
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/runner.rb
@@ -0,0 +1,335 @@
+require_relative "../thor"
+require_relative "group"
+
+require "digest/sha2"
+require "pathname" unless defined?(Pathname)
+
+class Bundler::Thor::Runner < Bundler::Thor #:nodoc:
+ map "-T" => :list, "-i" => :install, "-u" => :update, "-v" => :version
+
+ def self.banner(command, all = false, subcommand = false)
+ "thor " + command.formatted_usage(self, all, subcommand)
+ end
+
+ def self.exit_on_failure?
+ true
+ end
+
+ # Override Bundler::Thor#help so it can give information about any class and any method.
+ #
+ def help(meth = nil)
+ if meth && !respond_to?(meth)
+ initialize_thorfiles(meth)
+ klass, command = Bundler::Thor::Util.find_class_and_command_by_namespace(meth)
+ self.class.handle_no_command_error(command, false) if klass.nil?
+ klass.start(["-h", command].compact, shell: shell)
+ else
+ super
+ end
+ end
+
+ # If a command is not found on Bundler::Thor::Runner, method missing is invoked and
+ # Bundler::Thor::Runner is then responsible for finding the command in all classes.
+ #
+ def method_missing(meth, *args)
+ meth = meth.to_s
+ initialize_thorfiles(meth)
+ klass, command = Bundler::Thor::Util.find_class_and_command_by_namespace(meth)
+ self.class.handle_no_command_error(command, false) if klass.nil?
+ args.unshift(command) if command
+ klass.start(args, shell: shell)
+ end
+
+ desc "install NAME", "Install an optionally named Bundler::Thor file into your system commands"
+ method_options as: :string, relative: :boolean, force: :boolean
+ def install(name) # rubocop:disable Metrics/MethodLength
+ initialize_thorfiles
+
+ is_uri = name =~ %r{^https?\://}
+
+ if is_uri
+ base = name
+ package = :file
+ require "open-uri"
+ begin
+ contents = URI.open(name, &:read)
+ rescue OpenURI::HTTPError
+ raise Error, "Error opening URI '#{name}'"
+ end
+ else
+ # If a directory name is provided as the argument, look for a 'main.thor'
+ # command in said directory.
+ begin
+ if File.directory?(File.expand_path(name))
+ base = File.join(name, "main.thor")
+ package = :directory
+ contents = File.open(base, &:read)
+ else
+ base = name
+ package = :file
+ require "open-uri"
+ contents = URI.open(name, &:read)
+ end
+ rescue Errno::ENOENT
+ raise Error, "Error opening file '#{name}'"
+ end
+ end
+
+ say "Your Thorfile contains:"
+ say contents
+
+ unless options["force"]
+ return false if no?("Do you wish to continue [y/N]?")
+ end
+
+ as = options["as"] || begin
+ first_line = contents.split("\n")[0]
+ (match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil
+ end
+
+ unless as
+ basename = File.basename(name)
+ as = ask("Please specify a name for #{name} in the system repository [#{basename}]:")
+ as = basename if as.empty?
+ end
+
+ location = if options[:relative] || is_uri
+ name
+ else
+ File.expand_path(name)
+ end
+
+ thor_yaml[as] = {
+ filename: Digest::SHA256.hexdigest(name + as),
+ location: location,
+ namespaces: Bundler::Thor::Util.namespaces_in_content(contents, base)
+ }
+
+ save_yaml(thor_yaml)
+ say "Storing thor file in your system repository"
+ destination = File.join(thor_root, thor_yaml[as][:filename])
+
+ if package == :file
+ File.open(destination, "w") { |f| f.puts contents }
+ else
+ require "fileutils"
+ FileUtils.cp_r(name, destination)
+ end
+
+ thor_yaml[as][:filename] # Indicate success
+ end
+
+ desc "version", "Show Bundler::Thor version"
+ def version
+ require_relative "version"
+ say "Bundler::Thor #{Bundler::Thor::VERSION}"
+ end
+
+ desc "uninstall NAME", "Uninstall a named Bundler::Thor module"
+ def uninstall(name)
+ raise Error, "Can't find module '#{name}'" unless thor_yaml[name]
+ say "Uninstalling #{name}."
+ require "fileutils"
+ FileUtils.rm_rf(File.join(thor_root, (thor_yaml[name][:filename]).to_s))
+
+ thor_yaml.delete(name)
+ save_yaml(thor_yaml)
+
+ puts "Done."
+ end
+
+ desc "update NAME", "Update a Bundler::Thor file from its original location"
+ def update(name)
+ raise Error, "Can't find module '#{name}'" if !thor_yaml[name] || !thor_yaml[name][:location]
+
+ say "Updating '#{name}' from #{thor_yaml[name][:location]}"
+
+ old_filename = thor_yaml[name][:filename]
+ self.options = options.merge("as" => name)
+
+ if File.directory? File.expand_path(name)
+ require "fileutils"
+ FileUtils.rm_rf(File.join(thor_root, old_filename))
+
+ thor_yaml.delete(old_filename)
+ save_yaml(thor_yaml)
+
+ filename = install(name)
+ else
+ filename = install(thor_yaml[name][:location])
+ end
+
+ File.delete(File.join(thor_root, old_filename)) unless filename == old_filename
+ end
+
+ desc "installed", "List the installed Bundler::Thor modules and commands"
+ method_options internal: :boolean
+ def installed
+ initialize_thorfiles(nil, true)
+ display_klasses(true, options["internal"])
+ end
+
+ desc "list [SEARCH]", "List the available thor commands (--substring means .*SEARCH)"
+ method_options substring: :boolean, group: :string, all: :boolean, debug: :boolean
+ def list(search = "")
+ initialize_thorfiles
+
+ search = ".*#{search}" if options["substring"]
+ search = /^#{search}.*/i
+ group = options[:group] || "standard"
+
+ klasses = Bundler::Thor::Base.subclasses.select do |k|
+ (options[:all] || k.group == group) && k.namespace =~ search
+ end
+
+ display_klasses(false, false, klasses)
+ end
+
+private
+
+ def thor_root
+ Bundler::Thor::Util.thor_root
+ end
+
+ def thor_yaml
+ @thor_yaml ||= begin
+ yaml_file = File.join(thor_root, "thor.yml")
+ require "yaml"
+ yaml = YAML.load_file(yaml_file) if File.exist?(yaml_file)
+ yaml || {}
+ end
+ end
+
+ # Save the yaml file. If none exists in thor root, creates one.
+ #
+ def save_yaml(yaml)
+ yaml_file = File.join(thor_root, "thor.yml")
+
+ unless File.exist?(yaml_file)
+ require "fileutils"
+ FileUtils.mkdir_p(thor_root)
+ yaml_file = File.join(thor_root, "thor.yml")
+ FileUtils.touch(yaml_file)
+ end
+
+ File.open(yaml_file, "w") { |f| f.puts yaml.to_yaml }
+ end
+
+ # Load the Thorfiles. If relevant_to is supplied, looks for specific files
+ # in the thor_root instead of loading them all.
+ #
+ # By default, it also traverses the current path until find Bundler::Thor files, as
+ # described in thorfiles. This look up can be skipped by supplying
+ # skip_lookup true.
+ #
+ def initialize_thorfiles(relevant_to = nil, skip_lookup = false)
+ thorfiles(relevant_to, skip_lookup).each do |f|
+ Bundler::Thor::Util.load_thorfile(f, nil, options[:debug]) unless Bundler::Thor::Base.subclass_files.keys.include?(File.expand_path(f))
+ end
+ end
+
+ # Finds Thorfiles by traversing from your current directory down to the root
+ # directory of your system. If at any time we find a Bundler::Thor file, we stop.
+ #
+ # We also ensure that system-wide Thorfiles are loaded first, so local
+ # Thorfiles can override them.
+ #
+ # ==== Example
+ #
+ # If we start at /Users/wycats/dev/thor ...
+ #
+ # 1. /Users/wycats/dev/thor
+ # 2. /Users/wycats/dev
+ # 3. /Users/wycats <-- we find a Thorfile here, so we stop
+ #
+ # Suppose we start at c:\Documents and Settings\james\dev\thor ...
+ #
+ # 1. c:\Documents and Settings\james\dev\thor
+ # 2. c:\Documents and Settings\james\dev
+ # 3. c:\Documents and Settings\james
+ # 4. c:\Documents and Settings
+ # 5. c:\ <-- no Thorfiles found!
+ #
+ def thorfiles(relevant_to = nil, skip_lookup = false)
+ thorfiles = []
+
+ unless skip_lookup
+ Pathname.pwd.ascend do |path|
+ thorfiles = Bundler::Thor::Util.globs_for(path).map { |g| Dir[g] }.flatten
+ break unless thorfiles.empty?
+ end
+ end
+
+ files = (relevant_to ? thorfiles_relevant_to(relevant_to) : Bundler::Thor::Util.thor_root_glob)
+ files += thorfiles
+ files -= ["#{thor_root}/thor.yml"]
+
+ files.map! do |file|
+ File.directory?(file) ? File.join(file, "main.thor") : file
+ end
+ end
+
+ # Load Thorfiles relevant to the given method. If you provide "foo:bar" it
+ # will load all thor files in the thor.yaml that has "foo" e "foo:bar"
+ # namespaces registered.
+ #
+ def thorfiles_relevant_to(meth)
+ lookup = [meth, meth.split(":")[0...-1].join(":")]
+
+ files = thor_yaml.select do |_, v|
+ v[:namespaces] && !(v[:namespaces] & lookup).empty?
+ end
+
+ files.map { |_, v| File.join(thor_root, (v[:filename]).to_s) }
+ end
+
+ # Display information about the given klasses. If with_module is given,
+ # it shows a table with information extracted from the yaml file.
+ #
+ def display_klasses(with_modules = false, show_internal = false, klasses = Bundler::Thor::Base.subclasses)
+ klasses -= [Bundler::Thor, Bundler::Thor::Runner, Bundler::Thor::Group] unless show_internal
+
+ raise Error, "No Bundler::Thor commands available" if klasses.empty?
+ show_modules if with_modules && !thor_yaml.empty?
+
+ list = Hash.new { |h, k| h[k] = [] }
+ groups = klasses.select { |k| k.ancestors.include?(Bundler::Thor::Group) }
+
+ # Get classes which inherit from Bundler::Thor
+ (klasses - groups).each { |k| list[k.namespace.split(":").first] += k.printable_commands(false) }
+
+ # Get classes which inherit from Bundler::Thor::Base
+ groups.map! { |k| k.printable_commands(false).first }
+ list["root"] = groups
+
+ # Order namespaces with default coming first
+ list = list.sort { |a, b| a[0].sub(/^default/, "") <=> b[0].sub(/^default/, "") }
+ list.each { |n, commands| display_commands(n, commands) unless commands.empty? }
+ end
+
+ def display_commands(namespace, list) #:nodoc:
+ list.sort! { |a, b| a[0] <=> b[0] }
+
+ say shell.set_color(namespace, :blue, true)
+ say "-" * namespace.size
+
+ print_table(list, truncate: true)
+ say
+ end
+ alias_method :display_tasks, :display_commands
+
+ def show_modules #:nodoc:
+ info = []
+ labels = %w(Modules Namespaces)
+
+ info << labels
+ info << ["-" * labels[0].size, "-" * labels[1].size]
+
+ thor_yaml.each do |name, hash|
+ info << [name, hash[:namespaces].join(", ")]
+ end
+
+ print_table info
+ say ""
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/shell.rb b/lib/bundler/vendor/thor/lib/thor/shell.rb
new file mode 100644
index 0000000000..265f3ba046
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/shell.rb
@@ -0,0 +1,81 @@
+require "rbconfig"
+
+class Bundler::Thor
+ module Base
+ class << self
+ attr_writer :shell
+
+ # Returns the shell used in all Bundler::Thor classes. If you are in a Unix platform
+ # it will use a colored log, otherwise it will use a basic one without color.
+ #
+ def shell
+ @shell ||= if ENV["THOR_SHELL"] && !ENV["THOR_SHELL"].empty?
+ Bundler::Thor::Shell.const_get(ENV["THOR_SHELL"])
+ elsif RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ && !ENV["ANSICON"]
+ Bundler::Thor::Shell::Basic
+ else
+ Bundler::Thor::Shell::Color
+ end
+ end
+ end
+ end
+
+ module Shell
+ SHELL_DELEGATED_METHODS = [:ask, :error, :set_color, :yes?, :no?, :say, :say_error, :say_status, :print_in_columns, :print_table, :print_wrapped, :file_collision, :terminal_width]
+ attr_writer :shell
+
+ autoload :Basic, File.expand_path("shell/basic", __dir__)
+ autoload :Color, File.expand_path("shell/color", __dir__)
+ autoload :HTML, File.expand_path("shell/html", __dir__)
+
+ # Add shell to initialize config values.
+ #
+ # ==== Configuration
+ # shell<Object>:: An instance of the shell to be used.
+ #
+ # ==== Examples
+ #
+ # class MyScript < Bundler::Thor
+ # argument :first, :type => :numeric
+ # end
+ #
+ # MyScript.new [1.0], { :foo => :bar }, :shell => Bundler::Thor::Shell::Basic.new
+ #
+ def initialize(args = [], options = {}, config = {})
+ super
+ self.shell = config[:shell]
+ shell.base ||= self if shell.respond_to?(:base)
+ end
+
+ # Holds the shell for the given Bundler::Thor instance. If no shell is given,
+ # it gets a default shell from Bundler::Thor::Base.shell.
+ def shell
+ @shell ||= Bundler::Thor::Base.shell.new
+ end
+
+ # Common methods that are delegated to the shell.
+ SHELL_DELEGATED_METHODS.each do |method|
+ module_eval <<-METHOD, __FILE__, __LINE__ + 1
+ def #{method}(*args,&block)
+ shell.#{method}(*args,&block)
+ end
+ METHOD
+ end
+
+ # Yields the given block with padding.
+ def with_padding
+ shell.padding += 1
+ yield
+ ensure
+ shell.padding -= 1
+ end
+
+ protected
+
+ # Allow shell to be shared between invocations.
+ #
+ def _shared_configuration #:nodoc:
+ super.merge!(shell: shell)
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/shell/basic.rb b/lib/bundler/vendor/thor/lib/thor/shell/basic.rb
new file mode 100644
index 0000000000..da02b94227
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/shell/basic.rb
@@ -0,0 +1,384 @@
+require_relative "column_printer"
+require_relative "table_printer"
+require_relative "wrapped_printer"
+
+class Bundler::Thor
+ module Shell
+ class Basic
+ attr_accessor :base
+ attr_reader :padding
+
+ # Initialize base, mute and padding to nil.
+ #
+ def initialize #:nodoc:
+ @base = nil
+ @mute = false
+ @padding = 0
+ @always_force = false
+ end
+
+ # Mute everything that's inside given block
+ #
+ def mute
+ @mute = true
+ yield
+ ensure
+ @mute = false
+ end
+
+ # Check if base is muted
+ #
+ def mute?
+ @mute
+ end
+
+ # Sets the output padding, not allowing less than zero values.
+ #
+ def padding=(value)
+ @padding = [0, value].max
+ end
+
+ # Sets the output padding while executing a block and resets it.
+ #
+ def indent(count = 1)
+ orig_padding = padding
+ self.padding = padding + count
+ yield
+ self.padding = orig_padding
+ end
+
+ # Asks something to the user and receives a response.
+ #
+ # If a default value is specified it will be presented to the user
+ # and allows them to select that value with an empty response. This
+ # option is ignored when limited answers are supplied.
+ #
+ # If asked to limit the correct responses, you can pass in an
+ # array of acceptable answers. If one of those is not supplied,
+ # they will be shown a message stating that one of those answers
+ # must be given and re-asked the question.
+ #
+ # If asking for sensitive information, the :echo option can be set
+ # to false to mask user input from $stdin.
+ #
+ # If the required input is a path, then set the path option to
+ # true. This will enable tab completion for file paths relative
+ # to the current working directory on systems that support
+ # Readline.
+ #
+ # ==== Example
+ # ask("What is your name?")
+ #
+ # ask("What is the planet furthest from the sun?", :default => "Neptune")
+ #
+ # ask("What is your favorite Neopolitan flavor?", :limited_to => ["strawberry", "chocolate", "vanilla"])
+ #
+ # ask("What is your password?", :echo => false)
+ #
+ # ask("Where should the file be saved?", :path => true)
+ #
+ def ask(statement, *args)
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ color = args.first
+
+ if options[:limited_to]
+ ask_filtered(statement, color, options)
+ else
+ ask_simply(statement, color, options)
+ end
+ end
+
+ # Say (print) something to the user. If the sentence ends with a whitespace
+ # or tab character, a new line is not appended (print + flush). Otherwise
+ # are passed straight to puts (behavior got from Highline).
+ #
+ # ==== Example
+ # say("I know you knew that.")
+ #
+ def say(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/))
+ return if quiet?
+
+ buffer = prepare_message(message, *color)
+ buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
+
+ stdout.print(buffer)
+ stdout.flush
+ end
+
+ # Say (print) an error to the user. If the sentence ends with a whitespace
+ # or tab character, a new line is not appended (print + flush). Otherwise
+ # are passed straight to puts (behavior got from Highline).
+ #
+ # ==== Example
+ # say_error("error: something went wrong")
+ #
+ def say_error(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/))
+ return if quiet?
+
+ buffer = prepare_message(message, *color)
+ buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
+
+ stderr.print(buffer)
+ stderr.flush
+ end
+
+ # Say a status with the given color and appends the message. Since this
+ # method is used frequently by actions, it allows nil or false to be given
+ # in log_status, avoiding the message from being shown. If a Symbol is
+ # given in log_status, it's used as the color.
+ #
+ def say_status(status, message, log_status = true)
+ return if quiet? || log_status == false
+ spaces = " " * (padding + 1)
+ status = status.to_s.rjust(12)
+ margin = " " * status.length + spaces
+
+ color = log_status.is_a?(Symbol) ? log_status : :green
+ status = set_color status, color, true if color
+
+ message = message.to_s.chomp.gsub(/(?<!\A)^/, margin)
+ buffer = "#{status}#{spaces}#{message}\n"
+
+ stdout.print(buffer)
+ stdout.flush
+ end
+
+ # Asks the user a question and returns true if the user replies "y" or
+ # "yes".
+ #
+ def yes?(statement, color = nil)
+ !!(ask(statement, color, add_to_history: false) =~ is?(:yes))
+ end
+
+ # Asks the user a question and returns true if the user replies "n" or
+ # "no".
+ #
+ def no?(statement, color = nil)
+ !!(ask(statement, color, add_to_history: false) =~ is?(:no))
+ end
+
+ # Prints values in columns
+ #
+ # ==== Parameters
+ # Array[String, String, ...]
+ #
+ def print_in_columns(array)
+ printer = ColumnPrinter.new(stdout)
+ printer.print(array)
+ end
+
+ # Prints a table.
+ #
+ # ==== Parameters
+ # Array[Array[String, String, ...]]
+ #
+ # ==== Options
+ # indent<Integer>:: Indent the first column by indent value.
+ # colwidth<Integer>:: Force the first column to colwidth spaces wide.
+ # borders<Boolean>:: Adds ascii borders.
+ #
+ def print_table(array, options = {}) # rubocop:disable Metrics/MethodLength
+ printer = TablePrinter.new(stdout, options)
+ printer.print(array)
+ end
+
+ # Prints a long string, word-wrapping the text to the current width of the
+ # terminal display. Ideal for printing heredocs.
+ #
+ # ==== Parameters
+ # String
+ #
+ # ==== Options
+ # indent<Integer>:: Indent each line of the printed paragraph by indent value.
+ #
+ def print_wrapped(message, options = {})
+ printer = WrappedPrinter.new(stdout, options)
+ printer.print(message)
+ end
+
+ # Deals with file collision and returns true if the file should be
+ # overwritten and false otherwise. If a block is given, it uses the block
+ # response as the content for the diff.
+ #
+ # ==== Parameters
+ # destination<String>:: the destination file to solve conflicts
+ # block<Proc>:: an optional block that returns the value to be used in diff and merge
+ #
+ def file_collision(destination)
+ return true if @always_force
+ options = block_given? ? "[Ynaqdhm]" : "[Ynaqh]"
+
+ loop do
+ answer = ask(
+ %[Overwrite #{destination}? (enter "h" for help) #{options}],
+ add_to_history: false
+ )
+
+ case answer
+ when nil
+ say ""
+ return true
+ when is?(:yes), is?(:force), ""
+ return true
+ when is?(:no), is?(:skip)
+ return false
+ when is?(:always)
+ return @always_force = true
+ when is?(:quit)
+ say "Aborting..."
+ raise SystemExit
+ when is?(:diff)
+ show_diff(destination, yield) if block_given?
+ say "Retrying..."
+ when is?(:merge)
+ if block_given? && !merge_tool.empty?
+ merge(destination, yield)
+ return nil
+ end
+
+ say "Please specify merge tool to `THOR_MERGE` env."
+ else
+ say file_collision_help(block_given?)
+ end
+ end
+ end
+
+ # Called if something goes wrong during the execution. This is used by Bundler::Thor
+ # internally and should not be used inside your scripts. If something went
+ # wrong, you can always raise an exception. If you raise a Bundler::Thor::Error, it
+ # will be rescued and wrapped in the method below.
+ #
+ def error(statement)
+ stderr.puts statement
+ end
+
+ # Apply color to the given string with optional bold. Disabled in the
+ # Bundler::Thor::Shell::Basic class.
+ #
+ def set_color(string, *) #:nodoc:
+ string
+ end
+
+ protected
+
+ def prepare_message(message, *color)
+ spaces = " " * padding
+ spaces + set_color(message.to_s, *color)
+ end
+
+ def can_display_colors?
+ false
+ end
+
+ def lookup_color(color)
+ return color unless color.is_a?(Symbol)
+ self.class.const_get(color.to_s.upcase)
+ end
+
+ def stdout
+ $stdout
+ end
+
+ def stderr
+ $stderr
+ end
+
+ def is?(value) #:nodoc:
+ value = value.to_s
+
+ if value.size == 1
+ /\A#{value}\z/i
+ else
+ /\A(#{value}|#{value[0, 1]})\z/i
+ end
+ end
+
+ def file_collision_help(block_given) #:nodoc:
+ help = <<-HELP
+ Y - yes, overwrite
+ n - no, do not overwrite
+ a - all, overwrite this and all others
+ q - quit, abort
+ h - help, show this help
+ HELP
+ if block_given
+ help << <<-HELP
+ d - diff, show the differences between the old and the new
+ m - merge, run merge tool
+ HELP
+ end
+ help
+ end
+
+ def show_diff(destination, content) #:nodoc:
+ diff_cmd = ENV["THOR_DIFF"] || ENV["RAILS_DIFF"] || "diff -u"
+
+ require "tempfile"
+ Tempfile.open(File.basename(destination), File.dirname(destination), binmode: true) do |temp|
+ temp.write content
+ temp.rewind
+ system %(#{diff_cmd} "#{destination}" "#{temp.path}")
+ end
+ end
+
+ def quiet? #:nodoc:
+ mute? || (base && base.options[:quiet])
+ end
+
+ def unix?
+ Terminal.unix?
+ end
+
+ def ask_simply(statement, color, options)
+ default = options[:default]
+ message = [statement, ("(#{default})" if default), nil].uniq.join(" ")
+ message = prepare_message(message, *color)
+ result = Bundler::Thor::LineEditor.readline(message, options)
+
+ return unless result
+
+ result = result.strip
+
+ if default && result == ""
+ default
+ else
+ result
+ end
+ end
+
+ def ask_filtered(statement, color, options)
+ answer_set = options[:limited_to]
+ case_insensitive = options.fetch(:case_insensitive, false)
+ correct_answer = nil
+ until correct_answer
+ answers = answer_set.join(", ")
+ answer = ask_simply("#{statement} [#{answers}]", color, options)
+ correct_answer = answer_match(answer_set, answer, case_insensitive)
+ say("Your response must be one of: [#{answers}]. Please try again.") unless correct_answer
+ end
+ correct_answer
+ end
+
+ def answer_match(possibilities, answer, case_insensitive)
+ if case_insensitive
+ possibilities.detect{ |possibility| possibility.downcase == answer.downcase }
+ else
+ possibilities.detect{ |possibility| possibility == answer }
+ end
+ end
+
+ def merge(destination, content) #:nodoc:
+ require "tempfile"
+ Tempfile.open([File.basename(destination), File.extname(destination)], File.dirname(destination)) do |temp|
+ temp.write content
+ temp.rewind
+ system(merge_tool, temp.path, destination)
+ end
+ end
+
+ def merge_tool #:nodoc:
+ @merge_tool ||= ENV["THOR_MERGE"] || "git difftool --no-index"
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/shell/color.rb b/lib/bundler/vendor/thor/lib/thor/shell/color.rb
new file mode 100644
index 0000000000..5d708fadca
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/shell/color.rb
@@ -0,0 +1,112 @@
+require_relative "basic"
+
+class Bundler::Thor
+ module Shell
+ # Inherit from Bundler::Thor::Shell::Basic and add set_color behavior. Check
+ # Bundler::Thor::Shell::Basic to see all available methods.
+ #
+ class Color < Basic
+ # Embed in a String to clear all previous ANSI sequences.
+ CLEAR = "\e[0m"
+ # The start of an ANSI bold sequence.
+ BOLD = "\e[1m"
+
+ # Set the terminal's foreground ANSI color to black.
+ BLACK = "\e[30m"
+ # Set the terminal's foreground ANSI color to red.
+ RED = "\e[31m"
+ # Set the terminal's foreground ANSI color to green.
+ GREEN = "\e[32m"
+ # Set the terminal's foreground ANSI color to yellow.
+ YELLOW = "\e[33m"
+ # Set the terminal's foreground ANSI color to blue.
+ BLUE = "\e[34m"
+ # Set the terminal's foreground ANSI color to magenta.
+ MAGENTA = "\e[35m"
+ # Set the terminal's foreground ANSI color to cyan.
+ CYAN = "\e[36m"
+ # Set the terminal's foreground ANSI color to white.
+ WHITE = "\e[37m"
+
+ # Set the terminal's background ANSI color to black.
+ ON_BLACK = "\e[40m"
+ # Set the terminal's background ANSI color to red.
+ ON_RED = "\e[41m"
+ # Set the terminal's background ANSI color to green.
+ ON_GREEN = "\e[42m"
+ # Set the terminal's background ANSI color to yellow.
+ ON_YELLOW = "\e[43m"
+ # Set the terminal's background ANSI color to blue.
+ ON_BLUE = "\e[44m"
+ # Set the terminal's background ANSI color to magenta.
+ ON_MAGENTA = "\e[45m"
+ # Set the terminal's background ANSI color to cyan.
+ ON_CYAN = "\e[46m"
+ # Set the terminal's background ANSI color to white.
+ ON_WHITE = "\e[47m"
+
+ # Set color by using a string or one of the defined constants. If a third
+ # option is set to true, it also adds bold to the string. This is based
+ # on Highline implementation and it automatically appends CLEAR to the end
+ # of the returned String.
+ #
+ # Pass foreground, background and bold options to this method as
+ # symbols.
+ #
+ # Example:
+ #
+ # set_color "Hi!", :red, :on_white, :bold
+ #
+ # The available colors are:
+ #
+ # :bold
+ # :black
+ # :red
+ # :green
+ # :yellow
+ # :blue
+ # :magenta
+ # :cyan
+ # :white
+ # :on_black
+ # :on_red
+ # :on_green
+ # :on_yellow
+ # :on_blue
+ # :on_magenta
+ # :on_cyan
+ # :on_white
+ def set_color(string, *colors)
+ if colors.compact.empty? || !can_display_colors?
+ string
+ elsif colors.all? { |color| color.is_a?(Symbol) || color.is_a?(String) }
+ ansi_colors = colors.map { |color| lookup_color(color) }
+ "#{ansi_colors.join}#{string}#{CLEAR}"
+ else
+ # The old API was `set_color(color, bold=boolean)`. We
+ # continue to support the old API because you should never
+ # break old APIs unnecessarily :P
+ foreground, bold = colors
+ foreground = self.class.const_get(foreground.to_s.upcase) if foreground.is_a?(Symbol)
+
+ bold = bold ? BOLD : ""
+ "#{bold}#{foreground}#{string}#{CLEAR}"
+ end
+ end
+
+ protected
+
+ def can_display_colors?
+ are_colors_supported? && !are_colors_disabled?
+ end
+
+ def are_colors_supported?
+ stdout.tty? && ENV["TERM"] != "dumb"
+ end
+
+ def are_colors_disabled?
+ !ENV["NO_COLOR"].nil? && !ENV["NO_COLOR"].empty?
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/shell/column_printer.rb b/lib/bundler/vendor/thor/lib/thor/shell/column_printer.rb
new file mode 100644
index 0000000000..56a9e6181b
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/shell/column_printer.rb
@@ -0,0 +1,29 @@
+require_relative "terminal"
+
+class Bundler::Thor
+ module Shell
+ class ColumnPrinter
+ attr_reader :stdout, :options
+
+ def initialize(stdout, options = {})
+ @stdout = stdout
+ @options = options
+ @indent = options[:indent].to_i
+ end
+
+ def print(array)
+ return if array.empty?
+ colwidth = (array.map { |el| el.to_s.size }.max || 0) + 2
+ array.each_with_index do |value, index|
+ # Don't output trailing spaces when printing the last column
+ if ((((index + 1) % (Terminal.terminal_width / colwidth))).zero? && !index.zero?) || index + 1 == array.length
+ stdout.puts value
+ else
+ stdout.printf("%-#{colwidth}s", value)
+ end
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/bundler/vendor/thor/lib/thor/shell/html.rb b/lib/bundler/vendor/thor/lib/thor/shell/html.rb
new file mode 100644
index 0000000000..a0a8520e5c
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/shell/html.rb
@@ -0,0 +1,81 @@
+require_relative "basic"
+
+class Bundler::Thor
+ module Shell
+ # Inherit from Bundler::Thor::Shell::Basic and add set_color behavior. Check
+ # Bundler::Thor::Shell::Basic to see all available methods.
+ #
+ class HTML < Basic
+ # The start of an HTML bold sequence.
+ BOLD = "font-weight: bold"
+
+ # Set the terminal's foreground HTML color to black.
+ BLACK = "color: black"
+ # Set the terminal's foreground HTML color to red.
+ RED = "color: red"
+ # Set the terminal's foreground HTML color to green.
+ GREEN = "color: green"
+ # Set the terminal's foreground HTML color to yellow.
+ YELLOW = "color: yellow"
+ # Set the terminal's foreground HTML color to blue.
+ BLUE = "color: blue"
+ # Set the terminal's foreground HTML color to magenta.
+ MAGENTA = "color: magenta"
+ # Set the terminal's foreground HTML color to cyan.
+ CYAN = "color: cyan"
+ # Set the terminal's foreground HTML color to white.
+ WHITE = "color: white"
+
+ # Set the terminal's background HTML color to black.
+ ON_BLACK = "background-color: black"
+ # Set the terminal's background HTML color to red.
+ ON_RED = "background-color: red"
+ # Set the terminal's background HTML color to green.
+ ON_GREEN = "background-color: green"
+ # Set the terminal's background HTML color to yellow.
+ ON_YELLOW = "background-color: yellow"
+ # Set the terminal's background HTML color to blue.
+ ON_BLUE = "background-color: blue"
+ # Set the terminal's background HTML color to magenta.
+ ON_MAGENTA = "background-color: magenta"
+ # Set the terminal's background HTML color to cyan.
+ ON_CYAN = "background-color: cyan"
+ # Set the terminal's background HTML color to white.
+ ON_WHITE = "background-color: white"
+
+ # Set color by using a string or one of the defined constants. If a third
+ # option is set to true, it also adds bold to the string. This is based
+ # on Highline implementation and it automatically appends CLEAR to the end
+ # of the returned String.
+ #
+ def set_color(string, *colors)
+ if colors.all? { |color| color.is_a?(Symbol) || color.is_a?(String) }
+ html_colors = colors.map { |color| lookup_color(color) }
+ "<span style=\"#{html_colors.join('; ')};\">#{Bundler::Thor::Util.escape_html(string)}</span>"
+ else
+ color, bold = colors
+ html_color = self.class.const_get(color.to_s.upcase) if color.is_a?(Symbol)
+ styles = [html_color]
+ styles << BOLD if bold
+ "<span style=\"#{styles.join('; ')};\">#{Bundler::Thor::Util.escape_html(string)}</span>"
+ end
+ end
+
+ # Ask something to the user and receives a response.
+ #
+ # ==== Example
+ # ask("What is your name?")
+ #
+ # TODO: Implement #ask for Bundler::Thor::Shell::HTML
+ def ask(statement, color = nil)
+ raise NotImplementedError, "Implement #ask for Bundler::Thor::Shell::HTML"
+ end
+
+ protected
+
+ def can_display_colors?
+ true
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/shell/table_printer.rb b/lib/bundler/vendor/thor/lib/thor/shell/table_printer.rb
new file mode 100644
index 0000000000..dee3614753
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/shell/table_printer.rb
@@ -0,0 +1,118 @@
+require_relative "column_printer"
+require_relative "terminal"
+
+class Bundler::Thor
+ module Shell
+ class TablePrinter < ColumnPrinter
+ BORDER_SEPARATOR = :separator
+
+ def initialize(stdout, options = {})
+ super
+ @formats = []
+ @maximas = []
+ @colwidth = options[:colwidth]
+ @truncate = options[:truncate] == true ? Terminal.terminal_width : options[:truncate]
+ @padding = 1
+ end
+
+ def print(array)
+ return if array.empty?
+
+ prepare(array)
+
+ print_border_separator if options[:borders]
+
+ array.each do |row|
+ if options[:borders] && row == BORDER_SEPARATOR
+ print_border_separator
+ next
+ end
+
+ sentence = "".dup
+
+ row.each_with_index do |column, index|
+ sentence << format_cell(column, row.size, index)
+ end
+
+ sentence = truncate(sentence)
+ sentence << "|" if options[:borders]
+ stdout.puts indentation + sentence
+
+ end
+ print_border_separator if options[:borders]
+ end
+
+ private
+
+ def prepare(array)
+ array = array.reject{|row| row == BORDER_SEPARATOR }
+
+ @formats << "%-#{@colwidth + 2}s".dup if @colwidth
+ start = @colwidth ? 1 : 0
+
+ colcount = array.max { |a, b| a.size <=> b.size }.size
+
+ start.upto(colcount - 1) do |index|
+ maxima = array.map { |row| row[index] ? row[index].to_s.size : 0 }.max
+
+ @maximas << maxima
+ @formats << if options[:borders]
+ "%-#{maxima}s".dup
+ elsif index == colcount - 1
+ # Don't output 2 trailing spaces when printing the last column
+ "%-s".dup
+ else
+ "%-#{maxima + 2}s".dup
+ end
+ end
+
+ @formats << "%s"
+ end
+
+ def format_cell(column, row_size, index)
+ maxima = @maximas[index]
+
+ f = if column.is_a?(Numeric)
+ if options[:borders]
+ # With borders we handle padding separately
+ "%#{maxima}s"
+ elsif index == row_size - 1
+ # Don't output 2 trailing spaces when printing the last column
+ "%#{maxima}s"
+ else
+ "%#{maxima}s "
+ end
+ else
+ @formats[index]
+ end
+
+ cell = "".dup
+ cell << "|" + " " * @padding if options[:borders]
+ cell << f % column.to_s
+ cell << " " * @padding if options[:borders]
+ cell
+ end
+
+ def print_border_separator
+ separator = @maximas.map do |maxima|
+ "+" + "-" * (maxima + 2 * @padding)
+ end
+ stdout.puts indentation + separator.join + "+"
+ end
+
+ def truncate(string)
+ return string unless @truncate
+ chars = string.chars.to_a
+ if chars.length <= @truncate
+ chars.join
+ else
+ chars[0, @truncate - 3 - @indent].join + "..."
+ end
+ end
+
+ def indentation
+ " " * @indent
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/shell/terminal.rb b/lib/bundler/vendor/thor/lib/thor/shell/terminal.rb
new file mode 100644
index 0000000000..2c60684308
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/shell/terminal.rb
@@ -0,0 +1,42 @@
+class Bundler::Thor
+ module Shell
+ module Terminal
+ DEFAULT_TERMINAL_WIDTH = 80
+
+ class << self
+ # This code was copied from Rake, available under MIT-LICENSE
+ # Copyright (c) 2003, 2004 Jim Weirich
+ def terminal_width
+ result = if ENV["THOR_COLUMNS"]
+ ENV["THOR_COLUMNS"].to_i
+ else
+ unix? ? dynamic_width : DEFAULT_TERMINAL_WIDTH
+ end
+ result < 10 ? DEFAULT_TERMINAL_WIDTH : result
+ rescue
+ DEFAULT_TERMINAL_WIDTH
+ end
+
+ def unix?
+ RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris)/i
+ end
+
+ private
+
+ # Calculate the dynamic width of the terminal
+ def dynamic_width
+ @dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput)
+ end
+
+ def dynamic_width_stty
+ `stty size 2>/dev/null`.split[1].to_i
+ end
+
+ def dynamic_width_tput
+ `tput cols 2>/dev/null`.to_i
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/shell/wrapped_printer.rb b/lib/bundler/vendor/thor/lib/thor/shell/wrapped_printer.rb
new file mode 100644
index 0000000000..ba88e952db
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/shell/wrapped_printer.rb
@@ -0,0 +1,38 @@
+require_relative "column_printer"
+require_relative "terminal"
+
+class Bundler::Thor
+ module Shell
+ class WrappedPrinter < ColumnPrinter
+ def print(message)
+ width = Terminal.terminal_width - @indent
+ paras = message.split("\n\n")
+
+ paras.map! do |unwrapped|
+ words = unwrapped.split(" ")
+ counter = words.first.length
+ words.inject do |memo, word|
+ word = word.gsub(/\n\005/, "\n").gsub(/\005/, "\n")
+ counter = 0 if word.include? "\n"
+ if (counter + word.length + 1) < width
+ memo = "#{memo} #{word}"
+ counter += (word.length + 1)
+ else
+ memo = "#{memo}\n#{word}"
+ counter = word.length
+ end
+ memo
+ end
+ end.compact!
+
+ paras.each do |para|
+ para.split("\n").each do |line|
+ stdout.puts line.insert(0, " " * @indent)
+ end
+ stdout.puts unless para == paras.last
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/bundler/vendor/thor/lib/thor/util.rb b/lib/bundler/vendor/thor/lib/thor/util.rb
new file mode 100644
index 0000000000..cd8f9ece87
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/util.rb
@@ -0,0 +1,285 @@
+require "rbconfig"
+
+class Bundler::Thor
+ module Sandbox #:nodoc:
+ end
+
+ # This module holds several utilities:
+ #
+ # 1) Methods to convert thor namespaces to constants and vice-versa.
+ #
+ # Bundler::Thor::Util.namespace_from_thor_class(Foo::Bar::Baz) #=> "foo:bar:baz"
+ #
+ # 2) Loading thor files and sandboxing:
+ #
+ # Bundler::Thor::Util.load_thorfile("~/.thor/foo")
+ #
+ module Util
+ class << self
+ # Receives a namespace and search for it in the Bundler::Thor::Base subclasses.
+ #
+ # ==== Parameters
+ # namespace<String>:: The namespace to search for.
+ #
+ def find_by_namespace(namespace)
+ namespace = "default#{namespace}" if namespace.empty? || namespace =~ /^:/
+ Bundler::Thor::Base.subclasses.detect { |klass| klass.namespace == namespace }
+ end
+
+ # Receives a constant and converts it to a Bundler::Thor namespace. Since Bundler::Thor
+ # commands can be added to a sandbox, this method is also responsible for
+ # removing the sandbox namespace.
+ #
+ # This method should not be used in general because it's used to deal with
+ # older versions of Bundler::Thor. On current versions, if you need to get the
+ # namespace from a class, just call namespace on it.
+ #
+ # ==== Parameters
+ # constant<Object>:: The constant to be converted to the thor path.
+ #
+ # ==== Returns
+ # String:: If we receive Foo::Bar::Baz it returns "foo:bar:baz"
+ #
+ def namespace_from_thor_class(constant)
+ constant = constant.to_s.gsub(/^Bundler::Thor::Sandbox::/, "")
+ constant = snake_case(constant).squeeze(":")
+ constant
+ end
+
+ # Given the contents, evaluate it inside the sandbox and returns the
+ # namespaces defined in the sandbox.
+ #
+ # ==== Parameters
+ # contents<String>
+ #
+ # ==== Returns
+ # Array[Object]
+ #
+ def namespaces_in_content(contents, file = __FILE__)
+ old_constants = Bundler::Thor::Base.subclasses.dup
+ Bundler::Thor::Base.subclasses.clear
+
+ load_thorfile(file, contents)
+
+ new_constants = Bundler::Thor::Base.subclasses.dup
+ Bundler::Thor::Base.subclasses.replace(old_constants)
+
+ new_constants.map!(&:namespace)
+ new_constants.compact!
+ new_constants
+ end
+
+ # Returns the thor classes declared inside the given class.
+ #
+ def thor_classes_in(klass)
+ stringfied_constants = klass.constants.map(&:to_s)
+ Bundler::Thor::Base.subclasses.select do |subclass|
+ next unless subclass.name
+ stringfied_constants.include?(subclass.name.gsub("#{klass.name}::", ""))
+ end
+ end
+
+ # Receives a string and convert it to snake case. SnakeCase returns snake_case.
+ #
+ # ==== Parameters
+ # String
+ #
+ # ==== Returns
+ # String
+ #
+ def snake_case(str)
+ return str.downcase if str =~ /^[A-Z_]+$/
+ str.gsub(/\B[A-Z]/, '_\&').squeeze("_") =~ /_*(.*)/
+ Regexp.last_match(-1).downcase
+ end
+
+ # Receives a string and convert it to camel case. camel_case returns CamelCase.
+ #
+ # ==== Parameters
+ # String
+ #
+ # ==== Returns
+ # String
+ #
+ def camel_case(str)
+ return str if str !~ /_/ && str =~ /[A-Z]+.*/
+ str.split("_").map(&:capitalize).join
+ end
+
+ # Receives a namespace and tries to retrieve a Bundler::Thor or Bundler::Thor::Group class
+ # from it. It first searches for a class using the all the given namespace,
+ # if it's not found, removes the highest entry and searches for the class
+ # again. If found, returns the highest entry as the class name.
+ #
+ # ==== Examples
+ #
+ # class Foo::Bar < Bundler::Thor
+ # def baz
+ # end
+ # end
+ #
+ # class Baz::Foo < Bundler::Thor::Group
+ # end
+ #
+ # Bundler::Thor::Util.namespace_to_thor_class("foo:bar") #=> Foo::Bar, nil # will invoke default command
+ # Bundler::Thor::Util.namespace_to_thor_class("baz:foo") #=> Baz::Foo, nil
+ # Bundler::Thor::Util.namespace_to_thor_class("foo:bar:baz") #=> Foo::Bar, "baz"
+ #
+ # ==== Parameters
+ # namespace<String>
+ #
+ def find_class_and_command_by_namespace(namespace, fallback = true)
+ if namespace.include?(":") # look for a namespaced command
+ *pieces, command = namespace.split(":")
+ namespace = pieces.join(":")
+ namespace = "default" if namespace.empty?
+ klass = Bundler::Thor::Base.subclasses.detect { |thor| thor.namespace == namespace && thor.command_exists?(command) }
+ end
+ unless klass # look for a Bundler::Thor::Group with the right name
+ klass = Bundler::Thor::Util.find_by_namespace(namespace)
+ command = nil
+ end
+ if !klass && fallback # try a command in the default namespace
+ command = namespace
+ klass = Bundler::Thor::Util.find_by_namespace("")
+ end
+ [klass, command]
+ end
+ alias_method :find_class_and_task_by_namespace, :find_class_and_command_by_namespace
+
+ # Receives a path and load the thor file in the path. The file is evaluated
+ # inside the sandbox to avoid namespacing conflicts.
+ #
+ def load_thorfile(path, content = nil, debug = false)
+ content ||= File.read(path)
+
+ begin
+ Bundler::Thor::Sandbox.class_eval(content, path)
+ rescue StandardError => e
+ $stderr.puts("WARNING: unable to load thorfile #{path.inspect}: #{e.message}")
+ if debug
+ $stderr.puts(*e.backtrace)
+ else
+ $stderr.puts(e.backtrace.first)
+ end
+ end
+ end
+
+ def user_home
+ @@user_home ||= if ENV["HOME"]
+ ENV["HOME"]
+ elsif ENV["USERPROFILE"]
+ ENV["USERPROFILE"]
+ elsif ENV["HOMEDRIVE"] && ENV["HOMEPATH"]
+ File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"])
+ elsif ENV["APPDATA"]
+ ENV["APPDATA"]
+ else
+ begin
+ File.expand_path("~")
+ rescue
+ if File::ALT_SEPARATOR
+ "C:/"
+ else
+ "/"
+ end
+ end
+ end
+ end
+
+ # Returns the root where thor files are located, depending on the OS.
+ #
+ def thor_root
+ File.join(user_home, ".thor").tr("\\", "/")
+ end
+
+ # Returns the files in the thor root. On Windows thor_root will be something
+ # like this:
+ #
+ # C:\Documents and Settings\james\.thor
+ #
+ # If we don't #gsub the \ character, Dir.glob will fail.
+ #
+ def thor_root_glob
+ files = Dir["#{escape_globs(thor_root)}/*"]
+
+ files.map! do |file|
+ File.directory?(file) ? File.join(file, "main.thor") : file
+ end
+ end
+
+ # Where to look for Bundler::Thor files.
+ #
+ def globs_for(path)
+ path = escape_globs(path)
+ ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/**/*.thor"]
+ end
+
+ # Return the path to the ruby interpreter taking into account multiple
+ # installations and windows extensions.
+ #
+ def ruby_command
+ @ruby_command ||= begin
+ ruby_name = RbConfig::CONFIG["ruby_install_name"]
+ ruby = File.join(RbConfig::CONFIG["bindir"], ruby_name)
+ ruby << RbConfig::CONFIG["EXEEXT"]
+
+ # avoid using different name than ruby (on platforms supporting links)
+ if ruby_name != "ruby" && File.respond_to?(:readlink)
+ begin
+ alternate_ruby = File.join(RbConfig::CONFIG["bindir"], "ruby")
+ alternate_ruby << RbConfig::CONFIG["EXEEXT"]
+
+ # ruby is a symlink
+ if File.symlink? alternate_ruby
+ linked_ruby = File.readlink alternate_ruby
+
+ # symlink points to 'ruby_install_name'
+ ruby = alternate_ruby if linked_ruby == ruby_name || linked_ruby == ruby
+ end
+ rescue NotImplementedError # rubocop:disable Lint/HandleExceptions
+ # just ignore on windows
+ end
+ end
+
+ # escape string in case path to ruby executable contain spaces.
+ ruby.sub!(/.*\s.*/m, '"\&"')
+ ruby
+ end
+ end
+
+ # Returns a string that has had any glob characters escaped.
+ # The glob characters are `* ? { } [ ]`.
+ #
+ # ==== Examples
+ #
+ # Bundler::Thor::Util.escape_globs('[apps]') # => '\[apps\]'
+ #
+ # ==== Parameters
+ # String
+ #
+ # ==== Returns
+ # String
+ #
+ def escape_globs(path)
+ path.to_s.gsub(/[*?{}\[\]]/, '\\\\\\&')
+ end
+
+ # Returns a string that has had any HTML characters escaped.
+ #
+ # ==== Examples
+ #
+ # Bundler::Thor::Util.escape_html('<div>') # => "&lt;div&gt;"
+ #
+ # ==== Parameters
+ # String
+ #
+ # ==== Returns
+ # String
+ #
+ def escape_html(string)
+ CGI.escapeHTML(string)
+ end
+ end
+ end
+end
diff --git a/lib/bundler/vendor/thor/lib/thor/version.rb b/lib/bundler/vendor/thor/lib/thor/version.rb
new file mode 100644
index 0000000000..5474a2f71b
--- /dev/null
+++ b/lib/bundler/vendor/thor/lib/thor/version.rb
@@ -0,0 +1,3 @@
+class Bundler::Thor
+ VERSION = "1.4.0"
+end
diff --git a/lib/bundler/vendor/tsort/lib/tsort.rb b/lib/bundler/vendor/tsort/lib/tsort.rb
new file mode 100644
index 0000000000..cf8731f760
--- /dev/null
+++ b/lib/bundler/vendor/tsort/lib/tsort.rb
@@ -0,0 +1,455 @@
+# frozen_string_literal: true
+
+#--
+# tsort.rb - provides a module for topological sorting and strongly connected components.
+#++
+#
+
+#
+# Bundler::TSort implements topological sorting using Tarjan's algorithm for
+# strongly connected components.
+#
+# Bundler::TSort is designed to be able to be used with any object which can be
+# interpreted as a directed graph.
+#
+# Bundler::TSort requires two methods to interpret an object as a graph,
+# tsort_each_node and tsort_each_child.
+#
+# * tsort_each_node is used to iterate for all nodes over a graph.
+# * tsort_each_child is used to iterate for child nodes of a given node.
+#
+# The equality of nodes are defined by eql? and hash since
+# Bundler::TSort uses Hash internally.
+#
+# == A Simple Example
+#
+# The following example demonstrates how to mix the Bundler::TSort module into an
+# existing class (in this case, Hash). Here, we're treating each key in
+# the hash as a node in the graph, and so we simply alias the required
+# #tsort_each_node method to Hash's #each_key method. For each key in the
+# hash, the associated value is an array of the node's child nodes. This
+# choice in turn leads to our implementation of the required #tsort_each_child
+# method, which fetches the array of child nodes and then iterates over that
+# array using the user-supplied block.
+#
+# require 'bundler/vendor/tsort/lib/tsort'
+#
+# class Hash
+# include Bundler::TSort
+# alias tsort_each_node each_key
+# def tsort_each_child(node, &block)
+# fetch(node).each(&block)
+# end
+# end
+#
+# {1=>[2, 3], 2=>[3], 3=>[], 4=>[]}.tsort
+# #=> [3, 2, 1, 4]
+#
+# {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}.strongly_connected_components
+# #=> [[4], [2, 3], [1]]
+#
+# == A More Realistic Example
+#
+# A very simple `make' like tool can be implemented as follows:
+#
+# require 'bundler/vendor/tsort/lib/tsort'
+#
+# class Make
+# def initialize
+# @dep = {}
+# @dep.default = []
+# end
+#
+# def rule(outputs, inputs=[], &block)
+# triple = [outputs, inputs, block]
+# outputs.each {|f| @dep[f] = [triple]}
+# @dep[triple] = inputs
+# end
+#
+# def build(target)
+# each_strongly_connected_component_from(target) {|ns|
+# if ns.length != 1
+# fs = ns.delete_if {|n| Array === n}
+# raise Bundler::TSort::Cyclic.new("cyclic dependencies: #{fs.join ', '}")
+# end
+# n = ns.first
+# if Array === n
+# outputs, inputs, block = n
+# inputs_time = inputs.map {|f| File.mtime f}.max
+# begin
+# outputs_time = outputs.map {|f| File.mtime f}.min
+# rescue Errno::ENOENT
+# outputs_time = nil
+# end
+# if outputs_time == nil ||
+# inputs_time != nil && outputs_time <= inputs_time
+# sleep 1 if inputs_time != nil && inputs_time.to_i == Time.now.to_i
+# block.call
+# end
+# end
+# }
+# end
+#
+# def tsort_each_child(node, &block)
+# @dep[node].each(&block)
+# end
+# include Bundler::TSort
+# end
+#
+# def command(arg)
+# print arg, "\n"
+# system arg
+# end
+#
+# m = Make.new
+# m.rule(%w[t1]) { command 'date > t1' }
+# m.rule(%w[t2]) { command 'date > t2' }
+# m.rule(%w[t3]) { command 'date > t3' }
+# m.rule(%w[t4], %w[t1 t3]) { command 'cat t1 t3 > t4' }
+# m.rule(%w[t5], %w[t4 t2]) { command 'cat t4 t2 > t5' }
+# m.build('t5')
+#
+# == Bugs
+#
+# * 'tsort.rb' is wrong name because this library uses
+# Tarjan's algorithm for strongly connected components.
+# Although 'strongly_connected_components.rb' is correct but too long.
+#
+# == References
+#
+# R. E. Tarjan, "Depth First Search and Linear Graph Algorithms",
+# <em>SIAM Journal on Computing</em>, Vol. 1, No. 2, pp. 146-160, June 1972.
+#
+
+module Bundler::TSort
+
+ VERSION = "0.2.0"
+
+ class Cyclic < StandardError
+ end
+
+ # Returns a topologically sorted array of nodes.
+ # The array is sorted from children to parents, i.e.
+ # the first element has no child and the last node has no parent.
+ #
+ # If there is a cycle, Bundler::TSort::Cyclic is raised.
+ #
+ # class G
+ # include Bundler::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # p graph.tsort #=> [4, 2, 3, 1]
+ #
+ # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
+ # p graph.tsort # raises Bundler::TSort::Cyclic
+ #
+ def tsort
+ each_node = method(:tsort_each_node)
+ each_child = method(:tsort_each_child)
+ Bundler::TSort.tsort(each_node, each_child)
+ end
+
+ # Returns a topologically sorted array of nodes.
+ # The array is sorted from children to parents, i.e.
+ # the first element has no child and the last node has no parent.
+ #
+ # The graph is represented by _each_node_ and _each_child_.
+ # _each_node_ should have +call+ method which yields for each node in the graph.
+ # _each_child_ should have +call+ method which takes a node argument and yields for each child node.
+ #
+ # If there is a cycle, Bundler::TSort::Cyclic is raised.
+ #
+ # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # p Bundler::TSort.tsort(each_node, each_child) #=> [4, 2, 3, 1]
+ #
+ # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # p Bundler::TSort.tsort(each_node, each_child) # raises Bundler::TSort::Cyclic
+ #
+ def self.tsort(each_node, each_child)
+ tsort_each(each_node, each_child).to_a
+ end
+
+ # The iterator version of the #tsort method.
+ # <tt><em>obj</em>.tsort_each</tt> is similar to <tt><em>obj</em>.tsort.each</tt>, but
+ # modification of _obj_ during the iteration may lead to unexpected results.
+ #
+ # #tsort_each returns +nil+.
+ # If there is a cycle, Bundler::TSort::Cyclic is raised.
+ #
+ # class G
+ # include Bundler::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # graph.tsort_each {|n| p n }
+ # #=> 4
+ # # 2
+ # # 3
+ # # 1
+ #
+ def tsort_each(&block) # :yields: node
+ each_node = method(:tsort_each_node)
+ each_child = method(:tsort_each_child)
+ Bundler::TSort.tsort_each(each_node, each_child, &block)
+ end
+
+ # The iterator version of the Bundler::TSort.tsort method.
+ #
+ # The graph is represented by _each_node_ and _each_child_.
+ # _each_node_ should have +call+ method which yields for each node in the graph.
+ # _each_child_ should have +call+ method which takes a node argument and yields for each child node.
+ #
+ # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # Bundler::TSort.tsort_each(each_node, each_child) {|n| p n }
+ # #=> 4
+ # # 2
+ # # 3
+ # # 1
+ #
+ def self.tsort_each(each_node, each_child) # :yields: node
+ return to_enum(__method__, each_node, each_child) unless block_given?
+
+ each_strongly_connected_component(each_node, each_child) {|component|
+ if component.size == 1
+ yield component.first
+ else
+ raise Cyclic.new("topological sort failed: #{component.inspect}")
+ end
+ }
+ end
+
+ # Returns strongly connected components as an array of arrays of nodes.
+ # The array is sorted from children to parents.
+ # Each elements of the array represents a strongly connected component.
+ #
+ # class G
+ # include Bundler::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # p graph.strongly_connected_components #=> [[4], [2], [3], [1]]
+ #
+ # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
+ # p graph.strongly_connected_components #=> [[4], [2, 3], [1]]
+ #
+ def strongly_connected_components
+ each_node = method(:tsort_each_node)
+ each_child = method(:tsort_each_child)
+ Bundler::TSort.strongly_connected_components(each_node, each_child)
+ end
+
+ # Returns strongly connected components as an array of arrays of nodes.
+ # The array is sorted from children to parents.
+ # Each elements of the array represents a strongly connected component.
+ #
+ # The graph is represented by _each_node_ and _each_child_.
+ # _each_node_ should have +call+ method which yields for each node in the graph.
+ # _each_child_ should have +call+ method which takes a node argument and yields for each child node.
+ #
+ # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # p Bundler::TSort.strongly_connected_components(each_node, each_child)
+ # #=> [[4], [2], [3], [1]]
+ #
+ # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # p Bundler::TSort.strongly_connected_components(each_node, each_child)
+ # #=> [[4], [2, 3], [1]]
+ #
+ def self.strongly_connected_components(each_node, each_child)
+ each_strongly_connected_component(each_node, each_child).to_a
+ end
+
+ # The iterator version of the #strongly_connected_components method.
+ # <tt><em>obj</em>.each_strongly_connected_component</tt> is similar to
+ # <tt><em>obj</em>.strongly_connected_components.each</tt>, but
+ # modification of _obj_ during the iteration may lead to unexpected results.
+ #
+ # #each_strongly_connected_component returns +nil+.
+ #
+ # class G
+ # include Bundler::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # graph.each_strongly_connected_component {|scc| p scc }
+ # #=> [4]
+ # # [2]
+ # # [3]
+ # # [1]
+ #
+ # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
+ # graph.each_strongly_connected_component {|scc| p scc }
+ # #=> [4]
+ # # [2, 3]
+ # # [1]
+ #
+ def each_strongly_connected_component(&block) # :yields: nodes
+ each_node = method(:tsort_each_node)
+ each_child = method(:tsort_each_child)
+ Bundler::TSort.each_strongly_connected_component(each_node, each_child, &block)
+ end
+
+ # The iterator version of the Bundler::TSort.strongly_connected_components method.
+ #
+ # The graph is represented by _each_node_ and _each_child_.
+ # _each_node_ should have +call+ method which yields for each node in the graph.
+ # _each_child_ should have +call+ method which takes a node argument and yields for each child node.
+ #
+ # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # Bundler::TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc }
+ # #=> [4]
+ # # [2]
+ # # [3]
+ # # [1]
+ #
+ # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # Bundler::TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc }
+ # #=> [4]
+ # # [2, 3]
+ # # [1]
+ #
+ def self.each_strongly_connected_component(each_node, each_child) # :yields: nodes
+ return to_enum(__method__, each_node, each_child) unless block_given?
+
+ id_map = {}
+ stack = []
+ each_node.call {|node|
+ unless id_map.include? node
+ each_strongly_connected_component_from(node, each_child, id_map, stack) {|c|
+ yield c
+ }
+ end
+ }
+ nil
+ end
+
+ # Iterates over strongly connected component in the subgraph reachable from
+ # _node_.
+ #
+ # Return value is unspecified.
+ #
+ # #each_strongly_connected_component_from doesn't call #tsort_each_node.
+ #
+ # class G
+ # include Bundler::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # graph.each_strongly_connected_component_from(2) {|scc| p scc }
+ # #=> [4]
+ # # [2]
+ #
+ # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
+ # graph.each_strongly_connected_component_from(2) {|scc| p scc }
+ # #=> [4]
+ # # [2, 3]
+ #
+ def each_strongly_connected_component_from(node, id_map={}, stack=[], &block) # :yields: nodes
+ Bundler::TSort.each_strongly_connected_component_from(node, method(:tsort_each_child), id_map, stack, &block)
+ end
+
+ # Iterates over strongly connected components in a graph.
+ # The graph is represented by _node_ and _each_child_.
+ #
+ # _node_ is the first node.
+ # _each_child_ should have +call+ method which takes a node argument
+ # and yields for each child node.
+ #
+ # Return value is unspecified.
+ #
+ # #Bundler::TSort.each_strongly_connected_component_from is a class method and
+ # it doesn't need a class to represent a graph which includes Bundler::TSort.
+ #
+ # graph = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
+ # each_child = lambda {|n, &b| graph[n].each(&b) }
+ # Bundler::TSort.each_strongly_connected_component_from(1, each_child) {|scc|
+ # p scc
+ # }
+ # #=> [4]
+ # # [2, 3]
+ # # [1]
+ #
+ def self.each_strongly_connected_component_from(node, each_child, id_map={}, stack=[]) # :yields: nodes
+ return to_enum(__method__, node, each_child, id_map, stack) unless block_given?
+
+ minimum_id = node_id = id_map[node] = id_map.size
+ stack_length = stack.length
+ stack << node
+
+ each_child.call(node) {|child|
+ if id_map.include? child
+ child_id = id_map[child]
+ minimum_id = child_id if child_id && child_id < minimum_id
+ else
+ sub_minimum_id =
+ each_strongly_connected_component_from(child, each_child, id_map, stack) {|c|
+ yield c
+ }
+ minimum_id = sub_minimum_id if sub_minimum_id < minimum_id
+ end
+ }
+
+ if node_id == minimum_id
+ component = stack.slice!(stack_length .. -1)
+ component.each {|n| id_map[n] = nil}
+ yield component
+ end
+
+ minimum_id
+ end
+
+ # Should be implemented by a extended class.
+ #
+ # #tsort_each_node is used to iterate for all nodes over a graph.
+ #
+ def tsort_each_node # :yields: node
+ raise NotImplementedError.new
+ end
+
+ # Should be implemented by a extended class.
+ #
+ # #tsort_each_child is used to iterate for child nodes of _node_.
+ #
+ def tsort_each_child(node) # :yields: child
+ raise NotImplementedError.new
+ end
+end
diff --git a/lib/bundler/vendor/uri/lib/uri.rb b/lib/bundler/vendor/uri/lib/uri.rb
new file mode 100644
index 0000000000..57b380c480
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: false
+# Bundler::URI is a module providing classes to handle Uniform Resource Identifiers
+# (RFC2396[https://www.rfc-editor.org/rfc/rfc2396]).
+#
+# == Features
+#
+# * Uniform way of handling URIs.
+# * Flexibility to introduce custom Bundler::URI schemes.
+# * Flexibility to have an alternate Bundler::URI::Parser (or just different patterns
+# and regexp's).
+#
+# == Basic example
+#
+# require 'bundler/vendor/uri/lib/uri'
+#
+# uri = Bundler::URI("http://foo.com/posts?id=30&limit=5#time=1305298413")
+# #=> #<Bundler::URI::HTTP http://foo.com/posts?id=30&limit=5#time=1305298413>
+#
+# uri.scheme #=> "http"
+# uri.host #=> "foo.com"
+# uri.path #=> "/posts"
+# uri.query #=> "id=30&limit=5"
+# uri.fragment #=> "time=1305298413"
+#
+# uri.to_s #=> "http://foo.com/posts?id=30&limit=5#time=1305298413"
+#
+# == Adding custom URIs
+#
+# module Bundler::URI
+# class RSYNC < Generic
+# DEFAULT_PORT = 873
+# end
+# register_scheme 'RSYNC', RSYNC
+# end
+# #=> Bundler::URI::RSYNC
+#
+# Bundler::URI.scheme_list
+# #=> {"FILE"=>Bundler::URI::File, "FTP"=>Bundler::URI::FTP, "HTTP"=>Bundler::URI::HTTP,
+# # "HTTPS"=>Bundler::URI::HTTPS, "LDAP"=>Bundler::URI::LDAP, "LDAPS"=>Bundler::URI::LDAPS,
+# # "MAILTO"=>Bundler::URI::MailTo, "RSYNC"=>Bundler::URI::RSYNC}
+#
+# uri = Bundler::URI("rsync://rsync.foo.com")
+# #=> #<Bundler::URI::RSYNC rsync://rsync.foo.com>
+#
+# == RFC References
+#
+# A good place to view an RFC spec is http://www.ietf.org/rfc.html.
+#
+# Here is a list of all related RFC's:
+# - RFC822[https://www.rfc-editor.org/rfc/rfc822]
+# - RFC1738[https://www.rfc-editor.org/rfc/rfc1738]
+# - RFC2255[https://www.rfc-editor.org/rfc/rfc2255]
+# - RFC2368[https://www.rfc-editor.org/rfc/rfc2368]
+# - RFC2373[https://www.rfc-editor.org/rfc/rfc2373]
+# - RFC2396[https://www.rfc-editor.org/rfc/rfc2396]
+# - RFC2732[https://www.rfc-editor.org/rfc/rfc2732]
+# - RFC3986[https://www.rfc-editor.org/rfc/rfc3986]
+#
+# == Class tree
+#
+# - Bundler::URI::Generic (in uri/generic.rb)
+# - Bundler::URI::File - (in uri/file.rb)
+# - Bundler::URI::FTP - (in uri/ftp.rb)
+# - Bundler::URI::HTTP - (in uri/http.rb)
+# - Bundler::URI::HTTPS - (in uri/https.rb)
+# - Bundler::URI::LDAP - (in uri/ldap.rb)
+# - Bundler::URI::LDAPS - (in uri/ldaps.rb)
+# - Bundler::URI::MailTo - (in uri/mailto.rb)
+# - Bundler::URI::Parser - (in uri/common.rb)
+# - Bundler::URI::REGEXP - (in uri/common.rb)
+# - Bundler::URI::REGEXP::PATTERN - (in uri/common.rb)
+# - Bundler::URI::Util - (in uri/common.rb)
+# - Bundler::URI::Error - (in uri/common.rb)
+# - Bundler::URI::InvalidURIError - (in uri/common.rb)
+# - Bundler::URI::InvalidComponentError - (in uri/common.rb)
+# - Bundler::URI::BadURIError - (in uri/common.rb)
+#
+# == Copyright Info
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# Documentation::
+# Akira Yamada <akira@ruby-lang.org>
+# Dmitry V. Sabanin <sdmitry@lrn.ru>
+# Vincent Batts <vbatts@hashbangbash.com>
+# License::
+# Copyright (c) 2001 akira yamada <akira@ruby-lang.org>
+# You can redistribute it and/or modify it under the same term as Ruby.
+#
+
+module Bundler::URI
+end
+
+require_relative 'uri/version'
+require_relative 'uri/common'
+require_relative 'uri/generic'
+require_relative 'uri/file'
+require_relative 'uri/ftp'
+require_relative 'uri/http'
+require_relative 'uri/https'
+require_relative 'uri/ldap'
+require_relative 'uri/ldaps'
+require_relative 'uri/mailto'
+require_relative 'uri/ws'
+require_relative 'uri/wss'
diff --git a/lib/bundler/vendor/uri/lib/uri/common.rb b/lib/bundler/vendor/uri/lib/uri/common.rb
new file mode 100644
index 0000000000..38339119c5
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/common.rb
@@ -0,0 +1,922 @@
+# frozen_string_literal: true
+#--
+# = uri/common.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License::
+# You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Bundler::URI for general documentation
+#
+
+require_relative "rfc2396_parser"
+require_relative "rfc3986_parser"
+
+module Bundler::URI
+ # The default parser instance for RFC 2396.
+ RFC2396_PARSER = RFC2396_Parser.new
+ Ractor.make_shareable(RFC2396_PARSER) if defined?(Ractor)
+
+ # The default parser instance for RFC 3986.
+ RFC3986_PARSER = RFC3986_Parser.new
+ Ractor.make_shareable(RFC3986_PARSER) if defined?(Ractor)
+
+ # The default parser instance.
+ DEFAULT_PARSER = RFC3986_PARSER
+ Ractor.make_shareable(DEFAULT_PARSER) if defined?(Ractor)
+
+ # Set the default parser instance.
+ def self.parser=(parser = RFC3986_PARSER)
+ remove_const(:Parser) if defined?(::Bundler::URI::Parser)
+ const_set("Parser", parser.class)
+
+ remove_const(:PARSER) if defined?(::Bundler::URI::PARSER)
+ const_set("PARSER", parser)
+
+ remove_const(:REGEXP) if defined?(::Bundler::URI::REGEXP)
+ remove_const(:PATTERN) if defined?(::Bundler::URI::PATTERN)
+ if Parser == RFC2396_Parser
+ const_set("REGEXP", Bundler::URI::RFC2396_REGEXP)
+ const_set("PATTERN", Bundler::URI::RFC2396_REGEXP::PATTERN)
+ end
+
+ Parser.new.regexp.each_pair do |sym, str|
+ remove_const(sym) if const_defined?(sym, false)
+ const_set(sym, str)
+ end
+ end
+ self.parser = RFC3986_PARSER
+
+ def self.const_missing(const) # :nodoc:
+ if const == :REGEXP
+ warn "Bundler::URI::REGEXP is obsolete. Use Bundler::URI::RFC2396_REGEXP explicitly.", uplevel: 1 if $VERBOSE
+ Bundler::URI::RFC2396_REGEXP
+ elsif value = RFC2396_PARSER.regexp[const]
+ warn "Bundler::URI::#{const} is obsolete. Use Bundler::URI::RFC2396_PARSER.regexp[#{const.inspect}] explicitly.", uplevel: 1 if $VERBOSE
+ value
+ elsif value = RFC2396_Parser.const_get(const)
+ warn "Bundler::URI::#{const} is obsolete. Use Bundler::URI::RFC2396_Parser::#{const} explicitly.", uplevel: 1 if $VERBOSE
+ value
+ else
+ super
+ end
+ end
+
+ module Util # :nodoc:
+ def make_components_hash(klass, array_hash)
+ tmp = {}
+ if array_hash.kind_of?(Array) &&
+ array_hash.size == klass.component.size - 1
+ klass.component[1..-1].each_index do |i|
+ begin
+ tmp[klass.component[i + 1]] = array_hash[i].clone
+ rescue TypeError
+ tmp[klass.component[i + 1]] = array_hash[i]
+ end
+ end
+
+ elsif array_hash.kind_of?(Hash)
+ array_hash.each do |key, value|
+ begin
+ tmp[key] = value.clone
+ rescue TypeError
+ tmp[key] = value
+ end
+ end
+ else
+ raise ArgumentError,
+ "expected Array of or Hash of components of #{klass} (#{klass.component[1..-1].join(', ')})"
+ end
+ tmp[:scheme] = klass.to_s.sub(/\A.*::/, '').downcase
+
+ return tmp
+ end
+ module_function :make_components_hash
+ end
+
+ module Schemes # :nodoc:
+ class << self
+ ReservedChars = ".+-"
+ EscapedChars = "\u01C0\u01C1\u01C2"
+ # Use Lo category chars as escaped chars for TruffleRuby, which
+ # does not allow Symbol categories as identifiers.
+
+ def escape(name)
+ unless name and name.ascii_only?
+ return nil
+ end
+ name.upcase.tr(ReservedChars, EscapedChars)
+ end
+
+ def unescape(name)
+ name.tr(EscapedChars, ReservedChars).encode(Encoding::US_ASCII).upcase
+ end
+
+ def find(name)
+ const_get(name, false) if name and const_defined?(name, false)
+ end
+
+ def register(name, klass)
+ unless scheme = escape(name)
+ raise ArgumentError, "invalid character as scheme - #{name}"
+ end
+ const_set(scheme, klass)
+ end
+
+ def list
+ constants.map { |name|
+ [unescape(name.to_s), const_get(name)]
+ }.to_h
+ end
+ end
+ end
+ private_constant :Schemes
+
+ # Registers the given +klass+ as the class to be instantiated
+ # when parsing a \Bundler::URI with the given +scheme+:
+ #
+ # Bundler::URI.register_scheme('MS_SEARCH', Bundler::URI::Generic) # => Bundler::URI::Generic
+ # Bundler::URI.scheme_list['MS_SEARCH'] # => Bundler::URI::Generic
+ #
+ # Note that after calling String#upcase on +scheme+, it must be a valid
+ # constant name.
+ def self.register_scheme(scheme, klass)
+ Schemes.register(scheme, klass)
+ end
+
+ # Returns a hash of the defined schemes:
+ #
+ # Bundler::URI.scheme_list
+ # # =>
+ # {"MAILTO"=>Bundler::URI::MailTo,
+ # "LDAPS"=>Bundler::URI::LDAPS,
+ # "WS"=>Bundler::URI::WS,
+ # "HTTP"=>Bundler::URI::HTTP,
+ # "HTTPS"=>Bundler::URI::HTTPS,
+ # "LDAP"=>Bundler::URI::LDAP,
+ # "FILE"=>Bundler::URI::File,
+ # "FTP"=>Bundler::URI::FTP}
+ #
+ # Related: Bundler::URI.register_scheme.
+ def self.scheme_list
+ Schemes.list
+ end
+
+ # :stopdoc:
+ INITIAL_SCHEMES = scheme_list
+ private_constant :INITIAL_SCHEMES
+ Ractor.make_shareable(INITIAL_SCHEMES) if defined?(Ractor)
+ # :startdoc:
+
+ # Returns a new object constructed from the given +scheme+, +arguments+,
+ # and +default+:
+ #
+ # - The new object is an instance of <tt>Bundler::URI.scheme_list[scheme.upcase]</tt>.
+ # - The object is initialized by calling the class initializer
+ # using +scheme+ and +arguments+.
+ # See Bundler::URI::Generic.new.
+ #
+ # Examples:
+ #
+ # values = ['john.doe', 'www.example.com', '123', nil, '/forum/questions/', nil, 'tag=networking&order=newest', 'top']
+ # Bundler::URI.for('https', *values)
+ # # => #<Bundler::URI::HTTPS https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top>
+ # Bundler::URI.for('foo', *values, default: Bundler::URI::HTTP)
+ # # => #<Bundler::URI::HTTP foo://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top>
+ #
+ def self.for(scheme, *arguments, default: Generic)
+ const_name = Schemes.escape(scheme)
+
+ uri_class = INITIAL_SCHEMES[const_name]
+ uri_class ||= Schemes.find(const_name)
+ uri_class ||= default
+
+ return uri_class.new(scheme, *arguments)
+ end
+
+ #
+ # Base class for all Bundler::URI exceptions.
+ #
+ class Error < StandardError; end
+ #
+ # Not a Bundler::URI.
+ #
+ class InvalidURIError < Error; end
+ #
+ # Not a Bundler::URI component.
+ #
+ class InvalidComponentError < Error; end
+ #
+ # Bundler::URI is valid, bad usage is not.
+ #
+ class BadURIError < Error; end
+
+ # Returns a 9-element array representing the parts of the \Bundler::URI
+ # formed from the string +uri+;
+ # each array element is a string or +nil+:
+ #
+ # names = %w[scheme userinfo host port registry path opaque query fragment]
+ # values = Bundler::URI.split('https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top')
+ # names.zip(values)
+ # # =>
+ # [["scheme", "https"],
+ # ["userinfo", "john.doe"],
+ # ["host", "www.example.com"],
+ # ["port", "123"],
+ # ["registry", nil],
+ # ["path", "/forum/questions/"],
+ # ["opaque", nil],
+ # ["query", "tag=networking&order=newest"],
+ # ["fragment", "top"]]
+ #
+ def self.split(uri)
+ PARSER.split(uri)
+ end
+
+ # Returns a new \Bundler::URI object constructed from the given string +uri+:
+ #
+ # Bundler::URI.parse('https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top')
+ # # => #<Bundler::URI::HTTPS https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top>
+ # Bundler::URI.parse('http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top')
+ # # => #<Bundler::URI::HTTP http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top>
+ #
+ # It's recommended to first Bundler::URI::RFC2396_PARSER.escape string +uri+
+ # if it may contain invalid Bundler::URI characters.
+ #
+ def self.parse(uri)
+ PARSER.parse(uri)
+ end
+
+ # Merges the given Bundler::URI strings +str+
+ # per {RFC 2396}[https://www.rfc-editor.org/rfc/rfc2396.html].
+ #
+ # Each string in +str+ is converted to an
+ # {RFC3986 Bundler::URI}[https://www.rfc-editor.org/rfc/rfc3986.html] before being merged.
+ #
+ # Examples:
+ #
+ # Bundler::URI.join("http://example.com/","main.rbx")
+ # # => #<Bundler::URI::HTTP http://example.com/main.rbx>
+ #
+ # Bundler::URI.join('http://example.com', 'foo')
+ # # => #<Bundler::URI::HTTP http://example.com/foo>
+ #
+ # Bundler::URI.join('http://example.com', '/foo', '/bar')
+ # # => #<Bundler::URI::HTTP http://example.com/bar>
+ #
+ # Bundler::URI.join('http://example.com', '/foo', 'bar')
+ # # => #<Bundler::URI::HTTP http://example.com/bar>
+ #
+ # Bundler::URI.join('http://example.com', '/foo/', 'bar')
+ # # => #<Bundler::URI::HTTP http://example.com/foo/bar>
+ #
+ def self.join(*str)
+ DEFAULT_PARSER.join(*str)
+ end
+
+ #
+ # == Synopsis
+ #
+ # Bundler::URI::extract(str[, schemes][,&blk])
+ #
+ # == Args
+ #
+ # +str+::
+ # String to extract URIs from.
+ # +schemes+::
+ # Limit Bundler::URI matching to specific schemes.
+ #
+ # == Description
+ #
+ # Extracts URIs from a string. If block given, iterates through all matched URIs.
+ # Returns nil if block given or array with matches.
+ #
+ # == Usage
+ #
+ # require "bundler/vendor/uri/lib/uri"
+ #
+ # Bundler::URI.extract("text here http://foo.example.org/bla and here mailto:test@example.com and here also.")
+ # # => ["http://foo.example.com/bla", "mailto:test@example.com"]
+ #
+ def self.extract(str, schemes = nil, &block) # :nodoc:
+ warn "Bundler::URI.extract is obsolete", uplevel: 1 if $VERBOSE
+ PARSER.extract(str, schemes, &block)
+ end
+
+ #
+ # == Synopsis
+ #
+ # Bundler::URI::regexp([match_schemes])
+ #
+ # == Args
+ #
+ # +match_schemes+::
+ # Array of schemes. If given, resulting regexp matches to URIs
+ # whose scheme is one of the match_schemes.
+ #
+ # == Description
+ #
+ # Returns a Regexp object which matches to Bundler::URI-like strings.
+ # The Regexp object returned by this method includes arbitrary
+ # number of capture group (parentheses). Never rely on its number.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # # extract first Bundler::URI from html_string
+ # html_string.slice(Bundler::URI.regexp)
+ #
+ # # remove ftp URIs
+ # html_string.sub(Bundler::URI.regexp(['ftp']), '')
+ #
+ # # You should not rely on the number of parentheses
+ # html_string.scan(Bundler::URI.regexp) do |*matches|
+ # p $&
+ # end
+ #
+ def self.regexp(schemes = nil)# :nodoc:
+ warn "Bundler::URI.regexp is obsolete", uplevel: 1 if $VERBOSE
+ PARSER.make_regexp(schemes)
+ end
+
+ TBLENCWWWCOMP_ = {} # :nodoc:
+ 256.times do |i|
+ TBLENCWWWCOMP_[-i.chr] = -('%%%02X' % i)
+ end
+ TBLENCURICOMP_ = TBLENCWWWCOMP_.dup.freeze # :nodoc:
+ TBLENCWWWCOMP_[' '] = '+'
+ TBLENCWWWCOMP_.freeze
+ TBLDECWWWCOMP_ = {} # :nodoc:
+ 256.times do |i|
+ h, l = i>>4, i&15
+ TBLDECWWWCOMP_[-('%%%X%X' % [h, l])] = -i.chr
+ TBLDECWWWCOMP_[-('%%%x%X' % [h, l])] = -i.chr
+ TBLDECWWWCOMP_[-('%%%X%x' % [h, l])] = -i.chr
+ TBLDECWWWCOMP_[-('%%%x%x' % [h, l])] = -i.chr
+ end
+ TBLDECWWWCOMP_['+'] = ' '
+ TBLDECWWWCOMP_.freeze
+
+ # Returns a URL-encoded string derived from the given string +str+.
+ #
+ # The returned string:
+ #
+ # - Preserves:
+ #
+ # - Characters <tt>'*'</tt>, <tt>'.'</tt>, <tt>'-'</tt>, and <tt>'_'</tt>.
+ # - Character in ranges <tt>'a'..'z'</tt>, <tt>'A'..'Z'</tt>,
+ # and <tt>'0'..'9'</tt>.
+ #
+ # Example:
+ #
+ # Bundler::URI.encode_www_form_component('*.-_azAZ09')
+ # # => "*.-_azAZ09"
+ #
+ # - Converts:
+ #
+ # - Character <tt>' '</tt> to character <tt>'+'</tt>.
+ # - Any other character to "percent notation";
+ # the percent notation for character <i>c</i> is <tt>'%%%X' % c.ord</tt>.
+ #
+ # Example:
+ #
+ # Bundler::URI.encode_www_form_component('Here are some punctuation characters: ,;?:')
+ # # => "Here+are+some+punctuation+characters%3A+%2C%3B%3F%3A"
+ #
+ # Encoding:
+ #
+ # - If +str+ has encoding Encoding::ASCII_8BIT, argument +enc+ is ignored.
+ # - Otherwise +str+ is converted first to Encoding::UTF_8
+ # (with suitable character replacements),
+ # and then to encoding +enc+.
+ #
+ # In either case, the returned string has forced encoding Encoding::US_ASCII.
+ #
+ # Related: Bundler::URI.encode_uri_component (encodes <tt>' '</tt> as <tt>'%20'</tt>).
+ def self.encode_www_form_component(str, enc=nil)
+ _encode_uri_component(/[^*\-.0-9A-Z_a-z]/, TBLENCWWWCOMP_, str, enc)
+ end
+
+ # Returns a string decoded from the given \URL-encoded string +str+.
+ #
+ # The given string is first encoded as Encoding::ASCII-8BIT (using String#b),
+ # then decoded (as below), and finally force-encoded to the given encoding +enc+.
+ #
+ # The returned string:
+ #
+ # - Preserves:
+ #
+ # - Characters <tt>'*'</tt>, <tt>'.'</tt>, <tt>'-'</tt>, and <tt>'_'</tt>.
+ # - Character in ranges <tt>'a'..'z'</tt>, <tt>'A'..'Z'</tt>,
+ # and <tt>'0'..'9'</tt>.
+ #
+ # Example:
+ #
+ # Bundler::URI.decode_www_form_component('*.-_azAZ09')
+ # # => "*.-_azAZ09"
+ #
+ # - Converts:
+ #
+ # - Character <tt>'+'</tt> to character <tt>' '</tt>.
+ # - Each "percent notation" to an ASCII character.
+ #
+ # Example:
+ #
+ # Bundler::URI.decode_www_form_component('Here+are+some+punctuation+characters%3A+%2C%3B%3F%3A')
+ # # => "Here are some punctuation characters: ,;?:"
+ #
+ # Related: Bundler::URI.decode_uri_component (preserves <tt>'+'</tt>).
+ def self.decode_www_form_component(str, enc=Encoding::UTF_8)
+ _decode_uri_component(/\+|%\h\h/, str, enc)
+ end
+
+ # Like Bundler::URI.encode_www_form_component, except that <tt>' '</tt> (space)
+ # is encoded as <tt>'%20'</tt> (instead of <tt>'+'</tt>).
+ def self.encode_uri_component(str, enc=nil)
+ _encode_uri_component(/[^*\-.0-9A-Z_a-z]/, TBLENCURICOMP_, str, enc)
+ end
+
+ # Like Bundler::URI.decode_www_form_component, except that <tt>'+'</tt> is preserved.
+ def self.decode_uri_component(str, enc=Encoding::UTF_8)
+ _decode_uri_component(/%\h\h/, str, enc)
+ end
+
+ # Returns a string derived from the given string +str+ with
+ # Bundler::URI-encoded characters matching +regexp+ according to +table+.
+ def self._encode_uri_component(regexp, table, str, enc)
+ str = str.to_s.dup
+ if str.encoding != Encoding::ASCII_8BIT
+ if enc && enc != Encoding::ASCII_8BIT
+ str.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace)
+ str.encode!(enc, fallback: ->(x){"&##{x.ord};"})
+ end
+ str.force_encoding(Encoding::ASCII_8BIT)
+ end
+ str.gsub!(regexp, table)
+ str.force_encoding(Encoding::US_ASCII)
+ end
+ private_class_method :_encode_uri_component
+
+ # Returns a string decoding characters matching +regexp+ from the
+ # given \URL-encoded string +str+.
+ def self._decode_uri_component(regexp, str, enc)
+ raise ArgumentError, "invalid %-encoding (#{str})" if /%(?!\h\h)/.match?(str)
+ str.b.gsub(regexp, TBLDECWWWCOMP_).force_encoding(enc)
+ end
+ private_class_method :_decode_uri_component
+
+ # Returns a URL-encoded string derived from the given
+ # {Enumerable}[rdoc-ref:Enumerable@Enumerable+in+Ruby+Classes]
+ # +enum+.
+ #
+ # The result is suitable for use as form data
+ # for an \HTTP request whose <tt>Content-Type</tt> is
+ # <tt>'application/x-www-form-urlencoded'</tt>.
+ #
+ # The returned string consists of the elements of +enum+,
+ # each converted to one or more URL-encoded strings,
+ # and all joined with character <tt>'&'</tt>.
+ #
+ # Simple examples:
+ #
+ # Bundler::URI.encode_www_form([['foo', 0], ['bar', 1], ['baz', 2]])
+ # # => "foo=0&bar=1&baz=2"
+ # Bundler::URI.encode_www_form({foo: 0, bar: 1, baz: 2})
+ # # => "foo=0&bar=1&baz=2"
+ #
+ # The returned string is formed using method Bundler::URI.encode_www_form_component,
+ # which converts certain characters:
+ #
+ # Bundler::URI.encode_www_form('f#o': '/', 'b-r': '$', 'b z': '@')
+ # # => "f%23o=%2F&b-r=%24&b+z=%40"
+ #
+ # When +enum+ is Array-like, each element +ele+ is converted to a field:
+ #
+ # - If +ele+ is an array of two or more elements,
+ # the field is formed from its first two elements
+ # (and any additional elements are ignored):
+ #
+ # name = Bundler::URI.encode_www_form_component(ele[0], enc)
+ # value = Bundler::URI.encode_www_form_component(ele[1], enc)
+ # "#{name}=#{value}"
+ #
+ # Examples:
+ #
+ # Bundler::URI.encode_www_form([%w[foo bar], %w[baz bat bah]])
+ # # => "foo=bar&baz=bat"
+ # Bundler::URI.encode_www_form([['foo', 0], ['bar', :baz, 'bat']])
+ # # => "foo=0&bar=baz"
+ #
+ # - If +ele+ is an array of one element,
+ # the field is formed from <tt>ele[0]</tt>:
+ #
+ # Bundler::URI.encode_www_form_component(ele[0])
+ #
+ # Example:
+ #
+ # Bundler::URI.encode_www_form([['foo'], [:bar], [0]])
+ # # => "foo&bar&0"
+ #
+ # - Otherwise the field is formed from +ele+:
+ #
+ # Bundler::URI.encode_www_form_component(ele)
+ #
+ # Example:
+ #
+ # Bundler::URI.encode_www_form(['foo', :bar, 0])
+ # # => "foo&bar&0"
+ #
+ # The elements of an Array-like +enum+ may be mixture:
+ #
+ # Bundler::URI.encode_www_form([['foo', 0], ['bar', 1, 2], ['baz'], :bat])
+ # # => "foo=0&bar=1&baz&bat"
+ #
+ # When +enum+ is Hash-like,
+ # each +key+/+value+ pair is converted to one or more fields:
+ #
+ # - If +value+ is
+ # {Array-convertible}[rdoc-ref:implicit_conversion.rdoc@Array-Convertible+Objects],
+ # each element +ele+ in +value+ is paired with +key+ to form a field:
+ #
+ # name = Bundler::URI.encode_www_form_component(key, enc)
+ # value = Bundler::URI.encode_www_form_component(ele, enc)
+ # "#{name}=#{value}"
+ #
+ # Example:
+ #
+ # Bundler::URI.encode_www_form({foo: [:bar, 1], baz: [:bat, :bam, 2]})
+ # # => "foo=bar&foo=1&baz=bat&baz=bam&baz=2"
+ #
+ # - Otherwise, +key+ and +value+ are paired to form a field:
+ #
+ # name = Bundler::URI.encode_www_form_component(key, enc)
+ # value = Bundler::URI.encode_www_form_component(value, enc)
+ # "#{name}=#{value}"
+ #
+ # Example:
+ #
+ # Bundler::URI.encode_www_form({foo: 0, bar: 1, baz: 2})
+ # # => "foo=0&bar=1&baz=2"
+ #
+ # The elements of a Hash-like +enum+ may be mixture:
+ #
+ # Bundler::URI.encode_www_form({foo: [0, 1], bar: 2})
+ # # => "foo=0&foo=1&bar=2"
+ #
+ def self.encode_www_form(enum, enc=nil)
+ enum.map do |k,v|
+ if v.nil?
+ encode_www_form_component(k, enc)
+ elsif v.respond_to?(:to_ary)
+ v.to_ary.map do |w|
+ str = encode_www_form_component(k, enc)
+ unless w.nil?
+ str << '='
+ str << encode_www_form_component(w, enc)
+ end
+ end.join('&')
+ else
+ str = encode_www_form_component(k, enc)
+ str << '='
+ str << encode_www_form_component(v, enc)
+ end
+ end.join('&')
+ end
+
+ # Returns name/value pairs derived from the given string +str+,
+ # which must be an ASCII string.
+ #
+ # The method may be used to decode the body of Net::HTTPResponse object +res+
+ # for which <tt>res['Content-Type']</tt> is <tt>'application/x-www-form-urlencoded'</tt>.
+ #
+ # The returned data is an array of 2-element subarrays;
+ # each subarray is a name/value pair (both are strings).
+ # Each returned string has encoding +enc+,
+ # and has had invalid characters removed via
+ # {String#scrub}[rdoc-ref:String#scrub].
+ #
+ # A simple example:
+ #
+ # Bundler::URI.decode_www_form('foo=0&bar=1&baz')
+ # # => [["foo", "0"], ["bar", "1"], ["baz", ""]]
+ #
+ # The returned strings have certain conversions,
+ # similar to those performed in Bundler::URI.decode_www_form_component:
+ #
+ # Bundler::URI.decode_www_form('f%23o=%2F&b-r=%24&b+z=%40')
+ # # => [["f#o", "/"], ["b-r", "$"], ["b z", "@"]]
+ #
+ # The given string may contain consecutive separators:
+ #
+ # Bundler::URI.decode_www_form('foo=0&&bar=1&&baz=2')
+ # # => [["foo", "0"], ["", ""], ["bar", "1"], ["", ""], ["baz", "2"]]
+ #
+ # A different separator may be specified:
+ #
+ # Bundler::URI.decode_www_form('foo=0--bar=1--baz', separator: '--')
+ # # => [["foo", "0"], ["bar", "1"], ["baz", ""]]
+ #
+ def self.decode_www_form(str, enc=Encoding::UTF_8, separator: '&', use__charset_: false, isindex: false)
+ raise ArgumentError, "the input of #{self.name}.#{__method__} must be ASCII only string" unless str.ascii_only?
+ ary = []
+ return ary if str.empty?
+ enc = Encoding.find(enc)
+ str.b.each_line(separator) do |string|
+ string.chomp!(separator)
+ key, sep, val = string.partition('=')
+ if isindex
+ if sep.empty?
+ val = key
+ key = +''
+ end
+ isindex = false
+ end
+
+ if use__charset_ and key == '_charset_' and e = get_encoding(val)
+ enc = e
+ use__charset_ = false
+ end
+
+ key.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_)
+ if val
+ val.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_)
+ else
+ val = +''
+ end
+
+ ary << [key, val]
+ end
+ ary.each do |k, v|
+ k.force_encoding(enc)
+ k.scrub!
+ v.force_encoding(enc)
+ v.scrub!
+ end
+ ary
+ end
+
+ private
+=begin command for WEB_ENCODINGS_
+ curl https://encoding.spec.whatwg.org/encodings.json|
+ ruby -rjson -e 'H={}
+ h={
+ "shift_jis"=>"Windows-31J",
+ "euc-jp"=>"cp51932",
+ "iso-2022-jp"=>"cp50221",
+ "x-mac-cyrillic"=>"macCyrillic",
+ }
+ JSON($<.read).map{|x|x["encodings"]}.flatten.each{|x|
+ Encoding.find(n=h.fetch(n=x["name"].downcase,n))rescue next
+ x["labels"].each{|y|H[y]=n}
+ }
+ puts "{"
+ H.each{|k,v|puts %[ #{k.dump}=>#{v.dump},]}
+ puts "}"
+'
+=end
+ WEB_ENCODINGS_ = {
+ "unicode-1-1-utf-8"=>"utf-8",
+ "utf-8"=>"utf-8",
+ "utf8"=>"utf-8",
+ "866"=>"ibm866",
+ "cp866"=>"ibm866",
+ "csibm866"=>"ibm866",
+ "ibm866"=>"ibm866",
+ "csisolatin2"=>"iso-8859-2",
+ "iso-8859-2"=>"iso-8859-2",
+ "iso-ir-101"=>"iso-8859-2",
+ "iso8859-2"=>"iso-8859-2",
+ "iso88592"=>"iso-8859-2",
+ "iso_8859-2"=>"iso-8859-2",
+ "iso_8859-2:1987"=>"iso-8859-2",
+ "l2"=>"iso-8859-2",
+ "latin2"=>"iso-8859-2",
+ "csisolatin3"=>"iso-8859-3",
+ "iso-8859-3"=>"iso-8859-3",
+ "iso-ir-109"=>"iso-8859-3",
+ "iso8859-3"=>"iso-8859-3",
+ "iso88593"=>"iso-8859-3",
+ "iso_8859-3"=>"iso-8859-3",
+ "iso_8859-3:1988"=>"iso-8859-3",
+ "l3"=>"iso-8859-3",
+ "latin3"=>"iso-8859-3",
+ "csisolatin4"=>"iso-8859-4",
+ "iso-8859-4"=>"iso-8859-4",
+ "iso-ir-110"=>"iso-8859-4",
+ "iso8859-4"=>"iso-8859-4",
+ "iso88594"=>"iso-8859-4",
+ "iso_8859-4"=>"iso-8859-4",
+ "iso_8859-4:1988"=>"iso-8859-4",
+ "l4"=>"iso-8859-4",
+ "latin4"=>"iso-8859-4",
+ "csisolatincyrillic"=>"iso-8859-5",
+ "cyrillic"=>"iso-8859-5",
+ "iso-8859-5"=>"iso-8859-5",
+ "iso-ir-144"=>"iso-8859-5",
+ "iso8859-5"=>"iso-8859-5",
+ "iso88595"=>"iso-8859-5",
+ "iso_8859-5"=>"iso-8859-5",
+ "iso_8859-5:1988"=>"iso-8859-5",
+ "arabic"=>"iso-8859-6",
+ "asmo-708"=>"iso-8859-6",
+ "csiso88596e"=>"iso-8859-6",
+ "csiso88596i"=>"iso-8859-6",
+ "csisolatinarabic"=>"iso-8859-6",
+ "ecma-114"=>"iso-8859-6",
+ "iso-8859-6"=>"iso-8859-6",
+ "iso-8859-6-e"=>"iso-8859-6",
+ "iso-8859-6-i"=>"iso-8859-6",
+ "iso-ir-127"=>"iso-8859-6",
+ "iso8859-6"=>"iso-8859-6",
+ "iso88596"=>"iso-8859-6",
+ "iso_8859-6"=>"iso-8859-6",
+ "iso_8859-6:1987"=>"iso-8859-6",
+ "csisolatingreek"=>"iso-8859-7",
+ "ecma-118"=>"iso-8859-7",
+ "elot_928"=>"iso-8859-7",
+ "greek"=>"iso-8859-7",
+ "greek8"=>"iso-8859-7",
+ "iso-8859-7"=>"iso-8859-7",
+ "iso-ir-126"=>"iso-8859-7",
+ "iso8859-7"=>"iso-8859-7",
+ "iso88597"=>"iso-8859-7",
+ "iso_8859-7"=>"iso-8859-7",
+ "iso_8859-7:1987"=>"iso-8859-7",
+ "sun_eu_greek"=>"iso-8859-7",
+ "csiso88598e"=>"iso-8859-8",
+ "csisolatinhebrew"=>"iso-8859-8",
+ "hebrew"=>"iso-8859-8",
+ "iso-8859-8"=>"iso-8859-8",
+ "iso-8859-8-e"=>"iso-8859-8",
+ "iso-ir-138"=>"iso-8859-8",
+ "iso8859-8"=>"iso-8859-8",
+ "iso88598"=>"iso-8859-8",
+ "iso_8859-8"=>"iso-8859-8",
+ "iso_8859-8:1988"=>"iso-8859-8",
+ "visual"=>"iso-8859-8",
+ "csisolatin6"=>"iso-8859-10",
+ "iso-8859-10"=>"iso-8859-10",
+ "iso-ir-157"=>"iso-8859-10",
+ "iso8859-10"=>"iso-8859-10",
+ "iso885910"=>"iso-8859-10",
+ "l6"=>"iso-8859-10",
+ "latin6"=>"iso-8859-10",
+ "iso-8859-13"=>"iso-8859-13",
+ "iso8859-13"=>"iso-8859-13",
+ "iso885913"=>"iso-8859-13",
+ "iso-8859-14"=>"iso-8859-14",
+ "iso8859-14"=>"iso-8859-14",
+ "iso885914"=>"iso-8859-14",
+ "csisolatin9"=>"iso-8859-15",
+ "iso-8859-15"=>"iso-8859-15",
+ "iso8859-15"=>"iso-8859-15",
+ "iso885915"=>"iso-8859-15",
+ "iso_8859-15"=>"iso-8859-15",
+ "l9"=>"iso-8859-15",
+ "iso-8859-16"=>"iso-8859-16",
+ "cskoi8r"=>"koi8-r",
+ "koi"=>"koi8-r",
+ "koi8"=>"koi8-r",
+ "koi8-r"=>"koi8-r",
+ "koi8_r"=>"koi8-r",
+ "koi8-ru"=>"koi8-u",
+ "koi8-u"=>"koi8-u",
+ "dos-874"=>"windows-874",
+ "iso-8859-11"=>"windows-874",
+ "iso8859-11"=>"windows-874",
+ "iso885911"=>"windows-874",
+ "tis-620"=>"windows-874",
+ "windows-874"=>"windows-874",
+ "cp1250"=>"windows-1250",
+ "windows-1250"=>"windows-1250",
+ "x-cp1250"=>"windows-1250",
+ "cp1251"=>"windows-1251",
+ "windows-1251"=>"windows-1251",
+ "x-cp1251"=>"windows-1251",
+ "ansi_x3.4-1968"=>"windows-1252",
+ "ascii"=>"windows-1252",
+ "cp1252"=>"windows-1252",
+ "cp819"=>"windows-1252",
+ "csisolatin1"=>"windows-1252",
+ "ibm819"=>"windows-1252",
+ "iso-8859-1"=>"windows-1252",
+ "iso-ir-100"=>"windows-1252",
+ "iso8859-1"=>"windows-1252",
+ "iso88591"=>"windows-1252",
+ "iso_8859-1"=>"windows-1252",
+ "iso_8859-1:1987"=>"windows-1252",
+ "l1"=>"windows-1252",
+ "latin1"=>"windows-1252",
+ "us-ascii"=>"windows-1252",
+ "windows-1252"=>"windows-1252",
+ "x-cp1252"=>"windows-1252",
+ "cp1253"=>"windows-1253",
+ "windows-1253"=>"windows-1253",
+ "x-cp1253"=>"windows-1253",
+ "cp1254"=>"windows-1254",
+ "csisolatin5"=>"windows-1254",
+ "iso-8859-9"=>"windows-1254",
+ "iso-ir-148"=>"windows-1254",
+ "iso8859-9"=>"windows-1254",
+ "iso88599"=>"windows-1254",
+ "iso_8859-9"=>"windows-1254",
+ "iso_8859-9:1989"=>"windows-1254",
+ "l5"=>"windows-1254",
+ "latin5"=>"windows-1254",
+ "windows-1254"=>"windows-1254",
+ "x-cp1254"=>"windows-1254",
+ "cp1255"=>"windows-1255",
+ "windows-1255"=>"windows-1255",
+ "x-cp1255"=>"windows-1255",
+ "cp1256"=>"windows-1256",
+ "windows-1256"=>"windows-1256",
+ "x-cp1256"=>"windows-1256",
+ "cp1257"=>"windows-1257",
+ "windows-1257"=>"windows-1257",
+ "x-cp1257"=>"windows-1257",
+ "cp1258"=>"windows-1258",
+ "windows-1258"=>"windows-1258",
+ "x-cp1258"=>"windows-1258",
+ "x-mac-cyrillic"=>"macCyrillic",
+ "x-mac-ukrainian"=>"macCyrillic",
+ "chinese"=>"gbk",
+ "csgb2312"=>"gbk",
+ "csiso58gb231280"=>"gbk",
+ "gb2312"=>"gbk",
+ "gb_2312"=>"gbk",
+ "gb_2312-80"=>"gbk",
+ "gbk"=>"gbk",
+ "iso-ir-58"=>"gbk",
+ "x-gbk"=>"gbk",
+ "gb18030"=>"gb18030",
+ "big5"=>"big5",
+ "big5-hkscs"=>"big5",
+ "cn-big5"=>"big5",
+ "csbig5"=>"big5",
+ "x-x-big5"=>"big5",
+ "cseucpkdfmtjapanese"=>"cp51932",
+ "euc-jp"=>"cp51932",
+ "x-euc-jp"=>"cp51932",
+ "csiso2022jp"=>"cp50221",
+ "iso-2022-jp"=>"cp50221",
+ "csshiftjis"=>"Windows-31J",
+ "ms932"=>"Windows-31J",
+ "ms_kanji"=>"Windows-31J",
+ "shift-jis"=>"Windows-31J",
+ "shift_jis"=>"Windows-31J",
+ "sjis"=>"Windows-31J",
+ "windows-31j"=>"Windows-31J",
+ "x-sjis"=>"Windows-31J",
+ "cseuckr"=>"euc-kr",
+ "csksc56011987"=>"euc-kr",
+ "euc-kr"=>"euc-kr",
+ "iso-ir-149"=>"euc-kr",
+ "korean"=>"euc-kr",
+ "ks_c_5601-1987"=>"euc-kr",
+ "ks_c_5601-1989"=>"euc-kr",
+ "ksc5601"=>"euc-kr",
+ "ksc_5601"=>"euc-kr",
+ "windows-949"=>"euc-kr",
+ "utf-16be"=>"utf-16be",
+ "utf-16"=>"utf-16le",
+ "utf-16le"=>"utf-16le",
+ } # :nodoc:
+ Ractor.make_shareable(WEB_ENCODINGS_) if defined?(Ractor)
+
+ # :nodoc:
+ # return encoding or nil
+ # http://encoding.spec.whatwg.org/#concept-encoding-get
+ def self.get_encoding(label)
+ Encoding.find(WEB_ENCODINGS_[label.to_str.strip.downcase]) rescue nil
+ end
+end # module Bundler::URI
+
+module Bundler
+
+ #
+ # Returns a \Bundler::URI object derived from the given +uri+,
+ # which may be a \Bundler::URI string or an existing \Bundler::URI object:
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ # # Returns a new Bundler::URI.
+ # uri = Bundler::URI('http://github.com/ruby/ruby')
+ # # => #<Bundler::URI::HTTP http://github.com/ruby/ruby>
+ # # Returns the given Bundler::URI.
+ # Bundler::URI(uri)
+ # # => #<Bundler::URI::HTTP http://github.com/ruby/ruby>
+ #
+ # You must require 'bundler/vendor/uri/lib/uri' to use this method.
+ #
+ def URI(uri)
+ if uri.is_a?(Bundler::URI::Generic)
+ uri
+ elsif uri = String.try_convert(uri)
+ Bundler::URI.parse(uri)
+ else
+ raise ArgumentError,
+ "bad argument (expected Bundler::URI object or Bundler::URI string)"
+ end
+ end
+ module_function :URI
+end
diff --git a/lib/bundler/vendor/uri/lib/uri/file.rb b/lib/bundler/vendor/uri/lib/uri/file.rb
new file mode 100644
index 0000000000..21dd9ee535
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/file.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require_relative 'generic'
+
+module Bundler::URI
+
+ #
+ # The "file" Bundler::URI is defined by RFC8089.
+ #
+ class File < Generic
+ # A Default port of nil for Bundler::URI::File.
+ DEFAULT_PORT = nil
+
+ #
+ # An Array of the available components for Bundler::URI::File.
+ #
+ COMPONENT = [
+ :scheme,
+ :host,
+ :path
+ ].freeze
+
+ #
+ # == Description
+ #
+ # Creates a new Bundler::URI::File object from components, with syntax checking.
+ #
+ # The components accepted are +host+ and +path+.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[host, path]</code>.
+ #
+ # A path from e.g. the File class should be escaped before
+ # being passed.
+ #
+ # Examples:
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri1 = Bundler::URI::File.build(['host.example.com', '/path/file.zip'])
+ # uri1.to_s # => "file://host.example.com/path/file.zip"
+ #
+ # uri2 = Bundler::URI::File.build({:host => 'host.example.com',
+ # :path => '/ruby/src'})
+ # uri2.to_s # => "file://host.example.com/ruby/src"
+ #
+ # uri3 = Bundler::URI::File.build({:path => Bundler::URI::RFC2396_PARSER.escape('/path/my file.txt')})
+ # uri3.to_s # => "file:///path/my%20file.txt"
+ #
+ def self.build(args)
+ tmp = Util::make_components_hash(self, args)
+ super(tmp)
+ end
+
+ # Protected setter for the host component +v+.
+ #
+ # See also Bundler::URI::Generic.host=.
+ #
+ def set_host(v)
+ v = "" if v.nil? || v == "localhost"
+ @host = v
+ end
+
+ # do nothing
+ def set_port(v)
+ end
+
+ # raise InvalidURIError
+ def check_userinfo(user)
+ raise Bundler::URI::InvalidURIError, "cannot set userinfo for file Bundler::URI"
+ end
+
+ # raise InvalidURIError
+ def check_user(user)
+ raise Bundler::URI::InvalidURIError, "cannot set user for file Bundler::URI"
+ end
+
+ # raise InvalidURIError
+ def check_password(user)
+ raise Bundler::URI::InvalidURIError, "cannot set password for file Bundler::URI"
+ end
+
+ # do nothing
+ def set_userinfo(v)
+ end
+
+ # do nothing
+ def set_user(v)
+ end
+
+ # do nothing
+ def set_password(v)
+ end
+ end
+
+ register_scheme 'FILE', File
+end
diff --git a/lib/bundler/vendor/uri/lib/uri/ftp.rb b/lib/bundler/vendor/uri/lib/uri/ftp.rb
new file mode 100644
index 0000000000..f83985fd3d
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/ftp.rb
@@ -0,0 +1,267 @@
+# frozen_string_literal: false
+# = uri/ftp.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Bundler::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Bundler::URI
+
+ #
+ # FTP Bundler::URI syntax is defined by RFC1738 section 3.2.
+ #
+ # This class will be redesigned because of difference of implementations;
+ # the structure of its path. draft-hoffman-ftp-uri-04 is a draft but it
+ # is a good summary about the de facto spec.
+ # https://datatracker.ietf.org/doc/html/draft-hoffman-ftp-uri-04
+ #
+ class FTP < Generic
+ # A Default port of 21 for Bundler::URI::FTP.
+ DEFAULT_PORT = 21
+
+ #
+ # An Array of the available components for Bundler::URI::FTP.
+ #
+ COMPONENT = [
+ :scheme,
+ :userinfo, :host, :port,
+ :path, :typecode
+ ].freeze
+
+ #
+ # Typecode is "a", "i", or "d".
+ #
+ # * "a" indicates a text file (the FTP command was ASCII)
+ # * "i" indicates a binary file (FTP command IMAGE)
+ # * "d" indicates the contents of a directory should be displayed
+ #
+ TYPECODE = ['a', 'i', 'd'].freeze
+
+ # Typecode prefix ";type=".
+ TYPECODE_PREFIX = ';type='.freeze
+
+ def self.new2(user, password, host, port, path,
+ typecode = nil, arg_check = true) # :nodoc:
+ # Do not use this method! Not tested. [Bug #7301]
+ # This methods remains just for compatibility,
+ # Keep it undocumented until the active maintainer is assigned.
+ typecode = nil if typecode.size == 0
+ if typecode && !TYPECODE.include?(typecode)
+ raise ArgumentError,
+ "bad typecode is specified: #{typecode}"
+ end
+
+ # do escape
+
+ self.new('ftp',
+ [user, password],
+ host, port, nil,
+ typecode ? path + TYPECODE_PREFIX + typecode : path,
+ nil, nil, nil, arg_check)
+ end
+
+ #
+ # == Description
+ #
+ # Creates a new Bundler::URI::FTP object from components, with syntax checking.
+ #
+ # The components accepted are +userinfo+, +host+, +port+, +path+, and
+ # +typecode+.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[userinfo, host, port, path, typecode]</code>.
+ #
+ # If the path supplied is absolute, it will be escaped in order to
+ # make it absolute in the Bundler::URI.
+ #
+ # Examples:
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri1 = Bundler::URI::FTP.build(['user:password', 'ftp.example.com', nil,
+ # '/path/file.zip', 'i'])
+ # uri1.to_s # => "ftp://user:password@ftp.example.com/%2Fpath/file.zip;type=i"
+ #
+ # uri2 = Bundler::URI::FTP.build({:host => 'ftp.example.com',
+ # :path => 'ruby/src'})
+ # uri2.to_s # => "ftp://ftp.example.com/ruby/src"
+ #
+ def self.build(args)
+
+ # Fix the incoming path to be generic URL syntax
+ # FTP path -> URL path
+ # foo/bar /foo/bar
+ # /foo/bar /%2Ffoo/bar
+ #
+ if args.kind_of?(Array)
+ args[3] = '/' + args[3].sub(/^\//, '%2F')
+ else
+ args[:path] = '/' + args[:path].sub(/^\//, '%2F')
+ end
+
+ tmp = Util::make_components_hash(self, args)
+
+ if tmp[:typecode]
+ if tmp[:typecode].size == 1
+ tmp[:typecode] = TYPECODE_PREFIX + tmp[:typecode]
+ end
+ tmp[:path] << tmp[:typecode]
+ end
+
+ return super(tmp)
+ end
+
+ #
+ # == Description
+ #
+ # Creates a new Bundler::URI::FTP object from generic URL components with no
+ # syntax checking.
+ #
+ # Unlike build(), this method does not escape the path component as
+ # required by RFC1738; instead it is treated as per RFC2396.
+ #
+ # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+,
+ # +opaque+, +query+, and +fragment+, in that order.
+ #
+ def initialize(scheme,
+ userinfo, host, port, registry,
+ path, opaque,
+ query,
+ fragment,
+ parser = nil,
+ arg_check = false)
+ raise InvalidURIError unless path
+ path = path.sub(/^\//,'')
+ path.sub!(/^%2F/,'/')
+ super(scheme, userinfo, host, port, registry, path, opaque,
+ query, fragment, parser, arg_check)
+ @typecode = nil
+ if tmp = @path.index(TYPECODE_PREFIX)
+ typecode = @path[tmp + TYPECODE_PREFIX.size..-1]
+ @path = @path[0..tmp - 1]
+
+ if arg_check
+ self.typecode = typecode
+ else
+ self.set_typecode(typecode)
+ end
+ end
+ end
+
+ # typecode accessor.
+ #
+ # See Bundler::URI::FTP::COMPONENT.
+ attr_reader :typecode
+
+ # Validates typecode +v+,
+ # returns +true+ or +false+.
+ #
+ def check_typecode(v)
+ if TYPECODE.include?(v)
+ return true
+ else
+ raise InvalidComponentError,
+ "bad typecode(expected #{TYPECODE.join(', ')}): #{v}"
+ end
+ end
+ private :check_typecode
+
+ # Private setter for the typecode +v+.
+ #
+ # See also Bundler::URI::FTP.typecode=.
+ #
+ def set_typecode(v)
+ @typecode = v
+ end
+ protected :set_typecode
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the typecode +v+
+ # (with validation).
+ #
+ # See also Bundler::URI::FTP.check_typecode.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("ftp://john@ftp.example.com/my_file.img")
+ # #=> #<Bundler::URI::FTP ftp://john@ftp.example.com/my_file.img>
+ # uri.typecode = "i"
+ # uri
+ # #=> #<Bundler::URI::FTP ftp://john@ftp.example.com/my_file.img;type=i>
+ #
+ def typecode=(typecode)
+ check_typecode(typecode)
+ set_typecode(typecode)
+ typecode
+ end
+
+ def merge(oth) # :nodoc:
+ tmp = super(oth)
+ if self != tmp
+ tmp.set_typecode(oth.typecode)
+ end
+
+ return tmp
+ end
+
+ # Returns the path from an FTP Bundler::URI.
+ #
+ # RFC 1738 specifically states that the path for an FTP Bundler::URI does not
+ # include the / which separates the Bundler::URI path from the Bundler::URI host. Example:
+ #
+ # <code>ftp://ftp.example.com/pub/ruby</code>
+ #
+ # The above Bundler::URI indicates that the client should connect to
+ # ftp.example.com then cd to pub/ruby from the initial login directory.
+ #
+ # If you want to cd to an absolute directory, you must include an
+ # escaped / (%2F) in the path. Example:
+ #
+ # <code>ftp://ftp.example.com/%2Fpub/ruby</code>
+ #
+ # This method will then return "/pub/ruby".
+ #
+ def path
+ return @path.sub(/^\//,'').sub(/^%2F/,'/')
+ end
+
+ # Private setter for the path of the Bundler::URI::FTP.
+ def set_path(v)
+ super("/" + v.sub(/^\//, "%2F"))
+ end
+ protected :set_path
+
+ # Returns a String representation of the Bundler::URI::FTP.
+ def to_s
+ save_path = nil
+ if @typecode
+ save_path = @path
+ @path = @path + TYPECODE_PREFIX + @typecode
+ end
+ str = super
+ if @typecode
+ @path = save_path
+ end
+
+ return str
+ end
+ end
+
+ register_scheme 'FTP', FTP
+end
diff --git a/lib/bundler/vendor/uri/lib/uri/generic.rb b/lib/bundler/vendor/uri/lib/uri/generic.rb
new file mode 100644
index 0000000000..30dab60903
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/generic.rb
@@ -0,0 +1,1592 @@
+# frozen_string_literal: true
+
+# = uri/generic.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Bundler::URI for general documentation
+#
+
+require_relative 'common'
+autoload :IPSocket, 'socket'
+autoload :IPAddr, 'ipaddr'
+
+module Bundler::URI
+
+ #
+ # Base class for all Bundler::URI classes.
+ # Implements generic Bundler::URI syntax as per RFC 2396.
+ #
+ class Generic
+ include Bundler::URI
+
+ #
+ # A Default port of nil for Bundler::URI::Generic.
+ #
+ DEFAULT_PORT = nil
+
+ #
+ # Returns default port.
+ #
+ def self.default_port
+ self::DEFAULT_PORT
+ end
+
+ #
+ # Returns default port.
+ #
+ def default_port
+ self.class.default_port
+ end
+
+ #
+ # An Array of the available components for Bundler::URI::Generic.
+ #
+ COMPONENT = [
+ :scheme,
+ :userinfo, :host, :port, :registry,
+ :path, :opaque,
+ :query,
+ :fragment
+ ].freeze
+
+ #
+ # Components of the Bundler::URI in the order.
+ #
+ def self.component
+ self::COMPONENT
+ end
+
+ USE_REGISTRY = false # :nodoc:
+
+ def self.use_registry # :nodoc:
+ self::USE_REGISTRY
+ end
+
+ #
+ # == Synopsis
+ #
+ # See ::new.
+ #
+ # == Description
+ #
+ # At first, tries to create a new Bundler::URI::Generic instance using
+ # Bundler::URI::Generic::build. But, if exception Bundler::URI::InvalidComponentError is raised,
+ # then it does Bundler::URI::RFC2396_PARSER.escape all Bundler::URI components and tries again.
+ #
+ def self.build2(args)
+ begin
+ return self.build(args)
+ rescue InvalidComponentError
+ if args.kind_of?(Array)
+ return self.build(args.collect{|x|
+ if x.is_a?(String)
+ Bundler::URI::RFC2396_PARSER.escape(x)
+ else
+ x
+ end
+ })
+ elsif args.kind_of?(Hash)
+ tmp = {}
+ args.each do |key, value|
+ tmp[key] = if value
+ Bundler::URI::RFC2396_PARSER.escape(value)
+ else
+ value
+ end
+ end
+ return self.build(tmp)
+ end
+ end
+ end
+
+ #
+ # == Synopsis
+ #
+ # See ::new.
+ #
+ # == Description
+ #
+ # Creates a new Bundler::URI::Generic instance from components of Bundler::URI::Generic
+ # with check. Components are: scheme, userinfo, host, port, registry, path,
+ # opaque, query, and fragment. You can provide arguments either by an Array or a Hash.
+ # See ::new for hash keys to use or for order of array items.
+ #
+ def self.build(args)
+ if args.kind_of?(Array) &&
+ args.size == ::Bundler::URI::Generic::COMPONENT.size
+ tmp = args.dup
+ elsif args.kind_of?(Hash)
+ tmp = ::Bundler::URI::Generic::COMPONENT.collect do |c|
+ if args.include?(c)
+ args[c]
+ else
+ nil
+ end
+ end
+ else
+ component = self.component rescue ::Bundler::URI::Generic::COMPONENT
+ raise ArgumentError,
+ "expected Array of or Hash of components of #{self} (#{component.join(', ')})"
+ end
+
+ tmp << nil
+ tmp << true
+ return self.new(*tmp)
+ end
+
+ #
+ # == Args
+ #
+ # +scheme+::
+ # Protocol scheme, i.e. 'http','ftp','mailto' and so on.
+ # +userinfo+::
+ # User name and password, i.e. 'sdmitry:bla'.
+ # +host+::
+ # Server host name.
+ # +port+::
+ # Server port.
+ # +registry+::
+ # Registry of naming authorities.
+ # +path+::
+ # Path on server.
+ # +opaque+::
+ # Opaque part.
+ # +query+::
+ # Query data.
+ # +fragment+::
+ # Part of the Bundler::URI after '#' character.
+ # +parser+::
+ # Parser for internal use [Bundler::URI::DEFAULT_PARSER by default].
+ # +arg_check+::
+ # Check arguments [false by default].
+ #
+ # == Description
+ #
+ # Creates a new Bundler::URI::Generic instance from ``generic'' components without check.
+ #
+ def initialize(scheme,
+ userinfo, host, port, registry,
+ path, opaque,
+ query,
+ fragment,
+ parser = DEFAULT_PARSER,
+ arg_check = false)
+ @scheme = nil
+ @user = nil
+ @password = nil
+ @host = nil
+ @port = nil
+ @path = nil
+ @query = nil
+ @opaque = nil
+ @fragment = nil
+ @parser = parser == DEFAULT_PARSER ? nil : parser
+
+ if arg_check
+ self.scheme = scheme
+ self.hostname = host
+ self.port = port
+ self.userinfo = userinfo
+ self.path = path
+ self.query = query
+ self.opaque = opaque
+ self.fragment = fragment
+ else
+ self.set_scheme(scheme)
+ self.set_host(host)
+ self.set_port(port)
+ self.set_userinfo(userinfo)
+ self.set_path(path)
+ self.query = query
+ self.set_opaque(opaque)
+ self.fragment=(fragment)
+ end
+ if registry
+ raise InvalidURIError,
+ "the scheme #{@scheme} does not accept registry part: #{registry} (or bad hostname?)"
+ end
+
+ @scheme&.freeze
+ self.set_path('') if !@path && !@opaque # (see RFC2396 Section 5.2)
+ self.set_port(self.default_port) if self.default_port && !@port
+ end
+
+ #
+ # Returns the scheme component of the Bundler::URI.
+ #
+ # Bundler::URI("http://foo/bar/baz").scheme #=> "http"
+ #
+ attr_reader :scheme
+
+ # Returns the host component of the Bundler::URI.
+ #
+ # Bundler::URI("http://foo/bar/baz").host #=> "foo"
+ #
+ # It returns nil if no host component exists.
+ #
+ # Bundler::URI("mailto:foo@example.org").host #=> nil
+ #
+ # The component does not contain the port number.
+ #
+ # Bundler::URI("http://foo:8080/bar/baz").host #=> "foo"
+ #
+ # Since IPv6 addresses are wrapped with brackets in URIs,
+ # this method returns IPv6 addresses wrapped with brackets.
+ # This form is not appropriate to pass to socket methods such as TCPSocket.open.
+ # If unwrapped host names are required, use the #hostname method.
+ #
+ # Bundler::URI("http://[::1]/bar/baz").host #=> "[::1]"
+ # Bundler::URI("http://[::1]/bar/baz").hostname #=> "::1"
+ #
+ attr_reader :host
+
+ # Returns the port component of the Bundler::URI.
+ #
+ # Bundler::URI("http://foo/bar/baz").port #=> 80
+ # Bundler::URI("http://foo:8080/bar/baz").port #=> 8080
+ #
+ attr_reader :port
+
+ def registry # :nodoc:
+ nil
+ end
+
+ # Returns the path component of the Bundler::URI.
+ #
+ # Bundler::URI("http://foo/bar/baz").path #=> "/bar/baz"
+ #
+ attr_reader :path
+
+ # Returns the query component of the Bundler::URI.
+ #
+ # Bundler::URI("http://foo/bar/baz?search=FooBar").query #=> "search=FooBar"
+ #
+ attr_reader :query
+
+ # Returns the opaque part of the Bundler::URI.
+ #
+ # Bundler::URI("mailto:foo@example.org").opaque #=> "foo@example.org"
+ # Bundler::URI("http://foo/bar/baz").opaque #=> nil
+ #
+ # The portion of the path that does not make use of the slash '/'.
+ # The path typically refers to an absolute path or an opaque part.
+ # (See RFC2396 Section 3 and 5.2.)
+ #
+ attr_reader :opaque
+
+ # Returns the fragment component of the Bundler::URI.
+ #
+ # Bundler::URI("http://foo/bar/baz?search=FooBar#ponies").fragment #=> "ponies"
+ #
+ attr_reader :fragment
+
+ # Returns the parser to be used.
+ #
+ # Unless the +parser+ is defined, DEFAULT_PARSER is used.
+ #
+ def parser
+ if !defined?(@parser) || !@parser
+ DEFAULT_PARSER
+ else
+ @parser || DEFAULT_PARSER
+ end
+ end
+
+ # Replaces self by other Bundler::URI object.
+ #
+ def replace!(oth)
+ if self.class != oth.class
+ raise ArgumentError, "expected #{self.class} object"
+ end
+
+ component.each do |c|
+ self.__send__("#{c}=", oth.__send__(c))
+ end
+ end
+ private :replace!
+
+ #
+ # Components of the Bundler::URI in the order.
+ #
+ def component
+ self.class.component
+ end
+
+ #
+ # Checks the scheme +v+ component against the +parser+ Regexp for :SCHEME.
+ #
+ def check_scheme(v)
+ if v && parser.regexp[:SCHEME] !~ v
+ raise InvalidComponentError,
+ "bad component(expected scheme component): #{v}"
+ end
+
+ return true
+ end
+ private :check_scheme
+
+ # Protected setter for the scheme component +v+.
+ #
+ # See also Bundler::URI::Generic.scheme=.
+ #
+ def set_scheme(v)
+ @scheme = v&.downcase
+ end
+ protected :set_scheme
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the scheme component +v+
+ # (with validation).
+ #
+ # See also Bundler::URI::Generic.check_scheme.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("http://my.example.com")
+ # uri.scheme = "https"
+ # uri.to_s #=> "https://my.example.com"
+ #
+ def scheme=(v)
+ check_scheme(v)
+ set_scheme(v)
+ v
+ end
+
+ #
+ # Checks the +user+ and +password+.
+ #
+ # If +password+ is not provided, then +user+ is
+ # split, using Bundler::URI::Generic.split_userinfo, to
+ # pull +user+ and +password.
+ #
+ # See also Bundler::URI::Generic.check_user, Bundler::URI::Generic.check_password.
+ #
+ def check_userinfo(user, password = nil)
+ if !password
+ user, password = split_userinfo(user)
+ end
+ check_user(user)
+ check_password(password, user)
+
+ return true
+ end
+ private :check_userinfo
+
+ #
+ # Checks the user +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp for :USERINFO.
+ #
+ # Can not have a registry or opaque component defined,
+ # with a user component defined.
+ #
+ def check_user(v)
+ if @opaque
+ raise InvalidURIError,
+ "cannot set user with opaque"
+ end
+
+ return v unless v
+
+ if parser.regexp[:USERINFO] !~ v
+ raise InvalidComponentError,
+ "bad component(expected userinfo component or user component): #{v}"
+ end
+
+ return true
+ end
+ private :check_user
+
+ #
+ # Checks the password +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp for :USERINFO.
+ #
+ # Can not have a registry or opaque component defined,
+ # with a user component defined.
+ #
+ def check_password(v, user = @user)
+ if @opaque
+ raise InvalidURIError,
+ "cannot set password with opaque"
+ end
+ return v unless v
+
+ if !user
+ raise InvalidURIError,
+ "password component depends user component"
+ end
+
+ if parser.regexp[:USERINFO] !~ v
+ raise InvalidComponentError,
+ "bad password component"
+ end
+
+ return true
+ end
+ private :check_password
+
+ #
+ # Sets userinfo, argument is string like 'name:pass'.
+ #
+ def userinfo=(userinfo)
+ if userinfo.nil?
+ return nil
+ end
+ check_userinfo(*userinfo)
+ set_userinfo(*userinfo)
+ # returns userinfo
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the +user+ component
+ # (with validation).
+ #
+ # See also Bundler::URI::Generic.check_user.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("http://john:S3nsit1ve@my.example.com")
+ # uri.user = "sam"
+ # uri.to_s #=> "http://sam:V3ry_S3nsit1ve@my.example.com"
+ #
+ def user=(user)
+ check_user(user)
+ set_user(user)
+ # returns user
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the +password+ component
+ # (with validation).
+ #
+ # See also Bundler::URI::Generic.check_password.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("http://john:S3nsit1ve@my.example.com")
+ # uri.password = "V3ry_S3nsit1ve"
+ # uri.to_s #=> "http://john:V3ry_S3nsit1ve@my.example.com"
+ #
+ def password=(password)
+ check_password(password)
+ set_password(password)
+ # returns password
+ end
+
+ # Protected setter for the +user+ component, and +password+ if available
+ # (with validation).
+ #
+ # See also Bundler::URI::Generic.userinfo=.
+ #
+ def set_userinfo(user, password = nil)
+ unless password
+ user, password = split_userinfo(user)
+ end
+ @user = user
+ @password = password
+
+ [@user, @password]
+ end
+ protected :set_userinfo
+
+ # Protected setter for the user component +v+.
+ #
+ # See also Bundler::URI::Generic.user=.
+ #
+ def set_user(v)
+ set_userinfo(v, nil)
+ v
+ end
+ protected :set_user
+
+ # Protected setter for the password component +v+.
+ #
+ # See also Bundler::URI::Generic.password=.
+ #
+ def set_password(v)
+ @password = v
+ # returns v
+ end
+ protected :set_password
+
+ # Returns the userinfo +ui+ as <code>[user, password]</code>
+ # if properly formatted as 'user:password'.
+ def split_userinfo(ui)
+ return nil, nil unless ui
+ user, password = ui.split(':', 2)
+
+ return user, password
+ end
+ private :split_userinfo
+
+ # Escapes 'user:password' +v+ based on RFC 1738 section 3.1.
+ def escape_userpass(v)
+ parser.escape(v, /[@:\/]/o) # RFC 1738 section 3.1 #/
+ end
+ private :escape_userpass
+
+ # Returns the userinfo, either as 'user' or 'user:password'.
+ def userinfo
+ if @user.nil?
+ nil
+ elsif @password.nil?
+ @user
+ else
+ @user + ':' + @password
+ end
+ end
+
+ # Returns the user component (without Bundler::URI decoding).
+ def user
+ @user
+ end
+
+ # Returns the password component (without Bundler::URI decoding).
+ def password
+ @password
+ end
+
+ # Returns the authority info (array of user, password, host and
+ # port), if any is set. Or returns +nil+.
+ def authority
+ return @user, @password, @host, @port if @user || @password || @host || @port
+ end
+
+ # Returns the user component after Bundler::URI decoding.
+ def decoded_user
+ Bundler::URI.decode_uri_component(@user) if @user
+ end
+
+ # Returns the password component after Bundler::URI decoding.
+ def decoded_password
+ Bundler::URI.decode_uri_component(@password) if @password
+ end
+
+ #
+ # Checks the host +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp for :HOST.
+ #
+ # Can not have a registry or opaque component defined,
+ # with a host component defined.
+ #
+ def check_host(v)
+ return v unless v
+
+ if @opaque
+ raise InvalidURIError,
+ "cannot set host with registry or opaque"
+ elsif parser.regexp[:HOST] !~ v
+ raise InvalidComponentError,
+ "bad component(expected host component): #{v}"
+ end
+
+ return true
+ end
+ private :check_host
+
+ # Protected setter for the host component +v+.
+ #
+ # See also Bundler::URI::Generic.host=.
+ #
+ def set_host(v)
+ @host = v
+ end
+ protected :set_host
+
+ # Protected setter for the authority info (+user+, +password+, +host+
+ # and +port+). If +port+ is +nil+, +default_port+ will be set.
+ #
+ protected def set_authority(user, password, host, port = nil)
+ @user, @password, @host, @port = user, password, host, port || self.default_port
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the host component +v+
+ # (with validation).
+ #
+ # See also Bundler::URI::Generic.check_host.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("http://my.example.com")
+ # uri.host = "foo.com"
+ # uri.to_s #=> "http://foo.com"
+ #
+ def host=(v)
+ check_host(v)
+ set_host(v)
+ set_userinfo(nil)
+ v
+ end
+
+ # Extract the host part of the Bundler::URI and unwrap brackets for IPv6 addresses.
+ #
+ # This method is the same as Bundler::URI::Generic#host except
+ # brackets for IPv6 (and future IP) addresses are removed.
+ #
+ # uri = Bundler::URI("http://[::1]/bar")
+ # uri.hostname #=> "::1"
+ # uri.host #=> "[::1]"
+ #
+ def hostname
+ v = self.host
+ v&.start_with?('[') && v.end_with?(']') ? v[1..-2] : v
+ end
+
+ # Sets the host part of the Bundler::URI as the argument with brackets for IPv6 addresses.
+ #
+ # This method is the same as Bundler::URI::Generic#host= except
+ # the argument can be a bare IPv6 address.
+ #
+ # uri = Bundler::URI("http://foo/bar")
+ # uri.hostname = "::1"
+ # uri.to_s #=> "http://[::1]/bar"
+ #
+ # If the argument seems to be an IPv6 address,
+ # it is wrapped with brackets.
+ #
+ def hostname=(v)
+ v = "[#{v}]" if !(v&.start_with?('[') && v&.end_with?(']')) && v&.index(':')
+ self.host = v
+ end
+
+ #
+ # Checks the port +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp for :PORT.
+ #
+ # Can not have a registry or opaque component defined,
+ # with a port component defined.
+ #
+ def check_port(v)
+ return v unless v
+
+ if @opaque
+ raise InvalidURIError,
+ "cannot set port with registry or opaque"
+ elsif !v.kind_of?(Integer) && parser.regexp[:PORT] !~ v
+ raise InvalidComponentError,
+ "bad component(expected port component): #{v.inspect}"
+ end
+
+ return true
+ end
+ private :check_port
+
+ # Protected setter for the port component +v+.
+ #
+ # See also Bundler::URI::Generic.port=.
+ #
+ def set_port(v)
+ v = v.empty? ? nil : v.to_i unless !v || v.kind_of?(Integer)
+ @port = v
+ end
+ protected :set_port
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the port component +v+
+ # (with validation).
+ #
+ # See also Bundler::URI::Generic.check_port.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("http://my.example.com")
+ # uri.port = 8080
+ # uri.to_s #=> "http://my.example.com:8080"
+ #
+ def port=(v)
+ check_port(v)
+ set_port(v)
+ set_userinfo(nil)
+ port
+ end
+
+ def check_registry(v) # :nodoc:
+ raise InvalidURIError, "cannot set registry"
+ end
+ private :check_registry
+
+ def set_registry(v) # :nodoc:
+ raise InvalidURIError, "cannot set registry"
+ end
+ protected :set_registry
+
+ def registry=(v) # :nodoc:
+ raise InvalidURIError, "cannot set registry"
+ end
+
+ #
+ # Checks the path +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp
+ # for :ABS_PATH and :REL_PATH.
+ #
+ # Can not have a opaque component defined,
+ # with a path component defined.
+ #
+ def check_path(v)
+ # raise if both hier and opaque are not nil, because:
+ # absoluteURI = scheme ":" ( hier_part | opaque_part )
+ # hier_part = ( net_path | abs_path ) [ "?" query ]
+ if v && @opaque
+ raise InvalidURIError,
+ "path conflicts with opaque"
+ end
+
+ # If scheme is ftp, path may be relative.
+ # See RFC 1738 section 3.2.2, and RFC 2396.
+ if @scheme && @scheme != "ftp"
+ if v && v != '' && parser.regexp[:ABS_PATH] !~ v
+ raise InvalidComponentError,
+ "bad component(expected absolute path component): #{v}"
+ end
+ else
+ if v && v != '' && parser.regexp[:ABS_PATH] !~ v &&
+ parser.regexp[:REL_PATH] !~ v
+ raise InvalidComponentError,
+ "bad component(expected relative path component): #{v}"
+ end
+ end
+
+ return true
+ end
+ private :check_path
+
+ # Protected setter for the path component +v+.
+ #
+ # See also Bundler::URI::Generic.path=.
+ #
+ def set_path(v)
+ @path = v
+ end
+ protected :set_path
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the path component +v+
+ # (with validation).
+ #
+ # See also Bundler::URI::Generic.check_path.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("http://my.example.com/pub/files")
+ # uri.path = "/faq/"
+ # uri.to_s #=> "http://my.example.com/faq/"
+ #
+ def path=(v)
+ check_path(v)
+ set_path(v)
+ v
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the query component +v+.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("http://my.example.com/?id=25")
+ # uri.query = "id=1"
+ # uri.to_s #=> "http://my.example.com/?id=1"
+ #
+ def query=(v)
+ return @query = nil unless v
+ raise InvalidURIError, "query conflicts with opaque" if @opaque
+
+ x = v.to_str
+ v = x.dup if x.equal? v
+ v.encode!(Encoding::UTF_8) rescue nil
+ v.delete!("\t\r\n")
+ v.force_encoding(Encoding::ASCII_8BIT)
+ raise InvalidURIError, "invalid percent escape: #{$1}" if /(%\H\H)/n.match(v)
+ v.gsub!(/(?!%\h\h|[!$-&(-;=?-_a-~])./n.freeze){'%%%02X' % $&.ord}
+ v.force_encoding(Encoding::US_ASCII)
+ @query = v
+ end
+
+ #
+ # Checks the opaque +v+ component for RFC2396 compliance and
+ # against the +parser+ Regexp for :OPAQUE.
+ #
+ # Can not have a host, port, user, or path component defined,
+ # with an opaque component defined.
+ #
+ def check_opaque(v)
+ return v unless v
+
+ # raise if both hier and opaque are not nil, because:
+ # absoluteURI = scheme ":" ( hier_part | opaque_part )
+ # hier_part = ( net_path | abs_path ) [ "?" query ]
+ if @host || @port || @user || @path # userinfo = @user + ':' + @password
+ raise InvalidURIError,
+ "cannot set opaque with host, port, userinfo or path"
+ elsif v && parser.regexp[:OPAQUE] !~ v
+ raise InvalidComponentError,
+ "bad component(expected opaque component): #{v}"
+ end
+
+ return true
+ end
+ private :check_opaque
+
+ # Protected setter for the opaque component +v+.
+ #
+ # See also Bundler::URI::Generic.opaque=.
+ #
+ def set_opaque(v)
+ @opaque = v
+ end
+ protected :set_opaque
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the opaque component +v+
+ # (with validation).
+ #
+ # See also Bundler::URI::Generic.check_opaque.
+ #
+ def opaque=(v)
+ check_opaque(v)
+ set_opaque(v)
+ v
+ end
+
+ #
+ # Checks the fragment +v+ component against the +parser+ Regexp for :FRAGMENT.
+ #
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the fragment component +v+
+ # (with validation).
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("http://my.example.com/?id=25#time=1305212049")
+ # uri.fragment = "time=1305212086"
+ # uri.to_s #=> "http://my.example.com/?id=25#time=1305212086"
+ #
+ def fragment=(v)
+ return @fragment = nil unless v
+
+ x = v.to_str
+ v = x.dup if x.equal? v
+ v.encode!(Encoding::UTF_8) rescue nil
+ v.delete!("\t\r\n")
+ v.force_encoding(Encoding::ASCII_8BIT)
+ v.gsub!(/(?!%\h\h|[!-~])./n){'%%%02X' % $&.ord}
+ v.force_encoding(Encoding::US_ASCII)
+ @fragment = v
+ end
+
+ #
+ # Returns true if Bundler::URI is hierarchical.
+ #
+ # == Description
+ #
+ # Bundler::URI has components listed in order of decreasing significance from left to right,
+ # see RFC3986 https://www.rfc-editor.org/rfc/rfc3986 1.2.3.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("http://my.example.com/")
+ # uri.hierarchical?
+ # #=> true
+ # uri = Bundler::URI.parse("mailto:joe@example.com")
+ # uri.hierarchical?
+ # #=> false
+ #
+ def hierarchical?
+ if @path
+ true
+ else
+ false
+ end
+ end
+
+ #
+ # Returns true if Bundler::URI has a scheme (e.g. http:// or https://) specified.
+ #
+ def absolute?
+ if @scheme
+ true
+ else
+ false
+ end
+ end
+ alias absolute absolute?
+
+ #
+ # Returns true if Bundler::URI does not have a scheme (e.g. http:// or https://) specified.
+ #
+ def relative?
+ !absolute?
+ end
+
+ #
+ # Returns an Array of the path split on '/'.
+ #
+ def split_path(path)
+ path.split("/", -1)
+ end
+ private :split_path
+
+ #
+ # Merges a base path +base+, with relative path +rel+,
+ # returns a modified base path.
+ #
+ def merge_path(base, rel)
+
+ # RFC2396, Section 5.2, 5)
+ # RFC2396, Section 5.2, 6)
+ base_path = split_path(base)
+ rel_path = split_path(rel)
+
+ # RFC2396, Section 5.2, 6), a)
+ base_path << '' if base_path.last == '..'
+ while i = base_path.index('..')
+ base_path.slice!(i - 1, 2)
+ end
+
+ if (first = rel_path.first) and first.empty?
+ base_path.clear
+ rel_path.shift
+ end
+
+ # RFC2396, Section 5.2, 6), c)
+ # RFC2396, Section 5.2, 6), d)
+ rel_path.push('') if rel_path.last == '.' || rel_path.last == '..'
+ rel_path.delete('.')
+
+ # RFC2396, Section 5.2, 6), e)
+ tmp = []
+ rel_path.each do |x|
+ if x == '..' &&
+ !(tmp.empty? || tmp.last == '..')
+ tmp.pop
+ else
+ tmp << x
+ end
+ end
+
+ add_trailer_slash = !tmp.empty?
+ if base_path.empty?
+ base_path = [''] # keep '/' for root directory
+ elsif add_trailer_slash
+ base_path.pop
+ end
+ while x = tmp.shift
+ if x == '..'
+ # RFC2396, Section 4
+ # a .. or . in an absolute path has no special meaning
+ base_path.pop if base_path.size > 1
+ else
+ # if x == '..'
+ # valid absolute (but abnormal) path "/../..."
+ # else
+ # valid absolute path
+ # end
+ base_path << x
+ tmp.each {|t| base_path << t}
+ add_trailer_slash = false
+ break
+ end
+ end
+ base_path.push('') if add_trailer_slash
+
+ return base_path.join('/')
+ end
+ private :merge_path
+
+ #
+ # == Args
+ #
+ # +oth+::
+ # Bundler::URI or String
+ #
+ # == Description
+ #
+ # Destructive form of #merge.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("http://my.example.com")
+ # uri.merge!("/main.rbx?page=1")
+ # uri.to_s # => "http://my.example.com/main.rbx?page=1"
+ #
+ def merge!(oth)
+ t = merge(oth)
+ if self == t
+ nil
+ else
+ replace!(t)
+ self
+ end
+ end
+
+ #
+ # == Args
+ #
+ # +oth+::
+ # Bundler::URI or String
+ #
+ # == Description
+ #
+ # Merges two URIs.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("http://my.example.com")
+ # uri.merge("/main.rbx?page=1")
+ # # => "http://my.example.com/main.rbx?page=1"
+ #
+ def merge(oth)
+ rel = parser.__send__(:convert_to_uri, oth)
+
+ if rel.absolute?
+ #raise BadURIError, "both Bundler::URI are absolute" if absolute?
+ # hmm... should return oth for usability?
+ return rel
+ end
+
+ unless self.absolute?
+ raise BadURIError, "both Bundler::URI are relative"
+ end
+
+ base = self.dup
+
+ authority = rel.authority
+
+ # RFC2396, Section 5.2, 2)
+ if (rel.path.nil? || rel.path.empty?) && !authority && !rel.query
+ base.fragment=(rel.fragment) if rel.fragment
+ return base
+ end
+
+ base.query = nil
+ base.fragment=(nil)
+
+ # RFC2396, Section 5.2, 4)
+ if authority
+ base.set_authority(*authority)
+ base.set_path(rel.path)
+ elsif base.path && rel.path
+ base.set_path(merge_path(base.path, rel.path))
+ end
+
+ # RFC2396, Section 5.2, 7)
+ base.query = rel.query if rel.query
+ base.fragment=(rel.fragment) if rel.fragment
+
+ return base
+ end # merge
+ alias + merge
+
+ # :stopdoc:
+ def route_from_path(src, dst)
+ case dst
+ when src
+ # RFC2396, Section 4.2
+ return ''
+ when %r{(?:\A|/)\.\.?(?:/|\z)}
+ # dst has abnormal absolute path,
+ # like "/./", "/../", "/x/../", ...
+ return dst.dup
+ end
+
+ src_path = src.scan(%r{[^/]*/})
+ dst_path = dst.scan(%r{[^/]*/?})
+
+ # discard same parts
+ while !dst_path.empty? && dst_path.first == src_path.first
+ src_path.shift
+ dst_path.shift
+ end
+
+ tmp = dst_path.join
+
+ # calculate
+ if src_path.empty?
+ if tmp.empty?
+ return './'
+ elsif dst_path.first.include?(':') # (see RFC2396 Section 5)
+ return './' + tmp
+ else
+ return tmp
+ end
+ end
+
+ return '../' * src_path.size + tmp
+ end
+ private :route_from_path
+ # :startdoc:
+
+ # :stopdoc:
+ def route_from0(oth)
+ oth = parser.__send__(:convert_to_uri, oth)
+ if self.relative?
+ raise BadURIError,
+ "relative Bundler::URI: #{self}"
+ end
+ if oth.relative?
+ raise BadURIError,
+ "relative Bundler::URI: #{oth}"
+ end
+
+ if self.scheme != oth.scheme
+ return self, self.dup
+ end
+ rel = Bundler::URI::Generic.new(nil, # it is relative Bundler::URI
+ self.userinfo, self.host, self.port,
+ nil, self.path, self.opaque,
+ self.query, self.fragment, parser)
+
+ if rel.userinfo != oth.userinfo ||
+ rel.host.to_s.downcase != oth.host.to_s.downcase ||
+ rel.port != oth.port
+
+ if self.userinfo.nil? && self.host.nil?
+ return self, self.dup
+ end
+
+ rel.set_port(nil) if rel.port == oth.default_port
+ return rel, rel
+ end
+ rel.set_userinfo(nil)
+ rel.set_host(nil)
+ rel.set_port(nil)
+
+ if rel.path && rel.path == oth.path
+ rel.set_path('')
+ rel.query = nil if rel.query == oth.query
+ return rel, rel
+ elsif rel.opaque && rel.opaque == oth.opaque
+ rel.set_opaque('')
+ rel.query = nil if rel.query == oth.query
+ return rel, rel
+ end
+
+ # you can modify `rel', but cannot `oth'.
+ return oth, rel
+ end
+ private :route_from0
+ # :startdoc:
+
+ #
+ # == Args
+ #
+ # +oth+::
+ # Bundler::URI or String
+ #
+ # == Description
+ #
+ # Calculates relative path from oth to self.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse('http://my.example.com/main.rbx?page=1')
+ # uri.route_from('http://my.example.com')
+ # #=> #<Bundler::URI::Generic /main.rbx?page=1>
+ #
+ def route_from(oth)
+ # you can modify `rel', but cannot `oth'.
+ begin
+ oth, rel = route_from0(oth)
+ rescue
+ raise $!.class, $!.message
+ end
+ if oth == rel
+ return rel
+ end
+
+ rel.set_path(route_from_path(oth.path, self.path))
+ if rel.path == './' && self.query
+ # "./?foo" -> "?foo"
+ rel.set_path('')
+ end
+
+ return rel
+ end
+
+ alias - route_from
+
+ #
+ # == Args
+ #
+ # +oth+::
+ # Bundler::URI or String
+ #
+ # == Description
+ #
+ # Calculates relative path to oth from self.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse('http://my.example.com')
+ # uri.route_to('http://my.example.com/main.rbx?page=1')
+ # #=> #<Bundler::URI::Generic /main.rbx?page=1>
+ #
+ def route_to(oth)
+ parser.__send__(:convert_to_uri, oth).route_from(self)
+ end
+
+ #
+ # Returns normalized Bundler::URI.
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # Bundler::URI("HTTP://my.EXAMPLE.com").normalize
+ # #=> #<Bundler::URI::HTTP http://my.example.com/>
+ #
+ # Normalization here means:
+ #
+ # * scheme and host are converted to lowercase,
+ # * an empty path component is set to "/".
+ #
+ def normalize
+ uri = dup
+ uri.normalize!
+ uri
+ end
+
+ #
+ # Destructive version of #normalize.
+ #
+ def normalize!
+ if path&.empty?
+ set_path('/')
+ end
+ if scheme && scheme != scheme.downcase
+ set_scheme(self.scheme.downcase)
+ end
+ if host && host != host.downcase
+ set_host(self.host.downcase)
+ end
+ end
+
+ #
+ # Constructs String from Bundler::URI.
+ #
+ def to_s
+ str = ''.dup
+ if @scheme
+ str << @scheme
+ str << ':'
+ end
+
+ if @opaque
+ str << @opaque
+ else
+ if @host || %w[file postgres].include?(@scheme)
+ str << '//'
+ end
+ if self.userinfo
+ str << self.userinfo
+ str << '@'
+ end
+ if @host
+ str << @host
+ end
+ if @port && @port != self.default_port
+ str << ':'
+ str << @port.to_s
+ end
+ if (@host || @port) && !@path.empty? && !@path.start_with?('/')
+ str << '/'
+ end
+ str << @path
+ if @query
+ str << '?'
+ str << @query
+ end
+ end
+ if @fragment
+ str << '#'
+ str << @fragment
+ end
+ str
+ end
+ alias to_str to_s
+
+ #
+ # Compares two URIs.
+ #
+ def ==(oth)
+ if self.class == oth.class
+ self.normalize.component_ary == oth.normalize.component_ary
+ else
+ false
+ end
+ end
+
+ # Returns the hash value.
+ def hash
+ self.component_ary.hash
+ end
+
+ # Compares with _oth_ for Hash.
+ def eql?(oth)
+ self.class == oth.class &&
+ parser == oth.parser &&
+ self.component_ary.eql?(oth.component_ary)
+ end
+
+ # Returns an Array of the components defined from the COMPONENT Array.
+ def component_ary
+ component.collect do |x|
+ self.__send__(x)
+ end
+ end
+ protected :component_ary
+
+ # == Args
+ #
+ # +components+::
+ # Multiple Symbol arguments defined in Bundler::URI::HTTP.
+ #
+ # == Description
+ #
+ # Selects specified components from Bundler::URI.
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse('http://myuser:mypass@my.example.com/test.rbx')
+ # uri.select(:userinfo, :host, :path)
+ # # => ["myuser:mypass", "my.example.com", "/test.rbx"]
+ #
+ def select(*components)
+ components.collect do |c|
+ if component.include?(c)
+ self.__send__(c)
+ else
+ raise ArgumentError,
+ "expected of components of #{self.class} (#{self.class.component.join(', ')})"
+ end
+ end
+ end
+
+ def inspect # :nodoc:
+ "#<#{self.class} #{self}>"
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # Bundler::URI or String
+ #
+ # == Description
+ #
+ # Attempts to parse other Bundler::URI +oth+,
+ # returns [parsed_oth, self].
+ #
+ # == Usage
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("http://my.example.com")
+ # uri.coerce("http://foo.com")
+ # #=> [#<Bundler::URI::HTTP http://foo.com>, #<Bundler::URI::HTTP http://my.example.com>]
+ #
+ def coerce(oth)
+ case oth
+ when String
+ oth = parser.parse(oth)
+ else
+ super
+ end
+
+ return oth, self
+ end
+
+ # Returns a proxy Bundler::URI.
+ # The proxy Bundler::URI is obtained from environment variables such as http_proxy,
+ # ftp_proxy, no_proxy, etc.
+ # If there is no proper proxy, nil is returned.
+ #
+ # If the optional parameter +env+ is specified, it is used instead of ENV.
+ #
+ # Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.)
+ # are examined, too.
+ #
+ # But http_proxy and HTTP_PROXY is treated specially under CGI environment.
+ # It's because HTTP_PROXY may be set by Proxy: header.
+ # So HTTP_PROXY is not used.
+ # http_proxy is not used too if the variable is case insensitive.
+ # CGI_HTTP_PROXY can be used instead.
+ def find_proxy(env=ENV)
+ raise BadURIError, "relative Bundler::URI: #{self}" if self.relative?
+ name = self.scheme.downcase + '_proxy'
+ proxy_uri = nil
+ if name == 'http_proxy' && env.include?('REQUEST_METHOD') # CGI?
+ # HTTP_PROXY conflicts with *_proxy for proxy settings and
+ # HTTP_* for header information in CGI.
+ # So it should be careful to use it.
+ pairs = env.reject {|k, v| /\Ahttp_proxy\z/i !~ k }
+ case pairs.length
+ when 0 # no proxy setting anyway.
+ proxy_uri = nil
+ when 1
+ k, _ = pairs.shift
+ if k == 'http_proxy' && env[k.upcase] == nil
+ # http_proxy is safe to use because ENV is case sensitive.
+ proxy_uri = env[name]
+ else
+ proxy_uri = nil
+ end
+ else # http_proxy is safe to use because ENV is case sensitive.
+ proxy_uri = env.to_hash[name]
+ end
+ if !proxy_uri
+ # Use CGI_HTTP_PROXY. cf. libwww-perl.
+ proxy_uri = env["CGI_#{name.upcase}"]
+ end
+ elsif name == 'http_proxy'
+ if RUBY_ENGINE == 'jruby' && p_addr = ENV_JAVA['http.proxyHost']
+ p_port = ENV_JAVA['http.proxyPort']
+ if p_user = ENV_JAVA['http.proxyUser']
+ p_pass = ENV_JAVA['http.proxyPass']
+ proxy_uri = "http://#{p_user}:#{p_pass}@#{p_addr}:#{p_port}"
+ else
+ proxy_uri = "http://#{p_addr}:#{p_port}"
+ end
+ else
+ unless proxy_uri = env[name]
+ if proxy_uri = env[name.upcase]
+ warn 'The environment variable HTTP_PROXY is discouraged. Please use http_proxy instead.', uplevel: 1
+ end
+ end
+ end
+ else
+ proxy_uri = env[name] || env[name.upcase]
+ end
+
+ if proxy_uri.nil? || proxy_uri.empty?
+ return nil
+ end
+
+ if self.hostname
+ begin
+ addr = IPSocket.getaddress(self.hostname)
+ return nil if /\A127\.|\A::1\z/ =~ addr
+ rescue SocketError
+ end
+ end
+
+ name = 'no_proxy'
+ if no_proxy = env[name] || env[name.upcase]
+ return nil unless Bundler::URI::Generic.use_proxy?(self.hostname, addr, self.port, no_proxy)
+ end
+ Bundler::URI.parse(proxy_uri)
+ end
+
+ def self.use_proxy?(hostname, addr, port, no_proxy) # :nodoc:
+ hostname = hostname.downcase
+ dothostname = ".#{hostname}"
+ no_proxy.scan(/([^:,\s]+)(?::(\d+))?/) {|p_host, p_port|
+ if !p_port || port == p_port.to_i
+ if p_host.start_with?('.')
+ return false if hostname.end_with?(p_host.downcase)
+ else
+ return false if dothostname.end_with?(".#{p_host.downcase}")
+ end
+ if addr
+ begin
+ return false if IPAddr.new(p_host).include?(addr)
+ rescue IPAddr::InvalidAddressError
+ next
+ end
+ end
+ end
+ }
+ true
+ end
+ end
+end
diff --git a/lib/bundler/vendor/uri/lib/uri/http.rb b/lib/bundler/vendor/uri/lib/uri/http.rb
new file mode 100644
index 0000000000..9b217ee266
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/http.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: false
+# = uri/http.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Bundler::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Bundler::URI
+
+ #
+ # The syntax of HTTP URIs is defined in RFC1738 section 3.3.
+ #
+ # Note that the Ruby Bundler::URI library allows HTTP URLs containing usernames and
+ # passwords. This is not legal as per the RFC, but used to be
+ # supported in Internet Explorer 5 and 6, before the MS04-004 security
+ # update. See <URL:http://support.microsoft.com/kb/834489>.
+ #
+ class HTTP < Generic
+ # A Default port of 80 for Bundler::URI::HTTP.
+ DEFAULT_PORT = 80
+
+ # An Array of the available components for Bundler::URI::HTTP.
+ COMPONENT = %i[
+ scheme
+ userinfo host port
+ path
+ query
+ fragment
+ ].freeze
+
+ #
+ # == Description
+ #
+ # Creates a new Bundler::URI::HTTP object from components, with syntax checking.
+ #
+ # The components accepted are userinfo, host, port, path, query, and
+ # fragment.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[userinfo, host, port, path, query, fragment]</code>.
+ #
+ # Example:
+ #
+ # uri = Bundler::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar')
+ #
+ # uri = Bundler::URI::HTTP.build([nil, "www.example.com", nil, "/path",
+ # "query", 'fragment'])
+ #
+ # Currently, if passed userinfo components this method generates
+ # invalid HTTP URIs as per RFC 1738.
+ #
+ def self.build(args)
+ tmp = Util.make_components_hash(self, args)
+ super(tmp)
+ end
+
+ # Do not allow empty host names, as they are not allowed by RFC 3986.
+ def check_host(v)
+ ret = super
+
+ if ret && v.empty?
+ raise InvalidComponentError,
+ "bad component(expected host component): #{v}"
+ end
+
+ ret
+ end
+
+ #
+ # == Description
+ #
+ # Returns the full path for an HTTP request, as required by Net::HTTP::Get.
+ #
+ # If the Bundler::URI contains a query, the full path is Bundler::URI#path + '?' + Bundler::URI#query.
+ # Otherwise, the path is simply Bundler::URI#path.
+ #
+ # Example:
+ #
+ # uri = Bundler::URI::HTTP.build(path: '/foo/bar', query: 'test=true')
+ # uri.request_uri # => "/foo/bar?test=true"
+ #
+ def request_uri
+ return unless @path
+
+ url = @query ? "#@path?#@query" : @path.dup
+ url.start_with?(?/.freeze) ? url : ?/ + url
+ end
+
+ #
+ # == Description
+ #
+ # Returns the authority for an HTTP uri, as defined in
+ # https://www.rfc-editor.org/rfc/rfc3986#section-3.2.
+ #
+ #
+ # Example:
+ #
+ # Bundler::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar').authority #=> "www.example.com"
+ # Bundler::URI::HTTP.build(host: 'www.example.com', port: 8000, path: '/foo/bar').authority #=> "www.example.com:8000"
+ # Bundler::URI::HTTP.build(host: 'www.example.com', port: 80, path: '/foo/bar').authority #=> "www.example.com"
+ #
+ def authority
+ if port == default_port
+ host
+ else
+ "#{host}:#{port}"
+ end
+ end
+
+ #
+ # == Description
+ #
+ # Returns the origin for an HTTP uri, as defined in
+ # https://www.rfc-editor.org/rfc/rfc6454.
+ #
+ #
+ # Example:
+ #
+ # Bundler::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar').origin #=> "http://www.example.com"
+ # Bundler::URI::HTTP.build(host: 'www.example.com', port: 8000, path: '/foo/bar').origin #=> "http://www.example.com:8000"
+ # Bundler::URI::HTTP.build(host: 'www.example.com', port: 80, path: '/foo/bar').origin #=> "http://www.example.com"
+ # Bundler::URI::HTTPS.build(host: 'www.example.com', path: '/foo/bar').origin #=> "https://www.example.com"
+ #
+ def origin
+ "#{scheme}://#{authority}"
+ end
+ end
+
+ register_scheme 'HTTP', HTTP
+end
diff --git a/lib/bundler/vendor/uri/lib/uri/https.rb b/lib/bundler/vendor/uri/lib/uri/https.rb
new file mode 100644
index 0000000000..e4556e3ecb
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/https.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: false
+# = uri/https.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Bundler::URI for general documentation
+#
+
+require_relative 'http'
+
+module Bundler::URI
+
+ # The default port for HTTPS URIs is 443, and the scheme is 'https:' rather
+ # than 'http:'. Other than that, HTTPS URIs are identical to HTTP URIs;
+ # see Bundler::URI::HTTP.
+ class HTTPS < HTTP
+ # A Default port of 443 for Bundler::URI::HTTPS
+ DEFAULT_PORT = 443
+ end
+
+ register_scheme 'HTTPS', HTTPS
+end
diff --git a/lib/bundler/vendor/uri/lib/uri/ldap.rb b/lib/bundler/vendor/uri/lib/uri/ldap.rb
new file mode 100644
index 0000000000..9811b6e2f5
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/ldap.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: false
+# = uri/ldap.rb
+#
+# Author::
+# Takaaki Tateishi <ttate@jaist.ac.jp>
+# Akira Yamada <akira@ruby-lang.org>
+# License::
+# Bundler::URI::LDAP is copyrighted free software by Takaaki Tateishi and Akira Yamada.
+# You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Bundler::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Bundler::URI
+
+ #
+ # LDAP Bundler::URI SCHEMA (described in RFC2255).
+ #--
+ # ldap://<host>/<dn>[?<attrs>[?<scope>[?<filter>[?<extensions>]]]]
+ #++
+ class LDAP < Generic
+
+ # A Default port of 389 for Bundler::URI::LDAP.
+ DEFAULT_PORT = 389
+
+ # An Array of the available components for Bundler::URI::LDAP.
+ COMPONENT = [
+ :scheme,
+ :host, :port,
+ :dn,
+ :attributes,
+ :scope,
+ :filter,
+ :extensions,
+ ].freeze
+
+ # Scopes available for the starting point.
+ #
+ # * SCOPE_BASE - the Base DN
+ # * SCOPE_ONE - one level under the Base DN, not including the base DN and
+ # not including any entries under this
+ # * SCOPE_SUB - subtrees, all entries at all levels
+ #
+ SCOPE = [
+ SCOPE_ONE = 'one',
+ SCOPE_SUB = 'sub',
+ SCOPE_BASE = 'base',
+ ].freeze
+
+ #
+ # == Description
+ #
+ # Creates a new Bundler::URI::LDAP object from components, with syntax checking.
+ #
+ # The components accepted are host, port, dn, attributes,
+ # scope, filter, and extensions.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[host, port, dn, attributes, scope, filter, extensions]</code>.
+ #
+ # Example:
+ #
+ # uri = Bundler::URI::LDAP.build({:host => 'ldap.example.com',
+ # :dn => '/dc=example'})
+ #
+ # uri = Bundler::URI::LDAP.build(["ldap.example.com", nil,
+ # "/dc=example;dc=com", "query", nil, nil, nil])
+ #
+ def self.build(args)
+ tmp = Util::make_components_hash(self, args)
+
+ if tmp[:dn]
+ tmp[:path] = tmp[:dn]
+ end
+
+ query = []
+ [:extensions, :filter, :scope, :attributes].collect do |x|
+ next if !tmp[x] && query.size == 0
+ query.unshift(tmp[x])
+ end
+
+ tmp[:query] = query.join('?')
+
+ return super(tmp)
+ end
+
+ #
+ # == Description
+ #
+ # Creates a new Bundler::URI::LDAP object from generic Bundler::URI components as per
+ # RFC 2396. No LDAP-specific syntax checking is performed.
+ #
+ # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+,
+ # +opaque+, +query+, and +fragment+, in that order.
+ #
+ # Example:
+ #
+ # uri = Bundler::URI::LDAP.new("ldap", nil, "ldap.example.com", nil, nil,
+ # "/dc=example;dc=com", nil, "query", nil)
+ #
+ # See also Bundler::URI::Generic.new.
+ #
+ def initialize(*arg)
+ super(*arg)
+
+ if @fragment
+ raise InvalidURIError, 'bad LDAP URL'
+ end
+
+ parse_dn
+ parse_query
+ end
+
+ # Private method to cleanup +dn+ from using the +path+ component attribute.
+ def parse_dn
+ raise InvalidURIError, 'bad LDAP URL' unless @path
+ @dn = @path[1..-1]
+ end
+ private :parse_dn
+
+ # Private method to cleanup +attributes+, +scope+, +filter+, and +extensions+
+ # from using the +query+ component attribute.
+ def parse_query
+ @attributes = nil
+ @scope = nil
+ @filter = nil
+ @extensions = nil
+
+ if @query
+ attrs, scope, filter, extensions = @query.split('?')
+
+ @attributes = attrs if attrs && attrs.size > 0
+ @scope = scope if scope && scope.size > 0
+ @filter = filter if filter && filter.size > 0
+ @extensions = extensions if extensions && extensions.size > 0
+ end
+ end
+ private :parse_query
+
+ # Private method to assemble +query+ from +attributes+, +scope+, +filter+, and +extensions+.
+ def build_path_query
+ @path = '/' + @dn
+
+ query = []
+ [@extensions, @filter, @scope, @attributes].each do |x|
+ next if !x && query.size == 0
+ query.unshift(x)
+ end
+ @query = query.join('?')
+ end
+ private :build_path_query
+
+ # Returns dn.
+ def dn
+ @dn
+ end
+
+ # Private setter for dn +val+.
+ def set_dn(val)
+ @dn = val
+ build_path_query
+ @dn
+ end
+ protected :set_dn
+
+ # Setter for dn +val+.
+ def dn=(val)
+ set_dn(val)
+ val
+ end
+
+ # Returns attributes.
+ def attributes
+ @attributes
+ end
+
+ # Private setter for attributes +val+.
+ def set_attributes(val)
+ @attributes = val
+ build_path_query
+ @attributes
+ end
+ protected :set_attributes
+
+ # Setter for attributes +val+.
+ def attributes=(val)
+ set_attributes(val)
+ val
+ end
+
+ # Returns scope.
+ def scope
+ @scope
+ end
+
+ # Private setter for scope +val+.
+ def set_scope(val)
+ @scope = val
+ build_path_query
+ @scope
+ end
+ protected :set_scope
+
+ # Setter for scope +val+.
+ def scope=(val)
+ set_scope(val)
+ val
+ end
+
+ # Returns filter.
+ def filter
+ @filter
+ end
+
+ # Private setter for filter +val+.
+ def set_filter(val)
+ @filter = val
+ build_path_query
+ @filter
+ end
+ protected :set_filter
+
+ # Setter for filter +val+.
+ def filter=(val)
+ set_filter(val)
+ val
+ end
+
+ # Returns extensions.
+ def extensions
+ @extensions
+ end
+
+ # Private setter for extensions +val+.
+ def set_extensions(val)
+ @extensions = val
+ build_path_query
+ @extensions
+ end
+ protected :set_extensions
+
+ # Setter for extensions +val+.
+ def extensions=(val)
+ set_extensions(val)
+ val
+ end
+
+ # Checks if Bundler::URI has a path.
+ # For Bundler::URI::LDAP this will return +false+.
+ def hierarchical?
+ false
+ end
+ end
+
+ register_scheme 'LDAP', LDAP
+end
diff --git a/lib/bundler/vendor/uri/lib/uri/ldaps.rb b/lib/bundler/vendor/uri/lib/uri/ldaps.rb
new file mode 100644
index 0000000000..c786168450
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/ldaps.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: false
+# = uri/ldap.rb
+#
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Bundler::URI for general documentation
+#
+
+require_relative 'ldap'
+
+module Bundler::URI
+
+ # The default port for LDAPS URIs is 636, and the scheme is 'ldaps:' rather
+ # than 'ldap:'. Other than that, LDAPS URIs are identical to LDAP URIs;
+ # see Bundler::URI::LDAP.
+ class LDAPS < LDAP
+ # A Default port of 636 for Bundler::URI::LDAPS
+ DEFAULT_PORT = 636
+ end
+
+ register_scheme 'LDAPS', LDAPS
+end
diff --git a/lib/bundler/vendor/uri/lib/uri/mailto.rb b/lib/bundler/vendor/uri/lib/uri/mailto.rb
new file mode 100644
index 0000000000..ff2e30be86
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/mailto.rb
@@ -0,0 +1,293 @@
+# frozen_string_literal: false
+# = uri/mailto.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Bundler::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Bundler::URI
+
+ #
+ # RFC6068, the mailto URL scheme.
+ #
+ class MailTo < Generic
+ include RFC2396_REGEXP
+
+ # A Default port of nil for Bundler::URI::MailTo.
+ DEFAULT_PORT = nil
+
+ # An Array of the available components for Bundler::URI::MailTo.
+ COMPONENT = [ :scheme, :to, :headers ].freeze
+
+ # :stopdoc:
+ # "hname" and "hvalue" are encodings of an RFC 822 header name and
+ # value, respectively. As with "to", all URL reserved characters must
+ # be encoded.
+ #
+ # "#mailbox" is as specified in RFC 822 [RFC822]. This means that it
+ # consists of zero or more comma-separated mail addresses, possibly
+ # including "phrase" and "comment" components. Note that all URL
+ # reserved characters in "to" must be encoded: in particular,
+ # parentheses, commas, and the percent sign ("%"), which commonly occur
+ # in the "mailbox" syntax.
+ #
+ # Within mailto URLs, the characters "?", "=", "&" are reserved.
+
+ # ; RFC 6068
+ # hfields = "?" hfield *( "&" hfield )
+ # hfield = hfname "=" hfvalue
+ # hfname = *qchar
+ # hfvalue = *qchar
+ # qchar = unreserved / pct-encoded / some-delims
+ # some-delims = "!" / "$" / "'" / "(" / ")" / "*"
+ # / "+" / "," / ";" / ":" / "@"
+ #
+ # ; RFC3986
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+ # pct-encoded = "%" HEXDIG HEXDIG
+ HEADER_REGEXP = /\A(?<hfield>(?:%\h\h|[!$'-.0-;@-Z_a-z~])*=(?:%\h\h|[!$'-.0-;@-Z_a-z~])*)(?:&\g<hfield>)*\z/
+ # practical regexp for email address
+ # https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
+ EMAIL_REGEXP = /\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z/
+ # :startdoc:
+
+ #
+ # == Description
+ #
+ # Creates a new Bundler::URI::MailTo object from components, with syntax checking.
+ #
+ # Components can be provided as an Array or Hash. If an Array is used,
+ # the components must be supplied as <code>[to, headers]</code>.
+ #
+ # If a Hash is used, the keys are the component names preceded by colons.
+ #
+ # The headers can be supplied as a pre-encoded string, such as
+ # <code>"subject=subscribe&cc=address"</code>, or as an Array of Arrays
+ # like <code>[['subject', 'subscribe'], ['cc', 'address']]</code>.
+ #
+ # Examples:
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # m1 = Bundler::URI::MailTo.build(['joe@example.com', 'subject=Ruby'])
+ # m1.to_s # => "mailto:joe@example.com?subject=Ruby"
+ #
+ # m2 = Bundler::URI::MailTo.build(['john@example.com', [['Subject', 'Ruby'], ['Cc', 'jack@example.com']]])
+ # m2.to_s # => "mailto:john@example.com?Subject=Ruby&Cc=jack@example.com"
+ #
+ # m3 = Bundler::URI::MailTo.build({:to => 'listman@example.com', :headers => [['subject', 'subscribe']]})
+ # m3.to_s # => "mailto:listman@example.com?subject=subscribe"
+ #
+ def self.build(args)
+ tmp = Util.make_components_hash(self, args)
+
+ case tmp[:to]
+ when Array
+ tmp[:opaque] = tmp[:to].join(',')
+ when String
+ tmp[:opaque] = tmp[:to].dup
+ else
+ tmp[:opaque] = ''
+ end
+
+ if tmp[:headers]
+ query =
+ case tmp[:headers]
+ when Array
+ tmp[:headers].collect { |x|
+ if x.kind_of?(Array)
+ x[0] + '=' + x[1..-1].join
+ else
+ x.to_s
+ end
+ }.join('&')
+ when Hash
+ tmp[:headers].collect { |h,v|
+ h + '=' + v
+ }.join('&')
+ else
+ tmp[:headers].to_s
+ end
+ unless query.empty?
+ tmp[:opaque] << '?' << query
+ end
+ end
+
+ super(tmp)
+ end
+
+ #
+ # == Description
+ #
+ # Creates a new Bundler::URI::MailTo object from generic URL components with
+ # no syntax checking.
+ #
+ # This method is usually called from Bundler::URI::parse, which checks
+ # the validity of each component.
+ #
+ def initialize(*arg)
+ super(*arg)
+
+ @to = nil
+ @headers = []
+
+ # The RFC3986 parser does not normally populate opaque
+ @opaque = "?#{@query}" if @query && !@opaque
+
+ unless @opaque
+ raise InvalidComponentError,
+ "missing opaque part for mailto URL"
+ end
+ to, header = @opaque.split('?', 2)
+ # allow semicolon as a addr-spec separator
+ # http://support.microsoft.com/kb/820868
+ unless /\A(?:[^@,;]+@[^@,;]+(?:\z|[,;]))*\z/ =~ to
+ raise InvalidComponentError,
+ "unrecognised opaque part for mailtoURL: #{@opaque}"
+ end
+
+ if arg[10] # arg_check
+ self.to = to
+ self.headers = header
+ else
+ set_to(to)
+ set_headers(header)
+ end
+ end
+
+ # The primary e-mail address of the URL, as a String.
+ attr_reader :to
+
+ # E-mail headers set by the URL, as an Array of Arrays.
+ attr_reader :headers
+
+ # Checks the to +v+ component.
+ def check_to(v)
+ return true unless v
+ return true if v.size == 0
+
+ v.split(/[,;]/).each do |addr|
+ # check url safety as path-rootless
+ if /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*\z/ !~ addr
+ raise InvalidComponentError,
+ "an address in 'to' is invalid as Bundler::URI #{addr.dump}"
+ end
+
+ # check addr-spec
+ # don't s/\+/ /g
+ addr.gsub!(/%\h\h/, Bundler::URI::TBLDECWWWCOMP_)
+ if EMAIL_REGEXP !~ addr
+ raise InvalidComponentError,
+ "an address in 'to' is invalid as uri-escaped addr-spec #{addr.dump}"
+ end
+ end
+
+ true
+ end
+ private :check_to
+
+ # Private setter for to +v+.
+ def set_to(v)
+ @to = v
+ end
+ protected :set_to
+
+ # Setter for to +v+.
+ def to=(v)
+ check_to(v)
+ set_to(v)
+ v
+ end
+
+ # Checks the headers +v+ component against either
+ # * HEADER_REGEXP
+ def check_headers(v)
+ return true unless v
+ return true if v.size == 0
+ if HEADER_REGEXP !~ v
+ raise InvalidComponentError,
+ "bad component(expected opaque component): #{v}"
+ end
+
+ true
+ end
+ private :check_headers
+
+ # Private setter for headers +v+.
+ def set_headers(v)
+ @headers = []
+ if v
+ v.split('&').each do |x|
+ @headers << x.split(/=/, 2)
+ end
+ end
+ end
+ protected :set_headers
+
+ # Setter for headers +v+.
+ def headers=(v)
+ check_headers(v)
+ set_headers(v)
+ v
+ end
+
+ # Constructs String from Bundler::URI.
+ def to_s
+ @scheme + ':' +
+ if @to
+ @to
+ else
+ ''
+ end +
+ if @headers.size > 0
+ '?' + @headers.collect{|x| x.join('=')}.join('&')
+ else
+ ''
+ end +
+ if @fragment
+ '#' + @fragment
+ else
+ ''
+ end
+ end
+
+ # Returns the RFC822 e-mail text equivalent of the URL, as a String.
+ #
+ # Example:
+ #
+ # require 'bundler/vendor/uri/lib/uri'
+ #
+ # uri = Bundler::URI.parse("mailto:ruby-list@ruby-lang.org?Subject=subscribe&cc=myaddr")
+ # uri.to_mailtext
+ # # => "To: ruby-list@ruby-lang.org\nSubject: subscribe\nCc: myaddr\n\n\n"
+ #
+ def to_mailtext
+ to = Bundler::URI.decode_www_form_component(@to)
+ head = ''
+ body = ''
+ @headers.each do |x|
+ case x[0]
+ when 'body'
+ body = Bundler::URI.decode_www_form_component(x[1])
+ when 'to'
+ to << ', ' + Bundler::URI.decode_www_form_component(x[1])
+ else
+ head << Bundler::URI.decode_www_form_component(x[0]).capitalize + ': ' +
+ Bundler::URI.decode_www_form_component(x[1]) + "\n"
+ end
+ end
+
+ "To: #{to}
+#{head}
+#{body}
+"
+ end
+ alias to_rfc822text to_mailtext
+ end
+
+ register_scheme 'MAILTO', MailTo
+end
diff --git a/lib/bundler/vendor/uri/lib/uri/rfc2396_parser.rb b/lib/bundler/vendor/uri/lib/uri/rfc2396_parser.rb
new file mode 100644
index 0000000000..522113fe67
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/rfc2396_parser.rb
@@ -0,0 +1,547 @@
+# frozen_string_literal: false
+#--
+# = uri/common.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License::
+# You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Bundler::URI for general documentation
+#
+
+module Bundler::URI
+ #
+ # Includes Bundler::URI::REGEXP::PATTERN
+ #
+ module RFC2396_REGEXP
+ #
+ # Patterns used to parse Bundler::URI's
+ #
+ module PATTERN
+ # :stopdoc:
+
+ # RFC 2396 (Bundler::URI Generic Syntax)
+ # RFC 2732 (IPv6 Literal Addresses in URL's)
+ # RFC 2373 (IPv6 Addressing Architecture)
+
+ # alpha = lowalpha | upalpha
+ ALPHA = "a-zA-Z"
+ # alphanum = alpha | digit
+ ALNUM = "#{ALPHA}\\d"
+
+ # hex = digit | "A" | "B" | "C" | "D" | "E" | "F" |
+ # "a" | "b" | "c" | "d" | "e" | "f"
+ HEX = "a-fA-F\\d"
+ # escaped = "%" hex hex
+ ESCAPED = "%[#{HEX}]{2}"
+ # mark = "-" | "_" | "." | "!" | "~" | "*" | "'" |
+ # "(" | ")"
+ # unreserved = alphanum | mark
+ UNRESERVED = "\\-_.!~*'()#{ALNUM}"
+ # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
+ # "$" | ","
+ # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
+ # "$" | "," | "[" | "]" (RFC 2732)
+ RESERVED = ";/?:@&=+$,\\[\\]"
+
+ # domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
+ DOMLABEL = "(?:[#{ALNUM}](?:[-#{ALNUM}]*[#{ALNUM}])?)"
+ # toplabel = alpha | alpha *( alphanum | "-" ) alphanum
+ TOPLABEL = "(?:[#{ALPHA}](?:[-#{ALNUM}]*[#{ALNUM}])?)"
+ # hostname = *( domainlabel "." ) toplabel [ "." ]
+ HOSTNAME = "(?:#{DOMLABEL}\\.)*#{TOPLABEL}\\.?"
+
+ # :startdoc:
+ end # PATTERN
+
+ # :startdoc:
+ end # REGEXP
+
+ # Class that parses String's into Bundler::URI's.
+ #
+ # It contains a Hash set of patterns and Regexp's that match and validate.
+ #
+ class RFC2396_Parser
+ include RFC2396_REGEXP
+
+ #
+ # == Synopsis
+ #
+ # Bundler::URI::RFC2396_Parser.new([opts])
+ #
+ # == Args
+ #
+ # The constructor accepts a hash as options for parser.
+ # Keys of options are pattern names of Bundler::URI components
+ # and values of options are pattern strings.
+ # The constructor generates set of regexps for parsing URIs.
+ #
+ # You can use the following keys:
+ #
+ # * :ESCAPED (Bundler::URI::PATTERN::ESCAPED in default)
+ # * :UNRESERVED (Bundler::URI::PATTERN::UNRESERVED in default)
+ # * :DOMLABEL (Bundler::URI::PATTERN::DOMLABEL in default)
+ # * :TOPLABEL (Bundler::URI::PATTERN::TOPLABEL in default)
+ # * :HOSTNAME (Bundler::URI::PATTERN::HOSTNAME in default)
+ #
+ # == Examples
+ #
+ # p = Bundler::URI::RFC2396_Parser.new(:ESCAPED => "(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})")
+ # u = p.parse("http://example.jp/%uABCD") #=> #<Bundler::URI::HTTP http://example.jp/%uABCD>
+ # Bundler::URI.parse(u.to_s) #=> raises Bundler::URI::InvalidURIError
+ #
+ # s = "http://example.com/ABCD"
+ # u1 = p.parse(s) #=> #<Bundler::URI::HTTP http://example.com/ABCD>
+ # u2 = Bundler::URI.parse(s) #=> #<Bundler::URI::HTTP http://example.com/ABCD>
+ # u1 == u2 #=> true
+ # u1.eql?(u2) #=> false
+ #
+ def initialize(opts = {})
+ @pattern = initialize_pattern(opts)
+ @pattern.each_value(&:freeze)
+ @pattern.freeze
+
+ @regexp = initialize_regexp(@pattern)
+ @regexp.each_value(&:freeze)
+ @regexp.freeze
+ end
+
+ # The Hash of patterns.
+ #
+ # See also #initialize_pattern.
+ attr_reader :pattern
+
+ # The Hash of Regexp.
+ #
+ # See also #initialize_regexp.
+ attr_reader :regexp
+
+ # Returns a split Bundler::URI against +regexp[:ABS_URI]+.
+ def split(uri)
+ case uri
+ when ''
+ # null uri
+
+ when @regexp[:ABS_URI]
+ scheme, opaque, userinfo, host, port,
+ registry, path, query, fragment = $~[1..-1]
+
+ # Bundler::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+
+ # absoluteURI = scheme ":" ( hier_part | opaque_part )
+ # hier_part = ( net_path | abs_path ) [ "?" query ]
+ # opaque_part = uric_no_slash *uric
+
+ # abs_path = "/" path_segments
+ # net_path = "//" authority [ abs_path ]
+
+ # authority = server | reg_name
+ # server = [ [ userinfo "@" ] hostport ]
+
+ if !scheme
+ raise InvalidURIError,
+ "bad Bundler::URI (absolute but no scheme): #{uri}"
+ end
+ if !opaque && (!path && (!host && !registry))
+ raise InvalidURIError,
+ "bad Bundler::URI (absolute but no path): #{uri}"
+ end
+
+ when @regexp[:REL_URI]
+ scheme = nil
+ opaque = nil
+
+ userinfo, host, port, registry,
+ rel_segment, abs_path, query, fragment = $~[1..-1]
+ if rel_segment && abs_path
+ path = rel_segment + abs_path
+ elsif rel_segment
+ path = rel_segment
+ elsif abs_path
+ path = abs_path
+ end
+
+ # Bundler::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+
+ # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ]
+
+ # net_path = "//" authority [ abs_path ]
+ # abs_path = "/" path_segments
+ # rel_path = rel_segment [ abs_path ]
+
+ # authority = server | reg_name
+ # server = [ [ userinfo "@" ] hostport ]
+
+ else
+ raise InvalidURIError, "bad Bundler::URI (is not Bundler::URI?): #{uri}"
+ end
+
+ path = '' if !path && !opaque # (see RFC2396 Section 5.2)
+ ret = [
+ scheme,
+ userinfo, host, port, # X
+ registry, # X
+ path, # Y
+ opaque, # Y
+ query,
+ fragment
+ ]
+ return ret
+ end
+
+ #
+ # == Args
+ #
+ # +uri+::
+ # String
+ #
+ # == Description
+ #
+ # Parses +uri+ and constructs either matching Bundler::URI scheme object
+ # (File, FTP, HTTP, HTTPS, LDAP, LDAPS, or MailTo) or Bundler::URI::Generic.
+ #
+ # == Usage
+ #
+ # Bundler::URI::RFC2396_PARSER.parse("ldap://ldap.example.com/dc=example?user=john")
+ # #=> #<Bundler::URI::LDAP ldap://ldap.example.com/dc=example?user=john>
+ #
+ def parse(uri)
+ Bundler::URI.for(*self.split(uri), self)
+ end
+
+ #
+ # == Args
+ #
+ # +uris+::
+ # an Array of Strings
+ #
+ # == Description
+ #
+ # Attempts to parse and merge a set of URIs.
+ #
+ def join(*uris)
+ uris[0] = convert_to_uri(uris[0])
+ uris.inject :merge
+ end
+
+ #
+ # :call-seq:
+ # extract( str )
+ # extract( str, schemes )
+ # extract( str, schemes ) {|item| block }
+ #
+ # == Args
+ #
+ # +str+::
+ # String to search
+ # +schemes+::
+ # Patterns to apply to +str+
+ #
+ # == Description
+ #
+ # Attempts to parse and merge a set of URIs.
+ # If no +block+ given, then returns the result,
+ # else it calls +block+ for each element in result.
+ #
+ # See also #make_regexp.
+ #
+ def extract(str, schemes = nil)
+ if block_given?
+ str.scan(make_regexp(schemes)) { yield $& }
+ nil
+ else
+ result = []
+ str.scan(make_regexp(schemes)) { result.push $& }
+ result
+ end
+ end
+
+ # Returns Regexp that is default +self.regexp[:ABS_URI_REF]+,
+ # unless +schemes+ is provided. Then it is a Regexp.union with +self.pattern[:X_ABS_URI]+.
+ def make_regexp(schemes = nil)
+ unless schemes
+ @regexp[:ABS_URI_REF]
+ else
+ /(?=(?i:#{Regexp.union(*schemes).source}):)#{@pattern[:X_ABS_URI]}/x
+ end
+ end
+
+ #
+ # :call-seq:
+ # escape( str )
+ # escape( str, unsafe )
+ #
+ # == Args
+ #
+ # +str+::
+ # String to make safe
+ # +unsafe+::
+ # Regexp to apply. Defaults to +self.regexp[:UNSAFE]+
+ #
+ # == Description
+ #
+ # Constructs a safe String from +str+, removing unsafe characters,
+ # replacing them with codes.
+ #
+ def escape(str, unsafe = @regexp[:UNSAFE])
+ unless unsafe.kind_of?(Regexp)
+ # perhaps unsafe is String object
+ unsafe = Regexp.new("[#{Regexp.quote(unsafe)}]", false)
+ end
+ str.gsub(unsafe) do
+ us = $&
+ tmp = ''
+ us.each_byte do |uc|
+ tmp << sprintf('%%%02X', uc)
+ end
+ tmp
+ end.force_encoding(Encoding::US_ASCII)
+ end
+
+ #
+ # :call-seq:
+ # unescape( str )
+ # unescape( str, escaped )
+ #
+ # == Args
+ #
+ # +str+::
+ # String to remove escapes from
+ # +escaped+::
+ # Regexp to apply. Defaults to +self.regexp[:ESCAPED]+
+ #
+ # == Description
+ #
+ # Removes escapes from +str+.
+ #
+ def unescape(str, escaped = @regexp[:ESCAPED])
+ enc = str.encoding
+ enc = Encoding::UTF_8 if enc == Encoding::US_ASCII
+ str.gsub(escaped) { [$&[1, 2]].pack('H2').force_encoding(enc) }
+ end
+
+ TO_S = Kernel.instance_method(:to_s) # :nodoc:
+ if TO_S.respond_to?(:bind_call)
+ def inspect # :nodoc:
+ TO_S.bind_call(self)
+ end
+ else
+ def inspect # :nodoc:
+ TO_S.bind(self).call
+ end
+ end
+
+ private
+
+ # Constructs the default Hash of patterns.
+ def initialize_pattern(opts = {})
+ ret = {}
+ ret[:ESCAPED] = escaped = (opts.delete(:ESCAPED) || PATTERN::ESCAPED)
+ ret[:UNRESERVED] = unreserved = opts.delete(:UNRESERVED) || PATTERN::UNRESERVED
+ ret[:RESERVED] = reserved = opts.delete(:RESERVED) || PATTERN::RESERVED
+ ret[:DOMLABEL] = opts.delete(:DOMLABEL) || PATTERN::DOMLABEL
+ ret[:TOPLABEL] = opts.delete(:TOPLABEL) || PATTERN::TOPLABEL
+ ret[:HOSTNAME] = hostname = opts.delete(:HOSTNAME)
+
+ # RFC 2396 (Bundler::URI Generic Syntax)
+ # RFC 2732 (IPv6 Literal Addresses in URL's)
+ # RFC 2373 (IPv6 Addressing Architecture)
+
+ # uric = reserved | unreserved | escaped
+ ret[:URIC] = uric = "(?:[#{unreserved}#{reserved}]|#{escaped})"
+ # uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" |
+ # "&" | "=" | "+" | "$" | ","
+ ret[:URIC_NO_SLASH] = uric_no_slash = "(?:[#{unreserved};?:@&=+$,]|#{escaped})"
+ # query = *uric
+ ret[:QUERY] = query = "#{uric}*"
+ # fragment = *uric
+ ret[:FRAGMENT] = fragment = "#{uric}*"
+
+ # hostname = *( domainlabel "." ) toplabel [ "." ]
+ # reg-name = *( unreserved / pct-encoded / sub-delims ) # RFC3986
+ unless hostname
+ ret[:HOSTNAME] = hostname = "(?:[a-zA-Z0-9\\-.]|%\\h\\h)+"
+ end
+
+ # RFC 2373, APPENDIX B:
+ # IPv6address = hexpart [ ":" IPv4address ]
+ # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
+ # hexpart = hexseq | hexseq "::" [ hexseq ] | "::" [ hexseq ]
+ # hexseq = hex4 *( ":" hex4)
+ # hex4 = 1*4HEXDIG
+ #
+ # XXX: This definition has a flaw. "::" + IPv4address must be
+ # allowed too. Here is a replacement.
+ #
+ # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
+ ret[:IPV4ADDR] = ipv4addr = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"
+ # hex4 = 1*4HEXDIG
+ hex4 = "[#{PATTERN::HEX}]{1,4}"
+ # lastpart = hex4 | IPv4address
+ lastpart = "(?:#{hex4}|#{ipv4addr})"
+ # hexseq1 = *( hex4 ":" ) hex4
+ hexseq1 = "(?:#{hex4}:)*#{hex4}"
+ # hexseq2 = *( hex4 ":" ) lastpart
+ hexseq2 = "(?:#{hex4}:)*#{lastpart}"
+ # IPv6address = hexseq2 | [ hexseq1 ] "::" [ hexseq2 ]
+ ret[:IPV6ADDR] = ipv6addr = "(?:#{hexseq2}|(?:#{hexseq1})?::(?:#{hexseq2})?)"
+
+ # IPv6prefix = ( hexseq1 | [ hexseq1 ] "::" [ hexseq1 ] ) "/" 1*2DIGIT
+ # unused
+
+ # ipv6reference = "[" IPv6address "]" (RFC 2732)
+ ret[:IPV6REF] = ipv6ref = "\\[#{ipv6addr}\\]"
+
+ # host = hostname | IPv4address
+ # host = hostname | IPv4address | IPv6reference (RFC 2732)
+ ret[:HOST] = host = "(?:#{hostname}|#{ipv4addr}|#{ipv6ref})"
+ # port = *digit
+ ret[:PORT] = port = '\d*'
+ # hostport = host [ ":" port ]
+ ret[:HOSTPORT] = hostport = "#{host}(?::#{port})?"
+
+ # userinfo = *( unreserved | escaped |
+ # ";" | ":" | "&" | "=" | "+" | "$" | "," )
+ ret[:USERINFO] = userinfo = "(?:[#{unreserved};:&=+$,]|#{escaped})*"
+
+ # pchar = unreserved | escaped |
+ # ":" | "@" | "&" | "=" | "+" | "$" | ","
+ pchar = "(?:[#{unreserved}:@&=+$,]|#{escaped})"
+ # param = *pchar
+ param = "#{pchar}*"
+ # segment = *pchar *( ";" param )
+ segment = "#{pchar}*(?:;#{param})*"
+ # path_segments = segment *( "/" segment )
+ ret[:PATH_SEGMENTS] = path_segments = "#{segment}(?:/#{segment})*"
+
+ # server = [ [ userinfo "@" ] hostport ]
+ server = "(?:#{userinfo}@)?#{hostport}"
+ # reg_name = 1*( unreserved | escaped | "$" | "," |
+ # ";" | ":" | "@" | "&" | "=" | "+" )
+ ret[:REG_NAME] = reg_name = "(?:[#{unreserved}$,;:@&=+]|#{escaped})+"
+ # authority = server | reg_name
+ authority = "(?:#{server}|#{reg_name})"
+
+ # rel_segment = 1*( unreserved | escaped |
+ # ";" | "@" | "&" | "=" | "+" | "$" | "," )
+ ret[:REL_SEGMENT] = rel_segment = "(?:[#{unreserved};@&=+$,]|#{escaped})+"
+
+ # scheme = alpha *( alpha | digit | "+" | "-" | "." )
+ ret[:SCHEME] = scheme = "[#{PATTERN::ALPHA}][\\-+.#{PATTERN::ALPHA}\\d]*"
+
+ # abs_path = "/" path_segments
+ ret[:ABS_PATH] = abs_path = "/#{path_segments}"
+ # rel_path = rel_segment [ abs_path ]
+ ret[:REL_PATH] = rel_path = "#{rel_segment}(?:#{abs_path})?"
+ # net_path = "//" authority [ abs_path ]
+ ret[:NET_PATH] = net_path = "//#{authority}(?:#{abs_path})?"
+
+ # hier_part = ( net_path | abs_path ) [ "?" query ]
+ ret[:HIER_PART] = hier_part = "(?:#{net_path}|#{abs_path})(?:\\?(?:#{query}))?"
+ # opaque_part = uric_no_slash *uric
+ ret[:OPAQUE_PART] = opaque_part = "#{uric_no_slash}#{uric}*"
+
+ # absoluteURI = scheme ":" ( hier_part | opaque_part )
+ ret[:ABS_URI] = abs_uri = "#{scheme}:(?:#{hier_part}|#{opaque_part})"
+ # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ]
+ ret[:REL_URI] = rel_uri = "(?:#{net_path}|#{abs_path}|#{rel_path})(?:\\?#{query})?"
+
+ # Bundler::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+ ret[:URI_REF] = "(?:#{abs_uri}|#{rel_uri})?(?:##{fragment})?"
+
+ ret[:X_ABS_URI] = "
+ (#{scheme}): (?# 1: scheme)
+ (?:
+ (#{opaque_part}) (?# 2: opaque)
+ |
+ (?:(?:
+ //(?:
+ (?:(?:(#{userinfo})@)? (?# 3: userinfo)
+ (?:(#{host})(?::(\\d*))?))? (?# 4: host, 5: port)
+ |
+ (#{reg_name}) (?# 6: registry)
+ )
+ |
+ (?!//)) (?# XXX: '//' is the mark for hostport)
+ (#{abs_path})? (?# 7: path)
+ )(?:\\?(#{query}))? (?# 8: query)
+ )
+ (?:\\#(#{fragment}))? (?# 9: fragment)
+ "
+
+ ret[:X_REL_URI] = "
+ (?:
+ (?:
+ //
+ (?:
+ (?:(#{userinfo})@)? (?# 1: userinfo)
+ (#{host})?(?::(\\d*))? (?# 2: host, 3: port)
+ |
+ (#{reg_name}) (?# 4: registry)
+ )
+ )
+ |
+ (#{rel_segment}) (?# 5: rel_segment)
+ )?
+ (#{abs_path})? (?# 6: abs_path)
+ (?:\\?(#{query}))? (?# 7: query)
+ (?:\\#(#{fragment}))? (?# 8: fragment)
+ "
+
+ ret
+ end
+
+ # Constructs the default Hash of Regexp's.
+ def initialize_regexp(pattern)
+ ret = {}
+
+ # for Bundler::URI::split
+ ret[:ABS_URI] = Regexp.new('\A\s*+' + pattern[:X_ABS_URI] + '\s*\z', Regexp::EXTENDED)
+ ret[:REL_URI] = Regexp.new('\A\s*+' + pattern[:X_REL_URI] + '\s*\z', Regexp::EXTENDED)
+
+ # for Bundler::URI::extract
+ ret[:URI_REF] = Regexp.new(pattern[:URI_REF])
+ ret[:ABS_URI_REF] = Regexp.new(pattern[:X_ABS_URI], Regexp::EXTENDED)
+ ret[:REL_URI_REF] = Regexp.new(pattern[:X_REL_URI], Regexp::EXTENDED)
+
+ # for Bundler::URI::escape/unescape
+ ret[:ESCAPED] = Regexp.new(pattern[:ESCAPED])
+ ret[:UNSAFE] = Regexp.new("[^#{pattern[:UNRESERVED]}#{pattern[:RESERVED]}]")
+
+ # for Generic#initialize
+ ret[:SCHEME] = Regexp.new("\\A#{pattern[:SCHEME]}\\z")
+ ret[:USERINFO] = Regexp.new("\\A#{pattern[:USERINFO]}\\z")
+ ret[:HOST] = Regexp.new("\\A#{pattern[:HOST]}\\z")
+ ret[:PORT] = Regexp.new("\\A#{pattern[:PORT]}\\z")
+ ret[:OPAQUE] = Regexp.new("\\A#{pattern[:OPAQUE_PART]}\\z")
+ ret[:REGISTRY] = Regexp.new("\\A#{pattern[:REG_NAME]}\\z")
+ ret[:ABS_PATH] = Regexp.new("\\A#{pattern[:ABS_PATH]}\\z")
+ ret[:REL_PATH] = Regexp.new("\\A#{pattern[:REL_PATH]}\\z")
+ ret[:QUERY] = Regexp.new("\\A#{pattern[:QUERY]}\\z")
+ ret[:FRAGMENT] = Regexp.new("\\A#{pattern[:FRAGMENT]}\\z")
+
+ ret
+ end
+
+ # Returns +uri+ as-is if it is Bundler::URI, or convert it to Bundler::URI if it is
+ # a String.
+ def convert_to_uri(uri)
+ if uri.is_a?(Bundler::URI::Generic)
+ uri
+ elsif uri = String.try_convert(uri)
+ parse(uri)
+ else
+ raise ArgumentError,
+ "bad argument (expected Bundler::URI object or Bundler::URI string)"
+ end
+ end
+
+ end # class Parser
+
+ # Backward compatibility for Bundler::URI::REGEXP::PATTERN::*
+ RFC2396_Parser.new.pattern.each_pair do |sym, str|
+ unless RFC2396_REGEXP::PATTERN.const_defined?(sym, false)
+ RFC2396_REGEXP::PATTERN.const_set(sym, str)
+ end
+ end
+end # module Bundler::URI
diff --git a/lib/bundler/vendor/uri/lib/uri/rfc3986_parser.rb b/lib/bundler/vendor/uri/lib/uri/rfc3986_parser.rb
new file mode 100644
index 0000000000..d1ff28df23
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/rfc3986_parser.rb
@@ -0,0 +1,206 @@
+# frozen_string_literal: true
+module Bundler::URI
+ class RFC3986_Parser # :nodoc:
+ # Bundler::URI defined in RFC3986
+ HOST = %r[
+ (?<IP-literal>\[(?:
+ (?<IPv6address>
+ (?:\h{1,4}:){6}
+ (?<ls32>\h{1,4}:\h{1,4}
+ | (?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)
+ \.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>)
+ )
+ | ::(?:\h{1,4}:){5}\g<ls32>
+ | \h{1,4}?::(?:\h{1,4}:){4}\g<ls32>
+ | (?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>
+ | (?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>
+ | (?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>
+ | (?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>
+ | (?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}
+ | (?:(?:\h{1,4}:){,6}\h{1,4})?::
+ )
+ | (?<IPvFuture>v\h++\.[!$&-.0-9:;=A-Z_a-z~]++)
+ )\])
+ | \g<IPv4address>
+ | (?<reg-name>(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])*+)
+ ]x
+
+ USERINFO = /(?:%\h\h|[!$&-.0-9:;=A-Z_a-z~])*+/
+
+ SCHEME = %r[[A-Za-z][+\-.0-9A-Za-z]*+].source
+ SEG = %r[(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/])].source
+ SEG_NC = %r[(?:%\h\h|[!$&-.0-9;=@A-Z_a-z~])].source
+ FRAGMENT = %r[(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/?])*+].source
+
+ RFC3986_URI = %r[\A
+ (?<seg>#{SEG}){0}
+ (?<Bundler::URI>
+ (?<scheme>#{SCHEME}):
+ (?<hier-part>//
+ (?<authority>
+ (?:(?<userinfo>#{USERINFO.source})@)?
+ (?<host>#{HOST.source.delete(" \n")})
+ (?::(?<port>\d*+))?
+ )
+ (?<path-abempty>(?:/\g<seg>*+)?)
+ | (?<path-absolute>/((?!/)\g<seg>++)?)
+ | (?<path-rootless>(?!/)\g<seg>++)
+ | (?<path-empty>)
+ )
+ (?:\?(?<query>[^\#]*+))?
+ (?:\#(?<fragment>#{FRAGMENT}))?
+ )\z]x
+
+ RFC3986_relative_ref = %r[\A
+ (?<seg>#{SEG}){0}
+ (?<relative-ref>
+ (?<relative-part>//
+ (?<authority>
+ (?:(?<userinfo>#{USERINFO.source})@)?
+ (?<host>#{HOST.source.delete(" \n")}(?<!/))?
+ (?::(?<port>\d*+))?
+ )
+ (?<path-abempty>(?:/\g<seg>*+)?)
+ | (?<path-absolute>/\g<seg>*+)
+ | (?<path-noscheme>#{SEG_NC}++(?:/\g<seg>*+)?)
+ | (?<path-empty>)
+ )
+ (?:\?(?<query>[^#]*+))?
+ (?:\#(?<fragment>#{FRAGMENT}))?
+ )\z]x
+ attr_reader :regexp
+
+ def initialize
+ @regexp = default_regexp.each_value(&:freeze).freeze
+ end
+
+ def split(uri) #:nodoc:
+ begin
+ uri = uri.to_str
+ rescue NoMethodError
+ raise InvalidURIError, "bad Bundler::URI (is not Bundler::URI?): #{uri.inspect}"
+ end
+ uri.ascii_only? or
+ raise InvalidURIError, "Bundler::URI must be ascii only #{uri.dump}"
+ if m = RFC3986_URI.match(uri)
+ query = m["query"]
+ scheme = m["scheme"]
+ opaque = m["path-rootless"]
+ if opaque
+ opaque << "?#{query}" if query
+ [ scheme,
+ nil, # userinfo
+ nil, # host
+ nil, # port
+ nil, # registry
+ nil, # path
+ opaque,
+ nil, # query
+ m["fragment"]
+ ]
+ else # normal
+ [ scheme,
+ m["userinfo"],
+ m["host"],
+ m["port"],
+ nil, # registry
+ (m["path-abempty"] ||
+ m["path-absolute"] ||
+ m["path-empty"]),
+ nil, # opaque
+ query,
+ m["fragment"]
+ ]
+ end
+ elsif m = RFC3986_relative_ref.match(uri)
+ [ nil, # scheme
+ m["userinfo"],
+ m["host"],
+ m["port"],
+ nil, # registry,
+ (m["path-abempty"] ||
+ m["path-absolute"] ||
+ m["path-noscheme"] ||
+ m["path-empty"]),
+ nil, # opaque
+ m["query"],
+ m["fragment"]
+ ]
+ else
+ raise InvalidURIError, "bad Bundler::URI (is not Bundler::URI?): #{uri.inspect}"
+ end
+ end
+
+ def parse(uri) # :nodoc:
+ Bundler::URI.for(*self.split(uri), self)
+ end
+
+ def join(*uris) # :nodoc:
+ uris[0] = convert_to_uri(uris[0])
+ uris.inject :merge
+ end
+
+ # Compatibility for RFC2396 parser
+ def extract(str, schemes = nil, &block) # :nodoc:
+ warn "Bundler::URI::RFC3986_PARSER.extract is obsolete. Use Bundler::URI::RFC2396_PARSER.extract explicitly.", uplevel: 1 if $VERBOSE
+ RFC2396_PARSER.extract(str, schemes, &block)
+ end
+
+ # Compatibility for RFC2396 parser
+ def make_regexp(schemes = nil) # :nodoc:
+ warn "Bundler::URI::RFC3986_PARSER.make_regexp is obsolete. Use Bundler::URI::RFC2396_PARSER.make_regexp explicitly.", uplevel: 1 if $VERBOSE
+ RFC2396_PARSER.make_regexp(schemes)
+ end
+
+ # Compatibility for RFC2396 parser
+ def escape(str, unsafe = nil) # :nodoc:
+ warn "Bundler::URI::RFC3986_PARSER.escape is obsolete. Use Bundler::URI::RFC2396_PARSER.escape explicitly.", uplevel: 1 if $VERBOSE
+ unsafe ? RFC2396_PARSER.escape(str, unsafe) : RFC2396_PARSER.escape(str)
+ end
+
+ # Compatibility for RFC2396 parser
+ def unescape(str, escaped = nil) # :nodoc:
+ warn "Bundler::URI::RFC3986_PARSER.unescape is obsolete. Use Bundler::URI::RFC2396_PARSER.unescape explicitly.", uplevel: 1 if $VERBOSE
+ escaped ? RFC2396_PARSER.unescape(str, escaped) : RFC2396_PARSER.unescape(str)
+ end
+
+ @@to_s = Kernel.instance_method(:to_s)
+ if @@to_s.respond_to?(:bind_call)
+ def inspect
+ @@to_s.bind_call(self)
+ end
+ else
+ def inspect
+ @@to_s.bind(self).call
+ end
+ end
+
+ private
+
+ def default_regexp # :nodoc:
+ {
+ SCHEME: %r[\A#{SCHEME}\z]o,
+ USERINFO: %r[\A#{USERINFO}\z]o,
+ HOST: %r[\A#{HOST}\z]o,
+ ABS_PATH: %r[\A/#{SEG}*+\z]o,
+ REL_PATH: %r[\A(?!/)#{SEG}++\z]o,
+ QUERY: %r[\A(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/?])*+\z],
+ FRAGMENT: %r[\A#{FRAGMENT}\z]o,
+ OPAQUE: %r[\A(?:[^/].*)?\z],
+ PORT: /\A[\x09\x0a\x0c\x0d ]*+\d*[\x09\x0a\x0c\x0d ]*\z/,
+ }
+ end
+
+ def convert_to_uri(uri)
+ if uri.is_a?(Bundler::URI::Generic)
+ uri
+ elsif uri = String.try_convert(uri)
+ parse(uri)
+ else
+ raise ArgumentError,
+ "bad argument (expected Bundler::URI object or Bundler::URI string)"
+ end
+ end
+
+ end # class Parser
+end # module Bundler::URI
diff --git a/lib/bundler/vendor/uri/lib/uri/version.rb b/lib/bundler/vendor/uri/lib/uri/version.rb
new file mode 100644
index 0000000000..ad76308e81
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/version.rb
@@ -0,0 +1,6 @@
+module Bundler::URI
+ # :stopdoc:
+ VERSION = '1.1.1'.freeze
+ VERSION_CODE = VERSION.split('.').map{|s| s.rjust(2, '0')}.join.freeze
+ # :startdoc:
+end
diff --git a/lib/bundler/vendor/uri/lib/uri/ws.rb b/lib/bundler/vendor/uri/lib/uri/ws.rb
new file mode 100644
index 0000000000..10ae6f5834
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/ws.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: false
+# = uri/ws.rb
+#
+# Author:: Matt Muller <mamuller@amazon.com>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Bundler::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Bundler::URI
+
+ #
+ # The syntax of WS URIs is defined in RFC6455 section 3.
+ #
+ # Note that the Ruby Bundler::URI library allows WS URLs containing usernames and
+ # passwords. This is not legal as per the RFC, but used to be
+ # supported in Internet Explorer 5 and 6, before the MS04-004 security
+ # update. See <URL:http://support.microsoft.com/kb/834489>.
+ #
+ class WS < Generic
+ # A Default port of 80 for Bundler::URI::WS.
+ DEFAULT_PORT = 80
+
+ # An Array of the available components for Bundler::URI::WS.
+ COMPONENT = %i[
+ scheme
+ userinfo host port
+ path
+ query
+ ].freeze
+
+ #
+ # == Description
+ #
+ # Creates a new Bundler::URI::WS object from components, with syntax checking.
+ #
+ # The components accepted are userinfo, host, port, path, and query.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[userinfo, host, port, path, query]</code>.
+ #
+ # Example:
+ #
+ # uri = Bundler::URI::WS.build(host: 'www.example.com', path: '/foo/bar')
+ #
+ # uri = Bundler::URI::WS.build([nil, "www.example.com", nil, "/path", "query"])
+ #
+ # Currently, if passed userinfo components this method generates
+ # invalid WS URIs as per RFC 1738.
+ #
+ def self.build(args)
+ tmp = Util.make_components_hash(self, args)
+ super(tmp)
+ end
+
+ #
+ # == Description
+ #
+ # Returns the full path for a WS Bundler::URI, as required by Net::HTTP::Get.
+ #
+ # If the Bundler::URI contains a query, the full path is Bundler::URI#path + '?' + Bundler::URI#query.
+ # Otherwise, the path is simply Bundler::URI#path.
+ #
+ # Example:
+ #
+ # uri = Bundler::URI::WS.build(path: '/foo/bar', query: 'test=true')
+ # uri.request_uri # => "/foo/bar?test=true"
+ #
+ def request_uri
+ return unless @path
+
+ url = @query ? "#@path?#@query" : @path.dup
+ url.start_with?(?/.freeze) ? url : ?/ + url
+ end
+ end
+
+ register_scheme 'WS', WS
+end
diff --git a/lib/bundler/vendor/uri/lib/uri/wss.rb b/lib/bundler/vendor/uri/lib/uri/wss.rb
new file mode 100644
index 0000000000..e8db1ceabf
--- /dev/null
+++ b/lib/bundler/vendor/uri/lib/uri/wss.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: false
+# = uri/wss.rb
+#
+# Author:: Matt Muller <mamuller@amazon.com>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Bundler::URI for general documentation
+#
+
+require_relative 'ws'
+
+module Bundler::URI
+
+ # The default port for WSS URIs is 443, and the scheme is 'wss:' rather
+ # than 'ws:'. Other than that, WSS URIs are identical to WS URIs;
+ # see Bundler::URI::WS.
+ class WSS < WS
+ # A Default port of 443 for Bundler::URI::WSS
+ DEFAULT_PORT = 443
+ end
+
+ register_scheme 'WSS', WSS
+end
diff --git a/lib/bundler/vendored_fileutils.rb b/lib/bundler/vendored_fileutils.rb
new file mode 100644
index 0000000000..1be1138ce2
--- /dev/null
+++ b/lib/bundler/vendored_fileutils.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+module Bundler; end
+require_relative "vendor/fileutils/lib/fileutils"
diff --git a/lib/bundler/vendored_net_http.rb b/lib/bundler/vendored_net_http.rb
new file mode 100644
index 0000000000..8ff2ccd1fe
--- /dev/null
+++ b/lib/bundler/vendored_net_http.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# This defined? guard can be removed once RubyGems 3.4 support is dropped.
+#
+# Bundler specs load this code from `spec/support/vendored_net_http.rb` to avoid
+# activating the Bundler gem too early. Without this guard, we get redefinition
+# warnings once Bundler is actually activated and
+# `lib/bundler/vendored_net_http.rb` is required. This is not an issue in
+# RubyGems versions including `rubygems/vendored_net_http` since `require` takes
+# care of avoiding the double load.
+#
+unless defined?(Gem::Net)
+ begin
+ require "rubygems/vendored_net_http"
+ rescue LoadError
+ begin
+ require "rubygems/net/http"
+ rescue LoadError
+ require "net/http"
+ Gem::Net = Net
+ end
+ end
+end
diff --git a/lib/bundler/vendored_persistent.rb b/lib/bundler/vendored_persistent.rb
new file mode 100644
index 0000000000..ab985c267f
--- /dev/null
+++ b/lib/bundler/vendored_persistent.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Bundler
+ module Persistent
+ module Net
+ module HTTP
+ end
+ end
+ end
+end
+require_relative "vendor/net-http-persistent/lib/net/http/persistent"
diff --git a/lib/bundler/vendored_pub_grub.rb b/lib/bundler/vendored_pub_grub.rb
new file mode 100644
index 0000000000..b36a996b29
--- /dev/null
+++ b/lib/bundler/vendored_pub_grub.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+module Bundler; end
+require_relative "vendor/pub_grub/lib/pub_grub"
diff --git a/lib/bundler/vendored_securerandom.rb b/lib/bundler/vendored_securerandom.rb
new file mode 100644
index 0000000000..6a704dbd40
--- /dev/null
+++ b/lib/bundler/vendored_securerandom.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# Use RubyGems vendored copy when available. Otherwise fallback to Bundler
+# vendored copy. The vendored copy in Bundler can be removed once support for
+# RubyGems 3.5.18 is dropped.
+
+begin
+ require "rubygems/vendored_securerandom"
+rescue LoadError
+ require_relative "vendor/securerandom/lib/securerandom"
+ Gem::SecureRandom = Bundler::SecureRandom
+end
diff --git a/lib/bundler/vendored_thor.rb b/lib/bundler/vendored_thor.rb
new file mode 100644
index 0000000000..0666cfc9b9
--- /dev/null
+++ b/lib/bundler/vendored_thor.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Bundler
+ def self.require_thor_actions
+ require_relative "vendor/thor/lib/thor/actions"
+ end
+end
+require_relative "vendor/thor/lib/thor"
diff --git a/lib/bundler/vendored_timeout.rb b/lib/bundler/vendored_timeout.rb
new file mode 100644
index 0000000000..9b2507c0cc
--- /dev/null
+++ b/lib/bundler/vendored_timeout.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+begin
+ require "rubygems/vendored_timeout"
+rescue LoadError
+ begin
+ require "rubygems/timeout"
+ rescue LoadError
+ require "timeout"
+ Gem::Timeout = Timeout
+ end
+end
diff --git a/lib/bundler/vendored_tsort.rb b/lib/bundler/vendored_tsort.rb
new file mode 100644
index 0000000000..38aed0b5de
--- /dev/null
+++ b/lib/bundler/vendored_tsort.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+module Bundler; end
+require_relative "vendor/tsort/lib/tsort"
diff --git a/lib/bundler/vendored_uri.rb b/lib/bundler/vendored_uri.rb
new file mode 100644
index 0000000000..2efddc65f9
--- /dev/null
+++ b/lib/bundler/vendored_uri.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Bundler; end
+
+# Use RubyGems vendored copy when available. Otherwise fallback to Bundler
+# vendored copy. The vendored copy in Bundler can be removed once support for
+# RubyGems 3.5 is dropped.
+
+begin
+ require "rubygems/vendor/uri/lib/uri"
+rescue LoadError
+ require_relative "vendor/uri/lib/uri"
+ Gem::URI = Bundler::URI
+
+ module Gem
+ def URI(uri) # rubocop:disable Naming/MethodName
+ Bundler::URI(uri)
+ end
+ module_function :URI
+ end
+end
diff --git a/lib/bundler/version.rb b/lib/bundler/version.rb
new file mode 100644
index 0000000000..ca7bb0719a
--- /dev/null
+++ b/lib/bundler/version.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: false
+
+module Bundler
+ VERSION = "4.1.0.dev".freeze
+
+ def self.bundler_major_version
+ @bundler_major_version ||= gem_version.segments.first
+ end
+
+ def self.gem_version
+ @gem_version ||= Gem::Version.create(VERSION)
+ end
+
+ def self.verbose_version
+ @verbose_version ||= "#{VERSION}#{simulated_version ? " (simulating Bundler #{simulated_version})" : ""}"
+ end
+
+ def self.simulated_version
+ @simulated_version ||= Bundler.settings[:simulate_version]
+ end
+end
diff --git a/lib/bundler/vlad.rb b/lib/bundler/vlad.rb
new file mode 100644
index 0000000000..c3a3d949a6
--- /dev/null
+++ b/lib/bundler/vlad.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+require_relative "shared_helpers"
+Bundler::SharedHelpers.feature_removed! "The Bundler task for Vlad"
diff --git a/lib/bundler/worker.rb b/lib/bundler/worker.rb
new file mode 100644
index 0000000000..77f4f004aa
--- /dev/null
+++ b/lib/bundler/worker.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+module Bundler
+ class Worker
+ POISON = Object.new
+
+ class WrappedException < StandardError
+ attr_reader :exception
+ def initialize(exn)
+ @exception = exn
+ end
+ end
+
+ # @return [String] the name of the worker
+ attr_reader :name
+
+ # Creates a worker pool of specified size
+ #
+ # @param size [Integer] Size of pool
+ # @param name [String] name the name of the worker
+ # @param func [Proc] job to run in inside the worker pool
+ def initialize(size, name, func)
+ @name = name
+ @request_queue = Thread::Queue.new
+ @request_queue_with_priority = Thread::Queue.new
+ @response_queue = Thread::Queue.new
+ @func = func
+ @size = size
+ @threads = nil
+ @previous_interrupt_handler = nil
+ end
+
+ # Enqueue a request to be executed in the worker pool
+ #
+ # @param obj [String] mostly it is name of spec that should be downloaded
+ def enq(obj, priority: false)
+ queue = priority ? @request_queue_with_priority : @request_queue
+ create_threads unless @threads
+ queue.enq obj
+ end
+
+ # Retrieves results of job function being executed in worker pool
+ def deq
+ result = @response_queue.deq
+ raise result.exception if result.is_a?(WrappedException)
+ result
+ end
+
+ def stop
+ stop_threads
+ end
+
+ private
+
+ def process_queue(i)
+ loop do
+ obj = begin
+ @request_queue_with_priority.deq(true)
+ rescue ThreadError
+ @request_queue.deq(false, timeout: 0.05)
+ end
+
+ next if obj.nil?
+ break if obj.equal? POISON
+ @response_queue.enq apply_func(obj, i)
+ end
+ end
+
+ def apply_func(obj, i)
+ @func.call(obj, i)
+ rescue Exception => e # rubocop:disable Lint/RescueException
+ WrappedException.new(e)
+ end
+
+ # Stop the worker threads by sending a poison object down the request queue
+ # so as worker threads after retrieving it, shut themselves down
+ def stop_threads
+ return unless @threads
+
+ @threads.each { @request_queue.enq POISON }
+ @threads.each(&:join)
+
+ remove_interrupt_handler
+
+ @threads = nil
+ end
+
+ def abort_threads
+ Bundler.ui.debug("\n#{caller.join("\n")}")
+ @threads.each(&:exit)
+ exit 1
+ end
+
+ def create_threads
+ creation_errors = []
+
+ @threads = Array.new(@size) do |i|
+ Thread.start { process_queue(i) }.tap do |thread|
+ thread.name = "#{name} Worker ##{i}"
+ end
+ rescue ThreadError => e
+ creation_errors << e
+ nil
+ end.compact
+
+ add_interrupt_handler unless @threads.empty?
+
+ return if creation_errors.empty?
+
+ message = "Failed to create threads for the #{name} worker: #{creation_errors.map(&:to_s).uniq.join(", ")}"
+ raise ThreadCreationError, message if @threads.empty?
+ Bundler.ui.info message
+ end
+
+ def add_interrupt_handler
+ @previous_interrupt_handler = trap("INT") { abort_threads }
+ end
+
+ def remove_interrupt_handler
+ return unless @previous_interrupt_handler
+
+ trap "INT", @previous_interrupt_handler
+ end
+ end
+end
diff --git a/lib/bundler/yaml_serializer.rb b/lib/bundler/yaml_serializer.rb
new file mode 100644
index 0000000000..ab1eb6dbcf
--- /dev/null
+++ b/lib/bundler/yaml_serializer.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+module Bundler
+ # A stub yaml serializer that can handle only hashes and strings (as of now).
+ module YAMLSerializer
+ module_function
+
+ def dump(hash)
+ yaml = String.new("---")
+ yaml << dump_hash(hash)
+ end
+
+ def dump_hash(hash)
+ yaml = String.new("\n")
+ hash.each do |k, v|
+ yaml << k << ":"
+ if v.is_a?(Hash)
+ yaml << dump_hash(v).gsub(/^(?!$)/, " ") # indent all non-empty lines
+ elsif v.is_a?(Array) # Expected to be array of strings
+ if v.empty?
+ yaml << " []\n"
+ else
+ yaml << "\n- " << v.map {|s| s.to_s.gsub(/\s+/, " ").inspect }.join("\n- ") << "\n"
+ end
+ else
+ yaml << " " << v.to_s.gsub(/\s+/, " ").inspect << "\n"
+ end
+ end
+ yaml
+ end
+
+ ARRAY_REGEX = /
+ ^
+ (?:[ ]*-[ ]) # '- ' before array items
+ (['"]?) # optional opening quote
+ (.*) # value
+ \1 # matching closing quote
+ $
+ /xo
+
+ HASH_REGEX = /
+ ^
+ ([ ]*) # indentations
+ ([^#]+) # key excludes comment char '#'
+ (?::(?=(?:\s|$))) # : (without the lookahead the #key includes this when : is present in value)
+ [ ]?
+ (['"]?) # optional opening quote
+ (.*) # value
+ \3 # matching closing quote
+ $
+ /xo
+
+ def load(str)
+ res = {}
+ stack = [res]
+ last_hash = nil
+ last_empty_key = nil
+ str.split(/\r?\n/) do |line|
+ if match = HASH_REGEX.match(line)
+ indent, key, quote, val = match.captures
+ val = strip_comment(val)
+
+ depth = indent.size / 2
+ if quote.empty? && val.empty?
+ new_hash = {}
+ stack[depth][key] = new_hash
+ stack[depth + 1] = new_hash
+ last_empty_key = key
+ last_hash = stack[depth]
+ else
+ val = [] if val == "[]" # empty array
+ stack[depth][key] = val
+ end
+ elsif match = ARRAY_REGEX.match(line)
+ _, val = match.captures
+ val = strip_comment(val)
+
+ last_hash[last_empty_key] = [] unless last_hash[last_empty_key].is_a?(Array)
+
+ last_hash[last_empty_key].push(val)
+ end
+ end
+ res
+ end
+
+ def strip_comment(val)
+ if val.include?("#") && !val.start_with?("#")
+ val.split("#", 2).first.strip
+ else
+ val
+ end
+ end
+
+ class << self
+ private :dump_hash
+ end
+ end
+end
diff --git a/lib/cgi.rb b/lib/cgi.rb
index 6acf05b382..0c403bd19c 100644
--- a/lib/cgi.rb
+++ b/lib/cgi.rb
@@ -1,274 +1,9 @@
-#
-# cgi.rb - cgi support library
-#
-# Copyright (C) 2000 Network Applied Communication Laboratory, Inc.
-#
-# Copyright (C) 2000 Information-technology Promotion Agency, Japan
-#
-# Author: Wakou Aoyama <wakou@ruby-lang.org>
-#
-# Documentation: Wakou Aoyama (RDoc'd and embellished by William Webber)
-#
-# == Overview
-#
-# The Common Gateway Interface (CGI) is a simple protocol
-# for passing an HTTP request from a web server to a
-# standalone program, and returning the output to the web
-# browser. Basically, a CGI program is called with the
-# parameters of the request passed in either in the
-# environment (GET) or via $stdin (POST), and everything
-# it prints to $stdout is returned to the client.
-#
-# This file holds the +CGI+ class. This class provides
-# functionality for retrieving HTTP request parameters,
-# managing cookies, and generating HTML output. See the
-# class documentation for more details and examples of use.
-#
-# The file cgi/session.rb provides session management
-# functionality; see that file for more details.
-#
-# See http://www.w3.org/CGI/ for more information on the CGI
-# protocol.
+# frozen_string_literal: true
-raise "Please, use ruby 1.9.0 or later." if RUBY_VERSION < "1.9.0"
+require "cgi/escape"
+warn <<-WARNING, uplevel: Gem::BUNDLED_GEMS.uplevel if $VERBOSE
+CGI library is removed from Ruby 4.0. Please use cgi/escape instead for CGI.escape and CGI.unescape features.
-# CGI class. See documentation for the file cgi.rb for an overview
-# of the CGI protocol.
-#
-# == Introduction
-#
-# CGI is a large class, providing several categories of methods, many of which
-# are mixed in from other modules. Some of the documentation is in this class,
-# some in the modules CGI::QueryExtension and CGI::HtmlExtension. See
-# CGI::Cookie for specific information on handling cookies, and cgi/session.rb
-# (CGI::Session) for information on sessions.
-#
-# For queries, CGI provides methods to get at environmental variables,
-# parameters, cookies, and multipart request data. For responses, CGI provides
-# methods for writing output and generating HTML.
-#
-# Read on for more details. Examples are provided at the bottom.
-#
-# == Queries
-#
-# The CGI class dynamically mixes in parameter and cookie-parsing
-# functionality, environmental variable access, and support for
-# parsing multipart requests (including uploaded files) from the
-# CGI::QueryExtension module.
-#
-# === Environmental Variables
-#
-# The standard CGI environmental variables are available as read-only
-# attributes of a CGI object. The following is a list of these variables:
-#
-#
-# AUTH_TYPE HTTP_HOST REMOTE_IDENT
-# CONTENT_LENGTH HTTP_NEGOTIATE REMOTE_USER
-# CONTENT_TYPE HTTP_PRAGMA REQUEST_METHOD
-# GATEWAY_INTERFACE HTTP_REFERER SCRIPT_NAME
-# HTTP_ACCEPT HTTP_USER_AGENT SERVER_NAME
-# HTTP_ACCEPT_CHARSET PATH_INFO SERVER_PORT
-# HTTP_ACCEPT_ENCODING PATH_TRANSLATED SERVER_PROTOCOL
-# HTTP_ACCEPT_LANGUAGE QUERY_STRING SERVER_SOFTWARE
-# HTTP_CACHE_CONTROL REMOTE_ADDR
-# HTTP_FROM REMOTE_HOST
-#
-#
-# For each of these variables, there is a corresponding attribute with the
-# same name, except all lower case and without a preceding HTTP_.
-# +content_length+ and +server_port+ are integers; the rest are strings.
-#
-# === Parameters
-#
-# The method #params() returns a hash of all parameters in the request as
-# name/value-list pairs, where the value-list is an Array of one or more
-# values. The CGI object itself also behaves as a hash of parameter names
-# to values, but only returns a single value (as a String) for each
-# parameter name.
-#
-# For instance, suppose the request contains the parameter
-# "favourite_colours" with the multiple values "blue" and "green". The
-# following behaviour would occur:
-#
-# cgi.params["favourite_colours"] # => ["blue", "green"]
-# cgi["favourite_colours"] # => "blue"
-#
-# If a parameter does not exist, the former method will return an empty
-# array, the latter an empty string. The simplest way to test for existence
-# of a parameter is by the #has_key? method.
-#
-# === Cookies
-#
-# HTTP Cookies are automatically parsed from the request. They are available
-# from the #cookies() accessor, which returns a hash from cookie name to
-# CGI::Cookie object.
-#
-# === Multipart requests
-#
-# If a request's method is POST and its content type is multipart/form-data,
-# then it may contain uploaded files. These are stored by the QueryExtension
-# module in the parameters of the request. The parameter name is the name
-# attribute of the file input field, as usual. However, the value is not
-# a string, but an IO object, either an IOString for small files, or a
-# Tempfile for larger ones. This object also has the additional singleton
-# methods:
-#
-# #local_path():: the path of the uploaded file on the local filesystem
-# #original_filename():: the name of the file on the client computer
-# #content_type():: the content type of the file
-#
-# == Responses
-#
-# The CGI class provides methods for sending header and content output to
-# the HTTP client, and mixes in methods for programmatic HTML generation
-# from CGI::HtmlExtension and CGI::TagMaker modules. The precise version of HTML
-# to use for HTML generation is specified at object creation time.
-#
-# === Writing output
-#
-# The simplest way to send output to the HTTP client is using the #out() method.
-# This takes the HTTP headers as a hash parameter, and the body content
-# via a block. The headers can be generated as a string using the #header()
-# method. The output stream can be written directly to using the #print()
-# method.
-#
-# === Generating HTML
-#
-# Each HTML element has a corresponding method for generating that
-# element as a String. The name of this method is the same as that
-# of the element, all lowercase. The attributes of the element are
-# passed in as a hash, and the body as a no-argument block that evaluates
-# to a String. The HTML generation module knows which elements are
-# always empty, and silently drops any passed-in body. It also knows
-# which elements require matching closing tags and which don't. However,
-# it does not know what attributes are legal for which elements.
-#
-# There are also some additional HTML generation methods mixed in from
-# the CGI::HtmlExtension module. These include individual methods for the
-# different types of form inputs, and methods for elements that commonly
-# take particular attributes where the attributes can be directly specified
-# as arguments, rather than via a hash.
-#
-# == Examples of use
-#
-# === Get form values
-#
-# require "cgi"
-# cgi = CGI.new
-# value = cgi['field_name'] # <== value string for 'field_name'
-# # if not 'field_name' included, then return "".
-# fields = cgi.keys # <== array of field names
-#
-# # returns true if form has 'field_name'
-# cgi.has_key?('field_name')
-# cgi.has_key?('field_name')
-# cgi.include?('field_name')
-#
-# CAUTION! cgi['field_name'] returned an Array with the old
-# cgi.rb(included in ruby 1.6)
-#
-# === Get form values as hash
-#
-# require "cgi"
-# cgi = CGI.new
-# params = cgi.params
-#
-# cgi.params is a hash.
-#
-# cgi.params['new_field_name'] = ["value"] # add new param
-# cgi.params['field_name'] = ["new_value"] # change value
-# cgi.params.delete('field_name') # delete param
-# cgi.params.clear # delete all params
-#
-#
-# === Save form values to file
-#
-# require "pstore"
-# db = PStore.new("query.db")
-# db.transaction do
-# db["params"] = cgi.params
-# end
-#
-#
-# === Restore form values from file
-#
-# require "pstore"
-# db = PStore.new("query.db")
-# db.transaction do
-# cgi.params = db["params"]
-# end
-#
-#
-# === Get multipart form values
-#
-# require "cgi"
-# cgi = CGI.new
-# value = cgi['field_name'] # <== value string for 'field_name'
-# value.read # <== body of value
-# value.local_path # <== path to local file of value
-# value.original_filename # <== original filename of value
-# value.content_type # <== content_type of value
-#
-# and value has StringIO or Tempfile class methods.
-#
-# === Get cookie values
-#
-# require "cgi"
-# cgi = CGI.new
-# values = cgi.cookies['name'] # <== array of 'name'
-# # if not 'name' included, then return [].
-# names = cgi.cookies.keys # <== array of cookie names
-#
-# and cgi.cookies is a hash.
-#
-# === Get cookie objects
-#
-# require "cgi"
-# cgi = CGI.new
-# for name, cookie in cgi.cookies
-# cookie.expires = Time.now + 30
-# end
-# cgi.out("cookie" => cgi.cookies) {"string"}
-#
-# cgi.cookies # { "name1" => cookie1, "name2" => cookie2, ... }
-#
-# require "cgi"
-# cgi = CGI.new
-# cgi.cookies['name'].expires = Time.now + 30
-# cgi.out("cookie" => cgi.cookies['name']) {"string"}
-#
-# === Print http header and html string to $DEFAULT_OUTPUT ($>)
-#
-# require "cgi"
-# cgi = CGI.new("html3") # add HTML generation methods
-# cgi.out() do
-# cgi.html() do
-# cgi.head{ cgi.title{"TITLE"} } +
-# cgi.body() do
-# cgi.form() do
-# cgi.textarea("get_text") +
-# cgi.br +
-# cgi.submit
-# end +
-# cgi.pre() do
-# CGI::escapeHTML(
-# "params: " + cgi.params.inspect + "\n" +
-# "cookies: " + cgi.cookies.inspect + "\n" +
-# ENV.collect() do |key, value|
-# key + " --> " + value + "\n"
-# end.join("")
-# )
-# end
-# end
-# end
-# end
-#
-# # add HTML generation methods
-# CGI.new("html3") # html3.2
-# CGI.new("html4") # html4.01 (Strict)
-# CGI.new("html4Tr") # html4.01 Transitional
-# CGI.new("html4Fr") # html4.01 Frameset
-#
-require 'cgi/core'
-require 'cgi/cookie'
-require 'cgi/util'
+If you need to use the full features of CGI library, please add 'gem "cgi"' to your script
+or use Bundler to ensure you are using the cgi gem instead of this file.
+WARNING
diff --git a/lib/cgi/.document b/lib/cgi/.document
deleted file mode 100644
index 4153f97aa5..0000000000
--- a/lib/cgi/.document
+++ /dev/null
@@ -1 +0,0 @@
-session.rb \ No newline at end of file
diff --git a/lib/cgi/cookie.rb b/lib/cgi/cookie.rb
deleted file mode 100644
index befe1402e6..0000000000
--- a/lib/cgi/cookie.rb
+++ /dev/null
@@ -1,137 +0,0 @@
- # Class representing an HTTP cookie.
- #
- # In addition to its specific fields and methods, a Cookie instance
- # is a delegator to the array of its values.
- #
- # See RFC 2965.
- #
- # == Examples of use
- # cookie1 = CGI::Cookie::new("name", "value1", "value2", ...)
- # cookie1 = CGI::Cookie::new("name" => "name", "value" => "value")
- # cookie1 = CGI::Cookie::new('name' => 'name',
- # 'value' => ['value1', 'value2', ...],
- # 'path' => 'path', # optional
- # 'domain' => 'domain', # optional
- # 'expires' => Time.now, # optional
- # 'secure' => true # optional
- # )
- #
- # cgi.out("cookie" => [cookie1, cookie2]) { "string" }
- #
- # name = cookie1.name
- # values = cookie1.value
- # path = cookie1.path
- # domain = cookie1.domain
- # expires = cookie1.expires
- # secure = cookie1.secure
- #
- # cookie1.name = 'name'
- # cookie1.value = ['value1', 'value2', ...]
- # cookie1.path = 'path'
- # cookie1.domain = 'domain'
- # cookie1.expires = Time.now + 30
- # cookie1.secure = true
-class CGI
- class Cookie < Array
-
- # Create a new CGI::Cookie object.
- #
- # The contents of the cookie can be specified as a +name+ and one
- # or more +value+ arguments. Alternatively, the contents can
- # be specified as a single hash argument. The possible keywords of
- # this hash are as follows:
- #
- # name:: the name of the cookie. Required.
- # value:: the cookie's value or list of values.
- # path:: the path for which this cookie applies. Defaults to the
- # base directory of the CGI script.
- # domain:: the domain for which this cookie applies.
- # expires:: the time at which this cookie expires, as a +Time+ object.
- # secure:: whether this cookie is a secure cookie or not (default to
- # false). Secure cookies are only transmitted to HTTPS
- # servers.
- #
- # These keywords correspond to attributes of the cookie object.
- def initialize(name = "", *value)
- if name.kind_of?(String)
- @name = name
- @value = value
- %r|^(.*/)|.match(ENV["SCRIPT_NAME"])
- @path = ($1 or "")
- @secure = false
- return super(@value)
- end
-
- options = name
- unless options.has_key?("name")
- raise ArgumentError, "`name' required"
- end
-
- @name = options["name"]
- @value = Array(options["value"])
- # simple support for IE
- if options["path"]
- @path = options["path"]
- else
- %r|^(.*/)|.match(ENV["SCRIPT_NAME"])
- @path = ($1 or "")
- end
- @domain = options["domain"]
- @expires = options["expires"]
- @secure = options["secure"] == true ? true : false
-
- super(@value)
- end
-
- attr_accessor("name", "value", "path", "domain", "expires")
- attr_reader("secure")
-
- # Set whether the Cookie is a secure cookie or not.
- #
- # +val+ must be a boolean.
- def secure=(val)
- @secure = val if val == true or val == false
- @secure
- end
-
- # Convert the Cookie to its string representation.
- def to_s
- val = @value.kind_of?(String) ? CGI::escape(@value) : @value.collect{|v| CGI::escape(v) }.join("&")
- buf = "#{@name}=#{val}"
- buf << "; domain=#{@domain}" if @domain
- buf << "; path=#{@path}" if @path
- buf << "; expires=#{CGI::rfc1123_date(@expires)}" if @expires
- buf << "; secure" if @secure == true
- buf
- end
-
- end # class Cookie
-
-
- # Parse a raw cookie string into a hash of cookie-name=>Cookie
- # pairs.
- #
- # cookies = CGI::Cookie::parse("raw_cookie_string")
- # # { "name1" => cookie1, "name2" => cookie2, ... }
- #
- def Cookie::parse(raw_cookie)
- cookies = Hash.new([])
- return cookies unless raw_cookie
-
- raw_cookie.split(/[;,]\s?/).each do |pairs|
- name, values = pairs.split('=',2)
- next unless name and values
- name = CGI::unescape(name)
- values ||= ""
- values = values.split('&').collect{|v| CGI::unescape(v) }
- if cookies.has_key?(name)
- values = cookies[name].value + values
- end
- cookies[name] = Cookie::new(name, *values)
- end
-
- cookies
- end
-end
-
-
diff --git a/lib/cgi/core.rb b/lib/cgi/core.rb
deleted file mode 100644
index 779f326a19..0000000000
--- a/lib/cgi/core.rb
+++ /dev/null
@@ -1,784 +0,0 @@
-class CGI
-
- $CGI_ENV = ENV # for FCGI support
-
- # String for carriage return
- CR = "\015"
-
- # String for linefeed
- LF = "\012"
-
- # Standard internet newline sequence
- EOL = CR + LF
-
- REVISION = '$Id$' #:nodoc:
-
- NEEDS_BINMODE = true if /WIN/i.match(RUBY_PLATFORM)
-
- # Path separators in different environments.
- PATH_SEPARATOR = {'UNIX'=>'/', 'WINDOWS'=>'\\', 'MACINTOSH'=>':'}
-
- # HTTP status codes.
- HTTP_STATUS = {
- "OK" => "200 OK",
- "PARTIAL_CONTENT" => "206 Partial Content",
- "MULTIPLE_CHOICES" => "300 Multiple Choices",
- "MOVED" => "301 Moved Permanently",
- "REDIRECT" => "302 Found",
- "NOT_MODIFIED" => "304 Not Modified",
- "BAD_REQUEST" => "400 Bad Request",
- "AUTH_REQUIRED" => "401 Authorization Required",
- "FORBIDDEN" => "403 Forbidden",
- "NOT_FOUND" => "404 Not Found",
- "METHOD_NOT_ALLOWED" => "405 Method Not Allowed",
- "NOT_ACCEPTABLE" => "406 Not Acceptable",
- "LENGTH_REQUIRED" => "411 Length Required",
- "PRECONDITION_FAILED" => "412 Rrecondition Failed",
- "SERVER_ERROR" => "500 Internal Server Error",
- "NOT_IMPLEMENTED" => "501 Method Not Implemented",
- "BAD_GATEWAY" => "502 Bad Gateway",
- "VARIANT_ALSO_VARIES" => "506 Variant Also Negotiates"
- }
-
- # Abbreviated day-of-week names specified by RFC 822
- RFC822_DAYS = %w[ Sun Mon Tue Wed Thu Fri Sat ]
-
- # Abbreviated month names specified by RFC 822
- RFC822_MONTHS = %w[ Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ]
-
- # :startdoc:
-
- def env_table
- ENV
- end
-
- def stdinput
- $stdin
- end
-
- def stdoutput
- $stdout
- end
-
- private :env_table, :stdinput, :stdoutput
-
-
- # Create an HTTP header block as a string.
- #
- # Includes the empty line that ends the header block.
- #
- # +options+ can be a string specifying the Content-Type (defaults
- # to text/html), or a hash of header key/value pairs. The following
- # header keys are recognized:
- #
- # type:: the Content-Type header. Defaults to "text/html"
- # charset:: the charset of the body, appended to the Content-Type header.
- # nph:: a boolean value. If true, prepend protocol string and status code, and
- # date; and sets default values for "server" and "connection" if not
- # explicitly set.
- # status:: the HTTP status code, returned as the Status header. See the
- # list of available status codes below.
- # server:: the server software, returned as the Server header.
- # connection:: the connection type, returned as the Connection header (for
- # instance, "close".
- # length:: the length of the content that will be sent, returned as the
- # Content-Length header.
- # language:: the language of the content, returned as the Content-Language
- # header.
- # expires:: the time on which the current content expires, as a +Time+
- # object, returned as the Expires header.
- # cookie:: a cookie or cookies, returned as one or more Set-Cookie headers.
- # The value can be the literal string of the cookie; a CGI::Cookie
- # object; an Array of literal cookie strings or Cookie objects; or a
- # hash all of whose values are literal cookie strings or Cookie objects.
- # These cookies are in addition to the cookies held in the
- # @output_cookies field.
- #
- # Other header lines can also be set; they are appended as key: value.
- #
- # header
- # # Content-Type: text/html
- #
- # header("text/plain")
- # # Content-Type: text/plain
- #
- # header("nph" => true,
- # "status" => "OK", # == "200 OK"
- # # "status" => "200 GOOD",
- # "server" => ENV['SERVER_SOFTWARE'],
- # "connection" => "close",
- # "type" => "text/html",
- # "charset" => "iso-2022-jp",
- # # Content-Type: text/html; charset=iso-2022-jp
- # "length" => 103,
- # "language" => "ja",
- # "expires" => Time.now + 30,
- # "cookie" => [cookie1, cookie2],
- # "my_header1" => "my_value"
- # "my_header2" => "my_value")
- #
- # The status codes are:
- #
- # "OK" --> "200 OK"
- # "PARTIAL_CONTENT" --> "206 Partial Content"
- # "MULTIPLE_CHOICES" --> "300 Multiple Choices"
- # "MOVED" --> "301 Moved Permanently"
- # "REDIRECT" --> "302 Found"
- # "NOT_MODIFIED" --> "304 Not Modified"
- # "BAD_REQUEST" --> "400 Bad Request"
- # "AUTH_REQUIRED" --> "401 Authorization Required"
- # "FORBIDDEN" --> "403 Forbidden"
- # "NOT_FOUND" --> "404 Not Found"
- # "METHOD_NOT_ALLOWED" --> "405 Method Not Allowed"
- # "NOT_ACCEPTABLE" --> "406 Not Acceptable"
- # "LENGTH_REQUIRED" --> "411 Length Required"
- # "PRECONDITION_FAILED" --> "412 Precondition Failed"
- # "SERVER_ERROR" --> "500 Internal Server Error"
- # "NOT_IMPLEMENTED" --> "501 Method Not Implemented"
- # "BAD_GATEWAY" --> "502 Bad Gateway"
- # "VARIANT_ALSO_VARIES" --> "506 Variant Also Negotiates"
- #
- # This method does not perform charset conversion.
- def header(options='text/html')
- if options.is_a?(String)
- content_type = options
- buf = _header_for_string(content_type)
- elsif options.is_a?(Hash)
- if options.size == 1 && options.has_key?('type')
- content_type = options['type']
- buf = _header_for_string(content_type)
- else
- buf = _header_for_hash(options.dup)
- end
- else
- raise ArgumentError.new("expected String or Hash but got #{options.class}")
- end
- if defined?(MOD_RUBY)
- _header_for_modruby(buf)
- return ''
- else
- buf << EOL # empty line of separator
- return buf
- end
- end # header()
-
- def _header_for_string(content_type) #:nodoc:
- buf = ''
- if nph?()
- buf << "#{$CGI_ENV['SERVER_PROTOCOL'] || 'HTTP/1.0'} 200 OK#{EOL}"
- buf << "Date: #{CGI.rfc1123_date(Time.now)}#{EOL}"
- buf << "Server: #{$CGI_ENV['SERVER_SOFTWARE']}#{EOL}"
- buf << "Connection: close#{EOL}"
- end
- buf << "Content-Type: #{content_type}#{EOL}"
- if @output_cookies
- @output_cookies.each {|cookie| buf << "Set-Cookie: #{cookie}#{EOL}" }
- end
- return buf
- end # _header_for_string
- private :_header_for_string
-
- def _header_for_hash(options) #:nodoc:
- buf = ''
- ## add charset to option['type']
- options['type'] ||= 'text/html'
- charset = options.delete('charset')
- options['type'] += "; charset=#{charset}" if charset
- ## NPH
- options.delete('nph') if defined?(MOD_RUBY)
- if options.delete('nph') || nph?()
- protocol = $CGI_ENV['SERVER_PROTOCOL'] || 'HTTP/1.0'
- status = options.delete('status')
- status = HTTP_STATUS[status] || status || '200 OK'
- buf << "#{protocol} #{status}#{EOL}"
- buf << "Date: #{CGI.rfc1123_date(Time.now)}#{EOL}"
- options['server'] ||= $CGI_ENV['SERVER_SOFTWARE'] || ''
- options['connection'] ||= 'close'
- end
- ## common headers
- status = options.delete('status')
- buf << "Status: #{HTTP_STATUS[status] || status}#{EOL}" if status
- server = options.delete('server')
- buf << "Server: #{server}#{EOL}" if server
- connection = options.delete('connection')
- buf << "Connection: #{connection}#{EOL}" if connection
- type = options.delete('type')
- buf << "Content-Type: #{type}#{EOL}" #if type
- length = options.delete('length')
- buf << "Content-Length: #{length}#{EOL}" if length
- language = options.delete('language')
- buf << "Content-Language: #{language}#{EOL}" if language
- expires = options.delete('expires')
- buf << "Expires: #{CGI.rfc1123_date(expires)}#{EOL}" if expires
- ## cookie
- if cookie = options.delete('cookie')
- case cookie
- when String, Cookie
- buf << "Set-Cookie: #{cookie}#{EOL}"
- when Array
- arr = cookie
- arr.each {|c| buf << "Set-Cookie: #{c}#{EOL}" }
- when Hash
- hash = cookie
- hash.each {|name, c| buf << "Set-Cookie: #{c}#{EOL}" }
- end
- end
- if @output_cookies
- @output_cookies.each {|c| buf << "Set-Cookie: #{c}#{EOL}" }
- end
- ## other headers
- options.each do |key, value|
- buf << "#{key}: #{value}#{EOL}"
- end
- return buf
- end # _header_for_hash
- private :_header_for_hash
-
- def nph? #:nodoc:
- return /IIS\/(\d+)/.match($CGI_ENV['SERVER_SOFTWARE']) && $1.to_i < 5
- end
-
- def _header_for_modruby(buf) #:nodoc:
- request = Apache::request
- buf.scan(/([^:]+): (.+)#{EOL}/o) do |name, value|
- warn sprintf("name:%s value:%s\n", name, value) if $DEBUG
- case name
- when 'Set-Cookie'
- request.headers_out.add(name, value)
- when /^status$/i
- request.status_line = value
- request.status = value.to_i
- when /^content-type$/i
- request.content_type = value
- when /^content-encoding$/i
- request.content_encoding = value
- when /^location$/i
- request.status = 302 if request.status == 200
- request.headers_out[name] = value
- else
- request.headers_out[name] = value
- end
- end
- request.send_http_header
- return ''
- end
- private :_header_for_modruby
- #
-
- # Print an HTTP header and body to $DEFAULT_OUTPUT ($>)
- #
- # The header is provided by +options+, as for #header().
- # The body of the document is that returned by the passed-
- # in block. This block takes no arguments. It is required.
- #
- # cgi = CGI.new
- # cgi.out{ "string" }
- # # Content-Type: text/html
- # # Content-Length: 6
- # #
- # # string
- #
- # cgi.out("text/plain") { "string" }
- # # Content-Type: text/plain
- # # Content-Length: 6
- # #
- # # string
- #
- # cgi.out("nph" => true,
- # "status" => "OK", # == "200 OK"
- # "server" => ENV['SERVER_SOFTWARE'],
- # "connection" => "close",
- # "type" => "text/html",
- # "charset" => "iso-2022-jp",
- # # Content-Type: text/html; charset=iso-2022-jp
- # "language" => "ja",
- # "expires" => Time.now + (3600 * 24 * 30),
- # "cookie" => [cookie1, cookie2],
- # "my_header1" => "my_value",
- # "my_header2" => "my_value") { "string" }
- #
- # Content-Length is automatically calculated from the size of
- # the String returned by the content block.
- #
- # If ENV['REQUEST_METHOD'] == "HEAD", then only the header
- # is outputted (the content block is still required, but it
- # is ignored).
- #
- # If the charset is "iso-2022-jp" or "euc-jp" or "shift_jis" then
- # the content is converted to this charset, and the language is set
- # to "ja".
- def out(options = "text/html") # :yield:
-
- options = { "type" => options } if options.kind_of?(String)
- content = yield
- options["length"] = content.bytesize.to_s
- output = stdoutput
- output.binmode if defined? output.binmode
- output.print header(options)
- output.print content unless "HEAD" == env_table['REQUEST_METHOD']
- end
-
-
- # Print an argument or list of arguments to the default output stream
- #
- # cgi = CGI.new
- # cgi.print # default: cgi.print == $DEFAULT_OUTPUT.print
- def print(*options)
- stdoutput.print(*options)
- end
-
- # Parse an HTTP query string into a hash of key=>value pairs.
- #
- # params = CGI::parse("query_string")
- # # {"name1" => ["value1", "value2", ...],
- # # "name2" => ["value1", "value2", ...], ... }
- #
- def CGI::parse(query)
- params = {}
- query.split(/[&;]/).each do |pairs|
- key, value = pairs.split('=',2).collect{|v| CGI::unescape(v) }
- if key && value
- params.has_key?(key) ? params[key].push(value) : params[key] = [value]
- end
- end
- params.default=[].freeze
- params
- end
-
- # Maximum content length of post data
- ##MAX_CONTENT_LENGTH = 2 * 1024 * 1024
-
- # Maximum content length of multipart data
- MAX_MULTIPART_LENGTH = 128 * 1024 * 1024
-
- # Maximum number of request parameters when multipart
- MAX_MULTIPART_COUNT = 128
-
- # Mixin module. It provides the follow functionality groups:
- #
- # 1. Access to CGI environment variables as methods. See
- # documentation to the CGI class for a list of these variables.
- #
- # 2. Access to cookies, including the cookies attribute.
- #
- # 3. Access to parameters, including the params attribute, and overloading
- # [] to perform parameter value lookup by key.
- #
- # 4. The initialize_query method, for initialising the above
- # mechanisms, handling multipart forms, and allowing the
- # class to be used in "offline" mode.
- #
- module QueryExtension
-
- %w[ CONTENT_LENGTH SERVER_PORT ].each do |env|
- define_method(env.sub(/^HTTP_/, '').downcase) do
- (val = env_table[env]) && Integer(val)
- end
- end
-
- %w[ AUTH_TYPE CONTENT_TYPE GATEWAY_INTERFACE PATH_INFO
- PATH_TRANSLATED QUERY_STRING REMOTE_ADDR REMOTE_HOST
- REMOTE_IDENT REMOTE_USER REQUEST_METHOD SCRIPT_NAME
- SERVER_NAME SERVER_PROTOCOL SERVER_SOFTWARE
-
- HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
- HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM HTTP_HOST
- HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env|
- define_method(env.sub(/^HTTP_/, '').downcase) do
- env_table[env]
- end
- end
-
- # Get the raw cookies as a string.
- def raw_cookie
- env_table["HTTP_COOKIE"]
- end
-
- # Get the raw RFC2965 cookies as a string.
- def raw_cookie2
- env_table["HTTP_COOKIE2"]
- end
-
- # Get the cookies as a hash of cookie-name=>Cookie pairs.
- attr_accessor :cookies
-
- # Get the parameters as a hash of name=>values pairs, where
- # values is an Array.
- attr_reader :params
-
- # Get the uploaed files as a hash of name=>values pairs
- attr_reader :files
-
- # Set all the parameters.
- def params=(hash)
- @params.clear
- @params.update(hash)
- end
-
- def read_multipart(boundary, content_length)
- ## read first boundary
- stdin = $stdin
- first_line = "--#{boundary}#{EOL}"
- content_length -= first_line.bytesize
- status = stdin.read(first_line.bytesize)
- raise EOFError.new("no content body") unless status
- raise EOFError.new("bad content body") unless first_line == status
- ## parse and set params
- params = {}
- @files = {}
- boundary_rexp = /--#{Regexp.quote(boundary)}(#{EOL}|--)/
- boundary_size = "#{EOL}--#{boundary}#{EOL}".bytesize
- boundary_end = nil
- buf = ''
- bufsize = 10 * 1024
- max_count = MAX_MULTIPART_COUNT
- n = 0
- while true
- (n += 1) < max_count or raise StandardError.new("too many parameters.")
- ## create body (StringIO or Tempfile)
- body = create_body(bufsize < content_length)
- class << body
- alias local_path path
- attr_reader :original_filename, :content_type
- end
- ## find head and boundary
- head = nil
- separator = EOL * 2
- until head && matched = boundary_rexp.match(buf)
- if !head && pos = buf.index(separator)
- len = pos + EOL.bytesize
- head = buf[0, len]
- buf = buf[(pos+separator.bytesize)..-1]
- else
- if head && buf.size > boundary_size
- len = buf.size - boundary_size
- body.print(buf[0, len])
- buf[0, len] = ''
- end
- c = stdin.read(bufsize < content_length ? bufsize : content_length)
- raise EOFError.new("bad content body") if c.nil? || c.empty?
- buf << c
- content_length -= c.bytesize
- end
- end
- ## read to end of boundary
- m = matched
- len = m.begin(0)
- s = buf[0, len]
- if s =~ /(\r?\n)\z/
- s = buf[0, len - $1.bytesize]
- end
- body.print(s)
- buf = buf[m.end(0)..-1]
- boundary_end = m[1]
- content_length = -1 if boundary_end == '--'
- ## reset file cursor position
- body.rewind
- ## original filename
- /Content-Disposition:.* filename=(?:"(.*?)"|([^;\r\n]*))/i.match(head)
- filename = $1 || $2 || ''
- filename = CGI.unescape(filename) if unescape_filename?()
- body.instance_variable_set('@original_filename', filename.taint)
- ## content type
- /Content-Type: (.*)/i.match(head)
- (content_type = $1 || '').chomp!
- body.instance_variable_set('@content_type', content_type.taint)
- ## query parameter name
- /Content-Disposition:.* name=(?:"(.*?)"|([^;\r\n]*))/i.match(head)
- name = $1 || $2 || ''
- if body.original_filename.empty?
- value=body.read.dup.force_encoding(@accept_charset)
- (params[name] ||= []) << value
- unless value.valid_encoding?
- if @accept_charset_error_block
- @accept_charset_error_block.call(name,value)
- else
- raise InvalidEncoding,"Accept-Charset encoding error"
- end
- end
- class << params[name].last;self;end.class_eval do
- define_method(:read){self}
- define_method(:original_filename){""}
- define_method(:content_type){""}
- end
- else
- (params[name] ||= []) << body
- @files[name]=body
- end
- ## break loop
- break if buf.size == 0
- break if content_length == -1
- end
- raise EOFError, "bad boundary end of body part" unless boundary_end =~ /--/
- params.default = []
- params
- end # read_multipart
- private :read_multipart
- def create_body(is_large) #:nodoc:
- if is_large
- require 'tempfile'
- body = Tempfile.new('CGI', encoding: "ascii-8bit")
- else
- begin
- require 'stringio'
- body = StringIO.new("".force_encoding("ascii-8bit"))
- rescue LoadError
- require 'tempfile'
- body = Tempfile.new('CGI', encoding: "ascii-8bit")
- end
- end
- body.binmode if defined? body.binmode
- return body
- end
- def unescape_filename? #:nodoc:
- user_agent = $CGI_ENV['HTTP_USER_AGENT']
- return /Mac/i.match(user_agent) && /Mozilla/i.match(user_agent) && !/MSIE/i.match(user_agent)
- end
-
- # offline mode. read name=value pairs on standard input.
- def read_from_cmdline
- require "shellwords"
-
- string = unless ARGV.empty?
- ARGV.join(' ')
- else
- if STDIN.tty?
- STDERR.print(
- %|(offline mode: enter name=value pairs on standard input)\n|
- )
- end
- readlines.join(' ').gsub(/\n/, '')
- end.gsub(/\\=/, '%3D').gsub(/\\&/, '%26')
-
- words = Shellwords.shellwords(string)
-
- if words.find{|x| /=/.match(x) }
- words.join('&')
- else
- words.join('+')
- end
- end
- private :read_from_cmdline
-
- # A wrapper class to use a StringIO object as the body and switch
- # to a TempFile when the passed threshold is passed.
- # Initialize the data from the query.
- #
- # Handles multipart forms (in particular, forms that involve file uploads).
- # Reads query parameters in the @params field, and cookies into @cookies.
- def initialize_query()
- if ("POST" == env_table['REQUEST_METHOD']) and
- %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|.match(env_table['CONTENT_TYPE'])
- raise StandardError.new("too large multipart data.") if env_table['CONTENT_LENGTH'].to_i > MAX_MULTIPART_LENGTH
- boundary = $1.dup
- @multipart = true
- @params = read_multipart(boundary, Integer(env_table['CONTENT_LENGTH']))
- else
- @multipart = false
- @params = CGI::parse(
- case env_table['REQUEST_METHOD']
- when "GET", "HEAD"
- if defined?(MOD_RUBY)
- Apache::request.args or ""
- else
- env_table['QUERY_STRING'] or ""
- end
- when "POST"
- stdinput.binmode if defined? stdinput.binmode
- stdinput.read(Integer(env_table['CONTENT_LENGTH'])) or ''
- else
- read_from_cmdline
- end.dup.force_encoding(@accept_charset)
- )
- unless Encoding.find(@accept_charset) == Encoding::ASCII_8BIT
- @params.each do |key,values|
- values.each do |value|
- unless value.valid_encoding?
- if @accept_charset_error_block
- @accept_charset_error_block.call(key,value)
- else
- raise InvalidEncoding,"Accept-Charset encoding error"
- end
- end
- end
- end
- end
- end
-
- @cookies = CGI::Cookie::parse((env_table['HTTP_COOKIE'] or env_table['COOKIE']))
- end
- private :initialize_query
-
- def multipart?
- @multipart
- end
-
- # Get the value for the parameter with a given key.
- #
- # If the parameter has multiple values, only the first will be
- # retrieved; use #params() to get the array of values.
- def [](key)
- params = @params[key]
- return '' unless params
- value = params[0]
- if @multipart
- if value
- return value
- elsif defined? StringIO
- StringIO.new("".force_encoding("ascii-8bit"))
- else
- Tempfile.new("CGI",encoding:"ascii-8bit")
- end
- else
- str = if value then value.dup else "" end
- str
- end
- end
-
- # Return all parameter keys as an array.
- def keys(*args)
- @params.keys(*args)
- end
-
- # Returns true if a given parameter key exists in the query.
- def has_key?(*args)
- @params.has_key?(*args)
- end
- alias key? has_key?
- alias include? has_key?
-
- end # QueryExtension
-
- # InvalidEncoding Exception class
- class InvalidEncoding < Exception; end
-
- # @@accept_charset is default accept character set.
- # This default value default is "UTF-8"
- # If you want to change the default accept character set
- # when create a new CGI instance, set this:
- #
- # CGI.accept_charset = "EUC-JP"
- #
-
- @@accept_charset="UTF-8"
-
- def self.accept_charset
- @@accept_charset
- end
-
- def self.accept_charset=(accept_charset)
- @@accept_charset=accept_charset
- end
-
- # Create a new CGI instance.
- #
- # CGI accept constructor parameters either in a hash, string as a block.
- # But string is as same as using :tag_maker of hash.
- #
- # CGI.new("html3") #=> CGI.new(:tag_maker=>"html3")
- #
- # And, if you specify string, @accept_charset cannot be changed.
- # Instead, please use hash parameter.
- #
- # == accept_charset
- #
- # :accept_charset specifies encoding of received query string.
- # ( Default value is @@accept_charset. )
- # If not valid, raise CGI::InvalidEncoding
- #
- # Example. Suppose @@accept_charset # => "UTF-8"
- #
- # when not specified:
- #
- # cgi=CGI.new # @accept_charset # => "UTF-8"
- #
- # when specified "EUC-JP":
- #
- # cgi=CGI.new(:accept_charset => "EUC-JP") # => "EUC-JP"
- #
- # == block
- #
- # When you use a block, you can write a process
- # that query encoding is invalid. Example:
- #
- # encoding_error={}
- # cgi=CGI.new(:accept_charset=>"EUC-JP") do |name,value|
- # encoding_error[key] = value
- # end
- #
- # == tag_maker
- #
- # :tag_maker specifies which version of HTML to load the HTML generation
- # methods for. The following versions of HTML are supported:
- #
- # html3:: HTML 3.x
- # html4:: HTML 4.0
- # html4Tr:: HTML 4.0 Transitional
- # html4Fr:: HTML 4.0 with Framesets
- #
- # If not specified, no HTML generation methods will be loaded.
- #
- # If the CGI object is not created in a standard CGI call environment
- # (that is, it can't locate REQUEST_METHOD in its environment), then
- # it will run in "offline" mode. In this mode, it reads its parameters
- # from the command line or (failing that) from standard input. Otherwise,
- # cookies and other parameters are parsed automatically from the standard
- # CGI locations, which varies according to the REQUEST_METHOD. It works this:
- #
- # CGI.new(:tag_maker=>"html3")
- #
- # This will be obsolete:
- #
- # CGI.new("html3")
- #
- attr_reader :accept_charset
- def initialize(options = {},&block)
- @accept_charset_error_block=block if block_given?
- @options={:accept_charset=>@@accept_charset}
- case options
- when Hash
- @options.merge!(options)
- when String
- @options[:tag_maker]=options
- end
- @accept_charset=@options[:accept_charset]
- if defined?(MOD_RUBY) && !ENV.key?("GATEWAY_INTERFACE")
- Apache.request.setup_cgi_env
- end
-
- extend QueryExtension
- @multipart = false
-
- initialize_query() # set @params, @cookies
- @output_cookies = nil
- @output_hidden = nil
-
- case @options[:tag_maker]
- when "html3"
- require 'cgi/html'
- extend Html3
- element_init()
- extend HtmlExtension
- when "html4"
- require 'cgi/html'
- extend Html4
- element_init()
- extend HtmlExtension
- when "html4Tr"
- require 'cgi/html'
- extend Html4Tr
- element_init()
- extend HtmlExtension
- when "html4Fr"
- require 'cgi/html'
- extend Html4Tr
- element_init()
- extend Html4Fr
- element_init()
- extend HtmlExtension
- end
- end
-
-end # class CGI
-
-
diff --git a/lib/cgi/escape.rb b/lib/cgi/escape.rb
new file mode 100644
index 0000000000..555d24a5da
--- /dev/null
+++ b/lib/cgi/escape.rb
@@ -0,0 +1,232 @@
+# frozen_string_literal: true
+
+# Since Ruby 4.0, \CGI is a small holder for various escaping methods, included from CGI::Escape
+#
+# require 'cgi/escape'
+#
+# CGI.escape("Ruby programming language")
+# #=> "Ruby+programming+language"
+# CGI.escapeURIComponent("Ruby programming language")
+# #=> "Ruby%20programming%20language"
+#
+# See CGI::Escape module for methods list and their description.
+class CGI
+ module Escape; end
+ include Escape
+ extend Escape
+ module EscapeExt; end # :nodoc:
+end
+
+# Web-related escape/unescape functionality.
+module CGI::Escape
+ @@accept_charset = Encoding::UTF_8 unless defined?(@@accept_charset)
+
+ # URL-encode a string into application/x-www-form-urlencoded.
+ # Space characters (<tt>" "</tt>) are encoded with plus signs (<tt>"+"</tt>)
+ # url_encoded_string = CGI.escape("'Stop!' said Fred")
+ # # => "%27Stop%21%27+said+Fred"
+ def escape(string)
+ encoding = string.encoding
+ buffer = string.b
+ buffer.gsub!(/([^ a-zA-Z0-9_.\-~]+)/) do |m|
+ '%' + m.unpack('H2' * m.bytesize).join('%').upcase
+ end
+ buffer.tr!(' ', '+')
+ buffer.force_encoding(encoding)
+ end
+
+ # URL-decode an application/x-www-form-urlencoded string with encoding(optional).
+ # string = CGI.unescape("%27Stop%21%27+said+Fred")
+ # # => "'Stop!' said Fred"
+ def unescape(string, encoding = @@accept_charset)
+ str = string.tr('+', ' ')
+ str = str.b
+ str.gsub!(/((?:%[0-9a-fA-F]{2})+)/) do |m|
+ [m.delete('%')].pack('H*')
+ end
+ str.force_encoding(encoding)
+ str.valid_encoding? ? str : str.force_encoding(string.encoding)
+ end
+
+ # URL-encode a string following RFC 3986
+ # Space characters (<tt>" "</tt>) are encoded with (<tt>"%20"</tt>)
+ # url_encoded_string = CGI.escapeURIComponent("'Stop!' said Fred")
+ # # => "%27Stop%21%27%20said%20Fred"
+ def escapeURIComponent(string)
+ encoding = string.encoding
+ buffer = string.b
+ buffer.gsub!(/([^a-zA-Z0-9_.\-~]+)/) do |m|
+ '%' + m.unpack('H2' * m.bytesize).join('%').upcase
+ end
+ buffer.force_encoding(encoding)
+ end
+ alias escape_uri_component escapeURIComponent
+
+ # URL-decode a string following RFC 3986 with encoding(optional).
+ # string = CGI.unescapeURIComponent("%27Stop%21%27+said%20Fred")
+ # # => "'Stop!'+said Fred"
+ def unescapeURIComponent(string, encoding = @@accept_charset)
+ str = string.b
+ str.gsub!(/((?:%[0-9a-fA-F]{2})+)/) do |m|
+ [m.delete('%')].pack('H*')
+ end
+ str.force_encoding(encoding)
+ str.valid_encoding? ? str : str.force_encoding(string.encoding)
+ end
+
+ alias unescape_uri_component unescapeURIComponent
+
+ # The set of special characters and their escaped values
+ TABLE_FOR_ESCAPE_HTML__ = { # :nodoc:
+ "'" => '&#39;',
+ '&' => '&amp;',
+ '"' => '&quot;',
+ '<' => '&lt;',
+ '>' => '&gt;',
+ }
+
+ # \Escape special characters in HTML, namely <tt>'&\"<></tt>
+ # CGI.escapeHTML('Usage: foo "bar" <baz>')
+ # # => "Usage: foo &quot;bar&quot; &lt;baz&gt;"
+ def escapeHTML(string)
+ enc = string.encoding
+ unless enc.ascii_compatible?
+ if enc.dummy?
+ origenc = enc
+ enc = Encoding::Converter.asciicompat_encoding(enc)
+ string = enc ? string.encode(enc) : string.b
+ end
+ table = Hash[TABLE_FOR_ESCAPE_HTML__.map {|pair|pair.map {|s|s.encode(enc)}}]
+ string = string.gsub(/#{"['&\"<>]".encode(enc)}/, table)
+ string.encode!(origenc) if origenc
+ string
+ else
+ string = string.b
+ string.gsub!(/['&\"<>]/, TABLE_FOR_ESCAPE_HTML__)
+ string.force_encoding(enc)
+ end
+ end
+
+ # Unescape a string that has been HTML-escaped
+ # CGI.unescapeHTML("Usage: foo &quot;bar&quot; &lt;baz&gt;")
+ # # => "Usage: foo \"bar\" <baz>"
+ def unescapeHTML(string)
+ enc = string.encoding
+ unless enc.ascii_compatible?
+ if enc.dummy?
+ origenc = enc
+ enc = Encoding::Converter.asciicompat_encoding(enc)
+ string = enc ? string.encode(enc) : string.b
+ end
+ string = string.gsub(Regexp.new('&(apos|amp|quot|gt|lt|#[0-9]+|#x[0-9A-Fa-f]+);'.encode(enc))) do
+ case $1.encode(Encoding::US_ASCII)
+ when 'apos' then "'".encode(enc)
+ when 'amp' then '&'.encode(enc)
+ when 'quot' then '"'.encode(enc)
+ when 'gt' then '>'.encode(enc)
+ when 'lt' then '<'.encode(enc)
+ when /\A#0*(\d+)\z/ then $1.to_i.chr(enc)
+ when /\A#x([0-9a-f]+)\z/i then $1.hex.chr(enc)
+ end
+ end
+ string.encode!(origenc) if origenc
+ return string
+ end
+ return string unless string.include? '&'
+ charlimit = case enc
+ when Encoding::UTF_8; 0x10ffff
+ when Encoding::ISO_8859_1; 256
+ else 128
+ end
+ string = string.b
+ string.gsub!(/&(apos|amp|quot|gt|lt|\#[0-9]+|\#[xX][0-9A-Fa-f]+);/) do
+ match = $1.dup
+ case match
+ when 'apos' then "'"
+ when 'amp' then '&'
+ when 'quot' then '"'
+ when 'gt' then '>'
+ when 'lt' then '<'
+ when /\A#0*(\d+)\z/
+ n = $1.to_i
+ if n < charlimit
+ n.chr(enc)
+ else
+ "&##{$1};"
+ end
+ when /\A#x([0-9a-f]+)\z/i
+ n = $1.hex
+ if n < charlimit
+ n.chr(enc)
+ else
+ "&#x#{$1};"
+ end
+ else
+ "&#{match};"
+ end
+ end
+ string.force_encoding enc
+ end
+
+ alias escape_html escapeHTML
+ alias h escapeHTML
+
+ alias unescape_html unescapeHTML
+
+ # TruffleRuby runs the pure-Ruby variant faster, do not use the C extension there
+ unless RUBY_ENGINE == 'truffleruby'
+ begin
+ require 'cgi/escape.so'
+ rescue LoadError
+ end
+ end
+
+ # \Escape only the tags of certain HTML elements in +string+.
+ #
+ # Takes an element or elements or array of elements. Each element
+ # is specified by the name of the element, without angle brackets.
+ # This matches both the start and the end tag of that element.
+ # The attribute list of the open tag will also be escaped (for
+ # instance, the double-quotes surrounding attribute values).
+ #
+ # print CGI.escapeElement('<BR><A HREF="url"></A>', "A", "IMG")
+ # # "<BR>&lt;A HREF=&quot;url&quot;&gt;&lt;/A&gt"
+ #
+ # print CGI.escapeElement('<BR><A HREF="url"></A>', ["A", "IMG"])
+ # # "<BR>&lt;A HREF=&quot;url&quot;&gt;&lt;/A&gt"
+ def escapeElement(string, *elements)
+ elements = elements[0] if elements[0].kind_of?(Array)
+ unless elements.empty?
+ string.gsub(/<\/?(?:#{elements.join("|")})\b[^<>]*+>?/im) do
+ CGI.escapeHTML($&)
+ end
+ else
+ string
+ end
+ end
+
+ # Undo escaping such as that done by CGI.escapeElement
+ #
+ # print CGI.unescapeElement(
+ # CGI.escapeHTML('<BR><A HREF="url"></A>'), "A", "IMG")
+ # # "&lt;BR&gt;<A HREF="url"></A>"
+ #
+ # print CGI.unescapeElement(
+ # CGI.escapeHTML('<BR><A HREF="url"></A>'), ["A", "IMG"])
+ # # "&lt;BR&gt;<A HREF="url"></A>"
+ def unescapeElement(string, *elements)
+ elements = elements[0] if elements[0].kind_of?(Array)
+ unless elements.empty?
+ string.gsub(/&lt;\/?(?:#{elements.join("|")})\b(?>[^&]+|&(?![gl]t;)\w+;)*(?:&gt;)?/im) do
+ unescapeHTML($&)
+ end
+ else
+ string
+ end
+ end
+
+ alias escape_element escapeElement
+
+ alias unescape_element unescapeElement
+
+end
diff --git a/lib/cgi/html.rb b/lib/cgi/html.rb
deleted file mode 100644
index 62f1fc1898..0000000000
--- a/lib/cgi/html.rb
+++ /dev/null
@@ -1,1021 +0,0 @@
- # Base module for HTML-generation mixins.
- #
- # Provides methods for code generation for tags following
- # the various DTD element types.
-class CGI
- module TagMaker # :nodoc:
-
- # Generate code for an element with required start and end tags.
- #
- # - -
- def nn_element_def(element)
- nOE_element_def(element, <<-END)
- if block_given?
- yield.to_s
- else
- ""
- end +
- "</#{element.upcase}>"
- END
- end
-
- # Generate code for an empty element.
- #
- # - O EMPTY
- def nOE_element_def(element, append = nil)
- s = <<-END
- attributes={attributes=>nil} if attributes.kind_of?(String)
- "<#{element.upcase}" + attributes.collect{|name, value|
- next unless value
- " " + CGI::escapeHTML(name.to_s) +
- if true == value
- ""
- else
- '="' + CGI::escapeHTML(value.to_s) + '"'
- end
- }.join + ">"
- END
- s.sub!(/\Z/, " +") << append if append
- s
- end
-
- # Generate code for an element for which the end (and possibly the
- # start) tag is optional.
- #
- # O O or - O
- def nO_element_def(element)
- nOE_element_def(element, <<-END)
- if block_given?
- yield.to_s + "</#{element.upcase}>"
- else
- ""
- end
- END
- end
-
- end # TagMaker
-
-
- #
- # Mixin module providing HTML generation methods.
- #
- # For example,
- # cgi.a("http://www.example.com") { "Example" }
- # # => "<A HREF=\"http://www.example.com\">Example</A>"
- #
- # Modules Http3, Http4, etc., contain more basic HTML-generation methods
- # (:title, :center, etc.).
- #
- # See class CGI for a detailed example.
- #
- module HtmlExtension
-
-
- # Generate an Anchor element as a string.
- #
- # +href+ can either be a string, giving the URL
- # for the HREF attribute, or it can be a hash of
- # the element's attributes.
- #
- # The body of the element is the string returned by the no-argument
- # block passed in.
- #
- # a("http://www.example.com") { "Example" }
- # # => "<A HREF=\"http://www.example.com\">Example</A>"
- #
- # a("HREF" => "http://www.example.com", "TARGET" => "_top") { "Example" }
- # # => "<A HREF=\"http://www.example.com\" TARGET=\"_top\">Example</A>"
- #
- def a(href = "") # :yield:
- attributes = if href.kind_of?(String)
- { "HREF" => href }
- else
- href
- end
- if block_given?
- super(attributes){ yield }
- else
- super(attributes)
- end
- end
-
- # Generate a Document Base URI element as a String.
- #
- # +href+ can either by a string, giving the base URL for the HREF
- # attribute, or it can be a has of the element's attributes.
- #
- # The passed-in no-argument block is ignored.
- #
- # base("http://www.example.com/cgi")
- # # => "<BASE HREF=\"http://www.example.com/cgi\">"
- def base(href = "") # :yield:
- attributes = if href.kind_of?(String)
- { "HREF" => href }
- else
- href
- end
- if block_given?
- super(attributes){ yield }
- else
- super(attributes)
- end
- end
-
- # Generate a BlockQuote element as a string.
- #
- # +cite+ can either be a string, give the URI for the source of
- # the quoted text, or a hash, giving all attributes of the element,
- # or it can be omitted, in which case the element has no attributes.
- #
- # The body is provided by the passed-in no-argument block
- #
- # blockquote("http://www.example.com/quotes/foo.html") { "Foo!" }
- # #=> "<BLOCKQUOTE CITE=\"http://www.example.com/quotes/foo.html\">Foo!</BLOCKQUOTE>
- def blockquote(cite = {}) # :yield:
- attributes = if cite.kind_of?(String)
- { "CITE" => cite }
- else
- cite
- end
- if block_given?
- super(attributes){ yield }
- else
- super(attributes)
- end
- end
-
-
- # Generate a Table Caption element as a string.
- #
- # +align+ can be a string, giving the alignment of the caption
- # (one of top, bottom, left, or right). It can be a hash of
- # all the attributes of the element. Or it can be omitted.
- #
- # The body of the element is provided by the passed-in no-argument block.
- #
- # caption("left") { "Capital Cities" }
- # # => <CAPTION ALIGN=\"left\">Capital Cities</CAPTION>
- def caption(align = {}) # :yield:
- attributes = if align.kind_of?(String)
- { "ALIGN" => align }
- else
- align
- end
- if block_given?
- super(attributes){ yield }
- else
- super(attributes)
- end
- end
-
-
- # Generate a Checkbox Input element as a string.
- #
- # The attributes of the element can be specified as three arguments,
- # +name+, +value+, and +checked+. +checked+ is a boolean value;
- # if true, the CHECKED attribute will be included in the element.
- #
- # Alternatively, the attributes can be specified as a hash.
- #
- # checkbox("name")
- # # = checkbox("NAME" => "name")
- #
- # checkbox("name", "value")
- # # = checkbox("NAME" => "name", "VALUE" => "value")
- #
- # checkbox("name", "value", true)
- # # = checkbox("NAME" => "name", "VALUE" => "value", "CHECKED" => true)
- def checkbox(name = "", value = nil, checked = nil)
- attributes = if name.kind_of?(String)
- { "TYPE" => "checkbox", "NAME" => name,
- "VALUE" => value, "CHECKED" => checked }
- else
- name["TYPE"] = "checkbox"
- name
- end
- input(attributes)
- end
-
- # Generate a sequence of checkbox elements, as a String.
- #
- # The checkboxes will all have the same +name+ attribute.
- # Each checkbox is followed by a label.
- # There will be one checkbox for each value. Each value
- # can be specified as a String, which will be used both
- # as the value of the VALUE attribute and as the label
- # for that checkbox. A single-element array has the
- # same effect.
- #
- # Each value can also be specified as a three-element array.
- # The first element is the VALUE attribute; the second is the
- # label; and the third is a boolean specifying whether this
- # checkbox is CHECKED.
- #
- # Each value can also be specified as a two-element
- # array, by omitting either the value element (defaults
- # to the same as the label), or the boolean checked element
- # (defaults to false).
- #
- # checkbox_group("name", "foo", "bar", "baz")
- # # <INPUT TYPE="checkbox" NAME="name" VALUE="foo">foo
- # # <INPUT TYPE="checkbox" NAME="name" VALUE="bar">bar
- # # <INPUT TYPE="checkbox" NAME="name" VALUE="baz">baz
- #
- # checkbox_group("name", ["foo"], ["bar", true], "baz")
- # # <INPUT TYPE="checkbox" NAME="name" VALUE="foo">foo
- # # <INPUT TYPE="checkbox" CHECKED NAME="name" VALUE="bar">bar
- # # <INPUT TYPE="checkbox" NAME="name" VALUE="baz">baz
- #
- # checkbox_group("name", ["1", "Foo"], ["2", "Bar", true], "Baz")
- # # <INPUT TYPE="checkbox" NAME="name" VALUE="1">Foo
- # # <INPUT TYPE="checkbox" CHECKED NAME="name" VALUE="2">Bar
- # # <INPUT TYPE="checkbox" NAME="name" VALUE="Baz">Baz
- #
- # checkbox_group("NAME" => "name",
- # "VALUES" => ["foo", "bar", "baz"])
- #
- # checkbox_group("NAME" => "name",
- # "VALUES" => [["foo"], ["bar", true], "baz"])
- #
- # checkbox_group("NAME" => "name",
- # "VALUES" => [["1", "Foo"], ["2", "Bar", true], "Baz"])
- def checkbox_group(name = "", *values)
- if name.kind_of?(Hash)
- values = name["VALUES"]
- name = name["NAME"]
- end
- values.collect{|value|
- if value.kind_of?(String)
- checkbox(name, value) + value
- else
- if value[-1] == true || value[-1] == false
- checkbox(name, value[0], value[-1]) +
- value[-2]
- else
- checkbox(name, value[0]) +
- value[-1]
- end
- end
- }.join
- end
-
-
- # Generate an File Upload Input element as a string.
- #
- # The attributes of the element can be specified as three arguments,
- # +name+, +size+, and +maxlength+. +maxlength+ is the maximum length
- # of the file's _name_, not of the file's _contents_.
- #
- # Alternatively, the attributes can be specified as a hash.
- #
- # See #multipart_form() for forms that include file uploads.
- #
- # file_field("name")
- # # <INPUT TYPE="file" NAME="name" SIZE="20">
- #
- # file_field("name", 40)
- # # <INPUT TYPE="file" NAME="name" SIZE="40">
- #
- # file_field("name", 40, 100)
- # # <INPUT TYPE="file" NAME="name" SIZE="40" MAXLENGTH="100">
- #
- # file_field("NAME" => "name", "SIZE" => 40)
- # # <INPUT TYPE="file" NAME="name" SIZE="40">
- def file_field(name = "", size = 20, maxlength = nil)
- attributes = if name.kind_of?(String)
- { "TYPE" => "file", "NAME" => name,
- "SIZE" => size.to_s }
- else
- name["TYPE"] = "file"
- name
- end
- attributes["MAXLENGTH"] = maxlength.to_s if maxlength
- input(attributes)
- end
-
-
- # Generate a Form element as a string.
- #
- # +method+ should be either "get" or "post", and defaults to the latter.
- # +action+ defaults to the current CGI script name. +enctype+
- # defaults to "application/x-www-form-urlencoded".
- #
- # Alternatively, the attributes can be specified as a hash.
- #
- # See also #multipart_form() for forms that include file uploads.
- #
- # form{ "string" }
- # # <FORM METHOD="post" ENCTYPE="application/x-www-form-urlencoded">string</FORM>
- #
- # form("get") { "string" }
- # # <FORM METHOD="get" ENCTYPE="application/x-www-form-urlencoded">string</FORM>
- #
- # form("get", "url") { "string" }
- # # <FORM METHOD="get" ACTION="url" ENCTYPE="application/x-www-form-urlencoded">string</FORM>
- #
- # form("METHOD" => "post", "ENCTYPE" => "enctype") { "string" }
- # # <FORM METHOD="post" ENCTYPE="enctype">string</FORM>
- def form(method = "post", action = script_name, enctype = "application/x-www-form-urlencoded")
- attributes = if method.kind_of?(String)
- { "METHOD" => method, "ACTION" => action,
- "ENCTYPE" => enctype }
- else
- unless method.has_key?("METHOD")
- method["METHOD"] = "post"
- end
- unless method.has_key?("ENCTYPE")
- method["ENCTYPE"] = enctype
- end
- method
- end
- if block_given?
- body = yield
- else
- body = ""
- end
- if @output_hidden
- body += @output_hidden.collect{|k,v|
- "<INPUT TYPE=\"HIDDEN\" NAME=\"#{k}\" VALUE=\"#{v}\">"
- }.join
- end
- super(attributes){body}
- end
-
- # Generate a Hidden Input element as a string.
- #
- # The attributes of the element can be specified as two arguments,
- # +name+ and +value+.
- #
- # Alternatively, the attributes can be specified as a hash.
- #
- # hidden("name")
- # # <INPUT TYPE="hidden" NAME="name">
- #
- # hidden("name", "value")
- # # <INPUT TYPE="hidden" NAME="name" VALUE="value">
- #
- # hidden("NAME" => "name", "VALUE" => "reset", "ID" => "foo")
- # # <INPUT TYPE="hidden" NAME="name" VALUE="value" ID="foo">
- def hidden(name = "", value = nil)
- attributes = if name.kind_of?(String)
- { "TYPE" => "hidden", "NAME" => name, "VALUE" => value }
- else
- name["TYPE"] = "hidden"
- name
- end
- input(attributes)
- end
-
- # Generate a top-level HTML element as a string.
- #
- # The attributes of the element are specified as a hash. The
- # pseudo-attribute "PRETTY" can be used to specify that the generated
- # HTML string should be indented. "PRETTY" can also be specified as
- # a string as the sole argument to this method. The pseudo-attribute
- # "DOCTYPE", if given, is used as the leading DOCTYPE SGML tag; it
- # should include the entire text of this tag, including angle brackets.
- #
- # The body of the html element is supplied as a block.
- #
- # html{ "string" }
- # # <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><HTML>string</HTML>
- #
- # html("LANG" => "ja") { "string" }
- # # <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><HTML LANG="ja">string</HTML>
- #
- # html("DOCTYPE" => false) { "string" }
- # # <HTML>string</HTML>
- #
- # html("DOCTYPE" => '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">') { "string" }
- # # <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"><HTML>string</HTML>
- #
- # html("PRETTY" => " ") { "<BODY></BODY>" }
- # # <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
- # # <HTML>
- # # <BODY>
- # # </BODY>
- # # </HTML>
- #
- # html("PRETTY" => "\t") { "<BODY></BODY>" }
- # # <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
- # # <HTML>
- # # <BODY>
- # # </BODY>
- # # </HTML>
- #
- # html("PRETTY") { "<BODY></BODY>" }
- # # = html("PRETTY" => " ") { "<BODY></BODY>" }
- #
- # html(if $VERBOSE then "PRETTY" end) { "HTML string" }
- #
- def html(attributes = {}) # :yield:
- if nil == attributes
- attributes = {}
- elsif "PRETTY" == attributes
- attributes = { "PRETTY" => true }
- end
- pretty = attributes.delete("PRETTY")
- pretty = " " if true == pretty
- buf = ""
-
- if attributes.has_key?("DOCTYPE")
- if attributes["DOCTYPE"]
- buf += attributes.delete("DOCTYPE")
- else
- attributes.delete("DOCTYPE")
- end
- else
- buf += doctype
- end
-
- if block_given?
- buf += super(attributes){ yield }
- else
- buf += super(attributes)
- end
-
- if pretty
- CGI::pretty(buf, pretty)
- else
- buf
- end
-
- end
-
- # Generate an Image Button Input element as a string.
- #
- # +src+ is the URL of the image to use for the button. +name+
- # is the input name. +alt+ is the alternative text for the image.
- #
- # Alternatively, the attributes can be specified as a hash.
- #
- # image_button("url")
- # # <INPUT TYPE="image" SRC="url">
- #
- # image_button("url", "name", "string")
- # # <INPUT TYPE="image" SRC="url" NAME="name" ALT="string">
- #
- # image_button("SRC" => "url", "ATL" => "strng")
- # # <INPUT TYPE="image" SRC="url" ALT="string">
- def image_button(src = "", name = nil, alt = nil)
- attributes = if src.kind_of?(String)
- { "TYPE" => "image", "SRC" => src, "NAME" => name,
- "ALT" => alt }
- else
- src["TYPE"] = "image"
- src["SRC"] ||= ""
- src
- end
- input(attributes)
- end
-
-
- # Generate an Image element as a string.
- #
- # +src+ is the URL of the image. +alt+ is the alternative text for
- # the image. +width+ is the width of the image, and +height+ is
- # its height.
- #
- # Alternatively, the attributes can be specified as a hash.
- #
- # img("src", "alt", 100, 50)
- # # <IMG SRC="src" ALT="alt" WIDTH="100" HEIGHT="50">
- #
- # img("SRC" => "src", "ALT" => "alt", "WIDTH" => 100, "HEIGHT" => 50)
- # # <IMG SRC="src" ALT="alt" WIDTH="100" HEIGHT="50">
- def img(src = "", alt = "", width = nil, height = nil)
- attributes = if src.kind_of?(String)
- { "SRC" => src, "ALT" => alt }
- else
- src
- end
- attributes["WIDTH"] = width.to_s if width
- attributes["HEIGHT"] = height.to_s if height
- super(attributes)
- end
-
-
- # Generate a Form element with multipart encoding as a String.
- #
- # Multipart encoding is used for forms that include file uploads.
- #
- # +action+ is the action to perform. +enctype+ is the encoding
- # type, which defaults to "multipart/form-data".
- #
- # Alternatively, the attributes can be specified as a hash.
- #
- # multipart_form{ "string" }
- # # <FORM METHOD="post" ENCTYPE="multipart/form-data">string</FORM>
- #
- # multipart_form("url") { "string" }
- # # <FORM METHOD="post" ACTION="url" ENCTYPE="multipart/form-data">string</FORM>
- def multipart_form(action = nil, enctype = "multipart/form-data")
- attributes = if action == nil
- { "METHOD" => "post", "ENCTYPE" => enctype }
- elsif action.kind_of?(String)
- { "METHOD" => "post", "ACTION" => action,
- "ENCTYPE" => enctype }
- else
- unless action.has_key?("METHOD")
- action["METHOD"] = "post"
- end
- unless action.has_key?("ENCTYPE")
- action["ENCTYPE"] = enctype
- end
- action
- end
- if block_given?
- form(attributes){ yield }
- else
- form(attributes)
- end
- end
-
-
- # Generate a Password Input element as a string.
- #
- # +name+ is the name of the input field. +value+ is its default
- # value. +size+ is the size of the input field display. +maxlength+
- # is the maximum length of the inputted password.
- #
- # Alternatively, attributes can be specified as a hash.
- #
- # password_field("name")
- # # <INPUT TYPE="password" NAME="name" SIZE="40">
- #
- # password_field("name", "value")
- # # <INPUT TYPE="password" NAME="name" VALUE="value" SIZE="40">
- #
- # password_field("password", "value", 80, 200)
- # # <INPUT TYPE="password" NAME="name" VALUE="value" SIZE="80" MAXLENGTH="200">
- #
- # password_field("NAME" => "name", "VALUE" => "value")
- # # <INPUT TYPE="password" NAME="name" VALUE="value">
- def password_field(name = "", value = nil, size = 40, maxlength = nil)
- attributes = if name.kind_of?(String)
- { "TYPE" => "password", "NAME" => name,
- "VALUE" => value, "SIZE" => size.to_s }
- else
- name["TYPE"] = "password"
- name
- end
- attributes["MAXLENGTH"] = maxlength.to_s if maxlength
- input(attributes)
- end
-
- # Generate a Select element as a string.
- #
- # +name+ is the name of the element. The +values+ are the options that
- # can be selected from the Select menu. Each value can be a String or
- # a one, two, or three-element Array. If a String or a one-element
- # Array, this is both the value of that option and the text displayed for
- # it. If a three-element Array, the elements are the option value, displayed
- # text, and a boolean value specifying whether this option starts as selected.
- # The two-element version omits either the option value (defaults to the same
- # as the display text) or the boolean selected specifier (defaults to false).
- #
- # The attributes and options can also be specified as a hash. In this
- # case, options are specified as an array of values as described above,
- # with the hash key of "VALUES".
- #
- # popup_menu("name", "foo", "bar", "baz")
- # # <SELECT NAME="name">
- # # <OPTION VALUE="foo">foo</OPTION>
- # # <OPTION VALUE="bar">bar</OPTION>
- # # <OPTION VALUE="baz">baz</OPTION>
- # # </SELECT>
- #
- # popup_menu("name", ["foo"], ["bar", true], "baz")
- # # <SELECT NAME="name">
- # # <OPTION VALUE="foo">foo</OPTION>
- # # <OPTION VALUE="bar" SELECTED>bar</OPTION>
- # # <OPTION VALUE="baz">baz</OPTION>
- # # </SELECT>
- #
- # popup_menu("name", ["1", "Foo"], ["2", "Bar", true], "Baz")
- # # <SELECT NAME="name">
- # # <OPTION VALUE="1">Foo</OPTION>
- # # <OPTION SELECTED VALUE="2">Bar</OPTION>
- # # <OPTION VALUE="Baz">Baz</OPTION>
- # # </SELECT>
- #
- # popup_menu("NAME" => "name", "SIZE" => 2, "MULTIPLE" => true,
- # "VALUES" => [["1", "Foo"], ["2", "Bar", true], "Baz"])
- # # <SELECT NAME="name" MULTIPLE SIZE="2">
- # # <OPTION VALUE="1">Foo</OPTION>
- # # <OPTION SELECTED VALUE="2">Bar</OPTION>
- # # <OPTION VALUE="Baz">Baz</OPTION>
- # # </SELECT>
- def popup_menu(name = "", *values)
-
- if name.kind_of?(Hash)
- values = name["VALUES"]
- size = name["SIZE"].to_s if name["SIZE"]
- multiple = name["MULTIPLE"]
- name = name["NAME"]
- else
- size = nil
- multiple = nil
- end
-
- select({ "NAME" => name, "SIZE" => size,
- "MULTIPLE" => multiple }){
- values.collect{|value|
- if value.kind_of?(String)
- option({ "VALUE" => value }){ value }
- else
- if value[value.size - 1] == true
- option({ "VALUE" => value[0], "SELECTED" => true }){
- value[value.size - 2]
- }
- else
- option({ "VALUE" => value[0] }){
- value[value.size - 1]
- }
- end
- end
- }.join
- }
-
- end
-
- # Generates a radio-button Input element.
- #
- # +name+ is the name of the input field. +value+ is the value of
- # the field if checked. +checked+ specifies whether the field
- # starts off checked.
- #
- # Alternatively, the attributes can be specified as a hash.
- #
- # radio_button("name", "value")
- # # <INPUT TYPE="radio" NAME="name" VALUE="value">
- #
- # radio_button("name", "value", true)
- # # <INPUT TYPE="radio" NAME="name" VALUE="value" CHECKED>
- #
- # radio_button("NAME" => "name", "VALUE" => "value", "ID" => "foo")
- # # <INPUT TYPE="radio" NAME="name" VALUE="value" ID="foo">
- def radio_button(name = "", value = nil, checked = nil)
- attributes = if name.kind_of?(String)
- { "TYPE" => "radio", "NAME" => name,
- "VALUE" => value, "CHECKED" => checked }
- else
- name["TYPE"] = "radio"
- name
- end
- input(attributes)
- end
-
- # Generate a sequence of radio button Input elements, as a String.
- #
- # This works the same as #checkbox_group(). However, it is not valid
- # to have more than one radiobutton in a group checked.
- #
- # radio_group("name", "foo", "bar", "baz")
- # # <INPUT TYPE="radio" NAME="name" VALUE="foo">foo
- # # <INPUT TYPE="radio" NAME="name" VALUE="bar">bar
- # # <INPUT TYPE="radio" NAME="name" VALUE="baz">baz
- #
- # radio_group("name", ["foo"], ["bar", true], "baz")
- # # <INPUT TYPE="radio" NAME="name" VALUE="foo">foo
- # # <INPUT TYPE="radio" CHECKED NAME="name" VALUE="bar">bar
- # # <INPUT TYPE="radio" NAME="name" VALUE="baz">baz
- #
- # radio_group("name", ["1", "Foo"], ["2", "Bar", true], "Baz")
- # # <INPUT TYPE="radio" NAME="name" VALUE="1">Foo
- # # <INPUT TYPE="radio" CHECKED NAME="name" VALUE="2">Bar
- # # <INPUT TYPE="radio" NAME="name" VALUE="Baz">Baz
- #
- # radio_group("NAME" => "name",
- # "VALUES" => ["foo", "bar", "baz"])
- #
- # radio_group("NAME" => "name",
- # "VALUES" => [["foo"], ["bar", true], "baz"])
- #
- # radio_group("NAME" => "name",
- # "VALUES" => [["1", "Foo"], ["2", "Bar", true], "Baz"])
- def radio_group(name = "", *values)
- if name.kind_of?(Hash)
- values = name["VALUES"]
- name = name["NAME"]
- end
- values.collect{|value|
- if value.kind_of?(String)
- radio_button(name, value) + value
- else
- if value[-1] == true || value[-1] == false
- radio_button(name, value[0], value[-1]) +
- value[-2]
- else
- radio_button(name, value[0]) +
- value[-1]
- end
- end
- }.join
- end
-
- # Generate a reset button Input element, as a String.
- #
- # This resets the values on a form to their initial values. +value+
- # is the text displayed on the button. +name+ is the name of this button.
- #
- # Alternatively, the attributes can be specified as a hash.
- #
- # reset
- # # <INPUT TYPE="reset">
- #
- # reset("reset")
- # # <INPUT TYPE="reset" VALUE="reset">
- #
- # reset("VALUE" => "reset", "ID" => "foo")
- # # <INPUT TYPE="reset" VALUE="reset" ID="foo">
- def reset(value = nil, name = nil)
- attributes = if (not value) or value.kind_of?(String)
- { "TYPE" => "reset", "VALUE" => value, "NAME" => name }
- else
- value["TYPE"] = "reset"
- value
- end
- input(attributes)
- end
-
- alias scrolling_list popup_menu
-
- # Generate a submit button Input element, as a String.
- #
- # +value+ is the text to display on the button. +name+ is the name
- # of the input.
- #
- # Alternatively, the attributes can be specified as a hash.
- #
- # submit
- # # <INPUT TYPE="submit">
- #
- # submit("ok")
- # # <INPUT TYPE="submit" VALUE="ok">
- #
- # submit("ok", "button1")
- # # <INPUT TYPE="submit" VALUE="ok" NAME="button1">
- #
- # submit("VALUE" => "ok", "NAME" => "button1", "ID" => "foo")
- # # <INPUT TYPE="submit" VALUE="ok" NAME="button1" ID="foo">
- def submit(value = nil, name = nil)
- attributes = if (not value) or value.kind_of?(String)
- { "TYPE" => "submit", "VALUE" => value, "NAME" => name }
- else
- value["TYPE"] = "submit"
- value
- end
- input(attributes)
- end
-
- # Generate a text field Input element, as a String.
- #
- # +name+ is the name of the input field. +value+ is its initial
- # value. +size+ is the size of the input area. +maxlength+
- # is the maximum length of input accepted.
- #
- # Alternatively, the attributes can be specified as a hash.
- #
- # text_field("name")
- # # <INPUT TYPE="text" NAME="name" SIZE="40">
- #
- # text_field("name", "value")
- # # <INPUT TYPE="text" NAME="name" VALUE="value" SIZE="40">
- #
- # text_field("name", "value", 80)
- # # <INPUT TYPE="text" NAME="name" VALUE="value" SIZE="80">
- #
- # text_field("name", "value", 80, 200)
- # # <INPUT TYPE="text" NAME="name" VALUE="value" SIZE="80" MAXLENGTH="200">
- #
- # text_field("NAME" => "name", "VALUE" => "value")
- # # <INPUT TYPE="text" NAME="name" VALUE="value">
- def text_field(name = "", value = nil, size = 40, maxlength = nil)
- attributes = if name.kind_of?(String)
- { "TYPE" => "text", "NAME" => name, "VALUE" => value,
- "SIZE" => size.to_s }
- else
- name["TYPE"] = "text"
- name
- end
- attributes["MAXLENGTH"] = maxlength.to_s if maxlength
- input(attributes)
- end
-
- # Generate a TextArea element, as a String.
- #
- # +name+ is the name of the textarea. +cols+ is the number of
- # columns and +rows+ is the number of rows in the display.
- #
- # Alternatively, the attributes can be specified as a hash.
- #
- # The body is provided by the passed-in no-argument block
- #
- # textarea("name")
- # # = textarea("NAME" => "name", "COLS" => 70, "ROWS" => 10)
- #
- # textarea("name", 40, 5)
- # # = textarea("NAME" => "name", "COLS" => 40, "ROWS" => 5)
- def textarea(name = "", cols = 70, rows = 10) # :yield:
- attributes = if name.kind_of?(String)
- { "NAME" => name, "COLS" => cols.to_s,
- "ROWS" => rows.to_s }
- else
- name
- end
- if block_given?
- super(attributes){ yield }
- else
- super(attributes)
- end
- end
-
- end # HtmlExtension
-
-
- # Mixin module for HTML version 3 generation methods.
- module Html3 # :nodoc:
-
- # The DOCTYPE declaration for this version of HTML
- def doctype
- %|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">|
- end
-
- # Initialise the HTML generation methods for this version.
- def element_init
- extend TagMaker
- methods = ""
- # - -
- for element in %w[ A TT I B U STRIKE BIG SMALL SUB SUP EM STRONG
- DFN CODE SAMP KBD VAR CITE FONT ADDRESS DIV center MAP
- APPLET PRE XMP LISTING DL OL UL DIR MENU SELECT table TITLE
- STYLE SCRIPT H1 H2 H3 H4 H5 H6 TEXTAREA FORM BLOCKQUOTE
- CAPTION ]
- methods += <<-BEGIN + nn_element_def(element) + <<-END
- def #{element.downcase}(attributes = {})
- BEGIN
- end
- END
- end
-
- # - O EMPTY
- for element in %w[ IMG BASE BASEFONT BR AREA LINK PARAM HR INPUT
- ISINDEX META ]
- methods += <<-BEGIN + nOE_element_def(element) + <<-END
- def #{element.downcase}(attributes = {})
- BEGIN
- end
- END
- end
-
- # O O or - O
- for element in %w[ HTML HEAD BODY P PLAINTEXT DT DD LI OPTION tr
- th td ]
- methods += <<-BEGIN + nO_element_def(element) + <<-END
- def #{element.downcase}(attributes = {})
- BEGIN
- end
- END
- end
- eval(methods)
- end
-
- end # Html3
-
-
- # Mixin module for HTML version 4 generation methods.
- module Html4 # :nodoc:
-
- # The DOCTYPE declaration for this version of HTML
- def doctype
- %|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">|
- end
-
- # Initialise the HTML generation methods for this version.
- def element_init
- extend TagMaker
- methods = ""
- # - -
- for element in %w[ TT I B BIG SMALL EM STRONG DFN CODE SAMP KBD
- VAR CITE ABBR ACRONYM SUB SUP SPAN BDO ADDRESS DIV MAP OBJECT
- H1 H2 H3 H4 H5 H6 PRE Q INS DEL DL OL UL LABEL SELECT OPTGROUP
- FIELDSET LEGEND BUTTON TABLE TITLE STYLE SCRIPT NOSCRIPT
- TEXTAREA FORM A BLOCKQUOTE CAPTION ]
- methods += <<-BEGIN + nn_element_def(element) + <<-END
- def #{element.downcase}(attributes = {})
- BEGIN
- end
- END
- end
-
- # - O EMPTY
- for element in %w[ IMG BASE BR AREA LINK PARAM HR INPUT COL META ]
- methods += <<-BEGIN + nOE_element_def(element) + <<-END
- def #{element.downcase}(attributes = {})
- BEGIN
- end
- END
- end
-
- # O O or - O
- for element in %w[ HTML BODY P DT DD LI OPTION THEAD TFOOT TBODY
- COLGROUP TR TH TD HEAD]
- methods += <<-BEGIN + nO_element_def(element) + <<-END
- def #{element.downcase}(attributes = {})
- BEGIN
- end
- END
- end
- eval(methods)
- end
-
- end # Html4
-
-
- # Mixin module for HTML version 4 transitional generation methods.
- module Html4Tr # :nodoc:
-
- # The DOCTYPE declaration for this version of HTML
- def doctype
- %|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">|
- end
-
- # Initialise the HTML generation methods for this version.
- def element_init
- extend TagMaker
- methods = ""
- # - -
- for element in %w[ TT I B U S STRIKE BIG SMALL EM STRONG DFN
- CODE SAMP KBD VAR CITE ABBR ACRONYM FONT SUB SUP SPAN BDO
- ADDRESS DIV CENTER MAP OBJECT APPLET H1 H2 H3 H4 H5 H6 PRE Q
- INS DEL DL OL UL DIR MENU LABEL SELECT OPTGROUP FIELDSET
- LEGEND BUTTON TABLE IFRAME NOFRAMES TITLE STYLE SCRIPT
- NOSCRIPT TEXTAREA FORM A BLOCKQUOTE CAPTION ]
- methods += <<-BEGIN + nn_element_def(element) + <<-END
- def #{element.downcase}(attributes = {})
- BEGIN
- end
- END
- end
-
- # - O EMPTY
- for element in %w[ IMG BASE BASEFONT BR AREA LINK PARAM HR INPUT
- COL ISINDEX META ]
- methods += <<-BEGIN + nOE_element_def(element) + <<-END
- def #{element.downcase}(attributes = {})
- BEGIN
- end
- END
- end
-
- # O O or - O
- for element in %w[ HTML BODY P DT DD LI OPTION THEAD TFOOT TBODY
- COLGROUP TR TH TD HEAD ]
- methods += <<-BEGIN + nO_element_def(element) + <<-END
- def #{element.downcase}(attributes = {})
- BEGIN
- end
- END
- end
- eval(methods)
- end
-
- end # Html4Tr
-
-
- # Mixin module for generating HTML version 4 with framesets.
- module Html4Fr # :nodoc:
-
- # The DOCTYPE declaration for this version of HTML
- def doctype
- %|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">|
- end
-
- # Initialise the HTML generation methods for this version.
- def element_init
- methods = ""
- # - -
- for element in %w[ FRAMESET ]
- methods += <<-BEGIN + nn_element_def(element) + <<-END
- def #{element.downcase}(attributes = {})
- BEGIN
- end
- END
- end
-
- # - O EMPTY
- for element in %w[ FRAME ]
- methods += <<-BEGIN + nOE_element_def(element) + <<-END
- def #{element.downcase}(attributes = {})
- BEGIN
- end
- END
- end
- eval(methods)
- end
-
- end # Html4Fr
-end
-
-
diff --git a/lib/cgi/session.rb b/lib/cgi/session.rb
deleted file mode 100644
index 2b5aa846d9..0000000000
--- a/lib/cgi/session.rb
+++ /dev/null
@@ -1,537 +0,0 @@
-#
-# cgi/session.rb - session support for cgi scripts
-#
-# Copyright (C) 2001 Yukihiro "Matz" Matsumoto
-# Copyright (C) 2000 Network Applied Communication Laboratory, Inc.
-# Copyright (C) 2000 Information-technology Promotion Agency, Japan
-#
-# Author: Yukihiro "Matz" Matsumoto
-#
-# Documentation: William Webber (william@williamwebber.com)
-#
-# == Overview
-#
-# This file provides the +CGI::Session+ class, which provides session
-# support for CGI scripts. A session is a sequence of HTTP requests
-# and responses linked together and associated with a single client.
-# Information associated with the session is stored
-# on the server between requests. A session id is passed between client
-# and server with every request and response, transparently
-# to the user. This adds state information to the otherwise stateless
-# HTTP request/response protocol.
-#
-# See the documentation to the +CGI::Session+ class for more details
-# and examples of usage. See cgi.rb for the +CGI+ class itself.
-
-require 'cgi'
-require 'tmpdir'
-
-class CGI
-
- # Class representing an HTTP session. See documentation for the file
- # cgi/session.rb for an introduction to HTTP sessions.
- #
- # == Lifecycle
- #
- # A CGI::Session instance is created from a CGI object. By default,
- # this CGI::Session instance will start a new session if none currently
- # exists, or continue the current session for this client if one does
- # exist. The +new_session+ option can be used to either always or
- # never create a new session. See #new() for more details.
- #
- # #delete() deletes a session from session storage. It
- # does not however remove the session id from the client. If the client
- # makes another request with the same id, the effect will be to start
- # a new session with the old session's id.
- #
- # == Setting and retrieving session data.
- #
- # The Session class associates data with a session as key-value pairs.
- # This data can be set and retrieved by indexing the Session instance
- # using '[]', much the same as hashes (although other hash methods
- # are not supported).
- #
- # When session processing has been completed for a request, the
- # session should be closed using the close() method. This will
- # store the session's state to persistent storage. If you want
- # to store the session's state to persistent storage without
- # finishing session processing for this request, call the update()
- # method.
- #
- # == Storing session state
- #
- # The caller can specify what form of storage to use for the session's
- # data with the +database_manager+ option to CGI::Session::new. The
- # following storage classes are provided as part of the standard library:
- #
- # CGI::Session::FileStore:: stores data as plain text in a flat file. Only
- # works with String data. This is the default
- # storage type.
- # CGI::Session::MemoryStore:: stores data in an in-memory hash. The data
- # only persists for as long as the current ruby
- # interpreter instance does.
- # CGI::Session::PStore:: stores data in Marshalled format. Provided by
- # cgi/session/pstore.rb. Supports data of any type,
- # and provides file-locking and transaction support.
- #
- # Custom storage types can also be created by defining a class with
- # the following methods:
- #
- # new(session, options)
- # restore # returns hash of session data.
- # update
- # close
- # delete
- #
- # Changing storage type mid-session does not work. Note in particular
- # that by default the FileStore and PStore session data files have the
- # same name. If your application switches from one to the other without
- # making sure that filenames will be different
- # and clients still have old sessions lying around in cookies, then
- # things will break nastily!
- #
- # == Maintaining the session id.
- #
- # Most session state is maintained on the server. However, a session
- # id must be passed backwards and forwards between client and server
- # to maintain a reference to this session state.
- #
- # The simplest way to do this is via cookies. The CGI::Session class
- # provides transparent support for session id communication via cookies
- # if the client has cookies enabled.
- #
- # If the client has cookies disabled, the session id must be included
- # as a parameter of all requests sent by the client to the server. The
- # CGI::Session class in conjunction with the CGI class will transparently
- # add the session id as a hidden input field to all forms generated
- # using the CGI#form() HTML generation method. No built-in support is
- # provided for other mechanisms, such as URL re-writing. The caller is
- # responsible for extracting the session id from the session_id
- # attribute and manually encoding it in URLs and adding it as a hidden
- # input to HTML forms created by other mechanisms. Also, session expiry
- # is not automatically handled.
- #
- # == Examples of use
- #
- # === Setting the user's name
- #
- # require 'cgi'
- # require 'cgi/session'
- # require 'cgi/session/pstore' # provides CGI::Session::PStore
- #
- # cgi = CGI.new("html4")
- #
- # session = CGI::Session.new(cgi,
- # 'database_manager' => CGI::Session::PStore, # use PStore
- # 'session_key' => '_rb_sess_id', # custom session key
- # 'session_expires' => Time.now + 30 * 60, # 30 minute timeout
- # 'prefix' => 'pstore_sid_') # PStore option
- # if cgi.has_key?('user_name') and cgi['user_name'] != ''
- # # coerce to String: cgi[] returns the
- # # string-like CGI::QueryExtension::Value
- # session['user_name'] = cgi['user_name'].to_s
- # elsif !session['user_name']
- # session['user_name'] = "guest"
- # end
- # session.close
- #
- # === Creating a new session safely
- #
- # require 'cgi'
- # require 'cgi/session'
- #
- # cgi = CGI.new("html4")
- #
- # # We make sure to delete an old session if one exists,
- # # not just to free resources, but to prevent the session
- # # from being maliciously hijacked later on.
- # begin
- # session = CGI::Session.new(cgi, 'new_session' => false)
- # session.delete
- # rescue ArgumentError # if no old session
- # end
- # session = CGI::Session.new(cgi, 'new_session' => true)
- # session.close
- #
- class Session
-
- class NoSession < RuntimeError #:nodoc:
- end
-
- # The id of this session.
- attr_reader :session_id, :new_session
-
- def Session::callback(dbman) #:nodoc:
- Proc.new{
- dbman[0].close unless dbman.empty?
- }
- end
-
- # Create a new session id.
- #
- # The session id is an MD5 hash based upon the time,
- # a random number, and a constant string. This routine
- # is used internally for automatically generated
- # session ids.
- def create_new_id
- require 'securerandom'
- begin
- session_id = SecureRandom.hex(16)
- rescue NotImplementedError
- require 'digest/md5'
- md5 = Digest::MD5::new
- now = Time::now
- md5.update(now.to_s)
- md5.update(String(now.usec))
- md5.update(String(rand(0)))
- md5.update(String($$))
- md5.update('foobar')
- session_id = md5.hexdigest
- end
- session_id
- end
- private :create_new_id
-
- # Create a new CGI::Session object for +request+.
- #
- # +request+ is an instance of the +CGI+ class (see cgi.rb).
- # +option+ is a hash of options for initialising this
- # CGI::Session instance. The following options are
- # recognised:
- #
- # session_key:: the parameter name used for the session id.
- # Defaults to '_session_id'.
- # session_id:: the session id to use. If not provided, then
- # it is retrieved from the +session_key+ parameter
- # of the request, or automatically generated for
- # a new session.
- # new_session:: if true, force creation of a new session. If not set,
- # a new session is only created if none currently
- # exists. If false, a new session is never created,
- # and if none currently exists and the +session_id+
- # option is not set, an ArgumentError is raised.
- # database_manager:: the name of the class providing storage facilities
- # for session state persistence. Built-in support
- # is provided for +FileStore+ (the default),
- # +MemoryStore+, and +PStore+ (from
- # cgi/session/pstore.rb). See the documentation for
- # these classes for more details.
- #
- # The following options are also recognised, but only apply if the
- # session id is stored in a cookie.
- #
- # session_expires:: the time the current session expires, as a
- # +Time+ object. If not set, the session will terminate
- # when the user's browser is closed.
- # session_domain:: the hostname domain for which this session is valid.
- # If not set, defaults to the hostname of the server.
- # session_secure:: if +true+, this session will only work over HTTPS.
- # session_path:: the path for which this session applies. Defaults
- # to the directory of the CGI script.
- #
- # +option+ is also passed on to the session storage class initializer; see
- # the documentation for each session storage class for the options
- # they support.
- #
- # The retrieved or created session is automatically added to +request+
- # as a cookie, and also to its +output_hidden+ table, which is used
- # to add hidden input elements to forms.
- #
- # *WARNING* the +output_hidden+
- # fields are surrounded by a <fieldset> tag in HTML 4 generation, which
- # is _not_ invisible on many browsers; you may wish to disable the
- # use of fieldsets with code similar to the following
- # (see http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/37805)
- #
- # cgi = CGI.new("html4")
- # class << cgi
- # undef_method :fieldset
- # end
- #
- def initialize(request, option={})
- @new_session = false
- session_key = option['session_key'] || '_session_id'
- session_id = option['session_id']
- unless session_id
- if option['new_session']
- session_id = create_new_id
- @new_session = true
- end
- end
- unless session_id
- if request.key?(session_key)
- session_id = request[session_key]
- session_id = session_id.read if session_id.respond_to?(:read)
- end
- unless session_id
- session_id, = request.cookies[session_key]
- end
- unless session_id
- unless option.fetch('new_session', true)
- raise ArgumentError, "session_key `%s' should be supplied"%session_key
- end
- session_id = create_new_id
- @new_session = true
- end
- end
- @session_id = session_id
- dbman = option['database_manager'] || FileStore
- begin
- @dbman = dbman::new(self, option)
- rescue NoSession
- unless option.fetch('new_session', true)
- raise ArgumentError, "invalid session_id `%s'"%session_id
- end
- session_id = @session_id = create_new_id unless session_id
- @new_session=true
- retry
- end
- request.instance_eval do
- @output_hidden = {session_key => session_id} unless option['no_hidden']
- @output_cookies = [
- Cookie::new("name" => session_key,
- "value" => session_id,
- "expires" => option['session_expires'],
- "domain" => option['session_domain'],
- "secure" => option['session_secure'],
- "path" =>
- if option['session_path']
- option['session_path']
- elsif ENV["SCRIPT_NAME"]
- File::dirname(ENV["SCRIPT_NAME"])
- else
- ""
- end)
- ] unless option['no_cookies']
- end
- @dbprot = [@dbman]
- ObjectSpace::define_finalizer(self, Session::callback(@dbprot))
- end
-
- # Retrieve the session data for key +key+.
- def [](key)
- @data ||= @dbman.restore
- @data[key]
- end
-
- # Set the session date for key +key+.
- def []=(key, val)
- @write_lock ||= true
- @data ||= @dbman.restore
- @data[key] = val
- end
-
- # Store session data on the server. For some session storage types,
- # this is a no-op.
- def update
- @dbman.update
- end
-
- # Store session data on the server and close the session storage.
- # For some session storage types, this is a no-op.
- def close
- @dbman.close
- @dbprot.clear
- end
-
- # Delete the session from storage. Also closes the storage.
- #
- # Note that the session's data is _not_ automatically deleted
- # upon the session expiring.
- def delete
- @dbman.delete
- @dbprot.clear
- end
-
- # File-based session storage class.
- #
- # Implements session storage as a flat file of 'key=value' values.
- # This storage type only works directly with String values; the
- # user is responsible for converting other types to Strings when
- # storing and from Strings when retrieving.
- class FileStore
- # Create a new FileStore instance.
- #
- # This constructor is used internally by CGI::Session. The
- # user does not generally need to call it directly.
- #
- # +session+ is the session for which this instance is being
- # created. The session id must only contain alphanumeric
- # characters; automatically generated session ids observe
- # this requirement.
- #
- # +option+ is a hash of options for the initializer. The
- # following options are recognised:
- #
- # tmpdir:: the directory to use for storing the FileStore
- # file. Defaults to Dir::tmpdir (generally "/tmp"
- # on Unix systems).
- # prefix:: the prefix to add to the session id when generating
- # the filename for this session's FileStore file.
- # Defaults to "cgi_sid_".
- # suffix:: the prefix to add to the session id when generating
- # the filename for this session's FileStore file.
- # Defaults to the empty string.
- #
- # This session's FileStore file will be created if it does
- # not exist, or opened if it does.
- def initialize(session, option={})
- dir = option['tmpdir'] || Dir::tmpdir
- prefix = option['prefix'] || 'cgi_sid_'
- suffix = option['suffix'] || ''
- id = session.session_id
- require 'digest/md5'
- md5 = Digest::MD5.hexdigest(id)[0,16]
- @path = dir+"/"+prefix+md5+suffix
- if File::exist? @path
- @hash = nil
- else
- unless session.new_session
- raise CGI::Session::NoSession, "uninitialized session"
- end
- @hash = {}
- end
- end
-
- # Restore session state from the session's FileStore file.
- #
- # Returns the session state as a hash.
- def restore
- unless @hash
- @hash = {}
- begin
- lockf = File.open(@path+".lock", "r")
- lockf.flock File::LOCK_SH
- f = File.open(@path, 'r')
- for line in f
- line.chomp!
- k, v = line.split('=',2)
- @hash[CGI::unescape(k)] = Marshal.restore(CGI::unescape(v))
- end
- ensure
- f.close unless f.nil?
- lockf.close if lockf
- end
- end
- @hash
- end
-
- # Save session state to the session's FileStore file.
- def update
- return unless @hash
- begin
- lockf = File.open(@path+".lock", File::CREAT|File::RDWR, 0600)
- lockf.flock File::LOCK_EX
- f = File.open(@path+".new", File::CREAT|File::TRUNC|File::WRONLY, 0600)
- for k,v in @hash
- f.printf "%s=%s\n", CGI::escape(k), CGI::escape(String(Marshal.dump(v)))
- end
- f.close
- File.rename @path+".new", @path
- ensure
- f.close if f and !f.closed?
- lockf.close if lockf
- end
- end
-
- # Update and close the session's FileStore file.
- def close
- update
- end
-
- # Close and delete the session's FileStore file.
- def delete
- File::unlink @path+".lock" rescue nil
- File::unlink @path+".new" rescue nil
- File::unlink @path rescue Errno::ENOENT
- end
- end
-
- # In-memory session storage class.
- #
- # Implements session storage as a global in-memory hash. Session
- # data will only persist for as long as the ruby interpreter
- # instance does.
- class MemoryStore
- GLOBAL_HASH_TABLE = {} #:nodoc:
-
- # Create a new MemoryStore instance.
- #
- # +session+ is the session this instance is associated with.
- # +option+ is a list of initialisation options. None are
- # currently recognised.
- def initialize(session, option=nil)
- @session_id = session.session_id
- unless GLOBAL_HASH_TABLE.key?(@session_id)
- unless session.new_session
- raise CGI::Session::NoSession, "uninitialized session"
- end
- GLOBAL_HASH_TABLE[@session_id] = {}
- end
- end
-
- # Restore session state.
- #
- # Returns session data as a hash.
- def restore
- GLOBAL_HASH_TABLE[@session_id]
- end
-
- # Update session state.
- #
- # A no-op.
- def update
- # don't need to update; hash is shared
- end
-
- # Close session storage.
- #
- # A no-op.
- def close
- # don't need to close
- end
-
- # Delete the session state.
- def delete
- GLOBAL_HASH_TABLE.delete(@session_id)
- end
- end
-
- # Dummy session storage class.
- #
- # Implements session storage place holder. No actual storage
- # will be done.
- class NullStore
- # Create a new NullStore instance.
- #
- # +session+ is the session this instance is associated with.
- # +option+ is a list of initialisation options. None are
- # currently recognised.
- def initialize(session, option=nil)
- end
-
- # Restore (empty) session state.
- def restore
- {}
- end
-
- # Update session state.
- #
- # A no-op.
- def update
- end
-
- # Close session storage.
- #
- # A no-op.
- def close
- end
-
- # Delete the session state.
- #
- # A no-op.
- def delete
- end
- end
- end
-end
diff --git a/lib/cgi/session/pstore.rb b/lib/cgi/session/pstore.rb
deleted file mode 100644
index 3cd3e46000..0000000000
--- a/lib/cgi/session/pstore.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-#
-# cgi/session/pstore.rb - persistent storage of marshalled session data
-#
-# Documentation: William Webber (william@williamwebber.com)
-#
-# == Overview
-#
-# This file provides the CGI::Session::PStore class, which builds
-# persistent of session data on top of the pstore library. See
-# cgi/session.rb for more details on session storage managers.
-
-require 'cgi/session'
-require 'pstore'
-
-class CGI
- class Session
- # PStore-based session storage class.
- #
- # This builds upon the top-level PStore class provided by the
- # library file pstore.rb. Session data is marshalled and stored
- # in a file. File locking and transaction services are provided.
- class PStore
- # Create a new CGI::Session::PStore instance
- #
- # This constructor is used internally by CGI::Session. The
- # user does not generally need to call it directly.
- #
- # +session+ is the session for which this instance is being
- # created. The session id must only contain alphanumeric
- # characters; automatically generated session ids observe
- # this requirement.
- #
- # +option+ is a hash of options for the initializer. The
- # following options are recognised:
- #
- # tmpdir:: the directory to use for storing the PStore
- # file. Defaults to Dir::tmpdir (generally "/tmp"
- # on Unix systems).
- # prefix:: the prefix to add to the session id when generating
- # the filename for this session's PStore file.
- # Defaults to the empty string.
- #
- # This session's PStore file will be created if it does
- # not exist, or opened if it does.
- def initialize(session, option={})
- dir = option['tmpdir'] || Dir::tmpdir
- prefix = option['prefix'] || ''
- id = session.session_id
- require 'digest/md5'
- md5 = Digest::MD5.hexdigest(id)[0,16]
- path = dir+"/"+prefix+md5
- path.untaint
- if File::exist?(path)
- @hash = nil
- else
- unless session.new_session
- raise CGI::Session::NoSession, "uninitialized session"
- end
- @hash = {}
- end
- @p = ::PStore.new(path)
- @p.transaction do |p|
- File.chmod(0600, p.path)
- end
- end
-
- # Restore session state from the session's PStore file.
- #
- # Returns the session state as a hash.
- def restore
- unless @hash
- @p.transaction do
- @hash = @p['hash'] || {}
- end
- end
- @hash
- end
-
- # Save session state to the session's PStore file.
- def update
- @p.transaction do
- @p['hash'] = @hash
- end
- end
-
- # Update and close the session's PStore file.
- def close
- update
- end
-
- # Close and delete the session's PStore file.
- def delete
- path = @p.path
- File::unlink path
- end
-
- end
- end
-end
-
-if $0 == __FILE__
- # :enddoc:
- STDIN.reopen("/dev/null")
- cgi = CGI.new
- session = CGI::Session.new(cgi, 'database_manager' => CGI::Session::PStore)
- session['key'] = {'k' => 'v'}
- puts session['key'].class
- fail unless Hash === session['key']
- puts session['key'].inspect
- fail unless session['key'].inspect == '{"k"=>"v"}'
-end
diff --git a/lib/cgi/util.rb b/lib/cgi/util.rb
index 991b68ce73..50a2e91665 100644
--- a/lib/cgi/util.rb
+++ b/lib/cgi/util.rb
@@ -1,181 +1,7 @@
-class CGI
- # URL-encode a string.
- # url_encoded_string = CGI::escape("'Stop!' said Fred")
- # # => "%27Stop%21%27+said+Fred"
- def CGI::escape(string)
- string.gsub(/([^ a-zA-Z0-9_.-]+)/) do
- '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
- end.tr(' ', '+')
- end
+# frozen_string_literal: true
-
- # URL-decode a string.
- # string = CGI::unescape("%27Stop%21%27+said+Fred")
- # # => "'Stop!' said Fred"
- def CGI::unescape(string)
- enc = string.encoding
- string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/) do
- [$1.delete('%')].pack('H*').force_encoding(enc)
- end
- end
-
- TABLE_FOR_ESCAPE_HTML__ = {
- '&' => '&amp;',
- '"' => '&quot;',
- '<' => '&lt;',
- '>' => '&gt;',
- }
-
- # Escape special characters in HTML, namely &\"<>
- # CGI::escapeHTML('Usage: foo "bar" <baz>')
- # # => "Usage: foo &quot;bar&quot; &lt;baz&gt;"
- def CGI::escapeHTML(string)
- string.gsub(/[&\"<>]/, TABLE_FOR_ESCAPE_HTML__)
- end
-
-
- # Unescape a string that has been HTML-escaped
- # CGI::unescapeHTML("Usage: foo &quot;bar&quot; &lt;baz&gt;")
- # # => "Usage: foo \"bar\" <baz>"
- def CGI::unescapeHTML(string)
- enc = string.encoding
- if [Encoding::UTF_16BE, Encoding::UTF_16LE, Encoding::UTF_32BE, Encoding::UTF_32LE].include?(enc)
- return string.gsub(Regexp.new('&(amp|quot|gt|lt|#[0-9]+|#x[0-9A-Fa-f]+);'.encode(enc))) do
- case $1.encode("US-ASCII")
- when 'amp' then '&'.encode(enc)
- when 'quot' then '"'.encode(enc)
- when 'gt' then '>'.encode(enc)
- when 'lt' then '<'.encode(enc)
- when /\A#0*(\d+)\z/ then $1.to_i.chr(enc)
- when /\A#x([0-9a-f]+)\z/i then $1.hex.chr(enc)
- end
- end
- end
- asciicompat = Encoding.compatible?(string, "a")
- string.gsub(/&(amp|quot|gt|lt|\#[0-9]+|\#x[0-9A-Fa-f]+);/) do
- match = $1.dup
- case match
- when 'amp' then '&'
- when 'quot' then '"'
- when 'gt' then '>'
- when 'lt' then '<'
- when /\A#0*(\d+)\z/
- n = $1.to_i
- if enc == Encoding::UTF_8 or
- enc == Encoding::ISO_8859_1 && n < 256 or
- asciicompat && n < 128
- n.chr(enc)
- else
- "&##{$1};"
- end
- when /\A#x([0-9a-f]+)\z/i
- n = $1.hex
- if enc == Encoding::UTF_8 or
- enc == Encoding::ISO_8859_1 && n < 256 or
- asciicompat && n < 128
- n.chr(enc)
- else
- "&#x#{$1};"
- end
- else
- "&#{match};"
- end
- end
- end
- def CGI::escape_html(str)
- escapeHTML(str)
- end
- def CGI::unescape_html(str)
- unescapeHTML(str)
- end
-
- # Escape only the tags of certain HTML elements in +string+.
- #
- # Takes an element or elements or array of elements. Each element
- # is specified by the name of the element, without angle brackets.
- # This matches both the start and the end tag of that element.
- # The attribute list of the open tag will also be escaped (for
- # instance, the double-quotes surrounding attribute values).
- #
- # print CGI::escapeElement('<BR><A HREF="url"></A>', "A", "IMG")
- # # "<BR>&lt;A HREF=&quot;url&quot;&gt;&lt;/A&gt"
- #
- # print CGI::escapeElement('<BR><A HREF="url"></A>', ["A", "IMG"])
- # # "<BR>&lt;A HREF=&quot;url&quot;&gt;&lt;/A&gt"
- def CGI::escapeElement(string, *elements)
- elements = elements[0] if elements[0].kind_of?(Array)
- unless elements.empty?
- string.gsub(/<\/?(?:#{elements.join("|")})(?!\w)(?:.|\n)*?>/i) do
- CGI::escapeHTML($&)
- end
- else
- string
- end
- end
-
-
- # Undo escaping such as that done by CGI::escapeElement()
- #
- # print CGI::unescapeElement(
- # CGI::escapeHTML('<BR><A HREF="url"></A>'), "A", "IMG")
- # # "&lt;BR&gt;<A HREF="url"></A>"
- #
- # print CGI::unescapeElement(
- # CGI::escapeHTML('<BR><A HREF="url"></A>'), ["A", "IMG"])
- # # "&lt;BR&gt;<A HREF="url"></A>"
- def CGI::unescapeElement(string, *elements)
- elements = elements[0] if elements[0].kind_of?(Array)
- unless elements.empty?
- string.gsub(/&lt;\/?(?:#{elements.join("|")})(?!\w)(?:.|\n)*?&gt;/i) do
- CGI::unescapeHTML($&)
- end
- else
- string
- end
- end
- def CGI::escape_element(str)
- escapeElement(str)
- end
- def CGI::unescape_element(str)
- unescapeElement(str)
- end
-
- # Format a +Time+ object as a String using the format specified by RFC 1123.
- #
- # CGI::rfc1123_date(Time.now)
- # # Sat, 01 Jan 2000 00:00:00 GMT
- def CGI::rfc1123_date(time)
- t = time.clone.gmtime
- return format("%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT",
- RFC822_DAYS[t.wday], t.day, RFC822_MONTHS[t.month-1], t.year,
- t.hour, t.min, t.sec)
- end
-
- # Prettify (indent) an HTML string.
- #
- # +string+ is the HTML string to indent. +shift+ is the indentation
- # unit to use; it defaults to two spaces.
- #
- # print CGI::pretty("<HTML><BODY></BODY></HTML>")
- # # <HTML>
- # # <BODY>
- # # </BODY>
- # # </HTML>
- #
- # print CGI::pretty("<HTML><BODY></BODY></HTML>", "\t")
- # # <HTML>
- # # <BODY>
- # # </BODY>
- # # </HTML>
- #
- def CGI::pretty(string, shift = " ")
- lines = string.gsub(/(?!\A)<(?:.|\n)*?>/, "\n\\0").gsub(/<(?:.|\n)*?>(?!\n)/, "\\0\n")
- end_pos = 0
- while end_pos = lines.index(/^<\/(\w+)/, end_pos)
- element = $1.dup
- start_pos = lines.rindex(/^\s*<#{element}/i, end_pos)
- lines[start_pos ... end_pos] = "__" + lines[start_pos ... end_pos].gsub(/\n(?!\z)/, "\n" + shift) + "__"
- end
- lines.gsub(/^((?:#{Regexp::quote(shift)})*)__(?=<\/?\w)/, '\1')
- end
-end
+require "cgi/escape"
+warn <<-WARNING, uplevel: Gem::BUNDLED_GEMS.uplevel if $VERBOSE
+CGI::Util is removed from Ruby 4.0. Please use cgi/escape instead for CGI.escape and CGI.unescape features.
+If you are using CGI.parse, please install and use the cgi gem instead.
+WARNING
diff --git a/lib/cmath.rb b/lib/cmath.rb
deleted file mode 100644
index 95a30c336b..0000000000
--- a/lib/cmath.rb
+++ /dev/null
@@ -1,233 +0,0 @@
-module CMath
-
- include Math
-
- alias exp! exp
- alias log! log
- alias log10! log10
- alias sqrt! sqrt
-
- alias sin! sin
- alias cos! cos
- alias tan! tan
-
- alias sinh! sinh
- alias cosh! cosh
- alias tanh! tanh
-
- alias asin! asin
- alias acos! acos
- alias atan! atan
- alias atan2! atan2
-
- alias asinh! asinh
- alias acosh! acosh
- alias atanh! atanh
-
- def exp(z)
- if z.real?
- exp!(z)
- else
- Complex(exp!(z.real) * cos!(z.imag),
- exp!(z.real) * sin!(z.imag))
- end
- end
-
- def log(*args)
- z, b = args
- if z.real? and z >= 0 and (b.nil? or b >= 0)
- log!(*args)
- else
- r, theta = z.polar
- a = Complex(log!(r.abs), theta)
- if b
- a /= log(b)
- end
- a
- end
- end
-
- def log10(z)
- if z.real?
- log10!(z)
- else
- log(z) / log!(10)
- end
- end
-
- def sqrt(z)
- if z.real?
- if z < 0
- Complex(0, sqrt!(-z))
- else
- sqrt!(z)
- end
- else
- if z.imag < 0
- sqrt(z.conjugate).conjugate
- else
- r = z.abs
- x = z.real
- Complex(sqrt!((r + x) / 2), sqrt!((r - x) / 2))
- end
- end
- end
-
- def sin(z)
- if z.real?
- sin!(z)
- else
- Complex(sin!(z.real) * cosh!(z.imag),
- cos!(z.real) * sinh!(z.imag))
- end
- end
-
- def cos(z)
- if z.real?
- cos!(z)
- else
- Complex(cos!(z.real) * cosh!(z.imag),
- -sin!(z.real) * sinh!(z.imag))
- end
- end
-
- def tan(z)
- if z.real?
- tan!(z)
- else
- sin(z)/cos(z)
- end
- end
-
- def sinh(z)
- if z.real?
- sinh!(z)
- else
- Complex(sinh!(z.real) * cos!(z.imag),
- cosh!(z.real) * sin!(z.imag))
- end
- end
-
- def cosh(z)
- if z.real?
- cosh!(z)
- else
- Complex(cosh!(z.real) * cos!(z.imag),
- sinh!(z.real) * sin!(z.imag))
- end
- end
-
- def tanh(z)
- if z.real?
- tanh!(z)
- else
- sinh(z) / cosh(z)
- end
- end
-
- def asin(z)
- if z.real? and z >= -1 and z <= 1
- asin!(z)
- else
- Complex(0, -1.0) * log(Complex(0, 1.0) * z + sqrt(1.0 - z * z))
- end
- end
-
- def acos(z)
- if z.real? and z >= -1 and z <= 1
- acos!(z)
- else
- Complex(0, -1.0) * log(z + Complex(0, 1.0) * sqrt(1.0 - z * z))
- end
- end
-
- def atan(z)
- if z.real?
- atan!(z)
- else
- Complex(0, 1.0) * log((Complex(0, 1.0) + z) / (Complex(0, 1.0) - z)) / 2.0
- end
- end
-
- def atan2(y,x)
- if y.real? and x.real?
- atan2!(y,x)
- else
- Complex(0, -1.0) * log((x + Complex(0, 1.0) * y) / sqrt(x * x + y * y))
- end
- end
-
- def acosh(z)
- if z.real? and z >= 1
- acosh!(z)
- else
- log(z + sqrt(z * z - 1.0))
- end
- end
-
- def asinh(z)
- if z.real?
- asinh!(z)
- else
- log(z + sqrt(1.0 + z * z))
- end
- end
-
- def atanh(z)
- if z.real? and z >= -1 and z <= 1
- atanh!(z)
- else
- log((1.0 + z) / (1.0 - z)) / 2.0
- end
- end
-
- module_function :exp!
- module_function :exp
- module_function :log!
- module_function :log
- module_function :log10!
- module_function :log10
- module_function :sqrt!
- module_function :sqrt
-
- module_function :sin!
- module_function :sin
- module_function :cos!
- module_function :cos
- module_function :tan!
- module_function :tan
-
- module_function :sinh!
- module_function :sinh
- module_function :cosh!
- module_function :cosh
- module_function :tanh!
- module_function :tanh
-
- module_function :asin!
- module_function :asin
- module_function :acos!
- module_function :acos
- module_function :atan!
- module_function :atan
- module_function :atan2!
- module_function :atan2
-
- module_function :asinh!
- module_function :asinh
- module_function :acosh!
- module_function :acosh
- module_function :atanh!
- module_function :atanh
-
- module_function :log2
- module_function :cbrt
- module_function :frexp
- module_function :ldexp
- module_function :hypot
- module_function :erf
- module_function :erfc
- module_function :gamma
- module_function :lgamma
-
-end
diff --git a/lib/complex.rb b/lib/complex.rb
deleted file mode 100644
index 301879143f..0000000000
--- a/lib/complex.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require 'cmath'
-
-unless defined?(Math.exp!)
- Object.instance_eval{remove_const :Math}
- Math = CMath
-end
-
-def Complex.generic? (other)
- other.kind_of?(Integer) ||
- other.kind_of?(Float) ||
- other.kind_of?(Rational)
-end
-
-class Complex
-
- alias image imag
-
-end
-
-class Numeric
-
- def im() Complex(0, self) end
-
-end
diff --git a/lib/csv.rb b/lib/csv.rb
deleted file mode 100644
index fd83fdc354..0000000000
--- a/lib/csv.rb
+++ /dev/null
@@ -1,2311 +0,0 @@
-#!/usr/bin/env ruby -w
-# encoding: UTF-8
-# = csv.rb -- CSV Reading and Writing
-#
-# Created by James Edward Gray II on 2005-10-31.
-# Copyright 2005 James Edward Gray II. You can redistribute or modify this code
-# under the terms of Ruby's license.
-#
-# See CSV for documentation.
-#
-# == Description
-#
-# Welcome to the new and improved CSV.
-#
-# This version of the CSV library began its life as FasterCSV. FasterCSV was
-# intended as a replacement to Ruby's then standard CSV library. It was
-# designed to address concerns users of that library had and it had three
-# primary goals:
-#
-# 1. Be significantly faster than CSV while remaining a pure Ruby library.
-# 2. Use a smaller and easier to maintain code base. (FasterCSV eventually
-# grew larger, was also but considerably richer in features. The parsing
-# core remains quite small.)
-# 3. Improve on the CSV interface.
-#
-# Obviously, the last one is subjective. I did try to defer to the original
-# interface whenever I didn't have a compelling reason to change it though, so
-# hopefully this won't be too radically different.
-#
-# We must have met our goals because FasterCSV was renamed to CSV and replaced
-# the original library.
-#
-# == What's Different From the Old CSV?
-#
-# I'm sure I'll miss something, but I'll try to mention most of the major
-# differences I am aware of, to help others quickly get up to speed:
-#
-# === CSV Parsing
-#
-# * This parser is m17n aware. See CSV for full details.
-# * This library has a stricter parser and will throw MalformedCSVErrors on
-# problematic data.
-# * This library has a less liberal idea of a line ending than CSV. What you
-# set as the <tt>:row_sep</tt> is law. It can auto-detect your line endings
-# though.
-# * The old library returned empty lines as <tt>[nil]</tt>. This library calls
-# them <tt>[]</tt>.
-# * This library has a much faster parser.
-#
-# === Interface
-#
-# * CSV now uses Hash-style parameters to set options.
-# * CSV no longer has generate_row() or parse_row().
-# * The old CSV's Reader and Writer classes have been dropped.
-# * CSV::open() is now more like Ruby's open().
-# * CSV objects now support most standard IO methods.
-# * CSV now has a new() method used to wrap objects like String and IO for
-# reading and writing.
-# * CSV::generate() is different from the old method.
-# * CSV no longer supports partial reads. It works line-by-line.
-# * CSV no longer allows the instance methods to override the separators for
-# performance reasons. They must be set in the constructor.
-#
-# If you use this library and find yourself missing any functionality I have
-# trimmed, please {let me know}[mailto:james@grayproductions.net].
-#
-# == Documentation
-#
-# See CSV for documentation.
-#
-# == What is CSV, really?
-#
-# CSV maintains a pretty strict definition of CSV taken directly from
-# {the RFC}[http://www.ietf.org/rfc/rfc4180.txt]. I relax the rules in only one
-# place and that is to make using this library easier. CSV will parse all valid
-# CSV.
-#
-# What you don't want to do is feed CSV invalid data. Because of the way the
-# CSV format works, it's common for a parser to need to read until the end of
-# the file to be sure a field is invalid. This eats a lot of time and memory.
-#
-# Luckily, when working with invalid CSV, Ruby's built-in methods will almost
-# always be superior in every way. For example, parsing non-quoted fields is as
-# easy as:
-#
-# data.split(",")
-#
-# == Questions and/or Comments
-#
-# Feel free to email {James Edward Gray II}[mailto:james@grayproductions.net]
-# with any questions.
-
-require "forwardable"
-require "English"
-require "date"
-require "stringio"
-
-#
-# This class provides a complete interface to CSV files and data. It offers
-# tools to enable you to read and write to and from Strings or IO objects, as
-# needed.
-#
-# == Reading
-#
-# === From a File
-#
-# ==== A Line at a Time
-#
-# CSV.foreach("path/to/file.csv") do |row|
-# # use row here...
-# end
-#
-# ==== All at Once
-#
-# arr_of_arrs = CSV.read("path/to/file.csv")
-#
-# === From a String
-#
-# ==== A Line at a Time
-#
-# CSV.parse("CSV,data,String") do |row|
-# # use row here...
-# end
-#
-# ==== All at Once
-#
-# arr_of_arrs = CSV.parse("CSV,data,String")
-#
-# == Writing
-#
-# === To a File
-#
-# CSV.open("path/to/file.csv", "wb") do |csv|
-# csv << ["row", "of", "CSV", "data"]
-# csv << ["another", "row"]
-# # ...
-# end
-#
-# === To a String
-#
-# csv_string = CSV.generate do |csv|
-# csv << ["row", "of", "CSV", "data"]
-# csv << ["another", "row"]
-# # ...
-# end
-#
-# == Convert a Single Line
-#
-# csv_string = ["CSV", "data"].to_csv # to CSV
-# csv_array = "CSV,String".parse_csv # from CSV
-#
-# == Shortcut Interface
-#
-# CSV { |csv_out| csv_out << %w{my data here} } # to $stdout
-# CSV(csv = "") { |csv_str| csv_str << %w{my data here} } # to a String
-# CSV($stderr) { |csv_err| csv_err << %w{my data here} } # to $stderr
-#
-# == CSV and Character Encodings (M17n or Multilingualization)
-#
-# This new CSV parser is m17n savvy. The parser works in the Encoding of the IO
-# or String object being read from or written to. Your data is never transcoded
-# (unless you ask Ruby to transcode it for you) and will literally be parsed in
-# the Encoding it is in. Thus CSV will return Arrays or Rows of Strings in the
-# Encoding of your data. This is accomplished by transcoding the parser itself
-# into your Encoding.
-#
-# Some transcoding must take place, of course, to accomplish this multiencoding
-# support. For example, <tt>:col_sep</tt>, <tt>:row_sep</tt>, and
-# <tt>:quote_char</tt> must be transcoded to match your data. Hopefully this
-# makes the entire process feel transparent, since CSV's defaults should just
-# magically work for you data. However, you can set these values manually in
-# the target Encoding to avoid the translation.
-#
-# It's also important to note that while all of CSV's core parser is now
-# Encoding agnostic, some features are not. For example, the built-in
-# converters will try to transcode data to UTF-8 before making conversions.
-# Again, you can provide custom converters that are aware of your Encodings to
-# avoid this translation. It's just too hard for me to support native
-# conversions in all of Ruby's Encodings.
-#
-# Anyway, the practical side of this is simple: make sure IO and String objects
-# passed into CSV have the proper Encoding set and everything should just work.
-# CSV methods that allow you to open IO objects (CSV::foreach(), CSV::open(),
-# CSV::read(), and CSV::readlines()) do allow you to specify the Encoding.
-#
-# One minor exception comes when generating CSV into a String with an Encoding
-# that is not ASCII compatible. There's no existing data for CSV to use to
-# prepare itself and thus you will probably need to manually specify the desired
-# Encoding for most of those cases. It will try to guess using the fields in a
-# row of output though, when using CSV::generate_line() or Array#to_csv().
-#
-# I try to point out any other Encoding issues in the documentation of methods
-# as they come up.
-#
-# This has been tested to the best of my ability with all non-"dummy" Encodings
-# Ruby ships with. However, it is brave new code and may have some bugs.
-# Please feel free to {report}[mailto:james@grayproductions.net] any issues you
-# find with it.
-#
-class CSV
- # The version of the installed library.
- VERSION = "2.4.5".freeze
-
- #
- # A CSV::Row is part Array and part Hash. It retains an order for the fields
- # and allows duplicates just as an Array would, but also allows you to access
- # fields by name just as you could if they were in a Hash.
- #
- # All rows returned by CSV will be constructed from this class, if header row
- # processing is activated.
- #
- class Row
- #
- # Construct a new CSV::Row from +headers+ and +fields+, which are expected
- # to be Arrays. If one Array is shorter than the other, it will be padded
- # with +nil+ objects.
- #
- # The optional +header_row+ parameter can be set to +true+ to indicate, via
- # CSV::Row.header_row?() and CSV::Row.field_row?(), that this is a header
- # row. Otherwise, the row is assumes to be a field row.
- #
- # A CSV::Row object supports the following Array methods through delegation:
- #
- # * empty?()
- # * length()
- # * size()
- #
- def initialize(headers, fields, header_row = false)
- @header_row = header_row
-
- # handle extra headers or fields
- @row = if headers.size > fields.size
- headers.zip(fields)
- else
- fields.zip(headers).map { |pair| pair.reverse }
- end
- end
-
- # Internal data format used to compare equality.
- attr_reader :row
- protected :row
-
- ### Array Delegation ###
-
- extend Forwardable
- def_delegators :@row, :empty?, :length, :size
-
- # Returns +true+ if this is a header row.
- def header_row?
- @header_row
- end
-
- # Returns +true+ if this is a field row.
- def field_row?
- not header_row?
- end
-
- # Returns the headers of this row.
- def headers
- @row.map { |pair| pair.first }
- end
-
- #
- # :call-seq:
- # field( header )
- # field( header, offset )
- # field( index )
- #
- # This method will fetch the field value by +header+ or +index+. If a field
- # is not found, +nil+ is returned.
- #
- # When provided, +offset+ ensures that a header match occurrs on or later
- # than the +offset+ index. You can use this to find duplicate headers,
- # without resorting to hard-coding exact indices.
- #
- def field(header_or_index, minimum_index = 0)
- # locate the pair
- finder = header_or_index.is_a?(Integer) ? :[] : :assoc
- pair = @row[minimum_index..-1].send(finder, header_or_index)
-
- # return the field if we have a pair
- pair.nil? ? nil : pair.last
- end
- alias_method :[], :field
-
- #
- # :call-seq:
- # []=( header, value )
- # []=( header, offset, value )
- # []=( index, value )
- #
- # Looks up the field by the semantics described in CSV::Row.field() and
- # assigns the +value+.
- #
- # Assigning past the end of the row with an index will set all pairs between
- # to <tt>[nil, nil]</tt>. Assigning to an unused header appends the new
- # pair.
- #
- def []=(*args)
- value = args.pop
-
- if args.first.is_a? Integer
- if @row[args.first].nil? # extending past the end with index
- @row[args.first] = [nil, value]
- @row.map! { |pair| pair.nil? ? [nil, nil] : pair }
- else # normal index assignment
- @row[args.first][1] = value
- end
- else
- index = index(*args)
- if index.nil? # appending a field
- self << [args.first, value]
- else # normal header assignment
- @row[index][1] = value
- end
- end
- end
-
- #
- # :call-seq:
- # <<( field )
- # <<( header_and_field_array )
- # <<( header_and_field_hash )
- #
- # If a two-element Array is provided, it is assumed to be a header and field
- # and the pair is appended. A Hash works the same way with the key being
- # the header and the value being the field. Anything else is assumed to be
- # a lone field which is appended with a +nil+ header.
- #
- # This method returns the row for chaining.
- #
- def <<(arg)
- if arg.is_a?(Array) and arg.size == 2 # appending a header and name
- @row << arg
- elsif arg.is_a?(Hash) # append header and name pairs
- arg.each { |pair| @row << pair }
- else # append field value
- @row << [nil, arg]
- end
-
- self # for chaining
- end
-
- #
- # A shortcut for appending multiple fields. Equivalent to:
- #
- # args.each { |arg| csv_row << arg }
- #
- # This method returns the row for chaining.
- #
- def push(*args)
- args.each { |arg| self << arg }
-
- self # for chaining
- end
-
- #
- # :call-seq:
- # delete( header )
- # delete( header, offset )
- # delete( index )
- #
- # Used to remove a pair from the row by +header+ or +index+. The pair is
- # located as described in CSV::Row.field(). The deleted pair is returned,
- # or +nil+ if a pair could not be found.
- #
- def delete(header_or_index, minimum_index = 0)
- if header_or_index.is_a? Integer # by index
- @row.delete_at(header_or_index)
- else # by header
- @row.delete_at(index(header_or_index, minimum_index))
- end
- end
-
- #
- # The provided +block+ is passed a header and field for each pair in the row
- # and expected to return +true+ or +false+, depending on whether the pair
- # should be deleted.
- #
- # This method returns the row for chaining.
- #
- def delete_if(&block)
- @row.delete_if(&block)
-
- self # for chaining
- end
-
- #
- # This method accepts any number of arguments which can be headers, indices,
- # Ranges of either, or two-element Arrays containing a header and offset.
- # Each argument will be replaced with a field lookup as described in
- # CSV::Row.field().
- #
- # If called with no arguments, all fields are returned.
- #
- def fields(*headers_and_or_indices)
- if headers_and_or_indices.empty? # return all fields--no arguments
- @row.map { |pair| pair.last }
- else # or work like values_at()
- headers_and_or_indices.inject(Array.new) do |all, h_or_i|
- all + if h_or_i.is_a? Range
- index_begin = h_or_i.begin.is_a?(Integer) ? h_or_i.begin :
- index(h_or_i.begin)
- index_end = h_or_i.end.is_a?(Integer) ? h_or_i.end :
- index(h_or_i.end)
- new_range = h_or_i.exclude_end? ? (index_begin...index_end) :
- (index_begin..index_end)
- fields.values_at(new_range)
- else
- [field(*Array(h_or_i))]
- end
- end
- end
- end
- alias_method :values_at, :fields
-
- #
- # :call-seq:
- # index( header )
- # index( header, offset )
- #
- # This method will return the index of a field with the provided +header+.
- # The +offset+ can be used to locate duplicate header names, as described in
- # CSV::Row.field().
- #
- def index(header, minimum_index = 0)
- # find the pair
- index = headers[minimum_index..-1].index(header)
- # return the index at the right offset, if we found one
- index.nil? ? nil : index + minimum_index
- end
-
- # Returns +true+ if +name+ is a header for this row, and +false+ otherwise.
- def header?(name)
- headers.include? name
- end
- alias_method :include?, :header?
-
- #
- # Returns +true+ if +data+ matches a field in this row, and +false+
- # otherwise.
- #
- def field?(data)
- fields.include? data
- end
-
- include Enumerable
-
- #
- # Yields each pair of the row as header and field tuples (much like
- # iterating over a Hash).
- #
- # Support for Enumerable.
- #
- # This method returns the row for chaining.
- #
- def each(&block)
- @row.each(&block)
-
- self # for chaining
- end
-
- #
- # Returns +true+ if this row contains the same headers and fields in the
- # same order as +other+.
- #
- def ==(other)
- @row == other.row
- end
-
- #
- # Collapses the row into a simple Hash. Be warning that this discards field
- # order and clobbers duplicate fields.
- #
- def to_hash
- # flatten just one level of the internal Array
- Hash[*@row.inject(Array.new) { |ary, pair| ary.push(*pair) }]
- end
-
- #
- # Returns the row as a CSV String. Headers are not used. Equivalent to:
- #
- # csv_row.fields.to_csv( options )
- #
- def to_csv(options = Hash.new)
- fields.to_csv(options)
- end
- alias_method :to_s, :to_csv
-
- # A summary of fields, by header, in an ASCII compatible String.
- def inspect
- str = ["#<", self.class.to_s]
- each do |header, field|
- str << " " << (header.is_a?(Symbol) ? header.to_s : header.inspect) <<
- ":" << field.inspect
- end
- str << ">"
- begin
- str.join
- rescue # any encoding error
- str.map do |s|
- e = Encoding::Converter.asciicompat_encoding(s.encoding)
- e ? s.encode(e) : s.force_encoding("ASCII-8BIT")
- end.join
- end
- end
- end
-
- #
- # A CSV::Table is a two-dimensional data structure for representing CSV
- # documents. Tables allow you to work with the data by row or column,
- # manipulate the data, and even convert the results back to CSV, if needed.
- #
- # All tables returned by CSV will be constructed from this class, if header
- # row processing is activated.
- #
- class Table
- #
- # Construct a new CSV::Table from +array_of_rows+, which are expected
- # to be CSV::Row objects. All rows are assumed to have the same headers.
- #
- # A CSV::Table object supports the following Array methods through
- # delegation:
- #
- # * empty?()
- # * length()
- # * size()
- #
- def initialize(array_of_rows)
- @table = array_of_rows
- @mode = :col_or_row
- end
-
- # The current access mode for indexing and iteration.
- attr_reader :mode
-
- # Internal data format used to compare equality.
- attr_reader :table
- protected :table
-
- ### Array Delegation ###
-
- extend Forwardable
- def_delegators :@table, :empty?, :length, :size
-
- #
- # Returns a duplicate table object, in column mode. This is handy for
- # chaining in a single call without changing the table mode, but be aware
- # that this method can consume a fair amount of memory for bigger data sets.
- #
- # This method returns the duplicate table for chaining. Don't chain
- # destructive methods (like []=()) this way though, since you are working
- # with a duplicate.
- #
- def by_col
- self.class.new(@table.dup).by_col!
- end
-
- #
- # Switches the mode of this table to column mode. All calls to indexing and
- # iteration methods will work with columns until the mode is changed again.
- #
- # This method returns the table and is safe to chain.
- #
- def by_col!
- @mode = :col
-
- self
- end
-
- #
- # Returns a duplicate table object, in mixed mode. This is handy for
- # chaining in a single call without changing the table mode, but be aware
- # that this method can consume a fair amount of memory for bigger data sets.
- #
- # This method returns the duplicate table for chaining. Don't chain
- # destructive methods (like []=()) this way though, since you are working
- # with a duplicate.
- #
- def by_col_or_row
- self.class.new(@table.dup).by_col_or_row!
- end
-
- #
- # Switches the mode of this table to mixed mode. All calls to indexing and
- # iteration methods will use the default intelligent indexing system until
- # the mode is changed again. In mixed mode an index is assumed to be a row
- # reference while anything else is assumed to be column access by headers.
- #
- # This method returns the table and is safe to chain.
- #
- def by_col_or_row!
- @mode = :col_or_row
-
- self
- end
-
- #
- # Returns a duplicate table object, in row mode. This is handy for chaining
- # in a single call without changing the table mode, but be aware that this
- # method can consume a fair amount of memory for bigger data sets.
- #
- # This method returns the duplicate table for chaining. Don't chain
- # destructive methods (like []=()) this way though, since you are working
- # with a duplicate.
- #
- def by_row
- self.class.new(@table.dup).by_row!
- end
-
- #
- # Switches the mode of this table to row mode. All calls to indexing and
- # iteration methods will work with rows until the mode is changed again.
- #
- # This method returns the table and is safe to chain.
- #
- def by_row!
- @mode = :row
-
- self
- end
-
- #
- # Returns the headers for the first row of this table (assumed to match all
- # other rows). An empty Array is returned for empty tables.
- #
- def headers
- if @table.empty?
- Array.new
- else
- @table.first.headers
- end
- end
-
- #
- # In the default mixed mode, this method returns rows for index access and
- # columns for header access. You can force the index association by first
- # calling by_col!() or by_row!().
- #
- # Columns are returned as an Array of values. Altering that Array has no
- # effect on the table.
- #
- def [](index_or_header)
- if @mode == :row or # by index
- (@mode == :col_or_row and index_or_header.is_a? Integer)
- @table[index_or_header]
- else # by header
- @table.map { |row| row[index_or_header] }
- end
- end
-
- #
- # In the default mixed mode, this method assigns rows for index access and
- # columns for header access. You can force the index association by first
- # calling by_col!() or by_row!().
- #
- # Rows may be set to an Array of values (which will inherit the table's
- # headers()) or a CSV::Row.
- #
- # Columns may be set to a single value, which is copied to each row of the
- # column, or an Array of values. Arrays of values are assigned to rows top
- # to bottom in row major order. Excess values are ignored and if the Array
- # does not have a value for each row the extra rows will receive a +nil+.
- #
- # Assigning to an existing column or row clobbers the data. Assigning to
- # new columns creates them at the right end of the table.
- #
- def []=(index_or_header, value)
- if @mode == :row or # by index
- (@mode == :col_or_row and index_or_header.is_a? Integer)
- if value.is_a? Array
- @table[index_or_header] = Row.new(headers, value)
- else
- @table[index_or_header] = value
- end
- else # set column
- if value.is_a? Array # multiple values
- @table.each_with_index do |row, i|
- if row.header_row?
- row[index_or_header] = index_or_header
- else
- row[index_or_header] = value[i]
- end
- end
- else # repeated value
- @table.each do |row|
- if row.header_row?
- row[index_or_header] = index_or_header
- else
- row[index_or_header] = value
- end
- end
- end
- end
- end
-
- #
- # The mixed mode default is to treat a list of indices as row access,
- # returning the rows indicated. Anything else is considered columnar
- # access. For columnar access, the return set has an Array for each row
- # with the values indicated by the headers in each Array. You can force
- # column or row mode using by_col!() or by_row!().
- #
- # You cannot mix column and row access.
- #
- def values_at(*indices_or_headers)
- if @mode == :row or # by indices
- ( @mode == :col_or_row and indices_or_headers.all? do |index|
- index.is_a?(Integer) or
- ( index.is_a?(Range) and
- index.first.is_a?(Integer) and
- index.last.is_a?(Integer) )
- end )
- @table.values_at(*indices_or_headers)
- else # by headers
- @table.map { |row| row.values_at(*indices_or_headers) }
- end
- end
-
- #
- # Adds a new row to the bottom end of this table. You can provide an Array,
- # which will be converted to a CSV::Row (inheriting the table's headers()),
- # or a CSV::Row.
- #
- # This method returns the table for chaining.
- #
- def <<(row_or_array)
- if row_or_array.is_a? Array # append Array
- @table << Row.new(headers, row_or_array)
- else # append Row
- @table << row_or_array
- end
-
- self # for chaining
- end
-
- #
- # A shortcut for appending multiple rows. Equivalent to:
- #
- # rows.each { |row| self << row }
- #
- # This method returns the table for chaining.
- #
- def push(*rows)
- rows.each { |row| self << row }
-
- self # for chaining
- end
-
- #
- # Removes and returns the indicated column or row. In the default mixed
- # mode indices refer to rows and everything else is assumed to be a column
- # header. Use by_col!() or by_row!() to force the lookup.
- #
- def delete(index_or_header)
- if @mode == :row or # by index
- (@mode == :col_or_row and index_or_header.is_a? Integer)
- @table.delete_at(index_or_header)
- else # by header
- @table.map { |row| row.delete(index_or_header).last }
- end
- end
-
- #
- # Removes any column or row for which the block returns +true+. In the
- # default mixed mode or row mode, iteration is the standard row major
- # walking of rows. In column mode, interation will +yield+ two element
- # tuples containing the column name and an Array of values for that column.
- #
- # This method returns the table for chaining.
- #
- def delete_if(&block)
- if @mode == :row or @mode == :col_or_row # by index
- @table.delete_if(&block)
- else # by header
- to_delete = Array.new
- headers.each_with_index do |header, i|
- to_delete << header if block[[header, self[header]]]
- end
- to_delete.map { |header| delete(header) }
- end
-
- self # for chaining
- end
-
- include Enumerable
-
- #
- # In the default mixed mode or row mode, iteration is the standard row major
- # walking of rows. In column mode, interation will +yield+ two element
- # tuples containing the column name and an Array of values for that column.
- #
- # This method returns the table for chaining.
- #
- def each(&block)
- if @mode == :col
- headers.each { |header| block[[header, self[header]]] }
- else
- @table.each(&block)
- end
-
- self # for chaining
- end
-
- # Returns +true+ if all rows of this table ==() +other+'s rows.
- def ==(other)
- @table == other.table
- end
-
- #
- # Returns the table as an Array of Arrays. Headers will be the first row,
- # then all of the field rows will follow.
- #
- def to_a
- @table.inject([headers]) do |array, row|
- if row.header_row?
- array
- else
- array + [row.fields]
- end
- end
- end
-
- #
- # Returns the table as a complete CSV String. Headers will be listed first,
- # then all of the field rows.
- #
- def to_csv(options = Hash.new)
- @table.inject([headers.to_csv(options)]) do |rows, row|
- if row.header_row?
- rows
- else
- rows + [row.fields.to_csv(options)]
- end
- end.join
- end
- alias_method :to_s, :to_csv
-
- # Shows the mode and size of this table in a US-ASCII String.
- def inspect
- "#<#{self.class} mode:#{@mode} row_count:#{to_a.size}>".encode("US-ASCII")
- end
- end
-
- # The error thrown when the parser encounters illegal CSV formatting.
- class MalformedCSVError < RuntimeError; end
-
- #
- # A FieldInfo Struct contains details about a field's position in the data
- # source it was read from. CSV will pass this Struct to some blocks that make
- # decisions based on field structure. See CSV.convert_fields() for an
- # example.
- #
- # <b><tt>index</tt></b>:: The zero-based index of the field in its row.
- # <b><tt>line</tt></b>:: The line of the data source this row is from.
- # <b><tt>header</tt></b>:: The header for the column, when available.
- #
- FieldInfo = Struct.new(:index, :line, :header)
-
- # A Regexp used to find and convert some common Date formats.
- DateMatcher = / \A(?: (\w+,?\s+)?\w+\s+\d{1,2},?\s+\d{2,4} |
- \d{4}-\d{2}-\d{2} )\z /x
- # A Regexp used to find and convert some common DateTime formats.
- DateTimeMatcher =
- / \A(?: (\w+,?\s+)?\w+\s+\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2},?\s+\d{2,4} |
- \d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2} )\z /x
-
- # The encoding used by all converters.
- ConverterEncoding = Encoding.find("UTF-8")
-
- #
- # This Hash holds the built-in converters of CSV that can be accessed by name.
- # You can select Converters with CSV.convert() or through the +options+ Hash
- # passed to CSV::new().
- #
- # <b><tt>:integer</tt></b>:: Converts any field Integer() accepts.
- # <b><tt>:float</tt></b>:: Converts any field Float() accepts.
- # <b><tt>:numeric</tt></b>:: A combination of <tt>:integer</tt>
- # and <tt>:float</tt>.
- # <b><tt>:date</tt></b>:: Converts any field Date::parse() accepts.
- # <b><tt>:date_time</tt></b>:: Converts any field DateTime::parse() accepts.
- # <b><tt>:all</tt></b>:: All built-in converters. A combination of
- # <tt>:date_time</tt> and <tt>:numeric</tt>.
- #
- # All built-in converters transcode field data to UTF-8 before attempting a
- # conversion. If your data cannot be transcoded to UTF-8 the conversion will
- # fail and the field will remain unchanged.
- #
- # This Hash is intentionally left unfrozen and users should feel free to add
- # values to it that can be accessed by all CSV objects.
- #
- # To add a combo field, the value should be an Array of names. Combo fields
- # can be nested with other combo fields.
- #
- Converters = { integer: lambda { |f|
- Integer(f.encode(ConverterEncoding)) rescue f
- },
- float: lambda { |f|
- Float(f.encode(ConverterEncoding)) rescue f
- },
- numeric: [:integer, :float],
- date: lambda { |f|
- begin
- e = f.encode(ConverterEncoding)
- e =~ DateMatcher ? Date.parse(e) : f
- rescue # encoding conversion or date parse errors
- f
- end
- },
- date_time: lambda { |f|
- begin
- e = f.encode(ConverterEncoding)
- e =~ DateTimeMatcher ? DateTime.parse(e) : f
- rescue # encoding conversion or date parse errors
- f
- end
- },
- all: [:date_time, :numeric] }
-
- #
- # This Hash holds the built-in header converters of CSV that can be accessed
- # by name. You can select HeaderConverters with CSV.header_convert() or
- # through the +options+ Hash passed to CSV::new().
- #
- # <b><tt>:downcase</tt></b>:: Calls downcase() on the header String.
- # <b><tt>:symbol</tt></b>:: The header String is downcased, spaces are
- # replaced with underscores, non-word characters
- # are dropped, and finally to_sym() is called.
- #
- # All built-in header converters transcode header data to UTF-8 before
- # attempting a conversion. If your data cannot be transcoded to UTF-8 the
- # conversion will fail and the header will remain unchanged.
- #
- # This Hash is intetionally left unfrozen and users should feel free to add
- # values to it that can be accessed by all CSV objects.
- #
- # To add a combo field, the value should be an Array of names. Combo fields
- # can be nested with other combo fields.
- #
- HeaderConverters = {
- downcase: lambda { |h| h.encode(ConverterEncoding).downcase },
- symbol: lambda { |h|
- h.encode(ConverterEncoding).downcase.gsub(/\s+/, "_").
- gsub(/\W+/, "").to_sym
- }
- }
-
- #
- # The options used when no overrides are given by calling code. They are:
- #
- # <b><tt>:col_sep</tt></b>:: <tt>","</tt>
- # <b><tt>:row_sep</tt></b>:: <tt>:auto</tt>
- # <b><tt>:quote_char</tt></b>:: <tt>'"'</tt>
- # <b><tt>:field_size_limit</tt></b>:: +nil+
- # <b><tt>:converters</tt></b>:: +nil+
- # <b><tt>:unconverted_fields</tt></b>:: +nil+
- # <b><tt>:headers</tt></b>:: +false+
- # <b><tt>:return_headers</tt></b>:: +false+
- # <b><tt>:header_converters</tt></b>:: +nil+
- # <b><tt>:skip_blanks</tt></b>:: +false+
- # <b><tt>:force_quotes</tt></b>:: +false+
- #
- DEFAULT_OPTIONS = { col_sep: ",",
- row_sep: :auto,
- quote_char: '"',
- field_size_limit: nil,
- converters: nil,
- unconverted_fields: nil,
- headers: false,
- return_headers: false,
- header_converters: nil,
- skip_blanks: false,
- force_quotes: false }.freeze
-
- #
- # This method will return a CSV instance, just like CSV::new(), but the
- # instance will be cached and returned for all future calls to this method for
- # the same +data+ object (tested by Object#object_id()) with the same
- # +options+.
- #
- # If a block is given, the instance is passed to the block and the return
- # value becomes the return value of the block.
- #
- def self.instance(data = $stdout, options = Hash.new)
- # create a _signature_ for this method call, data object and options
- sig = [data.object_id] +
- options.values_at(*DEFAULT_OPTIONS.keys.sort_by { |sym| sym.to_s })
-
- # fetch or create the instance for this signature
- @@instances ||= Hash.new
- instance = (@@instances[sig] ||= new(data, options))
-
- if block_given?
- yield instance # run block, if given, returning result
- else
- instance # or return the instance
- end
- end
-
- #
- # This method allows you to serialize an Array of Ruby objects to a String or
- # File of CSV data. This is not as powerful as Marshal or YAML, but perhaps
- # useful for spreadsheet and database interaction.
- #
- # Out of the box, this method is intended to work with simple data objects or
- # Structs. It will serialize a list of instance variables and/or
- # Struct.members().
- #
- # If you need need more complicated serialization, you can control the process
- # by adding methods to the class to be serialized.
- #
- # A class method csv_meta() is responsible for returning the first row of the
- # document (as an Array). This row is considered to be a Hash of the form
- # key_1,value_1,key_2,value_2,... CSV::load() expects to find a class key
- # with a value of the stringified class name and CSV::dump() will create this,
- # if you do not define this method. This method is only called on the first
- # object of the Array.
- #
- # The next method you can provide is an instance method called csv_headers().
- # This method is expected to return the second line of the document (again as
- # an Array), which is to be used to give each column a header. By default,
- # CSV::load() will set an instance variable if the field header starts with an
- # @ character or call send() passing the header as the method name and
- # the field value as an argument. This method is only called on the first
- # object of the Array.
- #
- # Finally, you can provide an instance method called csv_dump(), which will
- # be passed the headers. This should return an Array of fields that can be
- # serialized for this object. This method is called once for every object in
- # the Array.
- #
- # The +io+ parameter can be used to serialize to a File, and +options+ can be
- # anything CSV::new() accepts.
- #
- def self.dump(ary_of_objs, io = "", options = Hash.new)
- obj_template = ary_of_objs.first
-
- csv = new(io, options)
-
- # write meta information
- begin
- csv << obj_template.class.csv_meta
- rescue NoMethodError
- csv << [:class, obj_template.class]
- end
-
- # write headers
- begin
- headers = obj_template.csv_headers
- rescue NoMethodError
- headers = obj_template.instance_variables.sort
- if obj_template.class.ancestors.find { |cls| cls.to_s =~ /\AStruct\b/ }
- headers += obj_template.members.map { |mem| "#{mem}=" }.sort
- end
- end
- csv << headers
-
- # serialize each object
- ary_of_objs.each do |obj|
- begin
- csv << obj.csv_dump(headers)
- rescue NoMethodError
- csv << headers.map do |var|
- if var[0] == ?@
- obj.instance_variable_get(var)
- else
- obj[var[0..-2]]
- end
- end
- end
- end
-
- if io.is_a? String
- csv.string
- else
- csv.close
- end
- end
-
- #
- # This method is the reading counterpart to CSV::dump(). See that method for
- # a detailed description of the process.
- #
- # You can customize loading by adding a class method called csv_load() which
- # will be passed a Hash of meta information, an Array of headers, and an Array
- # of fields for the object the method is expected to return.
- #
- # Remember that all fields will be Strings after this load. If you need
- # something else, use +options+ to setup converters or provide a custom
- # csv_load() implementation.
- #
- def self.load(io_or_str, options = Hash.new)
- csv = new(io_or_str, options)
-
- # load meta information
- meta = Hash[*csv.shift]
- cls = meta["class".encode(csv.encoding)].split("::".encode(csv.encoding)).
- inject(Object) do |c, const|
- c.const_get(const)
- end
-
- # load headers
- headers = csv.shift
-
- # unserialize each object stored in the file
- results = csv.inject(Array.new) do |all, row|
- begin
- obj = cls.csv_load(meta, headers, row)
- rescue NoMethodError
- obj = cls.allocate
- headers.zip(row) do |name, value|
- if name[0] == ?@
- obj.instance_variable_set(name, value)
- else
- obj.send(name, value)
- end
- end
- end
- all << obj
- end
-
- csv.close unless io_or_str.is_a? String
-
- results
- end
-
- #
- # :call-seq:
- # filter( options = Hash.new ) { |row| ... }
- # filter( input, options = Hash.new ) { |row| ... }
- # filter( input, output, options = Hash.new ) { |row| ... }
- #
- # This method is a convenience for building Unix-like filters for CSV data.
- # Each row is yielded to the provided block which can alter it as needed.
- # After the block returns, the row is appended to +output+ altered or not.
- #
- # The +input+ and +output+ arguments can be anything CSV::new() accepts
- # (generally String or IO objects). If not given, they default to
- # <tt>ARGF</tt> and <tt>$stdout</tt>.
- #
- # The +options+ parameter is also filtered down to CSV::new() after some
- # clever key parsing. Any key beginning with <tt>:in_</tt> or
- # <tt>:input_</tt> will have that leading identifier stripped and will only
- # be used in the +options+ Hash for the +input+ object. Keys starting with
- # <tt>:out_</tt> or <tt>:output_</tt> affect only +output+. All other keys
- # are assigned to both objects.
- #
- # The <tt>:output_row_sep</tt> +option+ defaults to
- # <tt>$INPUT_RECORD_SEPARATOR</tt> (<tt>$/</tt>).
- #
- def self.filter(*args)
- # parse options for input, output, or both
- in_options, out_options = Hash.new, {row_sep: $INPUT_RECORD_SEPARATOR}
- if args.last.is_a? Hash
- args.pop.each do |key, value|
- case key.to_s
- when /\Ain(?:put)?_(.+)\Z/
- in_options[$1.to_sym] = value
- when /\Aout(?:put)?_(.+)\Z/
- out_options[$1.to_sym] = value
- else
- in_options[key] = value
- out_options[key] = value
- end
- end
- end
- # build input and output wrappers
- input = new(args.shift || ARGF, in_options)
- output = new(args.shift || $stdout, out_options)
-
- # read, yield, write
- input.each do |row|
- yield row
- output << row
- end
- end
-
- #
- # This method is intended as the primary interface for reading CSV files. You
- # pass a +path+ and any +options+ you wish to set for the read. Each row of
- # file will be passed to the provided +block+ in turn.
- #
- # The +options+ parameter can be anything CSV::new() understands. This method
- # also understands an additional <tt>:encoding</tt> parameter that you can use
- # to specify the Encoding of the data in the file to be read. You must provide
- # this unless your data is in Encoding::default_external(). CSV will use this
- # to deterime how to parse the data. You may provide a second Encoding to
- # have the data transcoded as it is read. For example,
- # <tt>encoding: "UTF-32BE:UTF-8"</tt> would read UTF-32BE data from the file
- # but transcode it to UTF-8 before CSV parses it.
- #
- def self.foreach(path, options = Hash.new, &block)
- encoding = options.delete(:encoding)
- mode = "rb"
- mode << ":#{encoding}" if encoding
- open(path, mode, options) do |csv|
- csv.each(&block)
- end
- end
-
- #
- # :call-seq:
- # generate( str, options = Hash.new ) { |csv| ... }
- # generate( options = Hash.new ) { |csv| ... }
- #
- # This method wraps a String you provide, or an empty default String, in a
- # CSV object which is passed to the provided block. You can use the block to
- # append CSV rows to the String and when the block exits, the final String
- # will be returned.
- #
- # Note that a passed String *is* modfied by this method. Call dup() before
- # passing if you need a new String.
- #
- # The +options+ parameter can be anthing CSV::new() understands. This method
- # understands an additional <tt>:encoding</tt> parameter when not passed a
- # String to set the base Encoding for the output. CSV needs this hint if you
- # plan to output non-ASCII compatible data.
- #
- def self.generate(*args)
- # add a default empty String, if none was given
- if args.first.is_a? String
- io = StringIO.new(args.shift)
- io.seek(0, IO::SEEK_END)
- args.unshift(io)
- else
- encoding = args.last.is_a?(Hash) ? args.last.delete(:encoding) : nil
- str = ""
- str.encode!(encoding) if encoding
- args.unshift(str)
- end
- csv = new(*args) # wrap
- yield csv # yield for appending
- csv.string # return final String
- end
-
- #
- # This method is a shortcut for converting a single row (Array) into a CSV
- # String.
- #
- # The +options+ parameter can be anthing CSV::new() understands. This method
- # understands an additional <tt>:encoding</tt> parameter to set the base
- # Encoding for the output. This method will try to guess your Encoding from
- # the first non-+nil+ field in +row+, if possible, but you may need to use
- # this parameter as a backup plan.
- #
- # The <tt>:row_sep</tt> +option+ defaults to <tt>$INPUT_RECORD_SEPARATOR</tt>
- # (<tt>$/</tt>) when calling this method.
- #
- def self.generate_line(row, options = Hash.new)
- options = {row_sep: $INPUT_RECORD_SEPARATOR}.merge(options)
- encoding = options.delete(:encoding)
- str = ""
- if encoding
- str.force_encoding(encoding)
- elsif field = row.find { |f| not f.nil? }
- str.force_encoding(String(field).encoding)
- end
- (new(str, options) << row).string
- end
-
- #
- # :call-seq:
- # open( filename, mode = "rb", options = Hash.new ) { |faster_csv| ... }
- # open( filename, options = Hash.new ) { |faster_csv| ... }
- # open( filename, mode = "rb", options = Hash.new )
- # open( filename, options = Hash.new )
- #
- # This method opens an IO object, and wraps that with CSV. This is intended
- # as the primary interface for writing a CSV file.
- #
- # You must pass a +filename+ and may optionally add a +mode+ for Ruby's
- # open(). You may also pass an optional Hash containing any +options+
- # CSV::new() understands as the final argument.
- #
- # This method works like Ruby's open() call, in that it will pass a CSV object
- # to a provided block and close it when the block terminates, or it will
- # return the CSV object when no block is provided. (*Note*: This is different
- # from the Ruby 1.8 CSV library which passed rows to the block. Use
- # CSV::foreach() for that behavior.)
- #
- # You must provide a +mode+ with an embedded Encoding designator unless your
- # data is in Encoding::default_external(). CSV will check the Encoding of the
- # underlying IO object (set by the +mode+ you pass) to deterime how to parse
- # the data. You may provide a second Encoding to have the data transcoded as
- # it is read just as you can with a normal call to IO::open(). For example,
- # <tt>"rb:UTF-32BE:UTF-8"</tt> would read UTF-32BE data from the file but
- # transcode it to UTF-8 before CSV parses it.
- #
- # An opened CSV object will delegate to many IO methods for convenience. You
- # may call:
- #
- # * binmode()
- # * binmode?()
- # * close()
- # * close_read()
- # * close_write()
- # * closed?()
- # * eof()
- # * eof?()
- # * external_encoding()
- # * fcntl()
- # * fileno()
- # * flock()
- # * flush()
- # * fsync()
- # * internal_encoding()
- # * ioctl()
- # * isatty()
- # * path()
- # * pid()
- # * pos()
- # * pos=()
- # * reopen()
- # * seek()
- # * stat()
- # * sync()
- # * sync=()
- # * tell()
- # * to_i()
- # * to_io()
- # * truncate()
- # * tty?()
- #
- def self.open(*args)
- # find the +options+ Hash
- options = if args.last.is_a? Hash then args.pop else Hash.new end
- # default to a binary open mode
- args << "rb" if args.size == 1
- # wrap a File opened with the remaining +args+
- csv = new(File.open(*args), options)
-
- # handle blocks like Ruby's open(), not like the CSV library
- if block_given?
- begin
- yield csv
- ensure
- csv.close
- end
- else
- csv
- end
- end
-
- #
- # :call-seq:
- # parse( str, options = Hash.new ) { |row| ... }
- # parse( str, options = Hash.new )
- #
- # This method can be used to easily parse CSV out of a String. You may either
- # provide a +block+ which will be called with each row of the String in turn,
- # or just use the returned Array of Arrays (when no +block+ is given).
- #
- # You pass your +str+ to read from, and an optional +options+ Hash containing
- # anything CSV::new() understands.
- #
- def self.parse(*args, &block)
- csv = new(*args)
- if block.nil? # slurp contents, if no block is given
- begin
- csv.read
- ensure
- csv.close
- end
- else # or pass each row to a provided block
- csv.each(&block)
- end
- end
-
- #
- # This method is a shortcut for converting a single line of a CSV String into
- # a into an Array. Note that if +line+ contains multiple rows, anything
- # beyond the first row is ignored.
- #
- # The +options+ parameter can be anthing CSV::new() understands.
- #
- def self.parse_line(line, options = Hash.new)
- new(line, options).shift
- end
-
- #
- # Use to slurp a CSV file into an Array of Arrays. Pass the +path+ to the
- # file and any +options+ CSV::new() understands. This method also understands
- # an additional <tt>:encoding</tt> parameter that you can use to specify the
- # Encoding of the data in the file to be read. You must provide this unless
- # your data is in Encoding::default_external(). CSV will use this to deterime
- # how to parse the data. You may provide a second Encoding to have the data
- # transcoded as it is read. For example,
- # <tt>encoding: "UTF-32BE:UTF-8"</tt> would read UTF-32BE data from the file
- # but transcode it to UTF-8 before CSV parses it.
- #
- def self.read(path, options = Hash.new)
- encoding = options.delete(:encoding)
- mode = "rb"
- mode << ":#{encoding}" if encoding
- open(path, mode, options) { |csv| csv.read }
- end
-
- # Alias for CSV::read().
- def self.readlines(*args)
- read(*args)
- end
-
- #
- # A shortcut for:
- #
- # CSV.read( path, { headers: true,
- # converters: :numeric,
- # header_converters: :symbol }.merge(options) )
- #
- def self.table(path, options = Hash.new)
- read( path, { headers: true,
- converters: :numeric,
- header_converters: :symbol }.merge(options) )
- end
-
- #
- # This constructor will wrap either a String or IO object passed in +data+ for
- # reading and/or writing. In addition to the CSV instance methods, several IO
- # methods are delegated. (See CSV::open() for a complete list.) If you pass
- # a String for +data+, you can later retrieve it (after writing to it, for
- # example) with CSV.string().
- #
- # Note that a wrapped String will be positioned at at the beginning (for
- # reading). If you want it at the end (for writing), use CSV::generate().
- # If you want any other positioning, pass a preset StringIO object instead.
- #
- # You may set any reading and/or writing preferences in the +options+ Hash.
- # Available options are:
- #
- # <b><tt>:col_sep</tt></b>:: The String placed between each field.
- # This String will be transcoded into
- # the data's Encoding before parsing.
- # <b><tt>:row_sep</tt></b>:: The String appended to the end of each
- # row. This can be set to the special
- # <tt>:auto</tt> setting, which requests
- # that CSV automatically discover this
- # from the data. Auto-discovery reads
- # ahead in the data looking for the next
- # <tt>"\r\n"</tt>, <tt>"\n"</tt>, or
- # <tt>"\r"</tt> sequence. A sequence
- # will be selected even if it occurs in
- # a quoted field, assuming that you
- # would have the same line endings
- # there. If none of those sequences is
- # found, +data+ is <tt>ARGF</tt>,
- # <tt>STDIN</tt>, <tt>STDOUT</tt>, or
- # <tt>STDERR</tt>, or the stream is only
- # available for output, the default
- # <tt>$INPUT_RECORD_SEPARATOR</tt>
- # (<tt>$/</tt>) is used. Obviously,
- # discovery takes a little time. Set
- # manually if speed is important. Also
- # note that IO objects should be opened
- # in binary mode on Windows if this
- # feature will be used as the
- # line-ending translation can cause
- # problems with resetting the document
- # position to where it was before the
- # read ahead. This String will be
- # transcoded into the data's Encoding
- # before parsing.
- # <b><tt>:quote_char</tt></b>:: The character used to quote fields.
- # This has to be a single character
- # String. This is useful for
- # application that incorrectly use
- # <tt>'</tt> as the quote character
- # instead of the correct <tt>"</tt>.
- # CSV will always consider a double
- # sequence this character to be an
- # escaped quote. This String will be
- # transcoded into the data's Encoding
- # before parsing.
- # <b><tt>:field_size_limit</tt></b>:: This is a maximum size CSV will read
- # ahead looking for the closing quote
- # for a field. (In truth, it reads to
- # the first line ending beyond this
- # size.) If a quote cannot be found
- # within the limit CSV will raise a
- # MalformedCSVError, assuming the data
- # is faulty. You can use this limit to
- # prevent what are effectively DoS
- # attacks on the parser. However, this
- # limit can cause a legitimate parse to
- # fail and thus is set to +nil+, or off,
- # by default.
- # <b><tt>:converters</tt></b>:: An Array of names from the Converters
- # Hash and/or lambdas that handle custom
- # conversion. A single converter
- # doesn't have to be in an Array. All
- # built-in converters try to transcode
- # fields to UTF-8 before converting.
- # The conversion will fail if the data
- # cannot be transcoded, leaving the
- # field unchanged.
- # <b><tt>:unconverted_fields</tt></b>:: If set to +true+, an
- # unconverted_fields() method will be
- # added to all returned rows (Array or
- # CSV::Row) that will return the fields
- # as they were before conversion. Note
- # that <tt>:headers</tt> supplied by
- # Array or String were not fields of the
- # document and thus will have an empty
- # Array attached.
- # <b><tt>:headers</tt></b>:: If set to <tt>:first_row</tt> or
- # +true+, the initial row of the CSV
- # file will be treated as a row of
- # headers. If set to an Array, the
- # contents will be used as the headers.
- # If set to a String, the String is run
- # through a call of CSV::parse_line()
- # with the same <tt>:col_sep</tt>,
- # <tt>:row_sep</tt>, and
- # <tt>:quote_char</tt> as this instance
- # to produce an Array of headers. This
- # setting causes CSV#shift() to return
- # rows as CSV::Row objects instead of
- # Arrays and CSV#read() to return
- # CSV::Table objects instead of an Array
- # of Arrays.
- # <b><tt>:return_headers</tt></b>:: When +false+, header rows are silently
- # swallowed. If set to +true+, header
- # rows are returned in a CSV::Row object
- # with identical headers and
- # fields (save that the fields do not go
- # through the converters).
- # <b><tt>:write_headers</tt></b>:: When +true+ and <tt>:headers</tt> is
- # set, a header row will be added to the
- # output.
- # <b><tt>:header_converters</tt></b>:: Identical in functionality to
- # <tt>:converters</tt> save that the
- # conversions are only made to header
- # rows. All built-in converters try to
- # transcode headers to UTF-8 before
- # converting. The conversion will fail
- # if the data cannot be transcoded,
- # leaving the header unchanged.
- # <b><tt>:skip_blanks</tt></b>:: When set to a +true+ value, CSV will
- # skip over any rows with no content.
- # <b><tt>:force_quotes</tt></b>:: When set to a +true+ value, CSV will
- # quote all CSV fields it creates.
- #
- # See CSV::DEFAULT_OPTIONS for the default settings.
- #
- # Options cannot be overriden in the instance methods for performance reasons,
- # so be sure to set what you want here.
- #
- def initialize(data, options = Hash.new)
- # build the options for this read/write
- options = DEFAULT_OPTIONS.merge(options)
-
- # create the IO object we will read from
- @io = if data.is_a? String then StringIO.new(data) else data end
- # honor the IO encoding if we can, otherwise default to ASCII-8BIT
- @encoding = if @io.respond_to? :internal_encoding
- @io.internal_encoding || @io.external_encoding
- elsif @io.is_a? StringIO
- @io.string.encoding
- end
- @encoding ||= Encoding.default_internal || Encoding.default_external
- #
- # prepare for building safe regular expressions in the target encoding,
- # if we can transcode the needed characters
- #
- @re_esc = "\\".encode(@encoding) rescue ""
- @re_chars = %w[ \\ . [ ] - ^ $ ?
- * + { } ( ) | #
- \ \r \n \t \f \v ].
- map { |s| s.encode(@encoding) rescue nil }.compact
-
- init_separators(options)
- init_parsers(options)
- init_converters(options)
- init_headers(options)
-
- unless options.empty?
- raise ArgumentError, "Unknown options: #{options.keys.join(', ')}."
- end
-
- # track our own lineno since IO gets confused about line-ends is CSV fields
- @lineno = 0
- end
-
- #
- # The encoded <tt>:col_sep</tt> used in parsing and writing. See CSV::new
- # for details.
- #
- attr_reader :col_sep
- #
- # The encoded <tt>:row_sep</tt> used in parsing and writing. See CSV::new
- # for details.
- #
- attr_reader :row_sep
- #
- # The encoded <tt>:quote_char</tt> used in parsing and writing. See CSV::new
- # for details.
- #
- attr_reader :quote_char
- # The limit for field size, if any. See CSV::new for details.
- attr_reader :field_size_limit
- #
- # Returns the current list of converters in effect. See CSV::new for details.
- # Built-in converters will be returned by name, while others will be returned
- # as is.
- #
- def converters
- @converters.map do |converter|
- name = Converters.rassoc(converter)
- name ? name.first : converter
- end
- end
- #
- # Returns +true+ if unconverted_fields() to parsed results. See CSV::new
- # for details.
- #
- def unconverted_fields?() @unconverted_fields end
- #
- # Returns +nil+ if headers will not be used, +true+ if they will but have not
- # yet been read, or the actual headers after they have been read. See
- # CSV::new for details.
- #
- def headers
- @headers || true if @use_headers
- end
- #
- # Returns +true+ if headers will be returned as a row of results.
- # See CSV::new for details.
- #
- def return_headers?() @return_headers end
- # Returns +true+ if headers are written in output. See CSV::new for details.
- def write_headers?() @write_headers end
- #
- # Returns the current list of converters in effect for headers. See CSV::new
- # for details. Built-in converters will be returned by name, while others
- # will be returned as is.
- #
- def header_converters
- @header_converters.map do |converter|
- name = HeaderConverters.rassoc(converter)
- name ? name.first : converter
- end
- end
- #
- # Returns +true+ blank lines are skipped by the parser. See CSV::new
- # for details.
- #
- def skip_blanks?() @skip_blanks end
- # Returns +true+ if all output fields are quoted. See CSV::new for details.
- def force_quotes?() @force_quotes end
-
- #
- # The Encoding CSV is parsing or writing in. This will be the Encoding you
- # receive parsed data in and/or the Encoding data will be written in.
- #
- attr_reader :encoding
-
- #
- # The line number of the last row read from this file. Fields with nested
- # line-end characters will not affect this count.
- #
- attr_reader :lineno
-
- ### IO and StringIO Delegation ###
-
- extend Forwardable
- def_delegators :@io, :binmode, :binmode?, :close, :close_read, :close_write,
- :closed?, :eof, :eof?, :external_encoding, :fcntl,
- :fileno, :flock, :flush, :fsync, :internal_encoding,
- :ioctl, :isatty, :path, :pid, :pos, :pos=, :reopen,
- :seek, :stat, :string, :sync, :sync=, :tell, :to_i,
- :to_io, :truncate, :tty?
-
- # Rewinds the underlying IO object and resets CSV's lineno() counter.
- def rewind
- @headers = nil
- @lineno = 0
-
- @io.rewind
- end
-
- ### End Delegation ###
-
- #
- # The primary write method for wrapped Strings and IOs, +row+ (an Array or
- # CSV::Row) is converted to CSV and appended to the data source. When a
- # CSV::Row is passed, only the row's fields() are appended to the output.
- #
- # The data source must be open for writing.
- #
- def <<(row)
- # make sure headers have been assigned
- if header_row? and [Array, String].include? @use_headers.class
- parse_headers # won't read data for Array or String
- self << @headers if @write_headers
- end
-
- # handle CSV::Row objects and Hashes
- row = case row
- when self.class::Row then row.fields
- when Hash then @headers.map { |header| row[header] }
- else row
- end
-
- @headers = row if header_row?
- @lineno += 1
-
- @io << row.map(&@quote).join(@col_sep) + @row_sep # quote and separate
-
- self # for chaining
- end
- alias_method :add_row, :<<
- alias_method :puts, :<<
-
- #
- # :call-seq:
- # convert( name )
- # convert { |field| ... }
- # convert { |field, field_info| ... }
- #
- # You can use this method to install a CSV::Converters built-in, or provide a
- # block that handles a custom conversion.
- #
- # If you provide a block that takes one argument, it will be passed the field
- # and is expected to return the converted value or the field itself. If your
- # block takes two arguments, it will also be passed a CSV::FieldInfo Struct,
- # containing details about the field. Again, the block should return a
- # converted field or the field itself.
- #
- def convert(name = nil, &converter)
- add_converter(:converters, self.class::Converters, name, &converter)
- end
-
- #
- # :call-seq:
- # header_convert( name )
- # header_convert { |field| ... }
- # header_convert { |field, field_info| ... }
- #
- # Identical to CSV#convert(), but for header rows.
- #
- # Note that this method must be called before header rows are read to have any
- # effect.
- #
- def header_convert(name = nil, &converter)
- add_converter( :header_converters,
- self.class::HeaderConverters,
- name,
- &converter )
- end
-
- include Enumerable
-
- #
- # Yields each row of the data source in turn.
- #
- # Support for Enumerable.
- #
- # The data source must be open for reading.
- #
- def each
- while row = shift
- yield row
- end
- end
-
- #
- # Slurps the remaining rows and returns an Array of Arrays.
- #
- # The data source must be open for reading.
- #
- def read
- rows = to_a
- if @use_headers
- Table.new(rows)
- else
- rows
- end
- end
- alias_method :readlines, :read
-
- # Returns +true+ if the next row read will be a header row.
- def header_row?
- @use_headers and @headers.nil?
- end
-
- #
- # The primary read method for wrapped Strings and IOs, a single row is pulled
- # from the data source, parsed and returned as an Array of fields (if header
- # rows are not used) or a CSV::Row (when header rows are used).
- #
- # The data source must be open for reading.
- #
- def shift
- #########################################################################
- ### This method is purposefully kept a bit long as simple conditional ###
- ### checks are faster than numerous (expensive) method calls. ###
- #########################################################################
-
- # handle headers not based on document content
- if header_row? and @return_headers and
- [Array, String].include? @use_headers.class
- if @unconverted_fields
- return add_unconverted_fields(parse_headers, Array.new)
- else
- return parse_headers
- end
- end
-
- # begin with a blank line, so we can always add to it
- line = ""
-
- #
- # it can take multiple calls to <tt>@io.gets()</tt> to get a full line,
- # because of \r and/or \n characters embedded in quoted fields
- #
- loop do
- # add another read to the line
- (line += @io.gets(@row_sep)) rescue return nil
- # copy the line so we can chop it up in parsing
- parse = line.dup
- parse.sub!(@parsers[:line_end], "")
-
- #
- # I believe a blank line should be an <tt>Array.new</tt>, not Ruby 1.8
- # CSV's <tt>[nil]</tt>
- #
- if parse.empty?
- @lineno += 1
- if @skip_blanks
- line = ""
- next
- elsif @unconverted_fields
- return add_unconverted_fields(Array.new, Array.new)
- elsif @use_headers
- return self.class::Row.new(Array.new, Array.new)
- else
- return Array.new
- end
- end
-
- #
- # shave leading empty fields if needed, because the main parser chokes
- # on these
- #
- csv = if parse.sub!(@parsers[:leading_fields], "")
- [nil] * ($&.length / @col_sep.length)
- else
- Array.new
- end
- #
- # then parse the main fields with a hyper-tuned Regexp from
- # Mastering Regular Expressions, Second Edition
- #
- parse.gsub!(@parsers[:csv_row]) do
- csv << if $1.nil? # we found an unquoted field
- if $2.empty? # switch empty unquoted fields to +nil+...
- nil # for Ruby 1.8 CSV compatibility
- else
- # I decided to take a strict approach to CSV parsing...
- if $2.count(@parsers[:return_newline]).zero? # verify correctness
- $2
- else
- # or throw an Exception
- raise MalformedCSVError, "Unquoted fields do not allow " +
- "\\r or \\n (line #{lineno + 1})."
- end
- end
- else # we found a quoted field...
- $1.gsub(@quote_char * 2, @quote_char) # unescape contents
- end
- "" # gsub!'s replacement, clear the field
- end
-
- # if parse is empty?(), we found all the fields on the line...
- if parse.empty?
- @lineno += 1
-
- # save fields unconverted fields, if needed...
- unconverted = csv.dup if @unconverted_fields
-
- # convert fields, if needed...
- csv = convert_fields(csv) unless @use_headers or @converters.empty?
- # parse out header rows and handle CSV::Row conversions...
- csv = parse_headers(csv) if @use_headers
-
- # inject unconverted fields and accessor, if requested...
- if @unconverted_fields and not csv.respond_to? :unconverted_fields
- add_unconverted_fields(csv, unconverted)
- end
-
- # return the results
- break csv
- end
- # if we're not empty?() but at eof?(), a quoted field wasn't closed...
- if @io.eof?
- raise MalformedCSVError, "Unclosed quoted field on line #{lineno + 1}."
- elsif parse =~ @parsers[:bad_field]
- raise MalformedCSVError, "Illegal quoting on line #{lineno + 1}."
- elsif @field_size_limit and parse.length >= @field_size_limit
- raise MalformedCSVError, "Field size exceeded on line #{lineno + 1}."
- end
- # otherwise, we need to loop and pull some more data to complete the row
- end
- end
- alias_method :gets, :shift
- alias_method :readline, :shift
-
- #
- # Returns a simplified description of the key FasterCSV attributes in an
- # ASCII compatible String.
- #
- def inspect
- str = ["<#", self.class.to_s, " io_type:"]
- # show type of wrapped IO
- if @io == $stdout then str << "$stdout"
- elsif @io == $stdin then str << "$stdin"
- elsif @io == $stderr then str << "$stderr"
- else str << @io.class.to_s
- end
- # show IO.path(), if available
- if @io.respond_to?(:path) and (p = @io.path)
- str << " io_path:" << p.inspect
- end
- # show encoding
- str << " encoding:" << @encoding.name
- # show other attributes
- %w[ lineno col_sep row_sep
- quote_char skip_blanks ].each do |attr_name|
- if a = instance_variable_get("@#{attr_name}")
- str << " " << attr_name << ":" << a.inspect
- end
- end
- if @use_headers
- str << " headers:" << headers.inspect
- end
- str << ">"
- begin
- str.join
- rescue # any encoding error
- str.map do |s|
- e = Encoding::Converter.asciicompat_encoding(s.encoding)
- e ? s.encode(e) : s.force_encoding("ASCII-8BIT")
- end.join
- end
- end
-
- private
-
- #
- # Stores the indicated separators for later use.
- #
- # If auto-discovery was requested for <tt>@row_sep</tt>, this method will read
- # ahead in the <tt>@io</tt> and try to find one. +ARGF+, +STDIN+, +STDOUT+,
- # +STDERR+ and any stream open for output only with a default
- # <tt>@row_sep</tt> of <tt>$INPUT_RECORD_SEPARATOR</tt> (<tt>$/</tt>).
- #
- # This method also establishes the quoting rules used for CSV output.
- #
- def init_separators(options)
- # store the selected separators
- @col_sep = options.delete(:col_sep).to_s.encode(@encoding)
- @row_sep = options.delete(:row_sep) # encode after resolving :auto
- @quote_char = options.delete(:quote_char).to_s.encode(@encoding)
-
- if @quote_char.length != 1
- raise ArgumentError, ":quote_char has to be a single character String"
- end
-
- #
- # automatically discover row separator when requested
- # (not fully encoding safe)
- #
- if @row_sep == :auto
- if [ARGF, STDIN, STDOUT, STDERR].include?(@io) or
- (defined?(Zlib) and @io.class == Zlib::GzipWriter)
- @row_sep = $INPUT_RECORD_SEPARATOR
- else
- begin
- saved_pos = @io.pos # remember where we were
- while @row_sep == :auto
- #
- # if we run out of data, it's probably a single line
- # (use a sensible default)
- #
- if @io.eof?
- @row_sep = $INPUT_RECORD_SEPARATOR
- break
- end
-
- # read ahead a bit
- sample = read_to_char(1024)
- sample += read_to_char(1) if sample[-1..-1] == encode_str("\r") and
- not @io.eof?
-
- # try to find a standard separator
- if sample =~ encode_re("\r\n?|\n")
- @row_sep = $&
- break
- end
- end
- # tricky seek() clone to work around GzipReader's lack of seek()
- @io.rewind
- # reset back to the remembered position
- while saved_pos > 1024 # avoid loading a lot of data into memory
- @io.read(1024)
- saved_pos -= 1024
- end
- @io.read(saved_pos) if saved_pos.nonzero?
- rescue IOError # stream not opened for reading
- @row_sep = $INPUT_RECORD_SEPARATOR
- end
- end
- end
- @row_sep = @row_sep.to_s.encode(@encoding)
-
- # establish quoting rules
- @force_quotes = options.delete(:force_quotes)
- do_quote = lambda do |field|
- @quote_char +
- String(field).gsub(@quote_char, @quote_char * 2) +
- @quote_char
- end
- quotable_chars = encode_str("\r\n", @col_sep, @quote_char)
- @quote = if @force_quotes
- do_quote
- else
- lambda do |field|
- if field.nil? # represent +nil+ fields as empty unquoted fields
- ""
- else
- field = String(field) # Stringify fields
- # represent empty fields as empty quoted fields
- if field.empty? or
- field.count(quotable_chars).nonzero?
- do_quote.call(field)
- else
- field # unquoted field
- end
- end
- end
- end
- end
-
- # Pre-compiles parsers and stores them by name for access during reads.
- def init_parsers(options)
- # store the parser behaviors
- @skip_blanks = options.delete(:skip_blanks)
- @field_size_limit = options.delete(:field_size_limit)
-
- # prebuild Regexps for faster parsing
- esc_col_sep = escape_re(@col_sep)
- esc_row_sep = escape_re(@row_sep)
- esc_quote = escape_re(@quote_char)
- @parsers = {
- # for empty leading fields
- leading_fields: encode_re("\\A(?:", esc_col_sep, ")+"),
- # The Primary Parser
- csv_row: encode_re(
- "\\G(?:\\A|", esc_col_sep, ")", # anchor the match
- "(?:", esc_quote, # find quoted fields
- "((?>[^", esc_quote, "]*)", # "unrolling the loop"
- "(?>", esc_quote * 2, # double for escaping
- "[^", esc_quote, "]*)*)",
- esc_quote,
- "|", # ... or ...
- "([^", esc_quote, esc_col_sep, "]*))", # unquoted fields
- "(?=", esc_col_sep, "|\\z)" # ensure field is ended
- ),
- # a test for unescaped quotes
- bad_field: encode_re(
- "\\A", esc_col_sep, "?", # an optional comma
- "(?:", esc_quote, # a quoted field
- "(?>[^", esc_quote, "]*)", # "unrolling the loop"
- "(?>", esc_quote * 2, # double for escaping
- "[^", esc_quote, "]*)*",
- esc_quote, # the closing quote
- "[^", esc_quote, "]", # an extra character
- "|", # ... or ...
- "[^", esc_quote, esc_col_sep, "]+", # an unquoted field
- esc_quote, ")" # an extra quote
- ),
- # safer than chomp!()
- line_end: encode_re(esc_row_sep, "\\z"),
- # illegal unquoted characters
- return_newline: encode_str("\r\n")
- }
- end
-
- #
- # Loads any converters requested during construction.
- #
- # If +field_name+ is set <tt>:converters</tt> (the default) field converters
- # are set. When +field_name+ is <tt>:header_converters</tt> header converters
- # are added instead.
- #
- # The <tt>:unconverted_fields</tt> option is also actived for
- # <tt>:converters</tt> calls, if requested.
- #
- def init_converters(options, field_name = :converters)
- if field_name == :converters
- @unconverted_fields = options.delete(:unconverted_fields)
- end
-
- instance_variable_set("@#{field_name}", Array.new)
-
- # find the correct method to add the converters
- convert = method(field_name.to_s.sub(/ers\Z/, ""))
-
- # load converters
- unless options[field_name].nil?
- # allow a single converter not wrapped in an Array
- unless options[field_name].is_a? Array
- options[field_name] = [options[field_name]]
- end
- # load each converter...
- options[field_name].each do |converter|
- if converter.is_a? Proc # custom code block
- convert.call(&converter)
- else # by name
- convert.call(converter)
- end
- end
- end
-
- options.delete(field_name)
- end
-
- # Stores header row settings and loads header converters, if needed.
- def init_headers(options)
- @use_headers = options.delete(:headers)
- @return_headers = options.delete(:return_headers)
- @write_headers = options.delete(:write_headers)
-
- # headers must be delayed until shift(), in case they need a row of content
- @headers = nil
-
- init_converters(options, :header_converters)
- end
-
- #
- # The actual work method for adding converters, used by both CSV.convert() and
- # CSV.header_convert().
- #
- # This method requires the +var_name+ of the instance variable to place the
- # converters in, the +const+ Hash to lookup named converters in, and the
- # normal parameters of the CSV.convert() and CSV.header_convert() methods.
- #
- def add_converter(var_name, const, name = nil, &converter)
- if name.nil? # custom converter
- instance_variable_get("@#{var_name}") << converter
- else # named converter
- combo = const[name]
- case combo
- when Array # combo converter
- combo.each do |converter_name|
- add_converter(var_name, const, converter_name)
- end
- else # individual named converter
- instance_variable_get("@#{var_name}") << combo
- end
- end
- end
-
- #
- # Processes +fields+ with <tt>@converters</tt>, or <tt>@header_converters</tt>
- # if +headers+ is passed as +true+, returning the converted field set. Any
- # converter that changes the field into something other than a String halts
- # the pipeline of conversion for that field. This is primarily an efficiency
- # shortcut.
- #
- def convert_fields(fields, headers = false)
- # see if we are converting headers or fields
- converters = headers ? @header_converters : @converters
-
- fields.map.with_index do |field, index|
- converters.each do |converter|
- field = if converter.arity == 1 # straight field converter
- converter[field]
- else # FieldInfo converter
- header = @use_headers && !headers ? @headers[index] : nil
- converter[field, FieldInfo.new(index, lineno, header)]
- end
- break unless field.is_a? String # short-curcuit pipeline for speed
- end
- field # final state of each field, converted or original
- end
- end
-
- #
- # This methods is used to turn a finished +row+ into a CSV::Row. Header rows
- # are also dealt with here, either by returning a CSV::Row with identical
- # headers and fields (save that the fields do not go through the converters)
- # or by reading past them to return a field row. Headers are also saved in
- # <tt>@headers</tt> for use in future rows.
- #
- # When +nil+, +row+ is assumed to be a header row not based on an actual row
- # of the stream.
- #
- def parse_headers(row = nil)
- if @headers.nil? # header row
- @headers = case @use_headers # save headers
- # Array of headers
- when Array then @use_headers
- # CSV header String
- when String
- self.class.parse_line( @use_headers,
- col_sep: @col_sep,
- row_sep: @row_sep,
- quote_char: @quote_char )
- # first row is headers
- else row
- end
-
- # prepare converted and unconverted copies
- row = @headers if row.nil?
- @headers = convert_fields(@headers, true)
-
- if @return_headers # return headers
- return self.class::Row.new(@headers, row, true)
- elsif not [Array, String].include? @use_headers.class # skip to field row
- return shift
- end
- end
-
- self.class::Row.new(@headers, convert_fields(row)) # field row
- end
-
- #
- # Thiw methods injects an instance variable <tt>unconverted_fields</tt> into
- # +row+ and an accessor method for it called unconverted_fields(). The
- # variable is set to the contents of +fields+.
- #
- def add_unconverted_fields(row, fields)
- class << row
- attr_reader :unconverted_fields
- end
- row.instance_eval { @unconverted_fields = fields }
- row
- end
-
- #
- # This method is an encoding safe version of Regexp::escape(). I will escape
- # any characters that would change the meaning of a regular expression in the
- # encoding of +str+. Regular expression characters that cannot be transcoded
- # to the target encodign will be skipped and no escaping will be performed if
- # a backslash cannot be transcoded.
- #
- def escape_re(str)
- str.chars.map { |c| @re_chars.include?(c) ? @re_esc + c : c }.join
- end
-
- #
- # Builds a regular expression in <tt>@encoding</tt>. All +chunks+ will be
- # transcoded to that encoding.
- #
- def encode_re(*chunks)
- Regexp.new(encode_str(*chunks))
- end
-
- #
- # Builds a String in <tt>@encoding</tt>. All +chunks+ will be transcoded to
- # that encoding.
- #
- def encode_str(*chunks)
- chunks.map { |chunk| chunk.encode(@encoding.name) }.join
- end
-
- #
- # Reads at least +bytes+ from <tt>@io</tt>, but will read up 10 bytes ahead if
- # needed to ensure the data read is valid in the ecoding of that data. This
- # should ensure that it is safe to use regular expressions on the read data,
- # unless it is actually a broken encoding. The read data will be returned in
- # <tt>@encoding</tt>.
- #
- def read_to_char(bytes)
- return "" if @io.eof?
- data = @io.read(bytes)
- begin
- encoded = encode_str(data)
- raise unless encoded.valid_encoding?
- return encoded
- rescue # encoding error or my invalid data raise
- if @io.eof? or data.size >= bytes + 10
- return data
- else
- data += @io.read(1) until data.valid_encoding? or
- @io.eof? or
- data.size >= bytes + 10
- retry
- end
- end
- end
-end
-
-# Another name for CSV::instance().
-def CSV(*args, &block)
- CSV.instance(*args, &block)
-end
-
-class Array
- # Equivalent to <tt>CSV::generate_line(self, options)</tt>.
- def to_csv(options = Hash.new)
- CSV.generate_line(self, options)
- end
-end
-
-class String
- # Equivalent to <tt>CSV::parse_line(self, options)</tt>.
- def parse_csv(options = Hash.new)
- CSV.parse_line(self, options)
- end
-end
diff --git a/lib/date.rb b/lib/date.rb
deleted file mode 100644
index 2c9792562b..0000000000
--- a/lib/date.rb
+++ /dev/null
@@ -1,1834 +0,0 @@
-#
-# date.rb - date and time library
-#
-# Author: Tadayoshi Funaba 1998-2008
-#
-# Documentation: William Webber <william@williamwebber.com>
-#
-#--
-# $Id: date.rb,v 2.37 2008-01-17 20:16:31+09 tadf Exp $
-#++
-#
-# == Overview
-#
-# This file provides two classes for working with
-# dates and times.
-#
-# The first class, Date, represents dates.
-# It works with years, months, weeks, and days.
-# See the Date class documentation for more details.
-#
-# The second, DateTime, extends Date to include hours,
-# minutes, seconds, and fractions of a second. It
-# provides basic support for time zones. See the
-# DateTime class documentation for more details.
-#
-# === Ways of calculating the date.
-#
-# In common usage, the date is reckoned in years since or
-# before the Common Era (CE/BCE, also known as AD/BC), then
-# as a month and day-of-the-month within the current year.
-# This is known as the *Civil* *Date*, and abbreviated
-# as +civil+ in the Date class.
-#
-# Instead of year, month-of-the-year, and day-of-the-month,
-# the date can also be reckoned in terms of year and
-# day-of-the-year. This is known as the *Ordinal* *Date*,
-# and is abbreviated as +ordinal+ in the Date class. (Note
-# that referring to this as the Julian date is incorrect.)
-#
-# The date can also be reckoned in terms of year, week-of-the-year,
-# and day-of-the-week. This is known as the *Commercial*
-# *Date*, and is abbreviated as +commercial+ in the
-# Date class. The commercial week runs Monday (day-of-the-week
-# 1) to Sunday (day-of-the-week 7), in contrast to the civil
-# week which runs Sunday (day-of-the-week 0) to Saturday
-# (day-of-the-week 6). The first week of the commercial year
-# starts on the Monday on or before January 1, and the commercial
-# year itself starts on this Monday, not January 1.
-#
-# For scientific purposes, it is convenient to refer to a date
-# simply as a day count, counting from an arbitrary initial
-# day. The date first chosen for this was January 1, 4713 BCE.
-# A count of days from this date is the *Julian* *Day* *Number*
-# or *Julian* *Date*, which is abbreviated as +jd+ in the
-# Date class. This is in local time, and counts from midnight
-# on the initial day. The stricter usage is in UTC, and counts
-# from midday on the initial day. This is referred to in the
-# Date class as the *Astronomical* *Julian* *Day* *Number*, and
-# abbreviated as +ajd+. In the Date class, the Astronomical
-# Julian Day Number includes fractional days.
-#
-# Another absolute day count is the *Modified* *Julian* *Day*
-# *Number*, which takes November 17, 1858 as its initial day.
-# This is abbreviated as +mjd+ in the Date class. There
-# is also an *Astronomical* *Modified* *Julian* *Day* *Number*,
-# which is in UTC and includes fractional days. This is
-# abbreviated as +amjd+ in the Date class. Like the Modified
-# Julian Day Number (and unlike the Astronomical Julian
-# Day Number), it counts from midnight.
-#
-# Alternative calendars such as the Chinese Lunar Calendar,
-# the Islamic Calendar, or the French Revolutionary Calendar
-# are not supported by the Date class; nor are calendars that
-# are based on an Era different from the Common Era, such as
-# the Japanese Imperial Calendar or the Republic of China
-# Calendar.
-#
-# === Calendar Reform
-#
-# The standard civil year is 365 days long. However, the
-# solar year is fractionally longer than this. To account
-# for this, a *leap* *year* is occasionally inserted. This
-# is a year with 366 days, the extra day falling on February 29.
-# In the early days of the civil calendar, every fourth
-# year without exception was a leap year. This way of
-# reckoning leap years is the *Julian* *Calendar*.
-#
-# However, the solar year is marginally shorter than 365 1/4
-# days, and so the *Julian* *Calendar* gradually ran slow
-# over the centuries. To correct this, every 100th year
-# (but not every 400th year) was excluded as a leap year.
-# This way of reckoning leap years, which we use today, is
-# the *Gregorian* *Calendar*.
-#
-# The Gregorian Calendar was introduced at different times
-# in different regions. The day on which it was introduced
-# for a particular region is the *Day* *of* *Calendar*
-# *Reform* for that region. This is abbreviated as +sg+
-# (for Start of Gregorian calendar) in the Date class.
-#
-# Two such days are of particular
-# significance. The first is October 15, 1582, which was
-# the Day of Calendar Reform for Italy and most Catholic
-# countries. The second is September 14, 1752, which was
-# the Day of Calendar Reform for England and its colonies
-# (including what is now the United States). These two
-# dates are available as the constants Date::ITALY and
-# Date::ENGLAND, respectively. (By comparison, Germany and
-# Holland, less Catholic than Italy but less stubborn than
-# England, changed over in 1698; Sweden in 1753; Russia not
-# till 1918, after the Revolution; and Greece in 1923. Many
-# Orthodox churches still use the Julian Calendar. A complete
-# list of Days of Calendar Reform can be found at
-# http://www.polysyllabic.com/GregConv.html.)
-#
-# Switching from the Julian to the Gregorian calendar
-# involved skipping a number of days to make up for the
-# accumulated lag, and the later the switch was (or is)
-# done, the more days need to be skipped. So in 1582 in Italy,
-# 4th October was followed by 15th October, skipping 10 days; in 1752
-# in England, 2nd September was followed by 14th September, skipping
-# 11 days; and if I decided to switch from Julian to Gregorian
-# Calendar this midnight, I would go from 27th July 2003 (Julian)
-# today to 10th August 2003 (Gregorian) tomorrow, skipping
-# 13 days. The Date class is aware of this gap, and a supposed
-# date that would fall in the middle of it is regarded as invalid.
-#
-# The Day of Calendar Reform is relevant to all date representations
-# involving years. It is not relevant to the Julian Day Numbers,
-# except for converting between them and year-based representations.
-#
-# In the Date and DateTime classes, the Day of Calendar Reform or
-# +sg+ can be specified a number of ways. First, it can be as
-# the Julian Day Number of the Day of Calendar Reform. Second,
-# it can be using the constants Date::ITALY or Date::ENGLAND; these
-# are in fact the Julian Day Numbers of the Day of Calendar Reform
-# of the respective regions. Third, it can be as the constant
-# Date::JULIAN, which means to always use the Julian Calendar.
-# Finally, it can be as the constant Date::GREGORIAN, which means
-# to always use the Gregorian Calendar.
-#
-# Note: in the Julian Calendar, New Years Day was March 25. The
-# Date class does not follow this convention.
-#
-# === Time Zones
-#
-# DateTime objects support a simple representation
-# of time zones. Time zones are represented as an offset
-# from UTC, as a fraction of a day. This offset is the
-# how much local time is later (or earlier) than UTC.
-# UTC offset 0 is centred on England (also known as GMT).
-# As you travel east, the offset increases until you
-# reach the dateline in the middle of the Pacific Ocean;
-# as you travel west, the offset decreases. This offset
-# is abbreviated as +of+ in the Date class.
-#
-# This simple representation of time zones does not take
-# into account the common practice of Daylight Savings
-# Time or Summer Time.
-#
-# Most DateTime methods return the date and the
-# time in local time. The two exceptions are
-# #ajd() and #amjd(), which return the date and time
-# in UTC time, including fractional days.
-#
-# The Date class does not support time zone offsets, in that
-# there is no way to create a Date object with a time zone.
-# However, methods of the Date class when used by a
-# DateTime instance will use the time zone offset of this
-# instance.
-#
-# == Examples of use
-#
-# === Print out the date of every Sunday between two dates.
-#
-# def print_sundays(d1, d2)
-# d1 +=1 while (d1.wday != 0)
-# d1.step(d2, 7) do |date|
-# puts "#{Date::MONTHNAMES[date.mon]} #{date.day}"
-# end
-# end
-#
-# print_sundays(Date::civil(2003, 4, 8), Date::civil(2003, 5, 23))
-#
-# === Calculate how many seconds to go till midnight on New Year's Day.
-#
-# def secs_to_new_year(now = DateTime::now())
-# new_year = DateTime.new(now.year + 1, 1, 1)
-# dif = new_year - now
-# hours, mins, secs, ignore_fractions = Date::day_fraction_to_time(dif)
-# return hours * 60 * 60 + mins * 60 + secs
-# end
-#
-# puts secs_to_new_year()
-
-require 'date/format'
-
-# Class representing a date.
-#
-# See the documentation to the file date.rb for an overview.
-#
-# Internally, the date is represented as an Astronomical
-# Julian Day Number, +ajd+. The Day of Calendar Reform, +sg+, is
-# also stored, for conversions to other date formats. (There
-# is also an +of+ field for a time zone offset, but this
-# is only for the use of the DateTime subclass.)
-#
-# A new Date object is created using one of the object creation
-# class methods named after the corresponding date format, and the
-# arguments appropriate to that date format; for instance,
-# Date::civil() (aliased to Date::new()) with year, month,
-# and day-of-month, or Date::ordinal() with year and day-of-year.
-# All of these object creation class methods also take the
-# Day of Calendar Reform as an optional argument.
-#
-# Date objects are immutable once created.
-#
-# Once a Date has been created, date values
-# can be retrieved for the different date formats supported
-# using instance methods. For instance, #mon() gives the
-# Civil month, #cwday() gives the Commercial day of the week,
-# and #yday() gives the Ordinal day of the year. Date values
-# can be retrieved in any format, regardless of what format
-# was used to create the Date instance.
-#
-# The Date class includes the Comparable module, allowing
-# date objects to be compared and sorted, ranges of dates
-# to be created, and so forth.
-class Date
-
- include Comparable
-
- # Full month names, in English. Months count from 1 to 12; a
- # month's numerical representation indexed into this array
- # gives the name of that month (hence the first element is nil).
- MONTHNAMES = [nil] + %w(January February March April May June July
- August September October November December)
-
- # Full names of days of the week, in English. Days of the week
- # count from 0 to 6 (except in the commercial week); a day's numerical
- # representation indexed into this array gives the name of that day.
- DAYNAMES = %w(Sunday Monday Tuesday Wednesday Thursday Friday Saturday)
-
- # Abbreviated month names, in English.
- ABBR_MONTHNAMES = [nil] + %w(Jan Feb Mar Apr May Jun
- Jul Aug Sep Oct Nov Dec)
-
- # Abbreviated day names, in English.
- ABBR_DAYNAMES = %w(Sun Mon Tue Wed Thu Fri Sat)
-
- [MONTHNAMES, DAYNAMES, ABBR_MONTHNAMES, ABBR_DAYNAMES].each do |xs|
- xs.each{|x| x.freeze unless x.nil?}.freeze
- end
-
- class Infinity < Numeric # :nodoc:
-
- include Comparable
-
- def initialize(d=1) @d = d <=> 0 end
-
- def d() @d end
-
- protected :d
-
- def zero? () false end
- def finite? () false end
- def infinite? () d.nonzero? end
- def nan? () d.zero? end
-
- def abs() self.class.new end
-
- def -@ () self.class.new(-d) end
- def +@ () self.class.new(+d) end
-
- def <=> (other)
- case other
- when Infinity; return d <=> other.d
- when Numeric; return d
- else
- begin
- l, r = other.coerce(self)
- return l <=> r
- rescue NoMethodError
- end
- end
- nil
- end
-
- def coerce(other)
- case other
- when Numeric; return -d, d
- else
- super
- end
- end
-
- end
-
- # The Julian Day Number of the Day of Calendar Reform for Italy
- # and the Catholic countries.
- ITALY = 2299161 # 1582-10-15
-
- # The Julian Day Number of the Day of Calendar Reform for England
- # and her Colonies.
- ENGLAND = 2361222 # 1752-09-14
-
- # A constant used to indicate that a Date should always use the
- # Julian calendar.
- JULIAN = Infinity.new
-
- # A constant used to indicate that a Date should always use the
- # Gregorian calendar.
- GREGORIAN = -Infinity.new
-
- HALF_DAYS_IN_DAY = Rational(1, 2) # :nodoc:
- HOURS_IN_DAY = Rational(1, 24) # :nodoc:
- MINUTES_IN_DAY = Rational(1, 1440) # :nodoc:
- SECONDS_IN_DAY = Rational(1, 86400) # :nodoc:
- MILLISECONDS_IN_DAY = Rational(1, 86400*10**3) # :nodoc:
- NANOSECONDS_IN_DAY = Rational(1, 86400*10**9) # :nodoc:
- MILLISECONDS_IN_SECOND = Rational(1, 10**3) # :nodoc:
- NANOSECONDS_IN_SECOND = Rational(1, 10**9) # :nodoc:
-
- MJD_EPOCH_IN_AJD = Rational(4800001, 2) # 1858-11-17 # :nodoc:
- UNIX_EPOCH_IN_AJD = Rational(4881175, 2) # 1970-01-01 # :nodoc:
- MJD_EPOCH_IN_CJD = 2400001 # :nodoc:
- UNIX_EPOCH_IN_CJD = 2440588 # :nodoc:
- LD_EPOCH_IN_CJD = 2299160 # :nodoc:
-
- t = Module.new do
-
- private
-
- def find_fdoy(y, sg) # :nodoc:
- j = nil
- 1.upto(31) do |d|
- break if j = _valid_civil?(y, 1, d, sg)
- end
- j
- end
-
- def find_ldoy(y, sg) # :nodoc:
- j = nil
- 31.downto(1) do |d|
- break if j = _valid_civil?(y, 12, d, sg)
- end
- j
- end
-
- def find_fdom(y, m, sg) # :nodoc:
- j = nil
- 1.upto(31) do |d|
- break if j = _valid_civil?(y, m, d, sg)
- end
- j
- end
-
- def find_ldom(y, m, sg) # :nodoc:
- j = nil
- 31.downto(1) do |d|
- break if j = _valid_civil?(y, m, d, sg)
- end
- j
- end
-
- # Convert an Ordinal Date to a Julian Day Number.
- #
- # +y+ and +d+ are the year and day-of-year to convert.
- # +sg+ specifies the Day of Calendar Reform.
- #
- # Returns the corresponding Julian Day Number.
- def ordinal_to_jd(y, d, sg=GREGORIAN) # :nodoc:
- find_fdoy(y, sg) + d - 1
- end
-
- # Convert a Julian Day Number to an Ordinal Date.
- #
- # +jd+ is the Julian Day Number to convert.
- # +sg+ specifies the Day of Calendar Reform.
- #
- # Returns the corresponding Ordinal Date as
- # [year, day_of_year]
- def jd_to_ordinal(jd, sg=GREGORIAN) # :nodoc:
- y = jd_to_civil(jd, sg)[0]
- j = find_fdoy(y, sg)
- doy = jd - j + 1
- return y, doy
- end
-
- # Convert a Civil Date to a Julian Day Number.
- # +y+, +m+, and +d+ are the year, month, and day of the
- # month. +sg+ specifies the Day of Calendar Reform.
- #
- # Returns the corresponding Julian Day Number.
- def civil_to_jd(y, m, d, sg=GREGORIAN) # :nodoc:
- if m <= 2
- y -= 1
- m += 12
- end
- a = (y / 100.0).floor
- b = 2 - a + (a / 4.0).floor
- jd = (365.25 * (y + 4716)).floor +
- (30.6001 * (m + 1)).floor +
- d + b - 1524
- if jd < sg
- jd -= b
- end
- jd
- end
-
- # Convert a Julian Day Number to a Civil Date. +jd+ is
- # the Julian Day Number. +sg+ specifies the Day of
- # Calendar Reform.
- #
- # Returns the corresponding [year, month, day_of_month]
- # as a three-element array.
- def jd_to_civil(jd, sg=GREGORIAN) # :nodoc:
- if jd < sg
- a = jd
- else
- x = ((jd - 1867216.25) / 36524.25).floor
- a = jd + 1 + x - (x / 4.0).floor
- end
- b = a + 1524
- c = ((b - 122.1) / 365.25).floor
- d = (365.25 * c).floor
- e = ((b - d) / 30.6001).floor
- dom = b - d - (30.6001 * e).floor
- if e <= 13
- m = e - 1
- y = c - 4716
- else
- m = e - 13
- y = c - 4715
- end
- return y, m, dom
- end
-
- # Convert a Commercial Date to a Julian Day Number.
- #
- # +y+, +w+, and +d+ are the (commercial) year, week of the year,
- # and day of the week of the Commercial Date to convert.
- # +sg+ specifies the Day of Calendar Reform.
- def commercial_to_jd(y, w, d, sg=GREGORIAN) # :nodoc:
- j = find_fdoy(y, sg) + 3
- (j - (((j - 1) + 1) % 7)) +
- 7 * (w - 1) +
- (d - 1)
- end
-
- # Convert a Julian Day Number to a Commercial Date
- #
- # +jd+ is the Julian Day Number to convert.
- # +sg+ specifies the Day of Calendar Reform.
- #
- # Returns the corresponding Commercial Date as
- # [commercial_year, week_of_year, day_of_week]
- def jd_to_commercial(jd, sg=GREGORIAN) # :nodoc:
- a = jd_to_civil(jd - 3, sg)[0]
- y = if jd >= commercial_to_jd(a + 1, 1, 1, sg) then a + 1 else a end
- w = 1 + ((jd - commercial_to_jd(y, 1, 1, sg)) / 7).floor
- d = (jd + 1) % 7
- d = 7 if d == 0
- return y, w, d
- end
-
- def weeknum_to_jd(y, w, d, f=0, sg=GREGORIAN) # :nodoc:
- a = find_fdoy(y, sg) + 6
- (a - ((a - f) + 1) % 7 - 7) + 7 * w + d
- end
-
- def jd_to_weeknum(jd, f=0, sg=GREGORIAN) # :nodoc:
- y, m, d = jd_to_civil(jd, sg)
- a = find_fdoy(y, sg) + 6
- w, d = (jd - (a - ((a - f) + 1) % 7) + 7).divmod(7)
- return y, w, d
- end
-
- def nth_kday_to_jd(y, m, n, k, sg=GREGORIAN) # :nodoc:
- j = if n > 0
- find_fdom(y, m, sg) - 1
- else
- find_ldom(y, m, sg) + 7
- end
- (j - (((j - k) + 1) % 7)) + 7 * n
- end
-
- def jd_to_nth_kday(jd, sg=GREGORIAN) # :nodoc:
- y, m, d = jd_to_civil(jd, sg)
- j = find_fdom(y, m, sg)
- return y, m, ((jd - j) / 7).floor + 1, jd_to_wday(jd)
- end
-
- # Convert an Astronomical Julian Day Number to a (civil) Julian
- # Day Number.
- #
- # +ajd+ is the Astronomical Julian Day Number to convert.
- # +of+ is the offset from UTC as a fraction of a day (defaults to 0).
- #
- # Returns the (civil) Julian Day Number as [day_number,
- # fraction] where +fraction+ is always 1/2.
- def ajd_to_jd(ajd, of=0) (ajd + of + HALF_DAYS_IN_DAY).divmod(1) end # :nodoc:
-
- # Convert a (civil) Julian Day Number to an Astronomical Julian
- # Day Number.
- #
- # +jd+ is the Julian Day Number to convert, and +fr+ is a
- # fractional day.
- # +of+ is the offset from UTC as a fraction of a day (defaults to 0).
- #
- # Returns the Astronomical Julian Day Number as a single
- # numeric value.
- def jd_to_ajd(jd, fr, of=0) jd + fr - of - HALF_DAYS_IN_DAY end # :nodoc:
-
- # Convert a fractional day +fr+ to [hours, minutes, seconds,
- # fraction_of_a_second]
- def day_fraction_to_time(fr) # :nodoc:
- ss, fr = fr.divmod(SECONDS_IN_DAY) # 4p
- h, ss = ss.divmod(3600)
- min, s = ss.divmod(60)
- return h, min, s, fr * 86400
- end
-
- # Convert an +h+ hour, +min+ minutes, +s+ seconds period
- # to a fractional day.
- begin
- Rational(Rational(1, 2), 2) # a challenge
-
- def time_to_day_fraction(h, min, s)
- Rational(h * 3600 + min * 60 + s, 86400) # 4p
- end
- rescue
- def time_to_day_fraction(h, min, s)
- if Integer === h && Integer === min && Integer === s
- Rational(h * 3600 + min * 60 + s, 86400) # 4p
- else
- (h * 3600 + min * 60 + s).to_r/86400 # 4p
- end
- end
- end
-
- # Convert an Astronomical Modified Julian Day Number to an
- # Astronomical Julian Day Number.
- def amjd_to_ajd(amjd) amjd + MJD_EPOCH_IN_AJD end # :nodoc:
-
- # Convert an Astronomical Julian Day Number to an
- # Astronomical Modified Julian Day Number.
- def ajd_to_amjd(ajd) ajd - MJD_EPOCH_IN_AJD end # :nodoc:
-
- # Convert a Modified Julian Day Number to a Julian
- # Day Number.
- def mjd_to_jd(mjd) mjd + MJD_EPOCH_IN_CJD end # :nodoc:
-
- # Convert a Julian Day Number to a Modified Julian Day
- # Number.
- def jd_to_mjd(jd) jd - MJD_EPOCH_IN_CJD end # :nodoc:
-
- # Convert a count of the number of days since the adoption
- # of the Gregorian Calendar (in Italy) to a Julian Day Number.
- def ld_to_jd(ld) ld + LD_EPOCH_IN_CJD end # :nodoc:
-
- # Convert a Julian Day Number to the number of days since
- # the adoption of the Gregorian Calendar (in Italy).
- def jd_to_ld(jd) jd - LD_EPOCH_IN_CJD end # :nodoc:
-
- # Convert a Julian Day Number to the day of the week.
- #
- # Sunday is day-of-week 0; Saturday is day-of-week 6.
- def jd_to_wday(jd) (jd + 1) % 7 end # :nodoc:
-
- # Is +jd+ a valid Julian Day Number?
- #
- # If it is, returns it. In fact, any value is treated as a valid
- # Julian Day Number.
- def _valid_jd? (jd, sg=GREGORIAN) jd end # :nodoc:
-
- # Do the year +y+ and day-of-year +d+ make a valid Ordinal Date?
- # Returns the corresponding Julian Day Number if they do, or
- # nil if they don't.
- #
- # +d+ can be a negative number, in which case it counts backwards
- # from the end of the year (-1 being the last day of the year).
- # No year wraparound is performed, however, so valid values of
- # +d+ are -365 .. -1, 1 .. 365 on a non-leap-year,
- # -366 .. -1, 1 .. 366 on a leap year.
- # A date falling in the period skipped in the Day of Calendar Reform
- # adjustment is not valid.
- #
- # +sg+ specifies the Day of Calendar Reform.
- def _valid_ordinal? (y, d, sg=GREGORIAN) # :nodoc:
- if d < 0
- j = find_ldoy(y, sg)
- ny, nd = jd_to_ordinal(j + d + 1, sg)
- return unless ny == y
- d = nd
- end
- jd = ordinal_to_jd(y, d, sg)
- return unless [y, d] == jd_to_ordinal(jd, sg)
- jd
- end
-
- # Do year +y+, month +m+, and day-of-month +d+ make a
- # valid Civil Date? Returns the corresponding Julian
- # Day Number if they do, nil if they don't.
- #
- # +m+ and +d+ can be negative, in which case they count
- # backwards from the end of the year and the end of the
- # month respectively. No wraparound is performed, however,
- # and invalid values cause an ArgumentError to be raised.
- # A date falling in the period skipped in the Day of Calendar
- # Reform adjustment is not valid.
- #
- # +sg+ specifies the Day of Calendar Reform.
- def _valid_civil? (y, m, d, sg=GREGORIAN) # :nodoc:
- if m < 0
- m += 13
- end
- if d < 0
- j = find_ldom(y, m, sg)
- ny, nm, nd = jd_to_civil(j + d + 1, sg)
- return unless [ny, nm] == [y, m]
- d = nd
- end
- jd = civil_to_jd(y, m, d, sg)
- return unless [y, m, d] == jd_to_civil(jd, sg)
- jd
- end
-
- # Do year +y+, week-of-year +w+, and day-of-week +d+ make a
- # valid Commercial Date? Returns the corresponding Julian
- # Day Number if they do, nil if they don't.
- #
- # Monday is day-of-week 1; Sunday is day-of-week 7.
- #
- # +w+ and +d+ can be negative, in which case they count
- # backwards from the end of the year and the end of the
- # week respectively. No wraparound is performed, however,
- # and invalid values cause an ArgumentError to be raised.
- # A date falling in the period skipped in the Day of Calendar
- # Reform adjustment is not valid.
- #
- # +sg+ specifies the Day of Calendar Reform.
- def _valid_commercial? (y, w, d, sg=GREGORIAN) # :nodoc:
- if d < 0
- d += 8
- end
- if w < 0
- ny, nw, nd =
- jd_to_commercial(commercial_to_jd(y + 1, 1, 1, sg) + w * 7, sg)
- return unless ny == y
- w = nw
- end
- jd = commercial_to_jd(y, w, d, sg)
- return unless [y, w, d] == jd_to_commercial(jd, sg)
- jd
- end
-
- def _valid_weeknum? (y, w, d, f, sg=GREGORIAN) # :nodoc:
- if d < 0
- d += 7
- end
- if w < 0
- ny, nw, nd, nf =
- jd_to_weeknum(weeknum_to_jd(y + 1, 1, f, f, sg) + w * 7, f, sg)
- return unless ny == y
- w = nw
- end
- jd = weeknum_to_jd(y, w, d, f, sg)
- return unless [y, w, d] == jd_to_weeknum(jd, f, sg)
- jd
- end
-
- def _valid_nth_kday? (y, m, n, k, sg=GREGORIAN) # :nodoc:
- if k < 0
- k += 7
- end
- if n < 0
- ny, nm = (y * 12 + m).divmod(12)
- nm, = (nm + 1) .divmod(1)
- ny, nm, nn, nk =
- jd_to_nth_kday(nth_kday_to_jd(ny, nm, 1, k, sg) + n * 7, sg)
- return unless [ny, nm] == [y, m]
- n = nn
- end
- jd = nth_kday_to_jd(y, m, n, k, sg)
- return unless [y, m, n, k] == jd_to_nth_kday(jd, sg)
- jd
- end
-
- # Do hour +h+, minute +min+, and second +s+ constitute a valid time?
- #
- # If they do, returns their value as a fraction of a day. If not,
- # returns nil.
- #
- # The 24-hour clock is used. Negative values of +h+, +min+, and
- # +sec+ are treating as counting backwards from the end of the
- # next larger unit (e.g. a +min+ of -2 is treated as 58). No
- # wraparound is performed.
- def _valid_time? (h, min, s) # :nodoc:
- h += 24 if h < 0
- min += 60 if min < 0
- s += 60 if s < 0
- return unless ((0...24) === h &&
- (0...60) === min &&
- (0...60) === s) ||
- (24 == h &&
- 0 == min &&
- 0 == s)
- time_to_day_fraction(h, min, s)
- end
-
- end
-
- extend t
- include t
-
- # Is a year a leap year in the Julian calendar?
- #
- # All years divisible by 4 are leap years in the Julian calendar.
- def self.julian_leap? (y) y % 4 == 0 end
-
- # Is a year a leap year in the Gregorian calendar?
- #
- # All years divisible by 4 are leap years in the Gregorian calendar,
- # except for years divisible by 100 and not by 400.
- def self.gregorian_leap? (y) y % 4 == 0 && y % 100 != 0 || y % 400 == 0 end
-
- class << self; alias_method :leap?, :gregorian_leap? end
- class << self; alias_method :new!, :new end
-
- def self.valid_jd? (jd, sg=ITALY)
- !!_valid_jd?(jd, sg)
- end
-
- def self.valid_ordinal? (y, d, sg=ITALY)
- !!_valid_ordinal?(y, d, sg)
- end
-
- def self.valid_civil? (y, m, d, sg=ITALY)
- !!_valid_civil?(y, m, d, sg)
- end
-
- class << self; alias_method :valid_date?, :valid_civil? end
-
- def self.valid_commercial? (y, w, d, sg=ITALY)
- !!_valid_commercial?(y, w, d, sg)
- end
-
- def self.valid_weeknum? (y, w, d, f, sg=ITALY) # :nodoc:
- !!_valid_weeknum?(y, w, d, f, sg)
- end
-
- private_class_method :valid_weeknum?
-
- def self.valid_nth_kday? (y, m, n, k, sg=ITALY) # :nodoc:
- !!_valid_nth_kday?(y, m, n, k, sg)
- end
-
- private_class_method :valid_nth_kday?
-
- def self.valid_time? (h, min, s) # :nodoc:
- !!_valid_time?(h, min, s)
- end
-
- private_class_method :valid_time?
-
- # Create a new Date object from a Julian Day Number.
- #
- # +jd+ is the Julian Day Number; if not specified, it defaults to
- # 0.
- # +sg+ specifies the Day of Calendar Reform.
- def self.jd(jd=0, sg=ITALY)
- jd = _valid_jd?(jd, sg)
- new!(jd_to_ajd(jd, 0, 0), 0, sg)
- end
-
- # Create a new Date object from an Ordinal Date, specified
- # by year +y+ and day-of-year +d+. +d+ can be negative,
- # in which it counts backwards from the end of the year.
- # No year wraparound is performed, however. An invalid
- # value for +d+ results in an ArgumentError being raised.
- #
- # +y+ defaults to -4712, and +d+ to 1; this is Julian Day
- # Number day 0.
- #
- # +sg+ specifies the Day of Calendar Reform.
- def self.ordinal(y=-4712, d=1, sg=ITALY)
- unless jd = _valid_ordinal?(y, d, sg)
- raise ArgumentError, 'invalid date'
- end
- new!(jd_to_ajd(jd, 0, 0), 0, sg)
- end
-
- # Create a new Date object for the Civil Date specified by
- # year +y+, month +m+, and day-of-month +d+.
- #
- # +m+ and +d+ can be negative, in which case they count
- # backwards from the end of the year and the end of the
- # month respectively. No wraparound is performed, however,
- # and invalid values cause an ArgumentError to be raised.
- # can be negative
- #
- # +y+ defaults to -4712, +m+ to 1, and +d+ to 1; this is
- # Julian Day Number day 0.
- #
- # +sg+ specifies the Day of Calendar Reform.
- def self.civil(y=-4712, m=1, d=1, sg=ITALY)
- unless jd = _valid_civil?(y, m, d, sg)
- raise ArgumentError, 'invalid date'
- end
- new!(jd_to_ajd(jd, 0, 0), 0, sg)
- end
-
- class << self; alias_method :new, :civil end
-
- # Create a new Date object for the Commercial Date specified by
- # year +y+, week-of-year +w+, and day-of-week +d+.
- #
- # Monday is day-of-week 1; Sunday is day-of-week 7.
- #
- # +w+ and +d+ can be negative, in which case they count
- # backwards from the end of the year and the end of the
- # week respectively. No wraparound is performed, however,
- # and invalid values cause an ArgumentError to be raised.
- #
- # +y+ defaults to -4712, +w+ to 1, and +d+ to 1; this is
- # Julian Day Number day 0.
- #
- # +sg+ specifies the Day of Calendar Reform.
- def self.commercial(y=-4712, w=1, d=1, sg=ITALY)
- unless jd = _valid_commercial?(y, w, d, sg)
- raise ArgumentError, 'invalid date'
- end
- new!(jd_to_ajd(jd, 0, 0), 0, sg)
- end
-
- def self.weeknum(y=-4712, w=0, d=1, f=0, sg=ITALY)
- unless jd = _valid_weeknum?(y, w, d, f, sg)
- raise ArgumentError, 'invalid date'
- end
- new!(jd_to_ajd(jd, 0, 0), 0, sg)
- end
-
- private_class_method :weeknum
-
- def self.nth_kday(y=-4712, m=1, n=1, k=1, sg=ITALY)
- unless jd = _valid_nth_kday?(y, m, n, k, sg)
- raise ArgumentError, 'invalid date'
- end
- new!(jd_to_ajd(jd, 0, 0), 0, sg)
- end
-
- private_class_method :nth_kday
-
- def self.rewrite_frags(elem) # :nodoc:
- elem ||= {}
- if seconds = elem[:seconds]
- d, fr = seconds.divmod(86400)
- h, fr = fr.divmod(3600)
- min, fr = fr.divmod(60)
- s, fr = fr.divmod(1)
- elem[:jd] = UNIX_EPOCH_IN_CJD + d
- elem[:hour] = h
- elem[:min] = min
- elem[:sec] = s
- elem[:sec_fraction] = fr
- elem.delete(:seconds)
- elem.delete(:offset)
- end
- elem
- end
-
- private_class_method :rewrite_frags
-
- def self.complete_frags(elem) # :nodoc:
- i = 0
- g = [[:time, [:hour, :min, :sec]],
- [nil, [:jd]],
- [:ordinal, [:year, :yday, :hour, :min, :sec]],
- [:civil, [:year, :mon, :mday, :hour, :min, :sec]],
- [:commercial, [:cwyear, :cweek, :cwday, :hour, :min, :sec]],
- [:wday, [:wday, :hour, :min, :sec]],
- [:wnum0, [:year, :wnum0, :wday, :hour, :min, :sec]],
- [:wnum1, [:year, :wnum1, :wday, :hour, :min, :sec]],
- [nil, [:cwyear, :cweek, :wday, :hour, :min, :sec]],
- [nil, [:year, :wnum0, :cwday, :hour, :min, :sec]],
- [nil, [:year, :wnum1, :cwday, :hour, :min, :sec]]].
- collect{|k, a| e = elem.values_at(*a).compact; [k, a, e]}.
- select{|k, a, e| e.size > 0}.
- sort_by{|k, a, e| [e.size, i -= 1]}.last
-
- d = nil
-
- if g && g[0] && (g[1].size - g[2].size) != 0
- d ||= Date.today
-
- case g[0]
- when :ordinal
- elem[:year] ||= d.year
- elem[:yday] ||= 1
- when :civil
- g[1].each do |e|
- break if elem[e]
- elem[e] = d.__send__(e)
- end
- elem[:mon] ||= 1
- elem[:mday] ||= 1
- when :commercial
- g[1].each do |e|
- break if elem[e]
- elem[e] = d.__send__(e)
- end
- elem[:cweek] ||= 1
- elem[:cwday] ||= 1
- when :wday
- elem[:jd] ||= (d - d.wday + elem[:wday]).jd
- when :wnum0
- g[1].each do |e|
- break if elem[e]
- elem[e] = d.__send__(e)
- end
- elem[:wnum0] ||= 0
- elem[:wday] ||= 0
- when :wnum1
- g[1].each do |e|
- break if elem[e]
- elem[e] = d.__send__(e)
- end
- elem[:wnum1] ||= 0
- elem[:wday] ||= 0
- end
- end
-
- if g && g[0] == :time
- if self <= DateTime
- d ||= Date.today
- elem[:jd] ||= d.jd
- end
- end
-
- elem[:hour] ||= 0
- elem[:min] ||= 0
- elem[:sec] ||= 0
- elem[:sec] = [elem[:sec], 59].min
-
- elem
- end
-
- private_class_method :complete_frags
-
- def self.valid_date_frags?(elem, sg) # :nodoc:
- catch :jd do
- a = elem.values_at(:jd)
- if a.all?
- if jd = _valid_jd?(*(a << sg))
- throw :jd, jd
- end
- end
-
- a = elem.values_at(:year, :yday)
- if a.all?
- if jd = _valid_ordinal?(*(a << sg))
- throw :jd, jd
- end
- end
-
- a = elem.values_at(:year, :mon, :mday)
- if a.all?
- if jd = _valid_civil?(*(a << sg))
- throw :jd, jd
- end
- end
-
- a = elem.values_at(:cwyear, :cweek, :cwday)
- if a[2].nil? && elem[:wday]
- a[2] = elem[:wday].nonzero? || 7
- end
- if a.all?
- if jd = _valid_commercial?(*(a << sg))
- throw :jd, jd
- end
- end
-
- a = elem.values_at(:year, :wnum0, :wday)
- if a[2].nil? && elem[:cwday]
- a[2] = elem[:cwday] % 7
- end
- if a.all?
- if jd = _valid_weeknum?(*(a << 0 << sg))
- throw :jd, jd
- end
- end
-
- a = elem.values_at(:year, :wnum1, :wday)
- if a[2]
- a[2] = (a[2] - 1) % 7
- end
- if a[2].nil? && elem[:cwday]
- a[2] = (elem[:cwday] - 1) % 7
- end
- if a.all?
- if jd = _valid_weeknum?(*(a << 1 << sg))
- throw :jd, jd
- end
- end
- end
- end
-
- private_class_method :valid_date_frags?
-
- def self.valid_time_frags? (elem) # :nodoc:
- h, min, s = elem.values_at(:hour, :min, :sec)
- _valid_time?(h, min, s)
- end
-
- private_class_method :valid_time_frags?
-
- def self.new_by_frags(elem, sg) # :nodoc:
- elem = rewrite_frags(elem)
- elem = complete_frags(elem)
- unless jd = valid_date_frags?(elem, sg)
- raise ArgumentError, 'invalid date'
- end
- new!(jd_to_ajd(jd, 0, 0), 0, sg)
- end
-
- private_class_method :new_by_frags
-
- # Create a new Date object by parsing from a String
- # according to a specified format.
- #
- # +str+ is a String holding a date representation.
- # +fmt+ is the format that the date is in. See
- # date/format.rb for details on supported formats.
- #
- # The default +str+ is '-4712-01-01', and the default
- # +fmt+ is '%F', which means Year-Month-Day_of_Month.
- # This gives Julian Day Number day 0.
- #
- # +sg+ specifies the Day of Calendar Reform.
- #
- # An ArgumentError will be raised if +str+ cannot be
- # parsed.
- def self.strptime(str='-4712-01-01', fmt='%F', sg=ITALY)
- elem = _strptime(str, fmt)
- new_by_frags(elem, sg)
- end
-
- # Create a new Date object by parsing from a String,
- # without specifying the format.
- #
- # +str+ is a String holding a date representation.
- # +comp+ specifies whether to interpret 2-digit years
- # as 19XX (>= 69) or 20XX (< 69); the default is not to.
- # The method will attempt to parse a date from the String
- # using various heuristics; see #_parse in date/format.rb
- # for more details. If parsing fails, an ArgumentError
- # will be raised.
- #
- # The default +str+ is '-4712-01-01'; this is Julian
- # Day Number day 0.
- #
- # +sg+ specifies the Day of Calendar Reform.
- def self.parse(str='-4712-01-01', comp=true, sg=ITALY)
- elem = _parse(str, comp)
- new_by_frags(elem, sg)
- end
-
- def self.iso8601(str='-4712-01-01', sg=ITALY) # :nodoc:
- elem = _iso8601(str)
- new_by_frags(elem, sg)
- end
-
- def self.rfc3339(str='-4712-01-01T00:00:00+00:00', sg=ITALY) # :nodoc:
- elem = _rfc3339(str)
- new_by_frags(elem, sg)
- end
-
- def self.xmlschema(str='-4712-01-01', sg=ITALY) # :nodoc:
- elem = _xmlschema(str)
- new_by_frags(elem, sg)
- end
-
- def self.rfc2822(str='Mon, 1 Jan -4712 00:00:00 +0000', sg=ITALY) # :nodoc:
- elem = _rfc2822(str)
- new_by_frags(elem, sg)
- end
-
- class << self; alias_method :rfc822, :rfc2822 end
-
- def self.httpdate(str='Mon, 01 Jan -4712 00:00:00 GMT', sg=ITALY) # :nodoc:
- elem = _httpdate(str)
- new_by_frags(elem, sg)
- end
-
- def self.jisx0301(str='-4712-01-01', sg=ITALY) # :nodoc:
- elem = _jisx0301(str)
- new_by_frags(elem, sg)
- end
-
- class << self
-
- def once(*ids) # :nodoc: -- restricted
- for id in ids
- module_eval <<-"end;"
- alias_method :__#{id.object_id}__, :#{id.to_s}
- private :__#{id.object_id}__
- def #{id.to_s}(*args)
- @__ca__[#{id.object_id}] ||= __#{id.object_id}__(*args)
- end
- end;
- end
- end
-
- private :once
-
- end
-
- # *NOTE* this is the documentation for the method new!(). If
- # you are reading this as the documentation for new(), that is
- # because rdoc doesn't fully support the aliasing of the
- # initialize() method.
- # new() is in
- # fact an alias for #civil(): read the documentation for that
- # method instead.
- #
- # Create a new Date object.
- #
- # +ajd+ is the Astronomical Julian Day Number.
- # +of+ is the offset from UTC as a fraction of a day.
- # Both default to 0.
- #
- # +sg+ specifies the Day of Calendar Reform to use for this
- # Date object.
- #
- # Using one of the factory methods such as Date::civil is
- # generally easier and safer.
- def initialize(ajd=0, of=0, sg=ITALY)
- @ajd, @of, @sg = ajd, of, sg
- @__ca__ = {}
- end
-
- # Get the date as an Astronomical Julian Day Number.
- def ajd() @ajd end
-
- # Get the date as an Astronomical Modified Julian Day Number.
- def amjd() ajd_to_amjd(@ajd) end
-
- once :amjd
-
- # Get the date as a Julian Day Number.
- def jd() ajd_to_jd(@ajd, @of)[0] end
-
- # Get any fractional day part of the date.
- def day_fraction() ajd_to_jd(@ajd, @of)[1] end
-
- # Get the date as a Modified Julian Day Number.
- def mjd() jd_to_mjd(jd) end
-
- # Get the date as the number of days since the Day of Calendar
- # Reform (in Italy and the Catholic countries).
- def ld() jd_to_ld(jd) end
-
- once :jd, :day_fraction, :mjd, :ld
-
- # Get the date as a Civil Date, [year, month, day_of_month]
- def civil() jd_to_civil(jd, @sg) end # :nodoc:
-
- # Get the date as an Ordinal Date, [year, day_of_year]
- def ordinal() jd_to_ordinal(jd, @sg) end # :nodoc:
-
- # Get the date as a Commercial Date, [year, week_of_year, day_of_week]
- def commercial() jd_to_commercial(jd, @sg) end # :nodoc:
-
- def weeknum0() jd_to_weeknum(jd, 0, @sg) end # :nodoc:
- def weeknum1() jd_to_weeknum(jd, 1, @sg) end # :nodoc:
-
- once :civil, :ordinal, :commercial, :weeknum0, :weeknum1
- private :civil, :ordinal, :commercial, :weeknum0, :weeknum1
-
- # Get the year of this date.
- def year() civil[0] end
-
- # Get the day-of-the-year of this date.
- #
- # January 1 is day-of-the-year 1
- def yday() ordinal[1] end
-
- # Get the month of this date.
- #
- # January is month 1.
- def mon() civil[1] end
-
- # Get the day-of-the-month of this date.
- def mday() civil[2] end
-
- alias_method :month, :mon
- alias_method :day, :mday
-
- def wnum0() weeknum0[1] end # :nodoc:
- def wnum1() weeknum1[1] end # :nodoc:
-
- private :wnum0, :wnum1
-
- # Get the time of this date as [hours, minutes, seconds,
- # fraction_of_a_second]
- def time() day_fraction_to_time(day_fraction) end # :nodoc:
-
- once :time
- private :time
-
- # Get the hour of this date.
- def hour() time[0] end
-
- # Get the minute of this date.
- def min() time[1] end
-
- # Get the second of this date.
- def sec() time[2] end
-
- # Get the fraction-of-a-second of this date.
- def sec_fraction() time[3] end
-
- alias_method :minute, :min
- alias_method :second, :sec
- alias_method :second_fraction, :sec_fraction
-
- private :hour, :min, :sec, :sec_fraction,
- :minute, :second, :second_fraction
-
- def zone() strftime('%:z') end
-
- private :zone
-
- # Get the commercial year of this date. See *Commercial* *Date*
- # in the introduction for how this differs from the normal year.
- def cwyear() commercial[0] end
-
- # Get the commercial week of the year of this date.
- def cweek() commercial[1] end
-
- # Get the commercial day of the week of this date. Monday is
- # commercial day-of-week 1; Sunday is commercial day-of-week 7.
- def cwday() commercial[2] end
-
- # Get the week day of this date. Sunday is day-of-week 0;
- # Saturday is day-of-week 6.
- def wday() jd_to_wday(jd) end
-
- once :wday
-
-=begin
- MONTHNAMES.each_with_index do |n, i|
- if n
- define_method(n.downcase + '?'){mon == i}
- end
- end
-=end
-
- DAYNAMES.each_with_index do |n, i|
- define_method(n.downcase + '?'){wday == i}
- end
-
- def nth_kday? (n, k)
- k == wday && jd === nth_kday_to_jd(year, mon, n, k, start)
- end
-
- private :nth_kday?
-
- # Is the current date old-style (Julian Calendar)?
- def julian? () jd < @sg end
-
- # Is the current date new-style (Gregorian Calendar)?
- def gregorian? () !julian? end
-
- once :julian?, :gregorian?
-
- def fix_style # :nodoc:
- if julian?
- then self.class::JULIAN
- else self.class::GREGORIAN end
- end
-
- private :fix_style
-
- # Is this a leap year?
- def leap?
- jd_to_civil(civil_to_jd(year, 3, 1, fix_style) - 1,
- fix_style)[-1] == 29
- end
-
- once :leap?
-
- # When is the Day of Calendar Reform for this Date object?
- def start() @sg end
-
- # Create a copy of this Date object using a new Day of Calendar Reform.
- def new_start(sg=self.class::ITALY) self.class.new!(@ajd, @of, sg) end
-
- # Create a copy of this Date object that uses the Italian/Catholic
- # Day of Calendar Reform.
- def italy() new_start(self.class::ITALY) end
-
- # Create a copy of this Date object that uses the English/Colonial
- # Day of Calendar Reform.
- def england() new_start(self.class::ENGLAND) end
-
- # Create a copy of this Date object that always uses the Julian
- # Calendar.
- def julian() new_start(self.class::JULIAN) end
-
- # Create a copy of this Date object that always uses the Gregorian
- # Calendar.
- def gregorian() new_start(self.class::GREGORIAN) end
-
- def offset() @of end
-
- def new_offset(of=0)
- if String === of
- of = Rational(zone_to_diff(of) || 0, 86400)
- end
- self.class.new!(@ajd, of, @sg)
- end
-
- private :offset, :new_offset
-
- # Return a new Date object that is +n+ days later than the
- # current one.
- #
- # +n+ may be a negative value, in which case the new Date
- # is earlier than the current one; however, #-() might be
- # more intuitive.
- #
- # If +n+ is not a Numeric, a TypeError will be thrown. In
- # particular, two Dates cannot be added to each other.
- def + (n)
- case n
- when Numeric; return self.class.new!(@ajd + n, @of, @sg)
- end
- raise TypeError, 'expected numeric'
- end
-
- # If +x+ is a Numeric value, create a new Date object that is
- # +x+ days earlier than the current one.
- #
- # If +x+ is a Date, return the number of days between the
- # two dates; or, more precisely, how many days later the current
- # date is than +x+.
- #
- # If +x+ is neither Numeric nor a Date, a TypeError is raised.
- def - (x)
- case x
- when Numeric; return self.class.new!(@ajd - x, @of, @sg)
- when Date; return @ajd - x.ajd
- end
- raise TypeError, 'expected numeric or date'
- end
-
- # Compare this date with another date.
- #
- # +other+ can also be a Numeric value, in which case it is
- # interpreted as an Astronomical Julian Day Number.
- #
- # Comparison is by Astronomical Julian Day Number, including
- # fractional days. This means that both the time and the
- # timezone offset are taken into account when comparing
- # two DateTime instances. When comparing a DateTime instance
- # with a Date instance, the time of the latter will be
- # considered as falling on midnight UTC.
- def <=> (other)
- case other
- when Numeric; return @ajd <=> other
- when Date; return @ajd <=> other.ajd
- end
- nil
- end
-
- # The relationship operator for Date.
- #
- # Compares dates by Julian Day Number. When comparing
- # two DateTime instances, or a DateTime with a Date,
- # the instances will be regarded as equivalent if they
- # fall on the same date in local time.
- def === (other)
- case other
- when Numeric; return jd == other
- when Date; return jd == other.jd
- end
- false
- end
-
- def next_day(n=1) self + n end
- def prev_day(n=1) self - n end
-
- # Return a new Date one day after this one.
- def next() next_day end
-
- alias_method :succ, :next
-
- # Return a new Date object that is +n+ months later than
- # the current one.
- #
- # If the day-of-the-month of the current Date is greater
- # than the last day of the target month, the day-of-the-month
- # of the returned Date will be the last day of the target month.
- def >> (n)
- y, m = (year * 12 + (mon - 1) + n).divmod(12)
- m, = (m + 1) .divmod(1)
- d = mday
- d -= 1 until jd2 = _valid_civil?(y, m, d, @sg)
- self + (jd2 - jd)
- end
-
- # Return a new Date object that is +n+ months earlier than
- # the current one.
- #
- # If the day-of-the-month of the current Date is greater
- # than the last day of the target month, the day-of-the-month
- # of the returned Date will be the last day of the target month.
- def << (n) self >> -n end
-
- def next_month(n=1) self >> n end
- def prev_month(n=1) self << n end
-
- def next_year(n=1) self >> n * 12 end
- def prev_year(n=1) self << n * 12 end
-
- require 'enumerator'
-
- # Step the current date forward +step+ days at a
- # time (or backward, if +step+ is negative) until
- # we reach +limit+ (inclusive), yielding the resultant
- # date at each step.
- def step(limit, step=1) # :yield: date
-=begin
- if step.zero?
- raise ArgumentError, "step can't be 0"
- end
-=end
- unless block_given?
- return to_enum(:step, limit, step)
- end
- da = self
- op = %w(- <= >=)[step <=> 0]
- while da.__send__(op, limit)
- yield da
- da += step
- end
- self
- end
-
- # Step forward one day at a time until we reach +max+
- # (inclusive), yielding each date as we go.
- def upto(max, &block) # :yield: date
- step(max, +1, &block)
- end
-
- # Step backward one day at a time until we reach +min+
- # (inclusive), yielding each date as we go.
- def downto(min, &block) # :yield: date
- step(min, -1, &block)
- end
-
- # Is this Date equal to +other+?
- #
- # +other+ must both be a Date object, and represent the same date.
- def eql? (other) Date === other && self == other end
-
- # Calculate a hash value for this date.
- def hash() @ajd.hash end
-
- # Return internal object state as a programmer-readable string.
- def inspect
- format('#<%s: %s (%s,%s,%s)>', self.class, to_s, @ajd, @of, @sg)
- end
-
- # Return the date as a human-readable string.
- #
- # The format used is YYYY-MM-DD.
- def to_s() format('%.4d-%02d-%02d', year, mon, mday) end # 4p
-
- # Dump to Marshal format.
- def marshal_dump() [@ajd, @of, @sg] end
-
- # Load from Marshal format.
- def marshal_load(a)
- @ajd, @of, @sg, = a
- @__ca__ = {}
- end
-
-end
-
-# Class representing a date and time.
-#
-# See the documentation to the file date.rb for an overview.
-#
-# DateTime objects are immutable once created.
-#
-# == Other methods.
-#
-# The following methods are defined in Date, but declared private
-# there. They are made public in DateTime. They are documented
-# here.
-#
-# === hour()
-#
-# Get the hour-of-the-day of the time. This is given
-# using the 24-hour clock, counting from midnight. The first
-# hour after midnight is hour 0; the last hour of the day is
-# hour 23.
-#
-# === min()
-#
-# Get the minute-of-the-hour of the time.
-#
-# === sec()
-#
-# Get the second-of-the-minute of the time.
-#
-# === sec_fraction()
-#
-# Get the fraction of a second of the time. This is returned as
-# a +Rational+.
-#
-# === zone()
-#
-# Get the time zone as a String. This is representation of the
-# time offset such as "+1000", not the true time-zone name.
-#
-# === offset()
-#
-# Get the time zone offset as a fraction of a day. This is returned
-# as a +Rational+.
-#
-# === new_offset(of=0)
-#
-# Create a new DateTime object, identical to the current one, except
-# with a new time zone offset of +of+. +of+ is the new offset from
-# UTC as a fraction of a day.
-#
-class DateTime < Date
-
- # Create a new DateTime object corresponding to the specified
- # Julian Day Number +jd+ and hour +h+, minute +min+, second +s+.
- #
- # The 24-hour clock is used. Negative values of +h+, +min+, and
- # +sec+ are treating as counting backwards from the end of the
- # next larger unit (e.g. a +min+ of -2 is treated as 58). No
- # wraparound is performed. If an invalid time portion is specified,
- # an ArgumentError is raised.
- #
- # +of+ is the offset from UTC as a fraction of a day (defaults to 0).
- # +sg+ specifies the Day of Calendar Reform.
- #
- # All day/time values default to 0.
- def self.jd(jd=0, h=0, min=0, s=0, of=0, sg=ITALY)
- unless (jd = _valid_jd?(jd, sg)) &&
- (fr = _valid_time?(h, min, s))
- raise ArgumentError, 'invalid date'
- end
- if String === of
- of = Rational(zone_to_diff(of) || 0, 86400)
- end
- new!(jd_to_ajd(jd, fr, of), of, sg)
- end
-
- # Create a new DateTime object corresponding to the specified
- # Ordinal Date and hour +h+, minute +min+, second +s+.
- #
- # The 24-hour clock is used. Negative values of +h+, +min+, and
- # +sec+ are treating as counting backwards from the end of the
- # next larger unit (e.g. a +min+ of -2 is treated as 58). No
- # wraparound is performed. If an invalid time portion is specified,
- # an ArgumentError is raised.
- #
- # +of+ is the offset from UTC as a fraction of a day (defaults to 0).
- # +sg+ specifies the Day of Calendar Reform.
- #
- # +y+ defaults to -4712, and +d+ to 1; this is Julian Day Number
- # day 0. The time values default to 0.
- def self.ordinal(y=-4712, d=1, h=0, min=0, s=0, of=0, sg=ITALY)
- unless (jd = _valid_ordinal?(y, d, sg)) &&
- (fr = _valid_time?(h, min, s))
- raise ArgumentError, 'invalid date'
- end
- if String === of
- of = Rational(zone_to_diff(of) || 0, 86400)
- end
- new!(jd_to_ajd(jd, fr, of), of, sg)
- end
-
- # Create a new DateTime object corresponding to the specified
- # Civil Date and hour +h+, minute +min+, second +s+.
- #
- # The 24-hour clock is used. Negative values of +h+, +min+, and
- # +sec+ are treating as counting backwards from the end of the
- # next larger unit (e.g. a +min+ of -2 is treated as 58). No
- # wraparound is performed. If an invalid time portion is specified,
- # an ArgumentError is raised.
- #
- # +of+ is the offset from UTC as a fraction of a day (defaults to 0).
- # +sg+ specifies the Day of Calendar Reform.
- #
- # +y+ defaults to -4712, +m+ to 1, and +d+ to 1; this is Julian Day
- # Number day 0. The time values default to 0.
- def self.civil(y=-4712, m=1, d=1, h=0, min=0, s=0, of=0, sg=ITALY)
- unless (jd = _valid_civil?(y, m, d, sg)) &&
- (fr = _valid_time?(h, min, s))
- raise ArgumentError, 'invalid date'
- end
- if String === of
- of = Rational(zone_to_diff(of) || 0, 86400)
- end
- new!(jd_to_ajd(jd, fr, of), of, sg)
- end
-
- class << self; alias_method :new, :civil end
-
- # Create a new DateTime object corresponding to the specified
- # Commercial Date and hour +h+, minute +min+, second +s+.
- #
- # The 24-hour clock is used. Negative values of +h+, +min+, and
- # +sec+ are treating as counting backwards from the end of the
- # next larger unit (e.g. a +min+ of -2 is treated as 58). No
- # wraparound is performed. If an invalid time portion is specified,
- # an ArgumentError is raised.
- #
- # +of+ is the offset from UTC as a fraction of a day (defaults to 0).
- # +sg+ specifies the Day of Calendar Reform.
- #
- # +y+ defaults to -4712, +w+ to 1, and +d+ to 1; this is
- # Julian Day Number day 0.
- # The time values default to 0.
- def self.commercial(y=-4712, w=1, d=1, h=0, min=0, s=0, of=0, sg=ITALY)
- unless (jd = _valid_commercial?(y, w, d, sg)) &&
- (fr = _valid_time?(h, min, s))
- raise ArgumentError, 'invalid date'
- end
- if String === of
- of = Rational(zone_to_diff(of) || 0, 86400)
- end
- new!(jd_to_ajd(jd, fr, of), of, sg)
- end
-
- def self.weeknum(y=-4712, w=0, d=1, f=0, h=0, min=0, s=0, of=0, sg=ITALY) # :nodoc:
- unless (jd = _valid_weeknum?(y, w, d, f, sg)) &&
- (fr = _valid_time?(h, min, s))
- raise ArgumentError, 'invalid date'
- end
- if String === of
- of = Rational(zone_to_diff(of) || 0, 86400)
- end
- new!(jd_to_ajd(jd, fr, of), of, sg)
- end
-
- private_class_method :weeknum
-
- def self.nth_kday(y=-4712, m=1, n=1, k=1, h=0, min=0, s=0, of=0, sg=ITALY) # :nodoc:
- unless (jd = _valid_nth_kday?(y, m, n, k, sg)) &&
- (fr = _valid_time?(h, min, s))
- raise ArgumentError, 'invalid date'
- end
- if String === of
- of = Rational(zone_to_diff(of) || 0, 86400)
- end
- new!(jd_to_ajd(jd, fr, of), of, sg)
- end
-
- private_class_method :nth_kday
-
- def self.new_by_frags(elem, sg) # :nodoc:
- elem = rewrite_frags(elem)
- elem = complete_frags(elem)
- unless (jd = valid_date_frags?(elem, sg)) &&
- (fr = valid_time_frags?(elem))
- raise ArgumentError, 'invalid date'
- end
- fr += (elem[:sec_fraction] || 0) / 86400
- of = Rational(elem[:offset] || 0, 86400)
- new!(jd_to_ajd(jd, fr, of), of, sg)
- end
-
- private_class_method :new_by_frags
-
- # Create a new DateTime object by parsing from a String
- # according to a specified format.
- #
- # +str+ is a String holding a date-time representation.
- # +fmt+ is the format that the date-time is in. See
- # date/format.rb for details on supported formats.
- #
- # The default +str+ is '-4712-01-01T00:00:00+00:00', and the default
- # +fmt+ is '%FT%T%z'. This gives midnight on Julian Day Number day 0.
- #
- # +sg+ specifies the Day of Calendar Reform.
- #
- # An ArgumentError will be raised if +str+ cannot be
- # parsed.
- def self.strptime(str='-4712-01-01T00:00:00+00:00', fmt='%FT%T%z', sg=ITALY)
- elem = _strptime(str, fmt)
- new_by_frags(elem, sg)
- end
-
- # Create a new DateTime object by parsing from a String,
- # without specifying the format.
- #
- # +str+ is a String holding a date-time representation.
- # +comp+ specifies whether to interpret 2-digit years
- # as 19XX (>= 69) or 20XX (< 69); the default is not to.
- # The method will attempt to parse a date-time from the String
- # using various heuristics; see #_parse in date/format.rb
- # for more details. If parsing fails, an ArgumentError
- # will be raised.
- #
- # The default +str+ is '-4712-01-01T00:00:00+00:00'; this is Julian
- # Day Number day 0.
- #
- # +sg+ specifies the Day of Calendar Reform.
- def self.parse(str='-4712-01-01T00:00:00+00:00', comp=true, sg=ITALY)
- elem = _parse(str, comp)
- new_by_frags(elem, sg)
- end
-
- def self.iso8601(str='-4712-01-01T00:00:00+00:00', sg=ITALY) # :nodoc:
- elem = _iso8601(str)
- new_by_frags(elem, sg)
- end
-
- def self.rfc3339(str='-4712-01-01T00:00:00+00:00', sg=ITALY) # :nodoc:
- elem = _rfc3339(str)
- new_by_frags(elem, sg)
- end
-
- def self.xmlschema(str='-4712-01-01T00:00:00+00:00', sg=ITALY) # :nodoc:
- elem = _xmlschema(str)
- new_by_frags(elem, sg)
- end
-
- def self.rfc2822(str='Mon, 1 Jan -4712 00:00:00 +0000', sg=ITALY) # :nodoc:
- elem = _rfc2822(str)
- new_by_frags(elem, sg)
- end
-
- class << self; alias_method :rfc822, :rfc2822 end
-
- def self.httpdate(str='Mon, 01 Jan -4712 00:00:00 GMT', sg=ITALY) # :nodoc:
- elem = _httpdate(str)
- new_by_frags(elem, sg)
- end
-
- def self.jisx0301(str='-4712-01-01T00:00:00+00:00', sg=ITALY) # :nodoc:
- elem = _jisx0301(str)
- new_by_frags(elem, sg)
- end
-
- public :hour, :min, :sec, :sec_fraction, :zone, :offset, :new_offset,
- :minute, :second, :second_fraction
-
- def to_s # 4p
- format('%.4d-%02d-%02dT%02d:%02d:%02d%s',
- year, mon, mday, hour, min, sec, zone)
- end
-
-end
-
-class Time
-
- def to_time() getlocal end
-
- def to_date
- jd = Date.__send__(:civil_to_jd, year, mon, mday, Date::ITALY)
- Date.new!(Date.__send__(:jd_to_ajd, jd, 0, 0), 0, Date::ITALY)
- end
-
- def to_datetime
- jd = DateTime.__send__(:civil_to_jd, year, mon, mday, DateTime::ITALY)
- fr = DateTime.__send__(:time_to_day_fraction, hour, min, [sec, 59].min) +
- Rational(nsec, 86400_000_000_000)
- of = Rational(utc_offset, 86400)
- DateTime.new!(DateTime.__send__(:jd_to_ajd, jd, fr, of),
- of, DateTime::ITALY)
- end
-
-end
-
-class Date
-
- def to_time() Time.local(year, mon, mday) end
- def to_date() self end
- def to_datetime() DateTime.new!(jd_to_ajd(jd, 0, 0), @of, @sg) end
-
- # Create a new Date object representing today.
- #
- # +sg+ specifies the Day of Calendar Reform.
- def self.today(sg=ITALY)
- t = Time.now
- jd = civil_to_jd(t.year, t.mon, t.mday, sg)
- new!(jd_to_ajd(jd, 0, 0), 0, sg)
- end
-
- # Create a new DateTime object representing the current time.
- #
- # +sg+ specifies the Day of Calendar Reform.
- def self.now(sg=ITALY)
- t = Time.now
- jd = civil_to_jd(t.year, t.mon, t.mday, sg)
- fr = time_to_day_fraction(t.hour, t.min, [t.sec, 59].min) +
- Rational(t.nsec, 86400_000_000_000)
- of = Rational(t.utc_offset, 86400)
- new!(jd_to_ajd(jd, fr, of), of, sg)
- end
-
- private_class_method :now
-
-end
-
-class DateTime < Date
-
- def to_time
- d = new_offset(0)
- d.instance_eval do
- Time.utc(year, mon, mday, hour, min, sec +
- sec_fraction)
- end.
- getlocal
- end
-
- def to_date() Date.new!(jd_to_ajd(jd, 0, 0), 0, @sg) end
- def to_datetime() self end
-
- private_class_method :today
- public_class_method :now
-
-end
diff --git a/lib/date/format.rb b/lib/date/format.rb
deleted file mode 100644
index a83b29802e..0000000000
--- a/lib/date/format.rb
+++ /dev/null
@@ -1,1313 +0,0 @@
-# format.rb: Written by Tadayoshi Funaba 1999-2008
-# $Id: format.rb,v 2.43 2008-01-17 20:16:31+09 tadf Exp $
-
-class Date
-
- module Format # :nodoc:
-
- MONTHS = {
- 'january' => 1, 'february' => 2, 'march' => 3, 'april' => 4,
- 'may' => 5, 'june' => 6, 'july' => 7, 'august' => 8,
- 'september'=> 9, 'october' =>10, 'november' =>11, 'december' =>12
- }
-
- DAYS = {
- 'sunday' => 0, 'monday' => 1, 'tuesday' => 2, 'wednesday'=> 3,
- 'thursday' => 4, 'friday' => 5, 'saturday' => 6
- }
-
- ABBR_MONTHS = {
- 'jan' => 1, 'feb' => 2, 'mar' => 3, 'apr' => 4,
- 'may' => 5, 'jun' => 6, 'jul' => 7, 'aug' => 8,
- 'sep' => 9, 'oct' =>10, 'nov' =>11, 'dec' =>12
- }
-
- ABBR_DAYS = {
- 'sun' => 0, 'mon' => 1, 'tue' => 2, 'wed' => 3,
- 'thu' => 4, 'fri' => 5, 'sat' => 6
- }
-
- ZONES = {
- 'ut' => 0*3600, 'gmt' => 0*3600, 'est' => -5*3600, 'edt' => -4*3600,
- 'cst' => -6*3600, 'cdt' => -5*3600, 'mst' => -7*3600, 'mdt' => -6*3600,
- 'pst' => -8*3600, 'pdt' => -7*3600,
- 'a' => 1*3600, 'b' => 2*3600, 'c' => 3*3600, 'd' => 4*3600,
- 'e' => 5*3600, 'f' => 6*3600, 'g' => 7*3600, 'h' => 8*3600,
- 'i' => 9*3600, 'k' => 10*3600, 'l' => 11*3600, 'm' => 12*3600,
- 'n' => -1*3600, 'o' => -2*3600, 'p' => -3*3600, 'q' => -4*3600,
- 'r' => -5*3600, 's' => -6*3600, 't' => -7*3600, 'u' => -8*3600,
- 'v' => -9*3600, 'w' =>-10*3600, 'x' =>-11*3600, 'y' =>-12*3600,
- 'z' => 0*3600,
-
- 'utc' => 0*3600, 'wet' => 0*3600,
- 'at' => -2*3600, 'brst'=> -2*3600, 'ndt' => -(2*3600+1800),
- 'art' => -3*3600, 'adt' => -3*3600, 'brt' => -3*3600, 'clst'=> -3*3600,
- 'nst' => -(3*3600+1800),
- 'ast' => -4*3600, 'clt' => -4*3600,
- 'akdt'=> -8*3600, 'ydt' => -8*3600,
- 'akst'=> -9*3600, 'hadt'=> -9*3600, 'hdt' => -9*3600, 'yst' => -9*3600,
- 'ahst'=>-10*3600, 'cat' =>-10*3600, 'hast'=>-10*3600, 'hst' =>-10*3600,
- 'nt' =>-11*3600,
- 'idlw'=>-12*3600,
- 'bst' => 1*3600, 'cet' => 1*3600, 'fwt' => 1*3600, 'met' => 1*3600,
- 'mewt'=> 1*3600, 'mez' => 1*3600, 'swt' => 1*3600, 'wat' => 1*3600,
- 'west'=> 1*3600,
- 'cest'=> 2*3600, 'eet' => 2*3600, 'fst' => 2*3600, 'mest'=> 2*3600,
- 'mesz'=> 2*3600, 'sast'=> 2*3600, 'sst' => 2*3600,
- 'bt' => 3*3600, 'eat' => 3*3600, 'eest'=> 3*3600, 'msk' => 3*3600,
- 'msd' => 4*3600, 'zp4' => 4*3600,
- 'zp5' => 5*3600, 'ist' => (5*3600+1800),
- 'zp6' => 6*3600,
- 'wast'=> 7*3600,
- 'cct' => 8*3600, 'sgt' => 8*3600, 'wadt'=> 8*3600,
- 'jst' => 9*3600, 'kst' => 9*3600,
- 'east'=> 10*3600, 'gst' => 10*3600,
- 'eadt'=> 11*3600,
- 'idle'=> 12*3600, 'nzst'=> 12*3600, 'nzt' => 12*3600,
- 'nzdt'=> 13*3600,
-
- 'afghanistan' => 16200, 'alaskan' => -32400,
- 'arab' => 10800, 'arabian' => 14400,
- 'arabic' => 10800, 'atlantic' => -14400,
- 'aus central' => 34200, 'aus eastern' => 36000,
- 'azores' => -3600, 'canada central' => -21600,
- 'cape verde' => -3600, 'caucasus' => 14400,
- 'cen. australia' => 34200, 'central america' => -21600,
- 'central asia' => 21600, 'central europe' => 3600,
- 'central european' => 3600, 'central pacific' => 39600,
- 'central' => -21600, 'china' => 28800,
- 'dateline' => -43200, 'e. africa' => 10800,
- 'e. australia' => 36000, 'e. europe' => 7200,
- 'e. south america' => -10800, 'eastern' => -18000,
- 'egypt' => 7200, 'ekaterinburg' => 18000,
- 'fiji' => 43200, 'fle' => 7200,
- 'greenland' => -10800, 'greenwich' => 0,
- 'gtb' => 7200, 'hawaiian' => -36000,
- 'india' => 19800, 'iran' => 12600,
- 'jerusalem' => 7200, 'korea' => 32400,
- 'mexico' => -21600, 'mid-atlantic' => -7200,
- 'mountain' => -25200, 'myanmar' => 23400,
- 'n. central asia' => 21600, 'nepal' => 20700,
- 'new zealand' => 43200, 'newfoundland' => -12600,
- 'north asia east' => 28800, 'north asia' => 25200,
- 'pacific sa' => -14400, 'pacific' => -28800,
- 'romance' => 3600, 'russian' => 10800,
- 'sa eastern' => -10800, 'sa pacific' => -18000,
- 'sa western' => -14400, 'samoa' => -39600,
- 'se asia' => 25200, 'malay peninsula' => 28800,
- 'south africa' => 7200, 'sri lanka' => 21600,
- 'taipei' => 28800, 'tasmania' => 36000,
- 'tokyo' => 32400, 'tonga' => 46800,
- 'us eastern' => -18000, 'us mountain' => -25200,
- 'vladivostok' => 36000, 'w. australia' => 28800,
- 'w. central africa' => 3600, 'w. europe' => 3600,
- 'west asia' => 18000, 'west pacific' => 36000,
- 'yakutsk' => 32400
- }
-
- [MONTHS, DAYS, ABBR_MONTHS, ABBR_DAYS, ZONES].each do |x|
- x.freeze
- end
-
- class Bag # :nodoc:
-
- def initialize
- @elem = {}
- end
-
- def method_missing(t, *args, &block)
- t = t.to_s
- set = t.chomp!('=')
- t = t.intern
- if set
- @elem[t] = args[0]
- else
- @elem[t]
- end
- end
-
- def to_hash
- @elem.reject{|k, v| /\A_/ =~ k.to_s || v.nil?}
- end
-
- end
-
- end
-
- def emit(e, f) # :nodoc:
- case e
- when Numeric
- sign = %w(+ + -)[e <=> 0]
- e = e.abs
- end
-
- s = e.to_s
-
- if f[:s] && f[:p] == '0'
- f[:w] -= 1
- end
-
- if f[:s] && f[:p] == "\s"
- s[0,0] = sign
- end
-
- if f[:p] != '-'
- s = s.rjust(f[:w], f[:p])
- end
-
- if f[:s] && f[:p] != "\s"
- s[0,0] = sign
- end
-
- s = s.upcase if f[:u]
- s = s.downcase if f[:d]
- s
- end
-
- def emit_w(e, w, f) # :nodoc:
- f[:w] = [f[:w], w].compact.max
- emit(e, f)
- end
-
- def emit_n(e, w, f) # :nodoc:
- f[:p] ||= '0'
- emit_w(e, w, f)
- end
-
- def emit_sn(e, w, f) # :nodoc:
- if e < 0
- w += 1
- f[:s] = true
- end
- emit_n(e, w, f)
- end
-
- def emit_z(e, w, f) # :nodoc:
- w += 1
- f[:s] = true
- emit_n(e, w, f)
- end
-
- def emit_a(e, w, f) # :nodoc:
- f[:p] ||= "\s"
- emit_w(e, w, f)
- end
-
- def emit_ad(e, w, f) # :nodoc:
- if f[:x]
- f[:u] = true
- f[:d] = false
- end
- emit_a(e, w, f)
- end
-
- def emit_au(e, w, f) # :nodoc:
- if f[:x]
- f[:u] = false
- f[:d] = true
- end
- emit_a(e, w, f)
- end
-
- private :emit, :emit_w, :emit_n, :emit_sn, :emit_z,
- :emit_a, :emit_ad, :emit_au
-
- def strftime(fmt='%F')
- fmt.gsub(/%([-_0^#]+)?(\d+)?([EO]?(?::{1,3}z|.))/m) do
- f = {}
- m = $&
- s, w, c = $1, $2, $3
- if s
- s.scan(/./) do |k|
- case k
- when '-'; f[:p] = '-'
- when '_'; f[:p] = "\s"
- when '0'; f[:p] = '0'
- when '^'; f[:u] = true
- when '#'; f[:x] = true
- end
- end
- end
- if w
- f[:w] = w.to_i
- end
- case c
- when 'A'; emit_ad(DAYNAMES[wday], 0, f)
- when 'a'; emit_ad(ABBR_DAYNAMES[wday], 0, f)
- when 'B'; emit_ad(MONTHNAMES[mon], 0, f)
- when 'b'; emit_ad(ABBR_MONTHNAMES[mon], 0, f)
- when 'C', 'EC'; emit_sn((year / 100).floor, 2, f)
- when 'c', 'Ec'; emit_a(strftime('%a %b %e %H:%M:%S %Y'), 0, f)
- when 'D'; emit_a(strftime('%m/%d/%y'), 0, f)
- when 'd', 'Od'; emit_n(mday, 2, f)
- when 'e', 'Oe'; emit_a(mday, 2, f)
- when 'F'
- if m == '%F'
- format('%.4d-%02d-%02d', year, mon, mday) # 4p
- else
- emit_a(strftime('%Y-%m-%d'), 0, f)
- end
- when 'G'; emit_sn(cwyear, 4, f)
- when 'g'; emit_n(cwyear % 100, 2, f)
- when 'H', 'OH'; emit_n(hour, 2, f)
- when 'h'; emit_ad(strftime('%b'), 0, f)
- when 'I', 'OI'; emit_n((hour % 12).nonzero? || 12, 2, f)
- when 'j'; emit_n(yday, 3, f)
- when 'k'; emit_a(hour, 2, f)
- when 'L'
- f[:p] = nil
- w = f[:w] || 3
- u = 10**w
- emit_n((sec_fraction * u).floor, w, f)
- when 'l'; emit_a((hour % 12).nonzero? || 12, 2, f)
- when 'M', 'OM'; emit_n(min, 2, f)
- when 'm', 'Om'; emit_n(mon, 2, f)
- when 'N'
- f[:p] = nil
- w = f[:w] || 9
- u = 10**w
- emit_n((sec_fraction * u).floor, w, f)
- when 'n'; emit_a("\n", 0, f)
- when 'P'; emit_ad(strftime('%p').downcase, 0, f)
- when 'p'; emit_au(if hour < 12 then 'AM' else 'PM' end, 0, f)
- when 'Q'
- s = ((ajd - UNIX_EPOCH_IN_AJD) / MILLISECONDS_IN_DAY).round
- emit_sn(s, 1, f)
- when 'R'; emit_a(strftime('%H:%M'), 0, f)
- when 'r'; emit_a(strftime('%I:%M:%S %p'), 0, f)
- when 'S', 'OS'; emit_n(sec, 2, f)
- when 's'
- s = ((ajd - UNIX_EPOCH_IN_AJD) / SECONDS_IN_DAY).round
- emit_sn(s, 1, f)
- when 'T'
- if m == '%T'
- format('%02d:%02d:%02d', hour, min, sec) # 4p
- else
- emit_a(strftime('%H:%M:%S'), 0, f)
- end
- when 't'; emit_a("\t", 0, f)
- when 'U', 'W', 'OU', 'OW'
- emit_n(if c[-1,1] == 'U' then wnum0 else wnum1 end, 2, f)
- when 'u', 'Ou'; emit_n(cwday, 1, f)
- when 'V', 'OV'; emit_n(cweek, 2, f)
- when 'v'; emit_a(strftime('%e-%b-%Y'), 0, f)
- when 'w', 'Ow'; emit_n(wday, 1, f)
- when 'X', 'EX'; emit_a(strftime('%H:%M:%S'), 0, f)
- when 'x', 'Ex'; emit_a(strftime('%m/%d/%y'), 0, f)
- when 'Y', 'EY'; emit_sn(year, 4, f)
- when 'y', 'Ey', 'Oy'; emit_n(year % 100, 2, f)
- when 'Z'; emit_au(strftime('%:z'), 0, f)
- when /\A(:{0,3})z/
- t = $1.size
- sign = if offset < 0 then -1 else +1 end
- fr = offset.abs
- ss = fr.div(SECONDS_IN_DAY) # 4p
- hh, ss = ss.divmod(3600)
- mm, ss = ss.divmod(60)
- if t == 3
- if ss.nonzero? then t = 2
- elsif mm.nonzero? then t = 1
- else t = -1
- end
- end
- case t
- when -1
- tail = []
- sep = ''
- when 0
- f[:w] -= 2 if f[:w]
- tail = ['%02d' % mm]
- sep = ''
- when 1
- f[:w] -= 3 if f[:w]
- tail = ['%02d' % mm]
- sep = ':'
- when 2
- f[:w] -= 6 if f[:w]
- tail = ['%02d' % mm, '%02d' % ss]
- sep = ':'
- end
- ([emit_z(sign * hh, 2, f)] + tail).join(sep)
- when '%'; emit_a('%', 0, f)
- when '+'; emit_a(strftime('%a %b %e %H:%M:%S %Z %Y'), 0, f)
- else
- m
- end
- end
- end
-
-# alias_method :format, :strftime
-
- def asctime() strftime('%c') end
-
- alias_method :ctime, :asctime
-
- def iso8601() strftime('%F') end
-
- def rfc3339() iso8601 end
-
- def xmlschema() iso8601 end # :nodoc:
-
- def rfc2822() strftime('%a, %-d %b %Y %T %z') end
-
- alias_method :rfc822, :rfc2822
-
- def httpdate() new_offset(0).strftime('%a, %d %b %Y %T GMT') end # :nodoc:
-
- def jisx0301
- if jd < 2405160
- iso8601
- else
- case jd
- when 2405160...2419614
- g = 'M%02d' % (year - 1867)
- when 2419614...2424875
- g = 'T%02d' % (year - 1911)
- when 2424875...2447535
- g = 'S%02d' % (year - 1925)
- else
- g = 'H%02d' % (year - 1988)
- end
- g + strftime('.%m.%d')
- end
- end
-
-=begin
- def beat(n=0)
- i, f = (new_offset(HOURS_IN_DAY).day_fraction * 1000).divmod(1)
- ('@%03d' % i) +
- if n < 1
- ''
- else
- '.%0*d' % [n, (f / Rational(1, 10**n)).round]
- end
- end
-=end
-
- def self.num_pattern? (s) # :nodoc:
- /\A%[EO]?[CDdeFGgHIjkLlMmNQRrSsTUuVvWwXxYy\d]/ =~ s || /\A\d/ =~ s
- end
-
- private_class_method :num_pattern?
-
- def self._strptime_i(str, fmt, e) # :nodoc:
- fmt.scan(/%([EO]?(?::{1,3}z|.))|(.)/m) do |s, c|
- a = $&
- if s
- case s
- when 'A', 'a'
- return unless str.sub!(/\A(#{Format::DAYS.keys.join('|')})/io, '') ||
- str.sub!(/\A(#{Format::ABBR_DAYS.keys.join('|')})/io, '')
- val = Format::DAYS[$1.downcase] || Format::ABBR_DAYS[$1.downcase]
- return unless val
- e.wday = val
- when 'B', 'b', 'h'
- return unless str.sub!(/\A(#{Format::MONTHS.keys.join('|')})/io, '') ||
- str.sub!(/\A(#{Format::ABBR_MONTHS.keys.join('|')})/io, '')
- val = Format::MONTHS[$1.downcase] || Format::ABBR_MONTHS[$1.downcase]
- return unless val
- e.mon = val
- when 'C', 'EC'
- return unless str.sub!(if num_pattern?($')
- then /\A([-+]?\d{1,2})/
- else /\A([-+]?\d{1,})/
- end, '')
- val = $1.to_i
- e._cent = val
- when 'c', 'Ec'
- return unless _strptime_i(str, '%a %b %e %H:%M:%S %Y', e)
- when 'D'
- return unless _strptime_i(str, '%m/%d/%y', e)
- when 'd', 'e', 'Od', 'Oe'
- return unless str.sub!(/\A( \d|\d{1,2})/, '')
- val = $1.to_i
- return unless (1..31) === val
- e.mday = val
- when 'F'
- return unless _strptime_i(str, '%Y-%m-%d', e)
- when 'G'
- return unless str.sub!(if num_pattern?($')
- then /\A([-+]?\d{1,4})/
- else /\A([-+]?\d{1,})/
- end, '')
- val = $1.to_i
- e.cwyear = val
- when 'g'
- return unless str.sub!(/\A(\d{1,2})/, '')
- val = $1.to_i
- return unless (0..99) === val
- e.cwyear = val
- e._cent ||= if val >= 69 then 19 else 20 end
- when 'H', 'k', 'OH'
- return unless str.sub!(/\A( \d|\d{1,2})/, '')
- val = $1.to_i
- return unless (0..24) === val
- e.hour = val
- when 'I', 'l', 'OI'
- return unless str.sub!(/\A( \d|\d{1,2})/, '')
- val = $1.to_i
- return unless (1..12) === val
- e.hour = val
- when 'j'
- return unless str.sub!(/\A(\d{1,3})/, '')
- val = $1.to_i
- return unless (1..366) === val
- e.yday = val
- when 'L'
- return unless str.sub!(if num_pattern?($')
- then /\A([-+]?\d{1,3})/
- else /\A([-+]?\d{1,})/
- end, '')
-# val = Rational($1.to_i, 10**3)
- val = Rational($1.to_i, 10**$1.size)
- e.sec_fraction = val
- when 'M', 'OM'
- return unless str.sub!(/\A(\d{1,2})/, '')
- val = $1.to_i
- return unless (0..59) === val
- e.min = val
- when 'm', 'Om'
- return unless str.sub!(/\A(\d{1,2})/, '')
- val = $1.to_i
- return unless (1..12) === val
- e.mon = val
- when 'N'
- return unless str.sub!(if num_pattern?($')
- then /\A([-+]?\d{1,9})/
- else /\A([-+]?\d{1,})/
- end, '')
-# val = Rational($1.to_i, 10**9)
- val = Rational($1.to_i, 10**$1.size)
- e.sec_fraction = val
- when 'n', 't'
- return unless _strptime_i(str, "\s", e)
- when 'P', 'p'
- return unless str.sub!(/\A([ap])(?:m\b|\.m\.)/i, '')
- e._merid = if $1.downcase == 'a' then 0 else 12 end
- when 'Q'
- return unless str.sub!(/\A(-?\d{1,})/, '')
- val = Rational($1.to_i, 10**3)
- e.seconds = val
- when 'R'
- return unless _strptime_i(str, '%H:%M', e)
- when 'r'
- return unless _strptime_i(str, '%I:%M:%S %p', e)
- when 'S', 'OS'
- return unless str.sub!(/\A(\d{1,2})/, '')
- val = $1.to_i
- return unless (0..60) === val
- e.sec = val
- when 's'
- return unless str.sub!(/\A(-?\d{1,})/, '')
- val = $1.to_i
- e.seconds = val
- when 'T'
- return unless _strptime_i(str, '%H:%M:%S', e)
- when 'U', 'W', 'OU', 'OW'
- return unless str.sub!(/\A(\d{1,2})/, '')
- val = $1.to_i
- return unless (0..53) === val
- e.__send__(if s[-1,1] == 'U' then :wnum0= else :wnum1= end, val)
- when 'u', 'Ou'
- return unless str.sub!(/\A(\d{1})/, '')
- val = $1.to_i
- return unless (1..7) === val
- e.cwday = val
- when 'V', 'OV'
- return unless str.sub!(/\A(\d{1,2})/, '')
- val = $1.to_i
- return unless (1..53) === val
- e.cweek = val
- when 'v'
- return unless _strptime_i(str, '%e-%b-%Y', e)
- when 'w'
- return unless str.sub!(/\A(\d{1})/, '')
- val = $1.to_i
- return unless (0..6) === val
- e.wday = val
- when 'X', 'EX'
- return unless _strptime_i(str, '%H:%M:%S', e)
- when 'x', 'Ex'
- return unless _strptime_i(str, '%m/%d/%y', e)
- when 'Y', 'EY'
- return unless str.sub!(if num_pattern?($')
- then /\A([-+]?\d{1,4})/
- else /\A([-+]?\d{1,})/
- end, '')
- val = $1.to_i
- e.year = val
- when 'y', 'Ey', 'Oy'
- return unless str.sub!(/\A(\d{1,2})/, '')
- val = $1.to_i
- return unless (0..99) === val
- e.year = val
- e._cent ||= if val >= 69 then 19 else 20 end
- when 'Z', /\A:{0,3}z/
- return unless str.sub!(/\A((?:gmt|utc?)?[-+]\d+(?:[,.:]\d+(?::\d+)?)?
- |[[:alpha:].\s]+(?:standard|daylight)\s+time\b
- |[[:alpha:]]+(?:\s+dst)?\b
- )/ix, '')
- val = $1
- e.zone = val
- offset = zone_to_diff(val)
- e.offset = offset
- when '%'
- return unless str.sub!(/\A%/, '')
- when '+'
- return unless _strptime_i(str, '%a %b %e %H:%M:%S %Z %Y', e)
- else
- return unless str.sub!(Regexp.new('\\A' + Regexp.quote(a)), '')
- end
- else
- case c
- when /\A[\s\v]/
- str.sub!(/\A[\s\v]+/, '')
- else
- return unless str.sub!(Regexp.new('\\A' + Regexp.quote(a)), '')
- end
- end
- end
- end
-
- private_class_method :_strptime_i
-
- def self._strptime(str, fmt='%F')
- str = str.dup
- e = Format::Bag.new
- return unless _strptime_i(str, fmt, e)
-
- if e._cent
- if e.cwyear
- e.cwyear += e._cent * 100
- end
- if e.year
- e. year += e._cent * 100
- end
- end
-
- if e._merid
- if e.hour
- e.hour %= 12
- e.hour += e._merid
- end
- end
-
- unless str.empty?
- e.leftover = str
- end
-
- e.to_hash
- end
-
- def self.s3e(e, y, m, d, bc=false)
- unless String === m
- m = m.to_s
- end
-
- if y && m && !d
- y, m, d = d, y, m
- end
-
- if y == nil
- if d && d.size > 2
- y = d
- d = nil
- end
- if d && d[0,1] == "'"
- y = d
- d = nil
- end
- end
-
- if y
- y.scan(/(\d+)(.+)?/)
- if $2
- y, d = d, $1
- end
- end
-
- if m
- if m[0,1] == "'" || m.size > 2
- y, m, d = m, d, y # us -> be
- end
- end
-
- if d
- if d[0,1] == "'" || d.size > 2
- y, d = d, y
- end
- end
-
- if y
- y =~ /([-+])?(\d+)/
- if $1 || $2.size > 2
- c = false
- end
- iy = $&.to_i
- if bc
- iy = -iy + 1
- end
- e.year = iy
- end
-
- if m
- m =~ /\d+/
- e.mon = $&.to_i
- end
-
- if d
- d =~ /\d+/
- e.mday = $&.to_i
- end
-
- if c != nil
- e._comp = c
- end
-
- end
-
- private_class_method :s3e
-
- def self._parse_day(str, e) # :nodoc:
- if str.sub!(/\b(#{Format::ABBR_DAYS.keys.join('|')})[^-\d\s]*/io, ' ')
- e.wday = Format::ABBR_DAYS[$1.downcase]
- true
-=begin
- elsif str.sub!(/\b(?!\dth)(su|mo|tu|we|th|fr|sa)\b/i, ' ')
- e.wday = %w(su mo tu we th fr sa).index($1.downcase)
- true
-=end
- end
- end
-
- def self._parse_time(str, e) # :nodoc:
- if str.sub!(
- /(
- (?:
- \d+\s*:\s*\d+
- (?:
- \s*:\s*\d+(?:[,.]\d*)?
- )?
- |
- \d+\s*h(?:\s*\d+m?(?:\s*\d+s?)?)?
- )
- (?:
- \s*
- [ap](?:m\b|\.m\.)
- )?
- |
- \d+\s*[ap](?:m\b|\.m\.)
- )
- (?:
- \s*
- (
- (?:gmt|utc?)?[-+]\d+(?:[,.:]\d+(?::\d+)?)?
- |
- [[:alpha:].\s]+(?:standard|daylight)\stime\b
- |
- [[:alpha:]]+(?:\sdst)?\b
- )
- )?
- /ix,
- ' ')
-
- t = $1
- e.zone = $2 if $2
-
- t =~ /\A(\d+)h?
- (?:\s*:?\s*(\d+)m?
- (?:
- \s*:?\s*(\d+)(?:[,.](\d+))?s?
- )?
- )?
- (?:\s*([ap])(?:m\b|\.m\.))?/ix
-
- e.hour = $1.to_i
- e.min = $2.to_i if $2
- e.sec = $3.to_i if $3
- e.sec_fraction = Rational($4.to_i, 10**$4.size) if $4
-
- if $5
- e.hour %= 12
- if $5.downcase == 'p'
- e.hour += 12
- end
- end
- true
- end
- end
-
-=begin
- def self._parse_beat(str, e) # :nodoc:
- if str.sub!(/@\s*(\d+)(?:[,.](\d*))?/, ' ')
- beat = Rational($1.to_i)
- beat += Rational($2.to_i, 10**$2.size) if $2
- secs = Rational(beat, 1000)
- h, min, s, fr = self.day_fraction_to_time(secs)
- e.hour = h
- e.min = min
- e.sec = s
- e.sec_fraction = fr * 86400
- e.zone = '+01:00'
- true
- end
- end
-=end
-
- def self._parse_eu(str, e) # :nodoc:
- if str.sub!(
- /'?(\d+)[^-\d\s]*
- \s*
- (#{Format::ABBR_MONTHS.keys.join('|')})[^-\d\s']*
- (?:
- \s*
- (c(?:e|\.e\.)|b(?:ce|\.c\.e\.)|a(?:d|\.d\.)|b(?:c|\.c\.))?
- \s*
- ('?-?\d+(?:(?:st|nd|rd|th)\b)?)
- )?
- /iox,
- ' ') # '
- s3e(e, $4, Format::ABBR_MONTHS[$2.downcase], $1,
- $3 && $3[0,1].downcase == 'b')
- true
- end
- end
-
- def self._parse_us(str, e) # :nodoc:
- if str.sub!(
- /\b(#{Format::ABBR_MONTHS.keys.join('|')})[^-\d\s']*
- \s*
- ('?\d+)[^-\d\s']*
- (?:
- \s*
- (c(?:e|\.e\.)|b(?:ce|\.c\.e\.)|a(?:d|\.d\.)|b(?:c|\.c\.))?
- \s*
- ('?-?\d+)
- )?
- /iox,
- ' ') # '
- s3e(e, $4, Format::ABBR_MONTHS[$1.downcase], $2,
- $3 && $3[0,1].downcase == 'b')
- true
- end
- end
-
- def self._parse_iso(str, e) # :nodoc:
- if str.sub!(/('?[-+]?\d+)-(\d+)-('?-?\d+)/, ' ')
- s3e(e, $1, $2, $3)
- true
- end
- end
-
- def self._parse_iso2(str, e) # :nodoc:
- if str.sub!(/\b(\d{2}|\d{4})?-?w(\d{2})(?:-?(\d))?\b/i, ' ')
- e.cwyear = $1.to_i if $1
- e.cweek = $2.to_i
- e.cwday = $3.to_i if $3
- true
- elsif str.sub!(/-w-(\d)\b/i, ' ')
- e.cwday = $1.to_i
- true
- elsif str.sub!(/--(\d{2})?-(\d{2})\b/, ' ')
- e.mon = $1.to_i if $1
- e.mday = $2.to_i
- true
- elsif str.sub!(/--(\d{2})(\d{2})?\b/, ' ')
- e.mon = $1.to_i
- e.mday = $2.to_i if $2
- true
- elsif /[,.](\d{2}|\d{4})-\d{3}\b/ !~ str &&
- str.sub!(/\b(\d{2}|\d{4})-(\d{3})\b/, ' ')
- e.year = $1.to_i
- e.yday = $2.to_i
- true
- elsif /\d-\d{3}\b/ !~ str &&
- str.sub!(/\b-(\d{3})\b/, ' ')
- e.yday = $1.to_i
- true
- end
- end
-
- def self._parse_jis(str, e) # :nodoc:
- if str.sub!(/\b([mtsh])(\d+)\.(\d+)\.(\d+)/i, ' ')
- era = { 'm'=>1867,
- 't'=>1911,
- 's'=>1925,
- 'h'=>1988
- }[$1.downcase]
- e.year = $2.to_i + era
- e.mon = $3.to_i
- e.mday = $4.to_i
- true
- end
- end
-
- def self._parse_vms(str, e) # :nodoc:
- if str.sub!(/('?-?\d+)-(#{Format::ABBR_MONTHS.keys.join('|')})[^-]*
- -('?-?\d+)/iox, ' ')
- s3e(e, $3, Format::ABBR_MONTHS[$2.downcase], $1)
- true
- elsif str.sub!(/\b(#{Format::ABBR_MONTHS.keys.join('|')})[^-]*
- -('?-?\d+)(?:-('?-?\d+))?/iox, ' ')
- s3e(e, $3, Format::ABBR_MONTHS[$1.downcase], $2)
- true
- end
- end
-
- def self._parse_sla(str, e) # :nodoc:
- if str.sub!(%r|('?-?\d+)/\s*('?\d+)(?:\D\s*('?-?\d+))?|, ' ') # '
- s3e(e, $1, $2, $3)
- true
- end
- end
-
- def self._parse_dot(str, e) # :nodoc:
- if str.sub!(%r|('?-?\d+)\.\s*('?\d+)\.\s*('?-?\d+)|, ' ') # '
- s3e(e, $1, $2, $3)
- true
- end
- end
-
- def self._parse_year(str, e) # :nodoc:
- if str.sub!(/'(\d+)\b/, ' ')
- e.year = $1.to_i
- true
- end
- end
-
- def self._parse_mon(str, e) # :nodoc:
- if str.sub!(/\b(#{Format::ABBR_MONTHS.keys.join('|')})\S*/io, ' ')
- e.mon = Format::ABBR_MONTHS[$1.downcase]
- true
- end
- end
-
- def self._parse_mday(str, e) # :nodoc:
- if str.sub!(/(\d+)(st|nd|rd|th)\b/i, ' ')
- e.mday = $1.to_i
- true
- end
- end
-
- def self._parse_ddd(str, e) # :nodoc:
- if str.sub!(
- /([-+]?)(\d{2,14})
- (?:
- \s*
- t?
- \s*
- (\d{2,6})?(?:[,.](\d*))?
- )?
- (?:
- \s*
- (
- z\b
- |
- [-+]\d{1,4}\b
- |
- \[[-+]?\d[^\]]*\]
- )
- )?
- /ix,
- ' ')
- case $2.size
- when 2
- if $3.nil? && $4
- e.sec = $2[-2, 2].to_i
- else
- e.mday = $2[ 0, 2].to_i
- end
- when 4
- if $3.nil? && $4
- e.sec = $2[-2, 2].to_i
- e.min = $2[-4, 2].to_i
- else
- e.mon = $2[ 0, 2].to_i
- e.mday = $2[ 2, 2].to_i
- end
- when 6
- if $3.nil? && $4
- e.sec = $2[-2, 2].to_i
- e.min = $2[-4, 2].to_i
- e.hour = $2[-6, 2].to_i
- else
- e.year = ($1 + $2[ 0, 2]).to_i
- e.mon = $2[ 2, 2].to_i
- e.mday = $2[ 4, 2].to_i
- end
- when 8, 10, 12, 14
- if $3.nil? && $4
- e.sec = $2[-2, 2].to_i
- e.min = $2[-4, 2].to_i
- e.hour = $2[-6, 2].to_i
- e.mday = $2[-8, 2].to_i
- if $2.size >= 10
- e.mon = $2[-10, 2].to_i
- end
- if $2.size == 12
- e.year = ($1 + $2[-12, 2]).to_i
- end
- if $2.size == 14
- e.year = ($1 + $2[-14, 4]).to_i
- e._comp = false
- end
- else
- e.year = ($1 + $2[ 0, 4]).to_i
- e.mon = $2[ 4, 2].to_i
- e.mday = $2[ 6, 2].to_i
- e.hour = $2[ 8, 2].to_i if $2.size >= 10
- e.min = $2[10, 2].to_i if $2.size >= 12
- e.sec = $2[12, 2].to_i if $2.size >= 14
- e._comp = false
- end
- when 3
- if $3.nil? && $4
- e.sec = $2[-2, 2].to_i
- e.min = $2[-3, 1].to_i
- else
- e.yday = $2[ 0, 3].to_i
- end
- when 5
- if $3.nil? && $4
- e.sec = $2[-2, 2].to_i
- e.min = $2[-4, 2].to_i
- e.hour = $2[-5, 1].to_i
- else
- e.year = ($1 + $2[ 0, 2]).to_i
- e.yday = $2[ 2, 3].to_i
- end
- when 7
- if $3.nil? && $4
- e.sec = $2[-2, 2].to_i
- e.min = $2[-4, 2].to_i
- e.hour = $2[-6, 2].to_i
- e.mday = $2[-7, 1].to_i
- else
- e.year = ($1 + $2[ 0, 4]).to_i
- e.yday = $2[ 4, 3].to_i
- end
- end
- if $3
- if $4
- case $3.size
- when 2, 4, 6
- e.sec = $3[-2, 2].to_i
- e.min = $3[-4, 2].to_i if $3.size >= 4
- e.hour = $3[-6, 2].to_i if $3.size >= 6
- end
- else
- case $3.size
- when 2, 4, 6
- e.hour = $3[ 0, 2].to_i
- e.min = $3[ 2, 2].to_i if $3.size >= 4
- e.sec = $3[ 4, 2].to_i if $3.size >= 6
- end
- end
- end
- if $4
- e.sec_fraction = Rational($4.to_i, 10**$4.size)
- end
- if $5
- e.zone = $5
- if e.zone[0,1] == '['
- o, n, = e.zone[1..-2].split(':')
- e.zone = n || o
- if /\A\d/ =~ o
- o = format('+%s', o)
- end
- e.offset = zone_to_diff(o)
- end
- end
- true
- end
- end
-
- private_class_method :_parse_day, :_parse_time, # :_parse_beat,
- :_parse_eu, :_parse_us, :_parse_iso, :_parse_iso2,
- :_parse_jis, :_parse_vms, :_parse_sla, :_parse_dot,
- :_parse_year, :_parse_mon, :_parse_mday, :_parse_ddd
-
- def self._parse(str, comp=true)
- str = str.dup
-
- e = Format::Bag.new
-
- e._comp = comp
-
- str.gsub!(/[^-+',.\/:@[:alnum:]\[\]]+/, ' ')
-
- _parse_time(str, e) # || _parse_beat(str, e)
- _parse_day(str, e)
-
- _parse_eu(str, e) ||
- _parse_us(str, e) ||
- _parse_iso(str, e) ||
- _parse_jis(str, e) ||
- _parse_vms(str, e) ||
- _parse_sla(str, e) ||
- _parse_dot(str, e) ||
- _parse_iso2(str, e) ||
- _parse_year(str, e) ||
- _parse_mon(str, e) ||
- _parse_mday(str, e) ||
- _parse_ddd(str, e)
-
- if str.sub!(/\b(bc\b|bce\b|b\.c\.|b\.c\.e\.)/i, ' ')
- if e.year
- e.year = -e.year + 1
- end
- end
-
- if str.sub!(/\A\s*(\d{1,2})\s*\z/, ' ')
- if e.hour && !e.mday
- v = $1.to_i
- if (1..31) === v
- e.mday = v
- end
- end
- if e.mday && !e.hour
- v = $1.to_i
- if (0..24) === v
- e.hour = v
- end
- end
- end
-
- if e._comp
- if e.cwyear
- if e.cwyear >= 0 && e.cwyear <= 99
- e.cwyear += if e.cwyear >= 69
- then 1900 else 2000 end
- end
- end
- if e.year
- if e.year >= 0 && e.year <= 99
- e.year += if e.year >= 69
- then 1900 else 2000 end
- end
- end
- end
-
- e.offset ||= zone_to_diff(e.zone) if e.zone
-
- e.to_hash
- end
-
- def self._iso8601(str) # :nodoc:
- if /\A\s*(([-+]?\d{2,}|-)-\d{2}-\d{2}|
- ([-+]?\d{2,})?-\d{3}|
- (\d{2}|\d{4})?-w\d{2}-\d|
- -w-\d)
- (t
- \d{2}:\d{2}(:\d{2}([,.]\d+)?)?
- (z|[-+]\d{2}(:?\d{2})?)?)?\s*\z/ix =~ str
- _parse(str)
- elsif /\A\s*(([-+]?(\d{2}|\d{4})|--)\d{2}\d{2}|
- ([-+]?(\d{2}|\d{4}))?\d{3}|-\d{3}|
- (\d{2}|\d{4})?w\d{2}\d)
- (t?
- \d{2}\d{2}(\d{2}([,.]\d+)?)?
- (z|[-+]\d{2}(\d{2})?)?)?\s*\z/ix =~ str
- _parse(str)
- elsif /\A\s*(\d{2}:\d{2}(:\d{2}([,.]\d+)?)?
- (z|[-+]\d{2}(:?\d{2})?)?)?\s*\z/ix =~ str
- _parse(str)
- elsif /\A\s*(\d{2}\d{2}(\d{2}([,.]\d+)?)?
- (z|[-+]\d{2}(\d{2})?)?)?\s*\z/ix =~ str
- _parse(str)
- end
- end
-
- def self._rfc3339(str) # :nodoc:
- if /\A\s*-?\d{4}-\d{2}-\d{2} # allow minus, anyway
- (t|\s)
- \d{2}:\d{2}:\d{2}(\.\d+)?
- (z|[-+]\d{2}:\d{2})\s*\z/ix =~ str
- _parse(str)
- end
- end
-
- def self._xmlschema(str) # :nodoc:
- if /\A\s*(-?\d{4,})(?:-(\d{2})(?:-(\d{2}))?)?
- (?:t
- (\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?)?
- (z|[-+]\d{2}:\d{2})?\s*\z/ix =~ str
- e = Format::Bag.new
- e.year = $1.to_i
- e.mon = $2.to_i if $2
- e.mday = $3.to_i if $3
- e.hour = $4.to_i if $4
- e.min = $5.to_i if $5
- e.sec = $6.to_i if $6
- e.sec_fraction = Rational($7.to_i, 10**$7.size) if $7
- if $8
- e.zone = $8
- e.offset = zone_to_diff($8)
- end
- e.to_hash
- elsif /\A\s*(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?
- (z|[-+]\d{2}:\d{2})?\s*\z/ix =~ str
- e = Format::Bag.new
- e.hour = $1.to_i if $1
- e.min = $2.to_i if $2
- e.sec = $3.to_i if $3
- e.sec_fraction = Rational($4.to_i, 10**$4.size) if $4
- if $5
- e.zone = $5
- e.offset = zone_to_diff($5)
- end
- e.to_hash
- elsif /\A\s*(?:--(\d{2})(?:-(\d{2}))?|---(\d{2}))
- (z|[-+]\d{2}:\d{2})?\s*\z/ix =~ str
- e = Format::Bag.new
- e.mon = $1.to_i if $1
- e.mday = $2.to_i if $2
- e.mday = $3.to_i if $3
- if $4
- e.zone = $4
- e.offset = zone_to_diff($4)
- end
- e.to_hash
- end
- end
-
- def self._rfc2822(str) # :nodoc:
- if /\A\s*(?:(?:#{Format::ABBR_DAYS.keys.join('|')})\s*,\s+)?
- \d{1,2}\s+
- (?:#{Format::ABBR_MONTHS.keys.join('|')})\s+
- -?(\d{2,})\s+ # allow minus, anyway
- \d{2}:\d{2}(:\d{2})?\s*
- (?:[-+]\d{4}|ut|gmt|e[sd]t|c[sd]t|m[sd]t|p[sd]t|[a-ik-z])\s*\z/iox =~ str
- e = _parse(str, false)
- if $1.size < 4
- if e[:year] < 50
- e[:year] += 2000
- elsif e[:year] < 1000
- e[:year] += 1900
- end
- end
- e
- end
- end
-
- class << self; alias_method :_rfc822, :_rfc2822 end
-
- def self._httpdate(str) # :nodoc:
- if /\A\s*(#{Format::ABBR_DAYS.keys.join('|')})\s*,\s+
- \d{2}\s+
- (#{Format::ABBR_MONTHS.keys.join('|')})\s+
- -?\d{4}\s+ # allow minus, anyway
- \d{2}:\d{2}:\d{2}\s+
- gmt\s*\z/iox =~ str
- _rfc2822(str)
- elsif /\A\s*(#{Format::DAYS.keys.join('|')})\s*,\s+
- \d{2}\s*-\s*
- (#{Format::ABBR_MONTHS.keys.join('|')})\s*-\s*
- \d{2}\s+
- \d{2}:\d{2}:\d{2}\s+
- gmt\s*\z/iox =~ str
- _parse(str)
- elsif /\A\s*(#{Format::ABBR_DAYS.keys.join('|')})\s+
- (#{Format::ABBR_MONTHS.keys.join('|')})\s+
- \d{1,2}\s+
- \d{2}:\d{2}:\d{2}\s+
- \d{4}\s*\z/iox =~ str
- _parse(str)
- end
- end
-
- def self._jisx0301(str) # :nodoc:
- if /\A\s*[mtsh]?\d{2}\.\d{2}\.\d{2}
- (t
- (\d{2}:\d{2}(:\d{2}([,.]\d*)?)?
- (z|[-+]\d{2}(:?\d{2})?)?)?)?\s*\z/ix =~ str
- if /\A\s*\d/ =~ str
- _parse(str.sub(/\A\s*(\d)/, 'h\1'))
- else
- _parse(str)
- end
- else
- _iso8601(str)
- end
- end
-
- t = Module.new do
-
- private
-
- def zone_to_diff(zone) # :nodoc:
- zone = zone.downcase
- if zone.sub!(/\s+(standard|daylight)\s+time\z/, '')
- dst = $1 == 'daylight'
- else
- dst = zone.sub!(/\s+dst\z/, '')
- end
- if Format::ZONES.include?(zone)
- offset = Format::ZONES[zone]
- offset += 3600 if dst
- elsif zone.sub!(/\A(?:gmt|utc?)?([-+])/, '')
- sign = $1
- if zone.include?(':')
- hour, min, sec, = zone.split(':')
- elsif zone.include?(',') || zone.include?('.')
- hour, fr, = zone.split(/[,.]/)
- min = Rational(fr.to_i, 10**fr.size) * 60
- else
- case zone.size
- when 3
- hour = zone[0,1]
- min = zone[1,2]
- else
- hour = zone[0,2]
- min = zone[2,2]
- sec = zone[4,2]
- end
- end
- offset = hour.to_i * 3600 + min.to_i * 60 + sec.to_i
- offset *= -1 if sign == '-'
- end
- offset
- end
-
- end
-
- extend t
- include t
-
-end
-
-class DateTime < Date
-
- def strftime(fmt='%FT%T%:z')
- super(fmt)
- end
-
- def self._strptime(str, fmt='%FT%T%z')
- super(str, fmt)
- end
-
- def iso8601_timediv(n) # :nodoc:
- strftime('T%T' +
- if n < 1
- ''
- else
- '.%0*d' % [n, (sec_fraction / Rational(1, 10**n)).round]
- end +
- '%:z')
- end
-
- private :iso8601_timediv
-
- def iso8601(n=0)
- super() + iso8601_timediv(n)
- end
-
- def rfc3339(n=0) iso8601(n) end
-
- def xmlschema(n=0) iso8601(n) end # :nodoc:
-
- def jisx0301(n=0)
- super() + iso8601_timediv(n)
- end
-
-end
diff --git a/lib/debug.rb b/lib/debug.rb
deleted file mode 100644
index 7bb1450198..0000000000
--- a/lib/debug.rb
+++ /dev/null
@@ -1,907 +0,0 @@
-# Copyright (C) 2000 Network Applied Communication Laboratory, Inc.
-# Copyright (C) 2000 Information-technology Promotion Agency, Japan
-# Copyright (C) 2000-2003 NAKAMURA, Hiroshi <nahi@ruby-lang.org>
-
-require 'continuation'
-
-if $SAFE > 0
- STDERR.print "-r debug.rb is not available in safe mode\n"
- exit 1
-end
-
-require 'tracer'
-require 'pp'
-
-class Tracer
- def Tracer.trace_func(*vars)
- Single.trace_func(*vars)
- end
-end
-
-SCRIPT_LINES__ = {} unless defined? SCRIPT_LINES__
-
-class DEBUGGER__
-MUTEX = Mutex.new
-
-class Context
- DEBUG_LAST_CMD = []
-
- begin
- require 'readline'
- def readline(prompt, hist)
- Readline::readline(prompt, hist)
- end
- rescue LoadError
- def readline(prompt, hist)
- STDOUT.print prompt
- STDOUT.flush
- line = STDIN.gets
- exit unless line
- line.chomp!
- line
- end
- USE_READLINE = false
- end
-
- def initialize
- if Thread.current == Thread.main
- @stop_next = 1
- else
- @stop_next = 0
- end
- @last_file = nil
- @file = nil
- @line = nil
- @no_step = nil
- @frames = []
- @finish_pos = 0
- @trace = false
- @catch = "StandardError"
- @suspend_next = false
- end
-
- def stop_next(n=1)
- @stop_next = n
- end
-
- def set_suspend
- @suspend_next = true
- end
-
- def clear_suspend
- @suspend_next = false
- end
-
- def suspend_all
- DEBUGGER__.suspend
- end
-
- def resume_all
- DEBUGGER__.resume
- end
-
- def check_suspend
- while MUTEX.synchronize {
- if @suspend_next
- DEBUGGER__.waiting.push Thread.current
- @suspend_next = false
- true
- end
- }
- end
- end
-
- def trace?
- @trace
- end
-
- def set_trace(arg)
- @trace = arg
- end
-
- def stdout
- DEBUGGER__.stdout
- end
-
- def break_points
- DEBUGGER__.break_points
- end
-
- def display
- DEBUGGER__.display
- end
-
- def context(th)
- DEBUGGER__.context(th)
- end
-
- def set_trace_all(arg)
- DEBUGGER__.set_trace(arg)
- end
-
- def set_last_thread(th)
- DEBUGGER__.set_last_thread(th)
- end
-
- def debug_eval(str, binding)
- begin
- val = eval(str, binding)
- rescue StandardError, ScriptError => e
- at = eval("caller(1)", binding)
- stdout.printf "%s:%s\n", at.shift, e.to_s.sub(/\(eval\):1:(in `.*?':)?/, '')
- for i in at
- stdout.printf "\tfrom %s\n", i
- end
- throw :debug_error
- end
- end
-
- def debug_silent_eval(str, binding)
- begin
- eval(str, binding)
- rescue StandardError, ScriptError
- nil
- end
- end
-
- def var_list(ary, binding)
- ary.sort!
- for v in ary
- stdout.printf " %s => %s\n", v, eval(v, binding).inspect
- end
- end
-
- def debug_variable_info(input, binding)
- case input
- when /^\s*g(?:lobal)?\s*$/
- var_list(global_variables, binding)
-
- when /^\s*l(?:ocal)?\s*$/
- var_list(eval("local_variables", binding), binding)
-
- when /^\s*i(?:nstance)?\s+/
- obj = debug_eval($', binding)
- var_list(obj.instance_variables, obj.instance_eval{binding()})
-
- when /^\s*c(?:onst(?:ant)?)?\s+/
- obj = debug_eval($', binding)
- unless obj.kind_of? Module
- stdout.print "Should be Class/Module: ", $', "\n"
- else
- var_list(obj.constants, obj.module_eval{binding()})
- end
- end
- end
-
- def debug_method_info(input, binding)
- case input
- when /^i(:?nstance)?\s+/
- obj = debug_eval($', binding)
-
- len = 0
- for v in obj.methods.sort
- len += v.size + 1
- if len > 70
- len = v.size + 1
- stdout.print "\n"
- end
- stdout.print v, " "
- end
- stdout.print "\n"
-
- else
- obj = debug_eval(input, binding)
- unless obj.kind_of? Module
- stdout.print "Should be Class/Module: ", input, "\n"
- else
- len = 0
- for v in obj.instance_methods(false).sort
- len += v.size + 1
- if len > 70
- len = v.size + 1
- stdout.print "\n"
- end
- stdout.print v, " "
- end
- stdout.print "\n"
- end
- end
- end
-
- def thnum
- num = DEBUGGER__.instance_eval{@thread_list[Thread.current]}
- unless num
- DEBUGGER__.make_thread_list
- num = DEBUGGER__.instance_eval{@thread_list[Thread.current]}
- end
- num
- end
-
- def debug_command(file, line, id, binding)
- MUTEX.lock
- unless defined?($debugger_restart) and $debugger_restart
- callcc{|c| $debugger_restart = c}
- end
- set_last_thread(Thread.current)
- frame_pos = 0
- binding_file = file
- binding_line = line
- previous_line = nil
- if ENV['EMACS']
- stdout.printf "\032\032%s:%d:\n", binding_file, binding_line
- else
- stdout.printf "%s:%d:%s", binding_file, binding_line,
- line_at(binding_file, binding_line)
- end
- @frames[0] = [binding, file, line, id]
- display_expressions(binding)
- prompt = true
- while prompt and input = readline("(rdb:%d) "%thnum(), true)
- catch(:debug_error) do
- if input == ""
- next unless DEBUG_LAST_CMD[0]
- input = DEBUG_LAST_CMD[0]
- stdout.print input, "\n"
- else
- DEBUG_LAST_CMD[0] = input
- end
-
- case input
- when /^\s*tr(?:ace)?(?:\s+(on|off))?(?:\s+(all))?$/
- if defined?( $2 )
- if $1 == 'on'
- set_trace_all true
- else
- set_trace_all false
- end
- elsif defined?( $1 )
- if $1 == 'on'
- set_trace true
- else
- set_trace false
- end
- end
- if trace?
- stdout.print "Trace on.\n"
- else
- stdout.print "Trace off.\n"
- end
-
- when /^\s*b(?:reak)?\s+(?:(.+):)?([^.:]+)$/
- pos = $2
- if $1
- klass = debug_silent_eval($1, binding)
- file = $1
- end
- if pos =~ /^\d+$/
- pname = pos
- pos = pos.to_i
- else
- pname = pos = pos.intern.id2name
- end
- break_points.push [true, 0, klass || file, pos]
- stdout.printf "Set breakpoint %d at %s:%s\n", break_points.size, klass || file, pname
-
- when /^\s*b(?:reak)?\s+(.+)[#.]([^.:]+)$/
- pos = $2.intern.id2name
- klass = debug_eval($1, binding)
- break_points.push [true, 0, klass, pos]
- stdout.printf "Set breakpoint %d at %s.%s\n", break_points.size, klass, pos
-
- when /^\s*wat(?:ch)?\s+(.+)$/
- exp = $1
- break_points.push [true, 1, exp]
- stdout.printf "Set watchpoint %d:%s\n", break_points.size, exp
-
- when /^\s*b(?:reak)?$/
- if break_points.find{|b| b[1] == 0}
- n = 1
- stdout.print "Breakpoints:\n"
- break_points.each do |b|
- if b[0] and b[1] == 0
- stdout.printf " %d %s:%s\n", n, b[2], b[3]
- end
- n += 1
- end
- end
- if break_points.find{|b| b[1] == 1}
- n = 1
- stdout.print "\n"
- stdout.print "Watchpoints:\n"
- for b in break_points
- if b[0] and b[1] == 1
- stdout.printf " %d %s\n", n, b[2]
- end
- n += 1
- end
- end
- if break_points.size == 0
- stdout.print "No breakpoints\n"
- else
- stdout.print "\n"
- end
-
- when /^\s*del(?:ete)?(?:\s+(\d+))?$/
- pos = $1
- unless pos
- input = readline("Clear all breakpoints? (y/n) ", false)
- if input == "y"
- for b in break_points
- b[0] = false
- end
- end
- else
- pos = pos.to_i
- if break_points[pos-1]
- break_points[pos-1][0] = false
- else
- stdout.printf "Breakpoint %d is not defined\n", pos
- end
- end
-
- when /^\s*disp(?:lay)?\s+(.+)$/
- exp = $1
- display.push [true, exp]
- stdout.printf "%d: ", display.size
- display_expression(exp, binding)
-
- when /^\s*disp(?:lay)?$/
- display_expressions(binding)
-
- when /^\s*undisp(?:lay)?(?:\s+(\d+))?$/
- pos = $1
- unless pos
- input = readline("Clear all expressions? (y/n) ", false)
- if input == "y"
- for d in display
- d[0] = false
- end
- end
- else
- pos = pos.to_i
- if display[pos-1]
- display[pos-1][0] = false
- else
- stdout.printf "Display expression %d is not defined\n", pos
- end
- end
-
- when /^\s*c(?:ont)?$/
- prompt = false
-
- when /^\s*s(?:tep)?(?:\s+(\d+))?$/
- if $1
- lev = $1.to_i
- else
- lev = 1
- end
- @stop_next = lev
- prompt = false
-
- when /^\s*n(?:ext)?(?:\s+(\d+))?$/
- if $1
- lev = $1.to_i
- else
- lev = 1
- end
- @stop_next = lev
- @no_step = @frames.size - frame_pos
- prompt = false
-
- when /^\s*w(?:here)?$/, /^\s*f(?:rame)?$/
- display_frames(frame_pos)
-
- when /^\s*l(?:ist)?(?:\s+(.+))?$/
- if not $1
- b = previous_line ? previous_line + 10 : binding_line - 5
- e = b + 9
- elsif $1 == '-'
- b = previous_line ? previous_line - 10 : binding_line - 5
- e = b + 9
- else
- b, e = $1.split(/[-,]/)
- if e
- b = b.to_i
- e = e.to_i
- else
- b = b.to_i - 5
- e = b + 9
- end
- end
- previous_line = b
- display_list(b, e, binding_file, binding_line)
-
- when /^\s*up(?:\s+(\d+))?$/
- previous_line = nil
- if $1
- lev = $1.to_i
- else
- lev = 1
- end
- frame_pos += lev
- if frame_pos >= @frames.size
- frame_pos = @frames.size - 1
- stdout.print "At toplevel\n"
- end
- binding, binding_file, binding_line = @frames[frame_pos]
- stdout.print format_frame(frame_pos)
-
- when /^\s*down(?:\s+(\d+))?$/
- previous_line = nil
- if $1
- lev = $1.to_i
- else
- lev = 1
- end
- frame_pos -= lev
- if frame_pos < 0
- frame_pos = 0
- stdout.print "At stack bottom\n"
- end
- binding, binding_file, binding_line = @frames[frame_pos]
- stdout.print format_frame(frame_pos)
-
- when /^\s*fin(?:ish)?$/
- if frame_pos == @frames.size
- stdout.print "\"finish\" not meaningful in the outermost frame.\n"
- else
- @finish_pos = @frames.size - frame_pos
- frame_pos = 0
- prompt = false
- end
-
- when /^\s*cat(?:ch)?(?:\s+(.+))?$/
- if $1
- excn = $1
- if excn == 'off'
- @catch = nil
- stdout.print "Clear catchpoint.\n"
- else
- @catch = excn
- stdout.printf "Set catchpoint %s.\n", @catch
- end
- else
- if @catch
- stdout.printf "Catchpoint %s.\n", @catch
- else
- stdout.print "No catchpoint.\n"
- end
- end
-
- when /^\s*q(?:uit)?$/
- input = readline("Really quit? (y/n) ", false)
- if input == "y"
- exit! # exit -> exit!: No graceful way to stop threads...
- end
-
- when /^\s*v(?:ar)?\s+/
- debug_variable_info($', binding)
-
- when /^\s*m(?:ethod)?\s+/
- debug_method_info($', binding)
-
- when /^\s*th(?:read)?\s+/
- if DEBUGGER__.debug_thread_info($', binding) == :cont
- prompt = false
- end
-
- when /^\s*pp\s+/
- PP.pp(debug_eval($', binding), stdout)
-
- when /^\s*p\s+/
- stdout.printf "%s\n", debug_eval($', binding).inspect
-
- when /^\s*r(?:estart)?$/
- $debugger_restart.call
-
- when /^\s*h(?:elp)?$/
- debug_print_help()
-
- else
- v = debug_eval(input, binding)
- stdout.printf "%s\n", v.inspect
- end
- end
- end
- MUTEX.unlock
- resume_all
- end
-
- def debug_print_help
- stdout.print <<EOHELP
-Debugger help v.-0.002b
-Commands
- b[reak] [file:|class:]<line|method>
- b[reak] [class.]<line|method>
- set breakpoint to some position
- wat[ch] <expression> set watchpoint to some expression
- cat[ch] (<exception>|off) set catchpoint to an exception
- b[reak] list breakpoints
- cat[ch] show catchpoint
- del[ete][ nnn] delete some or all breakpoints
- disp[lay] <expression> add expression into display expression list
- undisp[lay][ nnn] delete one particular or all display expressions
- c[ont] run until program ends or hit breakpoint
- s[tep][ nnn] step (into methods) one line or till line nnn
- n[ext][ nnn] go over one line or till line nnn
- w[here] display frames
- f[rame] alias for where
- l[ist][ (-|nn-mm)] list program, - lists backwards
- nn-mm lists given lines
- up[ nn] move to higher frame
- down[ nn] move to lower frame
- fin[ish] return to outer frame
- tr[ace] (on|off) set trace mode of current thread
- tr[ace] (on|off) all set trace mode of all threads
- q[uit] exit from debugger
- v[ar] g[lobal] show global variables
- v[ar] l[ocal] show local variables
- v[ar] i[nstance] <object> show instance variables of object
- v[ar] c[onst] <object> show constants of object
- m[ethod] i[nstance] <obj> show methods of object
- m[ethod] <class|module> show instance methods of class or module
- th[read] l[ist] list all threads
- th[read] c[ur[rent]] show current thread
- th[read] [sw[itch]] <nnn> switch thread context to nnn
- th[read] stop <nnn> stop thread nnn
- th[read] resume <nnn> resume thread nnn
- p expression evaluate expression and print its value
- h[elp] print this help
- <everything else> evaluate
-EOHELP
- end
-
- def display_expressions(binding)
- n = 1
- for d in display
- if d[0]
- stdout.printf "%d: ", n
- display_expression(d[1], binding)
- end
- n += 1
- end
- end
-
- def display_expression(exp, binding)
- stdout.printf "%s = %s\n", exp, debug_silent_eval(exp, binding).to_s
- end
-
- def frame_set_pos(file, line)
- if @frames[0]
- @frames[0][1] = file
- @frames[0][2] = line
- end
- end
-
- def display_frames(pos)
- 0.upto(@frames.size - 1) do |n|
- if n == pos
- stdout.print "--> "
- else
- stdout.print " "
- end
- stdout.print format_frame(n)
- end
- end
-
- def format_frame(pos)
- bind, file, line, id = @frames[pos]
- sprintf "#%d %s:%s%s\n", pos + 1, file, line,
- (id ? ":in `#{id.id2name}'" : "")
- end
-
- def display_list(b, e, file, line)
- stdout.printf "[%d, %d] in %s\n", b, e, file
- if lines = SCRIPT_LINES__[file] and lines != true
- b.upto(e) do |n|
- if n > 0 && lines[n-1]
- if n == line
- stdout.printf "=> %d %s\n", n, lines[n-1].chomp
- else
- stdout.printf " %d %s\n", n, lines[n-1].chomp
- end
- end
- end
- else
- stdout.printf "No sourcefile available for %s\n", file
- end
- end
-
- def line_at(file, line)
- lines = SCRIPT_LINES__[file]
- if lines
- return "\n" if lines == true
- line = lines[line-1]
- return "\n" unless line
- return line
- end
- return "\n"
- end
-
- def debug_funcname(id)
- if id.nil?
- "toplevel"
- else
- id.id2name
- end
- end
-
- def check_break_points(file, klass, pos, binding, id)
- return false if break_points.empty?
- n = 1
- for b in break_points
- if b[0] # valid
- if b[1] == 0 # breakpoint
- if (b[2] == file and b[3] == pos) or
- (klass and b[2] == klass and b[3] == pos)
- stdout.printf "Breakpoint %d, %s at %s:%s\n", n, debug_funcname(id), file, pos
- return true
- end
- elsif b[1] == 1 # watchpoint
- if debug_silent_eval(b[2], binding)
- stdout.printf "Watchpoint %d, %s at %s:%s\n", n, debug_funcname(id), file, pos
- return true
- end
- end
- end
- n += 1
- end
- return false
- end
-
- def excn_handle(file, line, id, binding)
- if $!.class <= SystemExit
- set_trace_func nil
- exit
- end
-
- if @catch and ($!.class.ancestors.find { |e| e.to_s == @catch })
- stdout.printf "%s:%d: `%s' (%s)\n", file, line, $!, $!.class
- fs = @frames.size
- tb = caller(0)[-fs..-1]
- if tb
- for i in tb
- stdout.printf "\tfrom %s\n", i
- end
- end
- suspend_all
- debug_command(file, line, id, binding)
- end
- end
-
- def trace_func(event, file, line, id, binding, klass)
- Tracer.trace_func(event, file, line, id, binding, klass) if trace?
- context(Thread.current).check_suspend
- @file = file
- @line = line
- case event
- when 'line'
- frame_set_pos(file, line)
- if !@no_step or @frames.size == @no_step
- @stop_next -= 1
- @stop_next = -1 if @stop_next < 0
- elsif @frames.size < @no_step
- @stop_next = 0 # break here before leaving...
- else
- # nothing to do. skipped.
- end
- if @stop_next == 0 or check_break_points(file, nil, line, binding, id)
- @no_step = nil
- suspend_all
- debug_command(file, line, id, binding)
- end
-
- when 'call'
- @frames.unshift [binding, file, line, id]
- if check_break_points(file, klass, id.id2name, binding, id)
- suspend_all
- debug_command(file, line, id, binding)
- end
-
- when 'c-call'
- frame_set_pos(file, line)
-
- when 'class'
- @frames.unshift [binding, file, line, id]
-
- when 'return', 'end'
- if @frames.size == @finish_pos
- @stop_next = 1
- @finish_pos = 0
- end
- @frames.shift
-
- when 'raise'
- excn_handle(file, line, id, binding)
-
- end
- @last_file = file
- end
-end
-
-trap("INT") { DEBUGGER__.interrupt }
-@last_thread = Thread::main
-@max_thread = 1
-@thread_list = {Thread::main => 1}
-@break_points = []
-@display = []
-@waiting = []
-@stdout = STDOUT
-
-class << DEBUGGER__
- def stdout
- @stdout
- end
-
- def stdout=(s)
- @stdout = s
- end
-
- def display
- @display
- end
-
- def break_points
- @break_points
- end
-
- def waiting
- @waiting
- end
-
- def set_trace( arg )
- MUTEX.synchronize do
- make_thread_list
- for th, in @thread_list
- context(th).set_trace arg
- end
- end
- arg
- end
-
- def set_last_thread(th)
- @last_thread = th
- end
-
- def suspend
- MUTEX.synchronize do
- make_thread_list
- for th, in @thread_list
- next if th == Thread.current
- context(th).set_suspend
- end
- end
- # Schedule other threads to suspend as soon as possible.
- Thread.pass
- end
-
- def resume
- MUTEX.synchronize do
- make_thread_list
- @thread_list.each do |th,|
- next if th == Thread.current
- context(th).clear_suspend
- end
- waiting.each do |th|
- th.run
- end
- waiting.clear
- end
- # Schedule other threads to restart as soon as possible.
- Thread.pass
- end
-
- def context(thread=Thread.current)
- c = thread[:__debugger_data__]
- unless c
- thread[:__debugger_data__] = c = Context.new
- end
- c
- end
-
- def interrupt
- context(@last_thread).stop_next
- end
-
- def get_thread(num)
- th = @thread_list.key(num)
- unless th
- @stdout.print "No thread ##{num}\n"
- throw :debug_error
- end
- th
- end
-
- def thread_list(num)
- th = get_thread(num)
- if th == Thread.current
- @stdout.print "+"
- else
- @stdout.print " "
- end
- @stdout.printf "%d ", num
- @stdout.print th.inspect, "\t"
- file = context(th).instance_eval{@file}
- if file
- @stdout.print file,":",context(th).instance_eval{@line}
- end
- @stdout.print "\n"
- end
-
- def thread_list_all
- for th in @thread_list.values.sort
- thread_list(th)
- end
- end
-
- def make_thread_list
- hash = {}
- for th in Thread::list
- if @thread_list.key? th
- hash[th] = @thread_list[th]
- else
- @max_thread += 1
- hash[th] = @max_thread
- end
- end
- @thread_list = hash
- end
-
- def debug_thread_info(input, binding)
- case input
- when /^l(?:ist)?/
- make_thread_list
- thread_list_all
-
- when /^c(?:ur(?:rent)?)?$/
- make_thread_list
- thread_list(@thread_list[Thread.current])
-
- when /^(?:sw(?:itch)?\s+)?(\d+)/
- make_thread_list
- th = get_thread($1.to_i)
- if th == Thread.current
- @stdout.print "It's the current thread.\n"
- else
- thread_list(@thread_list[th])
- context(th).stop_next
- th.run
- return :cont
- end
-
- when /^stop\s+(\d+)/
- make_thread_list
- th = get_thread($1.to_i)
- if th == Thread.current
- @stdout.print "It's the current thread.\n"
- elsif th.stop?
- @stdout.print "Already stopped.\n"
- else
- thread_list(@thread_list[th])
- context(th).suspend
- end
-
- when /^resume\s+(\d+)/
- make_thread_list
- th = get_thread($1.to_i)
- if th == Thread.current
- @stdout.print "It's the current thread.\n"
- elsif !th.stop?
- @stdout.print "Already running."
- else
- thread_list(@thread_list[th])
- th.run
- end
- end
- end
-end
-
-stdout.printf "Debug.rb\n"
-stdout.printf "Emacs support available.\n\n"
-RubyVM::InstructionSequence.compile_option = {
- trace_instruction: true
-}
-set_trace_func proc { |event, file, line, id, binding, klass, *rest|
- DEBUGGER__.context.trace_func event, file, line, id, binding, klass
-}
-end
diff --git a/lib/delegate.gemspec b/lib/delegate.gemspec
new file mode 100644
index 0000000000..f7fcc1ceb9
--- /dev/null
+++ b/lib/delegate.gemspec
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1, ".").join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Yukihiro Matsumoto"]
+ spec.email = ["matz@ruby-lang.org"]
+
+ spec.summary = %q{Provides three abilities to delegate method calls to an object.}
+ spec.description = %q{Provides three abilities to delegate method calls to an object.}
+ spec.homepage = "https://github.com/ruby/delegate"
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
+ `git ls-files -z 2>#{IO::NULL}`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
+ end
+ spec.require_paths = ["lib"]
+ spec.required_ruby_version = '>= 3.0'
+end
diff --git a/lib/delegate.rb b/lib/delegate.rb
index 025e901a89..0ff9797bdb 100644
--- a/lib/delegate.rb
+++ b/lib/delegate.rb
@@ -1,9 +1,9 @@
+# frozen_string_literal: true
# = delegate -- Support for the Delegation Pattern
#
# Documentation by James Edward Gray II and Gavin Sinclair
-#
-# == Introduction
-#
+
+##
# This library provides three different ways to delegate method calls to an
# object. The easiest to use is SimpleDelegator. Pass an object to the
# constructor and all methods supported by the object will be delegated. This
@@ -15,109 +15,59 @@
#
# Finally, if you need full control over the delegation scheme, you can inherit
# from the abstract class Delegator and customize as needed. (If you find
-# yourself needing this control, have a look at _forwardable_, also in the
-# standard library. It may suit your needs better.)
-#
-# == Notes
-#
-# Be advised, RDoc will not detect delegated methods.
+# yourself needing this control, have a look at Forwardable which is also in
+# the standard library. It may suit your needs better.)
#
-# <b>delegate.rb provides full-class delegation via the
-# DelegateClass() method. For single-method delegation via
-# def_delegator(), see forwardable.rb.</b>
+# SimpleDelegator's implementation serves as a nice example of the use of
+# Delegator:
#
-# == Examples
+# require 'delegate'
#
-# === SimpleDelegator
-#
-# Here's a simple example that takes advantage of the fact that
-# SimpleDelegator's delegation object can be changed at any time.
-#
-# class Stats
-# def initialize
-# @source = SimpleDelegator.new([])
+# class SimpleDelegator < Delegator
+# def __getobj__
+# @delegate_sd_obj # return object we are delegating to, required
# end
-#
-# def stats( records )
-# @source.__setobj__(records)
-#
-# "Elements: #{@source.size}\n" +
-# " Non-Nil: #{@source.compact.size}\n" +
-# " Unique: #{@source.uniq.size}\n"
+#
+# def __setobj__(obj)
+# @delegate_sd_obj = obj # change delegation object,
+# # a feature we're providing
# end
# end
-#
-# s = Stats.new
-# puts s.stats(%w{James Edward Gray II})
-# puts
-# puts s.stats([1, 2, 3, nil, 4, 5, 1, 2])
#
-# <i>Prints:</i>
-#
-# Elements: 4
-# Non-Nil: 4
-# Unique: 4
-#
-# Elements: 8
-# Non-Nil: 7
-# Unique: 6
-#
-# === DelegateClass()
-#
-# Here's a sample of use from <i>tempfile.rb</i>.
-#
-# A _Tempfile_ object is really just a _File_ object with a few special rules
-# about storage location and/or when the File should be deleted. That makes for
-# an almost textbook perfect example of how to use delegation.
+# == Notes
#
-# class Tempfile < DelegateClass(File)
-# # constant and class member data initialization...
-#
-# def initialize(basename, tmpdir=Dir::tmpdir)
-# # build up file path/name in var tmpname...
-#
-# @tmpfile = File.open(tmpname, File::RDWR|File::CREAT|File::EXCL, 0600)
-#
-# # ...
-#
-# super(@tmpfile)
-#
-# # below this point, all methods of File are supported...
-# end
-#
-# # ...
-# end
+# Be advised, RDoc will not detect delegated methods.
#
-# === Delegator
-#
-# SimpleDelegator's implementation serves as a nice example here.
-#
-# class SimpleDelegator < Delegator
-# def initialize(obj)
-# super # pass obj to Delegator constructor, required
-# @delegate_sd_obj = obj # store obj for future use
-# end
-#
-# def __getobj__
-# @delegate_sd_obj # return object we are delegating to, required
-# end
-#
-# def __setobj__(obj)
-# @delegate_sd_obj = obj # change delegation object, a feature we're providing
-# end
-#
-# # ...
-# end
-
-#
-# Delegator is an abstract class used to build delegator pattern objects from
-# subclasses. Subclasses should redefine \_\_getobj\_\_. For a concrete
-# implementation, see SimpleDelegator.
-#
-class Delegator
- [:to_s,:inspect,:=~,:!~,:===].each do |m|
- undef_method m
+class Delegator < BasicObject
+ # The version string
+ VERSION = "0.6.1"
+
+ kernel = ::Kernel.dup
+ kernel.class_eval do
+ alias __raise__ raise
+ [:to_s, :inspect, :!~, :===, :<=>, :hash].each do |m|
+ undef_method m
+ end
+ private_instance_methods.each do |m|
+ if /\Ablock_given\?\z|\Aiterator\?\z|\A__.*__\z/ =~ m
+ next
+ end
+ undef_method m
+ end
end
+ include kernel
+
+ # :stopdoc:
+ def self.const_missing(n)
+ ::Object.const_get(n)
+ end
+ # :startdoc:
+
+ ##
+ # :method: raise
+ # Use #__raise__ if your Delegator does not have a object to delegate the
+ # #raise method call.
+ #
#
# Pass in the _obj_ to delegate method calls to. All methods supported by
@@ -127,44 +77,117 @@ class Delegator
__setobj__(obj)
end
- # Handles the magic of delegation through \_\_getobj\_\_.
- def method_missing(m, *args, &block)
- begin
- target = self.__getobj__
- unless target.respond_to?(m)
- super(m, *args, &block)
+ #
+ # Handles the magic of delegation through +__getobj__+.
+ #
+ ruby2_keywords def method_missing(m, *args, &block)
+ r = true
+ target = self.__getobj__ {r = false}
+
+ if r && target_respond_to?(target, m, false)
+ target.__send__(m, *args, &block)
+ elsif ::Kernel.method_defined?(m) || ::Kernel.private_method_defined?(m)
+ ::Kernel.instance_method(m).bind_call(self, *args, &block)
+ else
+ super(m, *args, &block)
+ end
+ end
+
+ #
+ # Checks for a method provided by this the delegate object by forwarding the
+ # call through +__getobj__+.
+ #
+ def respond_to_missing?(m, include_private)
+ r = true
+ target = self.__getobj__ {r = false}
+ r &&= target_respond_to?(target, m, include_private)
+ if r && include_private && !target_respond_to?(target, m, false)
+ warn "delegator does not forward private method \##{m}", uplevel: 3
+ return false
+ end
+ r
+ end
+
+ KERNEL_RESPOND_TO = ::Kernel.instance_method(:respond_to?) # :nodoc:
+ private_constant :KERNEL_RESPOND_TO
+
+ # Handle BasicObject instances
+ private def target_respond_to?(target, m, include_private)
+ case target
+ when Object
+ target.respond_to?(m, include_private)
+ else
+ if KERNEL_RESPOND_TO.bind_call(target, :respond_to?)
+ target.respond_to?(m, include_private)
else
- target.__send__(m, *args, &block)
+ KERNEL_RESPOND_TO.bind_call(target, m, include_private)
end
- rescue Exception
- $@.delete_if{|s| %r"\A#{__FILE__}:\d+:in `method_missing'\z"o =~ s}
- ::Kernel::raise
end
end
- #
- # Checks for a method provided by this the delegate object by fowarding the
- # call through \_\_getobj\_\_.
- #
- def respond_to?(m, include_private = false)
- return true if super
- return self.__getobj__.respond_to?(m, include_private)
+ #
+ # Returns the methods available to this delegate object as the union
+ # of this object's and +__getobj__+ methods.
+ #
+ def methods(all=true)
+ __getobj__.methods(all) | super
+ end
+
+ #
+ # Returns the methods available to this delegate object as the union
+ # of this object's and +__getobj__+ public methods.
+ #
+ def public_methods(all=true)
+ __getobj__.public_methods(all) | super
+ end
+
+ #
+ # Returns the methods available to this delegate object as the union
+ # of this object's and +__getobj__+ protected methods.
+ #
+ def protected_methods(all=true)
+ __getobj__.protected_methods(all) | super
end
- #
- # Returns true if two objects are considered same.
- #
+ # Note: no need to specialize private_methods, since they are not forwarded
+
+ #
+ # Returns true if two objects are considered of equal value.
+ #
def ==(obj)
return true if obj.equal?(self)
self.__getobj__ == obj
end
#
+ # Returns true if two objects are not considered of equal value.
+ #
+ def !=(obj)
+ return false if obj.equal?(self)
+ __getobj__ != obj
+ end
+
+ #
+ # Returns true if two objects are considered of equal value.
+ #
+ def eql?(obj)
+ return true if obj.equal?(self)
+ obj.eql?(__getobj__)
+ end
+
+ #
+ # Delegates ! to the +__getobj__+
+ #
+ def !
+ !__getobj__
+ end
+
+ #
# This method must be overridden by subclasses and should return the object
# method calls are being delegated to.
#
def __getobj__
- raise NotImplementedError, "need to define `__getobj__'"
+ __raise__ ::NotImplementedError, "need to define '__getobj__'"
end
#
@@ -172,41 +195,132 @@ class Delegator
# to _obj_.
#
def __setobj__(obj)
- raise NotImplementedError, "need to define `__setobj__'"
+ __raise__ ::NotImplementedError, "need to define '__setobj__'"
end
- # Serialization support for the object returned by \_\_getobj\_\_.
+ #
+ # Serialization support for the object returned by +__getobj__+.
+ #
def marshal_dump
- __getobj__
+ ivars = instance_variables.reject {|var| /\A@delegate_/ =~ var}
+ [
+ :__v2__,
+ ivars, ivars.map {|var| instance_variable_get(var)},
+ __getobj__
+ ]
end
+
+ #
# Reinitializes delegation from a serialized object.
- def marshal_load(obj)
- __setobj__(obj)
+ #
+ def marshal_load(data)
+ version, vars, values, obj = data
+ if version == :__v2__
+ vars.each_with_index {|var, i| instance_variable_set(var, values[i])}
+ __setobj__(obj)
+ else
+ __setobj__(data)
+ end
end
- # Clone support for the object returned by \_\_getobj\_\_.
- def clone
- new = super
- new.__setobj__(__getobj__.clone)
- new
+ def initialize_clone(obj, freeze: nil) # :nodoc:
+ self.__setobj__(obj.__getobj__.clone(freeze: freeze))
end
- # Duplication support for the object returned by \_\_getobj\_\_.
- def dup
- new = super
- new.__setobj__(__getobj__.dup)
- new
+ def initialize_dup(obj) # :nodoc:
+ self.__setobj__(obj.__getobj__.dup)
+ end
+ private :initialize_clone, :initialize_dup
+
+ ##
+ # :method: freeze
+ # Freeze both the object returned by +__getobj__+ and self.
+ #
+ def freeze
+ __getobj__.freeze
+ super()
+ end
+
+ @delegator_api = self.public_instance_methods
+ def self.public_api # :nodoc:
+ @delegator_api
end
end
-#
+##
# A concrete implementation of Delegator, this class provides the means to
# delegate all supported method calls to the object passed into the constructor
# and even to change the object being delegated to at a later time with
-# \_\_setobj\_\_ .
+# #__setobj__.
+#
+# class User
+# def born_on
+# Date.new(1989, 9, 10)
+# end
+# end
+#
+# require 'delegate'
+#
+# class UserDecorator < SimpleDelegator
+# def birth_year
+# born_on.year
+# end
+# end
+#
+# decorated_user = UserDecorator.new(User.new)
+# decorated_user.birth_year #=> 1989
+# decorated_user.__getobj__ #=> #<User: ...>
+#
+# A SimpleDelegator instance can take advantage of the fact that SimpleDelegator
+# is a subclass of +Delegator+ to call <tt>super</tt> to have methods called on
+# the object being delegated to.
+#
+# class SuperArray < SimpleDelegator
+# def [](*args)
+# super + 1
+# end
+# end
+#
+# SuperArray.new([1])[0] #=> 2
+#
+# Here's a simple example that takes advantage of the fact that
+# SimpleDelegator's delegation object can be changed at any time.
+#
+# class Stats
+# def initialize
+# @source = SimpleDelegator.new([])
+# end
#
-class SimpleDelegator<Delegator
+# def stats(records)
+# @source.__setobj__(records)
+#
+# "Elements: #{@source.size}\n" +
+# " Non-Nil: #{@source.compact.size}\n" +
+# " Unique: #{@source.uniq.size}\n"
+# end
+# end
+#
+# s = Stats.new
+# puts s.stats(%w{James Edward Gray II})
+# puts
+# puts s.stats([1, 2, 3, nil, 4, 5, 1, 2])
+#
+# Prints:
+#
+# Elements: 4
+# Non-Nil: 4
+# Unique: 4
+#
+# Elements: 8
+# Non-Nil: 7
+# Unique: 6
+#
+class SimpleDelegator < Delegator
# Returns the current object method calls are being delegated to.
def __getobj__
+ unless defined?(@delegate_sd_obj)
+ return yield if block_given?
+ __raise__ ::ArgumentError, "not delegated"
+ end
@delegate_sd_obj
end
@@ -225,87 +339,125 @@ class SimpleDelegator<Delegator
# puts names[1] # => Sinclair
#
def __setobj__(obj)
- raise ArgumentError, "cannot delegate to self" if self.equal?(obj)
+ __raise__ ::ArgumentError, "cannot delegate to self" if self.equal?(obj)
@delegate_sd_obj = obj
end
end
-# :stopdoc:
-def Delegator.delegating_block(mid)
- lambda do |*args, &block|
- begin
- __getobj__.__send__(mid, *args, &block)
- rescue
- re = /\A#{Regexp.quote(__FILE__)}:#{__LINE__-2}:/o
- $!.backtrace.delete_if {|t| re =~ t}
- raise
- end
- end
-end
-# :startdoc:
-
#
# The primary interface to this library. Use to setup delegation when defining
# your class.
#
-# class MyClass < DelegateClass( ClassToDelegateTo ) # Step 1
+# class MyClass < DelegateClass(ClassToDelegateTo) # Step 1
+# def initialize
+# super(obj_of_ClassToDelegateTo) # Step 2
+# end
+# end
+#
+# or:
+#
+# MyClass = DelegateClass(ClassToDelegateTo) do # Step 1
# def initialize
-# super(obj_of_ClassToDelegateTo) # Step 2
+# super(obj_of_ClassToDelegateTo) # Step 2
# end
# end
#
-def DelegateClass(superclass)
+# Here's a sample of use from Tempfile which is really a File object with a
+# few special rules about storage location and when the File should be
+# deleted. That makes for an almost textbook perfect example of how to use
+# delegation.
+#
+# class Tempfile < DelegateClass(File)
+# # constant and class member data initialization...
+#
+# def initialize(basename, tmpdir=Dir::tmpdir)
+# # build up file path/name in var tmpname...
+#
+# @tmpfile = File.open(tmpname, File::RDWR|File::CREAT|File::EXCL, 0600)
+#
+# # ...
+#
+# super(@tmpfile)
+#
+# # below this point, all methods of File are supported...
+# end
+#
+# # ...
+# end
+#
+def DelegateClass(superclass, &block)
klass = Class.new(Delegator)
- methods = superclass.public_instance_methods(true)
- methods -= ::Delegator.public_instance_methods
- methods -= [:to_s,:inspect,:=~,:!~,:===]
- klass.module_eval {
- def __getobj__ # :nodoc:
+ ignores = [*::Delegator.public_api, :to_s, :inspect, :=~, :!~, :===]
+ protected_instance_methods = superclass.protected_instance_methods
+ protected_instance_methods -= ignores
+ public_instance_methods = superclass.public_instance_methods
+ public_instance_methods -= ignores
+
+ methods_to_define =
+ public_instance_methods.map { |x| [x, false] } +
+ protected_instance_methods.map { |x| [x, true] }
+
+ source = []
+
+ methods_to_define.each do |target_name, is_protected|
+ unless target_name.match?(/\A[_a-zA-Z]\w*[!\?]?\z/)
+ placeholder_name = :__delegate
+ end
+
+ send_source =
+ if is_protected || placeholder_name
+ "__getobj__.__send__(#{target_name.inspect}, ...)"
+ else
+ "__getobj__.#{target_name}(...)"
+ end
+ source << "def #{placeholder_name || target_name}(...); #{send_source}; end"
+
+ if placeholder_name
+ source << "alias_method #{target_name.inspect}, :#{placeholder_name}"
+ source << "remove_method :#{placeholder_name}"
+ end
+ end
+
+ klass.module_eval do
+ def __getobj__ # :nodoc:
+ unless defined?(@delegate_dc_obj)
+ return yield if block_given?
+ __raise__ ::ArgumentError, "not delegated"
+ end
@delegate_dc_obj
end
+
def __setobj__(obj) # :nodoc:
- raise ArgumentError, "cannot delegate to self" if self.equal?(obj)
+ __raise__ ::ArgumentError, "cannot delegate to self" if self.equal?(obj)
@delegate_dc_obj = obj
end
- }
- klass.module_eval do
- methods.each do |method|
- define_method(method, Delegator.delegating_block(method))
- end
- end
- return klass
-end
-# :enddoc:
+ class_eval(source.join(";"), __FILE__, __LINE__)
-if __FILE__ == $0
- class ExtArray<DelegateClass(Array)
- def initialize()
- super([])
- end
+ protected(*protected_instance_methods)
end
- ary = ExtArray.new
- p ary.class
- ary.push 25
- p ary
- ary.push 42
- ary.each {|x| p x}
-
- foo = Object.new
- def foo.test
- 25
+ klass.define_singleton_method :public_instance_methods do |all=true|
+ super(all) | superclass.public_instance_methods
end
- def foo.iter
- yield self
+ klass.define_singleton_method :protected_instance_methods do |all=true|
+ super(all) | superclass.protected_instance_methods
end
- def foo.error
- raise 'this is OK'
+ klass.define_singleton_method :instance_methods do |all=true|
+ super(all) | superclass.instance_methods
end
- foo2 = SimpleDelegator.new(foo)
- p foo2
- foo2.instance_eval{print "foo\n"}
- p foo.test == foo2.test # => true
- p foo2.iter{[55,true]} # => true
- foo2.error # raise error!
+ klass.define_singleton_method :public_instance_method do |name|
+ super(name)
+ rescue NameError
+ raise unless self.public_instance_methods.include?(name)
+ superclass.public_instance_method(name)
+ end
+ klass.define_singleton_method :instance_method do |name|
+ super(name)
+ rescue NameError
+ raise unless self.instance_methods.include?(name)
+ superclass.instance_method(name)
+ end
+ klass.module_eval(&block) if block
+ return klass
end
diff --git a/lib/did_you_mean.rb b/lib/did_you_mean.rb
new file mode 100644
index 0000000000..640d910389
--- /dev/null
+++ b/lib/did_you_mean.rb
@@ -0,0 +1,131 @@
+require_relative "did_you_mean/version"
+require_relative "did_you_mean/core_ext/name_error"
+
+require_relative "did_you_mean/spell_checker"
+require_relative 'did_you_mean/spell_checkers/name_error_checkers'
+require_relative 'did_you_mean/spell_checkers/method_name_checker'
+require_relative 'did_you_mean/spell_checkers/key_error_checker'
+require_relative 'did_you_mean/spell_checkers/null_checker'
+require_relative 'did_you_mean/spell_checkers/require_path_checker'
+require_relative 'did_you_mean/spell_checkers/pattern_key_name_checker'
+require_relative 'did_you_mean/formatter'
+require_relative 'did_you_mean/tree_spell_checker'
+
+# The +DidYouMean+ gem adds functionality to suggest possible method/class
+# names upon errors such as +NameError+ and +NoMethodError+. In Ruby 2.3 or
+# later, it is automatically activated during startup.
+#
+# @example
+#
+# methosd
+# # => NameError: undefined local variable or method `methosd' for main:Object
+# # Did you mean? methods
+# # method
+#
+# OBject
+# # => NameError: uninitialized constant OBject
+# # Did you mean? Object
+#
+# @full_name = "Yuki Nishijima"
+# first_name, last_name = full_name.split(" ")
+# # => NameError: undefined local variable or method `full_name' for main:Object
+# # Did you mean? @full_name
+#
+# @@full_name = "Yuki Nishijima"
+# @@full_anme
+# # => NameError: uninitialized class variable @@full_anme in Object
+# # Did you mean? @@full_name
+#
+# full_name = "Yuki Nishijima"
+# full_name.starts_with?("Y")
+# # => NoMethodError: undefined method `starts_with?' for "Yuki Nishijima":String
+# # Did you mean? start_with?
+#
+# hash = {foo: 1, bar: 2, baz: 3}
+# hash.fetch(:fooo)
+# # => KeyError: key not found: :fooo
+# # Did you mean? :foo
+#
+#
+# == Disabling \DidYouMean
+#
+# Occasionally, you may want to disable the \DidYouMean gem for e.g.
+# debugging issues in the error object itself. You can disable it entirely by
+# specifying +--disable-did_you_mean+ option to the +ruby+ command:
+#
+# $ ruby --disable-did_you_mean -e "1.zeor?"
+# -e:1:in `<main>': undefined method `zeor?' for 1:Integer (NameError)
+#
+# When you do not have direct access to the +ruby+ command (e.g.
+# +rails console+, +irb+), you could applyoptions using the +RUBYOPT+
+# environment variable:
+#
+# $ RUBYOPT='--disable-did_you_mean' irb
+# irb:0> 1.zeor?
+# # => NoMethodError (undefined method `zeor?' for 1:Integer)
+#
+#
+# == Getting the original error message
+#
+# Sometimes, you do not want to disable the gem entirely, but need to get the
+# original error message without suggestions (e.g. testing). In this case, you
+# could use the +#original_message+ method on the error object:
+#
+# no_method_error = begin
+# 1.zeor?
+# rescue NoMethodError => error
+# error
+# end
+#
+# no_method_error.message
+# # => NoMethodError (undefined method `zeor?' for 1:Integer)
+# # Did you mean? zero?
+#
+# no_method_error.original_message
+# # => NoMethodError (undefined method `zeor?' for 1:Integer)
+#
+module DidYouMean
+ # Map of error types and spell checker objects.
+ @spell_checkers = Hash.new(NullChecker)
+
+ # Returns a sharable hash map of error types and spell checker objects.
+ def self.spell_checkers
+ @spell_checkers
+ end
+
+ # Adds +DidYouMean+ functionality to an error using a given spell checker
+ def self.correct_error(error_class, spell_checker)
+ if defined?(Ractor)
+ new_mapping = { **@spell_checkers, error_class.to_s => spell_checker }
+ new_mapping.default = NullChecker
+
+ @spell_checkers = Ractor.make_shareable(new_mapping)
+ else
+ spell_checkers[error_class.to_s] = spell_checker
+ end
+
+ error_class.prepend(Correctable) if error_class.is_a?(Class) && !(error_class < Correctable)
+ end
+
+ correct_error NameError, NameErrorCheckers
+ correct_error KeyError, KeyErrorChecker
+ correct_error NoMethodError, MethodNameChecker
+ correct_error LoadError, RequirePathChecker if RUBY_VERSION >= '2.8.0'
+ correct_error NoMatchingPatternKeyError, PatternKeyNameChecker if defined?(::NoMatchingPatternKeyError)
+
+ # Returns the currently set formatter. By default, it is set to +DidYouMean::Formatter+.
+ def self.formatter
+ if defined?(Ractor)
+ Ractor.current[:__did_you_mean_formatter__] || Formatter
+ else
+ Formatter
+ end
+ end
+
+ # Updates the primary formatter used to format the suggestions.
+ def self.formatter=(formatter)
+ if defined?(Ractor)
+ Ractor.current[:__did_you_mean_formatter__] = formatter
+ end
+ end
+end
diff --git a/lib/did_you_mean/core_ext/name_error.rb b/lib/did_you_mean/core_ext/name_error.rb
new file mode 100644
index 0000000000..8c170c4b90
--- /dev/null
+++ b/lib/did_you_mean/core_ext/name_error.rb
@@ -0,0 +1,57 @@
+module DidYouMean
+ module Correctable
+ if Exception.method_defined?(:detailed_message)
+ # just for compatibility
+ def original_message
+ # we cannot use alias here because
+ to_s
+ end
+
+ def detailed_message(highlight: true, did_you_mean: true, **)
+ msg = super.dup
+
+ return msg unless did_you_mean
+
+ suggestion = DidYouMean.formatter.message_for(corrections)
+
+ if highlight
+ suggestion = suggestion.gsub(/.+/) { "\e[1m" + $& + "\e[m" }
+ end
+
+ msg << suggestion
+ msg
+ rescue
+ super
+ end
+ else
+ SKIP_TO_S_FOR_SUPER_LOOKUP = true
+ private_constant :SKIP_TO_S_FOR_SUPER_LOOKUP
+
+ def original_message
+ meth = method(:to_s)
+ while meth.owner.const_defined?(:SKIP_TO_S_FOR_SUPER_LOOKUP)
+ meth = meth.super_method
+ end
+ meth.call
+ end
+
+ def to_s
+ msg = super.dup
+ suggestion = DidYouMean.formatter.message_for(corrections)
+
+ msg << suggestion if !msg.include?(suggestion)
+ msg
+ rescue
+ super
+ end
+ end
+
+ def corrections
+ @corrections ||= spell_checker.corrections
+ end
+
+ def spell_checker
+ DidYouMean.spell_checkers[self.class.to_s].new(self)
+ end
+ end
+end
diff --git a/lib/did_you_mean/did_you_mean.gemspec b/lib/did_you_mean/did_you_mean.gemspec
new file mode 100644
index 0000000000..be4ac76b4b
--- /dev/null
+++ b/lib/did_you_mean/did_you_mean.gemspec
@@ -0,0 +1,25 @@
+# coding: utf-8
+lib = File.expand_path('../lib', __FILE__)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+begin
+ require_relative "lib/did_you_mean/version"
+rescue LoadError # Fallback to load version file in ruby core repository
+ require_relative "version"
+end
+
+Gem::Specification.new do |spec|
+ spec.name = "did_you_mean"
+ spec.version = DidYouMean::VERSION
+ spec.authors = ["Yuki Nishijima"]
+ spec.email = ["mail@yukinishijima.net"]
+ spec.summary = '"Did you mean?" experience in Ruby'
+ spec.description = 'The gem that has been saving people from typos since 2014.'
+ spec.homepage = "https://github.com/ruby/did_you_mean"
+ spec.license = "MIT"
+
+ spec.files = `git ls-files`.split($/).reject{|path| path.start_with?('evaluation/') }
+ spec.test_files = spec.files.grep(%r{^(test)/})
+ spec.require_paths = ["lib"]
+
+ spec.required_ruby_version = '>= 2.5.0'
+end
diff --git a/lib/did_you_mean/experimental.rb b/lib/did_you_mean/experimental.rb
new file mode 100644
index 0000000000..f8e37e4532
--- /dev/null
+++ b/lib/did_you_mean/experimental.rb
@@ -0,0 +1,2 @@
+warn "Experimental features in the did_you_mean gem has been removed " \
+ "and `require \"did_you_mean/experimental\"' has no effect."
diff --git a/lib/did_you_mean/formatter.rb b/lib/did_you_mean/formatter.rb
new file mode 100644
index 0000000000..c43748f707
--- /dev/null
+++ b/lib/did_you_mean/formatter.rb
@@ -0,0 +1,44 @@
+# frozen-string-literal: true
+
+module DidYouMean
+ # The +DidYouMean::Formatter+ is the basic, default formatter for the
+ # gem. The formatter responds to the +message_for+ method and it returns a
+ # human readable string.
+ class Formatter
+
+ # Returns a human readable string that contains +corrections+. This
+ # formatter is designed to be less verbose to not take too much screen
+ # space while being helpful enough to the user.
+ #
+ # @example
+ #
+ # formatter = DidYouMean::Formatter.new
+ #
+ # # displays suggestions in two lines with the leading empty line
+ # puts formatter.message_for(["methods", "method"])
+ #
+ # Did you mean? methods
+ # method
+ # # => nil
+ #
+ # # displays an empty line
+ # puts formatter.message_for([])
+ #
+ # # => nil
+ #
+ def self.message_for(corrections)
+ corrections.empty? ? "" : "\nDid you mean? #{corrections.join("\n ")}"
+ end
+
+ def message_for(corrections)
+ warn "The instance method #message_for has been deprecated. Please use the class method " \
+ "DidYouMean::Formatter.message_for(...) instead."
+
+ self.class.message_for(corrections)
+ end
+ end
+
+ PlainFormatter = Formatter
+
+ deprecate_constant :PlainFormatter
+end
diff --git a/lib/did_you_mean/formatters/plain_formatter.rb b/lib/did_you_mean/formatters/plain_formatter.rb
new file mode 100644
index 0000000000..d669588e0f
--- /dev/null
+++ b/lib/did_you_mean/formatters/plain_formatter.rb
@@ -0,0 +1,4 @@
+require_relative '../formatter'
+
+warn "`require 'did_you_mean/formatters/plain_formatter'` is deprecated. Please `require 'did_you_mean/formatter'` " \
+ "instead."
diff --git a/lib/did_you_mean/formatters/verbose_formatter.rb b/lib/did_you_mean/formatters/verbose_formatter.rb
new file mode 100644
index 0000000000..f6623681f2
--- /dev/null
+++ b/lib/did_you_mean/formatters/verbose_formatter.rb
@@ -0,0 +1,10 @@
+# frozen-string-literal: true
+
+warn "`require 'did_you_mean/formatters/verbose_formatter'` is deprecated and falls back to the default formatter. "
+
+require_relative '../formatter'
+
+module DidYouMean
+ # For compatibility:
+ VerboseFormatter = Formatter
+end
diff --git a/lib/did_you_mean/jaro_winkler.rb b/lib/did_you_mean/jaro_winkler.rb
new file mode 100644
index 0000000000..9a3e57f6d7
--- /dev/null
+++ b/lib/did_you_mean/jaro_winkler.rb
@@ -0,0 +1,84 @@
+module DidYouMean
+ module Jaro
+ module_function
+
+ def distance(str1, str2)
+ str1, str2 = str2, str1 if str1.length > str2.length
+ length1, length2 = str1.length, str2.length
+
+ m = 0.0
+ t = 0.0
+ range = length2 > 3 ? length2 / 2 - 1 : 0
+ flags1 = 0
+ flags2 = 0
+
+ # Avoid duplicating enumerable objects
+ str1_codepoints = str1.codepoints
+ str2_codepoints = str2.codepoints
+
+ i = 0
+ while i < length1
+ last = i + range
+ j = (i >= range) ? i - range : 0
+
+ while j <= last
+ if flags2[j] == 0 && str1_codepoints[i] == str2_codepoints[j]
+ flags2 |= (1 << j)
+ flags1 |= (1 << i)
+ m += 1
+ break
+ end
+
+ j += 1
+ end
+
+ i += 1
+ end
+
+ k = i = 0
+ while i < length1
+ if flags1[i] != 0
+ j = index = k
+
+ k = while j < length2
+ index = j
+ break(j + 1) if flags2[j] != 0
+
+ j += 1
+ end
+
+ t += 1 if str1_codepoints[i] != str2_codepoints[index]
+ end
+
+ i += 1
+ end
+ t = (t / 2).floor
+
+ m == 0 ? 0 : (m / length1 + m / length2 + (m - t) / m) / 3
+ end
+ end
+
+ module JaroWinkler
+ WEIGHT = 0.1
+ THRESHOLD = 0.7
+
+ module_function
+
+ def distance(str1, str2)
+ jaro_distance = Jaro.distance(str1, str2)
+
+ if jaro_distance > THRESHOLD
+ codepoints2 = str2.codepoints
+ prefix_bonus = 0
+
+ str1.each_codepoint do |char1|
+ char1 == codepoints2[prefix_bonus] && prefix_bonus < 4 ? prefix_bonus += 1 : break
+ end
+
+ jaro_distance + (prefix_bonus * WEIGHT * (1 - jaro_distance))
+ else
+ jaro_distance
+ end
+ end
+ end
+end
diff --git a/lib/did_you_mean/levenshtein.rb b/lib/did_you_mean/levenshtein.rb
new file mode 100644
index 0000000000..098053470f
--- /dev/null
+++ b/lib/did_you_mean/levenshtein.rb
@@ -0,0 +1,57 @@
+module DidYouMean
+ module Levenshtein # :nodoc:
+ # This code is based directly on the Text gem implementation
+ # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher.
+ #
+ # Returns a value representing the "cost" of transforming str1 into str2
+ def distance(str1, str2)
+ n = str1.length
+ m = str2.length
+ return m if n.zero?
+ return n if m.zero?
+
+ d = (0..m).to_a
+ x = nil
+
+ # to avoid duplicating an enumerable object, create it outside of the loop
+ str2_codepoints = str2.codepoints
+
+ str1.each_codepoint.with_index(1) do |char1, i|
+ j = 0
+ while j < m
+ cost = (char1 == str2_codepoints[j]) ? 0 : 1
+ x = min3(
+ d[j+1] + 1, # insertion
+ i + 1, # deletion
+ d[j] + cost # substitution
+ )
+ d[j] = i
+ i = x
+
+ j += 1
+ end
+ d[m] = x
+ end
+
+ x
+ end
+ module_function :distance
+
+ private
+
+ # detects the minimum value out of three arguments. This method is
+ # faster than `[a, b, c].min` and puts less GC pressure.
+ # See https://github.com/ruby/did_you_mean/pull/1 for a performance
+ # benchmark.
+ def min3(a, b, c)
+ if a < b && a < c
+ a
+ elsif b < c
+ b
+ else
+ c
+ end
+ end
+ module_function :min3
+ end
+end
diff --git a/lib/did_you_mean/spell_checker.rb b/lib/did_you_mean/spell_checker.rb
new file mode 100644
index 0000000000..37da2fc7a6
--- /dev/null
+++ b/lib/did_you_mean/spell_checker.rb
@@ -0,0 +1,46 @@
+# frozen-string-literal: true
+
+require_relative "levenshtein"
+require_relative "jaro_winkler"
+
+module DidYouMean
+ class SpellChecker
+ def initialize(dictionary:)
+ @dictionary = dictionary
+ end
+
+ def correct(input)
+ normalized_input = normalize(input)
+ threshold = normalized_input.length > 3 ? 0.834 : 0.77
+
+ words = @dictionary.select { |word| JaroWinkler.distance(normalize(word), normalized_input) >= threshold }
+ words.reject! { |word| input.to_s == word.to_s }
+ words.sort_by! { |word| JaroWinkler.distance(word.to_s, normalized_input) }
+ words.reverse!
+
+ # Correct mistypes
+ threshold = (normalized_input.length * 0.25).ceil
+ corrections = words.select { |c| Levenshtein.distance(normalize(c), normalized_input) <= threshold }
+
+ # Correct misspells
+ if corrections.empty?
+ corrections = words.select do |word|
+ word = normalize(word)
+ length = normalized_input.length < word.length ? normalized_input.length : word.length
+
+ Levenshtein.distance(word, normalized_input) < length
+ end.first(1)
+ end
+
+ corrections
+ end
+
+ private
+
+ def normalize(str_or_symbol) #:nodoc:
+ str = str_or_symbol.to_s.downcase
+ str.tr!("@", "")
+ str
+ end
+ end
+end
diff --git a/lib/did_you_mean/spell_checkers/key_error_checker.rb b/lib/did_you_mean/spell_checkers/key_error_checker.rb
new file mode 100644
index 0000000000..955bff1be6
--- /dev/null
+++ b/lib/did_you_mean/spell_checkers/key_error_checker.rb
@@ -0,0 +1,28 @@
+require_relative "../spell_checker"
+
+module DidYouMean
+ class KeyErrorChecker
+ def initialize(key_error)
+ @key = key_error.key
+ @keys = key_error.receiver.keys
+ end
+
+ def corrections
+ @corrections ||= exact_matches.empty? ? SpellChecker.new(dictionary: @keys).correct(@key).map(&:inspect) : exact_matches
+ end
+
+ private
+
+ def exact_matches
+ @exact_matches ||= @keys.select { |word| @key == word.to_s }.map { |obj| format_object(obj) }
+ end
+
+ def format_object(symbol_or_object)
+ if symbol_or_object.is_a?(Symbol)
+ ":#{symbol_or_object}"
+ else
+ symbol_or_object.to_s
+ end
+ end
+ end
+end
diff --git a/lib/did_you_mean/spell_checkers/method_name_checker.rb b/lib/did_you_mean/spell_checkers/method_name_checker.rb
new file mode 100644
index 0000000000..b5cbbb5da6
--- /dev/null
+++ b/lib/did_you_mean/spell_checkers/method_name_checker.rb
@@ -0,0 +1,79 @@
+require_relative "../spell_checker"
+
+module DidYouMean
+ class MethodNameChecker
+ attr_reader :method_name, :receiver
+
+ NAMES_TO_EXCLUDE = { NilClass => nil.methods }
+ NAMES_TO_EXCLUDE.default = []
+ Ractor.make_shareable(NAMES_TO_EXCLUDE) if defined?(Ractor)
+
+ # +MethodNameChecker::RB_RESERVED_WORDS+ is the list of reserved words in
+ # Ruby that take an argument. Unlike
+ # +VariableNameChecker::RB_RESERVED_WORDS+, these reserved words require
+ # an argument, and a +NoMethodError+ is raised due to the presence of the
+ # argument.
+ #
+ # The +MethodNameChecker+ will use this list to suggest a reversed word if
+ # a +NoMethodError+ is raised and found closest matches.
+ #
+ # Also see +VariableNameChecker::RB_RESERVED_WORDS+.
+ RB_RESERVED_WORDS = %i(
+ alias
+ case
+ def
+ defined?
+ elsif
+ end
+ ensure
+ for
+ rescue
+ super
+ undef
+ unless
+ until
+ when
+ while
+ yield
+ )
+
+ Ractor.make_shareable(RB_RESERVED_WORDS) if defined?(Ractor)
+
+ def initialize(exception)
+ @method_name = exception.name
+ @receiver = exception.receiver
+ @private_call = exception.respond_to?(:private_call?) ? exception.private_call? : false
+ end
+
+ def corrections
+ @corrections ||= begin
+ dictionary = method_names
+ dictionary = RB_RESERVED_WORDS + dictionary if @private_call
+
+ SpellChecker.new(dictionary: dictionary).correct(method_name) - names_to_exclude
+ end
+ end
+
+ def method_names
+ if Object === receiver
+ method_names = receiver.methods + receiver.singleton_methods
+ method_names += receiver.private_methods if @private_call
+ method_names.uniq!
+ # Assume that people trying to use a writer are not interested in a reader
+ # and vice versa
+ if method_name.match?(/=\Z/)
+ method_names.select! { |name| name.match?(/=\Z/) }
+ else
+ method_names.reject! { |name| name.match?(/=\Z/) }
+ end
+ method_names
+ else
+ []
+ end
+ end
+
+ def names_to_exclude
+ Object === receiver ? NAMES_TO_EXCLUDE[receiver.class] : []
+ end
+ end
+end
diff --git a/lib/did_you_mean/spell_checkers/name_error_checkers.rb b/lib/did_you_mean/spell_checkers/name_error_checkers.rb
new file mode 100644
index 0000000000..6e2aaa4cb1
--- /dev/null
+++ b/lib/did_you_mean/spell_checkers/name_error_checkers.rb
@@ -0,0 +1,20 @@
+require_relative 'name_error_checkers/class_name_checker'
+require_relative 'name_error_checkers/variable_name_checker'
+
+module DidYouMean
+ class << (NameErrorCheckers = Object.new)
+ def new(exception)
+ case exception.original_message
+ when /uninitialized constant/
+ ClassNameChecker
+ when /undefined local variable or method/,
+ /undefined method/,
+ /uninitialized class variable/,
+ /no member '.*' in struct/
+ VariableNameChecker
+ else
+ NullChecker
+ end.new(exception)
+ end
+ end
+end
diff --git a/lib/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb b/lib/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb
new file mode 100644
index 0000000000..924265b929
--- /dev/null
+++ b/lib/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb
@@ -0,0 +1,49 @@
+# frozen-string-literal: true
+
+require_relative "../../spell_checker"
+
+module DidYouMean
+ class ClassNameChecker
+ attr_reader :class_name
+
+ def initialize(exception)
+ @class_name, @receiver, @original_message = exception.name, exception.receiver, exception.original_message
+ end
+
+ def corrections
+ @corrections ||= SpellChecker.new(dictionary: class_names)
+ .correct(class_name)
+ .map(&:full_name)
+ .reject {|qualified_name| @original_message.include?(qualified_name) }
+ end
+
+ def class_names
+ scopes.flat_map do |scope|
+ scope.constants.map do |c|
+ ClassName.new(c, scope == Object ? "" : "#{scope}::")
+ end
+ end
+ end
+
+ def scopes
+ @scopes ||= @receiver.to_s.split("::").inject([Object]) do |_scopes, scope|
+ _scopes << _scopes.last.const_get(scope)
+ end.uniq
+ end
+
+ class ClassName < String
+ attr :namespace
+
+ def initialize(name, namespace = '')
+ super(name.to_s)
+ @namespace = namespace
+ end
+
+ def full_name
+ self.class.new("#{namespace}#{self}")
+ end
+ end
+
+ private_constant :ClassName
+ end
+end
diff --git a/lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb b/lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb
new file mode 100644
index 0000000000..9a6e04fe64
--- /dev/null
+++ b/lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb
@@ -0,0 +1,85 @@
+# frozen-string-literal: true
+
+require_relative "../../spell_checker"
+
+module DidYouMean
+ class VariableNameChecker
+ attr_reader :name, :method_names, :lvar_names, :ivar_names, :cvar_names
+
+ NAMES_TO_EXCLUDE = { 'foo' => [:fork, :for] }
+ NAMES_TO_EXCLUDE.default = []
+ Ractor.make_shareable(NAMES_TO_EXCLUDE) if defined?(Ractor)
+
+ # +VariableNameChecker::RB_RESERVED_WORDS+ is the list of all reserved
+ # words in Ruby. They could be declared like methods are, and a typo would
+ # cause Ruby to raise a +NameError+ because of the way they are declared.
+ #
+ # The +:VariableNameChecker+ will use this list to suggest a reversed word
+ # if a +NameError+ is raised and found closest matches, excluding:
+ #
+ # * +do+
+ # * +if+
+ # * +in+
+ # * +or+
+ #
+ # Also see +MethodNameChecker::RB_RESERVED_WORDS+.
+ RB_RESERVED_WORDS = %i(
+ BEGIN
+ END
+ alias
+ and
+ begin
+ break
+ case
+ class
+ def
+ defined?
+ else
+ elsif
+ end
+ ensure
+ false
+ for
+ module
+ next
+ nil
+ not
+ redo
+ rescue
+ retry
+ return
+ self
+ super
+ then
+ true
+ undef
+ unless
+ until
+ when
+ while
+ yield
+ __LINE__
+ __FILE__
+ __ENCODING__
+ )
+
+ Ractor.make_shareable(RB_RESERVED_WORDS) if defined?(Ractor)
+
+ def initialize(exception)
+ @name = exception.name.to_s.tr("@", "")
+ @lvar_names = exception.respond_to?(:local_variables) ? exception.local_variables : []
+ receiver = exception.receiver
+
+ @method_names = receiver.methods + receiver.private_methods
+ @ivar_names = receiver.instance_variables
+ @cvar_names = receiver.class.class_variables
+ @cvar_names += receiver.class_variables if receiver.kind_of?(Module)
+ end
+
+ def corrections
+ @corrections ||= SpellChecker
+ .new(dictionary: (RB_RESERVED_WORDS + lvar_names + method_names + ivar_names + cvar_names))
+ .correct(name).uniq - NAMES_TO_EXCLUDE[@name]
+ end
+ end
+end
diff --git a/lib/did_you_mean/spell_checkers/null_checker.rb b/lib/did_you_mean/spell_checkers/null_checker.rb
new file mode 100644
index 0000000000..1306f69d4a
--- /dev/null
+++ b/lib/did_you_mean/spell_checkers/null_checker.rb
@@ -0,0 +1,6 @@
+module DidYouMean
+ class NullChecker
+ def initialize(*); end
+ def corrections; [] end
+ end
+end
diff --git a/lib/did_you_mean/spell_checkers/pattern_key_name_checker.rb b/lib/did_you_mean/spell_checkers/pattern_key_name_checker.rb
new file mode 100644
index 0000000000..622d4dee25
--- /dev/null
+++ b/lib/did_you_mean/spell_checkers/pattern_key_name_checker.rb
@@ -0,0 +1,28 @@
+require_relative "../spell_checker"
+
+module DidYouMean
+ class PatternKeyNameChecker
+ def initialize(no_matching_pattern_key_error)
+ @key = no_matching_pattern_key_error.key
+ @keys = no_matching_pattern_key_error.matchee.keys
+ end
+
+ def corrections
+ @corrections ||= exact_matches.empty? ? SpellChecker.new(dictionary: @keys).correct(@key).map(&:inspect) : exact_matches
+ end
+
+ private
+
+ def exact_matches
+ @exact_matches ||= @keys.select { |word| @key == word.to_s }.map { |obj| format_object(obj) }
+ end
+
+ def format_object(symbol_or_object)
+ if symbol_or_object.is_a?(Symbol)
+ ":#{symbol_or_object}"
+ else
+ symbol_or_object.to_s
+ end
+ end
+ end
+end
diff --git a/lib/did_you_mean/spell_checkers/require_path_checker.rb b/lib/did_you_mean/spell_checkers/require_path_checker.rb
new file mode 100644
index 0000000000..586ced37de
--- /dev/null
+++ b/lib/did_you_mean/spell_checkers/require_path_checker.rb
@@ -0,0 +1,39 @@
+# frozen-string-literal: true
+
+require_relative "../spell_checker"
+require_relative "../tree_spell_checker"
+require "rbconfig"
+
+module DidYouMean
+ class RequirePathChecker
+ attr_reader :path
+
+ INITIAL_LOAD_PATH = $LOAD_PATH.dup.freeze
+ Ractor.make_shareable(INITIAL_LOAD_PATH) if defined?(Ractor)
+
+ ENV_SPECIFIC_EXT = ".#{RbConfig::CONFIG["DLEXT"]}"
+ Ractor.make_shareable(ENV_SPECIFIC_EXT) if defined?(Ractor)
+
+ private_constant :INITIAL_LOAD_PATH, :ENV_SPECIFIC_EXT
+
+ def self.requireables
+ @requireables ||= INITIAL_LOAD_PATH
+ .flat_map {|path| Dir.glob("**/???*{.rb,#{ENV_SPECIFIC_EXT}}", base: path) }
+ .map {|path| path.chomp!(".rb") || path.chomp!(ENV_SPECIFIC_EXT) }
+ end
+
+ def initialize(exception)
+ @path = exception.path
+ end
+
+ def corrections
+ @corrections ||= begin
+ threshold = path.size * 2
+ dictionary = self.class.requireables.reject {|str| str.size >= threshold }
+ spell_checker = path.include?("/") ? TreeSpellChecker : SpellChecker
+
+ spell_checker.new(dictionary: dictionary).correct(path).uniq
+ end
+ end
+ end
+end
diff --git a/lib/did_you_mean/tree_spell_checker.rb b/lib/did_you_mean/tree_spell_checker.rb
new file mode 100644
index 0000000000..799f07fcf0
--- /dev/null
+++ b/lib/did_you_mean/tree_spell_checker.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+module DidYouMean
+ # spell checker for a dictionary that has a tree
+ # structure, see doc/tree_spell_checker_api.md
+ class TreeSpellChecker
+ attr_reader :dictionary, :separator, :augment
+
+ def initialize(dictionary:, separator: '/', augment: nil)
+ @dictionary = dictionary
+ @separator = separator
+ @augment = augment
+ end
+
+ def correct(input)
+ plausibles = plausible_dimensions(input)
+ return fall_back_to_normal_spell_check(input) if plausibles.empty?
+
+ suggestions = find_suggestions(input, plausibles)
+ return fall_back_to_normal_spell_check(input) if suggestions.empty?
+
+ suggestions
+ end
+
+ def dictionary_without_leaves
+ @dictionary_without_leaves ||= dictionary.map { |word| word.split(separator)[0..-2] }.uniq
+ end
+
+ def tree_depth
+ @tree_depth ||= dictionary_without_leaves.max { |a, b| a.size <=> b.size }.size
+ end
+
+ def dimensions
+ @dimensions ||= tree_depth.times.map do |index|
+ dictionary_without_leaves.map { |element| element[index] }.compact.uniq
+ end
+ end
+
+ def find_leaves(path)
+ path_with_separator = "#{path}#{separator}"
+
+ dictionary
+ .select {|str| str.include?(path_with_separator) }
+ .map {|str| str.gsub(path_with_separator, '') }
+ end
+
+ def plausible_dimensions(input)
+ input.split(separator)[0..-2]
+ .map
+ .with_index { |element, index| correct_element(dimensions[index], element) if dimensions[index] }
+ .compact
+ end
+
+ def possible_paths(states)
+ states.map { |state| state.join(separator) }
+ end
+
+ private
+
+ def find_suggestions(input, plausibles)
+ states = plausibles[0].product(*plausibles[1..-1])
+ paths = possible_paths(states)
+ leaf = input.split(separator).last
+
+ find_ideas(paths, leaf)
+ end
+
+ def fall_back_to_normal_spell_check(input)
+ return [] unless augment
+
+ ::DidYouMean::SpellChecker.new(dictionary: dictionary).correct(input)
+ end
+
+ def find_ideas(paths, leaf)
+ paths.flat_map do |path|
+ names = find_leaves(path)
+ ideas = correct_element(names, leaf)
+
+ ideas_to_paths(ideas, leaf, names, path)
+ end.compact
+ end
+
+ def ideas_to_paths(ideas, leaf, names, path)
+ if ideas.empty?
+ nil
+ elsif names.include?(leaf)
+ ["#{path}#{separator}#{leaf}"]
+ else
+ ideas.map {|str| "#{path}#{separator}#{str}" }
+ end
+ end
+
+ def correct_element(names, element)
+ return names if names.size == 1
+
+ str = normalize(element)
+
+ return [str] if names.include?(str)
+
+ ::DidYouMean::SpellChecker.new(dictionary: names).correct(str)
+ end
+
+ def normalize(str)
+ str.downcase!
+ str.tr!('@', ' ') if str.include?('@')
+ str
+ end
+ end
+end
diff --git a/lib/did_you_mean/verbose.rb b/lib/did_you_mean/verbose.rb
new file mode 100644
index 0000000000..1ff19aef80
--- /dev/null
+++ b/lib/did_you_mean/verbose.rb
@@ -0,0 +1,2 @@
+warn "The verbose formatter has been removed and now `require 'did_you_mean/verbose'` has no effect. Please " \
+ "remove this call."
diff --git a/lib/did_you_mean/version.rb b/lib/did_you_mean/version.rb
new file mode 100644
index 0000000000..85d80e4230
--- /dev/null
+++ b/lib/did_you_mean/version.rb
@@ -0,0 +1,3 @@
+module DidYouMean
+ VERSION = "2.0.0".freeze
+end
diff --git a/lib/drb.rb b/lib/drb.rb
deleted file mode 100644
index 93cc811e14..0000000000
--- a/lib/drb.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-require 'drb/drb'
-
diff --git a/lib/drb/acl.rb b/lib/drb/acl.rb
deleted file mode 100644
index 861c8a514d..0000000000
--- a/lib/drb/acl.rb
+++ /dev/null
@@ -1,146 +0,0 @@
-# acl-2.0 - simple Access Control List
-#
-# Copyright (c) 2000,2002,2003 Masatoshi SEKI
-#
-# acl.rb is copyrighted free software by Masatoshi SEKI.
-# You can redistribute it and/or modify it under the same terms as Ruby.
-
-require 'ipaddr'
-
-class ACL
- VERSION=["2.0.0"]
- class ACLEntry
- def initialize(str)
- if str == '*' or str == 'all'
- @pat = [:all]
- elsif str.include?('*')
- @pat = [:name, dot_pat(str)]
- else
- begin
- @pat = [:ip, IPAddr.new(str)]
- rescue ArgumentError
- @pat = [:name, dot_pat(str)]
- end
- end
- end
-
- private
- def dot_pat_str(str)
- list = str.split('.').collect { |s|
- (s == '*') ? '.+' : s
- }
- list.join("\\.")
- end
-
- private
- def dot_pat(str)
- exp = "^" + dot_pat_str(str) + "$"
- Regexp.new(exp)
- end
-
- public
- def match(addr)
- case @pat[0]
- when :all
- true
- when :ip
- begin
- ipaddr = IPAddr.new(addr[3])
- ipaddr = ipaddr.ipv4_mapped if @pat[1].ipv6? && ipaddr.ipv4?
- rescue ArgumentError
- return false
- end
- (@pat[1].include?(ipaddr)) ? true : false
- when :name
- (@pat[1] =~ addr[2]) ? true : false
- else
- false
- end
- end
- end
-
- class ACLList
- def initialize
- @list = []
- end
-
- public
- def match(addr)
- @list.each do |e|
- return true if e.match(addr)
- end
- false
- end
-
- public
- def add(str)
- @list.push(ACLEntry.new(str))
- end
- end
-
- DENY_ALLOW = 0
- ALLOW_DENY = 1
-
- def initialize(list=nil, order = DENY_ALLOW)
- @order = order
- @deny = ACLList.new
- @allow = ACLList.new
- install_list(list) if list
- end
-
- public
- def allow_socket?(soc)
- allow_addr?(soc.peeraddr)
- end
-
- public
- def allow_addr?(addr)
- case @order
- when DENY_ALLOW
- return true if @allow.match(addr)
- return false if @deny.match(addr)
- return true
- when ALLOW_DENY
- return false if @deny.match(addr)
- return true if @allow.match(addr)
- return false
- else
- false
- end
- end
-
- public
- def install_list(list)
- i = 0
- while i < list.size
- permission, domain = list.slice(i,2)
- case permission.downcase
- when 'allow'
- @allow.add(domain)
- when 'deny'
- @deny.add(domain)
- else
- raise "Invalid ACL entry #{list.to_s}"
- end
- i += 2
- end
- end
-end
-
-if __FILE__ == $0
- # example
- list = %w(deny all
- allow 192.168.1.1
- allow ::ffff:192.168.1.2
- allow 192.168.1.3
- )
-
- addr = ["AF_INET", 10, "lc630", "192.168.1.3"]
-
- acl = ACL.new
- p acl.allow_addr?(addr)
-
- acl = ACL.new(list, ACL::DENY_ALLOW)
- p acl.allow_addr?(addr)
-end
-
diff --git a/lib/drb/drb.rb b/lib/drb/drb.rb
deleted file mode 100644
index 13a89de07f..0000000000
--- a/lib/drb/drb.rb
+++ /dev/null
@@ -1,1783 +0,0 @@
-#
-# = drb/drb.rb
-#
-# Distributed Ruby: _dRuby_ version 2.0.4
-#
-# Copyright (c) 1999-2003 Masatoshi SEKI. You can redistribute it and/or
-# modify it under the same terms as Ruby.
-#
-# Author:: Masatoshi SEKI
-#
-# Documentation:: William Webber (william@williamwebber.com)
-#
-# == Overview
-#
-# dRuby is a distributed object system for Ruby. It allows an object in one
-# Ruby process to invoke methods on an object in another Ruby process on the
-# same or a different machine.
-#
-# The Ruby standard library contains the core classes of the dRuby package.
-# However, the full package also includes access control lists and the
-# Rinda tuple-space distributed task management system, as well as a
-# large number of samples. The full dRuby package can be downloaded from
-# the dRuby home page (see *References*).
-#
-# For an introduction and examples of usage see the documentation to the
-# DRb module.
-#
-# == References
-#
-# [http://www2a.biglobe.ne.jp/~seki/ruby/druby.html]
-# The dRuby home page, in Japanese. Contains the full dRuby package
-# and links to other Japanese-language sources.
-#
-# [http://www2a.biglobe.ne.jp/~seki/ruby/druby.en.html]
-# The English version of the dRuby home page.
-#
-# [http://www.chadfowler.com/ruby/drb.html]
-# A quick tutorial introduction to using dRuby by Chad Fowler.
-#
-# [http://www.linux-mag.com/2002-09/ruby_05.html]
-# A tutorial introduction to dRuby in Linux Magazine by Dave Thomas.
-# Includes a discussion of Rinda.
-#
-# [http://www.eng.cse.dmu.ac.uk/~hgs/ruby/dRuby/]
-# Links to English-language Ruby material collected by Hugh Sasse.
-#
-# [http://www.rubycentral.com/book/ospace.html]
-# The chapter from *Programming* *Ruby* by Dave Thomas and Andy Hunt
-# which discusses dRuby.
-#
-# [http://www.clio.ne.jp/home/web-i31s/Flotuard/Ruby/PRC2K_seki/dRuby.en.html]
-# Translation of presentation on Ruby by Masatoshi Seki.
-
-require 'socket'
-require 'thread'
-require 'fcntl'
-require 'drb/eq'
-
-#
-# == Overview
-#
-# dRuby is a distributed object system for Ruby. It is written in
-# pure Ruby and uses its own protocol. No add-in services are needed
-# beyond those provided by the Ruby runtime, such as TCP sockets. It
-# does not rely on or interoperate with other distributed object
-# systems such as CORBA, RMI, or .NET.
-#
-# dRuby allows methods to be called in one Ruby process upon a Ruby
-# object located in another Ruby process, even on another machine.
-# References to objects can be passed between processes. Method
-# arguments and return values are dumped and loaded in marshalled
-# format. All of this is done transparently to both the caller of the
-# remote method and the object that it is called upon.
-#
-# An object in a remote process is locally represented by a
-# DRb::DRbObject instance. This acts as a sort of proxy for the
-# remote object. Methods called upon this DRbObject instance are
-# forwarded to its remote object. This is arranged dynamically at run
-# time. There are no statically declared interfaces for remote
-# objects, such as CORBA's IDL.
-#
-# dRuby calls made into a process are handled by a DRb::DRbServer
-# instance within that process. This reconstitutes the method call,
-# invokes it upon the specified local object, and returns the value to
-# the remote caller. Any object can receive calls over dRuby. There
-# is no need to implement a special interface, or mixin special
-# functionality. Nor, in the general case, does an object need to
-# explicitly register itself with a DRbServer in order to receive
-# dRuby calls.
-#
-# One process wishing to make dRuby calls upon another process must
-# somehow obtain an initial reference to an object in the remote
-# process by some means other than as the return value of a remote
-# method call, as there is initially no remote object reference it can
-# invoke a method upon. This is done by attaching to the server by
-# URI. Each DRbServer binds itself to a URI such as
-# 'druby://example.com:8787'. A DRbServer can have an object attached
-# to it that acts as the server's *front* *object*. A DRbObject can
-# be explicitly created from the server's URI. This DRbObject's
-# remote object will be the server's front object. This front object
-# can then return references to other Ruby objects in the DRbServer's
-# process.
-#
-# Method calls made over dRuby behave largely the same as normal Ruby
-# method calls made within a process. Method calls with blocks are
-# supported, as are raising exceptions. In addition to a method's
-# standard errors, a dRuby call may also raise one of the
-# dRuby-specific errors, all of which are subclasses of DRb::DRbError.
-#
-# Any type of object can be passed as an argument to a dRuby call or
-# returned as its return value. By default, such objects are dumped
-# or marshalled at the local end, then loaded or unmarshalled at the
-# remote end. The remote end therefore receives a copy of the local
-# object, not a distributed reference to it; methods invoked upon this
-# copy are executed entirely in the remote process, not passed on to
-# the local original. This has semantics similar to pass-by-value.
-#
-# However, if an object cannot be marshalled, a dRuby reference to it
-# is passed or returned instead. This will turn up at the remote end
-# as a DRbObject instance. All methods invoked upon this remote proxy
-# are forwarded to the local object, as described in the discussion of
-# DRbObjects. This has semantics similar to the normal Ruby
-# pass-by-reference.
-#
-# The easiest way to signal that we want an otherwise marshallable
-# object to be passed or returned as a DRbObject reference, rather
-# than marshalled and sent as a copy, is to include the
-# DRb::DRbUndumped mixin module.
-#
-# dRuby supports calling remote methods with blocks. As blocks (or
-# rather the Proc objects that represent them) are not marshallable,
-# the block executes in the local, not the remote, context. Each
-# value yielded to the block is passed from the remote object to the
-# local block, then the value returned by each block invocation is
-# passed back to the remote execution context to be collected, before
-# the collected values are finally returned to the local context as
-# the return value of the method invocation.
-#
-# == Examples of usage
-#
-# For more dRuby samples, see the +samples+ directory in the full
-# dRuby distribution.
-#
-# === dRuby in client/server mode
-#
-# This illustrates setting up a simple client-server drb
-# system. Run the server and client code in different terminals,
-# starting the server code first.
-#
-# ==== Server code
-#
-# require 'drb/drb'
-#
-# # The URI for the server to connect to
-# URI="druby://localhost:8787"
-#
-# class TimeServer
-#
-# def get_current_time
-# return Time.now
-# end
-#
-# end
-#
-# # The object that handles requests on the server
-# FRONT_OBJECT=TimeServer.new
-#
-# $SAFE = 1 # disable eval() and friends
-#
-# DRb.start_service(URI, FRONT_OBJECT)
-# # Wait for the drb server thread to finish before exiting.
-# DRb.thread.join
-#
-# ==== Client code
-#
-# require 'drb/drb'
-#
-# # The URI to connect to
-# SERVER_URI="druby://localhost:8787"
-#
-# # Start a local DRbServer to handle callbacks.
-# #
-# # Not necessary for this small example, but will be required
-# # as soon as we pass a non-marshallable object as an argument
-# # to a dRuby call.
-# DRb.start_service
-#
-# timeserver = DRbObject.new_with_uri(SERVER_URI)
-# puts timeserver.get_current_time
-#
-# === Remote objects under dRuby
-#
-# This example illustrates returning a reference to an object
-# from a dRuby call. The Logger instances live in the server
-# process. References to them are returned to the client process,
-# where methods can be invoked upon them. These methods are
-# executed in the server process.
-#
-# ==== Server code
-#
-# require 'drb/drb'
-#
-# URI="druby://localhost:8787"
-#
-# class Logger
-#
-# # Make dRuby send Logger instances as dRuby references,
-# # not copies.
-# include DRb::DRbUndumped
-#
-# def initialize(n, fname)
-# @name = n
-# @filename = fname
-# end
-#
-# def log(message)
-# File.open(@filename, "a") do |f|
-# f.puts("#{Time.now}: #{@name}: #{message}")
-# end
-# end
-#
-# end
-#
-# # We have a central object for creating and retrieving loggers.
-# # This retains a local reference to all loggers created. This
-# # is so an existing logger can be looked up by name, but also
-# # to prevent loggers from being garbage collected. A dRuby
-# # reference to an object is not sufficient to prevent it being
-# # garbage collected!
-# class LoggerFactory
-#
-# def initialize(bdir)
-# @basedir = bdir
-# @loggers = {}
-# end
-#
-# def get_logger(name)
-# if !@loggers.has_key? name
-# # make the filename safe, then declare it to be so
-# fname = name.gsub(/[.\/]/, "_").untaint
-# @loggers[name] = Logger.new(name, @basedir + "/" + fname)
-# end
-# return @loggers[name]
-# end
-#
-# end
-#
-# FRONT_OBJECT=LoggerFactory.new("/tmp/dlog")
-#
-# $SAFE = 1 # disable eval() and friends
-#
-# DRb.start_service(URI, FRONT_OBJECT)
-# DRb.thread.join
-#
-# ==== Client code
-#
-# require 'drb/drb'
-#
-# SERVER_URI="druby://localhost:8787"
-#
-# DRb.start_service
-#
-# log_service=DRbObject.new_with_uri(SERVER_URI)
-#
-# ["loga", "logb", "logc"].each do |logname|
-#
-# logger=log_service.get_logger(logname)
-#
-# logger.log("Hello, world!")
-# logger.log("Goodbye, world!")
-# logger.log("=== EOT ===")
-#
-# end
-#
-# == Security
-#
-# As with all network services, security needs to be considered when
-# using dRuby. By allowing external access to a Ruby object, you are
-# not only allowing outside clients to call the methods you have
-# defined for that object, but by default to execute arbitrary Ruby
-# code on your server. Consider the following:
-#
-# # !!! UNSAFE CODE !!!
-# ro = DRbObject::new_with_uri("druby://your.server.com:8989")
-# class << ro
-# undef :instance_eval # force call to be passed to remote object
-# end
-# ro.instance_eval("`rm -rf *`")
-#
-# The dangers posed by instance_eval and friends are such that a
-# DRbServer should generally be run with $SAFE set to at least
-# level 1. This will disable eval() and related calls on strings
-# passed across the wire. The sample usage code given above follows
-# this practice.
-#
-# A DRbServer can be configured with an access control list to
-# selectively allow or deny access from specified IP addresses. The
-# main druby distribution provides the ACL class for this purpose. In
-# general, this mechanism should only be used alongside, rather than
-# as a replacement for, a good firewall.
-#
-# == dRuby internals
-#
-# dRuby is implemented using three main components: a remote method
-# call marshaller/unmarshaller; a transport protocol; and an
-# ID-to-object mapper. The latter two can be directly, and the first
-# indirectly, replaced, in order to provide different behaviour and
-# capabilities.
-#
-# Marshalling and unmarshalling of remote method calls is performed by
-# a DRb::DRbMessage instance. This uses the Marshal module to dump
-# the method call before sending it over the transport layer, then
-# reconstitute it at the other end. There is normally no need to
-# replace this component, and no direct way is provided to do so.
-# However, it is possible to implement an alternative marshalling
-# scheme as part of an implementation of the transport layer.
-#
-# The transport layer is responsible for opening client and server
-# network connections and forwarding dRuby request across them.
-# Normally, it uses DRb::DRbMessage internally to manage marshalling
-# and unmarshalling. The transport layer is managed by
-# DRb::DRbProtocol. Multiple protocols can be installed in
-# DRbProtocol at the one time; selection between them is determined by
-# the scheme of a dRuby URI. The default transport protocol is
-# selected by the scheme 'druby:', and implemented by
-# DRb::DRbTCPSocket. This uses plain TCP/IP sockets for
-# communication. An alternative protocol, using UNIX domain sockets,
-# is implemented by DRb::DRbUNIXSocket in the file drb/unix.rb, and
-# selected by the scheme 'drbunix:'. A sample implementation over
-# HTTP can be found in the samples accompanying the main dRuby
-# distribution.
-#
-# The ID-to-object mapping component maps dRuby object ids to the
-# objects they refer to, and vice versa. The implementation to use
-# can be specified as part of a DRb::DRbServer's configuration. The
-# default implementation is provided by DRb::DRbIdConv. It uses an
-# object's ObjectSpace id as its dRuby id. This means that the dRuby
-# reference to that object only remains meaningful for the lifetime of
-# the object's process and the lifetime of the object within that
-# process. A modified implementation is provided by DRb::TimerIdConv
-# in the file drb/timeridconv.rb. This implementation retains a local
-# reference to all objects exported over dRuby for a configurable
-# period of time (defaulting to ten minutes), to prevent them being
-# garbage-collected within this time. Another sample implementation
-# is provided in sample/name.rb in the main dRuby distribution. This
-# allows objects to specify their own id or "name". A dRuby reference
-# can be made persistent across processes by having each process
-# register an object using the same dRuby name.
-#
-module DRb
-
- # Superclass of all errors raised in the DRb module.
- class DRbError < RuntimeError; end
-
- # Error raised when an error occurs on the underlying communication
- # protocol.
- class DRbConnError < DRbError; end
-
- # Class responsible for converting between an object and its id.
- #
- # This, the default implementation, uses an object's local ObjectSpace
- # __id__ as its id. This means that an object's identification over
- # drb remains valid only while that object instance remains alive
- # within the server runtime.
- #
- # For alternative mechanisms, see DRb::TimerIdConv in rdb/timeridconv.rb
- # and DRbNameIdConv in sample/name.rb in the full drb distribution.
- class DRbIdConv
-
- # Convert an object reference id to an object.
- #
- # This implementation looks up the reference id in the local object
- # space and returns the object it refers to.
- def to_obj(ref)
- ObjectSpace._id2ref(ref)
- end
-
- # Convert an object into a reference id.
- #
- # This implementation returns the object's __id__ in the local
- # object space.
- def to_id(obj)
- obj.nil? ? nil : obj.__id__
- end
- end
-
- # Mixin module making an object undumpable or unmarshallable.
- #
- # If an object which includes this module is returned by method
- # called over drb, then the object remains in the server space
- # and a reference to the object is returned, rather than the
- # object being marshalled and moved into the client space.
- module DRbUndumped
- def _dump(dummy) # :nodoc:
- raise TypeError, 'can\'t dump'
- end
- end
-
- # Error raised by the DRb module when an attempt is made to refer to
- # the context's current drb server but the context does not have one.
- # See #current_server.
- class DRbServerNotFound < DRbError; end
-
- # Error raised by the DRbProtocol module when it cannot find any
- # protocol implementation support the scheme specified in a URI.
- class DRbBadURI < DRbError; end
-
- # Error raised by a dRuby protocol when it doesn't support the
- # scheme specified in a URI. See DRb::DRbProtocol.
- class DRbBadScheme < DRbError; end
-
- # An exception wrapping a DRb::DRbUnknown object
- class DRbUnknownError < DRbError
-
- # Create a new DRbUnknownError for the DRb::DRbUnknown object +unknown+
- def initialize(unknown)
- @unknown = unknown
- super(unknown.name)
- end
-
- # Get the wrapped DRb::DRbUnknown object.
- attr_reader :unknown
-
- def self._load(s) # :nodoc:
- Marshal::load(s)
- end
-
- def _dump(lv) # :nodoc:
- Marshal::dump(@unknown)
- end
- end
-
- # An exception wrapping an error object
- class DRbRemoteError < DRbError
- def initialize(error)
- @reason = error.class.to_s
- super("#{error.message} (#{error.class})")
- set_backtrace(error.backtrace)
- end
-
- # the class of the error, as a string.
- attr_reader :reason
- end
-
- # Class wrapping a marshalled object whose type is unknown locally.
- #
- # If an object is returned by a method invoked over drb, but the
- # class of the object is unknown in the client namespace, or
- # the object is a constant unknown in the client namespace, then
- # the still-marshalled object is returned wrapped in a DRbUnknown instance.
- #
- # If this object is passed as an argument to a method invoked over
- # drb, then the wrapped object is passed instead.
- #
- # The class or constant name of the object can be read from the
- # +name+ attribute. The marshalled object is held in the +buf+
- # attribute.
- class DRbUnknown
-
- # Create a new DRbUnknown object.
- #
- # +buf+ is a string containing a marshalled object that could not
- # be unmarshalled. +err+ is the error message that was raised
- # when the unmarshalling failed. It is used to determine the
- # name of the unmarshalled object.
- def initialize(err, buf)
- case err.to_s
- when /uninitialized constant (\S+)/
- @name = $1
- when /undefined class\/module (\S+)/
- @name = $1
- else
- @name = nil
- end
- @buf = buf
- end
-
- # The name of the unknown thing.
- #
- # Class name for unknown objects; variable name for unknown
- # constants.
- attr_reader :name
-
- # Buffer contained the marshalled, unknown object.
- attr_reader :buf
-
- def self._load(s) # :nodoc:
- begin
- Marshal::load(s)
- rescue NameError, ArgumentError
- DRbUnknown.new($!, s)
- end
- end
-
- def _dump(lv) # :nodoc:
- @buf
- end
-
- # Attempt to load the wrapped marshalled object again.
- #
- # If the class of the object is now known locally, the object
- # will be unmarshalled and returned. Otherwise, a new
- # but identical DRbUnknown object will be returned.
- def reload
- self.class._load(@buf)
- end
-
- # Create a DRbUnknownError exception containing this object.
- def exception
- DRbUnknownError.new(self)
- end
- end
-
- class DRbArray
- def initialize(ary)
- @ary = ary.collect { |obj|
- if obj.kind_of? DRbUndumped
- DRbObject.new(obj)
- else
- begin
- Marshal.dump(obj)
- obj
- rescue
- DRbObject.new(obj)
- end
- end
- }
- end
-
- def self._load(s)
- Marshal::load(s)
- end
-
- def _dump(lv)
- Marshal.dump(@ary)
- end
- end
-
- # Handler for sending and receiving drb messages.
- #
- # This takes care of the low-level marshalling and unmarshalling
- # of drb requests and responses sent over the wire between server
- # and client. This relieves the implementor of a new drb
- # protocol layer with having to deal with these details.
- #
- # The user does not have to directly deal with this object in
- # normal use.
- class DRbMessage
- def initialize(config) # :nodoc:
- @load_limit = config[:load_limit]
- @argc_limit = config[:argc_limit]
- end
-
- def dump(obj, error=false) # :nodoc:
- obj = make_proxy(obj, error) if obj.kind_of? DRbUndumped
- begin
- str = Marshal::dump(obj)
- rescue
- str = Marshal::dump(make_proxy(obj, error))
- end
- [str.size].pack('N') + str
- end
-
- def load(soc) # :nodoc:
- begin
- sz = soc.read(4) # sizeof (N)
- rescue
- raise(DRbConnError, $!.message, $!.backtrace)
- end
- raise(DRbConnError, 'connection closed') if sz.nil?
- raise(DRbConnError, 'premature header') if sz.size < 4
- sz = sz.unpack('N')[0]
- raise(DRbConnError, "too large packet #{sz}") if @load_limit < sz
- begin
- str = soc.read(sz)
- rescue
- raise(DRbConnError, $!.message, $!.backtrace)
- end
- raise(DRbConnError, 'connection closed') if str.nil?
- raise(DRbConnError, 'premature marshal format(can\'t read)') if str.size < sz
- DRb.mutex.synchronize do
- begin
- save = Thread.current[:drb_untaint]
- Thread.current[:drb_untaint] = []
- Marshal::load(str)
- rescue NameError, ArgumentError
- DRbUnknown.new($!, str)
- ensure
- Thread.current[:drb_untaint].each do |x|
- x.untaint
- end
- Thread.current[:drb_untaint] = save
- end
- end
- end
-
- def send_request(stream, ref, msg_id, arg, b) # :nodoc:
- ary = []
- ary.push(dump(ref.__drbref))
- ary.push(dump(msg_id.id2name))
- ary.push(dump(arg.length))
- arg.each do |e|
- ary.push(dump(e))
- end
- ary.push(dump(b))
- stream.write(ary.join(''))
- rescue
- raise(DRbConnError, $!.message, $!.backtrace)
- end
-
- def recv_request(stream) # :nodoc:
- ref = load(stream)
- ro = DRb.to_obj(ref)
- msg = load(stream)
- argc = load(stream)
- raise ArgumentError, 'too many arguments' if @argc_limit < argc
- argv = Array.new(argc, nil)
- argc.times do |n|
- argv[n] = load(stream)
- end
- block = load(stream)
- return ro, msg, argv, block
- end
-
- def send_reply(stream, succ, result) # :nodoc:
- stream.write(dump(succ) + dump(result, !succ))
- rescue
- raise(DRbConnError, $!.message, $!.backtrace)
- end
-
- def recv_reply(stream) # :nodoc:
- succ = load(stream)
- result = load(stream)
- [succ, result]
- end
-
- private
- def make_proxy(obj, error=false)
- if error
- DRbRemoteError.new(obj)
- else
- DRbObject.new(obj)
- end
- end
- end
-
- # Module managing the underlying network protocol(s) used by drb.
- #
- # By default, drb uses the DRbTCPSocket protocol. Other protocols
- # can be defined. A protocol must define the following class methods:
- #
- # [open(uri, config)] Open a client connection to the server at +uri+,
- # using configuration +config+. Return a protocol
- # instance for this connection.
- # [open_server(uri, config)] Open a server listening at +uri+,
- # using configuration +config+. Return a
- # protocol instance for this listener.
- # [uri_option(uri, config)] Take a URI, possibly containing an option
- # component (e.g. a trailing '?param=val'),
- # and return a [uri, option] tuple.
- #
- # All of these methods should raise a DRbBadScheme error if the URI
- # does not identify the protocol they support (e.g. "druby:" for
- # the standard Ruby protocol). This is how the DRbProtocol module,
- # given a URI, determines which protocol implementation serves that
- # protocol.
- #
- # The protocol instance returned by #open_server must have the
- # following methods:
- #
- # [accept] Accept a new connection to the server. Returns a protocol
- # instance capable of communicating with the client.
- # [close] Close the server connection.
- # [uri] Get the URI for this server.
- #
- # The protocol instance returned by #open must have the following methods:
- #
- # [send_request (ref, msg_id, arg, b)]
- # Send a request to +ref+ with the given message id and arguments.
- # This is most easily implemented by calling DRbMessage.send_request,
- # providing a stream that sits on top of the current protocol.
- # [recv_reply]
- # Receive a reply from the server and return it as a [success-boolean,
- # reply-value] pair. This is most easily implemented by calling
- # DRb.recv_reply, providing a stream that sits on top of the
- # current protocol.
- # [alive?]
- # Is this connection still alive?
- # [close]
- # Close this connection.
- #
- # The protocol instance returned by #open_server().accept() must have
- # the following methods:
- #
- # [recv_request]
- # Receive a request from the client and return a [object, message,
- # args, block] tuple. This is most easily implemented by calling
- # DRbMessage.recv_request, providing a stream that sits on top of
- # the current protocol.
- # [send_reply(succ, result)]
- # Send a reply to the client. This is most easily implemented
- # by calling DRbMessage.send_reply, providing a stream that sits
- # on top of the current protocol.
- # [close]
- # Close this connection.
- #
- # A new protocol is registered with the DRbProtocol module using
- # the add_protocol method.
- #
- # For examples of other protocols, see DRbUNIXSocket in drb/unix.rb,
- # and HTTP0 in sample/http0.rb and sample/http0serv.rb in the full
- # drb distribution.
- module DRbProtocol
-
- # Add a new protocol to the DRbProtocol module.
- def add_protocol(prot)
- @protocol.push(prot)
- end
- module_function :add_protocol
-
- # Open a client connection to +uri+ with the configuration +config+.
- #
- # The DRbProtocol module asks each registered protocol in turn to
- # try to open the URI. Each protocol signals that it does not handle that
- # URI by raising a DRbBadScheme error. If no protocol recognises the
- # URI, then a DRbBadURI error is raised. If a protocol accepts the
- # URI, but an error occurs in opening it, a DRbConnError is raised.
- def open(uri, config, first=true)
- @protocol.each do |prot|
- begin
- return prot.open(uri, config)
- rescue DRbBadScheme
- rescue DRbConnError
- raise($!)
- rescue
- raise(DRbConnError, "#{uri} - #{$!.inspect}")
- end
- end
- if first && (config[:auto_load] != false)
- auto_load(uri, config)
- return open(uri, config, false)
- end
- raise DRbBadURI, 'can\'t parse uri:' + uri
- end
- module_function :open
-
- # Open a server listening for connections at +uri+ with
- # configuration +config+.
- #
- # The DRbProtocol module asks each registered protocol in turn to
- # try to open a server at the URI. Each protocol signals that it does
- # not handle that URI by raising a DRbBadScheme error. If no protocol
- # recognises the URI, then a DRbBadURI error is raised. If a protocol
- # accepts the URI, but an error occurs in opening it, the underlying
- # error is passed on to the caller.
- def open_server(uri, config, first=true)
- @protocol.each do |prot|
- begin
- return prot.open_server(uri, config)
- rescue DRbBadScheme
- end
- end
- if first && (config[:auto_load] != false)
- auto_load(uri, config)
- return open_server(uri, config, false)
- end
- raise DRbBadURI, 'can\'t parse uri:' + uri
- end
- module_function :open_server
-
- # Parse +uri+ into a [uri, option] pair.
- #
- # The DRbProtocol module asks each registered protocol in turn to
- # try to parse the URI. Each protocol signals that it does not handle that
- # URI by raising a DRbBadScheme error. If no protocol recognises the
- # URI, then a DRbBadURI error is raised.
- def uri_option(uri, config, first=true)
- @protocol.each do |prot|
- begin
- uri, opt = prot.uri_option(uri, config)
- # opt = nil if opt == ''
- return uri, opt
- rescue DRbBadScheme
- end
- end
- if first && (config[:auto_load] != false)
- auto_load(uri, config)
- return uri_option(uri, config, false)
- end
- raise DRbBadURI, 'can\'t parse uri:' + uri
- end
- module_function :uri_option
-
- def auto_load(uri, config) # :nodoc:
- if uri =~ /^drb([a-z0-9]+):/
- require("drb/#{$1}") rescue nil
- end
- end
- module_function :auto_load
- end
-
- # The default drb protocol.
- #
- # Communicates over a TCP socket.
- class DRbTCPSocket
- private
- def self.parse_uri(uri)
- if uri =~ /^druby:\/\/(.*?):(\d+)(\?(.*))?$/
- host = $1
- port = $2.to_i
- option = $4
- [host, port, option]
- else
- raise(DRbBadScheme, uri) unless uri =~ /^druby:/
- raise(DRbBadURI, 'can\'t parse uri:' + uri)
- end
- end
-
- public
-
- # Open a client connection to +uri+ using configuration +config+.
- def self.open(uri, config)
- host, port, option = parse_uri(uri)
- host.untaint
- port.untaint
- soc = TCPSocket.open(host, port)
- self.new(uri, soc, config)
- end
-
- def self.getservername
- host = Socket::gethostname
- begin
- Socket::gethostbyname(host)[0]
- rescue
- 'localhost'
- end
- end
-
- def self.open_server_inaddr_any(host, port)
- infos = Socket::getaddrinfo(host, nil,
- Socket::AF_UNSPEC,
- Socket::SOCK_STREAM,
- 0,
- Socket::AI_PASSIVE)
- family = infos.collect { |af, *_| af }.uniq
- case family
- when ['AF_INET']
- return TCPServer.open('0.0.0.0', port)
- when ['AF_INET6']
- return TCPServer.open('::', port)
- else
- return TCPServer.open(port)
- end
- end
-
- # Open a server listening for connections at +uri+ using
- # configuration +config+.
- def self.open_server(uri, config)
- uri = 'druby://:0' unless uri
- host, port, opt = parse_uri(uri)
- config = {:tcp_original_host => host}.update(config)
- if host.size == 0
- host = getservername
- soc = open_server_inaddr_any(host, port)
- else
- soc = TCPServer.open(host, port)
- end
- port = soc.addr[1] if port == 0
- config[:tcp_port] = port
- uri = "druby://#{host}:#{port}"
- self.new(uri, soc, config)
- end
-
- # Parse +uri+ into a [uri, option] pair.
- def self.uri_option(uri, config)
- host, port, option = parse_uri(uri)
- return "druby://#{host}:#{port}", option
- end
-
- # Create a new DRbTCPSocket instance.
- #
- # +uri+ is the URI we are connected to.
- # +soc+ is the tcp socket we are bound to. +config+ is our
- # configuration.
- def initialize(uri, soc, config={})
- @uri = uri
- @socket = soc
- @config = config
- @acl = config[:tcp_acl]
- @msg = DRbMessage.new(config)
- set_sockopt(@socket)
- end
-
- # Get the URI that we are connected to.
- attr_reader :uri
-
- # Get the address of our TCP peer (the other end of the socket
- # we are bound to.
- def peeraddr
- @socket.peeraddr
- end
-
- # Get the socket.
- def stream; @socket; end
-
- # On the client side, send a request to the server.
- def send_request(ref, msg_id, arg, b)
- @msg.send_request(stream, ref, msg_id, arg, b)
- end
-
- # On the server side, receive a request from the client.
- def recv_request
- @msg.recv_request(stream)
- end
-
- # On the server side, send a reply to the client.
- def send_reply(succ, result)
- @msg.send_reply(stream, succ, result)
- end
-
- # On the client side, receive a reply from the server.
- def recv_reply
- @msg.recv_reply(stream)
- end
-
- public
-
- # Close the connection.
- #
- # If this is an instance returned by #open_server, then this stops
- # listening for new connections altogether. If this is an instance
- # returned by #open or by #accept, then it closes this particular
- # client-server session.
- def close
- if @socket
- @socket.close
- @socket = nil
- end
- end
-
- # On the server side, for an instance returned by #open_server,
- # accept a client connection and return a new instance to handle
- # the server's side of this client-server session.
- def accept
- while true
- s = @socket.accept
- break if (@acl ? @acl.allow_socket?(s) : true)
- s.close
- end
- if @config[:tcp_original_host].to_s.size == 0
- uri = "druby://#{s.addr[3]}:#{@config[:tcp_port]}"
- else
- uri = @uri
- end
- self.class.new(uri, s, @config)
- end
-
- # Check to see if this connection is alive.
- def alive?
- return false unless @socket
- if IO.select([@socket], nil, nil, 0)
- close
- return false
- end
- true
- end
-
- def set_sockopt(soc) # :nodoc:
- soc.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
- soc.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined? Fcntl::FD_CLOEXEC
- end
- end
-
- module DRbProtocol
- @protocol = [DRbTCPSocket] # default
- end
-
- class DRbURIOption # :nodoc: I don't understand the purpose of this class...
- def initialize(option)
- @option = option.to_s
- end
- attr :option
- def to_s; @option; end
-
- def ==(other)
- return false unless DRbURIOption === other
- @option == other.option
- end
-
- def hash
- @option.hash
- end
-
- alias eql? ==
- end
-
- # Object wrapping a reference to a remote drb object.
- #
- # Method calls on this object are relayed to the remote
- # object that this object is a stub for.
- class DRbObject
-
- # Unmarshall a marshalled DRbObject.
- #
- # If the referenced object is located within the local server, then
- # the object itself is returned. Otherwise, a new DRbObject is
- # created to act as a stub for the remote referenced object.
- def self._load(s)
- uri, ref = Marshal.load(s)
-
- if DRb.here?(uri)
- obj = DRb.to_obj(ref)
- if ((! obj.tainted?) && Thread.current[:drb_untaint])
- Thread.current[:drb_untaint].push(obj)
- end
- return obj
- end
-
- self.new_with(uri, ref)
- end
-
- def self.new_with(uri, ref)
- it = self.allocate
- it.instance_variable_set('@uri', uri)
- it.instance_variable_set('@ref', ref)
- it
- end
-
- # Create a new DRbObject from a URI alone.
- def self.new_with_uri(uri)
- self.new(nil, uri)
- end
-
- # Marshall this object.
- #
- # The URI and ref of the object are marshalled.
- def _dump(lv)
- Marshal.dump([@uri, @ref])
- end
-
- # Create a new remote object stub.
- #
- # +obj+ is the (local) object we want to create a stub for. Normally
- # this is +nil+. +uri+ is the URI of the remote object that this
- # will be a stub for.
- def initialize(obj, uri=nil)
- @uri = nil
- @ref = nil
- if obj.nil?
- return if uri.nil?
- @uri, option = DRbProtocol.uri_option(uri, DRb.config)
- @ref = DRbURIOption.new(option) unless option.nil?
- else
- @uri = uri ? uri : (DRb.uri rescue nil)
- @ref = obj ? DRb.to_id(obj) : nil
- end
- end
-
- # Get the URI of the remote object.
- def __drburi
- @uri
- end
-
- # Get the reference of the object, if local.
- def __drbref
- @ref
- end
-
- undef :to_s
- undef :to_a if respond_to?(:to_a)
-
- def respond_to?(msg_id, priv=false)
- case msg_id
- when :_dump
- true
- when :marshal_dump
- false
- else
- method_missing(:respond_to?, msg_id, priv)
- end
- end
-
- # Routes method calls to the referenced object.
- def method_missing(msg_id, *a, &b)
- if DRb.here?(@uri)
- obj = DRb.to_obj(@ref)
- DRb.current_server.check_insecure_method(obj, msg_id)
- return obj.__send__(msg_id, *a, &b)
- end
-
- succ, result = self.class.with_friend(@uri) do
- DRbConn.open(@uri) do |conn|
- conn.send_message(self, msg_id, a, b)
- end
- end
-
- if succ
- return result
- elsif DRbUnknown === result
- raise result
- else
- bt = self.class.prepare_backtrace(@uri, result)
- result.set_backtrace(bt + caller)
- raise result
- end
- end
-
- def self.with_friend(uri)
- friend = DRb.fetch_server(uri)
- return yield() unless friend
-
- save = Thread.current['DRb']
- Thread.current['DRb'] = { 'server' => friend }
- return yield
- ensure
- Thread.current['DRb'] = save if friend
- end
-
- def self.prepare_backtrace(uri, result)
- prefix = "(#{uri}) "
- bt = []
- result.backtrace.each do |x|
- break if /`__send__'$/ =~ x
- if /^\(druby:\/\// =~ x
- bt.push(x)
- else
- bt.push(prefix + x)
- end
- end
- bt
- end
-
- def pretty_print(q) # :nodoc:
- q.pp_object(self)
- end
-
- def pretty_print_cycle(q) # :nodoc:
- q.object_address_group(self) {
- q.breakable
- q.text '...'
- }
- end
- end
-
- # Class handling the connection between a DRbObject and the
- # server the real object lives on.
- #
- # This class maintains a pool of connections, to reduce the
- # overhead of starting and closing down connections for each
- # method call.
- #
- # This class is used internally by DRbObject. The user does
- # not normally need to deal with it directly.
- class DRbConn
- POOL_SIZE = 16 # :nodoc:
- @mutex = Mutex.new
- @pool = []
-
- def self.open(remote_uri) # :nodoc:
- begin
- conn = nil
-
- @mutex.synchronize do
- #FIXME
- new_pool = []
- @pool.each do |c|
- if conn.nil? and c.uri == remote_uri
- conn = c if c.alive?
- else
- new_pool.push c
- end
- end
- @pool = new_pool
- end
-
- conn = self.new(remote_uri) unless conn
- succ, result = yield(conn)
- return succ, result
-
- ensure
- if conn
- if succ
- @mutex.synchronize do
- @pool.unshift(conn)
- @pool.pop.close while @pool.size > POOL_SIZE
- end
- else
- conn.close
- end
- end
- end
- end
-
- def initialize(remote_uri) # :nodoc:
- @uri = remote_uri
- @protocol = DRbProtocol.open(remote_uri, DRb.config)
- end
- attr_reader :uri # :nodoc:
-
- def send_message(ref, msg_id, arg, block) # :nodoc:
- @protocol.send_request(ref, msg_id, arg, block)
- @protocol.recv_reply
- end
-
- def close # :nodoc:
- @protocol.close
- @protocol = nil
- end
-
- def alive? # :nodoc:
- return false unless @protocol
- @protocol.alive?
- end
- end
-
- # Class representing a drb server instance.
- #
- # A DRbServer must be running in the local process before any incoming
- # dRuby calls can be accepted, or any local objects can be passed as
- # dRuby references to remote processes, even if those local objects are
- # never actually called remotely. You do not need to start a DRbServer
- # in the local process if you are only making outgoing dRuby calls
- # passing marshalled parameters.
- #
- # Unless multiple servers are being used, the local DRbServer is normally
- # started by calling DRb.start_service.
- class DRbServer
- @@acl = nil
- @@idconv = DRbIdConv.new
- @@secondary_server = nil
- @@argc_limit = 256
- @@load_limit = 256 * 102400
- @@verbose = false
- @@safe_level = 0
-
- # Set the default value for the :argc_limit option.
- #
- # See #new(). The initial default value is 256.
- def self.default_argc_limit(argc)
- @@argc_limit = argc
- end
-
- # Set the default value for the :load_limit option.
- #
- # See #new(). The initial default value is 25 MB.
- def self.default_load_limit(sz)
- @@load_limit = sz
- end
-
- # Set the default value for the :acl option.
- #
- # See #new(). The initial default value is nil.
- def self.default_acl(acl)
- @@acl = acl
- end
-
- # Set the default value for the :id_conv option.
- #
- # See #new(). The initial default value is a DRbIdConv instance.
- def self.default_id_conv(idconv)
- @@idconv = idconv
- end
-
- def self.default_safe_level(level)
- @@safe_level = level
- end
-
- # Set the default value of the :verbose option.
- #
- # See #new(). The initial default value is false.
- def self.verbose=(on)
- @@verbose = on
- end
-
- # Get the default value of the :verbose option.
- def self.verbose
- @@verbose
- end
-
- def self.make_config(hash={}) # :nodoc:
- default_config = {
- :idconv => @@idconv,
- :verbose => @@verbose,
- :tcp_acl => @@acl,
- :load_limit => @@load_limit,
- :argc_limit => @@argc_limit,
- :safe_level => @@safe_level
- }
- default_config.update(hash)
- end
-
- # Create a new DRbServer instance.
- #
- # +uri+ is the URI to bind to. This is normally of the form
- # 'druby://<hostname>:<port>' where <hostname> is a hostname of
- # the local machine. If nil, then the system's default hostname
- # will be bound to, on a port selected by the system; these value
- # can be retrieved from the +uri+ attribute. 'druby:' specifies
- # the default dRuby transport protocol: another protocol, such
- # as 'drbunix:', can be specified instead.
- #
- # +front+ is the front object for the server, that is, the object
- # to which remote method calls on the server will be passed. If
- # nil, then the server will not accept remote method calls.
- #
- # If +config_or_acl+ is a hash, it is the configuration to
- # use for this server. The following options are recognised:
- #
- # :idconv :: an id-to-object conversion object. This defaults
- # to an instance of the class DRb::DRbIdConv.
- # :verbose :: if true, all unsuccessful remote calls on objects
- # in the server will be logged to $stdout. false
- # by default.
- # :tcp_acl :: the access control list for this server. See
- # the ACL class from the main dRuby distribution.
- # :load_limit :: the maximum message size in bytes accepted by
- # the server. Defaults to 25 MB (26214400).
- # :argc_limit :: the maximum number of arguments to a remote
- # method accepted by the server. Defaults to
- # 256.
- #
- # The default values of these options can be modified on
- # a class-wide basis by the class methods #default_argc_limit,
- # #default_load_limit, #default_acl, #default_id_conv,
- # and #verbose=
- #
- # If +config_or_acl+ is not a hash, but is not nil, it is
- # assumed to be the access control list for this server.
- # See the :tcp_acl option for more details.
- #
- # If no other server is currently set as the primary server,
- # this will become the primary server.
- #
- # The server will immediately start running in its own thread.
- def initialize(uri=nil, front=nil, config_or_acl=nil)
- if Hash === config_or_acl
- config = config_or_acl.dup
- else
- acl = config_or_acl || @@acl
- config = {
- :tcp_acl => acl
- }
- end
-
- @config = self.class.make_config(config)
-
- @protocol = DRbProtocol.open_server(uri, @config)
- @uri = @protocol.uri
-
- @front = front
- @idconv = @config[:idconv]
- @safe_level = @config[:safe_level]
-
- @grp = ThreadGroup.new
- @thread = run
-
- DRb.regist_server(self)
- end
-
- # The URI of this DRbServer.
- attr_reader :uri
-
- # The main thread of this DRbServer.
- #
- # This is the thread that listens for and accepts connections
- # from clients, not that handles each client's request-response
- # session.
- attr_reader :thread
-
- # The front object of the DRbServer.
- #
- # This object receives remote method calls made on the server's
- # URI alone, with an object id.
- attr_reader :front
-
- # The configuration of this DRbServer
- attr_reader :config
-
- attr_reader :safe_level
-
- # Set whether to operate in verbose mode.
- #
- # In verbose mode, failed calls are logged to stdout.
- def verbose=(v); @config[:verbose]=v; end
-
- # Get whether the server is in verbose mode.
- #
- # In verbose mode, failed calls are logged to stdout.
- def verbose; @config[:verbose]; end
-
- # Is this server alive?
- def alive?
- @thread.alive?
- end
-
- # Stop this server.
- def stop_service
- DRb.remove_server(self)
- if Thread.current['DRb'] && Thread.current['DRb']['server'] == self
- Thread.current['DRb']['stop_service'] = true
- else
- @thread.kill
- end
- end
-
- # Convert a dRuby reference to the local object it refers to.
- def to_obj(ref)
- return front if ref.nil?
- return front[ref.to_s] if DRbURIOption === ref
- @idconv.to_obj(ref)
- end
-
- # Convert a local object to a dRuby reference.
- def to_id(obj)
- return nil if obj.__id__ == front.__id__
- @idconv.to_id(obj)
- end
-
- private
- def kill_sub_thread
- Thread.new do
- grp = ThreadGroup.new
- grp.add(Thread.current)
- list = @grp.list
- while list.size > 0
- list.each do |th|
- th.kill if th.alive?
- end
- list = @grp.list
- end
- end
- end
-
- def run
- Thread.start do
- begin
- while true
- main_loop
- end
- ensure
- @protocol.close if @protocol
- kill_sub_thread
- end
- end
- end
-
- # List of insecure methods.
- #
- # These methods are not callable via dRuby.
- INSECURE_METHOD = [
- :__send__
- ]
-
- # Has a method been included in the list of insecure methods?
- def insecure_method?(msg_id)
- INSECURE_METHOD.include?(msg_id)
- end
-
- # Coerce an object to a string, providing our own representation if
- # to_s is not defined for the object.
- def any_to_s(obj)
- obj.to_s + ":#{obj.class}"
- rescue
- sprintf("#<%s:0x%lx>", obj.class, obj.__id__)
- end
-
- # Check that a method is callable via dRuby.
- #
- # +obj+ is the object we want to invoke the method on. +msg_id+ is the
- # method name, as a Symbol.
- #
- # If the method is an insecure method (see #insecure_method?) a
- # SecurityError is thrown. If the method is private or undefined,
- # a NameError is thrown.
- def check_insecure_method(obj, msg_id)
- return true if Proc === obj && msg_id == :__drb_yield
- raise(ArgumentError, "#{any_to_s(msg_id)} is not a symbol") unless Symbol == msg_id.class
- raise(SecurityError, "insecure method `#{msg_id}'") if insecure_method?(msg_id)
-
- if obj.private_methods.include?(msg_id)
- desc = any_to_s(obj)
- raise NoMethodError, "private method `#{msg_id}' called for #{desc}"
- elsif obj.protected_methods.include?(msg_id)
- desc = any_to_s(obj)
- raise NoMethodError, "protected method `#{msg_id}' called for #{desc}"
- else
- true
- end
- end
- public :check_insecure_method
-
- class InvokeMethod # :nodoc:
- def initialize(drb_server, client)
- @drb_server = drb_server
- @safe_level = drb_server.safe_level
- @client = client
- end
-
- def perform
- @result = nil
- @succ = false
- setup_message
-
- if $SAFE < @safe_level
- info = Thread.current['DRb']
- if @block
- @result = Thread.new {
- Thread.current['DRb'] = info
- $SAFE = @safe_level
- perform_with_block
- }.value
- else
- @result = Thread.new {
- Thread.current['DRb'] = info
- $SAFE = @safe_level
- perform_without_block
- }.value
- end
- else
- if @block
- @result = perform_with_block
- else
- @result = perform_without_block
- end
- end
- @succ = true
- if @msg_id == :to_ary && @result.class == Array
- @result = DRbArray.new(@result)
- end
- return @succ, @result
- rescue StandardError, ScriptError, Interrupt
- @result = $!
- return @succ, @result
- end
-
- private
- def init_with_client
- obj, msg, argv, block = @client.recv_request
- @obj = obj
- @msg_id = msg.intern
- @argv = argv
- @block = block
- end
-
- def check_insecure_method
- @drb_server.check_insecure_method(@obj, @msg_id)
- end
-
- def setup_message
- init_with_client
- check_insecure_method
- end
-
- def perform_without_block
- if Proc === @obj && @msg_id == :__drb_yield
- if @argv.size == 1
- ary = @argv
- else
- ary = [@argv]
- end
- ary.collect(&@obj)[0]
- else
- @obj.__send__(@msg_id, *@argv)
- end
- end
-
- end
-
- if RUBY_VERSION >= '1.8'
- require 'drb/invokemethod'
- class InvokeMethod
- include InvokeMethod18Mixin
- end
- else
- require 'drb/invokemethod16'
- class InvokeMethod
- include InvokeMethod16Mixin
- end
- end
-
- # The main loop performed by a DRbServer's internal thread.
- #
- # Accepts a connection from a client, and starts up its own
- # thread to handle it. This thread loops, receiving requests
- # from the client, invoking them on a local object, and
- # returning responses, until the client closes the connection
- # or a local method call fails.
- def main_loop
- Thread.start(@protocol.accept) do |client|
- @grp.add Thread.current
- Thread.current['DRb'] = { 'client' => client ,
- 'server' => self }
- loop do
- begin
- succ = false
- invoke_method = InvokeMethod.new(self, client)
- succ, result = invoke_method.perform
- if !succ && verbose
- p result
- result.backtrace.each do |x|
- puts x
- end
- end
- client.send_reply(succ, result) rescue nil
- ensure
- client.close unless succ
- if Thread.current['DRb']['stop_service']
- Thread.new { stop_service }
- end
- break unless succ
- end
- end
- end
- end
- end
-
- @primary_server = nil
-
- # Start a dRuby server locally.
- #
- # The new dRuby server will become the primary server, even
- # if another server is currently the primary server.
- #
- # +uri+ is the URI for the server to bind to. If nil,
- # the server will bind to random port on the default local host
- # name and use the default dRuby protocol.
- #
- # +front+ is the server's front object. This may be nil.
- #
- # +config+ is the configuration for the new server. This may
- # be nil.
- #
- # See DRbServer::new.
- def start_service(uri=nil, front=nil, config=nil)
- @primary_server = DRbServer.new(uri, front, config)
- end
- module_function :start_service
-
- # The primary local dRuby server.
- #
- # This is the server created by the #start_service call.
- attr_accessor :primary_server
- module_function :primary_server=, :primary_server
-
- # Get the 'current' server.
- #
- # In the context of execution taking place within the main
- # thread of a dRuby server (typically, as a result of a remote
- # call on the server or one of its objects), the current
- # server is that server. Otherwise, the current server is
- # the primary server.
- #
- # If the above rule fails to find a server, a DRbServerNotFound
- # error is raised.
- def current_server
- drb = Thread.current['DRb']
- server = (drb && drb['server']) ? drb['server'] : @primary_server
- raise DRbServerNotFound unless server
- return server
- end
- module_function :current_server
-
- # Stop the local dRuby server.
- #
- # This operates on the primary server. If there is no primary
- # server currently running, it is a noop.
- def stop_service
- @primary_server.stop_service if @primary_server
- @primary_server = nil
- end
- module_function :stop_service
-
- # Get the URI defining the local dRuby space.
- #
- # This is the URI of the current server. See #current_server.
- def uri
- drb = Thread.current['DRb']
- client = (drb && drb['client'])
- if client
- uri = client.uri
- return uri if uri
- end
- current_server.uri
- end
- module_function :uri
-
- # Is +uri+ the URI for the current local server?
- def here?(uri)
- (current_server.uri rescue nil) == uri
- end
- module_function :here?
-
- # Get the configuration of the current server.
- #
- # If there is no current server, this returns the default configuration.
- # See #current_server and DRbServer::make_config.
- def config
- current_server.config
- rescue
- DRbServer.make_config
- end
- module_function :config
-
- # Get the front object of the current server.
- #
- # This raises a DRbServerNotFound error if there is no current server.
- # See #current_server.
- def front
- current_server.front
- end
- module_function :front
-
- # Convert a reference into an object using the current server.
- #
- # This raises a DRbServerNotFound error if there is no current server.
- # See #current_server.
- def to_obj(ref)
- current_server.to_obj(ref)
- end
-
- # Get a reference id for an object using the current server.
- #
- # This raises a DRbServerNotFound error if there is no current server.
- # See #current_server.
- def to_id(obj)
- current_server.to_id(obj)
- end
- module_function :to_id
- module_function :to_obj
-
- # Get the thread of the primary server.
- #
- # This returns nil if there is no primary server. See #primary_server.
- def thread
- @primary_server ? @primary_server.thread : nil
- end
- module_function :thread
-
- # Set the default id conv object.
- #
- # See DRbServer#default_id_conv.
- def install_id_conv(idconv)
- DRbServer.default_id_conv(idconv)
- end
- module_function :install_id_conv
-
- # Set the default acl.
- #
- # See DRb::DRbServer.default_acl.
- def install_acl(acl)
- DRbServer.default_acl(acl)
- end
- module_function :install_acl
-
- @mutex = Mutex.new
- def mutex
- @mutex
- end
- module_function :mutex
-
- @server = {}
- def regist_server(server)
- @server[server.uri] = server
- mutex.synchronize do
- @primary_server = server unless @primary_server
- end
- end
- module_function :regist_server
-
- def remove_server(server)
- @server.delete(server.uri)
- end
- module_function :remove_server
-
- def fetch_server(uri)
- @server[uri]
- end
- module_function :fetch_server
-end
-
-DRbObject = DRb::DRbObject
-DRbUndumped = DRb::DRbUndumped
-DRbIdConv = DRb::DRbIdConv
diff --git a/lib/drb/eq.rb b/lib/drb/eq.rb
deleted file mode 100644
index e24512d6a7..0000000000
--- a/lib/drb/eq.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'drb/drb'
-
-module DRb
- class DRbObject
- def ==(other)
- return false unless DRbObject === other
- (@ref == other.__drbref) && (@uri == other.__drburi)
- end
-
- def hash
- [@uri, @ref].hash
- end
-
- alias eql? ==
- end
-end
diff --git a/lib/drb/extserv.rb b/lib/drb/extserv.rb
deleted file mode 100644
index af52250518..0000000000
--- a/lib/drb/extserv.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-=begin
- external service
- Copyright (c) 2000,2002 Masatoshi SEKI
-=end
-
-require 'drb/drb'
-require 'monitor'
-
-module DRb
- class ExtServ
- include MonitorMixin
- include DRbUndumped
-
- def initialize(there, name, server=nil)
- super()
- @server = server || DRb::primary_server
- @name = name
- ro = DRbObject.new(nil, there)
- synchronize do
- @invoker = ro.regist(name, DRbObject.new(self, @server.uri))
- end
- end
- attr_reader :server
-
- def front
- DRbObject.new(nil, @server.uri)
- end
-
- def stop_service
- synchronize do
- @invoker.unregist(@name)
- server = @server
- @server = nil
- server.stop_service
- true
- end
- end
-
- def alive?
- @server ? @server.alive? : false
- end
- end
-end
-
-if __FILE__ == $0
- class Foo
- include DRbUndumped
-
- def initialize(str)
- @str = str
- end
-
- def hello(it)
- "#{it}: #{self}"
- end
-
- def to_s
- @str
- end
- end
-
- cmd = ARGV.shift
- case cmd
- when 'itest1', 'itest2'
- front = Foo.new(cmd)
- manager = DRb::DRbServer.new(nil, front)
- es = DRb::ExtServ.new(ARGV.shift, ARGV.shift, manager)
- es.server.thread.join
- end
-end
-
diff --git a/lib/drb/extservm.rb b/lib/drb/extservm.rb
deleted file mode 100644
index be40aea9f5..0000000000
--- a/lib/drb/extservm.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-=begin
- external service manager
- Copyright (c) 2000 Masatoshi SEKI
-=end
-
-require 'drb/drb'
-require 'thread'
-require 'monitor'
-
-module DRb
- class ExtServManager
- include DRbUndumped
- include MonitorMixin
-
- @@command = {}
-
- def self.command
- @@command
- end
-
- def self.command=(cmd)
- @@command = cmd
- end
-
- def initialize
- super()
- @cond = new_cond
- @servers = {}
- @waiting = []
- @queue = Queue.new
- @thread = invoke_thread
- @uri = nil
- end
- attr_accessor :uri
-
- def service(name)
- synchronize do
- while true
- server = @servers[name]
- return server if server && server.alive?
- invoke_service(name)
- @cond.wait
- end
- end
- end
-
- def regist(name, ro)
- synchronize do
- @servers[name] = ro
- @cond.signal
- end
- self
- end
-
- def unregist(name)
- synchronize do
- @servers.delete(name)
- end
- end
-
- private
- def invoke_thread
- Thread.new do
- while true
- name = @queue.pop
- invoke_service_command(name, @@command[name])
- end
- end
- end
-
- def invoke_service(name)
- @queue.push(name)
- end
-
- def invoke_service_command(name, command)
- raise "invalid command. name: #{name}" unless command
- synchronize do
- return if @servers.include?(name)
- @servers[name] = false
- end
- uri = @uri || DRb.uri
- if RUBY_PLATFORM =~ /mswin32/ && /NT/ =~ ENV["OS"]
- system(%Q'cmd /c start "ruby" /b #{command} #{uri} #{name}')
- else
- system("#{command} #{uri} #{name} &")
- end
- end
- end
-end
diff --git a/lib/drb/gw.rb b/lib/drb/gw.rb
deleted file mode 100644
index b7a5f5383f..0000000000
--- a/lib/drb/gw.rb
+++ /dev/null
@@ -1,122 +0,0 @@
-require 'drb/drb'
-require 'monitor'
-
-module DRb
- class GWIdConv < DRbIdConv
- def to_obj(ref)
- if Array === ref && ref[0] == :DRbObject
- return DRbObject.new_with(ref[1], ref[2])
- end
- super(ref)
- end
- end
-
- class GW
- include MonitorMixin
- def initialize
- super()
- @hash = {}
- end
-
- def [](key)
- synchronize do
- @hash[key]
- end
- end
-
- def []=(key, v)
- synchronize do
- @hash[key] = v
- end
- end
- end
-
- class DRbObject
- def self._load(s)
- uri, ref = Marshal.load(s)
- if DRb.uri == uri
- return ref ? DRb.to_obj(ref) : DRb.front
- end
-
- self.new_with(DRb.uri, [:DRbObject, uri, ref])
- end
-
- def _dump(lv)
- if DRb.uri == @uri
- if Array === @ref && @ref[0] == :DRbObject
- Marshal.dump([@ref[1], @ref[2]])
- else
- Marshal.dump([@uri, @ref]) # ??
- end
- else
- Marshal.dump([DRb.uri, [:DRbObject, @uri, @ref]])
- end
- end
- end
-end
-
-=begin
-DRb.install_id_conv(DRb::GWIdConv.new)
-
-front = DRb::GW.new
-
-s1 = DRb::DRbServer.new('drbunix:/tmp/gw_b_a', front)
-s2 = DRb::DRbServer.new('drbunix:/tmp/gw_b_c', front)
-
-s1.thread.join
-s2.thread.join
-=end
-
-=begin
-# foo.rb
-
-require 'drb/drb'
-
-class Foo
- include DRbUndumped
- def initialize(name, peer=nil)
- @name = name
- @peer = peer
- end
-
- def ping(obj)
- puts "#{@name}: ping: #{obj.inspect}"
- @peer.ping(self) if @peer
- end
-end
-=end
-
-=begin
-# gw_a.rb
-require 'drb/unix'
-require 'foo'
-
-obj = Foo.new('a')
-DRb.start_service("drbunix:/tmp/gw_a", obj)
-
-robj = DRbObject.new_with_uri('drbunix:/tmp/gw_b_a')
-robj[:a] = obj
-
-DRb.thread.join
-=end
-
-=begin
-# gw_c.rb
-require 'drb/unix'
-require 'foo'
-
-foo = Foo.new('c', nil)
-
-DRb.start_service("drbunix:/tmp/gw_c", nil)
-
-robj = DRbObject.new_with_uri("drbunix:/tmp/gw_b_c")
-
-puts "c->b"
-a = robj[:a]
-sleep 2
-
-a.ping(foo)
-
-DRb.thread.join
-=end
-
diff --git a/lib/drb/invokemethod.rb b/lib/drb/invokemethod.rb
deleted file mode 100644
index 7da8ace88d..0000000000
--- a/lib/drb/invokemethod.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# for ruby-1.8.0
-
-module DRb
- class DRbServer
- module InvokeMethod18Mixin
- def block_yield(x)
- if x.size == 1 && x[0].class == Array
- x[0] = DRbArray.new(x[0])
- end
- block_value = @block.call(*x)
- end
-
- def perform_with_block
- @obj.__send__(@msg_id, *@argv) do |*x|
- jump_error = nil
- begin
- block_value = block_yield(x)
- rescue LocalJumpError
- jump_error = $!
- end
- if jump_error
- case jump_error.reason
- when :break
- break(jump_error.exit_value)
- else
- raise jump_error
- end
- end
- block_value
- end
- end
- end
- end
-end
diff --git a/lib/drb/observer.rb b/lib/drb/observer.rb
deleted file mode 100644
index e7f1668c52..0000000000
--- a/lib/drb/observer.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-require 'observer'
-
-module DRb
- module DRbObservable
- include Observable
-
- def notify_observers(*arg)
- if defined? @observer_state and @observer_state
- if defined? @observer_peers
- for i in @observer_peers.dup
- begin
- i.update(*arg)
- rescue
- delete_observer(i)
- end
- end
- end
- @observer_state = false
- end
- end
- end
-end
diff --git a/lib/drb/ssl.rb b/lib/drb/ssl.rb
deleted file mode 100644
index 58d6b7d1e0..0000000000
--- a/lib/drb/ssl.rb
+++ /dev/null
@@ -1,190 +0,0 @@
-require 'socket'
-require 'openssl'
-require 'drb/drb'
-require 'singleton'
-
-module DRb
-
- class DRbSSLSocket < DRbTCPSocket
-
- class SSLConfig
-
- DEFAULT = {
- :SSLCertificate => nil,
- :SSLPrivateKey => nil,
- :SSLClientCA => nil,
- :SSLCACertificatePath => nil,
- :SSLCACertificateFile => nil,
- :SSLVerifyMode => ::OpenSSL::SSL::VERIFY_NONE,
- :SSLVerifyDepth => nil,
- :SSLVerifyCallback => nil, # custom verification
- :SSLCertificateStore => nil,
- # Must specify if you use auto generated certificate.
- :SSLCertName => nil, # e.g. [["CN","fqdn.example.com"]]
- :SSLCertComment => "Generated by Ruby/OpenSSL"
- }
-
- def initialize(config)
- @config = config
- @cert = config[:SSLCertificate]
- @pkey = config[:SSLPrivateKey]
- @ssl_ctx = nil
- end
-
- def [](key);
- @config[key] || DEFAULT[key]
- end
-
- def connect(tcp)
- ssl = ::OpenSSL::SSL::SSLSocket.new(tcp, @ssl_ctx)
- ssl.sync = true
- ssl.connect
- ssl
- end
-
- def accept(tcp)
- ssl = OpenSSL::SSL::SSLSocket.new(tcp, @ssl_ctx)
- ssl.sync = true
- ssl.accept
- ssl
- end
-
- def setup_certificate
- if @cert && @pkey
- return
- end
-
- rsa = OpenSSL::PKey::RSA.new(512){|p, n|
- next unless self[:verbose]
- 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 = 3
- cert.serial = 0
- name = OpenSSL::X509::Name.new(self[:SSLCertName])
- 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)
- cert.extensions = [
- ef.create_extension("basicConstraints","CA:FALSE"),
- ef.create_extension("subjectKeyIdentifier", "hash") ]
- ef.issuer_certificate = cert
- cert.add_extension(ef.create_extension("authorityKeyIdentifier",
- "keyid:always,issuer:always"))
- if comment = self[:SSLCertComment]
- cert.add_extension(ef.create_extension("nsComment", comment))
- end
- cert.sign(rsa, OpenSSL::Digest::SHA1.new)
-
- @cert = cert
- @pkey = rsa
- end
-
- def setup_ssl_context
- ctx = ::OpenSSL::SSL::SSLContext.new
- ctx.cert = @cert
- ctx.key = @pkey
- ctx.client_ca = self[:SSLClientCA]
- ctx.ca_path = self[:SSLCACertificatePath]
- ctx.ca_file = self[:SSLCACertificateFile]
- ctx.verify_mode = self[:SSLVerifyMode]
- ctx.verify_depth = self[:SSLVerifyDepth]
- ctx.verify_callback = self[:SSLVerifyCallback]
- ctx.cert_store = self[:SSLCertificateStore]
- @ssl_ctx = ctx
- end
- end
-
- def self.parse_uri(uri)
- if uri =~ /^drbssl:\/\/(.*?):(\d+)(\?(.*))?$/
- host = $1
- port = $2.to_i
- option = $4
- [host, port, option]
- else
- raise(DRbBadScheme, uri) unless uri =~ /^drbssl:/
- raise(DRbBadURI, 'can\'t parse uri:' + uri)
- end
- end
-
- def self.open(uri, config)
- host, port, option = parse_uri(uri)
- host.untaint
- port.untaint
- soc = TCPSocket.open(host, port)
- ssl_conf = SSLConfig::new(config)
- ssl_conf.setup_ssl_context
- ssl = ssl_conf.connect(soc)
- self.new(uri, ssl, ssl_conf, true)
- end
-
- def self.open_server(uri, config)
- uri = 'drbssl://:0' unless uri
- host, port, opt = parse_uri(uri)
- if host.size == 0
- host = getservername
- soc = open_server_inaddr_any(host, port)
- else
- soc = TCPServer.open(host, port)
- end
- port = soc.addr[1] if port == 0
- @uri = "drbssl://#{host}:#{port}"
-
- ssl_conf = SSLConfig.new(config)
- ssl_conf.setup_certificate
- ssl_conf.setup_ssl_context
- self.new(@uri, soc, ssl_conf, false)
- end
-
- def self.uri_option(uri, config)
- host, port, option = parse_uri(uri)
- return "drbssl://#{host}:#{port}", option
- end
-
- def initialize(uri, soc, config, is_established)
- @ssl = is_established ? soc : nil
- super(uri, soc.to_io, config)
- end
-
- def stream; @ssl; end
-
- def close
- if @ssl
- @ssl.close
- @ssl = nil
- end
- super
- end
-
- def accept
- begin
- while true
- soc = @socket.accept
- break if (@acl ? @acl.allow_socket?(soc) : true)
- soc.close
- end
- ssl = @config.accept(soc)
- self.class.new(uri, ssl, @config, true)
- rescue OpenSSL::SSL::SSLError
- warn("#{__FILE__}:#{__LINE__}: warning: #{$!.message} (#{$!.class})") if @config[:verbose]
- retry
- end
- end
- end
-
- DRbProtocol.add_protocol(DRbSSLSocket)
-end
diff --git a/lib/drb/timeridconv.rb b/lib/drb/timeridconv.rb
deleted file mode 100644
index bb2c48d528..0000000000
--- a/lib/drb/timeridconv.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-require 'drb/drb'
-require 'monitor'
-
-module DRb
- class TimerIdConv < DRbIdConv
- class TimerHolder2
- include MonitorMixin
-
- class InvalidIndexError < RuntimeError; end
-
- def initialize(timeout=600)
- super()
- @sentinel = Object.new
- @gc = {}
- @curr = {}
- @renew = {}
- @timeout = timeout
- @keeper = keeper
- end
-
- def add(obj)
- synchronize do
- key = obj.__id__
- @curr[key] = obj
- return key
- end
- end
-
- def fetch(key, dv=@sentinel)
- synchronize do
- obj = peek(key)
- if obj == @sentinel
- return dv unless dv == @sentinel
- raise InvalidIndexError
- end
- @renew[key] = obj # KeepIt
- return obj
- end
- end
-
- def include?(key)
- synchronize do
- obj = peek(key)
- return false if obj == @sentinel
- true
- end
- end
-
- def peek(key)
- synchronize do
- return @curr.fetch(key, @renew.fetch(key, @gc.fetch(key, @sentinel)))
- end
- end
-
- private
- def alternate
- synchronize do
- @gc = @curr # GCed
- @curr = @renew
- @renew = {}
- end
- end
-
- def keeper
- Thread.new do
- loop do
- size = alternate
- sleep(@timeout)
- end
- end
- end
- end
-
- def initialize(timeout=600)
- @holder = TimerHolder2.new(timeout)
- end
-
- def to_obj(ref)
- return super if ref.nil?
- @holder.fetch(ref)
- rescue TimerHolder2::InvalidIndexError
- raise "invalid reference"
- end
-
- def to_id(obj)
- return @holder.add(obj)
- end
- end
-end
-
-# DRb.install_id_conv(TimerIdConv.new)
diff --git a/lib/drb/unix.rb b/lib/drb/unix.rb
deleted file mode 100644
index 57feed8301..0000000000
--- a/lib/drb/unix.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-require 'socket'
-require 'drb/drb'
-require 'tmpdir'
-
-raise(LoadError, "UNIXServer is required") unless defined?(UNIXServer)
-
-module DRb
-
- class DRbUNIXSocket < DRbTCPSocket
- def self.parse_uri(uri)
- if /^drbunix:(.*?)(\?(.*))?$/ =~ uri
- filename = $1
- option = $3
- [filename, option]
- else
- raise(DRbBadScheme, uri) unless uri =~ /^drbunix:/
- raise(DRbBadURI, 'can\'t parse uri:' + uri)
- end
- end
-
- def self.open(uri, config)
- filename, option = parse_uri(uri)
- filename.untaint
- soc = UNIXSocket.open(filename)
- self.new(uri, soc, config)
- end
-
- def self.open_server(uri, config)
- filename, option = parse_uri(uri)
- if filename.size == 0
- soc = temp_server
- filename = soc.path
- uri = 'drbunix:' + soc.path
- else
- soc = UNIXServer.open(filename)
- end
- owner = config[:UNIXFileOwner]
- group = config[:UNIXFileGroup]
- if owner || group
- require 'etc'
- owner = Etc.getpwnam( owner ).uid if owner
- group = Etc.getgrnam( group ).gid if group
- File.chown owner, group, filename
- end
- mode = config[:UNIXFileMode]
- File.chmod(mode, filename) if mode
-
- self.new(uri, soc, config, true)
- end
-
- def self.uri_option(uri, config)
- filename, option = parse_uri(uri)
- return "drbunix:#{filename}", option
- end
-
- def initialize(uri, soc, config={}, server_mode = false)
- super(uri, soc, config)
- set_sockopt(@socket)
- @server_mode = server_mode
- @acl = nil
- end
-
- # import from tempfile.rb
- Max_try = 10
- private
- def self.temp_server
- tmpdir = Dir::tmpdir
- n = 0
- while true
- begin
- tmpname = sprintf('%s/druby%d.%d', tmpdir, $$, n)
- lock = tmpname + '.lock'
- unless File.exist?(tmpname) or File.exist?(lock)
- Dir.mkdir(lock)
- break
- end
- rescue
- raise "cannot generate tempfile `%s'" % tmpname if n >= Max_try
- #sleep(1)
- end
- n += 1
- end
- soc = UNIXServer.new(tmpname)
- Dir.rmdir(lock)
- soc
- end
-
- public
- def close
- return unless @socket
- path = @socket.path if @server_mode
- @socket.close
- File.unlink(path) if @server_mode
- @socket = nil
- end
-
- def accept
- s = @socket.accept
- self.class.new(nil, s, @config)
- end
-
- def set_sockopt(soc)
- soc.fcntl(Fcntl::F_SETFL, Fcntl::FD_CLOEXEC) if defined? Fcntl::FD_CLOEXEC
- end
- end
-
- DRbProtocol.add_protocol(DRbUNIXSocket)
-end
diff --git a/lib/e2mmap.rb b/lib/e2mmap.rb
deleted file mode 100644
index b8d1d44f38..0000000000
--- a/lib/e2mmap.rb
+++ /dev/null
@@ -1,172 +0,0 @@
-#
-# e2mmap.rb - for ruby 1.1
-# $Release Version: 2.0$
-# $Revision: 1.10 $
-# by Keiju ISHITSUKA
-#
-# --
-# Usage:
-#
-# U1)
-# class Foo
-# extend Exception2MessageMapper
-# def_e2message ExistingExceptionClass, "message..."
-# def_exception :NewExceptionClass, "message..."[, superclass]
-# ...
-# end
-#
-# U2)
-# module Error
-# extend Exception2MessageMapper
-# def_e2meggage ExistingExceptionClass, "message..."
-# def_exception :NewExceptionClass, "message..."[, superclass]
-# ...
-# end
-# class Foo
-# include Error
-# ...
-# end
-#
-# foo = Foo.new
-# foo.Fail ....
-#
-# U3)
-# module Error
-# extend Exception2MessageMapper
-# def_e2message ExistingExceptionClass, "message..."
-# def_exception :NewExceptionClass, "message..."[, superclass]
-# ...
-# end
-# class Foo
-# extend Exception2MessageMapper
-# include Error
-# ...
-# end
-#
-# Foo.Fail NewExceptionClass, arg...
-# Foo.Fail ExistingExceptionClass, arg...
-#
-#
-module Exception2MessageMapper
- @RCS_ID='-$Id: e2mmap.rb,v 1.10 1999/02/17 12:33:17 keiju Exp keiju $-'
-
- E2MM = Exception2MessageMapper
-
- def E2MM.extend_object(cl)
- super
- cl.bind(self) unless cl < E2MM
- end
-
- def bind(cl)
- self.module_eval %[
- def Raise(err = nil, *rest)
- Exception2MessageMapper.Raise(self.class, err, *rest)
- end
- alias Fail Raise
-
- def self.included(mod)
- mod.extend Exception2MessageMapper
- end
- ]
- end
-
- # Fail(err, *rest)
- # err: exception
- # rest: message arguments
- #
- def Raise(err = nil, *rest)
- E2MM.Raise(self, err, *rest)
- end
- alias Fail Raise
- alias fail Raise
-
- # def_e2message(c, m)
- # c: exception
- # m: message_form
- # define exception c with message m.
- #
- def def_e2message(c, m)
- E2MM.def_e2message(self, c, m)
- end
-
- # def_exception(n, m, s)
- # n: exception_name
- # m: message_form
- # s: superclass(default: StandardError)
- # define exception named ``c'' with message m.
- #
- def def_exception(n, m, s = StandardError)
- E2MM.def_exception(self, n, m, s)
- end
-
- #
- # Private definitions.
- #
- # {[class, exp] => message, ...}
- @MessageMap = {}
-
- # E2MM.def_e2message(k, e, m)
- # k: class to define exception under.
- # e: exception
- # m: message_form
- # define exception c with message m.
- #
- def E2MM.def_e2message(k, c, m)
- E2MM.instance_eval{@MessageMap[[k, c]] = m}
- c
- end
-
- # E2MM.def_exception(k, n, m, s)
- # k: class to define exception under.
- # n: exception_name
- # m: message_form
- # s: superclass(default: StandardError)
- # define exception named ``c'' with message m.
- #
- def E2MM.def_exception(k, n, m, s = StandardError)
- n = n.id2name if n.kind_of?(Fixnum)
- e = Class.new(s)
- E2MM.instance_eval{@MessageMap[[k, e]] = m}
- k.const_set(n, e)
- end
-
- # Fail(klass, err, *rest)
- # klass: class to define exception under.
- # err: exception
- # rest: message arguments
- #
- def E2MM.Raise(klass = E2MM, err = nil, *rest)
- if form = e2mm_message(klass, err)
- b = $@.nil? ? caller(1) : $@
- #p $@
- #p __FILE__
- b.shift if b[0] =~ /^#{Regexp.quote(__FILE__)}:/
- raise err, sprintf(form, *rest), b
- else
- E2MM.Fail E2MM, ErrNotRegisteredException, err.inspect
- end
- end
- class <<E2MM
- alias Fail Raise
- end
-
- def E2MM.e2mm_message(klass, exp)
- for c in klass.ancestors
- if mes = @MessageMap[[c,exp]]
- #p mes
- m = klass.instance_eval('"' + mes + '"')
- return m
- end
- end
- nil
- end
- class <<self
- alias message e2mm_message
- end
-
- E2MM.def_exception(E2MM,
- :ErrNotRegisteredException,
- "not registerd exception(%s)")
-end
-
-
diff --git a/lib/erb.rb b/lib/erb.rb
index 62b59be249..bde90de841 100644
--- a/lib/erb.rb
+++ b/lib/erb.rb
@@ -1,7 +1,9 @@
+# -*- coding: us-ascii -*-
+# frozen_string_literal: true
# = ERB -- Ruby Templating
#
# Author:: Masatoshi SEKI
-# Documentation:: James Edward Gray II and Gavin Sinclair
+# Documentation:: James Edward Gray II, Gavin Sinclair, and Simon Chiang
#
# See ERB for primary documentation and ERB::Util for a couple of utility
# routines.
@@ -10,893 +12,1168 @@
#
# You can redistribute it and/or modify it under the same terms as Ruby.
-=begin rdoc
-= ERB -- Ruby Templating
-
-== Introduction
-
-ERB provides an easy to use but powerful templating system for Ruby. Using
-ERB, actual Ruby code can be added to any plain text document for the
-purposes of generating document information details and/or flow control.
-
-A very simple example is this:
-
- require 'erb'
-
- x = 42
- template = ERB.new <<-EOF
- The value of x is: <%= x %>
- EOF
- puts template.result(binding)
-
-<em>Prints:</em> The value of x is: 42
-
-More complex examples are given below.
-
-
-== Recognized Tags
-
-ERB recognizes certain tags in the provided template and converts them based
-on the rules below:
-
- <% Ruby code -- inline with output %>
- <%= Ruby expression -- replace with result %>
- <%# comment -- ignored -- useful in testing %>
- % a line of Ruby code -- treated as <% line %> (optional -- see ERB.new)
- %% replaced with % if first thing on a line and % processing is used
- <%% or %%> -- replace with <% or %> respectively
-
-All other text is passed through ERB filtering unchanged.
-
-
-== Options
-
-There are several settings you can change when you use ERB:
-* the nature of the tags that are recognized;
-* the value of <tt>$SAFE</tt> under which the template is run;
-* the binding used to resolve local variables in the template.
-
-See the ERB.new and ERB#result methods for more detail.
-
-== Character encodings
-
-ERB (or ruby code generated by ERB) returns a string in the same
-character encoding as the input string. When the input string has
-a magic comment, however, it returns a string in the encoding specified
-by the magic comment.
-
- # -*- coding: UTF-8 -*-
- require 'erb'
-
- template = ERB.new <<EOF
- <%#-*- coding: Big5 -*-%>
- \_\_ENCODING\_\_ is <%= \_\_ENCODING\_\_ %>.
- EOF
- puts template.result
-
-<em>Prints:</em> \_\_ENCODING\_\_ is Big5.
-
-
-== Examples
-
-=== Plain Text
-
-ERB is useful for any generic templating situation. Note that in this example, we use the
-convenient "% at start of line" tag, and we quote the template literally with
-<tt>%q{...}</tt> to avoid trouble with the backslash.
-
- require "erb"
-
- # Create template.
- template = %q{
- From: James Edward Gray II <james@grayproductions.net>
- To: <%= to %>
- Subject: Addressing Needs
-
- <%= to[/\w+/] %>:
-
- Just wanted to send a quick note assuring that your needs are being
- addressed.
-
- I want you to know that my team will keep working on the issues,
- especially:
-
- <%# ignore numerous minor requests -- focus on priorities %>
- % priorities.each do |priority|
- * <%= priority %>
- % end
-
- Thanks for your patience.
-
- James Edward Gray II
- }.gsub(/^ /, '')
-
- message = ERB.new(template, 0, "%<>")
-
- # Set up template data.
- to = "Community Spokesman <spokesman@ruby_community.org>"
- priorities = [ "Run Ruby Quiz",
- "Document Modules",
- "Answer Questions on Ruby Talk" ]
-
- # Produce result.
- email = message.result
- puts email
-
-<i>Generates:</i>
-
- From: James Edward Gray II <james@grayproductions.net>
- To: Community Spokesman <spokesman@ruby_community.org>
- Subject: Addressing Needs
-
- Community:
-
- Just wanted to send a quick note assuring that your needs are being addressed.
-
- I want you to know that my team will keep working on the issues, especially:
-
- * Run Ruby Quiz
- * Document Modules
- * Answer Questions on Ruby Talk
-
- Thanks for your patience.
-
- James Edward Gray II
-
-=== Ruby in HTML
-
-ERB is often used in <tt>.rhtml</tt> files (HTML with embedded Ruby). Notice the need in
-this example to provide a special binding when the template is run, so that the instance
-variables in the Product object can be resolved.
-
- require "erb"
-
- # Build template data class.
- class Product
- def initialize( code, name, desc, cost )
- @code = code
- @name = name
- @desc = desc
- @cost = cost
-
- @features = [ ]
- end
-
- def add_feature( feature )
- @features << feature
- end
-
- # Support templating of member data.
- def get_binding
- binding
- end
-
- # ...
- end
-
- # Create template.
- template = %{
- <html>
- <head><title>Ruby Toys -- <%= @name %></title></head>
- <body>
-
- <h1><%= @name %> (<%= @code %>)</h1>
- <p><%= @desc %></p>
-
- <ul>
- <% @features.each do |f| %>
- <li><b><%= f %></b></li>
- <% end %>
- </ul>
-
- <p>
- <% if @cost < 10 %>
- <b>Only <%= @cost %>!!!</b>
- <% else %>
- Call for a price, today!
- <% end %>
- </p>
-
- </body>
- </html>
- }.gsub(/^ /, '')
-
- rhtml = ERB.new(template)
-
- # Set up template data.
- toy = Product.new( "TZ-1002",
- "Rubysapien",
- "Geek's Best Friend! Responds to Ruby commands...",
- 999.95 )
- toy.add_feature("Listens for verbal commands in the Ruby language!")
- toy.add_feature("Ignores Perl, Java, and all C variants.")
- toy.add_feature("Karate-Chop Action!!!")
- toy.add_feature("Matz signature on left leg.")
- toy.add_feature("Gem studded eyes... Rubies, of course!")
-
- # Produce result.
- rhtml.run(toy.get_binding)
-
-<i>Generates (some blank lines removed):</i>
-
- <html>
- <head><title>Ruby Toys -- Rubysapien</title></head>
- <body>
-
- <h1>Rubysapien (TZ-1002)</h1>
- <p>Geek's Best Friend! Responds to Ruby commands...</p>
-
- <ul>
- <li><b>Listens for verbal commands in the Ruby language!</b></li>
- <li><b>Ignores Perl, Java, and all C variants.</b></li>
- <li><b>Karate-Chop Action!!!</b></li>
- <li><b>Matz signature on left leg.</b></li>
- <li><b>Gem studded eyes... Rubies, of course!</b></li>
- </ul>
-
- <p>
- Call for a price, today!
- </p>
-
- </body>
- </html>
-
-
-== Notes
+# A NOTE ABOUT TERMS:
+#
+# Formerly: The documentation in this file used the term _template_ to refer to an ERB object.
+#
+# Now: The documentation in this file uses the term _template_
+# to refer to the string input to ERB.new.
+#
+# The reason for the change: When documenting the ERB executable erb,
+# we need a term that refers to its string input;
+# _source_ is not a good idea, because ERB#src means something entirely different;
+# the two different sorts of sources would bring confusion.
+#
+# Therefore we use the term _template_ to refer to:
+#
+# - The string input to ERB.new
+# - The string input to executable erb.
+#
-There are a variety of templating solutions available in various Ruby projects:
-* ERB's big brother, eRuby, works the same but is written in C for speed;
-* Amrita (smart at producing HTML/XML);
-* cs/Template (written in C for speed);
-* RDoc, distributed with Ruby, uses its own template engine, which can be reused elsewhere;
-* and others; search the RAA.
+require 'erb/version'
+require 'erb/compiler'
+require 'erb/def_method'
+require 'erb/util'
-Rails, the web application framework, uses ERB to create views.
-=end
+# :markup: markdown
+#
+# Class **ERB** (the name stands for **Embedded Ruby**)
+# is an easy-to-use, but also very powerful, [template processor][template processor].
+#
+# ## Usage
+#
+# Before you can use \ERB, you must first require it
+# (examples on this page assume that this has been done):
+#
+# ```
+# require 'erb'
+# ```
+#
+# ## In Brief
+#
+# Here's how \ERB works:
+#
+# - You can create a *template*: a plain-text string that includes specially formatted *tags*..
+# - You can create an \ERB object to store the template.
+# - You can call instance method ERB#result to get the *result*.
+#
+# \ERB supports tags of three kinds:
+#
+# - [Expression tags][expression tags]:
+# each begins with `'<%='`, ends with `'%>'`; contains a Ruby expression;
+# in the result, the value of the expression replaces the entire tag:
+#
+# template = 'The magic word is <%= magic_word %>.'
+# erb = ERB.new(template)
+# magic_word = 'xyzzy'
+# erb.result(binding) # => "The magic word is xyzzy."
+#
+# The above call to #result passes argument `binding`,
+# which contains the binding of variable `magic_word` to its string value `'xyzzy'`.
+#
+# The below call to #result need not pass a binding,
+# because its expression `Date::DAYNAMES` is globally defined.
+#
+# ERB.new('Today is <%= Date::DAYNAMES[Date.today.wday] %>.').result # => "Today is Monday."
+#
+# - [Execution tags][execution tags]:
+# each begins with `'<%'`, ends with `'%>'`; contains Ruby code to be executed:
+#
+# template = '<% File.write("t.txt", "Some stuff.") %>'
+# ERB.new(template).result
+# File.read('t.txt') # => "Some stuff."
+#
+# - [Comment tags][comment tags]:
+# each begins with `'<%#'`, ends with `'%>'`; contains comment text;
+# in the result, the entire tag is omitted.
+#
+# template = 'Some stuff;<%# Note to self: figure out what the stuff is. %> more stuff.'
+# ERB.new(template).result # => "Some stuff; more stuff."
+#
+# ## Some Simple Examples
+#
+# Here's a simple example of \ERB in action:
+#
+# ```
+# template = 'The time is <%= Time.now %>.'
+# erb = ERB.new(template)
+# erb.result
+# # => "The time is 2025-09-09 10:49:26 -0500."
+# ```
+#
+# Details:
+#
+# 1. A plain-text string is assigned to variable `template`.
+# Its embedded [expression tag][expression tags] `'<%= Time.now %>'` includes a Ruby expression, `Time.now`.
+# 2. The string is put into a new \ERB object, and stored in variable `erb`.
+# 4. Method call `erb.result` generates a string that contains the run-time value of `Time.now`,
+# as computed at the time of the call.
+#
+# The
+# \ERB object may be re-used:
+#
+# ```
+# erb.result
+# # => "The time is 2025-09-09 10:49:33 -0500."
+# ```
+#
+# Another example:
+#
+# ```
+# template = 'The magic word is <%= magic_word %>.'
+# erb = ERB.new(template)
+# magic_word = 'abracadabra'
+# erb.result(binding)
+# # => "The magic word is abracadabra."
+# ```
+#
+# Details:
+#
+# 1. As before, a plain-text string is assigned to variable `template`.
+# Its embedded [expression tag][expression tags] `'<%= magic_word %>'` has a variable *name*, `magic_word`.
+# 2. The string is put into a new \ERB object, and stored in variable `erb`;
+# note that `magic_word` need not be defined before the \ERB object is created.
+# 3. `magic_word = 'abracadabra'` assigns a value to variable `magic_word`.
+# 4. Method call `erb.result(binding)` generates a string
+# that contains the *value* of `magic_word`.
+#
+# As before, the \ERB object may be re-used:
+#
+# ```
+# magic_word = 'xyzzy'
+# erb.result(binding)
+# # => "The magic word is xyzzy."
+# ```
+#
+# ## Bindings
+#
+# A call to method #result, which produces the formatted result string,
+# requires a [Binding object][binding object] as its argument.
+#
+# The binding object provides the bindings for expressions in [expression tags][expression tags].
+#
+# There are three ways to provide the required binding:
+#
+# - [Default binding][default binding].
+# - [Local binding][local binding].
+# - [Augmented binding][augmented binding]
+#
+# ### Default Binding
+#
+# When you pass no `binding` argument to method #result,
+# the method uses its default binding: the one returned by method #new_toplevel.
+# This binding has the bindings defined by Ruby itself,
+# which are those for Ruby's constants and variables.
+#
+# That binding is sufficient for an expression tag that refers only to Ruby's constants and variables;
+# these expression tags refer only to Ruby's global constant `RUBY_COPYRIGHT` and global variable `$0`:
+#
+# ```
+# template = <<TEMPLATE
+# The Ruby copyright is <%= RUBY_COPYRIGHT.inspect %>.
+# The current process is <%= $0 %>.
+# TEMPLATE
+# puts ERB.new(template).result
+# The Ruby copyright is "ruby - Copyright (C) 1993-2025 Yukihiro Matsumoto".
+# The current process is irb.
+# ```
+#
+# (The current process is `irb` because that's where we're doing these examples!)
+#
+# ### Local Binding
+#
+# The default binding is *not* sufficient for an expression
+# that refers to a a constant or variable that is not defined there:
+#
+# ```
+# Foo = 1 # Defines local constant Foo.
+# foo = 2 # Defines local variable foo.
+# template = <<TEMPLATE
+# The current value of constant Foo is <%= Foo %>.
+# The current value of variable foo is <%= foo %>.
+# The Ruby copyright is <%= RUBY_COPYRIGHT.inspect %>.
+# The current process is <%= $0 %>.
+# TEMPLATE
+# erb = ERB.new(template)
+# ```
+#
+# This call below raises `NameError` because although `Foo` and `foo` are defined locally,
+# they are not defined in the default binding:
+#
+# ```
+# erb.result # Raises NameError.
+# ```
+#
+# To make the locally-defined constants and variables available,
+# you can call #result with the local binding:
+#
+# ```
+# puts erb.result(binding)
+# The current value of constant Foo is 1.
+# The current value of variable foo is 2.
+# The Ruby copyright is "ruby - Copyright (C) 1993-2025 Yukihiro Matsumoto".
+# The current process is irb.
+# ```
+#
+# ### Augmented Binding
+#
+# Another way to make variable bindings (but not constant bindings) available
+# is to use method #result_with_hash(hash);
+# the passed hash has name/value pairs that are to be used to define and assign variables
+# in a copy of the default binding:
+#
+# ```
+# template = <<TEMPLATE
+# The current value of variable bar is <%= bar %>.
+# The current value of variable baz is <%= baz %>.
+# The Ruby copyright is <%= RUBY_COPYRIGHT.inspect %>.
+# The current process is <%= $0 %>.
+# TEMPLATE
+# erb = ERB.new(template)
+# ```
+#
+# Both of these calls raise `NameError`, because `bar` and `baz`
+# are not defined in either the default binding or the local binding.
+#
+# ```
+# puts erb.result # Raises NameError.
+# puts erb.result(binding) # Raises NameError.
+# ```
+#
+# This call passes a hash that causes `bar` and `baz` to be defined
+# in a new binding (derived from #new_toplevel):
+#
+# ```
+# hash = {bar: 3, baz: 4}
+# puts erb.result_with_hash(hash)
+# The current value of variable bar is 3.
+# The current value of variable baz is 4.
+# The Ruby copyright is "ruby - Copyright (C) 1993-2025 Yukihiro Matsumoto".
+# The current process is irb.
+# ```
+#
+# ## Tags
+#
+# The examples above use expression tags.
+# These are the tags available in \ERB:
+#
+# - [Expression tag][expression tags]: the tag contains a Ruby expression;
+# in the result, the entire tag is to be replaced with the run-time value of the expression.
+# - [Execution tag][execution tags]: the tag contains Ruby code;
+# in the result, the entire tag is to be replaced with the run-time value of the code.
+# - [Comment tag][comment tags]: the tag contains comment code;
+# in the result, the entire tag is to be omitted.
+#
+# ### Expression Tags
+#
+# You can embed a Ruby expression in a template using an *expression tag*.
+#
+# Its syntax is `<%= _expression_ %>`,
+# where *expression* is any valid Ruby expression.
+#
+# When you call method #result,
+# the method evaluates the expression and replaces the entire expression tag with the expression's value:
+#
+# ```
+# ERB.new('Today is <%= Date::DAYNAMES[Date.today.wday] %>.').result
+# # => "Today is Monday."
+# ERB.new('Tomorrow will be <%= Date::DAYNAMES[Date.today.wday + 1] %>.').result
+# # => "Tomorrow will be Tuesday."
+# ERB.new('Yesterday was <%= Date::DAYNAMES[Date.today.wday - 1] %>.').result
+# # => "Yesterday was Sunday."
+# ```
+#
+# Note that whitespace before and after the expression
+# is allowed but not required,
+# and that such whitespace is stripped from the result.
+#
+# ```
+# ERB.new('My appointment is on <%=Date::DAYNAMES[Date.today.wday + 2]%>.').result
+# # => "My appointment is on Wednesday."
+# ERB.new('My appointment is on <%= Date::DAYNAMES[Date.today.wday + 2] %>.').result
+# # => "My appointment is on Wednesday."
+# ```
+#
+# ### Execution Tags
+#
+# You can embed Ruby executable code in template using an *execution tag*.
+#
+# Its syntax is `<% _code_ %>`,
+# where *code* is any valid Ruby code.
+#
+# When you call method #result,
+# the method executes the code and removes the entire execution tag
+# (generating no text in the result):
+#
+# ```
+# ERB.new('foo <% Dir.chdir("C:/") %> bar').result # => "foo bar"
+# ```
+#
+# Whitespace before and after the embedded code is optional:
+#
+# ```
+# ERB.new('foo <%Dir.chdir("C:/")%> bar').result # => "foo bar"
+# ```
+#
+# You can interleave text with execution tags to form a control structure
+# such as a conditional, a loop, or a `case` statements.
+#
+# Conditional:
+#
+# ```
+# template = <<TEMPLATE
+# <% if verbosity %>
+# An error has occurred.
+# <% else %>
+# Oops!
+# <% end %>
+# TEMPLATE
+# erb = ERB.new(template)
+# verbosity = true
+# erb.result(binding)
+# # => "\nAn error has occurred.\n\n"
+# verbosity = false
+# erb.result(binding)
+# # => "\nOops!\n\n"
+# ```
+#
+# Note that the interleaved text may itself contain expression tags:
+#
+# Loop:
+#
+# ```
+# template = <<TEMPLATE
+# <% Date::ABBR_DAYNAMES.each do |dayname| %>
+# <%= dayname %>
+# <% end %>
+# TEMPLATE
+# ERB.new(template).result
+# # => "\nSun\n\nMon\n\nTue\n\nWed\n\nThu\n\nFri\n\nSat\n\n"
+# ```
+#
+# Other, non-control, lines of Ruby code may be interleaved with the text,
+# and the Ruby code may itself contain regular Ruby comments:
+#
+# ```
+# template = <<TEMPLATE
+# <% 3.times do %>
+# <%= Time.now %>
+# <% sleep(1) # Let's make the times different. %>
+# <% end %>
+# TEMPLATE
+# ERB.new(template).result
+# # => "\n2025-09-09 11:36:02 -0500\n\n\n2025-09-09 11:36:03 -0500\n\n\n2025-09-09 11:36:04 -0500\n\n\n"
+# ```
+#
+# The execution tag may also contain multiple lines of code:
+#
+# ```
+# template = <<TEMPLATE
+# <%
+# (0..2).each do |i|
+# (0..2).each do |j|
+# %>
+# * <%=i%>,<%=j%>
+# <%
+# end
+# end
+# %>
+# TEMPLATE
+# ERB.new(template).result
+# # => "\n* 0,0\n\n* 0,1\n\n* 0,2\n\n* 1,0\n\n* 1,1\n\n* 1,2\n\n* 2,0\n\n* 2,1\n\n* 2,2\n\n"
+# ```
+#
+# #### Shorthand Format for Execution Tags
+#
+# You can use keyword argument `trim_mode: '%'` to enable a shorthand format for execution tags;
+# this example uses the shorthand format `% _code_` instead of `<% _code_ %>`:
+#
+# ```
+# template = <<TEMPLATE
+# % priorities.each do |priority|
+# * <%= priority %>
+# % end
+# TEMPLATE
+# erb = ERB.new(template, trim_mode: '%')
+# priorities = [ 'Run Ruby Quiz',
+# 'Document Modules',
+# 'Answer Questions on Ruby Talk' ]
+# puts erb.result(binding)
+# * Run Ruby Quiz
+# * Document Modules
+# * Answer Questions on Ruby Talk
+# ```
+#
+# Note that in the shorthand format, the character `'%'` must be the first character in the code line
+# (no leading whitespace).
+#
+# #### Suppressing Unwanted Blank Lines
+#
+# With keyword argument `trim_mode` not given,
+# all blank lines go into the result:
+#
+# ```
+# template = <<TEMPLATE
+# <% if true %>
+# <%= RUBY_VERSION %>
+# <% end %>
+# TEMPLATE
+# ERB.new(template).result.lines.each {|line| puts line.inspect }
+# "\n"
+# "3.4.5\n"
+# "\n"
+# ```
+#
+# You can give `trim_mode: '-'`, you can suppress each blank line
+# whose source line ends with `-%>` (instead of `%>`):
+#
+# ```
+# template = <<TEMPLATE
+# <% if true -%>
+# <%= RUBY_VERSION %>
+# <% end -%>
+# TEMPLATE
+# ERB.new(template, trim_mode: '-').result.lines.each {|line| puts line.inspect }
+# "3.4.5\n"
+# ```
+#
+# It is an error to use the trailing `'-%>'` notation without `trim_mode: '-'`:
+#
+# ```
+# ERB.new(template).result.lines.each {|line| puts line.inspect } # Raises SyntaxError.
+# ```
+#
+# #### Suppressing Unwanted Newlines
+#
+# Consider this template:
+#
+# ```
+# template = <<TEMPLATE
+# <% RUBY_VERSION %>
+# <%= RUBY_VERSION %>
+# foo <% RUBY_VERSION %>
+# foo <%= RUBY_VERSION %>
+# TEMPLATE
+# ```
+#
+# With keyword argument `trim_mode` not given, all newlines go into the result:
+#
+# ```
+# ERB.new(template).result.lines.each {|line| puts line.inspect }
+# "\n"
+# "3.4.5\n"
+# "foo \n"
+# "foo 3.4.5\n"
+# ```
+#
+# You can give `trim_mode: '>'` to suppress the trailing newline
+# for each line that ends with `'%>'` (regardless of its beginning):
+#
+# ```
+# ERB.new(template, trim_mode: '>').result.lines.each {|line| puts line.inspect }
+# "3.4.5foo foo 3.4.5"
+# ```
+#
+# You can give `trim_mode: '<>'` to suppress the trailing newline
+# for each line that both begins with `'<%'` and ends with `'%>'`:
+#
+# ```
+# ERB.new(template, trim_mode: '<>').result.lines.each {|line| puts line.inspect }
+# "3.4.5foo \n"
+# "foo 3.4.5\n"
+# ```
+#
+# #### Combining Trim Modes
+#
+# You can combine certain trim modes:
+#
+# - `'%-'`: Enable shorthand and omit each blank line ending with `'-%>'`.
+# - `'%>'`: Enable shorthand and omit newline for each line ending with `'%>'`.
+# - `'%<>'`: Enable shorthand and omit newline for each line starting with `'<%'` and ending with `'%>'`.
+#
+# ### Comment Tags
+#
+# You can embed a comment in a template using a *comment tag*;
+# its syntax is `<%# _text_ %>`,
+# where *text* is the text of the comment.
+#
+# When you call method #result,
+# it removes the entire comment tag
+# (generating no text in the result).
+#
+# Example:
+#
+# ```
+# template = 'Some stuff;<%# Note to self: figure out what the stuff is. %> more stuff.'
+# ERB.new(template).result # => "Some stuff; more stuff."
+# ```
+#
+# A comment tag may appear anywhere in the template.
+#
+# Note that the beginning of the tag must be `'<%#'`, not `'<% #'`.
+#
+# In this example, the tag begins with `'<% #'`, and so is an execution tag, not a comment tag;
+# the cited code consists entirely of a Ruby-style comment (which is of course ignored):
+#
+# ```
+# ERB.new('Some stuff;<% # Note to self: figure out what the stuff is. %> more stuff.').result
+# # => "Some stuff;"
+# ```
+#
+# ## Encodings
+#
+# An \ERB object has an [encoding][encoding],
+# which is by default the encoding of the template string;
+# the result string will also have that encoding.
+#
+# ```
+# template = <<TEMPLATE
+# <%# Comment. %>
+# TEMPLATE
+# erb = ERB.new(template)
+# template.encoding # => #<Encoding:UTF-8>
+# erb.encoding # => #<Encoding:UTF-8>
+# erb.result.encoding # => #<Encoding:UTF-8>
+# ```
+#
+# You can specify a different encoding by adding a [magic comment][magic comments]
+# at the top of the given template:
+#
+# ```
+# template = <<TEMPLATE
+# <%#-*- coding: Big5 -*-%>
+# <%# Comment. %>
+# TEMPLATE
+# erb = ERB.new(template)
+# template.encoding # => #<Encoding:UTF-8>
+# erb.encoding # => #<Encoding:Big5>
+# erb.result.encoding # => #<Encoding:Big5>
+# ```
+#
+# ## Error Reporting
+#
+# Consider this template (containing an error):
+#
+# ```
+# template = '<%= nosuch %>'
+# erb = ERB.new(template)
+# ```
+#
+# When \ERB reports an error,
+# it includes a file name (if available) and a line number;
+# the file name comes from method #filename, the line number from method #lineno.
+#
+# Initially, those values are `nil` and `0`, respectively;
+# these initial values are reported as `'(erb)'` and `1`, respectively:
+#
+# ```
+# erb.filename # => nil
+# erb.lineno # => 0
+# erb.result
+# (erb):1:in '<main>': undefined local variable or method 'nosuch' for main (NameError)
+# ```
+#
+# You can use methods #filename= and #lineno= to assign values
+# that are more meaningful in your context:
+#
+# ```
+# erb.filename = 't.txt'
+# erb.lineno = 555
+# erb.result
+# t.txt:556:in '<main>': undefined local variable or method 'nosuch' for main (NameError)
+# ```
+#
+# You can use method #location= to set both values:
+#
+# ```
+# erb.location = ['u.txt', 999]
+# erb.result
+# u.txt:1000:in '<main>': undefined local variable or method 'nosuch' for main (NameError)
+# ```
+#
+# ## Plain Text with Embedded Ruby
+#
+# Here's a plain-text template;
+# it uses the literal notation `'%q{ ... }'` to define the template
+# (see [%q literals][%q literals]);
+# this avoids problems with backslashes.
+#
+# ```
+# template = %q{
+# From: James Edward Gray II <james@grayproductions.net>
+# To: <%= to %>
+# Subject: Addressing Needs
+#
+# <%= to[/\w+/] %>:
+#
+# Just wanted to send a quick note assuring that your needs are being
+# addressed.
+#
+# I want you to know that my team will keep working on the issues,
+# especially:
+#
+# <%# ignore numerous minor requests -- focus on priorities %>
+# % priorities.each do |priority|
+# * <%= priority %>
+# % end
+#
+# Thanks for your patience.
+#
+# James Edward Gray II
+# }
+# ```
+#
+# The template will need these:
+#
+# ```
+# to = 'Community Spokesman <spokesman@ruby_community.org>'
+# priorities = [ 'Run Ruby Quiz',
+# 'Document Modules',
+# 'Answer Questions on Ruby Talk' ]
+# ```
+#
+# Finally, create the \ERB object and get the result
+#
+# ```
+# erb = ERB.new(template, trim_mode: '%<>')
+# puts erb.result(binding)
+#
+# From: James Edward Gray II <james@grayproductions.net>
+# To: Community Spokesman <spokesman@ruby_community.org>
+# Subject: Addressing Needs
+#
+# Community:
+#
+# Just wanted to send a quick note assuring that your needs are being
+# addressed.
+#
+# I want you to know that my team will keep working on the issues,
+# especially:
+#
+# * Run Ruby Quiz
+# * Document Modules
+# * Answer Questions on Ruby Talk
+#
+# Thanks for your patience.
+#
+# James Edward Gray II
+# ```
+#
+# ## HTML with Embedded Ruby
+#
+# This example shows an HTML template.
+#
+# First, here's a custom class, `Product`:
+#
+# ```
+# class Product
+# def initialize(code, name, desc, cost)
+# @code = code
+# @name = name
+# @desc = desc
+# @cost = cost
+# @features = []
+# end
+#
+# def add_feature(feature)
+# @features << feature
+# end
+#
+# # Support templating of member data.
+# def get_binding
+# binding
+# end
+#
+# end
+# ```
+#
+# The template below will need these values:
+#
+# ```
+# toy = Product.new('TZ-1002',
+# 'Rubysapien',
+# "Geek's Best Friend! Responds to Ruby commands...",
+# 999.95
+# )
+# toy.add_feature('Listens for verbal commands in the Ruby language!')
+# toy.add_feature('Ignores Perl, Java, and all C variants.')
+# toy.add_feature('Karate-Chop Action!!!')
+# toy.add_feature('Matz signature on left leg.')
+# toy.add_feature('Gem studded eyes... Rubies, of course!')
+# ```
+#
+# Here's the HTML:
+#
+# ```
+# template = <<TEMPLATE
+# <html>
+# <head><title>Ruby Toys -- <%= @name %></title></head>
+# <body>
+# <h1><%= @name %> (<%= @code %>)</h1>
+# <p><%= @desc %></p>
+# <ul>
+# <% @features.each do |f| %>
+# <li><b><%= f %></b></li>
+# <% end %>
+# </ul>
+# <p>
+# <% if @cost < 10 %>
+# <b>Only <%= @cost %>!!!</b>
+# <% else %>
+# Call for a price, today!
+# <% end %>
+# </p>
+# </body>
+# </html>
+# TEMPLATE
+# ```
+#
+# Finally, create the \ERB object and get the result (omitting some blank lines):
+#
+# ```
+# erb = ERB.new(template)
+# puts erb.result(toy.get_binding)
+# <html>
+# <head><title>Ruby Toys -- Rubysapien</title></head>
+# <body>
+# <h1>Rubysapien (TZ-1002)</h1>
+# <p>Geek's Best Friend! Responds to Ruby commands...</p>
+# <ul>
+# <li><b>Listens for verbal commands in the Ruby language!</b></li>
+# <li><b>Ignores Perl, Java, and all C variants.</b></li>
+# <li><b>Karate-Chop Action!!!</b></li>
+# <li><b>Matz signature on left leg.</b></li>
+# <li><b>Gem studded eyes... Rubies, of course!</b></li>
+# </ul>
+# <p>
+# Call for a price, today!
+# </p>
+# </body>
+# </html>
+# ```
+#
+#
+# ## Other Template Processors
+#
+# Various Ruby projects have their own template processors.
+# The Ruby Processing System [RDoc][rdoc], for example, has one that can be used elsewhere.
+#
+# Other popular template processors may found in the [Template Engines][template engines] page
+# of the Ruby Toolbox.
+#
+# [%q literals]: https://docs.ruby-lang.org/en/master/syntax/literals_rdoc.html#label-25q-3A+Non-Interpolable+String+Literals
+# [augmented binding]: rdoc-ref:ERB@Augmented+Binding
+# [binding object]: https://docs.ruby-lang.org/en/master/Binding.html
+# [comment tags]: rdoc-ref:ERB@Comment+Tags
+# [default binding]: rdoc-ref:ERB@Default+Binding
+# [encoding]: https://docs.ruby-lang.org/en/master/Encoding.html
+# [execution tags]: rdoc-ref:ERB@Execution+Tags
+# [expression tags]: rdoc-ref:ERB@Expression+Tags
+# [kernel#binding]: https://docs.ruby-lang.org/en/master/Kernel.html#method-i-binding
+# [local binding]: rdoc-ref:ERB@Local+Binding
+# [magic comments]: https://docs.ruby-lang.org/en/master/syntax/comments_rdoc.html#label-Magic+Comments
+# [rdoc]: https://ruby.github.io/rdoc
+# [sprintf]: https://docs.ruby-lang.org/en/master/Kernel.html#method-i-sprintf
+# [template engines]: https://www.ruby-toolbox.com/categories/template_engines
+# [template processor]: https://en.wikipedia.org/wiki/Template_processor
+#
class ERB
- Revision = '$Date:: $' #'
-
- # Returns revision information for the erb.rb module.
+ # :markup: markdown
+ #
+ # :call-seq:
+ # self.version -> string
+ #
+ # Returns the string \ERB version.
def self.version
- "erb.rb [2.1.0 #{ERB::Revision.split[1]}]"
+ VERSION
end
-end
-
-#--
-# ERB::Compiler
-class ERB
- class Compiler # :nodoc:
- class PercentLine # :nodoc:
- def initialize(str)
- @value = str
- end
- attr_reader :value
- alias :to_s :value
-
- def empty?
- @value.empty?
- end
- end
-
- class Scanner # :nodoc:
- @scanner_map = {}
- def self.regist_scanner(klass, trim_mode, percent)
- @scanner_map[[trim_mode, percent]] = klass
- end
-
- def self.default_scanner=(klass)
- @default_scanner = klass
- end
-
- def self.make_scanner(src, trim_mode, percent)
- klass = @scanner_map.fetch([trim_mode, percent], @default_scanner)
- klass.new(src, trim_mode, percent)
- end
-
- def initialize(src, trim_mode, percent)
- @src = src
- @stag = nil
- end
- attr_accessor :stag
-
- def scan; end
- end
-
- class TrimScanner < Scanner # :nodoc:
- def initialize(src, trim_mode, percent)
- super
- @trim_mode = trim_mode
- @percent = percent
- if @trim_mode == '>'
- @scan_line = self.method(:trim_line1)
- elsif @trim_mode == '<>'
- @scan_line = self.method(:trim_line2)
- elsif @trim_mode == '-'
- @scan_line = self.method(:explicit_trim_line)
- else
- @scan_line = self.method(:scan_line)
- end
- end
- attr_accessor :stag
-
- def scan(&block)
- @stag = nil
- if @percent
- @src.each_line do |line|
- percent_line(line, &block)
- end
- else
- @scan_line.call(@src, &block)
- end
- nil
- end
-
- def percent_line(line, &block)
- if @stag || line[0] != ?%
- return @scan_line.call(line, &block)
- end
- line[0] = ''
- if line[0] == ?%
- @scan_line.call(line, &block)
- else
- yield(PercentLine.new(line.chomp))
- end
- end
-
- def scan_line(line)
- line.scan(/(.*?)(<%%|%%>|<%=|<%#|<%|%>|\n|\z)/m) do |tokens|
- tokens.each do |token|
- next if token.empty?
- yield(token)
- end
- end
- end
-
- def trim_line1(line)
- line.scan(/(.*?)(<%%|%%>|<%=|<%#|<%|%>\n|%>|\n|\z)/m) do |tokens|
- tokens.each do |token|
- next if token.empty?
- if token == "%>\n"
- yield('%>')
- yield(:cr)
- else
- yield(token)
- end
- end
- end
- end
-
- def trim_line2(line)
- head = nil
- line.scan(/(.*?)(<%%|%%>|<%=|<%#|<%|%>\n|%>|\n|\z)/m) do |tokens|
- tokens.each do |token|
- next if token.empty?
- head = token unless head
- if token == "%>\n"
- yield('%>')
- if is_erb_stag?(head)
- yield(:cr)
- else
- yield("\n")
- end
- head = nil
- else
- yield(token)
- head = nil if token == "\n"
- end
- end
- end
- end
-
- def explicit_trim_line(line)
- line.scan(/(.*?)(^[ \t]*<%\-|<%\-|<%%|%%>|<%=|<%#|<%|-%>\n|-%>|%>|\z)/m) do |tokens|
- tokens.each do |token|
- next if token.empty?
- if @stag.nil? && /[ \t]*<%-/ =~ token
- yield('<%')
- elsif @stag && token == "-%>\n"
- yield('%>')
- yield(:cr)
- elsif @stag && token == '-%>'
- yield('%>')
- else
- yield(token)
- end
- end
- end
- end
-
- ERB_STAG = %w(<%= <%# <%)
- def is_erb_stag?(s)
- ERB_STAG.member?(s)
- end
- end
-
- Scanner.default_scanner = TrimScanner
-
- class SimpleScanner < Scanner # :nodoc:
- def scan
- @src.scan(/(.*?)(<%%|%%>|<%=|<%#|<%|%>|\n|\z)/m) do |tokens|
- tokens.each do |token|
- next if token.empty?
- yield(token)
- end
- end
- end
- end
-
- Scanner.regist_scanner(SimpleScanner, nil, false)
-
- begin
- require 'strscan'
- class SimpleScanner2 < Scanner # :nodoc:
- def scan
- stag_reg = /(.*?)(<%%|<%=|<%#|<%|\z)/m
- etag_reg = /(.*?)(%%>|%>|\z)/m
- scanner = StringScanner.new(@src)
- while ! scanner.eos?
- scanner.scan(@stag ? etag_reg : stag_reg)
- yield(scanner[1])
- yield(scanner[2])
- end
- end
- end
- Scanner.regist_scanner(SimpleScanner2, nil, false)
-
- class ExplicitScanner < Scanner # :nodoc:
- def scan
- stag_reg = /(.*?)(^[ \t]*<%-|<%%|<%=|<%#|<%-|<%|\z)/m
- etag_reg = /(.*?)(%%>|-%>|%>|\z)/m
- scanner = StringScanner.new(@src)
- while ! scanner.eos?
- scanner.scan(@stag ? etag_reg : stag_reg)
- yield(scanner[1])
-
- elem = scanner[2]
- if /[ \t]*<%-/ =~ elem
- yield('<%')
- elsif elem == '-%>'
- yield('%>')
- yield(:cr) if scanner.scan(/(\n|\z)/)
- else
- yield(elem)
- end
- end
- end
- end
- Scanner.regist_scanner(ExplicitScanner, '-', false)
-
- rescue LoadError
- end
-
- class Buffer # :nodoc:
- def initialize(compiler, enc=nil)
- @compiler = compiler
- @line = []
- @script = enc ? "#coding:#{enc.to_s}\n" : ""
- @compiler.pre_cmd.each do |x|
- push(x)
- end
- end
- attr_reader :script
-
- def push(cmd)
- @line << cmd
- end
-
- def cr
- @script << (@line.join('; '))
- @line = []
- @script << "\n"
- end
-
- def close
- return unless @line
- @compiler.post_cmd.each do |x|
- push(x)
- end
- @script << (@line.join('; '))
- @line = nil
- end
- end
-
- def content_dump(s)
- n = s.count("\n")
- if n > 0
- s.dump + "\n" * n
- else
- s.dump
- end
- end
-
- def compile(s)
- enc = s.encoding
- raise ArgumentError, "#{enc} is not ASCII compatible" if enc.dummy?
- s = s.dup.force_encoding("ASCII-8BIT") # don't use constant Enoding::ASCII_8BIT for miniruby
- enc = detect_magic_comment(s) || enc
- out = Buffer.new(self, enc)
-
- content = ''
- scanner = make_scanner(s)
- scanner.scan do |token|
- next if token.nil?
- next if token == ''
- if scanner.stag.nil?
- case token
- when PercentLine
- out.push("#{@put_cmd} #{content_dump(content)}") if content.size > 0
- content = ''
- out.push(token.to_s)
- out.cr
- when :cr
- out.cr
- when '<%', '<%=', '<%#'
- scanner.stag = token
- out.push("#{@put_cmd} #{content_dump(content)}") if content.size > 0
- content = ''
- when "\n"
- content << "\n"
- out.push("#{@put_cmd} #{content_dump(content)}")
- content = ''
- when '<%%'
- content << '<%'
- else
- content << token
- end
- else
- case token
- when '%>'
- case scanner.stag
- when '<%'
- if content[-1] == ?\n
- content.chop!
- out.push(content)
- out.cr
- else
- out.push(content)
- end
- when '<%='
- out.push("#{@insert_cmd}((#{content}).to_s)")
- when '<%#'
- # out.push("# #{content_dump(content)}")
- end
- scanner.stag = nil
- content = ''
- when '%%>'
- content << '%>'
- else
- content << token
- end
- end
- end
- out.push("#{@put_cmd} #{content_dump(content)}") if content.size > 0
- out.close
- return out.script, enc
- end
-
- def prepare_trim_mode(mode)
- case mode
- when 1
- return [false, '>']
- when 2
- return [false, '<>']
- when 0
- return [false, nil]
- when String
- perc = mode.include?('%')
- if mode.include?('-')
- return [perc, '-']
- elsif mode.include?('<>')
- return [perc, '<>']
- elsif mode.include?('>')
- return [perc, '>']
- else
- [perc, nil]
- end
- else
- return [false, nil]
- end
- end
-
- def make_scanner(src)
- Scanner.make_scanner(src, @trim_mode, @percent)
- end
-
- def initialize(trim_mode)
- @percent, @trim_mode = prepare_trim_mode(trim_mode)
- @put_cmd = 'print'
- @insert_cmd = @put_cmd
- @pre_cmd = []
- @post_cmd = []
- end
- attr_reader :percent, :trim_mode
- attr_accessor :put_cmd, :insert_cmd, :pre_cmd, :post_cmd
-
- private
- def detect_magic_comment(s)
- if /\A<%#(.*)%>/ =~ s or (@percent and /\A%#(.*)/ =~ s)
- comment = $1
- comment = $1 if comment[/-\*-\s*(.*?)\s*-*-$/]
- if %r"coding\s*[=:]\s*([[:alnum:]\-_]+)" =~ comment
- enc = $1.sub(/-(?:mac|dos|unix)/i, '')
- enc = Encoding.find(enc)
- end
- end
- end
- end
-end
-
-#--
-# ERB
-class ERB
+ # :markup: markdown
+ #
+ # :call-seq:
+ # ERB.new(template, trim_mode: nil, eoutvar: '_erbout')
+ #
+ # Returns a new \ERB object containing the given string +template+.
+ #
+ # For details about `template`, its embedded tags, and generated results, see ERB.
+ #
+ # **Keyword Argument `trim_mode`**
+ #
+ # You can use keyword argument `trim_mode: '%'`
+ # to enable the [shorthand format][shorthand format] for execution tags.
+ #
+ # This value allows [blank line control][blank line control]:
+ #
+ # - `'-'`: Omit each blank line ending with `'%>'`.
+ #
+ # Other values allow [newline control][newline control]:
+ #
+ # - `'>'`: Omit newline for each line ending with `'%>'`.
+ # - `'<>'`: Omit newline for each line starting with `'<%'` and ending with `'%>'`.
+ #
+ # You can also [combine trim modes][combine trim modes].
#
- # Constructs a new ERB object with the template specified in _str_.
- #
- # An ERB object works by building a chunk of Ruby code that will output
- # the completed template when run. If _safe_level_ is set to a non-nil value,
- # ERB code will be run in a separate thread with <b>$SAFE</b> set to the
- # provided level.
- #
- # If _trim_mode_ is passed a String containing one or more of the following
- # modifiers, ERB will adjust its code generation as listed:
- #
- # % enables Ruby code processing for lines beginning with %
- # <> omit newline for lines starting with <% and ending in %>
- # > omit newline for lines ending in %>
- #
- # _eoutvar_ can be used to set the name of the variable ERB will build up
- # its output in. This is useful when you need to run multiple ERB
- # templates through the same binding and/or when you want to control where
- # output ends up. Pass the name of the variable to be used inside a String.
- #
- # === Example
- #
- # require "erb"
- #
- # # build data class
- # class Listings
- # PRODUCT = { :name => "Chicken Fried Steak",
- # :desc => "A well messages pattie, breaded and fried.",
- # :cost => 9.95 }
- #
- # attr_reader :product, :price
- #
- # def initialize( product = "", price = "" )
- # @product = product
- # @price = price
- # end
- #
- # def build
- # b = binding
- # # create and run templates, filling member data variables
- # ERB.new(<<-'END_PRODUCT'.gsub(/^\s+/, ""), 0, "", "@product").result b
- # <%= PRODUCT[:name] %>
- # <%= PRODUCT[:desc] %>
- # END_PRODUCT
- # ERB.new(<<-'END_PRICE'.gsub(/^\s+/, ""), 0, "", "@price").result b
- # <%= PRODUCT[:name] %> -- <%= PRODUCT[:cost] %>
- # <%= PRODUCT[:desc] %>
- # END_PRICE
- # end
- # end
- #
- # # setup template data
- # listings = Listings.new
- # listings.build
- #
- # puts listings.product + "\n" + listings.price
- #
- # _Generates_
- #
- # Chicken Fried Steak
- # A well messages pattie, breaded and fried.
- #
- # Chicken Fried Steak -- 9.95
- # A well messages pattie, breaded and fried.
- #
- def initialize(str, safe_level=nil, trim_mode=nil, eoutvar='_erbout')
- @safe_level = safe_level
- compiler = ERB::Compiler.new(trim_mode)
+ # **Keyword Argument `eoutvar`**
+ #
+ # The string value of keyword argument `eoutvar` specifies the name of the variable
+ # that method #result uses to construct its result string;
+ # see #src.
+ #
+ # This is useful when you need to run multiple \ERB templates through the same binding
+ # and/or when you want to control where output ends up.
+ #
+ # It's good practice to choose a variable name that begins with an underscore: `'_'`.
+ #
+ # [blank line control]: rdoc-ref:ERB@Suppressing+Unwanted+Blank+Lines
+ # [combine trim modes]: rdoc-ref:ERB@Combining+Trim+Modes
+ # [newline control]: rdoc-ref:ERB@Suppressing+Unwanted+Newlines
+ # [shorthand format]: rdoc-ref:ERB@Shorthand+Format+for+Execution+Tags
+ #
+ def initialize(str, trim_mode: nil, eoutvar: '_erbout')
+ compiler = make_compiler(trim_mode)
set_eoutvar(compiler, eoutvar)
- @src, @enc = *compiler.compile(str)
+ @src, @encoding, @frozen_string = *compiler.compile(str)
+ @src.freeze
@filename = nil
+ @lineno = 0
+ @_init = self.class.singleton_class
end
- # The Ruby code generated by ERB
+ # :markup: markdown
+ #
+ # :call-seq:
+ # make_compiler -> erb_compiler
+ #
+ # Returns a new ERB::Compiler with the given `trim_mode`;
+ # for `trim_mode` values, see ERB.new:
+ #
+ # ```
+ # ERB.new('').make_compiler(nil)
+ # # => #<ERB::Compiler:0x000001cff9467678 @insert_cmd="print", @percent=false, @post_cmd=[], @pre_cmd=[], @put_cmd="print", @trim_mode=nil>
+ # ```
+ #
+ def make_compiler(trim_mode)
+ ERB::Compiler.new(trim_mode)
+ end
+
+ # :markup: markdown
+ #
+ # Returns the Ruby code that, when executed, generates the result;
+ # the code is executed by method #result,
+ # and by its wrapper methods #result_with_hash and #run:
+ #
+ # ```
+ # template = 'The time is <%= Time.now %>.'
+ # erb = ERB.new(template)
+ # erb.src
+ # # => "#coding:UTF-8\n_erbout = +''; _erbout.<< \"The time is \".freeze; _erbout.<<(( Time.now ).to_s); _erbout.<< \".\".freeze; _erbout"
+ # erb.result
+ # # => "The time is 2025-09-18 15:58:08 -0500."
+ # ```
+ #
+ # In a more readable format:
+ #
+ # ```
+ # # puts erb.src.split('; ')
+ # # #coding:UTF-8
+ # # _erbout = +''
+ # # _erbout.<< "The time is ".freeze
+ # # _erbout.<<(( Time.now ).to_s)
+ # # _erbout.<< ".".freeze
+ # # _erbout
+ # ```
+ #
+ # Variable `_erbout` is used to store the intermediate results in the code;
+ # the name `_erbout` is the default in ERB.new,
+ # and can be changed via keyword argument `eoutvar`:
+ #
+ # ```
+ # erb = ERB.new(template, eoutvar: '_foo')
+ # puts template.src.split('; ')
+ # #coding:UTF-8
+ # _foo = +''
+ # _foo.<< "The time is ".freeze
+ # _foo.<<(( Time.now ).to_s)
+ # _foo.<< ".".freeze
+ # _foo
+ # ```
+ #
attr_reader :src
- # The optional _filename_ argument passed to Kernel#eval when the ERB code
- # is run
- attr_accessor :filename
+ # :markup: markdown
+ #
+ # Returns the encoding of `self`;
+ # see [Encodings][encodings]:
+ #
+ # [encodings]: rdoc-ref:ERB@Encodings
+ #
+ attr_reader :encoding
+ # :markup: markdown
#
- # Can be used to set _eoutvar_ as described in ERB#new. It's probably easier
- # to just use the constructor though, since calling this method requires the
- # setup of an ERB _compiler_ object.
+ # Sets or returns the file name to be used in reporting errors;
+ # see [Error Reporting][error reporting].
#
- def set_eoutvar(compiler, eoutvar = '_erbout')
- compiler.put_cmd = "#{eoutvar}.concat"
- compiler.insert_cmd = "#{eoutvar}.concat"
+ # [error reporting]: rdoc-ref:ERB@Error+Reporting
+ attr_accessor :filename
- cmd = []
- cmd.push "#{eoutvar} = ''"
-
- compiler.pre_cmd = cmd
+ # :markup: markdown
+ #
+ # Sets or returns the line number to be used in reporting errors;
+ # see [Error Reporting][error reporting].
+ #
+ # [error reporting]: rdoc-ref:ERB@Error+Reporting
+ attr_accessor :lineno
- cmd = []
- cmd.push("#{eoutvar}.force_encoding(__ENCODING__)")
+ # :markup: markdown
+ #
+ # :call-seq:
+ # location = [filename, lineno] => [filename, lineno]
+ # location = filename -> filename
+ #
+ # Sets the values of #filename and, if given, #lineno;
+ # see [Error Reporting][error reporting].
+ #
+ # [error reporting]: rdoc-ref:ERB@Error+Reporting
+ def location=((filename, lineno))
+ @filename = filename
+ @lineno = lineno if lineno
+ end
- compiler.post_cmd = cmd
+ # :markup: markdown
+ #
+ # :call-seq:
+ # set_eoutvar(compiler, eoutvar = '_erbout') -> [eoutvar]
+ #
+ # Sets the `eoutvar` value in the ERB::Compiler object `compiler`;
+ # returns a 1-element array containing the value of `eoutvar`:
+ #
+ # ```
+ # template = ERB.new('')
+ # compiler = template.make_compiler(nil)
+ # pp compiler
+ # #<ERB::Compiler:0x000001cff8a9aa00
+ # @insert_cmd="print",
+ # @percent=false,
+ # @post_cmd=[],
+ # @pre_cmd=[],
+ # @put_cmd="print",
+ # @trim_mode=nil>
+ # template.set_eoutvar(compiler, '_foo') # => ["_foo"]
+ # pp compiler
+ # #<ERB::Compiler:0x000001cff8a9aa00
+ # @insert_cmd="_foo.<<",
+ # @percent=false,
+ # @post_cmd=["_foo"],
+ # @pre_cmd=["_foo = +''"],
+ # @put_cmd="_foo.<<",
+ # @trim_mode=nil>
+ # ```
+ #
+ def set_eoutvar(compiler, eoutvar = '_erbout')
+ compiler.put_cmd = "#{eoutvar}.<<"
+ compiler.insert_cmd = "#{eoutvar}.<<"
+ compiler.pre_cmd = ["#{eoutvar} = +''"]
+ compiler.post_cmd = [eoutvar]
end
- # Generate results and print them. (see ERB#result)
- def run(b=TOPLEVEL_BINDING)
+ # :markup: markdown
+ #
+ # :call-seq:
+ # run(binding = new_toplevel) -> nil
+ #
+ # Like #result, but prints the result string (instead of returning it);
+ # returns `nil`.
+ def run(b=new_toplevel)
print self.result(b)
end
+ # :markup: markdown
+ #
+ # :call-seq:
+ # result(binding = new_toplevel) -> new_string
+ #
+ # Returns the string result formed by processing \ERB tags found in the stored template in `self`.
+ #
+ # With no argument given, uses the default binding;
+ # see [Default Binding][default binding].
+ #
+ # With argument `binding` given, uses the local binding;
+ # see [Local Binding][local binding].
+ #
+ # See also #result_with_hash.
+ #
+ # [default binding]: rdoc-ref:ERB@Default+Binding
+ # [local binding]: rdoc-ref:ERB@Local+Binding
+ #
+ def result(b=new_toplevel)
+ unless @_init.equal?(self.class.singleton_class)
+ raise ArgumentError, "not initialized"
+ end
+ eval(@src, b, (@filename || '(erb)'), @lineno)
+ end
+
+ # :markup: markdown
+ #
+ # :call-seq:
+ # result_with_hash(hash) -> new_string
+ #
+ # Returns the string result formed by processing \ERB tags found in the stored string in `self`;
+ # see [Augmented Binding][augmented binding].
+ #
+ # See also #result.
+ #
+ # [augmented binding]: rdoc-ref:ERB@Augmented+Binding
+ #
+ def result_with_hash(hash)
+ b = new_toplevel(hash.keys)
+ hash.each_pair do |key, value|
+ b.local_variable_set(key, value)
+ end
+ result(b)
+ end
+
+ # :markup: markdown
+ #
+ # :call-seq:
+ # new_toplevel(symbols) -> new_binding
#
- # Executes the generated ERB code to produce a completed template, returning
- # the results of that code. (See ERB#new for details on how this process can
- # be affected by _safe_level_.)
- #
- # _b_ accepts a Binding or Proc object which is used to set the context of
- # code evaluation.
- #
- def result(b=TOPLEVEL_BINDING)
- if @safe_level
- proc {
- $SAFE = @safe_level
- eval(@src, b, (@filename || '(erb)'), 0)
- }.call
- else
- eval(@src, b, (@filename || '(erb)'), 0)
+ # Returns a new binding based on `TOPLEVEL_BINDING`;
+ # used to create a default binding for a call to #result.
+ #
+ # See [Default Binding][default binding].
+ #
+ # Argument `symbols` is an array of symbols;
+ # each symbol `symbol` is defined as a new variable to hide and
+ # prevent it from overwriting a variable of the same name already
+ # defined within the binding.
+ #
+ # [default binding]: rdoc-ref:ERB@Default+Binding
+ def new_toplevel(vars = nil)
+ b = TOPLEVEL_BINDING
+ if vars
+ vars = vars.select {|v| b.local_variable_defined?(v)}
+ unless vars.empty?
+ return b.eval("tap {|;#{vars.join(',')}| break binding}")
+ end
end
+ b.dup
end
+ private :new_toplevel
- # Define _methodname_ as instance method of _mod_ from compiled ruby source.
+ # :markup: markdown
+ #
+ # :call-seq:
+ # def_method(module, method_signature, filename = '(ERB)') -> method_name
+ #
+ # Creates and returns a new instance method in the given module `module`;
+ # returns the method name as a symbol.
+ #
+ # The method is created from the given `method_signature`,
+ # which consists of the method name and its argument names (if any).
+ #
+ # The `filename` sets the value of #filename;
+ # see [Error Reporting][error reporting].
+ #
+ # [error reporting]: rdoc-ref:ERB@Error+Reporting
+ #
+ # ```
+ # template = '<%= arg1 %> <%= arg2 %>'
+ # erb = ERB.new(template)
+ # MyModule = Module.new
+ # erb.def_method(MyModule, 'render(arg1, arg2)') # => :render
+ # class MyClass; include MyModule; end
+ # MyClass.new.render('foo', 123) # => "foo 123"
+ # ```
#
- # example:
- # filename = 'example.rhtml' # 'arg1' and 'arg2' are used in example.rhtml
- # erb = ERB.new(File.read(filename))
- # erb.def_method(MyClass, 'render(arg1, arg2)', filename)
- # print MyClass.new.render('foo', 123)
def def_method(mod, methodname, fname='(ERB)')
- src = self.src
- magic_comment = "#coding:#{@enc}\n"
+ unless @_init.equal?(self.class.singleton_class)
+ raise ArgumentError, "not initialized"
+ end
+ src = self.src.sub(/^(?!#|$)/) {"def #{methodname}\n"} << "\nend\n"
mod.module_eval do
- eval(magic_comment + "def #{methodname}\n" + src + "\nend\n", binding, fname, -2)
+ eval(src, binding, fname, -1)
end
end
- # Create unnamed module, define _methodname_ as instance method of it, and return it.
- #
- # example:
- # filename = 'example.rhtml' # 'arg1' and 'arg2' are used in example.rhtml
- # erb = ERB.new(File.read(filename))
- # erb.filename = filename
- # MyModule = erb.def_module('render(arg1, arg2)')
- # class MyClass
- # include MyModule
- # end
+ # :markup: markdown
+ #
+ # :call-seq:
+ # def_module(method_name = 'erb') -> new_module
+ #
+ # Returns a new nameless module that has instance method `method_name`.
+ #
+ # ```
+ # template = '<%= arg1 %> <%= arg2 %>'
+ # erb = ERB.new(template)
+ # MyModule = template.def_module('render(arg1, arg2)')
+ # class MyClass
+ # include MyModule
+ # end
+ # MyClass.new.render('foo', 123)
+ # # => "foo 123"
+ # ```
+ #
def def_module(methodname='erb')
mod = Module.new
def_method(mod, methodname, @filename || '(ERB)')
mod
end
- # Define unnamed class which has _methodname_ as instance method, and return it.
+ # :markup: markdown
#
- # example:
- # class MyClass_
- # def initialize(arg1, arg2)
- # @arg1 = arg1; @arg2 = arg2
- # end
- # end
- # filename = 'example.rhtml' # @arg1 and @arg2 are used in example.rhtml
- # erb = ERB.new(File.read(filename))
- # erb.filename = filename
- # MyClass = erb.def_class(MyClass_, 'render()')
- # print MyClass.new('foo', 123).render()
- def def_class(superklass=Object, methodname='result')
- cls = Class.new(superklass)
- def_method(cls, methodname, @filename || '(ERB)')
- cls
- end
-end
-
-#--
-# ERB::Util
-class ERB
- # A utility module for conversion routines, often handy in HTML generation.
- module Util
- public
- #
- # A utility method for escaping HTML tag characters in _s_.
- #
- # require "erb"
- # include ERB::Util
- #
- # puts html_escape("is a > 0 & a < 10?")
- #
- # _Generates_
- #
- # is a &gt; 0 &amp; a &lt; 10?
- #
- def html_escape(s)
- s.to_s.gsub(/&/, "&amp;").gsub(/\"/, "&quot;").gsub(/>/, "&gt;").gsub(/</, "&lt;")
- end
- alias h html_escape
- module_function :h
- module_function :html_escape
-
- #
- # A utility method for encoding the String _s_ as a URL.
- #
- # require "erb"
- # include ERB::Util
- #
- # puts url_encode("Programming Ruby: The Pragmatic Programmer's Guide")
- #
- # _Generates_
- #
- # Programming%20Ruby%3A%20%20The%20Pragmatic%20Programmer%27s%20Guide
- #
- def url_encode(s)
- s.to_s.dup.force_encoding("ASCII-8BIT").gsub(/[^a-zA-Z0-9_\-.]/n) {
- sprintf("%%%02X", $&.unpack("C")[0])
- }
- end
- alias u url_encode
- module_function :u
- module_function :url_encode
- end
-end
-
-#--
-# ERB::DefMethod
-class ERB
- # Utility module to define eRuby script as instance method.
- #
- # === Example
- #
- # example.rhtml:
- # <% for item in @items %>
- # <b><%= item %></b>
- # <% end %>
- #
- # example.rb:
- # require 'erb'
- # class MyClass
- # extend ERB::DefMethod
- # def_erb_method('render()', 'example.rhtml')
- # def initialize(items)
- # @items = items
- # end
+ # :call-seq:
+ # def_class(super_class = Object, method_name = 'result') -> new_class
+ #
+ # Returns a new nameless class whose superclass is `super_class`,
+ # and which has instance method `method_name`.
+ #
+ # Create a template from HTML that has embedded expression tags that use `@arg1` and `@arg2`:
+ #
+ # ```
+ # html = <<TEMPLATE
+ # <html>
+ # <body>
+ # <p><%= @arg1 %></p>
+ # <p><%= @arg2 %></p>
+ # </body>
+ # </html>
+ # TEMPLATE
+ # template = ERB.new(html)
+ # ```
+ #
+ # Create a base class that has `@arg1` and `@arg2`:
+ #
+ # ```
+ # class MyBaseClass
+ # def initialize(arg1, arg2)
+ # @arg1 = arg1
+ # @arg2 = arg2
# end
- # print MyClass.new([10,20,30]).render()
+ # end
+ # ```
#
- # result:
+ # Use method #def_class to create a subclass that has method `:render`:
#
- # <b>10</b>
+ # ```
+ # MySubClass = template.def_class(MyBaseClass, :render)
+ # ```
#
- # <b>20</b>
+ # Generate the result:
#
- # <b>30</b>
+ # ```
+ # puts MySubClass.new('foo', 123).render
+ # <html>
+ # <body>
+ # <p>foo</p>
+ # <p>123</p>
+ # </body>
+ # </html>
+ # ```
#
- module DefMethod
- public
- # define _methodname_ as instance method of current module, using ERB object or eRuby file
- def def_erb_method(methodname, erb_or_fname)
- if erb_or_fname.kind_of? String
- fname = erb_or_fname
- erb = ERB.new(File.read(fname))
- erb.def_method(self, methodname, fname)
- else
- erb = erb_or_fname
- erb.def_method(self, methodname, erb.filename || '(ERB)')
- end
- end
- module_function :def_erb_method
+ def def_class(superklass=Object, methodname='result')
+ cls = Class.new(superklass)
+ def_method(cls, methodname, @filename || '(ERB)')
+ cls
end
end
diff --git a/lib/erb/compiler.rb b/lib/erb/compiler.rb
new file mode 100644
index 0000000000..6d70288b4f
--- /dev/null
+++ b/lib/erb/compiler.rb
@@ -0,0 +1,487 @@
+# frozen_string_literal: true
+#--
+# ERB::Compiler
+#
+# Compiles ERB templates into Ruby code; the compiled code produces the
+# template result when evaluated. ERB::Compiler provides hooks to define how
+# generated output is handled.
+#
+# Internally ERB does something like this to generate the code returned by
+# ERB#src:
+#
+# compiler = ERB::Compiler.new('<>')
+# compiler.pre_cmd = ["_erbout=+''"]
+# compiler.put_cmd = "_erbout.<<"
+# compiler.insert_cmd = "_erbout.<<"
+# compiler.post_cmd = ["_erbout"]
+#
+# code, enc = compiler.compile("Got <%= obj %>!\n")
+# puts code
+#
+# <i>Generates</i>:
+#
+# #coding:UTF-8
+# _erbout=+''; _erbout.<< "Got ".freeze; _erbout.<<(( obj ).to_s); _erbout.<< "!\n".freeze; _erbout
+#
+# By default the output is sent to the print method. For example:
+#
+# compiler = ERB::Compiler.new('<>')
+# code, enc = compiler.compile("Got <%= obj %>!\n")
+# puts code
+#
+# <i>Generates</i>:
+#
+# #coding:UTF-8
+# print "Got ".freeze; print(( obj ).to_s); print "!\n".freeze
+#
+# == Evaluation
+#
+# The compiled code can be used in any context where the names in the code
+# correctly resolve. Using the last example, each of these print 'Got It!'
+#
+# Evaluate using a variable:
+#
+# obj = 'It'
+# eval code
+#
+# Evaluate using an input:
+#
+# mod = Module.new
+# mod.module_eval %{
+# def get(obj)
+# #{code}
+# end
+# }
+# extend mod
+# get('It')
+#
+# Evaluate using an accessor:
+#
+# klass = Class.new Object
+# klass.class_eval %{
+# attr_accessor :obj
+# def initialize(obj)
+# @obj = obj
+# end
+# def get_it
+# #{code}
+# end
+# }
+# klass.new('It').get_it
+#
+# Good! See also ERB#def_method, ERB#def_module, and ERB#def_class.
+class ERB::Compiler # :nodoc:
+ class PercentLine # :nodoc:
+ def initialize(str)
+ @value = str
+ end
+ attr_reader :value
+ alias :to_s :value
+ end
+
+ class Scanner # :nodoc:
+ @scanner_map = defined?(Ractor) ? Ractor.make_shareable({}) : {}
+ class << self
+ if defined?(Ractor)
+ def register_scanner(klass, trim_mode, percent)
+ @scanner_map = Ractor.make_shareable({ **@scanner_map, [trim_mode, percent] => klass })
+ end
+ else
+ def register_scanner(klass, trim_mode, percent)
+ @scanner_map[[trim_mode, percent]] = klass
+ end
+ end
+ alias :regist_scanner :register_scanner
+ end
+
+ def self.default_scanner=(klass)
+ @default_scanner = klass
+ end
+
+ def self.make_scanner(src, trim_mode, percent)
+ klass = @scanner_map.fetch([trim_mode, percent], @default_scanner)
+ klass.new(src, trim_mode, percent)
+ end
+
+ DEFAULT_STAGS = %w(<%% <%= <%# <%).freeze
+ DEFAULT_ETAGS = %w(%%> %>).freeze
+ def initialize(src, trim_mode, percent)
+ @src = src
+ @stag = nil
+ @stags = DEFAULT_STAGS
+ @etags = DEFAULT_ETAGS
+ end
+ attr_accessor :stag
+ attr_reader :stags, :etags
+
+ def scan; end
+ end
+
+ class TrimScanner < Scanner # :nodoc:
+ def initialize(src, trim_mode, percent)
+ super
+ @trim_mode = trim_mode
+ @percent = percent
+ if @trim_mode == '>'
+ @scan_reg = /(.*?)(%>\r?\n|#{(stags + etags).join('|')}|\n|\z)/m
+ @scan_line = self.method(:trim_line1)
+ elsif @trim_mode == '<>'
+ @scan_reg = /(.*?)(%>\r?\n|#{(stags + etags).join('|')}|\n|\z)/m
+ @scan_line = self.method(:trim_line2)
+ elsif @trim_mode == '-'
+ @scan_reg = /(.*?)(^[ \t]*<%\-|<%\-|-%>\r?\n|-%>|#{(stags + etags).join('|')}|\z)/m
+ @scan_line = self.method(:explicit_trim_line)
+ else
+ @scan_reg = /(.*?)(#{(stags + etags).join('|')}|\n|\z)/m
+ @scan_line = self.method(:scan_line)
+ end
+ end
+
+ def scan(&block)
+ @stag = nil
+ if @percent
+ @src.each_line do |line|
+ percent_line(line, &block)
+ end
+ else
+ @scan_line.call(@src, &block)
+ end
+ nil
+ end
+
+ def percent_line(line, &block)
+ if @stag || line[0] != ?%
+ return @scan_line.call(line, &block)
+ end
+
+ line[0] = ''
+ if line[0] == ?%
+ @scan_line.call(line, &block)
+ else
+ yield(PercentLine.new(line.chomp))
+ end
+ end
+
+ def scan_line(line)
+ line.scan(@scan_reg) do |tokens|
+ tokens.each do |token|
+ next if token.empty?
+ yield(token)
+ end
+ end
+ end
+
+ def trim_line1(line)
+ line.scan(@scan_reg) do |tokens|
+ tokens.each do |token|
+ next if token.empty?
+ if token == "%>\n" || token == "%>\r\n"
+ yield('%>')
+ yield(:cr)
+ else
+ yield(token)
+ end
+ end
+ end
+ end
+
+ def trim_line2(line)
+ head = nil
+ line.scan(@scan_reg) do |tokens|
+ tokens.each do |token|
+ next if token.empty?
+ head = token unless head
+ if token == "%>\n" || token == "%>\r\n"
+ yield('%>')
+ if is_erb_stag?(head)
+ yield(:cr)
+ else
+ yield("\n")
+ end
+ head = nil
+ else
+ yield(token)
+ head = nil if token == "\n"
+ end
+ end
+ end
+ end
+
+ def explicit_trim_line(line)
+ line.scan(@scan_reg) do |tokens|
+ tokens.each do |token|
+ next if token.empty?
+ if @stag.nil? && /[ \t]*<%-/ =~ token
+ yield('<%')
+ elsif @stag && (token == "-%>\n" || token == "-%>\r\n")
+ yield('%>')
+ yield(:cr)
+ elsif @stag && token == '-%>'
+ yield('%>')
+ else
+ yield(token)
+ end
+ end
+ end
+ end
+
+ ERB_STAG = %w(<%= <%# <%).freeze
+ def is_erb_stag?(s)
+ ERB_STAG.member?(s)
+ end
+ end
+
+ Scanner.default_scanner = TrimScanner
+
+ begin
+ require 'strscan'
+ rescue LoadError
+ else
+ class SimpleScanner < Scanner # :nodoc:
+ def scan
+ stag_reg = (stags == DEFAULT_STAGS) ? /(.*?)(<%[%=#]?|\z)/m : /(.*?)(#{stags.join('|')}|\z)/m
+ etag_reg = (etags == DEFAULT_ETAGS) ? /(.*?)(%%?>|\z)/m : /(.*?)(#{etags.join('|')}|\z)/m
+ scanner = StringScanner.new(@src)
+ while ! scanner.eos?
+ scanner.scan(@stag ? etag_reg : stag_reg)
+ yield(scanner[1])
+ yield(scanner[2])
+ end
+ end
+ end
+ Scanner.register_scanner(SimpleScanner, nil, false)
+
+ class ExplicitScanner < Scanner # :nodoc:
+ def scan
+ stag_reg = /(.*?)(^[ \t]*<%-|<%-|#{stags.join('|')}|\z)/m
+ etag_reg = /(.*?)(-%>|#{etags.join('|')}|\z)/m
+ scanner = StringScanner.new(@src)
+ while ! scanner.eos?
+ scanner.scan(@stag ? etag_reg : stag_reg)
+ yield(scanner[1])
+
+ elem = scanner[2]
+ if /[ \t]*<%-/ =~ elem
+ yield('<%')
+ elsif elem == '-%>'
+ yield('%>')
+ yield(:cr) if scanner.scan(/(\r?\n|\z)/)
+ else
+ yield(elem)
+ end
+ end
+ end
+ end
+ Scanner.register_scanner(ExplicitScanner, '-', false)
+ end
+
+ class Buffer # :nodoc:
+ def initialize(compiler, enc=nil, frozen=nil)
+ @compiler = compiler
+ @line = []
+ @script = +''
+ @script << "#coding:#{enc}\n" if enc
+ @script << "#frozen-string-literal:#{frozen}\n" unless frozen.nil?
+ @compiler.pre_cmd.each do |x|
+ push(x)
+ end
+ end
+ attr_reader :script
+
+ def push(cmd)
+ @line << cmd
+ end
+
+ def cr
+ @script << (@line.join('; '))
+ @line = []
+ @script << "\n"
+ end
+
+ def close
+ return unless @line
+ @compiler.post_cmd.each do |x|
+ push(x)
+ end
+ @script << (@line.join('; '))
+ @line = nil
+ end
+ end
+
+ def add_put_cmd(out, content)
+ out.push("#{@put_cmd} #{content.dump}.freeze#{"\n" * content.count("\n")}")
+ end
+
+ def add_insert_cmd(out, content)
+ out.push("#{@insert_cmd}((#{content}).to_s)")
+ end
+
+ # Compiles an ERB template into Ruby code. Returns an array of the code
+ # and encoding like ["code", Encoding].
+ def compile(s)
+ enc = s.encoding
+ raise ArgumentError, "#{enc} is not ASCII compatible" if enc.dummy?
+ s = s.b # see String#b
+ magic_comment = detect_magic_comment(s, enc)
+ out = Buffer.new(self, *magic_comment)
+
+ self.content = +''
+ scanner = make_scanner(s)
+ scanner.scan do |token|
+ next if token.nil?
+ next if token == ''
+ if scanner.stag.nil?
+ compile_stag(token, out, scanner)
+ else
+ compile_etag(token, out, scanner)
+ end
+ end
+ add_put_cmd(out, content) if content.size > 0
+ out.close
+ return out.script, *magic_comment
+ end
+
+ def compile_stag(stag, out, scanner)
+ case stag
+ when PercentLine
+ add_put_cmd(out, content) if content.size > 0
+ self.content = +''
+ out.push(stag.to_s)
+ out.cr
+ when :cr
+ out.cr
+ when '<%', '<%=', '<%#'
+ scanner.stag = stag
+ add_put_cmd(out, content) if content.size > 0
+ self.content = +''
+ when "\n"
+ content << "\n"
+ add_put_cmd(out, content)
+ self.content = +''
+ when '<%%'
+ content << '<%'
+ else
+ content << stag
+ end
+ end
+
+ def compile_etag(etag, out, scanner)
+ case etag
+ when '%>'
+ compile_content(scanner.stag, out)
+ scanner.stag = nil
+ self.content = +''
+ when '%%>'
+ content << '%>'
+ else
+ content << etag
+ end
+ end
+
+ def compile_content(stag, out)
+ case stag
+ when '<%'
+ if content[-1] == ?\n
+ content.chop!
+ out.push(content)
+ out.cr
+ else
+ out.push(content)
+ end
+ when '<%='
+ add_insert_cmd(out, content)
+ when '<%#'
+ out.push("\n" * content.count("\n")) # only adjust lineno
+ end
+ end
+
+ def prepare_trim_mode(mode) # :nodoc:
+ case mode
+ when 1
+ return [false, '>']
+ when 2
+ return [false, '<>']
+ when 0, nil
+ return [false, nil]
+ when String
+ unless mode.match?(/\A(%|-|>|<>){1,2}\z/)
+ warn_invalid_trim_mode(mode, uplevel: 5)
+ end
+
+ perc = mode.include?('%')
+ if mode.include?('-')
+ return [perc, '-']
+ elsif mode.include?('<>')
+ return [perc, '<>']
+ elsif mode.include?('>')
+ return [perc, '>']
+ else
+ [perc, nil]
+ end
+ else
+ warn_invalid_trim_mode(mode, uplevel: 5)
+ return [false, nil]
+ end
+ end
+
+ def make_scanner(src) # :nodoc:
+ Scanner.make_scanner(src, @trim_mode, @percent)
+ end
+
+ # Construct a new compiler using the trim_mode. See ERB::new for available
+ # trim modes.
+ def initialize(trim_mode)
+ @percent, @trim_mode = prepare_trim_mode(trim_mode)
+ @put_cmd = 'print'
+ @insert_cmd = @put_cmd
+ @pre_cmd = []
+ @post_cmd = []
+ end
+ attr_reader :percent, :trim_mode
+
+ # The command to handle text that ends with a newline
+ attr_accessor :put_cmd
+
+ # The command to handle text that is inserted prior to a newline
+ attr_accessor :insert_cmd
+
+ # An array of commands prepended to compiled code
+ attr_accessor :pre_cmd
+
+ # An array of commands appended to compiled code
+ attr_accessor :post_cmd
+
+ private
+
+ # A buffered text in #compile
+ attr_accessor :content
+
+ def detect_magic_comment(s, enc = nil)
+ re = @percent ? /\G(?:<%#(.*)%>|%#(.*)\n)/ : /\G<%#(.*)%>/
+ frozen = nil
+ s.scan(re) do
+ comment = $+
+ comment = $1 if comment[/-\*-\s*([^\s].*?)\s*-\*-$/]
+ case comment
+ when %r"coding\s*[=:]\s*([[:alnum:]\-_]+)"
+ enc = Encoding.find($1.sub(/-(?:mac|dos|unix)/i, ''))
+ when %r"frozen[-_]string[-_]literal\s*:\s*([[:alnum:]]+)"
+ frozen = $1
+ end
+ end
+ return enc, frozen
+ end
+
+ # :stopdoc:
+ WARNING_UPLEVEL = Class.new {
+ attr_reader :c
+ def initialize from
+ @c = caller.length - from.length
+ end
+ }.new(caller(0)).c
+ private_constant :WARNING_UPLEVEL
+
+ def warn_invalid_trim_mode(mode, uplevel:)
+ warn "Invalid ERB trim mode: #{mode.inspect} (trim_mode: nil, 0, 1, 2, or String composed of '%' and/or '-', '>', '<>')", uplevel: uplevel + WARNING_UPLEVEL
+ end
+end
diff --git a/lib/erb/def_method.rb b/lib/erb/def_method.rb
new file mode 100644
index 0000000000..e503b37140
--- /dev/null
+++ b/lib/erb/def_method.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# ERB::DefMethod
+#
+# Utility module to define eRuby script as instance method.
+#
+# === Example
+#
+# example.rhtml:
+# <% for item in @items %>
+# <b><%= item %></b>
+# <% end %>
+#
+# example.rb:
+# require 'erb'
+# class MyClass
+# extend ERB::DefMethod
+# def_erb_method('render()', 'example.rhtml')
+# def initialize(items)
+# @items = items
+# end
+# end
+# print MyClass.new([10,20,30]).render()
+#
+# result:
+#
+# <b>10</b>
+#
+# <b>20</b>
+#
+# <b>30</b>
+#
+module ERB::DefMethod
+ # define _methodname_ as instance method of current module, using ERB
+ # object or eRuby file
+ def def_erb_method(methodname, erb_or_fname)
+ if erb_or_fname.kind_of? String
+ fname = erb_or_fname
+ erb = ERB.new(File.read(fname))
+ erb.def_method(self, methodname, fname)
+ else
+ erb = erb_or_fname
+ erb.def_method(self, methodname, erb.filename || '(ERB)')
+ end
+ end
+ module_function :def_erb_method
+end
diff --git a/lib/erb/erb.gemspec b/lib/erb/erb.gemspec
new file mode 100644
index 0000000000..70113a2a04
--- /dev/null
+++ b/lib/erb/erb.gemspec
@@ -0,0 +1,37 @@
+begin
+ require_relative 'lib/erb/version'
+rescue LoadError
+ # for Ruby core repository
+ require_relative 'version'
+end
+
+Gem::Specification.new do |spec|
+ spec.name = 'erb'
+ spec.version = ERB::VERSION
+ spec.authors = ['Masatoshi SEKI', 'Takashi Kokubun']
+ spec.email = ['seki@ruby-lang.org', 'k0kubun@ruby-lang.org']
+
+ spec.summary = %q{An easy to use but powerful templating system for Ruby.}
+ spec.description = %q{An easy to use but powerful templating system for Ruby.}
+ spec.homepage = 'https://github.com/ruby/erb'
+ spec.licenses = ['Ruby', 'BSD-2-Clause']
+
+ spec.metadata['homepage_uri'] = spec.homepage
+ spec.metadata['source_code_uri'] = spec.homepage
+ spec.metadata['changelog_uri'] = "https://github.com/ruby/erb/blob/v#{spec.version}/NEWS.md"
+
+ spec.files = Dir.chdir(__dir__) do
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|\.git|\.github)/}) }
+ end
+ spec.bindir = 'libexec'
+ spec.executables = ['erb']
+ spec.require_paths = ['lib']
+
+ spec.required_ruby_version = '>= 3.2.0'
+
+ if RUBY_ENGINE == 'jruby'
+ spec.platform = 'java'
+ else
+ spec.extensions = ['ext/erb/escape/extconf.rb']
+ end
+end
diff --git a/lib/erb/util.rb b/lib/erb/util.rb
new file mode 100644
index 0000000000..d7d69eb4f1
--- /dev/null
+++ b/lib/erb/util.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+# Load CGI.escapeHTML and CGI.escapeURIComponent.
+# CRuby:
+# cgi.gem v0.1.0+ (Ruby 2.7-3.4) and Ruby 4.0+ stdlib have 'cgi/escape' and CGI.escapeHTML.
+# cgi.gem v0.3.3+ (Ruby 3.2-3.4) and Ruby 4.0+ stdlib have CGI.escapeURIComponent.
+# JRuby: cgi.gem has a Java extension 'cgi/escape'.
+# TruffleRuby: lib/truffle/cgi/escape.rb requires 'cgi/util'.
+require 'cgi/escape'
+
+# Load or define ERB::Escape#html_escape.
+# We don't build the C extension 'cgi/escape' for JRuby, TruffleRuby, and WASM.
+# miniruby (used by CRuby build scripts) also fails to load erb/escape.so.
+begin
+ require 'erb/escape'
+rescue LoadError
+ # ERB::Escape
+ #
+ # A subset of ERB::Util. Unlike ERB::Util#html_escape, we expect/hope
+ # Rails will not monkey-patch ERB::Escape#html_escape.
+ module ERB::Escape
+ # :stopdoc:
+ def html_escape(s)
+ CGI.escapeHTML(s.to_s)
+ end
+ module_function :html_escape
+ end
+end
+
+# ERB::Util
+#
+# A utility module for conversion routines, often handy in HTML generation.
+module ERB::Util
+ #
+ # A utility method for escaping HTML tag characters in _s_.
+ #
+ # require "erb"
+ # include ERB::Util
+ #
+ # puts html_escape("is a > 0 & a < 10?")
+ #
+ # _Generates_
+ #
+ # is a &gt; 0 &amp; a &lt; 10?
+ #
+ include ERB::Escape # html_escape
+ module_function :html_escape
+ alias h html_escape
+ module_function :h
+
+ if CGI.respond_to?(:escapeURIComponent)
+ #
+ # A utility method for encoding the String _s_ as a URL.
+ #
+ # require "erb"
+ # include ERB::Util
+ #
+ # puts url_encode("Programming Ruby: The Pragmatic Programmer's Guide")
+ #
+ # _Generates_
+ #
+ # Programming%20Ruby%3A%20%20The%20Pragmatic%20Programmer%27s%20Guide
+ #
+ def url_encode(s)
+ CGI.escapeURIComponent(s.to_s)
+ end
+ else # cgi.gem <= v0.3.2
+ def url_encode(s)
+ s.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/n) do |m|
+ sprintf("%%%02X", m.unpack1("C"))
+ end
+ end
+ end
+ alias u url_encode
+ module_function :u
+ module_function :url_encode
+end
diff --git a/lib/erb/version.rb b/lib/erb/version.rb
new file mode 100644
index 0000000000..fde4a2776a
--- /dev/null
+++ b/lib/erb/version.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+class ERB
+ # The string \ERB version.
+ VERSION = '6.0.4'
+end
diff --git a/lib/error_highlight.rb b/lib/error_highlight.rb
new file mode 100644
index 0000000000..31db95d11b
--- /dev/null
+++ b/lib/error_highlight.rb
@@ -0,0 +1,2 @@
+require_relative "error_highlight/base"
+require_relative "error_highlight/core_ext"
diff --git a/lib/error_highlight/base.rb b/lib/error_highlight/base.rb
new file mode 100644
index 0000000000..5fffe5ec34
--- /dev/null
+++ b/lib/error_highlight/base.rb
@@ -0,0 +1,938 @@
+require_relative "version"
+
+module ErrorHighlight
+ # Identify the code fragment where a given exception occurred.
+ #
+ # Options:
+ #
+ # point_type: :name | :args
+ # :name (default) points to the method/variable name where the exception occurred.
+ # :args points to the arguments of the method call where the exception occurred.
+ #
+ # backtrace_location: Thread::Backtrace::Location
+ # It locates the code fragment of the given backtrace_location.
+ # By default, it uses the first frame of backtrace_locations of the given exception.
+ #
+ # Returns:
+ # {
+ # first_lineno: Integer,
+ # first_column: Integer,
+ # last_lineno: Integer,
+ # last_column: Integer,
+ # snippet: String,
+ # script_lines: [String],
+ # } | nil
+ #
+ # Limitations:
+ #
+ # Currently, ErrorHighlight.spot only supports a single-line code fragment.
+ # Therefore, if the return value is not nil, first_lineno and last_lineno will have
+ # the same value. If the relevant code fragment spans multiple lines
+ # (e.g., Array#[] of <tt>ary[(newline)expr(newline)]</tt>), the method will return nil.
+ # This restriction may be removed in the future.
+ def self.spot(obj, **opts)
+ case obj
+ when Exception
+ exc = obj
+ loc = opts[:backtrace_location]
+ opts = { point_type: opts.fetch(:point_type, :name) }
+
+ unless loc
+ case exc
+ when TypeError, ArgumentError
+ opts[:point_type] = :args
+ end
+
+ locs = exc.backtrace_locations
+ return nil unless locs
+
+ loc = locs.first
+ return nil unless loc
+
+ opts[:name] = exc.name if NameError === obj
+ end
+
+ return nil unless Thread::Backtrace::Location === loc
+
+ node =
+ begin
+ RubyVM::AbstractSyntaxTree.of(loc, keep_script_lines: true)
+ rescue RuntimeError => error
+ # RubyVM::AbstractSyntaxTree.of raises an error with a message that
+ # includes "prism" when the ISEQ was compiled with the prism compiler.
+ # In this case, we'll try to parse again with prism instead.
+ raise unless error.message.include?("prism")
+ prism_find(loc)
+ end
+
+ Spotter.new(node, **opts).spot
+
+ when RubyVM::AbstractSyntaxTree::Node, Prism::Node
+ Spotter.new(obj, **opts).spot
+
+ else
+ raise TypeError, "Exception is expected"
+ end
+
+ rescue SyntaxError,
+ SystemCallError, # file not found or something
+ ArgumentError # eval'ed code
+
+ return nil
+ end
+
+ # Accepts a Thread::Backtrace::Location object and returns a Prism::Node
+ # corresponding to the backtrace location in the source code.
+ def self.prism_find(location)
+ require "prism"
+ return nil if Prism::VERSION < "1.0.0"
+
+ absolute_path = location.absolute_path
+ return unless absolute_path
+
+ node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(location)
+ Prism.parse_file(absolute_path).value.breadth_first_search { |node| node.node_id == node_id }
+ end
+
+ private_class_method :prism_find
+
+ class Spotter
+ class NonAscii < Exception; end
+ private_constant :NonAscii
+
+ def initialize(node, point_type: :name, name: nil)
+ @node = node
+ @point_type = point_type
+ @name = name
+
+ # Not-implemented-yet options
+ @arg = nil # Specify the index or keyword at which argument caused the TypeError/ArgumentError
+ @multiline = false # Allow multiline spot
+
+ @fetch = -> (lineno, last_lineno = lineno) do
+ snippet = @node.script_lines[lineno - 1 .. last_lineno - 1].join("")
+ snippet += "\n" unless snippet.end_with?("\n")
+
+ # It requires some work to support Unicode (or multibyte) characters.
+ # Tentatively, we stop highlighting if the code snippet has non-ascii characters.
+ # See https://github.com/ruby/error_highlight/issues/4
+ raise NonAscii unless snippet.ascii_only?
+
+ snippet
+ end
+ end
+
+ def spot
+ return nil unless @node
+
+ # In Ruby 3.2 or later, a nested constant access (like `Foo::Bar::Baz`)
+ # is compiled to one instruction (opt_getconstant_path).
+ # @node points to the node of the whole `Foo::Bar::Baz` even if `Foo`
+ # or `Foo::Bar` causes NameError.
+ # So we try to spot the sub-node that causes the NameError by using
+ # `NameError#name`.
+ case @node.type
+ when :COLON2
+ subnodes = []
+ node = @node
+ while node.type == :COLON2
+ node2, const = node.children
+ subnodes << node if const == @name
+ node = node2
+ end
+ if node.type == :CONST || node.type == :COLON3
+ if node.children.first == @name
+ subnodes << node
+ end
+
+ # If we found only one sub-node whose name is equal to @name, use it
+ return nil if subnodes.size != 1
+ @node = subnodes.first
+ else
+ # Do nothing; opt_getconstant_path is used only when the const base is
+ # NODE_CONST (`Foo`) or NODE_COLON3 (`::Foo`)
+ end
+ when :constant_path_node
+ subnodes = []
+ node = @node
+
+ begin
+ subnodes << node if node.name == @name
+ end while (node = node.parent).is_a?(Prism::ConstantPathNode)
+
+ if node.is_a?(Prism::ConstantReadNode) && node.name == @name
+ subnodes << node
+ end
+
+ # If we found only one sub-node whose name is equal to @name, use it
+ return nil if subnodes.size != 1
+ @node = subnodes.first
+ end
+
+ case @node.type
+
+ when :CALL, :QCALL
+ case @point_type
+ when :name
+ spot_call_for_name
+ when :args
+ spot_call_for_args
+ end
+
+ when :ATTRASGN
+ case @point_type
+ when :name
+ spot_attrasgn_for_name
+ when :args
+ spot_attrasgn_for_args
+ end
+
+ when :OPCALL
+ case @point_type
+ when :name
+ spot_opcall_for_name
+ when :args
+ spot_opcall_for_args
+ end
+
+ when :FCALL
+ case @point_type
+ when :name
+ spot_fcall_for_name
+ when :args
+ spot_fcall_for_args
+ end
+
+ when :VCALL
+ spot_vcall
+
+ when :OP_ASGN1
+ case @point_type
+ when :name
+ spot_op_asgn1_for_name
+ when :args
+ spot_op_asgn1_for_args
+ end
+
+ when :OP_ASGN2
+ case @point_type
+ when :name
+ spot_op_asgn2_for_name
+ when :args
+ spot_op_asgn2_for_args
+ end
+
+ when :CONST
+ spot_vcall
+
+ when :COLON2
+ spot_colon2
+
+ when :COLON3
+ spot_vcall
+
+ when :OP_CDECL
+ spot_op_cdecl
+
+ when :DEFN
+ raise NotImplementedError if @point_type != :name
+ spot_defn
+
+ when :DEFS
+ raise NotImplementedError if @point_type != :name
+ spot_defs
+
+ when :LAMBDA
+ spot_lambda
+
+ when :ITER
+ spot_iter
+
+ when :call_node
+ case @point_type
+ when :name
+ prism_spot_call_for_name
+ when :args
+ prism_spot_call_for_args
+ end
+
+ when :local_variable_operator_write_node
+ case @point_type
+ when :name
+ prism_spot_local_variable_operator_write_for_name
+ when :args
+ prism_spot_local_variable_operator_write_for_args
+ end
+
+ when :call_operator_write_node
+ case @point_type
+ when :name
+ prism_spot_call_operator_write_for_name
+ when :args
+ prism_spot_call_operator_write_for_args
+ end
+
+ when :index_operator_write_node
+ case @point_type
+ when :name
+ prism_spot_index_operator_write_for_name
+ when :args
+ prism_spot_index_operator_write_for_args
+ end
+
+ when :constant_read_node
+ prism_spot_constant_read
+
+ when :constant_path_node
+ prism_spot_constant_path
+
+ when :constant_path_operator_write_node
+ prism_spot_constant_path_operator_write
+
+ when :def_node
+ case @point_type
+ when :name
+ prism_spot_def_for_name
+ when :args
+ raise NotImplementedError
+ end
+
+ when :lambda_node
+ case @point_type
+ when :name
+ prism_spot_lambda_for_name
+ when :args
+ raise NotImplementedError
+ end
+
+ when :block_node
+ case @point_type
+ when :name
+ prism_spot_block_for_name
+ when :args
+ raise NotImplementedError
+ end
+
+ end
+
+ if @snippet && @beg_column && @end_column && @beg_column < @end_column
+ return {
+ first_lineno: @beg_lineno,
+ first_column: @beg_column,
+ last_lineno: @end_lineno,
+ last_column: @end_column,
+ snippet: @snippet,
+ script_lines: @node.script_lines,
+ }
+ else
+ return nil
+ end
+
+ rescue NonAscii
+ nil
+ end
+
+ private
+
+ # Example:
+ # x.foo
+ # ^^^^
+ # x.foo(42)
+ # ^^^^
+ # x&.foo
+ # ^^^^^
+ # x[42]
+ # ^^^^
+ # x += 1
+ # ^
+ def spot_call_for_name
+ nd_recv, mid, nd_args = @node.children
+ lineno = nd_recv.last_lineno
+ lines = @fetch[lineno, @node.last_lineno]
+ if mid == :[] && lines.match(/\G[\s)]*(\[(?:\s*\])?)/, nd_recv.last_column)
+ @beg_column = $~.begin(1)
+ @snippet = lines[/.*\n/]
+ @beg_lineno = @end_lineno = lineno
+ if nd_args
+ if nd_recv.last_lineno == nd_args.last_lineno && @snippet.match(/\s*\]/, nd_args.last_column)
+ @end_column = $~.end(0)
+ end
+ else
+ if lines.match(/\G[\s)]*?\[\s*\]/, nd_recv.last_column)
+ @end_column = $~.end(0)
+ end
+ end
+ elsif lines.match(/\G[\s)]*?(\&?\.)(\s*?)(#{ Regexp.quote(mid) }).*\n/, nd_recv.last_column)
+ lines = $` + $&
+ @beg_column = $~.begin($2.include?("\n") ? 3 : 1)
+ @end_column = $~.end(3)
+ if i = lines[..@beg_column].rindex("\n")
+ @beg_lineno = @end_lineno = lineno + lines[..@beg_column].count("\n")
+ @snippet = lines[i + 1..]
+ @beg_column -= i + 1
+ @end_column -= i + 1
+ else
+ @snippet = lines
+ @beg_lineno = @end_lineno = lineno
+ end
+ elsif mid.to_s =~ /\A\W+\z/ && lines.match(/\G\s*(#{ Regexp.quote(mid) })=.*\n/, nd_recv.last_column)
+ @snippet = $` + $&
+ @beg_lineno = @end_lineno = lineno
+ @beg_column = $~.begin(1)
+ @end_column = $~.end(1)
+ end
+ end
+
+ # Example:
+ # x.foo(42)
+ # ^^
+ # x[42]
+ # ^^
+ # x += 1
+ # ^
+ def spot_call_for_args
+ _nd_recv, _mid, nd_args = @node.children
+ if nd_args && nd_args.first_lineno == nd_args.last_lineno
+ fetch_line(nd_args.first_lineno)
+ @beg_column = nd_args.first_column
+ @end_column = nd_args.last_column
+ end
+ # TODO: support @arg
+ end
+
+ # Example:
+ # x.foo = 1
+ # ^^^^^^
+ # x[42] = 1
+ # ^^^^^^
+ def spot_attrasgn_for_name
+ nd_recv, mid, nd_args = @node.children
+ *nd_args, _nd_last_arg, _nil = nd_args.children
+ fetch_line(nd_recv.last_lineno)
+ if mid == :[]= && @snippet.match(/\G[\s)]*(\[)/, nd_recv.last_column)
+ @beg_column = $~.begin(1)
+ args_last_column = $~.end(0)
+ if nd_args.last && nd_recv.last_lineno == nd_args.last.last_lineno
+ args_last_column = nd_args.last.last_column
+ end
+ if @snippet.match(/[\s)]*\]\s*=/, args_last_column)
+ @end_column = $~.end(0)
+ end
+ elsif @snippet.match(/\G[\s)]*(\.\s*#{ Regexp.quote(mid.to_s.sub(/=\z/, "")) }\s*=)/, nd_recv.last_column)
+ @beg_column = $~.begin(1)
+ @end_column = $~.end(1)
+ end
+ end
+
+ # Example:
+ # x.foo = 1
+ # ^
+ # x[42] = 1
+ # ^^^^^^^
+ # x[] = 1
+ # ^^^^^
+ def spot_attrasgn_for_args
+ nd_recv, mid, nd_args = @node.children
+ fetch_line(nd_recv.last_lineno)
+ if mid == :[]= && @snippet.match(/\G[\s)]*\[/, nd_recv.last_column)
+ @beg_column = $~.end(0)
+ if nd_recv.last_lineno == nd_args.last_lineno
+ @end_column = nd_args.last_column
+ end
+ elsif nd_args && nd_args.first_lineno == nd_args.last_lineno
+ @beg_column = nd_args.first_column
+ @end_column = nd_args.last_column
+ end
+ # TODO: support @arg
+ end
+
+ # Example:
+ # x + 1
+ # ^
+ # +x
+ # ^
+ def spot_opcall_for_name
+ nd_recv, op, nd_arg = @node.children
+ fetch_line(nd_recv.last_lineno)
+ if nd_arg
+ # binary operator
+ if @snippet.match(/\G[\s)]*(#{ Regexp.quote(op) })/, nd_recv.last_column)
+ @beg_column = $~.begin(1)
+ @end_column = $~.end(1)
+ end
+ else
+ # unary operator
+ if @snippet[...nd_recv.first_column].match(/(#{ Regexp.quote(op.to_s.sub(/@\z/, "")) })\s*\(?\s*\z/)
+ @beg_column = $~.begin(1)
+ @end_column = $~.end(1)
+ end
+ end
+ end
+
+ # Example:
+ # x + 1
+ # ^
+ def spot_opcall_for_args
+ _nd_recv, _op, nd_arg = @node.children
+ if nd_arg && nd_arg.first_lineno == nd_arg.last_lineno
+ # binary operator
+ fetch_line(nd_arg.first_lineno)
+ @beg_column = nd_arg.first_column
+ @end_column = nd_arg.last_column
+ end
+ end
+
+ # Example:
+ # foo(42)
+ # ^^^
+ # foo 42
+ # ^^^
+ def spot_fcall_for_name
+ mid, _nd_args = @node.children
+ fetch_line(@node.first_lineno)
+ if @snippet.match(/(#{ Regexp.quote(mid) })/, @node.first_column)
+ @beg_column = $~.begin(1)
+ @end_column = $~.end(1)
+ end
+ end
+
+ # Example:
+ # foo(42)
+ # ^^
+ # foo 42
+ # ^^
+ def spot_fcall_for_args
+ _mid, nd_args = @node.children
+ if nd_args && nd_args.first_lineno == nd_args.last_lineno
+ fetch_line(nd_args.first_lineno)
+ @beg_column = nd_args.first_column
+ @end_column = nd_args.last_column
+ end
+ end
+
+ # Example:
+ # foo
+ # ^^^
+ def spot_vcall
+ if @node.first_lineno == @node.last_lineno
+ fetch_line(@node.last_lineno)
+ @beg_column = @node.first_column
+ @end_column = @node.last_column
+ end
+ end
+
+ # Example:
+ # x[1] += 42
+ # ^^^ (for [])
+ # x[1] += 42
+ # ^ (for +)
+ # x[1] += 42
+ # ^^^^^^ (for []=)
+ def spot_op_asgn1_for_name
+ nd_recv, op, nd_args, _nd_rhs = @node.children
+ fetch_line(nd_recv.last_lineno)
+ if @snippet.match(/\G[\s)]*(\[)/, nd_recv.last_column)
+ bracket_beg_column = $~.begin(1)
+ args_last_column = $~.end(0)
+ if nd_args && nd_recv.last_lineno == nd_args.last_lineno
+ args_last_column = nd_args.last_column
+ end
+ if @snippet.match(/\s*\](\s*)(#{ Regexp.quote(op) })=()/, args_last_column)
+ case @name
+ when :[], :[]=
+ @beg_column = bracket_beg_column
+ @end_column = $~.begin(@name == :[] ? 1 : 3)
+ when op
+ @beg_column = $~.begin(2)
+ @end_column = $~.end(2)
+ end
+ end
+ end
+ end
+
+ # Example:
+ # x[1] += 42
+ # ^^^^^^^^
+ def spot_op_asgn1_for_args
+ nd_recv, mid, nd_args, nd_rhs = @node.children
+ fetch_line(nd_recv.last_lineno)
+ if mid == :[]= && @snippet.match(/\G\s*\[/, nd_recv.last_column)
+ @beg_column = $~.end(0)
+ if nd_recv.last_lineno == nd_rhs.last_lineno
+ @end_column = nd_rhs.last_column
+ end
+ elsif nd_args && nd_args.first_lineno == nd_rhs.last_lineno
+ @beg_column = nd_args.first_column
+ @end_column = nd_rhs.last_column
+ end
+ # TODO: support @arg
+ end
+
+ # Example:
+ # x.foo += 42
+ # ^^^ (for foo)
+ # x.foo += 42
+ # ^ (for +)
+ # x.foo += 42
+ # ^^^^^^^ (for foo=)
+ def spot_op_asgn2_for_name
+ nd_recv, _qcall, attr, op, _nd_rhs = @node.children
+ fetch_line(nd_recv.last_lineno)
+ if @snippet.match(/\G[\s)]*(\.)\s*#{ Regexp.quote(attr) }()\s*(#{ Regexp.quote(op) })(=)/, nd_recv.last_column)
+ case @name
+ when attr
+ @beg_column = $~.begin(1)
+ @end_column = $~.begin(2)
+ when op
+ @beg_column = $~.begin(3)
+ @end_column = $~.end(3)
+ when :"#{ attr }="
+ @beg_column = $~.begin(1)
+ @end_column = $~.end(4)
+ end
+ end
+ end
+
+ # Example:
+ # x.foo += 42
+ # ^^
+ def spot_op_asgn2_for_args
+ _nd_recv, _qcall, _attr, _op, nd_rhs = @node.children
+ if nd_rhs.first_lineno == nd_rhs.last_lineno
+ fetch_line(nd_rhs.first_lineno)
+ @beg_column = nd_rhs.first_column
+ @end_column = nd_rhs.last_column
+ end
+ end
+
+ # Example:
+ # Foo::Bar
+ # ^^^^^
+ def spot_colon2
+ nd_parent, const = @node.children
+ if nd_parent.last_lineno == @node.last_lineno
+ fetch_line(nd_parent.last_lineno)
+ @beg_column = nd_parent.last_column
+ @end_column = @node.last_column
+ else
+ fetch_line(@node.last_lineno)
+ if @snippet[...@node.last_column].match(/#{ Regexp.quote(const) }\z/)
+ @beg_lineno = @end_lineno = @node.last_lineno
+ @beg_column = $~.begin(0)
+ @end_column = $~.end(0)
+ end
+ end
+ end
+
+ # Example:
+ # Foo::Bar += 1
+ # ^^^^^^^^
+ def spot_op_cdecl
+ nd_lhs, op, _nd_rhs = @node.children
+ *nd_parent_lhs, _const = nd_lhs.children
+ if @name == op
+ fetch_line(nd_lhs.last_lineno)
+ if @snippet.match(/\G\s*(#{ Regexp.quote(op) })=/, nd_lhs.last_column)
+ @beg_column = $~.begin(1)
+ @end_column = $~.end(1)
+ end
+ else
+ # constant access error
+ @end_column = nd_lhs.last_column
+ if nd_parent_lhs.empty? # example: ::C += 1
+ if nd_lhs.first_lineno == nd_lhs.last_lineno
+ fetch_line(nd_lhs.last_lineno)
+ @beg_column = nd_lhs.first_column
+ end
+ else # example: Foo::Bar::C += 1
+ if nd_parent_lhs.last.last_lineno == nd_lhs.last_lineno
+ fetch_line(nd_lhs.last_lineno)
+ @beg_column = nd_parent_lhs.last.last_column
+ end
+ end
+ end
+ end
+
+ # Example:
+ # def bar; end
+ # ^^^
+ def spot_defn
+ mid, = @node.children
+ fetch_line(@node.first_lineno)
+ if @snippet.match(/\Gdef\s+(#{ Regexp.quote(mid) }\b)/, @node.first_column)
+ @beg_column = $~.begin(1)
+ @end_column = $~.end(1)
+ end
+ end
+
+ # Example:
+ # def Foo.bar; end
+ # ^^^^
+ def spot_defs
+ nd_recv, mid, = @node.children
+ fetch_line(nd_recv.last_lineno)
+ if @snippet.match(/\G\s*(\.\s*#{ Regexp.quote(mid) }\b)/, nd_recv.last_column)
+ @beg_column = $~.begin(1)
+ @end_column = $~.end(1)
+ end
+ end
+
+ # Example:
+ # -> { ... }
+ # ^^
+ def spot_lambda
+ fetch_line(@node.first_lineno)
+ if @snippet.match(/\G->/, @node.first_column)
+ @beg_column = $~.begin(0)
+ @end_column = $~.end(0)
+ end
+ end
+
+ # Example:
+ # lambda { ... }
+ # ^
+ # define_method :foo do
+ # ^^
+ def spot_iter
+ _nd_fcall, nd_scope = @node.children
+ fetch_line(nd_scope.first_lineno)
+ if @snippet.match(/\G(?:do\b|\{)/, nd_scope.first_column)
+ @beg_column = $~.begin(0)
+ @end_column = $~.end(0)
+ end
+ end
+
+ def fetch_line(lineno)
+ @beg_lineno = @end_lineno = lineno
+ @snippet = @fetch[lineno]
+ end
+
+ # Take a location from the prism parser and set the necessary instance
+ # variables.
+ def prism_location(location)
+ @beg_lineno = location.start_line
+ @beg_column = location.start_column
+ @end_lineno = location.end_line
+ @end_column = location.end_column
+ @snippet = @fetch[@beg_lineno, @end_lineno]
+ end
+
+ # Example:
+ # x.foo
+ # ^^^^
+ # x.foo(42)
+ # ^^^^
+ # x&.foo
+ # ^^^^^
+ # x[42]
+ # ^^^^
+ # x.foo = 1
+ # ^^^^^^
+ # x[42] = 1
+ # ^^^^^^
+ # x + 1
+ # ^
+ # +x
+ # ^
+ # foo(42)
+ # ^^^
+ # foo 42
+ # ^^^
+ # foo
+ # ^^^
+ def prism_spot_call_for_name
+ # Explicitly turn off foo.() syntax because error_highlight expects this
+ # to not work.
+ return nil if @node.name == :call && @node.message_loc.nil?
+
+ location = @node.message_loc || @node.call_operator_loc || @node.location
+ location = @node.call_operator_loc.join(location) if @node.call_operator_loc&.start_line == location.start_line
+
+ # If the method name ends with "=" but the message does not, then this is
+ # a method call using the "attribute assignment" syntax
+ # (e.g., foo.bar = 1). In this case we need to go retrieve the = sign and
+ # add it to the location.
+ if (name = @node.name).end_with?("=") && !@node.message.end_with?("=")
+ location = location.adjoin("=")
+ end
+
+ prism_location(location)
+
+ if !name.end_with?("=") && !name.match?(/[[:alpha:]_\[]/)
+ # If the method name is an operator, then error_highlight only
+ # highlights the first line.
+ fetch_line(location.start_line)
+ end
+ end
+
+ # Example:
+ # x.foo(42)
+ # ^^
+ # x[42]
+ # ^^
+ # x.foo = 1
+ # ^
+ # x[42] = 1
+ # ^^^^^^^
+ # x[] = 1
+ # ^^^^^
+ # x + 1
+ # ^
+ # foo(42)
+ # ^^
+ # foo 42
+ # ^^
+ def prism_spot_call_for_args
+ # Disallow highlighting arguments if there are no arguments.
+ return if @node.arguments.nil?
+
+ # Explicitly turn off foo.() syntax because error_highlight expects this
+ # to not work.
+ return nil if @node.name == :call && @node.message_loc.nil?
+
+ if @node.name == :[]= && @node.opening == "[" && (@node.arguments&.arguments || []).length == 1
+ prism_location(@node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1).join(@node.arguments.location))
+ else
+ prism_location(@node.arguments.location)
+ end
+ end
+
+ # Example:
+ # x += 1
+ # ^
+ def prism_spot_local_variable_operator_write_for_name
+ prism_location(@node.binary_operator_loc.chop)
+ end
+
+ # Example:
+ # x += 1
+ # ^
+ def prism_spot_local_variable_operator_write_for_args
+ prism_location(@node.value.location)
+ end
+
+ # Example:
+ # x.foo += 42
+ # ^^^ (for foo)
+ # x.foo += 42
+ # ^ (for +)
+ # x.foo += 42
+ # ^^^^^^^ (for foo=)
+ def prism_spot_call_operator_write_for_name
+ if !@name.start_with?(/[[:alpha:]_]/)
+ prism_location(@node.binary_operator_loc.chop)
+ else
+ location = @node.message_loc
+ if @node.call_operator_loc.start_line == location.start_line
+ location = @node.call_operator_loc.join(location)
+ end
+
+ location = location.adjoin("=") if @name.end_with?("=")
+ prism_location(location)
+ end
+ end
+
+ # Example:
+ # x.foo += 42
+ # ^^
+ def prism_spot_call_operator_write_for_args
+ prism_location(@node.value.location)
+ end
+
+ # Example:
+ # x[1] += 42
+ # ^^^ (for [])
+ # x[1] += 42
+ # ^ (for +)
+ # x[1] += 42
+ # ^^^^^^ (for []=)
+ def prism_spot_index_operator_write_for_name
+ case @name
+ when :[]
+ prism_location(@node.opening_loc.join(@node.closing_loc))
+ when :[]=
+ prism_location(@node.opening_loc.join(@node.closing_loc).adjoin("="))
+ else
+ # Explicitly turn off foo[] += 1 syntax when the operator is not on
+ # the same line because error_highlight expects this to not work.
+ return nil if @node.binary_operator_loc.start_line != @node.opening_loc.start_line
+
+ prism_location(@node.binary_operator_loc.chop)
+ end
+ end
+
+ # Example:
+ # x[1] += 42
+ # ^^^^^^^^
+ def prism_spot_index_operator_write_for_args
+ opening_loc =
+ if @node.arguments.nil?
+ @node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1)
+ else
+ @node.arguments.location
+ end
+
+ prism_location(opening_loc.join(@node.value.location))
+ end
+
+ # Example:
+ # Foo
+ # ^^^
+ def prism_spot_constant_read
+ prism_location(@node.location)
+ end
+
+ # Example:
+ # Foo::Bar
+ # ^^^^^
+ def prism_spot_constant_path
+ if @node.parent && @node.parent.location.end_line == @node.location.end_line
+ fetch_line(@node.parent.location.end_line)
+ prism_location(@node.delimiter_loc.join(@node.name_loc))
+ else
+ fetch_line(@node.location.end_line)
+ location = @node.name_loc
+ location = @node.delimiter_loc.join(location) if @node.delimiter_loc.end_line == location.start_line
+ prism_location(location)
+ end
+ end
+
+ # Example:
+ # Foo::Bar += 1
+ # ^^^^^^^^
+ def prism_spot_constant_path_operator_write
+ if @name == (target = @node.target).name
+ prism_location(target.delimiter_loc.join(target.name_loc))
+ else
+ prism_location(@node.binary_operator_loc.chop)
+ end
+ end
+
+ # Example:
+ # def foo()
+ # ^^^
+ def prism_spot_def_for_name
+ location = @node.name_loc
+ location = @node.operator_loc.join(location) if @node.operator_loc
+ prism_location(location)
+ end
+
+ # Example:
+ # -> x, y { }
+ # ^^
+ def prism_spot_lambda_for_name
+ prism_location(@node.operator_loc)
+ end
+
+ # Example:
+ # lambda { }
+ # ^
+ # define_method :foo do |x, y|
+ # ^
+ def prism_spot_block_for_name
+ prism_location(@node.opening_loc)
+ end
+ end
+
+ private_constant :Spotter
+end
diff --git a/lib/error_highlight/core_ext.rb b/lib/error_highlight/core_ext.rb
new file mode 100644
index 0000000000..c3354f46cd
--- /dev/null
+++ b/lib/error_highlight/core_ext.rb
@@ -0,0 +1,76 @@
+require_relative "formatter"
+
+module ErrorHighlight
+ module CoreExt
+ private def generate_snippet
+ if ArgumentError === self && message =~ /\A(?:wrong number of arguments|missing keyword[s]?|unknown keyword[s]?|no keywords accepted)\b/
+ locs = self.backtrace_locations
+ return "" if locs.size < 2
+ callee_loc, caller_loc = locs
+ callee_spot = ErrorHighlight.spot(self, backtrace_location: callee_loc, point_type: :name)
+ caller_spot = ErrorHighlight.spot(self, backtrace_location: caller_loc, point_type: :name)
+ if caller_spot && callee_spot &&
+ caller_loc.path == callee_loc.path &&
+ caller_loc.lineno == callee_loc.lineno &&
+ caller_spot == callee_spot
+ callee_loc = callee_spot = nil
+ end
+ ret = +"\n"
+ [["caller", caller_loc, caller_spot], ["callee", callee_loc, callee_spot]].each do |header, loc, spot|
+ out = nil
+ if loc
+ out = " #{ header }: #{ loc.path }:#{ loc.lineno }"
+ if spot
+ _, _, snippet, highlight = ErrorHighlight.formatter.message_for(spot).lines
+ out += "\n | #{ snippet } #{ highlight }"
+ else
+ # do nothing
+ end
+ end
+ ret << "\n" + out if out
+ end
+ ret
+ else
+ spot = ErrorHighlight.spot(self)
+ return "" unless spot
+ return ErrorHighlight.formatter.message_for(spot)
+ end
+ end
+
+ if Exception.method_defined?(:detailed_message)
+ def detailed_message(highlight: false, error_highlight: true, **)
+ return super unless error_highlight
+ snippet = generate_snippet
+ if highlight
+ snippet = snippet.gsub(/.+/) { "\e[1m" + $& + "\e[m" }
+ end
+ super + snippet
+ end
+ else
+ # This is a marker to let `DidYouMean::Correctable#original_message` skip
+ # the following method definition of `to_s`.
+ # See https://github.com/ruby/did_you_mean/pull/152
+ SKIP_TO_S_FOR_SUPER_LOOKUP = true
+ private_constant :SKIP_TO_S_FOR_SUPER_LOOKUP
+
+ def to_s
+ msg = super
+ snippet = generate_snippet
+ if snippet != "" && !msg.include?(snippet)
+ msg + snippet
+ else
+ msg
+ end
+ end
+ end
+ end
+
+ NameError.prepend(CoreExt)
+
+ if Exception.method_defined?(:detailed_message)
+ # ErrorHighlight is enabled for TypeError and ArgumentError only when Exception#detailed_message is available.
+ # This is because changing ArgumentError#message is highly incompatible.
+ TypeError.prepend(CoreExt)
+ ArgumentError.prepend(CoreExt)
+ end
+end
diff --git a/lib/error_highlight/error_highlight.gemspec b/lib/error_highlight/error_highlight.gemspec
new file mode 100644
index 0000000000..edfc4b776f
--- /dev/null
+++ b/lib/error_highlight/error_highlight.gemspec
@@ -0,0 +1,27 @@
+# coding: utf-8
+lib = File.expand_path('../lib', __FILE__)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+begin
+ require_relative "lib/error_highlight/version"
+rescue LoadError # Fallback to load version file in ruby core repository
+ require_relative "version"
+end
+
+Gem::Specification.new do |spec|
+ spec.name = "error_highlight"
+ spec.version = ErrorHighlight::VERSION
+ spec.authors = ["Yusuke Endoh"]
+ spec.email = ["mame@ruby-lang.org"]
+
+ spec.summary = 'Shows a one-line code snippet with an underline in the error backtrace'
+ spec.description = 'The gem enhances Exception#message by adding a short explanation where the exception is raised'
+ spec.homepage = "https://github.com/ruby/error_highlight"
+
+ spec.license = "MIT"
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
+
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
+ end
+ spec.require_paths = ["lib"]
+end
diff --git a/lib/error_highlight/formatter.rb b/lib/error_highlight/formatter.rb
new file mode 100644
index 0000000000..d2fad9e75c
--- /dev/null
+++ b/lib/error_highlight/formatter.rb
@@ -0,0 +1,74 @@
+module ErrorHighlight
+ class DefaultFormatter
+ MIN_SNIPPET_WIDTH = 20
+
+ def self.message_for(spot)
+ # currently only a one-line code snippet is supported
+ return "" unless spot[:first_lineno] == spot[:last_lineno]
+
+ snippet = spot[:snippet]
+ first_column = spot[:first_column]
+ last_column = spot[:last_column]
+ ellipsis = "..."
+
+ # truncate snippet to fit in the viewport
+ if max_snippet_width && snippet.size > max_snippet_width
+ available_width = max_snippet_width - ellipsis.size
+ center = first_column - max_snippet_width / 2
+
+ visible_start = last_column < available_width ? 0 : [center, 0].max
+ visible_end = visible_start + max_snippet_width
+ visible_start = snippet.size - max_snippet_width if visible_end > snippet.size
+
+ prefix = visible_start.positive? ? ellipsis : ""
+ suffix = visible_end < snippet.size ? ellipsis : ""
+
+ snippet = prefix + snippet[(visible_start + prefix.size)...(visible_end - suffix.size)] + suffix
+ snippet << "\n" unless snippet.end_with?("\n")
+
+ first_column -= visible_start
+ last_column = [last_column - visible_start, snippet.size - 1].min
+ end
+
+ indent = snippet[0...first_column].gsub(/[^\t]/, " ")
+ marker = indent + "^" * (last_column - first_column)
+
+ "\n\n#{ snippet }#{ marker }"
+ end
+
+ def self.max_snippet_width
+ return if Ractor.current[:__error_highlight_max_snippet_width__] == :disabled
+
+ Ractor.current[:__error_highlight_max_snippet_width__] ||= terminal_width
+ end
+
+ def self.max_snippet_width=(width)
+ return Ractor.current[:__error_highlight_max_snippet_width__] = :disabled if width.nil?
+
+ width = width.to_i
+
+ if width < MIN_SNIPPET_WIDTH
+ warn "'max_snippet_width' adjusted to minimum value of #{MIN_SNIPPET_WIDTH}."
+ width = MIN_SNIPPET_WIDTH
+ end
+
+ Ractor.current[:__error_highlight_max_snippet_width__] = width
+ end
+
+ def self.terminal_width
+ # lazy load io/console to avoid loading it when 'max_snippet_width' is manually set
+ require "io/console"
+ $stderr.winsize[1] if $stderr.tty?
+ rescue LoadError, NoMethodError, SystemCallError
+ # skip truncation when terminal window size is unavailable
+ end
+ end
+
+ def self.formatter
+ Ractor.current[:__error_highlight_formatter__] || DefaultFormatter
+ end
+
+ def self.formatter=(formatter)
+ Ractor.current[:__error_highlight_formatter__] = formatter
+ end
+end
diff --git a/lib/error_highlight/version.rb b/lib/error_highlight/version.rb
new file mode 100644
index 0000000000..f0a5376b14
--- /dev/null
+++ b/lib/error_highlight/version.rb
@@ -0,0 +1,3 @@
+module ErrorHighlight
+ VERSION = "0.7.1"
+end
diff --git a/lib/fileutils.gemspec b/lib/fileutils.gemspec
new file mode 100644
index 0000000000..2603d664da
--- /dev/null
+++ b/lib/fileutils.gemspec
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+source_version = ["", "lib/"].find do |dir|
+ begin
+ break File.open(File.join(__dir__, "#{dir}fileutils.rb")) {|f|
+ f.gets("\n VERSION = ")
+ f.gets[/\s*"(.+)"/, 1]
+ }
+ rescue Errno::ENOENT
+ end
+end
+
+Gem::Specification.new do |s|
+ s.name = "fileutils"
+ s.version = source_version
+ s.summary = "Several file utility methods for copying, moving, removing, etc."
+ s.description = "Several file utility methods for copying, moving, removing, etc."
+
+ s.require_path = %w{lib}
+ s.files = ["COPYING", "BSDL", "README.md", "Rakefile", "fileutils.gemspec", "lib/fileutils.rb"]
+ s.required_ruby_version = ">= 2.5.0"
+
+ s.authors = ["Minero Aoki"]
+ s.email = [nil]
+ s.homepage = "https://github.com/ruby/fileutils"
+ s.licenses = ["Ruby", "BSD-2-Clause"]
+
+ s.metadata = {
+ "source_code_uri" => "https://github.com/ruby/fileutils"
+ }
+end
diff --git a/lib/fileutils.rb b/lib/fileutils.rb
index 56fb19beb2..0706e007ca 100644
--- a/lib/fileutils.rb
+++ b/lib/fileutils.rb
@@ -1,101 +1,199 @@
-#
-# = fileutils.rb
-#
-# Copyright (c) 2000-2007 Minero Aoki
-#
-# This program is free software.
-# You can distribute/modify this program under the same terms of ruby.
-#
-# == module FileUtils
-#
-# Namespace for several file utility methods for copying, moving, removing, etc.
-#
-# === Module Functions
-#
-# cd(dir, options)
-# cd(dir, options) {|dir| .... }
-# pwd()
-# mkdir(dir, options)
-# mkdir(list, options)
-# mkdir_p(dir, options)
-# mkdir_p(list, options)
-# rmdir(dir, options)
-# rmdir(list, options)
-# ln(old, new, options)
-# ln(list, destdir, options)
-# ln_s(old, new, options)
-# ln_s(list, destdir, options)
-# ln_sf(src, dest, options)
-# cp(src, dest, options)
-# cp(list, dir, options)
-# cp_r(src, dest, options)
-# cp_r(list, dir, options)
-# mv(src, dest, options)
-# mv(list, dir, options)
-# rm(list, options)
-# rm_r(list, options)
-# rm_rf(list, options)
-# install(src, dest, mode = <src's>, options)
-# chmod(mode, list, options)
-# chmod_R(mode, list, options)
-# chown(user, group, list, options)
-# chown_R(user, group, list, options)
-# touch(list, options)
+# frozen_string_literal: true
+
+begin
+ require 'rbconfig'
+rescue LoadError
+ # for make rjit-headers
+end
+
+# Namespace for file utility methods for copying, moving, removing, etc.
#
-# The <tt>options</tt> parameter is a hash of options, taken from the list
-# <tt>:force</tt>, <tt>:noop</tt>, <tt>:preserve</tt>, and <tt>:verbose</tt>.
-# <tt>:noop</tt> means that no changes are made. The other two are obvious.
-# Each method documents the options that it honours.
+# == What's Here
#
-# All methods that have the concept of a "source" file or directory can take
-# either one file or a list of files in that argument. See the method
-# documentation for examples.
+# First, what’s elsewhere. \Module \FileUtils:
#
-# There are some `low level' methods, which do not accept any option:
+# - Inherits from {class Object}[rdoc-ref:Object].
+# - Supplements {class File}[rdoc-ref:File]
+# (but is not included or extended there).
#
-# copy_entry(src, dest, preserve = false, dereference = false)
-# copy_file(src, dest, preserve = false, dereference = true)
-# copy_stream(srcstream, deststream)
-# remove_entry(path, force = false)
-# remove_entry_secure(path, force = false)
-# remove_file(path, force = false)
-# compare_file(path_a, path_b)
-# compare_stream(stream_a, stream_b)
-# uptodate?(file, cmp_list)
+# Here, module \FileUtils provides methods that are useful for:
+#
+# - {Creating}[rdoc-ref:FileUtils@Creating].
+# - {Deleting}[rdoc-ref:FileUtils@Deleting].
+# - {Querying}[rdoc-ref:FileUtils@Querying].
+# - {Setting}[rdoc-ref:FileUtils@Setting].
+# - {Comparing}[rdoc-ref:FileUtils@Comparing].
+# - {Copying}[rdoc-ref:FileUtils@Copying].
+# - {Moving}[rdoc-ref:FileUtils@Moving].
+# - {Options}[rdoc-ref:FileUtils@Options].
+#
+# === Creating
+#
+# - ::mkdir: Creates directories.
+# - ::mkdir_p, ::makedirs, ::mkpath: Creates directories,
+# also creating ancestor directories as needed.
+# - ::link_entry: Creates a hard link.
+# - ::ln, ::link: Creates hard links.
+# - ::ln_s, ::symlink: Creates symbolic links.
+# - ::ln_sf: Creates symbolic links, overwriting if necessary.
+# - ::ln_sr: Creates symbolic links relative to targets
+#
+# === Deleting
+#
+# - ::remove_dir: Removes a directory and its descendants.
+# - ::remove_entry: Removes an entry, including its descendants if it is a directory.
+# - ::remove_entry_secure: Like ::remove_entry, but removes securely.
+# - ::remove_file: Removes a file entry.
+# - ::rm, ::remove: Removes entries.
+# - ::rm_f, ::safe_unlink: Like ::rm, but removes forcibly.
+# - ::rm_r: Removes entries and their descendants.
+# - ::rm_rf, ::rmtree: Like ::rm_r, but removes forcibly.
+# - ::rmdir: Removes directories.
+#
+# === Querying
+#
+# - ::pwd, ::getwd: Returns the path to the working directory.
+# - ::uptodate?: Returns whether a given entry is newer than given other entries.
+#
+# === Setting
+#
+# - ::cd, ::chdir: Sets the working directory.
+# - ::chmod: Sets permissions for an entry.
+# - ::chmod_R: Sets permissions for an entry and its descendants.
+# - ::chown: Sets the owner and group for entries.
+# - ::chown_R: Sets the owner and group for entries and their descendants.
+# - ::touch: Sets modification and access times for entries,
+# creating if necessary.
+#
+# === Comparing
+#
+# - ::compare_file, ::cmp, ::identical?: Returns whether two entries are identical.
+# - ::compare_stream: Returns whether two streams are identical.
+#
+# === Copying
+#
+# - ::copy_entry: Recursively copies an entry.
+# - ::copy_file: Copies an entry.
+# - ::copy_stream: Copies a stream.
+# - ::cp, ::copy: Copies files.
+# - ::cp_lr: Recursively creates hard links.
+# - ::cp_r: Recursively copies files, retaining mode, owner, and group.
+# - ::install: Recursively copies files, optionally setting mode,
+# owner, and group.
+#
+# === Moving
+#
+# - ::mv, ::move: Moves entries.
+#
+# === Options
+#
+# - ::collect_method: Returns the names of methods that accept a given option.
+# - ::commands: Returns the names of methods that accept options.
+# - ::have_option?: Returns whether a given method accepts a given option.
+# - ::options: Returns all option names.
+# - ::options_of: Returns the names of the options for a given method.
+#
+# == Path Arguments
+#
+# Some methods in \FileUtils accept _path_ arguments,
+# which are interpreted as paths to filesystem entries:
+#
+# - If the argument is a string, that value is the path.
+# - If the argument has method +:to_path+, it is converted via that method.
+# - If the argument has method +:to_str+, it is converted via that method.
+#
+# == About the Examples
+#
+# Some examples here involve trees of file entries.
+# For these, we sometimes display trees using the
+# {tree command-line utility}[https://en.wikipedia.org/wiki/Tree_(command)],
+# which is a recursive directory-listing utility that produces
+# a depth-indented listing of files and directories.
+#
+# We use a helper method to launch the command and control the format:
+#
+# def tree(dirpath = '.')
+# command = "tree --noreport --charset=ascii #{dirpath}"
+# system(command)
+# end
+#
+# To illustrate:
+#
+# tree('src0')
+# # => src0
+# # |-- sub0
+# # | |-- src0.txt
+# # | `-- src1.txt
+# # `-- sub1
+# # |-- src2.txt
+# # `-- src3.txt
+#
+# == Avoiding the TOCTTOU Vulnerability
+#
+# For certain methods that recursively remove entries,
+# there is a potential vulnerability called the
+# {Time-of-check to time-of-use}[https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use],
+# or TOCTTOU, vulnerability that can exist when:
+#
+# - An ancestor directory of the entry at the target path is world writable;
+# such directories include <tt>/tmp</tt>.
+# - The directory tree at the target path includes:
+#
+# - A world-writable descendant directory.
+# - A symbolic link.
+#
+# To avoid that vulnerability, you can use this method to remove entries:
+#
+# - FileUtils.remove_entry_secure: removes recursively
+# if the target path points to a directory.
+#
+# Also available are these methods,
+# each of which calls \FileUtils.remove_entry_secure:
+#
+# - FileUtils.rm_r with keyword argument <tt>secure: true</tt>.
+# - FileUtils.rm_rf with keyword argument <tt>secure: true</tt>.
+#
+# Finally, this method for moving entries calls \FileUtils.remove_entry_secure
+# if the source and destination are on different file systems
+# (which means that the "move" is really a copy and remove):
+#
+# - FileUtils.mv with keyword argument <tt>secure: true</tt>.
+#
+# \Method \FileUtils.remove_entry_secure removes securely
+# by applying a special pre-process:
+#
+# - If the target path points to a directory, this method uses methods
+# {File#chown}[rdoc-ref:File#chown]
+# and {File#chmod}[rdoc-ref:File#chmod]
+# in removing directories.
+# - The owner of the target directory should be either the current process
+# or the super user (root).
+#
+# WARNING: You must ensure that *ALL* parent directories cannot be
+# moved by other untrusted users. For example, parent directories
+# should not be owned by untrusted users, and should not be world
+# writable except when the sticky bit is set.
+#
+# For details of this security vulnerability, see Perl cases:
+#
+# - {CVE-2005-0448}[https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-0448].
+# - {CVE-2004-0452}[https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2004-0452].
#
-# == module FileUtils::Verbose
-#
-# This module has all methods of FileUtils module, but it outputs messages
-# before acting. This equates to passing the <tt>:verbose</tt> flag to methods
-# in FileUtils.
-#
-# == module FileUtils::NoWrite
-#
-# This module has all methods of FileUtils module, but never changes
-# files/directories. This equates to passing the <tt>:noop</tt> flag to methods
-# in FileUtils.
-#
-# == module FileUtils::DryRun
-#
-# This module has all methods of FileUtils module, but never changes
-# files/directories. This equates to passing the <tt>:noop</tt> and
-# <tt>:verbose</tt> flags to methods in FileUtils.
-#
-
module FileUtils
+ # The version number.
+ VERSION = "1.8.0"
def self.private_module_function(name) #:nodoc:
module_function name
private_class_method name
end
- # This hash table holds command options.
- OPT_TABLE = {} #:nodoc: internal use only
-
#
- # Options: (none)
+ # Returns a string containing the path to the current directory:
+ #
+ # FileUtils.pwd # => "/rdoc/fileutils"
#
- # Returns the name of the current directory.
+ # Related: FileUtils.cd.
#
def pwd
Dir.pwd
@@ -105,42 +203,66 @@ module FileUtils
alias getwd pwd
module_function :getwd
+ # Changes the working directory to the given +dir+, which
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]:
+ #
+ # With no block given,
+ # changes the current directory to the directory at +dir+; returns zero:
+ #
+ # FileUtils.pwd # => "/rdoc/fileutils"
+ # FileUtils.cd('..')
+ # FileUtils.pwd # => "/rdoc"
+ # FileUtils.cd('fileutils')
+ #
+ # With a block given, changes the current directory to the directory
+ # at +dir+, calls the block with argument +dir+,
+ # and restores the original current directory; returns the block's value:
+ #
+ # FileUtils.pwd # => "/rdoc/fileutils"
+ # FileUtils.cd('..') { |arg| [arg, FileUtils.pwd] } # => ["..", "/rdoc"]
+ # FileUtils.pwd # => "/rdoc/fileutils"
+ #
+ # Keyword arguments:
+ #
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.cd('..')
+ # FileUtils.cd('fileutils')
+ #
+ # Output:
#
- # Options: verbose
- #
- # Changes the current directory to the directory +dir+.
- #
- # If this method is called with block, resumes to the old
- # working directory after the block execution finished.
- #
- # FileUtils.cd('/', :verbose => true) # chdir and report it
- #
- def cd(dir, options = {}, &block) # :yield: dir
- fu_check_options options, OPT_TABLE['cd']
- fu_output_message "cd #{dir}" if options[:verbose]
- Dir.chdir(dir, &block)
- fu_output_message 'cd -' if options[:verbose] and block
+ # cd ..
+ # cd fileutils
+ #
+ # Related: FileUtils.pwd.
+ #
+ def cd(dir, verbose: nil, &block) # :yield: dir
+ fu_output_message "cd #{dir}" if verbose
+ result = Dir.chdir(dir, &block)
+ fu_output_message 'cd -' if verbose and block
+ result
end
module_function :cd
alias chdir cd
module_function :chdir
- OPT_TABLE['cd'] =
- OPT_TABLE['chdir'] = [:verbose]
-
#
- # Options: (none)
- #
- # Returns true if +newer+ is newer than all +old_list+.
- # Non-existent files are older than any file.
- #
- # FileUtils.uptodate?('hello.o', %w(hello.c hello.h)) or \
- # system 'make hello.o'
- #
- def uptodate?(new, old_list, options = nil)
- raise ArgumentError, 'uptodate? does not accept any option' if options
-
+ # Returns +true+ if the file at path +new+
+ # is newer than all the files at paths in array +old_list+;
+ # +false+ otherwise.
+ #
+ # Argument +new+ and the elements of +old_list+
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]:
+ #
+ # FileUtils.uptodate?('Rakefile', ['Gemfile', 'README.md']) # => true
+ # FileUtils.uptodate?('Gemfile', ['Rakefile', 'README.md']) # => false
+ #
+ # A non-existent file is considered to be infinitely old.
+ #
+ # Related: FileUtils.touch.
+ #
+ def uptodate?(new, old_list)
return false unless File.exist?(new)
new_time = File.mtime(new)
old_list.each do |old|
@@ -152,70 +274,112 @@ module FileUtils
end
module_function :uptodate?
+ def remove_trailing_slash(dir) #:nodoc:
+ dir == '/' ? dir : dir.chomp(?/)
+ end
+ private_module_function :remove_trailing_slash
+
+ #
+ # Creates directories at the paths in the given +list+
+ # (a single path or an array of paths);
+ # returns +list+ if it is an array, <tt>[list]</tt> otherwise.
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # With no keyword arguments, creates a directory at each +path+ in +list+
+ # by calling: <tt>Dir.mkdir(path, mode)</tt>;
+ # see {Dir.mkdir}[rdoc-ref:Dir.mkdir]:
#
- # Options: mode noop verbose
- #
- # Creates one or more directories.
- #
- # FileUtils.mkdir 'test'
- # FileUtils.mkdir %w( tmp data )
- # FileUtils.mkdir 'notexist', :noop => true # Does not really create.
- # FileUtils.mkdir 'tmp', :mode => 0700
- #
- def mkdir(list, options = {})
- fu_check_options options, OPT_TABLE['mkdir']
+ # FileUtils.mkdir(%w[tmp0 tmp1]) # => ["tmp0", "tmp1"]
+ # FileUtils.mkdir('tmp4') # => ["tmp4"]
+ #
+ # Keyword arguments:
+ #
+ # - <tt>mode: <i>mode</i></tt> - also calls <tt>File.chmod(mode, path)</tt>;
+ # see {File.chmod}[rdoc-ref:File.chmod].
+ # - <tt>noop: true</tt> - does not create directories.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.mkdir(%w[tmp0 tmp1], verbose: true)
+ # FileUtils.mkdir(%w[tmp2 tmp3], mode: 0700, verbose: true)
+ #
+ # Output:
+ #
+ # mkdir tmp0 tmp1
+ # mkdir -m 700 tmp2 tmp3
+ #
+ # Raises an exception if any path points to an existing
+ # file or directory, or if for any reason a directory cannot be created.
+ #
+ # Related: FileUtils.mkdir_p.
+ #
+ def mkdir(list, mode: nil, noop: nil, verbose: nil)
list = fu_list(list)
- fu_output_message "mkdir #{options[:mode] ? ('-m %03o ' % options[:mode]) : ''}#{list.join ' '}" if options[:verbose]
- return if options[:noop]
+ fu_output_message "mkdir #{mode ? ('-m %03o ' % mode) : ''}#{list.join ' '}" if verbose
+ return if noop
list.each do |dir|
- fu_mkdir dir, options[:mode]
+ fu_mkdir dir, mode
end
end
module_function :mkdir
- OPT_TABLE['mkdir'] = [:mode, :noop, :verbose]
-
- #
- # Options: mode noop verbose
- #
- # Creates a directory and all its parent directories.
- # For example,
- #
- # FileUtils.mkdir_p '/usr/local/lib/ruby'
- #
- # causes to make following directories, if it does not exist.
- # * /usr
- # * /usr/local
- # * /usr/local/lib
- # * /usr/local/lib/ruby
- #
- # You can pass several directories at a time in a list.
- #
- def mkdir_p(list, options = {})
- fu_check_options options, OPT_TABLE['mkdir_p']
+ #
+ # Creates directories at the paths in the given +list+
+ # (a single path or an array of paths),
+ # also creating ancestor directories as needed;
+ # returns +list+ if it is an array, <tt>[list]</tt> otherwise.
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # With no keyword arguments, creates a directory at each +path+ in +list+,
+ # along with any needed ancestor directories,
+ # by calling: <tt>Dir.mkdir(path, mode)</tt>;
+ # see {Dir.mkdir}[rdoc-ref:Dir.mkdir]:
+ #
+ # FileUtils.mkdir_p(%w[tmp0/tmp1 tmp2/tmp3]) # => ["tmp0/tmp1", "tmp2/tmp3"]
+ # FileUtils.mkdir_p('tmp4/tmp5') # => ["tmp4/tmp5"]
+ #
+ # Keyword arguments:
+ #
+ # - <tt>mode: <i>mode</i></tt> - also calls <tt>File.chmod(mode, path)</tt>;
+ # see {File.chmod}[rdoc-ref:File.chmod].
+ # - <tt>noop: true</tt> - does not create directories.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.mkdir_p(%w[tmp0 tmp1], verbose: true)
+ # FileUtils.mkdir_p(%w[tmp2 tmp3], mode: 0700, verbose: true)
+ #
+ # Output:
+ #
+ # mkdir -p tmp0 tmp1
+ # mkdir -p -m 700 tmp2 tmp3
+ #
+ # Raises an exception if for any reason a directory cannot be created.
+ #
+ # FileUtils.mkpath and FileUtils.makedirs are aliases for FileUtils.mkdir_p.
+ #
+ # Related: FileUtils.mkdir.
+ #
+ def mkdir_p(list, mode: nil, noop: nil, verbose: nil)
list = fu_list(list)
- fu_output_message "mkdir -p #{options[:mode] ? ('-m %03o ' % options[:mode]) : ''}#{list.join ' '}" if options[:verbose]
- return *list if options[:noop]
+ fu_output_message "mkdir -p #{mode ? ('-m %03o ' % mode) : ''}#{list.join ' '}" if verbose
+ return *list if noop
- list.map {|path| path.sub(%r</\z>, '') }.each do |path|
- # optimize for the most common case
- begin
- fu_mkdir path, options[:mode]
- next
- rescue SystemCallError
- next if File.directory?(path)
- end
+ list.each do |item|
+ path = remove_trailing_slash(item)
stack = []
- until path == stack.last # dirname("/")=="/", dirname("C:/")=="C:/"
+ until File.directory?(path) || File.dirname(path) == path
stack.push path
path = File.dirname(path)
end
stack.reverse_each do |dir|
begin
- fu_mkdir dir, options[:mode]
- rescue SystemCallError => err
+ fu_mkdir dir, mode
+ rescue SystemCallError
raise unless File.directory?(dir)
end
end
@@ -230,12 +394,8 @@ module FileUtils
module_function :mkpath
module_function :makedirs
- OPT_TABLE['mkdir_p'] =
- OPT_TABLE['mkpath'] =
- OPT_TABLE['makedirs'] = [:mode, :noop, :verbose]
-
def fu_mkdir(path, mode) #:nodoc:
- path = path.sub(%r</\z>, '')
+ path = remove_trailing_slash(path)
if mode
Dir.mkdir path, mode
File.chmod mode, path
@@ -246,56 +406,119 @@ module FileUtils
private_module_function :fu_mkdir
#
- # Options: noop, verbose
- #
- # Removes one or more directories.
- #
- # FileUtils.rmdir 'somedir'
- # FileUtils.rmdir %w(somedir anydir otherdir)
- # # Does not really remove directory; outputs message.
- # FileUtils.rmdir 'somedir', :verbose => true, :noop => true
- #
- def rmdir(list, options = {})
- fu_check_options options, OPT_TABLE['rmdir']
+ # Removes directories at the paths in the given +list+
+ # (a single path or an array of paths);
+ # returns +list+, if it is an array, <tt>[list]</tt> otherwise.
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # With no keyword arguments, removes the directory at each +path+ in +list+,
+ # by calling: <tt>Dir.rmdir(path)</tt>;
+ # see {Dir.rmdir}[rdoc-ref:Dir.rmdir]:
+ #
+ # FileUtils.rmdir(%w[tmp0/tmp1 tmp2/tmp3]) # => ["tmp0/tmp1", "tmp2/tmp3"]
+ # FileUtils.rmdir('tmp4/tmp5') # => ["tmp4/tmp5"]
+ #
+ # Keyword arguments:
+ #
+ # - <tt>parents: true</tt> - removes successive ancestor directories
+ # if empty.
+ # - <tt>noop: true</tt> - does not remove directories.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.rmdir(%w[tmp0/tmp1 tmp2/tmp3], parents: true, verbose: true)
+ # FileUtils.rmdir('tmp4/tmp5', parents: true, verbose: true)
+ #
+ # Output:
+ #
+ # rmdir -p tmp0/tmp1 tmp2/tmp3
+ # rmdir -p tmp4/tmp5
+ #
+ # Raises an exception if a directory does not exist
+ # or if for any reason a directory cannot be removed.
+ #
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
+ #
+ def rmdir(list, parents: nil, noop: nil, verbose: nil)
list = fu_list(list)
- fu_output_message "rmdir #{list.join ' '}" if options[:verbose]
- return if options[:noop]
+ fu_output_message "rmdir #{parents ? '-p ' : ''}#{list.join ' '}" if verbose
+ return if noop
list.each do |dir|
- Dir.rmdir dir.sub(%r</\z>, '')
+ Dir.rmdir(dir = remove_trailing_slash(dir))
+ if parents
+ begin
+ until (parent = File.dirname(dir)) == '.' or parent == dir
+ dir = parent
+ Dir.rmdir(dir)
+ end
+ rescue Errno::ENOTEMPTY, Errno::EEXIST, Errno::ENOENT
+ end
+ end
end
end
module_function :rmdir
- OPT_TABLE['rmdir'] = [:noop, :verbose]
-
- #
- # Options: force noop verbose
- #
- # <b><tt>ln(old, new, options = {})</tt></b>
- #
- # Creates a hard link +new+ which points to +old+.
- # If +new+ already exists and it is a directory, creates a link +new/old+.
- # If +new+ already exists and it is not a directory, raises Errno::EEXIST.
- # But if :force option is set, overwrite +new+.
- #
- # FileUtils.ln 'gcc', 'cc', :verbose => true
- # FileUtils.ln '/usr/bin/emacs21', '/usr/bin/emacs'
- #
- # <b><tt>ln(list, destdir, options = {})</tt></b>
- #
- # Creates several hard links in a directory, with each one pointing to the
- # item in +list+. If +destdir+ is not a directory, raises Errno::ENOTDIR.
- #
- # include FileUtils
- # cd '/sbin'
- # FileUtils.ln %w(cp mv mkdir), '/bin' # Now /sbin/cp and /bin/cp are linked.
- #
- def ln(src, dest, options = {})
- fu_check_options options, OPT_TABLE['ln']
- fu_output_message "ln#{options[:force] ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose]
- return if options[:noop]
+ # Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link].
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # When +src+ is the path to an existing file
+ # and +dest+ is the path to a non-existent file,
+ # creates a hard link at +dest+ pointing to +src+; returns zero:
+ #
+ # Dir.children('tmp0/') # => ["t.txt"]
+ # Dir.children('tmp1/') # => []
+ # FileUtils.ln('tmp0/t.txt', 'tmp1/t.lnk') # => 0
+ # Dir.children('tmp1/') # => ["t.lnk"]
+ #
+ # When +src+ is the path to an existing file
+ # and +dest+ is the path to an existing directory,
+ # creates a hard link at <tt>dest/src</tt> pointing to +src+; returns zero:
+ #
+ # Dir.children('tmp2') # => ["t.dat"]
+ # Dir.children('tmp3') # => []
+ # FileUtils.ln('tmp2/t.dat', 'tmp3') # => 0
+ # Dir.children('tmp3') # => ["t.dat"]
+ #
+ # When +src+ is an array of paths to existing files
+ # and +dest+ is the path to an existing directory,
+ # then for each path +target+ in +src+,
+ # creates a hard link at <tt>dest/target</tt> pointing to +target+;
+ # returns +src+:
+ #
+ # Dir.children('tmp4/') # => []
+ # FileUtils.ln(['tmp0/t.txt', 'tmp2/t.dat'], 'tmp4/') # => ["tmp0/t.txt", "tmp2/t.dat"]
+ # Dir.children('tmp4/') # => ["t.dat", "t.txt"]
+ #
+ # Keyword arguments:
+ #
+ # - <tt>force: true</tt> - overwrites +dest+ if it exists.
+ # - <tt>noop: true</tt> - does not create links.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.ln('tmp0/t.txt', 'tmp1/t.lnk', verbose: true)
+ # FileUtils.ln('tmp2/t.dat', 'tmp3', verbose: true)
+ # FileUtils.ln(['tmp0/t.txt', 'tmp2/t.dat'], 'tmp4/', verbose: true)
+ #
+ # Output:
+ #
+ # ln tmp0/t.txt tmp1/t.lnk
+ # ln tmp2/t.dat tmp3
+ # ln tmp0/t.txt tmp2/t.dat tmp4/
+ #
+ # Raises an exception if +dest+ is the path to an existing file
+ # and keyword argument +force+ is not +true+.
+ #
+ # Related: FileUtils.link_entry (has different options).
+ #
+ def ln(src, dest, force: nil, noop: nil, verbose: nil)
+ fu_output_message "ln#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose
+ return if noop
fu_each_src_dest0(src, dest) do |s,d|
- remove_file d, true if options[:force]
+ remove_file d, true if force
File.link s, d
end
end
@@ -304,37 +527,192 @@ module FileUtils
alias link ln
module_function :link
- OPT_TABLE['ln'] =
- OPT_TABLE['link'] = [:force, :noop, :verbose]
-
- #
- # Options: force noop verbose
- #
- # <b><tt>ln_s(old, new, options = {})</tt></b>
- #
- # Creates a symbolic link +new+ which points to +old+. If +new+ already
- # exists and it is a directory, creates a symbolic link +new/old+. If +new+
- # already exists and it is not a directory, raises Errno::EEXIST. But if
- # :force option is set, overwrite +new+.
- #
- # FileUtils.ln_s '/usr/bin/ruby', '/usr/local/bin/ruby'
- # FileUtils.ln_s 'verylongsourcefilename.c', 'c', :force => true
- #
- # <b><tt>ln_s(list, destdir, options = {})</tt></b>
- #
- # Creates several symbolic links in a directory, with each one pointing to the
- # item in +list+. If +destdir+ is not a directory, raises Errno::ENOTDIR.
- #
- # If +destdir+ is not a directory, raises Errno::ENOTDIR.
- #
- # FileUtils.ln_s Dir.glob('bin/*.rb'), '/home/aamine/bin'
- #
- def ln_s(src, dest, options = {})
- fu_check_options options, OPT_TABLE['ln_s']
- fu_output_message "ln -s#{options[:force] ? 'f' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose]
- return if options[:noop]
- fu_each_src_dest0(src, dest) do |s,d|
- remove_file d, true if options[:force]
+ # Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link].
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # If +src+ is the path to a directory and +dest+ does not exist,
+ # creates links +dest+ and descendents pointing to +src+ and its descendents:
+ #
+ # tree('src0')
+ # # => src0
+ # # |-- sub0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- sub1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # File.exist?('dest0') # => false
+ # FileUtils.cp_lr('src0', 'dest0')
+ # tree('dest0')
+ # # => dest0
+ # # |-- sub0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- sub1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ #
+ # If +src+ and +dest+ are both paths to directories,
+ # creates links <tt>dest/src</tt> and descendents
+ # pointing to +src+ and its descendents:
+ #
+ # tree('src1')
+ # # => src1
+ # # |-- sub0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- sub1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # FileUtils.mkdir('dest1')
+ # FileUtils.cp_lr('src1', 'dest1')
+ # tree('dest1')
+ # # => dest1
+ # # `-- src1
+ # # |-- sub0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- sub1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ #
+ # If +src+ is an array of paths to entries and +dest+ is the path to a directory,
+ # for each path +filepath+ in +src+, creates a link at <tt>dest/filepath</tt>
+ # pointing to that path:
+ #
+ # tree('src2')
+ # # => src2
+ # # |-- sub0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- sub1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # FileUtils.mkdir('dest2')
+ # FileUtils.cp_lr(['src2/sub0', 'src2/sub1'], 'dest2')
+ # tree('dest2')
+ # # => dest2
+ # # |-- sub0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- sub1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ #
+ # Keyword arguments:
+ #
+ # - <tt>dereference_root: false</tt> - if +src+ is a symbolic link,
+ # does not dereference it.
+ # - <tt>noop: true</tt> - does not create links.
+ # - <tt>remove_destination: true</tt> - removes +dest+ before creating links.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.cp_lr('src0', 'dest0', noop: true, verbose: true)
+ # FileUtils.cp_lr('src1', 'dest1', noop: true, verbose: true)
+ # FileUtils.cp_lr(['src2/sub0', 'src2/sub1'], 'dest2', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # cp -lr src0 dest0
+ # cp -lr src1 dest1
+ # cp -lr src2/sub0 src2/sub1 dest2
+ #
+ # Raises an exception if +dest+ is the path to an existing file or directory
+ # and keyword argument <tt>remove_destination: true</tt> is not given.
+ #
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
+ #
+ def cp_lr(src, dest, noop: nil, verbose: nil,
+ dereference_root: true, remove_destination: false)
+ fu_output_message "cp -lr#{remove_destination ? ' --remove-destination' : ''} #{[src,dest].flatten.join ' '}" if verbose
+ return if noop
+ fu_each_src_dest(src, dest) do |s, d|
+ link_entry s, d, dereference_root, remove_destination
+ end
+ end
+ module_function :cp_lr
+
+ # Creates {symbolic links}[https://en.wikipedia.org/wiki/Symbolic_link].
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # If +src+ is the path to an existing file:
+ #
+ # - When +dest+ is the path to a non-existent file,
+ # creates a symbolic link at +dest+ pointing to +src+:
+ #
+ # FileUtils.touch('src0.txt')
+ # File.exist?('dest0.txt') # => false
+ # FileUtils.ln_s('src0.txt', 'dest0.txt')
+ # File.symlink?('dest0.txt') # => true
+ #
+ # - When +dest+ is the path to an existing file,
+ # creates a symbolic link at +dest+ pointing to +src+
+ # if and only if keyword argument <tt>force: true</tt> is given
+ # (raises an exception otherwise):
+ #
+ # FileUtils.touch('src1.txt')
+ # FileUtils.touch('dest1.txt')
+ # FileUtils.ln_s('src1.txt', 'dest1.txt', force: true)
+ # FileTest.symlink?('dest1.txt') # => true
+ #
+ # FileUtils.ln_s('src1.txt', 'dest1.txt') # Raises Errno::EEXIST.
+ #
+ # If +dest+ is the path to a directory,
+ # creates a symbolic link at <tt>dest/src</tt> pointing to +src+:
+ #
+ # FileUtils.touch('src2.txt')
+ # FileUtils.mkdir('destdir2')
+ # FileUtils.ln_s('src2.txt', 'destdir2')
+ # File.symlink?('destdir2/src2.txt') # => true
+ #
+ # If +src+ is an array of paths to existing files and +dest+ is a directory,
+ # for each child +child+ in +src+ creates a symbolic link <tt>dest/child</tt>
+ # pointing to +child+:
+ #
+ # FileUtils.mkdir('srcdir3')
+ # FileUtils.touch('srcdir3/src0.txt')
+ # FileUtils.touch('srcdir3/src1.txt')
+ # FileUtils.mkdir('destdir3')
+ # FileUtils.ln_s(['srcdir3/src0.txt', 'srcdir3/src1.txt'], 'destdir3')
+ # File.symlink?('destdir3/src0.txt') # => true
+ # File.symlink?('destdir3/src1.txt') # => true
+ #
+ # Keyword arguments:
+ #
+ # - <tt>force: true</tt> - overwrites +dest+ if it exists.
+ # - <tt>relative: false</tt> - create links relative to +dest+.
+ # - <tt>noop: true</tt> - does not create links.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.ln_s('src0.txt', 'dest0.txt', noop: true, verbose: true)
+ # FileUtils.ln_s('src1.txt', 'destdir1', noop: true, verbose: true)
+ # FileUtils.ln_s('src2.txt', 'dest2.txt', force: true, noop: true, verbose: true)
+ # FileUtils.ln_s(['srcdir3/src0.txt', 'srcdir3/src1.txt'], 'destdir3', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # ln -s src0.txt dest0.txt
+ # ln -s src1.txt destdir1
+ # ln -sf src2.txt dest2.txt
+ # ln -s srcdir3/src0.txt srcdir3/src1.txt destdir3
+ #
+ # Related: FileUtils.ln_sf.
+ #
+ def ln_s(src, dest, force: nil, relative: false, target_directory: true, noop: nil, verbose: nil)
+ if relative
+ return ln_sr(src, dest, force: force, target_directory: target_directory, noop: noop, verbose: verbose)
+ end
+ fu_output_message "ln -s#{force ? 'f' : ''}#{
+ target_directory ? '' : 'T'} #{[src,dest].flatten.join ' '}" if verbose
+ return if noop
+ fu_each_src_dest0(src, dest, target_directory) do |s,d|
+ remove_file d, true if force
File.symlink s, d
end
end
@@ -343,44 +721,157 @@ module FileUtils
alias symlink ln_s
module_function :symlink
- OPT_TABLE['ln_s'] =
- OPT_TABLE['symlink'] = [:force, :noop, :verbose]
-
+ # Like FileUtils.ln_s, but always with keyword argument <tt>force: true</tt> given.
#
- # Options: noop verbose
- #
- # Same as
- # #ln_s(src, dest, :force)
- #
- def ln_sf(src, dest, options = {})
- fu_check_options options, OPT_TABLE['ln_sf']
- options = options.dup
- options[:force] = true
- ln_s src, dest, options
+ def ln_sf(src, dest, noop: nil, verbose: nil)
+ ln_s src, dest, force: true, noop: noop, verbose: verbose
end
module_function :ln_sf
- OPT_TABLE['ln_sf'] = [:noop, :verbose]
+ # Like FileUtils.ln_s, but create links relative to +dest+.
+ #
+ def ln_sr(src, dest, target_directory: true, force: nil, noop: nil, verbose: nil)
+ cmd = "ln -s#{force ? 'f' : ''}#{target_directory ? '' : 'T'}" if verbose
+ fu_each_src_dest0(src, dest, target_directory) do |s,d|
+ if target_directory
+ parent = File.dirname(d)
+ destdirs = fu_split_path(parent)
+ real_ddirs = fu_split_path(File.realpath(parent))
+ else
+ destdirs ||= fu_split_path(dest)
+ real_ddirs ||= fu_split_path(File.realdirpath(dest))
+ end
+ srcdirs = fu_split_path(s)
+ i = fu_common_components(srcdirs, destdirs)
+ n = destdirs.size - i
+ n -= 1 unless target_directory
+ link1 = fu_clean_components(*Array.new([n, 0].max, '..'), *srcdirs[i..-1])
+ begin
+ real_sdirs = fu_split_path(File.realdirpath(s)) rescue nil
+ rescue
+ else
+ i = fu_common_components(real_sdirs, real_ddirs)
+ n = real_ddirs.size - i
+ n -= 1 unless target_directory
+ link2 = fu_clean_components(*Array.new([n, 0].max, '..'), *real_sdirs[i..-1])
+ link1 = link2 if link1.size > link2.size
+ end
+ s = File.join(link1)
+ fu_output_message [cmd, s, d].flatten.join(' ') if verbose
+ next if noop
+ remove_file d, true if force
+ File.symlink s, d
+ end
+ end
+ module_function :ln_sr
+ # Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link]; returns +nil+.
+ #
+ # Arguments +src+ and +dest+
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
#
- # Options: preserve noop verbose
+ # If +src+ is the path to a file and +dest+ does not exist,
+ # creates a hard link at +dest+ pointing to +src+:
#
- # Copies a file content +src+ to +dest+. If +dest+ is a directory,
- # copies +src+ to +dest/src+.
+ # FileUtils.touch('src0.txt')
+ # File.exist?('dest0.txt') # => false
+ # FileUtils.link_entry('src0.txt', 'dest0.txt')
+ # File.file?('dest0.txt') # => true
#
- # If +src+ is a list of files, then +dest+ must be a directory.
+ # If +src+ is the path to a directory and +dest+ does not exist,
+ # recursively creates hard links at +dest+ pointing to paths in +src+:
#
- # FileUtils.cp 'eval.c', 'eval.c.org'
- # FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6'
- # FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6', :verbose => true
- # FileUtils.cp 'symlink', 'dest' # copy content, "dest" is not a symlink
- #
- def cp(src, dest, options = {})
- fu_check_options options, OPT_TABLE['cp']
- fu_output_message "cp#{options[:preserve] ? ' -p' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose]
- return if options[:noop]
+ # FileUtils.mkdir_p(['src1/dir0', 'src1/dir1'])
+ # src_file_paths = [
+ # 'src1/dir0/t0.txt',
+ # 'src1/dir0/t1.txt',
+ # 'src1/dir1/t2.txt',
+ # 'src1/dir1/t3.txt',
+ # ]
+ # FileUtils.touch(src_file_paths)
+ # File.directory?('dest1') # => true
+ # FileUtils.link_entry('src1', 'dest1')
+ # File.file?('dest1/dir0/t0.txt') # => true
+ # File.file?('dest1/dir0/t1.txt') # => true
+ # File.file?('dest1/dir1/t2.txt') # => true
+ # File.file?('dest1/dir1/t3.txt') # => true
+ #
+ # Optional arguments:
+ #
+ # - +dereference_root+ - dereferences +src+ if it is a symbolic link (+false+ by default).
+ # - +remove_destination+ - removes +dest+ before creating links (+false+ by default).
+ #
+ # Raises an exception if +dest+ is the path to an existing file or directory
+ # and optional argument +remove_destination+ is not given.
+ #
+ # Related: FileUtils.ln (has different options).
+ #
+ def link_entry(src, dest, dereference_root = false, remove_destination = false)
+ Entry_.new(src, nil, dereference_root).traverse do |ent|
+ destent = Entry_.new(dest, ent.rel, false)
+ File.unlink destent.path if remove_destination && File.file?(destent.path)
+ ent.link destent.path
+ end
+ end
+ module_function :link_entry
+
+ # Copies files.
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # If +src+ is the path to a file and +dest+ is not the path to a directory,
+ # copies +src+ to +dest+:
+ #
+ # FileUtils.touch('src0.txt')
+ # File.exist?('dest0.txt') # => false
+ # FileUtils.cp('src0.txt', 'dest0.txt')
+ # File.file?('dest0.txt') # => true
+ #
+ # If +src+ is the path to a file and +dest+ is the path to a directory,
+ # copies +src+ to <tt>dest/src</tt>:
+ #
+ # FileUtils.touch('src1.txt')
+ # FileUtils.mkdir('dest1')
+ # FileUtils.cp('src1.txt', 'dest1')
+ # File.file?('dest1/src1.txt') # => true
+ #
+ # If +src+ is an array of paths to files and +dest+ is the path to a directory,
+ # copies from each +src+ to +dest+:
+ #
+ # src_file_paths = ['src2.txt', 'src2.dat']
+ # FileUtils.touch(src_file_paths)
+ # FileUtils.mkdir('dest2')
+ # FileUtils.cp(src_file_paths, 'dest2')
+ # File.file?('dest2/src2.txt') # => true
+ # File.file?('dest2/src2.dat') # => true
+ #
+ # Keyword arguments:
+ #
+ # - <tt>preserve: true</tt> - preserves file times.
+ # - <tt>noop: true</tt> - does not copy files.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.cp('src0.txt', 'dest0.txt', noop: true, verbose: true)
+ # FileUtils.cp('src1.txt', 'dest1', noop: true, verbose: true)
+ # FileUtils.cp(src_file_paths, 'dest2', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # cp src0.txt dest0.txt
+ # cp src1.txt dest1
+ # cp src2.txt src2.dat dest2
+ #
+ # Raises an exception if +src+ is a directory.
+ #
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
+ #
+ def cp(src, dest, preserve: nil, noop: nil, verbose: nil)
+ fu_output_message "cp#{preserve ? ' -p' : ''} #{[src,dest].flatten.join ' '}" if verbose
+ return if noop
fu_each_src_dest(src, dest) do |s, d|
- copy_file s, d, options[:preserve]
+ copy_file s, d, preserve
end
end
module_function :cp
@@ -388,74 +879,196 @@ module FileUtils
alias copy cp
module_function :copy
- OPT_TABLE['cp'] =
- OPT_TABLE['copy'] = [:preserve, :noop, :verbose]
-
- #
- # Options: preserve noop verbose dereference_root remove_destination
- #
- # Copies +src+ to +dest+. If +src+ is a directory, this method copies
- # all its contents recursively. If +dest+ is a directory, copies
- # +src+ to +dest/src+.
- #
- # +src+ can be a list of files.
- #
- # # Installing ruby library "mylib" under the site_ruby
- # FileUtils.rm_r site_ruby + '/mylib', :force
- # FileUtils.cp_r 'lib/', site_ruby + '/mylib'
- #
- # # Examples of copying several files to target directory.
- # FileUtils.cp_r %w(mail.rb field.rb debug/), site_ruby + '/tmail'
- # FileUtils.cp_r Dir.glob('*.rb'), '/home/aamine/lib/ruby', :noop => true, :verbose => true
- #
- # # If you want to copy all contents of a directory instead of the
- # # directory itself, c.f. src/x -> dest/x, src/y -> dest/y,
- # # use following code.
- # FileUtils.cp_r 'src/.', 'dest' # cp_r('src', 'dest') makes src/dest,
- # # but this doesn't.
- #
- def cp_r(src, dest, options = {})
- fu_check_options options, OPT_TABLE['cp_r']
- fu_output_message "cp -r#{options[:preserve] ? 'p' : ''}#{options[:remove_destination] ? ' --remove-destination' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose]
- return if options[:noop]
+ # Recursively copies files.
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # The mode, owner, and group are retained in the copy;
+ # to change those, use FileUtils.install instead.
+ #
+ # If +src+ is the path to a file and +dest+ is not the path to a directory,
+ # copies +src+ to +dest+:
+ #
+ # FileUtils.touch('src0.txt')
+ # File.exist?('dest0.txt') # => false
+ # FileUtils.cp_r('src0.txt', 'dest0.txt')
+ # File.file?('dest0.txt') # => true
+ #
+ # If +src+ is the path to a file and +dest+ is the path to a directory,
+ # copies +src+ to <tt>dest/src</tt>:
+ #
+ # FileUtils.touch('src1.txt')
+ # FileUtils.mkdir('dest1')
+ # FileUtils.cp_r('src1.txt', 'dest1')
+ # File.file?('dest1/src1.txt') # => true
+ #
+ # If +src+ is the path to a directory and +dest+ does not exist,
+ # recursively copies +src+ to +dest+:
+ #
+ # tree('src2')
+ # # => src2
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # FileUtils.exist?('dest2') # => false
+ # FileUtils.cp_r('src2', 'dest2')
+ # tree('dest2')
+ # # => dest2
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ #
+ # If +src+ and +dest+ are paths to directories,
+ # recursively copies +src+ to <tt>dest/src</tt>:
+ #
+ # tree('src3')
+ # # => src3
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # FileUtils.mkdir('dest3')
+ # FileUtils.cp_r('src3', 'dest3')
+ # tree('dest3')
+ # # => dest3
+ # # `-- src3
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ #
+ # If +src+ is an array of paths and +dest+ is a directory,
+ # recursively copies from each path in +src+ to +dest+;
+ # the paths in +src+ may point to files and/or directories.
+ #
+ # Keyword arguments:
+ #
+ # - <tt>dereference_root: false</tt> - if +src+ is a symbolic link,
+ # does not dereference it.
+ # - <tt>noop: true</tt> - does not copy files.
+ # - <tt>preserve: true</tt> - preserves file times.
+ # - <tt>remove_destination: true</tt> - removes +dest+ before copying files.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.cp_r('src0.txt', 'dest0.txt', noop: true, verbose: true)
+ # FileUtils.cp_r('src1.txt', 'dest1', noop: true, verbose: true)
+ # FileUtils.cp_r('src2', 'dest2', noop: true, verbose: true)
+ # FileUtils.cp_r('src3', 'dest3', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # cp -r src0.txt dest0.txt
+ # cp -r src1.txt dest1
+ # cp -r src2 dest2
+ # cp -r src3 dest3
+ #
+ # Raises an exception of +src+ is the path to a directory
+ # and +dest+ is the path to a file.
+ #
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
+ #
+ def cp_r(src, dest, preserve: nil, noop: nil, verbose: nil,
+ dereference_root: true, remove_destination: nil)
+ fu_output_message "cp -r#{preserve ? 'p' : ''}#{remove_destination ? ' --remove-destination' : ''} #{[src,dest].flatten.join ' '}" if verbose
+ return if noop
fu_each_src_dest(src, dest) do |s, d|
- copy_entry s, d, options[:preserve], options[:dereference_root], options[:remove_destination]
+ copy_entry s, d, preserve, dereference_root, remove_destination
end
end
module_function :cp_r
- OPT_TABLE['cp_r'] = [:preserve, :noop, :verbose,
- :dereference_root, :remove_destination]
-
+ # Recursively copies files from +src+ to +dest+.
+ #
+ # Arguments +src+ and +dest+
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
#
- # Copies a file system entry +src+ to +dest+.
- # If +src+ is a directory, this method copies its contents recursively.
- # This method preserves file types, c.f. symlink, directory...
- # (FIFO, device files and etc. are not supported yet)
+ # If +src+ is the path to a file, copies +src+ to +dest+:
#
- # Both of +src+ and +dest+ must be a path name.
- # +src+ must exist, +dest+ must not exist.
+ # FileUtils.touch('src0.txt')
+ # File.exist?('dest0.txt') # => false
+ # FileUtils.copy_entry('src0.txt', 'dest0.txt')
+ # File.file?('dest0.txt') # => true
#
- # If +preserve+ is true, this method preserves owner, group, permissions
- # and modified time.
+ # If +src+ is a directory, recursively copies +src+ to +dest+:
#
- # If +dereference_root+ is true, this method dereference tree root.
+ # tree('src1')
+ # # => src1
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # FileUtils.copy_entry('src1', 'dest1')
+ # tree('dest1')
+ # # => dest1
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
#
- # If +remove_destination+ is true, this method removes each destination file before copy.
+ # The recursive copying preserves file types for regular files,
+ # directories, and symbolic links;
+ # other file types (FIFO streams, device files, etc.) are not supported.
+ #
+ # Optional arguments:
+ #
+ # - +dereference_root+ - if +src+ is a symbolic link,
+ # follows the link (+false+ by default).
+ # - +preserve+ - preserves file times (+false+ by default).
+ # - +remove_destination+ - removes +dest+ before copying files (+false+ by default).
+ #
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
#
def copy_entry(src, dest, preserve = false, dereference_root = false, remove_destination = false)
- Entry_.new(src, nil, dereference_root).traverse do |ent|
+ if dereference_root
+ src = File.realpath(src)
+ end
+
+ Entry_.new(src, nil, false).wrap_traverse(proc do |ent|
destent = Entry_.new(dest, ent.rel, false)
- File.unlink destent.path if remove_destination && File.file?(destent.path)
+ File.unlink destent.path if remove_destination && (File.file?(destent.path) || File.symlink?(destent.path))
ent.copy destent.path
+ end, proc do |ent|
+ destent = Entry_.new(dest, ent.rel, false)
ent.copy_metadata destent.path if preserve
- end
+ end)
end
module_function :copy_entry
+ # Copies file from +src+ to +dest+, which should not be directories.
+ #
+ # Arguments +src+ and +dest+
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Examples:
+ #
+ # FileUtils.touch('src0.txt')
+ # FileUtils.copy_file('src0.txt', 'dest0.txt')
+ # File.file?('dest0.txt') # => true
+ #
+ # Optional arguments:
+ #
+ # - +dereference+ - if +src+ is a symbolic link,
+ # follows the link (+true+ by default).
+ # - +preserve+ - preserves file times (+false+ by default).
+ # - +remove_destination+ - removes +dest+ before copying files (+false+ by default).
#
- # Copies file contents of +src+ to +dest+.
- # Both of +src+ and +dest+ must be a path name.
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
#
def copy_file(src, dest, preserve = false, dereference = true)
ent = Entry_.new(src, nil, dereference)
@@ -464,54 +1077,104 @@ module FileUtils
end
module_function :copy_file
+ # Copies \IO stream +src+ to \IO stream +dest+ via
+ # {IO.copy_stream}[rdoc-ref:IO.copy_stream].
#
- # Copies stream +src+ to +dest+.
- # +src+ must respond to #read(n) and
- # +dest+ must respond to #write(str).
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
#
def copy_stream(src, dest)
IO.copy_stream(src, dest)
end
module_function :copy_stream
- #
- # Options: force noop verbose
- #
- # Moves file(s) +src+ to +dest+. If +file+ and +dest+ exist on the different
- # disk partition, the file is copied instead.
- #
- # FileUtils.mv 'badname.rb', 'goodname.rb'
- # FileUtils.mv 'stuff.rb', '/notexist/lib/ruby', :force => true # no error
- #
- # FileUtils.mv %w(junk.txt dust.txt), '/home/aamine/.trash/'
- # FileUtils.mv Dir.glob('test*.rb'), 'test', :noop => true, :verbose => true
- #
- def mv(src, dest, options = {})
- fu_check_options options, OPT_TABLE['mv']
- fu_output_message "mv#{options[:force] ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose]
- return if options[:noop]
+ # Moves entries.
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # If +src+ and +dest+ are on different file systems,
+ # first copies, then removes +src+.
+ #
+ # May cause a local vulnerability if not called with keyword argument
+ # <tt>secure: true</tt>;
+ # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability].
+ #
+ # If +src+ is the path to a single file or directory and +dest+ does not exist,
+ # moves +src+ to +dest+:
+ #
+ # tree('src0')
+ # # => src0
+ # # |-- src0.txt
+ # # `-- src1.txt
+ # File.exist?('dest0') # => false
+ # FileUtils.mv('src0', 'dest0')
+ # File.exist?('src0') # => false
+ # tree('dest0')
+ # # => dest0
+ # # |-- src0.txt
+ # # `-- src1.txt
+ #
+ # If +src+ is an array of paths to files and directories
+ # and +dest+ is the path to a directory,
+ # copies from each path in the array to +dest+:
+ #
+ # File.file?('src1.txt') # => true
+ # tree('src1')
+ # # => src1
+ # # |-- src.dat
+ # # `-- src.txt
+ # Dir.empty?('dest1') # => true
+ # FileUtils.mv(['src1.txt', 'src1'], 'dest1')
+ # tree('dest1')
+ # # => dest1
+ # # |-- src1
+ # # | |-- src.dat
+ # # | `-- src.txt
+ # # `-- src1.txt
+ #
+ # Keyword arguments:
+ #
+ # - <tt>force: true</tt> - if the move includes removing +src+
+ # (that is, if +src+ and +dest+ are on different file systems),
+ # ignores raised exceptions of StandardError and its descendants.
+ # - <tt>noop: true</tt> - does not move files.
+ # - <tt>secure: true</tt> - removes +src+ securely;
+ # see details at FileUtils.remove_entry_secure.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.mv('src0', 'dest0', noop: true, verbose: true)
+ # FileUtils.mv(['src1.txt', 'src1'], 'dest1', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # mv src0 dest0
+ # mv src1.txt src1 dest1
+ #
+ def mv(src, dest, force: nil, noop: nil, verbose: nil, secure: nil)
+ fu_output_message "mv#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose
+ return if noop
fu_each_src_dest(src, dest) do |s, d|
destent = Entry_.new(d, nil, true)
begin
if destent.exist?
if destent.directory?
- raise Errno::EEXIST, dest
- else
- destent.remove_file if rename_cannot_overwrite_file?
+ raise Errno::EEXIST, d
end
end
begin
File.rename s, d
- rescue Errno::EXDEV
+ rescue Errno::EXDEV,
+ Errno::EPERM # move from unencrypted to encrypted dir (ext4)
copy_entry s, d, true
- if options[:secure]
- remove_entry_secure s, options[:force]
+ if secure
+ remove_entry_secure s, force
else
- remove_entry s, options[:force]
+ remove_entry s, force
end
end
rescue SystemCallError
- raise unless options[:force]
+ raise unless force
end
end
end
@@ -520,32 +1183,40 @@ module FileUtils
alias move mv
module_function :move
- OPT_TABLE['mv'] =
- OPT_TABLE['move'] = [:force, :noop, :verbose, :secure]
-
- def rename_cannot_overwrite_file? #:nodoc:
- /cygwin|mswin|mingw|bccwin|emx/ =~ RUBY_PLATFORM
- end
- private_module_function :rename_cannot_overwrite_file?
-
+ # Removes entries at the paths in the given +list+
+ # (a single path or an array of paths)
+ # returns +list+, if it is an array, <tt>[list]</tt> otherwise.
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
#
- # Options: force noop verbose
- #
- # Remove file(s) specified in +list+. This method cannot remove directories.
- # All StandardErrors are ignored when the :force option is set.
- #
- # FileUtils.rm %w( junk.txt dust.txt )
- # FileUtils.rm Dir.glob('*.so')
- # FileUtils.rm 'NotExistFile', :force => true # never raises exception
- #
- def rm(list, options = {})
- fu_check_options options, OPT_TABLE['rm']
+ # With no keyword arguments, removes files at the paths given in +list+:
+ #
+ # FileUtils.touch(['src0.txt', 'src0.dat'])
+ # FileUtils.rm(['src0.dat', 'src0.txt']) # => ["src0.dat", "src0.txt"]
+ #
+ # Keyword arguments:
+ #
+ # - <tt>force: true</tt> - ignores raised exceptions of StandardError
+ # and its descendants.
+ # - <tt>noop: true</tt> - does not remove files; returns +nil+.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.rm(['src0.dat', 'src0.txt'], noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # rm src0.dat src0.txt
+ #
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
+ #
+ def rm(list, force: nil, noop: nil, verbose: nil)
list = fu_list(list)
- fu_output_message "rm#{options[:force] ? ' -f' : ''} #{list.join ' '}" if options[:verbose]
- return if options[:noop]
+ fu_output_message "rm#{force ? ' -f' : ''} #{list.join ' '}" if verbose
+ return if noop
list.each do |path|
- remove_file path, options[:force]
+ remove_file path, force
end
end
module_function :rm
@@ -553,124 +1224,126 @@ module FileUtils
alias remove rm
module_function :remove
- OPT_TABLE['rm'] =
- OPT_TABLE['remove'] = [:force, :noop, :verbose]
-
+ # Equivalent to:
+ #
+ # FileUtils.rm(list, force: true, **kwargs)
#
- # Options: noop verbose
- #
- # Equivalent to
+ # Argument +list+ (a single path or an array of paths)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
#
- # #rm(list, :force => true)
+ # See FileUtils.rm for keyword arguments.
#
- def rm_f(list, options = {})
- fu_check_options options, OPT_TABLE['rm_f']
- options = options.dup
- options[:force] = true
- rm list, options
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
+ #
+ def rm_f(list, noop: nil, verbose: nil)
+ rm list, force: true, noop: noop, verbose: verbose
end
module_function :rm_f
alias safe_unlink rm_f
module_function :safe_unlink
- OPT_TABLE['rm_f'] =
- OPT_TABLE['safe_unlink'] = [:noop, :verbose]
-
- #
- # Options: force noop verbose secure
- #
- # remove files +list+[0] +list+[1]... If +list+[n] is a directory,
- # removes its all contents recursively. This method ignores
- # StandardError when :force option is set.
- #
- # FileUtils.rm_r Dir.glob('/tmp/*')
- # FileUtils.rm_r '/', :force => true # :-)
- #
- # WARNING: This method causes local vulnerability
- # if one of parent directories or removing directory tree are world
- # writable (including /tmp, whose permission is 1777), and the current
- # process has strong privilege such as Unix super user (root), and the
- # system has symbolic link. For secure removing, read the documentation
- # of #remove_entry_secure carefully, and set :secure option to true.
- # Default is :secure=>false.
- #
- # NOTE: This method calls #remove_entry_secure if :secure option is set.
- # See also #remove_entry_secure.
- #
- def rm_r(list, options = {})
- fu_check_options options, OPT_TABLE['rm_r']
- # options[:secure] = true unless options.key?(:secure)
+ # Removes entries at the paths in the given +list+
+ # (a single path or an array of paths);
+ # returns +list+, if it is an array, <tt>[list]</tt> otherwise.
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # May cause a local vulnerability if not called with keyword argument
+ # <tt>secure: true</tt>;
+ # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability].
+ #
+ # For each file path, removes the file at that path:
+ #
+ # FileUtils.touch(['src0.txt', 'src0.dat'])
+ # FileUtils.rm_r(['src0.dat', 'src0.txt'])
+ # File.exist?('src0.txt') # => false
+ # File.exist?('src0.dat') # => false
+ #
+ # For each directory path, recursively removes files and directories:
+ #
+ # tree('src1')
+ # # => src1
+ # # |-- dir0
+ # # | |-- src0.txt
+ # # | `-- src1.txt
+ # # `-- dir1
+ # # |-- src2.txt
+ # # `-- src3.txt
+ # FileUtils.rm_r('src1')
+ # File.exist?('src1') # => false
+ #
+ # Keyword arguments:
+ #
+ # - <tt>force: true</tt> - ignores raised exceptions of StandardError
+ # and its descendants.
+ # - <tt>noop: true</tt> - does not remove entries; returns +nil+.
+ # - <tt>secure: true</tt> - removes +src+ securely;
+ # see details at FileUtils.remove_entry_secure.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.rm_r(['src0.dat', 'src0.txt'], noop: true, verbose: true)
+ # FileUtils.rm_r('src1', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # rm -r src0.dat src0.txt
+ # rm -r src1
+ #
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
+ #
+ def rm_r(list, force: nil, noop: nil, verbose: nil, secure: nil)
list = fu_list(list)
- fu_output_message "rm -r#{options[:force] ? 'f' : ''} #{list.join ' '}" if options[:verbose]
- return if options[:noop]
+ fu_output_message "rm -r#{force ? 'f' : ''} #{list.join ' '}" if verbose
+ return if noop
list.each do |path|
- if options[:secure]
- remove_entry_secure path, options[:force]
+ if secure
+ remove_entry_secure path, force
else
- remove_entry path, options[:force]
+ remove_entry path, force
end
end
end
module_function :rm_r
- OPT_TABLE['rm_r'] = [:force, :noop, :verbose, :secure]
-
+ # Equivalent to:
+ #
+ # FileUtils.rm_r(list, force: true, **kwargs)
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
#
- # Options: noop verbose secure
- #
- # Equivalent to
+ # May cause a local vulnerability if not called with keyword argument
+ # <tt>secure: true</tt>;
+ # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability].
#
- # #rm_r(list, :force => true)
+ # See FileUtils.rm_r for keyword arguments.
#
- # WARNING: This method causes local vulnerability.
- # Read the documentation of #rm_r first.
- #
- def rm_rf(list, options = {})
- fu_check_options options, OPT_TABLE['rm_rf']
- options = options.dup
- options[:force] = true
- rm_r list, options
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
+ #
+ def rm_rf(list, noop: nil, verbose: nil, secure: nil)
+ rm_r list, force: true, noop: noop, verbose: verbose, secure: secure
end
module_function :rm_rf
alias rmtree rm_rf
module_function :rmtree
- OPT_TABLE['rm_rf'] =
- OPT_TABLE['rmtree'] = [:noop, :verbose, :secure]
-
- #
- # This method removes a file system entry +path+. +path+ shall be a
- # regular file, a directory, or something. If +path+ is a directory,
- # remove it recursively. This method is required to avoid TOCTTOU
- # (time-of-check-to-time-of-use) local security vulnerability of #rm_r.
- # #rm_r causes security hole when:
- #
- # * Parent directory is world writable (including /tmp).
- # * Removing directory tree includes world writable directory.
- # * The system has symbolic link.
+ # Securely removes the entry given by +path+,
+ # which should be the entry for a regular file, a symbolic link,
+ # or a directory.
#
- # To avoid this security hole, this method applies special preprocess.
- # If +path+ is a directory, this method chown(2) and chmod(2) all
- # removing directories. This requires the current process is the
- # owner of the removing whole directory tree, or is the super user (root).
+ # Argument +path+
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments].
#
- # WARNING: You must ensure that *ALL* parent directories are not
- # world writable. Otherwise this method does not work.
- # Only exception is temporary directory like /tmp and /var/tmp,
- # whose permission is 1777.
+ # Avoids a local vulnerability that can exist in certain circumstances;
+ # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability].
#
- # WARNING: Only the owner of the removing directory tree, or Unix super
- # user (root) should invoke this method. Otherwise this method does not
- # work.
+ # Optional argument +force+ specifies whether to ignore
+ # raised exceptions of StandardError and its descendants.
#
- # For details of this security vulnerability, see Perl's case:
- #
- # http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-0448
- # http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2004-0452
- #
- # For fileutils.rb, this vulnerability is reported in [ruby-dev:26100].
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
#
def remove_entry_secure(path, force = false)
unless fu_have_symlink?
@@ -692,17 +1365,38 @@ module FileUtils
unless parent_st.sticky?
raise ArgumentError, "parent directory is world writable, FileUtils#remove_entry_secure does not work; abort: #{path.inspect} (parent directory mode #{'%o' % parent_st.mode})"
end
+
# freeze tree root
euid = Process.euid
- File.open(fullpath + '/.') {|f|
- unless fu_stat_identical_entry?(st, f.stat)
- # symlink (TOC-to-TOU attack?)
- File.unlink fullpath
- return
- end
- f.chown euid, -1
- f.chmod 0700
- }
+ dot_file = fullpath + "/."
+ begin
+ File.open(dot_file) {|f|
+ unless fu_stat_identical_entry?(st, f.stat)
+ # symlink (TOC-to-TOU attack?)
+ File.unlink fullpath
+ return
+ end
+ f.chown euid, -1
+ f.chmod 0700
+ }
+ rescue Errno::EISDIR # JRuby in non-native mode can't open files as dirs
+ File.lstat(dot_file).tap {|fstat|
+ unless fu_stat_identical_entry?(st, fstat)
+ # symlink (TOC-to-TOU attack?)
+ File.unlink fullpath
+ return
+ end
+ File.chown euid, -1, dot_file
+ File.chmod 0700, dot_file
+ }
+ end
+
+ unless fu_stat_identical_entry?(st, File.lstat(fullpath))
+ # TOC-to-TOU attack?
+ File.unlink fullpath
+ return
+ end
+
# ---- tree root is frozen ----
root = Entry_.new(path)
root.preorder_traverse do |ent|
@@ -723,11 +1417,11 @@ module FileUtils
end
module_function :remove_entry_secure
- def fu_have_symlink? #:nodoc
+ def fu_have_symlink? #:nodoc:
File.symlink nil, nil
rescue NotImplementedError
return false
- rescue
+ rescue TypeError
return true
end
private_module_function :fu_have_symlink?
@@ -737,12 +1431,17 @@ module FileUtils
end
private_module_function :fu_stat_identical_entry?
+ # Removes the entry given by +path+,
+ # which should be the entry for a regular file, a symbolic link,
+ # or a directory.
+ #
+ # Argument +path+
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments].
#
- # This method removes a file system entry +path+.
- # +path+ might be a regular file, a directory, or something.
- # If +path+ is a directory, remove it recursively.
+ # Optional argument +force+ specifies whether to ignore
+ # raised exceptions of StandardError and its descendants.
#
- # See also #remove_entry_secure.
+ # Related: FileUtils.remove_entry_secure.
#
def remove_entry(path, force = false)
Entry_.new(path).postorder_traverse do |ent|
@@ -757,9 +1456,16 @@ module FileUtils
end
module_function :remove_entry
+ # Removes the file entry given by +path+,
+ # which should be the entry for a regular file or a symbolic link.
#
- # Removes a file +path+.
- # This method ignores StandardError if +force+ is true.
+ # Argument +path+
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Optional argument +force+ specifies whether to ignore
+ # raised exceptions of StandardError and its descendants.
+ #
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
#
def remove_file(path, force = false)
Entry_.new(path).remove_file
@@ -768,20 +1474,33 @@ module FileUtils
end
module_function :remove_file
+ # Recursively removes the directory entry given by +path+,
+ # which should be the entry for a regular file, a symbolic link,
+ # or a directory.
+ #
+ # Argument +path+
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Optional argument +force+ specifies whether to ignore
+ # raised exceptions of StandardError and its descendants.
#
- # Removes a directory +dir+ and its contents recursively.
- # This method ignores StandardError if +force+ is true.
+ # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting].
#
def remove_dir(path, force = false)
- remove_entry path, force # FIXME?? check if it is a directory
+ raise Errno::ENOTDIR, path unless force or File.directory?(path)
+ remove_entry path, force
end
module_function :remove_dir
+ # Returns +true+ if the contents of files +a+ and +b+ are identical,
+ # +false+ otherwise.
#
- # Returns true if the contents of a file A and a file B are identical.
- #
- # FileUtils.compare_file('somefile', 'somefile') #=> true
- # FileUtils.compare_file('/bin/cp', '/bin/mv') #=> maybe false
+ # Arguments +a+ and +b+
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # FileUtils.identical? and FileUtils.cmp are aliases for FileUtils.compare_file.
+ #
+ # Related: FileUtils.compare_stream.
#
def compare_file(a, b)
return false unless File.size(a) == File.size(b)
@@ -798,124 +1517,386 @@ module FileUtils
module_function :identical?
module_function :cmp
+ # Returns +true+ if the contents of streams +a+ and +b+ are identical,
+ # +false+ otherwise.
+ #
+ # Arguments +a+ and +b+
+ # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments].
#
- # Returns true if the contents of a stream +a+ and +b+ are identical.
+ # Related: FileUtils.compare_file.
#
def compare_stream(a, b)
bsize = fu_stream_blksize(a, b)
- sa = sb = nil
- while sa == sb
- sa = a.read(bsize)
- sb = b.read(bsize)
- unless sa and sb
- if sa.nil? and sb.nil?
- return true
- end
- end
- end
+
+ sa = String.new(capacity: bsize)
+ sb = String.new(capacity: bsize)
+
+ begin
+ a.read(bsize, sa)
+ b.read(bsize, sb)
+ return true if sa.empty? && sb.empty?
+ end while sa == sb
false
end
module_function :compare_stream
- #
- # Options: mode preserve noop verbose
- #
- # If +src+ is not same as +dest+, copies it and changes the permission
- # mode to +mode+. If +dest+ is a directory, destination is +dest+/+src+.
- # This method removes destination before copy.
- #
- # FileUtils.install 'ruby', '/usr/local/bin/ruby', :mode => 0755, :verbose => true
- # FileUtils.install 'lib.rb', '/usr/local/lib/ruby/site_ruby', :verbose => true
- #
- def install(src, dest, options = {})
- fu_check_options options, OPT_TABLE['install']
- fu_output_message "install -c#{options[:preserve] && ' -p'}#{options[:mode] ? (' -m 0%o' % options[:mode]) : ''} #{[src,dest].flatten.join ' '}" if options[:verbose]
- return if options[:noop]
+ # Copies a file entry.
+ # See {install(1)}[https://man7.org/linux/man-pages/man1/install.1.html].
+ #
+ # Arguments +src+ (a single path or an array of paths)
+ # and +dest+ (a single path)
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments];
+ #
+ # If the entry at +dest+ does not exist, copies from +src+ to +dest+:
+ #
+ # File.read('src0.txt') # => "aaa\n"
+ # File.exist?('dest0.txt') # => false
+ # FileUtils.install('src0.txt', 'dest0.txt')
+ # File.read('dest0.txt') # => "aaa\n"
+ #
+ # If +dest+ is a file entry, copies from +src+ to +dest+, overwriting:
+ #
+ # File.read('src1.txt') # => "aaa\n"
+ # File.read('dest1.txt') # => "bbb\n"
+ # FileUtils.install('src1.txt', 'dest1.txt')
+ # File.read('dest1.txt') # => "aaa\n"
+ #
+ # If +dest+ is a directory entry, copies from +src+ to <tt>dest/src</tt>,
+ # overwriting if necessary:
+ #
+ # File.read('src2.txt') # => "aaa\n"
+ # File.read('dest2/src2.txt') # => "bbb\n"
+ # FileUtils.install('src2.txt', 'dest2')
+ # File.read('dest2/src2.txt') # => "aaa\n"
+ #
+ # If +src+ is an array of paths and +dest+ points to a directory,
+ # copies each path +path+ in +src+ to <tt>dest/path</tt>:
+ #
+ # File.file?('src3.txt') # => true
+ # File.file?('src3.dat') # => true
+ # FileUtils.mkdir('dest3')
+ # FileUtils.install(['src3.txt', 'src3.dat'], 'dest3')
+ # File.file?('dest3/src3.txt') # => true
+ # File.file?('dest3/src3.dat') # => true
+ #
+ # Keyword arguments:
+ #
+ # - <tt>group: <i>group</i></tt> - changes the group if not +nil+,
+ # using {File.chown}[rdoc-ref:File.chown].
+ # - <tt>mode: <i>permissions</i></tt> - changes the permissions.
+ # using {File.chmod}[rdoc-ref:File.chmod].
+ # - <tt>noop: true</tt> - does not copy entries; returns +nil+.
+ # - <tt>owner: <i>owner</i></tt> - changes the owner if not +nil+,
+ # using {File.chown}[rdoc-ref:File.chown].
+ # - <tt>preserve: true</tt> - preserve timestamps
+ # using {File.utime}[rdoc-ref:File.utime].
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.install('src0.txt', 'dest0.txt', noop: true, verbose: true)
+ # FileUtils.install('src1.txt', 'dest1.txt', noop: true, verbose: true)
+ # FileUtils.install('src2.txt', 'dest2', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # install -c src0.txt dest0.txt
+ # install -c src1.txt dest1.txt
+ # install -c src2.txt dest2
+ #
+ # Related: {methods for copying}[rdoc-ref:FileUtils@Copying].
+ #
+ def install(src, dest, mode: nil, owner: nil, group: nil, preserve: nil,
+ noop: nil, verbose: nil)
+ if verbose
+ msg = +"install -c"
+ msg << ' -p' if preserve
+ msg << ' -m ' << mode_to_s(mode) if mode
+ msg << " -o #{owner}" if owner
+ msg << " -g #{group}" if group
+ msg << ' ' << [src,dest].flatten.join(' ')
+ fu_output_message msg
+ end
+ return if noop
+ uid = fu_get_uid(owner)
+ gid = fu_get_gid(group)
fu_each_src_dest(src, dest) do |s, d|
+ st = File.stat(s)
unless File.exist?(d) and compare_file(s, d)
remove_file d, true
- st = File.stat(s) if options[:preserve]
- copy_file s, d
- File.utime st.atime, st.mtime, d if options[:preserve]
- File.chmod options[:mode], d if options[:mode]
+ if d.end_with?('/')
+ mkdir_p d
+ copy_file s, d + File.basename(s)
+ else
+ mkdir_p File.expand_path('..', d)
+ copy_file s, d
+ end
+ File.utime st.atime, st.mtime, d if preserve
+ File.chmod fu_mode(mode, st), d if mode
+ File.chown uid, gid, d if uid or gid
end
end
end
module_function :install
- OPT_TABLE['install'] = [:mode, :preserve, :noop, :verbose]
+ def user_mask(target) #:nodoc:
+ target.each_char.inject(0) do |mask, chr|
+ case chr
+ when "u"
+ mask | 04700
+ when "g"
+ mask | 02070
+ when "o"
+ mask | 01007
+ when "a"
+ mask | 07777
+ else
+ raise ArgumentError, "invalid 'who' symbol in file mode: #{chr}"
+ end
+ end
+ end
+ private_module_function :user_mask
+ def apply_mask(mode, user_mask, op, mode_mask) #:nodoc:
+ case op
+ when '='
+ (mode & ~user_mask) | (user_mask & mode_mask)
+ when '+'
+ mode | (user_mask & mode_mask)
+ when '-'
+ mode & ~(user_mask & mode_mask)
+ end
+ end
+ private_module_function :apply_mask
+
+ def symbolic_modes_to_i(mode_sym, path) #:nodoc:
+ path = File.stat(path) unless File::Stat === path
+ mode = path.mode
+ mode_sym.split(/,/).inject(mode & 07777) do |current_mode, clause|
+ target, *actions = clause.split(/([=+-])/)
+ raise ArgumentError, "invalid file mode: #{mode_sym}" if actions.empty?
+ target = 'a' if target.empty?
+ user_mask = user_mask(target)
+ actions.each_slice(2) do |op, perm|
+ need_apply = op == '='
+ mode_mask = (perm || '').each_char.inject(0) do |mask, chr|
+ case chr
+ when "r"
+ mask | 0444
+ when "w"
+ mask | 0222
+ when "x"
+ mask | 0111
+ when "X"
+ if path.directory?
+ mask | 0111
+ else
+ mask
+ end
+ when "s"
+ mask | 06000
+ when "t"
+ mask | 01000
+ when "u", "g", "o"
+ if mask.nonzero?
+ current_mode = apply_mask(current_mode, user_mask, op, mask)
+ end
+ need_apply = false
+ copy_mask = user_mask(chr)
+ (current_mode & copy_mask) / (copy_mask & 0111) * (user_mask & 0111)
+ else
+ raise ArgumentError, "invalid 'perm' symbol in file mode: #{chr}"
+ end
+ end
+
+ if mode_mask.nonzero? || need_apply
+ current_mode = apply_mask(current_mode, user_mask, op, mode_mask)
+ end
+ end
+ current_mode
+ end
+ end
+ private_module_function :symbolic_modes_to_i
+
+ def fu_mode(mode, path) #:nodoc:
+ mode.is_a?(String) ? symbolic_modes_to_i(mode, path) : mode
+ end
+ private_module_function :fu_mode
+
+ def mode_to_s(mode) #:nodoc:
+ mode.is_a?(String) ? mode : "%o" % mode
+ end
+ private_module_function :mode_to_s
+
+ # Changes permissions on the entries at the paths given in +list+
+ # (a single path or an array of paths)
+ # to the permissions given by +mode+;
+ # returns +list+ if it is an array, <tt>[list]</tt> otherwise:
+ #
+ # - Modifies each entry that is a regular file using
+ # {File.chmod}[rdoc-ref:File.chmod].
+ # - Modifies each entry that is a symbolic link using
+ # {File.lchmod}[rdoc-ref:File.lchmod].
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Argument +mode+ may be either an integer or a string:
+ #
+ # - \Integer +mode+: represents the permission bits to be set:
+ #
+ # FileUtils.chmod(0755, 'src0.txt')
+ # FileUtils.chmod(0644, ['src0.txt', 'src0.dat'])
+ #
+ # - \String +mode+: represents the permissions to be set:
+ #
+ # The string is of the form <tt>[targets][[operator][perms[,perms]]</tt>, where:
+ #
+ # - +targets+ may be any combination of these letters:
+ #
+ # - <tt>'u'</tt>: permissions apply to the file's owner.
+ # - <tt>'g'</tt>: permissions apply to users in the file's group.
+ # - <tt>'o'</tt>: permissions apply to other users not in the file's group.
+ # - <tt>'a'</tt> (the default): permissions apply to all users.
+ #
+ # - +operator+ may be one of these letters:
+ #
+ # - <tt>'+'</tt>: adds permissions.
+ # - <tt>'-'</tt>: removes permissions.
+ # - <tt>'='</tt>: sets (replaces) permissions.
+ #
+ # - +perms+ (may be repeated, with separating commas)
+ # may be any combination of these letters:
+ #
+ # - <tt>'r'</tt>: Read.
+ # - <tt>'w'</tt>: Write.
+ # - <tt>'x'</tt>: Execute (search, for a directory).
+ # - <tt>'X'</tt>: Search (for a directories only;
+ # must be used with <tt>'+'</tt>)
+ # - <tt>'s'</tt>: Uid or gid.
+ # - <tt>'t'</tt>: Sticky bit.
+ #
+ # Examples:
+ #
+ # FileUtils.chmod('u=wrx,go=rx', 'src1.txt')
+ # FileUtils.chmod('u=wrx,go=rx', '/usr/bin/ruby')
#
- # Options: noop verbose
- #
- # Changes permission bits on the named files (in +list+) to the bit pattern
- # represented by +mode+.
- #
- # FileUtils.chmod 0755, 'somecommand'
- # FileUtils.chmod 0644, %w(my.rb your.rb his.rb her.rb)
- # FileUtils.chmod 0755, '/usr/bin/ruby', :verbose => true
- #
- def chmod(mode, list, options = {})
- fu_check_options options, OPT_TABLE['chmod']
+ # Keyword arguments:
+ #
+ # - <tt>noop: true</tt> - does not change permissions; returns +nil+.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.chmod(0755, 'src0.txt', noop: true, verbose: true)
+ # FileUtils.chmod(0644, ['src0.txt', 'src0.dat'], noop: true, verbose: true)
+ # FileUtils.chmod('u=wrx,go=rx', 'src1.txt', noop: true, verbose: true)
+ # FileUtils.chmod('u=wrx,go=rx', '/usr/bin/ruby', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # chmod 755 src0.txt
+ # chmod 644 src0.txt src0.dat
+ # chmod u=wrx,go=rx src1.txt
+ # chmod u=wrx,go=rx /usr/bin/ruby
+ #
+ # Related: FileUtils.chmod_R.
+ #
+ def chmod(mode, list, noop: nil, verbose: nil)
list = fu_list(list)
- fu_output_message sprintf('chmod %o %s', mode, list.join(' ')) if options[:verbose]
- return if options[:noop]
+ fu_output_message sprintf('chmod %s %s', mode_to_s(mode), list.join(' ')) if verbose
+ return if noop
list.each do |path|
- Entry_.new(path).chmod mode
+ Entry_.new(path).chmod(fu_mode(mode, path))
end
end
module_function :chmod
- OPT_TABLE['chmod'] = [:noop, :verbose]
-
+ # Like FileUtils.chmod, but changes permissions recursively.
#
- # Options: noop verbose force
- #
- # Changes permission bits on the named files (in +list+)
- # to the bit pattern represented by +mode+.
- #
- # FileUtils.chmod_R 0700, "/tmp/app.#{$$}"
- #
- def chmod_R(mode, list, options = {})
- fu_check_options options, OPT_TABLE['chmod_R']
+ def chmod_R(mode, list, noop: nil, verbose: nil, force: nil)
list = fu_list(list)
- fu_output_message sprintf('chmod -R%s %o %s',
- (options[:force] ? 'f' : ''),
- mode, list.join(' ')) if options[:verbose]
- return if options[:noop]
+ fu_output_message sprintf('chmod -R%s %s %s',
+ (force ? 'f' : ''),
+ mode_to_s(mode), list.join(' ')) if verbose
+ return if noop
list.each do |root|
Entry_.new(root).traverse do |ent|
begin
- ent.chmod mode
+ ent.chmod(fu_mode(mode, ent.path))
rescue
- raise unless options[:force]
+ raise unless force
end
end
end
end
module_function :chmod_R
- OPT_TABLE['chmod_R'] = [:noop, :verbose, :force]
-
- #
- # Options: noop verbose
- #
- # Changes owner and group on the named files (in +list+)
- # to the user +user+ and the group +group+. +user+ and +group+
- # may be an ID (Integer/String) or a name (String).
- # If +user+ or +group+ is nil, this method does not change
- # the attribute.
- #
- # FileUtils.chown 'root', 'staff', '/usr/local/bin/ruby'
- # FileUtils.chown nil, 'bin', Dir.glob('/usr/bin/*'), :verbose => true
- #
- def chown(user, group, list, options = {})
- fu_check_options options, OPT_TABLE['chown']
+ # Changes the owner and group on the entries at the paths given in +list+
+ # (a single path or an array of paths)
+ # to the given +user+ and +group+;
+ # returns +list+ if it is an array, <tt>[list]</tt> otherwise:
+ #
+ # - Modifies each entry that is a regular file using
+ # {File.chown}[rdoc-ref:File.chown].
+ # - Modifies each entry that is a symbolic link using
+ # {File.lchown}[rdoc-ref:File.lchown].
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # User and group:
+ #
+ # - Argument +user+ may be a user name or a user id;
+ # if +nil+ or +-1+, the user is not changed.
+ # - Argument +group+ may be a group name or a group id;
+ # if +nil+ or +-1+, the group is not changed.
+ # - The user must be a member of the group.
+ #
+ # Examples:
+ #
+ # # One path.
+ # # User and group as string names.
+ # File.stat('src0.txt').uid # => 1004
+ # File.stat('src0.txt').gid # => 1004
+ # FileUtils.chown('user2', 'group1', 'src0.txt')
+ # File.stat('src0.txt').uid # => 1006
+ # File.stat('src0.txt').gid # => 1005
+ #
+ # # User and group as uid and gid.
+ # FileUtils.chown(1004, 1004, 'src0.txt')
+ # File.stat('src0.txt').uid # => 1004
+ # File.stat('src0.txt').gid # => 1004
+ #
+ # # Array of paths.
+ # FileUtils.chown(1006, 1005, ['src0.txt', 'src0.dat'])
+ #
+ # # Directory (not recursive).
+ # FileUtils.chown('user2', 'group1', '.')
+ #
+ # Keyword arguments:
+ #
+ # - <tt>noop: true</tt> - does not change permissions; returns +nil+.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.chown('user2', 'group1', 'src0.txt', noop: true, verbose: true)
+ # FileUtils.chown(1004, 1004, 'src0.txt', noop: true, verbose: true)
+ # FileUtils.chown(1006, 1005, ['src0.txt', 'src0.dat'], noop: true, verbose: true)
+ # FileUtils.chown('user2', 'group1', path, noop: true, verbose: true)
+ # FileUtils.chown('user2', 'group1', '.', noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # chown user2:group1 src0.txt
+ # chown 1004:1004 src0.txt
+ # chown 1006:1005 src0.txt src0.dat
+ # chown user2:group1 src0.txt
+ # chown user2:group1 .
+ #
+ # Related: FileUtils.chown_R.
+ #
+ def chown(user, group, list, noop: nil, verbose: nil)
list = fu_list(list)
- fu_output_message sprintf('chown %s%s',
- [user,group].compact.join(':') + ' ',
- list.join(' ')) if options[:verbose]
- return if options[:noop]
+ fu_output_message sprintf('chown %s %s',
+ (group ? "#{user}:#{group}" : user || ':'),
+ list.join(' ')) if verbose
+ return if noop
uid = fu_get_uid(user)
gid = fu_get_gid(group)
list.each do |path|
@@ -924,99 +1905,109 @@ module FileUtils
end
module_function :chown
- OPT_TABLE['chown'] = [:noop, :verbose]
-
- #
- # Options: noop verbose force
- #
- # Changes owner and group on the named files (in +list+)
- # to the user +user+ and the group +group+ recursively.
- # +user+ and +group+ may be an ID (Integer/String) or
- # a name (String). If +user+ or +group+ is nil, this
- # method does not change the attribute.
- #
- # FileUtils.chown_R 'www', 'www', '/var/www/htdocs'
- # FileUtils.chown_R 'cvs', 'cvs', '/var/cvs', :verbose => true
- #
- def chown_R(user, group, list, options = {})
- fu_check_options options, OPT_TABLE['chown_R']
+ # Like FileUtils.chown, but changes owner and group recursively.
+ #
+ def chown_R(user, group, list, noop: nil, verbose: nil, force: nil)
list = fu_list(list)
- fu_output_message sprintf('chown -R%s %s%s',
- (options[:force] ? 'f' : ''),
- [user,group].compact.join(':') + ' ',
- list.join(' ')) if options[:verbose]
- return if options[:noop]
+ fu_output_message sprintf('chown -R%s %s %s',
+ (force ? 'f' : ''),
+ (group ? "#{user}:#{group}" : user || ':'),
+ list.join(' ')) if verbose
+ return if noop
uid = fu_get_uid(user)
gid = fu_get_gid(group)
- return unless uid or gid
list.each do |root|
Entry_.new(root).traverse do |ent|
begin
ent.chown uid, gid
rescue
- raise unless options[:force]
+ raise unless force
end
end
end
end
module_function :chown_R
- OPT_TABLE['chown_R'] = [:noop, :verbose, :force]
-
- begin
- require 'etc'
-
- def fu_get_uid(user) #:nodoc:
- return nil unless user
- user = user.to_s
- if /\A\d+\z/ =~ user
- then user.to_i
- else Etc.getpwnam(user).uid
- end
- end
- private_module_function :fu_get_uid
-
- def fu_get_gid(group) #:nodoc:
- return nil unless group
- if /\A\d+\z/ =~ group
- then group.to_i
- else Etc.getgrnam(group).gid
- end
- end
- private_module_function :fu_get_gid
-
- rescue LoadError
- # need Win32 support???
-
- def fu_get_uid(user) #:nodoc:
- user # FIXME
+ def fu_get_uid(user) #:nodoc:
+ return nil unless user
+ case user
+ when Integer
+ user
+ when /\A\d+\z/
+ user.to_i
+ else
+ require 'etc'
+ Etc.getpwnam(user) ? Etc.getpwnam(user).uid : nil
end
- private_module_function :fu_get_uid
-
- def fu_get_gid(group) #:nodoc:
- group # FIXME
+ end
+ private_module_function :fu_get_uid
+
+ def fu_get_gid(group) #:nodoc:
+ return nil unless group
+ case group
+ when Integer
+ group
+ when /\A\d+\z/
+ group.to_i
+ else
+ require 'etc'
+ Etc.getgrnam(group) ? Etc.getgrnam(group).gid : nil
end
- private_module_function :fu_get_gid
end
+ private_module_function :fu_get_gid
+ # Updates modification times (mtime) and access times (atime)
+ # of the entries given by the paths in +list+
+ # (a single path or an array of paths);
+ # returns +list+ if it is an array, <tt>[list]</tt> otherwise.
+ #
+ # By default, creates an empty file for any path to a non-existent entry;
+ # use keyword argument +nocreate+ to raise an exception instead.
+ #
+ # Argument +list+ or its elements
+ # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments].
+ #
+ # Examples:
+ #
+ # # Single path.
+ # f = File.new('src0.txt') # Existing file.
+ # f.atime # => 2022-06-10 11:11:21.200277 -0700
+ # f.mtime # => 2022-06-10 11:11:21.200277 -0700
+ # FileUtils.touch('src0.txt')
+ # f = File.new('src0.txt')
+ # f.atime # => 2022-06-11 08:28:09.8185343 -0700
+ # f.mtime # => 2022-06-11 08:28:09.8185343 -0700
#
- # Options: noop verbose
- #
- # Updates modification time (mtime) and access time (atime) of file(s) in
- # +list+. Files are created if they don't exist.
- #
- # FileUtils.touch 'timestamp'
- # FileUtils.touch Dir.glob('*.c'); system 'make'
- #
- def touch(list, options = {})
- fu_check_options options, OPT_TABLE['touch']
+ # # Array of paths.
+ # FileUtils.touch(['src0.txt', 'src0.dat'])
+ #
+ # Keyword arguments:
+ #
+ # - <tt>mtime: <i>time</i></tt> - sets the entry's mtime to the given time,
+ # instead of the current time.
+ # - <tt>nocreate: true</tt> - raises an exception if the entry does not exist.
+ # - <tt>noop: true</tt> - does not touch entries; returns +nil+.
+ # - <tt>verbose: true</tt> - prints an equivalent command:
+ #
+ # FileUtils.touch('src0.txt', noop: true, verbose: true)
+ # FileUtils.touch(['src0.txt', 'src0.dat'], noop: true, verbose: true)
+ # FileUtils.touch(path, noop: true, verbose: true)
+ #
+ # Output:
+ #
+ # touch src0.txt
+ # touch src0.txt src0.dat
+ # touch src0.txt
+ #
+ # Related: FileUtils.uptodate?.
+ #
+ def touch(list, noop: nil, verbose: nil, mtime: nil, nocreate: nil)
list = fu_list(list)
- created = nocreate = options[:nocreate]
- t = options[:mtime]
- if options[:verbose]
- fu_output_message "touch #{nocreate ? ' -c' : ''}#{t ? t.strftime(' -t %Y%m%d%H%M.%S') : ''}#{list.join ' '}"
+ t = mtime
+ if verbose
+ fu_output_message "touch #{nocreate ? '-c ' : ''}#{t ? t.strftime('-t %Y%m%d%H%M.%S ') : ''}#{list.join ' '}"
end
- return if options[:noop]
+ return if noop
list.each do |path|
created = nocreate
begin
@@ -1033,22 +2024,24 @@ module FileUtils
end
module_function :touch
- OPT_TABLE['touch'] = [:noop, :verbose, :mtime, :nocreate]
-
private
- module StreamUtils_
+ module StreamUtils_ # :nodoc:
+
private
- def fu_windows?
- /mswin|mingw|bccwin|emx/ =~ RUBY_PLATFORM
+ case (defined?(::RbConfig) ? ::RbConfig::CONFIG['host_os'] : ::RUBY_PLATFORM)
+ when /mswin|mingw/
+ def fu_windows?; true end #:nodoc:
+ else
+ def fu_windows?; false end #:nodoc:
end
def fu_copy_stream0(src, dest, blksize = nil) #:nodoc:
IO.copy_stream(src, dest)
end
- def fu_stream_blksize(*streams)
+ def fu_stream_blksize(*streams) #:nodoc:
streams.each do |s|
next unless s.respond_to?(:stat)
size = fu_blksize(s.stat)
@@ -1057,14 +2050,14 @@ module FileUtils
fu_default_blksize()
end
- def fu_blksize(st)
+ def fu_blksize(st) #:nodoc:
s = st.blksize
return nil unless s
return nil if s == 0
s
end
- def fu_default_blksize
+ def fu_default_blksize #:nodoc:
1024
end
end
@@ -1113,7 +2106,12 @@ module FileUtils
end
def exist?
- lstat! ? true : false
+ begin
+ lstat
+ true
+ rescue Errno::ENOENT
+ false
+ end
end
def file?
@@ -1159,9 +2157,13 @@ module FileUtils
end
def entries
- Dir.entries(path())\
- .reject {|n| n == '.' or n == '..' }\
- .map {|n| Entry_.new(prefix(), join(rel(), n.untaint)) }
+ opts = {}
+ opts[:encoding] = fu_windows? ? ::Encoding::UTF_8 : path.encoding
+
+ files = Dir.children(path, **opts)
+
+ untaint = RUBY_VERSION < '2.7'
+ files.map {|n| Entry_.new(prefix(), join(rel(), untaint ? n.untaint : n)) }
end
def stat
@@ -1206,6 +2208,7 @@ module FileUtils
else
File.chmod mode, path()
end
+ rescue Errno::EOPNOTSUPP
end
def chown(uid, gid)
@@ -1216,12 +2219,29 @@ module FileUtils
end
end
+ def link(dest)
+ case
+ when directory?
+ if !File.exist?(dest) and descendant_directory?(dest, path)
+ raise ArgumentError, "cannot link directory %s to itself %s" % [path, dest]
+ end
+ begin
+ Dir.mkdir dest
+ rescue
+ raise unless File.directory?(dest)
+ end
+ else
+ File.link path(), dest
+ end
+ end
+
def copy(dest)
+ lstat
case
when file?
copy_file dest
when directory?
- if !File.exist?(dest) and /^#{Regexp.quote(path)}/ =~ File.dirname(dest)
+ if !File.exist?(dest) and descendant_directory?(dest, path)
raise ArgumentError, "cannot copy directory %s to itself %s" % [path, dest]
end
begin
@@ -1231,18 +2251,21 @@ module FileUtils
end
when symlink?
File.symlink File.readlink(path()), dest
- when chardev?
- raise "cannot handle device file" unless File.respond_to?(:mknod)
- mknod dest, ?c, 0666, lstat().rdev
- when blockdev?
- raise "cannot handle device file" unless File.respond_to?(:mknod)
- mknod dest, ?b, 0666, lstat().rdev
+ when chardev?, blockdev?
+ raise "cannot handle device file"
when socket?
- raise "cannot handle socket" unless File.respond_to?(:mknod)
- mknod dest, nil, lstat().mode, 0
+ begin
+ require 'socket'
+ rescue LoadError
+ raise "cannot handle socket"
+ else
+ raise "cannot handle socket" unless defined?(UNIXServer)
+ end
+ UNIXServer.new(dest).close
+ File.chmod lstat().mode, dest
when pipe?
raise "cannot handle FIFO" unless File.respond_to?(:mkfifo)
- mkfifo dest, 0666
+ File.mkfifo dest, lstat().mode
when door?
raise "cannot handle door: #{path()}"
else
@@ -1251,19 +2274,39 @@ module FileUtils
end
def copy_file(dest)
- IO.copy_stream(path(), dest)
+ File.open(path()) do |s|
+ File.open(dest, 'wb', s.stat.mode) do |f|
+ IO.copy_stream(s, f)
+ end
+ end
end
def copy_metadata(path)
st = lstat()
- File.utime st.atime, st.mtime, path
+ if !st.symlink?
+ File.utime st.atime, st.mtime, path
+ end
+ mode = st.mode
begin
- File.chown st.uid, st.gid, path
- rescue Errno::EPERM
+ if st.symlink?
+ begin
+ File.lchown st.uid, st.gid, path
+ rescue NotImplementedError
+ end
+ else
+ File.chown st.uid, st.gid, path
+ end
+ rescue Errno::EPERM, Errno::EACCES
# clear setuid/setgid
- File.chmod st.mode & 01777, path
+ mode &= 01777
+ end
+ if st.symlink?
+ begin
+ File.lchmod mode, path
+ rescue NotImplementedError, Errno::EOPNOTSUPP
+ end
else
- File.chmod st.mode, path
+ File.chmod mode, path
end
end
@@ -1277,7 +2320,7 @@ module FileUtils
def remove_dir1
platform_support {
- Dir.rmdir path().sub(%r</\z>, '')
+ Dir.rmdir path().chomp(?/)
}
end
@@ -1319,7 +2362,16 @@ module FileUtils
def postorder_traverse
if directory?
- entries().each do |ent|
+ begin
+ children = entries()
+ rescue Errno::EACCES
+ # Failed to get the list of children.
+ # Assuming there is no children, try to process the parent directory.
+ yield self
+ return
+ end
+
+ children.each do |ent|
ent.postorder_traverse do |e|
yield e
end
@@ -1328,16 +2380,26 @@ module FileUtils
yield self
end
+ def wrap_traverse(pre, post)
+ pre.call self
+ if directory?
+ entries.each do |ent|
+ ent.wrap_traverse pre, post
+ end
+ end
+ post.call self
+ end
+
private
- $fileutils_rb_have_lchmod = nil
+ @@fileutils_rb_have_lchmod = nil
def have_lchmod?
# This is not MT-safe, but it does not matter.
- if $fileutils_rb_have_lchmod == nil
- $fileutils_rb_have_lchmod = check_have_lchmod?
+ if @@fileutils_rb_have_lchmod == nil
+ @@fileutils_rb_have_lchmod = check_have_lchmod?
end
- $fileutils_rb_have_lchmod
+ @@fileutils_rb_have_lchmod
end
def check_have_lchmod?
@@ -1348,14 +2410,14 @@ module FileUtils
return false
end
- $fileutils_rb_have_lchown = nil
+ @@fileutils_rb_have_lchown = nil
def have_lchown?
# This is not MT-safe, but it does not matter.
- if $fileutils_rb_have_lchown == nil
- $fileutils_rb_have_lchown = check_have_lchown?
+ if @@fileutils_rb_have_lchown == nil
+ @@fileutils_rb_have_lchown = check_have_lchown?
end
- $fileutils_rb_have_lchown
+ @@fileutils_rb_have_lchown
end
def check_have_lchown?
@@ -1369,7 +2431,29 @@ module FileUtils
def join(dir, base)
return File.path(dir) if not base or base == '.'
return File.path(base) if not dir or dir == '.'
- File.join(dir, base)
+ begin
+ File.join(dir, base)
+ rescue EncodingError
+ if fu_windows?
+ File.join(dir.encode(::Encoding::UTF_8), base.encode(::Encoding::UTF_8))
+ else
+ raise
+ end
+ end
+ end
+
+ if File::ALT_SEPARATOR
+ DIRECTORY_TERM = "(?=[/#{Regexp.quote(File::ALT_SEPARATOR)}]|\\z)"
+ else
+ DIRECTORY_TERM = "(?=/|\\z)"
+ end
+
+ def descendant_directory?(descendant, ascendant)
+ if File::FNM_SYSCASE.nonzero?
+ File.expand_path(File.dirname(descendant)).casecmp(File.expand_path(ascendant)) == 0
+ else
+ File.expand_path(File.dirname(descendant)) == File.expand_path(ascendant)
+ end
end
end # class Entry_
@@ -1386,15 +2470,19 @@ module FileUtils
end
private_module_function :fu_each_src_dest
- def fu_each_src_dest0(src, dest) #:nodoc:
+ def fu_each_src_dest0(src, dest, target_directory = true) #:nodoc:
if tmp = Array.try_convert(src)
+ unless target_directory or tmp.size <= 1
+ tmp = tmp.map {|f| File.path(f)} # A workaround for RBS
+ raise ArgumentError, "extra target #{tmp}"
+ end
tmp.each do |s|
s = File.path(s)
- yield s, File.join(dest, File.basename(s))
+ yield s, (target_directory ? File.join(dest, File.basename(s)) : dest)
end
else
src = File.path(src)
- if File.directory?(dest)
+ if target_directory and File.directory?(dest)
yield src, File.join(dest, File.basename(src))
else
yield src, File.path(dest)
@@ -1404,176 +2492,209 @@ module FileUtils
private_module_function :fu_each_src_dest0
def fu_same?(a, b) #:nodoc:
- if fu_have_st_ino?
- st1 = File.stat(a)
- st2 = File.stat(b)
- st1.dev == st2.dev and st1.ino == st2.ino
- else
- File.expand_path(a) == File.expand_path(b)
- end
- rescue Errno::ENOENT
- return false
+ File.identical?(a, b)
end
private_module_function :fu_same?
- def fu_have_st_ino? #:nodoc:
- not fu_windows?
+ def fu_output_message(msg) #:nodoc:
+ output = @fileutils_output if defined?(@fileutils_output)
+ output ||= $stdout
+ if defined?(@fileutils_label)
+ msg = @fileutils_label + msg
+ end
+ output.puts msg
end
- private_module_function :fu_have_st_ino?
+ private_module_function :fu_output_message
- def fu_check_options(options, optdecl) #:nodoc:
- h = options.dup
- optdecl.each do |opt|
- h.delete opt
+ def fu_split_path(path) #:nodoc:
+ path = File.path(path)
+ list = []
+ until (parent, base = File.split(path); parent == path or parent == ".")
+ if base != '..' and list.last == '..' and !(fu_have_symlink? && File.symlink?(path))
+ list.pop
+ else
+ list << base
+ end
+ path = parent
end
- raise ArgumentError, "no such option: #{h.keys.join(' ')}" unless h.empty?
+ list << path
+ list.reverse!
end
- private_module_function :fu_check_options
+ private_module_function :fu_split_path
- def fu_update_option(args, new) #:nodoc:
- if tmp = Hash.try_convert(args.last)
- args[-1] = tmp.dup.update(new)
- else
- args.push new
+ def fu_common_components(target, base) #:nodoc:
+ i = 0
+ while target[i]&.== base[i]
+ i += 1
end
- args
+ i
end
- private_module_function :fu_update_option
-
- @fileutils_output = $stderr
- @fileutils_label = ''
+ private_module_function :fu_common_components
+
+ def fu_clean_components(*comp) #:nodoc:
+ 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.sub!(%r((?<=\A|/)[^/]+/\z), "")
+ else
+ clean << c
+ path << c << "/"
+ end
+ end
+ clean
+ end
+ private_module_function :fu_clean_components
- def fu_output_message(msg) #:nodoc:
- @fileutils_output ||= $stderr
- @fileutils_label ||= ''
- @fileutils_output.puts @fileutils_label + msg
+ if fu_windows?
+ def fu_starting_path?(path) #:nodoc:
+ path&.start_with?(%r(\w:|/))
+ end
+ else
+ def fu_starting_path?(path) #:nodoc:
+ path&.start_with?("/")
+ end
end
- private_module_function :fu_output_message
+ private_module_function :fu_starting_path?
+ # This hash table holds command options.
+ OPT_TABLE = {} #:nodoc: internal use only
+ (private_instance_methods & methods(false)).inject(OPT_TABLE) {|tbl, name|
+ (tbl[name.to_s] = instance_method(name).parameters).map! {|t, n| n if t == :key}.compact!
+ tbl
+ }
+
+ public
+
+ # Returns an array of the string names of \FileUtils methods
+ # that accept one or more keyword arguments:
#
- # Returns an Array of method names which have any options.
- #
- # p FileUtils.commands #=> ["chmod", "cp", "cp_r", "install", ...]
+ # FileUtils.commands.sort.take(3) # => ["cd", "chdir", "chmod"]
#
- def FileUtils.commands
+ def self.commands
OPT_TABLE.keys
end
+ # Returns an array of the string keyword names:
#
- # Returns an Array of option names.
+ # FileUtils.options.take(3) # => ["noop", "verbose", "force"]
#
- # p FileUtils.options #=> ["noop", "force", "verbose", "preserve", "mode"]
- #
- def FileUtils.options
+ def self.options
OPT_TABLE.values.flatten.uniq.map {|sym| sym.to_s }
end
+ # Returns +true+ if method +mid+ accepts the given option +opt+, +false+ otherwise;
+ # the arguments may be strings or symbols:
#
- # Returns true if the method +mid+ have an option +opt+.
- #
- # p FileUtils.have_option?(:cp, :noop) #=> true
- # p FileUtils.have_option?(:rm, :force) #=> true
- # p FileUtils.have_option?(:rm, :perserve) #=> false
+ # FileUtils.have_option?(:chmod, :noop) # => true
+ # FileUtils.have_option?('chmod', 'secure') # => false
#
- def FileUtils.have_option?(mid, opt)
+ def self.have_option?(mid, opt)
li = OPT_TABLE[mid.to_s] or raise ArgumentError, "no such method: #{mid}"
li.include?(opt)
end
+ # Returns an array of the string keyword name for method +mid+;
+ # the argument may be a string or a symbol:
#
- # Returns an Array of option names of the method +mid+.
+ # FileUtils.options_of(:rm) # => ["force", "noop", "verbose"]
+ # FileUtils.options_of('mv') # => ["force", "noop", "verbose", "secure"]
#
- # p FileUtils.options(:rm) #=> ["noop", "verbose", "force"]
- #
- def FileUtils.options_of(mid)
+ def self.options_of(mid)
OPT_TABLE[mid.to_s].map {|sym| sym.to_s }
end
+ # Returns an array of the string method names of the methods
+ # that accept the given keyword option +opt+;
+ # the argument must be a symbol:
#
- # Returns an Array of method names which have the option +opt+.
- #
- # p FileUtils.collect_method(:preserve) #=> ["cp", "cp_r", "copy", "install"]
+ # FileUtils.collect_method(:preserve) # => ["cp", "copy", "cp_r", "install"]
#
- def FileUtils.collect_method(opt)
+ def self.collect_method(opt)
OPT_TABLE.keys.select {|m| OPT_TABLE[m].include?(opt) }
end
- METHODS = singleton_methods() - [:private_module_function,
+ private
+
+ LOW_METHODS = singleton_methods(false) - collect_method(:noop).map(&:intern) # :nodoc:
+ module LowMethods # :nodoc: internal use only
+ private
+ def _do_nothing(*)end
+ ::FileUtils::LOW_METHODS.map {|name| alias_method name, :_do_nothing}
+ end
+
+ METHODS = singleton_methods() - [:private_module_function, # :nodoc:
:commands, :options, :have_option?, :options_of, :collect_method]
- #
+ #
# This module has all methods of FileUtils module, but it outputs messages
# before acting. This equates to passing the <tt>:verbose</tt> flag to
# methods in FileUtils.
- #
+ #
module Verbose
include FileUtils
- @fileutils_output = $stderr
- @fileutils_label = ''
- ::FileUtils.collect_method(:verbose).each do |name|
+ names = ::FileUtils.collect_method(:verbose)
+ names.each do |name|
module_eval(<<-EOS, __FILE__, __LINE__ + 1)
- def #{name}(*args)
- super(*fu_update_option(args, :verbose => true))
+ def #{name}(*args, **options)
+ super(*args, **options, verbose: true)
end
- private :#{name}
EOS
end
+ private(*names)
extend self
class << self
- ::FileUtils::METHODS.each do |m|
- public m
- end
+ public(*::FileUtils::METHODS)
end
end
- #
+ #
# This module has all methods of FileUtils module, but never changes
# files/directories. This equates to passing the <tt>:noop</tt> flag
# to methods in FileUtils.
- #
+ #
module NoWrite
include FileUtils
- @fileutils_output = $stderr
- @fileutils_label = ''
- ::FileUtils.collect_method(:noop).each do |name|
+ include LowMethods
+ names = ::FileUtils.collect_method(:noop)
+ names.each do |name|
module_eval(<<-EOS, __FILE__, __LINE__ + 1)
- def #{name}(*args)
- super(*fu_update_option(args, :noop => true))
+ def #{name}(*args, **options)
+ super(*args, **options, noop: true)
end
- private :#{name}
EOS
end
+ private(*names)
extend self
class << self
- ::FileUtils::METHODS.each do |m|
- public m
- end
+ public(*::FileUtils::METHODS)
end
end
- #
+ #
# This module has all methods of FileUtils module, but never changes
# files/directories, with printing message before acting.
# This equates to passing the <tt>:noop</tt> and <tt>:verbose</tt> flag
# to methods in FileUtils.
- #
+ #
module DryRun
include FileUtils
- @fileutils_output = $stderr
- @fileutils_label = ''
- ::FileUtils.collect_method(:noop).each do |name|
+ include LowMethods
+ names = ::FileUtils.collect_method(:noop)
+ names.each do |name|
module_eval(<<-EOS, __FILE__, __LINE__ + 1)
- def #{name}(*args)
- super(*fu_update_option(args, :noop => true, :verbose => true))
+ def #{name}(*args, **options)
+ super(*args, **options, noop: true, verbose: true)
end
- private :#{name}
EOS
end
+ private(*names)
extend self
class << self
- ::FileUtils::METHODS.each do |m|
- public m
- end
+ public(*::FileUtils::METHODS)
end
end
diff --git a/lib/find.gemspec b/lib/find.gemspec
new file mode 100644
index 0000000000..aef24a5028
--- /dev/null
+++ b/lib/find.gemspec
@@ -0,0 +1,29 @@
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1).join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ['Kazuki Tsujimoto']
+ spec.email = ['kazuki@callcc.net']
+
+ spec.summary = %q{This module supports top-down traversal of a set of file paths.}
+ spec.description = %q{This module supports top-down traversal of a set of file paths.}
+ spec.homepage = "https://github.com/ruby/find"
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
+ end
+ spec.require_paths = ["lib"]
+end
diff --git a/lib/find.rb b/lib/find.rb
index 79ff7c1378..d9b81eb92d 100644
--- a/lib/find.rb
+++ b/lib/find.rb
@@ -1,77 +1,109 @@
+# frozen_string_literal: true
#
# find.rb: the Find module for processing all files under a given directory.
#
+# :markup: markdown
#
-# The +Find+ module supports the top-down traversal of a set of file paths.
-#
-# For example, to total the size of all files under your home directory,
-# ignoring anything in a "dot" directory (e.g. $HOME/.ssh):
-#
-# require 'find'
-#
-# total_size = 0
-#
-# Find.find(ENV["HOME"]) do |path|
-# if FileTest.directory?(path)
-# if File.basename(path)[0] == ?.
-# Find.prune # Don't look any further into this directory.
-# else
-# next
-# end
-# else
-# total_size += FileTest.size(path)
-# end
-# end
-#
+# \Module \Find supports the top-down traversal of entries in the file system.
module Find
+ # The version string
+ VERSION = "0.2.0"
+
+ # :markup: markdown
+ #
+ # With a block given, performs a depth-first traversal of each given path in `paths`;
+ # calls the block with each found file or directory path:
#
- # Calls the associated block with the name of every file and directory listed
- # as arguments, then recursively on their subdirectories, and so on.
+ # ```ruby
+ # paths = []
+ # Find.find('bin', 'jit') {|path| paths << path }
+ # paths
+ # # =>
+ # # ["bin",
+ # # "bin/gem",
+ # # "jit",
+ # # "jit/Cargo.toml",
+ # # "jit/src",
+ # # "jit/src/lib.rs"]
+ # ```
#
- # See the +Find+ module documentation for an example.
+ # Raises an exception if a given path cannot be read.
#
- def find(*paths) # :yield: path
- block_given? or return enum_for(__method__, *paths)
+ # When keyword argument `ignore_error` is given as `true` (the default),
+ # certain exceptions during traversal are ignored (i.e., silently rescued):
+ # Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG, Errno::EINVAL;
+ # when given as `false`, no exceptions are rescued.
+ #
+ # Note that these exceptions may be ignored only in `Find` traversal code;
+ # an exception raised before traversal begins,
+ # or raised while in the block is not ignored.
+ # Each of the calls below raises an Errno::ENOENT exception that is not ignored:
+ #
+ # ```ruby
+ # Find.find('nosuch') { }
+ # Find.find('lib') {|entry| raise Errno::ENOENT }
+ # ```
+ #
+ # With no block given, returns a new Enumerator.
+ def find(*paths, ignore_error: true) # :yield: path
+ block_given? or return enum_for(__method__, *paths, ignore_error: ignore_error)
- paths.collect!{|d| raise Errno::ENOENT unless File.exist?(d); d.dup}
- while file = paths.shift
- catch(:prune) do
- yield file.dup.taint
- next unless File.exist? file
- begin
- if File.lstat(file).directory? then
- d = Dir.open(file)
- begin
- for f in d
- next if f == "." or f == ".."
- if File::ALT_SEPARATOR and file =~ /^(?:[\/\\]|[A-Za-z]:[\/\\]?)$/ then
- f = file + f
- elsif file == "/" then
- f = "/" + f
- else
- f = File.join(file, f)
- end
- paths.unshift f.untaint
- end
- ensure
- d.close
- end
- end
- rescue Errno::ENOENT, Errno::EACCES
- end
+ fs_encoding = Encoding.find("filesystem")
+
+ paths.collect!{|d| raise Errno::ENOENT, d unless File.exist?(d); d.dup}.each do |path|
+ path = path.to_path if path.respond_to? :to_path
+ enc = path.encoding == Encoding::US_ASCII ? fs_encoding : path.encoding
+ ps = [path]
+ while file = ps.shift
+ catch(:prune) do
+ yield file.dup
+ begin
+ s = File.lstat(file)
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG, Errno::EINVAL
+ raise unless ignore_error
+ next
+ end
+ if s.directory? then
+ begin
+ fs = Dir.children(file, encoding: enc)
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG, Errno::EINVAL
+ raise unless ignore_error
+ next
+ end
+ fs.sort!
+ fs.reverse_each {|f|
+ f = File.join(file, f)
+ ps.unshift f
+ }
+ end
+ end
end
end
+ nil
end
+ # :markup: markdown
+ #
+ # call-seq:
+ # Find.prune
+ #
+ # This method is meaningful only within a block given with Find.find.
#
- # Skips the current file or directory, restarting the loop with the next
- # entry. If the current file is a directory, that directory will not be
- # recursively entered. Meaningful only within the block associated with
- # Find::find.
+ # Inside such a block,
+ # "prunes" the traversed file tree by not descending into the current directory:
#
- # See the +Find+ module documentation for an example.
+ # ```ruby
+ # files = []
+ # Find.find('.') do |path|
+ # Find.prune if File.basename(path) == 'test'
+ # next unless File.file?(path) && File.extname(path) == '.rb'
+ # files << path
+ # end
+ # files.size # => 6690
+ # files.take(3) # => ["./KNOWNBUGS.rb", "./array.rb", "./ast.rb"]
+ # ```
#
def prune
throw :prune
diff --git a/lib/forwardable.rb b/lib/forwardable.rb
index a4af0217d0..175d6d9c6b 100644
--- a/lib/forwardable.rb
+++ b/lib/forwardable.rb
@@ -1,66 +1,96 @@
+# frozen_string_literal: false
#
-# forwardable.rb -
-# $Release Version: 1.1$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ishitsuka.com)
-# original definition by delegator.rb
+# forwardable.rb -
+# $Release Version: 1.1$
+# $Revision$
+# by Keiju ISHITSUKA(keiju@ishitsuka.com)
+# original definition by delegator.rb
# Revised by Daniel J. Berger with suggestions from Florian Gross.
#
# Documentation by James Edward Gray II and Gavin Sinclair
+
+
+
+# The Forwardable module provides delegation of specified
+# methods to a designated object, using the methods #def_delegator
+# and #def_delegators.
#
-# == Introduction
+# For example, say you have a class RecordCollection which
+# contains an array <tt>@records</tt>. You could provide the lookup method
+# #record_number(), which simply calls #[] on the <tt>@records</tt>
+# array, like this:
#
-# This library allows you delegate method calls to an object, on a method by
-# method basis.
+# require 'forwardable'
#
-# == Notes
+# class RecordCollection
+# attr_accessor :records
+# extend Forwardable
+# def_delegator :@records, :[], :record_number
+# end
#
-# Be advised, RDoc will not detect delegated methods.
+# We can use the lookup method like so:
#
-# <b>forwardable.rb provides single-method delegation via the
-# def_delegator() and def_delegators() methods. For full-class
-# delegation via DelegateClass(), see delegate.rb.</b>
+# r = RecordCollection.new
+# r.records = [4,5,6]
+# r.record_number(0) # => 4
#
-# == Examples
+# Further, if you wish to provide the methods #size, #<<, and #map,
+# all of which delegate to @records, this is how you can do it:
+#
+# class RecordCollection # re-open RecordCollection class
+# def_delegators :@records, :size, :<<, :map
+# end
+#
+# r = RecordCollection.new
+# r.records = [1,2,3]
+# r.record_number(0) # => 1
+# r.size # => 3
+# r << 4 # => [1, 2, 3, 4]
+# r.map { |x| x * 2 } # => [2, 4, 6, 8]
+#
+# You can even extend regular objects with Forwardable.
+#
+# my_hash = Hash.new
+# my_hash.extend Forwardable # prepare object for delegation
+# my_hash.def_delegator "STDOUT", "puts" # add delegation for STDOUT.puts()
+# my_hash.puts "Howdy!"
#
-# === Forwardable
+# == Another example
#
-# Forwardable makes building a new class based on existing work, with a proper
-# interface, almost trivial. We want to rely on what has come before obviously,
-# but with delegation we can take just the methods we need and even rename them
-# as appropriate. In many cases this is preferable to inheritance, which gives
-# us the entire old interface, even if much of it isn't needed.
+# You could use Forwardable as an alternative to inheritance, when you don't want
+# to inherit all methods from the superclass. For instance, here is how you might
+# add a range of +Array+ instance methods to a new class +Queue+:
#
# class Queue
# extend Forwardable
-#
+#
# def initialize
# @q = [ ] # prepare delegate object
# end
-#
+#
# # setup preferred interface, enq() and deq()...
# def_delegator :@q, :push, :enq
# def_delegator :@q, :shift, :deq
-#
+#
# # support some general Array methods that fit Queues well
# def_delegators :@q, :clear, :first, :push, :shift, :size
# end
-#
-# q = Queue.new
+#
+# q = Thread::Queue.new
# q.enq 1, 2, 3, 4, 5
# q.push 6
-#
+#
# q.shift # => 1
# while q.size > 0
# puts q.deq
# end
-#
+#
# q.enq "Ruby", "Perl", "Python"
# puts q.first
# q.clear
# puts q.first
#
-# <i>Prints:</i>
+# This should output:
#
# 2
# 3
@@ -70,64 +100,46 @@
# Ruby
# nil
#
-# Forwardable can be used to setup delegation at the object level as well.
+# == Notes
#
-# printer = String.new
-# printer.extend Forwardable # prepare object for delegation
-# printer.def_delegator "STDOUT", "puts" # add delegation for STDOUT.puts()
-# printer.puts "Howdy!"
+# Be advised, RDoc will not detect delegated methods.
#
-# <i>Prints:</i>
+# +forwardable.rb+ provides single-method delegation via the def_delegator and
+# def_delegators methods. For full-class delegation via DelegateClass, see
+# +delegate.rb+.
#
-# Howdy!
+module Forwardable
+ # Version of +forwardable.rb+
+ VERSION = "1.4.0"
+ VERSION.freeze
-#
-# The Forwardable module provides delegation of specified
-# methods to a designated object, using the methods #def_delegator
-# and #def_delegators.
-#
-# For example, say you have a class RecordCollection which
-# contains an array <tt>@records</tt>. You could provide the lookup method
-# #record_number(), which simply calls #[] on the <tt>@records</tt>
-# array, like this:
-#
-# class RecordCollection
-# extend Forwardable
-# def_delegator :@records, :[], :record_number
-# end
-#
-# Further, if you wish to provide the methods #size, #<<, and #map,
-# all of which delegate to @records, this is how you can do it:
-#
-# class RecordCollection
-# # extend Forwardable, but we did that above
-# def_delegators :@records, :size, :<<, :map
-# end
-# f = Foo.new
-# f.printf ...
-# f.gets
-# f.content_at(1)
-#
-# Also see the example at forwardable.rb.
+ # Version for backward compatibility
+ FORWARDABLE_VERSION = VERSION
+ FORWARDABLE_VERSION.freeze
+
+ @debug = nil
+ class << self
+ # ignored
+ attr_accessor :debug
+ end
-module Forwardable
- FORWARDABLE_VERSION = "1.0.0"
-
# Takes a hash as its argument. The key is a symbol or an array of
- # symbols. These symbols correspond to method names. The value is
+ # symbols. These symbols correspond to method names, instance variable
+ # names, or constant names (see def_delegator). The value is
# the accessor to which the methods will be delegated.
#
# :call-seq:
# delegate method => accessor
# delegate [method, method, ...] => accessor
#
- def delegate(hash)
- hash.each{ |methods, accessor|
- methods = methods.to_s unless methods.respond_to?(:each)
- methods.each{ |method|
- def_instance_delegator(accessor, method)
- }
- }
+ def instance_delegate(hash)
+ hash.each do |methods, accessor|
+ unless defined?(methods.each)
+ def_instance_delegator(accessor, methods)
+ else
+ methods.each {|method| def_instance_delegator(accessor, method)}
+ end
+ end
end
#
@@ -142,36 +154,161 @@ module Forwardable
# def_delegator :@records, :map
#
def def_instance_delegators(accessor, *methods)
- methods.delete("__send__")
- methods.delete("__id__")
- methods.each{ |method|
+ methods.each do |method|
+ next if /\A__(?:send|id)__\z/ =~ method
def_instance_delegator(accessor, method)
- }
+ end
end
+ # Define +method+ as delegator instance method with an optional
+ # alias name +ali+. Method calls to +ali+ will be delegated to
+ # +accessor.method+. +accessor+ should be a method name, instance
+ # variable name, or constant name. Use the full path to the
+ # constant if providing the constant name.
+ # Returns the name of the method defined.
#
- # Defines a method _method_ which delegates to _obj_ (i.e. it calls
- # the method of the same name in _obj_). If _new_name_ is
- # provided, it is used as the name for the delegate method.
+ # class MyQueue
+ # CONST = 1
+ # extend Forwardable
+ # attr_reader :queue
+ # def initialize
+ # @queue = []
+ # end
+ #
+ # def_delegator :@queue, :push, :mypush
+ # def_delegator 'MyQueue::CONST', :to_i
+ # end
+ #
+ # q = MyQueue.new
+ # q.mypush 42
+ # q.queue #=> [42]
+ # q.push 23 #=> NoMethodError
+ # q.to_i #=> 1
#
def def_instance_delegator(accessor, method, ali = method)
- str = %Q{
- def #{ali}(*args, &block)
- #{accessor}.send(:#{method}, *args, &block)
- end
- }
+ gen = Forwardable._delegator_method(self, accessor, method, ali)
# If it's not a class or module, it's an instance
- begin
- module_eval(str)
- rescue
- instance_eval(str)
- end
+ mod = Module === self ? self : singleton_class
+ mod.module_eval(&gen)
end
+ alias delegate instance_delegate
alias def_delegators def_instance_delegators
alias def_delegator def_instance_delegator
+
+ # :nodoc:
+ def self._delegator_method(obj, accessor, method, ali)
+ accessor = accessor.to_s unless Symbol === accessor
+
+ if Module === obj ?
+ obj.method_defined?(accessor) || obj.private_method_defined?(accessor) :
+ obj.respond_to?(accessor, true)
+ accessor = "(#{accessor}())"
+ end
+
+ args = RUBY_VERSION >= '2.7' ? '...' : '*args, &block'
+ method_call = ".__send__(:#{method}, #{args})"
+ if method.match?(/\A[_a-zA-Z]\w*[?!]?\z/)
+ loc, = caller_locations(2,1)
+ pre = "_ ="
+ mesg = "#{Module === obj ? obj : obj.class}\##{ali} at #{loc.path}:#{loc.lineno} forwarding to private method "
+ method_call = <<~RUBY.chomp
+ if defined?(_.#{method})
+ _.#{method}(#{args})
+ else
+ ::Kernel.warn #{mesg.dump}"\#{_.class}"'##{method}', uplevel: 1
+ _#{method_call}
+ end
+ RUBY
+ end
+
+ eval(<<~RUBY, nil, __FILE__, __LINE__ + 1)
+ proc do
+ def #{ali}(#{args})
+ #{pre}#{accessor}
+ #{method_call}
+ end
+ end
+ RUBY
+ end
end
-# compatibility
-SingleForwardable = Forwardable
+# SingleForwardable can be used to setup delegation at the object level as well.
+#
+# printer = String.new
+# printer.extend SingleForwardable # prepare object for delegation
+# printer.def_delegator "STDOUT", "puts" # add delegation for STDOUT.puts()
+# printer.puts "Howdy!"
+#
+# Also, SingleForwardable can be used to set up delegation for a Class or Module.
+#
+# class Implementation
+# def self.service
+# puts "serviced!"
+# end
+# end
+#
+# module Facade
+# extend SingleForwardable
+# def_delegator :Implementation, :service
+# end
+#
+# Facade.service #=> serviced!
+#
+# If you want to use both Forwardable and SingleForwardable, you can
+# use methods def_instance_delegator and def_single_delegator, etc.
+module SingleForwardable
+ # Takes a hash as its argument. The key is a symbol or an array of
+ # symbols. These symbols correspond to method names. The value is
+ # the accessor to which the methods will be delegated.
+ #
+ # :call-seq:
+ # delegate method => accessor
+ # delegate [method, method, ...] => accessor
+ #
+ def single_delegate(hash)
+ hash.each do |methods, accessor|
+ unless defined?(methods.each)
+ def_single_delegator(accessor, methods)
+ else
+ methods.each {|method| def_single_delegator(accessor, method)}
+ end
+ end
+ end
+
+ #
+ # Shortcut for defining multiple delegator methods, but with no
+ # provision for using a different name. The following two code
+ # samples have the same effect:
+ #
+ # def_delegators :@records, :size, :<<, :map
+ #
+ # def_delegator :@records, :size
+ # def_delegator :@records, :<<
+ # def_delegator :@records, :map
+ #
+ def def_single_delegators(accessor, *methods)
+ methods.each do |method|
+ next if /\A__(?:send|id)__\z/ =~ method
+ def_single_delegator(accessor, method)
+ end
+ end
+
+ # :call-seq:
+ # def_single_delegator(accessor, method, new_name=method)
+ #
+ # Defines a method _method_ which delegates to _accessor_ (i.e. it calls
+ # the method of the same name in _accessor_). If _new_name_ is
+ # provided, it is used as the name for the delegate method.
+ # Returns the name of the method defined.
+ def def_single_delegator(accessor, method, ali = method)
+ gen = Forwardable._delegator_method(self, accessor, method, ali)
+
+ instance_eval(&gen)
+ end
+
+ alias delegate single_delegate
+ alias def_delegators def_single_delegators
+ alias def_delegator def_single_delegator
+end
diff --git a/lib/forwardable/forwardable.gemspec b/lib/forwardable/forwardable.gemspec
new file mode 100644
index 0000000000..1b539bcfcb
--- /dev/null
+++ b/lib/forwardable/forwardable.gemspec
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1, "..").join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Keiju ISHITSUKA"]
+ spec.email = ["keiju@ruby-lang.org"]
+
+ spec.summary = %q{Provides delegation of specified methods to a designated object.}
+ spec.description = %q{Provides delegation of specified methods to a designated object.}
+ spec.homepage = "https://github.com/ruby/forwardable"
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.required_ruby_version = '>= 2.4.0'
+ spec.files = ["forwardable.gemspec", "lib/forwardable.rb"]
+ spec.bindir = "exe"
+ spec.executables = []
+ spec.require_paths = ["lib"]
+end
diff --git a/lib/getoptlong.rb b/lib/getoptlong.rb
deleted file mode 100644
index 4cfb5fbd27..0000000000
--- a/lib/getoptlong.rb
+++ /dev/null
@@ -1,610 +0,0 @@
-#
-# GetoptLong for Ruby
-#
-# Copyright (C) 1998, 1999, 2000 Motoyuki Kasahara.
-#
-# You may redistribute and/or modify this library under the same license
-# terms as Ruby.
-#
-# See GetoptLong for documentation.
-#
-# Additional documents and the latest version of `getoptlong.rb' can be
-# found at http://www.sra.co.jp/people/m-kasahr/ruby/getoptlong/
-
-# The GetoptLong class allows you to parse command line options similarly to
-# the GNU getopt_long() C library call. Note, however, that GetoptLong is a
-# pure Ruby implementation.
-#
-# GetoptLong allows for POSIX-style options like <tt>--file</tt> as well
-# as single letter options like <tt>-f</tt>
-#
-# The empty option <tt>--</tt> (two minus symbols) is used to end option
-# processing. This can be particularly important if options have optional
-# arguments.
-#
-# Here is a simple example of usage:
-#
-# require 'getoptlong'
-# require 'rdoc/usage'
-#
-# opts = GetoptLong.new(
-# [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
-# [ '--repeat', '-n', GetoptLong::REQUIRED_ARGUMENT ],
-# [ '--name', GetoptLong::OPTIONAL_ARGUMENT ]
-# )
-#
-# dir = nil
-# name = nil
-# repetitions = 1
-# opts.each do |opt, arg|
-# case opt
-# when '--help'
-# puts <<-EOF
-# hello [OPTION] ... DIR
-#
-# -h, --help:
-# show help
-#
-# --repeat x, -n x:
-# repeat x times
-#
-# --name [name]:
-# greet user by name, if name not supplied default is John
-#
-# DIR: The directory in which to issue the greeting.
-# EOF
-# when '--repeat'
-# repetitions = arg.to_i
-# when '--name'
-# if arg == ''
-# name = 'John'
-# else
-# name = arg
-# end
-# end
-# end
-#
-# if ARGV.length != 1
-# puts "Missing dir argument (try --help)"
-# exit 0
-# end
-#
-# dir = ARGV.shift
-#
-# Dir.chdir(dir)
-# for i in (1..repetitions)
-# print "Hello"
-# if name
-# print ", #{name}"
-# end
-# puts
-# end
-#
-# Example command line:
-#
-# hello -n 6 --name -- /tmp
-#
-class GetoptLong
- #
- # Orderings.
- #
- ORDERINGS = [REQUIRE_ORDER = 0, PERMUTE = 1, RETURN_IN_ORDER = 2]
-
- #
- # Argument flags.
- #
- ARGUMENT_FLAGS = [NO_ARGUMENT = 0, REQUIRED_ARGUMENT = 1,
- OPTIONAL_ARGUMENT = 2]
-
- #
- # Status codes.
- #
- STATUS_YET, STATUS_STARTED, STATUS_TERMINATED = 0, 1, 2
-
- #
- # Error types.
- #
- class Error < StandardError; end
- class AmbiguousOption < Error; end
- class NeedlessArgument < Error; end
- class MissingArgument < Error; end
- class InvalidOption < Error; end
-
- #
- # Set up option processing.
- #
- # The options to support are passed to new() as an array of arrays.
- # Each sub-array contains any number of String option names which carry
- # the same meaning, and one of the following flags:
- #
- # GetoptLong::NO_ARGUMENT :: Option does not take an argument.
- #
- # GetoptLong::REQUIRED_ARGUMENT :: Option always takes an argument.
- #
- # GetoptLong::OPTIONAL_ARGUMENT :: Option may or may not take an argument.
- #
- # The first option name is considered to be the preferred (canonical) name.
- # Other than that, the elements of each sub-array can be in any order.
- #
- def initialize(*arguments)
- #
- # Current ordering.
- #
- if ENV.include?('POSIXLY_CORRECT')
- @ordering = REQUIRE_ORDER
- else
- @ordering = PERMUTE
- end
-
- #
- # Hash table of option names.
- # Keys of the table are option names, and their values are canonical
- # names of the options.
- #
- @canonical_names = Hash.new
-
- #
- # Hash table of argument flags.
- # Keys of the table are option names, and their values are argument
- # flags of the options.
- #
- @argument_flags = Hash.new
-
- #
- # Whether error messages are output to $stderr.
- #
- @quiet = FALSE
-
- #
- # Status code.
- #
- @status = STATUS_YET
-
- #
- # Error code.
- #
- @error = nil
-
- #
- # Error message.
- #
- @error_message = nil
-
- #
- # Rest of catenated short options.
- #
- @rest_singles = ''
-
- #
- # List of non-option-arguments.
- # Append them to ARGV when option processing is terminated.
- #
- @non_option_arguments = Array.new
-
- if 0 < arguments.length
- set_options(*arguments)
- end
- end
-
- #
- # Set the handling of the ordering of options and arguments.
- # A RuntimeError is raised if option processing has already started.
- #
- # The supplied value must be a member of GetoptLong::ORDERINGS. It alters
- # the processing of options as follows:
- #
- # <b>REQUIRE_ORDER</b> :
- #
- # Options are required to occur before non-options.
- #
- # Processing of options ends as soon as a word is encountered that has not
- # been preceded by an appropriate option flag.
- #
- # For example, if -a and -b are options which do not take arguments,
- # parsing command line arguments of '-a one -b two' would result in
- # 'one', '-b', 'two' being left in ARGV, and only ('-a', '') being
- # processed as an option/arg pair.
- #
- # This is the default ordering, if the environment variable
- # POSIXLY_CORRECT is set. (This is for compatibility with GNU getopt_long.)
- #
- # <b>PERMUTE</b> :
- #
- # Options can occur anywhere in the command line parsed. This is the
- # default behavior.
- #
- # Every sequence of words which can be interpreted as an option (with or
- # without argument) is treated as an option; non-option words are skipped.
- #
- # For example, if -a does not require an argument and -b optionally takes
- # an argument, parsing '-a one -b two three' would result in ('-a','') and
- # ('-b', 'two') being processed as option/arg pairs, and 'one','three'
- # being left in ARGV.
- #
- # If the ordering is set to PERMUTE but the environment variable
- # POSIXLY_CORRECT is set, REQUIRE_ORDER is used instead. This is for
- # compatibility with GNU getopt_long.
- #
- # <b>RETURN_IN_ORDER</b> :
- #
- # All words on the command line are processed as options. Words not
- # preceded by a short or long option flag are passed as arguments
- # with an option of '' (empty string).
- #
- # For example, if -a requires an argument but -b does not, a command line
- # of '-a one -b two three' would result in option/arg pairs of ('-a', 'one')
- # ('-b', ''), ('', 'two'), ('', 'three') being processed.
- #
- def ordering=(ordering)
- #
- # The method is failed if option processing has already started.
- #
- if @status != STATUS_YET
- set_error(ArgumentError, "argument error")
- raise RuntimeError,
- "invoke ordering=, but option processing has already started"
- end
-
- #
- # Check ordering.
- #
- if !ORDERINGS.include?(ordering)
- raise ArgumentError, "invalid ordering `#{ordering}'"
- end
- if ordering == PERMUTE && ENV.include?('POSIXLY_CORRECT')
- @ordering = REQUIRE_ORDER
- else
- @ordering = ordering
- end
- end
-
- #
- # Return ordering.
- #
- attr_reader :ordering
-
- #
- # Set options. Takes the same argument as GetoptLong.new.
- #
- # Raises a RuntimeError if option processing has already started.
- #
- def set_options(*arguments)
- #
- # The method is failed if option processing has already started.
- #
- if @status != STATUS_YET
- raise RuntimeError,
- "invoke set_options, but option processing has already started"
- end
-
- #
- # Clear tables of option names and argument flags.
- #
- @canonical_names.clear
- @argument_flags.clear
-
- arguments.each do |*arg|
- arg = arg.first # TODO: YARV Hack
- #
- # Find an argument flag and it set to `argument_flag'.
- #
- argument_flag = nil
- arg.each do |i|
- if ARGUMENT_FLAGS.include?(i)
- if argument_flag != nil
- raise ArgumentError, "too many argument-flags"
- end
- argument_flag = i
- end
- end
-
- raise ArgumentError, "no argument-flag" if argument_flag == nil
-
- canonical_name = nil
- arg.each do |i|
- #
- # Check an option name.
- #
- next if i == argument_flag
- begin
- if !i.is_a?(String) || i !~ /^-([^-]|-.+)$/
- raise ArgumentError, "an invalid option `#{i}'"
- end
- if (@canonical_names.include?(i))
- raise ArgumentError, "option redefined `#{i}'"
- end
- rescue
- @canonical_names.clear
- @argument_flags.clear
- raise
- end
-
- #
- # Register the option (`i') to the `@canonical_names' and
- # `@canonical_names' Hashes.
- #
- if canonical_name == nil
- canonical_name = i
- end
- @canonical_names[i] = canonical_name
- @argument_flags[i] = argument_flag
- end
- raise ArgumentError, "no option name" if canonical_name == nil
- end
- return self
- end
-
- #
- # Set/Unset `quiet' mode.
- #
- attr_writer :quiet
-
- #
- # Return the flag of `quiet' mode.
- #
- attr_reader :quiet
-
- #
- # `quiet?' is an alias of `quiet'.
- #
- alias quiet? quiet
-
- #
- # Explicitly terminate option processing.
- #
- def terminate
- return nil if @status == STATUS_TERMINATED
- raise RuntimeError, "an error has occured" if @error != nil
-
- @status = STATUS_TERMINATED
- @non_option_arguments.reverse_each do |argument|
- ARGV.unshift(argument)
- end
-
- @canonical_names = nil
- @argument_flags = nil
- @rest_singles = nil
- @non_option_arguments = nil
-
- return self
- end
-
- #
- # Returns true if option processing has terminated, false otherwise.
- #
- def terminated?
- return @status == STATUS_TERMINATED
- end
-
- #
- # Set an error (a protected method).
- #
- def set_error(type, message)
- $stderr.print("#{$0}: #{message}\n") if !@quiet
-
- @error = type
- @error_message = message
- @canonical_names = nil
- @argument_flags = nil
- @rest_singles = nil
- @non_option_arguments = nil
-
- raise type, message
- end
- protected :set_error
-
- #
- # Examine whether an option processing is failed.
- #
- attr_reader :error
-
- #
- # `error?' is an alias of `error'.
- #
- alias error? error
-
- # Return the appropriate error message in POSIX-defined format.
- # If no error has occurred, returns nil.
- #
- def error_message
- return @error_message
- end
-
- #
- # Get next option name and its argument, as an Array of two elements.
- #
- # The option name is always converted to the first (preferred)
- # name given in the original options to GetoptLong.new.
- #
- # Example: ['--option', 'value']
- #
- # Returns nil if the processing is complete (as determined by
- # STATUS_TERMINATED).
- #
- def get
- option_name, option_argument = nil, ''
-
- #
- # Check status.
- #
- return nil if @error != nil
- case @status
- when STATUS_YET
- @status = STATUS_STARTED
- when STATUS_TERMINATED
- return nil
- end
-
- #
- # Get next option argument.
- #
- if 0 < @rest_singles.length
- argument = '-' + @rest_singles
- elsif (ARGV.length == 0)
- terminate
- return nil
- elsif @ordering == PERMUTE
- while 0 < ARGV.length && ARGV[0] !~ /^-./
- @non_option_arguments.push(ARGV.shift)
- end
- if ARGV.length == 0
- terminate
- return nil
- end
- argument = ARGV.shift
- elsif @ordering == REQUIRE_ORDER
- if (ARGV[0] !~ /^-./)
- terminate
- return nil
- end
- argument = ARGV.shift
- else
- argument = ARGV.shift
- end
-
- #
- # Check the special argument `--'.
- # `--' indicates the end of the option list.
- #
- if argument == '--' && @rest_singles.length == 0
- terminate
- return nil
- end
-
- #
- # Check for long and short options.
- #
- if argument =~ /^(--[^=]+)/ && @rest_singles.length == 0
- #
- # This is a long style option, which start with `--'.
- #
- pattern = $1
- if @canonical_names.include?(pattern)
- option_name = pattern
- else
- #
- # The option `option_name' is not registered in `@canonical_names'.
- # It may be an abbreviated.
- #
- matches = []
- @canonical_names.each_key do |key|
- if key.index(pattern) == 0
- option_name = key
- matches << key
- end
- end
- if 2 <= matches.length
- set_error(AmbiguousOption, "option `#{argument}' is ambiguous between #{matches.join(', ')}")
- elsif matches.length == 0
- set_error(InvalidOption, "unrecognized option `#{argument}'")
- end
- end
-
- #
- # Check an argument to the option.
- #
- if @argument_flags[option_name] == REQUIRED_ARGUMENT
- if argument =~ /=(.*)$/
- option_argument = $1
- elsif 0 < ARGV.length
- option_argument = ARGV.shift
- else
- set_error(MissingArgument,
- "option `#{argument}' requires an argument")
- end
- elsif @argument_flags[option_name] == OPTIONAL_ARGUMENT
- if argument =~ /=(.*)$/
- option_argument = $1
- elsif 0 < ARGV.length && ARGV[0] !~ /^-./
- option_argument = ARGV.shift
- else
- option_argument = ''
- end
- elsif argument =~ /=(.*)$/
- set_error(NeedlessArgument,
- "option `#{option_name}' doesn't allow an argument")
- end
-
- elsif argument =~ /^(-(.))(.*)/
- #
- # This is a short style option, which start with `-' (not `--').
- # Short options may be catenated (e.g. `-l -g' is equivalent to
- # `-lg').
- #
- option_name, ch, @rest_singles = $1, $2, $3
-
- if @canonical_names.include?(option_name)
- #
- # The option `option_name' is found in `@canonical_names'.
- # Check its argument.
- #
- if @argument_flags[option_name] == REQUIRED_ARGUMENT
- if 0 < @rest_singles.length
- option_argument = @rest_singles
- @rest_singles = ''
- elsif 0 < ARGV.length
- option_argument = ARGV.shift
- else
- # 1003.2 specifies the format of this message.
- set_error(MissingArgument, "option requires an argument -- #{ch}")
- end
- elsif @argument_flags[option_name] == OPTIONAL_ARGUMENT
- if 0 < @rest_singles.length
- option_argument = @rest_singles
- @rest_singles = ''
- elsif 0 < ARGV.length && ARGV[0] !~ /^-./
- option_argument = ARGV.shift
- else
- option_argument = ''
- end
- end
- else
- #
- # This is an invalid option.
- # 1003.2 specifies the format of this message.
- #
- if ENV.include?('POSIXLY_CORRECT')
- set_error(InvalidOption, "invalid option -- #{ch}")
- else
- set_error(InvalidOption, "invalid option -- #{ch}")
- end
- end
- else
- #
- # This is a non-option argument.
- # Only RETURN_IN_ORDER falled into here.
- #
- return '', argument
- end
-
- return @canonical_names[option_name], option_argument
- end
-
- #
- # `get_option' is an alias of `get'.
- #
- alias get_option get
-
- # Iterator version of `get'.
- #
- # The block is called repeatedly with two arguments:
- # The first is the option name.
- # The second is the argument which followed it (if any).
- # Example: ('--opt', 'value')
- #
- # The option name is always converted to the first (preferred)
- # name given in the original options to GetoptLong.new.
- #
- def each
- loop do
- option_name, option_argument = get_option
- break if option_name == nil
- yield option_name, option_argument
- end
- end
-
- #
- # `each_option' is an alias of `each'.
- #
- alias each_option each
-end
diff --git a/lib/gserver.rb b/lib/gserver.rb
deleted file mode 100644
index 592e8661fe..0000000000
--- a/lib/gserver.rb
+++ /dev/null
@@ -1,253 +0,0 @@
-#
-# Copyright (C) 2001 John W. Small All Rights Reserved
-#
-# Author:: John W. Small
-# Documentation:: Gavin Sinclair
-# Licence:: Freeware.
-#
-# See the class GServer for documentation.
-#
-
-require "socket"
-require "thread"
-
-#
-# GServer implements a generic server, featuring thread pool management,
-# simple logging, and multi-server management. See HttpServer in
-# <tt>xmlrpc/httpserver.rb</tt> in the Ruby standard library for an example of
-# GServer in action.
-#
-# Any kind of application-level server can be implemented using this class.
-# It accepts multiple simultaneous connections from clients, up to an optional
-# maximum number. Several _services_ (i.e. one service per TCP port) can be
-# run simultaneously, and stopped at any time through the class method
-# <tt>GServer.stop(port)</tt>. All the threading issues are handled, saving
-# you the effort. All events are optionally logged, but you can provide your
-# own event handlers if you wish.
-#
-# === Example
-#
-# Using GServer is simple. Below we implement a simple time server, run it,
-# query it, and shut it down. Try this code in +irb+:
-#
-# require 'gserver'
-#
-# #
-# # A server that returns the time in seconds since 1970.
-# #
-# class TimeServer < GServer
-# def initialize(port=10001, *args)
-# super(port, *args)
-# end
-# def serve(io)
-# io.puts(Time.now.to_s)
-# end
-# end
-#
-# # Run the server with logging enabled (it's a separate thread).
-# server = TimeServer.new
-# server.audit = true # Turn logging on.
-# server.start
-#
-# # *** Now point your browser to http://localhost:10001 to see it working ***
-#
-# # See if it's still running.
-# GServer.in_service?(10001) # -> true
-# server.stopped? # -> false
-#
-# # Shut the server down gracefully.
-# server.shutdown
-#
-# # Alternatively, stop it immediately.
-# GServer.stop(10001)
-# # or, of course, "server.stop".
-#
-# All the business of accepting connections and exception handling is taken
-# care of. All we have to do is implement the method that actually serves the
-# client.
-#
-# === Advanced
-#
-# As the example above shows, the way to use GServer is to subclass it to
-# create a specific server, overriding the +serve+ method. You can override
-# other methods as well if you wish, perhaps to collect statistics, or emit
-# more detailed logging.
-#
-# connecting
-# disconnecting
-# starting
-# stopping
-#
-# The above methods are only called if auditing is enabled.
-#
-# You can also override +log+ and +error+ if, for example, you wish to use a
-# more sophisticated logging system.
-#
-class GServer
-
- DEFAULT_HOST = "127.0.0.1"
-
- def serve(io)
- end
-
- @@services = {} # Hash of opened ports, i.e. services
- @@servicesMutex = Mutex.new
-
- def GServer.stop(port, host = DEFAULT_HOST)
- @@servicesMutex.synchronize {
- @@services[host][port].stop
- }
- end
-
- def GServer.in_service?(port, host = DEFAULT_HOST)
- @@services.has_key?(host) and
- @@services[host].has_key?(port)
- end
-
- def stop
- @connectionsMutex.synchronize {
- if @tcpServerThread
- @tcpServerThread.raise "stop"
- end
- }
- end
-
- def stopped?
- @tcpServerThread == nil
- end
-
- def shutdown
- @shutdown = true
- end
-
- def connections
- @connections.size
- end
-
- def join
- @tcpServerThread.join if @tcpServerThread
- end
-
- attr_reader :port, :host, :maxConnections
- attr_accessor :stdlog, :audit, :debug
-
- def connecting(client)
- addr = client.peeraddr
- log("#{self.class.to_s} #{@host}:#{@port} client:#{addr[1]} " +
- "#{addr[2]}<#{addr[3]}> connect")
- true
- end
-
- def disconnecting(clientPort)
- log("#{self.class.to_s} #{@host}:#{@port} " +
- "client:#{clientPort} disconnect")
- end
-
- protected :connecting, :disconnecting
-
- def starting()
- log("#{self.class.to_s} #{@host}:#{@port} start")
- end
-
- def stopping()
- log("#{self.class.to_s} #{@host}:#{@port} stop")
- end
-
- protected :starting, :stopping
-
- def error(detail)
- log(detail.backtrace.join("\n"))
- end
-
- def log(msg)
- if @stdlog
- @stdlog.puts("[#{Time.new.ctime}] %s" % msg)
- @stdlog.flush
- end
- end
-
- protected :error, :log
-
- def initialize(port, host = DEFAULT_HOST, maxConnections = 4,
- stdlog = $stderr, audit = false, debug = false)
- @tcpServerThread = nil
- @port = port
- @host = host
- @maxConnections = maxConnections
- @connections = []
- @connectionsMutex = Mutex.new
- @connectionsCV = ConditionVariable.new
- @stdlog = stdlog
- @audit = audit
- @debug = debug
- end
-
- def start(maxConnections = -1)
- raise "running" if !stopped?
- @shutdown = false
- @maxConnections = maxConnections if maxConnections > 0
- @@servicesMutex.synchronize {
- if GServer.in_service?(@port,@host)
- raise "Port already in use: #{host}:#{@port}!"
- end
- @tcpServer = TCPServer.new(@host,@port)
- @port = @tcpServer.addr[1]
- @@services[@host] = {} unless @@services.has_key?(@host)
- @@services[@host][@port] = self;
- }
- @tcpServerThread = Thread.new {
- begin
- starting if @audit
- while !@shutdown
- @connectionsMutex.synchronize {
- while @connections.size >= @maxConnections
- @connectionsCV.wait(@connectionsMutex)
- end
- }
- client = @tcpServer.accept
- @connections << Thread.new(client) { |myClient|
- begin
- myPort = myClient.peeraddr[1]
- serve(myClient) if !@audit or connecting(myClient)
- rescue => detail
- error(detail) if @debug
- ensure
- begin
- myClient.close
- rescue
- end
- @connectionsMutex.synchronize {
- @connections.delete(Thread.current)
- @connectionsCV.signal
- }
- disconnecting(myPort) if @audit
- end
- }
- end
- rescue => detail
- error(detail) if @debug
- ensure
- begin
- @tcpServer.close
- rescue
- end
- if @shutdown
- @connectionsMutex.synchronize {
- while @connections.size > 0
- @connectionsCV.wait(@connectionsMutex)
- end
- }
- else
- @connections.each { |c| c.raise "stop" }
- end
- @tcpServerThread = nil
- @@servicesMutex.synchronize {
- @@services[@host].delete(@port)
- }
- stopping if @audit
- end
- }
- self
- end
-
-end
diff --git a/lib/ipaddr.gemspec b/lib/ipaddr.gemspec
new file mode 100644
index 0000000000..cabc9161ba
--- /dev/null
+++ b/lib/ipaddr.gemspec
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+# coding: utf-8
+
+if File.exist?(File.expand_path("ipaddr.gemspec"))
+ lib = File.expand_path("../lib", __FILE__)
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+
+ file = File.expand_path("ipaddr.rb", lib)
+else
+ # for ruby-core
+ file = File.expand_path("../ipaddr.rb", __FILE__)
+end
+
+version = File.foreach(file).find do |line|
+ /^\s*VERSION\s*=\s*["'](.*)["']/ =~ line and break $1
+end
+
+Gem::Specification.new do |spec|
+ spec.name = "ipaddr"
+ spec.version = version
+ spec.authors = ["Akinori MUSHA", "Hajimu UMEMOTO"]
+ spec.email = ["knu@idaemons.org", "ume@mahoroba.org"]
+
+ spec.summary = %q{A class to manipulate an IP address in ruby}
+ spec.description = <<-'DESCRIPTION'
+IPAddr provides a set of methods to manipulate an IP address.
+Both IPv4 and IPv6 are supported.
+ DESCRIPTION
+ spec.homepage = "https://github.com/ruby/ipaddr"
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.files = ["LICENSE.txt", "README.md", "lib/ipaddr.rb"]
+ spec.require_paths = ["lib"]
+
+ spec.required_ruby_version = ">= 2.4"
+end
diff --git a/lib/ipaddr.rb b/lib/ipaddr.rb
index 26364cd9ce..70b804f642 100644
--- a/lib/ipaddr.rb
+++ b/lib/ipaddr.rb
@@ -1,8 +1,9 @@
+# frozen_string_literal: true
#
# ipaddr.rb - A class to manipulate an IP address
#
# Copyright (c) 2002 Hajimu UMEMOTO <ume@mahoroba.org>.
-# Copyright (c) 2007 Akinori MUSHA <knu@iDaemons.org>.
+# Copyright (c) 2007, 2009, 2012 Akinori MUSHA <knu@iDaemons.org>.
# All rights reserved.
#
# You can redistribute and/or modify it under the same terms as Ruby.
@@ -17,97 +18,113 @@
#
require 'socket'
-unless Socket.const_defined? "AF_INET6"
- class Socket
- AF_INET6 = Object.new
- end
-
- class << IPSocket
- def valid_v4?(addr)
- if /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\Z/ =~ addr
- return $~.captures.all? {|i| i.to_i < 256}
- end
- return false
- end
-
- def valid_v6?(addr)
- # IPv6 (normal)
- return true if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*\Z/ =~ addr
- return true if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*)?\Z/ =~ addr
- return true if /\A::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*)?\Z/ =~ addr
- # IPv6 (IPv4 compat)
- return true if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:/ =~ addr && valid_v4?($')
- return true if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:)?/ =~ addr && valid_v4?($')
- return true if /\A::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:)?/ =~ addr && valid_v4?($')
-
- false
- end
-
- def valid?(addr)
- valid_v4?(addr) || valid_v6?(addr)
- end
-
- alias getaddress_orig getaddress
- def getaddress(s)
- if valid?(s)
- s
- elsif /\A[-A-Za-z\d.]+\Z/ =~ s
- getaddress_orig(s)
- else
- raise ArgumentError, "invalid address"
- end
- end
- end
-end
-
# IPAddr provides a set of methods to manipulate an IP address. Both IPv4 and
# IPv6 are supported.
#
# == Example
#
# require 'ipaddr'
-#
+#
# ipaddr1 = IPAddr.new "3ffe:505:2::1"
-#
-# p ipaddr1 #=> #<IPAddr: IPv6:3ffe:0505:0002:0000:0000:0000:0000:0001/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>
-#
-# p ipaddr1.to_s #=> "3ffe:505:2::1"
-#
-# ipaddr2 = ipaddr1.mask(48) #=> #<IPAddr: IPv6:3ffe:0505:0002:0000:0000:0000:0000:0000/ffff:ffff:ffff:0000:0000:0000:0000:0000>
-#
-# p ipaddr2.to_s #=> "3ffe:505:2::"
-#
+#
+# p ipaddr1 #=> #<IPAddr: IPv6:3ffe:0505:0002:0000:0000:0000:0000:0001/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>
+#
+# p ipaddr1.to_s #=> "3ffe:505:2::1"
+#
+# ipaddr2 = ipaddr1.mask(48) #=> #<IPAddr: IPv6:3ffe:0505:0002:0000:0000:0000:0000:0000/ffff:ffff:ffff:0000:0000:0000:0000:0000>
+#
+# p ipaddr2.to_s #=> "3ffe:505:2::"
+#
# ipaddr3 = IPAddr.new "192.168.2.0/24"
-#
-# p ipaddr3 #=> #<IPAddr: IPv4:192.168.2.0/255.255.255.0>
+#
+# p ipaddr3 #=> #<IPAddr: IPv4:192.168.2.0/255.255.255.0>
class IPAddr
+ # The version string
+ VERSION = "1.2.9"
+ # 32 bit mask for IPv4
IN4MASK = 0xffffffff
+ # 128 bit mask for IPv6
IN6MASK = 0xffffffffffffffffffffffffffffffff
- IN6FORMAT = (["%.4x"] * 8).join(':')
+ # Format string for IPv6
+ IN6FORMAT = (["%.4x"] * 8).join(':').freeze
+
+ # Regexp _internally_ used for parsing IPv4 address.
+ RE_IPV4ADDRLIKE = %r{
+ \A
+ \d+ \. \d+ \. \d+ \. \d+
+ \z
+ }x
+
+ # Regexp _internally_ used for parsing IPv6 address.
+ RE_IPV6ADDRLIKE_FULL = %r{
+ \A
+ (?:
+ (?: [\da-f]{1,4} : ){7} [\da-f]{1,4}
+ |
+ ( (?: [\da-f]{1,4} : ){6} )
+ (\d+) \. (\d+) \. (\d+) \. (\d+)
+ )
+ \z
+ }xi
+
+ # Regexp _internally_ used for parsing IPv6 address.
+ RE_IPV6ADDRLIKE_COMPRESSED = %r{
+ \A
+ ( (?: (?: [\da-f]{1,4} : )* [\da-f]{1,4} )? )
+ ::
+ ( (?:
+ ( (?: [\da-f]{1,4} : )* )
+ (?:
+ [\da-f]{1,4}
+ |
+ (\d+) \. (\d+) \. (\d+) \. (\d+)
+ )
+ )? )
+ \z
+ }xi
+
+ # Generic IPAddr related error. Exceptions raised in this class should
+ # inherit from Error.
+ class Error < ArgumentError; end
+
+ # Raised when the provided IP address is an invalid address.
+ class InvalidAddressError < Error; end
+
+ # Raised when the address family is invalid such as an address with an
+ # unsupported family, an address with an inconsistent family, or an address
+ # who's family cannot be determined.
+ class AddressFamilyError < Error; end
+
+ # Raised when the address is an invalid length.
+ class InvalidPrefixError < InvalidAddressError; end
# Returns the address family of this IP address.
attr_reader :family
# Creates a new ipaddr containing the given network byte ordered
# string form of an IP address.
- def IPAddr::new_ntoh(addr)
- return IPAddr.new(IPAddr::ntop(addr))
+ def self.new_ntoh(addr)
+ return new(ntop(addr))
end
# Convert a network byte ordered string form of an IP address into
# human readable form.
- def IPAddr::ntop(addr)
- case addr.size
+ # It expects the string to be encoded in Encoding::ASCII_8BIT (BINARY).
+ def self.ntop(addr)
+ if addr.is_a?(String) && addr.encoding != Encoding::BINARY
+ raise InvalidAddressError, "invalid encoding (given #{addr.encoding}, expected BINARY)"
+ end
+
+ case addr.bytesize
when 4
- s = addr.unpack('C4').join('.')
+ addr.unpack('C4').join('.')
when 16
- s = IN6FORMAT % addr.unpack('n8')
+ IN6FORMAT % addr.unpack('n8')
else
- raise ArgumentError, "unsupported address family"
+ raise AddressFamilyError, "unsupported address family"
end
- return s
end
# Returns a new ipaddr built by bitwise AND.
@@ -135,10 +152,27 @@ class IPAddr
return self.clone.set(addr_mask(~@addr))
end
+ # Returns a new ipaddr greater than the original address by offset
+ def +(offset)
+ self.clone.set(@addr + offset, @family)
+ end
+
+ # Returns a new ipaddr less than the original address by offset
+ def -(offset)
+ self.clone.set(@addr - offset, @family)
+ end
+
# Returns true if two ipaddrs are equal.
def ==(other)
+ if other.nil?
+ return false
+ end
+
other = coerce_other(other)
- return @family == other.family && @addr == other.to_i
+ rescue
+ false
+ else
+ @family == other.family && @addr == other.to_i
end
# Returns a new ipaddr built by masking IP address with the given
@@ -154,34 +188,15 @@ class IPAddr
# net1 = IPAddr.new("192.168.2.0/24")
# net2 = IPAddr.new("192.168.2.100")
# net3 = IPAddr.new("192.168.3.0")
- # p net1.include?(net2) #=> true
- # p net1.include?(net3) #=> false
+ # net4 = IPAddr.new("192.168.2.0/16")
+ # p net1.include?(net2) #=> true
+ # p net1.include?(net3) #=> false
+ # p net1.include?(net4) #=> false
+ # p net4.include?(net1) #=> true
def include?(other)
other = coerce_other(other)
- if ipv4_mapped?
- if (@mask_addr >> 32) != 0xffffffffffffffffffffffff
- return false
- end
- mask_addr = (@mask_addr & IN4MASK)
- addr = (@addr & IN4MASK)
- family = Socket::AF_INET
- else
- mask_addr = @mask_addr
- addr = @addr
- family = @family
- end
- if other.ipv4_mapped?
- other_addr = (other.to_i & IN4MASK)
- other_family = Socket::AF_INET
- else
- other_addr = other.to_i
- other_family = other.family
- end
-
- if family != other_family
- return false
- end
- return ((addr & mask_addr) == (other_addr & mask_addr))
+ return false unless other.family == family
+ begin_addr <= other.begin_addr && end_addr >= other.end_addr
end
alias === include?
@@ -197,7 +212,7 @@ class IPAddr
str.gsub!(/\b0{1,3}([\da-f]+)\b/i, '\1')
loop do
- break if str.sub!(/\A0:0:0:0:0:0:0:0\Z/, '::')
+ break if str.sub!(/\A0:0:0:0:0:0:0:0\z/, '::')
break if str.sub!(/\b0:0:0:0:0:0:0\b/, ':')
break if str.sub!(/\b0:0:0:0:0:0\b/, ':')
break if str.sub!(/\b0:0:0:0:0\b/, ':')
@@ -208,7 +223,7 @@ class IPAddr
end
str.sub!(/:{3,}/, '::')
- if /\A::(ffff:)?([\da-f]{1,4}):([\da-f]{1,4})\Z/i =~ str
+ if /\A::(ffff:)?([\da-f]{1,4}):([\da-f]{1,4})\z/i =~ str
str = sprintf('::%s%d.%d.%d.%d', $1, $2.hex / 256, $2.hex % 256, $3.hex / 256, $3.hex % 256)
end
@@ -218,7 +233,35 @@ class IPAddr
# Returns a string containing the IP address representation in
# canonical form.
def to_string
- return _to_string(@addr)
+ str = _to_string(@addr)
+
+ if @family == Socket::AF_INET6
+ str << zone_id.to_s
+ end
+
+ return str
+ end
+
+ # Returns a string containing the IP address representation with prefix.
+ def as_json(*)
+ if ipv4? && prefix == 32
+ to_s
+ elsif ipv6? && prefix == 128
+ to_s
+ else
+ cidr
+ end
+ end
+
+ # Returns a json string containing the IP address representation.
+ def to_json(*a)
+ %Q{"#{as_json(*a)}"}
+ end
+
+ # Returns a string containing the IP address representation in
+ # cidr notation
+ def cidr
+ "#{to_s}/#{prefix}"
end
# Returns a network byte ordered string form of the IP address.
@@ -228,10 +271,10 @@ class IPAddr
return [@addr].pack('N')
when Socket::AF_INET6
return (0..7).map { |i|
- (@addr >> (112 - 16 * i)) & 0xffff
+ (@addr >> (112 - 16 * i)) & 0xffff
}.pack('n8')
else
- raise "unsupported address family"
+ raise AddressFamilyError, "unsupported address family"
end
end
@@ -245,6 +288,65 @@ class IPAddr
return @family == Socket::AF_INET6
end
+ # Returns true if the ipaddr is a loopback address.
+ # Loopback IPv4 addresses in the IPv4-mapped IPv6
+ # address range are also considered as loopback addresses.
+ def loopback?
+ case @family
+ when Socket::AF_INET
+ @addr & 0xff000000 == 0x7f000000 # 127.0.0.1/8
+ when Socket::AF_INET6
+ @addr == 1 || # ::1
+ (@addr >> 32 == 0xffff && (
+ @addr & 0xff000000 == 0x7f000000 # ::ffff:127.0.0.1/8
+ ))
+ else
+ raise AddressFamilyError, "unsupported address family"
+ end
+ end
+
+ # Returns true if the ipaddr is a private address. IPv4 addresses
+ # in 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16 as defined in RFC
+ # 1918 and IPv6 Unique Local Addresses in fc00::/7 as defined in RFC
+ # 4193 are considered private. Private IPv4 addresses in the
+ # IPv4-mapped IPv6 address range are also considered private.
+ def private?
+ case @family
+ when Socket::AF_INET
+ @addr & 0xff000000 == 0x0a000000 || # 10.0.0.0/8
+ @addr & 0xfff00000 == 0xac100000 || # 172.16.0.0/12
+ @addr & 0xffff0000 == 0xc0a80000 # 192.168.0.0/16
+ when Socket::AF_INET6
+ @addr & 0xfe00_0000_0000_0000_0000_0000_0000_0000 == 0xfc00_0000_0000_0000_0000_0000_0000_0000 ||
+ (@addr >> 32 == 0xffff && (
+ @addr & 0xff000000 == 0x0a000000 || # ::ffff:10.0.0.0/8
+ @addr & 0xfff00000 == 0xac100000 || # ::ffff:172.16.0.0/12
+ @addr & 0xffff0000 == 0xc0a80000 # ::ffff:192.168.0.0/16
+ ))
+ else
+ raise AddressFamilyError, "unsupported address family"
+ end
+ end
+
+ # Returns true if the ipaddr is a link-local address. IPv4
+ # addresses in 169.254.0.0/16 reserved by RFC 3927 and link-local
+ # IPv6 Unicast Addresses in fe80::/10 reserved by RFC 4291 are
+ # considered link-local. Link-local IPv4 addresses in the
+ # IPv4-mapped IPv6 address range are also considered link-local.
+ def link_local?
+ case @family
+ when Socket::AF_INET
+ @addr & 0xffff0000 == 0xa9fe0000 # 169.254.0.0/16
+ when Socket::AF_INET6
+ @addr & 0xffc0_0000_0000_0000_0000_0000_0000_0000 == 0xfe80_0000_0000_0000_0000_0000_0000_0000 || # fe80::/10
+ (@addr >> 32 == 0xffff && (
+ @addr & 0xffff0000 == 0xa9fe0000 # ::ffff:169.254.0.0/16
+ ))
+ else
+ raise AddressFamilyError, "unsupported address family"
+ end
+ end
+
# Returns true if the ipaddr is an IPv4-mapped IPv6 address.
def ipv4_mapped?
return ipv6? && (@addr >> 32) == 0xffff
@@ -252,6 +354,11 @@ class IPAddr
# Returns true if the ipaddr is an IPv4-compatible IPv6 address.
def ipv4_compat?
+ warn "IPAddr\##{__callee__} is obsolete", uplevel: 1 if $VERBOSE
+ _ipv4_compat?
+ end
+
+ def _ipv4_compat? # :nodoc:
if !ipv6? || (@addr >> 32) != 0
return false
end
@@ -259,29 +366,36 @@ class IPAddr
return a != 0 && a != 1
end
+ private :_ipv4_compat?
+
# Returns a new ipaddr built by converting the native IPv4 address
# into an IPv4-mapped IPv6 address.
def ipv4_mapped
if !ipv4?
- raise ArgumentError, "not an IPv4 address"
+ raise InvalidAddressError, "not an IPv4 address: #{to_s}"
end
- return self.clone.set(@addr | 0xffff00000000, Socket::AF_INET6)
+ clone = self.clone.set(@addr | 0xffff00000000, Socket::AF_INET6)
+ clone.instance_variable_set(:@mask_addr, @mask_addr | 0xffffffffffffffffffffffff00000000)
+ clone
end
# Returns a new ipaddr built by converting the native IPv4 address
# into an IPv4-compatible IPv6 address.
def ipv4_compat
+ warn "IPAddr\##{__callee__} is obsolete", uplevel: 1 if $VERBOSE
if !ipv4?
- raise ArgumentError, "not an IPv4 address"
+ raise InvalidAddressError, "not an IPv4 address: #{to_s}"
end
- return self.clone.set(@addr, Socket::AF_INET6)
+ clone = self.clone.set(@addr, Socket::AF_INET6)
+ clone.instance_variable_set(:@mask_addr, @mask_addr | 0xffffffffffffffffffffffff00000000)
+ clone
end
# Returns a new ipaddr built by converting the IPv6 address into a
# native IPv4 address. If the IP address is not an IPv4-mapped or
# IPv4-compatible IPv6 address, returns self.
def native
- if !ipv4_mapped? && !ipv4_compat?
+ if !ipv4_mapped? && !_ipv4_compat?
return self
end
return self.clone.set(@addr & IN4MASK, Socket::AF_INET)
@@ -296,14 +410,14 @@ class IPAddr
when Socket::AF_INET6
return ip6_arpa
else
- raise "unsupported address family"
+ raise AddressFamilyError, "unsupported address family"
end
end
# Returns a string for DNS reverse lookup compatible with RFC3172.
def ip6_arpa
if !ipv6?
- raise ArgumentError, "not an IPv6 address"
+ raise InvalidAddressError, "not an IPv6 address: #{to_s}"
end
return _reverse + ".ip6.arpa"
end
@@ -311,7 +425,7 @@ class IPAddr
# Returns a string for DNS reverse lookup compatible with RFC1886.
def ip6_int
if !ipv6?
- raise ArgumentError, "not an IPv6 address"
+ raise InvalidAddressError, "not an IPv6 address: #{to_s}"
end
return _reverse + ".ip6.int"
end
@@ -324,27 +438,55 @@ class IPAddr
# Compares the ipaddr with another.
def <=>(other)
other = coerce_other(other)
+ rescue
+ nil
+ else
+ @addr <=> other.to_i if other.family == @family
+ end
+ include Comparable
- return nil if other.family != @family
+ # Checks equality used by Hash.
+ def eql?(other)
+ return self.class == other.class && self.hash == other.hash && self == other
+ end
- return @addr <=> other.to_i
+ # Returns a hash value used by Hash, Set, and Array classes
+ def hash
+ return ([@addr, @mask_addr, @zone_id].hash << 1) | (ipv4? ? 0 : 1)
end
- include Comparable
# Creates a Range object for the network address.
def to_range
- begin_addr = (@addr & @mask_addr)
+ self.class.new(begin_addr, @family)..self.class.new(end_addr, @family)
+ end
+ # Returns the prefix length in bits for the ipaddr.
+ def prefix
case @family
when Socket::AF_INET
- end_addr = (@addr | (IN4MASK ^ @mask_addr))
+ n = IN4MASK ^ @mask_addr
+ i = 32
when Socket::AF_INET6
- end_addr = (@addr | (IN6MASK ^ @mask_addr))
+ n = IN6MASK ^ @mask_addr
+ i = 128
else
- raise "unsupported address family"
+ raise AddressFamilyError, "unsupported address family"
end
+ while n.positive?
+ n >>= 1
+ i -= 1
+ end
+ i
+ end
- return clone.set(begin_addr, @family)..clone.set(end_addr, @family)
+ # Sets the prefix length in bits
+ def prefix=(prefix)
+ case prefix
+ when Integer
+ mask!(prefix)
+ else
+ raise InvalidPrefixError, "prefix must be an integer"
+ end
end
# Returns a string containing a human-readable representation of the
@@ -355,47 +497,124 @@ class IPAddr
af = "IPv4"
when Socket::AF_INET6
af = "IPv6"
+ zone_id = @zone_id.to_s
+ else
+ raise AddressFamilyError, "unsupported address family"
+ end
+ return sprintf("#<%s: %s:%s%s/%s>", self.class.name,
+ af, _to_string(@addr), zone_id, _to_string(@mask_addr))
+ end
+
+ # Returns the netmask in string format e.g. 255.255.0.0
+ def netmask
+ _to_string(@mask_addr)
+ end
+
+ # Returns the wildcard mask in string format e.g. 0.0.255.255
+ def wildcard_mask
+ case @family
+ when Socket::AF_INET
+ mask = IN4MASK ^ @mask_addr
+ when Socket::AF_INET6
+ mask = IN6MASK ^ @mask_addr
else
- raise "unsupported address family"
+ raise AddressFamilyError, "unsupported address family"
+ end
+
+ _to_string(mask)
+ end
+
+ # Returns the IPv6 zone identifier, if present.
+ # Raises InvalidAddressError if not an IPv6 address.
+ def zone_id
+ if @family == Socket::AF_INET6
+ @zone_id
+ else
+ raise InvalidAddressError, "not an IPv6 address"
+ end
+ end
+
+ # Returns the IPv6 zone identifier, if present.
+ # Raises InvalidAddressError if not an IPv6 address.
+ def zone_id=(zid)
+ if @family == Socket::AF_INET6
+ case zid
+ when nil, /\A%(\w+)\z/
+ @zone_id = zid
+ else
+ raise InvalidAddressError, "invalid zone identifier for address"
+ end
+ else
+ raise InvalidAddressError, "not an IPv6 address"
end
- return sprintf("#<%s: %s:%s/%s>", self.class.name,
- af, _to_string(@addr), _to_string(@mask_addr))
end
protected
+ # :stopdoc:
+
+ def begin_addr
+ @addr & @mask_addr
+ end
+
+ def end_addr
+ case @family
+ when Socket::AF_INET
+ @addr | (IN4MASK ^ @mask_addr)
+ when Socket::AF_INET6
+ @addr | (IN6MASK ^ @mask_addr)
+ else
+ raise AddressFamilyError, "unsupported address family"
+ end
+ end
+ #:startdoc:
+ # Set +@addr+, the internal stored ip address, to given +addr+. The
+ # parameter +addr+ is validated using the first +family+ member,
+ # which is +Socket::AF_INET+ or +Socket::AF_INET6+.
def set(addr, *family)
case family[0] ? family[0] : @family
when Socket::AF_INET
if addr < 0 || addr > IN4MASK
- raise ArgumentError, "invalid address"
+ raise InvalidAddressError, "invalid address: #{addr}"
end
when Socket::AF_INET6
if addr < 0 || addr > IN6MASK
- raise ArgumentError, "invalid address"
+ raise InvalidAddressError, "invalid address: #{addr}"
end
else
- raise ArgumentError, "unsupported address family"
+ raise AddressFamilyError, "unsupported address family"
end
@addr = addr
if family[0]
@family = family[0]
+ if @family == Socket::AF_INET
+ @mask_addr &= IN4MASK
+ end
end
return self
end
+ # Set current netmask to given mask.
def mask!(mask)
- if mask.kind_of?(String)
- if mask =~ /^\d+$/
- prefixlen = mask.to_i
+ case mask
+ when String
+ case mask
+ when /\A(0|[1-9]+\d*)\z/
+ prefixlen = mask.to_i
+ when /\A\d+\z/
+ raise InvalidPrefixError, "leading zeros in prefix"
else
- m = IPAddr.new(mask)
- if m.family != @family
- raise ArgumentError, "address family is not same"
- end
- @mask_addr = m.to_i
- @addr &= @mask_addr
- return self
+ m = IPAddr.new(mask)
+ if m.family != @family
+ raise InvalidPrefixError, "address family is not same"
+ end
+ @mask_addr = m.to_i
+ n = @mask_addr ^ m.instance_variable_get(:@mask_addr)
+ unless ((n + 1) & n).zero?
+ raise InvalidPrefixError, "invalid mask #{mask}"
+ end
+ @addr &= @mask_addr
+ return self
end
else
prefixlen = mask
@@ -403,18 +622,18 @@ class IPAddr
case @family
when Socket::AF_INET
if prefixlen < 0 || prefixlen > 32
- raise ArgumentError, "invalid length"
+ raise InvalidPrefixError, "invalid length"
end
masklen = 32 - prefixlen
@mask_addr = ((IN4MASK >> masklen) << masklen)
when Socket::AF_INET6
if prefixlen < 0 || prefixlen > 128
- raise ArgumentError, "invalid length"
+ raise InvalidPrefixError, "invalid length"
end
masklen = 128 - prefixlen
@mask_addr = ((IN6MASK >> masklen) << masklen)
else
- raise "unsupported address family"
+ raise AddressFamilyError, "unsupported address family"
end
@addr = ((@addr >> masklen) << masklen)
return self
@@ -425,7 +644,7 @@ class IPAddr
# Creates a new ipaddr object either from a human readable IP
# address representation in string, or from a packed in_addr value
# followed by an address family.
- #
+ #
# In the former case, the following are the valid formats that will
# be recognized: "address", "address/prefixlen" and "address/mask",
# where IPv6 address may be enclosed in square brackets (`[' and
@@ -433,14 +652,15 @@ class IPAddr
# IP address. Although the address family is determined
# automatically from a specified string, you can specify one
# explicitly by the optional second argument.
- #
- # Otherwise an IP addess is generated from a packed in_addr value
+ #
+ # Otherwise an IP address is generated from a packed in_addr value
# and an address family.
#
# The IPAddr class defines many methods and operators, and some of
# those, such as &, |, include? and ==, accept a string, or a packed
# in_addr value instead of an IPAddr object.
def initialize(addr = '::', family = Socket::AF_UNSPEC)
+ @mask_addr = nil
if !addr.kind_of?(String)
case family
when Socket::AF_INET, Socket::AF_INET6
@@ -448,37 +668,38 @@ class IPAddr
@mask_addr = (family == Socket::AF_INET) ? IN4MASK : IN6MASK
return
when Socket::AF_UNSPEC
- raise ArgumentError, "address family must be specified"
+ raise AddressFamilyError, "address family must be specified"
else
- raise ArgumentError, "unsupported address family: #{family}"
+ raise AddressFamilyError, "unsupported address family: #{family}"
end
end
- prefix, prefixlen = addr.split('/')
- if prefix =~ /^\[(.*)\]$/i
+ prefix, prefixlen = addr.split('/', 2)
+ if prefix =~ /\A\[(.*)\]\z/i
+ prefix = $1
+ family = Socket::AF_INET6
+ end
+ if prefix =~ /\A(.*)(%\w+)\z/
prefix = $1
+ zone_id = $2
family = Socket::AF_INET6
end
# It seems AI_NUMERICHOST doesn't do the job.
#Socket.getaddrinfo(left, nil, Socket::AF_INET6, Socket::SOCK_STREAM, nil,
- # Socket::AI_NUMERICHOST)
- begin
- IPSocket.getaddress(prefix) # test if address is vaild
- rescue
- raise ArgumentError, "invalid address"
- end
+ # Socket::AI_NUMERICHOST)
@addr = @family = nil
if family == Socket::AF_UNSPEC || family == Socket::AF_INET
@addr = in_addr(prefix)
if @addr
- @family = Socket::AF_INET
+ @family = Socket::AF_INET
end
end
if !@addr && (family == Socket::AF_UNSPEC || family == Socket::AF_INET6)
@addr = in6_addr(prefix)
@family = Socket::AF_INET6
end
+ @zone_id = zone_id
if family != Socket::AF_UNSPEC && @family != family
- raise ArgumentError, "address family mismatch"
+ raise AddressFamilyError, "address family mismatch"
end
if prefixlen
mask!(prefixlen)
@@ -487,6 +708,7 @@ class IPAddr
end
end
+ # :stopdoc:
def coerce_other(other)
case other
when IPAddr
@@ -499,26 +721,45 @@ class IPAddr
end
def in_addr(addr)
- if addr =~ /^\d+\.\d+\.\d+\.\d+$/
- return addr.split('.').inject(0) { |i, s|
- i << 8 | s.to_i
- }
+ case addr
+ when Array
+ octets = addr
+ else
+ RE_IPV4ADDRLIKE.match?(addr) or return nil
+ octets = addr.split('.')
end
- return nil
+ octets.inject(0) { |i, s|
+ (n = s.to_i) < 256 or raise InvalidAddressError, "invalid address: #{addr}"
+ (s != '0') && s.start_with?('0') and raise InvalidAddressError, "zero-filled number in IPv4 address is ambiguous: #{addr}"
+ i << 8 | n
+ }
end
def in6_addr(left)
case left
- when /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i
- return in_addr($1) + 0xffff00000000
- when /^::(\d+\.\d+\.\d+\.\d+)$/i
- return in_addr($1)
- when /[^0-9a-f:]/i
- raise ArgumentError, "invalid address"
- when /^(.*)::(.*)$/
- left, right = $1, $2
- else
+ when RE_IPV6ADDRLIKE_FULL
+ if $2
+ addr = in_addr($~[2,4])
+ left = $1 + ':'
+ else
+ addr = 0
+ end
right = ''
+ when RE_IPV6ADDRLIKE_COMPRESSED
+ if $4
+ left.count(':') <= 6 or raise InvalidAddressError, "invalid address: #{left}"
+ addr = in_addr($~[4,4])
+ left = $1
+ right = $3 + '0:0'
+ else
+ left.count(':') <= ($1.empty? || $2.empty? ? 8 : 7) or
+ raise InvalidAddressError, "invalid address: #{left}"
+ left = $1
+ right = $2
+ addr = 0
+ end
+ else
+ raise InvalidAddressError, "invalid address: #{left}"
end
l = left.split(':')
r = right.split(':')
@@ -526,9 +767,9 @@ class IPAddr
if rest < 0
return nil
end
- return (l + Array.new(rest, '0') + r).inject(0) { |i, s|
+ (l + Array.new(rest, '0') + r).inject(0) { |i, s|
i << 16 | s.hex
- }
+ } | addr
end
def addr_mask(addr)
@@ -538,7 +779,7 @@ class IPAddr
when Socket::AF_INET6
return addr & IN6MASK
else
- raise "unsupported address family"
+ raise AddressFamilyError, "unsupported address family"
end
end
@@ -546,12 +787,12 @@ class IPAddr
case @family
when Socket::AF_INET
return (0..3).map { |i|
- (@addr >> (8 * i)) & 0xff
+ (@addr >> (8 * i)) & 0xff
}.join('.')
when Socket::AF_INET6
return ("%.32x" % @addr).reverse!.gsub!(/.(?!$)/, '\&.')
else
- raise "unsupported address family"
+ raise AddressFamilyError, "unsupported address family"
end
end
@@ -559,255 +800,62 @@ class IPAddr
case @family
when Socket::AF_INET
return (0..3).map { |i|
- (addr >> (24 - 8 * i)) & 0xff
+ (addr >> (24 - 8 * i)) & 0xff
}.join('.')
when Socket::AF_INET6
return (("%.32x" % addr).gsub!(/.{4}(?!$)/, '\&:'))
else
- raise "unsupported address family"
+ raise AddressFamilyError, "unsupported address family"
end
end
end
-if $0 == __FILE__
- eval DATA.read, nil, $0, __LINE__+4
-end
-
-__END__
-
-require 'test/unit'
-
-class TC_IPAddr < Test::Unit::TestCase
- def test_s_new
- assert_nothing_raised {
- IPAddr.new("3FFE:505:ffff::/48")
- IPAddr.new("0:0:0:1::")
- IPAddr.new("2001:200:300::/48")
- }
-
- a = IPAddr.new
- assert_equal("::", a.to_s)
- assert_equal("0000:0000:0000:0000:0000:0000:0000:0000", a.to_string)
- assert_equal(Socket::AF_INET6, a.family)
-
- a = IPAddr.new("0123:4567:89ab:cdef:0ABC:DEF0:1234:5678")
- assert_equal("123:4567:89ab:cdef:abc:def0:1234:5678", a.to_s)
- assert_equal("0123:4567:89ab:cdef:0abc:def0:1234:5678", a.to_string)
- assert_equal(Socket::AF_INET6, a.family)
-
- a = IPAddr.new("3ffe:505:2::/48")
- assert_equal("3ffe:505:2::", a.to_s)
- assert_equal("3ffe:0505:0002:0000:0000:0000:0000:0000", a.to_string)
- assert_equal(Socket::AF_INET6, a.family)
- assert_equal(false, a.ipv4?)
- assert_equal(true, a.ipv6?)
- assert_equal("#<IPAddr: IPv6:3ffe:0505:0002:0000:0000:0000:0000:0000/ffff:ffff:ffff:0000:0000:0000:0000:0000>", a.inspect)
-
- a = IPAddr.new("3ffe:505:2::/ffff:ffff:ffff::")
- assert_equal("3ffe:505:2::", a.to_s)
- assert_equal("3ffe:0505:0002:0000:0000:0000:0000:0000", a.to_string)
- assert_equal(Socket::AF_INET6, a.family)
-
- a = IPAddr.new("0.0.0.0")
- assert_equal("0.0.0.0", a.to_s)
- assert_equal("0.0.0.0", a.to_string)
- assert_equal(Socket::AF_INET, a.family)
-
- a = IPAddr.new("192.168.1.2")
- assert_equal("192.168.1.2", a.to_s)
- assert_equal("192.168.1.2", a.to_string)
- assert_equal(Socket::AF_INET, a.family)
- assert_equal(true, a.ipv4?)
- assert_equal(false, a.ipv6?)
-
- a = IPAddr.new("192.168.1.2/24")
- assert_equal("192.168.1.0", a.to_s)
- assert_equal("192.168.1.0", a.to_string)
- assert_equal(Socket::AF_INET, a.family)
- assert_equal("#<IPAddr: IPv4:192.168.1.0/255.255.255.0>", a.inspect)
-
- a = IPAddr.new("192.168.1.2/255.255.255.0")
- assert_equal("192.168.1.0", a.to_s)
- assert_equal("192.168.1.0", a.to_string)
- assert_equal(Socket::AF_INET, a.family)
-
- assert_equal("0:0:0:1::", IPAddr.new("0:0:0:1::").to_s)
- assert_equal("2001:200:300::", IPAddr.new("2001:200:300::/48").to_s)
-
- assert_equal("2001:200:300::", IPAddr.new("[2001:200:300::]/48").to_s)
-
- [
- ["fe80::1%fxp0"],
- ["::1/255.255.255.0"],
- ["::1:192.168.1.2/120"],
- [IPAddr.new("::1").to_i],
- ["::ffff:192.168.1.2/120", Socket::AF_INET],
- ["[192.168.1.2]/120"],
- ].each { |args|
- assert_raises(ArgumentError) {
- IPAddr.new(*args)
- }
- }
- end
-
- def test_s_new_ntoh
- addr = ''
- IPAddr.new("1234:5678:9abc:def0:1234:5678:9abc:def0").hton.each_byte { |c|
- addr += sprintf("%02x", c)
- }
- assert_equal("123456789abcdef0123456789abcdef0", addr)
- addr = ''
- IPAddr.new("123.45.67.89").hton.each_byte { |c|
- addr += sprintf("%02x", c)
- }
- assert_equal(sprintf("%02x%02x%02x%02x", 123, 45, 67, 89), addr)
- a = IPAddr.new("3ffe:505:2::")
- assert_equal("3ffe:505:2::", IPAddr.new_ntoh(a.hton).to_s)
- a = IPAddr.new("192.168.2.1")
- assert_equal("192.168.2.1", IPAddr.new_ntoh(a.hton).to_s)
- end
-
- def test_ipv4_compat
- a = IPAddr.new("::192.168.1.2")
- assert_equal("::192.168.1.2", a.to_s)
- assert_equal("0000:0000:0000:0000:0000:0000:c0a8:0102", a.to_string)
- assert_equal(Socket::AF_INET6, a.family)
- assert_equal(true, a.ipv4_compat?)
- b = a.native
- assert_equal("192.168.1.2", b.to_s)
- assert_equal(Socket::AF_INET, b.family)
- assert_equal(false, b.ipv4_compat?)
-
- a = IPAddr.new("192.168.1.2")
- b = a.ipv4_compat
- assert_equal("::192.168.1.2", b.to_s)
- assert_equal(Socket::AF_INET6, b.family)
- end
-
- def test_ipv4_mapped
- a = IPAddr.new("::ffff:192.168.1.2")
- assert_equal("::ffff:192.168.1.2", a.to_s)
- assert_equal("0000:0000:0000:0000:0000:ffff:c0a8:0102", a.to_string)
- assert_equal(Socket::AF_INET6, a.family)
- assert_equal(true, a.ipv4_mapped?)
- b = a.native
- assert_equal("192.168.1.2", b.to_s)
- assert_equal(Socket::AF_INET, b.family)
- assert_equal(false, b.ipv4_mapped?)
-
- a = IPAddr.new("192.168.1.2")
- b = a.ipv4_mapped
- assert_equal("::ffff:192.168.1.2", b.to_s)
- assert_equal(Socket::AF_INET6, b.family)
- end
-
- def test_reverse
- assert_equal("f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.0.0.0.5.0.5.0.e.f.f.3.ip6.arpa", IPAddr.new("3ffe:505:2::f").reverse)
- assert_equal("1.2.168.192.in-addr.arpa", IPAddr.new("192.168.2.1").reverse)
- end
-
- def test_ip6_arpa
- assert_equal("f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.0.0.0.5.0.5.0.e.f.f.3.ip6.arpa", IPAddr.new("3ffe:505:2::f").ip6_arpa)
- assert_raises(ArgumentError) {
- IPAddr.new("192.168.2.1").ip6_arpa
- }
- end
-
- def test_ip6_int
- assert_equal("f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.0.0.0.5.0.5.0.e.f.f.3.ip6.int", IPAddr.new("3ffe:505:2::f").ip6_int)
- assert_raises(ArgumentError) {
- IPAddr.new("192.168.2.1").ip6_int
- }
- end
-
- def test_to_s
- assert_equal("3ffe:0505:0002:0000:0000:0000:0000:0001", IPAddr.new("3ffe:505:2::1").to_string)
- assert_equal("3ffe:505:2::1", IPAddr.new("3ffe:505:2::1").to_s)
- end
-end
-
-class TC_Operator < Test::Unit::TestCase
-
- IN6MASK32 = "ffff:ffff::"
- IN6MASK128 = "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"
-
- def setup
- @in6_addr_any = IPAddr.new()
- @a = IPAddr.new("3ffe:505:2::/48")
- @b = IPAddr.new("0:0:0:1::")
- @c = IPAddr.new(IN6MASK32)
- end
- alias set_up setup
-
- def test_or
- assert_equal("3ffe:505:2:1::", (@a | @b).to_s)
- a = @a
- a |= @b
- assert_equal("3ffe:505:2:1::", a.to_s)
- assert_equal("3ffe:505:2::", @a.to_s)
- assert_equal("3ffe:505:2:1::",
- (@a | 0x00000000000000010000000000000000).to_s)
+unless Socket.const_defined? :AF_INET6
+ class Socket < BasicSocket
+ # IPv6 protocol family
+ AF_INET6 = Object.new.freeze
end
- def test_and
- assert_equal("3ffe:505::", (@a & @c).to_s)
- a = @a
- a &= @c
- assert_equal("3ffe:505::", a.to_s)
- assert_equal("3ffe:505:2::", @a.to_s)
- assert_equal("3ffe:505::", (@a & 0xffffffff000000000000000000000000).to_s)
- end
-
- def test_shift_right
- assert_equal("0:3ffe:505:2::", (@a >> 16).to_s)
- a = @a
- a >>= 16
- assert_equal("0:3ffe:505:2::", a.to_s)
- assert_equal("3ffe:505:2::", @a.to_s)
- end
-
- def test_shift_left
- assert_equal("505:2::", (@a << 16).to_s)
- a = @a
- a <<= 16
- assert_equal("505:2::", a.to_s)
- assert_equal("3ffe:505:2::", @a.to_s)
- end
-
- def test_carrot
- a = ~@in6_addr_any
- assert_equal(IN6MASK128, a.to_s)
- assert_equal("::", @in6_addr_any.to_s)
- end
-
- def test_equal
- assert_equal(true, @a == IPAddr.new("3ffe:505:2::"))
- assert_equal(false, @a == IPAddr.new("3ffe:505:3::"))
- assert_equal(true, @a != IPAddr.new("3ffe:505:3::"))
- assert_equal(false, @a != IPAddr.new("3ffe:505:2::"))
- end
-
- def test_mask
- a = @a.mask(32)
- assert_equal("3ffe:505::", a.to_s)
- assert_equal("3ffe:505:2::", @a.to_s)
- end
+ class << IPSocket
+ private
+
+ def valid_v6?(addr) # :nodoc:
+ case addr
+ when IPAddr::RE_IPV6ADDRLIKE_FULL
+ if $2
+ $~[2,4].all? {|i| i.to_i < 256 }
+ else
+ true
+ end
+ when IPAddr::RE_IPV6ADDRLIKE_COMPRESSED
+ if $4
+ addr.count(':') <= 6 && $~[4,4].all? {|i| i.to_i < 256}
+ else
+ addr.count(':') <= 7
+ end
+ else
+ false
+ end
+ end
- def test_include?
- assert_equal(true, @a.include?(IPAddr.new("3ffe:505:2::")))
- assert_equal(true, @a.include?(IPAddr.new("3ffe:505:2::1")))
- assert_equal(false, @a.include?(IPAddr.new("3ffe:505:3::")))
- net1 = IPAddr.new("192.168.2.0/24")
- assert_equal(true, net1.include?(IPAddr.new("192.168.2.0")))
- assert_equal(true, net1.include?(IPAddr.new("192.168.2.255")))
- assert_equal(false, net1.include?(IPAddr.new("192.168.3.0")))
- # test with integer parameter
- int = (192 << 24) + (168 << 16) + (2 << 8) + 13
+ alias getaddress_orig getaddress
- assert_equal(true, net1.include?(int))
- assert_equal(false, net1.include?(int+255))
+ public
+ # Returns a +String+ based representation of a valid DNS hostname,
+ # IPv4 or IPv6 address.
+ #
+ # IPSocket.getaddress 'localhost' #=> "::1"
+ # IPSocket.getaddress 'broadcasthost' #=> "255.255.255.255"
+ # IPSocket.getaddress 'www.ruby-lang.org' #=> "221.186.184.68"
+ # IPSocket.getaddress 'www.ccc.de' #=> "2a00:1328:e102:ccc0::122"
+ def getaddress(s)
+ if valid_v6?(s)
+ s
+ else
+ getaddress_orig(s)
+ end
+ end
end
-
end
diff --git a/lib/irb.rb b/lib/irb.rb
deleted file mode 100644
index f5e662ac51..0000000000
--- a/lib/irb.rb
+++ /dev/null
@@ -1,346 +0,0 @@
-#
-# irb.rb - irb main module
-# $Release Version: 0.9.5 $
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-require "e2mmap"
-
-require "irb/init"
-require "irb/context"
-require "irb/extend-command"
-#require "irb/workspace"
-
-require "irb/ruby-lex"
-require "irb/input-method"
-require "irb/locale"
-
-STDOUT.sync = true
-
-module IRB
- @RCS_ID='-$Id$-'
-
- class Abort < Exception;end
-
- #
- @CONF = {}
-
- def IRB.conf
- @CONF
- end
-
- # IRB version method
- def IRB.version
- if v = @CONF[:VERSION] then return v end
-
- require "irb/version"
- rv = @RELEASE_VERSION.sub(/\.0/, "")
- @CONF[:VERSION] = format("irb %s(%s)", rv, @LAST_UPDATE_DATE)
- end
-
- def IRB.CurrentContext
- IRB.conf[:MAIN_CONTEXT]
- end
-
- # initialize IRB and start TOP_LEVEL irb
- def IRB.start(ap_path = nil)
- $0 = File::basename(ap_path, ".rb") if ap_path
-
- IRB.setup(ap_path)
-
- if @CONF[:SCRIPT]
- irb = Irb.new(nil, @CONF[:SCRIPT])
- else
- irb = Irb.new
- end
-
- @CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC]
- @CONF[:MAIN_CONTEXT] = irb.context
-
- trap("SIGINT") do
- irb.signal_handle
- end
-
- catch(:IRB_EXIT) do
- irb.eval_input
- end
-# print "\n"
- end
-
- def IRB.irb_exit(irb, ret)
- throw :IRB_EXIT, ret
- end
-
- def IRB.irb_abort(irb, exception = Abort)
- if defined? Thread
- irb.context.thread.raise exception, "abort then interrupt!!"
- else
- raise exception, "abort then interrupt!!"
- end
- end
-
- #
- # irb interpreter main routine
- #
- class Irb
- def initialize(workspace = nil, input_method = nil, output_method = nil)
- @context = Context.new(self, workspace, input_method, output_method)
- @context.main.extend ExtendCommandBundle
- @signal_status = :IN_IRB
-
- @scanner = RubyLex.new
- @scanner.exception_on_syntax_error = false
- end
- attr_reader :context
- attr_accessor :scanner
-
- def eval_input
- @scanner.set_prompt do
- |ltype, indent, continue, line_no|
- if ltype
- f = @context.prompt_s
- elsif continue
- f = @context.prompt_c
- elsif indent > 0
- f = @context.prompt_n
- else
- f = @context.prompt_i
- end
- f = "" unless f
- if @context.prompting?
- @context.io.prompt = p = prompt(f, ltype, indent, line_no)
- else
- @context.io.prompt = p = ""
- end
- if @context.auto_indent_mode
- unless ltype
- ind = prompt(@context.prompt_i, ltype, indent, line_no)[/.*\z/].size +
- indent * 2 - p.size
- ind += 2 if continue
- @context.io.prompt = p + " " * ind if ind > 0
- end
- end
- end
-
- @scanner.set_input(@context.io) do
- signal_status(:IN_INPUT) do
- if l = @context.io.gets
- print l if @context.verbose?
- else
- if @context.ignore_eof? and @context.io.readable_atfer_eof?
- l = "\n"
- if @context.verbose?
- printf "Use \"exit\" to leave %s\n", @context.ap_name
- end
- end
- end
- l
- end
- end
-
- @scanner.each_top_level_statement do |line, line_no|
- signal_status(:IN_EVAL) do
- begin
- line.untaint
- @context.evaluate(line, line_no)
- output_value if @context.echo?
- exc = nil
- rescue Interrupt => exc
- rescue SystemExit, SignalException
- raise
- rescue Exception => exc
- end
- if exc
- print exc.class, ": ", exc, "\n"
- if exc.backtrace[0] =~ /irb(2)?(\/.*|-.*|\.rb)?:/ && exc.class.to_s !~ /^IRB/ &&
- !(SyntaxError === exc)
- irb_bug = true
- else
- irb_bug = false
- end
-
- messages = []
- lasts = []
- levels = 0
- for m in exc.backtrace
- m = @context.workspace.filter_backtrace(m) unless irb_bug
- if m
- if messages.size < @context.back_trace_limit
- messages.push "\tfrom "+m
- else
- lasts.push "\tfrom "+m
- if lasts.size > @context.back_trace_limit
- lasts.shift
- levels += 1
- end
- end
- end
- end
- print messages.join("\n"), "\n"
- unless lasts.empty?
- printf "... %d levels...\n", levels if levels > 0
- print lasts.join("\n")
- end
- print "Maybe IRB bug!!\n" if irb_bug
- end
- if $SAFE > 2
- abort "Error: irb does not work for $SAFE level higher than 2"
- end
- end
- end
- end
-
- def suspend_name(path = nil, name = nil)
- @context.irb_path, back_path = path, @context.irb_path if path
- @context.irb_name, back_name = name, @context.irb_name if name
- begin
- yield back_path, back_name
- ensure
- @context.irb_path = back_path if path
- @context.irb_name = back_name if name
- end
- end
-
- def suspend_workspace(workspace)
- @context.workspace, back_workspace = workspace, @context.workspace
- begin
- yield back_workspace
- ensure
- @context.workspace = back_workspace
- end
- end
-
- def suspend_input_method(input_method)
- back_io = @context.io
- @context.instance_eval{@io = input_method}
- begin
- yield back_io
- ensure
- @context.instance_eval{@io = back_io}
- end
- end
-
- def suspend_context(context)
- @context, back_context = context, @context
- begin
- yield back_context
- ensure
- @context = back_context
- end
- end
-
- def signal_handle
- unless @context.ignore_sigint?
- print "\nabort!!\n" if @context.verbose?
- exit
- end
-
- case @signal_status
- when :IN_INPUT
- print "^C\n"
- raise RubyLex::TerminateLineInput
- when :IN_EVAL
- IRB.irb_abort(self)
- when :IN_LOAD
- IRB.irb_abort(self, LoadAbort)
- when :IN_IRB
- # ignore
- else
- # ignore other cases as well
- end
- end
-
- def signal_status(status)
- return yield if @signal_status == :IN_LOAD
-
- signal_status_back = @signal_status
- @signal_status = status
- begin
- yield
- ensure
- @signal_status = signal_status_back
- end
- end
-
- def prompt(prompt, ltype, indent, line_no)
- p = prompt.dup
- p.gsub!(/%([0-9]+)?([a-zA-Z])/) do
- case $2
- when "N"
- @context.irb_name
- when "m"
- @context.main.to_s
- when "M"
- @context.main.inspect
- when "l"
- ltype
- when "i"
- if $1
- format("%" + $1 + "d", indent)
- else
- indent.to_s
- end
- when "n"
- if $1
- format("%" + $1 + "d", line_no)
- else
- line_no.to_s
- end
- when "%"
- "%"
- end
- end
- p
- end
-
- def output_value
- if @context.inspect?
- printf @context.return_format, @context.last_value.inspect
- else
- printf @context.return_format, @context.last_value
- end
- end
-
- def inspect
- ary = []
- for iv in instance_variables
- case (iv = iv.to_s)
- when "@signal_status"
- ary.push format("%s=:%s", iv, @signal_status.id2name)
- when "@context"
- ary.push format("%s=%s", iv, eval(iv).__to_s__)
- else
- ary.push format("%s=%s", iv, eval(iv))
- end
- end
- format("#<%s: %s>", self.class, ary.join(", "))
- end
- end
-
- # Singleton method
- def @CONF.inspect
- IRB.version unless self[:VERSION]
-
- array = []
- for k, v in sort{|a1, a2| a1[0].id2name <=> a2[0].id2name}
- case k
- when :MAIN_CONTEXT, :__TMP__EHV__
- array.push format("CONF[:%s]=...myself...", k.id2name)
- when :PROMPT
- s = v.collect{
- |kk, vv|
- ss = vv.collect{|kkk, vvv| ":#{kkk.id2name}=>#{vvv.inspect}"}
- format(":%s=>{%s}", kk.id2name, ss.join(", "))
- }
- array.push format("CONF[:%s]={%s}", k.id2name, s.join(", "))
- else
- array.push format("CONF[:%s]=%s", k.id2name, v.inspect)
- end
- end
- array.join("\n")
- end
-end
diff --git a/lib/irb/cmd/chws.rb b/lib/irb/cmd/chws.rb
deleted file mode 100644
index c2db7e5d91..0000000000
--- a/lib/irb/cmd/chws.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-#
-# change-ws.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "irb/cmd/nop.rb"
-require "irb/ext/change-ws.rb"
-
-module IRB
- module ExtendCommand
-
- class CurrentWorkingWorkspace<Nop
- def execute(*obj)
- irb_context.main
- end
- end
-
- class ChangeWorkspace<Nop
- def execute(*obj)
- irb_context.change_workspace(*obj)
- irb_context.main
- end
- end
- end
-end
-
diff --git a/lib/irb/cmd/fork.rb b/lib/irb/cmd/fork.rb
deleted file mode 100644
index 6f4133c047..0000000000
--- a/lib/irb/cmd/fork.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-#
-# fork.rb -
-# $Release Version: 0.9.5 $
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-@RCS_ID='-$Id$-'
-
-
-module IRB
- module ExtendCommand
- class Fork<Nop
- def execute(&block)
- pid = send ExtendCommand.irb_original_method_name("fork")
- unless pid
- class<<self
- alias_method :exit, ExtendCommand.irb_original_method_name('exit')
- end
- if iterator?
- begin
- yield
- ensure
- exit
- end
- end
- end
- pid
- end
- end
- end
-end
-
-
diff --git a/lib/irb/cmd/help.rb b/lib/irb/cmd/help.rb
deleted file mode 100644
index e1f47e2c97..0000000000
--- a/lib/irb/cmd/help.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-#
-# help.rb - helper using ri
-# $Release Version: 0.9.5$
-# $Revision$
-#
-# --
-#
-#
-#
-
-require 'rdoc/ri/driver'
-require 'rdoc/ri/util'
-
-module IRB
- module ExtendCommand
- module Help
- begin
- @ri = RDoc::RI::Driver.new
- rescue SystemExit
- else
- def self.execute(context, *names)
- names.each do |name|
- begin
- @ri.get_info_for(name.to_s)
- rescue RDoc::RI::Error
- puts $!.message
- end
- end
- nil
- end
- end
- end
- end
-end
diff --git a/lib/irb/cmd/load.rb b/lib/irb/cmd/load.rb
deleted file mode 100644
index cda9a053fe..0000000000
--- a/lib/irb/cmd/load.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-#
-# load.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "irb/cmd/nop.rb"
-require "irb/ext/loader"
-
-module IRB
- module ExtendCommand
- class Load<Nop
- include IrbLoader
-
- def execute(file_name, priv = nil)
-# return ruby_load(file_name) unless IRB.conf[:USE_LOADER]
- return irb_load(file_name, priv)
- end
- end
-
- class Require<Nop
- include IrbLoader
-
- def execute(file_name)
-# return ruby_require(file_name) unless IRB.conf[:USE_LOADER]
-
- rex = Regexp.new("#{Regexp.quote(file_name)}(\.o|\.rb)?")
- return false if $".find{|f| f =~ rex}
-
- case file_name
- when /\.rb$/
- begin
- if irb_load(file_name)
- $".push file_name
- return true
- end
- rescue LoadError
- end
- when /\.(so|o|sl)$/
- return ruby_require(file_name)
- end
-
- begin
- irb_load(f = file_name + ".rb")
- $".push f
- return true
- rescue LoadError
- return ruby_require(file_name)
- end
- end
- end
-
- class Source<Nop
- include IrbLoader
- def execute(file_name)
- source_file(file_name)
- end
- end
- end
-
-end
diff --git a/lib/irb/cmd/nop.rb b/lib/irb/cmd/nop.rb
deleted file mode 100644
index 0b68098d4f..0000000000
--- a/lib/irb/cmd/nop.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-#
-# nop.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-module IRB
- module ExtendCommand
- class Nop
-
- @RCS_ID='-$Id$-'
-
- def self.execute(conf, *opts)
- command = new(conf)
- command.execute(*opts)
- end
-
- def initialize(conf)
- @irb_context = conf
- end
-
- attr_reader :irb_context
-
- def irb
- @irb_context.irb
- end
-
- def execute(*opts)
- #nop
- end
- end
- end
-end
-
diff --git a/lib/irb/cmd/pushws.rb b/lib/irb/cmd/pushws.rb
deleted file mode 100644
index b5b41501af..0000000000
--- a/lib/irb/cmd/pushws.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-#
-# change-ws.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "irb/cmd/nop.rb"
-require "irb/ext/workspaces.rb"
-
-module IRB
- module ExtendCommand
- class Workspaces<Nop
- def execute(*obj)
- irb_context.workspaces.collect{|ws| ws.main}
- end
- end
-
- class PushWorkspace<Workspaces
- def execute(*obj)
- irb_context.push_workspace(*obj)
- super
- end
- end
-
- class PopWorkspace<Workspaces
- def execute(*obj)
- irb_context.pop_workspace(*obj)
- super
- end
- end
- end
-end
-
diff --git a/lib/irb/cmd/subirb.rb b/lib/irb/cmd/subirb.rb
deleted file mode 100644
index 5eccf9f2c1..0000000000
--- a/lib/irb/cmd/subirb.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/local/bin/ruby
-#
-# multi.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "irb/cmd/nop.rb"
-require "irb/ext/multi-irb"
-
-module IRB
- module ExtendCommand
- class IrbCommand<Nop
- def execute(*obj)
- IRB.irb(nil, *obj)
- end
- end
-
- class Jobs<Nop
- def execute
- IRB.JobManager
- end
- end
-
- class Foreground<Nop
- def execute(key)
- IRB.JobManager.switch(key)
- end
- end
-
- class Kill<Nop
- def execute(*keys)
- IRB.JobManager.kill(*keys)
- end
- end
- end
-end
diff --git a/lib/irb/completion.rb b/lib/irb/completion.rb
deleted file mode 100644
index 26339f217d..0000000000
--- a/lib/irb/completion.rb
+++ /dev/null
@@ -1,207 +0,0 @@
-#
-# irb/completor.rb -
-# $Release Version: 0.9$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ishitsuka.com)
-# From Original Idea of shugo@ruby-lang.org
-#
-
-require "readline"
-
-module IRB
- module InputCompletor
-
- @RCS_ID='-$Id$-'
-
- ReservedWords = [
- "BEGIN", "END",
- "alias", "and",
- "begin", "break",
- "case", "class",
- "def", "defined", "do",
- "else", "elsif", "end", "ensure",
- "false", "for",
- "if", "in",
- "module",
- "next", "nil", "not",
- "or",
- "redo", "rescue", "retry", "return",
- "self", "super",
- "then", "true",
- "undef", "unless", "until",
- "when", "while",
- "yield",
- ]
-
- CompletionProc = proc { |input|
- bind = IRB.conf[:MAIN_CONTEXT].workspace.binding
-
-# puts "input: #{input}"
-
- case input
- when /^(\/[^\/]*\/)\.([^.]*)$/
- # Regexp
- receiver = $1
- message = Regexp.quote($2)
-
- candidates = Regexp.instance_methods.collect{|m| m.to_s}
- select_message(receiver, message, candidates)
-
- when /^([^\]]*\])\.([^.]*)$/
- # Array
- receiver = $1
- message = Regexp.quote($2)
-
- candidates = Array.instance_methods.collect{|m| m.to_s}
- select_message(receiver, message, candidates)
-
- when /^([^\}]*\})\.([^.]*)$/
- # Proc or Hash
- receiver = $1
- message = Regexp.quote($2)
-
- candidates = Proc.instance_methods.collect{|m| m.to_s}
- candidates |= Hash.instance_methods.collect{|m| m.to_s}
- select_message(receiver, message, candidates)
-
- when /^(:[^:.]*)$/
- # Symbol
- if Symbol.respond_to?(:all_symbols)
- sym = $1
- candidates = Symbol.all_symbols.collect{|s| ":" + s.id2name}
- candidates.grep(/^#{sym}/)
- else
- []
- end
-
- when /^::([A-Z][^:\.\(]*)$/
- # Absolute Constant or class methods
- receiver = $1
- candidates = Object.constants.collect{|m| m.to_s}
- candidates.grep(/^#{receiver}/).collect{|e| "::" + e}
-
- when /^(((::)?[A-Z][^:.\(]*)+)::?([^:.]*)$/
- # Constant or class methods
- receiver = $1
- message = Regexp.quote($4)
- begin
- candidates = eval("#{receiver}.constants.collect{|m| m.to_s}", bind)
- candidates |= eval("#{receiver}.methods.collect{|m| m.to_s}", bind)
- rescue Exception
- candidates = []
- end
- candidates.grep(/^#{message}/).collect{|e| receiver + "::" + e}
-
- when /^(:[^:.]+)\.([^.]*)$/
- # Symbol
- receiver = $1
- message = Regexp.quote($2)
-
- candidates = Symbol.instance_methods.collect{|m| m.to_s}
- select_message(receiver, message, candidates)
-
- when /^(-?(0[dbo])?[0-9_]+(\.[0-9_]+)?([eE]-?[0-9]+)?)\.([^.]*)$/
- # Numeric
- receiver = $1
- message = Regexp.quote($5)
-
- begin
- candidates = eval(receiver, bind).methods.collect{|m| m.to_s}
- rescue Exception
- candidates = []
- end
- select_message(receiver, message, candidates)
-
- when /^(-?0x[0-9a-fA-F_]+)\.([^.]*)$/
- # Numeric(0xFFFF)
- receiver = $1
- message = Regexp.quote($2)
-
- begin
- candidates = eval(receiver, bind).methods.collect{|m| m.to_s}
- rescue Exception
- candidates = []
- end
- select_message(receiver, message, candidates)
-
- when /^(\$[^.]*)$/
- regmessage = Regexp.new(Regexp.quote($1))
- candidates = global_variables.collect{|m| m.to_s}.grep(regmessage)
-
-# when /^(\$?(\.?[^.]+)+)\.([^.]*)$/
- when /^((\.?[^.]+)+)\.([^.]*)$/
- # variable
- receiver = $1
- message = Regexp.quote($3)
-
- gv = eval("global_variables", bind).collect{|m| m.to_s}
- lv = eval("local_variables", bind).collect{|m| m.to_s}
- cv = eval("self.class.constants", bind).collect{|m| m.to_s}
-
- if (gv | lv | cv).include?(receiver)
- # foo.func and foo is local var.
- candidates = eval("#{receiver}.methods", bind).collect{|m| m.to_s}
- elsif /^[A-Z]/ =~ receiver and /\./ !~ receiver
- # Foo::Bar.func
- begin
- candidates = eval("#{receiver}.methods", bind).collect{|m| m.to_s}
- rescue Exception
- candidates = []
- end
- else
- # func1.func2
- candidates = []
- ObjectSpace.each_object(Module){|m|
- begin
- name = m.name
- rescue Exception
- name = ""
- end
- next if name != "IRB::Context" and
- /^(IRB|SLex|RubyLex|RubyToken)/ =~ name
- candidates.concat m.instance_methods(false).collect{|x| x.to_s}
- }
- candidates.sort!
- candidates.uniq!
- end
- select_message(receiver, message, candidates)
-
- when /^\.([^.]*)$/
- # unknown(maybe String)
-
- receiver = ""
- message = Regexp.quote($1)
-
- candidates = String.instance_methods(true).collect{|m| m.to_s}
- select_message(receiver, message, candidates)
-
- else
- candidates = eval("methods | private_methods | local_variables | self.class.constants", bind).collect{|m| m.to_s}
-
- (candidates|ReservedWords).grep(/^#{Regexp.quote(input)}/)
- end
- }
-
- Operators = ["%", "&", "*", "**", "+", "-", "/",
- "<", "<<", "<=", "<=>", "==", "===", "=~", ">", ">=", ">>",
- "[]", "[]=", "^",]
-
- def self.select_message(receiver, message, candidates)
- candidates.grep(/^#{message}/).collect do |e|
- case e
- when /^[a-zA-Z_]/
- receiver + "." + e
- when /^[0-9]/
- when *Operators
- #receiver + " " + e
- end
- end
- end
- end
-end
-
-if Readline.respond_to?("basic_word_break_characters=")
- Readline.basic_word_break_characters= " \t\n\"\\'`><=;|&{("
-end
-Readline.completion_append_character = nil
-Readline.completion_proc = IRB::InputCompletor::CompletionProc
diff --git a/lib/irb/context.rb b/lib/irb/context.rb
deleted file mode 100644
index e2ab05a341..0000000000
--- a/lib/irb/context.rb
+++ /dev/null
@@ -1,255 +0,0 @@
-#
-# irb/context.rb - irb context
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-require "irb/workspace"
-
-module IRB
- class Context
- #
- # Arguments:
- # input_method: nil -- stdin or readline
- # String -- File
- # other -- using this as InputMethod
- #
- def initialize(irb, workspace = nil, input_method = nil, output_method = nil)
- @irb = irb
- if workspace
- @workspace = workspace
- else
- @workspace = WorkSpace.new
- end
- @thread = Thread.current if defined? Thread
-# @irb_level = 0
-
- # copy of default configuration
- @ap_name = IRB.conf[:AP_NAME]
- @rc = IRB.conf[:RC]
- @load_modules = IRB.conf[:LOAD_MODULES]
-
- @use_readline = IRB.conf[:USE_READLINE]
- @inspect_mode = IRB.conf[:INSPECT_MODE]
-
- self.math_mode = IRB.conf[:MATH_MODE] if IRB.conf[:MATH_MODE]
- self.use_tracer = IRB.conf[:USE_TRACER] if IRB.conf[:USE_TRACER]
- self.use_loader = IRB.conf[:USE_LOADER] if IRB.conf[:USE_LOADER]
- self.eval_history = IRB.conf[:EVAL_HISTORY] if IRB.conf[:EVAL_HISTORY]
-
- @ignore_sigint = IRB.conf[:IGNORE_SIGINT]
- @ignore_eof = IRB.conf[:IGNORE_EOF]
-
- @back_trace_limit = IRB.conf[:BACK_TRACE_LIMIT]
-
- self.prompt_mode = IRB.conf[:PROMPT_MODE]
-
- if IRB.conf[:SINGLE_IRB] or !defined?(JobManager)
- @irb_name = IRB.conf[:IRB_NAME]
- else
- @irb_name = "irb#"+IRB.JobManager.n_jobs.to_s
- end
- @irb_path = "(" + @irb_name + ")"
-
- case input_method
- when nil
- case use_readline?
- when nil
- if (defined?(ReadlineInputMethod) && STDIN.tty? &&
- IRB.conf[:PROMPT_MODE] != :INF_RUBY)
- @io = ReadlineInputMethod.new
- else
- @io = StdioInputMethod.new
- end
- when false
- @io = StdioInputMethod.new
- when true
- if defined?(ReadlineInputMethod)
- @io = ReadlineInputMethod.new
- else
- @io = StdioInputMethod.new
- end
- end
-
- when String
- @io = FileInputMethod.new(input_method)
- @irb_name = File.basename(input_method)
- @irb_path = input_method
- else
- @io = input_method
- end
- self.save_history = IRB.conf[:SAVE_HISTORY] if IRB.conf[:SAVE_HISTORY]
-
- if output_method
- @output_method = output_method
- else
- @output_method = StdioOutputMethod.new
- end
-
- @verbose = IRB.conf[:VERBOSE]
- @echo = IRB.conf[:ECHO]
- if @echo.nil?
- @echo = true
- end
- @debug_level = IRB.conf[:DEBUG_LEVEL]
- end
-
- def main
- @workspace.main
- end
-
- attr_reader :workspace_home
- attr_accessor :workspace
- attr_reader :thread
- attr_accessor :io
-
- attr_accessor :irb
- attr_accessor :ap_name
- attr_accessor :rc
- attr_accessor :load_modules
- attr_accessor :irb_name
- attr_accessor :irb_path
-
- attr_reader :use_readline
- attr_reader :inspect_mode
-
- attr_reader :prompt_mode
- attr_accessor :prompt_i
- attr_accessor :prompt_s
- attr_accessor :prompt_c
- attr_accessor :prompt_n
- attr_accessor :auto_indent_mode
- attr_accessor :return_format
-
- attr_accessor :ignore_sigint
- attr_accessor :ignore_eof
- attr_accessor :echo
- attr_accessor :verbose
- attr_reader :debug_level
-
- attr_accessor :back_trace_limit
-
- alias use_readline? use_readline
- alias rc? rc
- alias ignore_sigint? ignore_sigint
- alias ignore_eof? ignore_eof
- alias echo? echo
-
- def verbose?
- if @verbose.nil?
- if defined?(ReadlineInputMethod) && @io.kind_of?(ReadlineInputMethod)
- false
- elsif !STDIN.tty? or @io.kind_of?(FileInputMethod)
- true
- else
- false
- end
- end
- end
-
- def prompting?
- verbose? || (STDIN.tty? && @io.kind_of?(StdioInputMethod) ||
- (defined?(ReadlineInputMethod) && @io.kind_of?(ReadlineInputMethod)))
- end
-
- attr_reader :last_value
-
- def set_last_value(value)
- @last_value = value
- @workspace.evaluate self, "_ = IRB.CurrentContext.last_value"
- end
-
- attr_reader :irb_name
-
- def prompt_mode=(mode)
- @prompt_mode = mode
- pconf = IRB.conf[:PROMPT][mode]
- @prompt_i = pconf[:PROMPT_I]
- @prompt_s = pconf[:PROMPT_S]
- @prompt_c = pconf[:PROMPT_C]
- @prompt_n = pconf[:PROMPT_N]
- @return_format = pconf[:RETURN]
- if ai = pconf.include?(:AUTO_INDENT)
- @auto_indent_mode = ai
- else
- @auto_indent_mode = IRB.conf[:AUTO_INDENT]
- end
- end
-
- def inspect?
- @inspect_mode.nil? or @inspect_mode
- end
-
- def file_input?
- @io.class == FileInputMethod
- end
-
- def inspect_mode=(opt)
- if opt
- @inspect_mode = opt
- else
- @inspect_mode = !@inspect_mode
- end
- print "Switch to#{unless @inspect_mode; ' non';end} inspect mode.\n" if verbose?
- @inspect_mode
- end
-
- def use_readline=(opt)
- @use_readline = opt
- print "use readline module\n" if @use_readline
- end
-
- def debug_level=(value)
- @debug_level = value
- RubyLex.debug_level = value
- SLex.debug_level = value
- end
-
- def debug?
- @debug_level > 0
- end
-
- def evaluate(line, line_no)
- @line_no = line_no
- set_last_value(@workspace.evaluate(self, line, irb_path, line_no))
-# @workspace.evaluate("_ = IRB.conf[:MAIN_CONTEXT]._")
-# @_ = @workspace.evaluate(line, irb_path, line_no)
- end
-
- alias __exit__ exit
- def exit(ret = 0)
- IRB.irb_exit(@irb, ret)
- end
-
- NOPRINTING_IVARS = ["@last_value"]
- NO_INSPECTING_IVARS = ["@irb", "@io"]
- IDNAME_IVARS = ["@prompt_mode"]
-
- alias __inspect__ inspect
- def inspect
- array = []
- for ivar in instance_variables.sort{|e1, e2| e1 <=> e2}
- ivar = ivar.to_s
- name = ivar.sub(/^@(.*)$/, '\1')
- val = instance_eval(ivar)
- case ivar
- when *NOPRINTING_IVARS
- array.push format("conf.%s=%s", name, "...")
- when *NO_INSPECTING_IVARS
- array.push format("conf.%s=%s", name, val.to_s)
- when *IDNAME_IVARS
- array.push format("conf.%s=:%s", name, val.id2name)
- else
- array.push format("conf.%s=%s", name, val.inspect)
- end
- end
- array.join("\n")
- end
- alias __to_s__ to_s
- alias to_s inspect
- end
-end
diff --git a/lib/irb/ext/change-ws.rb b/lib/irb/ext/change-ws.rb
deleted file mode 100644
index 217d4a58ef..0000000000
--- a/lib/irb/ext/change-ws.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-#
-# irb/ext/cb.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-module IRB
- class Context
-
- def home_workspace
- if defined? @home_workspace
- @home_workspace
- else
- @home_workspace = @workspace
- end
- end
-
- def change_workspace(*_main)
- if _main.empty?
- @workspace = home_workspace
- return main
- end
-
- @workspace = WorkSpace.new(_main[0])
-
- if !(class<<main;ancestors;end).include?(ExtendCommandBundle)
- main.extend ExtendCommandBundle
- end
- end
-
-# def change_binding(*_main)
-# back = @workspace
-# @workspace = WorkSpace.new(*_main)
-# unless _main.empty?
-# begin
-# main.extend ExtendCommandBundle
-# rescue
-# print "can't change binding to: ", main.inspect, "\n"
-# @workspace = back
-# return nil
-# end
-# end
-# @irb_level += 1
-# begin
-# catch(:SU_EXIT) do
-# @irb.eval_input
-# end
-# ensure
-# @irb_level -= 1
-# @workspace = back
-# end
-# end
-# alias change_workspace change_binding
- end
-end
-
diff --git a/lib/irb/ext/history.rb b/lib/irb/ext/history.rb
deleted file mode 100644
index a12700ce19..0000000000
--- a/lib/irb/ext/history.rb
+++ /dev/null
@@ -1,109 +0,0 @@
-#
-# history.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-module IRB
-
- class Context
-
- NOPRINTING_IVARS.push "@eval_history_values"
-
- alias _set_last_value set_last_value
-
- def set_last_value(value)
- _set_last_value(value)
-
-# @workspace.evaluate self, "_ = IRB.CurrentContext.last_value"
- if @eval_history #and !@eval_history_values.equal?(llv)
- @eval_history_values.push @line_no, @last_value
- @workspace.evaluate self, "__ = IRB.CurrentContext.instance_eval{@eval_history_values}"
- end
-
- @last_value
- end
-
- attr_reader :eval_history
- def eval_history=(no)
- if no
- if defined?(@eval_history) && @eval_history
- @eval_history_values.size(no)
- else
- @eval_history_values = History.new(no)
- IRB.conf[:__TMP__EHV__] = @eval_history_values
- @workspace.evaluate(self, "__ = IRB.conf[:__TMP__EHV__]")
- IRB.conf.delete(:__TMP_EHV__)
- end
- else
- @eval_history_values = nil
- end
- @eval_history = no
- end
- end
-
- class History
- @RCS_ID='-$Id$-'
-
- def initialize(size = 16)
- @size = size
- @contents = []
- end
-
- def size(size)
- if size != 0 && size < @size
- @contents = @contents[@size - size .. @size]
- end
- @size = size
- end
-
- def [](idx)
- begin
- if idx >= 0
- @contents.find{|no, val| no == idx}[1]
- else
- @contents[idx][1]
- end
- rescue NameError
- nil
- end
- end
-
- def push(no, val)
- @contents.push [no, val]
- @contents.shift if @size != 0 && @contents.size > @size
- end
-
- alias real_inspect inspect
-
- def inspect
- if @contents.empty?
- return real_inspect
- end
-
- unless (last = @contents.pop)[1].equal?(self)
- @contents.push last
- last = nil
- end
- str = @contents.collect{|no, val|
- if val.equal?(self)
- "#{no} ...self-history..."
- else
- "#{no} #{val.inspect}"
- end
- }.join("\n")
- if str == ""
- str = "Empty."
- end
- @contents.push last if last
- str
- end
- end
-end
-
-
diff --git a/lib/irb/ext/loader.rb b/lib/irb/ext/loader.rb
deleted file mode 100644
index 2d4400caef..0000000000
--- a/lib/irb/ext/loader.rb
+++ /dev/null
@@ -1,119 +0,0 @@
-#
-# loader.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-
-module IRB
- class LoadAbort < Exception;end
-
- module IrbLoader
- @RCS_ID='-$Id$-'
-
- alias ruby_load load
- alias ruby_require require
-
- def irb_load(fn, priv = nil)
- path = search_file_from_ruby_path(fn)
- raise LoadError, "No such file to load -- #{fn}" unless path
-
- load_file(path, priv)
- end
-
- def search_file_from_ruby_path(fn)
- if /^#{Regexp.quote(File::Separator)}/ =~ fn
- return fn if File.exist?(fn)
- return nil
- end
-
- for path in $:
- if File.exist?(f = File.join(path, fn))
- return f
- end
- end
- return nil
- end
-
- def source_file(path)
- irb.suspend_name(path, File.basename(path)) do
- irb.suspend_input_method(FileInputMethod.new(path)) do
- |back_io|
- irb.signal_status(:IN_LOAD) do
- if back_io.kind_of?(FileInputMethod)
- irb.eval_input
- else
- begin
- irb.eval_input
- rescue LoadAbort
- print "load abort!!\n"
- end
- end
- end
- end
- end
- end
-
- def load_file(path, priv = nil)
- irb.suspend_name(path, File.basename(path)) do
-
- if priv
- ws = WorkSpace.new(Module.new)
- else
- ws = WorkSpace.new
- end
- irb.suspend_workspace(ws) do
- irb.suspend_input_method(FileInputMethod.new(path)) do
- |back_io|
- irb.signal_status(:IN_LOAD) do
-# p irb.conf
- if back_io.kind_of?(FileInputMethod)
- irb.eval_input
- else
- begin
- irb.eval_input
- rescue LoadAbort
- print "load abort!!\n"
- end
- end
- end
- end
- end
- end
- end
-
- def old
- back_io = @io
- back_path = @irb_path
- back_name = @irb_name
- back_scanner = @irb.scanner
- begin
- @io = FileInputMethod.new(path)
- @irb_name = File.basename(path)
- @irb_path = path
- @irb.signal_status(:IN_LOAD) do
- if back_io.kind_of?(FileInputMethod)
- @irb.eval_input
- else
- begin
- @irb.eval_input
- rescue LoadAbort
- print "load abort!!\n"
- end
- end
- end
- ensure
- @io = back_io
- @irb_name = back_name
- @irb_path = back_path
- @irb.scanner = back_scanner
- end
- end
- end
-end
-
diff --git a/lib/irb/ext/math-mode.rb b/lib/irb/ext/math-mode.rb
deleted file mode 100644
index 450a21eff7..0000000000
--- a/lib/irb/ext/math-mode.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-#
-# math-mode.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-require "mathn"
-
-module IRB
- class Context
- attr_reader :math_mode
- alias math? math_mode
-
- def math_mode=(opt)
- if @math_mode == true && opt == false
- IRB.fail CantReturnToNormalMode
- return
- end
-
- @math_mode = opt
- if math_mode
- main.extend Math
- print "start math mode\n" if verbose?
- end
- end
-
- def inspect?
- @inspect_mode.nil? && !@math_mode or @inspect_mode
- end
- end
-end
-
diff --git a/lib/irb/ext/multi-irb.rb b/lib/irb/ext/multi-irb.rb
deleted file mode 100644
index d32d41ff95..0000000000
--- a/lib/irb/ext/multi-irb.rb
+++ /dev/null
@@ -1,240 +0,0 @@
-#
-# irb/multi-irb.rb - multiple irb module
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-IRB.fail CantShiftToMultiIrbMode unless defined?(Thread)
-require "thread"
-
-module IRB
- # job management class
- class JobManager
- @RCS_ID='-$Id$-'
-
- def initialize
- # @jobs = [[thread, irb],...]
- @jobs = []
- @current_job = nil
- end
-
- attr_accessor :current_job
-
- def n_jobs
- @jobs.size
- end
-
- def thread(key)
- th, irb = search(key)
- th
- end
-
- def irb(key)
- th, irb = search(key)
- irb
- end
-
- def main_thread
- @jobs[0][0]
- end
-
- def main_irb
- @jobs[0][1]
- end
-
- def insert(irb)
- @jobs.push [Thread.current, irb]
- end
-
- def switch(key)
- th, irb = search(key)
- IRB.fail IrbAlreadyDead unless th.alive?
- IRB.fail IrbSwitchedToCurrentThread if th == Thread.current
- @current_job = irb
- th.run
- Thread.stop
- @current_job = irb(Thread.current)
- end
-
- def kill(*keys)
- for key in keys
- th, irb = search(key)
- IRB.fail IrbAlreadyDead unless th.alive?
- th.exit
- end
- end
-
- def search(key)
- job = case key
- when Integer
- @jobs[key]
- when Irb
- @jobs.find{|k, v| v.equal?(key)}
- when Thread
- @jobs.assoc(key)
- else
- @jobs.find{|k, v| v.context.main.equal?(key)}
- end
- IRB.fail NoSuchJob, key if job.nil?
- job
- end
-
- def delete(key)
- case key
- when Integer
- IRB.fail NoSuchJob, key unless @jobs[key]
- @jobs[key] = nil
- else
- catch(:EXISTS) do
- @jobs.each_index do
- |i|
- if @jobs[i] and (@jobs[i][0] == key ||
- @jobs[i][1] == key ||
- @jobs[i][1].context.main.equal?(key))
- @jobs[i] = nil
- throw :EXISTS
- end
- end
- IRB.fail NoSuchJob, key
- end
- end
- until assoc = @jobs.pop; end unless @jobs.empty?
- @jobs.push assoc
- end
-
- def inspect
- ary = []
- @jobs.each_index do
- |i|
- th, irb = @jobs[i]
- next if th.nil?
-
- if th.alive?
- if th.stop?
- t_status = "stop"
- else
- t_status = "running"
- end
- else
- t_status = "exited"
- end
- ary.push format("#%d->%s on %s (%s: %s)",
- i,
- irb.context.irb_name,
- irb.context.main,
- th,
- t_status)
- end
- ary.join("\n")
- end
- end
-
- @JobManager = JobManager.new
-
- def IRB.JobManager
- @JobManager
- end
-
- def IRB.CurrentContext
- IRB.JobManager.irb(Thread.current).context
- end
-
- # invoke multi-irb
- def IRB.irb(file = nil, *main)
- workspace = WorkSpace.new(*main)
- parent_thread = Thread.current
- Thread.start do
- begin
- irb = Irb.new(workspace, file)
- rescue
- print "Subirb can't start with context(self): ", workspace.main.inspect, "\n"
- print "return to main irb\n"
- Thread.pass
- Thread.main.wakeup
- Thread.exit
- end
- @CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC]
- @JobManager.insert(irb)
- @JobManager.current_job = irb
- begin
- system_exit = false
- catch(:IRB_EXIT) do
- irb.eval_input
- end
- rescue SystemExit
- system_exit = true
- raise
- #fail
- ensure
- unless system_exit
- @JobManager.delete(irb)
- if parent_thread.alive?
- @JobManager.current_job = @JobManager.irb(parent_thread)
- parent_thread.run
- else
- @JobManager.current_job = @JobManager.main_irb
- @JobManager.main_thread.run
- end
- end
- end
- end
- Thread.stop
- @JobManager.current_job = @JobManager.irb(Thread.current)
- end
-
-# class Context
-# def set_last_value(value)
-# @last_value = value
-# @workspace.evaluate "_ = IRB.JobManager.irb(Thread.current).context.last_value"
-# if @eval_history #and !@__.equal?(@last_value)
-# @eval_history_values.push @line_no, @last_value
-# @workspace.evaluate "__ = IRB.JobManager.irb(Thread.current).context.instance_eval{@eval_history_values}"
-# end
-# @last_value
-# end
-# end
-
-# module ExtendCommand
-# def irb_context
-# IRB.JobManager.irb(Thread.current).context
-# end
-# # alias conf irb_context
-# end
-
- @CONF[:SINGLE_IRB_MODE] = false
- @JobManager.insert(@CONF[:MAIN_CONTEXT].irb)
- @JobManager.current_job = @CONF[:MAIN_CONTEXT].irb
-
- class Irb
- def signal_handle
- unless @context.ignore_sigint?
- print "\nabort!!\n" if @context.verbose?
- exit
- end
-
- case @signal_status
- when :IN_INPUT
- print "^C\n"
- IRB.JobManager.thread(self).raise RubyLex::TerminateLineInput
- when :IN_EVAL
- IRB.irb_abort(self)
- when :IN_LOAD
- IRB.irb_abort(self, LoadAbort)
- when :IN_IRB
- # ignore
- else
- # ignore other cases as well
- end
- end
- end
-
- trap("SIGINT") do
- @JobManager.current_job.signal_handle
- Thread.stop
- end
-
-end
diff --git a/lib/irb/ext/save-history.rb b/lib/irb/ext/save-history.rb
deleted file mode 100644
index 88610fe9c9..0000000000
--- a/lib/irb/ext/save-history.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/usr/local/bin/ruby
-#
-# save-history.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "readline"
-
-module IRB
- module HistorySavingAbility
- @RCS_ID='-$Id$-'
- end
-
- class Context
- def init_save_history
- unless (class<<@io;self;end).include?(HistorySavingAbility)
- @io.extend(HistorySavingAbility)
- end
- end
-
- def save_history
- IRB.conf[:SAVE_HISTORY]
- end
-
- def save_history=(val)
- IRB.conf[:SAVE_HISTORY] = val
- if val
- main_context = IRB.conf[:MAIN_CONTEXT]
- main_context = self unless main_context
- main_context.init_save_history
- end
- end
-
- def history_file
- IRB.conf[:HISTORY_FILE]
- end
-
- def history_file=(hist)
- IRB.conf[:HISTORY_FILE] = hist
- end
- end
-
- module HistorySavingAbility
- include Readline
-
- def HistorySavingAbility.create_finalizer
- proc do
- if num = IRB.conf[:SAVE_HISTORY] and (num = num.to_i) > 0
- if history_file = IRB.conf[:HISTORY_FILE]
- history_file = File.expand_path(history_file)
- end
- history_file = IRB.rc_file("_history") unless history_file
- open(history_file, 'w' ) do |f|
- hist = HISTORY.to_a
- f.puts(hist[-num..-1] || hist)
- end
- end
- end
- end
-
- def HistorySavingAbility.extended(obj)
- ObjectSpace.define_finalizer(obj, HistorySavingAbility.create_finalizer)
- obj.load_history
- obj
- end
-
- def load_history
- if history_file = IRB.conf[:HISTORY_FILE]
- history_file = File.expand_path(history_file)
- end
- history_file = IRB.rc_file("_history") unless history_file
- if File.exist?(history_file)
- open(history_file) do |f|
- f.each {|l| HISTORY << l.chomp}
- end
- end
- end
- end
-end
-
diff --git a/lib/irb/ext/tracer.rb b/lib/irb/ext/tracer.rb
deleted file mode 100644
index df954af20b..0000000000
--- a/lib/irb/ext/tracer.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-#
-# irb/lib/tracer.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-require "tracer"
-
-module IRB
-
- # initialize tracing function
- def IRB.initialize_tracer
- Tracer.verbose = false
- Tracer.add_filter {
- |event, file, line, id, binding, *rests|
- /^#{Regexp.quote(@CONF[:IRB_LIB_PATH])}/ !~ file and
- File::basename(file) != "irb.rb"
- }
- end
-
- class Context
- attr_reader :use_tracer
- alias use_tracer? use_tracer
-
- def use_tracer=(opt)
- if opt
- Tracer.set_get_line_procs(@irb_path) {
- |line_no, *rests|
- @io.line(line_no)
- }
- elsif !opt && @use_tracer
- Tracer.off
- end
- @use_tracer=opt
- end
- end
-
- class WorkSpace
- alias __evaluate__ evaluate
- def evaluate(context, statements, file = nil, line = nil)
- if context.use_tracer? && file != nil && line != nil
- Tracer.on
- begin
- __evaluate__(context, statements, file, line)
- ensure
- Tracer.off
- end
- else
- __evaluate__(context, statements, file || __FILE__, line || __LINE__)
- end
- end
- end
-
- IRB.initialize_tracer
-end
-
diff --git a/lib/irb/ext/use-loader.rb b/lib/irb/ext/use-loader.rb
deleted file mode 100644
index 3836275fcd..0000000000
--- a/lib/irb/ext/use-loader.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-#
-# use-loader.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "irb/cmd/load"
-require "irb/ext/loader"
-
-class Object
- alias __original__load__IRB_use_loader__ load
- alias __original__require__IRB_use_loader__ require
-end
-
-module IRB
- module ExtendCommandBundle
- def irb_load(*opts, &b)
- ExtendCommand::Load.execute(irb_context, *opts, &b)
- end
- def irb_require(*opts, &b)
- ExtendCommand::Require.execute(irb_context, *opts, &b)
- end
- end
-
- class Context
-
- IRB.conf[:USE_LOADER] = false
-
- def use_loader
- IRB.conf[:USE_LOADER]
- end
-
- alias use_loader? use_loader
-
- def use_loader=(opt)
-
- if IRB.conf[:USE_LOADER] != opt
- IRB.conf[:USE_LOADER] = opt
- if opt
- if !$".include?("irb/cmd/load")
- end
- (class<<@workspace.main;self;end).instance_eval {
- alias_method :load, :irb_load
- alias_method :require, :irb_require
- }
- else
- (class<<@workspace.main;self;end).instance_eval {
- alias_method :load, :__original__load__IRB_use_loader__
- alias_method :require, :__original__require__IRB_use_loader__
- }
- end
- end
- print "Switch to load/require#{unless use_loader; ' non';end} trace mode.\n" if verbose?
- opt
- end
- end
-end
-
-
diff --git a/lib/irb/ext/workspaces.rb b/lib/irb/ext/workspaces.rb
deleted file mode 100644
index f3ae8d1ae8..0000000000
--- a/lib/irb/ext/workspaces.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-#
-# push-ws.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-module IRB
- class Context
-
- def irb_level
- workspace_stack.size
- end
-
- def workspaces
- if defined? @workspaces
- @workspaces
- else
- @workspaces = []
- end
- end
-
- def push_workspace(*_main)
- if _main.empty?
- if workspaces.empty?
- print "No other workspace\n"
- return nil
- end
- ws = workspaces.pop
- workspaces.push @workspace
- @workspace = ws
- return workspaces
- end
-
- workspaces.push @workspace
- @workspace = WorkSpace.new(@workspace.binding, _main[0])
- if !(class<<main;ancestors;end).include?(ExtendCommandBundle)
- main.extend ExtendCommandBundle
- end
- end
-
- def pop_workspace
- if workspaces.empty?
- print "workspace stack empty\n"
- return
- end
- @workspace = workspaces.pop
- end
- end
-end
-
diff --git a/lib/irb/extend-command.rb b/lib/irb/extend-command.rb
deleted file mode 100644
index 2816f35116..0000000000
--- a/lib/irb/extend-command.rb
+++ /dev/null
@@ -1,268 +0,0 @@
-#
-# irb/extend-command.rb - irb extend command
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-module IRB
- #
- # IRB extended command
- #
- module ExtendCommandBundle
- EXCB = ExtendCommandBundle
-
- NO_OVERRIDE = 0
- OVERRIDE_PRIVATE_ONLY = 0x01
- OVERRIDE_ALL = 0x02
-
- def irb_exit(ret = 0)
- irb_context.exit(ret)
- end
-
- def irb_context
- IRB.CurrentContext
- end
-
- @ALIASES = [
- [:context, :irb_context, NO_OVERRIDE],
- [:conf, :irb_context, NO_OVERRIDE],
- [:irb_quit, :irb_exit, OVERRIDE_PRIVATE_ONLY],
- [:exit, :irb_exit, OVERRIDE_PRIVATE_ONLY],
- [:quit, :irb_exit, OVERRIDE_PRIVATE_ONLY],
- ]
-
- @EXTEND_COMMANDS = [
- [:irb_current_working_workspace, :CurrentWorkingWorkspace, "irb/cmd/chws",
- [:irb_print_working_workspace, OVERRIDE_ALL],
- [:irb_cwws, OVERRIDE_ALL],
- [:irb_pwws, OVERRIDE_ALL],
-# [:irb_cww, OVERRIDE_ALL],
-# [:irb_pww, OVERRIDE_ALL],
- [:cwws, NO_OVERRIDE],
- [:pwws, NO_OVERRIDE],
-# [:cww, NO_OVERRIDE],
-# [:pww, NO_OVERRIDE],
- [:irb_current_working_binding, OVERRIDE_ALL],
- [:irb_print_working_binding, OVERRIDE_ALL],
- [:irb_cwb, OVERRIDE_ALL],
- [:irb_pwb, OVERRIDE_ALL],
-# [:cwb, NO_OVERRIDE],
-# [:pwb, NO_OVERRIDE]
- ],
- [:irb_change_workspace, :ChangeWorkspace, "irb/cmd/chws",
- [:irb_chws, OVERRIDE_ALL],
-# [:irb_chw, OVERRIDE_ALL],
- [:irb_cws, OVERRIDE_ALL],
-# [:irb_cw, OVERRIDE_ALL],
- [:chws, NO_OVERRIDE],
-# [:chw, NO_OVERRIDE],
- [:cws, NO_OVERRIDE],
-# [:cw, NO_OVERRIDE],
- [:irb_change_binding, OVERRIDE_ALL],
- [:irb_cb, OVERRIDE_ALL],
- [:cb, NO_OVERRIDE]],
-
- [:irb_workspaces, :Workspaces, "irb/cmd/pushws",
- [:workspaces, NO_OVERRIDE],
- [:irb_bindings, OVERRIDE_ALL],
- [:bindings, NO_OVERRIDE]],
- [:irb_push_workspace, :PushWorkspace, "irb/cmd/pushws",
- [:irb_pushws, OVERRIDE_ALL],
-# [:irb_pushw, OVERRIDE_ALL],
- [:pushws, NO_OVERRIDE],
-# [:pushw, NO_OVERRIDE],
- [:irb_push_binding, OVERRIDE_ALL],
- [:irb_pushb, OVERRIDE_ALL],
- [:pushb, NO_OVERRIDE]],
- [:irb_pop_workspace, :PopWorkspace, "irb/cmd/pushws",
- [:irb_popws, OVERRIDE_ALL],
-# [:irb_popw, OVERRIDE_ALL],
- [:popws, NO_OVERRIDE],
-# [:popw, NO_OVERRIDE],
- [:irb_pop_binding, OVERRIDE_ALL],
- [:irb_popb, OVERRIDE_ALL],
- [:popb, NO_OVERRIDE]],
-
- [:irb_load, :Load, "irb/cmd/load"],
- [:irb_require, :Require, "irb/cmd/load"],
- [:irb_source, :Source, "irb/cmd/load",
- [:source, NO_OVERRIDE]],
-
- [:irb, :IrbCommand, "irb/cmd/subirb"],
- [:irb_jobs, :Jobs, "irb/cmd/subirb",
- [:jobs, NO_OVERRIDE]],
- [:irb_fg, :Foreground, "irb/cmd/subirb",
- [:fg, NO_OVERRIDE]],
- [:irb_kill, :Kill, "irb/cmd/subirb",
- [:kill, OVERRIDE_PRIVATE_ONLY]],
-
- [:irb_help, :Help, "irb/cmd/help",
- [:help, NO_OVERRIDE]],
-
- ]
-
- def self.install_extend_commands
- for args in @EXTEND_COMMANDS
- def_extend_command(*args)
- end
- end
-
- # aliases = [commands_alias, flag], ...
- def self.def_extend_command(cmd_name, cmd_class, load_file = nil, *aliases)
- case cmd_class
- when Symbol
- cmd_class = cmd_class.id2name
- when String
- when Class
- cmd_class = cmd_class.name
- end
-
- if load_file
- eval %[
- def #{cmd_name}(*opts, &b)
- require "#{load_file}"
- arity = ExtendCommand::#{cmd_class}.instance_method(:execute).arity
- args = (1..arity.abs).map {|i| "arg" + i.to_s }
- args << "*opts" if arity < 0
- args << "&block"
- args = args.join(", ")
- eval %[
- def #{cmd_name}(\#{args})
- ExtendCommand::#{cmd_class}.execute(irb_context, \#{args})
- end
- ]
- send :#{cmd_name}, *opts, &b
- end
- ]
- else
- eval %[
- def #{cmd_name}(*opts, &b)
- ExtendCommand::#{cmd_class}.execute(irb_context, *opts, &b)
- end
- ]
- end
-
- for ali, flag in aliases
- @ALIASES.push [ali, cmd_name, flag]
- end
- end
-
- # override = {NO_OVERRIDE, OVERRIDE_PRIVATE_ONLY, OVERRIDE_ALL}
- def install_alias_method(to, from, override = NO_OVERRIDE)
- to = to.id2name unless to.kind_of?(String)
- from = from.id2name unless from.kind_of?(String)
-
- if override == OVERRIDE_ALL or
- (override == OVERRIDE_PRIVATE_ONLY) && !respond_to?(to) or
- (override == NO_OVERRIDE) && !respond_to?(to, true)
- target = self
- (class<<self;self;end).instance_eval{
- if target.respond_to?(to, true) &&
- !target.respond_to?(EXCB.irb_original_method_name(to), true)
- alias_method(EXCB.irb_original_method_name(to), to)
- end
- alias_method to, from
- }
- else
- print "irb: warn: can't alias #{to} from #{from}.\n"
- end
- end
-
- def self.irb_original_method_name(method_name)
- "irb_" + method_name + "_org"
- end
-
- def self.extend_object(obj)
- unless (class<<obj;ancestors;end).include?(EXCB)
- super
- for ali, com, flg in @ALIASES
- obj.install_alias_method(ali, com, flg)
- end
- end
- end
-
- install_extend_commands
- end
-
- # extension support for Context
- module ContextExtender
- CE = ContextExtender
-
- @EXTEND_COMMANDS = [
- [:eval_history=, "irb/ext/history.rb"],
- [:use_tracer=, "irb/ext/tracer.rb"],
- [:math_mode=, "irb/ext/math-mode.rb"],
- [:use_loader=, "irb/ext/use-loader.rb"],
- [:save_history=, "irb/ext/save-history.rb"],
- ]
-
- def self.install_extend_commands
- for args in @EXTEND_COMMANDS
- def_extend_command(*args)
- end
- end
-
- def self.def_extend_command(cmd_name, load_file, *aliases)
- Context.module_eval %[
- def #{cmd_name}(*opts, &b)
- Context.module_eval {remove_method(:#{cmd_name})}
- require "#{load_file}"
- send :#{cmd_name}, *opts, &b
- end
- for ali in aliases
- alias_method ali, cmd_name
- end
- ]
- end
-
- CE.install_extend_commands
- end
-
- module MethodExtender
- def def_pre_proc(base_method, extend_method)
- base_method = base_method.to_s
- extend_method = extend_method.to_s
-
- alias_name = new_alias_name(base_method)
- module_eval %[
- alias_method alias_name, base_method
- def #{base_method}(*opts)
- send :#{extend_method}, *opts
- send :#{alias_name}, *opts
- end
- ]
- end
-
- def def_post_proc(base_method, extend_method)
- base_method = base_method.to_s
- extend_method = extend_method.to_s
-
- alias_name = new_alias_name(base_method)
- module_eval %[
- alias_method alias_name, base_method
- def #{base_method}(*opts)
- send :#{alias_name}, *opts
- send :#{extend_method}, *opts
- end
- ]
- end
-
- # return #{prefix}#{name}#{postfix}<num>
- def new_alias_name(name, prefix = "__alias_of__", postfix = "__")
- base_name = "#{prefix}#{name}#{postfix}"
- all_methods = instance_methods(true) + private_instance_methods(true)
- same_methods = all_methods.grep(/^#{Regexp.quote(base_name)}[0-9]*$/)
- return base_name if same_methods.empty?
- no = same_methods.size
- while !same_methods.include?(alias_name = base_name + no)
- no += 1
- end
- alias_name
- end
- end
-end
-
diff --git a/lib/irb/frame.rb b/lib/irb/frame.rb
deleted file mode 100644
index 8a5d0696fb..0000000000
--- a/lib/irb/frame.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-#
-# frame.rb -
-# $Release Version: 0.9$
-# $Revision$
-# by Keiju ISHITSUKA(Nihon Rational Software Co.,Ltd)
-#
-# --
-#
-#
-#
-
-require "e2mmap"
-
-module IRB
- class Frame
- extend Exception2MessageMapper
- def_exception :FrameOverflow, "frame overflow"
- def_exception :FrameUnderflow, "frame underflow"
-
- INIT_STACK_TIMES = 3
- CALL_STACK_OFFSET = 3
-
- def initialize
- @frames = [TOPLEVEL_BINDING] * INIT_STACK_TIMES
- end
-
- def trace_func(event, file, line, id, binding)
- case event
- when 'call', 'class'
- @frames.push binding
- when 'return', 'end'
- @frames.pop
- end
- end
-
- def top(n = 0)
- bind = @frames[-(n + CALL_STACK_OFFSET)]
- Fail FrameUnderflow unless bind
- bind
- end
-
- def bottom(n = 0)
- bind = @frames[n]
- Fail FrameOverflow unless bind
- bind
- end
-
- # singleton functions
- def Frame.bottom(n = 0)
- @backtrace.bottom(n)
- end
-
- def Frame.top(n = 0)
- @backtrace.top(n)
- end
-
- def Frame.sender
- eval "self", @backtrace.top
- end
-
- @backtrace = Frame.new
- set_trace_func proc{|event, file, line, id, binding, klass|
- @backtrace.trace_func(event, file, line, id, binding)
- }
- end
-end
diff --git a/lib/irb/help.rb b/lib/irb/help.rb
deleted file mode 100644
index 2b064d5d6d..0000000000
--- a/lib/irb/help.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-#
-# irb/help.rb - print usage module
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ishitsuka.com)
-#
-# --
-#
-#
-#
-
-require 'irb/magic-file'
-
-module IRB
- def IRB.print_usage
- lc = IRB.conf[:LC_MESSAGES]
- path = lc.find("irb/help-message")
- space_line = false
- IRB::MagicFile.open(path){|f|
- f.each_line do |l|
- if /^\s*$/ =~ l
- lc.puts l unless space_line
- space_line = true
- next
- end
- space_line = false
-
- l.sub!(/#.*$/, "")
- next if /^\s*$/ =~ l
- lc.puts l
- end
- }
- end
-end
-
diff --git a/lib/irb/init.rb b/lib/irb/init.rb
deleted file mode 100644
index 62c862a1c3..0000000000
--- a/lib/irb/init.rb
+++ /dev/null
@@ -1,286 +0,0 @@
-#
-# irb/init.rb - irb initialize module
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-module IRB
-
- # initialize config
- def IRB.setup(ap_path)
- IRB.init_config(ap_path)
- IRB.init_error
- IRB.parse_opts
- IRB.run_config
- IRB.load_modules
-
- unless @CONF[:PROMPT][@CONF[:PROMPT_MODE]]
- IRB.fail(UndefinedPromptMode, @CONF[:PROMPT_MODE])
- end
- end
-
- # @CONF default setting
- def IRB.init_config(ap_path)
- # class instance variables
- @TRACER_INITIALIZED = false
-
- # default configurations
- unless ap_path and @CONF[:AP_NAME]
- ap_path = File.join(File.dirname(File.dirname(__FILE__)), "irb.rb")
- end
- @CONF[:AP_NAME] = File::basename(ap_path, ".rb")
-
- @CONF[:IRB_NAME] = "irb"
- @CONF[:IRB_LIB_PATH] = File.dirname(__FILE__)
-
- @CONF[:RC] = true
- @CONF[:LOAD_MODULES] = []
- @CONF[:IRB_RC] = nil
-
- @CONF[:MATH_MODE] = false
- @CONF[:USE_READLINE] = false unless defined?(ReadlineInputMethod)
- @CONF[:INSPECT_MODE] = nil
- @CONF[:USE_TRACER] = false
- @CONF[:USE_LOADER] = false
- @CONF[:IGNORE_SIGINT] = true
- @CONF[:IGNORE_EOF] = false
- @CONF[:ECHO] = nil
- @CONF[:VERBOSE] = nil
-
- @CONF[:EVAL_HISTORY] = nil
- @CONF[:SAVE_HISTORY] = nil
-
- @CONF[:BACK_TRACE_LIMIT] = 16
-
- @CONF[:PROMPT] = {
- :NULL => {
- :PROMPT_I => nil,
- :PROMPT_N => nil,
- :PROMPT_S => nil,
- :PROMPT_C => nil,
- :RETURN => "%s\n"
- },
- :DEFAULT => {
- :PROMPT_I => "%N(%m):%03n:%i> ",
- :PROMPT_N => "%N(%m):%03n:%i> ",
- :PROMPT_S => "%N(%m):%03n:%i%l ",
- :PROMPT_C => "%N(%m):%03n:%i* ",
- :RETURN => "=> %s\n"
- },
- :CLASSIC => {
- :PROMPT_I => "%N(%m):%03n:%i> ",
- :PROMPT_N => "%N(%m):%03n:%i> ",
- :PROMPT_S => "%N(%m):%03n:%i%l ",
- :PROMPT_C => "%N(%m):%03n:%i* ",
- :RETURN => "%s\n"
- },
- :SIMPLE => {
- :PROMPT_I => ">> ",
- :PROMPT_N => ">> ",
- :PROMPT_S => nil,
- :PROMPT_C => "?> ",
- :RETURN => "=> %s\n"
- },
- :INF_RUBY => {
- :PROMPT_I => "%N(%m):%03n:%i> ",
-# :PROMPT_N => "%N(%m):%03n:%i> ",
- :PROMPT_N => nil,
- :PROMPT_S => nil,
- :PROMPT_C => nil,
- :RETURN => "%s\n",
- :AUTO_INDENT => true
- },
- :XMP => {
- :PROMPT_I => nil,
- :PROMPT_N => nil,
- :PROMPT_S => nil,
- :PROMPT_C => nil,
- :RETURN => " ==>%s\n"
- }
- }
-
- @CONF[:PROMPT_MODE] = (STDIN.tty? ? :DEFAULT : :NULL)
- @CONF[:AUTO_INDENT] = false
-
- @CONF[:CONTEXT_MODE] = 3 # use binding in function on TOPLEVEL_BINDING
- @CONF[:SINGLE_IRB] = false
-
-# @CONF[:LC_MESSAGES] = "en"
- @CONF[:LC_MESSAGES] = Locale.new
-
- @CONF[:DEBUG_LEVEL] = 1
- end
-
- def IRB.init_error
- @CONF[:LC_MESSAGES].load("irb/error.rb")
- end
-
- FEATURE_IOPT_CHANGE_VERSION = "1.9.0"
-
- # option analyzing
- def IRB.parse_opts
- load_path = []
- while opt = ARGV.shift
- case opt
- when "-f"
- @CONF[:RC] = false
- when "-m"
- @CONF[:MATH_MODE] = true
- when "-d"
- $DEBUG = true
- when /^-r(.+)?/
- opt = $1 || ARGV.shift
- @CONF[:LOAD_MODULES].push opt if opt
- when /^-I(.+)?/
- opt = $1 || ARGV.shift
- load_path.concat(opt.split(File::PATH_SEPARATOR)) if opt
- when '-U'
- set_encoding("UTF-8", "UTF-8")
- when /^-E(.+)?/, /^--encoding(?:=(.+))?/
- opt = $1 || ARGV.shift
- set_encoding(*opt.split(':', 2))
- when "--inspect"
- @CONF[:INSPECT_MODE] = true
- when "--noinspect"
- @CONF[:INSPECT_MODE] = false
- when "--readline"
- @CONF[:USE_READLINE] = true
- when "--noreadline"
- @CONF[:USE_READLINE] = false
- when "--echo"
- @CONF[:ECHO] = true
- when "--noecho"
- @CONF[:ECHO] = false
- when "--verbose"
- @CONF[:VERBOSE] = true
- when "--noverbose"
- @CONF[:VERBOSE] = false
- when /^--prompt-mode(?:=(.+))?/, /^--prompt(?:=(.+))?/
- opt = $1 || ARGV.shift
- prompt_mode = opt.upcase.tr("-", "_").intern
- @CONF[:PROMPT_MODE] = prompt_mode
- when "--noprompt"
- @CONF[:PROMPT_MODE] = :NULL
- when "--inf-ruby-mode"
- @CONF[:PROMPT_MODE] = :INF_RUBY
- when "--sample-book-mode", "--simple-prompt"
- @CONF[:PROMPT_MODE] = :SIMPLE
- when "--tracer"
- @CONF[:USE_TRACER] = true
- when /^--back-trace-limit(?:=(.+))?/
- @CONF[:BACK_TRACE_LIMIT] = ($1 || ARGV.shift).to_i
- when /^--context-mode(?:=(.+))?/
- @CONF[:CONTEXT_MODE] = ($1 || ARGV.shift).to_i
- when "--single-irb"
- @CONF[:SINGLE_IRB] = true
- when /^--irb_debug=(?:=(.+))?/
- @CONF[:DEBUG_LEVEL] = ($1 || ARGV.shift).to_i
- when "-v", "--version"
- print IRB.version, "\n"
- exit 0
- when "-h", "--help"
- require "irb/help"
- IRB.print_usage
- exit 0
- when "--"
- if opt = ARGV.shfit
- @CONF[:SCRIPT] = opt
- $0 = opt
- end
- break
- when /^-/
- IRB.fail UnrecognizedSwitch, opt
- else
- @CONF[:SCRIPT] = opt
- $0 = opt
- break
- end
- end
- if RUBY_VERSION >= FEATURE_IOPT_CHANGE_VERSION
- load_path.collect! do |path|
- /\A\.\// =~ path ? path : File.expand_path(path)
- end
- end
- $LOAD_PATH.unshift(*load_path)
-
- end
-
- # running config
- def IRB.run_config
- if @CONF[:RC]
- begin
- load rc_file
- rescue LoadError, Errno::ENOENT
- rescue # StandardError, ScriptError
- print "load error: #{rc_file}\n"
- print $!.class, ": ", $!, "\n"
- for err in $@[0, $@.size - 2]
- print "\t", err, "\n"
- end
- end
- end
- end
-
- IRBRC_EXT = "rc"
- def IRB.rc_file(ext = IRBRC_EXT)
- if !@CONF[:RC_NAME_GENERATOR]
- rc_file_generators do |rcgen|
- @CONF[:RC_NAME_GENERATOR] ||= rcgen
- if File.exist?(rcgen.call(IRBRC_EXT))
- @CONF[:RC_NAME_GENERATOR] = rcgen
- break
- end
- end
- end
- @CONF[:RC_NAME_GENERATOR].call ext
- end
-
- # enumerate possible rc-file base name generators
- def IRB.rc_file_generators
- if irbrc = ENV["IRBRC"]
- yield proc{|rc| rc == "rc" ? irbrc : irbrc+rc}
- end
- if home = ENV["HOME"]
- yield proc{|rc| home+"/.irb#{rc}"}
- end
- home = Dir.pwd
- yield proc{|rc| home+"/.irb#{rc}"}
- yield proc{|rc| home+"/irb#{rc.sub(/\A_?/, '.')}"}
- yield proc{|rc| home+"/_irb#{rc}"}
- yield proc{|rc| home+"/$irb#{rc}"}
- end
-
- # loading modules
- def IRB.load_modules
- for m in @CONF[:LOAD_MODULES]
- begin
- require m
- rescue LoadError => err
- warn err.backtrace[0] << ":#{err.class}: #{err}"
- end
- end
- end
-
-
- DefaultEncodings = Struct.new(:external, :internal)
- class << IRB
- private
- def set_encoding(extern, intern = nil)
- verbose, $VERBOSE = $VERBOSE, nil
- Encoding.default_external = extern unless extern.nil? || extern.empty?
- Encoding.default_internal = intern unless intern.nil? || intern.empty?
- @CONF[:ENCODINGS] = IRB::DefaultEncodings.new(extern, intern)
- [$stdin, $stdout, $stderr].each do |io|
- io.set_encoding(extern, intern)
- end
- @CONF[:LC_MESSAGES].instance_variable_set(:@encoding, extern)
- ensure
- $VERBOSE = verbose
- end
- end
-end
diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb
deleted file mode 100644
index 0b22d9ca74..0000000000
--- a/lib/irb/input-method.rb
+++ /dev/null
@@ -1,142 +0,0 @@
-#
-# irb/input-method.rb - input methods used irb
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-require 'irb/src_encoding'
-require 'irb/magic-file'
-
-module IRB
- #
- # InputMethod
- # StdioInputMethod
- # FileInputMethod
- # (ReadlineInputMethod)
- #
- STDIN_FILE_NAME = "(line)"
- class InputMethod
- @RCS_ID='-$Id$-'
-
- def initialize(file = STDIN_FILE_NAME)
- @file_name = file
- end
- attr_reader :file_name
-
- attr_accessor :prompt
-
- def gets
- IRB.fail NotImplementedError, "gets"
- end
- public :gets
-
- def readable_atfer_eof?
- false
- end
- end
-
- class StdioInputMethod < InputMethod
- def initialize
- super
- @line_no = 0
- @line = []
- @stdin = IO.open(STDIN.to_i, :external_encoding => IRB.conf[:LC_MESSAGES].encoding, :internal_encoding => "-")
- @stdout = IO.open(STDOUT.to_i, 'w', :external_encoding => IRB.conf[:LC_MESSAGES].encoding, :internal_encoding => "-")
- end
-
- def gets
- print @prompt
- line = @stdin.gets
- @line[@line_no += 1] = line
- end
-
- def eof?
- @stdin.eof?
- end
-
- def readable_atfer_eof?
- true
- end
-
- def line(line_no)
- @line[line_no]
- end
-
- def encoding
- @stdin.external_encoding
- end
- end
-
- class FileInputMethod < InputMethod
- def initialize(file)
- super
- @io = IRB::MagicFile.open(file)
- end
- attr_reader :file_name
-
- def eof?
- @io.eof?
- end
-
- def gets
- print @prompt
- l = @io.gets
-# print @prompt, l
- l
- end
-
- def encoding
- @io.external_encoding
- end
- end
-
- begin
- require "readline"
- class ReadlineInputMethod < InputMethod
- include Readline
- def initialize
- super
-
- @line_no = 0
- @line = []
- @eof = false
-
- @stdin = IO.open(STDIN.to_i, :external_encoding => IRB.conf[:LC_MESSAGES].encoding, :internal_encoding => "-")
- @stdout = IO.open(STDOUT.to_i, 'w', :external_encoding => IRB.conf[:LC_MESSAGES].encoding, :internal_encoding => "-")
- end
-
- def gets
- Readline.input = @stdin
- Readline.output = @stdout
- if l = readline(@prompt, false)
- HISTORY.push(l) if !l.empty?
- @line[@line_no += 1] = l + "\n"
- else
- @eof = true
- l
- end
- end
-
- def eof?
- @eof
- end
-
- def readable_atfer_eof?
- true
- end
-
- def line(line_no)
- @line[line_no]
- end
-
- def encoding
- @stdin.external_encoding
- end
- end
- rescue LoadError
- end
-end
diff --git a/lib/irb/lc/error.rb b/lib/irb/lc/error.rb
deleted file mode 100644
index acfa22c2af..0000000000
--- a/lib/irb/lc/error.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-#
-# irb/lc/error.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-require "e2mmap"
-
-module IRB
-
- # exceptions
- extend Exception2MessageMapper
- def_exception :UnrecognizedSwitch, "Unrecognized switch: %s"
- def_exception :NotImplementedError, "Need to define `%s'"
- def_exception :CantReturnToNormalMode, "Can't return to normal mode."
- def_exception :IllegalParameter, "Invalid parameter(%s)."
- def_exception :IrbAlreadyDead, "Irb is already dead."
- def_exception :IrbSwitchedToCurrentThread, "Switched to current thread."
- def_exception :NoSuchJob, "No such job(%s)."
- def_exception :CantShiftToMultiIrbMode, "Can't shift to multi irb mode."
- def_exception :CantChangeBinding, "Can't change binding to (%s)."
- def_exception :UndefinedPromptMode, "Undefined prompt mode(%s)."
-
-end
-
diff --git a/lib/irb/lc/help-message b/lib/irb/lc/help-message
deleted file mode 100644
index 9c08a5c29d..0000000000
--- a/lib/irb/lc/help-message
+++ /dev/null
@@ -1,38 +0,0 @@
-# -*- coding: US-ASCII -*-
-#
-# irb/lc/help-message.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-Usage: irb.rb [options] [programfile] [arguments]
- -f Suppress read of ~/.irbrc
- -m Bc mode (load mathn, fraction or matrix are available)
- -d Set $DEBUG to true (same as `ruby -d')
- -r load-module Same as `ruby -r'
- -I path Specify $LOAD_PATH directory
- -U Same as `ruby -U`
- -E enc Same as `ruby -E`
- --inspect Use `inspect' for output (default except for bc mode)
- --noinspect Don't use inspect for output
- --readline Use Readline extension module
- --noreadline Don't use Readline extension module
- --prompt prompt-mode
- --prompt-mode prompt-mode
- Switch prompt mode. Pre-defined prompt modes are
- `default', `simple', `xmp' and `inf-ruby'
- --inf-ruby-mode Use prompt appropriate for inf-ruby-mode on emacs.
- Suppresses --readline.
- --simple-prompt Simple prompt mode
- --noprompt No prompt mode
- --tracer Display trace for each execution of commands.
- --back-trace-limit n
- Display backtrace top n and tail n. The default
- value is 16.
- --irb_debug n Set internal debug level to n (not for popular use)
- -v, --version Print the version of irb
-# vim:fileencoding=us-ascii
diff --git a/lib/irb/lc/ja/encoding_aliases.rb b/lib/irb/lc/ja/encoding_aliases.rb
deleted file mode 100644
index a713dff4be..0000000000
--- a/lib/irb/lc/ja/encoding_aliases.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module IRB
- class Locale
- @@legacy_encoding_alias_map = {
- 'ujis' => Encoding::EUC_JP,
- 'euc' => Encoding::EUC_JP
- }.freeze
- end
-end
diff --git a/lib/irb/lc/ja/error.rb b/lib/irb/lc/ja/error.rb
deleted file mode 100644
index dc0345e6df..0000000000
--- a/lib/irb/lc/ja/error.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-# irb/lc/ja/error.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-require "e2mmap"
-
-module IRB
- # exceptions
- extend Exception2MessageMapper
- def_exception :UnrecognizedSwitch, 'スイッãƒ(%s)ãŒåˆ†ã‚Šã¾ã›ã‚“'
- def_exception :NotImplementedError, '`%s\'ã®å®šç¾©ãŒå¿…è¦ã§ã™'
- def_exception :CantReturnToNormalMode, 'Normalãƒ¢ãƒ¼ãƒ‰ã«æˆ»ã‚Œã¾ã›ã‚“.'
- def_exception :IllegalParameter, 'パラメータ(%s)ãŒé–“é•ã£ã¦ã„ã¾ã™.'
- def_exception :IrbAlreadyDead, 'Irbã¯æ—¢ã«æ­»ã‚“ã§ã„ã¾ã™.'
- def_exception :IrbSwitchedToCurrentThread, 'カレントスレッドã«åˆ‡ã‚Šæ›¿ã‚りã¾ã—ãŸ.'
- def_exception :NoSuchJob, 'ãã®ã‚ˆã†ãªã‚¸ãƒ§ãƒ–(%s)ã¯ã‚りã¾ã›ã‚“.'
- def_exception :CantShiftToMultiIrbMode, 'multi-irb modeã«ç§»ã‚Œã¾ã›ã‚“.'
- def_exception :CantChangeBinding, 'ãƒã‚¤ãƒ³ãƒ‡ã‚£ãƒ³ã‚°(%s)ã«å¤‰æ›´ã§ãã¾ã›ã‚“.'
- def_exception :UndefinedPromptMode, 'プロンプトモード(%s)ã¯å®šç¾©ã•れã¦ã„ã¾ã›ã‚“.'
-end
-# vim:fileencoding=utf-8
diff --git a/lib/irb/lc/ja/help-message b/lib/irb/lc/ja/help-message
deleted file mode 100644
index d156039c9b..0000000000
--- a/lib/irb/lc/ja/help-message
+++ /dev/null
@@ -1,39 +0,0 @@
-# -*- coding: utf-8 -*-
-# irb/lc/ja/help-message.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-Usage: irb.rb [options] [programfile] [arguments]
- -f ~/.irbrc を読ã¿è¾¼ã¾ãªã„.
- -m bcモード(分数, 行列ã®è¨ˆç®—ãŒã§ãã‚‹)
- -d $DEBUG ã‚’trueã«ã™ã‚‹(ruby -d ã¨åŒã˜)
- -r load-module ruby -r ã¨åŒã˜.
- -I path $LOAD_PATH ã« path を追加ã™ã‚‹.
- -U ruby -U ã¨åŒã˜.
- -E enc ruby -E ã¨åŒã˜.
- --inspect çµæžœå‡ºåŠ›ã«inspectを用ã„ã‚‹(bcモード以外ã¯ãƒ‡ãƒ•ォルト).
- --noinspect çµæžœå‡ºåŠ›ã«inspectを用ã„ãªã„.
- --readline readlineライブラリを利用ã™ã‚‹.
- --noreadline readlineライブラリを利用ã—ãªã„.
- --prompt prompt-mode/--prompt-mode prompt-mode
- プロンプトモードを切替ãˆã¾ã™. ç¾åœ¨å®šç¾©ã•れã¦ã„るプ
- ロンプトモードã¯, default, simple, xmp, inf-rubyãŒ
- 用æ„ã•れã¦ã„ã¾ã™.
- --inf-ruby-mode emacsã®inf-ruby-mode用ã®ãƒ—ロンプト表示を行ãªã†. 特
- ã«æŒ‡å®šãŒãªã„é™ã‚Š, readlineライブラリã¯ä½¿ã‚ãªããªã‚‹.
- --simple-prompt éžå¸¸ã«ã‚·ãƒ³ãƒ—ルãªãƒ—ロンプトを用ã„るモードã§ã™.
- --noprompt プロンプト表示を行ãªã‚ãªã„.
- --tracer コマンド実行時ã«ãƒˆãƒ¬ãƒ¼ã‚¹ã‚’行ãªã†.
- --back-trace-limit n
- ãƒãƒƒã‚¯ãƒˆãƒ¬ãƒ¼ã‚¹è¡¨ç¤ºã‚’ãƒãƒƒã‚¯ãƒˆãƒ¬ãƒ¼ã‚¹ã®é ­ã‹ã‚‰ n, 後ã‚
- ã‹ã‚‰nã ã‘行ãªã†. デフォルトã¯16
- --irb_debug n irbã®ãƒ‡ãƒãƒƒã‚°ãƒ‡ãƒãƒƒã‚°ãƒ¬ãƒ™ãƒ«ã‚’nã«è¨­å®šã™ã‚‹(利用ã—ãª
- ã„æ–¹ãŒç„¡é›£ã§ã—ょã†).
- -v, --version irbã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’表示ã™ã‚‹
-
-# vim:fileencoding=utf-8
diff --git a/lib/irb/locale.rb b/lib/irb/locale.rb
deleted file mode 100644
index d4e2a0244a..0000000000
--- a/lib/irb/locale.rb
+++ /dev/null
@@ -1,195 +0,0 @@
-#
-# irb/locale.rb - internationalization module
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-module IRB
- class Locale
- @RCS_ID='-$Id$-'
-
- LOCALE_NAME_RE = %r[
- (?<language>[[:alpha:]]{2})
- (?:_
- (?<territory>[[:alpha:]]{2,3})
- (?:\.
- (?<codeset>[^@]+)
- )?
- )?
- (?:@
- (?<modifier>.*)
- )?
- ]x
- LOCALE_DIR = "/lc/"
-
- @@legacy_encoding_alias_map = {}.freeze
-
- def initialize(locale = nil)
- @lang = @territory = @encoding_name = @modifier = nil
- @locale = locale || ENV["IRB_LANG"] || ENV["LC_MESSAGES"] || ENV["LC_ALL"] || ENV["LANG"] || "C"
- if m = LOCALE_NAME_RE.match(@locale)
- @lang, @territory, @encoding_name, @modifier = m[:language], m[:territory], m[:codeset], m[:modifier]
-
- if @encoding_name
- begin load 'irb/encoding_aliases.rb'; rescue LoadError; end
- if @encoding = @@legacy_encoding_alias_map[@encoding_name]
- warn "%s is obsolete. use %s" % ["#{@lang}_#{@territory}.#{@encoding_name}", "#{@lang}_#{@territory}.#{@encoding.name}"]
- end
- @encoding = Encoding.find(@encoding_name) rescue nil
- end
- end
- @encoding ||= (Encoding.find('locale') rescue Encoding::ASCII_8BIT)
- end
-
- attr_reader :lang, :territory, :encoding, :modifieer
-
- def String(mes)
- mes = super(mes)
- if @encoding
- mes.encode(@encoding)
- else
- mes
- end
- end
-
- def format(*opts)
- String(super(*opts))
- end
-
- def gets(*rs)
- String(super(*rs))
- end
-
- def readline(*rs)
- String(super(*rs))
- end
-
- def print(*opts)
- ary = opts.collect{|opt| String(opt)}
- super(*ary)
- end
-
- def printf(*opts)
- s = format(*opts)
- print s
- end
-
- def puts(*opts)
- ary = opts.collect{|opt| String(opt)}
- super(*ary)
- end
-
- def require(file, priv = nil)
- rex = Regexp.new("lc/#{Regexp.quote(file)}\.(so|o|sl|rb)?")
- return false if $".find{|f| f =~ rex}
-
- case file
- when /\.rb$/
- begin
- load(file, priv)
- $".push file
- return true
- rescue LoadError
- end
- when /\.(so|o|sl)$/
- return super
- end
-
- begin
- load(f = file + ".rb")
- $".push f #"
- return true
- rescue LoadError
- return ruby_require(file)
- end
- end
-
- alias toplevel_load load
-
- def load(file, priv=nil)
- dir = File.dirname(file)
- dir = "" if dir == "."
- base = File.basename(file)
-
- if dir[0] == ?/ #/
- lc_path = search_file(dir, base)
- return real_load(lc_path, priv) if lc_path
- end
-
- for path in $:
- lc_path = search_file(path + "/" + dir, base)
- return real_load(lc_path, priv) if lc_path
- end
- raise LoadError, "No such file to load -- #{file}"
- end
-
- def real_load(path, priv)
- src = MagicFile.open(path){|f| f.read}
- if priv
- eval("self", TOPLEVEL_BINDING).extend(Module.new {eval(src, nil, path)})
- else
- eval(src, TOPLEVEL_BINDING, path)
- end
- end
- private :real_load
-
- def find(file , paths = $:)
- dir = File.dirname(file)
- dir = "" if dir == "."
- base = File.basename(file)
- if dir[0] == ?/ #/
- return lc_path = search_file(dir, base)
- else
- for path in $:
- if lc_path = search_file(path + "/" + dir, base)
- return lc_path
- end
- end
- end
- nil
- end
-
- def search_file(path, file)
- each_sublocale do |lc|
- full_path = path + lc_path(file, lc)
- return full_path if File.exist?(full_path)
- end
- nil
- end
- private :search_file
-
- def lc_path(file = "", lc = @locale)
- if lc.nil?
- LOCALE_DIR + file
- else
- LOCALE_DIR + @lang + "/" + file
- end
- end
- private :lc_path
-
- def each_sublocale
- if @lang
- if @territory
- if @encoding_name
- yield "#{@lang}_#{@territory}.#{@encoding_name}@#{@modifier}" if @modifier
- yield "#{@lang}_#{@territory}.#{@encoding_name}"
- end
- yield "#{@lang}_#{@territory}@#{@modifier}" if @modifier
- yield "#{@lang}_#{@territory}"
- end
- yield "#{@lang}@#{@modifier}" if @modifier
- yield "#{@lang}"
- end
- yield nil
- end
- private :each_sublocale
- end
-end
-
-
-
-
diff --git a/lib/irb/magic-file.rb b/lib/irb/magic-file.rb
deleted file mode 100644
index 861262050e..0000000000
--- a/lib/irb/magic-file.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module IRB
- class << (MagicFile = Object.new)
- # see parser_magic_comment in parse.y
- ENCODING_SPEC_RE = %r"coding\s*[=:]\s*([[:alnum:]\-_]+)"
-
- def open(path)
- io = File.open(path, 'rb')
- line = io.gets
- line = io.gets if line[0,2] == "#!"
- encoding = detect_encoding(line)
- encoding ||= default_src_encoding
- io.rewind
- io.set_encoding(encoding, nil)
-
- if block_given?
- begin
- return (yield io)
- ensure
- io.close
- end
- else
- return io
- end
- end
-
- private
- def detect_encoding(line)
- return unless line[0] == ?#
- line = line[1..-1]
- line = $1 if line[/-\*-\s*(.*?)\s*-*-$/]
- return nil unless ENCODING_SPEC_RE =~ line
- encoding = $1
- return encoding.sub(/-(?:mac|dos|unix)/i, '')
- end
- end
-end
diff --git a/lib/irb/notifier.rb b/lib/irb/notifier.rb
deleted file mode 100644
index 51f10ff398..0000000000
--- a/lib/irb/notifier.rb
+++ /dev/null
@@ -1,144 +0,0 @@
-#
-# notifier.rb - output methods used by irb
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "e2mmap"
-require "irb/output-method"
-
-module IRB
- module Notifier
- extend Exception2MessageMapper
- def_exception :ErrUndefinedNotifier,
- "undefined notifier level: %d is specified"
- def_exception :ErrUnrecognizedLevel,
- "unrecognized notifier level: %s is specified"
-
- def def_notifier(prefix = "", output_method = StdioOutputMethod.new)
- CompositeNotifier.new(prefix, output_method)
- end
- module_function :def_notifier
-
- class AbstructNotifier
- def initialize(prefix, base_notifier)
- @prefix = prefix
- @base_notifier = base_notifier
- end
-
- attr_reader :prefix
-
- def notify?
- true
- end
-
- def print(*opts)
- @base_notifier.print prefix, *opts if notify?
- end
-
- def printn(*opts)
- @base_notifier.printn prefix, *opts if notify?
- end
-
- def printf(format, *opts)
- @base_notifier.printf(prefix + format, *opts) if notify?
- end
-
- def puts(*objs)
- if notify?
- @base_notifier.puts(*objs.collect{|obj| prefix + obj.to_s})
- end
- end
-
- def pp(*objs)
- if notify?
- @base_notifier.ppx @prefix, *objs
- end
- end
-
- def ppx(prefix, *objs)
- if notify?
- @base_notifier.ppx @prefix+prefix, *objs
- end
- end
-
- def exec_if
- yield(@base_notifier) if notify?
- end
- end
-
- class CompositeNotifier<AbstructNotifier
- def initialize(prefix, base_notifier)
- super
-
- @notifiers = [D_NOMSG]
- @level_notifier = D_NOMSG
- end
-
- attr_reader :notifiers
-
- def def_notifier(level, prefix = "")
- notifier = LeveledNotifier.new(self, level, prefix)
- @notifiers[level] = notifier
- notifier
- end
-
- attr_reader :level_notifier
- alias level level_notifier
-
- def level_notifier=(value)
- case value
- when AbstructNotifier
- @level_notifier = value
- when Integer
- l = @notifiers[value]
- Notifier.Raise ErrUndefinedNotifer, value unless l
- @level_notifier = l
- else
- Notifier.Raise ErrUnrecognizedLevel, value unless l
- end
- end
-
- alias level= level_notifier=
- end
-
- class LeveledNotifier<AbstructNotifier
- include Comparable
-
- def initialize(base, level, prefix)
- super(prefix, base)
-
- @level = level
- end
-
- attr_reader :level
-
- def <=>(other)
- @level <=> other.level
- end
-
- def notify?
- @base_notifier.level >= self
- end
- end
-
- class NoMsgNotifier<LeveledNotifier
- def initialize
- @base_notifier = nil
- @level = 0
- @prefix = ""
- end
-
- def notify?
- false
- end
- end
-
- D_NOMSG = NoMsgNotifier.new
- end
-end
diff --git a/lib/irb/output-method.rb b/lib/irb/output-method.rb
deleted file mode 100644
index 301af7210e..0000000000
--- a/lib/irb/output-method.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-#
-# output-method.rb - optput methods used by irb
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "e2mmap"
-
-module IRB
- # OutputMethod
- # StdioOutputMethod
-
- class OutputMethod
- @RCS_ID='-$Id$-'
-
- def print(*opts)
- IRB.fail NotImplementError, "print"
- end
-
- def printn(*opts)
- print opts.join(" "), "\n"
- end
-
- # extend printf
- def printf(format, *opts)
- if /(%*)%I/ =~ format
- format, opts = parse_printf_format(format, opts)
- end
- print sprintf(format, *opts)
- end
-
- # %
- # <flag> [#0- +]
- # <minimum field width> (\*|\*[1-9][0-9]*\$|[1-9][0-9]*)
- # <precision>.(\*|\*[1-9][0-9]*\$|[1-9][0-9]*|)?
- # #<length modifier>(hh|h|l|ll|L|q|j|z|t)
- # <conversion specifier>[diouxXeEfgGcsb%]
- def parse_printf_format(format, opts)
- return format, opts if $1.size % 2 == 1
- end
-
- def puts(*objs)
- for obj in objs
- print(*obj)
- print "\n"
- end
- end
-
- def pp(*objs)
- puts(*objs.collect{|obj| obj.inspect})
- end
-
- def ppx(prefix, *objs)
- puts(*objs.collect{|obj| prefix+obj.inspect})
- end
-
- end
-
- class StdioOutputMethod<OutputMethod
- def print(*opts)
- STDOUT.print(*opts)
- end
- end
-end
diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb
deleted file mode 100644
index b21f0d34f8..0000000000
--- a/lib/irb/ruby-lex.rb
+++ /dev/null
@@ -1,1155 +0,0 @@
-#
-# irb/ruby-lex.rb - ruby lexcal analyzer
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "e2mmap"
-require "irb/slex"
-require "irb/ruby-token"
-
-class RubyLex
- @RCS_ID='-$Id$-'
-
- extend Exception2MessageMapper
- def_exception(:AlreadyDefinedToken, "Already defined token(%s)")
- def_exception(:TkReading2TokenNoKey, "key nothing(key='%s')")
- def_exception(:TkSymbol2TokenNoKey, "key nothing(key='%s')")
- def_exception(:TkReading2TokenDuplicateError,
- "key duplicate(token_n='%s', key='%s')")
- def_exception(:SyntaxError, "%s")
-
- def_exception(:TerminateLineInput, "Terminate Line Input")
-
- include RubyToken
-
- class << self
- attr_accessor :debug_level
- def debug?
- @debug_level > 0
- end
- end
- @debug_level = 0
-
- def initialize
- lex_init
- set_input(STDIN)
-
- @seek = 0
- @exp_line_no = @line_no = 1
- @base_char_no = 0
- @char_no = 0
- @rests = []
- @readed = []
- @here_readed = []
-
- @indent = 0
- @indent_stack = []
- @lex_state = EXPR_BEG
- @space_seen = false
- @here_header = false
-
- @continue = false
- @line = ""
-
- @skip_space = false
- @readed_auto_clean_up = false
- @exception_on_syntax_error = true
-
- @prompt = nil
- end
-
- attr_accessor :skip_space
- attr_accessor :readed_auto_clean_up
- attr_accessor :exception_on_syntax_error
-
- attr_reader :seek
- attr_reader :char_no
- attr_reader :line_no
- attr_reader :indent
-
- # io functions
- def set_input(io, p = nil, &block)
- @io = io
- if p.respond_to?(:call)
- @input = p
- elsif block_given?
- @input = block
- else
- @input = Proc.new{@io.gets}
- end
- end
-
- def get_readed
- if idx = @readed.reverse.index("\n")
- @base_char_no = idx
- else
- @base_char_no += @readed.size
- end
-
- readed = @readed.join("")
- @readed = []
- readed
- end
-
- def getc
- while @rests.empty?
-# return nil unless buf_input
- @rests.push nil unless buf_input
- end
- c = @rests.shift
- return if c == nil
- if @here_header
- @here_readed.push c
- else
- @readed.push c
- end
- @seek += 1
- if c == "\n"
- @line_no += 1
- @char_no = 0
- else
- @char_no += 1
- end
- c
- end
-
- def gets
- l = ""
- while c = getc
- l.concat(c)
- break if c == "\n"
- end
- return nil if l == "" and c.nil?
- l
- end
-
- def eof?
- @io.eof?
- end
-
- def getc_of_rests
- if @rests.empty?
- nil
- else
- getc
- end
- end
-
- def ungetc(c = nil)
- if @here_readed.empty?
- c2 = @readed.pop
- else
- c2 = @here_readed.pop
- end
- c = c2 unless c
- @rests.unshift c #c =
- @seek -= 1
- if c == "\n"
- @line_no -= 1
- if idx = @readed.reverse.index("\n")
- @char_no = @readed.size - idx
- else
- @char_no = @base_char_no + @readed.size
- end
- else
- @char_no -= 1
- end
- end
-
- def peek_equal?(str)
- chrs = str.split(//)
- until @rests.size >= chrs.size
- return false unless buf_input
- end
- @rests[0, chrs.size] == chrs
- end
-
- def peek_match?(regexp)
- while @rests.empty?
- return false unless buf_input
- end
- regexp =~ @rests.join("")
- end
-
- def peek(i = 0)
- while @rests.size <= i
- return nil unless buf_input
- end
- @rests[i]
- end
-
- def buf_input
- prompt
- line = @input.call
- return nil unless line
- @rests.concat line.chars.to_a
- true
- end
- private :buf_input
-
- def set_prompt(p = nil, &block)
- p = block if block_given?
- if p.respond_to?(:call)
- @prompt = p
- else
- @prompt = Proc.new{print p}
- end
- end
-
- def prompt
- if @prompt
- @prompt.call(@ltype, @indent, @continue, @line_no)
- end
- end
-
- def initialize_input
- @ltype = nil
- @quoted = nil
- @indent = 0
- @indent_stack = []
- @lex_state = EXPR_BEG
- @space_seen = false
- @here_header = false
-
- @continue = false
- prompt
-
- @line = ""
- @exp_line_no = @line_no
- end
-
- def each_top_level_statement
- initialize_input
- catch(:TERM_INPUT) do
- loop do
- begin
- @continue = false
- prompt
- unless l = lex
- throw :TERM_INPUT if @line == ''
- else
- @line.concat l
- if @ltype or @continue or @indent > 0
- next
- end
- end
- if @line != "\n"
- @line.force_encoding(@io.encoding)
- yield @line, @exp_line_no
- end
- break unless l
- @line = ''
- @exp_line_no = @line_no
-
- @indent = 0
- @indent_stack = []
- prompt
- rescue TerminateLineInput
- initialize_input
- prompt
- get_readed
- end
- end
- end
- end
-
- def lex
- until (((tk = token).kind_of?(TkNL) || tk.kind_of?(TkEND_OF_SCRIPT)) &&
- !@continue or
- tk.nil?)
- #p tk
- #p @lex_state
- #p self
- end
- line = get_readed
- # print self.inspect
- if line == "" and tk.kind_of?(TkEND_OF_SCRIPT) || tk.nil?
- nil
- else
- line
- end
- end
-
- def token
- # require "tracer"
- # Tracer.on
- @prev_seek = @seek
- @prev_line_no = @line_no
- @prev_char_no = @char_no
- begin
- begin
- tk = @OP.match(self)
- @space_seen = tk.kind_of?(TkSPACE)
- rescue SyntaxError
- raise if @exception_on_syntax_error
- tk = TkError.new(@seek, @line_no, @char_no)
- end
- end while @skip_space and tk.kind_of?(TkSPACE)
- if @readed_auto_clean_up
- get_readed
- end
- # Tracer.off
- tk
- end
-
- ENINDENT_CLAUSE = [
- "case", "class", "def", "do", "for", "if",
- "module", "unless", "until", "while", "begin" #, "when"
- ]
- DEINDENT_CLAUSE = ["end" #, "when"
- ]
-
- PERCENT_LTYPE = {
- "q" => "\'",
- "Q" => "\"",
- "x" => "\`",
- "r" => "/",
- "w" => "]",
- "W" => "]",
- "s" => ":"
- }
-
- PERCENT_PAREN = {
- "{" => "}",
- "[" => "]",
- "<" => ">",
- "(" => ")"
- }
-
- Ltype2Token = {
- "\'" => TkSTRING,
- "\"" => TkSTRING,
- "\`" => TkXSTRING,
- "/" => TkREGEXP,
- "]" => TkDSTRING,
- ":" => TkSYMBOL
- }
- DLtype2Token = {
- "\"" => TkDSTRING,
- "\`" => TkDXSTRING,
- "/" => TkDREGEXP,
- }
-
- def lex_init()
- @OP = IRB::SLex.new
- @OP.def_rules("\0", "\004", "\032") do |op, io|
- Token(TkEND_OF_SCRIPT)
- end
-
- @OP.def_rules(" ", "\t", "\f", "\r", "\13") do |op, io|
- @space_seen = true
- while getc =~ /[ \t\f\r\13]/; end
- ungetc
- Token(TkSPACE)
- end
-
- @OP.def_rule("#") do |op, io|
- identify_comment
- end
-
- @OP.def_rule("=begin",
- proc{|op, io| @prev_char_no == 0 && peek(0) =~ /\s/}) do
- |op, io|
- @ltype = "="
- until getc == "\n"; end
- until peek_equal?("=end") && peek(4) =~ /\s/
- until getc == "\n"; end
- end
- gets
- @ltype = nil
- Token(TkRD_COMMENT)
- end
-
- @OP.def_rule("\n") do |op, io|
- print "\\n\n" if RubyLex.debug?
- case @lex_state
- when EXPR_BEG, EXPR_FNAME, EXPR_DOT
- @continue = true
- else
- @continue = false
- @lex_state = EXPR_BEG
- until (@indent_stack.empty? ||
- [TkLPAREN, TkLBRACK, TkLBRACE,
- TkfLPAREN, TkfLBRACK, TkfLBRACE].include?(@indent_stack.last))
- @indent_stack.pop
- end
- end
- @here_header = false
- @here_readed = []
- Token(TkNL)
- end
-
- @OP.def_rules("*", "**",
- "=", "==", "===",
- "=~", "<=>",
- "<", "<=",
- ">", ">=", ">>") do
- |op, io|
- case @lex_state
- when EXPR_FNAME, EXPR_DOT
- @lex_state = EXPR_ARG
- else
- @lex_state = EXPR_BEG
- end
- Token(op)
- end
-
- @OP.def_rules("!", "!=", "!~") do
- |op, io|
- @lex_state = EXPR_BEG
- Token(op)
- end
-
- @OP.def_rules("<<") do
- |op, io|
- tk = nil
- if @lex_state != EXPR_END && @lex_state != EXPR_CLASS &&
- (@lex_state != EXPR_ARG || @space_seen)
- c = peek(0)
- if /\S/ =~ c && (/["'`]/ =~ c || /[\w_]/ =~ c || c == "-")
- tk = identify_here_document
- end
- end
- unless tk
- tk = Token(op)
- case @lex_state
- when EXPR_FNAME, EXPR_DOT
- @lex_state = EXPR_ARG
- else
- @lex_state = EXPR_BEG
- end
- end
- tk
- end
-
- @OP.def_rules("'", '"') do
- |op, io|
- identify_string(op)
- end
-
- @OP.def_rules("`") do
- |op, io|
- if @lex_state == EXPR_FNAME
- @lex_state = EXPR_END
- Token(op)
- else
- identify_string(op)
- end
- end
-
- @OP.def_rules('?') do
- |op, io|
- if @lex_state == EXPR_END
- @lex_state = EXPR_BEG
- Token(TkQUESTION)
- else
- ch = getc
- if @lex_state == EXPR_ARG && ch =~ /\s/
- ungetc
- @lex_state = EXPR_BEG;
- Token(TkQUESTION)
- else
- if (ch == '\\')
- read_escape
- end
- @lex_state = EXPR_END
- Token(TkINTEGER)
- end
- end
- end
-
- @OP.def_rules("&", "&&", "|", "||") do
- |op, io|
- @lex_state = EXPR_BEG
- Token(op)
- end
-
- @OP.def_rules("+=", "-=", "*=", "**=",
- "&=", "|=", "^=", "<<=", ">>=", "||=", "&&=") do
- |op, io|
- @lex_state = EXPR_BEG
- op =~ /^(.*)=$/
- Token(TkOPASGN, $1)
- end
-
- @OP.def_rule("+@", proc{|op, io| @lex_state == EXPR_FNAME}) do
- |op, io|
- @lex_state = EXPR_ARG
- Token(op)
- end
-
- @OP.def_rule("-@", proc{|op, io| @lex_state == EXPR_FNAME}) do
- |op, io|
- @lex_state = EXPR_ARG
- Token(op)
- end
-
- @OP.def_rules("+", "-") do
- |op, io|
- catch(:RET) do
- if @lex_state == EXPR_ARG
- if @space_seen and peek(0) =~ /[0-9]/
- throw :RET, identify_number
- else
- @lex_state = EXPR_BEG
- end
- elsif @lex_state != EXPR_END and peek(0) =~ /[0-9]/
- throw :RET, identify_number
- else
- @lex_state = EXPR_BEG
- end
- Token(op)
- end
- end
-
- @OP.def_rule(".") do
- |op, io|
- @lex_state = EXPR_BEG
- if peek(0) =~ /[0-9]/
- ungetc
- identify_number
- else
- # for "obj.if" etc.
- @lex_state = EXPR_DOT
- Token(TkDOT)
- end
- end
-
- @OP.def_rules("..", "...") do
- |op, io|
- @lex_state = EXPR_BEG
- Token(op)
- end
-
- lex_int2
- end
-
- def lex_int2
- @OP.def_rules("]", "}", ")") do
- |op, io|
- @lex_state = EXPR_END
- @indent -= 1
- @indent_stack.pop
- Token(op)
- end
-
- @OP.def_rule(":") do
- |op, io|
- if @lex_state == EXPR_END || peek(0) =~ /\s/
- @lex_state = EXPR_BEG
- Token(TkCOLON)
- else
- @lex_state = EXPR_FNAME;
- Token(TkSYMBEG)
- end
- end
-
- @OP.def_rule("::") do
- |op, io|
-# p @lex_state.id2name, @space_seen
- if @lex_state == EXPR_BEG or @lex_state == EXPR_ARG && @space_seen
- @lex_state = EXPR_BEG
- Token(TkCOLON3)
- else
- @lex_state = EXPR_DOT
- Token(TkCOLON2)
- end
- end
-
- @OP.def_rule("/") do
- |op, io|
- if @lex_state == EXPR_BEG || @lex_state == EXPR_MID
- identify_string(op)
- elsif peek(0) == '='
- getc
- @lex_state = EXPR_BEG
- Token(TkOPASGN, "/") #/)
- elsif @lex_state == EXPR_ARG and @space_seen and peek(0) !~ /\s/
- identify_string(op)
- else
- @lex_state = EXPR_BEG
- Token("/") #/)
- end
- end
-
- @OP.def_rules("^") do
- |op, io|
- @lex_state = EXPR_BEG
- Token("^")
- end
-
- # @OP.def_rules("^=") do
- # @lex_state = EXPR_BEG
- # Token(OP_ASGN, :^)
- # end
-
- @OP.def_rules(",") do
- |op, io|
- @lex_state = EXPR_BEG
- Token(op)
- end
-
- @OP.def_rules(";") do
- |op, io|
- @lex_state = EXPR_BEG
- until (@indent_stack.empty? ||
- [TkLPAREN, TkLBRACK, TkLBRACE,
- TkfLPAREN, TkfLBRACK, TkfLBRACE].include?(@indent_stack.last))
- @indent_stack.pop
- end
- Token(op)
- end
-
- @OP.def_rule("~") do
- |op, io|
- @lex_state = EXPR_BEG
- Token("~")
- end
-
- @OP.def_rule("~@", proc{|op, io| @lex_state == EXPR_FNAME}) do
- |op, io|
- @lex_state = EXPR_BEG
- Token("~")
- end
-
- @OP.def_rule("(") do
- |op, io|
- @indent += 1
- if @lex_state == EXPR_BEG || @lex_state == EXPR_MID
- @lex_state = EXPR_BEG
- tk_c = TkfLPAREN
- else
- @lex_state = EXPR_BEG
- tk_c = TkLPAREN
- end
- @indent_stack.push tk_c
- tk = Token(tk_c)
- end
-
- @OP.def_rule("[]", proc{|op, io| @lex_state == EXPR_FNAME}) do
- |op, io|
- @lex_state = EXPR_ARG
- Token("[]")
- end
-
- @OP.def_rule("[]=", proc{|op, io| @lex_state == EXPR_FNAME}) do
- |op, io|
- @lex_state = EXPR_ARG
- Token("[]=")
- end
-
- @OP.def_rule("[") do
- |op, io|
- @indent += 1
- if @lex_state == EXPR_FNAME
- tk_c = TkfLBRACK
- else
- if @lex_state == EXPR_BEG || @lex_state == EXPR_MID
- tk_c = TkLBRACK
- elsif @lex_state == EXPR_ARG && @space_seen
- tk_c = TkLBRACK
- else
- tk_c = TkfLBRACK
- end
- @lex_state = EXPR_BEG
- end
- @indent_stack.push tk_c
- Token(tk_c)
- end
-
- @OP.def_rule("{") do
- |op, io|
- @indent += 1
- if @lex_state != EXPR_END && @lex_state != EXPR_ARG
- tk_c = TkLBRACE
- else
- tk_c = TkfLBRACE
- end
- @lex_state = EXPR_BEG
- @indent_stack.push tk_c
- Token(tk_c)
- end
-
- @OP.def_rule('\\') do
- |op, io|
- if getc == "\n"
- @space_seen = true
- @continue = true
- Token(TkSPACE)
- else
- ungetc
- Token("\\")
- end
- end
-
- @OP.def_rule('%') do
- |op, io|
- if @lex_state == EXPR_BEG || @lex_state == EXPR_MID
- identify_quotation
- elsif peek(0) == '='
- getc
- Token(TkOPASGN, :%)
- elsif @lex_state == EXPR_ARG and @space_seen and peek(0) !~ /\s/
- identify_quotation
- else
- @lex_state = EXPR_BEG
- Token("%") #))
- end
- end
-
- @OP.def_rule('$') do
- |op, io|
- identify_gvar
- end
-
- @OP.def_rule('@') do
- |op, io|
- if peek(0) =~ /[\w_@]/
- ungetc
- identify_identifier
- else
- Token("@")
- end
- end
-
- # @OP.def_rule("def", proc{|op, io| /\s/ =~ io.peek(0)}) do
- # |op, io|
- # @indent += 1
- # @lex_state = EXPR_FNAME
- # # @lex_state = EXPR_END
- # # until @rests[0] == "\n" or @rests[0] == ";"
- # # rests.shift
- # # end
- # end
-
- @OP.def_rule("") do
- |op, io|
- printf "MATCH: start %s: %s\n", op, io.inspect if RubyLex.debug?
- if peek(0) =~ /[0-9]/
- t = identify_number
- elsif peek(0) =~ /[\w_]/
- t = identify_identifier
- end
- printf "MATCH: end %s: %s\n", op, io.inspect if RubyLex.debug?
- t
- end
-
- p @OP if RubyLex.debug?
- end
-
- def identify_gvar
- @lex_state = EXPR_END
-
- case ch = getc
- when /[~_*$?!@\/\\;,=:<>".]/ #"
- Token(TkGVAR, "$" + ch)
- when "-"
- Token(TkGVAR, "$-" + getc)
- when "&", "`", "'", "+"
- Token(TkBACK_REF, "$"+ch)
- when /[1-9]/
- while getc =~ /[0-9]/; end
- ungetc
- Token(TkNTH_REF)
- when /\w/
- ungetc
- ungetc
- identify_identifier
- else
- ungetc
- Token("$")
- end
- end
-
- def identify_identifier
- token = ""
- if peek(0) =~ /[$@]/
- token.concat(c = getc)
- if c == "@" and peek(0) == "@"
- token.concat getc
- end
- end
-
- while (ch = getc) =~ /\w|_/
- print ":", ch, ":" if RubyLex.debug?
- token.concat ch
- end
- ungetc
-
- if (ch == "!" || ch == "?") && token[0,1] =~ /\w/ && peek(0) != "="
- token.concat getc
- end
-
- # almost fix token
-
- case token
- when /^\$/
- return Token(TkGVAR, token)
- when /^\@\@/
- @lex_state = EXPR_END
- # p Token(TkCVAR, token)
- return Token(TkCVAR, token)
- when /^\@/
- @lex_state = EXPR_END
- return Token(TkIVAR, token)
- end
-
- if @lex_state != EXPR_DOT
- print token, "\n" if RubyLex.debug?
-
- token_c, *trans = TkReading2Token[token]
- if token_c
- # reserved word?
-
- if (@lex_state != EXPR_BEG &&
- @lex_state != EXPR_FNAME &&
- trans[1])
- # modifiers
- token_c = TkSymbol2Token[trans[1]]
- @lex_state = trans[0]
- else
- if @lex_state != EXPR_FNAME
- if ENINDENT_CLAUSE.include?(token)
- # check for ``class = val'' etc.
- valid = true
- case token
- when "class"
- valid = false unless peek_match?(/^\s*(<<|\w|::)/)
- when "def"
- valid = false if peek_match?(/^\s*(([+-\/*&\|^]|<<|>>|\|\||\&\&)=|\&\&|\|\|)/)
- when "do"
- valid = false if peek_match?(/^\s*([+-\/*]?=|\*|<|>|\&)/)
- when *ENINDENT_CLAUSE
- valid = false if peek_match?(/^\s*([+-\/*]?=|\*|<|>|\&|\|)/)
- else
- # no nothing
- end
- if valid
- if token == "do"
- if ![TkFOR, TkWHILE, TkUNTIL].include?(@indent_stack.last)
- @indent += 1
- @indent_stack.push token_c
- end
- else
- @indent += 1
- @indent_stack.push token_c
- end
-# p @indent_stack
- end
-
- elsif DEINDENT_CLAUSE.include?(token)
- @indent -= 1
- @indent_stack.pop
- end
- @lex_state = trans[0]
- else
- @lex_state = EXPR_END
- end
- end
- return Token(token_c, token)
- end
- end
-
- if @lex_state == EXPR_FNAME
- @lex_state = EXPR_END
- if peek(0) == '='
- token.concat getc
- end
- elsif @lex_state == EXPR_BEG || @lex_state == EXPR_DOT
- @lex_state = EXPR_ARG
- else
- @lex_state = EXPR_END
- end
-
- if token[0, 1] =~ /[A-Z]/
- return Token(TkCONSTANT, token)
- elsif token[token.size - 1, 1] =~ /[!?]/
- return Token(TkFID, token)
- else
- return Token(TkIDENTIFIER, token)
- end
- end
-
- def identify_here_document
- ch = getc
-# if lt = PERCENT_LTYPE[ch]
- if ch == "-"
- ch = getc
- indent = true
- end
- if /['"`]/ =~ ch
- lt = ch
- quoted = ""
- while (c = getc) && c != lt
- quoted.concat c
- end
- else
- lt = '"'
- quoted = ch.dup
- while (c = getc) && c =~ /\w/
- quoted.concat c
- end
- ungetc
- end
-
- ltback, @ltype = @ltype, lt
- reserve = []
- while ch = getc
- reserve.push ch
- if ch == "\\"
- reserve.push ch = getc
- elsif ch == "\n"
- break
- end
- end
-
- @here_header = false
- while l = gets
- l = l.sub(/(:?\r)?\n\z/, '')
- if (indent ? l.strip : l) == quoted
- break
- end
- end
-
- @here_header = true
- @here_readed.concat reserve
- while ch = reserve.pop
- ungetc ch
- end
-
- @ltype = ltback
- @lex_state = EXPR_END
- Token(Ltype2Token[lt])
- end
-
- def identify_quotation
- ch = getc
- if lt = PERCENT_LTYPE[ch]
- ch = getc
- elsif ch =~ /\W/
- lt = "\""
- else
- RubyLex.fail SyntaxError, "unknown type of %string"
- end
-# if ch !~ /\W/
-# ungetc
-# next
-# end
- #@ltype = lt
- @quoted = ch unless @quoted = PERCENT_PAREN[ch]
- identify_string(lt, @quoted)
- end
-
- def identify_number
- @lex_state = EXPR_END
-
- if peek(0) == "0" && peek(1) !~ /[.eE]/
- getc
- case peek(0)
- when /[xX]/
- ch = getc
- match = /[0-9a-fA-F_]/
- when /[bB]/
- ch = getc
- match = /[01_]/
- when /[oO]/
- ch = getc
- match = /[0-7_]/
- when /[dD]/
- ch = getc
- match = /[0-9_]/
- when /[0-7]/
- match = /[0-7_]/
- when /[89]/
- RubyLex.fail SyntaxError, "Invalid octal digit"
- else
- return Token(TkINTEGER)
- end
-
- len0 = true
- non_digit = false
- while ch = getc
- if match =~ ch
- if ch == "_"
- if non_digit
- RubyLex.fail SyntaxError, "trailing `#{ch}' in number"
- else
- non_digit = ch
- end
- else
- non_digit = false
- len0 = false
- end
- else
- ungetc
- if len0
- RubyLex.fail SyntaxError, "numeric literal without digits"
- end
- if non_digit
- RubyLex.fail SyntaxError, "trailing `#{non_digit}' in number"
- end
- break
- end
- end
- return Token(TkINTEGER)
- end
-
- type = TkINTEGER
- allow_point = true
- allow_e = true
- non_digit = false
- while ch = getc
- case ch
- when /[0-9]/
- non_digit = false
- when "_"
- non_digit = ch
- when allow_point && "."
- if non_digit
- RubyLex.fail SyntaxError, "trailing `#{non_digit}' in number"
- end
- type = TkFLOAT
- if peek(0) !~ /[0-9]/
- type = TkINTEGER
- ungetc
- break
- end
- allow_point = false
- when allow_e && "e", allow_e && "E"
- if non_digit
- RubyLex.fail SyntaxError, "trailing `#{non_digit}' in number"
- end
- type = TkFLOAT
- if peek(0) =~ /[+-]/
- getc
- end
- allow_e = false
- allow_point = false
- non_digit = ch
- else
- if non_digit
- RubyLex.fail SyntaxError, "trailing `#{non_digit}' in number"
- end
- ungetc
- break
- end
- end
- Token(type)
- end
-
- def identify_string(ltype, quoted = ltype)
- @ltype = ltype
- @quoted = quoted
- subtype = nil
- begin
- nest = 0
- while ch = getc
- if @quoted == ch and nest == 0
- break
- elsif @ltype != "'" && @ltype != "]" && @ltype != ":" and ch == "#"
- subtype = true
- elsif ch == '\\' and @ltype == "'" #'
- case ch = getc
- when "\\", "\n", "'"
- else
- ungetc
- end
- elsif ch == '\\' #'
- read_escape
- end
- if PERCENT_PAREN.values.include?(@quoted)
- if PERCENT_PAREN[ch] == @quoted
- nest += 1
- elsif ch == @quoted
- nest -= 1
- end
- end
- end
- if @ltype == "/"
- if peek(0) =~ /i|m|x|o|e|s|u|n/
- getc
- end
- end
- if subtype
- Token(DLtype2Token[ltype])
- else
- Token(Ltype2Token[ltype])
- end
- ensure
- @ltype = nil
- @quoted = nil
- @lex_state = EXPR_END
- end
- end
-
- def identify_comment
- @ltype = "#"
-
- while ch = getc
-# if ch == "\\" #"
-# read_escape
-# end
- if ch == "\n"
- @ltype = nil
- ungetc
- break
- end
- end
- return Token(TkCOMMENT)
- end
-
- def read_escape
- case ch = getc
- when "\n", "\r", "\f"
- when "\\", "n", "t", "r", "f", "v", "a", "e", "b", "s" #"
- when /[0-7]/
- ungetc ch
- 3.times do
- case ch = getc
- when /[0-7]/
- when nil
- break
- else
- ungetc
- break
- end
- end
-
- when "x"
- 2.times do
- case ch = getc
- when /[0-9a-fA-F]/
- when nil
- break
- else
- ungetc
- break
- end
- end
-
- when "M"
- if (ch = getc) != '-'
- ungetc
- else
- if (ch = getc) == "\\" #"
- read_escape
- end
- end
-
- when "C", "c" #, "^"
- if ch == "C" and (ch = getc) != "-"
- ungetc
- elsif (ch = getc) == "\\" #"
- read_escape
- end
- else
- # other characters
- end
- end
-end
diff --git a/lib/irb/ruby-token.rb b/lib/irb/ruby-token.rb
deleted file mode 100644
index 30a94b043c..0000000000
--- a/lib/irb/ruby-token.rb
+++ /dev/null
@@ -1,270 +0,0 @@
-#
-# irb/ruby-token.rb - ruby tokens
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-module RubyToken
- EXPR_BEG = :EXPR_BEG
- EXPR_MID = :EXPR_MID
- EXPR_END = :EXPR_END
- EXPR_ARG = :EXPR_ARG
- EXPR_FNAME = :EXPR_FNAME
- EXPR_DOT = :EXPR_DOT
- EXPR_CLASS = :EXPR_CLASS
-
- # for ruby 1.4X
- if !defined?(Symbol)
- Symbol = Integer
- end
-
- class Token
- def initialize(seek, line_no, char_no)
- @seek = seek
- @line_no = line_no
- @char_no = char_no
- end
- attr :seek, :line_no, :char_no
- end
-
- class TkNode < Token
- def initialize(seek, line_no, char_no)
- super
- end
- attr :node
- end
-
- class TkId < Token
- def initialize(seek, line_no, char_no, name)
- super(seek, line_no, char_no)
- @name = name
- end
- attr :name
- end
-
- class TkVal < Token
- def initialize(seek, line_no, char_no, value = nil)
- super(seek, line_no, char_no)
- @value = value
- end
- attr :value
- end
-
- class TkOp < Token
- attr_accessor :name
- end
-
- class TkOPASGN < TkOp
- def initialize(seek, line_no, char_no, op)
- super(seek, line_no, char_no)
- op = TkReading2Token[op][0] unless op.kind_of?(Symbol)
- @op = op
- end
- attr :op
- end
-
- class TkUnknownChar < Token
- def initialize(seek, line_no, char_no, id)
- super(seek, line_no, char_no)
- @name = name
- end
- attr :name
- end
-
- class TkError < Token
- end
-
- def Token(token, value = nil)
- case token
- when String
- if (tk = TkReading2Token[token]).nil?
- IRB.fail TkReading2TokenNoKey, token
- end
- tk = Token(tk[0], value)
- if tk.kind_of?(TkOp)
- tk.name = token
- end
- return tk
- when Symbol
- if (tk = TkSymbol2Token[token]).nil?
- IRB.fail TkSymbol2TokenNoKey, token
- end
- return Token(tk[0], value)
- else
- if (token.ancestors & [TkId, TkVal, TkOPASGN, TkUnknownChar]).empty?
- token.new(@prev_seek, @prev_line_no, @prev_char_no)
- else
- token.new(@prev_seek, @prev_line_no, @prev_char_no, value)
- end
- end
- end
-
- TokenDefinitions = [
- [:TkCLASS, TkId, "class", EXPR_CLASS],
- [:TkMODULE, TkId, "module", EXPR_BEG],
- [:TkDEF, TkId, "def", EXPR_FNAME],
- [:TkUNDEF, TkId, "undef", EXPR_FNAME],
- [:TkBEGIN, TkId, "begin", EXPR_BEG],
- [:TkRESCUE, TkId, "rescue", EXPR_MID],
- [:TkENSURE, TkId, "ensure", EXPR_BEG],
- [:TkEND, TkId, "end", EXPR_END],
- [:TkIF, TkId, "if", EXPR_BEG, :TkIF_MOD],
- [:TkUNLESS, TkId, "unless", EXPR_BEG, :TkUNLESS_MOD],
- [:TkTHEN, TkId, "then", EXPR_BEG],
- [:TkELSIF, TkId, "elsif", EXPR_BEG],
- [:TkELSE, TkId, "else", EXPR_BEG],
- [:TkCASE, TkId, "case", EXPR_BEG],
- [:TkWHEN, TkId, "when", EXPR_BEG],
- [:TkWHILE, TkId, "while", EXPR_BEG, :TkWHILE_MOD],
- [:TkUNTIL, TkId, "until", EXPR_BEG, :TkUNTIL_MOD],
- [:TkFOR, TkId, "for", EXPR_BEG],
- [:TkBREAK, TkId, "break", EXPR_END],
- [:TkNEXT, TkId, "next", EXPR_END],
- [:TkREDO, TkId, "redo", EXPR_END],
- [:TkRETRY, TkId, "retry", EXPR_END],
- [:TkIN, TkId, "in", EXPR_BEG],
- [:TkDO, TkId, "do", EXPR_BEG],
- [:TkRETURN, TkId, "return", EXPR_MID],
- [:TkYIELD, TkId, "yield", EXPR_END],
- [:TkSUPER, TkId, "super", EXPR_END],
- [:TkSELF, TkId, "self", EXPR_END],
- [:TkNIL, TkId, "nil", EXPR_END],
- [:TkTRUE, TkId, "true", EXPR_END],
- [:TkFALSE, TkId, "false", EXPR_END],
- [:TkAND, TkId, "and", EXPR_BEG],
- [:TkOR, TkId, "or", EXPR_BEG],
- [:TkNOT, TkId, "not", EXPR_BEG],
- [:TkIF_MOD, TkId],
- [:TkUNLESS_MOD, TkId],
- [:TkWHILE_MOD, TkId],
- [:TkUNTIL_MOD, TkId],
- [:TkALIAS, TkId, "alias", EXPR_FNAME],
- [:TkDEFINED, TkId, "defined?", EXPR_END],
- [:TklBEGIN, TkId, "BEGIN", EXPR_END],
- [:TklEND, TkId, "END", EXPR_END],
- [:Tk__LINE__, TkId, "__LINE__", EXPR_END],
- [:Tk__FILE__, TkId, "__FILE__", EXPR_END],
-
- [:TkIDENTIFIER, TkId],
- [:TkFID, TkId],
- [:TkGVAR, TkId],
- [:TkCVAR, TkId],
- [:TkIVAR, TkId],
- [:TkCONSTANT, TkId],
-
- [:TkINTEGER, TkVal],
- [:TkFLOAT, TkVal],
- [:TkSTRING, TkVal],
- [:TkXSTRING, TkVal],
- [:TkREGEXP, TkVal],
- [:TkSYMBOL, TkVal],
-
- [:TkDSTRING, TkNode],
- [:TkDXSTRING, TkNode],
- [:TkDREGEXP, TkNode],
- [:TkNTH_REF, TkNode],
- [:TkBACK_REF, TkNode],
-
- [:TkUPLUS, TkOp, "+@"],
- [:TkUMINUS, TkOp, "-@"],
- [:TkPOW, TkOp, "**"],
- [:TkCMP, TkOp, "<=>"],
- [:TkEQ, TkOp, "=="],
- [:TkEQQ, TkOp, "==="],
- [:TkNEQ, TkOp, "!="],
- [:TkGEQ, TkOp, ">="],
- [:TkLEQ, TkOp, "<="],
- [:TkANDOP, TkOp, "&&"],
- [:TkOROP, TkOp, "||"],
- [:TkMATCH, TkOp, "=~"],
- [:TkNMATCH, TkOp, "!~"],
- [:TkDOT2, TkOp, ".."],
- [:TkDOT3, TkOp, "..."],
- [:TkAREF, TkOp, "[]"],
- [:TkASET, TkOp, "[]="],
- [:TkLSHFT, TkOp, "<<"],
- [:TkRSHFT, TkOp, ">>"],
- [:TkCOLON2, TkOp],
- [:TkCOLON3, TkOp],
-# [:OPASGN, TkOp], # +=, -= etc. #
- [:TkASSOC, TkOp, "=>"],
- [:TkQUESTION, TkOp, "?"], #?
- [:TkCOLON, TkOp, ":"], #:
-
- [:TkfLPAREN], # func( #
- [:TkfLBRACK], # func[ #
- [:TkfLBRACE], # func{ #
- [:TkSTAR], # *arg
- [:TkAMPER], # &arg #
- [:TkSYMBEG], # :SYMBOL
-
- [:TkGT, TkOp, ">"],
- [:TkLT, TkOp, "<"],
- [:TkPLUS, TkOp, "+"],
- [:TkMINUS, TkOp, "-"],
- [:TkMULT, TkOp, "*"],
- [:TkDIV, TkOp, "/"],
- [:TkMOD, TkOp, "%"],
- [:TkBITOR, TkOp, "|"],
- [:TkBITXOR, TkOp, "^"],
- [:TkBITAND, TkOp, "&"],
- [:TkBITNOT, TkOp, "~"],
- [:TkNOTOP, TkOp, "!"],
-
- [:TkBACKQUOTE, TkOp, "`"],
-
- [:TkASSIGN, Token, "="],
- [:TkDOT, Token, "."],
- [:TkLPAREN, Token, "("], #(exp)
- [:TkLBRACK, Token, "["], #[arry]
- [:TkLBRACE, Token, "{"], #{hash}
- [:TkRPAREN, Token, ")"],
- [:TkRBRACK, Token, "]"],
- [:TkRBRACE, Token, "}"],
- [:TkCOMMA, Token, ","],
- [:TkSEMICOLON, Token, ";"],
-
- [:TkCOMMENT],
- [:TkRD_COMMENT],
- [:TkSPACE],
- [:TkNL],
- [:TkEND_OF_SCRIPT],
-
- [:TkBACKSLASH, TkUnknownChar, "\\"],
- [:TkAT, TkUnknownChar, "@"],
- [:TkDOLLAR, TkUnknownChar, "$"],
- ]
-
- # {reading => token_class}
- # {reading => [token_class, *opt]}
- TkReading2Token = {}
- TkSymbol2Token = {}
-
- def RubyToken.def_token(token_n, super_token = Token, reading = nil, *opts)
- token_n = token_n.id2name if token_n.kind_of?(Symbol)
- if RubyToken.const_defined?(token_n)
- IRB.fail AlreadyDefinedToken, token_n
- end
- token_c = eval("class #{token_n} < #{super_token}; end; #{token_n}")
-
- if reading
- if TkReading2Token[reading]
- IRB.fail TkReading2TokenDuplicateError, token_n, reading
- end
- if opts.empty?
- TkReading2Token[reading] = [token_c]
- else
- TkReading2Token[reading] = [token_c].concat(opts)
- end
- end
- TkSymbol2Token[token_n.intern] = token_c
- end
-
- for defs in TokenDefinitions
- def_token(*defs)
- end
-end
diff --git a/lib/irb/slex.rb b/lib/irb/slex.rb
deleted file mode 100644
index c8b40c6878..0000000000
--- a/lib/irb/slex.rb
+++ /dev/null
@@ -1,282 +0,0 @@
-#
-# irb/slex.rb - simple lex analyzer
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "e2mmap"
-require "irb/notifier"
-
-module IRB
- class SLex
- @RCS_ID='-$Id$-'
-
- extend Exception2MessageMapper
- def_exception :ErrNodeNothing, "node nothing"
- def_exception :ErrNodeAlreadyExists, "node already exists"
-
- DOUT = Notifier::def_notifier("SLex::")
- D_WARN = DOUT::def_notifier(1, "Warn: ")
- D_DEBUG = DOUT::def_notifier(2, "Debug: ")
- D_DETAIL = DOUT::def_notifier(4, "Detail: ")
-
- DOUT.level = Notifier::D_NOMSG
-
- def initialize
- @head = Node.new("")
- end
-
- def def_rule(token, preproc = nil, postproc = nil, &block)
- D_DETAIL.pp token
-
- postproc = block if block_given?
- node = create(token, preproc, postproc)
- end
-
- def def_rules(*tokens, &block)
- if block_given?
- p = block
- end
- for token in tokens
- def_rule(token, nil, p)
- end
- end
-
- def preproc(token, proc)
- node = search(token)
- node.preproc=proc
- end
-
- #$BMW%A%'%C%/(B?
- def postproc(token)
- node = search(token, proc)
- node.postproc=proc
- end
-
- def search(token)
- @head.search(token.split(//))
- end
-
- def create(token, preproc = nil, postproc = nil)
- @head.create_subnode(token.split(//), preproc, postproc)
- end
-
- def match(token)
- case token
- when Array
- when String
- return match(token.split(//))
- else
- return @head.match_io(token)
- end
- ret = @head.match(token)
- D_DETAIL.exec_if{D_DEATIL.printf "match end: %s:%s\n", ret, token.inspect}
- ret
- end
-
- def inspect
- format("<SLex: @head = %s>", @head.inspect)
- end
-
- #----------------------------------------------------------------------
- #
- # class Node -
- #
- #----------------------------------------------------------------------
- class Node
- # if postproc is nil, this node is an abstract node.
- # if postproc is non-nil, this node is a real node.
- def initialize(preproc = nil, postproc = nil)
- @Tree = {}
- @preproc = preproc
- @postproc = postproc
- end
-
- attr_accessor :preproc
- attr_accessor :postproc
-
- def search(chrs, opt = nil)
- return self if chrs.empty?
- ch = chrs.shift
- if node = @Tree[ch]
- node.search(chrs, opt)
- else
- if opt
- chrs.unshift ch
- self.create_subnode(chrs)
- else
- SLex.fail ErrNodeNothing
- end
- end
- end
-
- def create_subnode(chrs, preproc = nil, postproc = nil)
- if chrs.empty?
- if @postproc
- D_DETAIL.pp node
- SLex.fail ErrNodeAlreadyExists
- else
- D_DEBUG.puts "change abstract node to real node."
- @preproc = preproc
- @postproc = postproc
- end
- return self
- end
-
- ch = chrs.shift
- if node = @Tree[ch]
- if chrs.empty?
- if node.postproc
- DebugLogger.pp node
- DebugLogger.pp self
- DebugLogger.pp ch
- DebugLogger.pp chrs
- SLex.fail ErrNodeAlreadyExists
- else
- D_WARN.puts "change abstract node to real node"
- node.preproc = preproc
- node.postproc = postproc
- end
- else
- node.create_subnode(chrs, preproc, postproc)
- end
- else
- if chrs.empty?
- node = Node.new(preproc, postproc)
- else
- node = Node.new
- node.create_subnode(chrs, preproc, postproc)
- end
- @Tree[ch] = node
- end
- node
- end
-
- #
- # chrs: String
- # character array
- # io must have getc()/ungetc(); and ungetc() must be
- # able to be called arbitrary number of times.
- #
- def match(chrs, op = "")
- D_DETAIL.print "match>: ", chrs, "op:", op, "\n"
- if chrs.empty?
- if @preproc.nil? || @preproc.call(op, chrs)
- DOUT.printf(D_DETAIL, "op1: %s\n", op)
- @postproc.call(op, chrs)
- else
- nil
- end
- else
- ch = chrs.shift
- if node = @Tree[ch]
- if ret = node.match(chrs, op+ch)
- return ret
- else
- chrs.unshift ch
- if @postproc and @preproc.nil? || @preproc.call(op, chrs)
- DOUT.printf(D_DETAIL, "op2: %s\n", op.inspect)
- ret = @postproc.call(op, chrs)
- return ret
- else
- return nil
- end
- end
- else
- chrs.unshift ch
- if @postproc and @preproc.nil? || @preproc.call(op, chrs)
- DOUT.printf(D_DETAIL, "op3: %s\n", op)
- @postproc.call(op, chrs)
- return ""
- else
- return nil
- end
- end
- end
- end
-
- def match_io(io, op = "")
- if op == ""
- ch = io.getc
- if ch == nil
- return nil
- end
- else
- ch = io.getc_of_rests
- end
- if ch.nil?
- if @preproc.nil? || @preproc.call(op, io)
- D_DETAIL.printf("op1: %s\n", op)
- @postproc.call(op, io)
- else
- nil
- end
- else
- if node = @Tree[ch]
- if ret = node.match_io(io, op+ch)
- ret
- else
- io.ungetc ch
- if @postproc and @preproc.nil? || @preproc.call(op, io)
- DOUT.exec_if{D_DETAIL.printf "op2: %s\n", op.inspect}
- @postproc.call(op, io)
- else
- nil
- end
- end
- else
- io.ungetc ch
- if @postproc and @preproc.nil? || @preproc.call(op, io)
- D_DETAIL.printf("op3: %s\n", op)
- @postproc.call(op, io)
- else
- nil
- end
- end
- end
- end
- end
- end
-end
-
-if $0 == __FILE__
- # Tracer.on
- case $1
- when "1"
- tr = SLex.new
- print "0: ", tr.inspect, "\n"
- tr.def_rule("=") {print "=\n"}
- print "1: ", tr.inspect, "\n"
- tr.def_rule("==") {print "==\n"}
- print "2: ", tr.inspect, "\n"
-
- print "case 1:\n"
- print tr.match("="), "\n"
- print "case 2:\n"
- print tr.match("=="), "\n"
- print "case 3:\n"
- print tr.match("=>"), "\n"
-
- when "2"
- tr = SLex.new
- print "0: ", tr.inspect, "\n"
- tr.def_rule("=") {print "=\n"}
- print "1: ", tr.inspect, "\n"
- tr.def_rule("==", proc{false}) {print "==\n"}
- print "2: ", tr.inspect, "\n"
-
- print "case 1:\n"
- print tr.match("="), "\n"
- print "case 2:\n"
- print tr.match("=="), "\n"
- print "case 3:\n"
- print tr.match("=>"), "\n"
- end
- exit
-end
-
diff --git a/lib/irb/src_encoding.rb b/lib/irb/src_encoding.rb
deleted file mode 100644
index 958cef104c..0000000000
--- a/lib/irb/src_encoding.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# DO NOT WRITE ANY MAGIC COMMENT HERE.
-def default_src_encoding
- return __ENCODING__
-end
diff --git a/lib/irb/version.rb b/lib/irb/version.rb
deleted file mode 100644
index 32ecf940cf..0000000000
--- a/lib/irb/version.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-#
-# irb/version.rb - irb version definition file
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ishitsuka.com)
-#
-# --
-#
-#
-#
-
-module IRB
- @RELEASE_VERSION = "0.9.5"
- @LAST_UPDATE_DATE = "05/04/13"
-end
diff --git a/lib/irb/workspace.rb b/lib/irb/workspace.rb
deleted file mode 100644
index 7c95106c39..0000000000
--- a/lib/irb/workspace.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-#
-# irb/workspace-binding.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-module IRB
- class WorkSpace
- # create new workspace. set self to main if specified, otherwise
- # inherit main from TOPLEVEL_BINDING.
- def initialize(*main)
- if main[0].kind_of?(Binding)
- @binding = main.shift
- elsif IRB.conf[:SINGLE_IRB]
- @binding = TOPLEVEL_BINDING
- else
- case IRB.conf[:CONTEXT_MODE]
- when 0 # binding in proc on TOPLEVEL_BINDING
- @binding = eval("proc{binding}.call",
- TOPLEVEL_BINDING,
- __FILE__,
- __LINE__)
- when 1 # binding in loaded file
- require "tempfile"
- f = Tempfile.open("irb-binding")
- f.print <<EOF
- $binding = binding
-EOF
- f.close
- load f.path
- @binding = $binding
-
- when 2 # binding in loaded file(thread use)
- unless defined? BINDING_QUEUE
- require "thread"
-
- IRB.const_set("BINDING_QUEUE", SizedQueue.new(1))
- Thread.abort_on_exception = true
- Thread.start do
- eval "require \"irb/ws-for-case-2\"", TOPLEVEL_BINDING, __FILE__, __LINE__
- end
- Thread.pass
- end
- @binding = BINDING_QUEUE.pop
-
- when 3 # binging in function on TOPLEVEL_BINDING(default)
- @binding = eval("def irb_binding; binding; end; irb_binding",
- TOPLEVEL_BINDING,
- __FILE__,
- __LINE__ - 3)
- end
- end
- if main.empty?
- @main = eval("self", @binding)
- else
- @main = main[0]
- IRB.conf[:__MAIN__] = @main
- case @main
- when Module
- @binding = eval("IRB.conf[:__MAIN__].module_eval('binding', __FILE__, __LINE__)", @binding, __FILE__, __LINE__)
- else
- begin
- @binding = eval("IRB.conf[:__MAIN__].instance_eval('binding', __FILE__, __LINE__)", @binding, __FILE__, __LINE__)
- rescue TypeError
- IRB.fail CantChangeBinding, @main.inspect
- end
- end
- end
- eval("_=nil", @binding)
- end
-
- attr_reader :binding
- attr_reader :main
-
- def evaluate(context, statements, file = __FILE__, line = __LINE__)
- eval(statements, @binding, file, line)
- end
-
- # error message manipulator
- def filter_backtrace(bt)
- case IRB.conf[:CONTEXT_MODE]
- when 0
- return nil if bt =~ /\(irb_local_binding\)/
- when 1
- if(bt =~ %r!/tmp/irb-binding! or
- bt =~ %r!irb/.*\.rb! or
- bt =~ /irb\.rb/)
- return nil
- end
- when 2
- return nil if bt =~ /irb\/.*\.rb/
- return nil if bt =~ /irb\.rb/
- when 3
- return nil if bt =~ /irb\/.*\.rb/
- return nil if bt =~ /irb\.rb/
- bt.sub!(/:\s*in `irb_binding'/, '')
- end
- bt
- end
-
- def IRB.delete_caller
- end
- end
-end
diff --git a/lib/irb/ws-for-case-2.rb b/lib/irb/ws-for-case-2.rb
deleted file mode 100644
index 24c5fd5aa8..0000000000
--- a/lib/irb/ws-for-case-2.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-#
-# irb/ws-for-case-2.rb -
-# $Release Version: 0.9.5$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-while true
- IRB::BINDING_QUEUE.push b = binding
-end
diff --git a/lib/irb/xmp.rb b/lib/irb/xmp.rb
deleted file mode 100644
index af87b48887..0000000000
--- a/lib/irb/xmp.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-#
-# xmp.rb - irb version of gotoken xmp
-# $Release Version: 0.9$
-# $Revision$
-# by Keiju ISHITSUKA(Nippon Rational Inc.)
-#
-# --
-#
-#
-#
-
-require "irb"
-require "irb/frame"
-
-class XMP
- @RCS_ID='-$Id$-'
-
- def initialize(bind = nil)
- IRB.init_config(nil)
- #IRB.parse_opts
- #IRB.load_modules
-
- IRB.conf[:PROMPT_MODE] = :XMP
-
- bind = IRB::Frame.top(1) unless bind
- ws = IRB::WorkSpace.new(bind)
- @io = StringInputMethod.new
- @irb = IRB::Irb.new(ws, @io)
- @irb.context.ignore_sigint = false
-
-# IRB.conf[:IRB_RC].call(@irb.context) if IRB.conf[:IRB_RC]
- IRB.conf[:MAIN_CONTEXT] = @irb.context
- end
-
- def puts(exps)
- @io.puts exps
-
- if @irb.context.ignore_sigint
- begin
- trap_proc_b = trap("SIGINT"){@irb.signal_handle}
- catch(:IRB_EXIT) do
- @irb.eval_input
- end
- ensure
- trap("SIGINT", trap_proc_b)
- end
- else
- catch(:IRB_EXIT) do
- @irb.eval_input
- end
- end
- end
-
- class StringInputMethod < IRB::InputMethod
- def initialize
- super
- @exps = []
- end
-
- def eof?
- @exps.empty?
- end
-
- def gets
- while l = @exps.shift
- next if /^\s+$/ =~ l
- l.concat "\n"
- print @prompt, l
- break
- end
- l
- end
-
- def puts(exps)
- if @encoding and exps.encoding != @encoding
- enc = Encoding.compatible?(@exps.join("\n"), exps)
- if enc.nil?
- raise Encoding::CompatibilityError, "Encoding in which the passed exression is encoded is not compatible to the preceding's one"
- else
- @encoding = enc
- end
- else
- @encoding = exps.encoding
- end
- @exps.concat exps.split(/\n/)
- end
-
- attr_reader :encoding
- end
-end
-
-def xmp(exps, bind = nil)
- bind = IRB::Frame.top(1) unless bind
- xmp = XMP.new(bind)
- xmp.puts exps
- xmp
-end
diff --git a/lib/logger.rb b/lib/logger.rb
deleted file mode 100644
index 07699e7017..0000000000
--- a/lib/logger.rb
+++ /dev/null
@@ -1,732 +0,0 @@
-# logger.rb - simple logging utility
-# Copyright (C) 2000-2003, 2005 NAKAMURA, Hiroshi <nakahiro@sarion.co.jp>.
-
-require 'monitor'
-
-# = logger.rb
-#
-# Simple logging utility.
-#
-# Author:: NAKAMURA, Hiroshi <nakahiro@sarion.co.jp>
-# Documentation:: NAKAMURA, Hiroshi and Gavin Sinclair
-# License::
-# You can redistribute it and/or modify it under the same terms of Ruby's
-# license; either the dual license version in 2003, or any later version.
-# Revision:: $Id$
-#
-# See Logger for documentation.
-#
-
-
-#
-# == Description
-#
-# The Logger class provides a simple but sophisticated logging utility that
-# anyone can use because it's included in the Ruby 1.8.x standard library.
-#
-# The HOWTOs below give a code-based overview of Logger's usage, but the basic
-# concept is as follows. You create a Logger object (output to a file or
-# elsewhere), and use it to log messages. The messages will have varying
-# levels (+info+, +error+, etc), reflecting their varying importance. The
-# levels, and their meanings, are:
-#
-# +FATAL+:: an unhandleable error that results in a program crash
-# +ERROR+:: a handleable error condition
-# +WARN+:: a warning
-# +INFO+:: generic (useful) information about system operation
-# +DEBUG+:: low-level information for developers
-#
-# So each message has a level, and the Logger itself has a level, which acts
-# as a filter, so you can control the amount of information emitted from the
-# logger without having to remove actual messages.
-#
-# For instance, in a production system, you may have your logger(s) set to
-# +INFO+ (or +WARN+ if you don't want the log files growing large with
-# repetitive information). When you are developing it, though, you probably
-# want to know about the program's internal state, and would set them to
-# +DEBUG+.
-#
-# === Example
-#
-# A simple example demonstrates the above explanation:
-#
-# log = Logger.new(STDOUT)
-# log.level = Logger::WARN
-#
-# log.debug("Created logger")
-# log.info("Program started")
-# log.warn("Nothing to do!")
-#
-# begin
-# File.each_line(path) do |line|
-# unless line =~ /^(\w+) = (.*)$/
-# log.error("Line in wrong format: #{line}")
-# end
-# end
-# rescue => err
-# log.fatal("Caught exception; exiting")
-# log.fatal(err)
-# end
-#
-# Because the Logger's level is set to +WARN+, only the warning, error, and
-# fatal messages are recorded. The debug and info messages are silently
-# discarded.
-#
-# === Features
-#
-# There are several interesting features that Logger provides, like
-# auto-rolling of log files, setting the format of log messages, and
-# specifying a program name in conjunction with the message. The next section
-# shows you how to achieve these things.
-#
-#
-# == HOWTOs
-#
-# === How to create a logger
-#
-# The options below give you various choices, in more or less increasing
-# complexity.
-#
-# 1. Create a logger which logs messages to STDERR/STDOUT.
-#
-# logger = Logger.new(STDERR)
-# logger = Logger.new(STDOUT)
-#
-# 2. Create a logger for the file which has the specified name.
-#
-# logger = Logger.new('logfile.log')
-#
-# 3. Create a logger for the specified file.
-#
-# file = File.open('foo.log', File::WRONLY | File::APPEND)
-# # To create new (and to remove old) logfile, add File::CREAT like;
-# # file = open('foo.log', File::WRONLY | File::APPEND | File::CREAT)
-# logger = Logger.new(file)
-#
-# 4. Create a logger which ages logfile once it reaches a certain size. Leave
-# 10 "old log files" and each file is about 1,024,000 bytes.
-#
-# logger = Logger.new('foo.log', 10, 1024000)
-#
-# 5. Create a logger which ages logfile daily/weekly/monthly.
-#
-# logger = Logger.new('foo.log', 'daily')
-# logger = Logger.new('foo.log', 'weekly')
-# logger = Logger.new('foo.log', 'monthly')
-#
-# === How to log a message
-#
-# Notice the different methods (+fatal+, +error+, +info+) being used to log
-# messages of various levels. Other methods in this family are +warn+ and
-# +debug+. +add+ is used below to log a message of an arbitrary (perhaps
-# dynamic) level.
-#
-# 1. Message in block.
-#
-# logger.fatal { "Argument 'foo' not given." }
-#
-# 2. Message as a string.
-#
-# logger.error "Argument #{ @foo } mismatch."
-#
-# 3. With progname.
-#
-# logger.info('initialize') { "Initializing..." }
-#
-# 4. With severity.
-#
-# logger.add(Logger::FATAL) { 'Fatal error!' }
-#
-# === How to close a logger
-#
-# logger.close
-#
-# === Setting severity threshold
-#
-# 1. Original interface.
-#
-# logger.sev_threshold = Logger::WARN
-#
-# 2. Log4r (somewhat) compatible interface.
-#
-# logger.level = Logger::INFO
-#
-# DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN
-#
-#
-# == Format
-#
-# Log messages are rendered in the output stream in a certain format by
-# default. The default format and a sample are shown below:
-#
-# Log format:
-# SeverityID, [Date Time mSec #pid] SeverityLabel -- ProgName: message
-#
-# Log sample:
-# I, [Wed Mar 03 02:34:24 JST 1999 895701 #19074] INFO -- Main: info.
-#
-# You may change the date and time format in this manner:
-#
-# logger.datetime_format = "%Y-%m-%d %H:%M:%S"
-# # e.g. "2004-01-03 00:54:26"
-#
-# You may change the overall format with Logger#formatter= method.
-#
-# logger.formatter = proc { |severity, datetime, progname, msg|
-# "#{datetime}: #{msg}\n"
-# }
-# # e.g. "Thu Sep 22 08:51:08 GMT+9:00 2005: hello world"
-#
-
-
-class Logger
- VERSION = "1.2.6"
- id, name, rev = %w$Id$
- if name
- name = name.chomp(",v")
- else
- name = File.basename(__FILE__)
- end
- rev ||= "v#{VERSION}"
- ProgName = "#{name}/#{rev}"
-
- class Error < RuntimeError; end
- class ShiftingError < Error; end
-
- # Logging severity.
- module Severity
- DEBUG = 0
- INFO = 1
- WARN = 2
- ERROR = 3
- FATAL = 4
- UNKNOWN = 5
- end
- include Severity
-
- # Logging severity threshold (e.g. <tt>Logger::INFO</tt>).
- attr_accessor :level
-
- # Logging program name.
- attr_accessor :progname
-
- # Logging date-time format (string passed to +strftime+).
- def datetime_format=(datetime_format)
- @default_formatter.datetime_format = datetime_format
- end
-
- def datetime_format
- @default_formatter.datetime_format
- end
-
- # Logging formatter. formatter#call is invoked with 4 arguments; severity,
- # time, progname and msg for each log. Bear in mind that time is a Time and
- # msg is an Object that user passed and it could not be a String. It is
- # expected to return a logdev#write-able Object. Default formatter is used
- # when no formatter is set.
- attr_accessor :formatter
-
- alias sev_threshold level
- alias sev_threshold= level=
-
- # Returns +true+ iff the current severity level allows for the printing of
- # +DEBUG+ messages.
- def debug?; @level <= DEBUG; end
-
- # Returns +true+ iff the current severity level allows for the printing of
- # +INFO+ messages.
- def info?; @level <= INFO; end
-
- # Returns +true+ iff the current severity level allows for the printing of
- # +WARN+ messages.
- def warn?; @level <= WARN; end
-
- # Returns +true+ iff the current severity level allows for the printing of
- # +ERROR+ messages.
- def error?; @level <= ERROR; end
-
- # Returns +true+ iff the current severity level allows for the printing of
- # +FATAL+ messages.
- def fatal?; @level <= FATAL; end
-
- #
- # === Synopsis
- #
- # Logger.new(name, shift_age = 7, shift_size = 1048576)
- # Logger.new(name, shift_age = 'weekly')
- #
- # === Args
- #
- # +logdev+::
- # The log device. This is a filename (String) or IO object (typically
- # +STDOUT+, +STDERR+, or an open file).
- # +shift_age+::
- # Number of old log files to keep, *or* frequency of rotation (+daily+,
- # +weekly+ or +monthly+).
- # +shift_size+::
- # Maximum logfile size (only applies when +shift_age+ is a number).
- #
- # === Description
- #
- # Create an instance.
- #
- def initialize(logdev, shift_age = 0, shift_size = 1048576)
- @progname = nil
- @level = DEBUG
- @default_formatter = Formatter.new
- @formatter = nil
- @logdev = nil
- if logdev
- @logdev = LogDevice.new(logdev, :shift_age => shift_age,
- :shift_size => shift_size)
- end
- end
-
- #
- # === Synopsis
- #
- # Logger#add(severity, message = nil, progname = nil) { ... }
- #
- # === Args
- #
- # +severity+::
- # Severity. Constants are defined in Logger namespace: +DEBUG+, +INFO+,
- # +WARN+, +ERROR+, +FATAL+, or +UNKNOWN+.
- # +message+::
- # The log message. A String or Exception.
- # +progname+::
- # Program name string. Can be omitted. Treated as a message if no +message+ and
- # +block+ are given.
- # +block+::
- # Can be omitted. Called to get a message string if +message+ is nil.
- #
- # === Return
- #
- # +true+ if successful, +false+ otherwise.
- #
- # When the given severity is not high enough (for this particular logger), log
- # no message, and return +true+.
- #
- # === Description
- #
- # Log a message if the given severity is high enough. This is the generic
- # logging method. Users will be more inclined to use #debug, #info, #warn,
- # #error, and #fatal.
- #
- # <b>Message format</b>: +message+ can be any object, but it has to be
- # converted to a String in order to log it. Generally, +inspect+ is used
- # if the given object is not a String.
- # A special case is an +Exception+ object, which will be printed in detail,
- # including message, class, and backtrace. See #msg2str for the
- # implementation if required.
- #
- # === Bugs
- #
- # * Logfile is not locked.
- # * Append open does not need to lock file.
- # * But on the OS which supports multi I/O, records possibly be mixed.
- #
- def add(severity, message = nil, progname = nil, &block)
- severity ||= UNKNOWN
- if @logdev.nil? or severity < @level
- return true
- end
- progname ||= @progname
- if message.nil?
- if block_given?
- message = yield
- else
- message = progname
- progname = @progname
- end
- end
- @logdev.write(
- format_message(format_severity(severity), Time.now, progname, message))
- true
- end
- alias log add
-
- #
- # Dump given message to the log device without any formatting. If no log
- # device exists, return +nil+.
- #
- def <<(msg)
- unless @logdev.nil?
- @logdev.write(msg)
- end
- end
-
- #
- # Log a +DEBUG+ message.
- #
- # See #info for more information.
- #
- def debug(progname = nil, &block)
- add(DEBUG, nil, progname, &block)
- end
-
- #
- # Log an +INFO+ message.
- #
- # The message can come either from the +progname+ argument or the +block+. If
- # both are provided, then the +block+ is used as the message, and +progname+
- # is used as the program name.
- #
- # === Examples
- #
- # logger.info("MainApp") { "Received connection from #{ip}" }
- # # ...
- # logger.info "Waiting for input from user"
- # # ...
- # logger.info { "User typed #{input}" }
- #
- # You'll probably stick to the second form above, unless you want to provide a
- # program name (which you can do with <tt>Logger#progname=</tt> as well).
- #
- # === Return
- #
- # See #add.
- #
- def info(progname = nil, &block)
- add(INFO, nil, progname, &block)
- end
-
- #
- # Log a +WARN+ message.
- #
- # See #info for more information.
- #
- def warn(progname = nil, &block)
- add(WARN, nil, progname, &block)
- end
-
- #
- # Log an +ERROR+ message.
- #
- # See #info for more information.
- #
- def error(progname = nil, &block)
- add(ERROR, nil, progname, &block)
- end
-
- #
- # Log a +FATAL+ message.
- #
- # See #info for more information.
- #
- def fatal(progname = nil, &block)
- add(FATAL, nil, progname, &block)
- end
-
- #
- # Log an +UNKNOWN+ message. This will be printed no matter what the logger
- # level.
- #
- # See #info for more information.
- #
- def unknown(progname = nil, &block)
- add(UNKNOWN, nil, progname, &block)
- end
-
- #
- # Close the logging device.
- #
- def close
- @logdev.close if @logdev
- end
-
-private
-
- # Severity label for logging. (max 5 char)
- SEV_LABEL = %w(DEBUG INFO WARN ERROR FATAL ANY)
-
- def format_severity(severity)
- SEV_LABEL[severity] || 'ANY'
- end
-
- def format_message(severity, datetime, progname, msg)
- (@formatter || @default_formatter).call(severity, datetime, progname, msg)
- end
-
-
- class Formatter
- Format = "%s, [%s#%d] %5s -- %s: %s\n"
-
- attr_accessor :datetime_format
-
- def initialize
- @datetime_format = nil
- end
-
- def call(severity, time, progname, msg)
- Format % [severity[0..0], format_datetime(time), $$, severity, progname,
- msg2str(msg)]
- end
-
- private
-
- def format_datetime(time)
- if @datetime_format.nil?
- time.strftime("%Y-%m-%dT%H:%M:%S.") << "%06d " % time.usec
- else
- time.strftime(@datetime_format)
- end
- end
-
- def msg2str(msg)
- case msg
- when ::String
- msg
- when ::Exception
- "#{ msg.message } (#{ msg.class })\n" <<
- (msg.backtrace || []).join("\n")
- else
- msg.inspect
- end
- end
- end
-
-
- class LogDevice
- attr_reader :dev
- attr_reader :filename
-
- class LogDeviceMutex
- include MonitorMixin
- end
-
- def initialize(log = nil, opt = {})
- @dev = @filename = @shift_age = @shift_size = nil
- @mutex = LogDeviceMutex.new
- if log.respond_to?(:write) and log.respond_to?(:close)
- @dev = log
- else
- @dev = open_logfile(log)
- @dev.sync = true
- @filename = log
- @shift_age = opt[:shift_age] || 7
- @shift_size = opt[:shift_size] || 1048576
- end
- end
-
- def write(message)
- @mutex.synchronize do
- if @shift_age and @dev.respond_to?(:stat)
- begin
- check_shift_log
- rescue
- raise Logger::ShiftingError.new("Shifting failed. #{$!}")
- end
- end
- @dev.write(message)
- end
- end
-
- def close
- @mutex.synchronize do
- @dev.close
- end
- end
-
- private
-
- def open_logfile(filename)
- if (FileTest.exist?(filename))
- open(filename, (File::WRONLY | File::APPEND))
- else
- create_logfile(filename)
- end
- end
-
- def create_logfile(filename)
- logdev = open(filename, (File::WRONLY | File::APPEND | File::CREAT))
- logdev.sync = true
- add_log_header(logdev)
- logdev
- end
-
- def add_log_header(file)
- file.write(
- "# Logfile created on %s by %s\n" % [Time.now.to_s, Logger::ProgName]
- )
- end
-
- SiD = 24 * 60 * 60
-
- def check_shift_log
- if @shift_age.is_a?(Integer)
- # Note: always returns false if '0'.
- if @filename && (@shift_age > 0) && (@dev.stat.size > @shift_size)
- shift_log_age
- end
- else
- now = Time.now
- if @dev.stat.mtime <= previous_period_end(now)
- shift_log_period(now)
- end
- end
- end
-
- def shift_log_age
- (@shift_age-3).downto(0) do |i|
- if FileTest.exist?("#{@filename}.#{i}")
- File.rename("#{@filename}.#{i}", "#{@filename}.#{i+1}")
- end
- end
- @dev.close
- File.rename("#{@filename}", "#{@filename}.0")
- @dev = create_logfile(@filename)
- return true
- end
-
- def shift_log_period(now)
- postfix = previous_period_end(now).strftime("%Y%m%d") # YYYYMMDD
- age_file = "#{@filename}.#{postfix}"
- if FileTest.exist?(age_file)
- raise RuntimeError.new("'#{ age_file }' already exists.")
- end
- @dev.close
- File.rename("#{@filename}", age_file)
- @dev = create_logfile(@filename)
- return true
- end
-
- def previous_period_end(now)
- case @shift_age
- when /^daily$/
- eod(now - 1 * SiD)
- when /^weekly$/
- eod(now - ((now.wday + 1) * SiD))
- when /^monthly$/
- eod(now - now.mday * SiD)
- else
- now
- end
- end
-
- def eod(t)
- Time.mktime(t.year, t.month, t.mday, 23, 59, 59)
- end
- end
-
-
- #
- # == Description
- #
- # Application -- Add logging support to your application.
- #
- # == Usage
- #
- # 1. Define your application class as a sub-class of this class.
- # 2. Override 'run' method in your class to do many things.
- # 3. Instantiate it and invoke 'start'.
- #
- # == Example
- #
- # class FooApp < Application
- # def initialize(foo_app, application_specific, arguments)
- # super('FooApp') # Name of the application.
- # end
- #
- # def run
- # ...
- # log(WARN, 'warning', 'my_method1')
- # ...
- # @log.error('my_method2') { 'Error!' }
- # ...
- # end
- # end
- #
- # status = FooApp.new(....).start
- #
- class Application
- include Logger::Severity
-
- # Name of the application given at initialize.
- attr_reader :appname
-
- #
- # == Synopsis
- #
- # Application.new(appname = '')
- #
- # == Args
- #
- # +appname+:: Name of the application.
- #
- # == Description
- #
- # Create an instance. Log device is +STDERR+ by default. This can be
- # changed with #set_log.
- #
- def initialize(appname = nil)
- @appname = appname
- @log = Logger.new(STDERR)
- @log.progname = @appname
- @level = @log.level
- end
-
- #
- # Start the application. Return the status code.
- #
- def start
- status = -1
- begin
- log(INFO, "Start of #{ @appname }.")
- status = run
- rescue
- log(FATAL, "Detected an exception. Stopping ... #{$!} (#{$!.class})\n" << $@.join("\n"))
- ensure
- log(INFO, "End of #{ @appname }. (status: #{ status.to_s })")
- end
- status
- end
-
- # Logger for this application. See the class Logger for an explanation.
- def logger
- @log
- end
-
- #
- # Sets the logger for this application. See the class Logger for an explanation.
- #
- def logger=(logger)
- @log = logger
- end
-
- #
- # Sets the log device for this application. See <tt>Logger.new</tt> for an explanation
- # of the arguments.
- #
- def set_log(logdev, shift_age = 0, shift_size = 1024000)
- @log = Logger.new(logdev, shift_age, shift_size)
- @log.progname = @appname
- @log.level = @level
- end
-
- def log=(logdev)
- set_log(logdev)
- end
-
- #
- # Set the logging threshold, just like <tt>Logger#level=</tt>.
- #
- def level=(level)
- @level = level
- @log.level = @level
- end
-
- #
- # See Logger#add. This application's +appname+ is used.
- #
- def log(severity, message = nil, &block)
- @log.add(severity, message, @appname, &block) if @log
- end
-
- private
-
- def run
- raise RuntimeError.new('Method run must be defined in the derived class.')
- end
- end
-end
diff --git a/lib/mathn.rb b/lib/mathn.rb
deleted file mode 100644
index 0241f578e9..0000000000
--- a/lib/mathn.rb
+++ /dev/null
@@ -1,206 +0,0 @@
-#
-# mathn.rb -
-# $Release Version: 0.5 $
-# $Revision: 1.1.1.1.4.1 $
-# by Keiju ISHITSUKA(SHL Japan Inc.)
-#
-# --
-#
-#
-#
-
-require "cmath.rb"
-require "matrix.rb"
-require "prime.rb"
-
-require "mathn/rational"
-require "mathn/complex"
-
-unless defined?(Math.exp!)
- Object.instance_eval{remove_const :Math}
- Math = CMath
-end
-
-class Fixnum
- remove_method :/
- alias / quo
-
- alias power! ** unless defined?(0.power!)
-
- def ** (other)
- if self < 0 && other.round != other
- Complex(self, 0.0) ** other
- else
- power!(other)
- end
- end
-
-end
-
-class Bignum
- remove_method :/
- alias / quo
-
- alias power! ** unless defined?(0.power!)
-
- def ** (other)
- if self < 0 && other.round != other
- Complex(self, 0.0) ** other
- else
- power!(other)
- end
- end
-
-end
-
-class Rational
- def ** (other)
- if other.kind_of?(Rational)
- other2 = other
- if self < 0
- return Complex(self, 0.0) ** other
- elsif other == 0
- return Rational(1,1)
- elsif self == 0
- return Rational(0,1)
- elsif self == 1
- return Rational(1,1)
- end
-
- npd = numerator.prime_division
- dpd = denominator.prime_division
- if other < 0
- other = -other
- npd, dpd = dpd, npd
- end
-
- for elm in npd
- elm[1] = elm[1] * other
- if !elm[1].kind_of?(Integer) and elm[1].denominator != 1
- return Float(self) ** other2
- end
- elm[1] = elm[1].to_i
- end
-
- for elm in dpd
- elm[1] = elm[1] * other
- if !elm[1].kind_of?(Integer) and elm[1].denominator != 1
- return Float(self) ** other2
- end
- elm[1] = elm[1].to_i
- end
-
- num = Integer.from_prime_division(npd)
- den = Integer.from_prime_division(dpd)
-
- Rational(num,den)
-
- elsif other.kind_of?(Integer)
- if other > 0
- num = numerator ** other
- den = denominator ** other
- elsif other < 0
- num = denominator ** -other
- den = numerator ** -other
- elsif other == 0
- num = 1
- den = 1
- end
- Rational(num, den)
- elsif other.kind_of?(Float)
- Float(self) ** other
- else
- x , y = other.coerce(self)
- x ** y
- end
- end
-end
-
-module Math
- remove_method(:sqrt)
- def sqrt(a)
- if a.kind_of?(Complex)
- abs = sqrt(a.real*a.real + a.imag*a.imag)
-# if not abs.kind_of?(Rational)
-# return a**Rational(1,2)
-# end
- x = sqrt((a.real + abs)/Rational(2))
- y = sqrt((-a.real + abs)/Rational(2))
-# if !(x.kind_of?(Rational) and y.kind_of?(Rational))
-# return a**Rational(1,2)
-# end
- if a.imag >= 0
- Complex(x, y)
- else
- Complex(x, -y)
- end
- elsif a.respond_to?(:nan?) and a.nan?
- a
- elsif a >= 0
- rsqrt(a)
- else
- Complex(0,rsqrt(-a))
- end
- end
-
- def rsqrt(a)
- if a.kind_of?(Float)
- sqrt!(a)
- elsif a.kind_of?(Rational)
- rsqrt(a.numerator)/rsqrt(a.denominator)
- else
- src = a
- max = 2 ** 32
- byte_a = [src & 0xffffffff]
- # ruby's bug
- while (src >= max) and (src >>= 32)
- byte_a.unshift src & 0xffffffff
- end
-
- answer = 0
- main = 0
- side = 0
- for elm in byte_a
- main = (main << 32) + elm
- side <<= 16
- if answer != 0
- if main * 4 < side * side
- applo = main.div(side)
- else
- applo = ((sqrt!(side * side + 4 * main) - side)/2.0).to_i + 1
- end
- else
- applo = sqrt!(main).to_i + 1
- end
-
- while (x = (side + applo) * applo) > main
- applo -= 1
- end
- main -= x
- answer = (answer << 16) + applo
- side += applo * 2
- end
- if main == 0
- answer
- else
- sqrt!(a)
- end
- end
- end
-
- module_function :sqrt
- module_function :rsqrt
-end
-
-class Float
- alias power! **
-
- def ** (other)
- if self < 0 && other.round != other
- Complex(self, 0.0) ** other
- else
- power!(other)
- end
- end
-
-end
diff --git a/lib/matrix.rb b/lib/matrix.rb
deleted file mode 100644
index ec03c730fa..0000000000
--- a/lib/matrix.rb
+++ /dev/null
@@ -1,1382 +0,0 @@
-#!/usr/local/bin/ruby
-#--
-# matrix.rb -
-# $Release Version: 1.0$
-# $Revision: 1.13 $
-# Original Version from Smalltalk-80 version
-# on July 23, 1985 at 8:37:17 am
-# by Keiju ISHITSUKA
-#++
-#
-# = matrix.rb
-#
-# An implementation of Matrix and Vector classes.
-#
-# Author:: Keiju ISHITSUKA
-# Documentation:: Gavin Sinclair (sourced from <i>Ruby in a Nutshell</i> (Matsumoto, O'Reilly))
-#
-# See classes Matrix and Vector for documentation.
-#
-
-require "e2mmap.rb"
-
-module ExceptionForMatrix # :nodoc:
- extend Exception2MessageMapper
- def_e2message(TypeError, "wrong argument type %s (expected %s)")
- def_e2message(ArgumentError, "Wrong # of arguments(%d for %d)")
-
- def_exception("ErrDimensionMismatch", "\#{self.name} dimension mismatch")
- def_exception("ErrNotRegular", "Not Regular Matrix")
- def_exception("ErrOperationNotDefined", "This operation(%s) can\\'t defined")
-end
-
-#
-# The +Matrix+ class represents a mathematical matrix, and provides methods for creating
-# special-case matrices (zero, identity, diagonal, singular, vector), operating on them
-# arithmetically and algebraically, and determining their mathematical properties (trace, rank,
-# inverse, determinant).
-#
-# Note that although matrices should theoretically be rectangular, this is not
-# enforced by the class.
-#
-# Also note that the determinant of integer matrices may be incorrectly calculated unless you
-# also <tt>require 'mathn'</tt>. This may be fixed in the future.
-#
-# == Method Catalogue
-#
-# To create a matrix:
-# * <tt> Matrix[*rows] </tt>
-# * <tt> Matrix.[](*rows) </tt>
-# * <tt> Matrix.rows(rows, copy = true) </tt>
-# * <tt> Matrix.columns(columns) </tt>
-# * <tt> Matrix.diagonal(*values) </tt>
-# * <tt> Matrix.scalar(n, value) </tt>
-# * <tt> Matrix.scalar(n, value) </tt>
-# * <tt> Matrix.identity(n) </tt>
-# * <tt> Matrix.unit(n) </tt>
-# * <tt> Matrix.I(n) </tt>
-# * <tt> Matrix.zero(n) </tt>
-# * <tt> Matrix.row_vector(row) </tt>
-# * <tt> Matrix.column_vector(column) </tt>
-#
-# To access Matrix elements/columns/rows/submatrices/properties:
-# * <tt> [](i, j) </tt>
-# * <tt> #row_size </tt>
-# * <tt> #column_size </tt>
-# * <tt> #row(i) </tt>
-# * <tt> #column(j) </tt>
-# * <tt> #collect </tt>
-# * <tt> #map </tt>
-# * <tt> #minor(*param) </tt>
-#
-# Properties of a matrix:
-# * <tt> #regular? </tt>
-# * <tt> #singular? </tt>
-# * <tt> #square? </tt>
-#
-# Matrix arithmetic:
-# * <tt> *(m) </tt>
-# * <tt> +(m) </tt>
-# * <tt> -(m) </tt>
-# * <tt> #/(m) </tt>
-# * <tt> #inverse </tt>
-# * <tt> #inv </tt>
-# * <tt> ** </tt>
-#
-# Matrix functions:
-# * <tt> #determinant </tt>
-# * <tt> #det </tt>
-# * <tt> #rank </tt>
-# * <tt> #trace </tt>
-# * <tt> #tr </tt>
-# * <tt> #transpose </tt>
-# * <tt> #t </tt>
-#
-# Conversion to other data types:
-# * <tt> #coerce(other) </tt>
-# * <tt> #row_vectors </tt>
-# * <tt> #column_vectors </tt>
-# * <tt> #to_a </tt>
-#
-# String representations:
-# * <tt> #to_s </tt>
-# * <tt> #inspect </tt>
-#
-class Matrix
- @RCS_ID='-$Id: matrix.rb,v 1.13 2001/12/09 14:22:23 keiju Exp keiju $-'
-
-# extend Exception2MessageMapper
- include ExceptionForMatrix
-
- # instance creations
- private_class_method :new
-
- #
- # Creates a matrix where each argument is a row.
- # Matrix[ [25, 93], [-1, 66] ]
- # => 25 93
- # -1 66
- #
- def Matrix.[](*rows)
- new(:init_rows, rows, false)
- end
-
- #
- # Creates a matrix where +rows+ is an array of arrays, each of which is a row
- # to the matrix. If the optional argument +copy+ is false, use the given
- # arrays as the internal structure of the matrix without copying.
- # Matrix.rows([[25, 93], [-1, 66]])
- # => 25 93
- # -1 66
- def Matrix.rows(rows, copy = true)
- new(:init_rows, rows, copy)
- end
-
- #
- # Creates a matrix using +columns+ as an array of column vectors.
- # Matrix.columns([[25, 93], [-1, 66]])
- # => 25 -1
- # 93 66
- #
- #
- def Matrix.columns(columns)
- rows = (0 .. columns[0].size - 1).collect {|i|
- (0 .. columns.size - 1).collect {|j|
- columns[j][i]
- }
- }
- Matrix.rows(rows, false)
- end
-
- #
- # Creates a matrix where the diagonal elements are composed of +values+.
- # Matrix.diagonal(9, 5, -3)
- # => 9 0 0
- # 0 5 0
- # 0 0 -3
- #
- def Matrix.diagonal(*values)
- size = values.size
- rows = (0 .. size - 1).collect {|j|
- row = Array.new(size).fill(0, 0, size)
- row[j] = values[j]
- row
- }
- rows(rows, false)
- end
-
- #
- # Creates an +n+ by +n+ diagonal matrix where each diagonal element is
- # +value+.
- # Matrix.scalar(2, 5)
- # => 5 0
- # 0 5
- #
- def Matrix.scalar(n, value)
- Matrix.diagonal(*Array.new(n).fill(value, 0, n))
- end
-
- #
- # Creates an +n+ by +n+ identity matrix.
- # Matrix.identity(2)
- # => 1 0
- # 0 1
- #
- def Matrix.identity(n)
- Matrix.scalar(n, 1)
- end
- class << Matrix
- alias unit identity
- alias I identity
- end
-
- #
- # Creates an +n+ by +n+ zero matrix.
- # Matrix.zero(2)
- # => 0 0
- # 0 0
- #
- def Matrix.zero(n)
- Matrix.scalar(n, 0)
- end
-
- #
- # Creates a single-row matrix where the values of that row are as given in
- # +row+.
- # Matrix.row_vector([4,5,6])
- # => 4 5 6
- #
- def Matrix.row_vector(row)
- case row
- when Vector
- Matrix.rows([row.to_a], false)
- when Array
- Matrix.rows([row.dup], false)
- else
- Matrix.rows([[row]], false)
- end
- end
-
- #
- # Creates a single-column matrix where the values of that column are as given
- # in +column+.
- # Matrix.column_vector([4,5,6])
- # => 4
- # 5
- # 6
- #
- def Matrix.column_vector(column)
- case column
- when Vector
- Matrix.columns([column.to_a])
- when Array
- Matrix.columns([column])
- else
- Matrix.columns([[column]])
- end
- end
-
- #
- # This method is used by the other methods that create matrices, and is of no
- # use to general users.
- #
- def initialize(init_method, *argv)
- self.send(init_method, *argv)
- end
-
- def init_rows(rows, copy)
- if copy
- @rows = rows.collect{|row| row.dup}
- else
- @rows = rows
- end
- self
- end
- private :init_rows
-
- #
- # Returns element (+i+,+j+) of the matrix. That is: row +i+, column +j+.
- #
- def [](i, j)
- @rows[i][j]
- end
- alias element []
- alias component []
-
- def []=(i, j, v)
- @rows[i][j] = v
- end
- alias set_element []=
- alias set_component []=
- private :[]=, :set_element, :set_component
-
- #
- # Returns the number of rows.
- #
- def row_size
- @rows.size
- end
-
- #
- # Returns the number of columns. Note that it is possible to construct a
- # matrix with uneven columns (e.g. Matrix[ [1,2,3], [4,5] ]), but this is
- # mathematically unsound. This method uses the first row to determine the
- # result.
- #
- def column_size
- @rows[0].size
- end
-
- #
- # Returns row vector number +i+ of the matrix as a Vector (starting at 0 like
- # an array). When a block is given, the elements of that vector are iterated.
- #
- def row(i) # :yield: e
- if block_given?
- for e in @rows[i]
- yield e
- end
- else
- Vector.elements(@rows[i])
- end
- end
-
- #
- # Returns column vector number +j+ of the matrix as a Vector (starting at 0
- # like an array). When a block is given, the elements of that vector are
- # iterated.
- #
- def column(j) # :yield: e
- if block_given?
- 0.upto(row_size - 1) do |i|
- yield @rows[i][j]
- end
- else
- col = (0 .. row_size - 1).collect {|i|
- @rows[i][j]
- }
- Vector.elements(col, false)
- end
- end
-
- #
- # Returns a matrix that is the result of iteration of the given block over all
- # elements of the matrix.
- # Matrix[ [1,2], [3,4] ].collect { |e| e**2 }
- # => 1 4
- # 9 16
- #
- def collect # :yield: e
- rows = @rows.collect{|row| row.collect{|e| yield e}}
- Matrix.rows(rows, false)
- end
- alias map collect
-
- #
- # Returns a section of the matrix. The parameters are either:
- # * start_row, nrows, start_col, ncols; OR
- # * col_range, row_range
- #
- # Matrix.diagonal(9, 5, -3).minor(0..1, 0..2)
- # => 9 0 0
- # 0 5 0
- #
- def minor(*param)
- case param.size
- when 2
- from_row = param[0].first
- size_row = param[0].end - from_row
- size_row += 1 unless param[0].exclude_end?
- from_col = param[1].first
- size_col = param[1].end - from_col
- size_col += 1 unless param[1].exclude_end?
- when 4
- from_row = param[0]
- size_row = param[1]
- from_col = param[2]
- size_col = param[3]
- else
- Matrix.Raise ArgumentError, param.inspect
- end
-
- rows = @rows[from_row, size_row].collect{|row|
- row[from_col, size_col]
- }
- Matrix.rows(rows, false)
- end
-
- #--
- # TESTING -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
- #++
-
- #
- # Returns +true+ if this is a regular matrix.
- #
- def regular?
- square? and rank == column_size
- end
-
- #
- # Returns +true+ is this is a singular (i.e. non-regular) matrix.
- #
- def singular?
- not regular?
- end
-
- #
- # Returns +true+ is this is a square matrix. See note in column_size about this
- # being unreliable, though.
- #
- def square?
- column_size == row_size
- end
-
- #--
- # OBJECT METHODS -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
- #++
-
- #
- # Returns +true+ if and only if the two matrices contain equal elements.
- #
- def ==(other)
- return false unless Matrix === other
-
- other.compare_by_row_vectors(@rows)
- end
- def eql?(other)
- return false unless Matrix === other
-
- other.compare_by_row_vectors(@rows, :eql?)
- end
-
- #
- # Not really intended for general consumption.
- #
- def compare_by_row_vectors(rows, comparison = :==)
- return false unless @rows.size == rows.size
-
- 0.upto(@rows.size - 1) do |i|
- return false unless @rows[i].send(comparison, rows[i])
- end
- true
- end
-
- #
- # Returns a clone of the matrix, so that the contents of each do not reference
- # identical objects.
- #
- def clone
- Matrix.rows(@rows)
- end
-
- #
- # Returns a hash-code for the matrix.
- #
- def hash
- value = 0
- for row in @rows
- for e in row
- value ^= e.hash
- end
- end
- return value
- end
-
- #--
- # ARITHMETIC -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
- #++
-
- #
- # Matrix multiplication.
- # Matrix[[2,4], [6,8]] * Matrix.identity(2)
- # => 2 4
- # 6 8
- #
- def *(m) # m is matrix or vector or number
- case(m)
- when Numeric
- rows = @rows.collect {|row|
- row.collect {|e|
- e * m
- }
- }
- return Matrix.rows(rows, false)
- when Vector
- m = Matrix.column_vector(m)
- r = self * m
- return r.column(0)
- when Matrix
- Matrix.Raise ErrDimensionMismatch if column_size != m.row_size
-
- rows = (0 .. row_size - 1).collect {|i|
- (0 .. m.column_size - 1).collect {|j|
- vij = 0
- 0.upto(column_size - 1) do |k|
- vij += self[i, k] * m[k, j]
- end
- vij
- }
- }
- return Matrix.rows(rows, false)
- else
- x, y = m.coerce(self)
- return x * y
- end
- end
-
- #
- # Matrix addition.
- # Matrix.scalar(2,5) + Matrix[[1,0], [-4,7]]
- # => 6 0
- # -4 12
- #
- def +(m)
- case m
- when Numeric
- Matrix.Raise ErrOperationNotDefined, "+"
- when Vector
- m = Matrix.column_vector(m)
- when Matrix
- else
- x, y = m.coerce(self)
- return x + y
- end
-
- Matrix.Raise ErrDimensionMismatch unless row_size == m.row_size and column_size == m.column_size
-
- rows = (0 .. row_size - 1).collect {|i|
- (0 .. column_size - 1).collect {|j|
- self[i, j] + m[i, j]
- }
- }
- Matrix.rows(rows, false)
- end
-
- #
- # Matrix subtraction.
- # Matrix[[1,5], [4,2]] - Matrix[[9,3], [-4,1]]
- # => -8 2
- # 8 1
- #
- def -(m)
- case m
- when Numeric
- Matrix.Raise ErrOperationNotDefined, "-"
- when Vector
- m = Matrix.column_vector(m)
- when Matrix
- else
- x, y = m.coerce(self)
- return x - y
- end
-
- Matrix.Raise ErrDimensionMismatch unless row_size == m.row_size and column_size == m.column_size
-
- rows = (0 .. row_size - 1).collect {|i|
- (0 .. column_size - 1).collect {|j|
- self[i, j] - m[i, j]
- }
- }
- Matrix.rows(rows, false)
- end
-
- #
- # Matrix division (multiplication by the inverse).
- # Matrix[[7,6], [3,9]] / Matrix[[2,9], [3,1]]
- # => -7 1
- # -3 -6
- #
- def /(other)
- case other
- when Numeric
- rows = @rows.collect {|row|
- row.collect {|e|
- e / other
- }
- }
- return Matrix.rows(rows, false)
- when Matrix
- return self * other.inverse
- else
- x, y = other.coerce(self)
- rerurn x / y
- end
- end
-
- #
- # Returns the inverse of the matrix.
- # Matrix[[1, 2], [2, 1]].inverse
- # => -1 1
- # 0 -1
- #
- def inverse
- Matrix.Raise ErrDimensionMismatch unless square?
- Matrix.I(row_size).inverse_from(self)
- end
- alias inv inverse
-
- #
- # Not for public consumption?
- #
- def inverse_from(src)
- size = row_size - 1
- a = src.to_a
-
- for k in 0..size
- i = k
- akk = a[k][k].abs
- ((k+1)..size).each do |j|
- v = a[j][k].abs
- if v > akk
- i = j
- akk = v
- end
- end
- Matrix.Raise ErrNotRegular if akk == 0
- if i != k
- a[i], a[k] = a[k], a[i]
- @rows[i], @rows[k] = @rows[k], @rows[i]
- end
- akk = a[k][k]
-
- for i in 0 .. size
- next if i == k
- q = a[i][k].quo(akk)
- a[i][k] = 0
-
- for j in (k + 1).. size
- a[i][j] -= a[k][j] * q
- end
- for j in 0..size
- @rows[i][j] -= @rows[k][j] * q
- end
- end
-
- for j in (k + 1).. size
- a[k][j] = a[k][j].quo(akk)
- end
- for j in 0..size
- @rows[k][j] = @rows[k][j].quo(akk)
- end
- end
- self
- end
- #alias reciprocal inverse
-
- #
- # Matrix exponentiation. Defined for integer powers only. Equivalent to
- # multiplying the matrix by itself N times.
- # Matrix[[7,6], [3,9]] ** 2
- # => 67 96
- # 48 99
- #
- def ** (other)
- if other.kind_of?(Integer)
- x = self
- if other <= 0
- x = self.inverse
- return Matrix.identity(self.column_size) if other == 0
- other = -other
- end
- z = x
- n = other - 1
- while n != 0
- while (div, mod = n.divmod(2)
- mod == 0)
- x = x * x
- n = div
- end
- z *= x
- n -= 1
- end
- z
- elsif other.kind_of?(Float) || defined?(Rational) && other.kind_of?(Rational)
- Matrix.Raise ErrOperationNotDefined, "**"
- else
- Matrix.Raise ErrOperationNotDefined, "**"
- end
- end
-
- #--
- # MATRIX FUNCTIONS -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
- #++
-
- #
- # Returns the determinant of the matrix. If the matrix is not square, the
- # result is 0. This method's algorism is Gaussian elimination method
- # and using Numeric#quo(). Beware that using Float values, with their
- # usual lack of precision, can affect the value returned by this method. Use
- # Rational values or Matrix#det_e instead if this is important to you.
- #
- # Matrix[[7,6], [3,9]].determinant
- # => 63.0
- #
- def determinant
- return 0 unless square?
-
- size = row_size - 1
- a = to_a
-
- det = 1
- k = 0
- loop do
- if (akk = a[k][k]) == 0
- i = k
- loop do
- return 0 if (ii += 1) > size
- break unless a[i][k] == 0
- end
- a[i], a[k] = a[k], a[i]
- akk = a[k][k]
- det *= -1
- end
-
- for i in k + 1 .. size
- q = a[i][k].quo(akk)
- (k + 1).upto(size) do |j|
- a[i][j] -= a[k][j] * q
- end
- end
- det *= akk
- break unless (k += 1) <= size
- end
- det
- end
- alias det determinant
-
- #
- # Returns the determinant of the matrix. If the matrix is not square, the
- # result is 0. This method's algorism is Gaussian elimination method.
- # This method uses Euclidean algorism. If all elements are integer,
- # really exact value. But, if an element is a float, can't return
- # exact value.
- #
- # Matrix[[7,6], [3,9]].determinant
- # => 63
- #
- def determinant_e
- return 0 unless square?
-
- size = row_size - 1
- a = to_a
-
- det = 1
- k = 0
- loop do
- if a[k][k].zero?
- i = k
- loop do
- return 0 if (i += 1) > size
- break unless a[i][k].zero?
- end
- a[i], a[k] = a[k], a[i]
- det *= -1
- end
-
- for i in (k + 1)..size
- q = a[i][k].quo(a[k][k])
- k.upto(size) do |j|
- a[i][j] -= a[k][j] * q
- end
- unless a[i][k].zero?
- a[i], a[k] = a[k], a[i]
- det *= -1
- redo
- end
- end
- det *= a[k][k]
- break unless (k += 1) <= size
- end
- det
- end
- alias det_e determinant_e
-
- #
- # Returns the rank of the matrix. Beware that using Float values,
- # probably return faild value. Use Rational values or Matrix#rank_e
- # for getting exact result.
- #
- # Matrix[[7,6], [3,9]].rank
- # => 2
- #
- def rank
- if column_size > row_size
- a = transpose.to_a
- a_column_size = row_size
- a_row_size = column_size
- else
- a = to_a
- a_column_size = column_size
- a_row_size = row_size
- end
- rank = 0
- k = 0
- loop do
- if (akk = a[k][k]) == 0
- i = k
- exists = true
- loop do
- if (i += 1) > a_column_size - 1
- exists = false
- break
- end
- break unless a[i][k] == 0
- end
- if exists
- a[i], a[k] = a[k], a[i]
- akk = a[k][k]
- else
- i = k
- exists = true
- loop do
- if (i += 1) > a_row_size - 1
- exists = false
- break
- end
- break unless a[k][i] == 0
- end
- if exists
- k.upto(a_column_size - 1) do |j|
- a[j][k], a[j][i] = a[j][i], a[j][k]
- end
- akk = a[k][k]
- else
- next
- end
- end
- end
-
- for i in (k + 1)..(a_row_size - 1)
- q = a[i][k].quo(akk)
- for j in (k + 1)..(a_column_size - 1)
- a[i][j] -= a[k][j] * q
- end
- end
- rank += 1
- break unless (k += 1) <= a_column_size - 1
- end
- return rank
- end
-
- #
- # Returns the rank of the matrix. This method uses Euclidean
- # algorism. If all elements are integer, really exact value. But, if
- # an element is a float, can't return exact value.
- #
- # Matrix[[7,6], [3,9]].rank
- # => 2
- #
- def rank_e
- a = to_a
- a_column_size = column_size
- a_row_size = row_size
- pi = 0
- (0 ... a_column_size).each do |j|
- if i = (pi ... a_row_size).find{|i0| !a[i0][j].zero?}
- if i != pi
- a[pi], a[i] = a[i], a[pi]
- end
- (pi + 1 ... a_row_size).each do |k|
- q = a[k][j].quo(a[pi][j])
- (pi ... a_column_size).each do |j0|
- a[k][j0] -= q * a[pi][j0]
- end
- if k > pi && !a[k][j].zero?
- a[k], a[pi] = a[pi], a[k]
- redo
- end
- end
- pi += 1
- end
- end
- pi
- end
-
-
- #
- # Returns the trace (sum of diagonal elements) of the matrix.
- # Matrix[[7,6], [3,9]].trace
- # => 16
- #
- def trace
- tr = 0
- 0.upto(column_size - 1) do |i|
- tr += @rows[i][i]
- end
- tr
- end
- alias tr trace
-
- #
- # Returns the transpose of the matrix.
- # Matrix[[1,2], [3,4], [5,6]]
- # => 1 2
- # 3 4
- # 5 6
- # Matrix[[1,2], [3,4], [5,6]].transpose
- # => 1 3 5
- # 2 4 6
- #
- def transpose
- Matrix.columns(@rows)
- end
- alias t transpose
-
- #--
- # CONVERTING -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
- #++
-
- #
- # FIXME: describe #coerce.
- #
- def coerce(other)
- case other
- when Numeric
- return Scalar.new(other), self
- else
- raise TypeError, "#{self.class} can't be coerced into #{other.class}"
- end
- end
-
- #
- # Returns an array of the row vectors of the matrix. See Vector.
- #
- def row_vectors
- rows = (0 .. row_size - 1).collect {|i|
- row(i)
- }
- rows
- end
-
- #
- # Returns an array of the column vectors of the matrix. See Vector.
- #
- def column_vectors
- columns = (0 .. column_size - 1).collect {|i|
- column(i)
- }
- columns
- end
-
- #
- # Returns an array of arrays that describe the rows of the matrix.
- #
- def to_a
- @rows.collect{|row| row.collect{|e| e}}
- end
-
- def elements_to_f
- collect{|e| e.to_f}
- end
-
- def elements_to_i
- collect{|e| e.to_i}
- end
-
- def elements_to_r
- collect{|e| e.to_r}
- end
-
- #--
- # PRINTING -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
- #++
-
- #
- # Overrides Object#to_s
- #
- def to_s
- "Matrix[" + @rows.collect{|row|
- "[" + row.collect{|e| e.to_s}.join(", ") + "]"
- }.join(", ")+"]"
- end
-
- #
- # Overrides Object#inspect
- #
- def inspect
- "Matrix"+@rows.inspect
- end
-
- # Private CLASS
-
- class Scalar < Numeric # :nodoc:
- include ExceptionForMatrix
-
- def initialize(value)
- @value = value
- end
-
- # ARITHMETIC
- def +(other)
- case other
- when Numeric
- Scalar.new(@value + other)
- when Vector, Matrix
- Scalar.Raise WrongArgType, other.class, "Numeric or Scalar"
- when Scalar
- Scalar.new(@value + other.value)
- else
- x, y = other.coerce(self)
- x + y
- end
- end
-
- def -(other)
- case other
- when Numeric
- Scalar.new(@value - other)
- when Vector, Matrix
- Scalar.Raise WrongArgType, other.class, "Numeric or Scalar"
- when Scalar
- Scalar.new(@value - other.value)
- else
- x, y = other.coerce(self)
- x - y
- end
- end
-
- def *(other)
- case other
- when Numeric
- Scalar.new(@value * other)
- when Vector, Matrix
- other.collect{|e| @value * e}
- else
- x, y = other.coerce(self)
- x * y
- end
- end
-
- def / (other)
- case other
- when Numeric
- Scalar.new(@value / other)
- when Vector
- Scalar.Raise WrongArgType, other.class, "Numeric or Scalar or Matrix"
- when Matrix
- self * other.inverse
- else
- x, y = other.coerce(self)
- x.quo(y)
- end
- end
-
- def ** (other)
- case other
- when Numeric
- Scalar.new(@value ** other)
- when Vector
- Scalar.Raise WrongArgType, other.class, "Numeric or Scalar or Matrix"
- when Matrix
- other.powered_by(self)
- else
- x, y = other.coerce(self)
- x ** y
- end
- end
- end
-end
-
-
-#
-# The +Vector+ class represents a mathematical vector, which is useful in its own right, and
-# also constitutes a row or column of a Matrix.
-#
-# == Method Catalogue
-#
-# To create a Vector:
-# * <tt> Vector.[](*array) </tt>
-# * <tt> Vector.elements(array, copy = true) </tt>
-#
-# To access elements:
-# * <tt> [](i) </tt>
-#
-# To enumerate the elements:
-# * <tt> #each2(v) </tt>
-# * <tt> #collect2(v) </tt>
-#
-# Vector arithmetic:
-# * <tt> *(x) "is matrix or number" </tt>
-# * <tt> +(v) </tt>
-# * <tt> -(v) </tt>
-#
-# Vector functions:
-# * <tt> #inner_product(v) </tt>
-# * <tt> #collect </tt>
-# * <tt> #map </tt>
-# * <tt> #map2(v) </tt>
-# * <tt> #r </tt>
-# * <tt> #size </tt>
-#
-# Conversion to other data types:
-# * <tt> #covector </tt>
-# * <tt> #to_a </tt>
-# * <tt> #coerce(other) </tt>
-#
-# String representations:
-# * <tt> #to_s </tt>
-# * <tt> #inspect </tt>
-#
-class Vector
- include ExceptionForMatrix
-
- #INSTANCE CREATION
-
- private_class_method :new
-
- #
- # Creates a Vector from a list of elements.
- # Vector[7, 4, ...]
- #
- def Vector.[](*array)
- new(:init_elements, array, copy = false)
- end
-
- #
- # Creates a vector from an Array. The optional second argument specifies
- # whether the array itself or a copy is used internally.
- #
- def Vector.elements(array, copy = true)
- new(:init_elements, array, copy)
- end
-
- #
- # For internal use.
- #
- def initialize(method, array, copy)
- self.send(method, array, copy)
- end
-
- #
- # For internal use.
- #
- def init_elements(array, copy)
- if copy
- @elements = array.dup
- else
- @elements = array
- end
- end
-
- # ACCESSING
-
- #
- # Returns element number +i+ (starting at zero) of the vector.
- #
- def [](i)
- @elements[i]
- end
- alias element []
- alias component []
-
- def []=(i, v)
- @elements[i]= v
- end
- alias set_element []=
- alias set_component []=
- private :[]=, :set_element, :set_component
-
- #
- # Returns the number of elements in the vector.
- #
- def size
- @elements.size
- end
-
- #--
- # ENUMERATIONS -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
- #++
-
- #
- # Iterate over the elements of this vector and +v+ in conjunction.
- #
- def each2(v) # :yield: e1, e2
- Vector.Raise ErrDimensionMismatch if size != v.size
- 0.upto(size - 1) do |i|
- yield @elements[i], v[i]
- end
- end
-
- #
- # Collects (as in Enumerable#collect) over the elements of this vector and +v+
- # in conjunction.
- #
- def collect2(v) # :yield: e1, e2
- Vector.Raise ErrDimensionMismatch if size != v.size
- (0 .. size - 1).collect do |i|
- yield @elements[i], v[i]
- end
- end
-
- #--
- # COMPARING -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
- #++
-
- #
- # Returns +true+ iff the two vectors have the same elements in the same order.
- #
- def ==(other)
- return false unless Vector === other
-
- other.compare_by(@elements)
- end
- def eql?(other)
- return false unless Vector === other
-
- other.compare_by(@elements, :eql?)
- end
-
- #
- # For internal use.
- #
- def compare_by(elements, comparison = :==)
- @elements.send(comparison, elements)
- end
-
- #
- # Return a copy of the vector.
- #
- def clone
- Vector.elements(@elements)
- end
-
- #
- # Return a hash-code for the vector.
- #
- def hash
- @elements.hash
- end
-
- #--
- # ARITHMETIC -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
- #++
-
- #
- # Multiplies the vector by +x+, where +x+ is a number or another vector.
- #
- def *(x)
- case x
- when Numeric
- els = @elements.collect{|e| e * x}
- Vector.elements(els, false)
- when Matrix
- Matrix.column_vector(self) * x
- else
- s, x = x.coerce(self)
- s * x
- end
- end
-
- #
- # Vector addition.
- #
- def +(v)
- case v
- when Vector
- Vector.Raise ErrDimensionMismatch if size != v.size
- els = collect2(v) {|v1, v2|
- v1 + v2
- }
- Vector.elements(els, false)
- when Matrix
- Matrix.column_vector(self) + v
- else
- s, x = v.coerce(self)
- s + x
- end
- end
-
- #
- # Vector subtraction.
- #
- def -(v)
- case v
- when Vector
- Vector.Raise ErrDimensionMismatch if size != v.size
- els = collect2(v) {|v1, v2|
- v1 - v2
- }
- Vector.elements(els, false)
- when Matrix
- Matrix.column_vector(self) - v
- else
- s, x = v.coerce(self)
- s - x
- end
- end
-
- #--
- # VECTOR FUNCTIONS -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
- #++
-
- #
- # Returns the inner product of this vector with the other.
- # Vector[4,7].inner_product Vector[10,1] => 47
- #
- def inner_product(v)
- Vector.Raise ErrDimensionMismatch if size != v.size
-
- p = 0
- each2(v) {|v1, v2|
- p += v1 * v2
- }
- p
- end
-
- #
- # Like Array#collect.
- #
- def collect # :yield: e
- els = @elements.collect {|v|
- yield v
- }
- Vector.elements(els, false)
- end
- alias map collect
-
- #
- # Like Vector#collect2, but returns a Vector instead of an Array.
- #
- def map2(v) # :yield: e1, e2
- els = collect2(v) {|v1, v2|
- yield v1, v2
- }
- Vector.elements(els, false)
- end
-
- #
- # Returns the modulus (Pythagorean distance) of the vector.
- # Vector[5,8,2].r => 9.643650761
- #
- def r
- v = 0
- for e in @elements
- v += e*e
- end
- return Math.sqrt(v)
- end
-
- #--
- # CONVERTING
- #++
-
- #
- # Creates a single-row matrix from this vector.
- #
- def covector
- Matrix.row_vector(self)
- end
-
- #
- # Returns the elements of the vector in an array.
- #
- def to_a
- @elements.dup
- end
-
- def elements_to_f
- collect{|e| e.to_f}
- end
-
- def elements_to_i
- collect{|e| e.to_i}
- end
-
- def elements_to_r
- collect{|e| e.to_r}
- end
-
- #
- # FIXME: describe Vector#coerce.
- #
- def coerce(other)
- case other
- when Numeric
- return Matrix::Scalar.new(other), self
- else
- raise TypeError, "#{self.class} can't be coerced into #{other.class}"
- end
- end
-
- #--
- # PRINTING -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
- #++
-
- #
- # Overrides Object#to_s
- #
- def to_s
- "Vector[" + @elements.join(", ") + "]"
- end
-
- #
- # Overrides Object#inspect
- #
- def inspect
- str = "Vector"+@elements.inspect
- end
-end
-
-
-# Documentation comments:
-# - Matrix#coerce and Vector#coerce need to be documented
diff --git a/lib/minitest/autorun.rb b/lib/minitest/autorun.rb
deleted file mode 100644
index a9f9c67166..0000000000
--- a/lib/minitest/autorun.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-############################################################
-# This file is imported from a different project.
-# DO NOT make modifications in this repo.
-# File a patch instead and assign it to Ryan Davis
-############################################################
-
-require 'minitest/unit'
-
-MiniTest::Unit.autorun
diff --git a/lib/minitest/mock.rb b/lib/minitest/mock.rb
deleted file mode 100644
index 54af28c453..0000000000
--- a/lib/minitest/mock.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-############################################################
-# This file is imported from a different project.
-# DO NOT make modifications in this repo.
-# File a patch instead and assign it to Ryan Davis
-############################################################
-
-class MockExpectationError < StandardError; end
-
-module MiniTest
- class Mock
- def initialize
- @expected_calls = {}
- @actual_calls = Hash.new {|h,k| h[k] = [] }
- end
-
- def expect(name, retval, args=[])
- n, r, a = name, retval, args # for the closure below
- @expected_calls[name] = { :retval => retval, :args => args }
- self.class.__send__(:define_method, name) { |*x|
- raise ArgumentError unless @expected_calls[n][:args].size == x.size
- @actual_calls[n] << { :retval => r, :args => x }
- retval
- }
- self
- end
-
- def verify
- @expected_calls.each_key do |name|
- expected = @expected_calls[name]
- msg = "expected #{name}, #{expected.inspect}"
- raise MockExpectationError, msg unless
- @actual_calls.has_key? name and @actual_calls[name].include?(expected)
- end
- true
- end
- end
-end
diff --git a/lib/minitest/spec.rb b/lib/minitest/spec.rb
deleted file mode 100644
index 2158ec0d7b..0000000000
--- a/lib/minitest/spec.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-############################################################
-# This file is imported from a different project.
-# DO NOT make modifications in this repo.
-# File a patch instead and assign it to Ryan Davis
-############################################################
-
-#!/usr/bin/ruby -w
-
-require 'minitest/unit'
-
-class Module
- def infect_with_assertions pos_prefix, neg_prefix, skip_re, map = {}
- MiniTest::Assertions.public_instance_methods(false).each do |meth|
- meth = meth.to_s
-
- new_name = case meth
- when /^assert/ then
- meth.sub(/^assert/, pos_prefix.to_s)
- when /^refute/ then
- meth.sub(/^refute/, neg_prefix.to_s)
- end
- next unless new_name
- next if new_name =~ skip_re
-
- regexp, replacement = map.find { |re, _| new_name =~ re }
- new_name.sub! regexp, replacement if replacement
-
- # warn "%-22p -> %p %p" % [meth, new_name, regexp]
- self.class_eval <<-EOM
- def #{new_name} *args, &block
- return MiniTest::Spec.current.#{meth}(*args, &self) if Proc === self
- return MiniTest::Spec.current.#{meth}(args.first, self) if args.size == 1
- return MiniTest::Spec.current.#{meth}(self, *args)
- end
- EOM
- end
- end
-end
-
-Object.infect_with_assertions(:must, :wont,
- /^(must|wont)$|wont_(throw)|
- must_(block|not?_|nothing|raise$)/x,
- /(must_throw)s/ => '\1',
- /(?!not)_same/ => '_be_same_as',
- /_in_/ => '_be_within_',
- /_operator/ => '_be',
- /_includes/ => '_include',
- /(must|wont)_(.*_of|nil|empty)/ => '\1_be_\2',
- /must_raises/ => 'must_raise')
-
-class Object
- alias :must_be_close_to :must_be_within_delta
- alias :wont_be_close_to :wont_be_within_delta
-end
-
-module Kernel
- def describe desc, &block
- cls = Class.new(MiniTest::Spec)
- Object.const_set desc.to_s.split(/\W+/).map { |s| s.capitalize }.join, cls
-
- cls.class_eval(&block)
- end
- private :describe
-end
-
-class MiniTest::Spec < MiniTest::Unit::TestCase
- def self.current
- @@current_spec
- end
-
- def initialize name
- super
- @@current_spec = self
- end
-
- def self.before(type = :each, &block)
- raise "unsupported before type: #{type}" unless type == :each
- define_method :setup, &block
- end
-
- def self.after(type = :each, &block)
- raise "unsupported after type: #{type}" unless type == :each
- define_method :teardown, &block
- end
-
- def self.it desc, &block
- define_method "test_#{desc.gsub(/\W+/, '_').downcase}", &block
- end
-end
diff --git a/lib/minitest/unit.rb b/lib/minitest/unit.rb
deleted file mode 100644
index 0f71126b0b..0000000000
--- a/lib/minitest/unit.rb
+++ /dev/null
@@ -1,497 +0,0 @@
-############################################################
-# This file is imported from a different project.
-# DO NOT make modifications in this repo.
-# File a patch instead and assign it to Ryan Davis
-############################################################
-
-##
-#
-# Totally minimal drop-in replacement for test-unit
-#
-# TODO: refute -> debunk, prove/rebut, show/deny... lots of possibilities
-
-module MiniTest
- class Assertion < Exception; end
- class Skip < Assertion; end
-
- file = if RUBY_VERSION =~ /^1\.9/ then # bt's expanded, but __FILE__ isn't :(
- File.expand_path __FILE__
- elsif __FILE__ =~ /^[^\.]/ then # assume both relative
- require 'pathname'
- pwd = Pathname.new Dir.pwd
- pn = Pathname.new File.expand_path(__FILE__)
- pn = File.join(".", pn.relative_path_from(pwd)) unless pn.relative?
- pn.to_s
- else # assume both are expanded
- __FILE__
- end
-
- # './lib' in project dir, or '/usr/local/blahblah' if installed
- MINI_DIR = File.dirname(File.dirname(file))
-
- def self.filter_backtrace bt
- return ["No backtrace"] unless bt
-
- new_bt = []
- bt.each do |line|
- break if line.rindex(MINI_DIR, 0)
- new_bt << line
- end
-
- new_bt = bt.reject { |line| line.rindex(MINI_DIR, 0) } if new_bt.empty?
- new_bt = bt.dup if new_bt.empty?
- new_bt
- end
-
- module Assertions
- def mu_pp(obj)
- s = obj.inspect
- s = s.force_encoding(Encoding.default_external) if defined? Encoding
- s
- end
-
- def _assertions= n
- @_assertions = n
- end
-
- def _assertions
- @_assertions ||= 0
- end
-
- def assert test, msg = nil
- msg ||= "Failed assertion, no message given."
- self._assertions += 1
- unless test then
- msg = msg.call if Proc === msg
- raise MiniTest::Assertion, msg
- end
- true
- end
-
- def assert_block msg = nil
- msg = message(msg) { "Expected block to return true value" }
- assert yield, msg
- end
-
- def assert_empty obj, msg = nil
- msg = message(msg) { "Expected #{obj.inspect} to be empty" }
- assert_respond_to obj, :empty?
- assert obj.empty?, msg
- end
-
- def assert_equal exp, act, msg = nil
- msg = message(msg) { "Expected #{mu_pp(exp)}, not #{mu_pp(act)}" }
- assert(exp == act, msg)
- end
-
- 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
-
- def assert_in_epsilon a, b, epsilon = 0.001, msg = nil
- assert_in_delta a, b, [a, b].min * epsilon, msg
- end
-
- 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
-
- def assert_instance_of cls, obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}" }
- flip = (Module === obj) && ! (Module === cls) # HACK for specs
- obj, cls = cls, obj if flip
- assert obj.instance_of?(cls), msg
- end
-
- 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}" }
- flip = (Module === obj) && ! (Module === cls) # HACK for specs
- obj, cls = cls, obj if flip
- assert obj.kind_of?(cls), msg
- end
-
- def assert_match exp, act, msg = nil
- msg = message(msg) { "Expected #{mu_pp(exp)} to match #{mu_pp(act)}" }
- assert_respond_to act, :"=~"
- exp = /#{Regexp.escape(exp)}/ if String === exp && String === act
- assert exp =~ act, msg
- end
-
- def assert_nil obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" }
- assert obj.nil?, msg
- end
-
- def assert_operator o1, op, o2, msg = nil
- msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" }
- assert o1.__send__(op, o2), msg
- end
-
- def assert_raises *exp
- msg = String === exp.last ? exp.pop : nil
- should_raise = false
- begin
- yield
- should_raise = true
- rescue Exception => e
- assert(exp.any? { |ex|
- ex.instance_of?(Module) ? e.kind_of?(ex) : ex == e.class
- }, exception_details(e, "#{mu_pp(exp)} exception expected, not"))
-
- return e
- end
-
- exp = exp.first if exp.size == 1
- flunk "#{mu_pp(exp)} expected but nothing was raised." if should_raise
- end
-
- def assert_respond_to obj, meth, msg = nil
- msg = message(msg) {
- "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}"
- }
- flip = (Symbol === obj) && ! (Symbol === meth) # HACK for specs
- obj, meth = meth, obj if flip
- assert obj.respond_to?(meth), msg
- end
-
- 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 (0x%x) to be the same as %s (0x%x)" % data
- }
- assert exp.equal?(act), msg
- end
-
- def assert_send send_ary, m = nil
- recv, msg, *args = send_ary
- m = message(m) {
- "Expected #{mu_pp(recv)}.#{msg}(*#{mu_pp(args)}) to return true" }
- assert recv.__send__(msg, *args), m
- end
-
- def assert_throws sym, msg = nil
- default = "Expected #{mu_pp(sym)} to have been thrown"
- caught = true
- catch(sym) do
- begin
- yield
- 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 capture_io
- require 'stringio'
-
- orig_stdout, orig_stderr = $stdout, $stderr
- captured_stdout, captured_stderr = StringIO.new, StringIO.new
- $stdout, $stderr = captured_stdout, captured_stderr
-
- yield
-
- return captured_stdout.string, captured_stderr.string
- ensure
- $stdout = orig_stdout
- $stderr = orig_stderr
- end
-
- def exception_details e, msg
- "#{msg}\nClass: <#{e.class}>\nMessage: <#{e.message.inspect}>\n---Backtrace---\n#{MiniTest::filter_backtrace(e.backtrace).join("\n")}\n---------------"
- end
-
- def flunk msg = nil
- msg ||= "Epic Fail!"
- assert false, msg
- end
-
- def message msg = nil, &default
- proc {
- if msg then
- msg = msg.to_s unless String === msg
- msg += '.' unless msg.empty?
- msg += "\n#{default.call}."
- msg.strip
- else
- "#{default.call}."
- end
- }
- end
-
- # used for counting assertions
- def pass msg = nil
- assert true
- end
-
- def refute test, msg = nil
- msg ||= "Failed refutation, no message given"
- not assert(! test, msg)
- end
-
- def refute_empty obj, msg = nil
- msg = message(msg) { "Expected #{obj.inspect} to not be empty" }
- assert_respond_to obj, :empty?
- refute obj.empty?, msg
- end
-
- 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
-
- 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
-
- def refute_in_epsilon a, b, epsilon = 0.001, msg = nil
- refute_in_delta a, b, a * epsilon, msg
- end
-
- 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
-
- def refute_instance_of cls, obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to not be an instance of #{cls}" }
- flip = (Module === obj) && ! (Module === cls) # HACK for specs
- obj, cls = cls, obj if flip
- refute obj.instance_of?(cls), msg
- end
-
- 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}" }
- flip = (Module === obj) && ! (Module === cls) # HACK for specs
- obj, cls = cls, obj if flip
- refute obj.kind_of?(cls), msg
- end
-
- def refute_match exp, act, msg = nil
- msg = message(msg) { "Expected #{mu_pp(exp)} to not match #{mu_pp(act)}" }
- assert_respond_to act, :"=~"
- exp = /#{Regexp.escape(exp)}/ if String === exp && String === act
- refute exp =~ act, msg
- end
-
- def refute_nil obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to not be nil" }
- refute obj.nil?, msg
- end
-
- def refute_operator o1, op, o2, msg = nil
- msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op} #{mu_pp(o2)}" }
- refute o1.__send__(op, o2), msg
- end
-
- def refute_respond_to obj, meth, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" }
- flip = (Symbol === obj) && ! (Symbol === meth) # HACK for specs
- obj, meth = meth, obj if flip
- refute obj.respond_to?(meth), msg
- end
-
- def refute_same exp, act, msg = nil
- msg = message(msg) { "Expected #{mu_pp(act)} to not be the same as #{mu_pp(exp)}" }
- refute exp.equal?(act), msg
- end
-
- def skip msg = nil, bt = caller
- msg ||= "Skipped, no message given"
- raise MiniTest::Skip, msg, bt
- end
- end
-
- class Unit
- VERSION = "1.3.1"
-
- attr_accessor :report, :failures, :errors, :skips
- attr_accessor :test_count, :assertion_count
-
- @@installed_at_exit ||= false
- @@out = $stdout
-
- def self.autorun
- at_exit {
- next if $! # don't run if there was an exception
- exit_code = MiniTest::Unit.new.run(ARGV)
- exit false if exit_code && exit_code != 0
- } unless @@installed_at_exit
- @@installed_at_exit = true
- end
-
- def self.output= stream
- @@out = stream
- end
-
- def location e
- last_before_assertion = ""
- e.backtrace.reverse_each do |s|
- break if s =~ /in .(assert|refute|flunk|pass|fail|raise)/
- last_before_assertion = s
- end
- last_before_assertion.sub(/:in .*$/, '')
- end
-
- def puke klass, meth, e
- e = case e
- when MiniTest::Skip then
- @skips += 1
- "Skipped:\n#{meth}(#{klass}) [#{location e}]:\n#{e.message}\n"
- when MiniTest::Assertion then
- @failures += 1
- "Failure:\n#{meth}(#{klass}) [#{location e}]:\n#{e.message}\n"
- else
- @errors += 1
- bt = MiniTest::filter_backtrace(e.backtrace).join("\n ")
- "Error:\n#{meth}(#{klass}):\n#{e.class}: #{e.message}\n #{bt}\n"
- end
- @report << e
- e[0, 1]
- end
-
- def initialize
- @report = []
- @errors = @failures = @skips = 0
- @verbose = false
- end
-
- ##
- # Top level driver, controls all output and filtering.
-
- def run args = []
- @verbose = args.delete('-v')
-
- filter = if args.first =~ /^(-n|--name)$/ then
- args.shift
- arg = args.shift
- arg =~ /\/(.*)\// ? Regexp.new($1) : arg
- else
- /./ # anything - ^test_ already filtered by #tests
- end
-
- @@out.puts "Loaded suite #{$0.sub(/\.rb$/, '')}\nStarted"
-
- start = Time.now
- run_test_suites filter
-
- @@out.puts
- @@out.puts "Finished in #{'%.6f' % (Time.now - start)} seconds."
-
- @report.each_with_index do |msg, i|
- @@out.puts "\n%3d) %s" % [i + 1, msg]
- end
-
- @@out.puts
-
- format = "%d tests, %d assertions, %d failures, %d errors, %d skips"
- @@out.puts format % [test_count, assertion_count, failures, errors, skips]
-
- return failures + errors if @test_count > 0 # or return nil...
- end
-
- def run_test_suites filter = /./
- @test_count, @assertion_count = 0, 0
- old_sync, @@out.sync = @@out.sync, true if @@out.respond_to? :sync=
- TestCase.test_suites.each do |suite|
- suite.test_methods.grep(filter).each do |test|
- inst = suite.new test
- inst._assertions = 0
- @@out.print "#{suite}##{test}: " if @verbose
-
- t = Time.now if @verbose
- result = inst.run(self)
-
- @@out.print "%.2f s: " % (Time.now - t) if @verbose
- @@out.print result
- @@out.puts if @verbose
- @test_count += 1
- @assertion_count += inst._assertions
- end
- end
- @@out.sync = old_sync if @@out.respond_to? :sync=
- [@test_count, @assertion_count]
- end
-
- class TestCase
- attr_reader :name
-
- def run runner
- result = '.'
- begin
- @passed = nil
- self.setup
- self.__send__ self.name
- @passed = true
- rescue Exception => e
- @passed = false
- result = runner.puke(self.class, self.name, e)
- ensure
- begin
- self.teardown
- rescue Exception => e
- result = runner.puke(self.class, self.name, e)
- end
- end
- result
- end
-
- def initialize name
- @name = name
- @passed = nil
- end
-
- def self.reset
- @@test_suites = {}
- end
-
- reset
-
- def self.inherited klass
- @@test_suites[klass] = true
- end
-
- def self.test_order
- :random
- end
-
- def self.test_suites
- @@test_suites.keys.sort_by { |ts| ts.name }
- end
-
- def self.test_methods
- methods = public_instance_methods(true).grep(/^test/).map { |m|
- m.to_s
- }.sort
-
- if self.test_order == :random then
- max = methods.size
- methods = methods.sort_by { rand(max) }
- end
-
- methods
- end
-
- def setup; end
- def teardown; end
-
- def passed?
- @passed
- end
-
- include MiniTest::Assertions
- end # class TestCase
- end # class Test
-end # module Mini
diff --git a/lib/mkmf.rb b/lib/mkmf.rb
index e9d509ec39..37ee4a70d9 100644
--- a/lib/mkmf.rb
+++ b/lib/mkmf.rb
@@ -1,3 +1,5 @@
+# -*- coding: us-ascii -*-
+# frozen-string-literal: false
# module to create Makefile for extension modules
# invoke like: ruby -r mkmf extconf.rb
@@ -5,1908 +7,3069 @@ require 'rbconfig'
require 'fileutils'
require 'shellwords'
-CONFIG = RbConfig::MAKEFILE_CONFIG
-ORIG_LIBPATH = ENV['LIB']
+class String # :nodoc:
+ # Wraps a string in escaped quotes if it contains whitespace.
+ def quote
+ /\s/ =~ self ? "\"#{self}\"" : "#{self}"
+ end
-CXX_EXT = %w[cc cxx cpp]
-if /mswin|bccwin|mingw|os2/ !~ CONFIG['build_os']
- CXX_EXT.concat(%w[C])
-end
-SRC_EXT = %w[c m] << CXX_EXT
-$static = nil
-$config_h = '$(arch_hdrdir)/ruby/config.h'
-$default_static = $static
-
-unless defined? $configure_args
- $configure_args = {}
- args = CONFIG["configure_args"]
- if ENV["CONFIGURE_ARGS"]
- args << " " << ENV["CONFIGURE_ARGS"]
- end
- for arg in Shellwords::shellwords(args)
- arg, val = arg.split('=', 2)
- next unless arg
- arg.tr!('_', '-')
- if arg.sub!(/^(?!--)/, '--')
- val or next
- arg.downcase!
- end
- next if /^--(?:top|topsrc|src|cur)dir$/ =~ arg
- $configure_args[arg] = val || true
- end
- for arg in ARGV
- arg, val = arg.split('=', 2)
- next unless arg
- arg.tr!('_', '-')
- if arg.sub!(/^(?!--)/, '--')
- val or next
- arg.downcase!
- end
- $configure_args[arg] = val || true
+ # Escape whitespaces for Makefile.
+ def unspace
+ gsub(/\s/, '\\\\\\&')
end
-end
-$libdir = CONFIG["libdir"]
-$rubylibdir = CONFIG["rubylibdir"]
-$archdir = CONFIG["archdir"]
-$sitedir = CONFIG["sitedir"]
-$sitelibdir = CONFIG["sitelibdir"]
-$sitearchdir = CONFIG["sitearchdir"]
-$vendordir = CONFIG["vendordir"]
-$vendorlibdir = CONFIG["vendorlibdir"]
-$vendorarchdir = CONFIG["vendorarchdir"]
-
-$mswin = /mswin/ =~ RUBY_PLATFORM
-$bccwin = /bccwin/ =~ RUBY_PLATFORM
-$mingw = /mingw/ =~ RUBY_PLATFORM
-$cygwin = /cygwin/ =~ RUBY_PLATFORM
-$netbsd = /netbsd/ =~ RUBY_PLATFORM
-$os2 = /os2/ =~ RUBY_PLATFORM
-$beos = /beos/ =~ RUBY_PLATFORM
-$haiku = /haiku/ =~ RUBY_PLATFORM
-$solaris = /solaris/ =~ RUBY_PLATFORM
-$dest_prefix_pattern = (File::PATH_SEPARATOR == ';' ? /\A([[:alpha:]]:)?/ : /\A/)
-
-# :stopdoc:
-
-def config_string(key, config = CONFIG)
- s = config[key] and !s.empty? and block_given? ? yield(s) : s
-end
+ # Generates a string used as cpp macro name.
+ def tr_cpp
+ strip.upcase.tr_s("^A-Z0-9_*", "_").tr_s("*", "P")
+ end
+
+ def funcall_style
+ /\)\z/ =~ self ? dup : "#{self}()"
+ end
-def dir_re(dir)
- Regexp.new('\$(?:\('+dir+'\)|\{'+dir+'\})(?:\$(?:\(target_prefix\)|\{target_prefix\}))?')
+ def sans_arguments
+ self[/\A[^()]+/]
+ end
end
-def relative_from(path, base)
- dir = File.join(path, "")
- if File.expand_path(dir) == File.expand_path(dir, base)
- path
- else
- File.join(base, path)
+class Array # :nodoc:
+ # Wraps all strings in escaped quotes if they contain whitespace.
+ def quote
+ map {|s| s.quote}
end
end
-INSTALL_DIRS = [
- [dir_re('commondir'), "$(RUBYCOMMONDIR)"],
- [dir_re('sitedir'), "$(RUBYCOMMONDIR)"],
- [dir_re('vendordir'), "$(RUBYCOMMONDIR)"],
- [dir_re('rubylibdir'), "$(RUBYLIBDIR)"],
- [dir_re('archdir'), "$(RUBYARCHDIR)"],
- [dir_re('sitelibdir'), "$(RUBYLIBDIR)"],
- [dir_re('vendorlibdir'), "$(RUBYLIBDIR)"],
- [dir_re('sitearchdir'), "$(RUBYARCHDIR)"],
- [dir_re('vendorarchdir'), "$(RUBYARCHDIR)"],
- [dir_re('rubyhdrdir'), "$(RUBYHDRDIR)"],
- [dir_re('sitehdrdir'), "$(SITEHDRDIR)"],
- [dir_re('vendorhdrdir'), "$(VENDORHDRDIR)"],
- [dir_re('bindir'), "$(BINDIR)"],
-]
-
-def install_dirs(target_prefix = nil)
- if $extout
- dirs = [
- ['BINDIR', '$(extout)/bin'],
- ['RUBYCOMMONDIR', '$(extout)/common'],
- ['RUBYLIBDIR', '$(RUBYCOMMONDIR)$(target_prefix)'],
- ['RUBYARCHDIR', '$(extout)/$(arch)$(target_prefix)'],
- ['HDRDIR', '$(extout)/include/ruby$(target_prefix)'],
- ['ARCHHDRDIR', '$(extout)/include/$(arch)/ruby$(target_prefix)'],
- ['extout', "#$extout"],
- ['extout_prefix', "#$extout_prefix"],
- ]
- elsif $extmk
- dirs = [
- ['BINDIR', '$(bindir)'],
- ['RUBYCOMMONDIR', '$(rubylibdir)'],
- ['RUBYLIBDIR', '$(rubylibdir)$(target_prefix)'],
- ['RUBYARCHDIR', '$(archdir)$(target_prefix)'],
- ['HDRDIR', '$(rubyhdrdir)/ruby$(target_prefix)'],
- ['ARCHHDRDIR', '$(rubyhdrdir)/$(arch)/ruby$(target_prefix)'],
- ]
- elsif $configure_args.has_key?('--vendor')
- dirs = [
- ['BINDIR', '$(bindir)'],
- ['RUBYCOMMONDIR', '$(vendordir)$(target_prefix)'],
- ['RUBYLIBDIR', '$(vendorlibdir)$(target_prefix)'],
- ['RUBYARCHDIR', '$(vendorarchdir)$(target_prefix)'],
- ['HDRDIR', '$(rubyhdrdir)/ruby$(target_prefix)'],
- ['ARCHHDRDIR', '$(rubyhdrdir)/$(arch)/ruby$(target_prefix)'],
- ]
+##
+# \Module \MakeMakefile is used by Ruby C extensions to generate a Makefile which will
+# correctly compile and link the C extension to Ruby and a third-party
+# library.
+module MakeMakefile
+
+ target_rbconfig = nil
+ ARGV.delete_if do |arg|
+ opt = arg.delete_prefix("--target-rbconfig=")
+ unless opt == arg
+ target_rbconfig = opt
+ end
+ end
+ if target_rbconfig
+ # Load the RbConfig for the target platform into this module.
+ # Cross-compiling needs the same version of Ruby.
+ Kernel.load target_rbconfig, self
else
- dirs = [
- ['BINDIR', '$(bindir)'],
- ['RUBYCOMMONDIR', '$(sitedir)$(target_prefix)'],
- ['RUBYLIBDIR', '$(sitelibdir)$(target_prefix)'],
- ['RUBYARCHDIR', '$(sitearchdir)$(target_prefix)'],
- ['HDRDIR', '$(rubyhdrdir)/ruby$(target_prefix)'],
- ['ARCHHDRDIR', '$(rubyhdrdir)/$(arch)/ruby$(target_prefix)'],
- ]
+ # The RbConfig for the target platform where the built extension runs.
+ RbConfig = ::RbConfig
end
- dirs << ['target_prefix', (target_prefix ? "/#{target_prefix}" : "")]
- dirs
-end
-def map_dir(dir, map = nil)
- map ||= INSTALL_DIRS
- map.inject(dir) {|d, (orig, new)| d.gsub(orig, new)}
-end
+ #### defer until this module become global-state free.
+ # def self.extended(obj)
+ # obj.init_mkmf
+ # super
+ # end
+ #
+ # def initialize(*args, rbconfig: RbConfig, **rest)
+ # init_mkmf(rbconfig::MAKEFILE_CONFIG, rbconfig::CONFIG)
+ # super(*args, **rest)
+ # end
-topdir = File.dirname(libdir = File.dirname(__FILE__))
-extdir = File.expand_path("ext", topdir)
-path = File.expand_path($0)
-$extmk = path[0, topdir.size+1] == topdir+"/" && %r"\A(ext|enc|tool)\z" =~ File.dirname(path[topdir.size+1..-1])
-if not $extmk and File.exist?(($hdrdir = RbConfig::CONFIG["rubyhdrdir"]) + "/ruby/ruby.h")
- $topdir = $hdrdir
- $top_srcdir = $hdrdir
- $arch_hdrdir = $hdrdir + "/$(arch)"
-elsif File.exist?(($hdrdir = ($top_srcdir ||= topdir) + "/include") + "/ruby.h")
- $topdir ||= RbConfig::CONFIG["topdir"]
- $arch_hdrdir = "$(extout)/include/$(arch)"
-else
- abort "mkmf.rb can't find header files for ruby at #{$hdrdir}/ruby.h"
-end
+ ##
+ # The makefile configuration using the defaults from when Ruby was built.
-OUTFLAG = CONFIG['OUTFLAG']
-COUTFLAG = CONFIG['COUTFLAG']
-CPPOUTFILE = CONFIG['CPPOUTFILE']
+ CONFIG = RbConfig::MAKEFILE_CONFIG
-CONFTEST_C = "conftest.c".freeze
+ ##
+ # The saved original value of +LIB+ environment variable
+ ORIG_LIBPATH = ENV['LIB']
-class String
- # Wraps a string in escaped quotes if it contains whitespace.
- def quote
- /\s/ =~ self ? "\"#{self}\"" : "#{self}"
+ ##
+ # Extensions for files compiled with a C compiler
+
+ C_EXT = %w[c m]
+
+ ##
+ # Extensions for files compiled with a C++ compiler
+
+ CXX_EXT = %w[cc mm cxx cpp]
+ unless File.exist?(File.join(*File.split(__FILE__).tap {|d, b| b.swapcase}))
+ CXX_EXT.concat(%w[C])
end
- # Generates a string used as cpp macro name.
- def tr_cpp
- strip.upcase.tr_s("^A-Z0-9_", "_")
+ ##
+ # Extensions for source files
+
+ SRC_EXT = C_EXT + CXX_EXT
+
+ ##
+ # Extensions for header files
+
+ HDR_EXT = %w[h hpp]
+ $static = nil
+ $config_h = '$(arch_hdrdir)/ruby/config.h'
+ $default_static = $static
+
+ unless defined? $configure_args
+ $configure_args = {}
+ args = CONFIG["configure_args"].shellsplit
+ if arg = ENV["CONFIGURE_ARGS"]
+ args.push(*arg.shellsplit)
+ end
+ args.delete_if {|a| /\A--(?:top(?:src)?|src|cur)dir(?=\z|=)/ =~ a}
+ for arg in args.concat(ARGV)
+ arg, val = arg.split('=', 2)
+ next unless arg
+ arg.tr!('_', '-')
+ if arg.sub!(/\A(?!--)/, '--')
+ val or next
+ arg.downcase!
+ end
+ $configure_args[arg] = val || true
+ end
end
-end
-class Array
- # Wraps all strings in escaped quotes if they contain whitespace.
- def quote
- map {|s| s.quote}
+
+ $libdir = CONFIG["libdir"]
+ $rubylibdir = CONFIG["rubylibdir"]
+ $archdir = CONFIG["archdir"]
+ $sitedir = CONFIG["sitedir"]
+ $sitelibdir = CONFIG["sitelibdir"]
+ $sitearchdir = CONFIG["sitearchdir"]
+ $vendordir = CONFIG["vendordir"]
+ $vendorlibdir = CONFIG["vendorlibdir"]
+ $vendorarchdir = CONFIG["vendorarchdir"]
+
+ $mswin = /mswin/ =~ RUBY_PLATFORM
+ $mingw = /mingw/ =~ RUBY_PLATFORM
+ $cygwin = /cygwin/ =~ RUBY_PLATFORM
+ $netbsd = /netbsd/ =~ RUBY_PLATFORM
+ $haiku = /haiku/ =~ RUBY_PLATFORM
+ $solaris = /solaris/ =~ RUBY_PLATFORM
+ $universal = /universal/ =~ RUBY_PLATFORM
+ $dest_prefix_pattern = (File::PATH_SEPARATOR == ';' ? /\A([[:alpha:]]:)?/ : /\A/)
+
+ # :stopdoc:
+
+ def config_string(key, config = CONFIG)
+ s = config[key] and !s.empty? and block_given? ? yield(s) : s
end
-end
+ module_function :config_string
-def rm_f(*files)
- FileUtils.rm_f(Dir[*files])
-end
+ def dir_re(dir)
+ Regexp.new('\$(?:\('+dir+'\)|\{'+dir+'\})(?:\$(?:\(target_prefix\)|\{target_prefix\}))?')
+ end
+ module_function :dir_re
-def rm_rf(*files)
- FileUtils.rm_rf(Dir[*files])
-end
+ def relative_from(path, base)
+ dir = File.join(path, "")
+ if File.expand_path(dir) == File.expand_path(dir, base)
+ path
+ else
+ File.join(base, path)
+ end
+ end
-# Returns time stamp of the +target+ file if it exists and is newer
-# than or equal to all of +times+.
-def modified?(target, times)
- (t = File.mtime(target)) rescue return nil
- Array === times or times = [times]
- t if times.all? {|n| n <= t}
-end
+ INSTALL_DIRS = [
+ [dir_re('commondir'), "$(RUBYCOMMONDIR)"],
+ [dir_re('sitedir'), "$(RUBYCOMMONDIR)"],
+ [dir_re('vendordir'), "$(RUBYCOMMONDIR)"],
+ [dir_re('rubylibdir'), "$(RUBYLIBDIR)"],
+ [dir_re('archdir'), "$(RUBYARCHDIR)"],
+ [dir_re('sitelibdir'), "$(RUBYLIBDIR)"],
+ [dir_re('vendorlibdir'), "$(RUBYLIBDIR)"],
+ [dir_re('sitearchdir'), "$(RUBYARCHDIR)"],
+ [dir_re('vendorarchdir'), "$(RUBYARCHDIR)"],
+ [dir_re('rubyhdrdir'), "$(RUBYHDRDIR)"],
+ [dir_re('sitehdrdir'), "$(SITEHDRDIR)"],
+ [dir_re('vendorhdrdir'), "$(VENDORHDRDIR)"],
+ [dir_re('bindir'), "$(BINDIR)"],
+ ]
+
+ def install_dirs(target_prefix = nil)
+ if $extout and $extmk
+ dirs = [
+ ['BINDIR', '$(extout)/bin'],
+ ['RUBYCOMMONDIR', '$(extout)/common'],
+ ['RUBYLIBDIR', '$(RUBYCOMMONDIR)$(target_prefix)'],
+ ['RUBYARCHDIR', '$(extout)/$(arch)$(target_prefix)'],
+ ['HDRDIR', '$(extout)/include/ruby$(target_prefix)'],
+ ['ARCHHDRDIR', '$(extout)/include/$(arch)/ruby$(target_prefix)'],
+ ['extout', "#$extout"],
+ ['extout_prefix', "#$extout_prefix"],
+ ]
+ elsif $extmk
+ dirs = [
+ ['BINDIR', '$(bindir)'],
+ ['RUBYCOMMONDIR', '$(rubylibdir)'],
+ ['RUBYLIBDIR', '$(rubylibdir)$(target_prefix)'],
+ ['RUBYARCHDIR', '$(archdir)$(target_prefix)'],
+ ['HDRDIR', '$(rubyhdrdir)/ruby$(target_prefix)'],
+ ['ARCHHDRDIR', '$(rubyhdrdir)/$(arch)/ruby$(target_prefix)'],
+ ]
+ elsif $configure_args.has_key?('--vendor')
+ dirs = [
+ ['BINDIR', '$(bindir)'],
+ ['RUBYCOMMONDIR', '$(vendordir)$(target_prefix)'],
+ ['RUBYLIBDIR', '$(vendorlibdir)$(target_prefix)'],
+ ['RUBYARCHDIR', '$(vendorarchdir)$(target_prefix)'],
+ ['HDRDIR', '$(vendorhdrdir)$(target_prefix)'],
+ ['ARCHHDRDIR', '$(vendorarchhdrdir)$(target_prefix)'],
+ ]
+ else
+ dirs = [
+ ['BINDIR', '$(bindir)'],
+ ['RUBYCOMMONDIR', '$(sitedir)$(target_prefix)'],
+ ['RUBYLIBDIR', '$(sitelibdir)$(target_prefix)'],
+ ['RUBYARCHDIR', '$(sitearchdir)$(target_prefix)'],
+ ['HDRDIR', '$(sitehdrdir)$(target_prefix)'],
+ ['ARCHHDRDIR', '$(sitearchhdrdir)$(target_prefix)'],
+ ]
+ end
+ dirs << ['target_prefix', (target_prefix ? "/#{target_prefix}" : "")]
+ dirs
+ end
-def merge_libs(*libs)
- libs.inject([]) do |x, y|
- xy = x & y
- xn = yn = 0
- y = y.inject([]) {|ary, e| ary.last == e ? ary : ary << e}
- y.each_with_index do |v, yi|
- if xy.include?(v)
- xi = [x.index(v), xn].max()
- x[xi, 1] = y[yn..yi]
- xn, yn = xi + (yi - yn + 1), yi + 1
- end
+ def map_dir(dir, map = nil)
+ map ||= INSTALL_DIRS
+ map.inject(dir) {|d, (orig, new)| d.gsub(orig, new)}
+ end
+
+ topdir = File.dirname(File.dirname(__FILE__))
+ path = File.expand_path($0)
+ until (dir = File.dirname(path)) == path
+ if File.identical?(dir, topdir)
+ $extmk = true if %r"\A(?:ext|enc|tool|test)\z" =~ File.basename(path)
+ break
end
- x.concat(y[yn..-1] || [])
+ path = dir
+ end
+ $extmk ||= false
+ if not $extmk and File.exist?(($hdrdir = RbConfig::CONFIG["rubyhdrdir"]) + "/ruby/ruby.h")
+ $topdir = $hdrdir
+ $top_srcdir = $hdrdir
+ $arch_hdrdir = RbConfig::CONFIG["rubyarchhdrdir"]
+ elsif File.exist?(($hdrdir = ($top_srcdir ||= topdir) + "/include") + "/ruby.h")
+ $topdir ||= RbConfig::CONFIG["topdir"]
+ $arch_hdrdir = "$(extout)/include/$(arch)"
+ else
+ abort <<MESSAGE
+mkmf.rb can't find header files for ruby at #{$hdrdir}/ruby.h
+
+You might have to install separate package for the ruby development
+environment, ruby-dev or ruby-devel for example.
+MESSAGE
end
-end
-# This is a custom logging module. It generates an mkmf.log file when you
-# run your extconf.rb script. This can be useful for debugging unexpected
-# failures.
-#
-# This module and its associated methods are meant for internal use only.
-#
-module Logging
- @log = nil
- @logfile = 'mkmf.log'
- @orgerr = $stderr.dup
- @orgout = $stdout.dup
- @postpone = 0
- @quiet = $extmk
-
- def self::log_open
- @log ||= File::open(@logfile, 'wb')
- @log.sync = true
- end
-
- def self::open
- log_open
- $stderr.reopen(@log)
- $stdout.reopen(@log)
- yield
- ensure
- $stderr.reopen(@orgerr)
- $stdout.reopen(@orgout)
+ CONFTEST = "conftest".freeze
+ CONFTEST_C = "#{CONFTEST}.c"
+
+ OUTFLAG = CONFIG['OUTFLAG']
+ COUTFLAG = CONFIG['COUTFLAG']
+ CSRCFLAG = CONFIG['CSRCFLAG']
+ CPPOUTFILE = config_string('CPPOUTFILE') {|str| str.sub(/\bconftest\b/, CONFTEST)}
+
+ # :startdoc:
+
+ # Removes _files_.
+ def rm_f(*files)
+ opt = (Hash === files.last ? [files.pop] : [])
+ FileUtils.rm_f(Dir[*files.flatten], *opt)
end
+ module_function :rm_f
+
+ # Removes _files_ recursively.
+ def rm_rf(*files)
+ opt = (Hash === files.last ? [files.pop] : [])
+ FileUtils.rm_rf(Dir[*files.flatten], *opt)
+ end
+ module_function :rm_rf
+
+ # Returns time stamp of the +target+ file if it exists and is newer than or
+ # equal to all of +times+.
+ def modified?(target, times)
+ (t = File.mtime(target)) rescue return nil
+ Array === times or times = [times]
+ t if times.all? {|n| n <= t}
+ end
+
+ # :stopdoc:
- def self::message(*s)
- log_open
- @log.printf(*s)
+ def split_libs(*strs)
+ sep = $mswin ? /\s+/ : /\s+(?=-|\z)/
+ strs.flat_map {|s| s.lstrip.split(sep)}
end
- def self::logfile file
- @logfile = file
- if @log and not @log.closed?
- @log.flush
- @log.close
- @log = nil
+ def merge_libs(*libs)
+ libs.inject([]) do |x, y|
+ y = y.inject([]) {|ary, e| ary.last == e ? ary : ary << e}
+ y.each_with_index do |v, yi|
+ if xi = x.rindex(v)
+ x[(xi+1)..-1] = merge_libs(y[(yi+1)..-1], x[(xi+1)..-1])
+ x[xi, 0] = y[0...yi]
+ break
+ end
+ end and x.concat(y)
+ x
end
end
-
- def self::postpone
- tmplog = "mkmftmp#{@postpone += 1}.log"
- open do
- log, *save = @log, @logfile, @orgout, @orgerr
- @log, @logfile, @orgout, @orgerr = nil, tmplog, log, log
- begin
- log.print(open {yield})
+
+ # This is a custom logging module. It generates an mkmf.log file when you
+ # run your extconf.rb script. This can be useful for debugging unexpected
+ # failures.
+ #
+ # This module and its associated methods are meant for internal use only.
+ #
+ module Logging
+ @log = nil
+ @logfile = 'mkmf.log'
+ @orgerr = $stderr.dup
+ @orgout = $stdout.dup
+ @postpone = 0
+ @quiet = $extmk
+
+ def self::log_open
+ @log ||= File::open(@logfile, 'wb')
+ @log.sync = true
+ end
+
+ def self::log_opened?
+ @log and not @log.closed?
+ end
+
+ def self::open
+ log_open
+ $stderr.reopen(@log)
+ $stdout.reopen(@log)
+ yield
+ ensure
+ $stderr.reopen(@orgerr)
+ $stdout.reopen(@orgout)
+ end
+
+ def self::message(*s)
+ log_open
+ @log.printf(*s)
+ end
+
+ def self::logfile file
+ @logfile = file
+ log_close
+ end
+
+ def self::log_close
+ if @log and not @log.closed?
+ @log.flush
@log.close
- File::open(tmplog) {|t| FileUtils.copy_stream(t, log)}
- ensure
- @log, @logfile, @orgout, @orgerr = log, *save
- @postpone -= 1
- rm_f tmplog
+ @log = nil
end
end
+
+ def self::postpone
+ tmplog = "mkmftmp#{@postpone += 1}.log"
+ open do
+ log, *save = @log, @logfile, @orgout, @orgerr
+ @log, @logfile, @orgout, @orgerr = nil, tmplog, log, log
+ begin
+ log.print(open {yield @log})
+ ensure
+ @log.close if @log and not @log.closed?
+ File::open(tmplog) {|t| FileUtils.copy_stream(t, log)} if File.exist?(tmplog)
+ @log, @logfile, @orgout, @orgerr = log, *save
+ @postpone -= 1
+ MakeMakefile.rm_f tmplog
+ end
+ end
+ end
+
+ class << self
+ attr_accessor :quiet
+ end
end
- class << self
- attr_accessor :quiet
+ def libpath_env
+ # used only if native compiling
+ if libpathenv = config_string("LIBPATHENV")
+ pathenv = ENV[libpathenv]
+ libpath = RbConfig.expand($DEFLIBPATH.join(File::PATH_SEPARATOR))
+ {libpathenv => [libpath, pathenv].compact.join(File::PATH_SEPARATOR)}
+ else
+ {}
+ end
+ end
+
+ def expand_command(commands, envs = libpath_env)
+ varpat = /\$\((\w+)\)|\$\{(\w+)\}/
+ vars = nil
+ expand = proc do |command|
+ case command
+ when Array
+ command.map(&expand)
+ when String
+ if varpat =~ command
+ vars ||= Hash.new {|h, k| h[k] = ENV[k]}
+ command = command.dup
+ nil while command.gsub!(varpat) {vars[$1||$2]}
+ end
+ command
+ else
+ command
+ end
+ end
+ if Array === commands
+ env, *commands = commands if Hash === commands.first
+ envs.merge!(env) if env
+ end
+
+ # disable ASAN leak reporting - conftest programs almost always don't bother
+ # to free their memory.
+ envs['LSAN_OPTIONS'] = "detect_leaks=0" unless ENV.key?('LSAN_OPTIONS')
+
+ return envs, expand[commands]
end
-end
-def xsystem command
- varpat = /\$\((\w+)\)|\$\{(\w+)\}/
- if varpat =~ command
- vars = Hash.new {|h, k| h[k] = ''; ENV[k]}
- command = command.dup
- nil while command.gsub!(varpat) {vars[$1||$2]}
+ def env_quote(envs)
+ envs.map {|e, v| "#{e}=#{v.quote}"}
end
- Logging::open do
- puts command.quote
- system(command)
+
+ # :startdoc:
+
+ # call-seq:
+ # xsystem(command, werror: false) -> true or false
+ #
+ # Executes _command_ with expanding variables, and returns the exit
+ # status like as Kernel#system. If _werror_ is true and the error
+ # output is not empty, returns +false+. The output will logged.
+ def xsystem(command, werror: false)
+ env, command = expand_command(command)
+ Logging::open do
+ puts [env_quote(env), command.quote].join(' ')
+ if werror
+ result = nil
+ Logging.postpone do |log|
+ output = IO.popen(env, command, &:read)
+ result = ($?.success? and File.zero?(log.path))
+ output
+ end
+ result
+ else
+ system(env, *command)
+ end
+ end
end
-end
-def xpopen command, *mode, &block
- Logging::open do
- case mode[0]
- when nil, /^r/
- puts "#{command} |"
- else
- puts "| #{command}"
+ # Executes _command_ similarly to xsystem, but yields opened pipe.
+ def xpopen command, *mode, &block
+ env, commands = expand_command(command)
+ command = [env_quote(env), command].join(' ')
+ Logging::open do
+ case mode[0]
+ when nil, Hash, /^r/
+ puts "#{command} |"
+ else
+ puts "| #{command}"
+ end
+ IO.popen(env, commands, *mode, &block)
end
- IO.popen(command, *mode, &block)
end
-end
-def log_src(src)
- src = src.split(/^/)
- fmt = "%#{src.size.to_s.size}d: %s"
- Logging::message <<"EOM"
-checked program was:
+ # Logs _src_
+ def log_src(src, heading="checked program was")
+ src = src.split(/^/)
+ fmt = "%#{src.size.to_s.size}d: %s"
+ Logging::message <<"EOM"
+#{heading}:
/* begin */
EOM
- src.each_with_index {|line, no| Logging::message fmt, no+1, line}
- Logging::message <<"EOM"
+ src.each_with_index {|line, no| Logging::message fmt, no+1, line}
+ Logging::message <<"EOM"
/* end */
EOM
-end
+ end
-def create_tmpsrc(src)
- src = "#{COMMON_HEADERS}\n#{src}"
- src = yield(src) if block_given?
- src.gsub!(/[ \t]+$/, '')
- src.gsub!(/\A\n+|^\n+$/, '')
- src.sub!(/[^\n]\z/, "\\&\n")
- count = 0
- begin
- open(CONFTEST_C, "wb") do |cfile|
- cfile.print src
- end
- rescue Errno::EACCES
- if (count += 1) < 5
- sleep 0.2
- retry
+ # Returns the language-dependent source file name for configuration
+ # checks.
+ def conftest_source
+ CONFTEST_C
+ end
+
+ # Creats temporary source file from +COMMON_HEADERS+ and _src_.
+ # Yields the created source string and uses the returned string as
+ # the source code, if the block is given.
+ def create_tmpsrc(src)
+ src = "#{COMMON_HEADERS}\n#{src}"
+ src = yield(src) if block_given?
+ src.gsub!(/[ \t]+$/, '')
+ src.gsub!(/\A\n+|^\n+$/, '')
+ src.sub!(/[^\n]\z/, "\\&\n")
+ count = 0
+ begin
+ File.open(conftest_source, "wb") do |cfile|
+ cfile.print src
+ end
+ rescue Errno::EACCES
+ if (count += 1) < 5
+ sleep 0.2
+ retry
+ end
end
+ src
end
- src
-end
-def have_devel?
- unless defined? $have_devel
- $have_devel = true
- $have_devel = try_link(MAIN_DOES_NOTHING)
+ # :stopdoc:
+
+ def have_devel?
+ unless defined? $have_devel
+ $have_devel = true
+ $have_devel = try_link(MAIN_DOES_NOTHING)
+ end
+ $have_devel
end
- $have_devel
-end
-def try_do(src, command, &b)
- unless have_devel?
- raise <<MSG
-The complier failed to generate an executable file.
+ def try_do(src, command, **opts, &b)
+ unless have_devel?
+ raise <<MSG
+The compiler failed to generate an executable file.
You have to install development tools first.
MSG
+ end
+ begin
+ src = create_tmpsrc(src, &b)
+ xsystem(command, **opts)
+ ensure
+ log_src(src)
+ end
end
- src = create_tmpsrc(src, &b)
- xsystem(command)
-ensure
- log_src(src)
- rm_rf 'conftest.dSYM'
-end
-def link_command(ldflags, opt="", libpath=$DEFLIBPATH|$LIBPATH)
- conf = RbConfig::CONFIG.merge('hdrdir' => $hdrdir.quote,
- 'src' => CONFTEST_C,
- 'arch_hdrdir' => "#$arch_hdrdir",
- 'top_srcdir' => $top_srcdir.quote,
- 'INCFLAGS' => "#$INCFLAGS",
- 'CPPFLAGS' => "#$CPPFLAGS",
- 'CFLAGS' => "#$CFLAGS",
- 'ARCH_FLAG' => "#$ARCH_FLAG",
- 'LDFLAGS' => "#$LDFLAGS #{ldflags}",
- 'LIBPATH' => libpathflag(libpath),
- 'LOCAL_LIBS' => "#$LOCAL_LIBS #$libs",
- 'LIBS' => "#$LIBRUBYARG_STATIC #{opt} #$LIBS")
- RbConfig::expand(TRY_LINK.dup, conf)
-end
+ def link_config(ldflags, opt="", libpath=$DEFLIBPATH|$LIBPATH)
+ librubyarg = $extmk ? $LIBRUBYARG_STATIC : "$(LIBRUBYARG)"
+ conf = RbConfig::CONFIG.merge('hdrdir' => $hdrdir.quote,
+ 'src' => "#{conftest_source}",
+ 'arch_hdrdir' => $arch_hdrdir.quote,
+ 'top_srcdir' => $top_srcdir.quote,
+ 'INCFLAGS' => "#$INCFLAGS",
+ 'CPPFLAGS' => "#$CPPFLAGS",
+ 'CFLAGS' => "#$CFLAGS",
+ 'ARCH_FLAG' => "#$ARCH_FLAG",
+ 'LDFLAGS' => "#$LDFLAGS #{ldflags}",
+ 'LOCAL_LIBS' => "#$LOCAL_LIBS #$libs",
+ 'LIBS' => "#{librubyarg} #{opt} #$LIBS")
+ conf['LIBPATH'] = libpathflag(libpath.map {|s| RbConfig::expand(s.dup, conf)})
+ conf
+ end
-def cc_command(opt="")
- conf = RbConfig::CONFIG.merge('hdrdir' => $hdrdir.quote, 'srcdir' => $srcdir.quote,
- 'arch_hdrdir' => "#$arch_hdrdir",
- 'top_srcdir' => $top_srcdir.quote)
- RbConfig::expand("$(CC) #$INCFLAGS #$CPPFLAGS #$CFLAGS #$ARCH_FLAG #{opt} -c #{CONFTEST_C}",
- conf)
-end
+ def link_command(ldflags, *opts)
+ conf = link_config(ldflags, *opts)
+ RbConfig::expand(TRY_LINK.dup, conf)
+ end
-def cpp_command(outfile, opt="")
- conf = RbConfig::CONFIG.merge('hdrdir' => $hdrdir.quote, 'srcdir' => $srcdir.quote,
- 'arch_hdrdir' => "#$arch_hdrdir",
- 'top_srcdir' => $top_srcdir.quote)
- RbConfig::expand("$(CPP) #$INCFLAGS #$CPPFLAGS #$CFLAGS #{opt} #{CONFTEST_C} #{outfile}",
- conf)
-end
+ def cc_config(opt="")
+ conf = RbConfig::CONFIG.merge('hdrdir' => $hdrdir.quote, 'srcdir' => $srcdir.quote,
+ 'arch_hdrdir' => $arch_hdrdir.quote,
+ 'top_srcdir' => $top_srcdir.quote)
+ conf
+ end
+
+ def cc_command(opt="")
+ conf = cc_config(opt)
+ RbConfig::expand("$(CC) #$INCFLAGS #$CPPFLAGS #$CFLAGS #$ARCH_FLAG #{opt} -c #{CONFTEST_C}",
+ conf)
+ end
+
+ def cpp_config(opt)
+ conf = cc_config(opt)
+ if $universal and (arch_flag = conf['ARCH_FLAG']) and !arch_flag.empty?
+ conf['ARCH_FLAG'] = arch_flag.gsub(/(?:\G|\s)-arch\s+\S+/, '')
+ end
+ conf
+ end
+
+ def cpp_command(outfile, opt="")
+ conf = cpp_config(opt)
+ RbConfig::expand("$(CPP) #$INCFLAGS #$CPPFLAGS #$CFLAGS #{opt} #{CONFTEST_C} #{outfile}",
+ conf)
+ end
+
+ def libpathflag(libpath=$DEFLIBPATH|$LIBPATH)
+ libpathflags = nil
+ libpath.map{|x|
+ case x
+ when "$(topdir)", /\A\./
+ LIBPATHFLAG
+ else
+ libpathflags ||= [LIBPATHFLAG, RPATHFLAG].grep(/\S/).join(" ")
+ end % x.quote
+ }.join(" ")
+ end
+
+ def werror_flag(opt = nil)
+ config_string("WERRORFLAG") {|flag| opt = opt && !opt.empty? ? "#{opt} #{flag}" : flag}
+ opt
+ end
-def libpathflag(libpath=$DEFLIBPATH|$LIBPATH)
- libpath.map{|x|
- case x
- when "$(topdir)", /\A\./
- LIBPATHFLAG
+ def with_werror(opt, opts = nil)
+ opt = werror_flag(opt) if opts and (opts = opts.dup).delete(:werror)
+ yield(opt, opts)
+ end
+
+ def try_link0(src, opt = "", ldflags: "", **opts, &b) # :nodoc:
+ exe = CONFTEST+$EXEEXT
+ cmd = link_command(ldflags, opt)
+ if $universal
+ require 'tmpdir'
+ Dir.mktmpdir("mkmf_", oldtmpdir = ENV["TMPDIR"]) do |tmpdir|
+ begin
+ ENV["TMPDIR"] = tmpdir
+ try_do(src, cmd, **opts, &b)
+ ensure
+ ENV["TMPDIR"] = oldtmpdir
+ end
+ end
else
- LIBPATHFLAG+RPATHFLAG
- end % x.quote
- }.join
-end
+ try_do(src, cmd, **opts, &b)
+ end and File.executable?(exe) or return nil
+ exe
+ ensure
+ MakeMakefile.rm_rf(*Dir["#{CONFTEST}*"]-[exe])
+ end
+
+ # Returns whether or not the +src+ can be compiled as a C source and linked
+ # with its depending libraries successfully. +opt+ is passed to the linker
+ # as options. Note that <tt>$CFLAGS</tt> and <tt>$LDFLAGS</tt> are also
+ # passed to the linker.
+ #
+ # If a block given, it is called with the source before compilation. You can
+ # modify the source in the block.
+ #
+ # [+src+] a String which contains a C source
+ # [+opt+] a String which contains linker options
+ def try_link(src, opt = "", **opts, &b)
+ exe = try_link0(src, opt, **opts, &b) or return false
+ MakeMakefile.rm_f exe
+ true
+ end
-def try_link0(src, opt="", &b)
- try_do(src, link_command("", opt), &b)
-end
+ # Returns whether or not the +src+ can be compiled as a C source. +opt+ is
+ # passed to the C compiler as options. Note that <tt>$CFLAGS</tt> is also
+ # passed to the compiler.
+ #
+ # If a block given, it is called with the source before compilation. You can
+ # modify the source in the block.
+ #
+ # [+src+] a String which contains a C source
+ # [+opt+] a String which contains compiler options
+ def try_compile(src, opt = "", werror: nil, **opts, &b)
+ opt = werror_flag(opt) if werror
+ try_do(src, cc_command(opt), werror: werror, **opts, &b) and
+ File.file?("#{CONFTEST}.#{$OBJEXT}")
+ ensure
+ MakeMakefile.rm_f "#{CONFTEST}*"
+ end
+
+ # Returns whether or not the +src+ can be preprocessed with the C
+ # preprocessor. +opt+ is passed to the preprocessor as options. Note that
+ # <tt>$CFLAGS</tt> is also passed to the preprocessor.
+ #
+ # If a block given, it is called with the source before preprocessing. You
+ # can modify the source in the block.
+ #
+ # [+src+] a String which contains a C source
+ # [+opt+] a String which contains preprocessor options
+ def try_cpp(src, opt = "", **opts, &b)
+ try_do(src, cpp_command(CPPOUTFILE, opt), **opts, &b) and
+ File.file?("#{CONFTEST}.i")
+ ensure
+ MakeMakefile.rm_f "#{CONFTEST}*"
+ end
-def try_link(src, opt="", &b)
- try_link0(src, opt, &b)
-ensure
- rm_f "conftest*", "c0x32*"
-end
+ alias try_header try_compile
-def try_compile(src, opt="", &b)
- try_do(src, cc_command(opt), &b)
-ensure
- rm_f "conftest*"
-end
+ def cpp_include(header)
+ if header
+ header = [header] unless header.kind_of? Array
+ header.map {|h| String === h ? "#include <#{h}>\n" : h}.join
+ else
+ ""
+ end
+ end
-def try_cpp(src, opt="", &b)
- try_do(src, cpp_command(CPPOUTFILE, opt), &b)
-ensure
- rm_f "conftest*"
-end
+ # :startdoc:
-def cpp_include(header)
- if header
- header = [header] unless header.kind_of? Array
- header.map {|h| "#include <#{h}>\n"}.join
- else
- ""
+ # Sets <tt>$CPPFLAGS</tt> to _flags_ and yields. If the block returns a
+ # falsy value, <tt>$CPPFLAGS</tt> is reset to its previous value, remains
+ # set to _flags_ otherwise.
+ #
+ # [+flags+] a C preprocessor flag as a +String+
+ #
+ def with_cppflags(flags)
+ cppflags = $CPPFLAGS
+ $CPPFLAGS = flags.dup
+ ret = yield
+ ensure
+ $CPPFLAGS = cppflags unless ret
end
-end
-def with_cppflags(flags)
- cppflags = $CPPFLAGS
- $CPPFLAGS = flags
- ret = yield
-ensure
- $CPPFLAGS = cppflags unless ret
-end
+ # :nodoc:
+ def try_cppflags(flags, werror: true, **opts)
+ try_header(MAIN_DOES_NOTHING, flags, werror: werror, **opts)
+ end
-def with_cflags(flags)
- cflags = $CFLAGS
- $CFLAGS = flags
- ret = yield
-ensure
- $CFLAGS = cflags unless ret
-end
+ # Check whether each given C preprocessor flag is acceptable and append it
+ # to <tt>$CPPFLAGS</tt> if so.
+ #
+ # [+flags+] a C preprocessor flag as a +String+ or an +Array+ of them
+ #
+ def append_cppflags(flags, **opts)
+ Array(flags).each do |flag|
+ if checking_for("whether #{flag} is accepted as CPPFLAGS") {
+ try_cppflags(flag, **opts)
+ }
+ $CPPFLAGS << " " << flag
+ end
+ end
+ end
-def with_ldflags(flags)
- ldflags = $LDFLAGS
- $LDFLAGS = flags
- ret = yield
-ensure
- $LDFLAGS = ldflags unless ret
-end
+ # Sets <tt>$CFLAGS</tt> to _flags_ and yields. If the block returns a falsy
+ # value, <tt>$CFLAGS</tt> is reset to its previous value, remains set to
+ # _flags_ otherwise.
+ def with_cflags(flags)
+ cflags = $CFLAGS
+ $CFLAGS = flags.dup
+ ret = yield
+ ensure
+ $CFLAGS = cflags unless ret
+ end
+
+ # :nodoc:
+ def try_cflags(flags, werror: true, **opts)
+ try_compile(MAIN_DOES_NOTHING, flags, werror: werror, **opts)
+ end
+
+ # Sets <tt>$LDFLAGS</tt> to _flags_ and yields. If the block returns a
+ # falsy value, <tt>$LDFLAGS</tt> is reset to its previous value, remains set
+ # to _flags_ otherwise.
+ def with_ldflags(flags)
+ ldflags = $LDFLAGS
+ $LDFLAGS = flags.dup
+ ret = yield
+ ensure
+ $LDFLAGS = ldflags unless ret
+ end
+
+ # :nodoc:
+ def try_ldflags(flags, werror: $mswin, **opts)
+ try_link(MAIN_DOES_NOTHING, "", ldflags: flags, werror: werror, **opts)
+ end
+
+ # :startdoc:
+
+ # Check whether each given linker flag is acceptable and append it to
+ # <tt>$LDFLAGS</tt> if so.
+ #
+ # [+flags+] a linker flag as a +String+ or an +Array+ of them
+ #
+ def append_ldflags(flags, **opts)
+ Array(flags).each do |flag|
+ if checking_for("whether #{flag} is accepted as LDFLAGS") {
+ try_ldflags(flag, **opts)
+ }
+ $LDFLAGS << " " << flag
+ end
+ end
+ end
+
+ # :stopdoc:
-def try_static_assert(expr, headers = nil, opt = "", &b)
- headers = cpp_include(headers)
- try_compile(<<SRC, opt, &b)
+ def try_static_assert(expr, headers = nil, opt = "", &b)
+ headers = cpp_include(headers)
+ try_compile(<<SRC, opt, &b)
#{headers}
/*top*/
int conftest_const[(#{expr}) ? 1 : -1];
SRC
-end
+ end
-def try_constant(const, headers = nil, opt = "", &b)
- includes = cpp_include(headers)
- if CROSS_COMPILING
- if try_static_assert("#{const} > 0", headers, opt)
- # positive constant
- elsif try_static_assert("#{const} < 0", headers, opt)
- neg = true
- const = "-(#{const})"
- elsif try_static_assert("#{const} == 0", headers, opt)
- return 0
- else
- # not a constant
- return nil
- end
- upper = 1
- lower = 0
- until try_static_assert("#{const} <= #{upper}", headers, opt)
- lower = upper
- upper <<= 1
- end
- return nil unless lower
- while upper > lower + 1
- mid = (upper + lower) / 2
- if try_static_assert("#{const} > #{mid}", headers, opt)
- lower = mid
+ def try_constant(const, headers = nil, opt = "", &b)
+ includes = cpp_include(headers)
+ neg = try_static_assert("#{const} < 0", headers, opt)
+ if CROSS_COMPILING
+ if neg
+ const = "-(#{const})"
+ elsif try_static_assert("#{const} > 0", headers, opt)
+ # positive constant
+ elsif try_static_assert("#{const} == 0", headers, opt)
+ return 0
else
- upper = mid
+ # not a constant
+ return nil
end
- end
- upper = -upper if neg
- return upper
- else
- src = %{#{includes}
+ upper = 1
+ until try_static_assert("#{const} <= #{upper}", headers, opt)
+ lower = upper
+ upper <<= 1
+ end
+ return nil unless lower
+ while upper > lower + 1
+ mid = (upper + lower) / 2
+ if try_static_assert("#{const} > #{mid}", headers, opt)
+ lower = mid
+ else
+ upper = mid
+ end
+ end
+ upper = -upper if neg
+ return upper
+ else
+ src = %{#{includes}
#include <stdio.h>
/*top*/
-int conftest_const = (int)(#{const});
-int main() {printf("%d\\n", conftest_const); return 0;}
+typedef#{neg ? '' : ' unsigned'}
+#ifdef PRI_LL_PREFIX
+#define PRI_CONFTEST_PREFIX PRI_LL_PREFIX
+LONG_LONG
+#else
+#define PRI_CONFTEST_PREFIX "l"
+long
+#endif
+conftest_type;
+conftest_type conftest_const = (conftest_type)(#{const});
+int main() {printf("%"PRI_CONFTEST_PREFIX"#{neg ? 'd' : 'u'}\\n", conftest_const); return 0;}
}
- if try_link0(src, opt, &b)
- xpopen("./conftest") do |f|
- return Integer(f.gets)
+ begin
+ if try_link0(src, opt, &b)
+ xpopen("./#{CONFTEST}") do |f|
+ return Integer(f.gets)
+ end
+ end
+ ensure
+ MakeMakefile.rm_f "#{CONFTEST}#{$EXEEXT}"
end
end
+ nil
end
- nil
-end
-def try_func(func, libs, headers = nil, &b)
- headers = cpp_include(headers)
- try_link(<<"SRC", libs, &b) or try_link(<<"SRC", libs, &b)
+ # You should use +have_func+ rather than +try_func+.
+ #
+ # [+func+] a String which contains a symbol name
+ # [+libs+] a String which contains library names.
+ # [+headers+] a String or an Array of strings which contains names of header
+ # files.
+ def try_func(func, libs, headers = nil, opt = "", &b)
+ headers = cpp_include(headers)
+ prepare = String.new
+ case func
+ when /^&/
+ decltype = proc {|x|"const volatile void *#{x}"}
+ when /\)$/
+ strvars = []
+ call = func.gsub(/""/) {
+ v = "s#{strvars.size + 1}"
+ strvars << v
+ v
+ }
+ unless strvars.empty?
+ prepare << "char " << strvars.map {|v| %[#{v}[1024] = ""]}.join(", ") << "; "
+ end
+ when nil
+ call = ""
+ else
+ call = "#{func}()"
+ decltype = proc {|x| "void ((*#{x})())"}
+ end
+ if opt and !opt.empty?
+ [[:to_str], [:join, " "], [:to_s]].each do |meth, *args|
+ if opt.respond_to?(meth)
+ break opt = opt.__send__(meth, *args)
+ end
+ end
+ opt = "#{opt} #{libs}"
+ else
+ opt = libs
+ end
+ decltype && try_link(<<"SRC", opt, &b) or
#{headers}
/*top*/
-#{MAIN_DOES_NOTHING}
-int t() { void ((*volatile p)()); p = (void ((*)()))#{func}; return 0; }
+extern int t(void);
+#{MAIN_DOES_NOTHING 't'}
+int t(void) { #{decltype["volatile p"]}; p = (#{decltype[]})#{func}; return !p; }
SRC
+ call && try_link(<<"SRC", opt, &b)
#{headers}
/*top*/
-#{MAIN_DOES_NOTHING}
-int t() { #{func}(); return 0; }
+extern int t(void);
+#{MAIN_DOES_NOTHING 't'}
+#{"extern void #{call};" if decltype}
+int t(void) { #{prepare}#{call}; return 0; }
SRC
-end
+ end
-def try_var(var, headers = nil, &b)
- headers = cpp_include(headers)
- try_compile(<<"SRC", &b)
+ # You should use +have_var+ rather than +try_var+.
+ def try_var(var, headers = nil, opt = "", &b)
+ headers = cpp_include(headers)
+ try_compile(<<"SRC", opt, &b)
#{headers}
/*top*/
-#{MAIN_DOES_NOTHING}
-int t() { const volatile void *volatile p; p = &(&#{var})[0]; return 0; }
+extern int t(void);
+#{MAIN_DOES_NOTHING 't'}
+int t(void) { const volatile void *volatile p; p = &(&#{var})[0]; return !p; }
SRC
-end
+ end
-def egrep_cpp(pat, src, opt = "", &b)
- src = create_tmpsrc(src, &b)
- xpopen(cpp_command('', opt)) do |f|
- if Regexp === pat
- puts(" ruby -ne 'print if #{pat.inspect}'")
- f.grep(pat) {|l|
- puts "#{f.lineno}: #{l}"
- return true
- }
- false
- else
- puts(" egrep '#{pat}'")
- begin
- stdin = $stdin.dup
- $stdin.reopen(f)
- system("egrep", pat)
- ensure
- $stdin.reopen(stdin)
+ # :startdoc:
+
+ # Returns whether or not the +src+ can be preprocessed with the C
+ # preprocessor and matches with +pat+.
+ #
+ # If a block given, it is called with the source before compilation. You can
+ # modify the source in the block.
+ #
+ # [+pat+] a Regexp or a String
+ # [+src+] a String which contains a C source
+ # [+opt+] a String which contains preprocessor options
+ #
+ # NOTE: When pat is a Regexp the matching will be checked in process,
+ # otherwise egrep(1) will be invoked to check it.
+ def egrep_cpp(pat, src, opt = "", &b)
+ src = create_tmpsrc(src, &b)
+ xpopen(cpp_command('', opt)) do |f|
+ if Regexp === pat
+ puts(" ruby -ne 'print if #{pat.inspect}'")
+ !f.grep(pat) {|l|
+ puts "#{f.lineno}: #{l}"
+ }.empty?
+ else
+ puts(" egrep '#{pat}'")
+ system("egrep", pat, in: f)
end
end
+ ensure
+ MakeMakefile.rm_f "#{CONFTEST}*"
+ log_src(src)
end
-ensure
- rm_f "conftest*"
- log_src(src)
-end
-# This is used internally by the have_macro? method.
-def macro_defined?(macro, src, opt = "", &b)
- src = src.sub(/[^\n]\z/, "\\&\n")
- try_compile(src + <<"SRC", opt, &b)
+ # :stopdoc:
+
+ # This is used internally by the have_macro? method.
+ def macro_defined?(macro, src, opt = "", &b)
+ src = src.sub(/[^\n]\z/, "\\&\n")
+ try_compile(src + <<"SRC", opt, &b)
/*top*/
#ifndef #{macro}
# error
->>>>>> #{macro} undefined <<<<<<
+|:/ === #{macro} undefined === /:|
#endif
SRC
-end
-
-def try_run(src, opt = "", &b)
- if try_link0(src, opt, &b)
- xsystem("./conftest")
- else
- nil
end
-ensure
- rm_f "conftest*"
-end
-def install_files(mfile, ifiles, map = nil, srcprefix = nil)
- ifiles or return
- ifiles.empty? and return
- srcprefix ||= '$(srcdir)'
- RbConfig::expand(srcdir = srcprefix.dup)
- dirs = []
- path = Hash.new {|h, i| h[i] = dirs.push([i])[-1]}
- ifiles.each do |files, dir, prefix|
- dir = map_dir(dir, map)
- prefix &&= %r|\A#{Regexp.quote(prefix)}/?|
- if /\A\.\// =~ files
- # install files which are in current working directory.
- files = files[2..-1]
- len = nil
+ # Returns whether or not:
+ # * the +src+ can be compiled as a C source,
+ # * the result object can be linked with its depending libraries
+ # successfully,
+ # * the linked file can be invoked as an executable
+ # * and the executable exits successfully
+ #
+ # +opt+ is passed to the linker as options. Note that <tt>$CFLAGS</tt> and
+ # <tt>$LDFLAGS</tt> are also passed to the linker.
+ #
+ # If a block given, it is called with the source before compilation. You can
+ # modify the source in the block.
+ #
+ # [+src+] a String which contains a C source
+ # [+opt+] a String which contains linker options
+ #
+ # Returns true when the executable exits successfully, false when it fails,
+ # or nil when preprocessing, compilation or link fails.
+ def try_run(src, opt = "", &b)
+ raise "cannot run test program while cross compiling" if CROSS_COMPILING
+ if try_link0(src, opt, &b)
+ xsystem("./#{CONFTEST}")
else
- # install files which are under the $(srcdir).
- files = File.join(srcdir, files)
- len = srcdir.size
- end
- f = nil
- Dir.glob(files) do |fx|
- f = fx
- f[0..len] = "" if len
- case File.basename(f)
- when *$NONINSTALLFILES
- next
- end
- d = File.dirname(f)
- d.sub!(prefix, "") if prefix
- d = (d.empty? || d == ".") ? dir : File.join(dir, d)
- f = File.join(srcprefix, f) if len
- path[d] << f
+ nil
end
- unless len or f
- d = File.dirname(files)
- d.sub!(prefix, "") if prefix
- d = (d.empty? || d == ".") ? dir : File.join(dir, d)
- path[d] << files
+ ensure
+ MakeMakefile.rm_f "#{CONFTEST}*"
+ end
+
+ def install_files(mfile, ifiles, map = nil, srcprefix = nil)
+ ifiles or return
+ ifiles.empty? and return
+ srcprefix ||= "$(srcdir)/#{srcprefix}".chomp('/')
+ RbConfig::expand(srcdir = srcprefix.dup)
+ dirs = []
+ path = Hash.new {|h, i| h[i] = dirs.push([i])[-1]}
+ ifiles.each do |files, dir, prefix|
+ dir = map_dir(dir, map)
+ prefix &&= %r|\A#{Regexp.quote(prefix)}/?|
+ if /\A\.\// =~ files
+ # install files which are in current working directory.
+ files = files[2..-1]
+ len = nil
+ else
+ # install files which are under the $(srcdir).
+ files = File.join(srcdir, files)
+ len = srcdir.size
+ end
+ f = nil
+ Dir.glob(files) do |fx|
+ f = fx
+ f[0..len] = "" if len
+ case File.basename(f)
+ when *$NONINSTALLFILES
+ next
+ end
+ d = File.dirname(f)
+ d.sub!(prefix, "") if prefix
+ d = (d.empty? || d == ".") ? dir : File.join(dir, d)
+ f = File.join(srcprefix, f) if len
+ path[d] << f
+ end
+ unless len or f
+ d = File.dirname(files)
+ d.sub!(prefix, "") if prefix
+ d = (d.empty? || d == ".") ? dir : File.join(dir, d)
+ path[d] << files
+ end
end
+ dirs
end
- dirs
-end
-def install_rb(mfile, dest, srcdir = nil)
- install_files(mfile, [["lib/**/*.rb", dest, "lib"]], nil, srcdir)
-end
+ def install_rb(mfile, dest, srcdir = nil)
+ install_files(mfile, [["lib/**/*.rb", dest, "lib"]], nil, srcdir)
+ end
-def append_library(libs, lib) # :no-doc:
- format(LIBARG, lib) + " " + libs
-end
+ def append_library(libs, lib) # :no-doc:
+ format(LIBARG, lib) + " " + libs
+ end
-def message(*s)
- unless Logging.quiet and not $VERBOSE
- printf(*s)
- $stdout.flush
+ # Prints messages to $stdout, if verbose mode.
+ #
+ # Internal use only.
+ #
+ def message(*s)
+ unless Logging.quiet and not $VERBOSE
+ printf(*s)
+ $stdout.flush
+ end
end
-end
-# This emits a string to stdout that allows users to see the results of the
-# various have* and find* methods as they are tested.
-#
-# Internal use only.
-#
-def checking_for(m, fmt = nil)
- f = caller[0][/in `(.*)'$/, 1] and f << ": " #` for vim #'
- m = "checking #{/\Acheck/ =~ f ? '' : 'for '}#{m}... "
- message "%s", m
- a = r = nil
- Logging::postpone do
- r = yield
- a = (fmt ? fmt % r : r ? "yes" : "no") << "\n"
- "#{f}#{m}-------------------- #{a}\n"
- end
- message(a)
- Logging::message "--------------------\n\n"
- r
-end
+ # This emits a string to stdout that allows users to see the results of the
+ # various have* and find* methods as they are tested.
+ #
+ # Internal use only.
+ #
+ def checking_for(m, fmt = nil)
+ if f = caller_locations(1, 1).first.base_label and /\A\w/ =~ f
+ f += ": "
+ else
+ f = ""
+ end
+ m = "checking #{/\Acheck/ =~ f ? '' : 'for '}#{m}... "
+ message "%s", m
+ a = r = nil
+ Logging::postpone do
+ r = yield
+ a = (fmt ? "#{fmt % r}" : r ? "yes" : "no")
+ "#{f}#{m}-------------------- #{a}\n\n"
+ end
+ message "%s\n", a
+ Logging::message "--------------------\n\n"
+ r
+ end
-def checking_message(target, place = nil, opt = nil)
- [["in", place], ["with", opt]].inject("#{target}") do |msg, (pre, noun)|
- if noun
- [[:to_str], [:join, ","], [:to_s]].each do |meth, *args|
- if noun.respond_to?(meth)
- break noun = noun.send(meth, *args)
+ # Build a message for checking.
+ #
+ # Internal use only.
+ #
+ def checking_message(target, place = nil, opt = nil)
+ [["in", place], ["with", opt]].inject("#{target}") do |msg, (pre, noun)|
+ if noun
+ [[:to_str], [:join, ","], [:to_s]].each do |meth, *args|
+ if noun.respond_to?(meth)
+ break noun = noun.__send__(meth, *args)
+ end
+ end
+ unless noun.empty?
+ msg << " #{pre} " unless msg.empty?
+ msg << noun
end
end
- msg << " #{pre} #{noun}" unless noun.empty?
+ msg
end
- msg
end
-end
-# :startdoc:
+ # :startdoc:
-# Returns whether or not +macro+ is defined either in the common header
-# files or within any +headers+ you provide.
-#
-# Any options you pass to +opt+ are passed along to the compiler.
-#
-def have_macro(macro, headers = nil, opt = "", &b)
- checking_for checking_message(macro, headers, opt) do
- macro_defined?(macro, cpp_include(headers), opt, &b)
+ # Check whether each given C compiler flag is acceptable and append it
+ # to <tt>$CFLAGS</tt> if so.
+ #
+ # [+flags+] a C compiler flag as a +String+ or an +Array+ of them
+ #
+ def append_cflags(flags, **opts)
+ Array(flags).each do |flag|
+ if checking_for("whether #{flag} is accepted as CFLAGS") {
+ try_cflags(flag, **opts)
+ }
+ $CFLAGS << " " << flag
+ end
+ end
end
-end
-# Returns whether or not the given entry point +func+ can be found within
-# +lib+. If +func+ is nil, the 'main()' entry point is used by default.
-# If found, it adds the library to list of libraries to be used when linking
-# your extension.
-#
-# If +headers+ are provided, it will include those header files as the
-# header files it looks in when searching for +func+.
-#
-# The real name of the library to be linked can be altered by
-# '--with-FOOlib' configuration option.
-#
-def have_library(lib, func = nil, headers = nil, &b)
- func = "main" if !func or func.empty?
- lib = with_config(lib+'lib', lib)
- checking_for checking_message("#{func}()", LIBARG%lib) do
- if COMMON_LIBS.include?(lib)
- true
- else
- libs = append_library($libs, lib)
- if try_func(func, libs, headers, &b)
- $libs = libs
+ # Returns whether or not +macro+ is defined either in the common header
+ # files or within any +headers+ you provide.
+ #
+ # Any options you pass to +opt+ are passed along to the compiler.
+ #
+ def have_macro(macro, headers = nil, opt = "", &b)
+ checking_for checking_message(macro, headers, opt) do
+ macro_defined?(macro, cpp_include(headers), opt, &b)
+ end
+ end
+
+ # Returns whether or not the given entry point +func+ can be found within
+ # +lib+. If +func+ is +nil+, the <code>main()</code> entry point is used by
+ # default. If found, it adds the library to list of libraries to be used
+ # when linking your extension.
+ #
+ # If +headers+ are provided, it will include those header files as the
+ # header files it looks in when searching for +func+.
+ #
+ # The real name of the library to be linked can be altered by
+ # <code>--with-FOOlib</code> configuration option.
+ #
+ def have_library(lib, func = nil, headers = nil, opt = "", &b)
+ dir_config(lib)
+ lib = with_config(lib+'lib', lib)
+ checking_for checking_message(func && func.funcall_style, LIBARG%lib, opt) do
+ if COMMON_LIBS.include?(lib)
true
else
- false
+ libs = append_library($libs, lib)
+ if try_func(func, libs, headers, opt, &b)
+ $libs = libs
+ true
+ else
+ false
+ end
end
end
end
-end
-# Returns whether or not the entry point +func+ can be found within the library
-# +lib+ in one of the +paths+ specified, where +paths+ is an array of strings.
-# If +func+ is nil , then the main() function is used as the entry point.
-#
-# If +lib+ is found, then the path it was found on is added to the list of
-# library paths searched and linked against.
-#
-def find_library(lib, func, *paths, &b)
- func = "main" if !func or func.empty?
- lib = with_config(lib+'lib', lib)
- paths = paths.collect {|path| path.split(File::PATH_SEPARATOR)}.flatten
- checking_for "#{func}() in #{LIBARG%lib}" do
- libpath = $LIBPATH
- libs = append_library($libs, lib)
- begin
- until r = try_func(func, libs, &b) or paths.empty?
- $LIBPATH = libpath | [paths.shift]
+ # Returns whether or not the entry point +func+ can be found within the
+ # library +lib+ in one of the +paths+ specified, where +paths+ is an array
+ # of strings. If +func+ is +nil+ , then the <code>main()</code> function is
+ # used as the entry point.
+ #
+ # If +lib+ is found, then the path it was found on is added to the list of
+ # library paths searched and linked against.
+ #
+ def find_library(lib, func, *paths, &b)
+ dir_config(lib)
+ lib = with_config(lib+'lib', lib)
+ paths = paths.flat_map {|path| path.split(File::PATH_SEPARATOR)}
+ checking_for checking_message(func && func.funcall_style, LIBARG%lib) do
+ libpath = $LIBPATH
+ libs = append_library($libs, lib)
+ begin
+ until r = try_func(func, libs, &b) or paths.empty?
+ $LIBPATH = libpath | [paths.shift]
+ end
+ if r
+ $libs = libs
+ libpath = nil
+ end
+ ensure
+ $LIBPATH = libpath if libpath
end
- if r
- $libs = libs
- libpath = nil
+ r
+ end
+ end
+
+ # Returns whether or not the function +func+ can be found in the common
+ # header files, or within any +headers+ that you provide. If found, a macro
+ # is passed as a preprocessor constant to the compiler using the function
+ # name, in uppercase, prepended with +HAVE_+.
+ #
+ # To check functions in an additional library, you need to check that
+ # library first using <code>have_library()</code>. The +func+ shall be
+ # either mere function name or function name with arguments.
+ #
+ # For example, if <code>have_func('foo')</code> returned +true+, then the
+ # +HAVE_FOO+ preprocessor macro would be passed to the compiler.
+ #
+ def have_func(func, headers = nil, opt = "", &b)
+ checking_for checking_message(func.funcall_style, headers, opt) do
+ if try_func(func, $libs, headers, opt, &b)
+ $defs << "-DHAVE_#{func.sans_arguments.tr_cpp}"
+ true
+ else
+ false
end
- ensure
- $LIBPATH = libpath if libpath
end
- r
end
-end
-# Returns whether or not the function +func+ can be found in the common
-# header files, or within any +headers+ that you provide. If found, a
-# macro is passed as a preprocessor constant to the compiler using the
-# function name, in uppercase, prepended with 'HAVE_'.
-#
-# For example, if have_func('foo') returned true, then the HAVE_FOO
-# preprocessor macro would be passed to the compiler.
-#
-def have_func(func, headers = nil, &b)
- checking_for checking_message("#{func}()", headers) do
- if try_func(func, $libs, headers, &b)
- $defs.push(format("-DHAVE_%s", func.tr_cpp))
- true
- else
- false
+ # Returns whether or not the variable +var+ can be found in the common
+ # header files, or within any +headers+ that you provide. If found, a macro
+ # is passed as a preprocessor constant to the compiler using the variable
+ # name, in uppercase, prepended with +HAVE_+.
+ #
+ # To check variables in an additional library, you need to check that
+ # library first using <code>have_library()</code>.
+ #
+ # For example, if <code>have_var('foo')</code> returned true, then the
+ # +HAVE_FOO+ preprocessor macro would be passed to the compiler.
+ #
+ def have_var(var, headers = nil, opt = "", &b)
+ checking_for checking_message(var, headers, opt) do
+ if try_var(var, headers, opt, &b)
+ $defs.push(format("-DHAVE_%s", var.tr_cpp))
+ true
+ else
+ false
+ end
end
end
-end
-# Returns whether or not the variable +var+ can be found in the common
-# header files, or within any +headers+ that you provide. If found, a
-# macro is passed as a preprocessor constant to the compiler using the
-# variable name, in uppercase, prepended with 'HAVE_'.
-#
-# For example, if have_var('foo') returned true, then the HAVE_FOO
-# preprocessor macro would be passed to the compiler.
-#
-def have_var(var, headers = nil, &b)
- checking_for checking_message(var, headers) do
- if try_var(var, headers, &b)
- $defs.push(format("-DHAVE_%s", var.tr_cpp))
- true
- else
- false
+ # Returns whether or not the given +header+ file can be found on your system.
+ # If found, a macro is passed as a preprocessor constant to the compiler
+ # using the header file name, in uppercase, prepended with +HAVE_+.
+ #
+ # For example, if <code>have_header('foo.h')</code> returned true, then the
+ # +HAVE_FOO_H+ preprocessor macro would be passed to the compiler.
+ #
+ def have_header(header, preheaders = nil, opt = "", &b)
+ dir_config(header[/.*?(?=\/)|.*?(?=\.)/])
+ checking_for header do
+ if try_header(cpp_include(preheaders)+cpp_include(header), opt, &b)
+ $defs.push(format("-DHAVE_%s", header.tr_cpp))
+ true
+ else
+ false
+ end
end
end
-end
-# Returns whether or not the given +header+ file can be found on your system.
-# If found, a macro is passed as a preprocessor constant to the compiler using
-# the header file name, in uppercase, prepended with 'HAVE_'.
-#
-# For example, if have_header('foo.h') returned true, then the HAVE_FOO_H
-# preprocessor macro would be passed to the compiler.
-#
-def have_header(header, &b)
- checking_for header do
- if try_cpp(cpp_include(header), &b)
- $defs.push(format("-DHAVE_%s", header.tr("a-z./\055", "A-Z___")))
- true
+ # Returns whether or not the given +framework+ can be found on your system.
+ # If found, a macro is passed as a preprocessor constant to the compiler
+ # using the framework name, in uppercase, prepended with +HAVE_FRAMEWORK_+.
+ #
+ # For example, if <code>have_framework('Ruby')</code> returned true, then
+ # the +HAVE_FRAMEWORK_RUBY+ preprocessor macro would be passed to the
+ # compiler.
+ #
+ # If +fw+ is a pair of the framework name and its header file name
+ # that header file is checked, instead of the normally used header
+ # file which is named same as the framework.
+ def have_framework(fw, &b)
+ if Array === fw
+ fw, header = *fw
else
- false
+ header = "#{fw}.h"
+ end
+ checking_for fw do
+ src = cpp_include("#{fw}/#{header}") << "\n" "int main(void){return 0;}"
+ opt = " -framework #{fw}"
+ if try_link(src, opt, &b) or (objc = try_link(src, "-ObjC#{opt}", &b))
+ $defs.push(format("-DHAVE_FRAMEWORK_%s", fw.tr_cpp))
+ # TODO: non-worse way than this hack, to get rid of separating
+ # option and its argument.
+ $LDFLAGS << " -ObjC" if objc and /(\A|\s)-ObjC(\s|\z)/ !~ $LDFLAGS
+ $LIBS << opt
+ true
+ else
+ false
+ end
end
end
-end
-# Instructs mkmf to search for the given +header+ in any of the +paths+
-# provided, and returns whether or not it was found in those paths.
-#
-# If the header is found then the path it was found on is added to the list
-# of included directories that are sent to the compiler (via the -I switch).
-#
-def find_header(header, *paths)
- message = checking_message(header, paths)
- header = cpp_include(header)
- checking_for message do
- if try_cpp(header)
- true
- else
- found = false
- paths.each do |dir|
- opt = "-I#{dir}".quote
- if try_cpp(header, opt)
- $INCFLAGS << " " << opt
- found = true
- break
+ # Instructs mkmf to search for the given +header+ in any of the +paths+
+ # provided, and returns whether or not it was found in those paths.
+ #
+ # If the header is found then the path it was found on is added to the list
+ # of included directories that are sent to the compiler (via the
+ # <code>-I</code> switch).
+ #
+ def find_header(header, *paths)
+ message = checking_message(header, paths)
+ header = cpp_include(header)
+ checking_for message do
+ if try_header(header)
+ true
+ else
+ found = false
+ paths.each do |dir|
+ opt = "-I#{dir}".quote
+ if try_header(header, opt)
+ $INCFLAGS << " " << opt
+ found = true
+ break
+ end
end
+ found
end
- found
end
end
-end
-# Returns whether or not the struct of type +type+ contains +member+. If
-# it does not, or the struct type can't be found, then false is returned. You
-# may optionally specify additional +headers+ in which to look for the struct
-# (in addition to the common header files).
-#
-# If found, a macro is passed as a preprocessor constant to the compiler using
-# the type name and the member name, in uppercase, prepended with 'HAVE_'.
-#
-# For example, if have_struct_member('struct foo', 'bar') returned true, then the
-# HAVE_STRUCT_FOO_BAR preprocessor macro would be passed to the compiler.
-#
-# HAVE_ST_BAR is also defined for backward compatibility.
-#
-def have_struct_member(type, member, headers = nil, &b)
- checking_for checking_message("#{type}.#{member}", headers) do
- if try_compile(<<"SRC", &b)
+ # Returns whether or not the struct of type +type+ contains +member+. If
+ # it does not, or the struct type can't be found, then false is returned.
+ # You may optionally specify additional +headers+ in which to look for the
+ # struct (in addition to the common header files).
+ #
+ # If found, a macro is passed as a preprocessor constant to the compiler
+ # using the type name and the member name, in uppercase, prepended with
+ # +HAVE_+.
+ #
+ # For example, if <code>have_struct_member('struct foo', 'bar')</code>
+ # returned true, then the +HAVE_STRUCT_FOO_BAR+ preprocessor macro would be
+ # passed to the compiler.
+ #
+ # +HAVE_ST_BAR+ is also defined for backward compatibility.
+ #
+ def have_struct_member(type, member, headers = nil, opt = "", &b)
+ checking_for checking_message("#{type}.#{member}", headers) do
+ if try_compile(<<"SRC", opt, &b)
#{cpp_include(headers)}
/*top*/
-#{MAIN_DOES_NOTHING}
int s = (char *)&((#{type}*)0)->#{member} - (char *)0;
+#{MAIN_DOES_NOTHING}
SRC
- $defs.push(format("-DHAVE_%s_%s", type.tr_cpp, member.tr_cpp))
- $defs.push(format("-DHAVE_ST_%s", member.tr_cpp)) # backward compatibility
- true
- else
- false
+ $defs.push(format("-DHAVE_%s_%s", type.tr_cpp, member.tr_cpp))
+ $defs.push(format("-DHAVE_ST_%s", member.tr_cpp)) # backward compatibility
+ true
+ else
+ false
+ end
end
end
-end
-def try_type(type, headers = nil, opt = "", &b)
- if try_compile(<<"SRC", opt, &b)
+ # :nodoc:
+ # Returns whether or not the static type +type+ is defined.
+ #
+ # See also +have_type+
+ #
+ def try_type(type, headers = nil, opt = "", &b)
+ if try_compile(<<"SRC", opt, &b)
#{cpp_include(headers)}
/*top*/
typedef #{type} conftest_type;
int conftestval[sizeof(conftest_type)?1:-1];
SRC
- $defs.push(format("-DHAVE_TYPE_%s", type.tr_cpp))
- true
- else
- false
+ $defs.push(format("-DHAVE_TYPE_%s", type.tr_cpp))
+ true
+ else
+ false
+ end
end
-end
-# Returns whether or not the static type +type+ is defined. You may
-# optionally pass additional +headers+ to check against in addition to the
-# common header files.
-#
-# You may also pass additional flags to +opt+ which are then passed along to
-# the compiler.
-#
-# If found, a macro is passed as a preprocessor constant to the compiler using
-# the type name, in uppercase, prepended with 'HAVE_TYPE_'.
-#
-# For example, if have_type('foo') returned true, then the HAVE_TYPE_FOO
-# preprocessor macro would be passed to the compiler.
-#
-def have_type(type, headers = nil, opt = "", &b)
- checking_for checking_message(type, headers, opt) do
- try_type(type, headers, opt, &b)
+ # Returns whether or not the static type +type+ is defined. You may
+ # optionally pass additional +headers+ to check against in addition to the
+ # common header files.
+ #
+ # You may also pass additional flags to +opt+ which are then passed along to
+ # the compiler.
+ #
+ # If found, a macro is passed as a preprocessor constant to the compiler
+ # using the type name, in uppercase, prepended with +HAVE_TYPE_+.
+ #
+ # For example, if <code>have_type('foo')</code> returned true, then the
+ # +HAVE_TYPE_FOO+ preprocessor macro would be passed to the compiler.
+ #
+ def have_type(type, headers = nil, opt = "", &b)
+ checking_for checking_message(type, headers, opt) do
+ try_type(type, headers, opt, &b)
+ end
end
-end
-# Returns where the static type +type+ is defined.
-#
-# You may also pass additional flags to +opt+ which are then passed along to
-# the compiler.
-#
-# See also +have_type+.
-#
-def find_type(type, opt, *headers, &b)
- opt ||= ""
- fmt = "not found"
- def fmt.%(x)
- x ? x.respond_to?(:join) ? x.join(",") : x : self
- end
- checking_for checking_message(type, nil, opt), fmt do
- headers.find do |h|
- try_type(type, h, opt, &b)
+ # Returns where the static type +type+ is defined.
+ #
+ # You may also pass additional flags to +opt+ which are then passed along to
+ # the compiler.
+ #
+ # See also +have_type+.
+ #
+ def find_type(type, opt, *headers, &b)
+ opt ||= ""
+ fmt = "not found"
+ def fmt.%(x)
+ x ? x.respond_to?(:join) ? x.join(",") : x : self
+ end
+ checking_for checking_message(type, nil, opt), fmt do
+ headers.find do |h|
+ try_type(type, h, opt, &b)
+ end
end
end
-end
-def try_const(const, headers = nil, opt = "", &b)
- const, type = *const
- if try_compile(<<"SRC", opt, &b)
+ # :nodoc:
+ # Returns whether or not the constant +const+ is defined.
+ #
+ # See also +have_const+
+ #
+ def try_const(const, headers = nil, opt = "", &b)
+ const, type = *const
+ if try_compile(<<"SRC", opt, &b)
#{cpp_include(headers)}
/*top*/
typedef #{type || 'int'} conftest_type;
conftest_type conftestval = #{type ? '' : '(int)'}#{const};
SRC
- $defs.push(format("-DHAVE_CONST_%s", const.tr_cpp))
- true
- else
- false
+ $defs.push(format("-DHAVE_CONST_%s", const.tr_cpp))
+ true
+ else
+ false
+ end
end
-end
-# Returns whether or not the constant +const+ is defined. You may
-# optionally pass the +type+ of +const+ as <code>[const, type]</code>,
-# like as:
-#
-# have_const(%w[PTHREAD_MUTEX_INITIALIZER pthread_mutex_t], "pthread.h")
-#
-# You may also pass additional +headers+ to check against in addition
-# to the common header files, and additional flags to +opt+ which are
-# then passed along to the compiler.
-#
-# If found, a macro is passed as a preprocessor constant to the compiler using
-# the type name, in uppercase, prepended with 'HAVE_CONST_'.
-#
-# For example, if have_const('foo') returned true, then the HAVE_CONST_FOO
-# preprocessor macro would be passed to the compiler.
-#
-def have_const(const, headers = nil, opt = "", &b)
- checking_for checking_message([*const].compact.join(' '), headers, opt) do
- try_const(const, headers, opt, &b)
+ # Returns whether or not the constant +const+ is defined. You may
+ # optionally pass the +type+ of +const+ as <code>[const, type]</code>,
+ # such as:
+ #
+ # have_const(%w[PTHREAD_MUTEX_INITIALIZER pthread_mutex_t], "pthread.h")
+ #
+ # You may also pass additional +headers+ to check against in addition to the
+ # common header files, and additional flags to +opt+ which are then passed
+ # along to the compiler.
+ #
+ # If found, a macro is passed as a preprocessor constant to the compiler
+ # using the type name, in uppercase, prepended with +HAVE_CONST_+.
+ #
+ # For example, if <code>have_const('foo')</code> returned true, then the
+ # +HAVE_CONST_FOO+ preprocessor macro would be passed to the compiler.
+ #
+ def have_const(const, headers = nil, opt = "", &b)
+ checking_for checking_message([*const].compact.join(' '), headers, opt) do
+ try_const(const, headers, opt, &b)
+ end
end
-end
-# Returns the size of the given +type+. You may optionally specify additional
-# +headers+ to search in for the +type+.
-#
-# If found, a macro is passed as a preprocessor constant to the compiler using
-# the type name, in uppercase, prepended with 'SIZEOF_', followed by the type
-# name, followed by '=X' where 'X' is the actual size.
-#
-# For example, if check_sizeof('mystruct') returned 12, then the
-# SIZEOF_MYSTRUCT=12 preprocessor macro would be passed to the compiler.
-#
-def check_sizeof(type, headers = nil, &b)
- expr = "sizeof(#{type})"
- fmt = "%d"
- def fmt.%(x)
- x ? super : "failed"
- end
- checking_for checking_message("size of #{type}", headers), fmt do
- if size = try_constant(expr, headers, &b)
- $defs.push(format("-DSIZEOF_%s=%d", type.tr_cpp, size))
- size
+ # :stopdoc:
+ STRING_OR_FAILED_FORMAT = "%s"
+ class << STRING_OR_FAILED_FORMAT # :nodoc:
+ def %(x)
+ x ? super : "failed"
+ end
+ end
+
+ def typedef_expr(type, headers)
+ typename, member = type.split('.', 2)
+ prelude = cpp_include(headers).split(/$/)
+ prelude << "typedef #{typename} rbcv_typedef_;\n"
+ return "rbcv_typedef_", member, prelude
+ end
+
+ def try_signedness(type, member, headers = nil, opts = nil)
+ raise ArgumentError, "don't know how to tell signedness of members" if member
+ if try_static_assert("(#{type})-1 < 0", headers, opts)
+ return -1
+ elsif try_static_assert("(#{type})-1 > 0", headers, opts)
+ return +1
end
end
-end
-# :stopdoc:
+ # :startdoc:
+
+ # Returns the size of the given +type+. You may optionally specify
+ # additional +headers+ to search in for the +type+.
+ #
+ # If found, a macro is passed as a preprocessor constant to the compiler
+ # using the type name, in uppercase, prepended with +SIZEOF_+, followed by
+ # the type name, followed by <code>=X</code> where "X" is the actual size.
+ #
+ # For example, if <code>check_sizeof('mystruct')</code> returned 12, then
+ # the <code>SIZEOF_MYSTRUCT=12</code> preprocessor macro would be passed to
+ # the compiler.
+ #
+ def check_sizeof(type, headers = nil, opts = "", &b)
+ typedef, member, prelude = typedef_expr(type, headers)
+ prelude << "#{typedef} *rbcv_ptr_;\n"
+ prelude = [prelude]
+ expr = "sizeof((*rbcv_ptr_)#{"." << member if member})"
+ fmt = STRING_OR_FAILED_FORMAT
+ checking_for checking_message("size of #{type}", headers), fmt do
+ if size = try_constant(expr, prelude, opts, &b)
+ $defs.push(format("-DSIZEOF_%s=%s", type.tr_cpp, size))
+ size
+ end
+ end
+ end
+
+ # Returns the signedness of the given +type+. You may optionally specify
+ # additional +headers+ to search in for the +type+.
+ #
+ # If the +type+ is found and is a numeric type, a macro is passed as a
+ # preprocessor constant to the compiler using the +type+ name, in uppercase,
+ # prepended with +SIGNEDNESS_OF_+, followed by the +type+ name, followed by
+ # <code>=X</code> where "X" is positive integer if the +type+ is unsigned
+ # and a negative integer if the +type+ is signed.
+ #
+ # For example, if +size_t+ is defined as unsigned, then
+ # <code>check_signedness('size_t')</code> would return +1 and the
+ # <code>SIGNEDNESS_OF_SIZE_T=+1</code> preprocessor macro would be passed to
+ # the compiler. The <code>SIGNEDNESS_OF_INT=-1</code> macro would be set
+ # for <code>check_signedness('int')</code>
+ #
+ def check_signedness(type, headers = nil, opts = nil, &b)
+ typedef, member, prelude = typedef_expr(type, headers)
+ signed = nil
+ checking_for("signedness of #{type}", STRING_OR_FAILED_FORMAT) do
+ signed = try_signedness(typedef, member, [prelude], opts, &b) or next nil
+ $defs.push("-DSIGNEDNESS_OF_%s=%+d" % [type.tr_cpp, signed])
+ signed < 0 ? "signed" : "unsigned"
+ end
+ signed
+ end
+
+ # Returns the convertible integer type of the given +type+. You may
+ # optionally specify additional +headers+ to search in for the +type+.
+ # _convertible_ means actually the same type, or typedef'd from the same
+ # type.
+ #
+ # If the +type+ is an integer type and the _convertible_ type is found,
+ # the following macros are passed as preprocessor constants to the compiler
+ # using the +type+ name, in uppercase.
+ #
+ # * +TYPEOF_+, followed by the +type+ name, followed by <code>=X</code>
+ # where "X" is the found _convertible_ type name.
+ # * +TYP2NUM+ and +NUM2TYP+,
+ # where +TYP+ is the +type+ name in uppercase with replacing an +_t+
+ # suffix with "T", followed by <code>=X</code> where "X" is the macro name
+ # to convert +type+ to an Integer object, and vice versa.
+ #
+ # For example, if +foobar_t+ is defined as unsigned long, then
+ # <code>convertible_int("foobar_t")</code> would return "unsigned long", and
+ # define these macros:
+ #
+ # #define TYPEOF_FOOBAR_T unsigned long
+ # #define FOOBART2NUM ULONG2NUM
+ # #define NUM2FOOBART NUM2ULONG
+ #
+ def convertible_int(type, headers = nil, opts = nil, &b)
+ type, macname = *type
+ checking_for("convertible type of #{type}", STRING_OR_FAILED_FORMAT) do
+ if UNIVERSAL_INTS.include?(type)
+ type
+ else
+ typedef, member, prelude = typedef_expr(type, headers, &b)
+ if member
+ prelude << "static rbcv_typedef_ rbcv_var;"
+ compat = UNIVERSAL_INTS.find {|t|
+ try_static_assert("sizeof(rbcv_var.#{member}) == sizeof(#{t})", [prelude], opts, &b)
+ }
+ else
+ next unless signed = try_signedness(typedef, member, [prelude])
+ u = "unsigned " if signed > 0
+ prelude << "extern rbcv_typedef_ foo();"
+ compat = UNIVERSAL_INTS.find {|t|
+ try_compile([prelude, "extern #{u}#{t} foo();"].join("\n"), opts, werror: true, &b)
+ }
+ end
+ if compat
+ macname ||= type.sub(/_(?=t\z)/, '').tr_cpp
+ conv = (compat == "long long" ? "LL" : compat.upcase)
+ compat = "#{u}#{compat}"
+ typename = type.tr_cpp
+ $defs.push(format("-DSIZEOF_%s=SIZEOF_%s", typename, compat.tr_cpp))
+ $defs.push(format("-DTYPEOF_%s=%s", typename, compat.quote))
+ $defs.push(format("-DPRI_%s_PREFIX=PRI_%s_PREFIX", macname, conv))
+ conv = (u ? "U" : "") + conv
+ $defs.push(format("-D%s2NUM=%s2NUM", macname, conv))
+ $defs.push(format("-DNUM2%s=NUM2%s", macname, conv))
+ compat
+ end
+ end
+ end
+ end
+ # :stopdoc:
-# Used internally by the what_type? method to determine if +type+ is a scalar
-# pointer.
-def scalar_ptr_type?(type, member = nil, headers = nil, &b)
- try_compile(<<"SRC", &b) # pointer
+ # Used internally by the what_type? method to determine if +type+ is a scalar
+ # pointer.
+ def scalar_ptr_type?(type, member = nil, headers = nil, &b)
+ try_compile(<<"SRC", &b)
#{cpp_include(headers)}
/*top*/
volatile #{type} conftestval;
-#{MAIN_DOES_NOTHING}
-int t() {return (int)(1-*(conftestval#{member ? ".#{member}" : ""}));}
+extern int t(void);
+#{MAIN_DOES_NOTHING 't'}
+int t(void) {return (int)(1-*(conftestval#{member ? ".#{member}" : ""}));}
SRC
-end
+ end
-# Used internally by the what_type? method to determine if +type+ is a scalar
-# pointer.
-def scalar_type?(type, member = nil, headers = nil, &b)
- try_compile(<<"SRC", &b) # pointer
+ # Used internally by the what_type? method to determine if +type+ is a scalar
+ # pointer.
+ def scalar_type?(type, member = nil, headers = nil, &b)
+ try_compile(<<"SRC", &b)
#{cpp_include(headers)}
/*top*/
volatile #{type} conftestval;
-#{MAIN_DOES_NOTHING}
-int t() {return (int)(1-(conftestval#{member ? ".#{member}" : ""}));}
+extern int t(void);
+#{MAIN_DOES_NOTHING 't'}
+int t(void) {return (int)(1-(conftestval#{member ? ".#{member}" : ""}));}
SRC
-end
+ end
+
+ # Used internally by the what_type? method to check if the _typeof_ GCC
+ # extension is available.
+ def have_typeof?
+ return $typeof if defined?($typeof)
+ $typeof = %w[__typeof__ typeof].find do |t|
+ try_compile(<<SRC)
+int rbcv_foo;
+#{t}(rbcv_foo) rbcv_bar;
+SRC
+ end
+ end
-def what_type?(type, member = nil, headers = nil, &b)
- m = "#{type}"
- name = type
- if member
- m << "." << member
- name = "(((#{type} *)0)->#{member})"
- end
- fmt = "seems %s"
- def fmt.%(x)
- x ? super : "unknown"
- end
- checking_for checking_message(m, headers), fmt do
- if scalar_ptr_type?(type, member, headers, &b)
- if try_static_assert("sizeof(*#{name}) == 1", headers)
- "string"
- end
- elsif scalar_type?(type, member, headers, &b)
- if try_static_assert("sizeof(#{name}) > sizeof(long)", headers)
- "long long"
- elsif try_static_assert("sizeof(#{name}) > sizeof(int)", headers)
- "long"
- elsif try_static_assert("sizeof(#{name}) > sizeof(short)", headers)
- "int"
- elsif try_static_assert("sizeof(#{name}) > 1", headers)
- "short"
+ # :startdoc:
+
+ # Returns a string represents the type of _type_, or _member_ of
+ # _type_ if _member_ is not +nil+.
+ def what_type?(type, member = nil, headers = nil, &b)
+ m = "#{type}"
+ var = val = "*rbcv_var_"
+ func = "rbcv_func_(void)"
+ if member
+ m << "." << member
+ else
+ type, member = type.split('.', 2)
+ end
+ if member
+ val = "(#{var}).#{member}"
+ end
+ prelude = [cpp_include(headers).split(/^/)]
+ prelude << ["typedef #{type} rbcv_typedef_;\n",
+ "extern rbcv_typedef_ *#{func};\n",
+ "rbcv_typedef_ #{var};\n",
+ ]
+ type = "rbcv_typedef_"
+ fmt = member && !(typeof = have_typeof?) ? "seems %s" : "%s"
+ if typeof
+ var = "*rbcv_member_"
+ func = "rbcv_mem_func_(void)"
+ member = nil
+ type = "rbcv_mem_typedef_"
+ prelude[-1] << "typedef #{typeof}(#{val}) #{type};\n"
+ prelude[-1] << "extern #{type} *#{func};\n"
+ prelude[-1] << "#{type} #{var};\n"
+ val = var
+ end
+ def fmt.%(x)
+ x ? super : "unknown"
+ end
+ checking_for checking_message(m, headers), fmt do
+ if scalar_ptr_type?(type, member, prelude, &b)
+ if try_static_assert("sizeof(*#{var}) == 1", prelude)
+ return "string"
+ end
+ ptr = "*"
+ elsif scalar_type?(type, member, prelude, &b)
+ unless member and !typeof or try_static_assert("(#{type})-1 < 0", prelude)
+ unsigned = "unsigned"
+ end
+ ptr = ""
else
- "char"
+ next
+ end
+ type = UNIVERSAL_INTS.find do |t|
+ pre = prelude
+ unless member
+ pre += [["#{unsigned} #{t} #{ptr}#{var};\n",
+ "extern #{unsigned} #{t} #{ptr}*#{func};\n"]]
+ end
+ try_static_assert("sizeof(#{ptr}#{val}) == sizeof(#{unsigned} #{t})", pre)
end
+ type or next
+ [unsigned, type, ptr].join(" ").strip
end
end
-end
-# This method is used internally by the find_executable method.
-#
-# Internal use only.
-#
-def find_executable0(bin, path = nil)
- ext = config_string('EXEEXT')
- if File.expand_path(bin) == bin
- return bin if File.executable?(bin)
- ext and File.executable?(file = bin + ext) and return file
- return nil
- end
- if path ||= ENV['PATH']
- path = path.split(File::PATH_SEPARATOR)
- else
- path = %w[/usr/local/bin /usr/ucb /usr/bin /bin]
- end
- file = nil
- path.each do |dir|
- return file if File.executable?(file = File.join(dir, bin))
- return file if ext and File.executable?(file << ext)
+ # :nodoc:
+ #
+ # This method is used internally by the find_executable method.
+ #
+ # Internal use only.
+ #
+ def find_executable0(bin, path = nil)
+ executable_file = proc do |name|
+ begin
+ stat = File.stat(name)
+ rescue SystemCallError
+ else
+ next name if stat.file? and stat.executable?
+ end
+ end
+
+ exts = config_string('EXECUTABLE_EXTS') {|s| s.split} || config_string('EXEEXT') {|s| [s]}
+ if File.expand_path(bin) == bin
+ return bin if executable_file.call(bin)
+ if exts
+ exts.each {|ext| executable_file.call(file = bin + ext) and return file}
+ end
+ return nil
+ end
+ if path ||= ENV['PATH']
+ path = path.split(File::PATH_SEPARATOR)
+ else
+ path = %w[/usr/local/bin /usr/ucb /usr/bin /bin]
+ end
+ file = nil
+ path.each do |dir|
+ dir.sub!(/\A"(.*)"\z/m, '\1') if $mswin or $mingw
+ return file if executable_file.call(file = File.join(dir, bin))
+ if exts
+ exts.each {|ext| executable_file.call(ext = file + ext) and return ext}
+ end
+ end
+ nil
end
- nil
-end
-# :startdoc:
-
-# Searches for the executable +bin+ on +path+. The default path is your
-# PATH environment variable. If that isn't defined, it will resort to
-# searching /usr/local/bin, /usr/ucb, /usr/bin and /bin.
-#
-# If found, it will return the full path, including the executable name,
-# of where it was found.
-#
-# Note that this method does not actually affect the generated Makefile.
-#
-def find_executable(bin, path = nil)
- checking_for checking_message(bin, path) do
- find_executable0(bin, path)
+ # Searches for the executable +bin+ on +path+. The default path is your
+ # +PATH+ environment variable. If that isn't defined, it will resort to
+ # searching /usr/local/bin, /usr/ucb, /usr/bin and /bin.
+ #
+ # If found, it will return the full path, including the executable name, of
+ # where it was found.
+ #
+ # Note that this method does not actually affect the generated Makefile.
+ #
+ def find_executable(bin, path = nil)
+ checking_for checking_message(bin, path) do
+ find_executable0(bin, path)
+ end
end
-end
-# :stopdoc:
+ # :stopdoc:
-def arg_config(config, default=nil, &block)
- $arg_config << [config, default]
- defaults = []
- if default
- defaults << default
- elsif !block
- defaults << nil
+ def arg_config(config, default=nil, &block)
+ $arg_config << [config, default]
+ defaults = []
+ if default
+ defaults << default
+ elsif !block
+ defaults << nil
+ end
+ $configure_args.fetch(config.tr('_', '-'), *defaults, &block)
+ end
+
+ # :startdoc:
+
+ # Tests for the presence of a <tt>--with-</tt>_config_ or
+ # <tt>--without-</tt>_config_ option. Returns +true+ if the with option is
+ # given, +false+ if the without option is given, and the default value
+ # otherwise.
+ #
+ # This can be useful for adding custom definitions, such as debug
+ # information.
+ #
+ # Example:
+ #
+ # if with_config("debug")
+ # $defs.push("-DOSSL_DEBUG") unless $defs.include? "-DOSSL_DEBUG"
+ # end
+ #
+ def with_config(config, default=nil)
+ config = config.sub(/^--with[-_]/, '')
+ val = arg_config("--with-"+config) do
+ if arg_config("--without-"+config)
+ false
+ elsif block_given?
+ yield(config, default)
+ else
+ break default
+ end
+ end
+ case val
+ when "yes"
+ true
+ when "no"
+ false
+ else
+ val
+ end
end
- $configure_args.fetch(config.tr('_', '-'), *defaults, &block)
-end
-# :startdoc:
-
-# Tests for the presence of a --with-<tt>config</tt> or --without-<tt>config</tt>
-# option. Returns true if the with option is given, false if the without
-# option is given, and the default value otherwise.
-#
-# This can be useful for adding custom definitions, such as debug information.
-#
-# Example:
-#
-# if with_config("debug")
-# $defs.push("-DOSSL_DEBUG") unless $defs.include? "-DOSSL_DEBUG"
-# end
-#
-def with_config(config, default=nil)
- config = config.sub(/^--with[-_]/, '')
- val = arg_config("--with-"+config) do
- if arg_config("--without-"+config)
+ # Tests for the presence of an <tt>--enable-</tt>_config_ or
+ # <tt>--disable-</tt>_config_ option. Returns +true+ if the enable option is
+ # given, +false+ if the disable option is given, and the default value
+ # otherwise.
+ #
+ # This can be useful for adding custom definitions, such as debug
+ # information.
+ #
+ # Example:
+ #
+ # if enable_config("debug")
+ # $defs.push("-DOSSL_DEBUG") unless $defs.include? "-DOSSL_DEBUG"
+ # end
+ #
+ def enable_config(config, default=nil)
+ if arg_config("--enable-"+config)
+ true
+ elsif arg_config("--disable-"+config)
false
elsif block_given?
yield(config, default)
else
- break default
+ return default
end
end
- case val
- when "yes"
- true
- when "no"
- false
- else
- val
- end
-end
-# Tests for the presence of an --enable-<tt>config</tt> or
-# --disable-<tt>config</tt> option. Returns true if the enable option is given,
-# false if the disable option is given, and the default value otherwise.
-#
-# This can be useful for adding custom definitions, such as debug information.
-#
-# Example:
-#
-# if enable_config("debug")
-# $defs.push("-DOSSL_DEBUG") unless $defs.include? "-DOSSL_DEBUG"
-# end
-#
-def enable_config(config, default=nil)
- if arg_config("--enable-"+config)
- true
- elsif arg_config("--disable-"+config)
- false
- elsif block_given?
- yield(config, default)
- else
- return default
- end
-end
+ # Generates a header file consisting of the various macro definitions
+ # generated by other methods such as have_func and have_header. These are
+ # then wrapped in a custom <code>#ifndef</code> based on the +header+ file
+ # name, which defaults to "extconf.h".
+ #
+ # For example:
+ #
+ # # extconf.rb
+ # require 'mkmf'
+ # have_func('realpath')
+ # have_header('sys/utime.h')
+ # create_header
+ # create_makefile('foo')
+ #
+ # The above script would generate the following extconf.h file:
+ #
+ # #ifndef EXTCONF_H
+ # #define EXTCONF_H
+ # #define HAVE_REALPATH 1
+ # #define HAVE_SYS_UTIME_H 1
+ # #endif
+ #
+ # Given that the create_header method generates a file based on definitions
+ # set earlier in your extconf.rb file, you will probably want to make this
+ # one of the last methods you call in your script.
+ #
+ def create_header(header = "extconf.h")
+ message "creating %s\n", header
+ sym = header.tr_cpp
+ hdr = ["#ifndef #{sym}\n#define #{sym}\n"]
+ for line in $defs
+ case line
+ when /^-D([^=]+)(?:=(.*))?/
+ hdr << "#define #$1 #{$2 ? Shellwords.shellwords($2)[0].gsub(/(?=\t+)/, "\\\n") : 1}\n"
+ when /^-U(.*)/
+ hdr << "#undef #$1\n"
+ end
+ end
+ hdr << "#endif\n"
+ hdr = hdr.join("")
+ log_src(hdr, "#{header} is")
+ unless (File.read(header) == hdr rescue false)
+ File.open(header, "wb") do |hfile|
+ hfile.write(hdr)
+ end
+ end
+ $extconf_h = header
+ end
+
+ # call-seq:
+ # dir_config(target)
+ # dir_config(target, prefix)
+ # dir_config(target, idefault, ldefault)
+ #
+ # Sets a +target+ name that the user can then use to configure
+ # various "with" options with on the command line by using that
+ # name. For example, if the target is set to "foo", then the user
+ # could use the <code>--with-foo-dir=prefix</code>,
+ # <code>--with-foo-include=dir</code> and
+ # <code>--with-foo-lib=dir</code> command line options to tell where
+ # to search for header/library files.
+ #
+ # You may pass along additional parameters to specify default
+ # values. If one is given it is taken as default +prefix+, and if
+ # two are given they are taken as "include" and "lib" defaults in
+ # that order.
+ #
+ # In any case, the return value will be an array of determined
+ # "include" and "lib" directories, either of which can be nil if no
+ # corresponding command line option is given when no default value
+ # is specified.
+ #
+ # Note that dir_config only adds to the list of places to search for
+ # libraries and include files. It does not link the libraries into your
+ # application.
+ #
+ def dir_config(target, idefault=nil, ldefault=nil)
+ key = [target, idefault, ldefault].compact.join("\0")
+ if conf = $config_dirs[key]
+ return conf
+ end
-# Generates a header file consisting of the various macro definitions generated
-# by other methods such as have_func and have_header. These are then wrapped in
-# a custom #ifndef based on the +header+ file name, which defaults to
-# 'extconf.h'.
-#
-# For example:
-#
-# # extconf.rb
-# require 'mkmf'
-# have_func('realpath')
-# have_header('sys/utime.h')
-# create_header
-# create_makefile('foo')
-#
-# The above script would generate the following extconf.h file:
-#
-# #ifndef EXTCONF_H
-# #define EXTCONF_H
-# #define HAVE_REALPATH 1
-# #define HAVE_SYS_UTIME_H 1
-# #endif
-#
-# Given that the create_header method generates a file based on definitions
-# set earlier in your extconf.rb file, you will probably want to make this
-# one of the last methods you call in your script.
-#
-def create_header(header = "extconf.h")
- message "creating %s\n", header
- sym = header.tr("a-z./\055", "A-Z___")
- hdr = ["#ifndef #{sym}\n#define #{sym}\n"]
- for line in $defs
- case line
- when /^-D([^=]+)(?:=(.*))?/
- hdr << "#define #$1 #{$2 ? Shellwords.shellwords($2)[0] : 1}\n"
- when /^-U(.*)/
- hdr << "#undef #$1\n"
- end
- end
- hdr << "#endif\n"
- hdr = hdr.join
- unless (IO.read(header) == hdr rescue false)
- open(header, "w") do |hfile|
- hfile.write(hdr)
- end
- end
- $extconf_h = header
-end
+ if dir = with_config(target + "-dir", (idefault unless ldefault))
+ defaults = Array === dir ? dir : dir.split(File::PATH_SEPARATOR)
+ idefault = ldefault = nil
+ end
-# Sets a +target+ name that the user can then use to configure various 'with'
-# options with on the command line by using that name. For example, if the
-# target is set to "foo", then the user could use the --with-foo-dir command
-# line option.
-#
-# You may pass along additional 'include' or 'lib' defaults via the +idefault+
-# and +ldefault+ parameters, respectively.
-#
-# Note that dir_config only adds to the list of places to search for libraries
-# and include files. It does not link the libraries into your application.
-#
-def dir_config(target, idefault=nil, ldefault=nil)
- if dir = with_config(target + "-dir", (idefault unless ldefault))
- defaults = Array === dir ? dir : dir.split(File::PATH_SEPARATOR)
- idefault = ldefault = nil
- end
-
- idir = with_config(target + "-include", idefault)
- $arg_config.last[1] ||= "${#{target}-dir}/include"
- ldir = with_config(target + "-lib", ldefault)
- $arg_config.last[1] ||= "${#{target}-dir}/lib"
-
- idirs = idir ? Array === idir ? idir : idir.split(File::PATH_SEPARATOR) : []
- if defaults
- idirs.concat(defaults.collect {|d| d + "/include"})
- idir = ([idir] + idirs).compact.join(File::PATH_SEPARATOR)
- end
- unless idirs.empty?
- idirs.collect! {|d| "-I" + d}
- idirs -= Shellwords.shellwords($CPPFLAGS)
+ idir = with_config(target + "-include", idefault)
+ if conf = $arg_config.assoc("--with-#{target}-include")
+ conf[1] ||= "${#{target}-dir}/include"
+ end
+ ldir = with_config(target + "-lib", ldefault)
+ if conf = $arg_config.assoc("--with-#{target}-lib")
+ conf[1] ||= "${#{target}-dir}/#{_libdir_basename}"
+ end
+
+ idirs = idir ? Array === idir ? idir.dup : idir.split(File::PATH_SEPARATOR) : []
+ if defaults
+ idirs.concat(defaults.collect {|d| d + "/include"})
+ idir = ([idir] + idirs).compact.join(File::PATH_SEPARATOR)
+ end
unless idirs.empty?
- $CPPFLAGS = (idirs.quote << $CPPFLAGS).join(" ")
+ idirs.collect! {|d| "-I" + d}
+ idirs -= Shellwords.shellwords($CPPFLAGS)
+ unless idirs.empty?
+ $CPPFLAGS = (idirs.quote << $CPPFLAGS).join(" ")
+ end
end
- end
- ldirs = ldir ? Array === ldir ? ldir : ldir.split(File::PATH_SEPARATOR) : []
- if defaults
- ldirs.concat(defaults.collect {|d| d + "/lib"})
- ldir = ([ldir] + ldirs).compact.join(File::PATH_SEPARATOR)
- end
- $LIBPATH = ldirs | $LIBPATH
+ ldirs = ldir ? Array === ldir ? ldir.dup : ldir.split(File::PATH_SEPARATOR) : []
+ if defaults
+ ldirs.concat(defaults.collect {|d| "#{d}/#{_libdir_basename}"})
+ ldir = ([ldir] + ldirs).compact.join(File::PATH_SEPARATOR)
+ end
+ $LIBPATH = ldirs | $LIBPATH
+
+ $config_dirs[key] = [idir, ldir]
+ end
+
+ # Returns compile/link information about an installed library in a tuple of <code>[cflags,
+ # ldflags, libs]</code>, by using the command found first in the following commands:
+ #
+ # 1. If <code>--with-{pkg}-config={command}</code> is given via
+ # command line option: <code>{command} {options}</code>
+ #
+ # 2. <code>{pkg}-config {options}</code>
+ #
+ # 3. <code>pkg-config {options} {pkg}</code>
+ #
+ # Where +options+ is the option name without dashes, for instance <code>"cflags"</code> for the
+ # <code>--cflags</code> flag.
+ #
+ # The values obtained are appended to <code>$INCFLAGS</code>, <code>$CFLAGS</code>,
+ # <code>$LDFLAGS</code> and <code>$libs</code>.
+ #
+ # If one or more <code>options</code> argument is given, the config command is
+ # invoked with the options and a stripped output string is returned without
+ # modifying any of the global values mentioned above.
+ def pkg_config(pkg, *options)
+ fmt = "not found"
+ def fmt.%(x)
+ x ? x.inspect : self
+ end
- [idir, ldir]
-end
+ checking_for "pkg-config for #{pkg}", fmt do
+ _, ldir = dir_config(pkg)
+ if ldir
+ pkg_config_path = "#{ldir}/pkgconfig"
+ if File.directory?(pkg_config_path)
+ Logging.message("PKG_CONFIG_PATH = %s\n", pkg_config_path)
+ envs = ["PKG_CONFIG_PATH"=>[pkg_config_path, ENV["PKG_CONFIG_PATH"]].compact.join(File::PATH_SEPARATOR)]
+ end
+ end
+ if pkgconfig = with_config("#{pkg}-config") and find_executable0(pkgconfig)
+ # if and only if package specific config command is given
+ elsif ($PKGCONFIG ||=
+ (pkgconfig = with_config("pkg-config") {config_string("PKG_CONFIG") || ENV["PKG_CONFIG"] || "pkg-config"}) &&
+ find_executable0(pkgconfig) && pkgconfig) and
+ xsystem([*envs, $PKGCONFIG, "--exists", pkg])
+ # default to pkg-config command
+ pkgconfig = $PKGCONFIG
+ args = [pkg]
+ elsif find_executable0(pkgconfig = "#{pkg}-config")
+ # default to package specific config command, as a last resort.
+ else
+ pkgconfig = nil
+ end
+ if pkgconfig
+ get = proc {|opts|
+ opts = Array(opts).map { |o| "--#{o}" }
+ opts = xpopen([*envs, pkgconfig, *opts, *args], err:[:child, :out], &:read)
+ Logging.open {puts opts.each_line.map{|s|"=> #{s.inspect}"}}
+ if $?.success?
+ opts = opts.strip
+ libarg, libpath = LIBARG, LIBPATHFLAG.strip
+ opts = opts.shellsplit.map { |s|
+ if s.start_with?('-l')
+ libarg % s[2..]
+ elsif s.start_with?('-L')
+ libpath % s[2..]
+ else
+ s
+ end
+ }.quote.join(" ")
+ opts
+ end
+ }
+ end
+ orig_ldflags = $LDFLAGS
+ if get and !options.empty?
+ get[options]
+ elsif get and try_ldflags(ldflags = get['libs'])
+ if incflags = get['cflags-only-I']
+ $INCFLAGS << " " << incflags
+ cflags = get['cflags-only-other']
+ else
+ cflags = get['cflags']
+ end
+ libs = get['libs-only-l']
+ if cflags
+ $CFLAGS += " " << cflags
+ $CXXFLAGS += " " << cflags
+ end
+ if libs
+ ldflags = (Shellwords.shellwords(ldflags) - Shellwords.shellwords(libs)).quote.join(" ")
+ else
+ libs, ldflags = Shellwords.shellwords(ldflags).partition {|s| s =~ /-l([^ ]+)/ }.map {|l|l.quote.join(" ")}
+ end
+ $libs += " " << libs
-# :stopdoc:
-
-# Handles meta information about installed libraries. Uses your platform's
-# pkg-config program if it has one.
-def pkg_config(pkg)
- if pkgconfig = with_config("#{pkg}-config") and find_executable0(pkgconfig)
- # iff package specific config command is given
- get = proc {|opt| `#{pkgconfig} --#{opt}`.chomp}
- elsif ($PKGCONFIG ||=
- (pkgconfig = with_config("pkg-config", ("pkg-config" unless CROSS_COMPILING))) &&
- find_executable0(pkgconfig) && pkgconfig) and
- system("#{$PKGCONFIG} --exists #{pkg}")
- # default to pkg-config command
- get = proc {|opt| `#{$PKGCONFIG} --#{opt} #{pkg}`.chomp}
- elsif find_executable0(pkgconfig = "#{pkg}-config")
- # default to package specific config command, as a last resort.
- get = proc {|opt| `#{pkgconfig} --#{opt}`.chomp}
- end
- if get
- cflags = get['cflags']
- ldflags = get['libs']
- libs = get['libs-only-l']
- ldflags = (Shellwords.shellwords(ldflags) - Shellwords.shellwords(libs)).quote.join(" ")
- $CFLAGS += " " << cflags
- $LDFLAGS += " " << ldflags
- $libs += " " << libs
- Logging::message "package configuration for %s\n", pkg
- Logging::message "cflags: %s\nldflags: %s\nlibs: %s\n\n",
- cflags, ldflags, libs
- [cflags, ldflags, libs]
- else
- Logging::message "package configuration for %s is not found\n", pkg
- nil
+ $LDFLAGS = [orig_ldflags, ldflags].join(' ')
+ Logging::message "package configuration for %s\n", pkg
+ Logging::message "incflags: %s\ncflags: %s\nldflags: %s\nlibs: %s\n\n",
+ incflags, cflags, ldflags, libs
+ [[incflags, cflags].join(' '), ldflags, libs]
+ else
+ Logging::message "package configuration for %s is not found\n", pkg
+ nil
+ end
+ end
end
-end
-def with_destdir(dir)
- dir = dir.sub($dest_prefix_pattern, '')
- /\A\$[\(\{]/ =~ dir ? dir : "$(DESTDIR)"+dir
-end
+ # :stopdoc:
-# Converts forward slashes to backslashes. Aimed at MS Windows.
-#
-# Internal use only.
-#
-def winsep(s)
- s.tr('/', '\\')
-end
+ def with_destdir(dir)
+ dir = dir.sub($dest_prefix_pattern, '')
+ /\A\$[\(\{]/ =~ dir ? dir : "$(DESTDIR)"+dir
+ end
+
+ # Converts forward slashes to backslashes. Aimed at MS Windows.
+ #
+ # Internal use only.
+ #
+ def winsep(s)
+ s.tr('/', '\\')
+ end
-def configuration(srcdir)
- mk = []
- vpath = $VPATH.dup
+ # Converts native path to format acceptable in Makefile
+ #
+ # Internal use only.
+ #
if !CROSS_COMPILING
case CONFIG['build_os']
- when 'cygwin'
+ when 'mingw32'
+ def mkintpath(path)
+ # mingw uses make from msys and it needs special care
+ # converts from C:\some\path to /C/some/path
+ path = path.dup
+ path.tr!('\\', '/')
+ path.sub!(/\A([A-Za-z]):(?=\/)/, '/\1')
+ path
+ end
+ when 'cygwin', 'msys'
if CONFIG['target_os'] != 'cygwin'
- vpath = vpath.map {|p| p.sub(/.*/, '$(shell cygpath -u \&)')}
+ def mkintpath(path)
+ IO.popen(["cygpath", "-u", path], &:read).chomp
+ end
end
- when 'mingw32'
- CONFIG['PATH_SEPARATOR'] = ';'
end
end
- CONFIG["hdrdir"] ||= $hdrdir
- mk << %{
+ unless method_defined?(:mkintpath)
+ def mkintpath(path)
+ path
+ end
+ end
+
+ def configuration(srcdir)
+ mk = []
+ verbose = with_config('verbose') ? "1" : (CONFIG['MKMF_VERBOSE'] || "0")
+ vpath = $VPATH.dup
+ CONFIG["hdrdir"] ||= $hdrdir
+ mk << %{
SHELL = /bin/sh
+# V=0 quiet, V=1 verbose. other values don't work.
+V = #{verbose}
+V0 = $(V:0=)
+Q1 = $(V:1=)
+Q = $(Q1:0=@)
+ECHO1 = $(V:1=@ #{CONFIG['NULLCMD']})
+ECHO = $(ECHO1:0=@ echo)
+NULLCMD = #{CONFIG['NULLCMD']}
+
#### Start of system configuration section. ####
-#{
-if $extmk
- "top_srcdir = " + $top_srcdir.sub(%r"\A#{Regexp.quote($topdir)}/", "$(topdir)/")
-end
-}
-srcdir = #{srcdir.gsub(/\$\((srcdir)\)|\$\{(srcdir)\}/) {CONFIG[$1||$2]}.quote}
-topdir = #{($extmk ? CONFIG["topdir"] : $topdir).quote}
-hdrdir = #{CONFIG["hdrdir"].quote}
-arch_hdrdir = #{$arch_hdrdir}
+#{"top_srcdir = " + $top_srcdir.sub(%r"\A#{Regexp.quote($topdir)}/", "$(topdir)/") if $extmk}
+srcdir = #{srcdir.gsub(/\$\((srcdir)\)|\$\{(srcdir)\}/) {mkintpath(CONFIG[$1||$2]).unspace}}
+topdir = #{mkintpath(topdir = $extmk ? CONFIG["topdir"] : $topdir).unspace}
+hdrdir = #{(hdrdir = CONFIG["hdrdir"]) == topdir ? "$(topdir)" : mkintpath(hdrdir).unspace}
+arch_hdrdir = #{mkintpath($arch_hdrdir).unspace}
+PATH_SEPARATOR = #{CONFIG['PATH_SEPARATOR']}
VPATH = #{vpath.join(CONFIG['PATH_SEPARATOR'])}
}
- if $extmk
- mk << "RUBYLIB = -\nRUBYOPT = -r$(top_srcdir)/ext/purelib.rb\n"
- end
- if destdir = CONFIG["prefix"][$dest_prefix_pattern, 1]
- mk << "\nDESTDIR = #{destdir}\n"
- end
- CONFIG.each do |key, var|
- next unless /prefix$/ =~ key
- mk << "#{key} = #{with_destdir(var)}\n"
- end
- CONFIG.each do |key, var|
- next if /^abs_/ =~ key
- next if /^(?:src|top|hdr)dir$/ =~ key
- next unless /dir$/ =~ key
- mk << "#{key} = #{with_destdir(var)}\n"
- end
- if !$extmk and !$configure_args.has_key?('--ruby') and
- sep = config_string('BUILD_FILE_SEPARATOR')
- sep = ":/=#{sep}"
- else
- sep = ""
- end
- extconf_h = $extconf_h ? "-DRUBY_EXTCONF_H=\\\"$(RUBY_EXTCONF_H)\\\" " : $defs.join(" ") << " "
- mk << %{
+ if $extmk
+ mk << "RUBYLIB =\n""RUBYOPT = -\n"
+ end
+ prefix = mkintpath(CONFIG["prefix"])
+ if destdir = prefix[$dest_prefix_pattern, 1]
+ mk << "\nDESTDIR = #{destdir}\n"
+ prefix = prefix[destdir.size..-1]
+ end
+ mk << "prefix = #{with_destdir(prefix).unspace}\n"
+ CONFIG.each do |key, var|
+ mk << "#{key} = #{with_destdir(mkintpath(var)).unspace}\n" if /.prefix$/ =~ key
+ end
+ CONFIG.each do |key, var|
+ next if /^abs_/ =~ key
+ next if /^(?:src|top(?:_src)?|build|hdr)dir$/ =~ key
+ next unless /dir$/ =~ key
+ mk << "#{key} = #{with_destdir(var)}\n"
+ end
+ if !$extmk and !$configure_args.has_key?('--ruby') and
+ sep = config_string('BUILD_FILE_SEPARATOR')
+ sep = ":/=#{sep}"
+ else
+ sep = ""
+ end
+ possible_command = (proc {|s| s if /top_srcdir|tooldir/ !~ s} unless $extmk)
+ extconf_h = $extconf_h ? "-DRUBY_EXTCONF_H=\\\"$(RUBY_EXTCONF_H)\\\" " : $defs.join(" ") << " "
+ headers = %w[
+ $(hdrdir)/ruby.h
+ $(hdrdir)/ruby/backward.h
+ $(hdrdir)/ruby/ruby.h
+ $(hdrdir)/ruby/defines.h
+ $(hdrdir)/ruby/missing.h
+ $(hdrdir)/ruby/intern.h
+ $(hdrdir)/ruby/st.h
+ $(hdrdir)/ruby/subst.h
+ ]
+ headers += $headers
+ if RULE_SUBST
+ headers.each {|h| h.sub!(/.*/, &RULE_SUBST.method(:%))}
+ end
+ headers << $config_h
+ headers << '$(RUBY_EXTCONF_H)' if $extconf_h
+ mk << %{
+
+CC_WRAPPER = #{CONFIG['CC_WRAPPER']}
CC = #{CONFIG['CC']}
CXX = #{CONFIG['CXX']}
LIBRUBY = #{CONFIG['LIBRUBY']}
LIBRUBY_A = #{CONFIG['LIBRUBY_A']}
LIBRUBYARG_SHARED = #$LIBRUBYARG_SHARED
LIBRUBYARG_STATIC = #$LIBRUBYARG_STATIC
-OUTFLAG = #{OUTFLAG}
-COUTFLAG = #{COUTFLAG}
+empty =
+OUTFLAG = #{OUTFLAG}$(empty)
+COUTFLAG = #{COUTFLAG}$(empty)
+CSRCFLAG = #{CSRCFLAG}$(empty)
RUBY_EXTCONF_H = #{$extconf_h}
cflags = #{CONFIG['cflags']}
+cxxflags = #{CONFIG['cxxflags']}
optflags = #{CONFIG['optflags']}
debugflags = #{CONFIG['debugflags']}
-warnflags = #{CONFIG['warnflags']}
-CFLAGS = #{$static ? '' : CONFIG['CCDLFLAGS']} #$CFLAGS #$ARCH_FLAG
+warnflags = #{$warnflags}
+cppflags = #{CONFIG['cppflags']}
+CCDLFLAGS = #{$static ? '' : CONFIG['CCDLFLAGS']}
+CFLAGS = $(CCDLFLAGS) #$CFLAGS $(ARCH_FLAG)
INCFLAGS = -I. #$INCFLAGS
DEFS = #{CONFIG['DEFS']}
CPPFLAGS = #{extconf_h}#{$CPPFLAGS}
-CXXFLAGS = $(CFLAGS) #{CONFIG['CXXFLAGS']}
+CXXFLAGS = $(CCDLFLAGS) #$CXXFLAGS $(ARCH_FLAG)
ldflags = #{$LDFLAGS}
-dldflags = #{$DLDFLAGS}
-archflag = #{$ARCH_FLAG}
-DLDFLAGS = $(ldflags) $(dldflags) $(archflag)
+dldflags = #{$DLDFLAGS} #{CONFIG['EXTDLDFLAGS']}
+ARCH_FLAG = #{$ARCH_FLAG}
+DLDFLAGS = $(ldflags) $(dldflags) $(ARCH_FLAG)
LDSHARED = #{CONFIG['LDSHARED']}
LDSHAREDXX = #{config_string('LDSHAREDXX') || '$(LDSHARED)'}
+POSTLINK = #{config_string('POSTLINK', RbConfig::CONFIG)}
AR = #{CONFIG['AR']}
+LD = #{CONFIG['LD']}
EXEEXT = #{CONFIG['EXEEXT']}
-RUBY_INSTALL_NAME = #{CONFIG['RUBY_INSTALL_NAME']}
-RUBY_SO_NAME = #{CONFIG['RUBY_SO_NAME']}
+}
+ CONFIG.each do |key, val|
+ mk << "#{key} = #{val}\n" if /^RUBY.*NAME/ =~ key
+ end
+ mk << %{
arch = #{CONFIG['arch']}
sitearch = #{CONFIG['sitearch']}
ruby_version = #{RbConfig::CONFIG['ruby_version']}
-ruby = #{$ruby}
+ruby = #{$ruby.sub(%r[\A#{Regexp.quote(RbConfig::CONFIG['bindir'])}(?=/|\z)]) {'$(bindir)'}}
RUBY = $(ruby#{sep})
-RM = #{config_string('RM') || '$(RUBY) -run -e rm -- -f'}
-RM_RF = #{'$(RUBY) -run -e rm -- -rf'}
-MAKEDIRS = #{config_string('MAKEDIRS') || '@$(RUBY) -run -e mkdir -- -p'}
-INSTALL = #{config_string('INSTALL') || '@$(RUBY) -run -e install -- -vp'}
+BUILTRUBY = #{if defined?($builtruby) && $builtruby
+ $builtruby
+ else
+ File.join('$(bindir)', CONFIG["RUBY_INSTALL_NAME"] + CONFIG['EXEEXT'])
+ end}
+ruby_headers = #{headers.join(' ')}
+
+RM = #{config_string('RM', &possible_command) || '$(RUBY) -run -e rm -- -f'}
+RM_RF = #{config_string('RMALL', &possible_command) || '$(RUBY) -run -e rm -- -rf'}
+RMDIRS = #{config_string('RMDIRS', &possible_command) || '$(RUBY) -run -e rmdir -- -p'}
+MAKEDIRS = #{config_string('MAKEDIRS', &possible_command) || '@$(RUBY) -run -e mkdir -- -p'}
+INSTALL = #{config_string('INSTALL', &possible_command) || '@$(RUBY) -run -e install -- -vp'}
INSTALL_PROG = #{config_string('INSTALL_PROG') || '$(INSTALL) -m 0755'}
INSTALL_DATA = #{config_string('INSTALL_DATA') || '$(INSTALL) -m 0644'}
-COPY = #{config_string('CP') || '@$(RUBY) -run -e cp -- -v'}
+COPY = #{config_string('CP', &possible_command) || '@$(RUBY) -run -e cp -- -v'}
+TOUCH = exit >
#### End of system configuration section. ####
preload = #{defined?($preload) && $preload ? $preload.join(' ') : ''}
}
- if $nmake == ?b
- mk.each do |x|
- x.gsub!(/^(MAKEDIRS|INSTALL_(?:PROG|DATA))+\s*=.*\n/) do
- "!ifndef " + $1 + "\n" +
- $& +
- "!endif\n"
- end
- end
+ mk
end
- mk
-end
-# :startdoc:
-def dummy_makefile(srcdir)
- configuration(srcdir) << <<RULES << CLEANINGS
+ def timestamp_file(name, target_prefix = nil)
+ pat = {}
+ name = '$(RUBYARCHDIR)' if name == '$(TARGET_SO_DIR)'
+ install_dirs.each do |n, d|
+ pat[n] = $` if /\$\(target_prefix\)\z/ =~ d
+ end
+ name = name.gsub(/\$\((#{pat.keys.join("|")})\)/) {pat[$1]+target_prefix}
+ name.sub!(/(\$\((?:site)?arch\))\/*/, '')
+ arch = $1 || ''
+ name.chomp!('/')
+ name = name.gsub(/(\$[({]|[})])|(\/+)|[^-.\w]+/) {$1 ? "" : $2 ? ".-." : "_"}
+ File.join("$(TIMESTAMP_DIR)", arch, "#{name.sub(/\A(?=.)/, '.')}.time")
+ end
+ # :startdoc:
+
+ # Creates a stub Makefile.
+ #
+ def dummy_makefile(srcdir)
+ configuration(srcdir) << <<RULES << CLEANINGS
CLEANFILES = #{$cleanfiles.join(' ')}
DISTCLEANFILES = #{$distcleanfiles.join(' ')}
all install static install-so install-rb: Makefile
+ @$(NULLCMD)
+.PHONY: all install static install-so install-rb
+.PHONY: clean clean-so clean-static clean-rb
RULES
-end
+ end
-def depend_rules(depend)
- suffixes = []
- depout = []
- cont = implicit = nil
- impconv = proc do
- COMPILE_RULES.each {|rule| depout << (rule % implicit[0]) << implicit[1]}
- implicit = nil
- end
- ruleconv = proc do |line|
- if implicit
- if /\A\t/ =~ line
- implicit[1] << line
- next
+ def each_compile_rules # :nodoc:
+ vpath_splat = /\$\(\*VPATH\*\)/
+ COMPILE_RULES.each do |rule|
+ if vpath_splat =~ rule
+ $VPATH.each do |path|
+ yield rule.sub(vpath_splat) {path}
+ end
else
- impconv[]
- end
- end
- if m = /\A\.(\w+)\.(\w+)(?:\s*:)/.match(line)
- suffixes << m[1] << m[2]
- implicit = [[m[1], m[2]], [m.post_match]]
- next
- elsif RULE_SUBST and /\A(?!\s*\w+\s*=)[$\w][^#]*:/ =~ line
- line.gsub!(%r"(\s)(?!\.)([^$(){}+=:\s\/\\,]+)(?=\s|\z)") {$1 + RULE_SUBST % $2}
- end
- depout << line
- end
- depend.each_line do |line|
- line.gsub!(/\.o\b/, ".#{$OBJEXT}")
- line.gsub!(/\$\((?:hdr|top)dir\)\/config.h/, $config_h)
- line.gsub!(%r"\$\(hdrdir\)/(?!ruby(?![^:;/\s]))(?=[-\w]+\.h)", '\&ruby/')
- if $nmake && /\A\s*\$\(RM|COPY\)/ =~ line
- line.gsub!(%r"[-\w\./]{2,}"){$&.tr("/", "\\")}
- line.gsub!(/(\$\((?!RM|COPY)[^:)]+)(?=\))/, '\1:/=\\')
- end
- if /(?:^|[^\\])(?:\\\\)*\\$/ =~ line
- (cont ||= []) << line
- next
- elsif cont
- line = (cont << line).join
- cont = nil
- end
- ruleconv.call(line)
- end
- if cont
- ruleconv.call(cont.join)
- elsif implicit
- impconv.call
- end
- unless suffixes.empty?
- depout.unshift(".SUFFIXES: ." + suffixes.uniq.join(" .") + "\n\n")
- end
- depout.unshift("$(OBJS): $(RUBY_EXTCONF_H)\n\n") if $extconf_h
- depout.flatten!
- depout
-end
-
-# Generates the Makefile for your extension, passing along any options and
-# preprocessor constants that you may have generated through other methods.
-#
-# The +target+ name should correspond the name of the global function name
-# defined within your C extension, minus the 'Init_'. For example, if your
-# C extension is defined as 'Init_foo', then your target would simply be 'foo'.
-#
-# If any '/' characters are present in the target name, only the last name
-# is interpreted as the target name, and the rest are considered toplevel
-# directory names, and the generated Makefile will be altered accordingly to
-# follow that directory structure.
-#
-# For example, if you pass 'test/foo' as a target name, your extension will
-# be installed under the 'test' directory. This means that in order to
-# load the file within a Ruby program later, that directory structure will
-# have to be followed, e.g. "require 'test/foo'".
-#
-# The +srcprefix+ should be used when your source files are not in the same
-# directory as your build script. This will not only eliminate the need for
-# you to manually copy the source files into the same directory as your build
-# script, but it also sets the proper +target_prefix+ in the generated
-# Makefile.
-#
-# Setting the +target_prefix+ will, in turn, install the generated binary in
-# a directory under your Config::CONFIG['sitearchdir'] that mimics your local
-# filesystem when you run 'make install'.
-#
-# For example, given the following file tree:
-#
-# ext/
-# extconf.rb
-# test/
-# foo.c
-#
-# And given the following code:
-#
-# create_makefile('test/foo', 'test')
-#
-# That will set the +target_prefix+ in the generated Makefile to 'test'. That,
-# in turn, will create the following file tree when installed via the
-# 'make install' command:
-#
-# /path/to/ruby/sitearchdir/test/foo.so
-#
-# It is recommended that you use this approach to generate your makefiles,
-# instead of copying files around manually, because some third party
-# libraries may depend on the +target_prefix+ being set properly.
-#
-# The +srcprefix+ argument can be used to override the default source
-# directory, i.e. the current directory . It is included as part of the VPATH
-# and added to the list of INCFLAGS.
-#
-def create_makefile(target, srcprefix = nil)
- $target = target
- libpath = $DEFLIBPATH|$LIBPATH
- message "creating Makefile\n"
- rm_f "conftest*"
- if CONFIG["DLEXT"] == $OBJEXT
- for lib in libs = $libs.split
- lib.sub!(/-l(.*)/, %%"lib\\1.#{$LIBEXT}"%)
- end
- $defs.push(format("-DEXTLIB='%s'", libs.join(",")))
- end
-
- if target.include?('/')
- target_prefix, target = File.split(target)
- target_prefix[0,0] = '/'
- else
- target_prefix = ""
+ yield rule
+ end
+ end
end
- srcprefix ||= '$(srcdir)'
- RbConfig::expand(srcdir = srcprefix.dup)
-
- if not $objs
- $objs = []
- srcs = Dir[File.join(srcdir, "*.{#{SRC_EXT.join(%q{,})}}")]
- for f in srcs
- obj = File.basename(f, ".*") << ".o"
- $objs.push(obj) unless $objs.index(obj)
+ # Processes the data contents of the "depend" file. Each line of this file
+ # is expected to be a file name.
+ #
+ # Returns the output of findings, in Makefile format.
+ #
+ def depend_rules(depend)
+ suffixes = []
+ depout = []
+ cont = implicit = nil
+ impconv = proc do
+ each_compile_rules {|rule| depout << (rule % implicit[0]) << implicit[1]}
+ implicit = nil
+ end
+ ruleconv = proc do |line|
+ if implicit
+ if /\A\t/ =~ line
+ implicit[1] << line
+ next
+ else
+ impconv[]
+ end
+ end
+ if m = /\A\.(\w+)\.(\w+)(?:\s*:)/.match(line)
+ suffixes << m[1] << m[2]
+ implicit = [[m[1], m[2]], [m.post_match]]
+ next
+ elsif RULE_SUBST and /\A(?!\s*\w+\s*=)[$\w][^#]*:/ =~ line
+ line.sub!(/\s*\#.*$/, '')
+ comment = $&
+ line.gsub!(%r"(\s)(?!\.)([^$(){}+=:\s\\,]+)(?=\s|\z)") {$1 + RULE_SUBST % $2}
+ line = line.chomp + comment + "\n" if comment
+ end
+ depout << line
+ end
+ depend.each_line do |line|
+ line.gsub!(/\.o\b/, ".#{$OBJEXT}")
+ line.gsub!(/\{\$\(VPATH\)\}/, "") unless $nmake
+ line.gsub!(/\$\((?:hdr|top)dir\)\/config.h/, $config_h)
+ if $nmake && /\A\s*\$\(RM|COPY\)/ =~ line
+ line.gsub!(%r"[-\w\./]{2,}"){$&.tr("/", "\\")}
+ line.gsub!(/(\$\((?!RM|COPY)[^:)]+)(?=\))/, '\1:/=\\')
+ end
+ if /(?:^|[^\\])(?:\\\\)*\\$/ =~ line
+ (cont ||= []) << line
+ next
+ elsif cont
+ line = (cont << line).join
+ cont = nil
+ end
+ ruleconv.call(line)
+ end
+ if cont
+ ruleconv.call(cont.join)
+ elsif implicit
+ impconv.call
+ end
+ unless suffixes.empty?
+ depout.unshift(".SUFFIXES: ." + suffixes.uniq.join(" .") + "\n\n")
+ end
+ if $extconf_h
+ depout.unshift("$(OBJS): $(RUBY_EXTCONF_H)\n\n")
+ depout.unshift("$(OBJS): $(hdrdir)/ruby/win32.h\n\n") if $mswin or $mingw
+ end
+ depout.flatten!
+ depout
+ end
+
+ # Generates the Makefile for your extension, passing along any options and
+ # preprocessor constants that you may have generated through other methods.
+ #
+ # The +target+ name should correspond the name of the global function name
+ # defined within your C extension, minus the +Init_+. For example, if your
+ # C extension is defined as +Init_foo+, then your target would simply be
+ # "foo".
+ #
+ # If any "/" characters are present in the target name, only the last name
+ # is interpreted as the target name, and the rest are considered toplevel
+ # directory names, and the generated Makefile will be altered accordingly to
+ # follow that directory structure.
+ #
+ # For example, if you pass "test/foo" as a target name, your extension will
+ # be installed under the "test" directory. This means that in order to
+ # load the file within a Ruby program later, that directory structure will
+ # have to be followed, e.g. <code>require 'test/foo'</code>.
+ #
+ # The +srcprefix+ should be used when your source files are not in the same
+ # directory as your build script. This will not only eliminate the need for
+ # you to manually copy the source files into the same directory as your
+ # build script, but it also sets the proper +target_prefix+ in the generated
+ # Makefile.
+ #
+ # Setting the +target_prefix+ will, in turn, install the generated binary in
+ # a directory under your <code>RbConfig::CONFIG['sitearchdir']</code> that
+ # mimics your local filesystem when you run <code>make install</code>.
+ #
+ # For example, given the following file tree:
+ #
+ # ext/
+ # extconf.rb
+ # test/
+ # foo.c
+ #
+ # And given the following code:
+ #
+ # create_makefile('test/foo', 'test')
+ #
+ # That will set the +target_prefix+ in the generated Makefile to "test".
+ # That, in turn, will create the following file tree when installed via the
+ # <code>make install</code> command:
+ #
+ # /path/to/ruby/sitearchdir/test/foo.so
+ #
+ # It is recommended that you use this approach to generate your makefiles,
+ # instead of copying files around manually, because some third party
+ # libraries may depend on the +target_prefix+ being set properly.
+ #
+ # The +srcprefix+ argument can be used to override the default source
+ # directory, i.e. the current directory. It is included as part of the
+ # +VPATH+ and added to the list of +INCFLAGS+.
+ #
+ # Yields the configuration part of the makefile to be generated, as an array
+ # of strings, if the block is given. The returned value will be used the
+ # new configuration part.
+ #
+ # create_makefile('foo') {|conf|
+ # [
+ # *conf,
+ # "MACRO_YOU_NEED = something",
+ # ]
+ # }
+ #
+ # If "depend" file exist in the source directory, that content will be
+ # included in the generated makefile, with formatted by depend_rules method.
+ def create_makefile(target, srcprefix = nil)
+ $target = target
+ libpath = $DEFLIBPATH|$LIBPATH
+ message "creating Makefile\n"
+ MakeMakefile.rm_f "#{CONFTEST}*"
+ if CONFIG["DLEXT"] == $OBJEXT
+ for lib in libs = $libs.split(' ')
+ lib.sub!(/-l(.*)/, %%"lib\\1.#{$LIBEXT}"%)
+ end
+ $defs.push(format("-DEXTLIB='%s'", libs.join(",")))
end
- elsif !(srcs = $srcs)
- srcs = $objs.collect {|o| o.sub(/\.o\z/, '.c')}
- end
- $srcs = srcs
- for i in $objs
- i.sub!(/\.o\z/, ".#{$OBJEXT}")
- end
- $objs = $objs.join(" ")
- target = nil if $objs == ""
+ if target.include?('/')
+ target_prefix, target = File.split(target)
+ target_prefix[0,0] = '/'
+ else
+ target_prefix = ""
+ end
- if target and EXPORT_PREFIX
- if File.exist?(File.join(srcdir, target + '.def'))
- deffile = "$(srcdir)/$(TARGET).def"
- unless EXPORT_PREFIX.empty?
- makedef = %{-pe "$_.sub!(/^(?=\\w)/,'#{EXPORT_PREFIX}') unless 1../^EXPORTS$/i"}
+ srcprefix ||= "$(srcdir)/#{srcprefix}".chomp('/')
+ RbConfig.expand(srcdir = srcprefix.dup)
+
+ ext = ".#{$OBJEXT}"
+ orig_srcs = Dir[File.join(srcdir, "*.{#{SRC_EXT.join(%q{,})}}")]
+ if not $objs
+ srcs = $srcs || orig_srcs
+ $objs = []
+ objs = srcs.inject(Hash.new {[]}) {|h, f|
+ h.key?(o = File.basename(f, ".*") << ext) or $objs << o
+ h[o] <<= f
+ h
+ }
+ unless objs.delete_if {|b, f| f.size == 1}.empty?
+ dups = objs.map {|b, f|
+ "#{b[/.*\./]}{#{f.collect {|n| n[/([^.]+)\z/]}.join(',')}}"
+ }
+ abort "source files duplication - #{dups.join(", ")}"
end
else
- makedef = %{-e "puts 'EXPORTS', '#{EXPORT_PREFIX}Init_$(TARGET)'"}
+ $objs.collect! {|o| File.basename(o, ".*") << ext} unless $OBJEXT == "o"
+ srcs = $srcs || $objs.collect {|o| o.chomp(ext) << ".c"}
end
- if makedef
- $distcleanfiles << '$(DEFFILE)'
- origdef = deffile
- deffile = "$(TARGET)-$(arch).def"
+ $srcs = srcs
+
+ hdrs = Dir[File.join(srcdir, "*.{#{HDR_EXT.join(%q{,})}}")]
+
+ target = nil if $objs.empty?
+
+ if target and EXPORT_PREFIX
+ if File.exist?(File.join(srcdir, target + '.def'))
+ deffile = "$(srcdir)/$(TARGET).def"
+ unless EXPORT_PREFIX.empty?
+ makedef = %{$(RUBY) -pe "$$_.sub!(/^(?=\\w)/,'#{EXPORT_PREFIX}') unless 1../^EXPORTS$/i" #{deffile}}
+ end
+ else
+ makedef = %{(echo EXPORTS && echo $(TARGET_ENTRY))}
+ end
+ if makedef
+ $cleanfiles << '$(DEFFILE)'
+ origdef = deffile
+ deffile = "$(TARGET)-$(arch).def"
+ end
end
- end
- origdef ||= ''
+ origdef ||= ''
- if $extmk and not $extconf_h
- create_header
- end
+ if $extout and $INSTALLFILES
+ $cleanfiles.concat($INSTALLFILES.collect {|files, dir|File.join(dir, files.delete_prefix('./'))})
+ $distcleandirs.concat($INSTALLFILES.collect {|files, dir| dir})
+ end
+
+ if $extmk and $static
+ $defs << "-DRUBY_EXPORT=1"
+ end
+
+ if $extmk and not $extconf_h
+ create_header
+ end
- libpath = libpathflag(libpath)
+ libpath = libpathflag(libpath)
- dllib = target ? "$(TARGET).#{CONFIG['DLEXT']}" : ""
- staticlib = target ? "$(TARGET).#$LIBEXT" : ""
- mfile = open("Makefile", "wb")
- mfile.print(*configuration(srcprefix))
- mfile.print "
+ dllib = target ? "$(TARGET).#{CONFIG['DLEXT']}" : ""
+ staticlib = target ? "$(TARGET).#$LIBEXT" : ""
+ conf = configuration(srcprefix)
+ conf << "\
libpath = #{($DEFLIBPATH|$LIBPATH).join(" ")}
LIBPATH = #{libpath}
DEFFILE = #{deffile}
CLEANFILES = #{$cleanfiles.join(' ')}
DISTCLEANFILES = #{$distcleanfiles.join(' ')}
+DISTCLEANDIRS = #{$distcleandirs.join(' ')}
-extout = #{$extout}
+extout = #{$extout && $extout.quote}
extout_prefix = #{$extout_prefix}
target_prefix = #{target_prefix}
LOCAL_LIBS = #{$LOCAL_LIBS}
LIBS = #{$LIBRUBYARG} #{$libs} #{$LIBS}
-SRCS = #{srcs.collect(&File.method(:basename)).join(' ')}
-OBJS = #{$objs}
+ORIG_SRCS = #{orig_srcs.collect(&File.method(:basename)).join(' ')}
+SRCS = $(ORIG_SRCS) #{(srcs - orig_srcs).collect(&File.method(:basename)).join(' ')}
+OBJS = #{$objs.join(" ")}
+HDRS = #{hdrs.map{|h| '$(srcdir)/' + File.basename(h)}.join(' ')}
+LOCAL_HDRS = #{$headers.join(' ')}
TARGET = #{target}
+TARGET_NAME = #{target && target[/\A\w+/]}
+TARGET_ENTRY = #{EXPORT_PREFIX || ''}Init_$(TARGET_NAME)
DLLIB = #{dllib}
EXTSTATIC = #{$static || ""}
STATIC_LIB = #{staticlib unless $static.nil?}
-#{!$extout && defined?($installed_list) ? "INSTALLED_LIST = #{$installed_list}\n" : ""}
+#{!$extout && defined?($installed_list) ? %[INSTALLED_LIST = #{$installed_list}\n] : ""}
+TIMESTAMP_DIR = #{$extout && $extmk ? '$(extout)/.timestamp' : '.'}
+" #"
+ # TODO: fixme
+ install_dirs.each {|d| conf << ("%-14s= %s\n" % d) if /^[[:upper:]]/ =~ d[0]}
+ sodir = $extout ? '$(TARGET_SO_DIR)' : '$(RUBYARCHDIR)'
+ n = '$(TARGET_SO_DIR)$(TARGET)'
+ cleanobjs = ["$(OBJS)"]
+ cleanlibs = []
+ if $extmk
+ %w[bc i s].each {|ex| cleanobjs << "$(OBJS:.#{$OBJEXT}=.#{ex})"}
+ end
+ if target
+ config_string('cleanobjs') {|t| cleanobjs << t.gsub(/\$\*/, "$(TARGET)#{deffile ? '-$(arch)': ''}")}
+ cleanlibs << '$(TARGET_SO)'
+ end
+ config_string('cleanlibs') {|t| cleanlibs << t.gsub(/\$\*/) {n}}
+ conf << "\
+TARGET_SO_DIR =#{$extout ? " $(RUBYARCHDIR)/" : ''}
+TARGET_SO = $(TARGET_SO_DIR)$(DLLIB)
+CLEANLIBS = #{cleanlibs.join(' ')}
+CLEANOBJS = #{cleanobjs.join(' ')} *.bak
+TARGET_SO_DIR_TIMESTAMP = #{timestamp_file(sodir, target_prefix)}
" #"
- # TODO: fixme
- install_dirs.each {|d| mfile.print("%-14s= %s\n" % d) if /^[[:upper:]]/ =~ d[0]}
- n = ($extout ? '$(RUBYARCHDIR)/' : '') + '$(TARGET)'
- mfile.print "
-TARGET_SO = #{($extout ? '$(RUBYARCHDIR)/' : '')}$(DLLIB)
-CLEANLIBS = #{n}.#{CONFIG['DLEXT']} #{config_string('cleanlibs') {|t| t.gsub(/\$\*/) {n}}}
-CLEANOBJS = *.#{$OBJEXT} #{config_string('cleanobjs') {|t| t.gsub(/\$\*/, '$(TARGET)')}} *.bak
+ conf = yield(conf) if block_given?
+ mfile = File.open("Makefile", "wb")
+ mfile.puts(conf)
+ mfile.print "
all: #{$extout ? "install" : target ? "$(DLLIB)" : "Makefile"}
-static: $(STATIC_LIB)#{$extout ? " install-rb" : ""}
-"
- mfile.print CLEANINGS
- dirs = []
- mfile.print "install: install-so install-rb\n\n"
- sodir = (dir = "$(RUBYARCHDIR)").dup
- mfile.print("install-so: ")
- if target
- f = "$(DLLIB)"
- dest = "#{dir}/#{f}"
- mfile.puts dir, "install-so: #{dest}"
- unless $extout
- mfile.print "#{dest}: #{f}\n"
- if (sep = config_string('BUILD_FILE_SEPARATOR'))
- f.gsub!("/", sep)
- dir.gsub!("/", sep)
- sep = ":/="+sep
- f.gsub!(/(\$\(\w+)(\))/) {$1+sep+$2}
- f.gsub!(/(\$\{\w+)(\})/) {$1+sep+$2}
- dir.gsub!(/(\$\(\w+)(\))/) {$1+sep+$2}
- dir.gsub!(/(\$\{\w+)(\})/) {$1+sep+$2}
- end
- mfile.print "\t$(INSTALL_PROG) #{f} #{dir}\n"
- if defined?($installed_list)
- mfile.print "\t@echo #{dir}/#{File.basename(f)}>>$(INSTALLED_LIST)\n"
+static: #{$extmk && !$static ? "all" : %[$(STATIC_LIB)#{$extout ? " install-rb" : ""}]}
+.PHONY: all install static install-so install-rb
+.PHONY: clean clean-so clean-static clean-rb
+" #"
+ mfile.print CLEANINGS
+ fsep = config_string('BUILD_FILE_SEPARATOR') {|s| s unless s == "/"}
+ if fsep
+ sep = ":/=#{fsep}"
+ fseprepl = proc {|s|
+ s = s.gsub("/", fsep)
+ s = s.gsub(/(\$\(\w+)(\))/) {$1+sep+$2}
+ s.gsub(/(\$\{\w+)(\})/) {$1+sep+$2}
+ }
+ rsep = ":#{fsep}=/"
+ else
+ fseprepl = proc {|s| s}
+ sep = ""
+ rsep = ""
+ end
+ dirs = []
+ mfile.print "install: install-so install-rb\n\n"
+ dir = sodir.dup
+ mfile.print("install-so: ")
+ if target
+ f = "$(DLLIB)"
+ dest = "$(TARGET_SO)"
+ stamp = '$(TARGET_SO_DIR_TIMESTAMP)'
+ if $extout
+ mfile.puts dest
+ mfile.print "clean-so::\n"
+ mfile.print "\t-$(Q)$(RM) #{fseprepl[dest]} #{fseprepl[stamp]}\n"
+ mfile.print "\t-$(Q)$(RM_RF) #{fseprepl['$(CLEANLIBS)']}\n"
+ mfile.print "\t-$(Q)$(RMDIRS) #{fseprepl[dir]}#{$ignore_error}\n"
+ else
+ mfile.print "#{f} #{stamp}\n"
+ mfile.print "\t$(INSTALL_PROG) #{fseprepl[f]} #{dir}\n"
+ if defined?($installed_list)
+ mfile.print "\t@echo #{dir}/#{File.basename(f)}>>$(INSTALLED_LIST)\n"
+ end
end
+ mfile.print "clean-static::\n"
+ mfile.print "\t-$(Q)$(RM) $(STATIC_LIB)\n"
+ else
+ mfile.puts "Makefile"
end
- else
- mfile.puts "Makefile"
- end
- mfile.print("install-rb: pre-install-rb install-rb-default\n")
- mfile.print("install-rb-default: pre-install-rb-default\n")
- mfile.print("pre-install-rb: Makefile\n")
- mfile.print("pre-install-rb-default: Makefile\n")
- for sfx, i in [["-default", [["lib/**/*.rb", "$(RUBYLIBDIR)", "lib"]]], ["", $INSTALLFILES]]
- files = install_files(mfile, i, nil, srcprefix) or next
- for dir, *files in files
- unless dirs.include?(dir)
- dirs << dir
- mfile.print "pre-install-rb#{sfx}: #{dir}\n"
- end if $nmake
- for f in files
- dest = "#{dir}/#{File.basename(f)}"
- mfile.print("install-rb#{sfx}: #{dest}\n")
- mfile.print("#{dest}: #{f}\n")
- mfile.print("\t$(MAKEDIRS) $(@D)\n") unless $nmake
- mfile.print("\t$(#{$extout ? 'COPY' : 'INSTALL_DATA'}) ")
- sep = config_string('BUILD_FILE_SEPARATOR')
- if sep
- f = f.gsub("/", sep)
- sep = ":/="+sep
- f = f.gsub(/(\$\(\w+)(\))/) {$1+sep+$2}
- f = f.gsub(/(\$\{\w+)(\})/) {$1+sep+$2}
- else
- sep = ""
- end
- mfile.print("#{f} $(@D#{sep})\n")
- if defined?($installed_list) and !$extout
- mfile.print("\t@echo #{dest}>>$(INSTALLED_LIST)\n")
- end
- end
- end
- end
- dirs.unshift(sodir) if target and !dirs.include?(sodir)
- dirs.each {|d| mfile.print "#{d}:\n\t$(MAKEDIRS) $@\n" if $nmake || d == sodir}
-
- mfile.print <<-SITEINSTALL
+ mfile.print("install-rb: pre-install-rb do-install-rb install-rb-default\n")
+ mfile.print("install-rb-default: pre-install-rb-default do-install-rb-default\n")
+ mfile.print("pre-install-rb: Makefile\n")
+ mfile.print("pre-install-rb-default: Makefile\n")
+ mfile.print("do-install-rb:\n")
+ mfile.print("do-install-rb-default:\n")
+ for sfx, i in [["-default", [["lib/**/*.rb", "$(RUBYLIBDIR)", "lib"]]], ["", $INSTALLFILES]]
+ files = install_files(mfile, i, nil, srcprefix) or next
+ for dir, *files in files
+ unless dirs.include?(dir)
+ dirs << dir
+ mfile.print "pre-install-rb#{sfx}: #{timestamp_file(dir, target_prefix)}\n"
+ end
+ for f in files
+ dest = "#{dir}/#{File.basename(f)}"
+ mfile.print("do-install-rb#{sfx}: #{dest}\n")
+ mfile.print("#{dest}: #{f} #{timestamp_file(dir, target_prefix)}\n")
+ mfile.print("\t$(Q) $(#{$extout ? 'COPY' : 'INSTALL_DATA'}) #{f} $@\n")
+ if defined?($installed_list) and !$extout
+ mfile.print("\t@echo #{dest}>>$(INSTALLED_LIST)\n")
+ end
+ if $extout
+ mfile.print("clean-rb#{sfx}::\n")
+ mfile.print("\t-$(Q)$(RM) #{fseprepl[dest]}\n")
+ end
+ end
+ end
+ mfile.print "pre-install-rb#{sfx}:\n"
+ if files.empty?
+ mfile.print("\t@$(NULLCMD)\n")
+ else
+ q = "$(MAKE) -q do-install-rb#{sfx}"
+ if $nmake
+ mfile.print "!if \"$(Q)\" == \"@\"\n\t@#{q} || \\\n!endif\n\t"
+ else
+ mfile.print "\t$(Q1:0=@#{q} || )"
+ end
+ mfile.print "$(ECHO1:0=echo) installing#{sfx.sub(/^-/, " ")} #{target} libraries\n"
+ end
+ if $extout
+ dirs.uniq!
+ unless dirs.empty?
+ mfile.print("clean-rb#{sfx}::\n")
+ for dir in dirs.sort_by {|d| -d.count('/')}
+ stamp = timestamp_file(dir, target_prefix)
+ mfile.print("\t-$(Q)$(RM) #{fseprepl[stamp]}\n")
+ mfile.print("\t-$(Q)$(RMDIRS) #{fseprepl[dir]}#{$ignore_error}\n")
+ end
+ end
+ end
+ end
+ if target and !dirs.include?(sodir)
+ mfile.print "$(TARGET_SO_DIR_TIMESTAMP):\n\t$(Q) $(MAKEDIRS) $(@D) #{sodir}\n\t$(Q) $(TOUCH) $@\n"
+ end
+ dirs.each do |d|
+ t = timestamp_file(d, target_prefix)
+ mfile.print "#{t}:\n\t$(Q) $(MAKEDIRS) $(@D) #{d}\n\t$(Q) $(TOUCH) $@\n"
+ end
+
+ mfile.print <<-SITEINSTALL
site-install: site-install-so site-install-rb
site-install-so: install-so
site-install-rb: install-rb
- SITEINSTALL
+ SITEINSTALL
- return unless target
+ return unless target
- mfile.puts SRC_EXT.collect {|ext| ".path.#{ext} = $(VPATH)"} if $nmake == ?b
- mfile.print ".SUFFIXES: .#{SRC_EXT.join(' .')} .#{$OBJEXT}\n"
- mfile.print "\n"
+ mfile.print ".SUFFIXES: .#{(SRC_EXT + [$OBJEXT, $ASMEXT]).compact.join(' .')}\n"
+ mfile.print "\n"
- CXX_EXT.each do |ext|
- COMPILE_RULES.each do |rule|
- mfile.printf(rule, ext, $OBJEXT)
- mfile.printf("\n\t%s\n\n", COMPILE_CXX)
+ compile_command = "\n\t$(ECHO) compiling $(<#{rsep})\n\t$(Q) %s\n\n"
+ command = compile_command % COMPILE_CXX
+ asm_command = compile_command.sub(/compiling/, 'translating') % ASSEMBLE_CXX
+ CXX_EXT.each do |e|
+ each_compile_rules do |rule|
+ mfile.printf(rule, e, $OBJEXT)
+ mfile.print(command)
+ mfile.printf(rule, e, $ASMEXT)
+ mfile.print(asm_command)
+ end
end
- end
- %w[c].each do |ext|
- COMPILE_RULES.each do |rule|
- mfile.printf(rule, ext, $OBJEXT)
- mfile.printf("\n\t%s\n\n", COMPILE_C)
+ command = compile_command % COMPILE_C
+ asm_command = compile_command.sub(/compiling/, 'translating') % ASSEMBLE_C
+ C_EXT.each do |e|
+ each_compile_rules do |rule|
+ mfile.printf(rule, e, $OBJEXT)
+ mfile.print(command)
+ mfile.printf(rule, e, $ASMEXT)
+ mfile.print(asm_command)
+ end
end
- end
- sep = config_string('BUILD_FILE_SEPARATOR') {|s| ":/=#{s}" if s != "/"} || ""
- mfile.print "$(RUBYARCHDIR)/" if $extout
- mfile.print "$(DLLIB): "
- mfile.print "$(DEFFILE) " if makedef
- mfile.print "$(OBJS) Makefile\n"
- mfile.print "\t@-$(RM) $(@#{sep})\n"
- mfile.print "\t@-$(MAKEDIRS) $(@D)\n" if $extout
- link_so = LINK_SO.gsub(/^/, "\t")
- if srcs.any?(&%r"\.(?:#{CXX_EXT.join('|')})\z".method(:===))
- link_so = link_so.sub(/\bLDSHARED\b/, '\&XX')
- end
- mfile.print link_so, "\n\n"
- unless $static.nil?
- mfile.print "$(STATIC_LIB): $(OBJS)\n\t@-$(RM) $(@#{sep})\n\t"
- mfile.print "$(AR) #{config_string('ARFLAGS') || 'cru '}$@ $(OBJS)"
- config_string('RANLIB') do |ranlib|
- mfile.print "\n\t@-#{ranlib} $(DLLIB) 2> /dev/null || true"
- end
- end
- mfile.print "\n\n"
- if makedef
- mfile.print "$(DEFFILE): #{origdef}\n"
- mfile.print "\t$(RUBY) #{makedef} #{origdef} > $@\n\n"
- end
-
- depend = File.join(srcdir, "depend")
- if File.exist?(depend)
- mfile.print("###\n", *depend_rules(File.read(depend)))
- else
- headers = %w[ruby.h defines.h]
- if RULE_SUBST
- headers.each {|h| h.sub!(/.*/, &RULE_SUBST.method(:%))}
+ mfile.print "$(TARGET_SO): "
+ mfile.print "$(DEFFILE) " if makedef
+ mfile.print "$(OBJS) Makefile"
+ mfile.print " $(TARGET_SO_DIR_TIMESTAMP)" if $extout
+ mfile.print "\n"
+ mfile.print "\t$(ECHO) linking shared-object #{target_prefix.sub(/\A\/(.*)/, '\1/')}$(DLLIB)\n"
+ mfile.print "\t-$(Q)$(RM) $(@#{sep})\n"
+ link_so = LINK_SO.gsub(/^/, "\t$(Q) ")
+ if srcs.any?(&%r"\.(?:#{CXX_EXT.join('|')})\z".method(:===))
+ link_so = link_so.sub(/\bLDSHARED\b/, '\&XX')
end
- headers << $config_h
- headers << '$(RUBY_EXTCONF_H)' if $extconf_h
- mfile.print "$(OBJS): ", headers.join(' '), "\n"
+ mfile.print link_so, "\n\n"
+ unless $static.nil?
+ mfile.print "$(STATIC_LIB): $(OBJS)\n\t-$(Q)$(RM) $(@#{sep})\n\t"
+ mfile.print "$(ECHO) linking static-library $(@#{rsep})\n\t$(Q) "
+ mfile.print "$(AR) #{config_string('ARFLAGS') || 'cru '}$@ $(OBJS)"
+ config_string('RANLIB') do |ranlib|
+ mfile.print "\n\t-$(Q)#{ranlib} $(@)#{$ignore_error}"
+ end
+ end
+ mfile.print "\n\n"
+ if makedef
+ mfile.print "$(DEFFILE): #{origdef}\n"
+ mfile.print "\t$(ECHO) generating $(@#{rsep})\n"
+ mfile.print "\t$(Q) #{makedef} > $@\n\n"
+ end
+
+ depend = File.join(srcdir, "depend")
+ if File.exist?(depend)
+ mfile.print("###\n", *depend_rules(File.read(depend)))
+ else
+ mfile.print "$(OBJS): $(HDRS) $(ruby_headers)\n"
+ end
+
+ $makefile_created = true
+ ensure
+ mfile.close if mfile
end
- $makefile_created = true
-ensure
- mfile.close if mfile
-end
+ # :stopdoc:
-# :stopdoc:
-
-def init_mkmf(config = CONFIG)
- $makefile_created = false
- $arg_config = []
- $enable_shared = config['ENABLE_SHARED'] == 'yes'
- $defs = []
- $extconf_h = nil
- $CFLAGS = with_config("cflags", arg_config("CFLAGS", config["CFLAGS"])).dup
- $ARCH_FLAG = with_config("arch_flag", arg_config("ARCH_FLAG", config["ARCH_FLAG"])).dup
- $CPPFLAGS = with_config("cppflags", arg_config("CPPFLAGS", config["CPPFLAGS"])).dup
- $LDFLAGS = with_config("ldflags", arg_config("LDFLAGS", config["LDFLAGS"])).dup
- $INCFLAGS = "-I$(arch_hdrdir)"
- $INCFLAGS << " -I$(hdrdir)/ruby/backward" unless $extmk
- $INCFLAGS << " -I$(hdrdir) -I$(srcdir)"
- $DLDFLAGS = with_config("dldflags", arg_config("DLDFLAGS", config["DLDFLAGS"])).dup
- $LIBEXT = config['LIBEXT'].dup
- $OBJEXT = config["OBJEXT"].dup
- $LIBS = "#{config['LIBS']} #{config['DLDLIBS']}"
- $LIBRUBYARG = ""
- $LIBRUBYARG_STATIC = config['LIBRUBYARG_STATIC']
- $LIBRUBYARG_SHARED = config['LIBRUBYARG_SHARED']
- $DEFLIBPATH = $extmk ? ["$(topdir)"] : CROSS_COMPILING ? [] : ["$(libdir)"]
- $DEFLIBPATH.unshift(".")
- $LIBPATH = []
- $INSTALLFILES = []
- $NONINSTALLFILES = [/~\z/, /\A#.*#\z/, /\A\.#/, /\.bak\z/i, /\.orig\z/, /\.rej\z/, /\.l[ao]\z/, /\.o\z/]
- $VPATH = %w[$(srcdir) $(arch_hdrdir)/ruby $(hdrdir)/ruby]
-
- $objs = nil
- $srcs = nil
- $libs = ""
- if $enable_shared or RbConfig.expand(config["LIBRUBY"].dup) != RbConfig.expand(config["LIBRUBY_A"].dup)
- $LIBRUBYARG = config['LIBRUBYARG']
- end
-
- $LOCAL_LIBS = ""
-
- $cleanfiles = config_string('CLEANFILES') {|s| Shellwords.shellwords(s)} || []
- $cleanfiles << "mkmf.log"
- $distcleanfiles = config_string('DISTCLEANFILES') {|s| Shellwords.shellwords(s)} || []
-
- $extout ||= nil
- $extout_prefix ||= nil
-
- $arg_config.clear
- dir_config("opt")
-end
+ def init_mkmf(config = CONFIG, rbconfig = RbConfig::CONFIG)
+ $makefile_created = false
+ $arg_config = []
+ $enable_shared = config['ENABLE_SHARED'] == 'yes'
+ $defs = []
+ $extconf_h = nil
+ $config_dirs = {}
+
+ if $warnflags = CONFIG['warnflags'] and CONFIG['GCC'] == 'yes'
+ # turn warnings into errors only for bundled extensions.
+ config['warnflags'] = $warnflags.gsub(/(?:\A|\s)-W\Kerror[-=](?!implicit-function-declaration)/, '')
+ if /icc\z/ =~ config['CC']
+ config['warnflags'].gsub!(/(\A|\s)-W(?:division-by-zero|deprecated-declarations)/, '\1')
+ end
+ RbConfig.expand(rbconfig['warnflags'] = config['warnflags'].dup)
+ config.each do |key, val|
+ RbConfig.expand(rbconfig[key] = val.dup) if /warnflags/ =~ val
+ end
+ $warnflags = config['warnflags'] unless $extmk
+ end
+ if (w = rbconfig['CC_WRAPPER']) and !w.empty? and !File.executable?(w)
+ rbconfig['CC_WRAPPER'] = config['CC_WRAPPER'] = ''
+ end
+ $CFLAGS = with_config("cflags", arg_config("CFLAGS", config["CFLAGS"])).dup
+ $CXXFLAGS = (with_config("cxxflags", arg_config("CXXFLAGS", config["CXXFLAGS"]))||'').dup
+ $ARCH_FLAG = with_config("arch_flag", arg_config("ARCH_FLAG", config["ARCH_FLAG"])).dup
+ $CPPFLAGS = with_config("cppflags", arg_config("CPPFLAGS", config["CPPFLAGS"])).dup
+ $LDFLAGS = with_config("ldflags", arg_config("LDFLAGS", config["LDFLAGS"])).dup
+ $INCFLAGS = "-I$(arch_hdrdir)"
+ $INCFLAGS << " -I$(hdrdir)/ruby/backward" unless $extmk
+ $INCFLAGS << " -I$(hdrdir) -I$(srcdir)"
+ $DLDFLAGS = with_config("dldflags", arg_config("DLDFLAGS", config["DLDFLAGS"])).dup
+ config_string("ADDITIONAL_DLDFLAGS") {|flags| $DLDFLAGS << " " << flags} unless $extmk
+ $LIBEXT = config['LIBEXT'].dup
+ $OBJEXT = config["OBJEXT"].dup
+ $EXEEXT = config["EXEEXT"].dup
+ $ASMEXT = config_string('ASMEXT', &:dup) || 'S'
+ $LIBS = "#{config['LIBS']} #{config['DLDLIBS']}"
+ $LIBRUBYARG = ""
+ $LIBRUBYARG_STATIC = config['LIBRUBYARG_STATIC']
+ $LIBRUBYARG_SHARED = config['LIBRUBYARG_SHARED']
+ $DEFLIBPATH = [$extmk ? "$(topdir)" : "$(#{config["libdirname"] || "libdir"})"]
+ $DEFLIBPATH.unshift(".")
+ $LIBPATH = []
+ $INSTALLFILES = []
+ $NONINSTALLFILES = [/~\z/, /\A#.*#\z/, /\A\.#/, /\.bak\z/i, /\.orig\z/, /\.rej\z/, /\.l[ao]\z/, /\.o\z/]
+ $VPATH = %w[$(srcdir) $(arch_hdrdir)/ruby $(hdrdir)/ruby]
+
+ $objs = nil
+ $srcs = nil
+ $headers = []
+ $libs = ""
+ if $enable_shared or RbConfig.expand(config["LIBRUBY"].dup) != RbConfig.expand(config["LIBRUBY_A"].dup)
+ $LIBRUBYARG = config['LIBRUBYARG']
+ end
+
+ $LOCAL_LIBS = ""
+
+ $cleanfiles = config_string('CLEANFILES') {|s| Shellwords.shellwords(s)} || []
+ $cleanfiles << "mkmf.log"
+ $distcleanfiles = config_string('DISTCLEANFILES') {|s| Shellwords.shellwords(s)} || []
+ $distcleandirs = config_string('DISTCLEANDIRS') {|s| Shellwords.shellwords(s)} || []
-FailedMessage = <<MESSAGE
-Could not create Makefile due to some reason, probably lack of
-necessary libraries and/or headers. Check the mkmf.log file for more
-details. You may need configuration options.
+ $extout ||= nil
+ $extout_prefix ||= nil
+
+ $arg_config.clear
+ $config_dirs.clear
+ dir_config("opt")
+ end
+
+ FailedMessage = <<MESSAGE
+Could not create Makefile due to some reason, probably lack of necessary
+libraries and/or headers. Check the mkmf.log file for more details. You may
+need configuration options.
Provided configuration options:
MESSAGE
-# Returns whether or not the Makefile was successfully generated. If not,
-# the script will abort with an error message.
-#
-# Internal use only.
-#
-def mkmf_failed(path)
- unless $makefile_created or File.exist?("Makefile")
- opts = $arg_config.collect {|t, n| "\t#{t}#{n ? "=#{n}" : ""}\n"}
- abort "*** #{path} failed ***\n" + FailedMessage + opts.join
+ # Returns whether or not the Makefile was successfully generated. If not,
+ # the script will abort with an error message.
+ #
+ # Internal use only.
+ #
+ def mkmf_failed(path)
+ unless $makefile_created or File.exist?("Makefile")
+ opts = $arg_config.collect {|t, n| "\t#{t}#{n ? "=#{n}" : ""}\n"}
+ abort "*** #{path} failed ***\n" + FailedMessage + opts.join
+ end
end
-end
-# :startdoc:
+ private
-init_mkmf
+ def _libdir_basename
+ @libdir_basename ||= config_string("libdir") {|name| name[/\A\$\(exec_prefix\)\/(.*)/, 1]} || "lib"
+ end
-$make = with_config("make-prog", ENV["MAKE"] || "make")
-make, = Shellwords.shellwords($make)
-$nmake = nil
-case
-when $mswin
- $nmake = ?m if /nmake/i =~ make
-when $bccwin
- $nmake = ?b if /Borland/i =~ `#{make} -h`
-end
+ def MAIN_DOES_NOTHING(*refs)
+ src = MAIN_DOES_NOTHING
+ unless refs.empty?
+ src = src.sub(/\{/) do
+ $& +
+ "\n if (argc > 1000000) {\n" +
+ refs.map {|n|" int (* volatile #{n}p)(void)=(int (*)(void))&#{n};\n"}.join("") +
+ refs.map {|n|" printf(\"%d\", (*#{n}p)());\n"}.join("") +
+ " }\n"
+ end
+ end
+ src
+ end
-RbConfig::CONFIG["srcdir"] = CONFIG["srcdir"] =
- $srcdir = arg_config("--srcdir", File.dirname($0))
-$configure_args["--topsrcdir"] ||= $srcdir
-if $curdir = arg_config("--curdir")
- RbConfig.expand(curdir = $curdir.dup)
-else
- curdir = $curdir = "."
-end
-unless File.expand_path(RbConfig::CONFIG["topdir"]) == File.expand_path(curdir)
- CONFIG["topdir"] = $curdir
- RbConfig::CONFIG["topdir"] = curdir
-end
-$configure_args["--topdir"] ||= $curdir
-$ruby = arg_config("--ruby", File.join(RbConfig::CONFIG["bindir"], CONFIG["ruby_install_name"]))
+ extend self
+ init_mkmf
+
+ $make = with_config("make-prog", ENV["MAKE"] || "make")
+ make, = Shellwords.shellwords($make)
+ $nmake = nil
+ case
+ when $mswin
+ $nmake = ?m if /nmake/i =~ make
+ end
+ $ignore_error = " 2> #{File::NULL} || #{$mswin ? 'exit /b0' : 'true'}"
+
+ RbConfig::CONFIG["srcdir"] = CONFIG["srcdir"] =
+ $srcdir = arg_config("--srcdir", File.dirname($0))
+ $configure_args["--topsrcdir"] ||= $srcdir
+ if $curdir = arg_config("--curdir")
+ RbConfig.expand(curdir = $curdir.dup)
+ else
+ curdir = $curdir = "."
+ end
+ unless File.expand_path(RbConfig::CONFIG["topdir"]) == File.expand_path(curdir)
+ CONFIG["topdir"] = $curdir
+ RbConfig::CONFIG["topdir"] = curdir
+ end
+ $configure_args["--topdir"] ||= $curdir
+ $ruby = arg_config("--ruby", File.join(RbConfig::CONFIG["bindir"], CONFIG["ruby_install_name"]))
+
+ RbConfig.expand(CONFIG["RUBY_SO_NAME"])
+
+ # :startdoc:
+
+ split = Shellwords.method(:shellwords).to_proc
-split = Shellwords.method(:shellwords).to_proc
+ ##
+ # The prefix added to exported symbols automatically
-EXPORT_PREFIX = config_string('EXPORT_PREFIX') {|s| s.strip}
+ EXPORT_PREFIX = config_string('EXPORT_PREFIX') {|s| s.strip}
-hdr = ['#include "ruby.h"' "\n"]
-config_string('COMMON_MACROS') do |s|
- Shellwords.shellwords(s).each do |w|
- hdr << "#define " + w.split(/=/, 2).join(" ")
+ hdr = ['#include "ruby.h"' "\n"]
+ config_string('COMMON_MACROS') do |s|
+ Shellwords.shellwords(s).each do |w|
+ w, v = w.split(/=/, 2)
+ hdr << "#ifndef #{w}"
+ hdr << "#define #{[w, v].compact.join(" ")}"
+ hdr << "#endif /* #{w} */"
+ end
end
-end
-config_string('COMMON_HEADERS') do |s|
- Shellwords.shellwords(s).each {|w| hdr << "#include <#{w}>"}
-end
-COMMON_HEADERS = hdr.join("\n")
-COMMON_LIBS = config_string('COMMON_LIBS', &split) || []
-
-COMPILE_RULES = config_string('COMPILE_RULES', &split) || %w[.%s.%s:]
-RULE_SUBST = config_string('RULE_SUBST')
-COMPILE_C = config_string('COMPILE_C') || '$(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $<'
-COMPILE_CXX = config_string('COMPILE_CXX') || '$(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $<'
-TRY_LINK = config_string('TRY_LINK') ||
- "$(CC) #{OUTFLAG}conftest $(INCFLAGS) $(CPPFLAGS) " \
- "$(CFLAGS) $(src) $(LIBPATH) $(LDFLAGS) $(ARCH_FLAG) $(LOCAL_LIBS) $(LIBS)"
-LINK_SO = config_string('LINK_SO') ||
- if CONFIG["DLEXT"] == $OBJEXT
- "ld $(DLDFLAGS) -r -o $@ $(OBJS)\n"
- else
- "$(LDSHARED) #{OUTFLAG}$@ $(OBJS) " \
- "$(LIBPATH) $(DLDFLAGS) $(LOCAL_LIBS) $(LIBS)"
+ config_string('COMMON_HEADERS') do |s|
+ Shellwords.shellwords(s).each {|w| hdr << "#include <#{w}>"}
+ end
+
+ ##
+ # Common headers for Ruby C extensions
+
+ COMMON_HEADERS = hdr.join("\n")
+
+ ##
+ # Common libraries for Ruby C extensions
+
+ COMMON_LIBS = config_string('COMMON_LIBS', &split) || []
+
+ ##
+ # make compile rules
+
+ COMPILE_RULES = config_string('COMPILE_RULES', &split) || %w[.%s.%s:]
+
+ ##
+ # Substitution in rules for NMake
+
+ RULE_SUBST = config_string('RULE_SUBST')
+
+ ##
+ # Command which will compile C files in the generated Makefile
+
+ COMPILE_C = config_string('COMPILE_C') || '$(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<'
+
+ ##
+ # Command which will compile C++ files in the generated Makefile
+
+ COMPILE_CXX = config_string('COMPILE_CXX') || '$(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<'
+
+ ##
+ # Command which will translate C files to assembler sources in the generated Makefile
+
+ ASSEMBLE_C = config_string('ASSEMBLE_C') || COMPILE_C.sub(/(?<=\s)-c(?=\s)/, '-S')
+
+ ##
+ # Command which will translate C++ files to assembler sources in the generated Makefile
+
+ ASSEMBLE_CXX = config_string('ASSEMBLE_CXX') || COMPILE_CXX.sub(/(?<=\s)-c(?=\s)/, '-S')
+
+ ##
+ # Command which will compile a program in order to test linking a library
+
+ TRY_LINK = config_string('TRY_LINK') ||
+ "$(CC) #{OUTFLAG}#{CONFTEST}#{$EXEEXT} $(INCFLAGS) $(CPPFLAGS) " \
+ "$(CFLAGS) $(src) $(LIBPATH) $(LDFLAGS) $(ARCH_FLAG) $(LOCAL_LIBS) $(LIBS)"
+
+ ##
+ # Command which will link a shared library
+
+ LINK_SO = (config_string('LINK_SO') || "").sub(/^$/) do
+ if CONFIG["DLEXT"] == $OBJEXT
+ "ld $(DLDFLAGS) -r -o $@ $(OBJS)\n"
+ else
+ "$(LDSHARED) #{OUTFLAG}$@ $(OBJS) " \
+ "$(LIBPATH) $(DLDFLAGS) $(LOCAL_LIBS) $(LIBS)"
+ end
end
-LIBPATHFLAG = config_string('LIBPATHFLAG') || ' -L"%s"'
-RPATHFLAG = config_string('RPATHFLAG') || ''
-LIBARG = config_string('LIBARG') || '-l%s'
-MAIN_DOES_NOTHING = config_string('MAIN_DOES_NOTHING') || 'int main() {return 0;}'
-sep = config_string('BUILD_FILE_SEPARATOR') {|s| ":/=#{s}" if sep != "/"} || ""
-CLEANINGS = "
-clean:
-\t\t@-$(RM) $(CLEANLIBS#{sep}) $(CLEANOBJS#{sep}) $(CLEANFILES#{sep})
+ ##
+ # Argument which will add a library path to the linker
-distclean: clean
-\t\t@-$(RM) Makefile $(RUBY_EXTCONF_H) conftest.* mkmf.log
-\t\t@-$(RM) core ruby$(EXEEXT) *~ $(DISTCLEANFILES#{sep})
+ LIBPATHFLAG = config_string('LIBPATHFLAG') || '-L%s'
+
+ ##
+ # Argument which will add a runtime library path to the linker
+
+ RPATHFLAG = config_string('RPATHFLAG') || ''
+
+ ##
+ # Argument which will add a library to the linker
+
+ LIBARG = config_string('LIBARG') || '-l%s'
+
+ ##
+ # A C main function which does no work
+
+ MAIN_DOES_NOTHING = config_string('MAIN_DOES_NOTHING') || "int main(int argc, char **argv)\n{\n return !!argv[argc];\n}"
+
+ ##
+ # The type names for convertible_int
+
+ UNIVERSAL_INTS = config_string('UNIVERSAL_INTS') {|s| Shellwords.shellwords(s)} ||
+ %w[int short long long\ long]
+
+ sep = config_string('BUILD_FILE_SEPARATOR') {|s| ":/=#{s}" if s != "/"} || ""
+
+ ##
+ # Makefile rules that will clean the extension build directory
+
+ CLEANINGS = "
+clean-static::
+clean-rb-default::
+clean-rb::
+clean-so::
+clean: clean-so clean-static clean-rb-default clean-rb
+\t\t-$(Q)$(RM_RF) $(CLEANLIBS#{sep}) $(CLEANOBJS#{sep}) $(CLEANFILES#{sep}) .*.time
+
+distclean-rb-default::
+distclean-rb::
+distclean-so::
+distclean-static::
+distclean: clean distclean-so distclean-static distclean-rb-default distclean-rb
+\t\t-$(Q)$(RM) Makefile $(RUBY_EXTCONF_H) #{CONFTEST}.* mkmf.log#{' exts.mk' if $extmk}
+\t\t-$(Q)$(RM) core ruby$(EXEEXT) *~ $(DISTCLEANFILES#{sep})
+\t\t-$(Q)$(RMDIRS) $(DISTCLEANDIRS#{sep})#{$ignore_error}
realclean: distclean
"
+ @lang = Hash.new(self)
+
+ ##
+ # Retrieves the module for _name_ language.
+ def self.[](name)
+ @lang.fetch(name)
+ end
+
+ ##
+ # Defines the module for _name_ language.
+ def self.[]=(name, mod)
+ @lang[name] = mod
+ end
+
+ ##
+ # The language that this module is for
+ LANGUAGE = -"C"
+
+ self[self::LANGUAGE] = self
+
+ cxx = Module.new do
+ # Module for C++
+
+ include MakeMakefile
+ extend self
+
+ # :stopdoc:
+
+ CONFTEST_CXX = "#{CONFTEST}.#{config_string('CXX_EXT') || CXX_EXT[0]}"
+
+ TRY_LINK_CXX = config_string('TRY_LINK_CXX') ||
+ ((cmd = TRY_LINK.gsub(/\$\(C(?:C|(FLAGS))\)/, '$(CXX\1)')) != TRY_LINK && cmd) ||
+ "$(CXX) #{OUTFLAG}#{CONFTEST}#{$EXEEXT} $(INCFLAGS) $(CPPFLAGS) " \
+ "$(CXXFLAGS) $(src) $(LIBPATH) $(LDFLAGS) $(ARCH_FLAG) $(LOCAL_LIBS) $(LIBS)"
+
+ def have_devel?
+ unless defined? @have_devel
+ @have_devel = true
+ @have_devel = try_link(MAIN_DOES_NOTHING)
+ end
+ @have_devel
+ end
+
+ def conftest_source
+ CONFTEST_CXX
+ end
+
+ def cc_command(opt="")
+ conf = cc_config(opt)
+ cxx_command(opt, conf)
+ RbConfig::expand("$(CXX) #$INCFLAGS #$CPPFLAGS #$CXXFLAGS #$ARCH_FLAG #{opt} -c #{CONFTEST_CXX}",
+ conf)
+ end
+
+ def cpp_command(outfile, opt="")
+ conf = cpp_config(opt)
+ cxx = cxx_command(opt, conf)
+ cpp = conf['CPP'].sub(/(\A|\s)#{Regexp.quote(conf['CC'])}(?=\z|\s)/) {
+ "#$1#{cxx}"
+ }
+ RbConfig::expand("#{cpp} #$INCFLAGS #$CPPFLAGS #$CXXFLAGS #{opt} #{CONFTEST_CXX} #{outfile}",
+ conf)
+ end
+
+ def link_command(ldflags, *opts)
+ conf = link_config(ldflags, *opts)
+ RbConfig::expand(TRY_LINK_CXX.dup, conf)
+ end
+
+ def cxx_command(opt="", conf = cc_config(opt))
+ cxx = conf['CXX']
+ raise Errno::ENOENT, "C++ compiler not found" if !cxx or cxx == 'false'
+ cxx
+ end
+
+ # :startdoc:
+ end
+
+ cxx::LANGUAGE = -"C++"
+ self[cxx::LANGUAGE] = cxx
+end
+
+# MakeMakefile::Global = #
+m = Module.new {
+ include(MakeMakefile)
+ private(*MakeMakefile.public_instance_methods(false))
+}
+include m
+
if not $extmk and /\A(extconf|makefile).rb\z/ =~ File.basename($0)
END {mkmf_failed($0)}
end
diff --git a/lib/monitor.rb b/lib/monitor.rb
index 31234819b8..21329a5de7 100644
--- a/lib/monitor.rb
+++ b/lib/monitor.rb
@@ -1,157 +1,110 @@
-=begin
-
-= monitor.rb
-
-Copyright (C) 2001 Shugo Maeda <shugo@ruby-lang.org>
-
-This library is distributed under the terms of the Ruby license.
-You can freely distribute/modify this library.
-
-== example
-
-This is a simple example.
-
- require 'monitor.rb'
-
- buf = []
- buf.extend(MonitorMixin)
- empty_cond = buf.new_cond
-
- # consumer
- Thread.start do
- loop do
- buf.synchronize do
- empty_cond.wait_while { buf.empty? }
- print buf.shift
- end
- end
- end
-
- # producer
- while line = ARGF.gets
- buf.synchronize do
- buf.push(line)
- empty_cond.signal
- end
- end
-
-The consumer thread waits for the producer thread to push a line
-to buf while buf.empty?, and the producer thread (main thread)
-reads a line from ARGF and push it to buf, then call
-empty_cond.signal.
-
-=end
-
-require 'thread'
-
+# frozen_string_literal: false
+# = monitor.rb
+#
+# Copyright (C) 2001 Shugo Maeda <shugo@ruby-lang.org>
+#
+# This library is distributed under the terms of the Ruby license.
+# You can freely distribute/modify this library.
+#
+#
+# In concurrent programming, a monitor is an object or module intended to be
+# used safely by more than one thread. The defining characteristic of a
+# monitor is that its methods are executed with mutual exclusion. That is, at
+# each point in time, at most one thread may be executing any of its methods.
+# This mutual exclusion greatly simplifies reasoning about the implementation
+# of monitors compared to reasoning about parallel code that updates a data
+# structure.
#
-# Adds monitor functionality to an arbitrary object by mixing the module with
-# +include+. For example:
+# You can read more about the general principles on the Wikipedia page for
+# Monitors[https://en.wikipedia.org/wiki/Monitor_%28synchronization%29].
#
-# require 'monitor'
-#
-# buf = []
-# buf.extend(MonitorMixin)
-# empty_cond = buf.new_cond
-#
-# # consumer
-# Thread.start do
-# loop do
-# buf.synchronize do
-# empty_cond.wait_while { buf.empty? }
-# print buf.shift
-# end
-# end
-# end
-#
-# # producer
-# while line = ARGF.gets
-# buf.synchronize do
-# buf.push(line)
-# empty_cond.signal
-# end
-# end
-#
-# The consumer thread waits for the producer thread to push a line
-# to buf while buf.empty?, and the producer thread (main thread)
-# reads a line from ARGF and push it to buf, then call
-# empty_cond.signal.
+# == Examples
+#
+# === Simple object.extend
+#
+# require 'monitor.rb'
+#
+# buf = []
+# buf.extend(MonitorMixin)
+# empty_cond = buf.new_cond
+#
+# # consumer
+# Thread.start do
+# loop do
+# buf.synchronize do
+# empty_cond.wait_while { buf.empty? }
+# print buf.shift
+# end
+# end
+# end
+#
+# # producer
+# while line = ARGF.gets
+# buf.synchronize do
+# buf.push(line)
+# empty_cond.signal
+# end
+# end
+#
+# The consumer thread waits for the producer thread to push a line to buf
+# while <tt>buf.empty?</tt>. The producer thread (main thread) reads a
+# line from ARGF and pushes it into buf then calls <tt>empty_cond.signal</tt>
+# to notify the consumer thread of new data.
+#
+# === Simple Class include
+#
+# require 'monitor'
+#
+# class SynchronizedArray < Array
+#
+# include MonitorMixin
+#
+# def initialize(*args)
+# super(*args)
+# end
+#
+# alias :old_shift :shift
+# alias :old_unshift :unshift
+#
+# def shift(n=1)
+# self.synchronize do
+# self.old_shift(n)
+# end
+# end
+#
+# def unshift(item)
+# self.synchronize do
+# self.old_unshift(item)
+# end
+# end
+#
+# # other methods ...
+# end
+#
+# +SynchronizedArray+ implements an Array with synchronized access to items.
+# This Class is implemented as subclass of Array which includes the
+# MonitorMixin module.
#
module MonitorMixin
+ ConditionVariable = Monitor::ConditionVariable # :nodoc:
+
#
# FIXME: This isn't documented in Nutshell.
#
# Since MonitorMixin.new_cond returns a ConditionVariable, and the example
# above calls while_wait and signal, this class should be documented.
#
- class ConditionVariable
- class Timeout < Exception; end
-
- def wait(timeout = nil)
- if timeout
- raise NotImplementedError, "timeout is not implemented yet"
- end
- @monitor.send(:mon_check_owner)
- count = @monitor.send(:mon_exit_for_cond)
- begin
- @cond.wait(@monitor.instance_variable_get("@mon_mutex"))
- return true
- ensure
- @monitor.send(:mon_enter_for_cond, count)
- end
- end
-
- def wait_while
- while yield
- wait
- end
- end
-
- def wait_until
- until yield
- wait
- end
- end
-
- def signal
- @monitor.send(:mon_check_owner)
- @cond.signal
- end
-
- def broadcast
- @monitor.send(:mon_check_owner)
- @cond.broadcast
- end
-
- def count_waiters
- raise NotImplementedError
- end
-
- private
- def initialize(monitor)
- @monitor = monitor
- @cond = ::ConditionVariable.new
- end
- end
-
- def self.extend_object(obj)
+ def self.extend_object(obj) # :nodoc:
super(obj)
- obj.send(:mon_initialize)
+ obj.__send__(:mon_initialize)
end
-
+
#
# Attempts to enter exclusive section. Returns +false+ if lock fails.
#
def mon_try_enter
- if @mon_owner != Thread.current
- unless @mon_mutex.try_lock
- return false
- end
- @mon_owner = Thread.current
- end
- @mon_count += 1
- return true
+ @mon_data.try_enter
end
# For backward compatibility
alias try_mon_enter mon_try_enter
@@ -160,23 +113,29 @@ module MonitorMixin
# Enters exclusive section.
#
def mon_enter
- if @mon_owner != Thread.current
- @mon_mutex.lock
- @mon_owner = Thread.current
- end
- @mon_count += 1
+ @mon_data.enter
end
-
+
#
# Leaves exclusive section.
#
def mon_exit
mon_check_owner
- @mon_count -=1
- if @mon_count == 0
- @mon_owner = nil
- @mon_mutex.unlock
- end
+ @mon_data.exit
+ end
+
+ #
+ # Returns true if this monitor is locked by any thread
+ #
+ def mon_locked?
+ @mon_data.mon_locked?
+ end
+
+ #
+ # Returns true if this monitor is locked by current thread.
+ #
+ def mon_owned?
+ @mon_data.mon_owned?
end
#
@@ -184,63 +143,62 @@ module MonitorMixin
# section automatically when the block exits. See example under
# +MonitorMixin+.
#
- def mon_synchronize
- mon_enter
- begin
- yield
- ensure
- mon_exit
- end
+ def mon_synchronize(&b)
+ @mon_data.synchronize(&b)
end
alias synchronize mon_synchronize
-
+
#
- # FIXME: This isn't documented in Nutshell.
+ # Creates a new MonitorMixin::ConditionVariable associated with the
+ # Monitor object.
#
def new_cond
- return ConditionVariable.new(self)
+ unless defined?(@mon_data)
+ mon_initialize
+ @mon_initialized_by_new_cond = true
+ end
+ return ConditionVariable.new(@mon_data)
end
private
- def initialize(*args)
+ # Use <tt>extend MonitorMixin</tt> or <tt>include MonitorMixin</tt> instead
+ # of this constructor. Have look at the examples above to understand how to
+ # use this module.
+ def initialize(...)
super
mon_initialize
end
+ # Initializes the MonitorMixin after being included in a class or when an
+ # object has been extended with the MonitorMixin
def mon_initialize
- @mon_owner = nil
- @mon_count = 0
- @mon_mutex = Mutex.new
- end
-
- def mon_check_owner
- if @mon_owner != Thread.current
- raise ThreadError, "current thread not owner"
+ if defined?(@mon_data)
+ if defined?(@mon_initialized_by_new_cond)
+ return # already initialized.
+ elsif @mon_data_owner_object_id == self.object_id
+ raise ThreadError, "already initialized"
+ end
end
+ @mon_data = ::Monitor.new
+ @mon_data_owner_object_id = self.object_id
end
- def mon_enter_for_cond(count)
- @mon_owner = Thread.current
- @mon_count = count
- end
-
- def mon_exit_for_cond
- count = @mon_count
- @mon_owner = nil
- @mon_count = 0
- return count
+ # Ensures that the MonitorMixin is owned by the current thread,
+ # otherwise raises an exception.
+ def mon_check_owner
+ @mon_data.mon_check_owner
end
end
-class Monitor
- include MonitorMixin
- alias try_enter try_mon_enter
- alias enter mon_enter
- alias exit mon_exit
+class Monitor # :nodoc:
+ alias try_mon_enter try_enter
+ alias mon_try_enter try_enter
+ alias mon_enter enter
+ alias mon_exit exit
+ alias mon_synchronize synchronize
end
-
# Documentation comments:
# - All documentation comes from Nutshell.
# - MonitorMixin.new_cond appears in the example, but is not documented in
@@ -248,8 +206,6 @@ end
# - All the internals (internal modules Accessible and Initializable, class
# ConditionVariable) appear in RDoc. It might be good to hide them, by
# making them private, or marking them :nodoc:, etc.
-# - The entire example from the RD section at the top is replicated in the RDoc
-# comment for MonitorMixin. Does the RD section need to remain?
# - RDoc doesn't recognise aliases, so we have mon_synchronize documented, but
# not synchronize.
# - mon_owner is in Nutshell, but appears as an accessor in a separate module
@@ -258,8 +214,3 @@ end
# directly in the RDoc output.
# - in short, it may be worth changing the code layout in this file to make the
# documentation easier
-
-# Local variables:
-# mode: Ruby
-# tab-width: 8
-# End:
diff --git a/lib/mutex_m.rb b/lib/mutex_m.rb
deleted file mode 100644
index f46f866fa5..0000000000
--- a/lib/mutex_m.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-#
-# mutex_m.rb -
-# $Release Version: 3.0$
-# $Revision: 1.7 $
-# Original from mutex.rb
-# by Keiju ISHITSUKA(keiju@ishitsuka.com)
-# modified by matz
-# patched by akira yamada
-#
-# --
-# Usage:
-# require "mutex_m.rb"
-# obj = Object.new
-# obj.extend Mutex_m
-# ...
-# extended object can be handled like Mutex
-# or
-# class Foo
-# include Mutex_m
-# ...
-# end
-# obj = Foo.new
-# this obj can be handled like Mutex
-#
-
-require 'thread'
-
-module Mutex_m
- def Mutex_m.define_aliases(cl)
- cl.module_eval %q{
- alias locked? mu_locked?
- alias lock mu_lock
- alias unlock mu_unlock
- alias try_lock mu_try_lock
- alias synchronize mu_synchronize
- }
- end
-
- def Mutex_m.append_features(cl)
- super
- define_aliases(cl) unless cl.instance_of?(Module)
- end
-
- def Mutex_m.extend_object(obj)
- super
- obj.mu_extended
- end
-
- def mu_extended
- unless (defined? locked? and
- defined? lock and
- defined? unlock and
- defined? try_lock and
- defined? synchronize)
- Mutex_m.define_aliases(class<<self;self;end)
- end
- mu_initialize
- end
-
- # locking
- def mu_synchronize(&block)
- @_mutex.synchronize(&block)
- end
-
- def mu_locked?
- @_mutex.locked?
- end
-
- def mu_try_lock
- @_mutex.try_lock
- end
-
- def mu_lock
- @_mutex.lock
- end
-
- def mu_unlock
- @_mutex.unlock
- end
-
- private
-
- def mu_initialize
- @_mutex = Mutex.new
- end
-
- def initialize(*args)
- mu_initialize
- super
- end
-end
diff --git a/lib/net/.document b/lib/net/.document
deleted file mode 100644
index 6332bb9e7e..0000000000
--- a/lib/net/.document
+++ /dev/null
@@ -1,8 +0,0 @@
-ftp.rb
-http.rb
-https.rb
-imap.rb
-pop.rb
-smtp.rb
-smtps.rb
-telnet.rb
diff --git a/lib/net/ftp.rb b/lib/net/ftp.rb
deleted file mode 100644
index 06cc3eafa2..0000000000
--- a/lib/net/ftp.rb
+++ /dev/null
@@ -1,981 +0,0 @@
-#
-# = net/ftp.rb - FTP Client Library
-#
-# Written by Shugo Maeda <shugo@ruby-lang.org>.
-#
-# Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas)
-# and "Ruby In a Nutshell" (Matsumoto), used with permission.
-#
-# This library is distributed under the terms of the Ruby license.
-# You can freely distribute/modify this library.
-#
-# It is included in the Ruby standard library.
-#
-# See the Net::FTP class for an overview.
-#
-
-require "socket"
-require "monitor"
-
-module Net
-
- # :stopdoc:
- class FTPError < StandardError; end
- class FTPReplyError < FTPError; end
- class FTPTempError < FTPError; end
- class FTPPermError < FTPError; end
- class FTPProtoError < FTPError; end
- # :startdoc:
-
- #
- # This class implements the File Transfer Protocol. If you have used a
- # command-line FTP program, and are familiar with the commands, you will be
- # able to use this class easily. Some extra features are included to take
- # advantage of Ruby's style and strengths.
- #
- # == Example
- #
- # require 'net/ftp'
- #
- # === Example 1
- #
- # ftp = Net::FTP.new('ftp.netlab.co.jp')
- # ftp.login
- # files = ftp.chdir('pub/lang/ruby/contrib')
- # files = ftp.list('n*')
- # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
- # ftp.close
- #
- # === Example 2
- #
- # Net::FTP.open('ftp.netlab.co.jp') do |ftp|
- # ftp.login
- # files = ftp.chdir('pub/lang/ruby/contrib')
- # files = ftp.list('n*')
- # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
- # end
- #
- # == Major Methods
- #
- # The following are the methods most likely to be useful to users:
- # - FTP.open
- # - #getbinaryfile
- # - #gettextfile
- # - #putbinaryfile
- # - #puttextfile
- # - #chdir
- # - #nlst
- # - #size
- # - #rename
- # - #delete
- #
- class FTP
- include MonitorMixin
-
- # :stopdoc:
- FTP_PORT = 21
- CRLF = "\r\n"
- DEFAULT_BLOCKSIZE = 4096
- # :startdoc:
-
- # When +true+, transfers are performed in binary mode. Default: +true+.
- attr_reader :binary
-
- # When +true+, the connection is in passive mode. Default: +false+.
- attr_accessor :passive
-
- # When +true+, all traffic to and from the server is written
- # to +$stdout+. Default: +false+.
- attr_accessor :debug_mode
-
- # Sets or retrieves the +resume+ status, which decides whether incomplete
- # transfers are resumed or restarted. Default: +false+.
- attr_accessor :resume
-
- # The server's welcome message.
- attr_reader :welcome
-
- # The server's last response code.
- attr_reader :last_response_code
- alias lastresp last_response_code
-
- # The server's last response.
- attr_reader :last_response
-
- #
- # A synonym for <tt>FTP.new</tt>, but with a mandatory host parameter.
- #
- # If a block is given, it is passed the +FTP+ object, which will be closed
- # when the block finishes, or when an exception is raised.
- #
- def FTP.open(host, user = nil, passwd = nil, acct = nil)
- if block_given?
- ftp = new(host, user, passwd, acct)
- begin
- yield ftp
- ensure
- ftp.close
- end
- else
- new(host, user, passwd, acct)
- end
- end
-
- #
- # Creates and returns a new +FTP+ object. If a +host+ is given, a connection
- # is made. Additionally, if the +user+ is given, the given user name,
- # password, and (optionally) account are used to log in. See #login.
- #
- def initialize(host = nil, user = nil, passwd = nil, acct = nil)
- super()
- @binary = false
- @passive = false
- @debug_mode = false
- @resume = false
- if host
- connect(host)
- if user
- login(user, passwd, acct)
- end
- end
- end
-
- def binary=(newmode)
- if newmode != @binary
- @binary = newmode
- @binary ? voidcmd("TYPE I") : voidcmd("TYPE A")
- end
- end
-
- def with_binary(newmode)
- oldmode = binary
- self.binary = newmode
- begin
- yield
- ensure
- self.binary = oldmode
- end
- end
- private :with_binary
-
- # Obsolete
- def return_code
- $stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing")
- return "\n"
- end
-
- # Obsolete
- def return_code=(s)
- $stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing")
- end
-
- def open_socket(host, port)
- if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
- @passive = true
- return SOCKSSocket.open(host, port)
- else
- return TCPSocket.open(host, port)
- end
- end
- private :open_socket
-
- #
- # Establishes an FTP connection to host, optionally overriding the default
- # port. If the environment variable +SOCKS_SERVER+ is set, sets up the
- # connection through a SOCKS proxy. Raises an exception (typically
- # <tt>Errno::ECONNREFUSED</tt>) if the connection cannot be established.
- #
- def connect(host, port = FTP_PORT)
- if @debug_mode
- print "connect: ", host, ", ", port, "\n"
- end
- synchronize do
- @sock = open_socket(host, port)
- voidresp
- end
- end
-
- #
- # WRITEME or make private
- #
- def set_socket(sock, get_greeting = true)
- synchronize do
- @sock = sock
- if get_greeting
- voidresp
- end
- end
- end
-
- def sanitize(s)
- if s =~ /^PASS /i
- return s[0, 5] + "*" * (s.length - 5)
- else
- return s
- end
- end
- private :sanitize
-
- def putline(line)
- if @debug_mode
- print "put: ", sanitize(line), "\n"
- end
- line = line + CRLF
- @sock.write(line)
- end
- private :putline
-
- def getline
- line = @sock.readline # if get EOF, raise EOFError
- line.sub!(/(\r\n|\n|\r)\z/n, "")
- if @debug_mode
- print "get: ", sanitize(line), "\n"
- end
- return line
- end
- private :getline
-
- def getmultiline
- line = getline
- buff = line
- if line[3] == ?-
- code = line[0, 3]
- begin
- line = getline
- buff << "\n" << line
- end until line[0, 3] == code and line[3] != ?-
- end
- return buff << "\n"
- end
- private :getmultiline
-
- def getresp
- @last_response = getmultiline
- @last_response_code = @last_response[0, 3]
- case @last_response_code
- when /\A[123]/
- return @last_response
- when /\A4/
- raise FTPTempError, @last_response
- when /\A5/
- raise FTPPermError, @last_response
- else
- raise FTPProtoError, @last_response
- end
- end
- private :getresp
-
- def voidresp
- resp = getresp
- if resp[0] != ?2
- raise FTPReplyError, resp
- end
- end
- private :voidresp
-
- #
- # Sends a command and returns the response.
- #
- def sendcmd(cmd)
- synchronize do
- putline(cmd)
- return getresp
- end
- end
-
- #
- # Sends a command and expect a response beginning with '2'.
- #
- def voidcmd(cmd)
- synchronize do
- putline(cmd)
- voidresp
- end
- end
-
- def sendport(host, port)
- af = (@sock.peeraddr)[0]
- if af == "AF_INET"
- cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
- elsif af == "AF_INET6"
- cmd = sprintf("EPRT |2|%s|%d|", host, port)
- else
- raise FTPProtoError, host
- end
- voidcmd(cmd)
- end
- private :sendport
-
- def makeport
- sock = TCPServer.open(@sock.addr[3], 0)
- port = sock.addr[1]
- host = sock.addr[3]
- resp = sendport(host, port)
- return sock
- end
- private :makeport
-
- def makepasv
- if @sock.peeraddr[0] == "AF_INET"
- host, port = parse227(sendcmd("PASV"))
- else
- host, port = parse229(sendcmd("EPSV"))
- # host, port = parse228(sendcmd("LPSV"))
- end
- return host, port
- end
- private :makepasv
-
- def transfercmd(cmd, rest_offset = nil)
- if @passive
- host, port = makepasv
- conn = open_socket(host, port)
- if @resume and rest_offset
- resp = sendcmd("REST " + rest_offset.to_s)
- if resp[0] != ?3
- raise FTPReplyError, resp
- end
- end
- resp = sendcmd(cmd)
- # skip 2XX for some ftp servers
- resp = getresp if resp[0] == ?2
- if resp[0] != ?1
- raise FTPReplyError, resp
- end
- else
- sock = makeport
- if @resume and rest_offset
- resp = sendcmd("REST " + rest_offset.to_s)
- if resp[0] != ?3
- raise FTPReplyError, resp
- end
- end
- resp = sendcmd(cmd)
- # skip 2XX for some ftp servers
- resp = getresp if resp[0] == ?2
- if resp[0] != ?1
- raise FTPReplyError, resp
- end
- conn = sock.accept
- sock.close
- end
- return conn
- end
- private :transfercmd
-
- def getaddress
- thishost = Socket.gethostname
- if not thishost.index(".")
- thishost = Socket.gethostbyname(thishost)[0]
- end
- if ENV.has_key?("LOGNAME")
- realuser = ENV["LOGNAME"]
- elsif ENV.has_key?("USER")
- realuser = ENV["USER"]
- else
- realuser = "anonymous"
- end
- return realuser + "@" + thishost
- end
- private :getaddress
-
- #
- # Logs in to the remote host. The session must have been previously
- # connected. If +user+ is the string "anonymous" and the +password+ is
- # +nil+, a password of <tt>user@host</tt> is synthesized. If the +acct+
- # parameter is not +nil+, an FTP ACCT command is sent following the
- # successful login. Raises an exception on error (typically
- # <tt>Net::FTPPermError</tt>).
- #
- def login(user = "anonymous", passwd = nil, acct = nil)
- if user == "anonymous" and passwd == nil
- passwd = getaddress
- end
-
- resp = ""
- synchronize do
- resp = sendcmd('USER ' + user)
- if resp[0] == ?3
- raise FTPReplyError, resp if passwd.nil?
- resp = sendcmd('PASS ' + passwd)
- end
- if resp[0] == ?3
- raise FTPReplyError, resp if acct.nil?
- resp = sendcmd('ACCT ' + acct)
- end
- end
- if resp[0] != ?2
- raise FTPReplyError, resp
- end
- @welcome = resp
- self.binary = true
- end
-
- #
- # Puts the connection into binary (image) mode, issues the given command,
- # and fetches the data returned, passing it to the associated block in
- # chunks of +blocksize+ characters. Note that +cmd+ is a server command
- # (such as "RETR myfile").
- #
- def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data
- synchronize do
- with_binary(true) do
- conn = transfercmd(cmd, rest_offset)
- loop do
- data = conn.read(blocksize)
- break if data == nil
- yield(data)
- end
- conn.close
- voidresp
- end
- end
- end
-
- #
- # Puts the connection into ASCII (text) mode, issues the given command, and
- # passes the resulting data, one line at a time, to the associated block. If
- # no block is given, prints the lines. Note that +cmd+ is a server command
- # (such as "RETR myfile").
- #
- def retrlines(cmd) # :yield: line
- synchronize do
- with_binary(false) do
- conn = transfercmd(cmd)
- loop do
- line = conn.gets
- break if line == nil
- if line[-2, 2] == CRLF
- line = line[0 .. -3]
- elsif line[-1] == ?\n
- line = line[0 .. -2]
- end
- yield(line)
- end
- conn.close
- voidresp
- end
- end
- end
-
- #
- # Puts the connection into binary (image) mode, issues the given server-side
- # command (such as "STOR myfile"), and sends the contents of the file named
- # +file+ to the server. If the optional block is given, it also passes it
- # the data, in chunks of +blocksize+ characters.
- #
- def storbinary(cmd, file, blocksize, rest_offset = nil, &block) # :yield: data
- if rest_offset
- file.seek(rest_offset, IO::SEEK_SET)
- end
- synchronize do
- with_binary(true) do
- conn = transfercmd(cmd, rest_offset)
- loop do
- buf = file.read(blocksize)
- break if buf == nil
- conn.write(buf)
- yield(buf) if block
- end
- conn.close
- voidresp
- end
- end
- rescue Errno::EPIPE
- # EPIPE, in this case, means that the data connection was unexpectedly
- # terminated. Rather than just raising EPIPE to the caller, check the
- # response on the control connection. If getresp doesn't raise a more
- # appropriate exception, re-raise the original exception.
- getresp
- raise
- end
-
- #
- # Puts the connection into ASCII (text) mode, issues the given server-side
- # command (such as "STOR myfile"), and sends the contents of the file
- # named +file+ to the server, one line at a time. If the optional block is
- # given, it also passes it the lines.
- #
- def storlines(cmd, file, &block) # :yield: line
- synchronize do
- with_binary(false) do
- conn = transfercmd(cmd)
- loop do
- buf = file.gets
- break if buf == nil
- if buf[-2, 2] != CRLF
- buf = buf.chomp + CRLF
- end
- conn.write(buf)
- yield(buf) if block
- end
- conn.close
- voidresp
- end
- end
- rescue Errno::EPIPE
- # EPIPE, in this case, means that the data connection was unexpectedly
- # terminated. Rather than just raising EPIPE to the caller, check the
- # response on the control connection. If getresp doesn't raise a more
- # appropriate exception, re-raise the original exception.
- getresp
- raise
- end
-
- #
- # Retrieves +remotefile+ in binary mode, storing the result in +localfile+.
- # If +localfile+ is nil, returns retrieved data.
- # If a block is supplied, it is passed the retrieved data in +blocksize+
- # chunks.
- #
- def getbinaryfile(remotefile, localfile = File.basename(remotefile),
- blocksize = DEFAULT_BLOCKSIZE) # :yield: data
- result = nil
- if localfile
- if @resume
- rest_offset = File.size?(localfile)
- f = open(localfile, "a")
- else
- rest_offset = nil
- f = open(localfile, "w")
- end
- elsif !block_given?
- result = ""
- end
- begin
- f.binmode if localfile
- retrbinary("RETR " + remotefile, blocksize, rest_offset) do |data|
- f.write(data) if localfile
- yield(data) if block_given?
- result.concat(data) if result
- end
- return result
- ensure
- f.close if localfile
- end
- end
-
- #
- # Retrieves +remotefile+ in ASCII (text) mode, storing the result in
- # +localfile+.
- # If +localfile+ is nil, returns retrieved data.
- # If a block is supplied, it is passed the retrieved data one
- # line at a time.
- #
- def gettextfile(remotefile, localfile = File.basename(remotefile)) # :yield: line
- result = nil
- if localfile
- f = open(localfile, "w")
- elsif !block_given?
- result = ""
- end
- begin
- retrlines("RETR " + remotefile) do |line|
- f.puts(line) if localfile
- yield(line) if block_given?
- result.concat(line + "\n") if result
- end
- return result
- ensure
- f.close if localfile
- end
- end
-
- #
- # Retrieves +remotefile+ in whatever mode the session is set (text or
- # binary). See #gettextfile and #getbinaryfile.
- #
- def get(remotefile, localfile = File.basename(remotefile),
- blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
- if @binary
- getbinaryfile(remotefile, localfile, blocksize, &block)
- else
- gettextfile(remotefile, localfile, &block)
- end
- end
-
- #
- # Transfers +localfile+ to the server in binary mode, storing the result in
- # +remotefile+. If a block is supplied, calls it, passing in the transmitted
- # data in +blocksize+ chunks.
- #
- def putbinaryfile(localfile, remotefile = File.basename(localfile),
- blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
- if @resume
- begin
- rest_offset = size(remotefile)
- rescue Net::FTPPermError
- rest_offset = nil
- end
- else
- rest_offset = nil
- end
- f = open(localfile)
- begin
- f.binmode
- storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block)
- ensure
- f.close
- end
- end
-
- #
- # Transfers +localfile+ to the server in ASCII (text) mode, storing the result
- # in +remotefile+. If callback or an associated block is supplied, calls it,
- # passing in the transmitted data one line at a time.
- #
- def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line
- f = open(localfile)
- begin
- storlines("STOR " + remotefile, f, &block)
- ensure
- f.close
- end
- end
-
- #
- # Transfers +localfile+ to the server in whatever mode the session is set
- # (text or binary). See #puttextfile and #putbinaryfile.
- #
- def put(localfile, remotefile = File.basename(localfile),
- blocksize = DEFAULT_BLOCKSIZE, &block)
- if @binary
- putbinaryfile(localfile, remotefile, blocksize, &block)
- else
- puttextfile(localfile, remotefile, &block)
- end
- end
-
- #
- # Sends the ACCT command. TODO: more info.
- #
- def acct(account)
- cmd = "ACCT " + account
- voidcmd(cmd)
- end
-
- #
- # Returns an array of filenames in the remote directory.
- #
- def nlst(dir = nil)
- cmd = "NLST"
- if dir
- cmd = cmd + " " + dir
- end
- files = []
- retrlines(cmd) do |line|
- files.push(line)
- end
- return files
- end
-
- #
- # Returns an array of file information in the directory (the output is like
- # `ls -l`). If a block is given, it iterates through the listing.
- #
- def list(*args, &block) # :yield: line
- cmd = "LIST"
- args.each do |arg|
- cmd = cmd + " " + arg
- end
- if block
- retrlines(cmd, &block)
- else
- lines = []
- retrlines(cmd) do |line|
- lines << line
- end
- return lines
- end
- end
- alias ls list
- alias dir list
-
- #
- # Renames a file on the server.
- #
- def rename(fromname, toname)
- resp = sendcmd("RNFR " + fromname)
- if resp[0] != ?3
- raise FTPReplyError, resp
- end
- voidcmd("RNTO " + toname)
- end
-
- #
- # Deletes a file on the server.
- #
- def delete(filename)
- resp = sendcmd("DELE " + filename)
- if resp[0, 3] == "250"
- return
- elsif resp[0] == ?5
- raise FTPPermError, resp
- else
- raise FTPReplyError, resp
- end
- end
-
- #
- # Changes the (remote) directory.
- #
- def chdir(dirname)
- if dirname == ".."
- begin
- voidcmd("CDUP")
- return
- rescue FTPPermError => e
- if e.message[0, 3] != "500"
- raise e
- end
- end
- end
- cmd = "CWD " + dirname
- voidcmd(cmd)
- end
-
- #
- # Returns the size of the given (remote) filename.
- #
- def size(filename)
- with_binary(true) do
- resp = sendcmd("SIZE " + filename)
- if resp[0, 3] != "213"
- raise FTPReplyError, resp
- end
- return resp[3..-1].strip.to_i
- end
- end
-
- MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ # :nodoc:
-
- #
- # Returns the last modification time of the (remote) file. If +local+ is
- # +true+, it is returned as a local time, otherwise it's a UTC time.
- #
- def mtime(filename, local = false)
- str = mdtm(filename)
- ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i}
- return local ? Time.local(*ary) : Time.gm(*ary)
- end
-
- #
- # Creates a remote directory.
- #
- def mkdir(dirname)
- resp = sendcmd("MKD " + dirname)
- return parse257(resp)
- end
-
- #
- # Removes a remote directory.
- #
- def rmdir(dirname)
- voidcmd("RMD " + dirname)
- end
-
- #
- # Returns the current remote directory.
- #
- def pwd
- resp = sendcmd("PWD")
- return parse257(resp)
- end
- alias getdir pwd
-
- #
- # Returns system information.
- #
- def system
- resp = sendcmd("SYST")
- if resp[0, 3] != "215"
- raise FTPReplyError, resp
- end
- return resp[4 .. -1]
- end
-
- #
- # Aborts the previous command (ABOR command).
- #
- def abort
- line = "ABOR" + CRLF
- print "put: ABOR\n" if @debug_mode
- @sock.send(line, Socket::MSG_OOB)
- resp = getmultiline
- unless ["426", "226", "225"].include?(resp[0, 3])
- raise FTPProtoError, resp
- end
- return resp
- end
-
- #
- # Returns the status (STAT command).
- #
- def status
- line = "STAT" + CRLF
- print "put: STAT\n" if @debug_mode
- @sock.send(line, Socket::MSG_OOB)
- return getresp
- end
-
- #
- # Issues the MDTM command. TODO: more info.
- #
- def mdtm(filename)
- resp = sendcmd("MDTM " + filename)
- if resp[0, 3] == "213"
- return resp[3 .. -1].strip
- end
- end
-
- #
- # Issues the HELP command.
- #
- def help(arg = nil)
- cmd = "HELP"
- if arg
- cmd = cmd + " " + arg
- end
- sendcmd(cmd)
- end
-
- #
- # Exits the FTP session.
- #
- def quit
- voidcmd("QUIT")
- end
-
- #
- # Issues a NOOP command.
- #
- def noop
- voidcmd("NOOP")
- end
-
- #
- # Issues a SITE command.
- #
- def site(arg)
- cmd = "SITE " + arg
- voidcmd(cmd)
- end
-
- #
- # Closes the connection. Further operations are impossible until you open
- # a new connection with #connect.
- #
- def close
- @sock.close if @sock and not @sock.closed?
- end
-
- #
- # Returns +true+ iff the connection is closed.
- #
- def closed?
- @sock == nil or @sock.closed?
- end
-
- def parse227(resp)
- if resp[0, 3] != "227"
- raise FTPReplyError, resp
- end
- left = resp.index("(")
- right = resp.index(")")
- if left == nil or right == nil
- raise FTPProtoError, resp
- end
- numbers = resp[left + 1 .. right - 1].split(",")
- if numbers.length != 6
- raise FTPProtoError, resp
- end
- host = numbers[0, 4].join(".")
- port = (numbers[4].to_i << 8) + numbers[5].to_i
- return host, port
- end
- private :parse227
-
- def parse228(resp)
- if resp[0, 3] != "228"
- raise FTPReplyError, resp
- end
- left = resp.index("(")
- right = resp.index(")")
- if left == nil or right == nil
- raise FTPProtoError, resp
- end
- numbers = resp[left + 1 .. right - 1].split(",")
- if numbers[0] == "4"
- if numbers.length != 9 || numbers[1] != "4" || numbers[2 + 4] != "2"
- raise FTPProtoError, resp
- end
- host = numbers[2, 4].join(".")
- port = (numbers[7].to_i << 8) + numbers[8].to_i
- elsif numbers[0] == "6"
- if numbers.length != 21 || numbers[1] != "16" || numbers[2 + 16] != "2"
- raise FTPProtoError, resp
- end
- v6 = ["", "", "", "", "", "", "", ""]
- for i in 0 .. 7
- v6[i] = sprintf("%02x%02x", numbers[(i * 2) + 2].to_i,
- numbers[(i * 2) + 3].to_i)
- end
- host = v6[0, 8].join(":")
- port = (numbers[19].to_i << 8) + numbers[20].to_i
- end
- return host, port
- end
- private :parse228
-
- def parse229(resp)
- if resp[0, 3] != "229"
- raise FTPReplyError, resp
- end
- left = resp.index("(")
- right = resp.index(")")
- if left == nil or right == nil
- raise FTPProtoError, resp
- end
- numbers = resp[left + 1 .. right - 1].split(resp[left + 1, 1])
- if numbers.length != 4
- raise FTPProtoError, resp
- end
- port = numbers[3].to_i
- host = (@sock.peeraddr())[3]
- return host, port
- end
- private :parse229
-
- def parse257(resp)
- if resp[0, 3] != "257"
- raise FTPReplyError, resp
- end
- if resp[3, 2] != ' "'
- return ""
- end
- dirname = ""
- i = 5
- n = resp.length
- while i < n
- c = resp[i, 1]
- i = i + 1
- if c == '"'
- if i > n or resp[i, 1] != '"'
- break
- end
- i = i + 1
- end
- dirname = dirname + c
- end
- return dirname
- end
- private :parse257
- end
-
-end
-
-
-# Documentation comments:
-# - sourced from pickaxe and nutshell, with improvements (hopefully)
-# - three methods should be private (search WRITEME)
-# - two methods need more information (search TODO)
diff --git a/lib/net/http.rb b/lib/net/http.rb
index 8e0c3b208f..53295fe90c 100644
--- a/lib/net/http.rb
+++ b/lib/net/http.rb
@@ -1,32 +1,29 @@
+# frozen_string_literal: true
#
# = net/http.rb
#
# Copyright (c) 1999-2007 Yukihiro Matsumoto
# Copyright (c) 1999-2007 Minero Aoki
# Copyright (c) 2001 GOTOU Yuuzou
-#
+#
# Written and maintained by Minero Aoki <aamine@loveruby.net>.
# HTTPS support added by GOTOU Yuuzou <gotoyuzo@notwork.org>.
#
# This file is derived from "http-access.rb".
#
# Documented by Minero Aoki; converted to RDoc by William Webber.
-#
+#
# This program is free software. You can re-distribute and/or
# modify this program under the same terms of ruby itself ---
# Ruby Distribution License or GNU General Public License.
#
-# See Net::HTTP for an overview and examples.
-#
-# NOTE: You can find Japanese version of this document here:
-# http://www.ruby-lang.org/ja/man/html/net_http.html
-#
-#--
-# $Id$
-#++
+# See Net::HTTP for an overview and examples.
+#
require 'net/protocol'
require 'uri'
+require 'resolv'
+autoload :OpenSSL, 'openssl'
module Net #:nodoc:
@@ -35,295 +32,721 @@ module Net #:nodoc:
class HTTPHeaderSyntaxError < StandardError; end
# :startdoc:
- # == What Is This Library?
- #
- # This library provides your program functions to access WWW
- # documents via HTTP, Hyper Text Transfer Protocol version 1.1.
- # For details of HTTP, refer [RFC2616]
- # (http://www.ietf.org/rfc/rfc2616.txt).
- #
- # == Examples
- #
- # === Getting Document From WWW Server
- #
- # Example #1: Simple GET+print
- #
- # require 'net/http'
- # Net::HTTP.get_print 'www.example.com', '/index.html'
- #
- # Example #2: Simple GET+print by URL
- #
- # require 'net/http'
- # require 'uri'
- # Net::HTTP.get_print URI.parse('http://www.example.com/index.html')
- #
- # Example #3: More generic GET+print
- #
- # require 'net/http'
- # require 'uri'
- #
- # url = URI.parse('http://www.example.com/index.html')
- # res = Net::HTTP.start(url.host, url.port) {|http|
- # http.get('/index.html')
- # }
- # puts res.body
- #
- # Example #4: More generic GET+print
- #
- # require 'net/http'
- #
- # url = URI.parse('http://www.example.com/index.html')
- # req = Net::HTTP::Get.new(url.path)
- # res = Net::HTTP.start(url.host, url.port) {|http|
- # http.request(req)
- # }
- # puts res.body
- #
- # === Posting Form Data
- #
- # require 'net/http'
- # require 'uri'
- #
- # #1: Simple POST
- # res = Net::HTTP.post_form(URI.parse('http://www.example.com/search.cgi'),
- # {'q' => 'ruby', 'max' => '50'})
- # puts res.body
- #
- # #2: POST with basic authentication
- # res = Net::HTTP.post_form(URI.parse('http://jack:pass@www.example.com/todo.cgi'),
- # {'from' => '2005-01-01',
- # 'to' => '2005-03-31'})
- # puts res.body
- #
- # #3: Detailed control
- # url = URI.parse('http://www.example.com/todo.cgi')
- # req = Net::HTTP::Post.new(url.path)
- # req.basic_auth 'jack', 'pass'
- # req.set_form_data({'from' => '2005-01-01', 'to' => '2005-03-31'}, ';')
- # res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }
+ # \Class \Net::HTTP provides a rich library that implements the client
+ # in a client-server model that uses the \HTTP request-response protocol.
+ # For information about \HTTP, see:
+ #
+ # - {Hypertext Transfer Protocol}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol].
+ # - {Technology}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Technology].
+ #
+ # == About the Examples
+ #
+ # :include: doc/net-http/examples.rdoc
+ #
+ # == Strategies
+ #
+ # - If you will make only a few GET requests,
+ # consider using {OpenURI}[rdoc-ref:OpenURI].
+ # - If you will make only a few requests of all kinds,
+ # consider using the various singleton convenience methods in this class.
+ # Each of the following methods automatically starts and finishes
+ # a {session}[rdoc-ref:Net::HTTP@Sessions] that sends a single request:
+ #
+ # # Return string response body.
+ # Net::HTTP.get(hostname, path)
+ # Net::HTTP.get(uri)
+ #
+ # # Write string response body to $stdout.
+ # Net::HTTP.get_print(hostname, path)
+ # Net::HTTP.get_print(uri)
+ #
+ # # Return response as Net::HTTPResponse object.
+ # Net::HTTP.get_response(hostname, path)
+ # Net::HTTP.get_response(uri)
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # Net::HTTP.post(uri, data)
+ # params = {title: 'foo', body: 'bar', userId: 1}
+ # Net::HTTP.post_form(uri, params)
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # Net::HTTP.put(uri, data)
+ #
+ # - If performance is important, consider using sessions, which lower request overhead.
+ # This {session}[rdoc-ref:Net::HTTP@Sessions] has multiple requests for
+ # {HTTP methods}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Method]
+ # and {WebDAV methods}[https://en.wikipedia.org/wiki/WebDAV#Implementation]:
+ #
+ # Net::HTTP.start(hostname) do |http|
+ # # Session started automatically before block execution.
+ # http.get(path)
+ # http.head(path)
+ # body = 'Some text'
+ # http.post(path, body) # Can also have a block.
+ # http.put(path, body)
+ # http.delete(path)
+ # http.options(path)
+ # http.trace(path)
+ # http.patch(path, body) # Can also have a block.
+ # http.copy(path)
+ # http.lock(path, body)
+ # http.mkcol(path, body)
+ # http.move(path)
+ # http.propfind(path, body)
+ # http.proppatch(path, body)
+ # http.unlock(path, body)
+ # # Session finished automatically at block exit.
+ # end
+ #
+ # The methods cited above are convenience methods that, via their few arguments,
+ # allow minimal control over the requests.
+ # For greater control, consider using {request objects}[rdoc-ref:Net::HTTPRequest].
+ #
+ # == URIs
+ #
+ # On the internet, a URI
+ # ({Universal Resource Identifier}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier])
+ # is a string that identifies a particular resource.
+ # It consists of some or all of: scheme, hostname, path, query, and fragment;
+ # see {URI syntax}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax].
+ #
+ # A Ruby {URI::Generic}[rdoc-ref:URI::Generic] object
+ # represents an internet URI.
+ # It provides, among others, methods
+ # +scheme+, +hostname+, +path+, +query+, and +fragment+.
+ #
+ # === Schemes
+ #
+ # An internet \URI has
+ # a {scheme}[https://en.wikipedia.org/wiki/List_of_URI_schemes].
+ #
+ # The two schemes supported in \Net::HTTP are <tt>'https'</tt> and <tt>'http'</tt>:
+ #
+ # uri.scheme # => "https"
+ # URI('http://example.com').scheme # => "http"
+ #
+ # === Hostnames
+ #
+ # A hostname identifies a server (host) to which requests may be sent:
+ #
+ # hostname = uri.hostname # => "jsonplaceholder.typicode.com"
+ # Net::HTTP.start(hostname) do |http|
+ # # Some HTTP stuff.
+ # end
+ #
+ # === Paths
+ #
+ # A host-specific path identifies a resource on the host:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/todos/1'
+ # hostname = _uri.hostname
+ # path = _uri.path
+ # Net::HTTP.get(hostname, path)
+ #
+ # === Queries
+ #
+ # A host-specific query adds name/value pairs to the URI:
+ #
+ # _uri = uri.dup
+ # params = {userId: 1, completed: false}
+ # _uri.query = URI.encode_www_form(params)
+ # _uri # => #<URI::HTTPS https://jsonplaceholder.typicode.com?userId=1&completed=false>
+ # Net::HTTP.get(_uri)
+ #
+ # === Fragments
+ #
+ # A {URI fragment}[https://en.wikipedia.org/wiki/URI_fragment] has no effect
+ # in \Net::HTTP;
+ # the same data is returned, regardless of whether a fragment is included.
+ #
+ # == Request Headers
+ #
+ # Request headers may be used to pass additional information to the host,
+ # similar to arguments passed in a method call;
+ # each header is a name/value pair.
+ #
+ # Each of the \Net::HTTP methods that sends a request to the host
+ # has optional argument +headers+,
+ # where the headers are expressed as a hash of field-name/value pairs:
+ #
+ # headers = {Accept: 'application/json', Connection: 'Keep-Alive'}
+ # Net::HTTP.get(uri, headers)
+ #
+ # See lists of both standard request fields and common request fields at
+ # {Request Fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields].
+ # A host may also accept other custom fields.
+ #
+ # == \HTTP Sessions
+ #
+ # A _session_ is a connection between a server (host) and a client that:
+ #
+ # - Is begun by instance method Net::HTTP#start.
+ # - May contain any number of requests.
+ # - Is ended by instance method Net::HTTP#finish.
+ #
+ # See example sessions at {Strategies}[rdoc-ref:Net::HTTP@Strategies].
+ #
+ # === Session Using \Net::HTTP.start
+ #
+ # If you have many requests to make to a single host (and port),
+ # consider using singleton method Net::HTTP.start with a block;
+ # the method handles the session automatically by:
+ #
+ # - Calling #start before block execution.
+ # - Executing the block.
+ # - Calling #finish after block execution.
+ #
+ # In the block, you can use these instance methods,
+ # each of which that sends a single request:
+ #
+ # - {HTTP methods}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Method]:
+ #
+ # - #get, #request_get: GET.
+ # - #head, #request_head: HEAD.
+ # - #post, #request_post: POST.
+ # - #delete: DELETE.
+ # - #options: OPTIONS.
+ # - #trace: TRACE.
+ # - #patch: PATCH.
+ #
+ # - {WebDAV methods}[https://en.wikipedia.org/wiki/WebDAV#Implementation]:
+ #
+ # - #copy: COPY.
+ # - #lock: LOCK.
+ # - #mkcol: MKCOL.
+ # - #move: MOVE.
+ # - #propfind: PROPFIND.
+ # - #proppatch: PROPPATCH.
+ # - #unlock: UNLOCK.
+ #
+ # === Session Using \Net::HTTP.start and \Net::HTTP.finish
+ #
+ # You can manage a session manually using methods #start and #finish:
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.start
+ # http.get('/todos/1')
+ # http.get('/todos/2')
+ # http.delete('/posts/1')
+ # http.finish # Needed to free resources.
+ #
+ # === Single-Request Session
+ #
+ # Certain convenience methods automatically handle a session by:
+ #
+ # - Creating an \HTTP object
+ # - Starting a session.
+ # - Sending a single request.
+ # - Finishing the session.
+ # - Destroying the object.
+ #
+ # Such methods that send GET requests:
+ #
+ # - ::get: Returns the string response body.
+ # - ::get_print: Writes the string response body to $stdout.
+ # - ::get_response: Returns a Net::HTTPResponse object.
+ #
+ # Such methods that send POST requests:
+ #
+ # - ::post: Posts data to the host.
+ # - ::post_form: Posts form data to the host.
+ #
+ # == \HTTP Requests and Responses
+ #
+ # Many of the methods above are convenience methods,
+ # each of which sends a request and returns a string
+ # without directly using \Net::HTTPRequest and \Net::HTTPResponse objects.
+ #
+ # You can, however, directly create a request object, send the request,
+ # and retrieve the response object; see:
+ #
+ # - Net::HTTPRequest.
+ # - Net::HTTPResponse.
+ #
+ # == Following Redirection
+ #
+ # Each returned response is an instance of a subclass of Net::HTTPResponse.
+ # See the {response class hierarchy}[rdoc-ref:Net::HTTPResponse@Response+Subclasses].
+ #
+ # In particular, class Net::HTTPRedirection is the parent
+ # of all redirection classes.
+ # This allows you to craft a case statement to handle redirections properly:
+ #
+ # def fetch(uri, limit = 10)
+ # # You should choose a better exception.
+ # raise ArgumentError, 'Too many HTTP redirects' if limit == 0
+ #
+ # res = Net::HTTP.get_response(URI(uri))
# case res
- # when Net::HTTPSuccess, Net::HTTPRedirection
- # # OK
- # else
- # res.error!
+ # when Net::HTTPSuccess # Any success class.
+ # res
+ # when Net::HTTPRedirection # Any redirection class.
+ # location = res['Location']
+ # warn "Redirected to #{location}"
+ # fetch(location, limit - 1)
+ # else # Any other class.
+ # res.value
# end
+ # end
#
- # #4: Multiple values
- # res = Net::HTTP.post_form(URI.parse('http://www.example.com/search.cgi'),
- # {'q' => ['ruby', 'perl'], 'max' => '50'})
- # puts res.body
- #
- # === Accessing via Proxy
- #
- # Net::HTTP.Proxy creates http proxy class. It has same
- # methods of Net::HTTP but its instances always connect to
- # proxy, instead of given host.
- #
- # require 'net/http'
- #
- # proxy_addr = 'your.proxy.host'
- # proxy_port = 8080
- # :
- # Net::HTTP::Proxy(proxy_addr, proxy_port).start('www.example.com') {|http|
- # # always connect to your.proxy.addr:8080
- # :
- # }
- #
- # Since Net::HTTP.Proxy returns Net::HTTP itself when proxy_addr is nil,
- # there's no need to change code if there's proxy or not.
- #
- # There are two additional parameters in Net::HTTP.Proxy which allow to
- # specify proxy user name and password:
- #
- # Net::HTTP::Proxy(proxy_addr, proxy_port, proxy_user = nil, proxy_pass = nil)
- #
- # You may use them to work with authorization-enabled proxies:
- #
- # require 'net/http'
- # require 'uri'
- #
- # proxy_host = 'your.proxy.host'
- # proxy_port = 8080
- # uri = URI.parse(ENV['http_proxy'])
- # proxy_user, proxy_pass = uri.userinfo.split(/:/) if uri.userinfo
- # Net::HTTP::Proxy(proxy_host, proxy_port,
- # proxy_user, proxy_pass).start('www.example.com') {|http|
- # # always connect to your.proxy.addr:8080 using specified username and password
- # :
- # }
- #
- # Note that net/http never rely on HTTP_PROXY environment variable.
- # If you want to use proxy, set it explicitly.
- #
- # === Following Redirection
- #
- # require 'net/http'
- # require 'uri'
- #
- # def fetch(uri_str, limit = 10)
- # # You should choose better exception.
- # raise ArgumentError, 'HTTP redirect too deep' if limit == 0
- #
- # response = Net::HTTP.get_response(URI.parse(uri_str))
- # case response
- # when Net::HTTPSuccess then response
- # when Net::HTTPRedirection then fetch(response['location'], limit - 1)
- # else
- # response.error!
+ # fetch(uri)
+ #
+ # == Basic Authentication
+ #
+ # Basic authentication is performed according to
+ # {RFC2617}[http://www.ietf.org/rfc/rfc2617.txt]:
+ #
+ # req = Net::HTTP::Get.new(uri)
+ # req.basic_auth('user', 'pass')
+ # res = Net::HTTP.start(hostname) do |http|
+ # http.request(req)
+ # end
+ #
+ # == Streaming Response Bodies
+ #
+ # By default \Net::HTTP reads an entire response into memory. If you are
+ # handling large files or wish to implement a progress bar you can instead
+ # stream the body directly to an IO.
+ #
+ # Net::HTTP.start(hostname) do |http|
+ # req = Net::HTTP::Get.new(uri)
+ # http.request(req) do |res|
+ # open('t.tmp', 'w') do |f|
+ # res.read_body do |chunk|
+ # f.write chunk
+ # end
# end
# end
- #
- # print fetch('http://www.ruby-lang.org')
- #
- # Net::HTTPSuccess and Net::HTTPRedirection is a HTTPResponse class.
- # All HTTPResponse objects belong to its own response class which
- # indicate HTTP result status. For details of response classes,
- # see section "HTTP Response Classes".
- #
- # === Basic Authentication
- #
- # require 'net/http'
- #
- # Net::HTTP.start('www.example.com') {|http|
- # req = Net::HTTP::Get.new('/secret-page.html')
- # req.basic_auth 'account', 'password'
- # response = http.request(req)
- # print response.body
- # }
- #
- # === HTTP Request Classes
- #
- # Here is HTTP request class hierarchy.
- #
- # Net::HTTPRequest
- # Net::HTTP::Get
- # Net::HTTP::Head
- # Net::HTTP::Post
- # Net::HTTP::Put
- # Net::HTTP::Proppatch
- # Net::HTTP::Lock
- # Net::HTTP::Unlock
- # Net::HTTP::Options
- # Net::HTTP::Propfind
- # Net::HTTP::Delete
- # Net::HTTP::Move
- # Net::HTTP::Copy
- # Net::HTTP::Mkcol
- # Net::HTTP::Trace
- #
- # === HTTP Response Classes
- #
- # Here is HTTP response class hierarchy.
- # All classes are defined in Net module.
- #
- # HTTPResponse
- # HTTPUnknownResponse
- # HTTPInformation # 1xx
- # HTTPContinue # 100
- # HTTPSwitchProtocl # 101
- # HTTPSuccess # 2xx
- # HTTPOK # 200
- # HTTPCreated # 201
- # HTTPAccepted # 202
- # HTTPNonAuthoritativeInformation # 203
- # HTTPNoContent # 204
- # HTTPResetContent # 205
- # HTTPPartialContent # 206
- # HTTPRedirection # 3xx
- # HTTPMultipleChoice # 300
- # HTTPMovedPermanently # 301
- # HTTPFound # 302
- # HTTPSeeOther # 303
- # HTTPNotModified # 304
- # HTTPUseProxy # 305
- # HTTPTemporaryRedirect # 307
- # HTTPClientError # 4xx
- # HTTPBadRequest # 400
- # HTTPUnauthorized # 401
- # HTTPPaymentRequired # 402
- # HTTPForbidden # 403
- # HTTPNotFound # 404
- # HTTPMethodNotAllowed # 405
- # HTTPNotAcceptable # 406
- # HTTPProxyAuthenticationRequired # 407
- # HTTPRequestTimeOut # 408
- # HTTPConflict # 409
- # HTTPGone # 410
- # HTTPLengthRequired # 411
- # HTTPPreconditionFailed # 412
- # HTTPRequestEntityTooLarge # 413
- # HTTPRequestURITooLong # 414
- # HTTPUnsupportedMediaType # 415
- # HTTPRequestedRangeNotSatisfiable # 416
- # HTTPExpectationFailed # 417
- # HTTPServerError # 5xx
- # HTTPInternalServerError # 500
- # HTTPNotImplemented # 501
- # HTTPBadGateway # 502
- # HTTPServiceUnavailable # 503
- # HTTPGatewayTimeOut # 504
- # HTTPVersionNotSupported # 505
- #
- # == Switching Net::HTTP versions
- #
- # You can use net/http.rb 1.1 features (bundled with Ruby 1.6)
- # by calling HTTP.version_1_1. Calling Net::HTTP.version_1_2
- # allows you to use 1.2 features again.
- #
- # # example
- # Net::HTTP.start {|http1| ...(http1 has 1.2 features)... }
- #
- # Net::HTTP.version_1_1
- # Net::HTTP.start {|http2| ...(http2 has 1.1 features)... }
- #
- # Net::HTTP.version_1_2
- # Net::HTTP.start {|http3| ...(http3 has 1.2 features)... }
- #
- # This function is NOT thread-safe.
+ # end
+ #
+ # == HTTPS
+ #
+ # HTTPS is enabled for an \HTTP connection by Net::HTTP#use_ssl=:
+ #
+ # Net::HTTP.start(hostname, :use_ssl => true) do |http|
+ # req = Net::HTTP::Get.new(uri)
+ # res = http.request(req)
+ # end
+ #
+ # Or if you simply want to make a GET request, you may pass in a URI
+ # object that has an \HTTPS URL. \Net::HTTP automatically turns on TLS
+ # verification if the URI object has a 'https' URI scheme:
+ #
+ # uri # => #<URI::HTTPS https://jsonplaceholder.typicode.com/>
+ # Net::HTTP.get(uri)
+ #
+ # == Proxy Server
+ #
+ # An \HTTP object can have
+ # a {proxy server}[https://en.wikipedia.org/wiki/Proxy_server].
+ #
+ # You can create an \HTTP object with a proxy server
+ # using method Net::HTTP.new or method Net::HTTP.start.
+ #
+ # The proxy may be defined either by argument +p_addr+
+ # or by environment variable <tt>'http_proxy'</tt>.
+ #
+ # === Proxy Using Argument +p_addr+ as a \String
+ #
+ # When argument +p_addr+ is a string hostname,
+ # the returned +http+ has the given host as its proxy:
+ #
+ # http = Net::HTTP.new(hostname, nil, 'proxy.example')
+ # http.proxy? # => true
+ # http.proxy_from_env? # => false
+ # http.proxy_address # => "proxy.example"
+ # # These use default values.
+ # http.proxy_port # => 80
+ # http.proxy_user # => nil
+ # http.proxy_pass # => nil
+ #
+ # The port, username, and password for the proxy may also be given:
+ #
+ # http = Net::HTTP.new(hostname, nil, 'proxy.example', 8000, 'pname', 'ppass')
+ # # => #<Net::HTTP jsonplaceholder.typicode.com:80 open=false>
+ # http.proxy? # => true
+ # http.proxy_from_env? # => false
+ # http.proxy_address # => "proxy.example"
+ # http.proxy_port # => 8000
+ # http.proxy_user # => "pname"
+ # http.proxy_pass # => "ppass"
+ #
+ # === Proxy Using '<tt>ENV['http_proxy']</tt>'
+ #
+ # When environment variable <tt>'http_proxy'</tt>
+ # is set to a \URI string,
+ # the returned +http+ will have the server at that URI as its proxy;
+ # note that the \URI string must have a protocol
+ # such as <tt>'http'</tt> or <tt>'https'</tt>:
+ #
+ # ENV['http_proxy'] = 'http://example.com'
+ # http = Net::HTTP.new(hostname)
+ # http.proxy? # => true
+ # http.proxy_from_env? # => true
+ # http.proxy_address # => "example.com"
+ # # These use default values.
+ # http.proxy_port # => 80
+ # http.proxy_user # => nil
+ # http.proxy_pass # => nil
+ #
+ # The \URI string may include proxy username, password, and port number:
+ #
+ # ENV['http_proxy'] = 'http://pname:ppass@example.com:8000'
+ # http = Net::HTTP.new(hostname)
+ # http.proxy? # => true
+ # http.proxy_from_env? # => true
+ # http.proxy_address # => "example.com"
+ # http.proxy_port # => 8000
+ # http.proxy_user # => "pname"
+ # http.proxy_pass # => "ppass"
+ #
+ # === Filtering Proxies
+ #
+ # With method Net::HTTP.new (but not Net::HTTP.start),
+ # you can use argument +p_no_proxy+ to filter proxies:
+ #
+ # - Reject a certain address:
+ #
+ # http = Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example')
+ # http.proxy_address # => nil
+ #
+ # - Reject certain domains or subdomains:
+ #
+ # http = Net::HTTP.new('example.com', nil, 'my.proxy.example', 8000, 'pname', 'ppass', 'proxy.example')
+ # http.proxy_address # => nil
+ #
+ # - Reject certain addresses and port combinations:
+ #
+ # http = Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example:1234')
+ # http.proxy_address # => "proxy.example"
+ #
+ # http = Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example:8000')
+ # http.proxy_address # => nil
+ #
+ # - Reject a list of the types above delimited using a comma:
+ #
+ # http = Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'my.proxy,proxy.example:8000')
+ # http.proxy_address # => nil
+ #
+ # http = Net::HTTP.new('example.com', nil, 'my.proxy', 8000, 'pname', 'ppass', 'my.proxy,proxy.example:8000')
+ # http.proxy_address # => nil
+ #
+ # == Compression and Decompression
+ #
+ # \Net::HTTP does not compress the body of a request before sending.
+ #
+ # By default, \Net::HTTP adds header <tt>'Accept-Encoding'</tt>
+ # to a new {request object}[rdoc-ref:Net::HTTPRequest]:
+ #
+ # Net::HTTP::Get.new(uri)['Accept-Encoding']
+ # # => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
+ #
+ # This requests the server to zip-encode the response body if there is one;
+ # the server is not required to do so.
+ #
+ # \Net::HTTP does not automatically decompress a response body
+ # if the response has header <tt>'Content-Range'</tt>.
+ #
+ # Otherwise decompression (or not) depends on the value of header
+ # {Content-Encoding}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Content-Encoding_2]:
+ #
+ # - <tt>'deflate'</tt>, <tt>'gzip'</tt>, or <tt>'x-gzip'</tt>:
+ # decompresses the body and deletes the header.
+ # - <tt>'none'</tt> or <tt>'identity'</tt>:
+ # does not decompress the body, but deletes the header.
+ # - Any other value:
+ # leaves the body and header unchanged.
+ #
+ # == What's Here
+ #
+ # First, what's elsewhere. Class Net::HTTP:
+ #
+ # - Inherits from {class Object}[rdoc-ref:Object#class-object-whats-here].
+ #
+ # This is a categorized summary of methods and attributes.
+ #
+ # === \Net::HTTP Objects
+ #
+ # - {::new}[rdoc-ref:Net::HTTP.new]:
+ # Creates a new instance.
+ # - {#inspect}[rdoc-ref:Net::HTTP#inspect]:
+ # Returns a string representation of +self+.
+ #
+ # === Sessions
+ #
+ # - {::start}[rdoc-ref:Net::HTTP.start]:
+ # Begins a new session in a new \Net::HTTP object.
+ # - {#started?}[rdoc-ref:Net::HTTP#started?]:
+ # Returns whether in a session.
+ # - {#finish}[rdoc-ref:Net::HTTP#finish]:
+ # Ends an active session.
+ # - {#start}[rdoc-ref:Net::HTTP#start]:
+ # Begins a new session in an existing \Net::HTTP object (+self+).
+ #
+ # === Connections
+ #
+ # - {:continue_timeout}[rdoc-ref:Net::HTTP#continue_timeout]:
+ # Returns the continue timeout.
+ # - {#continue_timeout=}[rdoc-ref:Net::HTTP#continue_timeout=]:
+ # Sets the continue timeout seconds.
+ # - {:keep_alive_timeout}[rdoc-ref:Net::HTTP#keep_alive_timeout]:
+ # Returns the keep-alive timeout.
+ # - {:keep_alive_timeout=}[rdoc-ref:Net::HTTP#keep_alive_timeout=]:
+ # Sets the keep-alive timeout.
+ # - {:max_retries}[rdoc-ref:Net::HTTP#max_retries]:
+ # Returns the maximum retries.
+ # - {#max_retries=}[rdoc-ref:Net::HTTP#max_retries=]:
+ # Sets the maximum retries.
+ # - {:open_timeout}[rdoc-ref:Net::HTTP#open_timeout]:
+ # Returns the open timeout.
+ # - {:open_timeout=}[rdoc-ref:Net::HTTP#open_timeout=]:
+ # Sets the open timeout.
+ # - {:read_timeout}[rdoc-ref:Net::HTTP#read_timeout]:
+ # Returns the open timeout.
+ # - {:read_timeout=}[rdoc-ref:Net::HTTP#read_timeout=]:
+ # Sets the read timeout.
+ # - {:ssl_timeout}[rdoc-ref:Net::HTTP#ssl_timeout]:
+ # Returns the ssl timeout.
+ # - {:ssl_timeout=}[rdoc-ref:Net::HTTP#ssl_timeout=]:
+ # Sets the ssl timeout.
+ # - {:write_timeout}[rdoc-ref:Net::HTTP#write_timeout]:
+ # Returns the write timeout.
+ # - {write_timeout=}[rdoc-ref:Net::HTTP#write_timeout=]:
+ # Sets the write timeout.
+ #
+ # === Requests
+ #
+ # - {::get}[rdoc-ref:Net::HTTP.get]:
+ # Sends a GET request and returns the string response body.
+ # - {::get_print}[rdoc-ref:Net::HTTP.get_print]:
+ # Sends a GET request and write the string response body to $stdout.
+ # - {::get_response}[rdoc-ref:Net::HTTP.get_response]:
+ # Sends a GET request and returns a response object.
+ # - {::post_form}[rdoc-ref:Net::HTTP.post_form]:
+ # Sends a POST request with form data and returns a response object.
+ # - {::post}[rdoc-ref:Net::HTTP.post]:
+ # Sends a POST request with data and returns a response object.
+ # - {::put}[rdoc-ref:Net::HTTP.put]:
+ # Sends a PUT request with data and returns a response object.
+ # - {#copy}[rdoc-ref:Net::HTTP#copy]:
+ # Sends a COPY request and returns a response object.
+ # - {#delete}[rdoc-ref:Net::HTTP#delete]:
+ # Sends a DELETE request and returns a response object.
+ # - {#get}[rdoc-ref:Net::HTTP#get]:
+ # Sends a GET request and returns a response object.
+ # - {#head}[rdoc-ref:Net::HTTP#head]:
+ # Sends a HEAD request and returns a response object.
+ # - {#lock}[rdoc-ref:Net::HTTP#lock]:
+ # Sends a LOCK request and returns a response object.
+ # - {#mkcol}[rdoc-ref:Net::HTTP#mkcol]:
+ # Sends a MKCOL request and returns a response object.
+ # - {#move}[rdoc-ref:Net::HTTP#move]:
+ # Sends a MOVE request and returns a response object.
+ # - {#options}[rdoc-ref:Net::HTTP#options]:
+ # Sends a OPTIONS request and returns a response object.
+ # - {#patch}[rdoc-ref:Net::HTTP#patch]:
+ # Sends a PATCH request and returns a response object.
+ # - {#post}[rdoc-ref:Net::HTTP#post]:
+ # Sends a POST request and returns a response object.
+ # - {#propfind}[rdoc-ref:Net::HTTP#propfind]:
+ # Sends a PROPFIND request and returns a response object.
+ # - {#proppatch}[rdoc-ref:Net::HTTP#proppatch]:
+ # Sends a PROPPATCH request and returns a response object.
+ # - {#put}[rdoc-ref:Net::HTTP#put]:
+ # Sends a PUT request and returns a response object.
+ # - {#request}[rdoc-ref:Net::HTTP#request]:
+ # Sends a request and returns a response object.
+ # - {#request_get}[rdoc-ref:Net::HTTP#request_get]:
+ # Sends a GET request and forms a response object;
+ # if a block given, calls the block with the object,
+ # otherwise returns the object.
+ # - {#request_head}[rdoc-ref:Net::HTTP#request_head]:
+ # Sends a HEAD request and forms a response object;
+ # if a block given, calls the block with the object,
+ # otherwise returns the object.
+ # - {#request_post}[rdoc-ref:Net::HTTP#request_post]:
+ # Sends a POST request and forms a response object;
+ # if a block given, calls the block with the object,
+ # otherwise returns the object.
+ # - {#send_request}[rdoc-ref:Net::HTTP#send_request]:
+ # Sends a request and returns a response object.
+ # - {#trace}[rdoc-ref:Net::HTTP#trace]:
+ # Sends a TRACE request and returns a response object.
+ # - {#unlock}[rdoc-ref:Net::HTTP#unlock]:
+ # Sends an UNLOCK request and returns a response object.
+ #
+ # === Responses
+ #
+ # - {:close_on_empty_response}[rdoc-ref:Net::HTTP#close_on_empty_response]:
+ # Returns whether to close connection on empty response.
+ # - {:close_on_empty_response=}[rdoc-ref:Net::HTTP#close_on_empty_response=]:
+ # Sets whether to close connection on empty response.
+ # - {:ignore_eof}[rdoc-ref:Net::HTTP#ignore_eof]:
+ # Returns whether to ignore end-of-file when reading a response body
+ # with <tt>Content-Length</tt> headers.
+ # - {:ignore_eof=}[rdoc-ref:Net::HTTP#ignore_eof=]:
+ # Sets whether to ignore end-of-file when reading a response body
+ # with <tt>Content-Length</tt> headers.
+ # - {:response_body_encoding}[rdoc-ref:Net::HTTP#response_body_encoding]:
+ # Returns the encoding to use for the response body.
+ # - {#response_body_encoding=}[rdoc-ref:Net::HTTP#response_body_encoding=]:
+ # Sets the response body encoding.
+ #
+ # === Proxies
+ #
+ # - {:proxy_address}[rdoc-ref:Net::HTTP#proxy_address]:
+ # Returns the proxy address.
+ # - {:proxy_address=}[rdoc-ref:Net::HTTP#proxy_address=]:
+ # Sets the proxy address.
+ # - {::proxy_class?}[rdoc-ref:Net::HTTP.proxy_class?]:
+ # Returns whether +self+ is a proxy class.
+ # - {#proxy?}[rdoc-ref:Net::HTTP#proxy?]:
+ # Returns whether +self+ has a proxy.
+ # - {#proxy_address}[rdoc-ref:Net::HTTP#proxy_address]:
+ # Returns the proxy address.
+ # - {#proxy_from_env?}[rdoc-ref:Net::HTTP#proxy_from_env?]:
+ # Returns whether the proxy is taken from an environment variable.
+ # - {:proxy_from_env=}[rdoc-ref:Net::HTTP#proxy_from_env=]:
+ # Sets whether the proxy is to be taken from an environment variable.
+ # - {:proxy_pass}[rdoc-ref:Net::HTTP#proxy_pass]:
+ # Returns the proxy password.
+ # - {:proxy_pass=}[rdoc-ref:Net::HTTP#proxy_pass=]:
+ # Sets the proxy password.
+ # - {:proxy_port}[rdoc-ref:Net::HTTP#proxy_port]:
+ # Returns the proxy port.
+ # - {:proxy_port=}[rdoc-ref:Net::HTTP#proxy_port=]:
+ # Sets the proxy port.
+ # - {#proxy_user}[rdoc-ref:Net::HTTP#proxy_user]:
+ # Returns the proxy user name.
+ # - {:proxy_user=}[rdoc-ref:Net::HTTP#proxy_user=]:
+ # Sets the proxy user.
+ #
+ # === Security
+ #
+ # - {:ca_file}[rdoc-ref:Net::HTTP#ca_file]:
+ # Returns the path to a CA certification file.
+ # - {:ca_file=}[rdoc-ref:Net::HTTP#ca_file=]:
+ # Sets the path to a CA certification file.
+ # - {:ca_path}[rdoc-ref:Net::HTTP#ca_path]:
+ # Returns the path of to CA directory containing certification files.
+ # - {:ca_path=}[rdoc-ref:Net::HTTP#ca_path=]:
+ # Sets the path of to CA directory containing certification files.
+ # - {:cert}[rdoc-ref:Net::HTTP#cert]:
+ # Returns the OpenSSL::X509::Certificate object to be used for client certification.
+ # - {:cert=}[rdoc-ref:Net::HTTP#cert=]:
+ # Sets the OpenSSL::X509::Certificate object to be used for client certification.
+ # - {:cert_store}[rdoc-ref:Net::HTTP#cert_store]:
+ # Returns the X509::Store to be used for verifying peer certificate.
+ # - {:cert_store=}[rdoc-ref:Net::HTTP#cert_store=]:
+ # Sets the X509::Store to be used for verifying peer certificate.
+ # - {:ciphers}[rdoc-ref:Net::HTTP#ciphers]:
+ # Returns the available SSL ciphers.
+ # - {:ciphers=}[rdoc-ref:Net::HTTP#ciphers=]:
+ # Sets the available SSL ciphers.
+ # - {:extra_chain_cert}[rdoc-ref:Net::HTTP#extra_chain_cert]:
+ # Returns the extra X509 certificates to be added to the certificate chain.
+ # - {:extra_chain_cert=}[rdoc-ref:Net::HTTP#extra_chain_cert=]:
+ # Sets the extra X509 certificates to be added to the certificate chain.
+ # - {:key}[rdoc-ref:Net::HTTP#key]:
+ # Returns the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
+ # - {:key=}[rdoc-ref:Net::HTTP#key=]:
+ # Sets the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
+ # - {:max_version}[rdoc-ref:Net::HTTP#max_version]:
+ # Returns the maximum SSL version.
+ # - {:max_version=}[rdoc-ref:Net::HTTP#max_version=]:
+ # Sets the maximum SSL version.
+ # - {:min_version}[rdoc-ref:Net::HTTP#min_version]:
+ # Returns the minimum SSL version.
+ # - {:min_version=}[rdoc-ref:Net::HTTP#min_version=]:
+ # Sets the minimum SSL version.
+ # - {#peer_cert}[rdoc-ref:Net::HTTP#peer_cert]:
+ # Returns the X509 certificate chain for the session's socket peer.
+ # - {:ssl_version}[rdoc-ref:Net::HTTP#ssl_version]:
+ # Returns the SSL version.
+ # - {:ssl_version=}[rdoc-ref:Net::HTTP#ssl_version=]:
+ # Sets the SSL version.
+ # - {#use_ssl=}[rdoc-ref:Net::HTTP#use_ssl=]:
+ # Sets whether a new session is to use Transport Layer Security.
+ # - {#use_ssl?}[rdoc-ref:Net::HTTP#use_ssl?]:
+ # Returns whether +self+ uses SSL.
+ # - {:verify_callback}[rdoc-ref:Net::HTTP#verify_callback]:
+ # Returns the callback for the server certification verification.
+ # - {:verify_callback=}[rdoc-ref:Net::HTTP#verify_callback=]:
+ # Sets the callback for the server certification verification.
+ # - {:verify_depth}[rdoc-ref:Net::HTTP#verify_depth]:
+ # Returns the maximum depth for the certificate chain verification.
+ # - {:verify_depth=}[rdoc-ref:Net::HTTP#verify_depth=]:
+ # Sets the maximum depth for the certificate chain verification.
+ # - {:verify_hostname}[rdoc-ref:Net::HTTP#verify_hostname]:
+ # Returns the flags for server the certification verification at the beginning of the SSL/TLS session.
+ # - {:verify_hostname=}[rdoc-ref:Net::HTTP#verify_hostname=]:
+ # Sets he flags for server the certification verification at the beginning of the SSL/TLS session.
+ # - {:verify_mode}[rdoc-ref:Net::HTTP#verify_mode]:
+ # Returns the flags for server the certification verification at the beginning of the SSL/TLS session.
+ # - {:verify_mode=}[rdoc-ref:Net::HTTP#verify_mode=]:
+ # Sets the flags for server the certification verification at the beginning of the SSL/TLS session.
+ #
+ # === Addresses and Ports
+ #
+ # - {:address}[rdoc-ref:Net::HTTP#address]:
+ # Returns the string host name or host IP.
+ # - {::default_port}[rdoc-ref:Net::HTTP.default_port]:
+ # Returns integer 80, the default port to use for HTTP requests.
+ # - {::http_default_port}[rdoc-ref:Net::HTTP.http_default_port]:
+ # Returns integer 80, the default port to use for HTTP requests.
+ # - {::https_default_port}[rdoc-ref:Net::HTTP.https_default_port]:
+ # Returns integer 443, the default port to use for HTTPS requests.
+ # - {#ipaddr}[rdoc-ref:Net::HTTP#ipaddr]:
+ # Returns the IP address for the connection.
+ # - {#ipaddr=}[rdoc-ref:Net::HTTP#ipaddr=]:
+ # Sets the IP address for the connection.
+ # - {:local_host}[rdoc-ref:Net::HTTP#local_host]:
+ # Returns the string local host used to establish the connection.
+ # - {:local_host=}[rdoc-ref:Net::HTTP#local_host=]:
+ # Sets the string local host used to establish the connection.
+ # - {:local_port}[rdoc-ref:Net::HTTP#local_port]:
+ # Returns the integer local port used to establish the connection.
+ # - {:local_port=}[rdoc-ref:Net::HTTP#local_port=]:
+ # Sets the integer local port used to establish the connection.
+ # - {:port}[rdoc-ref:Net::HTTP#port]:
+ # Returns the integer port number.
+ #
+ # === \HTTP Version
+ #
+ # - {::version_1_2?}[rdoc-ref:Net::HTTP.version_1_2?]
+ # (aliased as {::version_1_2}[rdoc-ref:Net::HTTP.version_1_2]):
+ # Returns true; retained for compatibility.
+ #
+ # === Debugging
+ #
+ # - {#set_debug_output}[rdoc-ref:Net::HTTP#set_debug_output]:
+ # Sets the output stream for debugging.
#
class HTTP < Protocol
# :stopdoc:
- Revision = %q$Revision$.split[1]
+ VERSION = "0.9.1"
HTTPVersion = '1.1'
- @newimpl = true
begin
require 'zlib'
- require 'stringio' #for our purposes (unpacking gzip) lump these together
HAVE_ZLIB=true
rescue LoadError
HAVE_ZLIB=false
end
# :startdoc:
- # Turns on net/http 1.2 (ruby 1.8) features.
- # Defaults to ON in ruby 1.8.
- #
- # I strongly recommend to call this method always.
- #
- # require 'net/http'
- # Net::HTTP.version_1_2
- #
+ # Returns +true+; retained for compatibility.
def HTTP.version_1_2
- @newimpl = true
- end
-
- # Turns on net/http 1.1 (ruby 1.6) features.
- # Defaults to OFF in ruby 1.8.
- def HTTP.version_1_1
- @newimpl = false
+ true
end
- # true if net/http is in version 1.2 mode.
- # Defaults to true.
+ # Returns +true+; retained for compatibility.
def HTTP.version_1_2?
- @newimpl
+ true
end
- # true if net/http is in version 1.1 compatible mode.
- # Defaults to true.
- def HTTP.version_1_1?
- not @newimpl
+ # Returns +false+; retained for compatibility.
+ def HTTP.version_1_1? #:nodoc:
+ false
end
class << HTTP
@@ -331,23 +754,14 @@ module Net #:nodoc:
alias is_version_1_2? version_1_2? #:nodoc:
end
+ # :call-seq:
+ # Net::HTTP.get_print(hostname, path, port = 80) -> nil
+ # Net::HTTP:get_print(uri, headers = {}, port = uri.port) -> nil
#
- # short cut methods
- #
-
- #
- # Get body from target and output it to +$stdout+. The
- # target can either be specified as (+uri+), or as
- # (+host+, +path+, +port+ = 80); so:
- #
- # Net::HTTP.get_print URI.parse('http://www.example.com/index.html')
- #
- # or:
- #
- # Net::HTTP.get_print 'www.example.com', '/index.html'
- #
- def HTTP.get_print(uri_or_host, path = nil, port = nil)
- get_response(uri_or_host, path, port) {|res|
+ # Like Net::HTTP.get, but writes the returned body to $stdout;
+ # returns +nil+.
+ def HTTP.get_print(uri_or_host, path_or_headers = nil, port = nil)
+ get_response(uri_or_host, path_or_headers, port) {|res|
res.read_body do |chunk|
$stdout.print chunk
end
@@ -355,85 +769,185 @@ module Net #:nodoc:
nil
end
- # Send a GET request to the target and return the response
- # as a string. The target can either be specified as
- # (+uri+), or as (+host+, +path+, +port+ = 80); so:
- #
- # print Net::HTTP.get(URI.parse('http://www.example.com/index.html'))
+ # :call-seq:
+ # Net::HTTP.get(hostname, path, port = 80) -> body
+ # Net::HTTP:get(uri, headers = {}, port = uri.port) -> body
#
- # or:
+ # Sends a GET request and returns the \HTTP response body as a string.
#
- # print Net::HTTP.get('www.example.com', '/index.html')
+ # With string arguments +hostname+ and +path+:
#
- def HTTP.get(uri_or_host, path = nil, port = nil)
- get_response(uri_or_host, path, port).body
- end
-
- # Send a GET request to the target and return the response
- # as a Net::HTTPResponse object. The target can either be specified as
- # (+uri+), or as (+host+, +path+, +port+ = 80); so:
- #
- # res = Net::HTTP.get_response(URI.parse('http://www.example.com/index.html'))
- # print res.body
+ # hostname = 'jsonplaceholder.typicode.com'
+ # path = '/todos/1'
+ # puts Net::HTTP.get(hostname, path)
+ #
+ # Output:
+ #
+ # {
+ # "userId": 1,
+ # "id": 1,
+ # "title": "delectus aut autem",
+ # "completed": false
+ # }
+ #
+ # With URI object +uri+ and optional hash argument +headers+:
+ #
+ # uri = URI('https://jsonplaceholder.typicode.com/todos/1')
+ # headers = {'Content-type' => 'application/json; charset=UTF-8'}
+ # Net::HTTP.get(uri, headers)
#
- # or:
+ # Related:
#
- # res = Net::HTTP.get_response('www.example.com', '/index.html')
- # print res.body
+ # - Net::HTTP::Get: request class for \HTTP method +GET+.
+ # - Net::HTTP#get: convenience method for \HTTP method +GET+.
#
- def HTTP.get_response(uri_or_host, path = nil, port = nil, &block)
- if path
+ def HTTP.get(uri_or_host, path_or_headers = nil, port = nil)
+ get_response(uri_or_host, path_or_headers, port).body
+ end
+
+ # :call-seq:
+ # Net::HTTP.get_response(hostname, path, port = 80) -> http_response
+ # Net::HTTP:get_response(uri, headers = {}, port = uri.port) -> http_response
+ #
+ # Like Net::HTTP.get, but returns a Net::HTTPResponse object
+ # instead of the body string.
+ def HTTP.get_response(uri_or_host, path_or_headers = nil, port = nil, &block)
+ if path_or_headers && !path_or_headers.is_a?(Hash)
host = uri_or_host
+ path = path_or_headers
new(host, port || HTTP.default_port).start {|http|
return http.request_get(path, &block)
}
else
uri = uri_or_host
- new(uri.host, uri.port).start {|http|
- return http.request_get(uri.request_uri, &block)
+ headers = path_or_headers
+ start(uri.hostname, uri.port,
+ :use_ssl => uri.scheme == 'https') {|http|
+ return http.request_get(uri, headers, &block)
}
end
end
- # Posts HTML form data to the +URL+.
- # Form data must be represented as a Hash of String to String, e.g:
+ # Posts data to a host; returns a Net::HTTPResponse object.
#
- # { "cmd" => "search", "q" => "ruby", "max" => "50" }
+ # Argument +url+ must be a URL;
+ # argument +data+ must be a string:
#
- # This method also does Basic Authentication iff +URL+.user exists.
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # headers = {'content-type': 'application/json'}
+ # res = Net::HTTP.post(_uri, data, headers) # => #<Net::HTTPCreated 201 Created readbody=true>
+ # puts res.body
#
- # Example:
+ # Output:
+ #
+ # {
+ # "title": "foo",
+ # "body": "bar",
+ # "userId": 1,
+ # "id": 101
+ # }
#
- # require 'net/http'
- # require 'uri'
+ # Related:
#
- # HTTP.post_form URI.parse('http://www.example.com/search.cgi'),
- # { "q" => "ruby", "max" => "50" }
+ # - Net::HTTP::Post: request class for \HTTP method +POST+.
+ # - Net::HTTP#post: convenience method for \HTTP method +POST+.
+ #
+ def HTTP.post(url, data, header = nil)
+ start(url.hostname, url.port,
+ :use_ssl => url.scheme == 'https' ) {|http|
+ http.post(url, data, header)
+ }
+ end
+
+ # Posts data to a host; returns a Net::HTTPResponse object.
+ #
+ # Argument +url+ must be a URI;
+ # argument +data+ must be a hash:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # data = {title: 'foo', body: 'bar', userId: 1}
+ # res = Net::HTTP.post_form(_uri, data) # => #<Net::HTTPCreated 201 Created readbody=true>
+ # puts res.body
+ #
+ # Output:
+ #
+ # {
+ # "title": "foo",
+ # "body": "bar",
+ # "userId": "1",
+ # "id": 101
+ # }
#
def HTTP.post_form(url, params)
- req = Post.new(url.path)
+ req = Post.new(url)
req.form_data = params
req.basic_auth url.user, url.password if url.user
- new(url.host, url.port).start {|http|
+ start(url.hostname, url.port,
+ :use_ssl => url.scheme == 'https' ) {|http|
http.request(req)
}
end
+ # Sends a PUT request to the server; returns a Net::HTTPResponse object.
+ #
+ # Argument +url+ must be a URL;
+ # argument +data+ must be a string:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # headers = {'content-type': 'application/json'}
+ # res = Net::HTTP.put(_uri, data, headers) # => #<Net::HTTPCreated 201 Created readbody=true>
+ # puts res.body
+ #
+ # Output:
+ #
+ # {
+ # "title": "foo",
+ # "body": "bar",
+ # "userId": 1,
+ # "id": 101
+ # }
#
- # HTTP session management
+ # Related:
#
+ # - Net::HTTP::Put: request class for \HTTP method +PUT+.
+ # - Net::HTTP#put: convenience method for \HTTP method +PUT+.
+ #
+ def HTTP.put(url, data, header = nil)
+ start(url.hostname, url.port,
+ :use_ssl => url.scheme == 'https' ) {|http|
+ http.put(url, data, header)
+ }
+ end
- # The default port to use for HTTP requests; defaults to 80.
+ #
+ # \HTTP session management
+ #
+
+ # Returns integer +80+, the default port to use for \HTTP requests:
+ #
+ # Net::HTTP.default_port # => 80
+ #
def HTTP.default_port
http_default_port()
end
- # The default port to use for HTTP requests; defaults to 80.
+ # Returns integer +80+, the default port to use for \HTTP requests:
+ #
+ # Net::HTTP.http_default_port # => 80
+ #
def HTTP.http_default_port
80
end
- # The default port to use for HTTPS requests; defaults to 443.
+ # Returns integer +443+, the default port to use for HTTPS requests:
+ #
+ # Net::HTTP.https_default_port # => 443
+ #
def HTTP.https_default_port
443
end
@@ -442,119 +956,671 @@ module Net #:nodoc:
BufferedIO
end
- # creates a new Net::HTTP object and opens its TCP connection and
- # HTTP session. If the optional block is given, the newly
- # created Net::HTTP object is passed to it and closed when the
- # block finishes. In this case, the return value of this method
- # is the return value of the block. If no block is given, the
- # return value of this method is the newly created Net::HTTP object
- # itself, and the caller is responsible for closing it upon completion.
- def HTTP.start(address, port = nil, p_addr = nil, p_port = nil, p_user = nil, p_pass = nil, &block) # :yield: +http+
- new(address, port, p_addr, p_port, p_user, p_pass).start(&block)
+ # :call-seq:
+ # HTTP.start(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, opts) -> http
+ # HTTP.start(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, opts) {|http| ... } -> object
+ #
+ # Creates a new \Net::HTTP object, +http+, via \Net::HTTP.new:
+ #
+ # - For arguments +address+ and +port+, see Net::HTTP.new.
+ # - For proxy-defining arguments +p_addr+ through +p_pass+,
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
+ # - For argument +opts+, see below.
+ #
+ # With no block given:
+ #
+ # - Calls <tt>http.start</tt> with no block (see #start),
+ # which opens a TCP connection and \HTTP session.
+ # - Returns +http+.
+ # - The caller should call #finish to close the session:
+ #
+ # http = Net::HTTP.start(hostname)
+ # http.started? # => true
+ # http.finish
+ # http.started? # => false
+ #
+ # With a block given:
+ #
+ # - Calls <tt>http.start</tt> with the block (see #start), which:
+ #
+ # - Opens a TCP connection and \HTTP session.
+ # - Calls the block,
+ # which may make any number of requests to the host.
+ # - Closes the \HTTP session and TCP connection on block exit.
+ # - Returns the block's value +object+.
+ #
+ # - Returns +object+.
+ #
+ # Example:
+ #
+ # hostname = 'jsonplaceholder.typicode.com'
+ # Net::HTTP.start(hostname) do |http|
+ # puts http.get('/todos/1').body
+ # puts http.get('/todos/2').body
+ # end
+ #
+ # Output:
+ #
+ # {
+ # "userId": 1,
+ # "id": 1,
+ # "title": "delectus aut autem",
+ # "completed": false
+ # }
+ # {
+ # "userId": 1,
+ # "id": 2,
+ # "title": "quis ut nam facilis et officia qui",
+ # "completed": false
+ # }
+ #
+ # If the last argument given is a hash, it is the +opts+ hash,
+ # where each key is a method or accessor to be called,
+ # and its value is the value to be set.
+ #
+ # The keys may include:
+ #
+ # - #ca_file
+ # - #ca_path
+ # - #cert
+ # - #cert_store
+ # - #ciphers
+ # - #close_on_empty_response
+ # - +ipaddr+ (calls #ipaddr=)
+ # - #keep_alive_timeout
+ # - #key
+ # - #open_timeout
+ # - #read_timeout
+ # - #ssl_timeout
+ # - #ssl_version
+ # - +use_ssl+ (calls #use_ssl=)
+ # - #verify_callback
+ # - #verify_depth
+ # - #verify_mode
+ # - #write_timeout
+ #
+ # Note: If +port+ is +nil+ and <tt>opts[:use_ssl]</tt> is a truthy value,
+ # the value passed to +new+ is Net::HTTP.https_default_port, not +port+.
+ #
+ def HTTP.start(address, *arg, &block) # :yield: +http+
+ arg.pop if opt = Hash.try_convert(arg[-1])
+ port, p_addr, p_port, p_user, p_pass = *arg
+ p_addr = :ENV if arg.size < 2
+ port = https_default_port if !port && opt && opt[:use_ssl]
+ http = new(address, port, p_addr, p_port, p_user, p_pass)
+ http.ipaddr = opt[:ipaddr] if opt && opt[:ipaddr]
+
+ if opt
+ if opt[:use_ssl]
+ opt = {verify_mode: OpenSSL::SSL::VERIFY_PEER}.update(opt)
+ end
+ http.methods.grep(/\A(\w+)=\z/) do |meth|
+ key = $1.to_sym
+ opt.key?(key) or next
+ http.__send__(meth, opt[key])
+ end
+ end
+
+ http.start(&block)
end
class << HTTP
- alias newobj new
+ alias newobj new # :nodoc:
end
- # Creates a new Net::HTTP object.
- # If +proxy_addr+ is given, creates an Net::HTTP object with proxy support.
- # This method does not open the TCP connection.
- def HTTP.new(address, port = nil, p_addr = nil, p_port = nil, p_user = nil, p_pass = nil)
- h = Proxy(p_addr, p_port, p_user, p_pass).newobj(address, port)
- h.instance_eval {
- @newimpl = ::Net::HTTP.version_1_2?
- }
- h
+ # Returns a new \Net::HTTP object +http+
+ # (but does not open a TCP connection or \HTTP session).
+ #
+ # With only string argument +address+ given
+ # (and <tt>ENV['http_proxy']</tt> undefined or +nil+),
+ # the returned +http+:
+ #
+ # - Has the given address.
+ # - Has the default port number, Net::HTTP.default_port (80).
+ # - Has no proxy.
+ #
+ # Example:
+ #
+ # http = Net::HTTP.new(hostname)
+ # # => #<Net::HTTP jsonplaceholder.typicode.com:80 open=false>
+ # http.address # => "jsonplaceholder.typicode.com"
+ # http.port # => 80
+ # http.proxy? # => false
+ #
+ # With integer argument +port+ also given,
+ # the returned +http+ has the given port:
+ #
+ # http = Net::HTTP.new(hostname, 8000)
+ # # => #<Net::HTTP jsonplaceholder.typicode.com:8000 open=false>
+ # http.port # => 8000
+ #
+ # For proxy-defining arguments +p_addr+ through +p_no_proxy+,
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
+ #
+ def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil, p_use_ssl = nil)
+ http = super address, port
+
+ if proxy_class? then # from Net::HTTP::Proxy()
+ http.proxy_from_env = @proxy_from_env
+ http.proxy_address = @proxy_address
+ http.proxy_port = @proxy_port
+ http.proxy_user = @proxy_user
+ http.proxy_pass = @proxy_pass
+ http.proxy_use_ssl = @proxy_use_ssl
+ elsif p_addr == :ENV then
+ http.proxy_from_env = true
+ else
+ if p_addr && p_no_proxy && !URI::Generic.use_proxy?(address, address, port, p_no_proxy)
+ p_addr = nil
+ p_port = nil
+ end
+ http.proxy_address = p_addr
+ http.proxy_port = p_port || default_port
+ http.proxy_user = p_user
+ http.proxy_pass = p_pass
+ http.proxy_use_ssl = p_use_ssl
+ end
+
+ http
end
- # Creates a new Net::HTTP object for the specified +address+.
- # This method does not open the TCP connection.
- def initialize(address, port = nil)
+ class << HTTP
+ # Allows to set the default configuration that will be used
+ # when creating a new connection.
+ #
+ # Example:
+ #
+ # Net::HTTP.default_configuration = {
+ # read_timeout: 1,
+ # write_timeout: 1
+ # }
+ # http = Net::HTTP.new(hostname)
+ # http.open_timeout # => 60
+ # http.read_timeout # => 1
+ # http.write_timeout # => 1
+ #
+ attr_accessor :default_configuration
+ end
+
+ # Creates a new \Net::HTTP object for the specified server address,
+ # without opening the TCP connection or initializing the \HTTP session.
+ # The +address+ should be a DNS hostname or IP address.
+ def initialize(address, port = nil) # :nodoc:
+ defaults = {
+ keep_alive_timeout: 2,
+ close_on_empty_response: false,
+ open_timeout: 60,
+ read_timeout: 60,
+ write_timeout: 60,
+ continue_timeout: nil,
+ max_retries: 1,
+ debug_output: nil,
+ response_body_encoding: false,
+ ignore_eof: true
+ }
+ options = defaults.merge(self.class.default_configuration || {})
+
@address = address
@port = (port || HTTP.default_port)
+ @ipaddr = nil
+ @local_host = nil
+ @local_port = nil
@curr_http_version = HTTPVersion
- @no_keepalive_server = false
- @close_on_empty_response = false
+ @keep_alive_timeout = options[:keep_alive_timeout]
+ @last_communicated = nil
+ @close_on_empty_response = options[:close_on_empty_response]
@socket = nil
@started = false
- @open_timeout = nil
- @read_timeout = 60
- @debug_output = nil
+ @open_timeout = options[:open_timeout]
+ @read_timeout = options[:read_timeout]
+ @write_timeout = options[:write_timeout]
+ @continue_timeout = options[:continue_timeout]
+ @max_retries = options[:max_retries]
+ @debug_output = options[:debug_output]
+ @response_body_encoding = options[:response_body_encoding]
+ @ignore_eof = options[:ignore_eof]
+ @tcpsocket_supports_open_timeout = nil
+
+ @proxy_from_env = false
+ @proxy_uri = nil
+ @proxy_address = nil
+ @proxy_port = nil
+ @proxy_user = nil
+ @proxy_pass = nil
+ @proxy_use_ssl = nil
+
@use_ssl = false
@ssl_context = nil
- @enable_post_connection_check = true
- @compression = nil
+ @ssl_session = nil
@sspi_enabled = false
- if defined?(SSL_ATTRIBUTES)
- SSL_ATTRIBUTES.each do |name|
- instance_variable_set "@#{name}", nil
- end
+ SSL_IVNAMES.each do |ivname|
+ instance_variable_set ivname, nil
end
end
+ # Returns a string representation of +self+:
+ #
+ # Net::HTTP.new(hostname).inspect
+ # # => "#<Net::HTTP jsonplaceholder.typicode.com:80 open=false>"
+ #
def inspect
"#<#{self.class} #{@address}:#{@port} open=#{started?}>"
end
- # *WARNING* This method causes serious security hole.
+ # *WARNING* This method opens a serious security hole.
# Never use this method in production code.
#
- # Set an output stream for debugging.
- #
- # http = Net::HTTP.new
- # http.set_debug_output $stderr
- # http.start { .... }
+ # Sets the output stream for debugging:
+ #
+ # http = Net::HTTP.new(hostname)
+ # File.open('t.tmp', 'w') do |file|
+ # http.set_debug_output(file)
+ # http.start
+ # http.get('/nosuch/1')
+ # http.finish
+ # end
+ # puts File.read('t.tmp')
+ #
+ # Output:
+ #
+ # opening connection to jsonplaceholder.typicode.com:80...
+ # opened
+ # <- "GET /nosuch/1 HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: jsonplaceholder.typicode.com\r\n\r\n"
+ # -> "HTTP/1.1 404 Not Found\r\n"
+ # -> "Date: Mon, 12 Dec 2022 21:14:11 GMT\r\n"
+ # -> "Content-Type: application/json; charset=utf-8\r\n"
+ # -> "Content-Length: 2\r\n"
+ # -> "Connection: keep-alive\r\n"
+ # -> "X-Powered-By: Express\r\n"
+ # -> "X-Ratelimit-Limit: 1000\r\n"
+ # -> "X-Ratelimit-Remaining: 999\r\n"
+ # -> "X-Ratelimit-Reset: 1670879660\r\n"
+ # -> "Vary: Origin, Accept-Encoding\r\n"
+ # -> "Access-Control-Allow-Credentials: true\r\n"
+ # -> "Cache-Control: max-age=43200\r\n"
+ # -> "Pragma: no-cache\r\n"
+ # -> "Expires: -1\r\n"
+ # -> "X-Content-Type-Options: nosniff\r\n"
+ # -> "Etag: W/\"2-vyGp6PvFo4RvsFtPoIWeCReyIC8\"\r\n"
+ # -> "Via: 1.1 vegur\r\n"
+ # -> "CF-Cache-Status: MISS\r\n"
+ # -> "Server-Timing: cf-q-config;dur=1.3000000762986e-05\r\n"
+ # -> "Report-To: {\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=yOr40jo%2BwS1KHzhTlVpl54beJ5Wx2FcG4gGV0XVrh3X9OlR5q4drUn2dkt5DGO4GDcE%2BVXT7CNgJvGs%2BZleIyMu8CLieFiDIvOviOY3EhHg94m0ZNZgrEdpKD0S85S507l1vsEwEHkoTm%2Ff19SiO\"}],\"group\":\"cf-nel\",\"max_age\":604800}\r\n"
+ # -> "NEL: {\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}\r\n"
+ # -> "Server: cloudflare\r\n"
+ # -> "CF-RAY: 778977dc484ce591-DFW\r\n"
+ # -> "alt-svc: h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400\r\n"
+ # -> "\r\n"
+ # reading 2 bytes...
+ # -> "{}"
+ # read 2 bytes
+ # Conn keep-alive
#
def set_debug_output(output)
- warn 'Net::HTTP#set_debug_output called after HTTP started' if started?
+ warn 'Net::HTTP#set_debug_output called after HTTP started', uplevel: 1 if started?
@debug_output = output
end
- # The host name to connect to.
+ # Returns the string host name or host IP given as argument +address+ in ::new.
attr_reader :address
- # The port number to connect to.
+ # Returns the integer port number given as argument +port+ in ::new.
attr_reader :port
- # Seconds to wait until connection is opened.
- # If the HTTP object cannot open a connection in this many seconds,
- # it raises a TimeoutError exception.
+ # Sets or returns the string local host used to establish the connection;
+ # initially +nil+.
+ attr_accessor :local_host
+
+ # Sets or returns the integer local port used to establish the connection;
+ # initially +nil+.
+ attr_accessor :local_port
+
+ # Returns the encoding to use for the response body;
+ # see #response_body_encoding=.
+ attr_reader :response_body_encoding
+
+ # Sets the encoding to be used for the response body;
+ # returns the encoding.
+ #
+ # The given +value+ may be:
+ #
+ # - An Encoding object.
+ # - The name of an encoding.
+ # - An alias for an encoding name.
+ #
+ # See {Encoding}[rdoc-ref:Encoding].
+ #
+ # Examples:
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.response_body_encoding = Encoding::US_ASCII # => #<Encoding:US-ASCII>
+ # http.response_body_encoding = 'US-ASCII' # => "US-ASCII"
+ # http.response_body_encoding = 'ASCII' # => "ASCII"
+ #
+ def response_body_encoding=(value)
+ value = Encoding.find(value) if value.is_a?(String)
+ @response_body_encoding = value
+ end
+
+ # Sets whether to determine the proxy from environment variable
+ # '<tt>ENV['http_proxy']</tt>';
+ # see {Proxy Using ENV['http_proxy']}[rdoc-ref:Net::HTTP@Proxy+Using+ENVHTTPProxy].
+ attr_writer :proxy_from_env
+
+ # Sets the proxy address;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
+ attr_writer :proxy_address
+
+ # Sets the proxy port;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
+ attr_writer :proxy_port
+
+ # Sets the proxy user;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
+ attr_writer :proxy_user
+
+ # Sets the proxy password;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
+ attr_writer :proxy_pass
+
+ # Sets whether the proxy uses SSL;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
+ attr_writer :proxy_use_ssl
+
+ # Returns the IP address for the connection.
+ #
+ # If the session has not been started,
+ # returns the value set by #ipaddr=,
+ # or +nil+ if it has not been set:
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.ipaddr # => nil
+ # http.ipaddr = '172.67.155.76'
+ # http.ipaddr # => "172.67.155.76"
+ #
+ # If the session has been started,
+ # returns the IP address from the socket:
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.start
+ # http.ipaddr # => "172.67.155.76"
+ # http.finish
+ #
+ def ipaddr
+ started? ? @socket.io.peeraddr[3] : @ipaddr
+ end
+
+ # Sets the IP address for the connection:
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.ipaddr # => nil
+ # http.ipaddr = '172.67.155.76'
+ # http.ipaddr # => "172.67.155.76"
+ #
+ # The IP address may not be set if the session has been started.
+ def ipaddr=(addr)
+ raise IOError, "ipaddr value changed, but session already started" if started?
+ @ipaddr = addr
+ end
+
+ # Sets or returns the numeric (\Integer or \Float) number of seconds
+ # to wait for a connection to open;
+ # initially 60.
+ # If the connection is not made in the given interval,
+ # an exception is raised.
attr_accessor :open_timeout
- # Seconds to wait until reading one block (by one read(2) call).
- # If the HTTP object cannot open a connection in this many seconds,
- # it raises a TimeoutError exception.
+ # Returns the numeric (\Integer or \Float) number of seconds
+ # to wait for one block to be read (via one read(2) call);
+ # see #read_timeout=.
attr_reader :read_timeout
- # Setter for the read_timeout attribute.
+ # Returns the numeric (\Integer or \Float) number of seconds
+ # to wait for one block to be written (via one write(2) call);
+ # see #write_timeout=.
+ attr_reader :write_timeout
+
+ # Sets the maximum number of times to retry an idempotent request in case of
+ # \Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET,
+ # Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError,
+ # Timeout::Error.
+ # The initial value is 1.
+ #
+ # Argument +retries+ must be a non-negative numeric value:
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.max_retries = 2 # => 2
+ # http.max_retries # => 2
+ #
+ def max_retries=(retries)
+ retries = retries.to_int
+ if retries < 0
+ raise ArgumentError, 'max_retries should be non-negative integer number'
+ end
+ @max_retries = retries
+ end
+
+ # Returns the maximum number of times to retry an idempotent request;
+ # see #max_retries=.
+ attr_reader :max_retries
+
+ # Sets the read timeout, in seconds, for +self+ to integer +sec+;
+ # the initial value is 60.
+ #
+ # Argument +sec+ must be a non-negative numeric value:
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.read_timeout # => 60
+ # http.get('/todos/1') # => #<Net::HTTPOK 200 OK readbody=true>
+ # http.read_timeout = 0
+ # http.get('/todos/1') # Raises Net::ReadTimeout.
+ #
def read_timeout=(sec)
@socket.read_timeout = sec if @socket
@read_timeout = sec
end
- # returns true if the HTTP session is started.
+ # Sets the write timeout, in seconds, for +self+ to integer +sec+;
+ # the initial value is 60.
+ #
+ # Argument +sec+ must be a non-negative numeric value:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # body = 'bar' * 200000
+ # data = <<EOF
+ # {"title": "foo", "body": "#{body}", "userId": "1"}
+ # EOF
+ # headers = {'content-type': 'application/json'}
+ # http = Net::HTTP.new(hostname)
+ # http.write_timeout # => 60
+ # http.post(_uri.path, data, headers)
+ # # => #<Net::HTTPCreated 201 Created readbody=true>
+ # http.write_timeout = 0
+ # http.post(_uri.path, data, headers) # Raises Net::WriteTimeout.
+ #
+ def write_timeout=(sec)
+ @socket.write_timeout = sec if @socket
+ @write_timeout = sec
+ end
+
+ # Returns the continue timeout value;
+ # see continue_timeout=.
+ attr_reader :continue_timeout
+
+ # Sets the continue timeout value,
+ # which is the number of seconds to wait for an expected 100 Continue response.
+ # If the \HTTP object does not receive a response in this many seconds
+ # it sends the request body.
+ def continue_timeout=(sec)
+ @socket.continue_timeout = sec if @socket
+ @continue_timeout = sec
+ end
+
+ # Sets or returns the numeric (\Integer or \Float) number of seconds
+ # to keep the connection open after a request is sent;
+ # initially 2.
+ # If a new request is made during the given interval,
+ # the still-open connection is used;
+ # otherwise the connection will have been closed
+ # and a new connection is opened.
+ attr_accessor :keep_alive_timeout
+
+ # Sets or returns whether to ignore end-of-file when reading a response body
+ # with <tt>Content-Length</tt> headers;
+ # initially +true+.
+ attr_accessor :ignore_eof
+
+ # Returns +true+ if the \HTTP session has been started:
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.started? # => false
+ # http.start
+ # http.started? # => true
+ # http.finish # => nil
+ # http.started? # => false
+ #
+ # Net::HTTP.start(hostname) do |http|
+ # http.started?
+ # end # => true
+ # http.started? # => false
+ #
def started?
@started
end
alias active? started? #:nodoc: obsolete
+ # Sets or returns whether to close the connection when the response is empty;
+ # initially +false+.
attr_accessor :close_on_empty_response
- # returns true if use SSL/TLS with HTTP.
+ # Returns +true+ if +self+ uses SSL, +false+ otherwise.
+ # See Net::HTTP#use_ssl=.
def use_ssl?
- false # redefined in net/https
+ @use_ssl
end
- # Opens TCP connection and HTTP session.
- #
- # When this method is called with block, gives a HTTP object
- # to the block and closes the TCP connection / HTTP session
- # after the block executed.
+ # Sets whether a new session is to use
+ # {Transport Layer Security}[https://en.wikipedia.org/wiki/Transport_Layer_Security]:
#
- # When called with a block, returns the return value of the
- # block; otherwise, returns self.
+ # Raises IOError if attempting to change during a session.
+ #
+ # Raises OpenSSL::SSL::SSLError if the port is not an HTTPS port.
+ def use_ssl=(flag)
+ flag = flag ? true : false
+ if started? and @use_ssl != flag
+ raise IOError, "use_ssl value changed, but session already started"
+ end
+ @use_ssl = flag
+ end
+
+ SSL_ATTRIBUTES = [
+ :ca_file,
+ :ca_path,
+ :cert,
+ :cert_store,
+ :ciphers,
+ :extra_chain_cert,
+ :key,
+ :ssl_timeout,
+ :ssl_version,
+ :min_version,
+ :max_version,
+ :verify_callback,
+ :verify_depth,
+ :verify_mode,
+ :verify_hostname,
+ ].freeze # :nodoc:
+
+ SSL_IVNAMES = SSL_ATTRIBUTES.map { |a| "@#{a}".to_sym }.freeze # :nodoc:
+
+ # Sets or returns the path to a CA certification file in PEM format.
+ attr_accessor :ca_file
+
+ # Sets or returns the path of to CA directory
+ # containing certification files in PEM format.
+ attr_accessor :ca_path
+
+ # Sets or returns the OpenSSL::X509::Certificate object
+ # to be used for client certification.
+ attr_accessor :cert
+
+ # Sets or returns the X509::Store to be used for verifying peer certificate.
+ attr_accessor :cert_store
+
+ # Sets or returns the available SSL ciphers.
+ # See {OpenSSL::SSL::SSLContext#ciphers=}[OpenSSL::SSL::SSL::Context#ciphers=].
+ attr_accessor :ciphers
+
+ # Sets or returns the extra X509 certificates to be added to the certificate chain.
+ # See {OpenSSL::SSL::SSLContext#add_certificate}[OpenSSL::SSL::SSL::Context#add_certificate].
+ attr_accessor :extra_chain_cert
+
+ # Sets or returns the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
+ attr_accessor :key
+
+ # Sets or returns the SSL timeout seconds.
+ attr_accessor :ssl_timeout
+
+ # Sets or returns the SSL version.
+ # See {OpenSSL::SSL::SSLContext#ssl_version=}[OpenSSL::SSL::SSL::Context#ssl_version=].
+ attr_accessor :ssl_version
+
+ # Sets or returns the minimum SSL version.
+ # See {OpenSSL::SSL::SSLContext#min_version=}[OpenSSL::SSL::SSL::Context#min_version=].
+ attr_accessor :min_version
+
+ # Sets or returns the maximum SSL version.
+ # See {OpenSSL::SSL::SSLContext#max_version=}[OpenSSL::SSL::SSL::Context#max_version=].
+ attr_accessor :max_version
+
+ # Sets or returns the callback for the server certification verification.
+ attr_accessor :verify_callback
+
+ # Sets or returns the maximum depth for the certificate chain verification.
+ attr_accessor :verify_depth
+
+ # Sets or returns the flags for server the certification verification
+ # at the beginning of the SSL/TLS session.
+ # OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable.
+ attr_accessor :verify_mode
+
+ # Sets or returns whether to verify that the server certificate is valid
+ # for the hostname.
+ # See {OpenSSL::SSL::SSLContext#verify_hostname=}[OpenSSL::SSL::SSL::Context#verify_hostname=].
+ attr_accessor :verify_hostname
+
+ # Returns the X509 certificate chain (an array of strings)
+ # for the session's socket peer,
+ # or +nil+ if none.
+ def peer_cert
+ if not use_ssl? or not @socket
+ return nil
+ end
+ @socket.io.peer_cert
+ end
+
+ # Starts an \HTTP session.
+ #
+ # Without a block, returns +self+:
+ #
+ # http = Net::HTTP.new(hostname)
+ # # => #<Net::HTTP jsonplaceholder.typicode.com:80 open=false>
+ # http.start
+ # # => #<Net::HTTP jsonplaceholder.typicode.com:80 open=true>
+ # http.started? # => true
+ # http.finish
+ #
+ # With a block, calls the block with +self+,
+ # finishes the session when the block exits,
+ # and returns the block's value:
+ #
+ # http.start do |http|
+ # http
+ # end
+ # # => #<Net::HTTP jsonplaceholder.typicode.com:80 open=false>
+ # http.started? # => false
#
def start # :yield: http
raise IOError, 'HTTP session already opened' if @started
@@ -570,6 +1636,21 @@ module Net #:nodoc:
self
end
+ # Finishes the \HTTP session:
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.start
+ # http.started? # => true
+ # http.finish # => nil
+ # http.started? # => false
+ #
+ # Raises IOError if not in a session.
+ def finish
+ raise IOError, 'HTTP session not yet started' unless started?
+ do_finish
+ end
+
+ # :stopdoc:
def do_start
connect
@started = true
@@ -577,60 +1658,150 @@ module Net #:nodoc:
private :do_start
def connect
- D "opening connection to #{conn_address()}..."
- s = timeout(@open_timeout) { TCPSocket.open(conn_address(), conn_port()) }
- D "opened"
if use_ssl?
- ssl_parameters = Hash.new
- SSL_ATTRIBUTES.each do |name|
- if value = instance_variable_get("@#{name}")
- ssl_parameters[name] = value
- end
- end
+ # reference early to load OpenSSL before connecting,
+ # as OpenSSL may take time to load.
@ssl_context = OpenSSL::SSL::SSLContext.new
- @ssl_context.set_params(ssl_parameters)
- s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
- s.sync_close = true
end
- @socket = BufferedIO.new(s)
- @socket.read_timeout = @read_timeout
- @socket.debug_output = @debug_output
+
+ if proxy? then
+ conn_addr = proxy_address
+ conn_port = proxy_port
+ else
+ conn_addr = conn_address
+ conn_port = port
+ end
+
+ debug "opening connection to #{conn_addr}:#{conn_port}..."
+ begin
+ s = timeouted_connect(conn_addr, conn_port)
+ rescue => e
+ if (defined?(IO::TimeoutError) && e.is_a?(IO::TimeoutError)) || e.is_a?(Errno::ETIMEDOUT) # for compatibility with previous versions
+ e = Net::OpenTimeout.new(e)
+ end
+ raise e, "Failed to open TCP connection to " +
+ "#{conn_addr}:#{conn_port} (#{e.message})"
+ end
+ s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
+ debug "opened"
if use_ssl?
if proxy?
- @socket.writeline sprintf('CONNECT %s:%s HTTP/%s',
- @address, @port, HTTPVersion)
- @socket.writeline "Host: #{@address}:#{@port}"
+ if @proxy_use_ssl
+ proxy_sock = OpenSSL::SSL::SSLSocket.new(s)
+ ssl_socket_connect(proxy_sock, @open_timeout)
+ else
+ proxy_sock = s
+ end
+ proxy_sock = BufferedIO.new(proxy_sock, read_timeout: @read_timeout,
+ write_timeout: @write_timeout,
+ continue_timeout: @continue_timeout,
+ debug_output: @debug_output)
+ buf = +"CONNECT #{conn_address}:#{@port} HTTP/#{HTTPVersion}\r\n" \
+ "Host: #{@address}:#{@port}\r\n"
if proxy_user
- credential = ["#{proxy_user}:#{proxy_pass}"].pack('m')
- credential.delete!("\r\n")
- @socket.writeline "Proxy-Authorization: Basic #{credential}"
+ credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0')
+ buf << "Proxy-Authorization: Basic #{credential}\r\n"
end
- @socket.writeline ''
- HTTPResponse.read_new(@socket).value
+ buf << "\r\n"
+ proxy_sock.write(buf)
+ HTTPResponse.read_new(proxy_sock).value
+ # assuming nothing left in buffers after successful CONNECT response
+ end
+
+ ssl_parameters = Hash.new
+ iv_list = instance_variables
+ SSL_IVNAMES.each_with_index do |ivname, i|
+ if iv_list.include?(ivname)
+ value = instance_variable_get(ivname)
+ unless value.nil?
+ ssl_parameters[SSL_ATTRIBUTES[i]] = value
+ end
+ end
+ end
+ @ssl_context.set_params(ssl_parameters)
+ unless @ssl_context.session_cache_mode.nil? # a dummy method on JRuby
+ @ssl_context.session_cache_mode =
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
end
- s.connect
- if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
+ if @ssl_context.respond_to?(:session_new_cb) # not implemented under JRuby
+ @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
+ end
+
+ # Still do the post_connection_check below even if connecting
+ # to IP address
+ verify_hostname = @ssl_context.verify_hostname
+
+ # Server Name Indication (SNI) RFC 3546/6066
+ case @address
+ when Resolv::IPv4::Regex, Resolv::IPv6::Regex
+ # don't set SNI, as IP addresses in SNI is not valid
+ # per RFC 6066, section 3.
+
+ # Avoid openssl warning
+ @ssl_context.verify_hostname = false
+ else
+ ssl_host_address = @address
+ end
+
+ debug "starting SSL for #{conn_addr}:#{conn_port}..."
+ s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
+ s.sync_close = true
+ s.hostname = ssl_host_address if s.respond_to?(:hostname=) && ssl_host_address
+
+ if @ssl_session and
+ Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
+ s.session = @ssl_session
+ end
+ ssl_socket_connect(s, @open_timeout)
+ if (@ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE) && verify_hostname
s.post_connection_check(@address)
end
+ debug "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}"
end
+ @socket = BufferedIO.new(s, read_timeout: @read_timeout,
+ write_timeout: @write_timeout,
+ continue_timeout: @continue_timeout,
+ debug_output: @debug_output)
+ @last_communicated = nil
on_connect
+ rescue => exception
+ if s
+ debug "Conn close because of connect error #{exception}"
+ s.close
+ end
+ raise
end
private :connect
- def on_connect
+ tcp_socket_parameters = TCPSocket.instance_method(:initialize).parameters
+ TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT = if tcp_socket_parameters != [[:rest]]
+ tcp_socket_parameters.include?([:key, :open_timeout])
+ else
+ # Use Socket.tcp to find out since there is no parameters information for TCPSocket#initialize
+ # See discussion in https://github.com/ruby/net-http/pull/224
+ Socket.method(:tcp).parameters.include?([:key, :open_timeout])
end
- private :on_connect
+ private_constant :TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT
- # Finishes HTTP session and closes TCP connection.
- # Raises IOError if not started.
- def finish
- raise IOError, 'HTTP session not yet started' unless started?
- do_finish
+ def timeouted_connect(conn_addr, conn_port)
+ if TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT
+ TCPSocket.open(conn_addr, conn_port, @local_host, @local_port, open_timeout: @open_timeout)
+ else
+ Timeout.timeout(@open_timeout, Net::OpenTimeout) {
+ TCPSocket.open(conn_addr, conn_port, @local_host, @local_port)
+ }
+ end
end
+ private :timeouted_connect
+
+ def on_connect
+ end
+ private :on_connect
def do_finish
@started = false
- @socket.close if @socket and not @socket.closed?
+ @socket.close if @socket
@socket = nil
end
private :do_finish
@@ -643,113 +1814,166 @@ module Net #:nodoc:
# no proxy
@is_proxy_class = false
+ @proxy_from_env = false
@proxy_addr = nil
@proxy_port = nil
@proxy_user = nil
@proxy_pass = nil
+ @proxy_use_ssl = nil
- # Creates an HTTP proxy class.
- # Arguments are address/port of proxy host and username/password
- # if authorization on proxy server is required.
- # You can replace the HTTP class with created proxy class.
- #
- # If ADDRESS is nil, this method returns self (Net::HTTP).
- #
- # # Example
- # proxy_class = Net::HTTP::Proxy('proxy.example.com', 8080)
- # :
- # proxy_class.start('www.ruby-lang.org') {|http|
- # # connecting proxy.foo.org:8080
- # :
- # }
- #
- def HTTP.Proxy(p_addr, p_port = nil, p_user = nil, p_pass = nil)
+ # Creates an \HTTP proxy class which behaves like \Net::HTTP, but
+ # performs all access via the specified proxy.
+ #
+ # This class is obsolete. You may pass these same parameters directly to
+ # \Net::HTTP.new. See Net::HTTP.new for details of the arguments.
+ def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_use_ssl = nil) #:nodoc:
return self unless p_addr
- delta = ProxyDelta
- proxyclass = Class.new(self)
- proxyclass.module_eval {
- include delta
- # with proxy
+
+ Class.new(self) {
@is_proxy_class = true
- @proxy_address = p_addr
- @proxy_port = p_port || default_port()
- @proxy_user = p_user
- @proxy_pass = p_pass
+
+ if p_addr == :ENV then
+ @proxy_from_env = true
+ @proxy_address = nil
+ @proxy_port = nil
+ else
+ @proxy_from_env = false
+ @proxy_address = p_addr
+ @proxy_port = p_port || default_port
+ end
+
+ @proxy_user = p_user
+ @proxy_pass = p_pass
+ @proxy_use_ssl = p_use_ssl
}
- proxyclass
end
+ # :startdoc:
+
class << HTTP
- # returns true if self is a class which was created by HTTP::Proxy.
+ # Returns true if self is a class which was created by HTTP::Proxy.
def proxy_class?
- @is_proxy_class
+ defined?(@is_proxy_class) ? @is_proxy_class : false
end
+ # Returns the address of the proxy host, or +nil+ if none;
+ # see Net::HTTP@Proxy+Server.
attr_reader :proxy_address
+
+ # Returns the port number of the proxy host, or +nil+ if none;
+ # see Net::HTTP@Proxy+Server.
attr_reader :proxy_port
+
+ # Returns the user name for accessing the proxy, or +nil+ if none;
+ # see Net::HTTP@Proxy+Server.
attr_reader :proxy_user
+
+ # Returns the password for accessing the proxy, or +nil+ if none;
+ # see Net::HTTP@Proxy+Server.
attr_reader :proxy_pass
+
+ # Use SSL when talking to the proxy. If Net::HTTP does not use a proxy, nil.
+ attr_reader :proxy_use_ssl
end
- # True if self is a HTTP proxy class.
+ # Returns +true+ if a proxy server is defined, +false+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
def proxy?
- self.class.proxy_class?
+ !!(@proxy_from_env ? proxy_uri : @proxy_address)
+ end
+
+ # Returns +true+ if the proxy server is defined in the environment,
+ # +false+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
+ def proxy_from_env?
+ @proxy_from_env
end
- # Address of proxy host. If self does not use a proxy, nil.
+ # The proxy URI determined from the environment for this connection.
+ def proxy_uri # :nodoc:
+ return if @proxy_uri == false
+ @proxy_uri ||= URI::HTTP.new(
+ "http", nil, address, port, nil, nil, nil, nil, nil
+ ).find_proxy || false
+ @proxy_uri || nil
+ end
+
+ # Returns the address of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
def proxy_address
- self.class.proxy_address
+ if @proxy_from_env then
+ proxy_uri&.hostname
+ else
+ @proxy_address
+ end
end
- # Port number of proxy host. If self does not use a proxy, nil.
+ # Returns the port number of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
def proxy_port
- self.class.proxy_port
+ if @proxy_from_env then
+ proxy_uri&.port
+ else
+ @proxy_port
+ end
end
- # User name for accessing proxy. If self does not use a proxy, nil.
+ # Returns the user name of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
def proxy_user
- self.class.proxy_user
+ if @proxy_from_env
+ user = proxy_uri&.user
+ unescape(user) if user
+ else
+ @proxy_user
+ end
end
- # User password for accessing proxy. If self does not use a proxy, nil.
+ # Returns the password of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
def proxy_pass
- self.class.proxy_pass
+ if @proxy_from_env
+ pass = proxy_uri&.password
+ unescape(pass) if pass
+ else
+ @proxy_pass
+ end
end
alias proxyaddr proxy_address #:nodoc: obsolete
alias proxyport proxy_port #:nodoc: obsolete
private
+ # :stopdoc:
+
+ def unescape(value)
+ require 'cgi/escape'
+ require 'cgi/util' unless defined?(CGI::EscapeExt)
+ CGI.unescape(value)
+ end
- # without proxy
+ # without proxy, obsolete
- def conn_address
- address()
+ def conn_address # :nodoc:
+ @ipaddr || address()
end
- def conn_port
+ def conn_port # :nodoc:
port()
end
def edit_path(path)
- path
- end
-
- module ProxyDelta #:nodoc: internal use only
- private
-
- def conn_address
- proxy_address()
- end
-
- def conn_port
- proxy_port()
- end
-
- def edit_path(path)
- use_ssl? ? path : "http://#{addr_port()}#{path}"
+ if proxy?
+ if path.start_with?("ftp://") || use_ssl?
+ path
+ else
+ "http://#{addr_port}#{path}"
+ end
+ else
+ path
end
end
+ # :startdoc:
#
# HTTP operations
@@ -757,300 +1981,356 @@ module Net #:nodoc:
public
- # Gets data from +path+ on the connected-to host.
- # +initheader+ must be a Hash like { 'Accept' => '*/*', ... },
- # and it defaults to an empty hash.
- # If +initheader+ doesn't have the key 'accept-encoding', then
- # a value of "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" is used,
- # so that gzip compression is used in preference to deflate
- # compression, which is used in preference to no compression.
- # Ruby doesn't have libraries to support the compress (Lempel-Ziv)
- # compression, so that is not supported. The intent of this is
- # to reduce bandwidth by default. If this routine sets up
- # compression, then it does the decompression also, removing
- # the header as well to prevent confusion. Otherwise
- # it leaves the body as it found it.
- #
- # In version 1.1 (ruby 1.6), this method returns a pair of objects,
- # a Net::HTTPResponse object and the entity body string.
- # In version 1.2 (ruby 1.8), this method returns a Net::HTTPResponse
- # object.
- #
- # If called with a block, yields each fragment of the
- # entity body in turn as a string as it is read from
- # the socket. Note that in this case, the returned response
- # object will *not* contain a (meaningful) body.
- #
- # +dest+ argument is obsolete.
- # It still works but you must not use it.
- #
- # In version 1.1, this method might raise an exception for
- # 3xx (redirect). In this case you can get a HTTPResponse object
- # by "anException.response".
- #
- # In version 1.2, this method never raises exception.
- #
- # # version 1.1 (bundled with Ruby 1.6)
- # response, body = http.get('/index.html')
- #
- # # version 1.2 (bundled with Ruby 1.8 or later)
- # response = http.get('/index.html')
- #
- # # using block
- # File.open('result.txt', 'w') {|f|
- # http.get('/~foo/') do |str|
- # f.write str
- # end
- # }
- #
- def get(path, initheader = {}, dest = nil, &block) # :yield: +body_segment+
+ # :call-seq:
+ # get(path, initheader = nil) {|res| ... }
+ #
+ # Sends a GET request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Get object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # With a block given, calls the block with the response body:
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.get('/todos/1') do |res|
+ # p res
+ # end # => #<Net::HTTPOK 200 OK readbody=true>
+ #
+ # Output:
+ #
+ # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}"
+ #
+ # With no block given, simply returns the response object:
+ #
+ # http.get('/') # => #<Net::HTTPOK 200 OK readbody=true>
+ #
+ # Related:
+ #
+ # - Net::HTTP::Get: request class for \HTTP method GET.
+ # - Net::HTTP.get: sends GET request, returns response body.
+ #
+ def get(path, initheader = nil, dest = nil, &block) # :yield: +body_segment+
res = nil
- if HAVE_ZLIB
- unless initheader.keys.any?{|k| k.downcase == "accept-encoding"}
- initheader["accept-encoding"] = "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
- @compression = true
- end
- end
+
request(Get.new(path, initheader)) {|r|
- if r.key?("content-encoding") and @compression
- @compression = nil # Clear it till next set.
- the_body = r.read_body dest, &block
- case r["content-encoding"]
- when "gzip"
- r.body= Zlib::GzipReader.new(StringIO.new(the_body)).read
- r.delete("content-encoding")
- when "deflate"
- r.body= Zlib::Inflate.inflate(the_body);
- r.delete("content-encoding")
- when "identity"
- ; # nothing needed
- else
- ; # Don't do anything dramatic, unless we need to later
- end
- else
- r.read_body dest, &block
- end
+ r.read_body dest, &block
res = r
}
- unless @newimpl
- res.value
- return res, res.body
- end
-
res
end
- # Gets only the header from +path+ on the connected-to host.
- # +header+ is a Hash like { 'Accept' => '*/*', ... }.
- #
- # This method returns a Net::HTTPResponse object.
- #
- # In version 1.1, this method might raise an exception for
- # 3xx (redirect). On the case you can get a HTTPResponse object
- # by "anException.response".
- # In version 1.2, this method never raises an exception.
- #
- # response = nil
- # Net::HTTP.start('some.www.server', 80) {|http|
- # response = http.head('/index.html')
- # }
- # p response['content-type']
- #
- def head(path, initheader = nil)
- res = request(Head.new(path, initheader))
- res.value unless @newimpl
- res
+ # Sends a HEAD request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Head object
+ # created from string +path+ and initial headers hash +initheader+:
+ #
+ # res = http.head('/todos/1') # => #<Net::HTTPOK 200 OK readbody=true>
+ # res.body # => nil
+ # res.to_hash.take(3)
+ # # =>
+ # [["date", ["Wed, 15 Feb 2023 15:25:42 GMT"]],
+ # ["content-type", ["application/json; charset=utf-8"]],
+ # ["connection", ["close"]]]
+ #
+ def head(path, initheader = nil)
+ request(Head.new(path, initheader))
end
- # Posts +data+ (must be a String) to +path+. +header+ must be a Hash
- # like { 'Accept' => '*/*', ... }.
- #
- # In version 1.1 (ruby 1.6), this method returns a pair of objects, a
- # Net::HTTPResponse object and an entity body string.
- # In version 1.2 (ruby 1.8), this method returns a Net::HTTPResponse object.
- #
- # If called with a block, yields each fragment of the
- # entity body in turn as a string as it are read from
- # the socket. Note that in this case, the returned response
- # object will *not* contain a (meaningful) body.
- #
- # +dest+ argument is obsolete.
- # It still works but you must not use it.
- #
- # In version 1.1, this method might raise an exception for
- # 3xx (redirect). In this case you can get an HTTPResponse object
- # by "anException.response".
- # In version 1.2, this method never raises exception.
- #
- # # version 1.1
- # response, body = http.post('/cgi-bin/search.rb', 'query=foo')
- #
- # # version 1.2
- # response = http.post('/cgi-bin/search.rb', 'query=foo')
- #
- # # using block
- # File.open('result.txt', 'w') {|f|
- # http.post('/cgi-bin/search.rb', 'query=foo') do |str|
- # f.write str
- # end
- # }
- #
- # You should set Content-Type: header field for POST.
- # If no Content-Type: field given, this method uses
- # "application/x-www-form-urlencoded" by default.
+ # :call-seq:
+ # post(path, data, initheader = nil) {|res| ... }
+ #
+ # Sends a POST request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Post object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # With a block given, calls the block with the response body:
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Net::HTTP.new(hostname)
+ # http.post('/todos', data) do |res|
+ # p res
+ # end # => #<Net::HTTPCreated 201 Created readbody=true>
+ #
+ # Output:
+ #
+ # "{\n \"{\\\"userId\\\": 1, \\\"id\\\": 1, \\\"title\\\": \\\"delectus aut autem\\\", \\\"completed\\\": false}\": \"\",\n \"id\": 201\n}"
+ #
+ # With no block given, simply returns the response object:
+ #
+ # http.post('/todos', data) # => #<Net::HTTPCreated 201 Created readbody=true>
+ #
+ # Related:
+ #
+ # - Net::HTTP::Post: request class for \HTTP method POST.
+ # - Net::HTTP.post: sends POST request, returns response body.
#
def post(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
- res = nil
- request(Post.new(path, initheader), data) {|r|
- r.read_body dest, &block
- res = r
- }
- unless @newimpl
- res.value
- return res, res.body
- end
- res
+ send_entity(path, data, initheader, dest, Post, &block)
end
- def put(path, data, initheader = nil) #:nodoc:
- res = request(Put.new(path, initheader), data)
- res.value unless @newimpl
- res
+ # :call-seq:
+ # patch(path, data, initheader = nil) {|res| ... }
+ #
+ # Sends a PATCH request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Patch object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # With a block given, calls the block with the response body:
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Net::HTTP.new(hostname)
+ # http.patch('/todos/1', data) do |res|
+ # p res
+ # end # => #<Net::HTTPOK 200 OK readbody=true>
+ #
+ # Output:
+ #
+ # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false,\n \"{\\\"userId\\\": 1, \\\"id\\\": 1, \\\"title\\\": \\\"delectus aut autem\\\", \\\"completed\\\": false}\": \"\"\n}"
+ #
+ # With no block given, simply returns the response object:
+ #
+ # http.patch('/todos/1', data) # => #<Net::HTTPCreated 201 Created readbody=true>
+ #
+ def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
+ send_entity(path, data, initheader, dest, Patch, &block)
end
- # Sends a PROPPATCH request to the +path+ and gets a response,
- # as an HTTPResponse object.
+ # Sends a PUT request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Put object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Net::HTTP.new(hostname)
+ # http.put('/todos/1', data) # => #<Net::HTTPOK 200 OK readbody=true>
+ #
+ # Related:
+ #
+ # - Net::HTTP::Put: request class for \HTTP method PUT.
+ # - Net::HTTP.put: sends PUT request, returns response body.
+ #
+ def put(path, data, initheader = nil)
+ request(Put.new(path, initheader), data)
+ end
+
+ # Sends a PROPPATCH request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Proppatch object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Net::HTTP.new(hostname)
+ # http.proppatch('/todos/1', data)
+ #
def proppatch(path, body, initheader = nil)
request(Proppatch.new(path, initheader), body)
end
- # Sends a LOCK request to the +path+ and gets a response,
- # as an HTTPResponse object.
+ # Sends a LOCK request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Lock object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Net::HTTP.new(hostname)
+ # http.lock('/todos/1', data)
+ #
def lock(path, body, initheader = nil)
request(Lock.new(path, initheader), body)
end
- # Sends a UNLOCK request to the +path+ and gets a response,
- # as an HTTPResponse object.
+ # Sends an UNLOCK request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Unlock object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Net::HTTP.new(hostname)
+ # http.unlock('/todos/1', data)
+ #
def unlock(path, body, initheader = nil)
request(Unlock.new(path, initheader), body)
end
- # Sends a OPTIONS request to the +path+ and gets a response,
- # as an HTTPResponse object.
+ # Sends an Options request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Options object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.options('/')
+ #
def options(path, initheader = nil)
request(Options.new(path, initheader))
end
- # Sends a PROPFIND request to the +path+ and gets a response,
- # as an HTTPResponse object.
+ # Sends a PROPFIND request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Propfind object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Net::HTTP.new(hostname)
+ # http.propfind('/todos/1', data)
+ #
def propfind(path, body = nil, initheader = {'Depth' => '0'})
request(Propfind.new(path, initheader), body)
end
- # Sends a DELETE request to the +path+ and gets a response,
- # as an HTTPResponse object.
+ # Sends a DELETE request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Delete object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.delete('/todos/1')
+ #
def delete(path, initheader = {'Depth' => 'Infinity'})
request(Delete.new(path, initheader))
end
- # Sends a MOVE request to the +path+ and gets a response,
- # as an HTTPResponse object.
+ # Sends a MOVE request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Move object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.move('/todos/1')
+ #
def move(path, initheader = nil)
request(Move.new(path, initheader))
end
- # Sends a COPY request to the +path+ and gets a response,
- # as an HTTPResponse object.
+ # Sends a COPY request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Copy object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.copy('/todos/1')
+ #
def copy(path, initheader = nil)
request(Copy.new(path, initheader))
end
- # Sends a MKCOL request to the +path+ and gets a response,
- # as an HTTPResponse object.
+ # Sends a MKCOL request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Mkcol object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http.mkcol('/todos/1', data)
+ # http = Net::HTTP.new(hostname)
+ #
def mkcol(path, body = nil, initheader = nil)
request(Mkcol.new(path, initheader), body)
end
- # Sends a TRACE request to the +path+ and gets a response,
- # as an HTTPResponse object.
+ # Sends a TRACE request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Trace object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.trace('/todos/1')
+ #
def trace(path, initheader = nil)
request(Trace.new(path, initheader))
end
- # Sends a GET request to the +path+ and gets a response,
- # as an HTTPResponse object.
- #
- # When called with a block, yields an HTTPResponse object.
- # The body of this response will not have been read yet;
- # the caller can process it using HTTPResponse#read_body,
- # if desired.
- #
- # Returns the response.
- #
- # This method never raises Net::* exceptions.
- #
- # response = http.request_get('/index.html')
- # # The entity body is already read here.
- # p response['content-type']
- # puts response.body
- #
- # # using block
- # http.request_get('/index.html') {|response|
- # p response['content-type']
- # response.read_body do |str| # read body now
- # print str
- # end
- # }
+ # Sends a GET request to the server;
+ # forms the response into a Net::HTTPResponse object.
+ #
+ # The request is based on the Net::HTTP::Get object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # With no block given, returns the response object:
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.request_get('/todos') # => #<Net::HTTPOK 200 OK readbody=true>
+ #
+ # With a block given, calls the block with the response object
+ # and returns the response object:
+ #
+ # http.request_get('/todos') do |res|
+ # p res
+ # end # => #<Net::HTTPOK 200 OK readbody=true>
+ #
+ # Output:
+ #
+ # #<Net::HTTPOK 200 OK readbody=false>
#
def request_get(path, initheader = nil, &block) # :yield: +response+
request(Get.new(path, initheader), &block)
end
- # Sends a HEAD request to the +path+ and gets a response,
- # as an HTTPResponse object.
+ # Sends a HEAD request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
#
- # Returns the response.
- #
- # This method never raises Net::* exceptions.
- #
- # response = http.request_head('/index.html')
- # p response['content-type']
+ # The request is based on the Net::HTTP::Head object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.head('/todos/1') # => #<Net::HTTPOK 200 OK readbody=true>
#
def request_head(path, initheader = nil, &block)
request(Head.new(path, initheader), &block)
end
- # Sends a POST request to the +path+ and gets a response,
- # as an HTTPResponse object.
- #
- # When called with a block, yields an HTTPResponse object.
- # The body of this response will not have been read yet;
- # the caller can process it using HTTPResponse#read_body,
- # if desired.
- #
- # Returns the response.
- #
- # This method never raises Net::* exceptions.
- #
- # # example
- # response = http.request_post('/cgi-bin/nice.rb', 'datadatadata...')
- # p response.status
- # puts response.body # body is already read
- #
- # # using block
- # http.request_post('/cgi-bin/nice.rb', 'datadatadata...') {|response|
- # p response.status
- # p response['content-type']
- # response.read_body do |str| # read body now
- # print str
- # end
- # }
+ # Sends a POST request to the server;
+ # forms the response into a Net::HTTPResponse object.
+ #
+ # The request is based on the Net::HTTP::Post object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # With no block given, returns the response object:
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.post('/todos', 'xyzzy')
+ # # => #<Net::HTTPCreated 201 Created readbody=true>
+ #
+ # With a block given, calls the block with the response body
+ # and returns the response object:
+ #
+ # http.post('/todos', 'xyzzy') do |res|
+ # p res
+ # end # => #<Net::HTTPCreated 201 Created readbody=true>
+ #
+ # Output:
+ #
+ # "{\n \"xyzzy\": \"\",\n \"id\": 201\n}"
#
def request_post(path, data, initheader = nil, &block) # :yield: +response+
request Post.new(path, initheader), data, &block
end
+ # Sends a PUT request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTP::Put object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # http = Net::HTTP.new(hostname)
+ # http.put('/todos/1', 'xyzzy')
+ # # => #<Net::HTTPOK 200 OK readbody=true>
+ #
def request_put(path, data, initheader = nil, &block) #:nodoc:
request Put.new(path, initheader), data, &block
end
@@ -1060,34 +2340,61 @@ module Net #:nodoc:
alias post2 request_post #:nodoc: obsolete
alias put2 request_put #:nodoc: obsolete
-
- # Sends an HTTP request to the HTTP server.
- # This method also sends DATA string if DATA is given.
+ # Sends an \HTTP request to the server;
+ # returns an instance of a subclass of Net::HTTPResponse.
+ #
+ # The request is based on the Net::HTTPRequest object
+ # created from string +path+, string +data+, and initial headers hash +header+.
+ # That object is an instance of the
+ # {subclass of Net::HTTPRequest}[rdoc-ref:Net::HTTPRequest@Request+Subclasses],
+ # that corresponds to the given uppercase string +name+,
+ # which must be
+ # an {HTTP request method}[https://en.wikipedia.org/wiki/HTTP#Request_methods]
+ # or a {WebDAV request method}[https://en.wikipedia.org/wiki/WebDAV#Implementation].
#
- # Returns a HTTPResponse object.
- #
- # This method never raises Net::* exceptions.
+ # Examples:
#
- # response = http.send_request('GET', '/index.html')
- # puts response.body
+ # http = Net::HTTP.new(hostname)
+ # http.send_request('GET', '/todos/1')
+ # # => #<Net::HTTPOK 200 OK readbody=true>
+ # http.send_request('POST', '/todos', 'xyzzy')
+ # # => #<Net::HTTPCreated 201 Created readbody=true>
#
def send_request(name, path, data = nil, header = nil)
- r = HTTPGenericRequest.new(name,(data ? true : false),true,path,header)
+ has_response_body = name != 'HEAD'
+ r = HTTPGenericRequest.new(name,(data ? true : false),has_response_body,path,header)
request r, data
end
- # Sends an HTTPRequest object REQUEST to the HTTP server.
- # This method also sends DATA string if REQUEST is a post/put request.
- # Giving DATA for get/head request causes ArgumentError.
- #
- # When called with a block, yields an HTTPResponse object.
- # The body of this response will not have been read yet;
- # the caller can process it using HTTPResponse#read_body,
- # if desired.
+ # Sends the given request +req+ to the server;
+ # forms the response into a Net::HTTPResponse object.
+ #
+ # The given +req+ must be an instance of a
+ # {subclass of Net::HTTPRequest}[rdoc-ref:Net::HTTPRequest@Request+Subclasses].
+ # Argument +body+ should be given only if needed for the request.
#
- # Returns a HTTPResponse object.
- #
- # This method never raises Net::* exceptions.
+ # With no block given, returns the response object:
+ #
+ # http = Net::HTTP.new(hostname)
+ #
+ # req = Net::HTTP::Get.new('/todos/1')
+ # http.request(req)
+ # # => #<Net::HTTPOK 200 OK readbody=true>
+ #
+ # req = Net::HTTP::Post.new('/todos')
+ # http.request(req, 'xyzzy')
+ # # => #<Net::HTTPCreated 201 Created readbody=true>
+ #
+ # With a block given, calls the block with the response and returns the response:
+ #
+ # req = Net::HTTP::Get.new('/todos/1')
+ # http.request(req) do |res|
+ # p res
+ # end # => #<Net::HTTPOK 200 OK readbody=true>
+ #
+ # Output:
+ #
+ # #<Net::HTTPOK 200 OK readbody=false>
#
def request(req, body = nil, &block) # :yield: +response+
unless started?
@@ -1110,38 +2417,112 @@ module Net #:nodoc:
private
+ # Executes a request which uses a representation
+ # and returns its body.
+ def send_entity(path, data, initheader, dest, type, &block)
+ res = nil
+ request(type.new(path, initheader), data) {|r|
+ r.read_body dest, &block
+ res = r
+ }
+ res
+ end
+
+ # :stopdoc:
+
+ IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/.freeze # :nodoc:
+
def transport_request(req)
- begin_transport req
- req.exec @socket, @curr_http_version, edit_path(req.path)
+ count = 0
begin
- res = HTTPResponse.read_new(@socket)
- end while res.kind_of?(HTTPContinue)
- res.reading_body(@socket, req.response_body_permitted?) {
- yield res if block_given?
- }
+ begin_transport req
+ res = catch(:response) {
+ begin
+ req.exec @socket, @curr_http_version, edit_path(req.path)
+ rescue Errno::EPIPE
+ # Failure when writing full request, but we can probably
+ # still read the received response.
+ end
+
+ begin
+ res = HTTPResponse.read_new(@socket)
+ res.decode_content = req.decode_content
+ res.body_encoding = @response_body_encoding
+ res.ignore_eof = @ignore_eof
+ end while res.kind_of?(HTTPInformation)
+
+ res.uri = req.uri
+
+ res
+ }
+ res.reading_body(@socket, req.response_body_permitted?) {
+ if block_given?
+ count = max_retries # Don't restart in the middle of a download
+ yield res
+ end
+ }
+ rescue Net::OpenTimeout
+ raise
+ rescue Net::ReadTimeout, IOError, EOFError,
+ Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, Errno::ETIMEDOUT,
+ # avoid a dependency on OpenSSL
+ defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError,
+ Timeout::Error => exception
+ if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method)
+ count += 1
+ @socket.close if @socket
+ debug "Conn close because of error #{exception}, and retry"
+ retry
+ end
+ debug "Conn close because of error #{exception}"
+ @socket.close if @socket
+ raise
+ end
+
end_transport req, res
res
+ rescue => exception
+ debug "Conn close because of error #{exception}"
+ @socket.close if @socket
+ raise exception
end
def begin_transport(req)
- connect if @socket.closed?
+ if @socket.closed?
+ connect
+ elsif @last_communicated
+ if @last_communicated + @keep_alive_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ debug 'Conn close because of keep_alive_timeout'
+ @socket.close
+ connect
+ elsif @socket.io.to_io.wait_readable(0) && @socket.eof?
+ debug "Conn close because of EOF"
+ @socket.close
+ connect
+ end
+ end
+
if not req.response_body_permitted? and @close_on_empty_response
req['connection'] ||= 'close'
end
+
+ req.update_uri address, port, use_ssl?
req['host'] ||= addr_port()
end
def end_transport(req, res)
@curr_http_version = res.http_version
+ @last_communicated = nil
if @socket.closed?
- D 'Conn socket closed'
+ debug 'Conn socket closed'
elsif not res.body and @close_on_empty_response
- D 'Conn close'
+ debug 'Conn close'
@socket.close
elsif keep_alive?(req, res)
- D 'Conn keep-alive'
+ debug 'Conn keep-alive'
+ @last_communicated = Process.clock_gettime(Process::CLOCK_MONOTONIC)
else
- D 'Conn close'
+ debug 'Conn close'
@socket.close
end
end
@@ -1190,1209 +2571,38 @@ module Net #:nodoc:
private
def addr_port
- if use_ssl?
- address() + (port == HTTP.https_default_port ? '' : ":#{port()}")
- else
- address() + (port == HTTP.http_default_port ? '' : ":#{port()}")
- end
+ addr = address
+ addr = "[#{addr}]" if addr.include?(":")
+ default_port = use_ssl? ? HTTP.https_default_port : HTTP.http_default_port
+ default_port == port ? addr : "#{addr}:#{port}"
end
- def D(msg)
+ # Adds a message to debugging output
+ def debug(msg)
return unless @debug_output
@debug_output << msg
@debug_output << "\n"
end
+ alias_method :D, :debug
end
+ # for backward compatibility until Ruby 4.0
+ # https://bugs.ruby-lang.org/issues/20900
+ # https://github.com/bblimke/webmock/pull/1081
HTTPSession = HTTP
+ deprecate_constant :HTTPSession
+end
+require_relative 'http/exceptions'
- #
- # Header module.
- #
- # Provides access to @header in the mixed-into class as a hash-like
- # object, except with case-insensitive keys. Also provides
- # methods for accessing commonly-used header values in a more
- # convenient format.
- #
- module HTTPHeader
-
- def initialize_http_header(initheader)
- @header = {}
- return unless initheader
- initheader.each do |key, value|
- warn "net/http: warning: duplicated HTTP header: #{key}" if key?(key) and $VERBOSE
- @header[key.downcase] = [value.strip]
- end
- end
-
- def size #:nodoc: obsolete
- @header.size
- end
-
- alias length size #:nodoc: obsolete
-
- # Returns the header field corresponding to the case-insensitive key.
- # For example, a key of "Content-Type" might return "text/html"
- def [](key)
- a = @header[key.downcase] or return nil
- a.join(', ')
- end
-
- # Sets the header field corresponding to the case-insensitive key.
- def []=(key, val)
- unless val
- @header.delete key.downcase
- return val
- end
- @header[key.downcase] = [val]
- end
-
- # [Ruby 1.8.3]
- # Adds header field instead of replace.
- # Second argument +val+ must be a String.
- # See also #[]=, #[] and #get_fields.
- #
- # request.add_field 'X-My-Header', 'a'
- # p request['X-My-Header'] #=> "a"
- # p request.get_fields('X-My-Header') #=> ["a"]
- # request.add_field 'X-My-Header', 'b'
- # p request['X-My-Header'] #=> "a, b"
- # p request.get_fields('X-My-Header') #=> ["a", "b"]
- # request.add_field 'X-My-Header', 'c'
- # p request['X-My-Header'] #=> "a, b, c"
- # p request.get_fields('X-My-Header') #=> ["a", "b", "c"]
- #
- def add_field(key, val)
- if @header.key?(key.downcase)
- @header[key.downcase].push val
- else
- @header[key.downcase] = [val]
- end
- end
-
- # [Ruby 1.8.3]
- # Returns an array of header field strings corresponding to the
- # case-insensitive +key+. This method allows you to get duplicated
- # header fields without any processing. See also #[].
- #
- # p response.get_fields('Set-Cookie')
- # #=> ["session=al98axx; expires=Fri, 31-Dec-1999 23:58:23",
- # "query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"]
- # p response['Set-Cookie']
- # #=> "session=al98axx; expires=Fri, 31-Dec-1999 23:58:23, query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"
- #
- def get_fields(key)
- return nil unless @header[key.downcase]
- @header[key.downcase].dup
- end
-
- # Returns the header field corresponding to the case-insensitive key.
- # Returns the default value +args+, or the result of the block, or nil,
- # if there's no header field named key. See Hash#fetch
- def fetch(key, *args, &block) #:yield: +key+
- a = @header.fetch(key.downcase, *args, &block)
- a.join(', ')
- end
-
- # Iterates for each header names and values.
- def each_header #:yield: +key+, +value+
- @header.each do |k,va|
- yield k, va.join(', ')
- end
- end
-
- alias each each_header
-
- # Iterates for each header names.
- def each_name(&block) #:yield: +key+
- @header.each_key(&block)
- end
-
- alias each_key each_name
-
- # Iterates for each capitalized header names.
- def each_capitalized_name(&block) #:yield: +key+
- @header.each_key do |k|
- yield capitalize(k)
- end
- end
-
- # Iterates for each header values.
- def each_value #:yield: +value+
- @header.each_value do |va|
- yield va.join(', ')
- end
- end
-
- # Removes a header field.
- def delete(key)
- @header.delete(key.downcase)
- end
-
- # true if +key+ header exists.
- def key?(key)
- @header.key?(key.downcase)
- end
-
- # Returns a Hash consist of header names and values.
- def to_hash
- @header.dup
- end
-
- # As for #each_header, except the keys are provided in capitalized form.
- def each_capitalized
- @header.each do |k,v|
- yield capitalize(k), v.join(', ')
- end
- end
-
- alias canonical_each each_capitalized
-
- def capitalize(name)
- name.split(/-/).map {|s| s.capitalize }.join('-')
- end
- private :capitalize
-
- # Returns an Array of Range objects which represents Range: header field,
- # or +nil+ if there is no such header.
- def range
- return nil unless @header['range']
- self['Range'].split(/,/).map {|spec|
- m = /bytes\s*=\s*(\d+)?\s*-\s*(\d+)?/i.match(spec) or
- raise HTTPHeaderSyntaxError, "wrong Range: #{spec}"
- d1 = m[1].to_i
- d2 = m[2].to_i
- if m[1] and m[2] then d1..d2
- elsif m[1] then d1..-1
- elsif m[2] then -d2..-1
- else
- raise HTTPHeaderSyntaxError, 'range is not specified'
- end
- }
- end
-
- # Set Range: header from Range (arg r) or beginning index and
- # length from it (arg idx&len).
- #
- # req.range = (0..1023)
- # req.set_range 0, 1023
- #
- def set_range(r, e = nil)
- unless r
- @header.delete 'range'
- return r
- end
- r = (r...r+e) if e
- case r
- when Numeric
- n = r.to_i
- rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}")
- when Range
- first = r.first
- last = r.last
- last -= 1 if r.exclude_end?
- if last == -1
- rangestr = (first > 0 ? "#{first}-" : "-#{-first}")
- else
- raise HTTPHeaderSyntaxError, 'range.first is negative' if first < 0
- raise HTTPHeaderSyntaxError, 'range.last is negative' if last < 0
- raise HTTPHeaderSyntaxError, 'must be .first < .last' if first > last
- rangestr = "#{first}-#{last}"
- end
- else
- raise TypeError, 'Range/Integer is required'
- end
- @header['range'] = ["bytes=#{rangestr}"]
- r
- end
-
- alias range= set_range
-
- # Returns an Integer object which represents the Content-Length: header field
- # or +nil+ if that field is not provided.
- def content_length
- return nil unless key?('Content-Length')
- len = self['Content-Length'].slice(/\d+/) or
- raise HTTPHeaderSyntaxError, 'wrong Content-Length format'
- len.to_i
- end
-
- def content_length=(len)
- unless len
- @header.delete 'content-length'
- return nil
- end
- @header['content-length'] = [len.to_i.to_s]
- end
-
- # Returns "true" if the "transfer-encoding" header is present and
- # set to "chunked". This is an HTTP/1.1 feature, allowing the
- # the content to be sent in "chunks" without at the outset
- # stating the entire content length.
- def chunked?
- return false unless @header['transfer-encoding']
- field = self['Transfer-Encoding']
- (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false
- end
-
- # Returns a Range object which represents Content-Range: header field.
- # This indicates, for a partial entity body, where this fragment
- # fits inside the full entity body, as range of byte offsets.
- def content_range
- return nil unless @header['content-range']
- m = %r<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(self['Content-Range']) or
- raise HTTPHeaderSyntaxError, 'wrong Content-Range format'
- m[1].to_i .. m[2].to_i + 1
- end
-
- # The length of the range represented in Content-Range: header.
- def range_length
- r = content_range() or return nil
- r.end - r.begin
- end
-
- # Returns a content type string such as "text/html".
- # This method returns nil if Content-Type: header field does not exist.
- def content_type
- return nil unless main_type()
- if sub_type()
- then "#{main_type()}/#{sub_type()}"
- else main_type()
- end
- end
-
- # Returns a content type string such as "text".
- # This method returns nil if Content-Type: header field does not exist.
- def main_type
- return nil unless @header['content-type']
- self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip
- end
-
- # Returns a content type string such as "html".
- # This method returns nil if Content-Type: header field does not exist
- # or sub-type is not given (e.g. "Content-Type: text").
- def sub_type
- return nil unless @header['content-type']
- main, sub = *self['Content-Type'].split(';').first.to_s.split('/')
- return nil unless sub
- sub.strip
- end
-
- # Returns content type parameters as a Hash as like
- # {"charset" => "iso-2022-jp"}.
- def type_params
- result = {}
- list = self['Content-Type'].to_s.split(';')
- list.shift
- list.each do |param|
- k, v = *param.split('=', 2)
- result[k.strip] = v.strip
- end
- result
- end
-
- # Set Content-Type: header field by +type+ and +params+.
- # +type+ must be a String, +params+ must be a Hash.
- def set_content_type(type, params = {})
- @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')]
- end
-
- alias content_type= set_content_type
-
- # Set header fields and a body from HTML form data.
- # +params+ should be a Hash containing HTML form data.
- # Optional argument +sep+ means data record separator.
- #
- # This method also set Content-Type: header field to
- # application/x-www-form-urlencoded.
- #
- # Example:
- # http.form_data = {"q" => "ruby", "lang" => "en"}
- # http.form_data = {"q" => ["ruby", "perl"], "lang" => "en"}
- # http.set_form_data({"q" => "ruby", "lang" => "en"}, ';')
- #
- def set_form_data(params, sep = '&')
- self.body = params.map {|k, v| encode_kvpair(k, v) }.flatten.join(sep)
- self.content_type = 'application/x-www-form-urlencoded'
- end
-
- alias form_data= set_form_data
-
- def encode_kvpair(k, vs)
- Array(vs).map {|v| "#{urlencode(k)}=#{urlencode(v.to_s)}" }
- end
- private :encode_kvpair
-
- def urlencode(str)
- str.dup.force_encoding('ASCII-8BIT').gsub(/[^a-zA-Z0-9_\.\-]/){'%%%02x' % $&.ord}
- end
- private :urlencode
-
- # Set the Authorization: header for "Basic" authorization.
- def basic_auth(account, password)
- @header['authorization'] = [basic_encode(account, password)]
- end
-
- # Set Proxy-Authorization: header for "Basic" authorization.
- def proxy_basic_auth(account, password)
- @header['proxy-authorization'] = [basic_encode(account, password)]
- end
-
- def basic_encode(account, password)
- 'Basic ' + ["#{account}:#{password}"].pack('m').delete("\r\n")
- end
- private :basic_encode
-
- def connection_close?
- tokens(@header['connection']).include?('close') or
- tokens(@header['proxy-connection']).include?('close')
- end
-
- def connection_keep_alive?
- tokens(@header['connection']).include?('keep-alive') or
- tokens(@header['proxy-connection']).include?('keep-alive')
- end
-
- def tokens(vals)
- return [] unless vals
- vals.map {|v| v.split(',') }.flatten\
- .reject {|str| str.strip.empty? }\
- .map {|tok| tok.strip.downcase }
- end
- private :tokens
-
- end
-
-
- #
- # Parent of HTTPRequest class. Do not use this directly; use
- # a subclass of HTTPRequest.
- #
- # Mixes in the HTTPHeader module.
- #
- class HTTPGenericRequest
-
- include HTTPHeader
-
- def initialize(m, reqbody, resbody, path, initheader = nil)
- @method = m
- @request_has_body = reqbody
- @response_has_body = resbody
- raise ArgumentError, "no HTTP request path given" unless path
- raise ArgumentError, "HTTP request path is empty" if path.empty?
- @path = path
- initialize_http_header initheader
- self['Accept'] ||= '*/*'
- self['User-Agent'] ||= 'Ruby'
- @body = nil
- @body_stream = nil
- end
-
- attr_reader :method
- attr_reader :path
-
- def inspect
- "\#<#{self.class} #{@method}>"
- end
-
- def request_body_permitted?
- @request_has_body
- end
-
- def response_body_permitted?
- @response_has_body
- end
-
- def body_exist?
- warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?" if $VERBOSE
- response_body_permitted?
- end
-
- attr_reader :body
-
- def body=(str)
- @body = str
- @body_stream = nil
- str
- end
-
- attr_reader :body_stream
-
- def body_stream=(input)
- @body = nil
- @body_stream = input
- input
- end
-
- def set_body_internal(str) #:nodoc: internal use only
- raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream)
- self.body = str if str
- end
-
- #
- # write
- #
-
- def exec(sock, ver, path) #:nodoc: internal use only
- if @body
- send_request_with_body sock, ver, path, @body
- elsif @body_stream
- send_request_with_body_stream sock, ver, path, @body_stream
- else
- write_header sock, ver, path
- end
- end
-
- private
-
- def send_request_with_body(sock, ver, path, body)
- self.content_length = body.bytesize
- delete 'Transfer-Encoding'
- supply_default_content_type
- write_header sock, ver, path
- sock.write body
- end
-
- def send_request_with_body_stream(sock, ver, path, f)
- unless content_length() or chunked?
- raise ArgumentError,
- "Content-Length not given and Transfer-Encoding is not `chunked'"
- end
- supply_default_content_type
- write_header sock, ver, path
- if chunked?
- while s = f.read(1024)
- sock.write(sprintf("%x\r\n", s.length) << s << "\r\n")
- end
- sock.write "0\r\n\r\n"
- else
- while s = f.read(1024)
- sock.write s
- end
- end
- end
-
- def supply_default_content_type
- return if content_type()
- warn 'net/http: warning: Content-Type did not set; using application/x-www-form-urlencoded' if $VERBOSE
- set_content_type 'application/x-www-form-urlencoded'
- end
-
- def write_header(sock, ver, path)
- buf = "#{@method} #{path} HTTP/#{ver}\r\n"
- each_capitalized do |k,v|
- buf << "#{k}: #{v}\r\n"
- end
- buf << "\r\n"
- sock.write buf
- end
-
- end
-
-
- #
- # HTTP request class. This class wraps request header and entity path.
- # You *must* use its subclass, Net::HTTP::Get, Post, Head.
- #
- class HTTPRequest < HTTPGenericRequest
-
- # Creates HTTP request object.
- def initialize(path, initheader = nil)
- super self.class::METHOD,
- self.class::REQUEST_HAS_BODY,
- self.class::RESPONSE_HAS_BODY,
- path, initheader
- end
- end
-
-
- class HTTP # reopen
- #
- # HTTP 1.1 methods --- RFC2616
- #
-
- class Get < HTTPRequest
- METHOD = 'GET'
- REQUEST_HAS_BODY = false
- RESPONSE_HAS_BODY = true
- end
-
- class Head < HTTPRequest
- METHOD = 'HEAD'
- REQUEST_HAS_BODY = false
- RESPONSE_HAS_BODY = false
- end
-
- class Post < HTTPRequest
- METHOD = 'POST'
- REQUEST_HAS_BODY = true
- RESPONSE_HAS_BODY = true
- end
-
- class Put < HTTPRequest
- METHOD = 'PUT'
- REQUEST_HAS_BODY = true
- RESPONSE_HAS_BODY = true
- end
-
- class Delete < HTTPRequest
- METHOD = 'DELETE'
- REQUEST_HAS_BODY = false
- RESPONSE_HAS_BODY = true
- end
+require_relative 'http/header'
- class Options < HTTPRequest
- METHOD = 'OPTIONS'
- REQUEST_HAS_BODY = false
- RESPONSE_HAS_BODY = false
- end
-
- class Trace < HTTPRequest
- METHOD = 'TRACE'
- REQUEST_HAS_BODY = false
- RESPONSE_HAS_BODY = true
- end
-
- #
- # WebDAV methods --- RFC2518
- #
-
- class Propfind < HTTPRequest
- METHOD = 'PROPFIND'
- REQUEST_HAS_BODY = true
- RESPONSE_HAS_BODY = true
- end
-
- class Proppatch < HTTPRequest
- METHOD = 'PROPPATCH'
- REQUEST_HAS_BODY = true
- RESPONSE_HAS_BODY = true
- end
-
- class Mkcol < HTTPRequest
- METHOD = 'MKCOL'
- REQUEST_HAS_BODY = true
- RESPONSE_HAS_BODY = true
- end
-
- class Copy < HTTPRequest
- METHOD = 'COPY'
- REQUEST_HAS_BODY = false
- RESPONSE_HAS_BODY = true
- end
-
- class Move < HTTPRequest
- METHOD = 'MOVE'
- REQUEST_HAS_BODY = false
- RESPONSE_HAS_BODY = true
- end
-
- class Lock < HTTPRequest
- METHOD = 'LOCK'
- REQUEST_HAS_BODY = true
- RESPONSE_HAS_BODY = true
- end
-
- class Unlock < HTTPRequest
- METHOD = 'UNLOCK'
- REQUEST_HAS_BODY = true
- RESPONSE_HAS_BODY = true
- end
- end
-
-
- ###
- ### Response
- ###
-
- # HTTP exception class.
- # You must use its subclasses.
- module HTTPExceptions
- def initialize(msg, res) #:nodoc:
- super msg
- @response = res
- end
- attr_reader :response
- alias data response #:nodoc: obsolete
- end
- class HTTPError < ProtocolError
- include HTTPExceptions
- end
- class HTTPRetriableError < ProtoRetriableError
- include HTTPExceptions
- end
- class HTTPServerException < ProtoServerError
- # We cannot use the name "HTTPServerError", it is the name of the response.
- include HTTPExceptions
- end
- class HTTPFatalError < ProtoFatalError
- include HTTPExceptions
- end
-
-
- # HTTP response class. This class wraps response header and entity.
- # Mixes in the HTTPHeader module, which provides access to response
- # header values both via hash-like methods and individual readers.
- # Note that each possible HTTP response code defines its own
- # HTTPResponse subclass. These are listed below.
- # All classes are
- # defined under the Net module. Indentation indicates inheritance.
- #
- # xxx HTTPResponse
- #
- # 1xx HTTPInformation
- # 100 HTTPContinue
- # 101 HTTPSwitchProtocol
- #
- # 2xx HTTPSuccess
- # 200 HTTPOK
- # 201 HTTPCreated
- # 202 HTTPAccepted
- # 203 HTTPNonAuthoritativeInformation
- # 204 HTTPNoContent
- # 205 HTTPResetContent
- # 206 HTTPPartialContent
- #
- # 3xx HTTPRedirection
- # 300 HTTPMultipleChoice
- # 301 HTTPMovedPermanently
- # 302 HTTPFound
- # 303 HTTPSeeOther
- # 304 HTTPNotModified
- # 305 HTTPUseProxy
- # 307 HTTPTemporaryRedirect
- #
- # 4xx HTTPClientError
- # 400 HTTPBadRequest
- # 401 HTTPUnauthorized
- # 402 HTTPPaymentRequired
- # 403 HTTPForbidden
- # 404 HTTPNotFound
- # 405 HTTPMethodNotAllowed
- # 406 HTTPNotAcceptable
- # 407 HTTPProxyAuthenticationRequired
- # 408 HTTPRequestTimeOut
- # 409 HTTPConflict
- # 410 HTTPGone
- # 411 HTTPLengthRequired
- # 412 HTTPPreconditionFailed
- # 413 HTTPRequestEntityTooLarge
- # 414 HTTPRequestURITooLong
- # 415 HTTPUnsupportedMediaType
- # 416 HTTPRequestedRangeNotSatisfiable
- # 417 HTTPExpectationFailed
- #
- # 5xx HTTPServerError
- # 500 HTTPInternalServerError
- # 501 HTTPNotImplemented
- # 502 HTTPBadGateway
- # 503 HTTPServiceUnavailable
- # 504 HTTPGatewayTimeOut
- # 505 HTTPVersionNotSupported
- #
- # xxx HTTPUnknownResponse
- #
- class HTTPResponse
- # true if the response has body.
- def HTTPResponse.body_permitted?
- self::HAS_BODY
- end
-
- def HTTPResponse.exception_type # :nodoc: internal use only
- self::EXCEPTION_TYPE
- end
- end # reopened after
-
- # :stopdoc:
-
- class HTTPUnknownResponse < HTTPResponse
- HAS_BODY = true
- EXCEPTION_TYPE = HTTPError
- end
- class HTTPInformation < HTTPResponse # 1xx
- HAS_BODY = false
- EXCEPTION_TYPE = HTTPError
- end
- class HTTPSuccess < HTTPResponse # 2xx
- HAS_BODY = true
- EXCEPTION_TYPE = HTTPError
- end
- class HTTPRedirection < HTTPResponse # 3xx
- HAS_BODY = true
- EXCEPTION_TYPE = HTTPRetriableError
- end
- class HTTPClientError < HTTPResponse # 4xx
- HAS_BODY = true
- EXCEPTION_TYPE = HTTPServerException # for backward compatibility
- end
- class HTTPServerError < HTTPResponse # 5xx
- HAS_BODY = true
- EXCEPTION_TYPE = HTTPFatalError # for backward compatibility
- end
-
- class HTTPContinue < HTTPInformation # 100
- HAS_BODY = false
- end
- class HTTPSwitchProtocol < HTTPInformation # 101
- HAS_BODY = false
- end
-
- class HTTPOK < HTTPSuccess # 200
- HAS_BODY = true
- end
- class HTTPCreated < HTTPSuccess # 201
- HAS_BODY = true
- end
- class HTTPAccepted < HTTPSuccess # 202
- HAS_BODY = true
- end
- class HTTPNonAuthoritativeInformation < HTTPSuccess # 203
- HAS_BODY = true
- end
- class HTTPNoContent < HTTPSuccess # 204
- HAS_BODY = false
- end
- class HTTPResetContent < HTTPSuccess # 205
- HAS_BODY = false
- end
- class HTTPPartialContent < HTTPSuccess # 206
- HAS_BODY = true
- end
-
- class HTTPMultipleChoice < HTTPRedirection # 300
- HAS_BODY = true
- end
- class HTTPMovedPermanently < HTTPRedirection # 301
- HAS_BODY = true
- end
- class HTTPFound < HTTPRedirection # 302
- HAS_BODY = true
- end
- HTTPMovedTemporarily = HTTPFound
- class HTTPSeeOther < HTTPRedirection # 303
- HAS_BODY = true
- end
- class HTTPNotModified < HTTPRedirection # 304
- HAS_BODY = false
- end
- class HTTPUseProxy < HTTPRedirection # 305
- HAS_BODY = false
- end
- # 306 unused
- class HTTPTemporaryRedirect < HTTPRedirection # 307
- HAS_BODY = true
- end
-
- class HTTPBadRequest < HTTPClientError # 400
- HAS_BODY = true
- end
- class HTTPUnauthorized < HTTPClientError # 401
- HAS_BODY = true
- end
- class HTTPPaymentRequired < HTTPClientError # 402
- HAS_BODY = true
- end
- class HTTPForbidden < HTTPClientError # 403
- HAS_BODY = true
- end
- class HTTPNotFound < HTTPClientError # 404
- HAS_BODY = true
- end
- class HTTPMethodNotAllowed < HTTPClientError # 405
- HAS_BODY = true
- end
- class HTTPNotAcceptable < HTTPClientError # 406
- HAS_BODY = true
- end
- class HTTPProxyAuthenticationRequired < HTTPClientError # 407
- HAS_BODY = true
- end
- class HTTPRequestTimeOut < HTTPClientError # 408
- HAS_BODY = true
- end
- class HTTPConflict < HTTPClientError # 409
- HAS_BODY = true
- end
- class HTTPGone < HTTPClientError # 410
- HAS_BODY = true
- end
- class HTTPLengthRequired < HTTPClientError # 411
- HAS_BODY = true
- end
- class HTTPPreconditionFailed < HTTPClientError # 412
- HAS_BODY = true
- end
- class HTTPRequestEntityTooLarge < HTTPClientError # 413
- HAS_BODY = true
- end
- class HTTPRequestURITooLong < HTTPClientError # 414
- HAS_BODY = true
- end
- HTTPRequestURITooLarge = HTTPRequestURITooLong
- class HTTPUnsupportedMediaType < HTTPClientError # 415
- HAS_BODY = true
- end
- class HTTPRequestedRangeNotSatisfiable < HTTPClientError # 416
- HAS_BODY = true
- end
- class HTTPExpectationFailed < HTTPClientError # 417
- HAS_BODY = true
- end
-
- class HTTPInternalServerError < HTTPServerError # 500
- HAS_BODY = true
- end
- class HTTPNotImplemented < HTTPServerError # 501
- HAS_BODY = true
- end
- class HTTPBadGateway < HTTPServerError # 502
- HAS_BODY = true
- end
- class HTTPServiceUnavailable < HTTPServerError # 503
- HAS_BODY = true
- end
- class HTTPGatewayTimeOut < HTTPServerError # 504
- HAS_BODY = true
- end
- class HTTPVersionNotSupported < HTTPServerError # 505
- HAS_BODY = true
- end
-
- # :startdoc:
-
-
- class HTTPResponse # reopen
-
- CODE_CLASS_TO_OBJ = {
- '1' => HTTPInformation,
- '2' => HTTPSuccess,
- '3' => HTTPRedirection,
- '4' => HTTPClientError,
- '5' => HTTPServerError
- }
- CODE_TO_OBJ = {
- '100' => HTTPContinue,
- '101' => HTTPSwitchProtocol,
-
- '200' => HTTPOK,
- '201' => HTTPCreated,
- '202' => HTTPAccepted,
- '203' => HTTPNonAuthoritativeInformation,
- '204' => HTTPNoContent,
- '205' => HTTPResetContent,
- '206' => HTTPPartialContent,
-
- '300' => HTTPMultipleChoice,
- '301' => HTTPMovedPermanently,
- '302' => HTTPFound,
- '303' => HTTPSeeOther,
- '304' => HTTPNotModified,
- '305' => HTTPUseProxy,
- '307' => HTTPTemporaryRedirect,
-
- '400' => HTTPBadRequest,
- '401' => HTTPUnauthorized,
- '402' => HTTPPaymentRequired,
- '403' => HTTPForbidden,
- '404' => HTTPNotFound,
- '405' => HTTPMethodNotAllowed,
- '406' => HTTPNotAcceptable,
- '407' => HTTPProxyAuthenticationRequired,
- '408' => HTTPRequestTimeOut,
- '409' => HTTPConflict,
- '410' => HTTPGone,
- '411' => HTTPLengthRequired,
- '412' => HTTPPreconditionFailed,
- '413' => HTTPRequestEntityTooLarge,
- '414' => HTTPRequestURITooLong,
- '415' => HTTPUnsupportedMediaType,
- '416' => HTTPRequestedRangeNotSatisfiable,
- '417' => HTTPExpectationFailed,
-
- '500' => HTTPInternalServerError,
- '501' => HTTPNotImplemented,
- '502' => HTTPBadGateway,
- '503' => HTTPServiceUnavailable,
- '504' => HTTPGatewayTimeOut,
- '505' => HTTPVersionNotSupported
- }
-
- class << HTTPResponse
- def read_new(sock) #:nodoc: internal use only
- httpv, code, msg = read_status_line(sock)
- res = response_class(code).new(httpv, code, msg)
- each_response_header(sock) do |k,v|
- res.add_field k, v
- end
- res
- end
-
- private
-
- def read_status_line(sock)
- str = sock.readline
- m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/in.match(str) or
- raise HTTPBadResponse, "wrong status line: #{str.dump}"
- m.captures
- end
-
- def response_class(code)
- CODE_TO_OBJ[code] or
- CODE_CLASS_TO_OBJ[code[0,1]] or
- HTTPUnknownResponse
- end
-
- def each_response_header(sock)
- while true
- line = sock.readuntil("\n", true).sub(/\s+\z/, '')
- break if line.empty?
- m = /\A([^:]+):\s*/.match(line) or
- raise HTTPBadResponse, 'wrong header line format'
- yield m[1], m.post_match
- end
- end
- end
-
- # next is to fix bug in RDoc, where the private inside class << self
- # spills out.
- public
-
- include HTTPHeader
-
- def initialize(httpv, code, msg) #:nodoc: internal use only
- @http_version = httpv
- @code = code
- @message = msg
- initialize_http_header nil
- @body = nil
- @read = false
- end
-
- # The HTTP version supported by the server.
- attr_reader :http_version
-
- # HTTP result code string. For example, '302'. You can also
- # determine the response type by which response subclass the
- # response object is an instance of.
- attr_reader :code
-
- # HTTP result message. For example, 'Not Found'.
- attr_reader :message
- alias msg message # :nodoc: obsolete
-
- def inspect
- "#<#{self.class} #{@code} #{@message} readbody=#{@read}>"
- end
-
- # For backward compatibility.
- # To allow Net::HTTP 1.1 style assignment
- # e.g.
- # response, body = Net::HTTP.get(....)
- #
- def to_ary
- warn "net/http.rb: warning: Net::HTTP v1.1 style assignment found at #{caller(1)[0]}; use `response = http.get(...)' instead." if $VERBOSE
- res = self.dup
- class << res
- undef to_ary
- end
- [res, res.body]
- end
-
- #
- # response <-> exception relationship
- #
-
- def code_type #:nodoc:
- self.class
- end
-
- def error! #:nodoc:
- raise error_type().new(@code + ' ' + @message.dump, self)
- end
-
- def error_type #:nodoc:
- self.class::EXCEPTION_TYPE
- end
-
- # Raises HTTP error if the response is not 2xx.
- def value
- error! unless self.kind_of?(HTTPSuccess)
- end
-
- #
- # header (for backward compatibility only; DO NOT USE)
- #
-
- def response #:nodoc:
- warn "#{caller(1)[0]}: warning: HTTPResponse#response is obsolete" if $VERBOSE
- self
- end
-
- def header #:nodoc:
- warn "#{caller(1)[0]}: warning: HTTPResponse#header is obsolete" if $VERBOSE
- self
- end
-
- def read_header #:nodoc:
- warn "#{caller(1)[0]}: warning: HTTPResponse#read_header is obsolete" if $VERBOSE
- self
- end
-
- #
- # body
- #
-
- def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only
- @socket = sock
- @body_exist = reqmethodallowbody && self.class.body_permitted?
- begin
- yield
- self.body # ensure to read body
- ensure
- @socket = nil
- end
- end
-
- # Gets entity body. If the block given, yields it to +block+.
- # The body is provided in fragments, as it is read in from the socket.
- #
- # Calling this method a second or subsequent time will return the
- # already read string.
- #
- # http.request_get('/index.html') {|res|
- # puts res.read_body
- # }
- #
- # http.request_get('/index.html') {|res|
- # p res.read_body.object_id # 538149362
- # p res.read_body.object_id # 538149362
- # }
- #
- # # using iterator
- # http.request_get('/index.html') {|res|
- # res.read_body do |segment|
- # print segment
- # end
- # }
- #
- def read_body(dest = nil, &block)
- if @read
- raise IOError, "#{self.class}\#read_body called twice" if dest or block
- return @body
- end
- to = procdest(dest, block)
- stream_check
- if @body_exist
- read_body_0 to
- @body = to
- else
- @body = nil
- end
- @read = true
-
- @body
- end
-
- # Returns the entity body.
- #
- # Calling this method a second or subsequent time will return the
- # already read string.
- #
- # http.request_get('/index.html') {|res|
- # puts res.body
- # }
- #
- # http.request_get('/index.html') {|res|
- # p res.body.object_id # 538149362
- # p res.body.object_id # 538149362
- # }
- #
- def body
- read_body()
- end
-
- # Because it may be necessary to modify the body, Eg, decompression
- # this method facilitates that.
- def body=(value)
- @body = value
- end
-
- alias entity body #:nodoc: obsolete
-
- private
-
- def read_body_0(dest)
- if chunked?
- read_chunked dest
- return
- end
- clen = content_length()
- if clen
- @socket.read clen, dest, true # ignore EOF
- return
- end
- clen = range_length()
- if clen
- @socket.read clen, dest
- return
- end
- @socket.read_all dest
- end
-
- def read_chunked(dest)
- len = nil
- total = 0
- while true
- line = @socket.readline
- hexlen = line.slice(/[0-9a-fA-F]+/) or
- raise HTTPBadResponse, "wrong chunk size line: #{line}"
- len = hexlen.hex
- break if len == 0
- @socket.read len, dest; total += len
- @socket.read 2 # \r\n
- end
- until @socket.readline.empty?
- # none
- end
- end
-
- def stream_check
- raise IOError, 'attempt to read body out of block' if @socket.closed?
- end
-
- def procdest(dest, block)
- raise ArgumentError, 'both arg and block given for HTTP method' \
- if dest and block
- if block
- ReadAdapter.new(block)
- else
- dest || ''
- end
- end
-
- end
-
-
- # :enddoc:
-
- #--
- # for backward compatibility
- class HTTP
- ProxyMod = ProxyDelta
- end
- module NetPrivate
- HTTPRequest = ::Net::HTTPRequest
- end
+require_relative 'http/generic_request'
+require_relative 'http/request'
+require_relative 'http/requests'
- HTTPInformationCode = HTTPInformation
- HTTPSuccessCode = HTTPSuccess
- HTTPRedirectionCode = HTTPRedirection
- HTTPRetriableCode = HTTPRedirection
- HTTPClientErrorCode = HTTPClientError
- HTTPFatalErrorCode = HTTPClientError
- HTTPServerErrorCode = HTTPServerError
- HTTPResponceReceiver = HTTPResponse
+require_relative 'http/response'
+require_relative 'http/responses'
-end # module Net
+require_relative 'http/proxy_delta'
diff --git a/lib/net/http/exceptions.rb b/lib/net/http/exceptions.rb
new file mode 100644
index 0000000000..4342cfc0ef
--- /dev/null
+++ b/lib/net/http/exceptions.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+module Net
+ # Net::HTTP exception class.
+ # You cannot use Net::HTTPExceptions directly; instead, you must use
+ # its subclasses.
+ module HTTPExceptions # :nodoc:
+ def initialize(msg, res) #:nodoc:
+ super msg
+ @response = res
+ end
+ attr_reader :response
+ alias data response #:nodoc: obsolete
+ end
+
+ # :stopdoc:
+ class HTTPError < ProtocolError
+ include HTTPExceptions
+ end
+
+ class HTTPRetriableError < ProtoRetriableError
+ include HTTPExceptions
+ end
+
+ class HTTPClientException < ProtoServerError
+ include HTTPExceptions
+ end
+
+ class HTTPFatalError < ProtoFatalError
+ include HTTPExceptions
+ end
+
+ # We cannot use the name "HTTPServerError", it is the name of the response.
+ HTTPServerException = HTTPClientException # :nodoc:
+ deprecate_constant(:HTTPServerException)
+end
diff --git a/lib/net/http/generic_request.rb b/lib/net/http/generic_request.rb
new file mode 100644
index 0000000000..5b01ea4abd
--- /dev/null
+++ b/lib/net/http/generic_request.rb
@@ -0,0 +1,429 @@
+# frozen_string_literal: true
+#
+# \HTTPGenericRequest is the parent of the Net::HTTPRequest class.
+#
+# Do not use this directly; instead, use a subclass of Net::HTTPRequest.
+#
+# == About the Examples
+#
+# :include: doc/net-http/examples.rdoc
+#
+class Net::HTTPGenericRequest
+
+ include Net::HTTPHeader
+
+ def initialize(m, reqbody, resbody, uri_or_path, initheader = nil) # :nodoc:
+ @method = m
+ @request_has_body = reqbody
+ @response_has_body = resbody
+
+ if URI === uri_or_path then
+ raise ArgumentError, "not an HTTP URI" unless URI::HTTP === uri_or_path
+ hostname = uri_or_path.host
+ raise ArgumentError, "no host component for URI" unless (hostname && hostname.length > 0)
+ @uri = uri_or_path.dup
+ @path = uri_or_path.request_uri
+ raise ArgumentError, "no HTTP request path given" unless @path
+ else
+ @uri = nil
+ raise ArgumentError, "no HTTP request path given" unless uri_or_path
+ raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty?
+ @path = uri_or_path.dup
+ end
+
+ @decode_content = false
+
+ if Net::HTTP::HAVE_ZLIB then
+ if !initheader ||
+ !initheader.keys.any? { |k|
+ %w[accept-encoding range].include? k.downcase
+ } then
+ @decode_content = true if @response_has_body
+ initheader = initheader ? initheader.dup : {}
+ initheader["accept-encoding"] =
+ "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
+ end
+ end
+
+ initialize_http_header initheader
+ self['Accept'] ||= '*/*'
+ self['User-Agent'] ||= 'Ruby'
+ self['Host'] ||= @uri.authority if @uri
+ @body = nil
+ @body_stream = nil
+ @body_data = nil
+ end
+
+ # Returns the string method name for the request:
+ #
+ # Net::HTTP::Get.new(uri).method # => "GET"
+ # Net::HTTP::Post.new(uri).method # => "POST"
+ #
+ attr_reader :method
+
+ # Returns the string path for the request:
+ #
+ # Net::HTTP::Get.new(uri).path # => "/"
+ # Net::HTTP::Post.new('example.com').path # => "example.com"
+ #
+ attr_reader :path
+
+ # Returns the URI object for the request, or +nil+ if none:
+ #
+ # Net::HTTP::Get.new(uri).uri
+ # # => #<URI::HTTPS https://jsonplaceholder.typicode.com/>
+ # Net::HTTP::Get.new('example.com').uri # => nil
+ #
+ attr_reader :uri
+
+ # Returns +false+ if the request's header <tt>'Accept-Encoding'</tt>
+ # has been set manually or deleted
+ # (indicating that the user intends to handle encoding in the response),
+ # +true+ otherwise:
+ #
+ # req = Net::HTTP::Get.new(uri) # => #<Net::HTTP::Get GET>
+ # req['Accept-Encoding'] # => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
+ # req.decode_content # => true
+ # req['Accept-Encoding'] = 'foo'
+ # req.decode_content # => false
+ # req.delete('Accept-Encoding')
+ # req.decode_content # => false
+ #
+ attr_reader :decode_content
+
+ # Returns a string representation of the request:
+ #
+ # Net::HTTP::Post.new(uri).inspect # => "#<Net::HTTP::Post POST>"
+ #
+ def inspect
+ "\#<#{self.class} #{@method}>"
+ end
+
+ # Returns a string representation of the request with the details for pp:
+ #
+ # require 'pp'
+ # post = Net::HTTP::Post.new(uri)
+ # post.inspect # => "#<Net::HTTP::Post POST>"
+ # post.pretty_inspect
+ # # => #<Net::HTTP::Post
+ # POST
+ # path="/"
+ # headers={"accept-encoding" => ["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
+ # "accept" => ["*/*"],
+ # "user-agent" => ["Ruby"],
+ # "host" => ["www.ruby-lang.org"]}>
+ #
+ def pretty_print(q)
+ q.object_group(self) {
+ q.breakable
+ q.text @method
+ q.breakable
+ q.text "path="; q.pp @path
+ q.breakable
+ q.text "headers="; q.pp to_hash
+ }
+ end
+
+ ##
+ # Don't automatically decode response content-encoding if the user indicates
+ # they want to handle it.
+
+ def []=(key, val) # :nodoc:
+ @decode_content = false if key.downcase == 'accept-encoding'
+
+ super key, val
+ end
+
+ # Returns whether the request may have a body:
+ #
+ # Net::HTTP::Post.new(uri).request_body_permitted? # => true
+ # Net::HTTP::Get.new(uri).request_body_permitted? # => false
+ #
+ def request_body_permitted?
+ @request_has_body
+ end
+
+ # Returns whether the response may have a body:
+ #
+ # Net::HTTP::Post.new(uri).response_body_permitted? # => true
+ # Net::HTTP::Head.new(uri).response_body_permitted? # => false
+ #
+ def response_body_permitted?
+ @response_has_body
+ end
+
+ def body_exist? # :nodoc:
+ warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?", uplevel: 1 if $VERBOSE
+ response_body_permitted?
+ end
+
+ # Returns the string body for the request, or +nil+ if there is none:
+ #
+ # req = Net::HTTP::Post.new(uri)
+ # req.body # => nil
+ # req.body = '{"title": "foo","body": "bar","userId": 1}'
+ # req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}"
+ #
+ attr_reader :body
+
+ # Sets the body for the request:
+ #
+ # req = Net::HTTP::Post.new(uri)
+ # req.body # => nil
+ # req.body = '{"title": "foo","body": "bar","userId": 1}'
+ # req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}"
+ #
+ def body=(str)
+ @body = str
+ @body_stream = nil
+ @body_data = nil
+ str
+ end
+
+ # Returns the body stream object for the request, or +nil+ if there is none:
+ #
+ # req = Net::HTTP::Post.new(uri) # => #<Net::HTTP::Post POST>
+ # req.body_stream # => nil
+ # require 'stringio'
+ # req.body_stream = StringIO.new('xyzzy') # => #<StringIO:0x0000027d1e5affa8>
+ # req.body_stream # => #<StringIO:0x0000027d1e5affa8>
+ #
+ attr_reader :body_stream
+
+ # Sets the body stream for the request:
+ #
+ # req = Net::HTTP::Post.new(uri) # => #<Net::HTTP::Post POST>
+ # req.body_stream # => nil
+ # require 'stringio'
+ # req.body_stream = StringIO.new('xyzzy') # => #<StringIO:0x0000027d1e5affa8>
+ # req.body_stream # => #<StringIO:0x0000027d1e5affa8>
+ #
+ def body_stream=(input)
+ @body = nil
+ @body_stream = input
+ @body_data = nil
+ input
+ end
+
+ def set_body_internal(str) #:nodoc: internal use only
+ raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream)
+ self.body = str if str
+ if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted?
+ self.body = ''
+ end
+ end
+
+ #
+ # write
+ #
+
+ def exec(sock, ver, path) #:nodoc: internal use only
+ if @body
+ send_request_with_body sock, ver, path, @body
+ elsif @body_stream
+ send_request_with_body_stream sock, ver, path, @body_stream
+ elsif @body_data
+ send_request_with_body_data sock, ver, path, @body_data
+ else
+ write_header sock, ver, path
+ end
+ end
+
+ def update_uri(addr, port, ssl) # :nodoc: internal use only
+ # reflect the connection and @path to @uri
+ return unless @uri
+
+ if ssl
+ scheme = 'https'
+ klass = URI::HTTPS
+ else
+ scheme = 'http'
+ klass = URI::HTTP
+ end
+
+ if host = self['host']
+ host = URI.parse("//#{host}").host # Remove a port component from the existing Host header
+ elsif host = @uri.host
+ else
+ host = addr
+ end
+ # convert the class of the URI
+ if @uri.is_a?(klass)
+ @uri.host = host
+ @uri.port = port
+ else
+ @uri = klass.new(
+ scheme, @uri.userinfo,
+ host, port, nil,
+ @uri.path, nil, @uri.query, nil)
+ end
+ end
+
+ private
+
+ # :stopdoc:
+
+ class Chunker #:nodoc:
+ def initialize(sock)
+ @sock = sock
+ @prev = nil
+ end
+
+ def write(buf)
+ # avoid memcpy() of buf, buf can huge and eat memory bandwidth
+ rv = buf.bytesize
+ @sock.write("#{rv.to_s(16)}\r\n", buf, "\r\n")
+ rv
+ end
+
+ def finish
+ @sock.write("0\r\n\r\n")
+ end
+ end
+
+ def send_request_with_body(sock, ver, path, body)
+ self.content_length = body.bytesize
+ delete 'Transfer-Encoding'
+ write_header sock, ver, path
+ wait_for_continue sock, ver if sock.continue_timeout
+ sock.write body
+ end
+
+ def send_request_with_body_stream(sock, ver, path, f)
+ unless content_length() or chunked?
+ raise ArgumentError,
+ "Content-Length not given and Transfer-Encoding is not `chunked'"
+ end
+ write_header sock, ver, path
+ wait_for_continue sock, ver if sock.continue_timeout
+ if chunked?
+ chunker = Chunker.new(sock)
+ IO.copy_stream(f, chunker)
+ chunker.finish
+ else
+ IO.copy_stream(f, sock)
+ end
+ end
+
+ def send_request_with_body_data(sock, ver, path, params)
+ if /\Amultipart\/form-data\z/i !~ self.content_type
+ self.content_type = 'application/x-www-form-urlencoded'
+ return send_request_with_body(sock, ver, path, URI.encode_www_form(params))
+ end
+
+ opt = @form_option.dup
+ require 'securerandom' unless defined?(SecureRandom)
+ opt[:boundary] ||= SecureRandom.urlsafe_base64(40)
+ self.set_content_type(self.content_type, boundary: opt[:boundary])
+ if chunked?
+ write_header sock, ver, path
+ encode_multipart_form_data(sock, params, opt)
+ else
+ require 'tempfile'
+ file = Tempfile.new('multipart')
+ file.binmode
+ encode_multipart_form_data(file, params, opt)
+ file.rewind
+ self.content_length = file.size
+ write_header sock, ver, path
+ IO.copy_stream(file, sock)
+ file.close(true)
+ end
+ end
+
+ def encode_multipart_form_data(out, params, opt)
+ charset = opt[:charset]
+ boundary = opt[:boundary]
+ require 'securerandom' unless defined?(SecureRandom)
+ boundary ||= SecureRandom.urlsafe_base64(40)
+ chunked_p = chunked?
+
+ buf = +''
+ params.each do |key, value, h={}|
+ key = quote_string(key, charset)
+ filename =
+ h.key?(:filename) ? h[:filename] :
+ value.respond_to?(:to_path) ? File.basename(value.to_path) :
+ nil
+
+ buf << "--#{boundary}\r\n"
+ if filename
+ filename = quote_string(filename, charset)
+ type = h[:content_type] || 'application/octet-stream'
+ buf << "Content-Disposition: form-data; " \
+ "name=\"#{key}\"; filename=\"#{filename}\"\r\n" \
+ "Content-Type: #{type}\r\n\r\n"
+ if !out.respond_to?(:write) || !value.respond_to?(:read)
+ # if +out+ is not an IO or +value+ is not an IO
+ buf << (value.respond_to?(:read) ? value.read : value)
+ elsif value.respond_to?(:size) && chunked_p
+ # if +out+ is an IO and +value+ is a File, use IO.copy_stream
+ flush_buffer(out, buf, chunked_p)
+ out << "%x\r\n" % value.size if chunked_p
+ IO.copy_stream(value, out)
+ out << "\r\n" if chunked_p
+ else
+ # +out+ is an IO, and +value+ is not a File but an IO
+ flush_buffer(out, buf, chunked_p)
+ 1 while flush_buffer(out, value.read(4096), chunked_p)
+ end
+ else
+ # non-file field:
+ # HTML5 says, "The parts of the generated multipart/form-data
+ # resource that correspond to non-file fields must not have a
+ # Content-Type header specified."
+ buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
+ buf << (value.respond_to?(:read) ? value.read : value)
+ end
+ buf << "\r\n"
+ end
+ buf << "--#{boundary}--\r\n"
+ flush_buffer(out, buf, chunked_p)
+ out << "0\r\n\r\n" if chunked_p
+ end
+
+ def quote_string(str, charset)
+ str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset
+ str.gsub(/[\\"]/, '\\\\\&')
+ end
+
+ def flush_buffer(out, buf, chunked_p)
+ return unless buf
+ out << "%x\r\n"%buf.bytesize if chunked_p
+ out << buf
+ out << "\r\n" if chunked_p
+ buf.clear
+ end
+
+ ##
+ # Waits up to the continue timeout for a response from the server provided
+ # we're speaking HTTP 1.1 and are expecting a 100-continue response.
+
+ def wait_for_continue(sock, ver)
+ if ver >= '1.1' and @header['expect'] and
+ @header['expect'].include?('100-continue')
+ if sock.io.to_io.wait_readable(sock.continue_timeout)
+ res = Net::HTTPResponse.read_new(sock)
+ unless res.kind_of?(Net::HTTPContinue)
+ res.decode_content = @decode_content
+ throw :response, res
+ end
+ end
+ end
+ end
+
+ def write_header(sock, ver, path)
+ reqline = "#{@method} #{path} HTTP/#{ver}"
+ if /[\r\n]/ =~ reqline
+ raise ArgumentError, "A Request-Line must not contain CR or LF"
+ end
+ buf = +''
+ buf << reqline << "\r\n"
+ each_capitalized do |k,v|
+ buf << "#{k}: #{v}\r\n"
+ end
+ buf << "\r\n"
+ sock.write buf
+ end
+
+end
diff --git a/lib/net/http/header.rb b/lib/net/http/header.rb
new file mode 100644
index 0000000000..5dcdcc7d74
--- /dev/null
+++ b/lib/net/http/header.rb
@@ -0,0 +1,985 @@
+# frozen_string_literal: true
+#
+# The \HTTPHeader module provides access to \HTTP headers.
+#
+# The module is included in:
+#
+# - Net::HTTPGenericRequest (and therefore Net::HTTPRequest).
+# - Net::HTTPResponse.
+#
+# The headers are a hash-like collection of key/value pairs called _fields_.
+#
+# == Request and Response Fields
+#
+# Headers may be included in:
+#
+# - A Net::HTTPRequest object:
+# the object's headers will be sent with the request.
+# Any fields may be defined in the request;
+# see {Setters}[rdoc-ref:Net::HTTPHeader@Setters].
+# - A Net::HTTPResponse object:
+# the objects headers are usually those returned from the host.
+# Fields may be retrieved from the object;
+# see {Getters}[rdoc-ref:Net::HTTPHeader@Getters]
+# and {Iterators}[rdoc-ref:Net::HTTPHeader@Iterators].
+#
+# Exactly which fields should be sent or expected depends on the host;
+# see:
+#
+# - {Request fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields].
+# - {Response fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields].
+#
+# == About the Examples
+#
+# :include: doc/net-http/examples.rdoc
+#
+# == Fields
+#
+# A header field is a key/value pair.
+#
+# === Field Keys
+#
+# A field key may be:
+#
+# - A string: Key <tt>'Accept'</tt> is treated as if it were
+# <tt>'Accept'.downcase</tt>; i.e., <tt>'accept'</tt>.
+# - A symbol: Key <tt>:Accept</tt> is treated as if it were
+# <tt>:Accept.to_s.downcase</tt>; i.e., <tt>'accept'</tt>.
+#
+# Examples:
+#
+# req = Net::HTTP::Get.new(uri)
+# req[:accept] # => "*/*"
+# req['Accept'] # => "*/*"
+# req['ACCEPT'] # => "*/*"
+#
+# req['accept'] = 'text/html'
+# req[:accept] = 'text/html'
+# req['ACCEPT'] = 'text/html'
+#
+# === Field Values
+#
+# A field value may be returned as an array of strings or as a string:
+#
+# - These methods return field values as arrays:
+#
+# - #get_fields: Returns the array value for the given key,
+# or +nil+ if it does not exist.
+# - #to_hash: Returns a hash of all header fields:
+# each key is a field name; its value is the array value for the field.
+#
+# - These methods return field values as string;
+# the string value for a field is equivalent to
+# <tt>self[key.downcase.to_s].join(', '))</tt>:
+#
+# - #[]: Returns the string value for the given key,
+# or +nil+ if it does not exist.
+# - #fetch: Like #[], but accepts a default value
+# to be returned if the key does not exist.
+#
+# The field value may be set:
+#
+# - #[]=: Sets the value for the given key;
+# the given value may be a string, a symbol, an array, or a hash.
+# - #add_field: Adds a given value to a value for the given key
+# (not overwriting the existing value).
+# - #delete: Deletes the field for the given key.
+#
+# Example field values:
+#
+# - \String:
+#
+# req['Accept'] = 'text/html' # => "text/html"
+# req['Accept'] # => "text/html"
+# req.get_fields('Accept') # => ["text/html"]
+#
+# - \Symbol:
+#
+# req['Accept'] = :text # => :text
+# req['Accept'] # => "text"
+# req.get_fields('Accept') # => ["text"]
+#
+# - Simple array:
+#
+# req[:foo] = %w[bar baz bat]
+# req[:foo] # => "bar, baz, bat"
+# req.get_fields(:foo) # => ["bar", "baz", "bat"]
+#
+# - Simple hash:
+#
+# req[:foo] = {bar: 0, baz: 1, bat: 2}
+# req[:foo] # => "bar, 0, baz, 1, bat, 2"
+# req.get_fields(:foo) # => ["bar", "0", "baz", "1", "bat", "2"]
+#
+# - Nested:
+#
+# req[:foo] = [%w[bar baz], {bat: 0, bam: 1}]
+# req[:foo] # => "bar, baz, bat, 0, bam, 1"
+# req.get_fields(:foo) # => ["bar", "baz", "bat", "0", "bam", "1"]
+#
+# req[:foo] = {bar: %w[baz bat], bam: {bah: 0, bad: 1}}
+# req[:foo] # => "bar, baz, bat, bam, bah, 0, bad, 1"
+# req.get_fields(:foo) # => ["bar", "baz", "bat", "bam", "bah", "0", "bad", "1"]
+#
+# == Convenience Methods
+#
+# Various convenience methods retrieve values, set values, query values,
+# set form values, or iterate over fields.
+#
+# === Setters
+#
+# \Method #[]= can set any field, but does little to validate the new value;
+# some of the other setter methods provide some validation:
+#
+# - #[]=: Sets the string or array value for the given key.
+# - #add_field: Creates or adds to the array value for the given key.
+# - #basic_auth: Sets the string authorization header for <tt>'Authorization'</tt>.
+# - #content_length=: Sets the integer length for field <tt>'Content-Length</tt>.
+# - #content_type=: Sets the string value for field <tt>'Content-Type'</tt>.
+# - #proxy_basic_auth: Sets the string authorization header for <tt>'Proxy-Authorization'</tt>.
+# - #set_range: Sets the value for field <tt>'Range'</tt>.
+#
+# === Form Setters
+#
+# - #set_form: Sets an HTML form data set.
+# - #set_form_data: Sets header fields and a body from HTML form data.
+#
+# === Getters
+#
+# \Method #[] can retrieve the value of any field that exists,
+# but always as a string;
+# some of the other getter methods return something different
+# from the simple string value:
+#
+# - #[]: Returns the string field value for the given key.
+# - #content_length: Returns the integer value of field <tt>'Content-Length'</tt>.
+# - #content_range: Returns the Range value of field <tt>'Content-Range'</tt>.
+# - #content_type: Returns the string value of field <tt>'Content-Type'</tt>.
+# - #fetch: Returns the string field value for the given key.
+# - #get_fields: Returns the array field value for the given +key+.
+# - #main_type: Returns first part of the string value of field <tt>'Content-Type'</tt>.
+# - #sub_type: Returns second part of the string value of field <tt>'Content-Type'</tt>.
+# - #range: Returns an array of Range objects of field <tt>'Range'</tt>, or +nil+.
+# - #range_length: Returns the integer length of the range given in field <tt>'Content-Range'</tt>.
+# - #type_params: Returns the string parameters for <tt>'Content-Type'</tt>.
+#
+# === Queries
+#
+# - #chunked?: Returns whether field <tt>'Transfer-Encoding'</tt> is set to <tt>'chunked'</tt>.
+# - #connection_close?: Returns whether field <tt>'Connection'</tt> is set to <tt>'close'</tt>.
+# - #connection_keep_alive?: Returns whether field <tt>'Connection'</tt> is set to <tt>'keep-alive'</tt>.
+# - #key?: Returns whether a given key exists.
+#
+# === Iterators
+#
+# - #each_capitalized: Passes each field capitalized-name/value pair to the block.
+# - #each_capitalized_name: Passes each capitalized field name to the block.
+# - #each_header: Passes each field name/value pair to the block.
+# - #each_name: Passes each field name to the block.
+# - #each_value: Passes each string field value to the block.
+#
+module Net::HTTPHeader
+ # The maximum length of HTTP header keys.
+ MAX_KEY_LENGTH = 1024
+ # The maximum length of HTTP header values.
+ MAX_FIELD_LENGTH = 65536
+
+ def initialize_http_header(initheader) #:nodoc:
+ @header = {}
+ return unless initheader
+ initheader.each do |key, value|
+ warn "net/http: duplicated HTTP header: #{key}", uplevel: 3 if key?(key) and $VERBOSE
+ if value.nil?
+ warn "net/http: nil HTTP header: #{key}", uplevel: 3 if $VERBOSE
+ else
+ value = value.strip # raise error for invalid byte sequences
+ if key.to_s.bytesize > MAX_KEY_LENGTH
+ raise ArgumentError, "too long (#{key.bytesize} bytes) header: #{key[0, 30].inspect}..."
+ end
+ if value.to_s.bytesize > MAX_FIELD_LENGTH
+ raise ArgumentError, "header #{key} has too long field value: #{value.bytesize}"
+ end
+ if value.count("\r\n") > 0
+ raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF"
+ end
+ @header[key.downcase.to_s] = [value]
+ end
+ end
+ end
+
+ def size #:nodoc: obsolete
+ @header.size
+ end
+
+ alias length size #:nodoc: obsolete
+
+ # Returns the string field value for the case-insensitive field +key+,
+ # or +nil+ if there is no such key;
+ # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Connection'] # => "keep-alive"
+ # res['Nosuch'] # => nil
+ #
+ # Note that some field values may be retrieved via convenience methods;
+ # see {Getters}[rdoc-ref:Net::HTTPHeader@Getters].
+ def [](key)
+ a = @header[key.downcase.to_s] or return nil
+ a.join(', ')
+ end
+
+ # Sets the value for the case-insensitive +key+ to +val+,
+ # overwriting the previous value if the field exists;
+ # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]:
+ #
+ # req = Net::HTTP::Get.new(uri)
+ # req['Accept'] # => "*/*"
+ # req['Accept'] = 'text/html'
+ # req['Accept'] # => "text/html"
+ #
+ # Note that some field values may be set via convenience methods;
+ # see {Setters}[rdoc-ref:Net::HTTPHeader@Setters].
+ def []=(key, val)
+ unless val
+ @header.delete key.downcase.to_s
+ return val
+ end
+ set_field(key, val)
+ end
+
+ # Adds value +val+ to the value array for field +key+ if the field exists;
+ # creates the field with the given +key+ and +val+ if it does not exist.
+ # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]:
+ #
+ # req = Net::HTTP::Get.new(uri)
+ # req.add_field('Foo', 'bar')
+ # req['Foo'] # => "bar"
+ # req.add_field('Foo', 'baz')
+ # req['Foo'] # => "bar, baz"
+ # req.add_field('Foo', %w[baz bam])
+ # req['Foo'] # => "bar, baz, baz, bam"
+ # req.get_fields('Foo') # => ["bar", "baz", "baz", "bam"]
+ #
+ def add_field(key, val)
+ stringified_downcased_key = key.downcase.to_s
+ if @header.key?(stringified_downcased_key)
+ append_field_value(@header[stringified_downcased_key], val)
+ else
+ set_field(key, val)
+ end
+ end
+
+ # :stopdoc:
+ private def set_field(key, val)
+ case val
+ when Enumerable
+ ary = []
+ append_field_value(ary, val)
+ @header[key.downcase.to_s] = ary
+ else
+ val = val.to_s # for compatibility use to_s instead of to_str
+ if val.b.count("\r\n") > 0
+ raise ArgumentError, 'header field value cannot include CR/LF'
+ end
+ @header[key.downcase.to_s] = [val]
+ end
+ end
+
+ private def append_field_value(ary, val)
+ case val
+ when Enumerable
+ val.each{|x| append_field_value(ary, x)}
+ else
+ val = val.to_s
+ if /[\r\n]/n.match?(val.b)
+ raise ArgumentError, 'header field value cannot include CR/LF'
+ end
+ ary.push val
+ end
+ end
+ # :startdoc:
+
+ # Returns the array field value for the given +key+,
+ # or +nil+ if there is no such field;
+ # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res.get_fields('Connection') # => ["keep-alive"]
+ # res.get_fields('Nosuch') # => nil
+ #
+ def get_fields(key)
+ stringified_downcased_key = key.downcase.to_s
+ return nil unless @header[stringified_downcased_key]
+ @header[stringified_downcased_key].dup
+ end
+
+ # call-seq:
+ # fetch(key, default_val = nil) {|key| ... } -> object
+ # fetch(key, default_val = nil) -> value or default_val
+ #
+ # With a block, returns the string value for +key+ if it exists;
+ # otherwise returns the value of the block;
+ # ignores the +default_val+;
+ # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ #
+ # # Field exists; block not called.
+ # res.fetch('Connection') do |value|
+ # fail 'Cannot happen'
+ # end # => "keep-alive"
+ #
+ # # Field does not exist; block called.
+ # res.fetch('Nosuch') do |value|
+ # value.downcase
+ # end # => "nosuch"
+ #
+ # With no block, returns the string value for +key+ if it exists;
+ # otherwise, returns +default_val+ if it was given;
+ # otherwise raises an exception:
+ #
+ # res.fetch('Connection', 'Foo') # => "keep-alive"
+ # res.fetch('Nosuch', 'Foo') # => "Foo"
+ # res.fetch('Nosuch') # Raises KeyError.
+ #
+ def fetch(key, *args, &block) #:yield: +key+
+ a = @header.fetch(key.downcase.to_s, *args, &block)
+ a.kind_of?(Array) ? a.join(', ') : a
+ end
+
+ # Calls the block with each key/value pair:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res.each_header do |key, value|
+ # p [key, value] if key.start_with?('c')
+ # end
+ #
+ # Output:
+ #
+ # ["content-type", "application/json; charset=utf-8"]
+ # ["connection", "keep-alive"]
+ # ["cache-control", "max-age=43200"]
+ # ["cf-cache-status", "HIT"]
+ # ["cf-ray", "771d17e9bc542cf5-ORD"]
+ #
+ # Returns an enumerator if no block is given.
+ #
+ # Net::HTTPHeader#each is an alias for Net::HTTPHeader#each_header.
+ def each_header #:yield: +key+, +value+
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each do |k,va|
+ yield k, va.join(', ')
+ end
+ end
+
+ alias each each_header
+
+ # Calls the block with each field key:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res.each_key do |key|
+ # p key if key.start_with?('c')
+ # end
+ #
+ # Output:
+ #
+ # "content-type"
+ # "connection"
+ # "cache-control"
+ # "cf-cache-status"
+ # "cf-ray"
+ #
+ # Returns an enumerator if no block is given.
+ #
+ # Net::HTTPHeader#each_name is an alias for Net::HTTPHeader#each_key.
+ def each_name(&block) #:yield: +key+
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each_key(&block)
+ end
+
+ alias each_key each_name
+
+ # Calls the block with each capitalized field name:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res.each_capitalized_name do |key|
+ # p key if key.start_with?('C')
+ # end
+ #
+ # Output:
+ #
+ # "Content-Type"
+ # "Connection"
+ # "Cache-Control"
+ # "Cf-Cache-Status"
+ # "Cf-Ray"
+ #
+ # The capitalization is system-dependent;
+ # see {Case Mapping}[rdoc-ref:case_mapping.rdoc].
+ #
+ # Returns an enumerator if no block is given.
+ def each_capitalized_name #:yield: +key+
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each_key do |k|
+ yield capitalize(k)
+ end
+ end
+
+ # Calls the block with each string field value:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res.each_value do |value|
+ # p value if value.start_with?('c')
+ # end
+ #
+ # Output:
+ #
+ # "chunked"
+ # "cf-q-config;dur=6.0000002122251e-06"
+ # "cloudflare"
+ #
+ # Returns an enumerator if no block is given.
+ def each_value #:yield: +value+
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each_value do |va|
+ yield va.join(', ')
+ end
+ end
+
+ # Removes the header for the given case-insensitive +key+
+ # (see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]);
+ # returns the deleted value, or +nil+ if no such field exists:
+ #
+ # req = Net::HTTP::Get.new(uri)
+ # req.delete('Accept') # => ["*/*"]
+ # req.delete('Nosuch') # => nil
+ #
+ def delete(key)
+ @header.delete(key.downcase.to_s)
+ end
+
+ # Returns +true+ if the field for the case-insensitive +key+ exists, +false+ otherwise:
+ #
+ # req = Net::HTTP::Get.new(uri)
+ # req.key?('Accept') # => true
+ # req.key?('Nosuch') # => false
+ #
+ def key?(key)
+ @header.key?(key.downcase.to_s)
+ end
+
+ # Returns a hash of the key/value pairs:
+ #
+ # req = Net::HTTP::Get.new(uri)
+ # req.to_hash
+ # # =>
+ # {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
+ # "accept"=>["*/*"],
+ # "user-agent"=>["Ruby"],
+ # "host"=>["jsonplaceholder.typicode.com"]}
+ #
+ def to_hash
+ @header.dup
+ end
+
+ # Like #each_header, but the keys are returned in capitalized form.
+ #
+ # Net::HTTPHeader#canonical_each is an alias for Net::HTTPHeader#each_capitalized.
+ def each_capitalized
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each do |k,v|
+ yield capitalize(k), v.join(', ')
+ end
+ end
+
+ alias canonical_each each_capitalized
+
+ def capitalize(name) # :nodoc:
+ name.to_s.split('-'.freeze).map {|s| s.capitalize }.join('-'.freeze)
+ end
+ private :capitalize
+
+ # Returns an array of Range objects that represent
+ # the value of field <tt>'Range'</tt>,
+ # or +nil+ if there is no such field;
+ # see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]:
+ #
+ # req = Net::HTTP::Get.new(uri)
+ # req['Range'] = 'bytes=0-99,200-299,400-499'
+ # req.range # => [0..99, 200..299, 400..499]
+ # req.delete('Range')
+ # req.range # # => nil
+ #
+ def range
+ return nil unless @header['range']
+
+ value = self['Range']
+ # byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec )
+ # *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] )
+ # corrected collected ABNF
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5
+ unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value
+ raise Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'"
+ end
+
+ byte_range_set = $1
+ result = byte_range_set.split(/,/).map {|spec|
+ m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or
+ raise Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'"
+ d1 = m[1].to_i
+ d2 = m[2].to_i
+ if m[1] and m[2]
+ if d1 > d2
+ raise Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'"
+ end
+ d1..d2
+ elsif m[1]
+ d1..-1
+ elsif m[2]
+ -d2..-1
+ else
+ raise Net::HTTPHeaderSyntaxError, 'range is not specified'
+ end
+ }
+ # if result.empty?
+ # byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec
+ # but above regexp already denies it.
+ if result.size == 1 && result[0].begin == 0 && result[0].end == -1
+ raise Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length'
+ end
+ result
+ end
+
+ # call-seq:
+ # set_range(length) -> length
+ # set_range(offset, length) -> range
+ # set_range(begin..length) -> range
+ #
+ # Sets the value for field <tt>'Range'</tt>;
+ # see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]:
+ #
+ # With argument +length+:
+ #
+ # req = Net::HTTP::Get.new(uri)
+ # req.set_range(100) # => 100
+ # req['Range'] # => "bytes=0-99"
+ #
+ # With arguments +offset+ and +length+:
+ #
+ # req.set_range(100, 100) # => 100...200
+ # req['Range'] # => "bytes=100-199"
+ #
+ # With argument +range+:
+ #
+ # req.set_range(100..199) # => 100..199
+ # req['Range'] # => "bytes=100-199"
+ #
+ # Net::HTTPHeader#range= is an alias for Net::HTTPHeader#set_range.
+ def set_range(r, e = nil)
+ unless r
+ @header.delete 'range'
+ return r
+ end
+ r = (r...r+e) if e
+ case r
+ when Numeric
+ n = r.to_i
+ rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}")
+ when Range
+ first = r.first
+ last = r.end
+ last -= 1 if r.exclude_end?
+ if last == -1
+ rangestr = (first > 0 ? "#{first}-" : "-#{-first}")
+ else
+ raise Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0
+ raise Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0
+ raise Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last
+ rangestr = "#{first}-#{last}"
+ end
+ else
+ raise TypeError, 'Range/Integer is required'
+ end
+ @header['range'] = ["bytes=#{rangestr}"]
+ r
+ end
+
+ alias range= set_range
+
+ # Returns the value of field <tt>'Content-Length'</tt> as an integer,
+ # or +nil+ if there is no such field;
+ # see {Content-Length request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-request-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/nosuch/1')
+ # res.content_length # => 2
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res.content_length # => nil
+ #
+ def content_length
+ return nil unless key?('Content-Length')
+ len = self['Content-Length'].slice(/\d+/) or
+ raise Net::HTTPHeaderSyntaxError, 'wrong Content-Length format'
+ len.to_i
+ end
+
+ # Sets the value of field <tt>'Content-Length'</tt> to the given numeric;
+ # see {Content-Length response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-response-header]:
+ #
+ # _uri = uri.dup
+ # hostname = _uri.hostname # => "jsonplaceholder.typicode.com"
+ # _uri.path = '/posts' # => "/posts"
+ # req = Net::HTTP::Post.new(_uri) # => #<Net::HTTP::Post POST>
+ # req.body = '{"title": "foo","body": "bar","userId": 1}'
+ # req.content_length = req.body.size # => 42
+ # req.content_type = 'application/json'
+ # res = Net::HTTP.start(hostname) do |http|
+ # http.request(req)
+ # end # => #<Net::HTTPCreated 201 Created readbody=true>
+ #
+ def content_length=(len)
+ unless len
+ @header.delete 'content-length'
+ return nil
+ end
+ @header['content-length'] = [len.to_i.to_s]
+ end
+
+ # Returns +true+ if field <tt>'Transfer-Encoding'</tt>
+ # exists and has value <tt>'chunked'</tt>,
+ # +false+ otherwise;
+ # see {Transfer-Encoding response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#transfer-encoding-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Transfer-Encoding'] # => "chunked"
+ # res.chunked? # => true
+ #
+ def chunked?
+ return false unless @header['transfer-encoding']
+ field = self['Transfer-Encoding']
+ (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false
+ end
+
+ # Returns a Range object representing the value of field
+ # <tt>'Content-Range'</tt>, or +nil+ if no such field exists;
+ # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Content-Range'] # => nil
+ # res['Content-Range'] = 'bytes 0-499/1000'
+ # res['Content-Range'] # => "bytes 0-499/1000"
+ # res.content_range # => 0..499
+ #
+ def content_range
+ return nil unless @header['content-range']
+ m = %r<\A\s*(\w+)\s+(\d+)-(\d+)/(\d+|\*)>.match(self['Content-Range']) or
+ raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format'
+ return unless m[1] == 'bytes'
+ m[2].to_i .. m[3].to_i
+ end
+
+ # Returns the integer representing length of the value of field
+ # <tt>'Content-Range'</tt>, or +nil+ if no such field exists;
+ # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Content-Range'] # => nil
+ # res['Content-Range'] = 'bytes 0-499/1000'
+ # res.range_length # => 500
+ #
+ def range_length
+ r = content_range() or return nil
+ r.end - r.begin + 1
+ end
+
+ # Returns the {media type}[https://en.wikipedia.org/wiki/Media_type]
+ # from the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.content_type # => "application/json"
+ #
+ def content_type
+ main = main_type()
+ return nil unless main
+
+ sub = sub_type()
+ if sub
+ "#{main}/#{sub}"
+ else
+ main
+ end
+ end
+
+ # Returns the leading ('type') part of the
+ # {media type}[https://en.wikipedia.org/wiki/Media_type]
+ # from the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.main_type # => "application"
+ #
+ def main_type
+ return nil unless @header['content-type']
+ self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip
+ end
+
+ # Returns the trailing ('subtype') part of the
+ # {media type}[https://en.wikipedia.org/wiki/Media_type]
+ # from the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.sub_type # => "json"
+ #
+ def sub_type
+ return nil unless @header['content-type']
+ _, sub = *self['Content-Type'].split(';').first.to_s.split('/')
+ return nil unless sub
+ sub.strip
+ end
+
+ # Returns the trailing ('parameters') part of the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.type_params # => {"charset"=>"utf-8"}
+ #
+ def type_params
+ result = {}
+ list = self['Content-Type'].to_s.split(';')
+ list.shift
+ list.each do |param|
+ k, v = *param.split('=', 2)
+ result[k.strip] = v.strip
+ end
+ result
+ end
+
+ # Sets the value of field <tt>'Content-Type'</tt>;
+ # returns the new value;
+ # see {Content-Type request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-request-header]:
+ #
+ # req = Net::HTTP::Get.new(uri)
+ # req.set_content_type('application/json') # => ["application/json"]
+ #
+ # Net::HTTPHeader#content_type= is an alias for Net::HTTPHeader#set_content_type.
+ def set_content_type(type, params = {})
+ @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')]
+ end
+
+ alias content_type= set_content_type
+
+ # Sets the request body to a URL-encoded string derived from argument +params+,
+ # and sets request header field <tt>'Content-Type'</tt>
+ # to <tt>'application/x-www-form-urlencoded'</tt>.
+ #
+ # The resulting request is suitable for HTTP request +POST+ or +PUT+.
+ #
+ # Argument +params+ must be suitable for use as argument +enum+ to
+ # {URI.encode_www_form}[rdoc-ref:URI.encode_www_form].
+ #
+ # With only argument +params+ given,
+ # sets the body to a URL-encoded string with the default separator <tt>'&'</tt>:
+ #
+ # req = Net::HTTP::Post.new('example.com')
+ #
+ # req.set_form_data(q: 'ruby', lang: 'en')
+ # req.body # => "q=ruby&lang=en"
+ # req['Content-Type'] # => "application/x-www-form-urlencoded"
+ #
+ # req.set_form_data([['q', 'ruby'], ['lang', 'en']])
+ # req.body # => "q=ruby&lang=en"
+ #
+ # req.set_form_data(q: ['ruby', 'perl'], lang: 'en')
+ # req.body # => "q=ruby&q=perl&lang=en"
+ #
+ # req.set_form_data([['q', 'ruby'], ['q', 'perl'], ['lang', 'en']])
+ # req.body # => "q=ruby&q=perl&lang=en"
+ #
+ # With string argument +sep+ also given,
+ # uses that string as the separator:
+ #
+ # req.set_form_data({q: 'ruby', lang: 'en'}, '|')
+ # req.body # => "q=ruby|lang=en"
+ #
+ # Net::HTTPHeader#form_data= is an alias for Net::HTTPHeader#set_form_data.
+ def set_form_data(params, sep = '&')
+ query = URI.encode_www_form(params)
+ query.gsub!(/&/, sep) if sep != '&'
+ self.body = query
+ self.content_type = 'application/x-www-form-urlencoded'
+ end
+
+ alias form_data= set_form_data
+
+ # Stores form data to be used in a +POST+ or +PUT+ request.
+ #
+ # The form data given in +params+ consists of zero or more fields;
+ # each field is:
+ #
+ # - A scalar value.
+ # - A name/value pair.
+ # - An IO stream opened for reading.
+ #
+ # Argument +params+ should be an
+ # {Enumerable}[rdoc-ref:Enumerable@Enumerable+in+Ruby+Classes]
+ # (method <tt>params.map</tt> will be called),
+ # and is often an array or hash.
+ #
+ # First, we set up a request:
+ #
+ # _uri = uri.dup
+ # _uri.path ='/posts'
+ # req = Net::HTTP::Post.new(_uri)
+ #
+ # <b>Argument +params+ As an Array</b>
+ #
+ # When +params+ is an array,
+ # each of its elements is a subarray that defines a field;
+ # the subarray may contain:
+ #
+ # - One string:
+ #
+ # req.set_form([['foo'], ['bar'], ['baz']])
+ #
+ # - Two strings:
+ #
+ # req.set_form([%w[foo 0], %w[bar 1], %w[baz 2]])
+ #
+ # - When argument +enctype+ (see below) is given as
+ # <tt>'multipart/form-data'</tt>:
+ #
+ # - A string name and an IO stream opened for reading:
+ #
+ # require 'stringio'
+ # req.set_form([['file', StringIO.new('Ruby is cool.')]])
+ #
+ # - A string name, an IO stream opened for reading,
+ # and an options hash, which may contain these entries:
+ #
+ # - +:filename+: The name of the file to use.
+ # - +:content_type+: The content type of the uploaded file.
+ #
+ # Example:
+ #
+ # req.set_form([['file', file, {filename: "other-filename.foo"}]]
+ #
+ # The various forms may be mixed:
+ #
+ # req.set_form(['foo', %w[bar 1], ['file', file]])
+ #
+ # <b>Argument +params+ As a Hash</b>
+ #
+ # When +params+ is a hash,
+ # each of its entries is a name/value pair that defines a field:
+ #
+ # - The name is a string.
+ # - The value may be:
+ #
+ # - +nil+.
+ # - Another string.
+ # - An IO stream opened for reading
+ # (only when argument +enctype+ -- see below -- is given as
+ # <tt>'multipart/form-data'</tt>).
+ #
+ # Examples:
+ #
+ # # Nil-valued fields.
+ # req.set_form({'foo' => nil, 'bar' => nil, 'baz' => nil})
+ #
+ # # String-valued fields.
+ # req.set_form({'foo' => 0, 'bar' => 1, 'baz' => 2})
+ #
+ # # IO-valued field.
+ # require 'stringio'
+ # req.set_form({'file' => StringIO.new('Ruby is cool.')})
+ #
+ # # Mixture of fields.
+ # req.set_form({'foo' => nil, 'bar' => 1, 'file' => file})
+ #
+ # Optional argument +enctype+ specifies the value to be given
+ # to field <tt>'Content-Type'</tt>, and must be one of:
+ #
+ # - <tt>'application/x-www-form-urlencoded'</tt> (the default).
+ # - <tt>'multipart/form-data'</tt>;
+ # see {RFC 7578}[https://www.rfc-editor.org/rfc/rfc7578].
+ #
+ # Optional argument +formopt+ is a hash of options
+ # (applicable only when argument +enctype+
+ # is <tt>'multipart/form-data'</tt>)
+ # that may include the following entries:
+ #
+ # - +:boundary+: The value is the boundary string for the multipart message.
+ # If not given, the boundary is a random string.
+ # See {Boundary}[https://www.rfc-editor.org/rfc/rfc7578#section-4.1].
+ # - +:charset+: Value is the character set for the form submission.
+ # Field names and values of non-file fields should be encoded with this charset.
+ #
+ def set_form(params, enctype='application/x-www-form-urlencoded', formopt={})
+ @body_data = params
+ @body = nil
+ @body_stream = nil
+ @form_option = formopt
+ case enctype
+ when /\Aapplication\/x-www-form-urlencoded\z/i,
+ /\Amultipart\/form-data\z/i
+ self.content_type = enctype
+ else
+ raise ArgumentError, "invalid enctype: #{enctype}"
+ end
+ end
+
+ # Sets header <tt>'Authorization'</tt> using the given
+ # +account+ and +password+ strings:
+ #
+ # req.basic_auth('my_account', 'my_password')
+ # req['Authorization']
+ # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA=="
+ #
+ def basic_auth(account, password)
+ @header['authorization'] = [basic_encode(account, password)]
+ end
+
+ # Sets header <tt>'Proxy-Authorization'</tt> using the given
+ # +account+ and +password+ strings:
+ #
+ # req.proxy_basic_auth('my_account', 'my_password')
+ # req['Proxy-Authorization']
+ # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA=="
+ #
+ def proxy_basic_auth(account, password)
+ @header['proxy-authorization'] = [basic_encode(account, password)]
+ end
+
+ def basic_encode(account, password) # :nodoc:
+ 'Basic ' + ["#{account}:#{password}"].pack('m0')
+ end
+ private :basic_encode
+
+ # Returns whether the HTTP session is to be closed.
+ def connection_close?
+ token = /(?:\A|,)\s*close\s*(?:\z|,)/i
+ @header['connection']&.grep(token) {return true}
+ @header['proxy-connection']&.grep(token) {return true}
+ false
+ end
+
+ # Returns whether the HTTP session is to be kept alive.
+ def connection_keep_alive?
+ token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i
+ @header['connection']&.grep(token) {return true}
+ @header['proxy-connection']&.grep(token) {return true}
+ false
+ end
+
+end
diff --git a/lib/net/http/net-http.gemspec b/lib/net/http/net-http.gemspec
new file mode 100644
index 0000000000..d59d5c3b74
--- /dev/null
+++ b/lib/net/http/net-http.gemspec
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1, "..").join("/")].find do |dir|
+ file = File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")
+ begin
+ break File.foreach(file, mode: "rb") do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end
+ rescue SystemCallError
+ next
+ end
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["NARUSE, Yui"]
+ spec.email = ["naruse@airemix.jp"]
+
+ spec.summary = %q{HTTP client api for Ruby.}
+ spec.description = %q{HTTP client api for Ruby.}
+ spec.homepage = "https://github.com/ruby/net-http"
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.metadata["changelog_uri"] = spec.homepage + "/releases"
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ excludes = %W[/.git* /bin /test /test_sig /*file /#{File.basename(__FILE__)}]
+ spec.files = IO.popen(%W[git -C #{__dir__} ls-files -z --] + excludes.map {|e| ":^#{e}"}, &:read).split("\x0")
+ spec.bindir = "exe"
+ spec.require_paths = ["lib"]
+
+ spec.add_dependency "uri", ">= 0.11.1"
+end
diff --git a/lib/net/http/proxy_delta.rb b/lib/net/http/proxy_delta.rb
new file mode 100644
index 0000000000..e7d30def64
--- /dev/null
+++ b/lib/net/http/proxy_delta.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+module Net::HTTP::ProxyDelta #:nodoc: internal use only
+ private
+
+ def conn_address
+ proxy_address()
+ end
+
+ def conn_port
+ proxy_port()
+ end
+
+ def edit_path(path)
+ use_ssl? ? path : "http://#{addr_port()}#{path}"
+ end
+end
+
diff --git a/lib/net/http/request.rb b/lib/net/http/request.rb
new file mode 100644
index 0000000000..4a138572e9
--- /dev/null
+++ b/lib/net/http/request.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+# This class is the base class for \Net::HTTP request classes.
+# The class should not be used directly;
+# instead you should use its subclasses, listed below.
+#
+# == Creating a Request
+#
+# An request object may be created with either a URI or a string hostname:
+#
+# require 'net/http'
+# uri = URI('https://jsonplaceholder.typicode.com/')
+# req = Net::HTTP::Get.new(uri) # => #<Net::HTTP::Get GET>
+# req = Net::HTTP::Get.new(uri.hostname) # => #<Net::HTTP::Get GET>
+#
+# And with any of the subclasses:
+#
+# req = Net::HTTP::Head.new(uri) # => #<Net::HTTP::Head HEAD>
+# req = Net::HTTP::Post.new(uri) # => #<Net::HTTP::Post POST>
+# req = Net::HTTP::Put.new(uri) # => #<Net::HTTP::Put PUT>
+# # ...
+#
+# The new instance is suitable for use as the argument to Net::HTTP#request.
+#
+# == Request Headers
+#
+# A new request object has these header fields by default:
+#
+# req.to_hash
+# # =>
+# {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
+# "accept"=>["*/*"],
+# "user-agent"=>["Ruby"],
+# "host"=>["jsonplaceholder.typicode.com"]}
+#
+# See:
+#
+# - {Request header Accept-Encoding}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Accept-Encoding]
+# and {Compression and Decompression}[rdoc-ref:Net::HTTP@Compression+and+Decompression].
+# - {Request header Accept}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#accept-request-header].
+# - {Request header User-Agent}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#user-agent-request-header].
+# - {Request header Host}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#host-request-header].
+#
+# You can add headers or override default headers:
+#
+# # res = Net::HTTP::Get.new(uri, {'foo' => '0', 'bar' => '1'})
+#
+# This class (and therefore its subclasses) also includes (indirectly)
+# module Net::HTTPHeader, which gives access to its
+# {methods for setting headers}[rdoc-ref:Net::HTTPHeader@Setters].
+#
+# == Request Subclasses
+#
+# Subclasses for HTTP requests:
+#
+# - Net::HTTP::Get
+# - Net::HTTP::Head
+# - Net::HTTP::Post
+# - Net::HTTP::Put
+# - Net::HTTP::Delete
+# - Net::HTTP::Options
+# - Net::HTTP::Trace
+# - Net::HTTP::Patch
+#
+# Subclasses for WebDAV requests:
+#
+# - Net::HTTP::Propfind
+# - Net::HTTP::Proppatch
+# - Net::HTTP::Mkcol
+# - Net::HTTP::Copy
+# - Net::HTTP::Move
+# - Net::HTTP::Lock
+# - Net::HTTP::Unlock
+#
+class Net::HTTPRequest < Net::HTTPGenericRequest
+ # Creates an HTTP request object for +path+.
+ #
+ # +initheader+ are the default headers to use. Net::HTTP adds
+ # Accept-Encoding to enable compression of the response body unless
+ # Accept-Encoding or Range are supplied in +initheader+.
+
+ def initialize(path, initheader = nil)
+ super self.class::METHOD,
+ self.class::REQUEST_HAS_BODY,
+ self.class::RESPONSE_HAS_BODY,
+ path, initheader
+ end
+end
diff --git a/lib/net/http/requests.rb b/lib/net/http/requests.rb
new file mode 100644
index 0000000000..8dc79a9f66
--- /dev/null
+++ b/lib/net/http/requests.rb
@@ -0,0 +1,444 @@
+# frozen_string_literal: true
+
+# HTTP/1.1 methods --- RFC2616
+
+# \Class for representing
+# {HTTP method GET}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#GET_method]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Net::HTTP::Get.new(uri) # => #<Net::HTTP::Get GET>
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: optional.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/HTTP#Safe_method]: yes.
+# - {Idempotent}[https://en.wikipedia.org/wiki/HTTP#Idempotent_method]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/HTTP#Cacheable_method]: yes.
+#
+# Related:
+#
+# - Net::HTTP.get: sends +GET+ request, returns response body.
+# - Net::HTTP#get: sends +GET+ request, returns response object.
+#
+class Net::HTTP::Get < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'GET'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method HEAD}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#HEAD_method]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Net::HTTP::Head.new(uri) # => #<Net::HTTP::Head HEAD>
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: optional.
+# - Response body: no.
+# - {Safe}[https://en.wikipedia.org/wiki/HTTP#Safe_method]: yes.
+# - {Idempotent}[https://en.wikipedia.org/wiki/HTTP#Idempotent_method]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/HTTP#Cacheable_method]: yes.
+#
+# Related:
+#
+# - Net::HTTP#head: sends +HEAD+ request, returns response object.
+#
+class Net::HTTP::Head < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'HEAD'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = false
+end
+
+# \Class for representing
+# {HTTP method POST}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#POST_method]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# uri.path = '/posts'
+# req = Net::HTTP::Post.new(uri) # => #<Net::HTTP::Post POST>
+# req.body = '{"title": "foo","body": "bar","userId": 1}'
+# req.content_type = 'application/json'
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: yes.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/HTTP#Safe_method]: no.
+# - {Idempotent}[https://en.wikipedia.org/wiki/HTTP#Idempotent_method]: no.
+# - {Cacheable}[https://en.wikipedia.org/wiki/HTTP#Cacheable_method]: yes.
+#
+# Related:
+#
+# - Net::HTTP.post: sends +POST+ request, returns response object.
+# - Net::HTTP#post: sends +POST+ request, returns response object.
+#
+class Net::HTTP::Post < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'POST'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method PUT}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PUT_method]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# uri.path = '/posts'
+# req = Net::HTTP::Put.new(uri) # => #<Net::HTTP::Put PUT>
+# req.body = '{"title": "foo","body": "bar","userId": 1}'
+# req.content_type = 'application/json'
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: yes.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/HTTP#Safe_method]: no.
+# - {Idempotent}[https://en.wikipedia.org/wiki/HTTP#Idempotent_method]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/HTTP#Cacheable_method]: no.
+#
+# Related:
+#
+# - Net::HTTP.put: sends +PUT+ request, returns response object.
+# - Net::HTTP#put: sends +PUT+ request, returns response object.
+#
+class Net::HTTP::Put < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'PUT'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method DELETE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#DELETE_method]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# uri.path = '/posts/1'
+# req = Net::HTTP::Delete.new(uri) # => #<Net::HTTP::Delete DELETE>
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: optional.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/HTTP#Safe_method]: no.
+# - {Idempotent}[https://en.wikipedia.org/wiki/HTTP#Idempotent_method]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/HTTP#Cacheable_method]: no.
+#
+# Related:
+#
+# - Net::HTTP#delete: sends +DELETE+ request, returns response object.
+#
+class Net::HTTP::Delete < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'DELETE'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method OPTIONS}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#OPTIONS_method]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Net::HTTP::Options.new(uri) # => #<Net::HTTP::Options OPTIONS>
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: optional.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/HTTP#Safe_method]: yes.
+# - {Idempotent}[https://en.wikipedia.org/wiki/HTTP#Idempotent_method]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/HTTP#Cacheable_method]: no.
+#
+# Related:
+#
+# - Net::HTTP#options: sends +OPTIONS+ request, returns response object.
+#
+class Net::HTTP::Options < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'OPTIONS'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method TRACE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#TRACE_method]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Net::HTTP::Trace.new(uri) # => #<Net::HTTP::Trace TRACE>
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: no.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/HTTP#Safe_method]: yes.
+# - {Idempotent}[https://en.wikipedia.org/wiki/HTTP#Idempotent_method]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/HTTP#Cacheable_method]: no.
+#
+# Related:
+#
+# - Net::HTTP#trace: sends +TRACE+ request, returns response object.
+#
+class Net::HTTP::Trace < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'TRACE'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method PATCH}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PATCH_method]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# uri.path = '/posts'
+# req = Net::HTTP::Patch.new(uri) # => #<Net::HTTP::Patch PATCH>
+# req.body = '{"title": "foo","body": "bar","userId": 1}'
+# req.content_type = 'application/json'
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: yes.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/HTTP#Safe_method]: no.
+# - {Idempotent}[https://en.wikipedia.org/wiki/HTTP#Idempotent_method]: no.
+# - {Cacheable}[https://en.wikipedia.org/wiki/HTTP#Cacheable_method]: no.
+#
+# Related:
+#
+# - Net::HTTP#patch: sends +PATCH+ request, returns response object.
+#
+class Net::HTTP::Patch < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'PATCH'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+#
+# WebDAV methods --- RFC2518
+#
+
+# \Class for representing
+# {WebDAV method PROPFIND}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Net::HTTP::Propfind.new(uri) # => #<Net::HTTP::Propfind PROPFIND>
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Net::HTTP#propfind: sends +PROPFIND+ request, returns response object.
+#
+class Net::HTTP::Propfind < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'PROPFIND'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method PROPPATCH}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Net::HTTP::Proppatch.new(uri) # => #<Net::HTTP::Proppatch PROPPATCH>
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Net::HTTP#proppatch: sends +PROPPATCH+ request, returns response object.
+#
+class Net::HTTP::Proppatch < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'PROPPATCH'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method MKCOL}[http://www.webdav.org/specs/rfc4918.html#METHOD_MKCOL]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Net::HTTP::Mkcol.new(uri) # => #<Net::HTTP::Mkcol MKCOL>
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Net::HTTP#mkcol: sends +MKCOL+ request, returns response object.
+#
+class Net::HTTP::Mkcol < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'MKCOL'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method COPY}[http://www.webdav.org/specs/rfc4918.html#METHOD_COPY]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Net::HTTP::Copy.new(uri) # => #<Net::HTTP::Copy COPY>
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Net::HTTP#copy: sends +COPY+ request, returns response object.
+#
+class Net::HTTP::Copy < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'COPY'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method MOVE}[http://www.webdav.org/specs/rfc4918.html#METHOD_MOVE]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Net::HTTP::Move.new(uri) # => #<Net::HTTP::Move MOVE>
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Net::HTTP#move: sends +MOVE+ request, returns response object.
+#
+class Net::HTTP::Move < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'MOVE'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method LOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_LOCK]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Net::HTTP::Lock.new(uri) # => #<Net::HTTP::Lock LOCK>
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Net::HTTP#lock: sends +LOCK+ request, returns response object.
+#
+class Net::HTTP::Lock < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'LOCK'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method UNLOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_UNLOCK]:
+#
+# require 'net/http'
+# uri = URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Net::HTTP::Unlock.new(uri) # => #<Net::HTTP::Unlock UNLOCK>
+# res = Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Net::HTTP#unlock: sends +UNLOCK+ request, returns response object.
+#
+class Net::HTTP::Unlock < Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'UNLOCK'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
diff --git a/lib/net/http/response.rb b/lib/net/http/response.rb
new file mode 100644
index 0000000000..8804a99c9e
--- /dev/null
+++ b/lib/net/http/response.rb
@@ -0,0 +1,739 @@
+# frozen_string_literal: true
+
+# This class is the base class for \Net::HTTP response classes.
+#
+# == About the Examples
+#
+# :include: doc/net-http/examples.rdoc
+#
+# == Returned Responses
+#
+# \Method Net::HTTP.get_response returns
+# an instance of one of the subclasses of \Net::HTTPResponse:
+#
+# Net::HTTP.get_response(uri)
+# # => #<Net::HTTPOK 200 OK readbody=true>
+# Net::HTTP.get_response(hostname, '/nosuch')
+# # => #<Net::HTTPNotFound 404 Not Found readbody=true>
+#
+# As does method Net::HTTP#request:
+#
+# req = Net::HTTP::Get.new(uri)
+# Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end # => #<Net::HTTPOK 200 OK readbody=true>
+#
+# \Class \Net::HTTPResponse includes module Net::HTTPHeader,
+# which provides access to response header values via (among others):
+#
+# - \Hash-like method <tt>[]</tt>.
+# - Specific reader methods, such as +content_type+.
+#
+# Examples:
+#
+# res = Net::HTTP.get_response(uri) # => #<Net::HTTPOK 200 OK readbody=true>
+# res['Content-Type'] # => "text/html; charset=UTF-8"
+# res.content_type # => "text/html"
+#
+# == Response Subclasses
+#
+# \Class \Net::HTTPResponse has a subclass for each
+# {HTTP status code}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes].
+# You can look up the response class for a given code:
+#
+# Net::HTTPResponse::CODE_TO_OBJ['200'] # => Net::HTTPOK
+# Net::HTTPResponse::CODE_TO_OBJ['400'] # => Net::HTTPBadRequest
+# Net::HTTPResponse::CODE_TO_OBJ['404'] # => Net::HTTPNotFound
+#
+# And you can retrieve the status code for a response object:
+#
+# Net::HTTP.get_response(uri).code # => "200"
+# Net::HTTP.get_response(hostname, '/nosuch').code # => "404"
+#
+# The response subclasses (indentation shows class hierarchy):
+#
+# - Net::HTTPUnknownResponse (for unhandled \HTTP extensions).
+#
+# - Net::HTTPInformation:
+#
+# - Net::HTTPContinue (100)
+# - Net::HTTPSwitchProtocol (101)
+# - Net::HTTPProcessing (102)
+# - Net::HTTPEarlyHints (103)
+#
+# - Net::HTTPSuccess:
+#
+# - Net::HTTPOK (200)
+# - Net::HTTPCreated (201)
+# - Net::HTTPAccepted (202)
+# - Net::HTTPNonAuthoritativeInformation (203)
+# - Net::HTTPNoContent (204)
+# - Net::HTTPResetContent (205)
+# - Net::HTTPPartialContent (206)
+# - Net::HTTPMultiStatus (207)
+# - Net::HTTPAlreadyReported (208)
+# - Net::HTTPIMUsed (226)
+#
+# - Net::HTTPRedirection:
+#
+# - Net::HTTPMultipleChoices (300)
+# - Net::HTTPMovedPermanently (301)
+# - Net::HTTPFound (302)
+# - Net::HTTPSeeOther (303)
+# - Net::HTTPNotModified (304)
+# - Net::HTTPUseProxy (305)
+# - Net::HTTPTemporaryRedirect (307)
+# - Net::HTTPPermanentRedirect (308)
+#
+# - Net::HTTPClientError:
+#
+# - Net::HTTPBadRequest (400)
+# - Net::HTTPUnauthorized (401)
+# - Net::HTTPPaymentRequired (402)
+# - Net::HTTPForbidden (403)
+# - Net::HTTPNotFound (404)
+# - Net::HTTPMethodNotAllowed (405)
+# - Net::HTTPNotAcceptable (406)
+# - Net::HTTPProxyAuthenticationRequired (407)
+# - Net::HTTPRequestTimeOut (408)
+# - Net::HTTPConflict (409)
+# - Net::HTTPGone (410)
+# - Net::HTTPLengthRequired (411)
+# - Net::HTTPPreconditionFailed (412)
+# - Net::HTTPRequestEntityTooLarge (413)
+# - Net::HTTPRequestURITooLong (414)
+# - Net::HTTPUnsupportedMediaType (415)
+# - Net::HTTPRequestedRangeNotSatisfiable (416)
+# - Net::HTTPExpectationFailed (417)
+# - Net::HTTPMisdirectedRequest (421)
+# - Net::HTTPUnprocessableEntity (422)
+# - Net::HTTPLocked (423)
+# - Net::HTTPFailedDependency (424)
+# - Net::HTTPUpgradeRequired (426)
+# - Net::HTTPPreconditionRequired (428)
+# - Net::HTTPTooManyRequests (429)
+# - Net::HTTPRequestHeaderFieldsTooLarge (431)
+# - Net::HTTPUnavailableForLegalReasons (451)
+#
+# - Net::HTTPServerError:
+#
+# - Net::HTTPInternalServerError (500)
+# - Net::HTTPNotImplemented (501)
+# - Net::HTTPBadGateway (502)
+# - Net::HTTPServiceUnavailable (503)
+# - Net::HTTPGatewayTimeOut (504)
+# - Net::HTTPVersionNotSupported (505)
+# - Net::HTTPVariantAlsoNegotiates (506)
+# - Net::HTTPInsufficientStorage (507)
+# - Net::HTTPLoopDetected (508)
+# - Net::HTTPNotExtended (510)
+# - Net::HTTPNetworkAuthenticationRequired (511)
+#
+# There is also the Net::HTTPBadResponse exception which is raised when
+# there is a protocol error.
+#
+class Net::HTTPResponse
+ class << self
+ # true if the response has a body.
+ def body_permitted?
+ self::HAS_BODY
+ end
+
+ def exception_type # :nodoc: internal use only
+ self::EXCEPTION_TYPE
+ end
+
+ def read_new(sock) #:nodoc: internal use only
+ httpv, code, msg = read_status_line(sock)
+ res = response_class(code).new(httpv, code, msg)
+ each_response_header(sock) do |k,v|
+ res.add_field k, v
+ end
+ res
+ end
+
+ private
+ # :stopdoc:
+
+ def read_status_line(sock)
+ str = sock.readline
+ m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or
+ raise Net::HTTPBadResponse, "wrong status line: #{str.dump}"
+ m.captures
+ end
+
+ def response_class(code)
+ CODE_TO_OBJ[code] or
+ CODE_CLASS_TO_OBJ[code[0,1]] or
+ Net::HTTPUnknownResponse
+ end
+
+ def each_response_header(sock)
+ key = value = nil
+ while true
+ line = sock.readuntil("\n", true).sub(/\s+\z/, '')
+ break if line.empty?
+ if line[0] == ?\s or line[0] == ?\t and value
+ value << ' ' unless value.empty?
+ value << line.strip
+ else
+ yield key, value if key
+ key, value = line.strip.split(/\s*:\s*/, 2)
+ raise Net::HTTPBadResponse, 'wrong header line format' if value.nil?
+ end
+ end
+ yield key, value if key
+ end
+ end
+
+ # next is to fix bug in RDoc, where the private inside class << self
+ # spills out.
+ public
+
+ include Net::HTTPHeader
+
+ def initialize(httpv, code, msg) #:nodoc: internal use only
+ @http_version = httpv
+ @code = code
+ @message = msg
+ initialize_http_header nil
+ @body = nil
+ @read = false
+ @uri = nil
+ @decode_content = false
+ @body_encoding = false
+ @ignore_eof = true
+ end
+
+ # The HTTP version supported by the server.
+ attr_reader :http_version
+
+ # The HTTP result code string. For example, '302'. You can also
+ # determine the response type by examining which response subclass
+ # the response object is an instance of.
+ attr_reader :code
+
+ # The HTTP result message sent by the server. For example, 'Not Found'.
+ attr_reader :message
+ alias msg message # :nodoc: obsolete
+
+ # The URI used to fetch this response. The response URI is only available
+ # if a URI was used to create the request.
+ attr_reader :uri
+
+ # Set to true automatically when the request did not contain an
+ # Accept-Encoding header from the user.
+ attr_accessor :decode_content
+
+ # Returns the value set by body_encoding=, or +false+ if none;
+ # see #body_encoding=.
+ attr_reader :body_encoding
+
+ # Sets the encoding that should be used when reading the body:
+ #
+ # - If the given value is an Encoding object, that encoding will be used.
+ # - Otherwise if the value is a string, the value of
+ # {Encoding#find(value)}[rdoc-ref:Encoding.find]
+ # will be used.
+ # - Otherwise an encoding will be deduced from the body itself.
+ #
+ # Examples:
+ #
+ # http = Net::HTTP.new(hostname)
+ # req = Net::HTTP::Get.new('/')
+ #
+ # http.request(req) do |res|
+ # p res.body.encoding # => #<Encoding:ASCII-8BIT>
+ # end
+ #
+ # http.request(req) do |res|
+ # res.body_encoding = "UTF-8"
+ # p res.body.encoding # => #<Encoding:UTF-8>
+ # end
+ #
+ def body_encoding=(value)
+ value = Encoding.find(value) if value.is_a?(String)
+ @body_encoding = value
+ end
+
+ # Whether to ignore EOF when reading bodies with a specified Content-Length
+ # header.
+ attr_accessor :ignore_eof
+
+ def inspect # :nodoc:
+ "#<#{self.class} #{@code} #{@message} readbody=#{@read}>"
+ end
+
+ #
+ # response <-> exception relationship
+ #
+
+ def code_type #:nodoc:
+ self.class
+ end
+
+ def error! #:nodoc:
+ message = @code
+ message = "#{message} #{@message.dump}" if @message
+ raise error_type().new(message, self)
+ end
+
+ def error_type #:nodoc:
+ self.class::EXCEPTION_TYPE
+ end
+
+ # Raises an HTTP error if the response is not 2xx (success).
+ def value
+ error! unless self.kind_of?(Net::HTTPSuccess)
+ end
+
+ def uri= uri # :nodoc:
+ @uri = uri.dup if uri
+ end
+
+ #
+ # header (for backward compatibility only; DO NOT USE)
+ #
+
+ def response #:nodoc:
+ warn "Net::HTTPResponse#response is obsolete", uplevel: 1 if $VERBOSE
+ self
+ end
+
+ def header #:nodoc:
+ warn "Net::HTTPResponse#header is obsolete", uplevel: 1 if $VERBOSE
+ self
+ end
+
+ def read_header #:nodoc:
+ warn "Net::HTTPResponse#read_header is obsolete", uplevel: 1 if $VERBOSE
+ self
+ end
+
+ #
+ # body
+ #
+
+ def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only
+ @socket = sock
+ @body_exist = reqmethodallowbody && self.class.body_permitted?
+ begin
+ yield
+ self.body # ensure to read body
+ ensure
+ @socket = nil
+ end
+ end
+
+ # Gets the entity body returned by the remote HTTP server.
+ #
+ # If a block is given, the body is passed to the block, and
+ # the body is provided in fragments, as it is read in from the socket.
+ #
+ # If +dest+ argument is given, response is read into that variable,
+ # with <code>dest#<<</code> method (it could be String or IO, or any
+ # other object responding to <code><<</code>).
+ #
+ # Calling this method a second or subsequent time for the same
+ # HTTPResponse object will return the value already read.
+ #
+ # http.request_get('/index.html') {|res|
+ # puts res.read_body
+ # }
+ #
+ # http.request_get('/index.html') {|res|
+ # p res.read_body.object_id # 538149362
+ # p res.read_body.object_id # 538149362
+ # }
+ #
+ # # using iterator
+ # http.request_get('/index.html') {|res|
+ # res.read_body do |segment|
+ # print segment
+ # end
+ # }
+ #
+ def read_body(dest = nil, &block)
+ if @read
+ raise IOError, "#{self.class}\#read_body called twice" if dest or block
+ return @body
+ end
+ to = procdest(dest, block)
+ stream_check
+ if @body_exist
+ read_body_0 to
+ @body = to
+ else
+ @body = nil
+ end
+ @read = true
+ return if @body.nil?
+
+ case enc = @body_encoding
+ when Encoding, false, nil
+ # Encoding: force given encoding
+ # false/nil: do not force encoding
+ else
+ # other value: detect encoding from body
+ enc = detect_encoding(@body)
+ end
+
+ @body.force_encoding(enc) if enc
+
+ @body
+ end
+
+ # Returns the string response body;
+ # note that repeated calls for the unmodified body return a cached string:
+ #
+ # path = '/todos/1'
+ # Net::HTTP.start(hostname) do |http|
+ # res = http.get(path)
+ # p res.body
+ # p http.head(path).body # No body.
+ # end
+ #
+ # Output:
+ #
+ # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}"
+ # nil
+ #
+ def body
+ read_body()
+ end
+
+ # Sets the body of the response to the given value.
+ def body=(value)
+ @body = value
+ end
+
+ alias entity body #:nodoc: obsolete
+
+ private
+
+ # :nodoc:
+ def detect_encoding(str, encoding=nil)
+ if encoding
+ elsif encoding = type_params['charset']
+ elsif encoding = check_bom(str)
+ else
+ encoding = case content_type&.downcase
+ when %r{text/x(?:ht)?ml|application/(?:[^+]+\+)?xml}
+ /\A<xml[ \t\r\n]+
+ version[ \t\r\n]*=[ \t\r\n]*(?:"[0-9.]+"|'[0-9.]*')[ \t\r\n]+
+ encoding[ \t\r\n]*=[ \t\r\n]*
+ (?:"([A-Za-z][\-A-Za-z0-9._]*)"|'([A-Za-z][\-A-Za-z0-9._]*)')/x =~ str
+ encoding = $1 || $2 || Encoding::UTF_8
+ when %r{text/html.*}
+ sniff_encoding(str)
+ end
+ end
+ return encoding
+ end
+
+ # :nodoc:
+ def sniff_encoding(str, encoding=nil)
+ # the encoding sniffing algorithm
+ # http://www.w3.org/TR/html5/parsing.html#determining-the-character-encoding
+ if enc = scanning_meta(str)
+ enc
+ # 6. last visited page or something
+ # 7. frequency
+ elsif str.ascii_only?
+ Encoding::US_ASCII
+ elsif str.dup.force_encoding(Encoding::UTF_8).valid_encoding?
+ Encoding::UTF_8
+ end
+ # 8. implementation-defined or user-specified
+ end
+
+ # :nodoc:
+ def check_bom(str)
+ case str.byteslice(0, 2)
+ when "\xFE\xFF"
+ return Encoding::UTF_16BE
+ when "\xFF\xFE"
+ return Encoding::UTF_16LE
+ end
+ if "\xEF\xBB\xBF" == str.byteslice(0, 3)
+ return Encoding::UTF_8
+ end
+ nil
+ end
+
+ # :nodoc:
+ def scanning_meta(str)
+ require 'strscan'
+ ss = StringScanner.new(str)
+ if ss.scan_until(/<meta[\t\n\f\r ]*/)
+ attrs = {} # attribute_list
+ got_pragma = false
+ need_pragma = nil
+ charset = nil
+
+ # step: Attributes
+ while attr = get_attribute(ss)
+ name, value = *attr
+ next if attrs[name]
+ attrs[name] = true
+ case name
+ when 'http-equiv'
+ got_pragma = true if value == 'content-type'
+ when 'content'
+ encoding = extracting_encodings_from_meta_elements(value)
+ unless charset
+ charset = encoding
+ end
+ need_pragma = true
+ when 'charset'
+ need_pragma = false
+ charset = value
+ end
+ end
+
+ # step: Processing
+ return if need_pragma.nil?
+ return if need_pragma && !got_pragma
+
+ charset = Encoding.find(charset) rescue nil
+ return unless charset
+ charset = Encoding::UTF_8 if charset == Encoding::UTF_16
+ return charset # tentative
+ end
+ nil
+ end
+
+ def get_attribute(ss)
+ ss.scan(/[\t\n\f\r \/]*/)
+ if ss.peek(1) == '>'
+ ss.getch
+ return nil
+ end
+ name = ss.scan(/[^=\t\n\f\r \/>]*/)
+ name.downcase!
+ raise if name.empty?
+ ss.skip(/[\t\n\f\r ]*/)
+ if ss.getch != '='
+ value = ''
+ return [name, value]
+ end
+ ss.skip(/[\t\n\f\r ]*/)
+ case ss.peek(1)
+ when '"'
+ ss.getch
+ value = ss.scan(/[^"]+/)
+ value.downcase!
+ ss.getch
+ when "'"
+ ss.getch
+ value = ss.scan(/[^']+/)
+ value.downcase!
+ ss.getch
+ when '>'
+ value = ''
+ else
+ value = ss.scan(/[^\t\n\f\r >]+/)
+ value.downcase!
+ end
+ [name, value]
+ end
+
+ def extracting_encodings_from_meta_elements(value)
+ # http://dev.w3.org/html5/spec/fetching-resources.html#algorithm-for-extracting-an-encoding-from-a-meta-element
+ if /charset[\t\n\f\r ]*=(?:"([^"]*)"|'([^']*)'|["']|\z|([^\t\n\f\r ;]+))/i =~ value
+ return $1 || $2 || $3
+ end
+ return nil
+ end
+
+ ##
+ # Checks for a supported Content-Encoding header and yields an Inflate
+ # wrapper for this response's socket when zlib is present. If the
+ # Content-Encoding is not supported or zlib is missing, the plain socket is
+ # yielded.
+ #
+ # If a Content-Range header is present, a plain socket is yielded as the
+ # bytes in the range may not be a complete deflate block.
+
+ def inflater # :nodoc:
+ return yield @socket unless Net::HTTP::HAVE_ZLIB
+ return yield @socket unless @decode_content
+ return yield @socket if self['content-range']
+
+ v = self['content-encoding']
+ case v&.downcase
+ when 'deflate', 'gzip', 'x-gzip' then
+ self.delete 'content-encoding'
+
+ inflate_body_io = Inflater.new(@socket)
+
+ begin
+ yield inflate_body_io
+ success = true
+ ensure
+ begin
+ inflate_body_io.finish
+ if self['content-length']
+ self['content-length'] = inflate_body_io.bytes_inflated.to_s
+ end
+ rescue => err
+ # Ignore #finish's error if there is an exception from yield
+ raise err if success
+ end
+ end
+ when 'none', 'identity' then
+ self.delete 'content-encoding'
+
+ yield @socket
+ else
+ yield @socket
+ end
+ end
+
+ def read_body_0(dest)
+ inflater do |inflate_body_io|
+ if chunked?
+ read_chunked dest, inflate_body_io
+ return
+ end
+
+ @socket = inflate_body_io
+
+ clen = content_length()
+ if clen
+ @socket.read clen, dest, @ignore_eof
+ return
+ end
+ clen = range_length()
+ if clen
+ @socket.read clen, dest
+ return
+ end
+ @socket.read_all dest
+ end
+ end
+
+ ##
+ # read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF,
+ # etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip
+ # encoded.
+ #
+ # See RFC 2616 section 3.6.1 for definitions
+
+ def read_chunked(dest, chunk_data_io) # :nodoc:
+ total = 0
+ while true
+ line = @socket.readline
+ hexlen = line.slice(/[0-9a-fA-F]+/) or
+ raise Net::HTTPBadResponse, "wrong chunk size line: #{line}"
+ len = hexlen.hex
+ break if len == 0
+ begin
+ chunk_data_io.read len, dest
+ ensure
+ total += len
+ @socket.read 2 # \r\n
+ end
+ end
+ until @socket.readline.empty?
+ # none
+ end
+ end
+
+ def stream_check
+ raise IOError, 'attempt to read body out of block' if @socket.nil? || @socket.closed?
+ end
+
+ def procdest(dest, block)
+ raise ArgumentError, 'both arg and block given for HTTP method' if
+ dest and block
+ if block
+ Net::ReadAdapter.new(block)
+ else
+ dest || +''
+ end
+ end
+
+ ##
+ # Inflater is a wrapper around Net::BufferedIO that transparently inflates
+ # zlib and gzip streams.
+
+ class Inflater # :nodoc:
+
+ ##
+ # Creates a new Inflater wrapping +socket+
+
+ def initialize socket
+ @socket = socket
+ # zlib with automatic gzip detection
+ @inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
+ end
+
+ ##
+ # Finishes the inflate stream.
+
+ def finish
+ return if @inflate.total_in == 0
+ @inflate.finish
+ end
+
+ ##
+ # The number of bytes inflated, used to update the Content-Length of
+ # the response.
+
+ def bytes_inflated
+ @inflate.total_out
+ end
+
+ ##
+ # Returns a Net::ReadAdapter that inflates each read chunk into +dest+.
+ #
+ # This allows a large response body to be inflated without storing the
+ # entire body in memory.
+
+ def inflate_adapter(dest)
+ if dest.respond_to?(:set_encoding)
+ dest.set_encoding(Encoding::ASCII_8BIT)
+ elsif dest.respond_to?(:force_encoding)
+ dest.force_encoding(Encoding::ASCII_8BIT)
+ end
+ block = proc do |compressed_chunk|
+ @inflate.inflate(compressed_chunk) do |chunk|
+ compressed_chunk.clear
+ dest << chunk
+ end
+ end
+
+ Net::ReadAdapter.new(block)
+ end
+
+ ##
+ # Reads +clen+ bytes from the socket, inflates them, then writes them to
+ # +dest+. +ignore_eof+ is passed down to Net::BufferedIO#read
+ #
+ # Unlike Net::BufferedIO#read, this method returns more than +clen+ bytes.
+ # At this time there is no way for a user of Net::HTTPResponse to read a
+ # specific number of bytes from the HTTP response body, so this internal
+ # API does not return the same number of bytes as were requested.
+ #
+ # See https://bugs.ruby-lang.org/issues/6492 for further discussion.
+
+ def read clen, dest, ignore_eof = false
+ temp_dest = inflate_adapter(dest)
+
+ @socket.read clen, temp_dest, ignore_eof
+ end
+
+ ##
+ # Reads the rest of the socket, inflates it, then writes it to +dest+.
+
+ def read_all dest
+ temp_dest = inflate_adapter(dest)
+
+ @socket.read_all temp_dest
+ end
+
+ end
+
+end
+
diff --git a/lib/net/http/responses.rb b/lib/net/http/responses.rb
new file mode 100644
index 0000000000..941a6fed80
--- /dev/null
+++ b/lib/net/http/responses.rb
@@ -0,0 +1,1242 @@
+# frozen_string_literal: true
+#--
+# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+
+module Net
+
+ # Unknown HTTP response
+ class HTTPUnknownResponse < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPError #
+ end
+
+ # Parent class for informational (1xx) HTTP response classes.
+ #
+ # An informational response indicates that the request was received and understood.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.1xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#1xx_informational_response].
+ #
+ class HTTPInformation < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = false
+ EXCEPTION_TYPE = HTTPError #
+ end
+
+ # Parent class for success (2xx) HTTP response classes.
+ #
+ # A success response indicates the action requested by the client
+ # was received, understood, and accepted.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.2xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_success].
+ #
+ class HTTPSuccess < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPError #
+ end
+
+ # Parent class for redirection (3xx) HTTP response classes.
+ #
+ # A redirection response indicates the client must take additional action
+ # to complete the request.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.3xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_redirection].
+ #
+ class HTTPRedirection < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPRetriableError #
+ end
+
+ # Parent class for client error (4xx) HTTP response classes.
+ #
+ # A client error response indicates that the client may have caused an error.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.4xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors].
+ #
+ class HTTPClientError < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPClientException #
+ end
+
+ # Parent class for server error (5xx) HTTP response classes.
+ #
+ # A server error response indicates that the server failed to fulfill a request.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.5xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#5xx_server_errors].
+ #
+ class HTTPServerError < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPFatalError #
+ end
+
+ # Response class for +Continue+ responses (status code 100).
+ #
+ # A +Continue+ response indicates that the server has received the request headers.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/100].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-100-continue].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#100].
+ #
+ class HTTPContinue < HTTPInformation
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Switching Protocol</tt> responses (status code 101).
+ #
+ # The <tt>Switching Protocol<tt> response indicates that the server has received
+ # a request to switch protocols, and has agreed to do so.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#101].
+ #
+ class HTTPSwitchProtocol < HTTPInformation
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for +Processing+ responses (status code 102).
+ #
+ # The +Processing+ response indicates that the server has received
+ # and is processing the request, but no response is available yet.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 2518}[https://www.rfc-editor.org/rfc/rfc2518#section-10.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#102].
+ #
+ class HTTPProcessing < HTTPInformation
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Early Hints</tt> responses (status code 103).
+ #
+ # The <tt>Early Hints</tt> indicates that the server has received
+ # and is processing the request, and contains certain headers;
+ # the final response is not available yet.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103].
+ # - {RFC 8297}[https://www.rfc-editor.org/rfc/rfc8297.html#section-2].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#103].
+ #
+ class HTTPEarlyHints < HTTPInformation
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for +OK+ responses (status code 200).
+ #
+ # The +OK+ response indicates that the server has received
+ # a request and has responded successfully.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-200-ok].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#200].
+ #
+ class HTTPOK < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for +Created+ responses (status code 201).
+ #
+ # The +Created+ response indicates that the server has received
+ # and has fulfilled a request to create a new resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-201-created].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#201].
+ #
+ class HTTPCreated < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for +Accepted+ responses (status code 202).
+ #
+ # The +Accepted+ response indicates that the server has received
+ # and is processing a request, but the processing has not yet been completed.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-202-accepted].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#202].
+ #
+ class HTTPAccepted < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Non-Authoritative Information</tt> responses (status code 203).
+ #
+ # The <tt>Non-Authoritative Information</tt> response indicates that the server
+ # is a transforming proxy (such as a Web accelerator)
+ # that received a 200 OK response from its origin,
+ # and is returning a modified version of the origin's response.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/203].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-203-non-authoritative-infor].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#203].
+ #
+ class HTTPNonAuthoritativeInformation < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>No Content</tt> responses (status code 204).
+ #
+ # The <tt>No Content</tt> response indicates that the server
+ # successfully processed the request, and is not returning any content.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-204-no-content].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#204].
+ #
+ class HTTPNoContent < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Reset Content</tt> responses (status code 205).
+ #
+ # The <tt>Reset Content</tt> response indicates that the server
+ # successfully processed the request,
+ # asks that the client reset its document view, and is not returning any content.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/205].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-205-reset-content].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#205].
+ #
+ class HTTPResetContent < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Partial Content</tt> responses (status code 206).
+ #
+ # The <tt>Partial Content</tt> response indicates that the server is delivering
+ # only part of the resource (byte serving)
+ # due to a Range header in the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-206-partial-content].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#206].
+ #
+ class HTTPPartialContent < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Multi-Status (WebDAV)</tt> responses (status code 207).
+ #
+ # The <tt>Multi-Status (WebDAV)</tt> response indicates that the server
+ # has received the request,
+ # and that the message body can contain a number of separate response codes.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 4818}[https://www.rfc-editor.org/rfc/rfc4918#section-11.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#207].
+ #
+ class HTTPMultiStatus < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Already Reported (WebDAV)</tt> responses (status code 208).
+ #
+ # The <tt>Already Reported (WebDAV)</tt> response indicates that the server
+ # has received the request,
+ # and that the members of a DAV binding have already been enumerated
+ # in a preceding part of the (multi-status) response,
+ # and are not being included again.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 5842}[https://www.rfc-editor.org/rfc/rfc5842.html#section-7.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#208].
+ #
+ class HTTPAlreadyReported < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>IM Used</tt> responses (status code 226).
+ #
+ # The <tt>IM Used</tt> response indicates that the server has fulfilled a request
+ # for the resource, and the response is a representation of the result
+ # of one or more instance-manipulations applied to the current instance.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 3229}[https://www.rfc-editor.org/rfc/rfc3229.html#section-10.4.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#226].
+ #
+ class HTTPIMUsed < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Multiple Choices</tt> responses (status code 300).
+ #
+ # The <tt>Multiple Choices</tt> response indicates that the server
+ # offers multiple options for the resource from which the client may choose.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/300].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-300-multiple-choices].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#300].
+ #
+ class HTTPMultipleChoices < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPMultipleChoice = HTTPMultipleChoices
+
+ # Response class for <tt>Moved Permanently</tt> responses (status code 301).
+ #
+ # The <tt>Moved Permanently</tt> response indicates that links or records
+ # returning this response should be updated to use the given URL.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-301-moved-permanently].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#301].
+ #
+ class HTTPMovedPermanently < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Found</tt> responses (status code 302).
+ #
+ # The <tt>Found</tt> response indicates that the client
+ # should look at (browse to) another URL.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-302-found].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#302].
+ #
+ class HTTPFound < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPMovedTemporarily = HTTPFound
+
+ # Response class for <tt>See Other</tt> responses (status code 303).
+ #
+ # The response to the request can be found under another URI using the GET method.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-303-see-other].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#303].
+ #
+ class HTTPSeeOther < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Not Modified</tt> responses (status code 304).
+ #
+ # Indicates that the resource has not been modified since the version
+ # specified by the request headers.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-304-not-modified].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#304].
+ #
+ class HTTPNotModified < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Use Proxy</tt> responses (status code 305).
+ #
+ # The requested resource is available only through a proxy,
+ # whose address is provided in the response.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-305-use-proxy].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#305].
+ #
+ class HTTPUseProxy < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Temporary Redirect</tt> responses (status code 307).
+ #
+ # The request should be repeated with another URI;
+ # however, future requests should still use the original URI.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-307-temporary-redirect].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#307].
+ #
+ class HTTPTemporaryRedirect < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Permanent Redirect</tt> responses (status code 308).
+ #
+ # This and all future requests should be directed to the given URI.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-308-permanent-redirect].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#308].
+ #
+ class HTTPPermanentRedirect < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Bad Request</tt> responses (status code 400).
+ #
+ # The server cannot or will not process the request due to an apparent client error.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-400-bad-request].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#400].
+ #
+ class HTTPBadRequest < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Unauthorized</tt> responses (status code 401).
+ #
+ # Authentication is required, but either was not provided or failed.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-401-unauthorized].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#401].
+ #
+ class HTTPUnauthorized < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Payment Required</tt> responses (status code 402).
+ #
+ # Reserved for future use.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-402-payment-required].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#402].
+ #
+ class HTTPPaymentRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Forbidden</tt> responses (status code 403).
+ #
+ # The request contained valid data and was understood by the server,
+ # but the server is refusing action.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-403-forbidden].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#403].
+ #
+ class HTTPForbidden < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Not Found</tt> responses (status code 404).
+ #
+ # The requested resource could not be found but may be available in the future.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-404-not-found].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#404].
+ #
+ class HTTPNotFound < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Method Not Allowed</tt> responses (status code 405).
+ #
+ # The request method is not supported for the requested resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-405-method-not-allowed].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#405].
+ #
+ class HTTPMethodNotAllowed < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Not Acceptable</tt> responses (status code 406).
+ #
+ # The requested resource is capable of generating only content
+ # that not acceptable according to the Accept headers sent in the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-406-not-acceptable].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#406].
+ #
+ class HTTPNotAcceptable < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Proxy Authentication Required</tt> responses (status code 407).
+ #
+ # The client must first authenticate itself with the proxy.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-407-proxy-authentication-re].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#407].
+ #
+ class HTTPProxyAuthenticationRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Request Timeout</tt> responses (status code 408).
+ #
+ # The server timed out waiting for the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-408-request-timeout].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#408].
+ #
+ class HTTPRequestTimeout < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPRequestTimeOut = HTTPRequestTimeout
+
+ # Response class for <tt>Conflict</tt> responses (status code 409).
+ #
+ # The request could not be processed because of conflict in the current state of the resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-409-conflict].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#409].
+ #
+ class HTTPConflict < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Gone</tt> responses (status code 410).
+ #
+ # The resource requested was previously in use but is no longer available
+ # and will not be available again.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-410-gone].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#410].
+ #
+ class HTTPGone < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Length Required</tt> responses (status code 411).
+ #
+ # The request did not specify the length of its content,
+ # which is required by the requested resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-411-length-required].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#411].
+ #
+ class HTTPLengthRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Precondition Failed</tt> responses (status code 412).
+ #
+ # The server does not meet one of the preconditions
+ # specified in the request headers.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-412-precondition-failed].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#412].
+ #
+ class HTTPPreconditionFailed < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Payload Too Large</tt> responses (status code 413).
+ #
+ # The request is larger than the server is willing or able to process.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-413-content-too-large].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#413].
+ #
+ class HTTPPayloadTooLarge < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPRequestEntityTooLarge = HTTPPayloadTooLarge
+
+ # Response class for <tt>URI Too Long</tt> responses (status code 414).
+ #
+ # The URI provided was too long for the server to process.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-414-uri-too-long].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#414].
+ #
+ class HTTPURITooLong < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPRequestURITooLong = HTTPURITooLong
+ HTTPRequestURITooLarge = HTTPRequestURITooLong
+
+ # Response class for <tt>Unsupported Media Type</tt> responses (status code 415).
+ #
+ # The request entity has a media type which the server or resource does not support.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-415-unsupported-media-type].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#415].
+ #
+ class HTTPUnsupportedMediaType < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Range Not Satisfiable</tt> responses (status code 416).
+ #
+ # The request entity has a media type which the server or resource does not support.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-416-range-not-satisfiable].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#416].
+ #
+ class HTTPRangeNotSatisfiable < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPRequestedRangeNotSatisfiable = HTTPRangeNotSatisfiable
+
+ # Response class for <tt>Expectation Failed</tt> responses (status code 417).
+ #
+ # The server cannot meet the requirements of the Expect request-header field.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/417].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-417-expectation-failed].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#417].
+ #
+ class HTTPExpectationFailed < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # 418 I'm a teapot - RFC 2324; a joke RFC
+ # See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#418.
+
+ # 420 Enhance Your Calm - Twitter
+
+ # Response class for <tt>Misdirected Request</tt> responses (status code 421).
+ #
+ # The request was directed at a server that is not able to produce a response.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-421-misdirected-request].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#421].
+ #
+ class HTTPMisdirectedRequest < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Unprocessable Entity</tt> responses (status code 422).
+ #
+ # The request was well-formed but had semantic errors.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-422-unprocessable-content].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#422].
+ #
+ class HTTPUnprocessableEntity < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Locked (WebDAV)</tt> responses (status code 423).
+ #
+ # The requested resource is locked.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.3].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#423].
+ #
+ class HTTPLocked < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Failed Dependency (WebDAV)</tt> responses (status code 424).
+ #
+ # The request failed because it depended on another request and that request failed.
+ # See {424 Failed Dependency (WebDAV)}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#424].
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.4].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#424].
+ #
+ class HTTPFailedDependency < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # 425 Too Early
+ # https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#425.
+
+ # Response class for <tt>Upgrade Required</tt> responses (status code 426).
+ #
+ # The client should switch to the protocol given in the Upgrade header field.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/426].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-426-upgrade-required].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#426].
+ #
+ class HTTPUpgradeRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Precondition Required</tt> responses (status code 428).
+ #
+ # The origin server requires the request to be conditional.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/428].
+ # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-3].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#428].
+ #
+ class HTTPPreconditionRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Too Many Requests</tt> responses (status code 429).
+ #
+ # The user has sent too many requests in a given amount of time.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429].
+ # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-4].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#429].
+ #
+ class HTTPTooManyRequests < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Request Header Fields Too Large</tt> responses (status code 431).
+ #
+ # An individual header field is too large,
+ # or all the header fields collectively, are too large.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431].
+ # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-5].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#431].
+ #
+ class HTTPRequestHeaderFieldsTooLarge < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Unavailable For Legal Reasons</tt> responses (status code 451).
+ #
+ # A server operator has received a legal demand to deny access to a resource or to a set of resources
+ # that includes the requested resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/451].
+ # - {RFC 7725}[https://www.rfc-editor.org/rfc/rfc7725.html#section-3].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#451].
+ #
+ class HTTPUnavailableForLegalReasons < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ # 444 No Response - Nginx
+ # 449 Retry With - Microsoft
+ # 450 Blocked by Windows Parental Controls - Microsoft
+ # 499 Client Closed Request - Nginx
+
+ # Response class for <tt>Internal Server Error</tt> responses (status code 500).
+ #
+ # An unexpected condition was encountered and no more specific message is suitable.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-500-internal-server-error].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#500].
+ #
+ class HTTPInternalServerError < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Not Implemented</tt> responses (status code 501).
+ #
+ # The server either does not recognize the request method,
+ # or it lacks the ability to fulfil the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/501].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-501-not-implemented].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#501].
+ #
+ class HTTPNotImplemented < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Bad Gateway</tt> responses (status code 502).
+ #
+ # The server was acting as a gateway or proxy
+ # and received an invalid response from the upstream server.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-502-bad-gateway].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#502].
+ #
+ class HTTPBadGateway < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Service Unavailable</tt> responses (status code 503).
+ #
+ # The server cannot handle the request
+ # (because it is overloaded or down for maintenance).
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-503-service-unavailable].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#503].
+ #
+ class HTTPServiceUnavailable < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Gateway Timeout</tt> responses (status code 504).
+ #
+ # The server was acting as a gateway or proxy
+ # and did not receive a timely response from the upstream server.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-504-gateway-timeout].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#504].
+ #
+ class HTTPGatewayTimeout < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPGatewayTimeOut = HTTPGatewayTimeout
+
+ # Response class for <tt>HTTP Version Not Supported</tt> responses (status code 505).
+ #
+ # The server does not support the HTTP version used in the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/505].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-505-http-version-not-suppor].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#505].
+ #
+ class HTTPVersionNotSupported < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Variant Also Negotiates</tt> responses (status code 506).
+ #
+ # Transparent content negotiation for the request results in a circular reference.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/506].
+ # - {RFC 2295}[https://www.rfc-editor.org/rfc/rfc2295#section-8.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#506].
+ #
+ class HTTPVariantAlsoNegotiates < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Insufficient Storage (WebDAV)</tt> responses (status code 507).
+ #
+ # The server is unable to store the representation needed to complete the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/507].
+ # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.5].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#507].
+ #
+ class HTTPInsufficientStorage < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Loop Detected (WebDAV)</tt> responses (status code 508).
+ #
+ # The server detected an infinite loop while processing the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/508].
+ # - {RFC 5942}[https://www.rfc-editor.org/rfc/rfc5842.html#section-7.2].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#508].
+ #
+ class HTTPLoopDetected < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ # 509 Bandwidth Limit Exceeded - Apache bw/limited extension
+
+ # Response class for <tt>Not Extended</tt> responses (status code 510).
+ #
+ # Further extensions to the request are required for the server to fulfill it.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/510].
+ # - {RFC 2774}[https://www.rfc-editor.org/rfc/rfc2774.html#section-7].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#510].
+ #
+ class HTTPNotExtended < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Network Authentication Required</tt> responses (status code 511).
+ #
+ # The client needs to authenticate to gain network access.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/511].
+ # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-6].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#511].
+ #
+ class HTTPNetworkAuthenticationRequired < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+end
+
+class Net::HTTPResponse
+ # :stopdoc:
+ CODE_CLASS_TO_OBJ = {
+ '1' => Net::HTTPInformation,
+ '2' => Net::HTTPSuccess,
+ '3' => Net::HTTPRedirection,
+ '4' => Net::HTTPClientError,
+ '5' => Net::HTTPServerError
+ }.freeze
+ CODE_TO_OBJ = {
+ '100' => Net::HTTPContinue,
+ '101' => Net::HTTPSwitchProtocol,
+ '102' => Net::HTTPProcessing,
+ '103' => Net::HTTPEarlyHints,
+
+ '200' => Net::HTTPOK,
+ '201' => Net::HTTPCreated,
+ '202' => Net::HTTPAccepted,
+ '203' => Net::HTTPNonAuthoritativeInformation,
+ '204' => Net::HTTPNoContent,
+ '205' => Net::HTTPResetContent,
+ '206' => Net::HTTPPartialContent,
+ '207' => Net::HTTPMultiStatus,
+ '208' => Net::HTTPAlreadyReported,
+ '226' => Net::HTTPIMUsed,
+
+ '300' => Net::HTTPMultipleChoices,
+ '301' => Net::HTTPMovedPermanently,
+ '302' => Net::HTTPFound,
+ '303' => Net::HTTPSeeOther,
+ '304' => Net::HTTPNotModified,
+ '305' => Net::HTTPUseProxy,
+ '307' => Net::HTTPTemporaryRedirect,
+ '308' => Net::HTTPPermanentRedirect,
+
+ '400' => Net::HTTPBadRequest,
+ '401' => Net::HTTPUnauthorized,
+ '402' => Net::HTTPPaymentRequired,
+ '403' => Net::HTTPForbidden,
+ '404' => Net::HTTPNotFound,
+ '405' => Net::HTTPMethodNotAllowed,
+ '406' => Net::HTTPNotAcceptable,
+ '407' => Net::HTTPProxyAuthenticationRequired,
+ '408' => Net::HTTPRequestTimeout,
+ '409' => Net::HTTPConflict,
+ '410' => Net::HTTPGone,
+ '411' => Net::HTTPLengthRequired,
+ '412' => Net::HTTPPreconditionFailed,
+ '413' => Net::HTTPPayloadTooLarge,
+ '414' => Net::HTTPURITooLong,
+ '415' => Net::HTTPUnsupportedMediaType,
+ '416' => Net::HTTPRangeNotSatisfiable,
+ '417' => Net::HTTPExpectationFailed,
+ '421' => Net::HTTPMisdirectedRequest,
+ '422' => Net::HTTPUnprocessableEntity,
+ '423' => Net::HTTPLocked,
+ '424' => Net::HTTPFailedDependency,
+ '426' => Net::HTTPUpgradeRequired,
+ '428' => Net::HTTPPreconditionRequired,
+ '429' => Net::HTTPTooManyRequests,
+ '431' => Net::HTTPRequestHeaderFieldsTooLarge,
+ '451' => Net::HTTPUnavailableForLegalReasons,
+
+ '500' => Net::HTTPInternalServerError,
+ '501' => Net::HTTPNotImplemented,
+ '502' => Net::HTTPBadGateway,
+ '503' => Net::HTTPServiceUnavailable,
+ '504' => Net::HTTPGatewayTimeout,
+ '505' => Net::HTTPVersionNotSupported,
+ '506' => Net::HTTPVariantAlsoNegotiates,
+ '507' => Net::HTTPInsufficientStorage,
+ '508' => Net::HTTPLoopDetected,
+ '510' => Net::HTTPNotExtended,
+ '511' => Net::HTTPNetworkAuthenticationRequired,
+ }.freeze
+end
diff --git a/lib/net/http/status.rb b/lib/net/http/status.rb
new file mode 100644
index 0000000000..e70b47d9fb
--- /dev/null
+++ b/lib/net/http/status.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require_relative '../http'
+
+if $0 == __FILE__
+ require 'open-uri'
+ File.foreach(__FILE__) do |line|
+ puts line
+ break if line.start_with?('end')
+ end
+ puts
+ puts "Net::HTTP::STATUS_CODES = {"
+ url = "https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv"
+ URI(url).read.each_line do |line|
+ code, mes, = line.split(',')
+ next if ['(Unused)', 'Unassigned', 'Description'].include?(mes)
+ puts " #{code} => '#{mes}',"
+ end
+ puts "} # :nodoc:"
+end
+
+Net::HTTP::STATUS_CODES = {
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 102 => 'Processing',
+ 103 => 'Early Hints',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status',
+ 208 => 'Already Reported',
+ 226 => 'IM Used',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 307 => 'Temporary Redirect',
+ 308 => 'Permanent 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 => 'Content Too Large',
+ 414 => 'URI Too Long',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 421 => 'Misdirected Request',
+ 422 => 'Unprocessable Content',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 425 => 'Too Early',
+ 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',
+ 506 => 'Variant Also Negotiates',
+ 507 => 'Insufficient Storage',
+ 508 => 'Loop Detected',
+ 510 => 'Not Extended (OBSOLETED)',
+ 511 => 'Network Authentication Required',
+} # :nodoc:
diff --git a/lib/net/https.rb b/lib/net/https.rb
index 897bbb744b..0f23e1fb13 100644
--- a/lib/net/https.rb
+++ b/lib/net/https.rb
@@ -1,6 +1,12 @@
+# frozen_string_literal: true
=begin
-= $RCSfile$ -- SSL/TLS enhancement for Net::HTTP.
+= net/https -- SSL/TLS enhancement for Net::HTTP.
+
+ This file has been merged with net/http. There is no longer any need to
+ require 'net/https' to use HTTPS.
+
+ See Net::HTTP for details on how to make HTTPS connections.
== Info
'OpenSSL for Ruby 2' project
@@ -8,129 +14,10 @@
All rights reserved.
== Licence
- This program is licenced under the same licence as Ruby.
+ This program is licensed under the same licence as Ruby.
(See the file 'LICENCE'.)
-== Requirements
- This program requires Net 1.2.0 or higher version.
- You can get it from RAA or Ruby's CVS repository.
-
-== Version
- $Id$
-
- 2001-11-06: Contiributed to Ruby/OpenSSL project.
- 2004-03-06: Some code is merged in to net/http.
-
-== Example
-
-Here is a simple HTTP client:
-
- require 'net/http'
- require 'uri'
-
- uri = URI.parse(ARGV[0] || 'http://localhost/')
- http = Net::HTTP.new(uri.host, uri.port)
- http.start {
- http.request_get(uri.path) {|res|
- print res.body
- }
- }
-
-It can be replaced by the following code:
-
- require 'net/https'
- require 'uri'
-
- uri = URI.parse(ARGV[0] || 'https://localhost/')
- http = Net::HTTP.new(uri.host, uri.port)
- http.use_ssl = true if uri.scheme == "https" # enable SSL/TLS
- http.start {
- http.request_get(uri.path) {|res|
- print res.body
- }
- }
-
-== class Net::HTTP
-
-=== Instance Methods
-
-: use_ssl?
- returns true if use SSL/TLS with HTTP.
-
-: use_ssl=((|true_or_false|))
- sets use_ssl.
-
-: peer_cert
- return the X.509 certificates the server presented.
-
-: key, key=((|key|))
- Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
- (This method is appeared in Michal Rokos's OpenSSL extension.)
-
-: cert, cert=((|cert|))
- Sets an OpenSSL::X509::Certificate object as client certificate
- (This method is appeared in Michal Rokos's OpenSSL extension).
-
-: ca_file, ca_file=((|path|))
- Sets path of a CA certification file in PEM format.
- The file can contrain several CA certificats.
-
-: ca_path, ca_path=((|path|))
- Sets path of a CA certification directory containing certifications
- in PEM format.
-
-: verify_mode, verify_mode=((|mode|))
- Sets the flags for server the certification verification at
- begining of SSL/TLS session.
- OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER is acceptable.
-
-: verify_callback, verify_callback=((|proc|))
- Sets the verify callback for the server certification verification.
-
-: verify_depth, verify_depth=((|num|))
- Sets the maximum depth for the certificate chain verification.
-
-: cert_store, cert_store=((|store|))
- Sets the X509::Store to verify peer certificate.
-
-: ssl_timeout, ssl_timeout=((|sec|))
- Sets the SSL timeout seconds.
-
=end
-require 'net/http'
+require_relative 'http'
require 'openssl'
-
-module Net
- class HTTP
- remove_method :use_ssl?
- def use_ssl?
- @use_ssl
- end
-
- # Turn on/off SSL.
- # This flag must be set before starting session.
- # If you change use_ssl value after session started,
- # a Net::HTTP object raises IOError.
- def use_ssl=(flag)
- flag = (flag ? true : false)
- if started? and @use_ssl != flag
- raise IOError, "use_ssl value changed, but session already started"
- end
- @use_ssl = flag
- end
-
- SSL_ATTRIBUTES = %w(
- ssl_version key cert ca_file ca_path cert_store ciphers
- verify_mode verify_callback verify_depth ssl_timeout
- )
- attr_accessor(*SSL_ATTRIBUTES)
-
- def peer_cert
- if not use_ssl? or not @socket
- return nil
- end
- @socket.io.peer_cert
- end
- end
-end
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
deleted file mode 100644
index 09b5349298..0000000000
--- a/lib/net/imap.rb
+++ /dev/null
@@ -1,3449 +0,0 @@
-#
-# = net/imap.rb
-#
-# Copyright (C) 2000 Shugo Maeda <shugo@ruby-lang.org>
-#
-# This library is distributed under the terms of the Ruby license.
-# You can freely distribute/modify this library.
-#
-# Documentation: Shugo Maeda, with RDoc conversion and overview by William
-# Webber.
-#
-# See Net::IMAP for documentation.
-#
-
-
-require "socket"
-require "monitor"
-require "digest/md5"
-require "strscan"
-begin
- require "openssl/ssl"
-rescue LoadError
-end
-
-module Net
-
- #
- # Net::IMAP implements Internet Message Access Protocol (IMAP) client
- # functionality. The protocol is described in [IMAP].
- #
- # == IMAP Overview
- #
- # An IMAP client connects to a server, and then authenticates
- # itself using either #authenticate() or #login(). Having
- # authenticated itself, there is a range of commands
- # available to it. Most work with mailboxes, which may be
- # arranged in an hierarchical namespace, and each of which
- # contains zero or more messages. How this is implemented on
- # the server is implementation-dependent; on a UNIX server, it
- # will frequently be implemented as a files in mailbox format
- # within a hierarchy of directories.
- #
- # To work on the messages within a mailbox, the client must
- # first select that mailbox, using either #select() or (for
- # read-only access) #examine(). Once the client has successfully
- # selected a mailbox, they enter _selected_ state, and that
- # mailbox becomes the _current_ mailbox, on which mail-item
- # related commands implicitly operate.
- #
- # Messages have two sorts of identifiers: message sequence
- # numbers, and UIDs.
- #
- # Message sequence numbers number messages within a mail box
- # from 1 up to the number of items in the mail box. If new
- # message arrives during a session, it receives a sequence
- # number equal to the new size of the mail box. If messages
- # are expunged from the mailbox, remaining messages have their
- # sequence numbers "shuffled down" to fill the gaps.
- #
- # UIDs, on the other hand, are permanently guaranteed not to
- # identify another message within the same mailbox, even if
- # the existing message is deleted. UIDs are required to
- # be assigned in ascending (but not necessarily sequential)
- # order within a mailbox; this means that if a non-IMAP client
- # rearranges the order of mailitems within a mailbox, the
- # UIDs have to be reassigned. An IMAP client cannot thus
- # rearrange message orders.
- #
- # == Examples of Usage
- #
- # === List sender and subject of all recent messages in the default mailbox
- #
- # imap = Net::IMAP.new('mail.example.com')
- # imap.authenticate('LOGIN', 'joe_user', 'joes_password')
- # imap.examine('INBOX')
- # imap.search(["RECENT"]).each do |message_id|
- # envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"]
- # puts "#{envelope.from[0].name}: \t#{envelope.subject}"
- # end
- #
- # === Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03"
- #
- # imap = Net::IMAP.new('mail.example.com')
- # imap.authenticate('LOGIN', 'joe_user', 'joes_password')
- # imap.select('Mail/sent-mail')
- # if not imap.list('Mail/', 'sent-apr03')
- # imap.create('Mail/sent-apr03')
- # end
- # imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id|
- # imap.copy(message_id, "Mail/sent-apr03")
- # imap.store(message_id, "+FLAGS", [:Deleted])
- # end
- # imap.expunge
- #
- # == Thread Safety
- #
- # Net::IMAP supports concurrent threads. For example,
- #
- # imap = Net::IMAP.new("imap.foo.net", "imap2")
- # imap.authenticate("cram-md5", "bar", "password")
- # imap.select("inbox")
- # fetch_thread = Thread.start { imap.fetch(1..-1, "UID") }
- # search_result = imap.search(["BODY", "hello"])
- # fetch_result = fetch_thread.value
- # imap.disconnect
- #
- # This script invokes the FETCH command and the SEARCH command concurrently.
- #
- # == Errors
- #
- # An IMAP server can send three different types of responses to indicate
- # failure:
- #
- # NO:: the attempted command could not be successfully completed. For
- # instance, the username/password used for logging in are incorrect;
- # the selected mailbox does not exists; etc.
- #
- # BAD:: the request from the client does not follow the server's
- # understanding of the IMAP protocol. This includes attempting
- # commands from the wrong client state; for instance, attempting
- # to perform a SEARCH command without having SELECTed a current
- # mailbox. It can also signal an internal server
- # failure (such as a disk crash) has occurred.
- #
- # BYE:: the server is saying goodbye. This can be part of a normal
- # logout sequence, and can be used as part of a login sequence
- # to indicate that the server is (for some reason) unwilling
- # to accept our connection. As a response to any other command,
- # it indicates either that the server is shutting down, or that
- # the server is timing out the client connection due to inactivity.
- #
- # These three error response are represented by the errors
- # Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, and
- # Net::IMAP::ByeResponseError, all of which are subclasses of
- # Net::IMAP::ResponseError. Essentially, all methods that involve
- # sending a request to the server can generate one of these errors.
- # Only the most pertinent instances have been documented below.
- #
- # Because the IMAP class uses Sockets for communication, its methods
- # are also susceptible to the various errors that can occur when
- # working with sockets. These are generally represented as
- # Errno errors. For instance, any method that involves sending a
- # request to the server and/or receiving a response from it could
- # raise an Errno::EPIPE error if the network connection unexpectedly
- # goes down. See the socket(7), ip(7), tcp(7), socket(2), connect(2),
- # and associated man pages.
- #
- # Finally, a Net::IMAP::DataFormatError is thrown if low-level data
- # is found to be in an incorrect format (for instance, when converting
- # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is
- # thrown if a server response is non-parseable.
- #
- #
- # == References
- #
- # [[IMAP]]
- # M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1",
- # RFC 2060, December 1996. (Note: since obsoleted by RFC 3501)
- #
- # [[LANGUAGE-TAGS]]
- # Alvestrand, H., "Tags for the Identification of
- # Languages", RFC 1766, March 1995.
- #
- # [[MD5]]
- # Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC
- # 1864, October 1995.
- #
- # [[MIME-IMB]]
- # Freed, N., and N. Borenstein, "MIME (Multipurpose Internet
- # Mail Extensions) Part One: Format of Internet Message Bodies", RFC
- # 2045, November 1996.
- #
- # [[RFC-822]]
- # Crocker, D., "Standard for the Format of ARPA Internet Text
- # Messages", STD 11, RFC 822, University of Delaware, August 1982.
- #
- # [[RFC-2087]]
- # Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997.
- #
- # [[RFC-2086]]
- # Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997.
- #
- # [[RFC-2195]]
- # Klensin, J., Catoe, R., and Krumviede, P., "IMAP/POP AUTHorize Extension
- # for Simple Challenge/Response", RFC 2195, September 1997.
- #
- # [[SORT-THREAD-EXT]]
- # Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - SORT and THREAD
- # Extensions", draft-ietf-imapext-sort, May 2003.
- #
- # [[OSSL]]
- # http://www.openssl.org
- #
- # [[RSSL]]
- # http://savannah.gnu.org/projects/rubypki
- #
- # [[UTF7]]
- # Goldsmith, D. and Davis, M., "UTF-7: A Mail-Safe Transformation Format of
- # Unicode", RFC 2152, May 1997.
- #
- class IMAP
- include MonitorMixin
- if defined?(OpenSSL)
- include OpenSSL
- include SSL
- end
-
- # Returns an initial greeting response from the server.
- attr_reader :greeting
-
- # Returns recorded untagged responses. For example:
- #
- # imap.select("inbox")
- # p imap.responses["EXISTS"][-1]
- # #=> 2
- # p imap.responses["UIDVALIDITY"][-1]
- # #=> 968263756
- attr_reader :responses
-
- # Returns all response handlers.
- attr_reader :response_handlers
-
- # The thread to receive exceptions.
- attr_accessor :client_thread
-
- # Flag indicating a message has been seen
- SEEN = :Seen
-
- # Flag indicating a message has been answered
- ANSWERED = :Answered
-
- # Flag indicating a message has been flagged for special or urgent
- # attention
- FLAGGED = :Flagged
-
- # Flag indicating a message has been marked for deletion. This
- # will occur when the mailbox is closed or expunged.
- DELETED = :Deleted
-
- # Flag indicating a message is only a draft or work-in-progress version.
- DRAFT = :Draft
-
- # Flag indicating that the message is "recent", meaning that this
- # session is the first session in which the client has been notified
- # of this message.
- RECENT = :Recent
-
- # Flag indicating that a mailbox context name cannot contain
- # children.
- NOINFERIORS = :Noinferiors
-
- # Flag indicating that a mailbox is not selected.
- NOSELECT = :Noselect
-
- # Flag indicating that a mailbox has been marked "interesting" by
- # the server; this commonly indicates that the mailbox contains
- # new messages.
- MARKED = :Marked
-
- # Flag indicating that the mailbox does not contains new messages.
- UNMARKED = :Unmarked
-
- # Returns the debug mode.
- def self.debug
- return @@debug
- end
-
- # Sets the debug mode.
- def self.debug=(val)
- return @@debug = val
- end
-
- # Adds an authenticator for Net::IMAP#authenticate. +auth_type+
- # is the type of authentication this authenticator supports
- # (for instance, "LOGIN"). The +authenticator+ is an object
- # which defines a process() method to handle authentication with
- # the server. See Net::IMAP::LoginAuthenticator,
- # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator
- # for examples.
- #
- #
- # If +auth_type+ refers to an existing authenticator, it will be
- # replaced by the new one.
- def self.add_authenticator(auth_type, authenticator)
- @@authenticators[auth_type] = authenticator
- end
-
- # Disconnects from the server.
- def disconnect
- begin
- begin
- # try to call SSL::SSLSocket#io.
- @sock.io.shutdown
- rescue NoMethodError
- # @sock is not an SSL::SSLSocket.
- @sock.shutdown
- end
- rescue Errno::ENOTCONN
- # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
- end
- @receiver_thread.join
- @sock.close
- end
-
- # Returns true if disconnected from the server.
- def disconnected?
- return @sock.closed?
- end
-
- # Sends a CAPABILITY command, and returns an array of
- # capabilities that the server supports. Each capability
- # is a string. See [IMAP] for a list of possible
- # capabilities.
- #
- # Note that the Net::IMAP class does not modify its
- # behaviour according to the capabilities of the server;
- # it is up to the user of the class to ensure that
- # a certain capability is supported by a server before
- # using it.
- def capability
- synchronize do
- send_command("CAPABILITY")
- return @responses.delete("CAPABILITY")[-1]
- end
- end
-
- # Sends a NOOP command to the server. It does nothing.
- def noop
- send_command("NOOP")
- end
-
- # Sends a LOGOUT command to inform the server that the client is
- # done with the connection.
- def logout
- send_command("LOGOUT")
- end
-
- # Sends a STARTTLS command to start TLS session.
- def starttls(options = {}, verify = true)
- send_command("STARTTLS") do |resp|
- if resp.kind_of?(TaggedResponse) && resp.name == "OK"
- begin
- # for backward compatibility
- certs = options.to_str
- options = create_ssl_params(certs, verify)
- rescue NoMethodError
- end
- start_tls_session(options)
- end
- end
- end
-
- # Sends an AUTHENTICATE command to authenticate the client.
- # The +auth_type+ parameter is a string that represents
- # the authentication mechanism to be used. Currently Net::IMAP
- # supports authentication mechanisms:
- #
- # LOGIN:: login using cleartext user and password.
- # CRAM-MD5:: login with cleartext user and encrypted password
- # (see [RFC-2195] for a full description). This
- # mechanism requires that the server have the user's
- # password stored in clear-text password.
- #
- # For both these mechanisms, there should be two +args+: username
- # and (cleartext) password. A server may not support one or other
- # of these mechanisms; check #capability() for a capability of
- # the form "AUTH=LOGIN" or "AUTH=CRAM-MD5".
- #
- # Authentication is done using the appropriate authenticator object:
- # see @@authenticators for more information on plugging in your own
- # authenticator.
- #
- # For example:
- #
- # imap.authenticate('LOGIN', user, password)
- #
- # A Net::IMAP::NoResponseError is raised if authentication fails.
- def authenticate(auth_type, *args)
- auth_type = auth_type.upcase
- unless @@authenticators.has_key?(auth_type)
- raise ArgumentError,
- format('unknown auth type - "%s"', auth_type)
- end
- authenticator = @@authenticators[auth_type].new(*args)
- send_command("AUTHENTICATE", auth_type) do |resp|
- if resp.instance_of?(ContinuationRequest)
- data = authenticator.process(resp.data.text.unpack("m")[0])
- s = [data].pack("m").gsub(/\n/, "")
- send_string_data(s)
- put_string(CRLF)
- end
- end
- end
-
- # Sends a LOGIN command to identify the client and carries
- # the plaintext +password+ authenticating this +user+. Note
- # that, unlike calling #authenticate() with an +auth_type+
- # of "LOGIN", #login() does *not* use the login authenticator.
- #
- # A Net::IMAP::NoResponseError is raised if authentication fails.
- def login(user, password)
- send_command("LOGIN", user, password)
- end
-
- # Sends a SELECT command to select a +mailbox+ so that messages
- # in the +mailbox+ can be accessed.
- #
- # After you have selected a mailbox, you may retrieve the
- # number of items in that mailbox from @responses["EXISTS"][-1],
- # and the number of recent messages from @responses["RECENT"][-1].
- # Note that these values can change if new messages arrive
- # during a session; see #add_response_handler() for a way of
- # detecting this event.
- #
- # A Net::IMAP::NoResponseError is raised if the mailbox does not
- # exist or is for some reason non-selectable.
- def select(mailbox)
- synchronize do
- @responses.clear
- send_command("SELECT", mailbox)
- end
- end
-
- # Sends a EXAMINE command to select a +mailbox+ so that messages
- # in the +mailbox+ can be accessed. Behaves the same as #select(),
- # except that the selected +mailbox+ is identified as read-only.
- #
- # A Net::IMAP::NoResponseError is raised if the mailbox does not
- # exist or is for some reason non-examinable.
- def examine(mailbox)
- synchronize do
- @responses.clear
- send_command("EXAMINE", mailbox)
- end
- end
-
- # Sends a CREATE command to create a new +mailbox+.
- #
- # A Net::IMAP::NoResponseError is raised if a mailbox with that name
- # cannot be created.
- def create(mailbox)
- send_command("CREATE", mailbox)
- end
-
- # Sends a DELETE command to remove the +mailbox+.
- #
- # A Net::IMAP::NoResponseError is raised if a mailbox with that name
- # cannot be deleted, either because it does not exist or because the
- # client does not have permission to delete it.
- def delete(mailbox)
- send_command("DELETE", mailbox)
- end
-
- # Sends a RENAME command to change the name of the +mailbox+ to
- # +newname+.
- #
- # A Net::IMAP::NoResponseError is raised if a mailbox with the
- # name +mailbox+ cannot be renamed to +newname+ for whatever
- # reason; for instance, because +mailbox+ does not exist, or
- # because there is already a mailbox with the name +newname+.
- def rename(mailbox, newname)
- send_command("RENAME", mailbox, newname)
- end
-
- # Sends a SUBSCRIBE command to add the specified +mailbox+ name to
- # the server's set of "active" or "subscribed" mailboxes as returned
- # by #lsub().
- #
- # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be
- # subscribed to, for instance because it does not exist.
- def subscribe(mailbox)
- send_command("SUBSCRIBE", mailbox)
- end
-
- # Sends a UNSUBSCRIBE command to remove the specified +mailbox+ name
- # from the server's set of "active" or "subscribed" mailboxes.
- #
- # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be
- # unsubscribed from, for instance because the client is not currently
- # subscribed to it.
- def unsubscribe(mailbox)
- send_command("UNSUBSCRIBE", mailbox)
- end
-
- # Sends a LIST command, and returns a subset of names from
- # the complete set of all names available to the client.
- # +refname+ provides a context (for instance, a base directory
- # in a directory-based mailbox hierarchy). +mailbox+ specifies
- # a mailbox or (via wildcards) mailboxes under that context.
- # Two wildcards may be used in +mailbox+: '*', which matches
- # all characters *including* the hierarchy delimiter (for instance,
- # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%',
- # which matches all characters *except* the hierarchy delimiter.
- #
- # If +refname+ is empty, +mailbox+ is used directly to determine
- # which mailboxes to match. If +mailbox+ is empty, the root
- # name of +refname+ and the hierarchy delimiter are returned.
- #
- # The return value is an array of +Net::IMAP::MailboxList+. For example:
- #
- # imap.create("foo/bar")
- # imap.create("foo/baz")
- # p imap.list("", "foo/%")
- # #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\
- # #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\
- # #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">]
- def list(refname, mailbox)
- synchronize do
- send_command("LIST", refname, mailbox)
- return @responses.delete("LIST")
- end
- end
-
- # Sends the GETQUOTAROOT command along with specified +mailbox+.
- # This command is generally available to both admin and user.
- # If mailbox exists, returns an array containing objects of
- # Net::IMAP::MailboxQuotaRoot and Net::IMAP::MailboxQuota.
- def getquotaroot(mailbox)
- synchronize do
- send_command("GETQUOTAROOT", mailbox)
- result = []
- result.concat(@responses.delete("QUOTAROOT"))
- result.concat(@responses.delete("QUOTA"))
- return result
- end
- end
-
- # Sends the GETQUOTA command along with specified +mailbox+.
- # If this mailbox exists, then an array containing a
- # Net::IMAP::MailboxQuota object is returned. This
- # command generally is only available to server admin.
- def getquota(mailbox)
- synchronize do
- send_command("GETQUOTA", mailbox)
- return @responses.delete("QUOTA")
- end
- end
-
- # Sends a SETQUOTA command along with the specified +mailbox+ and
- # +quota+. If +quota+ is nil, then quota will be unset for that
- # mailbox. Typically one needs to be logged in as server admin
- # for this to work. The IMAP quota commands are described in
- # [RFC-2087].
- def setquota(mailbox, quota)
- if quota.nil?
- data = '()'
- else
- data = '(STORAGE ' + quota.to_s + ')'
- end
- send_command("SETQUOTA", mailbox, RawData.new(data))
- end
-
- # Sends the SETACL command along with +mailbox+, +user+ and the
- # +rights+ that user is to have on that mailbox. If +rights+ is nil,
- # then that user will be stripped of any rights to that mailbox.
- # The IMAP ACL commands are described in [RFC-2086].
- def setacl(mailbox, user, rights)
- if rights.nil?
- send_command("SETACL", mailbox, user, "")
- else
- send_command("SETACL", mailbox, user, rights)
- end
- end
-
- # Send the GETACL command along with specified +mailbox+.
- # If this mailbox exists, an array containing objects of
- # Net::IMAP::MailboxACLItem will be returned.
- def getacl(mailbox)
- synchronize do
- send_command("GETACL", mailbox)
- return @responses.delete("ACL")[-1]
- end
- end
-
- # Sends a LSUB command, and returns a subset of names from the set
- # of names that the user has declared as being "active" or
- # "subscribed". +refname+ and +mailbox+ are interpreted as
- # for #list().
- # The return value is an array of +Net::IMAP::MailboxList+.
- def lsub(refname, mailbox)
- synchronize do
- send_command("LSUB", refname, mailbox)
- return @responses.delete("LSUB")
- end
- end
-
- # Sends a STATUS command, and returns the status of the indicated
- # +mailbox+. +attr+ is a list of one or more attributes that
- # we are request the status of. Supported attributes include:
- #
- # MESSAGES:: the number of messages in the mailbox.
- # RECENT:: the number of recent messages in the mailbox.
- # UNSEEN:: the number of unseen messages in the mailbox.
- #
- # The return value is a hash of attributes. For example:
- #
- # p imap.status("inbox", ["MESSAGES", "RECENT"])
- # #=> {"RECENT"=>0, "MESSAGES"=>44}
- #
- # A Net::IMAP::NoResponseError is raised if status values
- # for +mailbox+ cannot be returned, for instance because it
- # does not exist.
- def status(mailbox, attr)
- synchronize do
- send_command("STATUS", mailbox, attr)
- return @responses.delete("STATUS")[-1].attr
- end
- end
-
- # Sends a APPEND command to append the +message+ to the end of
- # the +mailbox+. The optional +flags+ argument is an array of
- # flags to initially passing to the new message. The optional
- # +date_time+ argument specifies the creation time to assign to the
- # new message; it defaults to the current time.
- # For example:
- #
- # imap.append("inbox", <<EOF.gsub(/\n/, "\r\n"), [:Seen], Time.now)
- # Subject: hello
- # From: shugo@ruby-lang.org
- # To: shugo@ruby-lang.org
- #
- # hello world
- # EOF
- #
- # A Net::IMAP::NoResponseError is raised if the mailbox does
- # not exist (it is not created automatically), or if the flags,
- # date_time, or message arguments contain errors.
- def append(mailbox, message, flags = nil, date_time = nil)
- args = []
- if flags
- args.push(flags)
- end
- args.push(date_time) if date_time
- args.push(Literal.new(message))
- send_command("APPEND", mailbox, *args)
- end
-
- # Sends a CHECK command to request a checkpoint of the currently
- # selected mailbox. This performs implementation-specific
- # housekeeping, for instance, reconciling the mailbox's
- # in-memory and on-disk state.
- def check
- send_command("CHECK")
- end
-
- # Sends a CLOSE command to close the currently selected mailbox.
- # The CLOSE command permanently removes from the mailbox all
- # messages that have the \Deleted flag set.
- def close
- send_command("CLOSE")
- end
-
- # Sends a EXPUNGE command to permanently remove from the currently
- # selected mailbox all messages that have the \Deleted flag set.
- def expunge
- synchronize do
- send_command("EXPUNGE")
- return @responses.delete("EXPUNGE")
- end
- end
-
- # Sends a SEARCH command to search the mailbox for messages that
- # match the given searching criteria, and returns message sequence
- # numbers. +keys+ can either be a string holding the entire
- # search string, or a single-dimension array of search keywords and
- # arguments. The following are some common search criteria;
- # see [IMAP] section 6.4.4 for a full list.
- #
- # <message set>:: a set of message sequence numbers. ',' indicates
- # an interval, ':' indicates a range. For instance,
- # '2,10:12,15' means "2,10,11,12,15".
- #
- # BEFORE <date>:: messages with an internal date strictly before
- # <date>. The date argument has a format similar
- # to 8-Aug-2002.
- #
- # BODY <string>:: messages that contain <string> within their body.
- #
- # CC <string>:: messages containing <string> in their CC field.
- #
- # FROM <string>:: messages that contain <string> in their FROM field.
- #
- # NEW:: messages with the \Recent, but not the \Seen, flag set.
- #
- # NOT <search-key>:: negate the following search key.
- #
- # OR <search-key> <search-key>:: "or" two search keys together.
- #
- # ON <date>:: messages with an internal date exactly equal to <date>,
- # which has a format similar to 8-Aug-2002.
- #
- # SINCE <date>:: messages with an internal date on or after <date>.
- #
- # SUBJECT <string>:: messages with <string> in their subject.
- #
- # TO <string>:: messages with <string> in their TO field.
- #
- # For example:
- #
- # p imap.search(["SUBJECT", "hello", "NOT", "NEW"])
- # #=> [1, 6, 7, 8]
- def search(keys, charset = nil)
- return search_internal("SEARCH", keys, charset)
- end
-
- # As for #search(), but returns unique identifiers.
- def uid_search(keys, charset = nil)
- return search_internal("UID SEARCH", keys, charset)
- end
-
- # Sends a FETCH command to retrieve data associated with a message
- # in the mailbox. The +set+ parameter is a number or an array of
- # numbers or a Range object. The number is a message sequence
- # number. +attr+ is a list of attributes to fetch; see the
- # documentation for Net::IMAP::FetchData for a list of valid
- # attributes.
- # The return value is an array of Net::IMAP::FetchData. For example:
- #
- # p imap.fetch(6..8, "UID")
- # #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, \\
- # #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, \\
- # #<Net::IMAP::FetchData seqno=8, attr={"UID"=>100}>]
- # p imap.fetch(6, "BODY[HEADER.FIELDS (SUBJECT)]")
- # #=> [#<Net::IMAP::FetchData seqno=6, attr={"BODY[HEADER.FIELDS (SUBJECT)]"=>"Subject: test\r\n\r\n"}>]
- # data = imap.uid_fetch(98, ["RFC822.SIZE", "INTERNALDATE"])[0]
- # p data.seqno
- # #=> 6
- # p data.attr["RFC822.SIZE"]
- # #=> 611
- # p data.attr["INTERNALDATE"]
- # #=> "12-Oct-2000 22:40:59 +0900"
- # p data.attr["UID"]
- # #=> 98
- def fetch(set, attr)
- return fetch_internal("FETCH", set, attr)
- end
-
- # As for #fetch(), but +set+ contains unique identifiers.
- def uid_fetch(set, attr)
- return fetch_internal("UID FETCH", set, attr)
- end
-
- # Sends a STORE command to alter data associated with messages
- # in the mailbox, in particular their flags. The +set+ parameter
- # is a number or an array of numbers or a Range object. Each number
- # is a message sequence number. +attr+ is the name of a data item
- # to store: 'FLAGS' means to replace the message's flag list
- # with the provided one; '+FLAGS' means to add the provided flags;
- # and '-FLAGS' means to remove them. +flags+ is a list of flags.
- #
- # The return value is an array of Net::IMAP::FetchData. For example:
- #
- # p imap.store(6..8, "+FLAGS", [:Deleted])
- # #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
- # #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
- # #<Net::IMAP::FetchData seqno=8, attr={"FLAGS"=>[:Seen, :Deleted]}>]
- def store(set, attr, flags)
- return store_internal("STORE", set, attr, flags)
- end
-
- # As for #store(), but +set+ contains unique identifiers.
- def uid_store(set, attr, flags)
- return store_internal("UID STORE", set, attr, flags)
- end
-
- # Sends a COPY command to copy the specified message(s) to the end
- # of the specified destination +mailbox+. The +set+ parameter is
- # a number or an array of numbers or a Range object. The number is
- # a message sequence number.
- def copy(set, mailbox)
- copy_internal("COPY", set, mailbox)
- end
-
- # As for #copy(), but +set+ contains unique identifiers.
- def uid_copy(set, mailbox)
- copy_internal("UID COPY", set, mailbox)
- end
-
- # Sends a SORT command to sort messages in the mailbox.
- # Returns an array of message sequence numbers. For example:
- #
- # p imap.sort(["FROM"], ["ALL"], "US-ASCII")
- # #=> [1, 2, 3, 5, 6, 7, 8, 4, 9]
- # p imap.sort(["DATE"], ["SUBJECT", "hello"], "US-ASCII")
- # #=> [6, 7, 8, 1]
- #
- # See [SORT-THREAD-EXT] for more details.
- def sort(sort_keys, search_keys, charset)
- return sort_internal("SORT", sort_keys, search_keys, charset)
- end
-
- # As for #sort(), but returns an array of unique identifiers.
- def uid_sort(sort_keys, search_keys, charset)
- return sort_internal("UID SORT", sort_keys, search_keys, charset)
- end
-
- # Adds a response handler. For example, to detect when
- # the server sends us a new EXISTS response (which normally
- # indicates new messages being added to the mail box),
- # you could add the following handler after selecting the
- # mailbox.
- #
- # imap.add_response_handler { |resp|
- # if resp.kind_of?(Net::IMAP::UntaggedResponse) and resp.name == "EXISTS"
- # puts "Mailbox now has #{resp.data} messages"
- # end
- # }
- #
- def add_response_handler(handler = Proc.new)
- @response_handlers.push(handler)
- end
-
- # Removes the response handler.
- def remove_response_handler(handler)
- @response_handlers.delete(handler)
- end
-
- # As for #search(), but returns message sequence numbers in threaded
- # format, as a Net::IMAP::ThreadMember tree. The supported algorithms
- # are:
- #
- # ORDEREDSUBJECT:: split into single-level threads according to subject,
- # ordered by date.
- # REFERENCES:: split into threads by parent/child relationships determined
- # by which message is a reply to which.
- #
- # Unlike #search(), +charset+ is a required argument. US-ASCII
- # and UTF-8 are sample values.
- #
- # See [SORT-THREAD-EXT] for more details.
- def thread(algorithm, search_keys, charset)
- return thread_internal("THREAD", algorithm, search_keys, charset)
- end
-
- # As for #thread(), but returns unique identifiers instead of
- # message sequence numbers.
- def uid_thread(algorithm, search_keys, charset)
- return thread_internal("UID THREAD", algorithm, search_keys, charset)
- end
-
- # Decode a string from modified UTF-7 format to UTF-8.
- #
- # UTF-7 is a 7-bit encoding of Unicode [UTF7]. IMAP uses a
- # slightly modified version of this to encode mailbox names
- # containing non-ASCII characters; see [IMAP] section 5.1.3.
- #
- # Net::IMAP does _not_ automatically encode and decode
- # mailbox names to and from utf7.
- def self.decode_utf7(s)
- return s.gsub(/&(.*?)-/n) {
- if $1.empty?
- "&"
- else
- base64 = $1.tr(",", "/")
- x = base64.length % 4
- if x > 0
- base64.concat("=" * (4 - x))
- end
- base64.unpack("m")[0].unpack("n*").pack("U*")
- end
- }.force_encoding("UTF-8")
- end
-
- # Encode a string from UTF-8 format to modified UTF-7.
- def self.encode_utf7(s)
- return s.gsub(/(&)|([^\x20-\x25\x27-\x7e]+)/u) {
- if $1
- "&-"
- else
- base64 = [$&.unpack("U*").pack("n*")].pack("m")
- "&" + base64.delete("=\n").tr("/", ",") + "-"
- end
- }.force_encoding("ASCII-8BIT")
- end
-
- private
-
- CRLF = "\r\n" # :nodoc:
- PORT = 143 # :nodoc:
- SSL_PORT = 993 # :nodoc:
-
- @@debug = false
- @@authenticators = {}
-
- # call-seq:
- # Net::IMAP.new(host, options = {})
- #
- # Creates a new Net::IMAP object and connects it to the specified
- # +host+.
- #
- # +options+ is an option hash, each key of which is a symbol.
- #
- # The available options are:
- #
- # port:: port number (default value is 143 for imap, or 993 for imaps)
- # ssl:: if options[:ssl] is true, then an attempt will be made
- # to use SSL (now TLS) to connect to the server. For this to work
- # OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] extensions need to
- # be installed.
- # if options[:ssl] is a hash, it's passed to
- # OpenSSL::SSL::SSLContext#set_params as parameters.
- #
- # The most common errors are:
- #
- # Errno::ECONNREFUSED:: connection refused by +host+ or an intervening
- # firewall.
- # Errno::ETIMEDOUT:: connection timed out (possibly due to packets
- # being dropped by an intervening firewall).
- # Errno::ENETUNREACH:: there is no route to that network.
- # SocketError:: hostname not known or other socket error.
- # Net::IMAP::ByeResponseError:: we connected to the host, but they
- # immediately said goodbye to us.
- def initialize(host, port_or_options = {},
- usessl = false, certs = nil, verify = true)
- super()
- @host = host
- begin
- options = port_or_options.to_hash
- rescue NoMethodError
- # for backward compatibility
- options = {}
- options[:port] = port_or_options
- if usessl
- options[:ssl] = create_ssl_params(certs, verify)
- end
- end
- @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT)
- @tag_prefix = "RUBY"
- @tagno = 0
- @parser = ResponseParser.new
- @sock = TCPSocket.open(@host, @port)
- if options[:ssl]
- start_tls_session(options[:ssl])
- @usessl = true
- else
- @usessl = false
- end
- @responses = Hash.new([].freeze)
- @tagged_responses = {}
- @response_handlers = []
- @tagged_response_arrival = new_cond
- @continuation_request_arrival = new_cond
- @logout_command_tag = nil
- @debug_output_bol = true
- @exception = nil
-
- @greeting = get_response
- if @greeting.name == "BYE"
- @sock.close
- raise ByeResponseError, @greeting.raw_data
- end
-
- @client_thread = Thread.current
- @receiver_thread = Thread.start {
- receive_responses
- }
- end
-
- def receive_responses
- while true
- synchronize do
- @exception = nil
- end
- begin
- resp = get_response
- rescue Exception => e
- synchronize do
- @sock.close
- @exception = e
- end
- break
- end
- unless resp
- synchronize do
- @exception = EOFError.new("end of file reached")
- end
- break
- end
- begin
- synchronize do
- case resp
- when TaggedResponse
- @tagged_responses[resp.tag] = resp
- @tagged_response_arrival.broadcast
- if resp.tag == @logout_command_tag
- return
- end
- when UntaggedResponse
- record_response(resp.name, resp.data)
- if resp.data.instance_of?(ResponseText) &&
- (code = resp.data.code)
- record_response(code.name, code.data)
- end
- if resp.name == "BYE" && @logout_command_tag.nil?
- @sock.close
- @exception = ByeResponseError.new(resp.raw_data)
- break
- end
- when ContinuationRequest
- @continuation_request_arrival.signal
- end
- @response_handlers.each do |handler|
- handler.call(resp)
- end
- end
- rescue Exception => e
- @exception = e
- synchronize do
- @tagged_response_arrival.broadcast
- @continuation_request_arrival.broadcast
- end
- end
- end
- synchronize do
- @tagged_response_arrival.broadcast
- @continuation_request_arrival.broadcast
- end
- end
-
- def get_tagged_response(tag, cmd)
- until @tagged_responses.key?(tag)
- raise @exception if @exception
- @tagged_response_arrival.wait
- end
- resp = @tagged_responses.delete(tag)
- case resp.name
- when /\A(?:NO)\z/ni
- raise NoResponseError, resp.data.text
- when /\A(?:BAD)\z/ni
- raise BadResponseError, resp.data.text
- else
- return resp
- end
- end
-
- def get_response
- buff = ""
- while true
- s = @sock.gets(CRLF)
- break unless s
- buff.concat(s)
- if /\{(\d+)\}\r\n/n =~ s
- s = @sock.read($1.to_i)
- buff.concat(s)
- else
- break
- end
- end
- return nil if buff.length == 0
- if @@debug
- $stderr.print(buff.gsub(/^/n, "S: "))
- end
- return @parser.parse(buff)
- end
-
- def record_response(name, data)
- unless @responses.has_key?(name)
- @responses[name] = []
- end
- @responses[name].push(data)
- end
-
- def send_command(cmd, *args, &block)
- synchronize do
- tag = generate_tag
- put_string(tag + " " + cmd)
- args.each do |i|
- put_string(" ")
- send_data(i)
- end
- put_string(CRLF)
- if cmd == "LOGOUT"
- @logout_command_tag = tag
- end
- if block
- add_response_handler(block)
- end
- begin
- return get_tagged_response(tag, cmd)
- ensure
- if block
- remove_response_handler(block)
- end
- end
- end
- end
-
- def generate_tag
- @tagno += 1
- return format("%s%04d", @tag_prefix, @tagno)
- end
-
- def put_string(str)
- @sock.print(str)
- if @@debug
- if @debug_output_bol
- $stderr.print("C: ")
- end
- $stderr.print(str.gsub(/\n(?!\z)/n, "\nC: "))
- if /\r\n\z/n.match(str)
- @debug_output_bol = true
- else
- @debug_output_bol = false
- end
- end
- end
-
- def send_data(data)
- case data
- when nil
- put_string("NIL")
- when String
- send_string_data(data)
- when Integer
- send_number_data(data)
- when Array
- send_list_data(data)
- when Time
- send_time_data(data)
- when Symbol
- send_symbol_data(data)
- else
- data.send_data(self)
- end
- end
-
- def send_string_data(str)
- case str
- when ""
- put_string('""')
- when /[\x80-\xff\r\n]/n
- # literal
- send_literal(str)
- when /[(){ \x00-\x1f\x7f%*"\\]/n
- # quoted string
- send_quoted_string(str)
- else
- put_string(str)
- end
- end
-
- def send_quoted_string(str)
- put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"')
- end
-
- def send_literal(str)
- put_string("{" + str.length.to_s + "}" + CRLF)
- @continuation_request_arrival.wait
- raise @exception if @exception
- put_string(str)
- end
-
- def send_number_data(num)
- if num < 0 || num >= 4294967296
- raise DataFormatError, num.to_s
- end
- put_string(num.to_s)
- end
-
- def send_list_data(list)
- put_string("(")
- first = true
- list.each do |i|
- if first
- first = false
- else
- put_string(" ")
- end
- send_data(i)
- end
- put_string(")")
- end
-
- DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
-
- def send_time_data(time)
- t = time.dup.gmtime
- s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
- t.day, DATE_MONTH[t.month - 1], t.year,
- t.hour, t.min, t.sec)
- put_string(s)
- end
-
- def send_symbol_data(symbol)
- put_string("\\" + symbol.to_s)
- end
-
- def search_internal(cmd, keys, charset)
- if keys.instance_of?(String)
- keys = [RawData.new(keys)]
- else
- normalize_searching_criteria(keys)
- end
- synchronize do
- if charset
- send_command(cmd, "CHARSET", charset, *keys)
- else
- send_command(cmd, *keys)
- end
- return @responses.delete("SEARCH")[-1]
- end
- end
-
- def fetch_internal(cmd, set, attr)
- if attr.instance_of?(String)
- attr = RawData.new(attr)
- end
- synchronize do
- @responses.delete("FETCH")
- send_command(cmd, MessageSet.new(set), attr)
- return @responses.delete("FETCH")
- end
- end
-
- def store_internal(cmd, set, attr, flags)
- if attr.instance_of?(String)
- attr = RawData.new(attr)
- end
- synchronize do
- @responses.delete("FETCH")
- send_command(cmd, MessageSet.new(set), attr, flags)
- return @responses.delete("FETCH")
- end
- end
-
- def copy_internal(cmd, set, mailbox)
- send_command(cmd, MessageSet.new(set), mailbox)
- end
-
- def sort_internal(cmd, sort_keys, search_keys, charset)
- if search_keys.instance_of?(String)
- search_keys = [RawData.new(search_keys)]
- else
- normalize_searching_criteria(search_keys)
- end
- normalize_searching_criteria(search_keys)
- synchronize do
- send_command(cmd, sort_keys, charset, *search_keys)
- return @responses.delete("SORT")[-1]
- end
- end
-
- def thread_internal(cmd, algorithm, search_keys, charset)
- if search_keys.instance_of?(String)
- search_keys = [RawData.new(search_keys)]
- else
- normalize_searching_criteria(search_keys)
- end
- normalize_searching_criteria(search_keys)
- send_command(cmd, algorithm, charset, *search_keys)
- return @responses.delete("THREAD")[-1]
- end
-
- def normalize_searching_criteria(keys)
- keys.collect! do |i|
- case i
- when -1, Range, Array
- MessageSet.new(i)
- else
- i
- end
- end
- end
-
- def create_ssl_params(certs = nil, verify = true)
- params = {}
- if certs
- if File.file?(certs)
- params[:ca_file] = certs
- elsif File.directory?(certs)
- params[:ca_path] = certs
- end
- end
- if verify
- params[:verify_mode] = VERIFY_PEER
- else
- params[:verify_mode] = VERIFY_NONE
- end
- return params
- end
-
- def start_tls_session(params = {})
- unless defined?(OpenSSL)
- raise "SSL extension not installed"
- end
- if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
- raise RuntimeError, "already using SSL"
- end
- begin
- params = params.to_hash
- rescue NoMethodError
- params = {}
- end
- context = SSLContext.new
- context.set_params(params)
- if defined?(VerifyCallbackProc)
- context.verify_callback = VerifyCallbackProc
- end
- @sock = SSLSocket.new(@sock, context)
- @sock.sync_close = true
- @sock.connect
- if context.verify_mode != VERIFY_NONE
- @sock.post_connection_check(@host)
- end
- end
-
- class RawData # :nodoc:
- def send_data(imap)
- imap.send(:put_string, @data)
- end
-
- private
-
- def initialize(data)
- @data = data
- end
- end
-
- class Atom # :nodoc:
- def send_data(imap)
- imap.send(:put_string, @data)
- end
-
- private
-
- def initialize(data)
- @data = data
- end
- end
-
- class QuotedString # :nodoc:
- def send_data(imap)
- imap.send(:send_quoted_string, @data)
- end
-
- private
-
- def initialize(data)
- @data = data
- end
- end
-
- class Literal # :nodoc:
- def send_data(imap)
- imap.send(:send_literal, @data)
- end
-
- private
-
- def initialize(data)
- @data = data
- end
- end
-
- class MessageSet # :nodoc:
- def send_data(imap)
- imap.send(:put_string, format_internal(@data))
- end
-
- private
-
- def initialize(data)
- @data = data
- end
-
- def format_internal(data)
- case data
- when "*"
- return data
- when Integer
- ensure_nz_number(data)
- if data == -1
- return "*"
- else
- return data.to_s
- end
- when Range
- return format_internal(data.first) +
- ":" + format_internal(data.last)
- when Array
- return data.collect {|i| format_internal(i)}.join(",")
- when ThreadMember
- return data.seqno.to_s +
- ":" + data.children.collect {|i| format_internal(i).join(",")}
- else
- raise DataFormatError, data.inspect
- end
- end
-
- def ensure_nz_number(num)
- if num < -1 || num == 0 || num >= 4294967296
- msg = "nz_number must be non-zero unsigned 32-bit integer: " +
- num.inspect
- raise DataFormatError, msg
- end
- end
- end
-
- # Net::IMAP::ContinuationRequest represents command continuation requests.
- #
- # The command continuation request response is indicated by a "+" token
- # instead of a tag. This form of response indicates that the server is
- # ready to accept the continuation of a command from the client. The
- # remainder of this response is a line of text.
- #
- # continue_req ::= "+" SPACE (resp_text / base64)
- #
- # ==== Fields:
- #
- # data:: Returns the data (Net::IMAP::ResponseText).
- #
- # raw_data:: Returns the raw data string.
- ContinuationRequest = Struct.new(:data, :raw_data)
-
- # Net::IMAP::UntaggedResponse represents untagged responses.
- #
- # Data transmitted by the server to the client and status responses
- # that do not indicate command completion are prefixed with the token
- # "*", and are called untagged responses.
- #
- # response_data ::= "*" SPACE (resp_cond_state / resp_cond_bye /
- # mailbox_data / message_data / capability_data)
- #
- # ==== Fields:
- #
- # name:: Returns the name such as "FLAGS", "LIST", "FETCH"....
- #
- # data:: Returns the data such as an array of flag symbols,
- # a ((<Net::IMAP::MailboxList>)) object....
- #
- # raw_data:: Returns the raw data string.
- UntaggedResponse = Struct.new(:name, :data, :raw_data)
-
- # Net::IMAP::TaggedResponse represents tagged responses.
- #
- # The server completion result response indicates the success or
- # failure of the operation. It is tagged with the same tag as the
- # client command which began the operation.
- #
- # response_tagged ::= tag SPACE resp_cond_state CRLF
- #
- # tag ::= 1*<any ATOM_CHAR except "+">
- #
- # resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text
- #
- # ==== Fields:
- #
- # tag:: Returns the tag.
- #
- # name:: Returns the name. the name is one of "OK", "NO", "BAD".
- #
- # data:: Returns the data. See ((<Net::IMAP::ResponseText>)).
- #
- # raw_data:: Returns the raw data string.
- #
- TaggedResponse = Struct.new(:tag, :name, :data, :raw_data)
-
- # Net::IMAP::ResponseText represents texts of responses.
- # The text may be prefixed by the response code.
- #
- # resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text)
- # ;; text SHOULD NOT begin with "[" or "="
- #
- # ==== Fields:
- #
- # code:: Returns the response code. See ((<Net::IMAP::ResponseCode>)).
- #
- # text:: Returns the text.
- #
- ResponseText = Struct.new(:code, :text)
-
- #
- # Net::IMAP::ResponseCode represents response codes.
- #
- # resp_text_code ::= "ALERT" / "PARSE" /
- # "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" /
- # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
- # "UIDVALIDITY" SPACE nz_number /
- # "UNSEEN" SPACE nz_number /
- # atom [SPACE 1*<any TEXT_CHAR except "]">]
- #
- # ==== Fields:
- #
- # name:: Returns the name such as "ALERT", "PERMANENTFLAGS", "UIDVALIDITY"....
- #
- # data:: Returns the data if it exists.
- #
- ResponseCode = Struct.new(:name, :data)
-
- # Net::IMAP::MailboxList represents contents of the LIST response.
- #
- # mailbox_list ::= "(" #("\Marked" / "\Noinferiors" /
- # "\Noselect" / "\Unmarked" / flag_extension) ")"
- # SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox
- #
- # ==== Fields:
- #
- # attr:: Returns the name attributes. Each name attribute is a symbol
- # capitalized by String#capitalize, such as :Noselect (not :NoSelect).
- #
- # delim:: Returns the hierarchy delimiter
- #
- # name:: Returns the mailbox name.
- #
- MailboxList = Struct.new(:attr, :delim, :name)
-
- # Net::IMAP::MailboxQuota represents contents of GETQUOTA response.
- # This object can also be a response to GETQUOTAROOT. In the syntax
- # specification below, the delimiter used with the "#" construct is a
- # single space (SPACE).
- #
- # quota_list ::= "(" #quota_resource ")"
- #
- # quota_resource ::= atom SPACE number SPACE number
- #
- # quota_response ::= "QUOTA" SPACE astring SPACE quota_list
- #
- # ==== Fields:
- #
- # mailbox:: The mailbox with the associated quota.
- #
- # usage:: Current storage usage of mailbox.
- #
- # quota:: Quota limit imposed on mailbox.
- #
- MailboxQuota = Struct.new(:mailbox, :usage, :quota)
-
- # Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT
- # response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.)
- #
- # quotaroot_response ::= "QUOTAROOT" SPACE astring *(SPACE astring)
- #
- # ==== Fields:
- #
- # mailbox:: The mailbox with the associated quota.
- #
- # quotaroots:: Zero or more quotaroots that effect the quota on the
- # specified mailbox.
- #
- MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots)
-
- # Net::IMAP::MailboxACLItem represents response from GETACL.
- #
- # acl_data ::= "ACL" SPACE mailbox *(SPACE identifier SPACE rights)
- #
- # identifier ::= astring
- #
- # rights ::= astring
- #
- # ==== Fields:
- #
- # user:: Login name that has certain rights to the mailbox
- # that was specified with the getacl command.
- #
- # rights:: The access rights the indicated user has to the
- # mailbox.
- #
- MailboxACLItem = Struct.new(:user, :rights)
-
- # Net::IMAP::StatusData represents contents of the STATUS response.
- #
- # ==== Fields:
- #
- # mailbox:: Returns the mailbox name.
- #
- # attr:: Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT",
- # "UIDVALIDITY", "UNSEEN". Each value is a number.
- #
- StatusData = Struct.new(:mailbox, :attr)
-
- # Net::IMAP::FetchData represents contents of the FETCH response.
- #
- # ==== Fields:
- #
- # seqno:: Returns the message sequence number.
- # (Note: not the unique identifier, even for the UID command response.)
- #
- # attr:: Returns a hash. Each key is a data item name, and each value is
- # its value.
- #
- # The current data items are:
- #
- # [BODY]
- # A form of BODYSTRUCTURE without extension data.
- # [BODY[<section>]<<origin_octet>>]
- # A string expressing the body contents of the specified section.
- # [BODYSTRUCTURE]
- # An object that describes the [MIME-IMB] body structure of a message.
- # See Net::IMAP::BodyTypeBasic, Net::IMAP::BodyTypeText,
- # Net::IMAP::BodyTypeMessage, Net::IMAP::BodyTypeMultipart.
- # [ENVELOPE]
- # A Net::IMAP::Envelope object that describes the envelope
- # structure of a message.
- # [FLAGS]
- # A array of flag symbols that are set for this message. flag symbols
- # are capitalized by String#capitalize.
- # [INTERNALDATE]
- # A string representing the internal date of the message.
- # [RFC822]
- # Equivalent to BODY[].
- # [RFC822.HEADER]
- # Equivalent to BODY.PEEK[HEADER].
- # [RFC822.SIZE]
- # A number expressing the [RFC-822] size of the message.
- # [RFC822.TEXT]
- # Equivalent to BODY[TEXT].
- # [UID]
- # A number expressing the unique identifier of the message.
- #
- FetchData = Struct.new(:seqno, :attr)
-
- # Net::IMAP::Envelope represents envelope structures of messages.
- #
- # ==== Fields:
- #
- # date:: Returns a string that represents the date.
- #
- # subject:: Returns a string that represents the subject.
- #
- # from:: Returns an array of Net::IMAP::Address that represents the from.
- #
- # sender:: Returns an array of Net::IMAP::Address that represents the sender.
- #
- # reply_to:: Returns an array of Net::IMAP::Address that represents the reply-to.
- #
- # to:: Returns an array of Net::IMAP::Address that represents the to.
- #
- # cc:: Returns an array of Net::IMAP::Address that represents the cc.
- #
- # bcc:: Returns an array of Net::IMAP::Address that represents the bcc.
- #
- # in_reply_to:: Returns a string that represents the in-reply-to.
- #
- # message_id:: Returns a string that represents the message-id.
- #
- Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to,
- :to, :cc, :bcc, :in_reply_to, :message_id)
-
- #
- # Net::IMAP::Address represents electronic mail addresses.
- #
- # ==== Fields:
- #
- # name:: Returns the phrase from [RFC-822] mailbox.
- #
- # route:: Returns the route from [RFC-822] route-addr.
- #
- # mailbox:: nil indicates end of [RFC-822] group.
- # If non-nil and host is nil, returns [RFC-822] group name.
- # Otherwise, returns [RFC-822] local-part
- #
- # host:: nil indicates [RFC-822] group syntax.
- # Otherwise, returns [RFC-822] domain name.
- #
- Address = Struct.new(:name, :route, :mailbox, :host)
-
- #
- # Net::IMAP::ContentDisposition represents Content-Disposition fields.
- #
- # ==== Fields:
- #
- # dsp_type:: Returns the disposition type.
- #
- # param:: Returns a hash that represents parameters of the Content-Disposition
- # field.
- #
- ContentDisposition = Struct.new(:dsp_type, :param)
-
- # Net::IMAP::ThreadMember represents a thread-node returned
- # by Net::IMAP#thread
- #
- # ==== Fields:
- #
- # seqno:: The sequence number of this message.
- #
- # children:: an array of Net::IMAP::ThreadMember objects for mail
- # items that are children of this in the thread.
- #
- ThreadMember = Struct.new(:seqno, :children)
-
- # Net::IMAP::BodyTypeBasic represents basic body structures of messages.
- #
- # ==== Fields:
- #
- # media_type:: Returns the content media type name as defined in [MIME-IMB].
- #
- # subtype:: Returns the content subtype name as defined in [MIME-IMB].
- #
- # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
- #
- # content_id:: Returns a string giving the content id as defined in [MIME-IMB].
- #
- # description:: Returns a string giving the content description as defined in
- # [MIME-IMB].
- #
- # encoding:: Returns a string giving the content transfer encoding as defined in
- # [MIME-IMB].
- #
- # size:: Returns a number giving the size of the body in octets.
- #
- # md5:: Returns a string giving the body MD5 value as defined in [MD5].
- #
- # disposition:: Returns a Net::IMAP::ContentDisposition object giving
- # the content disposition.
- #
- # language:: Returns a string or an array of strings giving the body
- # language value as defined in [LANGUAGE-TAGS].
- #
- # extension:: Returns extension data.
- #
- # multipart?:: Returns false.
- #
- class BodyTypeBasic < Struct.new(:media_type, :subtype,
- :param, :content_id,
- :description, :encoding, :size,
- :md5, :disposition, :language,
- :extension)
- def multipart?
- return false
- end
-
- # Obsolete: use +subtype+ instead. Calling this will
- # generate a warning message to +stderr+, then return
- # the value of +subtype+.
- def media_subtype
- $stderr.printf("warning: media_subtype is obsolete.\n")
- $stderr.printf(" use subtype instead.\n")
- return subtype
- end
- end
-
- # Net::IMAP::BodyTypeText represents TEXT body structures of messages.
- #
- # ==== Fields:
- #
- # lines:: Returns the size of the body in text lines.
- #
- # And Net::IMAP::BodyTypeText has all fields of Net::IMAP::BodyTypeBasic.
- #
- class BodyTypeText < Struct.new(:media_type, :subtype,
- :param, :content_id,
- :description, :encoding, :size,
- :lines,
- :md5, :disposition, :language,
- :extension)
- def multipart?
- return false
- end
-
- # Obsolete: use +subtype+ instead. Calling this will
- # generate a warning message to +stderr+, then return
- # the value of +subtype+.
- def media_subtype
- $stderr.printf("warning: media_subtype is obsolete.\n")
- $stderr.printf(" use subtype instead.\n")
- return subtype
- end
- end
-
- # Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages.
- #
- # ==== Fields:
- #
- # envelope:: Returns a Net::IMAP::Envelope giving the envelope structure.
- #
- # body:: Returns an object giving the body structure.
- #
- # And Net::IMAP::BodyTypeMessage has all methods of Net::IMAP::BodyTypeText.
- #
- class BodyTypeMessage < Struct.new(:media_type, :subtype,
- :param, :content_id,
- :description, :encoding, :size,
- :envelope, :body, :lines,
- :md5, :disposition, :language,
- :extension)
- def multipart?
- return false
- end
-
- # Obsolete: use +subtype+ instead. Calling this will
- # generate a warning message to +stderr+, then return
- # the value of +subtype+.
- def media_subtype
- $stderr.printf("warning: media_subtype is obsolete.\n")
- $stderr.printf(" use subtype instead.\n")
- return subtype
- end
- end
-
- # Net::IMAP::BodyTypeMultipart represents multipart body structures
- # of messages.
- #
- # ==== Fields:
- #
- # media_type:: Returns the content media type name as defined in [MIME-IMB].
- #
- # subtype:: Returns the content subtype name as defined in [MIME-IMB].
- #
- # parts:: Returns multiple parts.
- #
- # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
- #
- # disposition:: Returns a Net::IMAP::ContentDisposition object giving
- # the content disposition.
- #
- # language:: Returns a string or an array of strings giving the body
- # language value as defined in [LANGUAGE-TAGS].
- #
- # extension:: Returns extension data.
- #
- # multipart?:: Returns true.
- #
- class BodyTypeMultipart < Struct.new(:media_type, :subtype,
- :parts,
- :param, :disposition, :language,
- :extension)
- def multipart?
- return true
- end
-
- # Obsolete: use +subtype+ instead. Calling this will
- # generate a warning message to +stderr+, then return
- # the value of +subtype+.
- def media_subtype
- $stderr.printf("warning: media_subtype is obsolete.\n")
- $stderr.printf(" use subtype instead.\n")
- return subtype
- end
- end
-
- class ResponseParser # :nodoc:
- def parse(str)
- @str = str
- @pos = 0
- @lex_state = EXPR_BEG
- @token = nil
- return response
- end
-
- private
-
- EXPR_BEG = :EXPR_BEG
- EXPR_DATA = :EXPR_DATA
- EXPR_TEXT = :EXPR_TEXT
- EXPR_RTEXT = :EXPR_RTEXT
- EXPR_CTEXT = :EXPR_CTEXT
-
- T_SPACE = :SPACE
- T_NIL = :NIL
- T_NUMBER = :NUMBER
- T_ATOM = :ATOM
- T_QUOTED = :QUOTED
- T_LPAR = :LPAR
- T_RPAR = :RPAR
- T_BSLASH = :BSLASH
- T_STAR = :STAR
- T_LBRA = :LBRA
- T_RBRA = :RBRA
- T_LITERAL = :LITERAL
- T_PLUS = :PLUS
- T_PERCENT = :PERCENT
- T_CRLF = :CRLF
- T_EOF = :EOF
- T_TEXT = :TEXT
-
- BEG_REGEXP = /\G(?:\
-(?# 1: SPACE )( +)|\
-(?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
-(?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
-(?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\
-(?# 5: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
-(?# 6: LPAR )(\()|\
-(?# 7: RPAR )(\))|\
-(?# 8: BSLASH )(\\)|\
-(?# 9: STAR )(\*)|\
-(?# 10: LBRA )(\[)|\
-(?# 11: RBRA )(\])|\
-(?# 12: LITERAL )\{(\d+)\}\r\n|\
-(?# 13: PLUS )(\+)|\
-(?# 14: PERCENT )(%)|\
-(?# 15: CRLF )(\r\n)|\
-(?# 16: EOF )(\z))/ni
-
- DATA_REGEXP = /\G(?:\
-(?# 1: SPACE )( )|\
-(?# 2: NIL )(NIL)|\
-(?# 3: NUMBER )(\d+)|\
-(?# 4: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
-(?# 5: LITERAL )\{(\d+)\}\r\n|\
-(?# 6: LPAR )(\()|\
-(?# 7: RPAR )(\)))/ni
-
- TEXT_REGEXP = /\G(?:\
-(?# 1: TEXT )([^\x00\r\n]*))/ni
-
- RTEXT_REGEXP = /\G(?:\
-(?# 1: LBRA )(\[)|\
-(?# 2: TEXT )([^\x00\r\n]*))/ni
-
- CTEXT_REGEXP = /\G(?:\
-(?# 1: TEXT )([^\x00\r\n\]]*))/ni
-
- Token = Struct.new(:symbol, :value)
-
- def response
- token = lookahead
- case token.symbol
- when T_PLUS
- result = continue_req
- when T_STAR
- result = response_untagged
- else
- result = response_tagged
- end
- match(T_CRLF)
- match(T_EOF)
- return result
- end
-
- def continue_req
- match(T_PLUS)
- match(T_SPACE)
- return ContinuationRequest.new(resp_text, @str)
- end
-
- def response_untagged
- match(T_STAR)
- match(T_SPACE)
- token = lookahead
- if token.symbol == T_NUMBER
- return numeric_response
- elsif token.symbol == T_ATOM
- case token.value
- when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
- return response_cond
- when /\A(?:FLAGS)\z/ni
- return flags_response
- when /\A(?:LIST|LSUB)\z/ni
- return list_response
- when /\A(?:QUOTA)\z/ni
- return getquota_response
- when /\A(?:QUOTAROOT)\z/ni
- return getquotaroot_response
- when /\A(?:ACL)\z/ni
- return getacl_response
- when /\A(?:SEARCH|SORT)\z/ni
- return search_response
- when /\A(?:THREAD)\z/ni
- return thread_response
- when /\A(?:STATUS)\z/ni
- return status_response
- when /\A(?:CAPABILITY)\z/ni
- return capability_response
- else
- return text_response
- end
- else
- parse_error("unexpected token %s", token.symbol)
- end
- end
-
- def response_tagged
- tag = atom
- match(T_SPACE)
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- return TaggedResponse.new(tag, name, resp_text, @str)
- end
-
- def response_cond
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- return UntaggedResponse.new(name, resp_text, @str)
- end
-
- def numeric_response
- n = number
- match(T_SPACE)
- token = match(T_ATOM)
- name = token.value.upcase
- case name
- when "EXISTS", "RECENT", "EXPUNGE"
- return UntaggedResponse.new(name, n, @str)
- when "FETCH"
- shift_token
- match(T_SPACE)
- data = FetchData.new(n, msg_att)
- return UntaggedResponse.new(name, data, @str)
- end
- end
-
- def msg_att
- match(T_LPAR)
- attr = {}
- while true
- token = lookahead
- case token.symbol
- when T_RPAR
- shift_token
- break
- when T_SPACE
- shift_token
- token = lookahead
- end
- case token.value
- when /\A(?:ENVELOPE)\z/ni
- name, val = envelope_data
- when /\A(?:FLAGS)\z/ni
- name, val = flags_data
- when /\A(?:INTERNALDATE)\z/ni
- name, val = internaldate_data
- when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
- name, val = rfc822_text
- when /\A(?:RFC822\.SIZE)\z/ni
- name, val = rfc822_size
- when /\A(?:BODY(?:STRUCTURE)?)\z/ni
- name, val = body_data
- when /\A(?:UID)\z/ni
- name, val = uid_data
- else
- parse_error("unknown attribute `%s'", token.value)
- end
- attr[name] = val
- end
- return attr
- end
-
- def envelope_data
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- return name, envelope
- end
-
- def envelope
- @lex_state = EXPR_DATA
- token = lookahead
- if token.symbol == T_NIL
- shift_token
- result = nil
- else
- match(T_LPAR)
- date = nstring
- match(T_SPACE)
- subject = nstring
- match(T_SPACE)
- from = address_list
- match(T_SPACE)
- sender = address_list
- match(T_SPACE)
- reply_to = address_list
- match(T_SPACE)
- to = address_list
- match(T_SPACE)
- cc = address_list
- match(T_SPACE)
- bcc = address_list
- match(T_SPACE)
- in_reply_to = nstring
- match(T_SPACE)
- message_id = nstring
- match(T_RPAR)
- result = Envelope.new(date, subject, from, sender, reply_to,
- to, cc, bcc, in_reply_to, message_id)
- end
- @lex_state = EXPR_BEG
- return result
- end
-
- def flags_data
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- return name, flag_list
- end
-
- def internaldate_data
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- token = match(T_QUOTED)
- return name, token.value
- end
-
- def rfc822_text
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- return name, nstring
- end
-
- def rfc822_size
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- return name, number
- end
-
- def body_data
- token = match(T_ATOM)
- name = token.value.upcase
- token = lookahead
- if token.symbol == T_SPACE
- shift_token
- return name, body
- end
- name.concat(section)
- token = lookahead
- if token.symbol == T_ATOM
- name.concat(token.value)
- shift_token
- end
- match(T_SPACE)
- data = nstring
- return name, data
- end
-
- def body
- @lex_state = EXPR_DATA
- token = lookahead
- if token.symbol == T_NIL
- shift_token
- result = nil
- else
- match(T_LPAR)
- token = lookahead
- if token.symbol == T_LPAR
- result = body_type_mpart
- else
- result = body_type_1part
- end
- match(T_RPAR)
- end
- @lex_state = EXPR_BEG
- return result
- end
-
- def body_type_1part
- token = lookahead
- case token.value
- when /\A(?:TEXT)\z/ni
- return body_type_text
- when /\A(?:MESSAGE)\z/ni
- return body_type_msg
- else
- return body_type_basic
- end
- end
-
- def body_type_basic
- mtype, msubtype = media_type
- token = lookahead
- if token.symbol == T_RPAR
- return BodyTypeBasic.new(mtype, msubtype)
- end
- match(T_SPACE)
- param, content_id, desc, enc, size = body_fields
- md5, disposition, language, extension = body_ext_1part
- return BodyTypeBasic.new(mtype, msubtype,
- param, content_id,
- desc, enc, size,
- md5, disposition, language, extension)
- end
-
- def body_type_text
- mtype, msubtype = media_type
- match(T_SPACE)
- param, content_id, desc, enc, size = body_fields
- match(T_SPACE)
- lines = number
- md5, disposition, language, extension = body_ext_1part
- return BodyTypeText.new(mtype, msubtype,
- param, content_id,
- desc, enc, size,
- lines,
- md5, disposition, language, extension)
- end
-
- def body_type_msg
- mtype, msubtype = media_type
- match(T_SPACE)
- param, content_id, desc, enc, size = body_fields
- match(T_SPACE)
- env = envelope
- match(T_SPACE)
- b = body
- match(T_SPACE)
- lines = number
- md5, disposition, language, extension = body_ext_1part
- return BodyTypeMessage.new(mtype, msubtype,
- param, content_id,
- desc, enc, size,
- env, b, lines,
- md5, disposition, language, extension)
- end
-
- def body_type_mpart
- parts = []
- while true
- token = lookahead
- if token.symbol == T_SPACE
- shift_token
- break
- end
- parts.push(body)
- end
- mtype = "MULTIPART"
- msubtype = case_insensitive_string
- param, disposition, language, extension = body_ext_mpart
- return BodyTypeMultipart.new(mtype, msubtype, parts,
- param, disposition, language,
- extension)
- end
-
- def media_type
- mtype = case_insensitive_string
- match(T_SPACE)
- msubtype = case_insensitive_string
- return mtype, msubtype
- end
-
- def body_fields
- param = body_fld_param
- match(T_SPACE)
- content_id = nstring
- match(T_SPACE)
- desc = nstring
- match(T_SPACE)
- enc = case_insensitive_string
- match(T_SPACE)
- size = number
- return param, content_id, desc, enc, size
- end
-
- def body_fld_param
- token = lookahead
- if token.symbol == T_NIL
- shift_token
- return nil
- end
- match(T_LPAR)
- param = {}
- while true
- token = lookahead
- case token.symbol
- when T_RPAR
- shift_token
- break
- when T_SPACE
- shift_token
- end
- name = case_insensitive_string
- match(T_SPACE)
- val = string
- param[name] = val
- end
- return param
- end
-
- def body_ext_1part
- token = lookahead
- if token.symbol == T_SPACE
- shift_token
- else
- return nil
- end
- md5 = nstring
-
- token = lookahead
- if token.symbol == T_SPACE
- shift_token
- else
- return md5
- end
- disposition = body_fld_dsp
-
- token = lookahead
- if token.symbol == T_SPACE
- shift_token
- else
- return md5, disposition
- end
- language = body_fld_lang
-
- token = lookahead
- if token.symbol == T_SPACE
- shift_token
- else
- return md5, disposition, language
- end
-
- extension = body_extensions
- return md5, disposition, language, extension
- end
-
- def body_ext_mpart
- token = lookahead
- if token.symbol == T_SPACE
- shift_token
- else
- return nil
- end
- param = body_fld_param
-
- token = lookahead
- if token.symbol == T_SPACE
- shift_token
- else
- return param
- end
- disposition = body_fld_dsp
- match(T_SPACE)
- language = body_fld_lang
-
- token = lookahead
- if token.symbol == T_SPACE
- shift_token
- else
- return param, disposition, language
- end
-
- extension = body_extensions
- return param, disposition, language, extension
- end
-
- def body_fld_dsp
- token = lookahead
- if token.symbol == T_NIL
- shift_token
- return nil
- end
- match(T_LPAR)
- dsp_type = case_insensitive_string
- match(T_SPACE)
- param = body_fld_param
- match(T_RPAR)
- return ContentDisposition.new(dsp_type, param)
- end
-
- def body_fld_lang
- token = lookahead
- if token.symbol == T_LPAR
- shift_token
- result = []
- while true
- token = lookahead
- case token.symbol
- when T_RPAR
- shift_token
- return result
- when T_SPACE
- shift_token
- end
- result.push(case_insensitive_string)
- end
- else
- lang = nstring
- if lang
- return lang.upcase
- else
- return lang
- end
- end
- end
-
- def body_extensions
- result = []
- while true
- token = lookahead
- case token.symbol
- when T_RPAR
- return result
- when T_SPACE
- shift_token
- end
- result.push(body_extension)
- end
- end
-
- def body_extension
- token = lookahead
- case token.symbol
- when T_LPAR
- shift_token
- result = body_extensions
- match(T_RPAR)
- return result
- when T_NUMBER
- return number
- else
- return nstring
- end
- end
-
- def section
- str = ""
- token = match(T_LBRA)
- str.concat(token.value)
- token = match(T_ATOM, T_NUMBER, T_RBRA)
- if token.symbol == T_RBRA
- str.concat(token.value)
- return str
- end
- str.concat(token.value)
- token = lookahead
- if token.symbol == T_SPACE
- shift_token
- str.concat(token.value)
- token = match(T_LPAR)
- str.concat(token.value)
- while true
- token = lookahead
- case token.symbol
- when T_RPAR
- str.concat(token.value)
- shift_token
- break
- when T_SPACE
- shift_token
- str.concat(token.value)
- end
- str.concat(format_string(astring))
- end
- end
- token = match(T_RBRA)
- str.concat(token.value)
- return str
- end
-
- def format_string(str)
- case str
- when ""
- return '""'
- when /[\x80-\xff\r\n]/n
- # literal
- return "{" + str.length.to_s + "}" + CRLF + str
- when /[(){ \x00-\x1f\x7f%*"\\]/n
- # quoted string
- return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
- else
- # atom
- return str
- end
- end
-
- def uid_data
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- return name, number
- end
-
- def text_response
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- @lex_state = EXPR_TEXT
- token = match(T_TEXT)
- @lex_state = EXPR_BEG
- return UntaggedResponse.new(name, token.value)
- end
-
- def flags_response
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- return UntaggedResponse.new(name, flag_list, @str)
- end
-
- def list_response
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- return UntaggedResponse.new(name, mailbox_list, @str)
- end
-
- def mailbox_list
- attr = flag_list
- match(T_SPACE)
- token = match(T_QUOTED, T_NIL)
- if token.symbol == T_NIL
- delim = nil
- else
- delim = token.value
- end
- match(T_SPACE)
- name = astring
- return MailboxList.new(attr, delim, name)
- end
-
- def getquota_response
- # If quota never established, get back
- # `NO Quota root does not exist'.
- # If quota removed, get `()' after the
- # folder spec with no mention of `STORAGE'.
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- mailbox = astring
- match(T_SPACE)
- match(T_LPAR)
- token = lookahead
- case token.symbol
- when T_RPAR
- shift_token
- data = MailboxQuota.new(mailbox, nil, nil)
- return UntaggedResponse.new(name, data, @str)
- when T_ATOM
- shift_token
- match(T_SPACE)
- token = match(T_NUMBER)
- usage = token.value
- match(T_SPACE)
- token = match(T_NUMBER)
- quota = token.value
- match(T_RPAR)
- data = MailboxQuota.new(mailbox, usage, quota)
- return UntaggedResponse.new(name, data, @str)
- else
- parse_error("unexpected token %s", token.symbol)
- end
- end
-
- def getquotaroot_response
- # Similar to getquota, but only admin can use getquota.
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- mailbox = astring
- quotaroots = []
- while true
- token = lookahead
- break unless token.symbol == T_SPACE
- shift_token
- quotaroots.push(astring)
- end
- data = MailboxQuotaRoot.new(mailbox, quotaroots)
- return UntaggedResponse.new(name, data, @str)
- end
-
- def getacl_response
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- mailbox = astring
- data = []
- token = lookahead
- if token.symbol == T_SPACE
- shift_token
- while true
- token = lookahead
- case token.symbol
- when T_CRLF
- break
- when T_SPACE
- shift_token
- end
- user = astring
- match(T_SPACE)
- rights = astring
- ##XXX data.push([user, rights])
- data.push(MailboxACLItem.new(user, rights))
- end
- end
- return UntaggedResponse.new(name, data, @str)
- end
-
- def search_response
- token = match(T_ATOM)
- name = token.value.upcase
- token = lookahead
- if token.symbol == T_SPACE
- shift_token
- data = []
- while true
- token = lookahead
- case token.symbol
- when T_CRLF
- break
- when T_SPACE
- shift_token
- end
- data.push(number)
- end
- else
- data = []
- end
- return UntaggedResponse.new(name, data, @str)
- end
-
- def thread_response
- token = match(T_ATOM)
- name = token.value.upcase
- token = lookahead
-
- if token.symbol == T_SPACE
- threads = []
-
- while true
- shift_token
- token = lookahead
-
- case token.symbol
- when T_LPAR
- threads << thread_branch(token)
- when T_CRLF
- break
- end
- end
- else
- # no member
- threads = []
- end
-
- return UntaggedResponse.new(name, threads, @str)
- end
-
- def thread_branch(token)
- rootmember = nil
- lastmember = nil
-
- while true
- shift_token # ignore first T_LPAR
- token = lookahead
-
- case token.symbol
- when T_NUMBER
- # new member
- newmember = ThreadMember.new(number, [])
- if rootmember.nil?
- rootmember = newmember
- else
- lastmember.children << newmember
- end
- lastmember = newmember
- when T_SPACE
- # do nothing
- when T_LPAR
- if rootmember.nil?
- # dummy member
- lastmember = rootmember = ThreadMember.new(nil, [])
- end
-
- lastmember.children << thread_branch(token)
- when T_RPAR
- break
- end
- end
-
- return rootmember
- end
-
- def status_response
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- mailbox = astring
- match(T_SPACE)
- match(T_LPAR)
- attr = {}
- while true
- token = lookahead
- case token.symbol
- when T_RPAR
- shift_token
- break
- when T_SPACE
- shift_token
- end
- token = match(T_ATOM)
- key = token.value.upcase
- match(T_SPACE)
- val = number
- attr[key] = val
- end
- data = StatusData.new(mailbox, attr)
- return UntaggedResponse.new(name, data, @str)
- end
-
- def capability_response
- token = match(T_ATOM)
- name = token.value.upcase
- match(T_SPACE)
- data = []
- while true
- token = lookahead
- case token.symbol
- when T_CRLF
- break
- when T_SPACE
- shift_token
- end
- data.push(atom.upcase)
- end
- return UntaggedResponse.new(name, data, @str)
- end
-
- def resp_text
- @lex_state = EXPR_RTEXT
- token = lookahead
- if token.symbol == T_LBRA
- code = resp_text_code
- else
- code = nil
- end
- token = match(T_TEXT)
- @lex_state = EXPR_BEG
- return ResponseText.new(code, token.value)
- end
-
- def resp_text_code
- @lex_state = EXPR_BEG
- match(T_LBRA)
- token = match(T_ATOM)
- name = token.value.upcase
- case name
- when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n
- result = ResponseCode.new(name, nil)
- when /\A(?:PERMANENTFLAGS)\z/n
- match(T_SPACE)
- result = ResponseCode.new(name, flag_list)
- when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
- match(T_SPACE)
- result = ResponseCode.new(name, number)
- else
- match(T_SPACE)
- @lex_state = EXPR_CTEXT
- token = match(T_TEXT)
- @lex_state = EXPR_BEG
- result = ResponseCode.new(name, token.value)
- end
- match(T_RBRA)
- @lex_state = EXPR_RTEXT
- return result
- end
-
- def address_list
- token = lookahead
- if token.symbol == T_NIL
- shift_token
- return nil
- else
- result = []
- match(T_LPAR)
- while true
- token = lookahead
- case token.symbol
- when T_RPAR
- shift_token
- break
- when T_SPACE
- shift_token
- end
- result.push(address)
- end
- return result
- end
- end
-
- ADDRESS_REGEXP = /\G\
-(?# 1: NAME )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
-(?# 2: ROUTE )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
-(?# 3: MAILBOX )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
-(?# 4: HOST )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\
-\)/ni
-
- def address
- match(T_LPAR)
- if @str.index(ADDRESS_REGEXP, @pos)
- # address does not include literal.
- @pos = $~.end(0)
- name = $1
- route = $2
- mailbox = $3
- host = $4
- for s in [name, route, mailbox, host]
- if s
- s.gsub!(/\\(["\\])/n, "\\1")
- end
- end
- else
- name = nstring
- match(T_SPACE)
- route = nstring
- match(T_SPACE)
- mailbox = nstring
- match(T_SPACE)
- host = nstring
- match(T_RPAR)
- end
- return Address.new(name, route, mailbox, host)
- end
-
-# def flag_list
-# result = []
-# match(T_LPAR)
-# while true
-# token = lookahead
-# case token.symbol
-# when T_RPAR
-# shift_token
-# break
-# when T_SPACE
-# shift_token
-# end
-# result.push(flag)
-# end
-# return result
-# end
-
-# def flag
-# token = lookahead
-# if token.symbol == T_BSLASH
-# shift_token
-# token = lookahead
-# if token.symbol == T_STAR
-# shift_token
-# return token.value.intern
-# else
-# return atom.intern
-# end
-# else
-# return atom
-# end
-# end
-
- FLAG_REGEXP = /\
-(?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
-(?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
-
- def flag_list
- if @str.index(/\(([^)]*)\)/ni, @pos)
- @pos = $~.end(0)
- return $1.scan(FLAG_REGEXP).collect { |flag, atom|
- atom || flag.capitalize.intern
- }
- else
- parse_error("invalid flag list")
- end
- end
-
- def nstring
- token = lookahead
- if token.symbol == T_NIL
- shift_token
- return nil
- else
- return string
- end
- end
-
- def astring
- token = lookahead
- if string_token?(token)
- return string
- else
- return atom
- end
- end
-
- def string
- token = lookahead
- if token.symbol == T_NIL
- shift_token
- return nil
- end
- token = match(T_QUOTED, T_LITERAL)
- return token.value
- end
-
- STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL]
-
- def string_token?(token)
- return STRING_TOKENS.include?(token.symbol)
- end
-
- def case_insensitive_string
- token = lookahead
- if token.symbol == T_NIL
- shift_token
- return nil
- end
- token = match(T_QUOTED, T_LITERAL)
- return token.value.upcase
- end
-
- def atom
- result = ""
- while true
- token = lookahead
- if atom_token?(token)
- result.concat(token.value)
- shift_token
- else
- if result.empty?
- parse_error("unexpected token %s", token.symbol)
- else
- return result
- end
- end
- end
- end
-
- ATOM_TOKENS = [
- T_ATOM,
- T_NUMBER,
- T_NIL,
- T_LBRA,
- T_RBRA,
- T_PLUS
- ]
-
- def atom_token?(token)
- return ATOM_TOKENS.include?(token.symbol)
- end
-
- def number
- token = lookahead
- if token.symbol == T_NIL
- shift_token
- return nil
- end
- token = match(T_NUMBER)
- return token.value.to_i
- end
-
- def nil_atom
- match(T_NIL)
- return nil
- end
-
- def match(*args)
- token = lookahead
- unless args.include?(token.symbol)
- parse_error('unexpected token %s (expected %s)',
- token.symbol.id2name,
- args.collect {|i| i.id2name}.join(" or "))
- end
- shift_token
- return token
- end
-
- def lookahead
- unless @token
- @token = next_token
- end
- return @token
- end
-
- def shift_token
- @token = nil
- end
-
- def next_token
- case @lex_state
- when EXPR_BEG
- if @str.index(BEG_REGEXP, @pos)
- @pos = $~.end(0)
- if $1
- return Token.new(T_SPACE, $+)
- elsif $2
- return Token.new(T_NIL, $+)
- elsif $3
- return Token.new(T_NUMBER, $+)
- elsif $4
- return Token.new(T_ATOM, $+)
- elsif $5
- return Token.new(T_QUOTED,
- $+.gsub(/\\(["\\])/n, "\\1"))
- elsif $6
- return Token.new(T_LPAR, $+)
- elsif $7
- return Token.new(T_RPAR, $+)
- elsif $8
- return Token.new(T_BSLASH, $+)
- elsif $9
- return Token.new(T_STAR, $+)
- elsif $10
- return Token.new(T_LBRA, $+)
- elsif $11
- return Token.new(T_RBRA, $+)
- elsif $12
- len = $+.to_i
- val = @str[@pos, len]
- @pos += len
- return Token.new(T_LITERAL, val)
- elsif $13
- return Token.new(T_PLUS, $+)
- elsif $14
- return Token.new(T_PERCENT, $+)
- elsif $15
- return Token.new(T_CRLF, $+)
- elsif $16
- return Token.new(T_EOF, $+)
- else
- parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
- end
- else
- @str.index(/\S*/n, @pos)
- parse_error("unknown token - %s", $&.dump)
- end
- when EXPR_DATA
- if @str.index(DATA_REGEXP, @pos)
- @pos = $~.end(0)
- if $1
- return Token.new(T_SPACE, $+)
- elsif $2
- return Token.new(T_NIL, $+)
- elsif $3
- return Token.new(T_NUMBER, $+)
- elsif $4
- return Token.new(T_QUOTED,
- $+.gsub(/\\(["\\])/n, "\\1"))
- elsif $5
- len = $+.to_i
- val = @str[@pos, len]
- @pos += len
- return Token.new(T_LITERAL, val)
- elsif $6
- return Token.new(T_LPAR, $+)
- elsif $7
- return Token.new(T_RPAR, $+)
- else
- parse_error("[Net::IMAP BUG] DATA_REGEXP is invalid")
- end
- else
- @str.index(/\S*/n, @pos)
- parse_error("unknown token - %s", $&.dump)
- end
- when EXPR_TEXT
- if @str.index(TEXT_REGEXP, @pos)
- @pos = $~.end(0)
- if $1
- return Token.new(T_TEXT, $+)
- else
- parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid")
- end
- else
- @str.index(/\S*/n, @pos)
- parse_error("unknown token - %s", $&.dump)
- end
- when EXPR_RTEXT
- if @str.index(RTEXT_REGEXP, @pos)
- @pos = $~.end(0)
- if $1
- return Token.new(T_LBRA, $+)
- elsif $2
- return Token.new(T_TEXT, $+)
- else
- parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid")
- end
- else
- @str.index(/\S*/n, @pos)
- parse_error("unknown token - %s", $&.dump)
- end
- when EXPR_CTEXT
- if @str.index(CTEXT_REGEXP, @pos)
- @pos = $~.end(0)
- if $1
- return Token.new(T_TEXT, $+)
- else
- parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid")
- end
- else
- @str.index(/\S*/n, @pos) #/
- parse_error("unknown token - %s", $&.dump)
- end
- else
- parse_error("invalid @lex_state - %s", @lex_state.inspect)
- end
- end
-
- def parse_error(fmt, *args)
- if IMAP.debug
- $stderr.printf("@str: %s\n", @str.dump)
- $stderr.printf("@pos: %d\n", @pos)
- $stderr.printf("@lex_state: %s\n", @lex_state)
- if @token
- $stderr.printf("@token.symbol: %s\n", @token.symbol)
- $stderr.printf("@token.value: %s\n", @token.value.inspect)
- end
- end
- raise ResponseParseError, format(fmt, *args)
- end
- end
-
- # Authenticator for the "LOGIN" authentication type. See
- # #authenticate().
- class LoginAuthenticator
- def process(data)
- case @state
- when STATE_USER
- @state = STATE_PASSWORD
- return @user
- when STATE_PASSWORD
- return @password
- end
- end
-
- private
-
- STATE_USER = :USER
- STATE_PASSWORD = :PASSWORD
-
- def initialize(user, password)
- @user = user
- @password = password
- @state = STATE_USER
- end
- end
- add_authenticator "LOGIN", LoginAuthenticator
-
- # Authenticator for the "PLAIN" authentication type. See
- # #authenticate().
- class PlainAuthenticator
- def process(data)
- return "\0#{@user}\0#{@password}"
- end
-
- private
-
- def initialize(user, password)
- @user = user
- @password = password
- end
- end
- add_authenticator "PLAIN", PlainAuthenticator
-
- # Authenticator for the "CRAM-MD5" authentication type. See
- # #authenticate().
- class CramMD5Authenticator
- def process(challenge)
- digest = hmac_md5(challenge, @password)
- return @user + " " + digest
- end
-
- private
-
- def initialize(user, password)
- @user = user
- @password = password
- end
-
- def hmac_md5(text, key)
- if key.length > 64
- key = Digest::MD5.digest(key)
- end
-
- k_ipad = key + "\0" * (64 - key.length)
- k_opad = key + "\0" * (64 - key.length)
- for i in 0..63
- k_ipad[i] ^= 0x36
- k_opad[i] ^= 0x5c
- end
-
- digest = Digest::MD5.digest(k_ipad + text)
-
- return Digest::MD5.hexdigest(k_opad + digest)
- end
- end
- add_authenticator "CRAM-MD5", CramMD5Authenticator
-
- # Authenticator for the "DIGEST-MD5" authentication type. See
- # #authenticate().
- class DigestMD5Authenticator
- def process(challenge)
- case @stage
- when STAGE_ONE
- @stage = STAGE_TWO
- sparams = {}
- c = StringScanner.new(challenge)
- while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/)
- k, v = c[1], c[2]
- if v =~ /^"(.*)"$/
- v = $1
- if v =~ /,/
- v = v.split(',')
- end
- end
- sparams[k] = v
- end
-
- raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0
- raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
-
- response = {
- :nonce => sparams['nonce'],
- :username => @user,
- :realm => sparams['realm'],
- :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
- :'digest-uri' => 'imap/' + sparams['realm'],
- :qop => 'auth',
- :maxbuf => 65535,
- :nc => "%08d" % nc(sparams['nonce']),
- :charset => sparams['charset'],
- }
-
- response[:authzid] = @authname unless @authname.nil?
-
- # now, the real thing
- a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
-
- a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
- a1 << ':' + response[:authzid] unless response[:authzid].nil?
-
- a2 = "AUTHENTICATE:" + response[:'digest-uri']
- a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
-
- response[:response] = Digest::MD5.hexdigest(
- [
- Digest::MD5.hexdigest(a1),
- response.values_at(:nonce, :nc, :cnonce, :qop),
- Digest::MD5.hexdigest(a2)
- ].join(':')
- )
-
- return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
- when STAGE_TWO
- @stage = nil
- # if at the second stage, return an empty string
- if challenge =~ /rspauth=/
- return ''
- else
- raise ResponseParseError, challenge
- end
- else
- raise ResponseParseError, challenge
- end
- end
-
- def initialize(user, password, authname = nil)
- @user, @password, @authname = user, password, authname
- @nc, @stage = {}, STAGE_ONE
- end
-
- private
-
- STAGE_ONE = :stage_one
- STAGE_TWO = :stage_two
-
- def nc(nonce)
- if @nc.has_key? nonce
- @nc[nonce] = @nc[nonce] + 1
- else
- @nc[nonce] = 1
- end
- return @nc[nonce]
- end
-
- # some responses need quoting
- def qdval(k, v)
- return if k.nil? or v.nil?
- if %w"username authzid realm nonce cnonce digest-uri qop".include? k
- v.gsub!(/([\\"])/, "\\\1")
- return '%s="%s"' % [k, v]
- else
- return '%s=%s' % [k, v]
- end
- end
- end
- add_authenticator "DIGEST-MD5", DigestMD5Authenticator
-
- # Superclass of IMAP errors.
- class Error < StandardError
- end
-
- # Error raised when data is in the incorrect format.
- class DataFormatError < Error
- end
-
- # Error raised when a response from the server is non-parseable.
- class ResponseParseError < Error
- end
-
- # Superclass of all errors used to encapsulate "fail" responses
- # from the server.
- class ResponseError < Error
- end
-
- # Error raised upon a "NO" response from the server, indicating
- # that the client command could not be completed successfully.
- class NoResponseError < ResponseError
- end
-
- # Error raised upon a "BAD" response from the server, indicating
- # that the client command violated the IMAP protocol, or an internal
- # server failure has occurred.
- class BadResponseError < ResponseError
- end
-
- # Error raised upon a "BYE" response from the server, indicating
- # that the client is not being allowed to login, or has been timed
- # out due to inactivity.
- class ByeResponseError < ResponseError
- end
- end
-end
-
-if __FILE__ == $0
- # :enddoc:
- require "getoptlong"
-
- $stdout.sync = true
- $port = nil
- $user = ENV["USER"] || ENV["LOGNAME"]
- $auth = "login"
- $ssl = false
-
- def usage
- $stderr.print <<EOF
-usage: #{$0} [options] <host>
-
- --help print this message
- --port=PORT specifies port
- --user=USER specifies user
- --auth=AUTH specifies auth type
- --ssl use ssl
-EOF
- end
-
- def get_password
- print "password: "
- system("stty", "-echo")
- begin
- return gets.chop
- ensure
- system("stty", "echo")
- print "\n"
- end
- end
-
- def get_command
- printf("%s@%s> ", $user, $host)
- if line = gets
- return line.strip.split(/\s+/)
- else
- return nil
- end
- end
-
- parser = GetoptLong.new
- parser.set_options(['--debug', GetoptLong::NO_ARGUMENT],
- ['--help', GetoptLong::NO_ARGUMENT],
- ['--port', GetoptLong::REQUIRED_ARGUMENT],
- ['--user', GetoptLong::REQUIRED_ARGUMENT],
- ['--auth', GetoptLong::REQUIRED_ARGUMENT],
- ['--ssl', GetoptLong::NO_ARGUMENT])
- begin
- parser.each_option do |name, arg|
- case name
- when "--port"
- $port = arg
- when "--user"
- $user = arg
- when "--auth"
- $auth = arg
- when "--ssl"
- $ssl = true
- when "--debug"
- Net::IMAP.debug = true
- when "--help"
- usage
- exit(1)
- end
- end
- rescue
- usage
- exit(1)
- end
-
- $host = ARGV.shift
- unless $host
- usage
- exit(1)
- end
-
- imap = Net::IMAP.new($host, :port => $port, :ssl => $ssl)
- begin
- password = get_password
- imap.authenticate($auth, $user, password)
- while true
- cmd, *args = get_command
- break unless cmd
- begin
- case cmd
- when "list"
- for mbox in imap.list("", args[0] || "*")
- if mbox.attr.include?(Net::IMAP::NOSELECT)
- prefix = "!"
- elsif mbox.attr.include?(Net::IMAP::MARKED)
- prefix = "*"
- else
- prefix = " "
- end
- print prefix, mbox.name, "\n"
- end
- when "select"
- imap.select(args[0] || "inbox")
- print "ok\n"
- when "close"
- imap.close
- print "ok\n"
- when "summary"
- unless messages = imap.responses["EXISTS"][-1]
- puts "not selected"
- next
- end
- if messages > 0
- for data in imap.fetch(1..-1, ["ENVELOPE"])
- print data.seqno, ": ", data.attr["ENVELOPE"].subject, "\n"
- end
- else
- puts "no message"
- end
- when "fetch"
- if args[0]
- data = imap.fetch(args[0].to_i, ["RFC822.HEADER", "RFC822.TEXT"])[0]
- puts data.attr["RFC822.HEADER"]
- puts data.attr["RFC822.TEXT"]
- else
- puts "missing argument"
- end
- when "logout", "exit", "quit"
- break
- when "help", "?"
- print <<EOF
-list [pattern] list mailboxes
-select [mailbox] select mailbox
-close close mailbox
-summary display summary
-fetch [msgno] display message
-logout logout
-help, ? display help message
-EOF
- else
- print "unknown command: ", cmd, "\n"
- end
- rescue Net::IMAP::Error
- puts $!
- end
- end
- ensure
- imap.logout
- imap.disconnect
- end
-end
-
diff --git a/lib/net/net-protocol.gemspec b/lib/net/net-protocol.gemspec
new file mode 100644
index 0000000000..2d911a966c
--- /dev/null
+++ b/lib/net/net-protocol.gemspec
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-"), "..").join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Yukihiro Matsumoto"]
+ spec.email = ["matz@ruby-lang.org"]
+
+ spec.summary = %q{The abstract interface for net-* client.}
+ spec.description = %q{The abstract interface for net-* client.}
+ spec.homepage = "https://github.com/ruby/net-protocol"
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+ spec.metadata["changelog_uri"] = spec.homepage + "/releases"
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ excludes = %W[/.git* /bin /test /*file /#{File.basename(__FILE__)}]
+ spec.files = IO.popen(%W[git -C #{__dir__} ls-files -z --] + excludes.map {|e| ":^#{e}"}, &:read).split("\x0")
+ spec.require_paths = ["lib"]
+
+ spec.add_dependency "timeout"
+end
diff --git a/lib/net/pop.rb b/lib/net/pop.rb
deleted file mode 100644
index 7d234c191c..0000000000
--- a/lib/net/pop.rb
+++ /dev/null
@@ -1,1000 +0,0 @@
-# = net/pop.rb
-#
-# Copyright (c) 1999-2007 Yukihiro Matsumoto.
-#
-# Copyright (c) 1999-2007 Minero Aoki.
-#
-# Written & maintained by Minero Aoki <aamine@loveruby.net>.
-#
-# Documented by William Webber and Minero Aoki.
-#
-# This program is free software. You can re-distribute and/or
-# modify this program under the same terms as Ruby itself,
-# Ruby Distribute License.
-#
-# NOTE: You can find Japanese version of this document at:
-# http://www.ruby-lang.org/ja/man/html/net_pop.html
-#
-# $Id$
-#
-# See Net::POP3 for documentation.
-#
-
-require 'net/protocol'
-require 'digest/md5'
-require 'timeout'
-
-begin
- require "openssl/ssl"
-rescue LoadError
-end
-
-module Net
-
- # Non-authentication POP3 protocol error
- # (reply code "-ERR", except authentication).
- class POPError < ProtocolError; end
-
- # POP3 authentication error.
- class POPAuthenticationError < ProtoAuthError; end
-
- # Unexpected response from the server.
- class POPBadResponse < POPError; end
-
- #
- # = Net::POP3
- #
- # == What is This Library?
- #
- # This library provides functionality for retrieving
- # email via POP3, the Post Office Protocol version 3. For details
- # of POP3, see [RFC1939] (http://www.ietf.org/rfc/rfc1939.txt).
- #
- # == Examples
- #
- # === Retrieving Messages
- #
- # This example retrieves messages from the server and deletes them
- # on the server.
- #
- # Messages are written to files named 'inbox/1', 'inbox/2', ....
- # Replace 'pop.example.com' with your POP3 server address, and
- # 'YourAccount' and 'YourPassword' with the appropriate account
- # details.
- #
- # require 'net/pop'
- #
- # pop = Net::POP3.new('pop.example.com')
- # pop.start('YourAccount', 'YourPassword') # (1)
- # if pop.mails.empty?
- # puts 'No mail.'
- # else
- # i = 0
- # pop.each_mail do |m| # or "pop.mails.each ..." # (2)
- # File.open("inbox/#{i}", 'w') do |f|
- # f.write m.pop
- # end
- # m.delete
- # i += 1
- # end
- # puts "#{pop.mails.size} mails popped."
- # end
- # pop.finish # (3)
- #
- # 1. Call Net::POP3#start and start POP session.
- # 2. Access messages by using POP3#each_mail and/or POP3#mails.
- # 3. Close POP session by calling POP3#finish or use the block form of #start.
- #
- # === Shortened Code
- #
- # The example above is very verbose. You can shorten the code by using
- # some utility methods. First, the block form of Net::POP3.start can
- # be used instead of POP3.new, POP3#start and POP3#finish.
- #
- # require 'net/pop'
- #
- # Net::POP3.start('pop.example.com', 110,
- # 'YourAccount', 'YourPassword') do |pop|
- # if pop.mails.empty?
- # puts 'No mail.'
- # else
- # i = 0
- # pop.each_mail do |m| # or "pop.mails.each ..."
- # File.open("inbox/#{i}", 'w') do |f|
- # f.write m.pop
- # end
- # m.delete
- # i += 1
- # end
- # puts "#{pop.mails.size} mails popped."
- # end
- # end
- #
- # POP3#delete_all is an alternative for #each_mail and #delete.
- #
- # require 'net/pop'
- #
- # Net::POP3.start('pop.example.com', 110,
- # 'YourAccount', 'YourPassword') do |pop|
- # if pop.mails.empty?
- # puts 'No mail.'
- # else
- # i = 1
- # pop.delete_all do |m|
- # File.open("inbox/#{i}", 'w') do |f|
- # f.write m.pop
- # end
- # i += 1
- # end
- # end
- # end
- #
- # And here is an even shorter example.
- #
- # require 'net/pop'
- #
- # i = 0
- # Net::POP3.delete_all('pop.example.com', 110,
- # 'YourAccount', 'YourPassword') do |m|
- # File.open("inbox/#{i}", 'w') do |f|
- # f.write m.pop
- # end
- # i += 1
- # end
- #
- # === Memory Space Issues
- #
- # All the examples above get each message as one big string.
- # This example avoids this.
- #
- # require 'net/pop'
- #
- # i = 1
- # Net::POP3.delete_all('pop.example.com', 110,
- # 'YourAccount', 'YourPassword') do |m|
- # File.open("inbox/#{i}", 'w') do |f|
- # m.pop do |chunk| # get a message little by little.
- # f.write chunk
- # end
- # i += 1
- # end
- # end
- #
- # === Using APOP
- #
- # The net/pop library supports APOP authentication.
- # To use APOP, use the Net::APOP class instead of the Net::POP3 class.
- # You can use the utility method, Net::POP3.APOP(). For example:
- #
- # require 'net/pop'
- #
- # # Use APOP authentication if $isapop == true
- # pop = Net::POP3.APOP($is_apop).new('apop.example.com', 110)
- # pop.start(YourAccount', 'YourPassword') do |pop|
- # # Rest of the code is the same.
- # end
- #
- # === Fetch Only Selected Mail Using 'UIDL' POP Command
- #
- # If your POP server provides UIDL functionality,
- # you can grab only selected mails from the POP server.
- # e.g.
- #
- # def need_pop?( id )
- # # determine if we need pop this mail...
- # end
- #
- # Net::POP3.start('pop.example.com', 110,
- # 'Your account', 'Your password') do |pop|
- # pop.mails.select { |m| need_pop?(m.unique_id) }.each do |m|
- # do_something(m.pop)
- # end
- # end
- #
- # The POPMail#unique_id() method returns the unique-id of the message as a
- # String. Normally the unique-id is a hash of the message.
- #
- class POP3 < Protocol
-
- Revision = %q$Revision$.split[1]
-
- #
- # Class Parameters
- #
-
- def POP3.default_port
- default_pop3_port()
- end
-
- # The default port for POP3 connections, port 110
- def POP3.default_pop3_port
- 110
- end
-
- # The default port for POP3S connections, port 995
- def POP3.default_pop3s_port
- 995
- end
-
- def POP3.socket_type #:nodoc: obsolete
- Net::InternetMessageIO
- end
-
- #
- # Utilities
- #
-
- # Returns the APOP class if +isapop+ is true; otherwise, returns
- # the POP class. For example:
- #
- # # Example 1
- # pop = Net::POP3::APOP($is_apop).new(addr, port)
- #
- # # Example 2
- # Net::POP3::APOP($is_apop).start(addr, port) do |pop|
- # ....
- # end
- #
- def POP3.APOP(isapop)
- isapop ? APOP : POP3
- end
-
- # Starts a POP3 session and iterates over each POPMail object,
- # yielding it to the +block+.
- # This method is equivalent to:
- #
- # Net::POP3.start(address, port, account, password) do |pop|
- # pop.each_mail do |m|
- # yield m
- # end
- # end
- #
- # This method raises a POPAuthenticationError if authentication fails.
- #
- # === Example
- #
- # Net::POP3.foreach('pop.example.com', 110,
- # 'YourAccount', 'YourPassword') do |m|
- # file.write m.pop
- # m.delete if $DELETE
- # end
- #
- def POP3.foreach(address, port = nil,
- account = nil, password = nil,
- isapop = false, &block) # :yields: message
- start(address, port, account, password, isapop) {|pop|
- pop.each_mail(&block)
- }
- end
-
- # Starts a POP3 session and deletes all messages on the server.
- # If a block is given, each POPMail object is yielded to it before
- # being deleted.
- #
- # This method raises a POPAuthenticationError if authentication fails.
- #
- # === Example
- #
- # Net::POP3.delete_all('pop.example.com', 110,
- # 'YourAccount', 'YourPassword') do |m|
- # file.write m.pop
- # end
- #
- def POP3.delete_all(address, port = nil,
- account = nil, password = nil,
- isapop = false, &block)
- start(address, port, account, password, isapop) {|pop|
- pop.delete_all(&block)
- }
- end
-
- # Opens a POP3 session, attempts authentication, and quits.
- #
- # This method raises POPAuthenticationError if authentication fails.
- #
- # === Example: normal POP3
- #
- # Net::POP3.auth_only('pop.example.com', 110,
- # 'YourAccount', 'YourPassword')
- #
- # === Example: APOP
- #
- # Net::POP3.auth_only('pop.example.com', 110,
- # 'YourAccount', 'YourPassword', true)
- #
- def POP3.auth_only(address, port = nil,
- account = nil, password = nil,
- isapop = false)
- new(address, port, isapop).auth_only account, password
- end
-
- # Starts a pop3 session, attempts authentication, and quits.
- # This method must not be called while POP3 session is opened.
- # This method raises POPAuthenticationError if authentication fails.
- def auth_only(account, password)
- raise IOError, 'opening previously opened POP session' if started?
- start(account, password) {
- ;
- }
- end
-
- #
- # SSL
- #
-
- @ssl_params = nil
-
- # call-seq:
- # Net::POP.enable_ssl(params = {})
- #
- # Enable SSL for all new instances.
- # +params+ is passed to OpenSSL::SSLContext#set_params.
- def POP3.enable_ssl(*args)
- @ssl_params = create_ssl_params(*args)
- end
-
- def POP3.create_ssl_params(verify_or_params = {}, certs = nil)
- begin
- params = verify_or_params.to_hash
- rescue NoMethodError
- params = {}
- params[:verify_mode] = verify_or_params
- if certs
- if File.file?(certs)
- params[:ca_file] = certs
- elsif File.directory?(certs)
- params[:ca_path] = certs
- end
- end
- end
- return params
- end
-
- # Disable SSL for all new instances.
- def POP3.disable_ssl
- @ssl_params = nil
- end
-
- def POP3.ssl_params
- return @ssl_params
- end
-
- def POP3.use_ssl?
- return !@ssl_params.nil?
- end
-
- def POP3.verify
- return @ssl_params[:verify_mode]
- end
-
- def POP3.certs
- return @ssl_params[:ca_file] || @ssl_params[:ca_path]
- end
-
- #
- # Session management
- #
-
- # Creates a new POP3 object and open the connection. Equivalent to
- #
- # Net::POP3.new(address, port, isapop).start(account, password)
- #
- # If +block+ is provided, yields the newly-opened POP3 object to it,
- # and automatically closes it at the end of the session.
- #
- # === Example
- #
- # Net::POP3.start(addr, port, account, password) do |pop|
- # pop.each_mail do |m|
- # file.write m.pop
- # m.delete
- # end
- # end
- #
- def POP3.start(address, port = nil,
- account = nil, password = nil,
- isapop = false, &block) # :yield: pop
- new(address, port, isapop).start(account, password, &block)
- end
-
- # Creates a new POP3 object.
- #
- # +address+ is the hostname or ip address of your POP3 server.
- #
- # The optional +port+ is the port to connect to.
- #
- # The optional +isapop+ specifies whether this connection is going
- # to use APOP authentication; it defaults to +false+.
- #
- # This method does *not* open the TCP connection.
- def initialize(addr, port = nil, isapop = false)
- @address = addr
- @ssl_params = POP3.ssl_params
- @port = port
- @apop = isapop
-
- @command = nil
- @socket = nil
- @started = false
- @open_timeout = 30
- @read_timeout = 60
- @debug_output = nil
-
- @mails = nil
- @n_mails = nil
- @n_bytes = nil
- end
-
- # Does this instance use APOP authentication?
- def apop?
- @apop
- end
-
- # does this instance use SSL?
- def use_ssl?
- return !@ssl_params.nil?
- end
-
- # call-seq:
- # Net::POP#enable_ssl(params = {})
- #
- # Enables SSL for this instance. Must be called before the connection is
- # established to have any effect.
- # +params[:port]+ is port to establish the SSL connection on; Defaults to 995.
- # +params+ (except :port) is passed to OpenSSL::SSLContext#set_params.
- def enable_ssl(verify_or_params = {}, certs = nil, port = nil)
- begin
- @ssl_params = verify_or_params.to_hash.dup
- @port = @ssl_params.delete(:port) || @port
- rescue NoMethodError
- @ssl_params = POP3.create_ssl_params(verify_or_params, certs)
- @port = port || @port
- end
- end
-
- def disable_ssl
- @ssl_params = nil
- end
-
- # Provide human-readable stringification of class state.
- def inspect
- "#<#{self.class} #{@address}:#{@port} open=#{@started}>"
- end
-
- # *WARNING*: This method causes a serious security hole.
- # Use this method only for debugging.
- #
- # Set an output stream for debugging.
- #
- # === Example
- #
- # pop = Net::POP.new(addr, port)
- # pop.set_debug_output $stderr
- # pop.start(account, passwd) do |pop|
- # ....
- # end
- #
- def set_debug_output(arg)
- @debug_output = arg
- end
-
- # The address to connect to.
- attr_reader :address
-
- # The port number to connect to.
- def port
- return @port || (use_ssl? ? POP3.default_pop3s_port : POP3.default_pop3_port)
- end
-
- # Seconds to wait until a connection is opened.
- # If the POP3 object cannot open a connection within this time,
- # it raises a TimeoutError exception.
- attr_accessor :open_timeout
-
- # Seconds to wait until reading one block (by one read(1) call).
- # If the POP3 object cannot complete a read() within this time,
- # it raises a TimeoutError exception.
- attr_reader :read_timeout
-
- # Set the read timeout.
- def read_timeout=(sec)
- @command.socket.read_timeout = sec if @command
- @read_timeout = sec
- end
-
- # +true+ if the POP3 session has started.
- def started?
- @started
- end
-
- alias active? started? #:nodoc: obsolete
-
- # Starts a POP3 session.
- #
- # When called with block, gives a POP3 object to the block and
- # closes the session after block call finishes.
- #
- # This method raises a POPAuthenticationError if authentication fails.
- def start(account, password) # :yield: pop
- raise IOError, 'POP session already started' if @started
- if block_given?
- begin
- do_start account, password
- return yield(self)
- ensure
- do_finish
- end
- else
- do_start account, password
- return self
- end
- end
-
- def do_start(account, password)
- s = timeout(@open_timeout) { TCPSocket.open(@address, port) }
- if use_ssl?
- raise 'openssl library not installed' unless defined?(OpenSSL)
- context = OpenSSL::SSL::SSLContext.new
- context.set_params(@ssl_params)
- s = OpenSSL::SSL::SSLSocket.new(s, context)
- s.sync_close = true
- s.connect
- if context.verify_mode != OpenSSL::SSL::VERIFY_NONE
- s.post_connection_check(@address)
- end
- end
- @socket = InternetMessageIO.new(s)
- logging "POP session started: #{@address}:#{@port} (#{@apop ? 'APOP' : 'POP'})"
- @socket.read_timeout = @read_timeout
- @socket.debug_output = @debug_output
- on_connect
- @command = POP3Command.new(@socket)
- if apop?
- @command.apop account, password
- else
- @command.auth account, password
- end
- @started = true
- ensure
- # Authentication failed, clean up connection.
- unless @started
- s.close if s and not s.closed?
- @socket = nil
- @command = nil
- end
- end
- private :do_start
-
- def on_connect
- end
- private :on_connect
-
- # Finishes a POP3 session and closes TCP connection.
- def finish
- raise IOError, 'POP session not yet started' unless started?
- do_finish
- end
-
- def do_finish
- @mails = nil
- @n_mails = nil
- @n_bytes = nil
- @command.quit if @command
- ensure
- @started = false
- @command = nil
- @socket.close if @socket and not @socket.closed?
- @socket = nil
- end
- private :do_finish
-
- def command
- raise IOError, 'POP session not opened yet' \
- if not @socket or @socket.closed?
- @command
- end
- private :command
-
- #
- # POP protocol wrapper
- #
-
- # Returns the number of messages on the POP server.
- def n_mails
- return @n_mails if @n_mails
- @n_mails, @n_bytes = command().stat
- @n_mails
- end
-
- # Returns the total size in bytes of all the messages on the POP server.
- def n_bytes
- return @n_bytes if @n_bytes
- @n_mails, @n_bytes = command().stat
- @n_bytes
- end
-
- # Returns an array of Net::POPMail objects, representing all the
- # messages on the server. This array is renewed when the session
- # restarts; otherwise, it is fetched from the server the first time
- # this method is called (directly or indirectly) and cached.
- #
- # This method raises a POPError if an error occurs.
- def mails
- return @mails.dup if @mails
- if n_mails() == 0
- # some popd raises error for LIST on the empty mailbox.
- @mails = []
- return []
- end
-
- @mails = command().list.map {|num, size|
- POPMail.new(num, size, self, command())
- }
- @mails.dup
- end
-
- # Yields each message to the passed-in block in turn.
- # Equivalent to:
- #
- # pop3.mails.each do |popmail|
- # ....
- # end
- #
- # This method raises a POPError if an error occurs.
- def each_mail(&block) # :yield: message
- mails().each(&block)
- end
-
- alias each each_mail
-
- # Deletes all messages on the server.
- #
- # If called with a block, yields each message in turn before deleting it.
- #
- # === Example
- #
- # n = 1
- # pop.delete_all do |m|
- # File.open("inbox/#{n}") do |f|
- # f.write m.pop
- # end
- # n += 1
- # end
- #
- # This method raises a POPError if an error occurs.
- #
- def delete_all # :yield: message
- mails().each do |m|
- yield m if block_given?
- m.delete unless m.deleted?
- end
- end
-
- # Resets the session. This clears all "deleted" marks from messages.
- #
- # This method raises a POPError if an error occurs.
- def reset
- command().rset
- mails().each do |m|
- m.instance_eval {
- @deleted = false
- }
- end
- end
-
- def set_all_uids #:nodoc: internal use only (called from POPMail#uidl)
- uidl = command().uidl
- @mails.each {|m| m.uid = uidl[m.number] }
- end
-
- def logging(msg)
- @debug_output << msg + "\n" if @debug_output
- end
-
- end # class POP3
-
- # class aliases
- POP = POP3
- POPSession = POP3
- POP3Session = POP3
-
- #
- # This class is equivalent to POP3, except that it uses APOP authentication.
- #
- class APOP < POP3
- # Always returns true.
- def apop?
- true
- end
- end
-
- # class aliases
- APOPSession = APOP
-
- #
- # This class represents a message which exists on the POP server.
- # Instances of this class are created by the POP3 class; they should
- # not be directly created by the user.
- #
- class POPMail
-
- def initialize(num, len, pop, cmd) #:nodoc:
- @number = num
- @length = len
- @pop = pop
- @command = cmd
- @deleted = false
- @uid = nil
- end
-
- # The sequence number of the message on the server.
- attr_reader :number
-
- # The length of the message in octets.
- attr_reader :length
- alias size length
-
- # Provide human-readable stringification of class state.
- def inspect
- "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>"
- end
-
- #
- # This method fetches the message. If called with a block, the
- # message is yielded to the block one chunk at a time. If called
- # without a block, the message is returned as a String. The optional
- # +dest+ argument will be prepended to the returned String; this
- # argument is essentially obsolete.
- #
- # === Example without block
- #
- # POP3.start('pop.example.com', 110,
- # 'YourAccount, 'YourPassword') do |pop|
- # n = 1
- # pop.mails.each do |popmail|
- # File.open("inbox/#{n}", 'w') do |f|
- # f.write popmail.pop
- # end
- # popmail.delete
- # n += 1
- # end
- # end
- #
- # === Example with block
- #
- # POP3.start('pop.example.com', 110,
- # 'YourAccount, 'YourPassword') do |pop|
- # n = 1
- # pop.mails.each do |popmail|
- # File.open("inbox/#{n}", 'w') do |f|
- # popmail.pop do |chunk| ####
- # f.write chunk
- # end
- # end
- # n += 1
- # end
- # end
- #
- # This method raises a POPError if an error occurs.
- #
- def pop( dest = '', &block ) # :yield: message_chunk
- if block_given?
- @command.retr(@number, &block)
- nil
- else
- @command.retr(@number) do |chunk|
- dest << chunk
- end
- dest
- end
- end
-
- alias all pop #:nodoc: obsolete
- alias mail pop #:nodoc: obsolete
-
- # Fetches the message header and +lines+ lines of body.
- #
- # The optional +dest+ argument is obsolete.
- #
- # This method raises a POPError if an error occurs.
- def top(lines, dest = '')
- @command.top(@number, lines) do |chunk|
- dest << chunk
- end
- dest
- end
-
- # Fetches the message header.
- #
- # The optional +dest+ argument is obsolete.
- #
- # This method raises a POPError if an error occurs.
- def header(dest = '')
- top(0, dest)
- end
-
- # Marks a message for deletion on the server. Deletion does not
- # actually occur until the end of the session; deletion may be
- # cancelled for _all_ marked messages by calling POP3#reset().
- #
- # This method raises a POPError if an error occurs.
- #
- # === Example
- #
- # POP3.start('pop.example.com', 110,
- # 'YourAccount, 'YourPassword') do |pop|
- # n = 1
- # pop.mails.each do |popmail|
- # File.open("inbox/#{n}", 'w') do |f|
- # f.write popmail.pop
- # end
- # popmail.delete ####
- # n += 1
- # end
- # end
- #
- def delete
- @command.dele @number
- @deleted = true
- end
-
- alias delete! delete #:nodoc: obsolete
-
- # True if the mail has been deleted.
- def deleted?
- @deleted
- end
-
- # Returns the unique-id of the message.
- # Normally the unique-id is a hash string of the message.
- #
- # This method raises a POPError if an error occurs.
- def unique_id
- return @uid if @uid
- @pop.set_all_uids
- @uid
- end
-
- alias uidl unique_id
-
- def uid=(uid) #:nodoc: internal use only
- @uid = uid
- end
-
- end # class POPMail
-
-
- class POP3Command #:nodoc: internal use only
-
- def initialize(sock)
- @socket = sock
- @error_occured = false
- res = check_response(critical { recv_response() })
- @apop_stamp = res.slice(/<[!-~]+@[!-~]+>/)
- end
-
- attr_reader :socket
-
- def inspect
- "#<#{self.class} socket=#{@socket}>"
- end
-
- def auth(account, password)
- check_response_auth(critical {
- check_response_auth(get_response('USER %s', account))
- get_response('PASS %s', password)
- })
- end
-
- def apop(account, password)
- raise POPAuthenticationError, 'not APOP server; cannot login' \
- unless @apop_stamp
- check_response_auth(critical {
- get_response('APOP %s %s',
- account,
- Digest::MD5.hexdigest(@apop_stamp + password))
- })
- end
-
- def list
- critical {
- getok 'LIST'
- list = []
- @socket.each_list_item do |line|
- m = /\A(\d+)[ \t]+(\d+)/.match(line) or
- raise POPBadResponse, "bad response: #{line}"
- list.push [m[1].to_i, m[2].to_i]
- end
- return list
- }
- end
-
- def stat
- res = check_response(critical { get_response('STAT') })
- m = /\A\+OK\s+(\d+)\s+(\d+)/.match(res) or
- raise POPBadResponse, "wrong response format: #{res}"
- [m[1].to_i, m[2].to_i]
- end
-
- def rset
- check_response(critical { get_response('RSET') })
- end
-
- def top(num, lines = 0, &block)
- critical {
- getok('TOP %d %d', num, lines)
- @socket.each_message_chunk(&block)
- }
- end
-
- def retr(num, &block)
- critical {
- getok('RETR %d', num)
- @socket.each_message_chunk(&block)
- }
- end
-
- def dele(num)
- check_response(critical { get_response('DELE %d', num) })
- end
-
- def uidl(num = nil)
- if num
- res = check_response(critical { get_response('UIDL %d', num) })
- return res.split(/ /)[1]
- else
- critical {
- getok('UIDL')
- table = {}
- @socket.each_list_item do |line|
- num, uid = line.split
- table[num.to_i] = uid
- end
- return table
- }
- end
- end
-
- def quit
- check_response(critical { get_response('QUIT') })
- end
-
- private
-
- def getok(fmt, *fargs)
- @socket.writeline sprintf(fmt, *fargs)
- check_response(recv_response())
- end
-
- def get_response(fmt, *fargs)
- @socket.writeline sprintf(fmt, *fargs)
- recv_response()
- end
-
- def recv_response
- @socket.readline
- end
-
- def check_response(res)
- raise POPError, res unless /\A\+OK/i =~ res
- res
- end
-
- def check_response_auth(res)
- raise POPAuthenticationError, res unless /\A\+OK/i =~ res
- res
- end
-
- def critical
- return '+OK dummy ok response' if @error_occured
- begin
- return yield()
- rescue Exception
- @error_occured = true
- raise
- end
- end
-
- end # class POP3Command
-
-end # module Net
diff --git a/lib/net/protocol.rb b/lib/net/protocol.rb
index 0d489cdbc8..8c81298c0e 100644
--- a/lib/net/protocol.rb
+++ b/lib/net/protocol.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
#
# = net/protocol.rb
#
@@ -20,10 +21,13 @@
require 'socket'
require 'timeout'
+require 'io/wait'
module Net # :nodoc:
class Protocol #:nodoc: internal use only
+ VERSION = "0.2.2"
+
private
def Protocol.protocol_param(name, val)
module_eval(<<-End, __FILE__, __LINE__ + 1)
@@ -32,9 +36,38 @@ module Net # :nodoc:
end
End
end
+
+ def ssl_socket_connect(s, timeout)
+ if timeout
+ while true
+ raise Net::OpenTimeout if timeout <= 0
+ start = Process.clock_gettime Process::CLOCK_MONOTONIC
+ # to_io is required because SSLSocket doesn't have wait_readable yet
+ case s.connect_nonblock(exception: false)
+ when :wait_readable; s.to_io.wait_readable(timeout)
+ when :wait_writable; s.to_io.wait_writable(timeout)
+ else; break
+ end
+ timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
+ end
+ else
+ s.connect
+ end
+ end
+
+ tcp_socket_parameters = TCPSocket.instance_method(:initialize).parameters
+ TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT = if tcp_socket_parameters != [[:rest]]
+ tcp_socket_parameters.include?([:key, :open_timeout])
+ else
+ # Use Socket.tcp to find out since there is no parameters information for TCPSocket#initialize
+ # See discussion in https://github.com/ruby/net-http/pull/224
+ Socket.method(:tcp).parameters.include?([:key, :open_timeout])
+ end
+ private_constant :TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT
end
+ # :stopdoc:
class ProtocolError < StandardError; end
class ProtoSyntaxError < ProtocolError; end
class ProtoFatalError < ProtocolError; end
@@ -44,24 +77,81 @@ module Net # :nodoc:
class ProtoCommandError < ProtocolError; end
class ProtoRetriableError < ProtocolError; end
ProtocRetryError = ProtoRetriableError
+ # :startdoc:
+
+ ##
+ # OpenTimeout, a subclass of Timeout::Error, is raised if a connection cannot
+ # be created within the open_timeout.
+
+ class OpenTimeout < Timeout::Error; end
+
+ ##
+ # ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the
+ # response cannot be read within the read_timeout.
+
+ class ReadTimeout < Timeout::Error
+ # :stopdoc:
+ def initialize(io = nil)
+ @io = io
+ end
+ attr_reader :io
+
+ def message
+ msg = super
+ if @io
+ msg = "#{msg} with #{@io.inspect}"
+ end
+ msg
+ end
+ end
+
+ ##
+ # WriteTimeout, a subclass of Timeout::Error, is raised if a chunk of the
+ # response cannot be written within the write_timeout. Not raised on Windows.
+
+ class WriteTimeout < Timeout::Error
+ # :stopdoc:
+ def initialize(io = nil)
+ @io = io
+ end
+ attr_reader :io
+
+ def message
+ msg = super
+ if @io
+ msg = "#{msg} with #{@io.inspect}"
+ end
+ msg
+ end
+ end
class BufferedIO #:nodoc: internal use only
- def initialize(io)
+ def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil)
@io = io
- @read_timeout = 60
- @debug_output = nil
- @rbuf = ''
+ @read_timeout = read_timeout
+ @write_timeout = write_timeout
+ @continue_timeout = continue_timeout
+ @debug_output = debug_output
+ @rbuf = ''.b
+ @rbuf_empty = true
+ @rbuf_offset = 0
end
attr_reader :io
attr_accessor :read_timeout
+ attr_accessor :write_timeout
+ attr_accessor :continue_timeout
attr_accessor :debug_output
def inspect
"#<#{self.class} io=#{@io}>"
end
+ def eof?
+ @io.eof?
+ end
+
def closed?
@io.closed?
end
@@ -76,17 +166,20 @@ module Net # :nodoc:
public
- def read(len, dest = '', ignore_eof = false)
+ def read(len, dest = ''.b, ignore_eof = false)
LOG "reading #{len} bytes..."
read_bytes = 0
begin
- while read_bytes + @rbuf.size < len
- dest << (s = rbuf_consume(@rbuf.size))
- read_bytes += s.size
+ while read_bytes + rbuf_size < len
+ if s = rbuf_consume_all
+ read_bytes += s.bytesize
+ dest << s
+ end
rbuf_fill
end
- dest << (s = rbuf_consume(len - read_bytes))
- read_bytes += s.size
+ s = rbuf_consume(len - read_bytes)
+ read_bytes += s.bytesize
+ dest << s
rescue EOFError
raise unless ignore_eof
end
@@ -94,13 +187,15 @@ module Net # :nodoc:
dest
end
- def read_all(dest = '')
+ def read_all(dest = ''.b)
LOG 'reading all...'
read_bytes = 0
begin
while true
- dest << (s = rbuf_consume(@rbuf.size))
- read_bytes += s.size
+ if s = rbuf_consume_all
+ read_bytes += s.bytesize
+ dest << s
+ end
rbuf_fill
end
rescue EOFError
@@ -111,17 +206,19 @@ module Net # :nodoc:
end
def readuntil(terminator, ignore_eof = false)
+ offset = @rbuf_offset
begin
- until idx = @rbuf.index(terminator)
+ until idx = @rbuf.index(terminator, offset)
+ offset = @rbuf.bytesize
rbuf_fill
end
- return rbuf_consume(idx + terminator.size)
+ return rbuf_consume(idx + terminator.bytesize - @rbuf_offset)
rescue EOFError
raise unless ignore_eof
- return rbuf_consume(@rbuf.size)
+ return rbuf_consume
end
end
-
+
def readline
readuntil("\n").chop
end
@@ -131,13 +228,64 @@ module Net # :nodoc:
BUFSIZE = 1024 * 16
def rbuf_fill
- timeout(@read_timeout) {
- @rbuf << @io.sysread(BUFSIZE)
- }
+ tmp = @rbuf_empty ? @rbuf : nil
+ case rv = @io.read_nonblock(BUFSIZE, tmp, exception: false)
+ when String
+ @rbuf_empty = false
+ if rv.equal?(tmp)
+ @rbuf_offset = 0
+ else
+ @rbuf << rv
+ rv.clear
+ end
+ return
+ when :wait_readable
+ (io = @io.to_io).wait_readable(@read_timeout) or raise Net::ReadTimeout.new(io)
+ # continue looping
+ when :wait_writable
+ # OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable.
+ # http://www.openssl.org/support/faq.html#PROG10
+ (io = @io.to_io).wait_writable(@read_timeout) or raise Net::ReadTimeout.new(io)
+ # continue looping
+ when nil
+ raise EOFError, 'end of file reached'
+ end while true
+ end
+
+ def rbuf_flush
+ if @rbuf_empty
+ @rbuf.clear
+ @rbuf_offset = 0
+ end
+ nil
+ end
+
+ def rbuf_size
+ @rbuf.bytesize - @rbuf_offset
+ end
+
+ def rbuf_consume_all
+ rbuf_consume if rbuf_size > 0
end
- def rbuf_consume(len)
- s = @rbuf.slice!(0, len)
+ def rbuf_consume(len = nil)
+ if @rbuf_offset == 0 && (len.nil? || len == @rbuf.bytesize)
+ s = @rbuf
+ @rbuf = ''.b
+ @rbuf_offset = 0
+ @rbuf_empty = true
+ elsif len.nil?
+ s = @rbuf.byteslice(@rbuf_offset..-1)
+ @rbuf = ''.b
+ @rbuf_offset = 0
+ @rbuf_empty = true
+ else
+ s = @rbuf.byteslice(@rbuf_offset, len)
+ @rbuf_offset += len
+ @rbuf_empty = @rbuf_offset == @rbuf.bytesize
+ rbuf_flush
+ end
+
@debug_output << %Q[-> #{s.dump}\n] if @debug_output
s
end
@@ -148,12 +296,14 @@ module Net # :nodoc:
public
- def write(str)
+ def write(*strs)
writing {
- write0 str
+ write0(*strs)
}
end
+ alias << write
+
def writeline(str)
writing {
write0 str + "\r\n"
@@ -172,11 +322,34 @@ module Net # :nodoc:
bytes
end
- def write0(str)
- @debug_output << str.dump if @debug_output
- len = @io.write(str)
- @written_bytes += len
- len
+ def write0(*strs)
+ @debug_output << strs.map(&:dump).join if @debug_output
+ orig_written_bytes = @written_bytes
+ strs.each_with_index do |str, i|
+ need_retry = true
+ case len = @io.write_nonblock(str, exception: false)
+ when Integer
+ @written_bytes += len
+ len -= str.bytesize
+ if len == 0
+ if strs.size == i+1
+ return @written_bytes - orig_written_bytes
+ else
+ need_retry = false
+ # next string
+ end
+ elsif len < 0
+ str = str.byteslice(len, -len)
+ else # len > 0
+ need_retry = false
+ # next string
+ end
+ # continue looping
+ when :wait_writable
+ (io = @io.to_io).wait_writable(@write_timeout) or raise Net::WriteTimeout.new(io)
+ # continue looping
+ end while need_retry
+ end
end
#
@@ -202,7 +375,7 @@ module Net # :nodoc:
class InternetMessageIO < BufferedIO #:nodoc: internal use only
- def initialize(io)
+ def initialize(*, **)
super
@wbuf = nil
end
@@ -217,12 +390,12 @@ module Net # :nodoc:
read_bytes = 0
while (line = readuntil("\r\n")) != ".\r\n"
read_bytes += line.size
- yield line.sub(/\A\./, '')
+ yield line.delete_prefix('.')
end
LOG_on()
LOG "read message (#{read_bytes} bytes)"
end
-
+
# *library private* (cannot handle 'break')
def each_list_item
while (str = readuntil("\r\n")) != ".\r\n"
@@ -233,7 +406,7 @@ module Net # :nodoc:
def write_message_0(src)
prev = @written_bytes
each_crlf_line(src) do |line|
- write0 line.sub(/\A\./, '..')
+ write0 dot_stuff(line)
end
@written_bytes - prev
end
@@ -261,7 +434,7 @@ module Net # :nodoc:
len = writing {
using_each_crlf_line {
begin
- block.call(WriteAdapter.new(self, :write_message_0))
+ block.call(WriteAdapter.new(self.method(:write_message_0)))
rescue LocalJumpError
# allow `break' from writer block
end
@@ -274,11 +447,15 @@ module Net # :nodoc:
private
+ def dot_stuff(s)
+ s.sub(/\A\./, '..')
+ end
+
def using_each_crlf_line
- @wbuf = ''
+ @wbuf = ''.b
yield
if not @wbuf.empty? # unterminated last line
- write0 @wbuf.chomp + "\r\n"
+ write0 dot_stuff(@wbuf.chomp) + "\r\n"
elsif @written_bytes == 0 # empty src
write0 "\r\n"
end
@@ -288,7 +465,7 @@ module Net # :nodoc:
def each_crlf_line(src)
buffer_filling(@wbuf, src) do
- while line = @wbuf.slice!(/\A.*(?:\n|\r\n|\r(?!\z))/n)
+ while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/)
yield line.chomp("\n") + "\r\n"
end
end
@@ -321,17 +498,17 @@ module Net # :nodoc:
# The writer adapter class
#
class WriteAdapter
- def initialize(socket, method)
- @socket = socket
- @method_id = method
+ # :stopdoc:
+ def initialize(writer)
+ @writer = writer
end
def inspect
- "#<#{self.class} socket=#{@socket.inspect}>"
+ "#<#{self.class} writer=#{@writer.inspect}>"
end
def write(str)
- @socket.__send__(@method_id, str)
+ @writer.call(str)
end
alias print write
diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb
deleted file mode 100644
index b58e73029b..0000000000
--- a/lib/net/smtp.rb
+++ /dev/null
@@ -1,1014 +0,0 @@
-# = net/smtp.rb
-#
-# Copyright (c) 1999-2007 Yukihiro Matsumoto.
-#
-# Copyright (c) 1999-2007 Minero Aoki.
-#
-# Written & maintained by Minero Aoki <aamine@loveruby.net>.
-#
-# Documented by William Webber and Minero Aoki.
-#
-# This program is free software. You can re-distribute and/or
-# modify this program under the same terms as Ruby itself.
-#
-# NOTE: You can find Japanese version of this document at:
-# http://www.ruby-lang.org/ja/man/html/net_smtp.html
-#
-# $Id$
-#
-# See Net::SMTP for documentation.
-#
-
-require 'net/protocol'
-require 'digest/md5'
-require 'timeout'
-begin
- require 'openssl'
-rescue LoadError
-end
-
-module Net
-
- # Module mixed in to all SMTP error classes
- module SMTPError
- # This *class* is a module for backward compatibility.
- # In later release, this module becomes a class.
- end
-
- # Represents an SMTP authentication error.
- class SMTPAuthenticationError < ProtoAuthError
- include SMTPError
- end
-
- # Represents SMTP error code 420 or 450, a temporary error.
- class SMTPServerBusy < ProtoServerError
- include SMTPError
- end
-
- # Represents an SMTP command syntax error (error code 500)
- class SMTPSyntaxError < ProtoSyntaxError
- include SMTPError
- end
-
- # Represents a fatal SMTP error (error code 5xx, except for 500)
- class SMTPFatalError < ProtoFatalError
- include SMTPError
- end
-
- # Unexpected reply code returned from server.
- class SMTPUnknownError < ProtoUnknownError
- include SMTPError
- end
-
- # Command is not supported on server.
- class SMTPUnsupportedCommand < ProtocolError
- include SMTPError
- end
-
- #
- # = Net::SMTP
- #
- # == What is This Library?
- #
- # This library provides functionality to send internet
- # mail via SMTP, the Simple Mail Transfer Protocol. For details of
- # SMTP itself, see [RFC2821] (http://www.ietf.org/rfc/rfc2821.txt).
- #
- # == What is This Library NOT?
- #
- # This library does NOT provide functions to compose internet mails.
- # You must create them by yourself. If you want better mail support,
- # try RubyMail or TMail. You can get both libraries from RAA.
- # (http://www.ruby-lang.org/en/raa.html)
- #
- # FYI: the official documentation on internet mail is: [RFC2822] (http://www.ietf.org/rfc/rfc2822.txt).
- #
- # == Examples
- #
- # === Sending Messages
- #
- # You must open a connection to an SMTP server before sending messages.
- # The first argument is the address of your SMTP server, and the second
- # argument is the port number. Using SMTP.start with a block is the simplest
- # way to do this. This way, the SMTP connection is closed automatically
- # after the block is executed.
- #
- # require 'net/smtp'
- # Net::SMTP.start('your.smtp.server', 25) do |smtp|
- # # Use the SMTP object smtp only in this block.
- # end
- #
- # Replace 'your.smtp.server' with your SMTP server. Normally
- # your system manager or internet provider supplies a server
- # for you.
- #
- # Then you can send messages.
- #
- # msgstr = <<END_OF_MESSAGE
- # From: Your Name <your@mail.address>
- # To: Destination Address <someone@example.com>
- # Subject: test message
- # Date: Sat, 23 Jun 2001 16:26:43 +0900
- # Message-Id: <unique.message.id.string@example.com>
- #
- # This is a test message.
- # END_OF_MESSAGE
- #
- # require 'net/smtp'
- # Net::SMTP.start('your.smtp.server', 25) do |smtp|
- # smtp.send_message msgstr,
- # 'your@mail.address',
- # 'his_addess@example.com'
- # end
- #
- # === Closing the Session
- #
- # You MUST close the SMTP session after sending messages, by calling
- # the #finish method:
- #
- # # using SMTP#finish
- # smtp = Net::SMTP.start('your.smtp.server', 25)
- # smtp.send_message msgstr, 'from@address', 'to@address'
- # smtp.finish
- #
- # You can also use the block form of SMTP.start/SMTP#start. This closes
- # the SMTP session automatically:
- #
- # # using block form of SMTP.start
- # Net::SMTP.start('your.smtp.server', 25) do |smtp|
- # smtp.send_message msgstr, 'from@address', 'to@address'
- # end
- #
- # I strongly recommend this scheme. This form is simpler and more robust.
- #
- # === HELO domain
- #
- # In almost all situations, you must provide a third argument
- # to SMTP.start/SMTP#start. This is the domain name which you are on
- # (the host to send mail from). It is called the "HELO domain".
- # The SMTP server will judge whether it should send or reject
- # the SMTP session by inspecting the HELO domain.
- #
- # Net::SMTP.start('your.smtp.server', 25,
- # 'mail.from.domain') { |smtp| ... }
- #
- # === SMTP Authentication
- #
- # The Net::SMTP class supports three authentication schemes;
- # PLAIN, LOGIN and CRAM MD5. (SMTP Authentication: [RFC2554])
- # To use SMTP authentication, pass extra arguments to
- # SMTP.start/SMTP#start.
- #
- # # PLAIN
- # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain',
- # 'Your Account', 'Your Password', :plain)
- # # LOGIN
- # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain',
- # 'Your Account', 'Your Password', :login)
- #
- # # CRAM MD5
- # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain',
- # 'Your Account', 'Your Password', :cram_md5)
- #
- class SMTP
-
- Revision = %q$Revision$.split[1]
-
- # The default SMTP port number, 25.
- def SMTP.default_port
- 25
- end
-
- # The default mail submission port number, 587.
- def SMTP.default_submission_port
- 587
- end
-
- # The default SMTPS port number, 465.
- def SMTP.default_tls_port
- 465
- end
-
- class << self
- alias default_ssl_port default_tls_port
- end
-
- def SMTP.default_ssl_context
- OpenSSL::SSL::SSLContext.new
- end
-
- #
- # Creates a new Net::SMTP object.
- #
- # +address+ is the hostname or ip address of your SMTP
- # server. +port+ is the port to connect to; it defaults to
- # port 25.
- #
- # This method does not open the TCP connection. You can use
- # SMTP.start instead of SMTP.new if you want to do everything
- # at once. Otherwise, follow SMTP.new with SMTP#start.
- #
- def initialize(address, port = nil)
- @address = address
- @port = (port || SMTP.default_port)
- @esmtp = true
- @capabilities = nil
- @socket = nil
- @started = false
- @open_timeout = 30
- @read_timeout = 60
- @error_occured = false
- @debug_output = nil
- @tls = false
- @starttls = false
- @ssl_context = nil
- end
-
- # Provide human-readable stringification of class state.
- def inspect
- "#<#{self.class} #{@address}:#{@port} started=#{@started}>"
- end
-
- # +true+ if the SMTP object uses ESMTP (which it does by default).
- def esmtp?
- @esmtp
- end
-
- #
- # Set whether to use ESMTP or not. This should be done before
- # calling #start. Note that if #start is called in ESMTP mode,
- # and the connection fails due to a ProtocolError, the SMTP
- # object will automatically switch to plain SMTP mode and
- # retry (but not vice versa).
- #
- def esmtp=(bool)
- @esmtp = bool
- end
-
- alias esmtp esmtp?
-
- # true if server advertises STARTTLS.
- # You cannot get valid value before opening SMTP session.
- def capable_starttls?
- capable?('STARTTLS')
- end
-
- def capable?(key)
- return nil unless @capabilities
- @capabilities[key] ? true : false
- end
- private :capable?
-
- # true if server advertises AUTH PLAIN.
- # You cannot get valid value before opening SMTP session.
- def capable_plain_auth?
- auth_capable?('PLAIN')
- end
-
- # true if server advertises AUTH LOGIN.
- # You cannot get valid value before opening SMTP session.
- def capable_login_auth?
- auth_capable?('LOGIN')
- end
-
- # true if server advertises AUTH CRAM-MD5.
- # You cannot get valid value before opening SMTP session.
- def capable_cram_md5_auth?
- auth_capable?('CRAM-MD5')
- end
-
- def auth_capable?(type)
- return nil unless @capabilities
- return false unless @capabilities['AUTH']
- @capabilities['AUTH'].include?(type)
- end
- private :auth_capable?
-
- # Returns supported authentication methods on this server.
- # You cannot get valid value before opening SMTP session.
- def capable_auth_types
- return [] unless @capabilities
- return [] unless @capabilities['AUTH']
- @capabilities['AUTH']
- end
-
- # true if this object uses SMTP/TLS (SMTPS).
- def tls?
- @tls
- end
-
- alias ssl? tls?
-
- # Enables SMTP/TLS (SMTPS: SMTP over direct TLS connection) for
- # this object. Must be called before the connection is established
- # to have any effect. +context+ is a OpenSSL::SSL::SSLContext object.
- def enable_tls(context = SMTP.default_ssl_context)
- raise 'openssl library not installed' unless defined?(OpenSSL)
- raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls
- @tls = true
- @ssl_context = context
- end
-
- alias enable_ssl enable_tls
-
- # Disables SMTP/TLS for this object. Must be called before the
- # connection is established to have any effect.
- def disable_tls
- @tls = false
- @ssl_context = nil
- end
-
- alias disable_ssl disable_tls
-
- # Returns truth value if this object uses STARTTLS.
- # If this object always uses STARTTLS, returns :always.
- # If this object uses STARTTLS when the server support TLS, returns :auto.
- def starttls?
- @starttls
- end
-
- # true if this object uses STARTTLS.
- def starttls_always?
- @starttls == :always
- end
-
- # true if this object uses STARTTLS when server advertises STARTTLS.
- def starttls_auto?
- @starttls == :auto
- end
-
- # Enables SMTP/TLS (STARTTLS) for this object.
- # +context+ is a OpenSSL::SSL::SSLContext object.
- def enable_starttls(context = SMTP.default_ssl_context)
- raise 'openssl library not installed' unless defined?(OpenSSL)
- raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
- @starttls = :always
- @ssl_context = context
- end
-
- # Enables SMTP/TLS (STARTTLS) for this object if server accepts.
- # +context+ is a OpenSSL::SSL::SSLContext object.
- def enable_starttls_auto(context = SMTP.default_ssl_context)
- raise 'openssl library not installed' unless defined?(OpenSSL)
- raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
- @starttls = :auto
- @ssl_context = context
- end
-
- # Disables SMTP/TLS (STARTTLS) for this object. Must be called
- # before the connection is established to have any effect.
- def disable_starttls
- @starttls = false
- @ssl_context = nil
- end
-
- # The address of the SMTP server to connect to.
- attr_reader :address
-
- # The port number of the SMTP server to connect to.
- attr_reader :port
-
- # Seconds to wait while attempting to open a connection.
- # If the connection cannot be opened within this time, a
- # TimeoutError is raised.
- attr_accessor :open_timeout
-
- # Seconds to wait while reading one block (by one read(2) call).
- # If the read(2) call does not complete within this time, a
- # TimeoutError is raised.
- attr_reader :read_timeout
-
- # Set the number of seconds to wait until timing-out a read(2)
- # call.
- def read_timeout=(sec)
- @socket.read_timeout = sec if @socket
- @read_timeout = sec
- end
-
- #
- # WARNING: This method causes serious security holes.
- # Use this method for only debugging.
- #
- # Set an output stream for debug logging.
- # You must call this before #start.
- #
- # # example
- # smtp = Net::SMTP.new(addr, port)
- # smtp.set_debug_output $stderr
- # smtp.start do |smtp|
- # ....
- # end
- #
- def debug_output=(arg)
- @debug_output = arg
- end
-
- alias set_debug_output debug_output=
-
- #
- # SMTP session control
- #
-
- #
- # Creates a new Net::SMTP object and connects to the server.
- #
- # This method is equivalent to:
- #
- # Net::SMTP.new(address, port).start(helo_domain, account, password, authtype)
- #
- # === Example
- #
- # Net::SMTP.start('your.smtp.server') do |smtp|
- # smtp.send_message msgstr, 'from@example.com', ['dest@example.com']
- # end
- #
- # === Block Usage
- #
- # If called with a block, the newly-opened Net::SMTP object is yielded
- # to the block, and automatically closed when the block finishes. If called
- # without a block, the newly-opened Net::SMTP object is returned to
- # the caller, and it is the caller's responsibility to close it when
- # finished.
- #
- # === Parameters
- #
- # +address+ is the hostname or ip address of your smtp server.
- #
- # +port+ is the port to connect to; it defaults to port 25.
- #
- # +helo+ is the _HELO_ _domain_ provided by the client to the
- # server (see overview comments); it defaults to 'localhost'.
- #
- # The remaining arguments are used for SMTP authentication, if required
- # or desired. +user+ is the account name; +secret+ is your password
- # or other authentication token; and +authtype+ is the authentication
- # type, one of :plain, :login, or :cram_md5. See the discussion of
- # SMTP Authentication in the overview notes.
- #
- # === Errors
- #
- # This method may raise:
- #
- # * Net::SMTPAuthenticationError
- # * Net::SMTPServerBusy
- # * Net::SMTPSyntaxError
- # * Net::SMTPFatalError
- # * Net::SMTPUnknownError
- # * IOError
- # * TimeoutError
- #
- def SMTP.start(address, port = nil, helo = 'localhost',
- user = nil, secret = nil, authtype = nil,
- &block) # :yield: smtp
- new(address, port).start(helo, user, secret, authtype, &block)
- end
-
- # +true+ if the SMTP session has been started.
- def started?
- @started
- end
-
- #
- # Opens a TCP connection and starts the SMTP session.
- #
- # === Parameters
- #
- # +helo+ is the _HELO_ _domain_ that you'll dispatch mails from; see
- # the discussion in the overview notes.
- #
- # If both of +user+ and +secret+ are given, SMTP authentication
- # will be attempted using the AUTH command. +authtype+ specifies
- # the type of authentication to attempt; it must be one of
- # :login, :plain, and :cram_md5. See the notes on SMTP Authentication
- # in the overview.
- #
- # === Block Usage
- #
- # When this methods is called with a block, the newly-started SMTP
- # object is yielded to the block, and automatically closed after
- # the block call finishes. Otherwise, it is the caller's
- # responsibility to close the session when finished.
- #
- # === Example
- #
- # This is very similar to the class method SMTP.start.
- #
- # require 'net/smtp'
- # smtp = Net::SMTP.new('smtp.mail.server', 25)
- # smtp.start(helo_domain, account, password, authtype) do |smtp|
- # smtp.send_message msgstr, 'from@example.com', ['dest@example.com']
- # end
- #
- # The primary use of this method (as opposed to SMTP.start)
- # is probably to set debugging (#set_debug_output) or ESMTP
- # (#esmtp=), which must be done before the session is
- # started.
- #
- # === Errors
- #
- # If session has already been started, an IOError will be raised.
- #
- # This method may raise:
- #
- # * Net::SMTPAuthenticationError
- # * Net::SMTPServerBusy
- # * Net::SMTPSyntaxError
- # * Net::SMTPFatalError
- # * Net::SMTPUnknownError
- # * IOError
- # * TimeoutError
- #
- def start(helo = 'localhost',
- user = nil, secret = nil, authtype = nil) # :yield: smtp
- if block_given?
- begin
- do_start helo, user, secret, authtype
- return yield(self)
- ensure
- do_finish
- end
- else
- do_start helo, user, secret, authtype
- return self
- end
- end
-
- # Finishes the SMTP session and closes TCP connection.
- # Raises IOError if not started.
- def finish
- raise IOError, 'not yet started' unless started?
- do_finish
- end
-
- private
-
- def do_start(helo_domain, user, secret, authtype)
- raise IOError, 'SMTP session already started' if @started
- if user or secret
- check_auth_method(authtype || DEFAULT_AUTH_TYPE)
- check_auth_args user, secret
- end
- s = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
- logging "Connection opened: #{@address}:#{@port}"
- @socket = new_internet_message_io(tls? ? tlsconnect(s) : s)
- check_response critical { recv_response() }
- do_helo helo_domain
- if starttls_always? or (capable_starttls? and starttls_auto?)
- unless capable_starttls?
- raise SMTPUnsupportedCommand,
- "STARTTLS is not supported on this server"
- end
- starttls
- @socket = new_internet_message_io(tlsconnect(s))
- # helo response may be different after STARTTLS
- do_helo helo_domain
- end
- authenticate user, secret, (authtype || DEFAULT_AUTH_TYPE) if user
- @started = true
- ensure
- unless @started
- # authentication failed, cancel connection.
- s.close if s and not s.closed?
- @socket = nil
- end
- end
-
- def tlsconnect(s)
- s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
- logging "TLS connection started"
- s.sync_close = true
- s.connect
- if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
- s.post_connection_check(@address)
- end
- s
- end
-
- def new_internet_message_io(s)
- io = InternetMessageIO.new(s)
- io.read_timeout = @read_timeout
- io.debug_output = @debug_output
- io
- end
-
- def do_helo(helo_domain)
- res = @esmtp ? ehlo(helo_domain) : helo(helo_domain)
- @capabilities = res.capabilities
- rescue SMTPError
- if @esmtp
- @esmtp = false
- @error_occured = false
- retry
- end
- raise
- end
-
- def do_finish
- quit if @socket and not @socket.closed? and not @error_occured
- ensure
- @started = false
- @error_occured = false
- @socket.close if @socket and not @socket.closed?
- @socket = nil
- end
-
- #
- # Message Sending
- #
-
- public
-
- #
- # Sends +msgstr+ as a message. Single CR ("\r") and LF ("\n") found
- # in the +msgstr+, are converted into the CR LF pair. You cannot send a
- # binary message with this method. +msgstr+ should include both
- # the message headers and body.
- #
- # +from_addr+ is a String representing the source mail address.
- #
- # +to_addr+ is a String or Strings or Array of Strings, representing
- # the destination mail address or addresses.
- #
- # === Example
- #
- # Net::SMTP.start('smtp.example.com') do |smtp|
- # smtp.send_message msgstr,
- # 'from@example.com',
- # ['dest@example.com', 'dest2@example.com']
- # end
- #
- # === Errors
- #
- # This method may raise:
- #
- # * Net::SMTPServerBusy
- # * Net::SMTPSyntaxError
- # * Net::SMTPFatalError
- # * Net::SMTPUnknownError
- # * IOError
- # * TimeoutError
- #
- def send_message(msgstr, from_addr, *to_addrs)
- raise IOError, 'closed session' unless @socket
- mailfrom from_addr
- rcptto_list to_addrs
- data msgstr
- end
-
- alias send_mail send_message
- alias sendmail send_message # obsolete
-
- #
- # Opens a message writer stream and gives it to the block.
- # The stream is valid only in the block, and has these methods:
- #
- # puts(str = ''):: outputs STR and CR LF.
- # print(str):: outputs STR.
- # printf(fmt, *args):: outputs sprintf(fmt,*args).
- # write(str):: outputs STR and returns the length of written bytes.
- # <<(str):: outputs STR and returns self.
- #
- # If a single CR ("\r") or LF ("\n") is found in the message,
- # it is converted to the CR LF pair. You cannot send a binary
- # message with this method.
- #
- # === Parameters
- #
- # +from_addr+ is a String representing the source mail address.
- #
- # +to_addr+ is a String or Strings or Array of Strings, representing
- # the destination mail address or addresses.
- #
- # === Example
- #
- # Net::SMTP.start('smtp.example.com', 25) do |smtp|
- # smtp.open_message_stream('from@example.com', ['dest@example.com']) do |f|
- # f.puts 'From: from@example.com'
- # f.puts 'To: dest@example.com'
- # f.puts 'Subject: test message'
- # f.puts
- # f.puts 'This is a test message.'
- # end
- # end
- #
- # === Errors
- #
- # This method may raise:
- #
- # * Net::SMTPServerBusy
- # * Net::SMTPSyntaxError
- # * Net::SMTPFatalError
- # * Net::SMTPUnknownError
- # * IOError
- # * TimeoutError
- #
- def open_message_stream(from_addr, *to_addrs, &block) # :yield: stream
- raise IOError, 'closed session' unless @socket
- mailfrom from_addr
- rcptto_list to_addrs
- data(&block)
- end
-
- alias ready open_message_stream # obsolete
-
- #
- # Authentication
- #
-
- public
-
- DEFAULT_AUTH_TYPE = :plain
-
- def authenticate(user, secret, authtype = DEFAULT_AUTH_TYPE)
- check_auth_method authtype
- check_auth_args user, secret
- send auth_method(authtype), user, secret
- end
-
- def auth_plain(user, secret)
- check_auth_args user, secret
- res = critical {
- get_response('AUTH PLAIN ' + base64_encode("\0#{user}\0#{secret}"))
- }
- check_auth_response res
- res
- end
-
- def auth_login(user, secret)
- check_auth_args user, secret
- res = critical {
- check_auth_continue get_response('AUTH LOGIN')
- check_auth_continue get_response(base64_encode(user))
- get_response(base64_encode(secret))
- }
- check_auth_response res
- res
- end
-
- def auth_cram_md5(user, secret)
- check_auth_args user, secret
- res = critical {
- res0 = get_response('AUTH CRAM-MD5')
- check_auth_continue res0
- crammed = cram_md5_response(secret, res0.cram_md5_challenge)
- get_response(base64_encode("#{user} #{crammed}"))
- }
- check_auth_response res
- res
- end
-
- private
-
- def check_auth_method(type)
- unless respond_to?(auth_method(type), true)
- raise ArgumentError, "wrong authentication type #{type}"
- end
- end
-
- def auth_method(type)
- "auth_#{type.to_s.downcase}".intern
- end
-
- def check_auth_args(user, secret)
- unless user
- raise ArgumentError, 'SMTP-AUTH requested but missing user name'
- end
- unless secret
- raise ArgumentError, 'SMTP-AUTH requested but missing secret phrase'
- end
- end
-
- def base64_encode(str)
- # expects "str" may not become too long
- [str].pack('m').gsub(/\s+/, '')
- end
-
- IMASK = 0x36
- OMASK = 0x5c
-
- # CRAM-MD5: [RFC2195]
- def cram_md5_response(secret, challenge)
- tmp = Digest::MD5.digest(cram_secret(secret, IMASK) + challenge)
- Digest::MD5.hexdigest(cram_secret(secret, OMASK) + tmp)
- end
-
- CRAM_BUFSIZE = 64
-
- def cram_secret(secret, mask)
- secret = Digest::MD5.digest(secret) if secret.size > CRAM_BUFSIZE
- buf = secret.ljust(CRAM_BUFSIZE, "\0")
- 0.upto(buf.size - 1) do |i|
- buf[i] = (buf[i].ord ^ mask).chr
- end
- buf
- end
-
- #
- # SMTP command dispatcher
- #
-
- public
-
- def starttls
- getok('STARTTLS')
- end
-
- def helo(domain)
- getok("HELO #{domain}")
- end
-
- def ehlo(domain)
- getok("EHLO #{domain}")
- end
-
- def mailfrom(from_addr)
- if $SAFE > 0
- raise SecurityError, 'tainted from_addr' if from_addr.tainted?
- end
- getok("MAIL FROM:<#{from_addr}>")
- end
-
- def rcptto_list(to_addrs)
- raise ArgumentError, 'mail destination not given' if to_addrs.empty?
- to_addrs.flatten.each do |addr|
- rcptto addr
- end
- end
-
- def rcptto(to_addr)
- if $SAFE > 0
- raise SecurityError, 'tainted to_addr' if to_addr.tainted?
- end
- getok("RCPT TO:<#{to_addr}>")
- end
-
- # This method sends a message.
- # If +msgstr+ is given, sends it as a message.
- # If block is given, yield a message writer stream.
- # You must write message before the block is closed.
- #
- # # Example 1 (by string)
- # smtp.data(<<EndMessage)
- # From: john@example.com
- # To: betty@example.com
- # Subject: I found a bug
- #
- # Check vm.c:58879.
- # EndMessage
- #
- # # Example 2 (by block)
- # smtp.data {|f|
- # f.puts "From: john@example.com"
- # f.puts "To: betty@example.com"
- # f.puts "Subject: I found a bug"
- # f.puts ""
- # f.puts "Check vm.c:58879."
- # }
- #
- def data(msgstr = nil, &block) #:yield: stream
- if msgstr and block
- raise ArgumentError, "message and block are exclusive"
- end
- unless msgstr or block
- raise ArgumentError, "message or block is required"
- end
- res = critical {
- check_continue get_response('DATA')
- if msgstr
- @socket.write_message msgstr
- else
- @socket.write_message_by_block(&block)
- end
- recv_response()
- }
- check_response res
- res
- end
-
- def quit
- getok('QUIT')
- end
-
- private
-
- def getok(reqline)
- res = critical {
- @socket.writeline reqline
- recv_response()
- }
- check_response res
- res
- end
-
- def get_response(reqline)
- @socket.writeline reqline
- recv_response()
- end
-
- def recv_response
- buf = ''
- while true
- line = @socket.readline
- buf << line << "\n"
- break unless line[3,1] == '-' # "210-PIPELINING"
- end
- Response.parse(buf)
- end
-
- def critical(&block)
- return '200 dummy reply code' if @error_occured
- begin
- return yield()
- rescue Exception
- @error_occured = true
- raise
- end
- end
-
- def check_response(res)
- unless res.success?
- raise res.exception_class, res.message
- end
- end
-
- def check_continue(res)
- unless res.continue?
- raise SMTPUnknownError, "could not get 3xx (#{res.status})"
- end
- end
-
- def check_auth_response(res)
- unless res.success?
- raise SMTPAuthenticationError, res.message
- end
- end
-
- def check_auth_continue(res)
- unless res.continue?
- raise res.exception_class, res.message
- end
- end
-
- class Response
- def Response.parse(str)
- new(str[0,3], str)
- end
-
- def initialize(status, string)
- @status = status
- @string = string
- end
-
- attr_reader :status
- attr_reader :string
-
- def status_type_char
- @status[0, 1]
- end
-
- def success?
- status_type_char() == '2'
- end
-
- def continue?
- status_type_char() == '3'
- end
-
- def message
- @string.lines.first
- end
-
- def cram_md5_challenge
- @string.split(/ /)[1].unpack('m')[0]
- end
-
- def capabilities
- return {} unless @string[3, 1] == '-'
- h = {}
- @string.lines.drop(1).each do |line|
- k, *v = line[4..-1].chomp.split(nil)
- h[k] = v
- end
- h
- end
-
- def exception_class
- case @status
- when /\A4/ then SMTPServerBusy
- when /\A50/ then SMTPSyntaxError
- when /\A53/ then SMTPAuthenticationError
- when /\A5/ then SMTPFatalError
- else SMTPUnknownError
- end
- end
- end
-
- def logging(msg)
- @debug_output << msg + "\n" if @debug_output
- end
-
- end # class SMTP
-
- SMTPSession = SMTP
-
-end
diff --git a/lib/net/telnet.rb b/lib/net/telnet.rb
deleted file mode 100644
index 67fd656c63..0000000000
--- a/lib/net/telnet.rb
+++ /dev/null
@@ -1,759 +0,0 @@
-# = net/telnet.rb - Simple Telnet Client Library
-#
-# Author:: Wakou Aoyama <wakou@ruby-lang.org>
-# Documentation:: William Webber and Wakou Aoyama
-#
-# This file holds the class Net::Telnet, which provides client-side
-# telnet functionality.
-#
-# For documentation, see Net::Telnet.
-#
-
-require "socket"
-require "delegate"
-require "timeout"
-require "English"
-
-module Net
-
- #
- # == Net::Telnet
- #
- # Provides telnet client functionality.
- #
- # This class also has, through delegation, all the methods of a
- # socket object (by default, a +TCPSocket+, but can be set by the
- # +Proxy+ option to <tt>new()</tt>). This provides methods such as
- # <tt>close()</tt> to end the session and <tt>sysread()</tt> to read
- # data directly from the host, instead of via the <tt>waitfor()</tt>
- # mechanism. Note that if you do use <tt>sysread()</tt> directly
- # when in telnet mode, you should probably pass the output through
- # <tt>preprocess()</tt> to extract telnet command sequences.
- #
- # == Overview
- #
- # The telnet protocol allows a client to login remotely to a user
- # account on a server and execute commands via a shell. The equivalent
- # is done by creating a Net::Telnet class with the +Host+ option
- # set to your host, calling #login() with your user and password,
- # issuing one or more #cmd() calls, and then calling #close()
- # to end the session. The #waitfor(), #print(), #puts(), and
- # #write() methods, which #cmd() is implemented on top of, are
- # only needed if you are doing something more complicated.
- #
- # A Net::Telnet object can also be used to connect to non-telnet
- # services, such as SMTP or HTTP. In this case, you normally
- # want to provide the +Port+ option to specify the port to
- # connect to, and set the +Telnetmode+ option to false to prevent
- # the client from attempting to interpret telnet command sequences.
- # Generally, #login() will not work with other protocols, and you
- # have to handle authentication yourself.
- #
- # For some protocols, it will be possible to specify the +Prompt+
- # option once when you create the Telnet object and use #cmd() calls;
- # for others, you will have to specify the response sequence to
- # look for as the Match option to every #cmd() call, or call
- # #puts() and #waitfor() directly; for yet others, you will have
- # to use #sysread() instead of #waitfor() and parse server
- # responses yourself.
- #
- # It is worth noting that when you create a new Net::Telnet object,
- # you can supply a proxy IO channel via the Proxy option. This
- # can be used to attach the Telnet object to other Telnet objects,
- # to already open sockets, or to any read-write IO object. This
- # can be useful, for instance, for setting up a test fixture for
- # unit testing.
- #
- # == Examples
- #
- # === Log in and send a command, echoing all output to stdout
- #
- # localhost = Net::Telnet::new("Host" => "localhost",
- # "Timeout" => 10,
- # "Prompt" => /[$%#>] \z/n)
- # localhost.login("username", "password") { |c| print c }
- # localhost.cmd("command") { |c| print c }
- # localhost.close
- #
- #
- # === Check a POP server to see if you have mail
- #
- # pop = Net::Telnet::new("Host" => "your_destination_host_here",
- # "Port" => 110,
- # "Telnetmode" => false,
- # "Prompt" => /^\+OK/n)
- # pop.cmd("user " + "your_username_here") { |c| print c }
- # pop.cmd("pass " + "your_password_here") { |c| print c }
- # pop.cmd("list") { |c| print c }
- #
- # == References
- #
- # There are a large number of RFCs relevant to the Telnet protocol.
- # RFCs 854-861 define the base protocol. For a complete listing
- # of relevant RFCs, see
- # http://www.omnifarious.org/~hopper/technical/telnet-rfc.html
- #
- class Telnet < SimpleDelegator
-
- # :stopdoc:
- IAC = 255.chr # "\377" # "\xff" # interpret as command
- DONT = 254.chr # "\376" # "\xfe" # you are not to use option
- DO = 253.chr # "\375" # "\xfd" # please, you use option
- WONT = 252.chr # "\374" # "\xfc" # I won't use option
- WILL = 251.chr # "\373" # "\xfb" # I will use option
- SB = 250.chr # "\372" # "\xfa" # interpret as subnegotiation
- GA = 249.chr # "\371" # "\xf9" # you may reverse the line
- EL = 248.chr # "\370" # "\xf8" # erase the current line
- EC = 247.chr # "\367" # "\xf7" # erase the current character
- AYT = 246.chr # "\366" # "\xf6" # are you there
- AO = 245.chr # "\365" # "\xf5" # abort output--but let prog finish
- IP = 244.chr # "\364" # "\xf4" # interrupt process--permanently
- BREAK = 243.chr # "\363" # "\xf3" # break
- DM = 242.chr # "\362" # "\xf2" # data mark--for connect. cleaning
- NOP = 241.chr # "\361" # "\xf1" # nop
- SE = 240.chr # "\360" # "\xf0" # end sub negotiation
- EOR = 239.chr # "\357" # "\xef" # end of record (transparent mode)
- ABORT = 238.chr # "\356" # "\xee" # Abort process
- SUSP = 237.chr # "\355" # "\xed" # Suspend process
- EOF = 236.chr # "\354" # "\xec" # End of file
- SYNCH = 242.chr # "\362" # "\xf2" # for telfunc calls
-
- OPT_BINARY = 0.chr # "\000" # "\x00" # Binary Transmission
- OPT_ECHO = 1.chr # "\001" # "\x01" # Echo
- OPT_RCP = 2.chr # "\002" # "\x02" # Reconnection
- OPT_SGA = 3.chr # "\003" # "\x03" # Suppress Go Ahead
- OPT_NAMS = 4.chr # "\004" # "\x04" # Approx Message Size Negotiation
- OPT_STATUS = 5.chr # "\005" # "\x05" # Status
- OPT_TM = 6.chr # "\006" # "\x06" # Timing Mark
- OPT_RCTE = 7.chr # "\a" # "\x07" # Remote Controlled Trans and Echo
- OPT_NAOL = 8.chr # "\010" # "\x08" # Output Line Width
- OPT_NAOP = 9.chr # "\t" # "\x09" # Output Page Size
- OPT_NAOCRD = 10.chr # "\n" # "\x0a" # Output Carriage-Return Disposition
- OPT_NAOHTS = 11.chr # "\v" # "\x0b" # Output Horizontal Tab Stops
- OPT_NAOHTD = 12.chr # "\f" # "\x0c" # Output Horizontal Tab Disposition
- OPT_NAOFFD = 13.chr # "\r" # "\x0d" # Output Formfeed Disposition
- OPT_NAOVTS = 14.chr # "\016" # "\x0e" # Output Vertical Tabstops
- OPT_NAOVTD = 15.chr # "\017" # "\x0f" # Output Vertical Tab Disposition
- OPT_NAOLFD = 16.chr # "\020" # "\x10" # Output Linefeed Disposition
- OPT_XASCII = 17.chr # "\021" # "\x11" # Extended ASCII
- OPT_LOGOUT = 18.chr # "\022" # "\x12" # Logout
- OPT_BM = 19.chr # "\023" # "\x13" # Byte Macro
- OPT_DET = 20.chr # "\024" # "\x14" # Data Entry Terminal
- OPT_SUPDUP = 21.chr # "\025" # "\x15" # SUPDUP
- OPT_SUPDUPOUTPUT = 22.chr # "\026" # "\x16" # SUPDUP Output
- OPT_SNDLOC = 23.chr # "\027" # "\x17" # Send Location
- OPT_TTYPE = 24.chr # "\030" # "\x18" # Terminal Type
- OPT_EOR = 25.chr # "\031" # "\x19" # End of Record
- OPT_TUID = 26.chr # "\032" # "\x1a" # TACACS User Identification
- OPT_OUTMRK = 27.chr # "\e" # "\x1b" # Output Marking
- OPT_TTYLOC = 28.chr # "\034" # "\x1c" # Terminal Location Number
- OPT_3270REGIME = 29.chr # "\035" # "\x1d" # Telnet 3270 Regime
- OPT_X3PAD = 30.chr # "\036" # "\x1e" # X.3 PAD
- OPT_NAWS = 31.chr # "\037" # "\x1f" # Negotiate About Window Size
- OPT_TSPEED = 32.chr # " " # "\x20" # Terminal Speed
- OPT_LFLOW = 33.chr # "!" # "\x21" # Remote Flow Control
- OPT_LINEMODE = 34.chr # "\"" # "\x22" # Linemode
- OPT_XDISPLOC = 35.chr # "#" # "\x23" # X Display Location
- OPT_OLD_ENVIRON = 36.chr # "$" # "\x24" # Environment Option
- OPT_AUTHENTICATION = 37.chr # "%" # "\x25" # Authentication Option
- OPT_ENCRYPT = 38.chr # "&" # "\x26" # Encryption Option
- OPT_NEW_ENVIRON = 39.chr # "'" # "\x27" # New Environment Option
- OPT_EXOPL = 255.chr # "\377" # "\xff" # Extended-Options-List
-
- NULL = "\000"
- CR = "\015"
- LF = "\012"
- EOL = CR + LF
- REVISION = '$Id$'
- # :startdoc:
-
- #
- # Creates a new Net::Telnet object.
- #
- # Attempts to connect to the host (unless the Proxy option is
- # provided: see below). If a block is provided, it is yielded
- # status messages on the attempt to connect to the server, of
- # the form:
- #
- # Trying localhost...
- # Connected to localhost.
- #
- # +options+ is a hash of options. The following example lists
- # all options and their default values.
- #
- # host = Net::Telnet::new(
- # "Host" => "localhost", # default: "localhost"
- # "Port" => 23, # default: 23
- # "Binmode" => false, # default: false
- # "Output_log" => "output_log", # default: nil (no output)
- # "Dump_log" => "dump_log", # default: nil (no output)
- # "Prompt" => /[$%#>] \z/n, # default: /[$%#>] \z/n
- # "Telnetmode" => true, # default: true
- # "Timeout" => 10, # default: 10
- # # if ignore timeout then set "Timeout" to false.
- # "Waittime" => 0, # default: 0
- # "Proxy" => proxy # default: nil
- # # proxy is Net::Telnet or IO object
- # )
- #
- # The options have the following meanings:
- #
- # Host:: the hostname or IP address of the host to connect to, as a String.
- # Defaults to "localhost".
- #
- # Port:: the port to connect to. Defaults to 23.
- #
- # Binmode:: if false (the default), newline substitution is performed.
- # Outgoing LF is
- # converted to CRLF, and incoming CRLF is converted to LF. If
- # true, this substitution is not performed. This value can
- # also be set with the #binmode() method. The
- # outgoing conversion only applies to the #puts() and #print()
- # methods, not the #write() method. The precise nature of
- # the newline conversion is also affected by the telnet options
- # SGA and BIN.
- #
- # Output_log:: the name of the file to write connection status messages
- # and all received traffic to. In the case of a proper
- # Telnet session, this will include the client input as
- # echoed by the host; otherwise, it only includes server
- # responses. Output is appended verbatim to this file.
- # By default, no output log is kept.
- #
- # Dump_log:: as for Output_log, except that output is written in hexdump
- # format (16 bytes per line as hex pairs, followed by their
- # printable equivalent), with connection status messages
- # preceded by '#', sent traffic preceded by '>', and
- # received traffic preceded by '<'. By default, not dump log
- # is kept.
- #
- # Prompt:: a regular expression matching the host's command-line prompt
- # sequence. This is needed by the Telnet class to determine
- # when the output from a command has finished and the host is
- # ready to receive a new command. By default, this regular
- # expression is /[$%#>] \z/n.
- #
- # Telnetmode:: a boolean value, true by default. In telnet mode,
- # traffic received from the host is parsed for special
- # command sequences, and these sequences are escaped
- # in outgoing traffic sent using #puts() or #print()
- # (but not #write()). If you are using the Net::Telnet
- # object to connect to a non-telnet service (such as
- # SMTP or POP), this should be set to "false" to prevent
- # undesired data corruption. This value can also be set
- # by the #telnetmode() method.
- #
- # Timeout:: the number of seconds to wait before timing out both the
- # initial attempt to connect to host (in this constructor),
- # and all attempts to read data from the host (in #waitfor(),
- # #cmd(), and #login()). Exceeding this timeout causes a
- # TimeoutError to be raised. The default value is 10 seconds.
- # You can disable the timeout by setting this value to false.
- # In this case, the connect attempt will eventually timeout
- # on the underlying connect(2) socket call with an
- # Errno::ETIMEDOUT error (but generally only after a few
- # minutes), but other attempts to read data from the host
- # will hand indefinitely if no data is forthcoming.
- #
- # Waittime:: the amount of time to wait after seeing what looks like a
- # prompt (that is, received data that matches the Prompt
- # option regular expression) to see if more data arrives.
- # If more data does arrive in this time, Net::Telnet assumes
- # that what it saw was not really a prompt. This is to try to
- # avoid false matches, but it can also lead to missing real
- # prompts (if, for instance, a background process writes to
- # the terminal soon after the prompt is displayed). By
- # default, set to 0, meaning not to wait for more data.
- #
- # Proxy:: a proxy object to used instead of opening a direct connection
- # to the host. Must be either another Net::Telnet object or
- # an IO object. If it is another Net::Telnet object, this
- # instance will use that one's socket for communication. If an
- # IO object, it is used directly for communication. Any other
- # kind of object will cause an error to be raised.
- #
- def initialize(options) # :yield: mesg
- @options = options
- @options["Host"] = "localhost" unless @options.has_key?("Host")
- @options["Port"] = 23 unless @options.has_key?("Port")
- @options["Prompt"] = /[$%#>] \z/n unless @options.has_key?("Prompt")
- @options["Timeout"] = 10 unless @options.has_key?("Timeout")
- @options["Waittime"] = 0 unless @options.has_key?("Waittime")
- unless @options.has_key?("Binmode")
- @options["Binmode"] = false
- else
- unless (true == @options["Binmode"] or false == @options["Binmode"])
- raise ArgumentError, "Binmode option must be true or false"
- end
- end
-
- unless @options.has_key?("Telnetmode")
- @options["Telnetmode"] = true
- else
- unless (true == @options["Telnetmode"] or false == @options["Telnetmode"])
- raise ArgumentError, "Telnetmode option must be true or false"
- end
- end
-
- @telnet_option = { "SGA" => false, "BINARY" => false }
-
- if @options.has_key?("Output_log")
- @log = File.open(@options["Output_log"], 'a+')
- @log.sync = true
- @log.binmode
- end
-
- if @options.has_key?("Dump_log")
- @dumplog = File.open(@options["Dump_log"], 'a+')
- @dumplog.sync = true
- @dumplog.binmode
- def @dumplog.log_dump(dir, x) # :nodoc:
- len = x.length
- addr = 0
- offset = 0
- while 0 < len
- if len < 16
- line = x[offset, len]
- else
- line = x[offset, 16]
- end
- hexvals = line.unpack('H*')[0]
- hexvals += ' ' * (32 - hexvals.length)
- hexvals = format("%s %s %s %s " * 4, *hexvals.unpack('a2' * 16))
- line = line.gsub(/[\000-\037\177-\377]/n, '.')
- printf "%s 0x%5.5x: %s%s\n", dir, addr, hexvals, line
- addr += 16
- offset += 16
- len -= 16
- end
- print "\n"
- end
- end
-
- if @options.has_key?("Proxy")
- if @options["Proxy"].kind_of?(Net::Telnet)
- @sock = @options["Proxy"].sock
- elsif @options["Proxy"].kind_of?(IO)
- @sock = @options["Proxy"]
- else
- raise "Error: Proxy must be an instance of Net::Telnet or IO."
- end
- else
- message = "Trying " + @options["Host"] + "...\n"
- yield(message) if block_given?
- @log.write(message) if @options.has_key?("Output_log")
- @dumplog.log_dump('#', message) if @options.has_key?("Dump_log")
-
- begin
- if @options["Timeout"] == false
- @sock = TCPSocket.open(@options["Host"], @options["Port"])
- else
- timeout(@options["Timeout"]) do
- @sock = TCPSocket.open(@options["Host"], @options["Port"])
- end
- end
- rescue TimeoutError
- raise TimeoutError, "timed out while opening a connection to the host"
- rescue
- @log.write($ERROR_INFO.to_s + "\n") if @options.has_key?("Output_log")
- @dumplog.log_dump('#', $ERROR_INFO.to_s + "\n") if @options.has_key?("Dump_log")
- raise
- end
- @sock.sync = true
- @sock.binmode
-
- message = "Connected to " + @options["Host"] + ".\n"
- yield(message) if block_given?
- @log.write(message) if @options.has_key?("Output_log")
- @dumplog.log_dump('#', message) if @options.has_key?("Dump_log")
- end
-
- super(@sock)
- end # initialize
-
- # The socket the Telnet object is using. Note that this object becomes
- # a delegate of the Telnet object, so normally you invoke its methods
- # directly on the Telnet object.
- attr :sock
-
- # Set telnet command interpretation on (+mode+ == true) or off
- # (+mode+ == false), or return the current value (+mode+ not
- # provided). It should be on for true telnet sessions, off if
- # using Net::Telnet to connect to a non-telnet service such
- # as SMTP.
- def telnetmode(mode = nil)
- case mode
- when nil
- @options["Telnetmode"]
- when true, false
- @options["Telnetmode"] = mode
- else
- raise ArgumentError, "argument must be true or false, or missing"
- end
- end
-
- # Turn telnet command interpretation on (true) or off (false). It
- # should be on for true telnet sessions, off if using Net::Telnet
- # to connect to a non-telnet service such as SMTP.
- def telnetmode=(mode)
- if (true == mode or false == mode)
- @options["Telnetmode"] = mode
- else
- raise ArgumentError, "argument must be true or false"
- end
- end
-
- # Turn newline conversion on (+mode+ == false) or off (+mode+ == true),
- # or return the current value (+mode+ is not specified).
- def binmode(mode = nil)
- case mode
- when nil
- @options["Binmode"]
- when true, false
- @options["Binmode"] = mode
- else
- raise ArgumentError, "argument must be true or false"
- end
- end
-
- # Turn newline conversion on (false) or off (true).
- def binmode=(mode)
- if (true == mode or false == mode)
- @options["Binmode"] = mode
- else
- raise ArgumentError, "argument must be true or false"
- end
- end
-
- # Preprocess received data from the host.
- #
- # Performs newline conversion and detects telnet command sequences.
- # Called automatically by #waitfor(). You should only use this
- # method yourself if you have read input directly using sysread()
- # or similar, and even then only if in telnet mode.
- def preprocess(string)
- # combine CR+NULL into CR
- string = string.gsub(/#{CR}#{NULL}/no, CR) if @options["Telnetmode"]
-
- # combine EOL into "\n"
- string = string.gsub(/#{EOL}/no, "\n") unless @options["Binmode"]
-
- # remove NULL
- string = string.gsub(/#{NULL}/no, '') unless @options["Binmode"]
-
- string.gsub(/#{IAC}(
- [#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]|
- [#{DO}#{DONT}#{WILL}#{WONT}]
- [#{OPT_BINARY}-#{OPT_NEW_ENVIRON}#{OPT_EXOPL}]|
- #{SB}[^#{IAC}]*#{IAC}#{SE}
- )/xno) do
- if IAC == $1 # handle escaped IAC characters
- IAC
- elsif AYT == $1 # respond to "IAC AYT" (are you there)
- self.write("nobody here but us pigeons" + EOL)
- ''
- elsif DO[0] == $1[0] # respond to "IAC DO x"
- if OPT_BINARY[0] == $1[1]
- @telnet_option["BINARY"] = true
- self.write(IAC + WILL + OPT_BINARY)
- else
- self.write(IAC + WONT + $1[1..1])
- end
- ''
- elsif DONT[0] == $1[0] # respond to "IAC DON'T x" with "IAC WON'T x"
- self.write(IAC + WONT + $1[1..1])
- ''
- elsif WILL[0] == $1[0] # respond to "IAC WILL x"
- if OPT_BINARY[0] == $1[1]
- self.write(IAC + DO + OPT_BINARY)
- elsif OPT_ECHO[0] == $1[1]
- self.write(IAC + DO + OPT_ECHO)
- elsif OPT_SGA[0] == $1[1]
- @telnet_option["SGA"] = true
- self.write(IAC + DO + OPT_SGA)
- else
- self.write(IAC + DONT + $1[1..1])
- end
- ''
- elsif WONT[0] == $1[0] # respond to "IAC WON'T x"
- if OPT_ECHO[0] == $1[1]
- self.write(IAC + DONT + OPT_ECHO)
- elsif OPT_SGA[0] == $1[1]
- @telnet_option["SGA"] = false
- self.write(IAC + DONT + OPT_SGA)
- else
- self.write(IAC + DONT + $1[1..1])
- end
- ''
- else
- ''
- end
- end
- end # preprocess
-
- # Read data from the host until a certain sequence is matched.
- #
- # If a block is given, the received data will be yielded as it
- # is read in (not necessarily all in one go), or nil if EOF
- # occurs before any data is received. Whether a block is given
- # or not, all data read will be returned in a single string, or again
- # nil if EOF occurs before any data is received. Note that
- # received data includes the matched sequence we were looking for.
- #
- # +options+ can be either a regular expression or a hash of options.
- # If a regular expression, this specifies the data to wait for.
- # If a hash, this can specify the following options:
- #
- # Match:: a regular expression, specifying the data to wait for.
- # Prompt:: as for Match; used only if Match is not specified.
- # String:: as for Match, except a string that will be converted
- # into a regular expression. Used only if Match and
- # Prompt are not specified.
- # Timeout:: the number of seconds to wait for data from the host
- # before raising a TimeoutError. If set to false,
- # no timeout will occur. If not specified, the
- # Timeout option value specified when this instance
- # was created will be used, or, failing that, the
- # default value of 10 seconds.
- # Waittime:: the number of seconds to wait after matching against
- # the input data to see if more data arrives. If more
- # data arrives within this time, we will judge ourselves
- # not to have matched successfully, and will continue
- # trying to match. If not specified, the Waittime option
- # value specified when this instance was created will be
- # used, or, failing that, the default value of 0 seconds,
- # which means not to wait for more input.
- # FailEOF:: if true, when the remote end closes the connection then an
- # EOFError will be raised. Otherwise, defaults to the old
- # behaviour that the function will return whatever data
- # has been received already, or nil if nothing was received.
- #
- def waitfor(options) # :yield: recvdata
- time_out = @options["Timeout"]
- waittime = @options["Waittime"]
- fail_eof = @options["FailEOF"]
-
- if options.kind_of?(Hash)
- prompt = if options.has_key?("Match")
- options["Match"]
- elsif options.has_key?("Prompt")
- options["Prompt"]
- elsif options.has_key?("String")
- Regexp.new( Regexp.quote(options["String"]) )
- end
- time_out = options["Timeout"] if options.has_key?("Timeout")
- waittime = options["Waittime"] if options.has_key?("Waittime")
- fail_eof = options["FailEOF"] if options.has_key?("FailEOF")
- else
- prompt = options
- end
-
- if time_out == false
- time_out = nil
- end
-
- line = ''
- buf = ''
- rest = ''
- until(prompt === line and not IO::select([@sock], nil, nil, waittime))
- unless IO::select([@sock], nil, nil, time_out)
- raise TimeoutError, "timed out while waiting for more data"
- end
- begin
- c = @sock.readpartial(1024 * 1024)
- @dumplog.log_dump('<', c) if @options.has_key?("Dump_log")
- if @options["Telnetmode"]
- c = rest + c
- if Integer(c.rindex(/#{IAC}#{SE}/no) || 0) <
- Integer(c.rindex(/#{IAC}#{SB}/no) || 0)
- buf = preprocess(c[0 ... c.rindex(/#{IAC}#{SB}/no)])
- rest = c[c.rindex(/#{IAC}#{SB}/no) .. -1]
- elsif pt = c.rindex(/#{IAC}[^#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]?\z/no) ||
- c.rindex(/\r\z/no)
- buf = preprocess(c[0 ... pt])
- rest = c[pt .. -1]
- else
- buf = preprocess(c)
- rest = ''
- end
- else
- # Not Telnetmode.
- #
- # We cannot use preprocess() on this data, because that
- # method makes some Telnetmode-specific assumptions.
- buf = rest + c
- rest = ''
- unless @options["Binmode"]
- if pt = buf.rindex(/\r\z/no)
- buf = buf[0 ... pt]
- rest = buf[pt .. -1]
- end
- buf.gsub!(/#{EOL}/no, "\n")
- end
- end
- @log.print(buf) if @options.has_key?("Output_log")
- line += buf
- yield buf if block_given?
- rescue EOFError # End of file reached
- raise if fail_eof
- if line == ''
- line = nil
- yield nil if block_given?
- end
- break
- end
- end
- line
- end
-
- # Write +string+ to the host.
- #
- # Does not perform any conversions on +string+. Will log +string+ to the
- # dumplog, if the Dump_log option is set.
- def write(string)
- length = string.length
- while 0 < length
- IO::select(nil, [@sock])
- @dumplog.log_dump('>', string[-length..-1]) if @options.has_key?("Dump_log")
- length -= @sock.syswrite(string[-length..-1])
- end
- end
-
- # Sends a string to the host.
- #
- # This does _not_ automatically append a newline to the string. Embedded
- # newlines may be converted and telnet command sequences escaped
- # depending upon the values of telnetmode, binmode, and telnet options
- # set by the host.
- def print(string)
- string = string.gsub(/#{IAC}/no, IAC + IAC) if @options["Telnetmode"]
-
- if @options["Binmode"]
- self.write(string)
- else
- if @telnet_option["BINARY"] and @telnet_option["SGA"]
- # IAC WILL SGA IAC DO BIN send EOL --> CR
- self.write(string.gsub(/\n/n, CR))
- elsif @telnet_option["SGA"]
- # IAC WILL SGA send EOL --> CR+NULL
- self.write(string.gsub(/\n/n, CR + NULL))
- else
- # NONE send EOL --> CR+LF
- self.write(string.gsub(/\n/n, EOL))
- end
- end
- end
-
- # Sends a string to the host.
- #
- # Same as #print(), but appends a newline to the string.
- def puts(string)
- self.print(string + "\n")
- end
-
- # Send a command to the host.
- #
- # More exactly, sends a string to the host, and reads in all received
- # data until is sees the prompt or other matched sequence.
- #
- # If a block is given, the received data will be yielded to it as
- # it is read in. Whether a block is given or not, the received data
- # will be return as a string. Note that the received data includes
- # the prompt and in most cases the host's echo of our command.
- #
- # +options+ is either a String, specified the string or command to
- # send to the host; or it is a hash of options. If a hash, the
- # following options can be specified:
- #
- # String:: the command or other string to send to the host.
- # Match:: a regular expression, the sequence to look for in
- # the received data before returning. If not specified,
- # the Prompt option value specified when this instance
- # was created will be used, or, failing that, the default
- # prompt of /[$%#>] \z/n.
- # Timeout:: the seconds to wait for data from the host before raising
- # a Timeout error. If not specified, the Timeout option
- # value specified when this instance was created will be
- # used, or, failing that, the default value of 10 seconds.
- #
- # The command or other string will have the newline sequence appended
- # to it.
- def cmd(options) # :yield: recvdata
- match = @options["Prompt"]
- time_out = @options["Timeout"]
-
- if options.kind_of?(Hash)
- string = options["String"]
- match = options["Match"] if options.has_key?("Match")
- time_out = options["Timeout"] if options.has_key?("Timeout")
- else
- string = options
- end
-
- self.puts(string)
- if block_given?
- waitfor({"Prompt" => match, "Timeout" => time_out}){|c| yield c }
- else
- waitfor({"Prompt" => match, "Timeout" => time_out})
- end
- end
-
- # Login to the host with a given username and password.
- #
- # The username and password can either be provided as two string
- # arguments in that order, or as a hash with keys "Name" and
- # "Password".
- #
- # This method looks for the strings "login" and "Password" from the
- # host to determine when to send the username and password. If the
- # login sequence does not follow this pattern (for instance, you
- # are connecting to a service other than telnet), you will need
- # to handle login yourself.
- #
- # The password can be omitted, either by only
- # provided one String argument, which will be used as the username,
- # or by providing a has that has no "Password" key. In this case,
- # the method will not look for the "Password:" prompt; if it is
- # sent, it will have to be dealt with by later calls.
- #
- # The method returns all data received during the login process from
- # the host, including the echoed username but not the password (which
- # the host should not echo). If a block is passed in, this received
- # data is also yielded to the block as it is received.
- def login(options, password = nil) # :yield: recvdata
- login_prompt = /[Ll]ogin[: ]*\z/n
- password_prompt = /[Pp]ass(?:word|phrase)[: ]*\z/n
- if options.kind_of?(Hash)
- username = options["Name"]
- password = options["Password"]
- login_prompt = options["LoginPrompt"] if options["LoginPrompt"]
- password_prompt = options["PasswordPrompt"] if options["PasswordPrompt"]
- else
- username = options
- end
-
- if block_given?
- line = waitfor(login_prompt){|c| yield c }
- if password
- line += cmd({"String" => username,
- "Match" => password_prompt}){|c| yield c }
- line += cmd(password){|c| yield c }
- else
- line += cmd(username){|c| yield c }
- end
- else
- line = waitfor(login_prompt)
- if password
- line += cmd({"String" => username,
- "Match" => password_prompt})
- line += cmd(password)
- else
- line += cmd(username)
- end
- end
- line
- end
-
- end # class Telnet
-end # module Net
-
diff --git a/lib/observer.rb b/lib/observer.rb
deleted file mode 100644
index 472a154395..0000000000
--- a/lib/observer.rb
+++ /dev/null
@@ -1,193 +0,0 @@
-#
-# observer.rb implements the _Observer_ object-oriented design pattern. The
-# following documentation is copied, with modifications, from "Programming
-# Ruby", by Hunt and Thomas; http://www.rubycentral.com/book/lib_patterns.html.
-#
-# == About
-#
-# The Observer pattern, also known as Publish/Subscribe, provides a simple
-# mechanism for one object to inform a set of interested third-party objects
-# when its state changes.
-#
-# == Mechanism
-#
-# In the Ruby implementation, the notifying class mixes in the +Observable+
-# module, which provides the methods for managing the associated observer
-# objects.
-#
-# The observers must implement the +update+ method to receive notifications.
-#
-# The observable object must:
-# * assert that it has +changed+
-# * call +notify_observers+
-#
-# == Example
-#
-# The following example demonstrates this nicely. A +Ticker+, when run,
-# continually receives the stock +Price+ for its +@symbol+. A +Warner+ is a
-# general observer of the price, and two warners are demonstrated, a +WarnLow+
-# and a +WarnHigh+, which print a warning if the price is below or above their
-# set limits, respectively.
-#
-# The +update+ callback allows the warners to run without being explicitly
-# called. The system is set up with the +Ticker+ and several observers, and the
-# observers do their duty without the top-level code having to interfere.
-#
-# Note that the contract between publisher and subscriber (observable and
-# observer) is not declared or enforced. The +Ticker+ publishes a time and a
-# price, and the warners receive that. But if you don't ensure that your
-# contracts are correct, nothing else can warn you.
-#
-# require "observer"
-#
-# class Ticker ### Periodically fetch a stock price.
-# include Observable
-#
-# def initialize(symbol)
-# @symbol = symbol
-# end
-#
-# def run
-# lastPrice = nil
-# loop do
-# price = Price.fetch(@symbol)
-# print "Current price: #{price}\n"
-# if price != lastPrice
-# changed # notify observers
-# lastPrice = price
-# notify_observers(Time.now, price)
-# end
-# sleep 1
-# end
-# end
-# end
-#
-# class Price ### A mock class to fetch a stock price (60 - 140).
-# def Price.fetch(symbol)
-# 60 + rand(80)
-# end
-# end
-#
-# class Warner ### An abstract observer of Ticker objects.
-# def initialize(ticker, limit)
-# @limit = limit
-# ticker.add_observer(self)
-# end
-# end
-#
-# class WarnLow < Warner
-# def update(time, price) # callback for observer
-# if price < @limit
-# print "--- #{time.to_s}: Price below #@limit: #{price}\n"
-# end
-# end
-# end
-#
-# class WarnHigh < Warner
-# def update(time, price) # callback for observer
-# if price > @limit
-# print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
-# end
-# end
-# end
-#
-# ticker = Ticker.new("MSFT")
-# WarnLow.new(ticker, 80)
-# WarnHigh.new(ticker, 120)
-# ticker.run
-#
-# Produces:
-#
-# Current price: 83
-# Current price: 75
-# --- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 75
-# Current price: 90
-# Current price: 134
-# +++ Sun Jun 09 00:10:25 CDT 2002: Price above 120: 134
-# Current price: 134
-# Current price: 112
-# Current price: 79
-# --- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 79
-
-
-#
-# Implements the Observable design pattern as a mixin so that other objects can
-# be notified of changes in state. See observer.rb for details and an example.
-#
-module Observable
-
- #
- # Add +observer+ as an observer on this object. +observer+ will now receive
- # notifications. The second optional argument specifies a method to notify
- # updates, of which default value is +update+.
- #
- def add_observer(observer, func=:update)
- @observer_peers = {} unless defined? @observer_peers
- unless observer.respond_to? func
- raise NoMethodError, "observer does not respond to `#{func.to_s}'"
- end
- @observer_peers[observer] = func
- end
-
- #
- # Delete +observer+ as an observer on this object. It will no longer receive
- # notifications.
- #
- def delete_observer(observer)
- @observer_peers.delete observer if defined? @observer_peers
- end
-
- #
- # Delete all observers associated with this object.
- #
- def delete_observers
- @observer_peers.clear if defined? @observer_peers
- end
-
- #
- # Return the number of observers associated with this object.
- #
- def count_observers
- if defined? @observer_peers
- @observer_peers.size
- else
- 0
- end
- end
-
- #
- # Set the changed state of this object. Notifications will be sent only if
- # the changed +state+ is +true+.
- #
- def changed(state=true)
- @observer_state = state
- end
-
- #
- # Query the changed state of this object.
- #
- def changed?
- if defined? @observer_state and @observer_state
- true
- else
- false
- end
- end
-
- #
- # If this object's changed state is +true+, invoke the update method in each
- # currently associated observer in turn, passing it the given arguments. The
- # changed state is then set to +false+.
- #
- def notify_observers(*arg)
- if defined? @observer_state and @observer_state
- if defined? @observer_peers
- @observer_peers.each { |k, v|
- k.send v, *arg
- }
- end
- @observer_state = false
- end
- end
-
-end
diff --git a/lib/open-uri.gemspec b/lib/open-uri.gemspec
new file mode 100644
index 0000000000..b6aaf35200
--- /dev/null
+++ b/lib/open-uri.gemspec
@@ -0,0 +1,32 @@
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", "."].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Tanaka Akira"]
+ spec.email = ["akr@fsij.org"]
+
+ spec.summary = %q{An easy-to-use wrapper for Net::HTTP, Net::HTTPS and Net::FTP.}
+ spec.description = %q{An easy-to-use wrapper for Net::HTTP, Net::HTTPS and Net::FTP.}
+ spec.homepage = "https://github.com/ruby/open-uri"
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A((bin|test|spec|features)/|\.git|[Rr]ake|Gemfile)|\.gemspec\z}) }
+ end
+ spec.executables = []
+ spec.require_paths = ["lib"]
+
+ spec.add_dependency "uri"
+ spec.add_dependency "stringio"
+ spec.add_dependency "time"
+end
diff --git a/lib/open-uri.rb b/lib/open-uri.rb
index 2a6c544fe6..844865b13a 100644
--- a/lib/open-uri.rb
+++ b/lib/open-uri.rb
@@ -1,30 +1,29 @@
+# frozen_string_literal: true
require 'uri'
require 'stringio'
require 'time'
-module Kernel
- private
- alias open_uri_original_open open # :nodoc:
- class << self
- alias open_uri_original_open open # :nodoc:
- end
-
- # makes possible to open various resources including URIs.
- # If the first argument respond to `open' method,
- # the method is called with the rest arguments.
+module URI
+ # Allows the opening of various resources including URIs. Example:
+ #
+ # require "open-uri"
+ # URI.open("http://example.com") { |f| f.read }
+ #
+ # If the first argument responds to the +open+ method, +open+ is called on
+ # it with the rest of the arguments.
+ #
+ # If the first argument is a string that begins with <code>(protocol)://</code>, it is parsed by
+ # URI.parse. If the parsed object responds to the +open+ method,
+ # +open+ is called on it with the rest of the arguments.
#
- # If the first argument is a string which begins with xxx://,
- # it is parsed by URI.parse. If the parsed object respond to `open' method,
- # the method is called with the rest arguments.
+ # Otherwise, Kernel#open is called.
#
- # Otherwise original open is called.
+ # OpenURI::OpenRead#open provides URI::HTTP#open, URI::HTTPS#open and
+ # URI::FTP#open, Kernel#open.
#
- # Since open-uri.rb provides URI::HTTP#open, URI::HTTPS#open and
- # URI::FTP#open,
- # Kernel[#.]open can accepts such URIs and strings which begins with
- # http://, https:// and ftp://.
- # In these case, the opened file object is extended by OpenURI::Meta.
- def open(name, *rest, &block) # :doc:
+ # We can accept URIs and strings that begin with <code>http://</code>, <code>https://</code> and
+ # <code>ftp://</code>. In these cases, the opened file object is extended by OpenURI::Meta.
+ def self.open(name, *rest, &block)
if name.respond_to?(:open)
name.open(*rest, &block)
elsif name.respond_to?(:to_str) &&
@@ -32,26 +31,26 @@ module Kernel
(uri = URI.parse(name)).respond_to?(:open)
uri.open(*rest, &block)
else
- open_uri_original_open(name, *rest, &block)
+ super
end
end
- module_function :open
+ singleton_class.send(:ruby2_keywords, :open) if respond_to?(:ruby2_keywords, true)
end
-# OpenURI is an easy-to-use wrapper for net/http, net/https and net/ftp.
+# OpenURI is an easy-to-use wrapper for Net::HTTP, Net::HTTPS and Net::FTP.
#
-#== Example
+# == Example
#
-# It is possible to open http/https/ftp URL as usual like opening a file:
+# It is possible to open an http, https or ftp URL as though it were a file:
#
-# open("http://www.ruby-lang.org/") {|f|
+# URI.open("http://www.ruby-lang.org/") {|f|
# f.each_line {|line| p line}
# }
#
-# The opened file has several methods for meta information as follows since
-# it is extended by OpenURI::Meta.
+# The opened file has several getter methods for its meta-information, as
+# follows, since it is extended by OpenURI::Meta.
#
-# open("http://www.ruby-lang.org/en") {|f|
+# URI.open("http://www.ruby-lang.org/en") {|f|
# f.each_line {|line| p line}
# p f.base_uri # <URI::HTTP:0x40e6ef2 URL:http://www.ruby-lang.org/en/>
# p f.content_type # "text/html"
@@ -62,7 +61,7 @@ end
#
# Additional header fields can be specified by an optional hash argument.
#
-# open("http://www.ruby-lang.org/en/",
+# URI.open("http://www.ruby-lang.org/en/",
# "User-Agent" => "Ruby/#{RUBY_VERSION}",
# "From" => "foo@bar.invalid",
# "Referer" => "http://www.ruby-lang.org/") {|f|
@@ -70,12 +69,14 @@ end
# }
#
# The environment variables such as http_proxy, https_proxy and ftp_proxy
-# are in effect by default. :proxy => nil disables proxy.
+# are in effect by default. Here we disable proxy:
#
-# open("http://www.ruby-lang.org/en/raa.html", :proxy => nil) {|f|
+# URI.open("http://www.ruby-lang.org/en/", :proxy => nil) {|f|
# # ...
# }
#
+# See OpenURI::OpenRead.open and URI.open for more on available options.
+#
# URI objects can be opened in a similar way.
#
# uri = URI.parse("http://www.ruby-lang.org/en/")
@@ -92,6 +93,11 @@ end
# Author:: Tanaka Akira <akr@m17n.org>
module OpenURI
+
+ # The version string
+ VERSION = "0.5.0"
+
+ # The default options
Options = {
:proxy => true,
:proxy_http_basic_authentication => true,
@@ -99,10 +105,16 @@ module OpenURI
:content_length_proc => true,
:http_basic_authentication => true,
:read_timeout => true,
+ :open_timeout => true,
:ssl_ca_cert => nil,
:ssl_verify_mode => nil,
+ :ssl_min_version => nil,
+ :ssl_max_version => nil,
:ftp_active_mode => false,
:redirect => true,
+ :encoding => nil,
+ :max_redirects => 64,
+ :request_specific_fields => nil,
}
def OpenURI.check_options(options) # :nodoc:
@@ -126,7 +138,7 @@ module OpenURI
def OpenURI.open_uri(name, *rest) # :nodoc:
uri = URI::Generic === name ? name : URI.parse(name)
- mode, perm, rest = OpenURI.scan_open_optional_arguments(*rest)
+ mode, _, rest = OpenURI.scan_open_optional_arguments(*rest)
options = rest.shift if !rest.empty? && Hash === rest.first
raise ArgumentError.new("extra arguments") if !rest.empty?
options ||= {}
@@ -136,7 +148,17 @@ module OpenURI
encoding, = $1,Encoding.find($1) if $1
mode = nil
end
-
+ if options.has_key? :encoding
+ if !encoding.nil?
+ raise ArgumentError, "encoding specified twice"
+ end
+ encoding = Encoding.find(options[:encoding])
+ end
+ if options.has_key? :request_specific_fields
+ if !(options[:request_specific_fields].is_a?(Hash) || options[:request_specific_fields].is_a?(Proc))
+ raise ArgumentError, "Invalid request_specific_fields option: #{options[:request_specific_fields].inspect}"
+ end
+ end
unless mode == nil ||
mode == 'r' || mode == 'rb' ||
mode == File::RDONLY
@@ -149,7 +171,11 @@ module OpenURI
begin
yield io
ensure
- io.close
+ if io.respond_to? :close!
+ io.close! # Tempfile
+ else
+ io.close if !io.closed?
+ end
end
else
io
@@ -196,11 +222,20 @@ module OpenURI
end
uri_set = {}
+ max_redirects = options[:max_redirects] || Options.fetch(:max_redirects)
buf = nil
while true
+ request_specific_fields = {}
+ if options.has_key? :request_specific_fields
+ request_specific_fields = if options[:request_specific_fields].is_a?(Hash)
+ options[:request_specific_fields]
+ else options[:request_specific_fields].is_a?(Proc)
+ options[:request_specific_fields].call(uri)
+ end
+ end
redirect = catch(:open_uri_redirect) {
buf = Buffer.new
- uri.buffer_open(buf, find_proxy.call(uri), options)
+ uri.buffer_open(buf, find_proxy.call(uri), options.merge(request_specific_fields))
nil
}
if redirect
@@ -220,9 +255,14 @@ module OpenURI
options = options.dup
options.delete :http_basic_authentication
end
+ if options.include?(:request_specific_fields) && options[:request_specific_fields].is_a?(Hash)
+ # Send request specific headers only for the initial request.
+ options.delete :request_specific_fields
+ end
uri = redirect
raise "HTTP redirection loop: #{uri}" if uri_set.include? uri.to_s
uri_set[uri.to_s] = true
+ raise TooManyRedirects.new("Too many redirects", buf.io) if max_redirects && uri_set.size > max_redirects
else
break
end
@@ -234,13 +274,13 @@ module OpenURI
def OpenURI.redirectable?(uri1, uri2) # :nodoc:
# This test is intended to forbid a redirection from http://... to
- # file:///etc/passwd.
+ # file:///etc/passwd, file:///dev/zero, etc. CVE-2011-1521
# https to http redirect is also forbidden intentionally.
# It avoids sending secure cookie or referer by non-secure HTTP protocol.
# (RFC 2109 4.3.1, RFC 2965 3.3, RFC 2616 15.1.3)
# However this is ad hoc. It should be extensible/configurable.
uri1.scheme.downcase == uri2.scheme.downcase ||
- (/\A(?:http|ftp)\z/i =~ uri1.scheme && /\A(?:http|ftp)\z/i =~ uri2.scheme)
+ (/\A(?:http|ftp)\z/i =~ uri1.scheme && /\A(?:https?|ftp)\z/i =~ uri2.scheme)
end
def OpenURI.open_http(buf, target, proxy, options) # :nodoc:
@@ -249,8 +289,7 @@ module OpenURI
raise "Non-HTTP proxy URI: #{proxy_uri}" if proxy_uri.class != URI::HTTP
end
- if target.userinfo && "1.9.0" <= RUBY_VERSION
- # don't raise for 1.8 because compatibility.
+ if target.userinfo
raise ArgumentError, "userinfo not supported. [RFC3986]"
end
@@ -262,36 +301,44 @@ module OpenURI
if URI::HTTP === target
# HTTP or HTTPS
if proxy
+ unless proxy_user && proxy_pass
+ proxy_user, proxy_pass = proxy_uri.userinfo.split(':') if proxy_uri.userinfo
+ end
if proxy_user && proxy_pass
- klass = Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_user, proxy_pass)
+ klass = Net::HTTP::Proxy(proxy_uri.hostname, proxy_uri.port, proxy_user, proxy_pass)
else
- klass = Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port)
+ klass = Net::HTTP::Proxy(proxy_uri.hostname, proxy_uri.port)
end
end
- target_host = target.host
+ target_host = target.hostname
target_port = target.port
request_uri = target.request_uri
else
# FTP over HTTP proxy
- target_host = proxy_uri.host
+ target_host = proxy_uri.hostname
target_port = proxy_uri.port
request_uri = target.to_s
if proxy_user && proxy_pass
- header["Proxy-Authorization"] = 'Basic ' + ["#{proxy_user}:#{proxy_pass}"].pack('m').delete("\r\n")
+ header["Proxy-Authorization"] =
+ 'Basic ' + ["#{proxy_user}:#{proxy_pass}"].pack('m0')
end
end
- http = klass.new(target_host, target_port)
+ http = proxy ? klass.new(target_host, target_port) : klass.new(target_host, target_port, nil)
if target.class == URI::HTTPS
require 'net/https'
http.use_ssl = true
http.verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
+ http.min_version = options[:ssl_min_version]
+ http.max_version = options[:ssl_max_version]
store = OpenSSL::X509::Store.new
if options[:ssl_ca_cert]
- if File.directory? options[:ssl_ca_cert]
- store.add_path options[:ssl_ca_cert]
- else
- store.add_file options[:ssl_ca_cert]
+ Array(options[:ssl_ca_cert]).each do |cert|
+ if File.directory? cert
+ store.add_path cert
+ else
+ store.add_file cert
+ end
end
else
store.set_default_paths
@@ -301,6 +348,9 @@ module OpenURI
if options.include? :read_timeout
http.read_timeout = options[:read_timeout]
end
+ if options.include? :open_timeout
+ http.open_timeout = options[:open_timeout]
+ end
resp = nil
http.start {
@@ -323,19 +373,21 @@ module OpenURI
if options[:progress_proc] && Net::HTTPSuccess === resp
options[:progress_proc].call(buf.size)
end
+ str.clear
}
}
}
io = buf.io
io.rewind
io.status = [resp.code, resp.message]
- resp.each {|name,value| buf.io.meta_add_field name, value }
+ resp.each_name {|name| buf.io.meta_add_field2 name, resp.get_fields(name) }
case resp
when Net::HTTPSuccess
when Net::HTTPMovedPermanently, # 301
Net::HTTPFound, # 302
Net::HTTPSeeOther, # 303
- Net::HTTPTemporaryRedirect # 307
+ Net::HTTPTemporaryRedirect, # 307
+ Net::HTTPPermanentRedirect # 308
begin
loc_uri = URI.parse(resp['location'])
rescue URI::InvalidURIError
@@ -347,23 +399,32 @@ module OpenURI
end
end
+ # Raised on HTTP session failure
class HTTPError < StandardError
- def initialize(message, io)
+ def initialize(message, io) # :nodoc:
super(message)
@io = io
end
+ # StringIO having the received data
attr_reader :io
end
+ # Raised on redirection,
+ # only occurs when +redirect+ option for HTTP is +false+.
class HTTPRedirect < HTTPError
- def initialize(message, io, uri)
+ def initialize(message, io, uri) # :nodoc:
super(message, io)
@uri = uri
end
+ # URI to redirect
attr_reader :uri
end
- class Buffer # :nodoc:
+ # Raised on too many redirection,
+ class TooManyRedirects < HTTPError
+ end
+
+ class Buffer # :nodoc: all
def initialize
@io = StringIO.new
@size = 0
@@ -390,34 +451,50 @@ module OpenURI
end
end
+ # :stopdoc:
+ RE_LWS = /[\r\n\t ]+/n
+ RE_TOKEN = %r{[^\x00- ()<>@,;:\\"/\[\]?={}\x7f]+}n
+ RE_QUOTED_STRING = %r{"(?:[\r\n\t !#-\[\]-~\x80-\xff]|\\[\x00-\x7f])*"}n
+ RE_PARAMETERS = %r{(?:;#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?=#{RE_LWS}?(?:#{RE_TOKEN}|#{RE_QUOTED_STRING})#{RE_LWS}?)*}n
+ # :startdoc:
+
# Mixin for holding meta-information.
module Meta
def Meta.init(obj, src=nil) # :nodoc:
obj.extend Meta
obj.instance_eval {
@base_uri = nil
- @meta = {}
+ @meta = {} # name to string. legacy.
+ @metas = {} # name to array of strings.
}
if src
obj.status = src.status
obj.base_uri = src.base_uri
- src.meta.each {|name, value|
- obj.meta_add_field(name, value)
+ src.metas.each {|name, values|
+ obj.meta_add_field2(name, values)
}
end
end
- # returns an Array which consists status code and message.
+ # returns an Array that consists of status code and message.
attr_accessor :status
- # returns a URI which is base of relative URIs in the data.
- # It may differ from the URI supplied by a user because redirection.
+ # returns a URI that is the base of relative URIs in the data.
+ # It may differ from the URI supplied by a user due to redirection.
attr_accessor :base_uri
- # returns a Hash which represents header fields.
+ # returns a Hash that represents header fields.
# The Hash keys are downcased for canonicalization.
+ # The Hash values are a field body.
+ # If there are multiple field with same field name,
+ # the field values are concatenated with a comma.
attr_reader :meta
+ # returns a Hash that represents header fields.
+ # The Hash keys are downcased for canonicalization.
+ # The Hash value are an array of field values.
+ attr_reader :metas
+
def meta_setup_encoding # :nodoc:
charset = self.charset
enc = nil
@@ -437,35 +514,38 @@ module OpenURI
end
end
- def meta_add_field(name, value) # :nodoc:
+ def meta_add_field2(name, values) # :nodoc:
name = name.downcase
- @meta[name] = value
+ @metas[name] = values
+ @meta[name] = values.join(', ')
meta_setup_encoding if name == 'content-type'
end
- # returns a Time which represents Last-Modified field.
+ def meta_add_field(name, value) # :nodoc:
+ meta_add_field2(name, [value])
+ end
+
+ # returns a Time that represents the Last-Modified field.
def last_modified
- if v = @meta['last-modified']
+ if vs = @metas['last-modified']
+ v = vs.join(', ')
Time.httpdate(v)
else
nil
end
end
- RE_LWS = /[\r\n\t ]+/n
- RE_TOKEN = %r{[^\x00- ()<>@,;:\\"/\[\]?={}\x7f]+}n
- RE_QUOTED_STRING = %r{"(?:[\r\n\t !#-\[\]-~\x80-\xff]|\\[\x00-\x7f])*"}n
- RE_PARAMETERS = %r{(?:;#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?=#{RE_LWS}?(?:#{RE_TOKEN}|#{RE_QUOTED_STRING})#{RE_LWS}?)*}n
-
def content_type_parse # :nodoc:
- v = @meta['content-type']
+ vs = @metas['content-type']
# The last (?:;#{RE_LWS}?)? matches extra ";" which violates RFC2045.
- if v && %r{\A#{RE_LWS}?(#{RE_TOKEN})#{RE_LWS}?/(#{RE_TOKEN})#{RE_LWS}?(#{RE_PARAMETERS})(?:;#{RE_LWS}?)?\z}no =~ v
+ if vs && %r{\A#{RE_LWS}?(#{RE_TOKEN})#{RE_LWS}?/(#{RE_TOKEN})#{RE_LWS}?(#{RE_PARAMETERS})(?:;#{RE_LWS}?)?\z}no =~ vs.join(', ')
type = $1.downcase
subtype = $2.downcase
parameters = []
$3.scan(/;#{RE_LWS}?(#{RE_TOKEN})#{RE_LWS}?=#{RE_LWS}?(?:(#{RE_TOKEN})|(#{RE_QUOTED_STRING}))/no) {|att, val, qval|
- val = qval.gsub(/[\r\n\t !#-\[\]-~\x80-\xff]+|(\\[\x00-\x7f])/n) { $1 ? $1[1,1] : $& } if qval
+ if qval
+ val = qval[1...-1].gsub(/[\r\n\t !#-\[\]-~\x80-\xff]+|(\\[\x00-\x7f])/n) { $1 ? $1[1,1] : $& }
+ end
parameters << [att.downcase, val]
}
["#{type}/#{subtype}", *parameters]
@@ -478,7 +558,7 @@ module OpenURI
# It is downcased for canonicalization.
# Content-Type parameters are stripped.
def content_type
- type, *parameters = content_type_parse
+ type, *_ = content_type_parse
type || 'application/octet-stream'
end
@@ -490,28 +570,28 @@ module OpenURI
# It can be used to guess charset.
#
# If charset parameter and block is not given,
- # nil is returned except text type in HTTP.
- # In that case, "iso-8859-1" is returned as defined by RFC2616 3.7.1.
+ # nil is returned except text type.
+ # In that case, "utf-8" is returned as defined by RFC6838 4.2.1
def charset
type, *parameters = content_type_parse
if pair = parameters.assoc('charset')
pair.last.downcase
elsif block_given?
yield
- elsif type && %r{\Atext/} =~ type &&
- @base_uri && /\Ahttp\z/i =~ @base_uri.scheme
- "iso-8859-1" # RFC2616 3.7.1
+ elsif type && %r{\Atext/} =~ type
+ "utf-8" # RFC6838 4.2.1
else
nil
end
end
- # returns a list of encodings in Content-Encoding field
- # as an Array of String.
+ # Returns a list of encodings in Content-Encoding field as an array of
+ # strings.
+ #
# The encodings are downcased for canonicalization.
def content_encoding
- v = @meta['content-encoding']
- if v && %r{\A#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?(?:,#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?)*}o =~ v
+ vs = @metas['content-encoding']
+ if vs && %r{\A#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?(?:,#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?)*}o =~ (v = vs.join(', '))
v.scan(RE_TOKEN).map {|content_coding| content_coding.downcase}
else
[]
@@ -524,22 +604,25 @@ module OpenURI
# OpenURI::OpenRead#open provides `open' for URI::HTTP and URI::FTP.
#
# OpenURI::OpenRead#open takes optional 3 arguments as:
- # OpenURI::OpenRead#open([mode [, perm]] [, options]) [{|io| ... }]
#
- # `mode', `perm' is same as Kernel#open.
+ # OpenURI::OpenRead#open([mode [, perm]] [, options]) [{|io| ... }]
+ #
+ # OpenURI::OpenRead#open returns an IO-like object if block is not given.
+ # Otherwise it yields the IO object and return the value of the block.
+ # The IO object is extended with OpenURI::Meta.
+ #
+ # +mode+ and +perm+ are the same as Kernel#open.
#
- # However, `mode' must be read mode because OpenURI::OpenRead#open doesn't
+ # However, +mode+ must be read mode because OpenURI::OpenRead#open doesn't
# support write mode (yet).
- # Also `perm' is just ignored because it is meaningful only for file
- # creation.
+ # Also +perm+ is ignored because it is meaningful only for file creation.
#
- # `options' must be a hash.
+ # +options+ must be a hash.
#
- # Each pairs which key is a string in the hash specify a extra header
- # field for HTTP.
- # I.e. it is ignored for FTP without HTTP proxy.
+ # Each option with a string key specifies an extra header field for HTTP.
+ # I.e., it is ignored for FTP without HTTP proxy.
#
- # The hash may include other options which key is a symbol:
+ # The hash may include other options, where keys are symbols:
#
# [:proxy]
# Synopsis:
@@ -551,22 +634,28 @@ module OpenURI
#
# If :proxy option is specified, the value should be String, URI,
# boolean or nil.
+ #
# When String or URI is given, it is treated as proxy URI.
+ #
# When true is given or the option itself is not specified,
# environment variable `scheme_proxy' is examined.
# `scheme' is replaced by `http', `https' or `ftp'.
+ #
# When false or nil is given, the environment variables are ignored and
# connection will be made to a server directly.
#
# [:proxy_http_basic_authentication]
# Synopsis:
- # :proxy_http_basic_authentication => ["http://proxy.foo.com:8000/", "proxy-user", "proxy-password"]
- # :proxy_http_basic_authentication => [URI.parse("http://proxy.foo.com:8000/"), "proxy-user", "proxy-password"]
+ # :proxy_http_basic_authentication =>
+ # ["http://proxy.foo.com:8000/", "proxy-user", "proxy-password"]
+ # :proxy_http_basic_authentication =>
+ # [URI.parse("http://proxy.foo.com:8000/"),
+ # "proxy-user", "proxy-password"]
#
- # If :proxy option is specified, the value should be an Array with 3 elements.
- # It should contain a proxy URI, a proxy user name and a proxy password.
- # The proxy URI should be a String, an URI or nil.
- # The proxy user name and password should be a String.
+ # If :proxy option is specified, the value should be an Array with 3
+ # elements. It should contain a proxy URI, a proxy user name and a proxy
+ # password. The proxy URI should be a String, an URI or nil. The proxy
+ # user name and password should be a String.
#
# If nil is given for the proxy URI, this option is just ignored.
#
@@ -588,14 +677,13 @@ module OpenURI
#
# If :content_length_proc option is specified, the option value procedure
# is called before actual transfer is started.
- # It takes one argument which is expected content length in bytes.
+ # It takes one argument, which is expected content length in bytes.
#
- # If two or more transfer is done by HTTP redirection, the procedure
- # is called only one for a last transfer.
+ # If two or more transfers are performed by HTTP redirection, the
+ # procedure is called only once for the last transfer.
#
# When expected content length is unknown, the procedure is called with
- # nil.
- # It is happen when HTTP response has no Content-Length header.
+ # nil. This happens when the HTTP response has no Content-Length header.
#
# [:progress_proc]
# Synopsis:
@@ -603,7 +691,7 @@ module OpenURI
#
# If :progress_proc option is specified, the proc is called with one
# argument each time when `open' gets content fragment from network.
- # The argument `size' `size' is a accumulated transfered size in bytes.
+ # The argument +size+ is the accumulated transferred size in bytes.
#
# If two or more transfer is done by HTTP redirection, the procedure
# is called only one for a last transfer.
@@ -631,9 +719,16 @@ module OpenURI
#
# :read_timeout option specifies a timeout of read for http connections.
#
+ # [:open_timeout]
+ # Synopsis:
+ # :open_timeout=>nil (no timeout)
+ # :open_timeout=>10 (10 second)
+ #
+ # :open_timeout option specifies a timeout of open for http connections.
+ #
# [:ssl_ca_cert]
# Synopsis:
- # :ssl_ca_cert=>filename
+ # :ssl_ca_cert=>filename or an Array of filenames
#
# :ssl_ca_cert is used to specify CA certificate for SSL.
# If it is given, default certificates are not used.
@@ -644,35 +739,85 @@ module OpenURI
#
# :ssl_verify_mode is used to specify openssl verify mode.
#
- # OpenURI::OpenRead#open returns an IO like object if block is not given.
- # Otherwise it yields the IO object and return the value of the block.
- # The IO object is extended with OpenURI::Meta.
+ # [:ssl_min_version]
+ # Synopsis:
+ # :ssl_min_version=>:TLS1_2
+ #
+ # :ssl_min_version option specifies the minimum allowed SSL/TLS protocol
+ # version. See also OpenSSL::SSL::SSLContext#min_version=.
+ #
+ # [:ssl_max_version]
+ # Synopsis:
+ # :ssl_max_version=>:TLS1_2
+ #
+ # :ssl_max_version option specifies the maximum allowed SSL/TLS protocol
+ # version. See also OpenSSL::SSL::SSLContext#max_version=.
#
# [:ftp_active_mode]
# Synopsis:
# :ftp_active_mode=>bool
#
- # :ftp_active_mode=>true is used to make ftp active mode.
- # Note that the active mode is default in Ruby 1.8 or prior.
- # Ruby 1.9 uses passive mode by default.
+ # <tt>:ftp_active_mode => true</tt> is used to make ftp active mode.
+ # Ruby 1.9 uses passive mode by default.
+ # Note that the active mode is default in Ruby 1.8 or prior.
#
# [:redirect]
# Synopsis:
# :redirect=>bool
#
- # :redirect=>false is used to disable HTTP redirects at all.
- # OpenURI::HTTPRedirect exception raised on redirection.
- # It is true by default.
- # The true means redirections between http and ftp is permitted.
+ # +:redirect+ is true by default. <tt>:redirect => false</tt> is used to
+ # disable all HTTP redirects.
+ #
+ # OpenURI::HTTPRedirect exception raised on redirection.
+ # Using +true+ also means that redirections between http and ftp are
+ # permitted.
+ #
+ # [:max_redirects]
+ # Synopsis:
+ # :max_redirects=>int
+ #
+ # Number of HTTP redirects allowed before OpenURI::TooManyRedirects is raised.
+ # The default is 64.
+ #
+ # [:request_specific_fields]
+ # Synopsis:
+ # :request_specific_fields => {}
+ # :request_specific_fields => lambda {|url| ...}
+ #
+ # :request_specific_fields option allows specifying custom header fields that
+ # are sent with the HTTP request. It can be passed as a Hash or a Proc that
+ # gets evaluated on each request and returns a Hash of header fields.
+ #
+ # If a Hash is provided, it specifies the headers only for the initial
+ # request and these headers will not be sent on redirects.
+ #
+ # If a Proc is provided, it will be executed for each request including
+ # redirects, allowing dynamic header customization based on the request URL.
+ # It is important that the Proc returns a Hash. And this Hash specifies the
+ # headers to be sent with the request.
+ #
+ # For Example with Hash
+ # URI.open("http://...",
+ # request_specific_fields: {"Authorization" => "token dummy"}) {|f| ... }
+ #
+ # For Example with Proc:
+ # URI.open("http://...",
+ # request_specific_fields: lambda { |uri|
+ # if uri.host == "example.com"
+ # {"Authorization" => "token dummy"}
+ # else
+ # {}
+ # end
+ # }) {|f| ... }
#
def open(*rest, &block)
OpenURI.open_uri(self, *rest, &block)
end
- # OpenURI::OpenRead#read([options]) reads a content referenced by self and
+ # OpenURI::OpenRead#read([ options ]) reads a content referenced by self and
# returns the content as string.
# The string is extended with OpenURI::Meta.
- # The argument `options' is same as OpenURI::OpenRead#open.
+ # The argument +options+ is same as OpenURI::OpenRead#open.
def read(options={})
self.open(options) {|f|
str = f.read
@@ -684,84 +829,6 @@ module OpenURI
end
module URI
- class Generic
- # returns a proxy URI.
- # The proxy URI is obtained from environment variables such as http_proxy,
- # ftp_proxy, no_proxy, etc.
- # If there is no proper proxy, nil is returned.
- #
- # Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.)
- # are examined too.
- #
- # But http_proxy and HTTP_PROXY is treated specially under CGI environment.
- # It's because HTTP_PROXY may be set by Proxy: header.
- # So HTTP_PROXY is not used.
- # http_proxy is not used too if the variable is case insensitive.
- # CGI_HTTP_PROXY can be used instead.
- def find_proxy
- name = self.scheme.downcase + '_proxy'
- proxy_uri = nil
- if name == 'http_proxy' && ENV.include?('REQUEST_METHOD') # CGI?
- # HTTP_PROXY conflicts with *_proxy for proxy settings and
- # HTTP_* for header information in CGI.
- # So it should be careful to use it.
- pairs = ENV.reject {|k, v| /\Ahttp_proxy\z/i !~ k }
- case pairs.length
- when 0 # no proxy setting anyway.
- proxy_uri = nil
- when 1
- k, v = pairs.shift
- if k == 'http_proxy' && ENV[k.upcase] == nil
- # http_proxy is safe to use because ENV is case sensitive.
- proxy_uri = ENV[name]
- else
- proxy_uri = nil
- end
- else # http_proxy is safe to use because ENV is case sensitive.
- proxy_uri = ENV.to_hash[name]
- end
- if !proxy_uri
- # Use CGI_HTTP_PROXY. cf. libwww-perl.
- proxy_uri = ENV["CGI_#{name.upcase}"]
- end
- elsif name == 'http_proxy'
- unless proxy_uri = ENV[name]
- if proxy_uri = ENV[name.upcase]
- warn 'The environment variable HTTP_PROXY is discouraged. Use http_proxy.'
- end
- end
- else
- proxy_uri = ENV[name] || ENV[name.upcase]
- end
-
- if proxy_uri && self.host
- require 'socket'
- begin
- addr = IPSocket.getaddress(self.host)
- proxy_uri = nil if /\A127\.|\A::1\z/ =~ addr
- rescue SocketError
- end
- end
-
- if proxy_uri
- proxy_uri = URI.parse(proxy_uri)
- name = 'no_proxy'
- if no_proxy = ENV[name] || ENV[name.upcase]
- no_proxy.scan(/([^:,]*)(?::(\d+))?/) {|host, port|
- if /(\A|\.)#{Regexp.quote host}\z/i =~ self.host &&
- (!port || self.port == port.to_i)
- proxy_uri = nil
- break
- end
- }
- end
- proxy_uri
- else
- nil
- end
- end
- end
-
class HTTP
def buffer_open(buf, proxy, options) # :nodoc:
OpenURI.open_http(buf, self, proxy, options)
@@ -776,10 +843,16 @@ module URI
OpenURI.open_http(buf, self, proxy, options)
return
end
- require 'net/ftp'
- directories = self.path.split(%r{/}, -1)
- directories.shift if directories[0] == '' # strip a field before leading slash
+ begin
+ require 'net/ftp'
+ rescue LoadError
+ abort "net/ftp is not found. You may need to `gem install net-ftp` to install net/ftp."
+ end
+
+ path = self.path
+ path = path.sub(%r{\A/}, '%2F') # re-encode the beginning slash because uri library decodes it.
+ directories = path.split(%r{/}, -1)
directories.each {|d|
d.gsub!(/%([0-9A-Fa-f][0-9A-Fa-f])/) { [$1].pack("H2") }
}
@@ -800,8 +873,9 @@ module URI
end
# The access sequence is defined by RFC 1738
- ftp = Net::FTP.open(self.host)
- ftp.passive = true if !options[:ftp_active_mode]
+ ftp = Net::FTP.new
+ ftp.connect(self.hostname, self.port)
+ ftp.passive = !options[:ftp_active_mode]
# todo: extract user/passwd from .netrc.
user = 'anonymous'
passwd = nil
diff --git a/lib/open3.rb b/lib/open3.rb
index d776de7445..74d00b86d9 100644
--- a/lib/open3.rb
+++ b/lib/open3.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#
# = open3.rb: Popen, but with stderr, too
#
@@ -9,90 +11,1400 @@
#
#
-# Open3 grants you access to stdin, stdout, stderr and a thread to wait the
+# Open3 grants you access to stdin, stdout, stderr and a thread to wait for the
# child process when running another program.
+# You can specify various attributes, redirections, current directory, etc., of
+# the program in the same way as for Process.spawn.
+#
+# - Open3.popen3 : pipes for stdin, stdout, stderr
+# - Open3.popen2 : pipes for stdin, stdout
+# - Open3.popen2e : pipes for stdin, merged stdout and stderr
+# - Open3.capture3 : give a string for stdin; get strings for stdout, stderr
+# - Open3.capture2 : give a string for stdin; get a string for stdout
+# - Open3.capture2e : give a string for stdin; get a string for merged stdout and stderr
+# - Open3.pipeline_rw : pipes for first stdin and last stdout of a pipeline
+# - Open3.pipeline_r : pipe for last stdout of a pipeline
+# - Open3.pipeline_w : pipe for first stdin of a pipeline
+# - Open3.pipeline_start : run a pipeline without waiting
+# - Open3.pipeline : run a pipeline and wait for its completion
+#
+
+require 'open3/version'
+
+# \Module \Open3 supports creating child processes
+# with access to their $stdin, $stdout, and $stderr streams.
#
-# Example:
+# == What's Here
#
-# require "open3"
-# include Open3
-#
-# stdin, stdout, stderr, wait_thr = popen3('nroff -man')
+# Each of these methods executes a given command in a new process or subshell,
+# or multiple commands in new processes and/or subshells:
#
-# Open3.popen3 can also take a block which will receive stdin, stdout,
-# stderr and wait_thr as parameters.
-# This ensures stdin, stdout and stderr are closed and
-# the process is terminated once the block exits.
+# - Each of these methods executes a single command in a process or subshell,
+# accepts a string for input to $stdin,
+# and returns string output from $stdout, $stderr, or both:
#
-# Example:
+# - Open3.capture2: Executes the command;
+# returns the string from $stdout.
+# - Open3.capture2e: Executes the command;
+# returns the string from merged $stdout and $stderr.
+# - Open3.capture3: Executes the command;
+# returns strings from $stdout and $stderr.
#
-# require "open3"
+# - Each of these methods executes a single command in a process or subshell,
+# and returns pipes for $stdin, $stdout, and/or $stderr:
#
-# Open3.popen3('nroff -man') { |stdin, stdout, stderr, wait_thr| ... }
+# - Open3.popen2: Executes the command;
+# returns pipes for $stdin and $stdout.
+# - Open3.popen2e: Executes the command;
+# returns pipes for $stdin and merged $stdout and $stderr.
+# - Open3.popen3: Executes the command;
+# returns pipes for $stdin, $stdout, and $stderr.
+#
+# - Each of these methods executes one or more commands in processes and/or subshells,
+# returns pipes for the first $stdin, the last $stdout, or both:
+#
+# - Open3.pipeline_r: Returns a pipe for the last $stdout.
+# - Open3.pipeline_rw: Returns pipes for the first $stdin and the last $stdout.
+# - Open3.pipeline_w: Returns a pipe for the first $stdin.
+# - Open3.pipeline_start: Does not wait for processes to complete.
+# - Open3.pipeline: Waits for processes to complete.
+#
+# Each of the methods above accepts:
+#
+# - An optional hash of environment variable names and values;
+# see {Execution Environment}[rdoc-ref:Process@Execution+Environment].
+# - A required string argument that is a +command_line+ or +exe_path+;
+# see {Argument command_line or exe_path}[rdoc-ref:Process@Argument+command_line+or+exe_path].
+# - An optional hash of execution options;
+# see {Execution Options}[rdoc-ref:Process@Execution+Options].
#
-
module Open3
- #
- # Open stdin, stdout, and stderr streams and start external executable.
- # In addition, a thread for waiting the started process is noticed.
- # The thread has a thread variable :pid which is the pid of the started
- # process.
- #
- # Non-block form:
- #
- # stdin, stdout, stderr, wait_thr = Open3.popen3(cmd)
- # pid = wait_thr[:pid] # pid of the started process.
- # ...
- # stdin.close # stdin, stdout and stderr should be closed in this form.
+
+ # :call-seq:
+ # Open3.popen3([env, ] command_line, options = {}) -> [stdin, stdout, stderr, wait_thread]
+ # Open3.popen3([env, ] exe_path, *args, options = {}) -> [stdin, stdout, stderr, wait_thread]
+ # Open3.popen3([env, ] command_line, options = {}) {|stdin, stdout, stderr, wait_thread| ... } -> object
+ # Open3.popen3([env, ] exe_path, *args, options = {}) {|stdin, stdout, stderr, wait_thread| ... } -> object
+ #
+ # Basically a wrapper for
+ # {Process.spawn}[rdoc-ref:Process.spawn]
+ # that:
+ #
+ # - Creates a child process, by calling Process.spawn with the given arguments.
+ # - Creates streams +stdin+, +stdout+, and +stderr+,
+ # which are the standard input, standard output, and standard error streams
+ # in the child process.
+ # - Creates thread +wait_thread+ that waits for the child process to exit;
+ # the thread has method +pid+, which returns the process ID
+ # of the child process.
+ #
+ # With no block given, returns the array
+ # <tt>[stdin, stdout, stderr, wait_thread]</tt>.
+ # The caller should close each of the three returned streams.
+ #
+ # stdin, stdout, stderr, wait_thread = Open3.popen3('echo')
+ # # => [#<IO:fd 8>, #<IO:fd 10>, #<IO:fd 12>, #<Process::Waiter:0x00007f58d5428f58 run>]
+ # stdin.close
# stdout.close
# stderr.close
- # exit_status = wait_thr.value # Process::Status object returned.
+ # wait_thread.pid # => 2210481
+ # wait_thread.value # => #<Process::Status: pid 2210481 exit 0>
+ #
+ # With a block given, calls the block with the four variables
+ # (three streams and the wait thread)
+ # and returns the block's return value.
+ # The caller need not close the streams:
+ #
+ # Open3.popen3('echo') do |stdin, stdout, stderr, wait_thread|
+ # p stdin
+ # p stdout
+ # p stderr
+ # p wait_thread
+ # p wait_thread.pid
+ # p wait_thread.value
+ # end
+ #
+ # Output:
+ #
+ # #<IO:fd 6>
+ # #<IO:fd 7>
+ # #<IO:fd 9>
+ # #<Process::Waiter:0x00007f58d53606e8 sleep>
+ # 2211047
+ # #<Process::Status: pid 2211047 exit 0>
+ #
+ # Like Process.spawn, this method has potential security vulnerabilities
+ # if called with untrusted input;
+ # see {Command Injection}[rdoc-ref:command_injection.rdoc@Command+Injection].
+ #
+ # Unlike Process.spawn, this method waits for the child process to exit
+ # before returning, so the caller need not do so.
+ #
+ # If the first argument is a hash, it becomes leading argument +env+
+ # in the call to Process.spawn;
+ # see {Execution Environment}[rdoc-ref:Process@Execution+Environment].
+ #
+ # If the last argument is a hash, it becomes trailing argument +options+
+ # in the call to Process.spawn;
+ # see {Execution Options}[rdoc-ref:Process@Execution+Options].
+ #
+ # The single required argument is one of the following:
#
- # Block form:
+ # - +command_line+ if it is a string,
+ # and if it begins with a shell reserved word or special built-in,
+ # or if it contains one or more metacharacters.
+ # - +exe_path+ otherwise.
#
- # Open3.popen3(cmd) { |stdin, stdout, stderr, wait_thr| ... }
+ # <b>Argument +command_line+</b>
#
- # The parameter +cmd+ is passed directly to Kernel#spawn.
+ # \String argument +command_line+ is a command line to be passed to a shell;
+ # it must begin with a shell reserved word, begin with a special built-in,
+ # or contain meta characters:
#
- # wait_thr.value waits the termination of the process.
- # The block form also waits the process when it returns.
+ # Open3.popen3('if true; then echo "Foo"; fi') {|*args| p args } # Shell reserved word.
+ # Open3.popen3('echo') {|*args| p args } # Built-in.
+ # Open3.popen3('date > date.tmp') {|*args| p args } # Contains meta character.
#
- # Closing stdin, stdout and stderr does not wait the process.
+ # Output (similar for each call above):
#
- def popen3(*cmd)
- pw = IO::pipe # pipe[0] for read, pipe[1] for write
- pr = IO::pipe
- pe = IO::pipe
+ # [#<IO:(closed)>, #<IO:(closed)>, #<IO:(closed)>, #<Process::Waiter:0x00007f58d52f28c8 dead>]
+ #
+ # The command line may also contain arguments and options for the command:
+ #
+ # Open3.popen3('echo "Foo"') { |i, o, e, t| o.gets }
+ # "Foo\n"
+ #
+ # <b>Argument +exe_path+</b>
+ #
+ # Argument +exe_path+ is one of the following:
+ #
+ # - The string path to an executable to be called.
+ # - A 2-element array containing the path to an executable
+ # and the string to be used as the name of the executing process.
+ #
+ # Example:
+ #
+ # Open3.popen3('/usr/bin/date') { |i, o, e, t| o.gets }
+ # # => "Wed Sep 27 02:56:44 PM CDT 2023\n"
+ #
+ # Ruby invokes the executable directly, with no shell and no shell expansion:
+ #
+ # Open3.popen3('doesnt_exist') { |i, o, e, t| o.gets } # Raises Errno::ENOENT
+ #
+ # If one or more +args+ is given, each is an argument or option
+ # to be passed to the executable:
+ #
+ # Open3.popen3('echo', 'C #') { |i, o, e, t| o.gets }
+ # # => "C #\n"
+ # Open3.popen3('echo', 'hello', 'world') { |i, o, e, t| o.gets }
+ # # => "hello world\n"
+ #
+ # Take care to avoid deadlocks.
+ # Output streams +stdout+ and +stderr+ have fixed-size buffers,
+ # so reading extensively from one but not the other can cause a deadlock
+ # when the unread buffer fills.
+ # To avoid that, +stdout+ and +stderr+ should be read simultaneously
+ # (using threads or IO.select).
+ #
+ # Related:
+ #
+ # - Open3.popen2: Makes the standard input and standard output streams
+ # of the child process available as separate streams,
+ # with no access to the standard error stream.
+ # - Open3.popen2e: Makes the standard input and the merge
+ # of the standard output and standard error streams
+ # of the child process available as separate streams.
+ #
+ def popen3(*cmd, &block)
+ if Hash === cmd.last
+ opts = cmd.pop.dup
+ else
+ opts = {}
+ end
+
+ in_r, in_w = IO.pipe
+ opts[:in] = in_r
+ in_w.sync = true
+
+ out_r, out_w = IO.pipe
+ opts[:out] = out_w
- pid = spawn(*cmd, STDIN=>pw[0], STDOUT=>pr[1], STDERR=>pe[1])
+ err_r, err_w = IO.pipe
+ opts[:err] = err_w
+
+ popen_run(cmd, opts, [in_r, out_w, err_w], [in_w, out_r, err_r], &block)
+ end
+ module_function :popen3
+
+ # :call-seq:
+ # Open3.popen2([env, ] command_line, options = {}) -> [stdin, stdout, wait_thread]
+ # Open3.popen2([env, ] exe_path, *args, options = {}) -> [stdin, stdout, wait_thread]
+ # Open3.popen2([env, ] command_line, options = {}) {|stdin, stdout, wait_thread| ... } -> object
+ # Open3.popen2([env, ] exe_path, *args, options = {}) {|stdin, stdout, wait_thread| ... } -> object
+ #
+ # Basically a wrapper for
+ # {Process.spawn}[rdoc-ref:Process.spawn]
+ # that:
+ #
+ # - Creates a child process, by calling Process.spawn with the given arguments.
+ # - Creates streams +stdin+ and +stdout+,
+ # which are the standard input and standard output streams
+ # in the child process.
+ # - Creates thread +wait_thread+ that waits for the child process to exit;
+ # the thread has method +pid+, which returns the process ID
+ # of the child process.
+ #
+ # With no block given, returns the array
+ # <tt>[stdin, stdout, wait_thread]</tt>.
+ # The caller should close each of the two returned streams.
+ #
+ # stdin, stdout, wait_thread = Open3.popen2('echo')
+ # # => [#<IO:fd 6>, #<IO:fd 7>, #<Process::Waiter:0x00007f58d52dbe98 run>]
+ # stdin.close
+ # stdout.close
+ # wait_thread.pid # => 2263572
+ # wait_thread.value # => #<Process::Status: pid 2263572 exit 0>
+ #
+ # With a block given, calls the block with the three variables
+ # (two streams and the wait thread)
+ # and returns the block's return value.
+ # The caller need not close the streams:
+ #
+ # Open3.popen2('echo') do |stdin, stdout, wait_thread|
+ # p stdin
+ # p stdout
+ # p wait_thread
+ # p wait_thread.pid
+ # p wait_thread.value
+ # end
+ #
+ # Output:
+ #
+ # #<IO:fd 6>
+ # #<IO:fd 7>
+ # #<Process::Waiter:0x00007f58d59a34b0 sleep>
+ # 2263636
+ # #<Process::Status: pid 2263636 exit 0>
+ #
+ # Like Process.spawn, this method has potential security vulnerabilities
+ # if called with untrusted input;
+ # see {Command Injection}[rdoc-ref:command_injection.rdoc@Command+Injection].
+ #
+ # Unlike Process.spawn, this method waits for the child process to exit
+ # before returning, so the caller need not do so.
+ #
+ # If the first argument is a hash, it becomes leading argument +env+
+ # in the call to Process.spawn;
+ # see {Execution Environment}[rdoc-ref:Process@Execution+Environment].
+ #
+ # If the last argument is a hash, it becomes trailing argument +options+
+ # in the call to Process.spawn;
+ # see {Execution Options}[rdoc-ref:Process@Execution+Options].
+ #
+ # The single required argument is one of the following:
+ #
+ # - +command_line+ if it is a string,
+ # and if it begins with a shell reserved word or special built-in,
+ # or if it contains one or more metacharacters.
+ # - +exe_path+ otherwise.
+ #
+ # <b>Argument +command_line+</b>
+ #
+ # \String argument +command_line+ is a command line to be passed to a shell;
+ # it must begin with a shell reserved word, begin with a special built-in,
+ # or contain meta characters:
+ #
+ # Open3.popen2('if true; then echo "Foo"; fi') {|*args| p args } # Shell reserved word.
+ # Open3.popen2('echo') {|*args| p args } # Built-in.
+ # Open3.popen2('date > date.tmp') {|*args| p args } # Contains meta character.
+ #
+ # Output (similar for each call above):
+ #
+ # # => [#<IO:(closed)>, #<IO:(closed)>, #<Process::Waiter:0x00007f7577dfe410 dead>]
+ #
+ # The command line may also contain arguments and options for the command:
+ #
+ # Open3.popen2('echo "Foo"') { |i, o, t| o.gets }
+ # "Foo\n"
+ #
+ # <b>Argument +exe_path+</b>
+ #
+ # Argument +exe_path+ is one of the following:
+ #
+ # - The string path to an executable to be called.
+ # - A 2-element array containing the path to an executable
+ # and the string to be used as the name of the executing process.
+ #
+ # Example:
+ #
+ # Open3.popen2('/usr/bin/date') { |i, o, t| o.gets }
+ # # => "Thu Sep 28 09:41:06 AM CDT 2023\n"
+ #
+ # Ruby invokes the executable directly, with no shell and no shell expansion:
+ #
+ # Open3.popen2('doesnt_exist') { |i, o, t| o.gets } # Raises Errno::ENOENT
+ #
+ # If one or more +args+ is given, each is an argument or option
+ # to be passed to the executable:
+ #
+ # Open3.popen2('echo', 'C #') { |i, o, t| o.gets }
+ # # => "C #\n"
+ # Open3.popen2('echo', 'hello', 'world') { |i, o, t| o.gets }
+ # # => "hello world\n"
+ #
+ #
+ # Related:
+ #
+ # - Open3.popen2e: Makes the standard input and the merge
+ # of the standard output and standard error streams
+ # of the child process available as separate streams.
+ # - Open3.popen3: Makes the standard input, standard output,
+ # and standard error streams
+ # of the child process available as separate streams.
+ #
+ def popen2(*cmd, &block)
+ if Hash === cmd.last
+ opts = cmd.pop.dup
+ else
+ opts = {}
+ end
+
+ in_r, in_w = IO.pipe
+ opts[:in] = in_r
+ in_w.sync = true
+
+ out_r, out_w = IO.pipe
+ opts[:out] = out_w
+
+ popen_run(cmd, opts, [in_r, out_w], [in_w, out_r], &block)
+ end
+ module_function :popen2
+
+ # :call-seq:
+ # Open3.popen2e([env, ] command_line, options = {}) -> [stdin, stdout_and_stderr, wait_thread]
+ # Open3.popen2e([env, ] exe_path, *args, options = {}) -> [stdin, stdout_and_stderr, wait_thread]
+ # Open3.popen2e([env, ] command_line, options = {}) {|stdin, stdout_and_stderr, wait_thread| ... } -> object
+ # Open3.popen2e([env, ] exe_path, *args, options = {}) {|stdin, stdout_and_stderr, wait_thread| ... } -> object
+ #
+ # Basically a wrapper for
+ # {Process.spawn}[rdoc-ref:Process.spawn]
+ # that:
+ #
+ # - Creates a child process, by calling Process.spawn with the given arguments.
+ # - Creates streams +stdin+, +stdout_and_stderr+,
+ # which are the standard input and the merge of the standard output
+ # and standard error streams in the child process.
+ # - Creates thread +wait_thread+ that waits for the child process to exit;
+ # the thread has method +pid+, which returns the process ID
+ # of the child process.
+ #
+ # With no block given, returns the array
+ # <tt>[stdin, stdout_and_stderr, wait_thread]</tt>.
+ # The caller should close each of the two returned streams.
+ #
+ # stdin, stdout_and_stderr, wait_thread = Open3.popen2e('echo')
+ # # => [#<IO:fd 6>, #<IO:fd 7>, #<Process::Waiter:0x00007f7577da4398 run>]
+ # stdin.close
+ # stdout_and_stderr.close
+ # wait_thread.pid # => 2274600
+ # wait_thread.value # => #<Process::Status: pid 2274600 exit 0>
+ #
+ # With a block given, calls the block with the three variables
+ # (two streams and the wait thread)
+ # and returns the block's return value.
+ # The caller need not close the streams:
+ #
+ # Open3.popen2e('echo') do |stdin, stdout_and_stderr, wait_thread|
+ # p stdin
+ # p stdout_and_stderr
+ # p wait_thread
+ # p wait_thread.pid
+ # p wait_thread.value
+ # end
+ #
+ # Output:
+ #
+ # #<IO:fd 6>
+ # #<IO:fd 7>
+ # #<Process::Waiter:0x00007f75777578c8 sleep>
+ # 2274763
+ # #<Process::Status: pid 2274763 exit 0>
+ #
+ # Like Process.spawn, this method has potential security vulnerabilities
+ # if called with untrusted input;
+ # see {Command Injection}[rdoc-ref:command_injection.rdoc@Command+Injection].
+ #
+ # Unlike Process.spawn, this method waits for the child process to exit
+ # before returning, so the caller need not do so.
+ #
+ # If the first argument is a hash, it becomes leading argument +env+
+ # in the call to Process.spawn;
+ # see {Execution Environment}[rdoc-ref:Process@Execution+Environment].
+ #
+ # If the last argument is a hash, it becomes trailing argument +options+
+ # in the call to Process.spawn;
+ # see {Execution Options}[rdoc-ref:Process@Execution+Options].
+ #
+ # The single required argument is one of the following:
+ #
+ # - +command_line+ if it is a string,
+ # and if it begins with a shell reserved word or special built-in,
+ # or if it contains one or more metacharacters.
+ # - +exe_path+ otherwise.
+ #
+ # <b>Argument +command_line+</b>
+ #
+ # \String argument +command_line+ is a command line to be passed to a shell;
+ # it must begin with a shell reserved word, begin with a special built-in,
+ # or contain meta characters:
+ #
+ # Open3.popen2e('if true; then echo "Foo"; fi') {|*args| p args } # Shell reserved word.
+ # Open3.popen2e('echo') {|*args| p args } # Built-in.
+ # Open3.popen2e('date > date.tmp') {|*args| p args } # Contains meta character.
+ #
+ # Output (similar for each call above):
+ #
+ # # => [#<IO:(closed)>, #<IO:(closed)>, #<Process::Waiter:0x00007f7577d8a1f0 dead>]
+ #
+ # The command line may also contain arguments and options for the command:
+ #
+ # Open3.popen2e('echo "Foo"') { |i, o_and_e, t| o_and_e.gets }
+ # "Foo\n"
+ #
+ # <b>Argument +exe_path+</b>
+ #
+ # Argument +exe_path+ is one of the following:
+ #
+ # - The string path to an executable to be called.
+ # - A 2-element array containing the path to an executable
+ # and the string to be used as the name of the executing process.
+ #
+ # Example:
+ #
+ # Open3.popen2e('/usr/bin/date') { |i, o_and_e, t| o_and_e.gets }
+ # # => "Thu Sep 28 01:58:45 PM CDT 2023\n"
+ #
+ # Ruby invokes the executable directly, with no shell and no shell expansion:
+ #
+ # Open3.popen2e('doesnt_exist') { |i, o_and_e, t| o_and_e.gets } # Raises Errno::ENOENT
+ #
+ # If one or more +args+ is given, each is an argument or option
+ # to be passed to the executable:
+ #
+ # Open3.popen2e('echo', 'C #') { |i, o_and_e, t| o_and_e.gets }
+ # # => "C #\n"
+ # Open3.popen2e('echo', 'hello', 'world') { |i, o_and_e, t| o_and_e.gets }
+ # # => "hello world\n"
+ #
+ # Related:
+ #
+ # - Open3.popen2: Makes the standard input and standard output streams
+ # of the child process available as separate streams,
+ # with no access to the standard error stream.
+ # - Open3.popen3: Makes the standard input, standard output,
+ # and standard error streams
+ # of the child process available as separate streams.
+ #
+ def popen2e(*cmd, &block)
+ if Hash === cmd.last
+ opts = cmd.pop.dup
+ else
+ opts = {}
+ end
+
+ in_r, in_w = IO.pipe
+ opts[:in] = in_r
+ in_w.sync = true
+
+ out_r, out_w = IO.pipe
+ opts[[:out, :err]] = out_w
+
+ popen_run(cmd, opts, [in_r, out_w], [in_w, out_r], &block)
+ ensure
+ if block
+ in_r.close
+ in_w.close
+ out_r.close
+ out_w.close
+ end
+ end
+ module_function :popen2e
+
+ def popen_run(cmd, opts, child_io, parent_io) # :nodoc:
+ pid = spawn(*cmd, opts)
wait_thr = Process.detach(pid)
- pw[0].close
- pr[1].close
- pe[1].close
- pi = [pw[1], pr[0], pe[0], wait_thr]
- pw[1].sync = true
+ child_io.each(&:close)
+ result = [*parent_io, wait_thr]
if defined? yield
begin
- return yield(*pi)
+ return yield(*result)
ensure
- [pw[1], pr[0], pe[0]].each{|p| p.close unless p.closed?}
+ parent_io.each(&:close)
wait_thr.join
end
end
- pi
+ result
+ end
+ module_function :popen_run
+ class << self
+ private :popen_run
end
- module_function :popen3
-end
-if $0 == __FILE__
- a = Open3.popen3("nroff -man")
- Thread.start do
- while line = gets
- a[0].print line
+ # :call-seq:
+ # Open3.capture3([env, ] command_line, options = {}) -> [stdout_s, stderr_s, status]
+ # Open3.capture3([env, ] exe_path, *args, options = {}) -> [stdout_s, stderr_s, status]
+ #
+ # Basically a wrapper for Open3.popen3 that:
+ #
+ # - Creates a child process, by calling Open3.popen3 with the given arguments
+ # (except for certain entries in hash +options+; see below).
+ # - Returns as strings +stdout_s+ and +stderr_s+ the standard output
+ # and standard error of the child process.
+ # - Returns as +status+ a <tt>Process::Status</tt> object
+ # that represents the exit status of the child process.
+ #
+ # Returns the array <tt>[stdout_s, stderr_s, status]</tt>:
+ #
+ # stdout_s, stderr_s, status = Open3.capture3('echo "Foo"')
+ # # => ["Foo\n", "", #<Process::Status: pid 2281954 exit 0>]
+ #
+ # Like Process.spawn, this method has potential security vulnerabilities
+ # if called with untrusted input;
+ # see {Command Injection}[rdoc-ref:command_injection.rdoc@Command+Injection].
+ #
+ # Unlike Process.spawn, this method waits for the child process to exit
+ # before returning, so the caller need not do so.
+ #
+ # If the first argument is a hash, it becomes leading argument +env+
+ # in the call to Open3.popen3;
+ # see {Execution Environment}[rdoc-ref:Process@Execution+Environment].
+ #
+ # If the last argument is a hash, it becomes trailing argument +options+
+ # in the call to Open3.popen3;
+ # see {Execution Options}[rdoc-ref:Process@Execution+Options].
+ #
+ # The hash +options+ is given;
+ # two options have local effect in method Open3.capture3:
+ #
+ # - If entry <tt>options[:stdin_data]</tt> exists, the entry is removed
+ # and its string value is sent to the command's standard input:
+ #
+ # Open3.capture3('tee', stdin_data: 'Foo')
+ # # => ["Foo", "", #<Process::Status: pid 2319575 exit 0>]
+ #
+ # - If entry <tt>options[:binmode]</tt> exists, the entry is removed and
+ # the internal streams are set to binary mode.
+ #
+ # The single required argument is one of the following:
+ #
+ # - +command_line+ if it is a string,
+ # and if it begins with a shell reserved word or special built-in,
+ # or if it contains one or more metacharacters.
+ # - +exe_path+ otherwise.
+ #
+ # <b>Argument +command_line+</b>
+ #
+ # \String argument +command_line+ is a command line to be passed to a shell;
+ # it must begin with a shell reserved word, begin with a special built-in,
+ # or contain meta characters:
+ #
+ # Open3.capture3('if true; then echo "Foo"; fi') # Shell reserved word.
+ # # => ["Foo\n", "", #<Process::Status: pid 2282025 exit 0>]
+ # Open3.capture3('echo') # Built-in.
+ # # => ["\n", "", #<Process::Status: pid 2282092 exit 0>]
+ # Open3.capture3('date > date.tmp') # Contains meta character.
+ # # => ["", "", #<Process::Status: pid 2282110 exit 0>]
+ #
+ # The command line may also contain arguments and options for the command:
+ #
+ # Open3.capture3('echo "Foo"')
+ # # => ["Foo\n", "", #<Process::Status: pid 2282092 exit 0>]
+ #
+ # <b>Argument +exe_path+</b>
+ #
+ # Argument +exe_path+ is one of the following:
+ #
+ # - The string path to an executable to be called.
+ # - A 2-element array containing the path to an executable
+ # and the string to be used as the name of the executing process.
+ #
+ # Example:
+ #
+ # Open3.capture3('/usr/bin/date')
+ # # => ["Thu Sep 28 05:03:51 PM CDT 2023\n", "", #<Process::Status: pid 2282300 exit 0>]
+ #
+ # Ruby invokes the executable directly, with no shell and no shell expansion:
+ #
+ # Open3.capture3('doesnt_exist') # Raises Errno::ENOENT
+ #
+ # If one or more +args+ is given, each is an argument or option
+ # to be passed to the executable:
+ #
+ # Open3.capture3('echo', 'C #')
+ # # => ["C #\n", "", #<Process::Status: pid 2282368 exit 0>]
+ # Open3.capture3('echo', 'hello', 'world')
+ # # => ["hello world\n", "", #<Process::Status: pid 2282372 exit 0>]
+ #
+ def capture3(*cmd)
+ if Hash === cmd.last
+ opts = cmd.pop.dup
+ else
+ opts = {}
end
- a[0].close
+
+ stdin_data = opts.delete(:stdin_data) || ''
+ binmode = opts.delete(:binmode)
+
+ popen3(*cmd, opts) {|i, o, e, t|
+ if binmode
+ i.binmode
+ o.binmode
+ e.binmode
+ end
+ out_reader = Thread.new { o.read }
+ err_reader = Thread.new { e.read }
+ begin
+ if stdin_data.respond_to? :readpartial
+ IO.copy_stream(stdin_data, i)
+ else
+ i.write stdin_data
+ end
+ rescue Errno::EPIPE
+ end
+ i.close
+ [out_reader.value, err_reader.value, t.value]
+ }
end
- while line = a[1].gets
- print ":", line
+ module_function :capture3
+
+ # :call-seq:
+ # Open3.capture2([env, ] command_line, options = {}) -> [stdout_s, status]
+ # Open3.capture2([env, ] exe_path, *args, options = {}) -> [stdout_s, status]
+ #
+ # Basically a wrapper for Open3.popen3 that:
+ #
+ # - Creates a child process, by calling Open3.popen3 with the given arguments
+ # (except for certain entries in hash +options+; see below).
+ # - Returns as string +stdout_s+ the standard output of the child process.
+ # - Returns as +status+ a <tt>Process::Status</tt> object
+ # that represents the exit status of the child process.
+ #
+ # Returns the array <tt>[stdout_s, status]</tt>:
+ #
+ # stdout_s, status = Open3.capture2('echo "Foo"')
+ # # => ["Foo\n", #<Process::Status: pid 2326047 exit 0>]
+ #
+ # Like Process.spawn, this method has potential security vulnerabilities
+ # if called with untrusted input;
+ # see {Command Injection}[rdoc-ref:command_injection.rdoc@Command+Injection].
+ #
+ # Unlike Process.spawn, this method waits for the child process to exit
+ # before returning, so the caller need not do so.
+ #
+ # If the first argument is a hash, it becomes leading argument +env+
+ # in the call to Open3.popen3;
+ # see {Execution Environment}[rdoc-ref:Process@Execution+Environment].
+ #
+ # If the last argument is a hash, it becomes trailing argument +options+
+ # in the call to Open3.popen3;
+ # see {Execution Options}[rdoc-ref:Process@Execution+Options].
+ #
+ # The hash +options+ is given;
+ # two options have local effect in method Open3.capture2:
+ #
+ # - If entry <tt>options[:stdin_data]</tt> exists, the entry is removed
+ # and its string value is sent to the command's standard input:
+ #
+ # Open3.capture2('tee', stdin_data: 'Foo')
+ #
+ # # => ["Foo", #<Process::Status: pid 2326087 exit 0>]
+ #
+ # - If entry <tt>options[:binmode]</tt> exists, the entry is removed and
+ # the internal streams are set to binary mode.
+ #
+ # The single required argument is one of the following:
+ #
+ # - +command_line+ if it is a string,
+ # and if it begins with a shell reserved word or special built-in,
+ # or if it contains one or more metacharacters.
+ # - +exe_path+ otherwise.
+ #
+ # <b>Argument +command_line+</b>
+ #
+ # \String argument +command_line+ is a command line to be passed to a shell;
+ # it must begin with a shell reserved word, begin with a special built-in,
+ # or contain meta characters:
+ #
+ # Open3.capture2('if true; then echo "Foo"; fi') # Shell reserved word.
+ # # => ["Foo\n", #<Process::Status: pid 2326131 exit 0>]
+ # Open3.capture2('echo') # Built-in.
+ # # => ["\n", #<Process::Status: pid 2326139 exit 0>]
+ # Open3.capture2('date > date.tmp') # Contains meta character.
+ # # => ["", #<Process::Status: pid 2326174 exit 0>]
+ #
+ # The command line may also contain arguments and options for the command:
+ #
+ # Open3.capture2('echo "Foo"')
+ # # => ["Foo\n", #<Process::Status: pid 2326183 exit 0>]
+ #
+ # <b>Argument +exe_path+</b>
+ #
+ # Argument +exe_path+ is one of the following:
+ #
+ # - The string path to an executable to be called.
+ # - A 2-element array containing the path to an executable
+ # and the string to be used as the name of the executing process.
+ #
+ # Example:
+ #
+ # Open3.capture2('/usr/bin/date')
+ # # => ["Fri Sep 29 01:00:39 PM CDT 2023\n", #<Process::Status: pid 2326222 exit 0>]
+ #
+ # Ruby invokes the executable directly, with no shell and no shell expansion:
+ #
+ # Open3.capture2('doesnt_exist') # Raises Errno::ENOENT
+ #
+ # If one or more +args+ is given, each is an argument or option
+ # to be passed to the executable:
+ #
+ # Open3.capture2('echo', 'C #')
+ # # => ["C #\n", #<Process::Status: pid 2326267 exit 0>]
+ # Open3.capture2('echo', 'hello', 'world')
+ # # => ["hello world\n", #<Process::Status: pid 2326299 exit 0>]
+ #
+ def capture2(*cmd)
+ if Hash === cmd.last
+ opts = cmd.pop.dup
+ else
+ opts = {}
+ end
+
+ stdin_data = opts.delete(:stdin_data)
+ binmode = opts.delete(:binmode)
+
+ popen2(*cmd, opts) {|i, o, t|
+ if binmode
+ i.binmode
+ o.binmode
+ end
+ out_reader = Thread.new { o.read }
+ if stdin_data
+ begin
+ if stdin_data.respond_to? :readpartial
+ IO.copy_stream(stdin_data, i)
+ else
+ i.write stdin_data
+ end
+ rescue Errno::EPIPE
+ end
+ end
+ i.close
+ [out_reader.value, t.value]
+ }
+ end
+ module_function :capture2
+
+ # :call-seq:
+ # Open3.capture2e([env, ] command_line, options = {}) -> [stdout_and_stderr_s, status]
+ # Open3.capture2e([env, ] exe_path, *args, options = {}) -> [stdout_and_stderr_s, status]
+ #
+ # Basically a wrapper for Open3.popen3 that:
+ #
+ # - Creates a child process, by calling Open3.popen3 with the given arguments
+ # (except for certain entries in hash +options+; see below).
+ # - Returns as string +stdout_and_stderr_s+ the merged standard output
+ # and standard error of the child process.
+ # - Returns as +status+ a <tt>Process::Status</tt> object
+ # that represents the exit status of the child process.
+ #
+ # Returns the array <tt>[stdout_and_stderr_s, status]</tt>:
+ #
+ # stdout_and_stderr_s, status = Open3.capture2e('echo "Foo"')
+ # # => ["Foo\n", #<Process::Status: pid 2371692 exit 0>]
+ #
+ # Like Process.spawn, this method has potential security vulnerabilities
+ # if called with untrusted input;
+ # see {Command Injection}[rdoc-ref:command_injection.rdoc@Command+Injection].
+ #
+ # Unlike Process.spawn, this method waits for the child process to exit
+ # before returning, so the caller need not do so.
+ #
+ # If the first argument is a hash, it becomes leading argument +env+
+ # in the call to Open3.popen3;
+ # see {Execution Environment}[rdoc-ref:Process@Execution+Environment].
+ #
+ # If the last argument is a hash, it becomes trailing argument +options+
+ # in the call to Open3.popen3;
+ # see {Execution Options}[rdoc-ref:Process@Execution+Options].
+ #
+ # The hash +options+ is given;
+ # two options have local effect in method Open3.capture2e:
+ #
+ # - If entry <tt>options[:stdin_data]</tt> exists, the entry is removed
+ # and its string value is sent to the command's standard input:
+ #
+ # Open3.capture2e('tee', stdin_data: 'Foo')
+ # # => ["Foo", #<Process::Status: pid 2371732 exit 0>]
+ #
+ # - If entry <tt>options[:binmode]</tt> exists, the entry is removed and
+ # the internal streams are set to binary mode.
+ #
+ # The single required argument is one of the following:
+ #
+ # - +command_line+ if it is a string,
+ # and if it begins with a shell reserved word or special built-in,
+ # or if it contains one or more metacharacters.
+ # - +exe_path+ otherwise.
+ #
+ # <b>Argument +command_line+</b>
+ #
+ # \String argument +command_line+ is a command line to be passed to a shell;
+ # it must begin with a shell reserved word, begin with a special built-in,
+ # or contain meta characters:
+ #
+ # Open3.capture2e('if true; then echo "Foo"; fi') # Shell reserved word.
+ # # => ["Foo\n", #<Process::Status: pid 2371740 exit 0>]
+ # Open3.capture2e('echo') # Built-in.
+ # # => ["\n", #<Process::Status: pid 2371774 exit 0>]
+ # Open3.capture2e('date > date.tmp') # Contains meta character.
+ # # => ["", #<Process::Status: pid 2371812 exit 0>]
+ #
+ # The command line may also contain arguments and options for the command:
+ #
+ # Open3.capture2e('echo "Foo"')
+ # # => ["Foo\n", #<Process::Status: pid 2326183 exit 0>]
+ #
+ # <b>Argument +exe_path+</b>
+ #
+ # Argument +exe_path+ is one of the following:
+ #
+ # - The string path to an executable to be called.
+ # - A 2-element array containing the path to an executable
+ # and the string to be used as the name of the executing process.
+ #
+ # Example:
+ #
+ # Open3.capture2e('/usr/bin/date')
+ # # => ["Sat Sep 30 09:01:46 AM CDT 2023\n", #<Process::Status: pid 2371820 exit 0>]
+ #
+ # Ruby invokes the executable directly, with no shell and no shell expansion:
+ #
+ # Open3.capture2e('doesnt_exist') # Raises Errno::ENOENT
+ #
+ # If one or more +args+ is given, each is an argument or option
+ # to be passed to the executable:
+ #
+ # Open3.capture2e('echo', 'C #')
+ # # => ["C #\n", #<Process::Status: pid 2371856 exit 0>]
+ # Open3.capture2e('echo', 'hello', 'world')
+ # # => ["hello world\n", #<Process::Status: pid 2371894 exit 0>]
+ #
+ def capture2e(*cmd)
+ if Hash === cmd.last
+ opts = cmd.pop.dup
+ else
+ opts = {}
+ end
+
+ stdin_data = opts.delete(:stdin_data)
+ binmode = opts.delete(:binmode)
+
+ popen2e(*cmd, opts) {|i, oe, t|
+ if binmode
+ i.binmode
+ oe.binmode
+ end
+ outerr_reader = Thread.new { oe.read }
+ if stdin_data
+ begin
+ if stdin_data.respond_to? :readpartial
+ IO.copy_stream(stdin_data, i)
+ else
+ i.write stdin_data
+ end
+ rescue Errno::EPIPE
+ end
+ end
+ i.close
+ [outerr_reader.value, t.value]
+ }
+ end
+ module_function :capture2e
+
+ # :call-seq:
+ # Open3.pipeline_rw([env, ] *cmds, options = {}) -> [first_stdin, last_stdout, wait_threads]
+ #
+ # Basically a wrapper for
+ # {Process.spawn}[rdoc-ref:Process.spawn]
+ # that:
+ #
+ # - Creates a child process for each of the given +cmds+
+ # by calling Process.spawn.
+ # - Pipes the +stdout+ from each child to the +stdin+ of the next child,
+ # or, for the first child, from the caller's +stdin+,
+ # or, for the last child, to the caller's +stdout+.
+ #
+ # The method does not wait for child processes to exit,
+ # so the caller must do so.
+ #
+ # With no block given, returns a 3-element array containing:
+ #
+ # - The +stdin+ stream of the first child process.
+ # - The +stdout+ stream of the last child process.
+ # - An array of the wait threads for all of the child processes.
+ #
+ # Example:
+ #
+ # first_stdin, last_stdout, wait_threads = Open3.pipeline_rw('sort', 'cat -n')
+ # # => [#<IO:fd 20>, #<IO:fd 21>, [#<Process::Waiter:0x000055e8de29ab40 sleep>, #<Process::Waiter:0x000055e8de29a690 sleep>]]
+ # first_stdin.puts("foo\nbar\nbaz")
+ # first_stdin.close # Send EOF to sort.
+ # puts last_stdout.read
+ # wait_threads.each do |wait_thread|
+ # wait_thread.join
+ # end
+ #
+ # Output:
+ #
+ # 1 bar
+ # 2 baz
+ # 3 foo
+ #
+ # With a block given, calls the block with the +stdin+ stream of the first child,
+ # the +stdout+ stream of the last child,
+ # and an array of the wait processes:
+ #
+ # Open3.pipeline_rw('sort', 'cat -n') do |first_stdin, last_stdout, wait_threads|
+ # first_stdin.puts "foo\nbar\nbaz"
+ # first_stdin.close # send EOF to sort.
+ # puts last_stdout.read
+ # wait_threads.each do |wait_thread|
+ # wait_thread.join
+ # end
+ # end
+ #
+ # Output:
+ #
+ # 1 bar
+ # 2 baz
+ # 3 foo
+ #
+ # Like Process.spawn, this method has potential security vulnerabilities
+ # if called with untrusted input;
+ # see {Command Injection}[rdoc-ref:command_injection.rdoc@Command+Injection].
+ #
+ # If the first argument is a hash, it becomes leading argument +env+
+ # in each call to Process.spawn;
+ # see {Execution Environment}[rdoc-ref:Process@Execution+Environment].
+ #
+ # If the last argument is a hash, it becomes trailing argument +options+
+ # in each call to Process.spawn;
+ # see {Execution Options}[rdoc-ref:Process@Execution+Options].
+ #
+ # Each remaining argument in +cmds+ is one of:
+ #
+ # - A +command_line+: a string that begins with a shell reserved word
+ # or special built-in, or contains one or more metacharacters.
+ # - An +exe_path+: the string path to an executable to be called.
+ # - An array containing a +command_line+ or an +exe_path+,
+ # along with zero or more string arguments for the command.
+ #
+ # See {Argument command_line or exe_path}[rdoc-ref:Process@Argument+command_line+or+exe_path].
+ #
+ def pipeline_rw(*cmds, &block)
+ if Hash === cmds.last
+ opts = cmds.pop.dup
+ else
+ opts = {}
+ end
+
+ in_r, in_w = IO.pipe
+ opts[:in] = in_r
+ in_w.sync = true
+
+ out_r, out_w = IO.pipe
+ opts[:out] = out_w
+
+ pipeline_run(cmds, opts, [in_r, out_w], [in_w, out_r], &block)
+ end
+ module_function :pipeline_rw
+
+ # :call-seq:
+ # Open3.pipeline_r([env, ] *cmds, options = {}) -> [last_stdout, wait_threads]
+ #
+ # Basically a wrapper for
+ # {Process.spawn}[rdoc-ref:Process.spawn]
+ # that:
+ #
+ # - Creates a child process for each of the given +cmds+
+ # by calling Process.spawn.
+ # - Pipes the +stdout+ from each child to the +stdin+ of the next child,
+ # or, for the last child, to the caller's +stdout+.
+ #
+ # The method does not wait for child processes to exit,
+ # so the caller must do so.
+ #
+ # With no block given, returns a 2-element array containing:
+ #
+ # - The +stdout+ stream of the last child process.
+ # - An array of the wait threads for all of the child processes.
+ #
+ # Example:
+ #
+ # last_stdout, wait_threads = Open3.pipeline_r('ls', 'grep R')
+ # # => [#<IO:fd 5>, [#<Process::Waiter:0x000055e8de2f9898 dead>, #<Process::Waiter:0x000055e8de2f94b0 sleep>]]
+ # puts last_stdout.read
+ # wait_threads.each do |wait_thread|
+ # wait_thread.join
+ # end
+ #
+ # Output:
+ #
+ # Rakefile
+ # README.md
+ #
+ # With a block given, calls the block with the +stdout+ stream
+ # of the last child process,
+ # and an array of the wait processes:
+ #
+ # Open3.pipeline_r('ls', 'grep R') do |last_stdout, wait_threads|
+ # puts last_stdout.read
+ # wait_threads.each do |wait_thread|
+ # wait_thread.join
+ # end
+ # end
+ #
+ # Output:
+ #
+ # Rakefile
+ # README.md
+ #
+ # Like Process.spawn, this method has potential security vulnerabilities
+ # if called with untrusted input;
+ # see {Command Injection}[rdoc-ref:command_injection.rdoc@Command+Injection].
+ #
+ # If the first argument is a hash, it becomes leading argument +env+
+ # in each call to Process.spawn;
+ # see {Execution Environment}[rdoc-ref:Process@Execution+Environment].
+ #
+ # If the last argument is a hash, it becomes trailing argument +options+
+ # in each call to Process.spawn;
+ # see {Execution Options}[rdoc-ref:Process@Execution+Options].
+ #
+ # Each remaining argument in +cmds+ is one of:
+ #
+ # - A +command_line+: a string that begins with a shell reserved word
+ # or special built-in, or contains one or more metacharacters.
+ # - An +exe_path+: the string path to an executable to be called.
+ # - An array containing a +command_line+ or an +exe_path+,
+ # along with zero or more string arguments for the command.
+ #
+ # See {Argument command_line or exe_path}[rdoc-ref:Process@Argument+command_line+or+exe_path].
+ #
+ def pipeline_r(*cmds, &block)
+ if Hash === cmds.last
+ opts = cmds.pop.dup
+ else
+ opts = {}
+ end
+
+ out_r, out_w = IO.pipe
+ opts[:out] = out_w
+
+ pipeline_run(cmds, opts, [out_w], [out_r], &block)
+ end
+ module_function :pipeline_r
+
+
+ # :call-seq:
+ # Open3.pipeline_w([env, ] *cmds, options = {}) -> [first_stdin, wait_threads]
+ #
+ # Basically a wrapper for
+ # {Process.spawn}[rdoc-ref:Process.spawn]
+ # that:
+ #
+ # - Creates a child process for each of the given +cmds+
+ # by calling Process.spawn.
+ # - Pipes the +stdout+ from each child to the +stdin+ of the next child,
+ # or, for the first child, pipes the caller's +stdout+ to the child's +stdin+.
+ #
+ # The method does not wait for child processes to exit,
+ # so the caller must do so.
+ #
+ # With no block given, returns a 2-element array containing:
+ #
+ # - The +stdin+ stream of the first child process.
+ # - An array of the wait threads for all of the child processes.
+ #
+ # Example:
+ #
+ # first_stdin, wait_threads = Open3.pipeline_w('sort', 'cat -n')
+ # # => [#<IO:fd 7>, [#<Process::Waiter:0x000055e8de928278 run>, #<Process::Waiter:0x000055e8de923e80 run>]]
+ # first_stdin.puts("foo\nbar\nbaz")
+ # first_stdin.close # Send EOF to sort.
+ # wait_threads.each do |wait_thread|
+ # wait_thread.join
+ # end
+ #
+ # Output:
+ #
+ # 1 bar
+ # 2 baz
+ # 3 foo
+ #
+ # With a block given, calls the block with the +stdin+ stream
+ # of the first child process,
+ # and an array of the wait processes:
+ #
+ # Open3.pipeline_w('sort', 'cat -n') do |first_stdin, wait_threads|
+ # first_stdin.puts("foo\nbar\nbaz")
+ # first_stdin.close # Send EOF to sort.
+ # wait_threads.each do |wait_thread|
+ # wait_thread.join
+ # end
+ # end
+ #
+ # Output:
+ #
+ # 1 bar
+ # 2 baz
+ # 3 foo
+ #
+ # Like Process.spawn, this method has potential security vulnerabilities
+ # if called with untrusted input;
+ # see {Command Injection}[rdoc-ref:command_injection.rdoc@Command+Injection].
+ #
+ # If the first argument is a hash, it becomes leading argument +env+
+ # in each call to Process.spawn;
+ # see {Execution Environment}[rdoc-ref:Process@Execution+Environment].
+ #
+ # If the last argument is a hash, it becomes trailing argument +options+
+ # in each call to Process.spawn;
+ # see {Execution Options}[rdoc-ref:Process@Execution+Options].
+ #
+ # Each remaining argument in +cmds+ is one of:
+ #
+ # - A +command_line+: a string that begins with a shell reserved word
+ # or special built-in, or contains one or more metacharacters.
+ # - An +exe_path+: the string path to an executable to be called.
+ # - An array containing a +command_line+ or an +exe_path+,
+ # along with zero or more string arguments for the command.
+ #
+ # See {Argument command_line or exe_path}[rdoc-ref:Process@Argument+command_line+or+exe_path].
+ #
+ def pipeline_w(*cmds, &block)
+ if Hash === cmds.last
+ opts = cmds.pop.dup
+ else
+ opts = {}
+ end
+
+ in_r, in_w = IO.pipe
+ opts[:in] = in_r
+ in_w.sync = true
+
+ pipeline_run(cmds, opts, [in_r], [in_w], &block)
+ end
+ module_function :pipeline_w
+
+ # :call-seq:
+ # Open3.pipeline_start([env, ] *cmds, options = {}) -> [wait_threads]
+ #
+ # Basically a wrapper for
+ # {Process.spawn}[rdoc-ref:Process.spawn]
+ # that:
+ #
+ # - Creates a child process for each of the given +cmds+
+ # by calling Process.spawn.
+ # - Does not wait for child processes to exit.
+ #
+ # With no block given, returns an array of the wait threads
+ # for all of the child processes.
+ #
+ # Example:
+ #
+ # wait_threads = Open3.pipeline_start('ls', 'grep R')
+ # # => [#<Process::Waiter:0x000055e8de9d2bb0 run>, #<Process::Waiter:0x000055e8de9d2890 run>]
+ # wait_threads.each do |wait_thread|
+ # wait_thread.join
+ # end
+ #
+ # Output:
+ #
+ # Rakefile
+ # README.md
+ #
+ # With a block given, calls the block with an array of the wait processes:
+ #
+ # Open3.pipeline_start('ls', 'grep R') do |wait_threads|
+ # wait_threads.each do |wait_thread|
+ # wait_thread.join
+ # end
+ # end
+ #
+ # Output:
+ #
+ # Rakefile
+ # README.md
+ #
+ # Like Process.spawn, this method has potential security vulnerabilities
+ # if called with untrusted input;
+ # see {Command Injection}[rdoc-ref:command_injection.rdoc@Command+Injection].
+ #
+ # If the first argument is a hash, it becomes leading argument +env+
+ # in each call to Process.spawn;
+ # see {Execution Environment}[rdoc-ref:Process@Execution+Environment].
+ #
+ # If the last argument is a hash, it becomes trailing argument +options+
+ # in each call to Process.spawn;
+ # see {Execution Options}[rdoc-ref:Process@Execution+Options].
+ #
+ # Each remaining argument in +cmds+ is one of:
+ #
+ # - A +command_line+: a string that begins with a shell reserved word
+ # or special built-in, or contains one or more metacharacters.
+ # - An +exe_path+: the string path to an executable to be called.
+ # - An array containing a +command_line+ or an +exe_path+,
+ # along with zero or more string arguments for the command.
+ #
+ # See {Argument command_line or exe_path}[rdoc-ref:Process@Argument+command_line+or+exe_path].
+ #
+ def pipeline_start(*cmds, &block)
+ if Hash === cmds.last
+ opts = cmds.pop.dup
+ else
+ opts = {}
+ end
+
+ if block
+ pipeline_run(cmds, opts, [], [], &block)
+ else
+ ts, = pipeline_run(cmds, opts, [], [])
+ ts
+ end
+ end
+ module_function :pipeline_start
+
+ # :call-seq:
+ # Open3.pipeline([env, ] *cmds, options = {}) -> array_of_statuses
+ #
+ # Basically a wrapper for
+ # {Process.spawn}[rdoc-ref:Process.spawn]
+ # that:
+ #
+ # - Creates a child process for each of the given +cmds+
+ # by calling Process.spawn.
+ # - Pipes the +stdout+ from each child to the +stdin+ of the next child,
+ # or, for the last child, to the caller's +stdout+.
+ # - Waits for the child processes to exit.
+ # - Returns an array of Process::Status objects (one for each child).
+ #
+ # Example:
+ #
+ # wait_threads = Open3.pipeline('ls', 'grep R')
+ # # => [#<Process::Status: pid 2139200 exit 0>, #<Process::Status: pid 2139202 exit 0>]
+ #
+ # Output:
+ #
+ # Rakefile
+ # README.md
+ #
+ # Like Process.spawn, this method has potential security vulnerabilities
+ # if called with untrusted input;
+ # see {Command Injection}[rdoc-ref:command_injection.rdoc@Command+Injection].
+ #
+ # If the first argument is a hash, it becomes leading argument +env+
+ # in each call to Process.spawn;
+ # see {Execution Environment}[rdoc-ref:Process@Execution+Environment].
+ #
+ # If the last argument is a hash, it becomes trailing argument +options+
+ # in each call to Process.spawn'
+ # see {Execution Options}[rdoc-ref:Process@Execution+Options].
+ #
+ # Each remaining argument in +cmds+ is one of:
+ #
+ # - A +command_line+: a string that begins with a shell reserved word
+ # or special built-in, or contains one or more metacharacters.
+ # - An +exe_path+: the string path to an executable to be called.
+ # - An array containing a +command_line+ or an +exe_path+,
+ # along with zero or more string arguments for the command.
+ #
+ # See {Argument command_line or exe_path}[rdoc-ref:Process@Argument+command_line+or+exe_path].
+ #
+ def pipeline(*cmds)
+ if Hash === cmds.last
+ opts = cmds.pop.dup
+ else
+ opts = {}
+ end
+
+ pipeline_run(cmds, opts, [], []) {|ts|
+ ts.map(&:value)
+ }
end
+ module_function :pipeline
+
+ def pipeline_run(cmds, pipeline_opts, child_io, parent_io) # :nodoc:
+ if cmds.empty?
+ raise ArgumentError, "no commands"
+ end
+
+ opts_base = pipeline_opts.dup
+ opts_base.delete :in
+ opts_base.delete :out
+
+ wait_thrs = []
+ r = nil
+ cmds.each_with_index {|cmd, i|
+ cmd_opts = opts_base.dup
+ if String === cmd
+ cmd = [cmd]
+ else
+ cmd_opts.update cmd.pop if Hash === cmd.last
+ end
+ if i == 0
+ if !cmd_opts.include?(:in)
+ if pipeline_opts.include?(:in)
+ cmd_opts[:in] = pipeline_opts[:in]
+ end
+ end
+ else
+ cmd_opts[:in] = r
+ end
+ if i != cmds.length - 1
+ r2, w2 = IO.pipe
+ cmd_opts[:out] = w2
+ else
+ if !cmd_opts.include?(:out)
+ if pipeline_opts.include?(:out)
+ cmd_opts[:out] = pipeline_opts[:out]
+ end
+ end
+ end
+ pid = spawn(*cmd, cmd_opts)
+ wait_thrs << Process.detach(pid)
+ r&.close
+ w2&.close
+ r = r2
+ }
+ result = parent_io + [wait_thrs]
+ child_io.each(&:close)
+ if defined? yield
+ begin
+ return yield(*result)
+ ensure
+ parent_io.each(&:close)
+ wait_thrs.each(&:join)
+ end
+ end
+ result
+ end
+ module_function :pipeline_run
+ class << self
+ private :pipeline_run
+ end
+
end
+
+# JRuby uses different popen logic on Windows, require it here to reuse wrapper methods above.
+require 'open3/jruby_windows' if RUBY_ENGINE == 'jruby' && JRuby::Util::ON_WINDOWS
diff --git a/lib/open3/open3.gemspec b/lib/open3/open3.gemspec
new file mode 100644
index 0000000000..21980decac
--- /dev/null
+++ b/lib/open3/open3.gemspec
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1, "..").join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}/version.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Yukihiro Matsumoto"]
+ spec.email = ["matz@ruby-lang.org"]
+
+ spec.summary = %q{Popen, but with stderr, too}
+ spec.description = spec.summary
+ spec.homepage = "https://github.com/ruby/open3"
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+ spec.required_ruby_version = ">= 2.6.0"
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
+ `git ls-files -z 2>#{IO::NULL}`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
+ end
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+end
diff --git a/lib/open3/version.rb b/lib/open3/version.rb
new file mode 100644
index 0000000000..322dd71e2a
--- /dev/null
+++ b/lib/open3/version.rb
@@ -0,0 +1,4 @@
+module Open3
+ # The version string
+ VERSION = "0.2.1"
+end
diff --git a/lib/optionparser.rb b/lib/optionparser.rb
new file mode 100644
index 0000000000..4b9b40d82a
--- /dev/null
+++ b/lib/optionparser.rb
@@ -0,0 +1,2 @@
+# frozen_string_literal: false
+require_relative 'optparse'
diff --git a/lib/optparse.rb b/lib/optparse.rb
index 0397382a6b..97178e284b 100644
--- a/lib/optparse.rb
+++ b/lib/optparse.rb
@@ -1,15 +1,17 @@
+# frozen_string_literal: true
#
# optparse.rb - command-line option analysis with the OptionParser class.
-#
+#
# Author:: Nobu Nakada
# Documentation:: Nobu Nakada and Gavin Sinclair.
#
-# See OptionParser for documentation.
+# See OptionParser for documentation.
#
+require 'set' unless defined?(Set)
-
-# == Developer Documentation (not for RDoc output)
-#
+#--
+# == Developer Documentation (not for RDoc output)
+#
# === Class tree
#
# - OptionParser:: front end
@@ -42,8 +44,14 @@
# | all instances)|
# +---------------+
#
+#++
+#
# == OptionParser
#
+# === New to +OptionParser+?
+#
+# See the {Tutorial}[optparse/tutorial.rdoc].
+#
# === Introduction
#
# OptionParser is a class for command-line option analysis. It is much more
@@ -51,7 +59,7 @@
# solution.
#
# === Features
-#
+#
# 1. The argument specification and the code to handle it are written in the
# same place.
# 2. It can output an option summary; you don't need to maintain this string
@@ -60,17 +68,18 @@
# 4. Arguments can be automatically converted to a specified class.
# 5. Arguments can be restricted to a certain set.
#
-# All of these features are demonstrated in the examples below.
+# All of these features are demonstrated in the examples below. See
+# #make_switch for full documentation.
#
# === Minimal example
#
# require 'optparse'
#
# options = {}
-# OptionParser.new do |opts|
-# opts.banner = "Usage: example.rb [options]"
+# OptionParser.new do |parser|
+# parser.banner = "Usage: example.rb [options]"
#
-# opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
+# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
# options[:verbose] = v
# end
# end.parse!
@@ -78,6 +87,181 @@
# p options
# p ARGV
#
+# === Generating Help
+#
+# OptionParser can be used to automatically generate help for the commands you
+# write:
+#
+# require 'optparse'
+#
+# Options = Struct.new(:name)
+#
+# class Parser
+# def self.parse(options)
+# args = Options.new("world")
+#
+# opt_parser = OptionParser.new do |parser|
+# parser.banner = "Usage: example.rb [options]"
+#
+# parser.on("-nNAME", "--name=NAME", "Name to say hello to") do |n|
+# args.name = n
+# end
+#
+# parser.on("-h", "--help", "Prints this help") do
+# puts parser
+# exit
+# end
+# end
+#
+# opt_parser.parse!(options)
+# return args
+# end
+# end
+# options = Parser.parse %w[--help]
+#
+# #=>
+# # Usage: example.rb [options]
+# # -n, --name=NAME Name to say hello to
+# # -h, --help Prints this help
+#
+# === Required Arguments
+#
+# For options that require an argument, option specification strings may include an
+# option name in all caps. If an option is used without the required argument,
+# an exception will be raised.
+#
+# require 'optparse'
+#
+# options = {}
+# OptionParser.new do |parser|
+# parser.on("-r", "--require LIBRARY",
+# "Require the LIBRARY before executing your script") do |lib|
+# puts "You required #{lib}!"
+# end
+# end.parse!
+#
+# Used:
+#
+# $ ruby optparse-test.rb -r
+# optparse-test.rb:9:in '<main>': missing argument: -r (OptionParser::MissingArgument)
+# $ ruby optparse-test.rb -r my-library
+# You required my-library!
+#
+# === Type Coercion
+#
+# OptionParser supports the ability to coerce command line arguments
+# into objects for us.
+#
+# OptionParser comes with a few ready-to-use kinds of type
+# coercion. They are:
+#
+# - Date -- Anything accepted by +Date.parse+ (need to require +optparse/date+)
+# - DateTime -- Anything accepted by +DateTime.parse+ (need to require +optparse/date+)
+# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+ (need to require +optparse/time+)
+# - URI -- Anything accepted by +URI.parse+ (need to require +optparse/uri+)
+# - Shellwords -- Anything accepted by +Shellwords.shellwords+ (need to require +optparse/shellwords+)
+# - String -- Any non-empty string
+# - Integer -- Any integer. Will convert octal. (e.g. 124, -3, 040)
+# - Float -- Any float. (e.g. 10, 3.14, -100E+13)
+# - Numeric -- Any integer, float, or rational (1, 3.4, 1/3)
+# - DecimalInteger -- Like +Integer+, but no octal format.
+# - OctalInteger -- Like +Integer+, but no decimal format.
+# - DecimalNumeric -- Decimal integer or float.
+# - TrueClass -- Accepts '+, yes, true, -, no, false' and
+# defaults as +true+
+# - FalseClass -- Same as +TrueClass+, but defaults to +false+
+# - Array -- Strings separated by ',' (e.g. 1,2,3)
+# - Regexp -- Regular expressions. Also includes options.
+#
+# We can also add our own coercions, which we will cover below.
+#
+# ==== Using Built-in Conversions
+#
+# As an example, the built-in +Time+ conversion is used. The other built-in
+# conversions behave in the same way.
+# OptionParser will attempt to parse the argument
+# as a +Time+. If it succeeds, that time will be passed to the
+# handler block. Otherwise, an exception will be raised.
+#
+# require 'optparse'
+# require 'optparse/time'
+# OptionParser.new do |parser|
+# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
+# p time
+# end
+# end.parse!
+#
+# Used:
+#
+# $ ruby optparse-test.rb -t nonsense
+# ... invalid argument: -t nonsense (OptionParser::InvalidArgument)
+# $ ruby optparse-test.rb -t 10-11-12
+# 2010-11-12 00:00:00 -0500
+# $ ruby optparse-test.rb -t 9:30
+# 2014-08-13 09:30:00 -0400
+#
+# ==== Creating Custom Conversions
+#
+# The +accept+ method on OptionParser may be used to create converters.
+# It specifies which conversion block to call whenever a class is specified.
+# The example below uses it to fetch a +User+ object before the +on+ handler receives it.
+#
+# require 'optparse'
+#
+# User = Struct.new(:id, :name)
+#
+# def find_user id
+# not_found = ->{ raise "No User Found for id #{id}" }
+# [ User.new(1, "Sam"),
+# User.new(2, "Gandalf") ].find(not_found) do |u|
+# u.id == id
+# end
+# end
+#
+# op = OptionParser.new
+# op.accept(User) do |user_id|
+# find_user user_id.to_i
+# end
+#
+# op.on("--user ID", User) do |user|
+# puts user
+# end
+#
+# op.parse!
+#
+# Used:
+#
+# $ ruby optparse-test.rb --user 1
+# #<struct User id=1, name="Sam">
+# $ ruby optparse-test.rb --user 2
+# #<struct User id=2, name="Gandalf">
+# $ ruby optparse-test.rb --user 3
+# optparse-test.rb:15:in 'block in find_user': No User Found for id 3 (RuntimeError)
+#
+# === Store options to a Hash
+#
+# The +into+ option of +order+, +parse+ and so on methods stores command line options into a Hash.
+#
+# require 'optparse'
+#
+# options = {}
+# OptionParser.new do |parser|
+# parser.on('-a')
+# parser.on('-b NUM', Integer)
+# parser.on('-v', '--verbose')
+# end.parse!(into: options)
+#
+# p options
+#
+# Used:
+#
+# $ ruby optparse-test.rb -a
+# {:a=>true}
+# $ ruby optparse-test.rb -a -v
+# {:a=>true, :verbose=>true}
+# $ ruby optparse-test.rb -a -b 100
+# {:a=>true, :b=>100}
+#
# === Complete example
#
# The following example is a complete Ruby program. You can run it and see the
@@ -88,126 +272,165 @@
# require 'optparse/time'
# require 'ostruct'
# require 'pp'
-#
+#
# class OptparseExample
-#
+# Version = '1.0.0'
+#
# CODES = %w[iso-2022-jp shift_jis euc-jp utf8 binary]
# CODE_ALIASES = { "jis" => "iso-2022-jp", "sjis" => "shift_jis" }
-#
-# #
-# # Return a structure describing the options.
-# #
-# def self.parse(args)
-# # The options specified on the command line will be collected in *options*.
-# # We set default values here.
-# options = OpenStruct.new
-# options.library = []
-# options.inplace = false
-# options.encoding = "utf8"
-# options.transfer_type = :auto
-# options.verbose = false
-#
-# opts = OptionParser.new do |opts|
-# opts.banner = "Usage: example.rb [options]"
-#
-# opts.separator ""
-# opts.separator "Specific options:"
-#
-# # Mandatory argument.
-# opts.on("-r", "--require LIBRARY",
-# "Require the LIBRARY before executing your script") do |lib|
-# options.library << lib
+#
+# class ScriptOptions
+# attr_accessor :library, :inplace, :encoding, :transfer_type,
+# :verbose, :extension, :delay, :time, :record_separator,
+# :list
+#
+# def initialize
+# self.library = []
+# self.inplace = false
+# self.encoding = "utf8"
+# self.transfer_type = :auto
+# self.verbose = false
+# end
+#
+# def define_options(parser)
+# parser.banner = "Usage: example.rb [options]"
+# parser.separator ""
+# parser.separator "Specific options:"
+#
+# # add additional options
+# perform_inplace_option(parser)
+# delay_execution_option(parser)
+# execute_at_time_option(parser)
+# specify_record_separator_option(parser)
+# list_example_option(parser)
+# specify_encoding_option(parser)
+# optional_option_argument_with_keyword_completion_option(parser)
+# boolean_verbose_option(parser)
+#
+# parser.separator ""
+# parser.separator "Common options:"
+# # No argument, shows at tail. This will print an options summary.
+# # Try it and see!
+# parser.on_tail("-h", "--help", "Show this message") do
+# puts parser
+# exit
# end
-#
-# # Optional argument; multi-line description.
-# opts.on("-i", "--inplace [EXTENSION]",
-# "Edit ARGV files in place",
-# " (make backup if EXTENSION supplied)") do |ext|
-# options.inplace = true
-# options.extension = ext || ''
-# options.extension.sub!(/\A\.?(?=.)/, ".") # Ensure extension begins with dot.
+# # Another typical switch to print the version.
+# parser.on_tail("--version", "Show version") do
+# puts Version
+# exit
+# end
+# end
+#
+# def perform_inplace_option(parser)
+# # Specifies an optional option argument
+# parser.on("-i", "--inplace [EXTENSION]",
+# "Edit ARGV files in place",
+# "(make backup if EXTENSION supplied)") do |ext|
+# self.inplace = true
+# self.extension = ext || ''
+# self.extension.sub!(/\A\.?(?=.)/, ".") # Ensure extension begins with dot.
# end
-#
+# end
+#
+# def delay_execution_option(parser)
# # Cast 'delay' argument to a Float.
-# opts.on("--delay N", Float, "Delay N seconds before executing") do |n|
-# options.delay = n
+# parser.on("--delay N", Float, "Delay N seconds before executing") do |n|
+# self.delay = n
# end
-#
+# end
+#
+# def execute_at_time_option(parser)
# # Cast 'time' argument to a Time object.
-# opts.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
-# options.time = time
+# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
+# self.time = time
# end
-#
+# end
+#
+# def specify_record_separator_option(parser)
# # Cast to octal integer.
-# opts.on("-F", "--irs [OCTAL]", OptionParser::OctalInteger,
-# "Specify record separator (default \\0)") do |rs|
-# options.record_separator = rs
+# parser.on("-F", "--irs [OCTAL]", OptionParser::OctalInteger,
+# "Specify record separator (default \\0)") do |rs|
+# self.record_separator = rs
# end
-#
+# end
+#
+# def list_example_option(parser)
# # List of arguments.
-# opts.on("--list x,y,z", Array, "Example 'list' of arguments") do |list|
-# options.list = list
+# parser.on("--list x,y,z", Array, "Example 'list' of arguments") do |list|
+# self.list = list
# end
-#
+# end
+#
+# def specify_encoding_option(parser)
# # Keyword completion. We are specifying a specific set of arguments (CODES
# # and CODE_ALIASES - notice the latter is a Hash), and the user may provide
# # the shortest unambiguous text.
-# code_list = (CODE_ALIASES.keys + CODES).join(',')
-# opts.on("--code CODE", CODES, CODE_ALIASES, "Select encoding",
-# " (#{code_list})") do |encoding|
-# options.encoding = encoding
+# code_list = (CODE_ALIASES.keys + CODES).join(', ')
+# parser.on("--code CODE", CODES, CODE_ALIASES, "Select encoding",
+# "(#{code_list})") do |encoding|
+# self.encoding = encoding
# end
-#
-# # Optional argument with keyword completion.
-# opts.on("--type [TYPE]", [:text, :binary, :auto],
-# "Select transfer type (text, binary, auto)") do |t|
-# options.transfer_type = t
+# end
+#
+# def optional_option_argument_with_keyword_completion_option(parser)
+# # Optional '--type' option argument with keyword completion.
+# parser.on("--type [TYPE]", [:text, :binary, :auto],
+# "Select transfer type (text, binary, auto)") do |t|
+# self.transfer_type = t
# end
-#
+# end
+#
+# def boolean_verbose_option(parser)
# # Boolean switch.
-# opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
-# options.verbose = v
-# end
-#
-# opts.separator ""
-# opts.separator "Common options:"
-#
-# # No argument, shows at tail. This will print an options summary.
-# # Try it and see!
-# opts.on_tail("-h", "--help", "Show this message") do
-# puts opts
-# exit
-# end
-#
-# # Another typical switch to print the version.
-# opts.on_tail("--version", "Show version") do
-# puts OptionParser::Version.join('.')
-# exit
+# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
+# self.verbose = v
# end
# end
-#
-# opts.parse!(args)
-# options
-# end # parse()
-#
+# end
+#
+# #
+# # Return a structure describing the options.
+# #
+# def parse(args)
+# # The options specified on the command line will be collected in
+# # *options*.
+#
+# @options = ScriptOptions.new
+# @args = OptionParser.new do |parser|
+# @options.define_options(parser)
+# parser.parse!(args)
+# end
+# @options
+# end
+#
+# attr_reader :parser, :options
# end # class OptparseExample
-#
-# options = OptparseExample.parse(ARGV)
-# pp options
+#
+# example = OptparseExample.new
+# options = example.parse(ARGV)
+# pp options # example.options
+# pp ARGV
+#
+# === Shell Completion
+#
+# For modern shells (e.g. bash, zsh, etc.), you can use shell
+# completion for command line options.
#
# === Further documentation
#
-# The above examples should be enough to learn how to use this class. If you
-# have any questions, email me (gsinclair@soyabean.com.au) and I will update
-# this document.
+# The above examples, along with the accompanying
+# {Tutorial}[optparse/tutorial.rdoc],
+# should be enough to learn how to use this class.
+# If you have any questions, file a ticket at http://bugs.ruby-lang.org.
#
class OptionParser
- # :stopdoc:
- RCSID = %w$Id$[1..-1].each {|s| s.freeze}.freeze
- Version = (RCSID[1].split('.').collect {|s| s.to_i}.extend(Comparable).freeze if RCSID[1])
- LastModified = (Time.gm(*RCSID[2, 2].join('-').scan(/\d+/).collect {|s| s.to_i}) if RCSID[2])
- Release = RCSID[2]
+ # The version string
+ VERSION = "0.8.1"
+ # An alias for compatibility
+ Version = VERSION
+ # :stopdoc:
NoArgument = [NO_ARGUMENT = :NONE, nil].freeze
RequiredArgument = [REQUIRED_ARGUMENT = :REQUIRED, true].freeze
OptionalArgument = [OPTIONAL_ARGUMENT = :OPTIONAL, false].freeze
@@ -218,14 +441,18 @@ class OptionParser
# and resolved against a list of acceptable values.
#
module Completion
- def complete(key, icase = false, pat = nil)
- pat ||= Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'),
- icase)
- canon, sw, cn = nil
+ # :nodoc:
+
+ def self.regexp(key, icase)
+ Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'), icase)
+ end
+
+ def self.candidate(key, icase = false, pat = nil, &block)
+ pat ||= Completion.regexp(key, icase)
candidates = []
- each do |k, *v|
+ block.call do |k, *v|
(if Regexp === k
- kn = nil
+ kn = ""
k === key
else
kn = defined?(k.id2name) ? k.id2name : k
@@ -234,7 +461,19 @@ class OptionParser
v << k if v.empty?
candidates << [k, v, kn]
end
- candidates = candidates.sort_by {|k, v, kn| kn.size}
+ candidates
+ end
+
+ def self.completable?(key)
+ String.try_convert(key) or defined?(key.id2name)
+ end
+
+ def candidate(key, icase = false, pat = nil, &_)
+ Completion.candidate(key, icase, pat, &method(:each))
+ end
+
+ def complete(key, icase = false, pat = nil)
+ candidates = candidate(key, icase, pat, &method(:each)).sort_by {|k, v, kn| kn.size}
if candidates.size == 1
canon, sw, * = candidates[0]
elsif candidates.size > 1
@@ -263,7 +502,6 @@ class OptionParser
end
end
-
#
# Map from option/keyword string to object with completion.
#
@@ -271,14 +509,15 @@ class OptionParser
include Completion
end
-
#
# Individual switch class. Not important to the user.
#
# Defined within Switch are several Switch-derived classes: NoArgument,
- # RequiredArgument, etc.
+ # RequiredArgument, etc.
#
class Switch
+ # :nodoc:
+
attr_reader :pattern, :conv, :short, :long, :arg, :desc, :block
#
@@ -311,17 +550,18 @@ class OptionParser
def initialize(pattern = nil, conv = nil,
short = nil, long = nil, arg = nil,
- desc = ([] if short or long), block = Proc.new)
+ desc = ([] if short or long), block = nil, values = nil, &_block)
raise if Array === pattern
- @pattern, @conv, @short, @long, @arg, @desc, @block =
- pattern, conv, short, long, arg, desc, block
+ block ||= _block
+ @pattern, @conv, @short, @long, @arg, @desc, @block, @values =
+ pattern, conv, short, long, arg, desc, block, values
end
#
# Parses +arg+ and returns rest of +arg+ and matched portion to the
# argument pattern. Yields when the pattern doesn't match substring.
#
- def parse_arg(arg)
+ private def parse_arg(arg) # :nodoc:
pattern or return nil, [arg]
unless m = pattern.match(arg)
yield(InvalidArgument, arg)
@@ -339,22 +579,24 @@ class OptionParser
yield(InvalidArgument, arg) # didn't match whole arg
return arg[s.length..-1], m
end
- private :parse_arg
#
# Parses argument, converts and returns +arg+, +block+ and result of
# conversion. Yields at semi-error condition instead of raising an
# exception.
#
- def conv_arg(arg, val = [])
+ private def conv_arg(arg, val = []) # :nodoc:
+ v, = *val
if conv
val = conv.call(*val)
else
val = proc {|v| v}.call(*val)
end
+ if @values
+ @values.include?(val) or raise InvalidArgument, v
+ end
return arg, block, val
end
- private :conv_arg
#
# Produces the summary text. Each line of the summary is yielded to the
@@ -368,7 +610,7 @@ class OptionParser
# +max+ columns.
# +indent+:: Prefix string indents all summarized lines.
#
- def summarize(sdone = [], ldone = [], width = 1, max = width - 1, indent = "")
+ def summarize(sdone = {}, ldone = {}, width = 1, max = width - 1, indent = "")
sopts, lopts = [], [], nil
@short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short
@long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long
@@ -380,14 +622,20 @@ class OptionParser
while s = lopts.shift
l = left[-1].length + s.length
l += arg.length if left.size == 1 && arg
- l < max or sopts.empty? or left << ''
- left[-1] << if left[-1].empty? then ' ' * 4 else ', ' end << s
+ l < max or sopts.empty? or left << +''
+ left[-1] << (left[-1].empty? ? ' ' * 4 : ', ') << s
end
- left[0] << arg if arg
+ if arg
+ left[0] << (left[1] ? arg.sub(/\A(\[?)=/, '\1') + ',' : arg)
+ end
mlen = left.collect {|ss| ss.length}.max.to_i
while mlen > width and l = left.shift
mlen = left.collect {|ss| ss.length}.max.to_i if l.length == mlen
+ if l.length < width and (r = right[0]) and !r.empty?
+ l = l.to_s.ljust(width) + ' ' + r
+ right.shift
+ end
yield(indent + l)
end
@@ -418,6 +666,52 @@ class OptionParser
(long.first || short.first).sub(/\A-+(?:\[no-\])?/, '')
end
+ def compsys(sdone, ldone) # :nodoc:
+ sopts, lopts = [], []
+ @short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short
+ @long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long
+ return if sopts.empty? and lopts.empty? # completely hidden
+
+ (sopts+lopts).each do |opt|
+ # "(-x -c -r)-l[left justify]"
+ if /\A--\[no-\](.+)$/ =~ opt
+ o = $1
+ yield("--#{o}", desc.join(""))
+ yield("--no-#{o}", desc.join(""))
+ else
+ yield("#{opt}", desc.join(""))
+ end
+ end
+ end
+
+ def pretty_print_contents(q) # :nodoc:
+ if @block
+ q.text ":" + @block.source_location.join(":") + ":"
+ first = false
+ else
+ first = true
+ end
+ [@short, @long].each do |list|
+ list.each do |opt|
+ if first
+ q.text ":"
+ first = false
+ end
+ q.breakable
+ q.text opt
+ end
+ end
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) {pretty_print_contents(q)}
+ end
+
+ def omitted_argument(val) # :nodoc:
+ val.pop if val.size == 3 and val.last.nil?
+ val
+ end
+
#
# Switch that takes no arguments.
#
@@ -431,12 +725,16 @@ class OptionParser
conv_arg(arg)
end
- def self.incompatible_argument_styles(*)
+ def self.incompatible_argument_styles(*) # :nodoc:
end
- def self.pattern
+ def self.pattern # :nodoc:
Object
end
+
+ def pretty_head # :nodoc:
+ "NoArgument"
+ end
end
#
@@ -447,13 +745,17 @@ class OptionParser
#
# Raises an exception if argument is not present.
#
- def parse(arg, argv)
+ def parse(arg, argv, &_)
unless arg
raise MissingArgument if argv.empty?
arg = argv.shift
end
conv_arg(*parse_arg(arg, &method(:raise)))
end
+
+ def pretty_head # :nodoc:
+ "Required"
+ end
end
#
@@ -468,32 +770,41 @@ class OptionParser
if arg
conv_arg(*parse_arg(arg, &error))
else
- conv_arg(arg)
+ omitted_argument conv_arg(arg)
end
end
+
+ def pretty_head # :nodoc:
+ "Optional"
+ end
end
#
- # Switch that takes an argument, which does not begin with '-'.
+ # Switch that takes an argument, which does not begin with '-' or is '-'.
#
class PlacedArgument < self
#
- # Returns nil if argument is not present or begins with '-'.
+ # Returns nil if argument is not present or begins with '-' and is not '-'.
#
def parse(arg, argv, &error)
- if !(val = arg) and (argv.empty? or /\A-/ =~ (val = argv[0]))
- return nil, block, nil
+ if !(val = arg) and (argv.empty? or /\A-./ =~ (val = argv[0]))
+ return nil, block
end
opt = (val = parse_arg(val, &error))[1]
val = conv_arg(*val)
if opt and !arg
argv.shift
else
+ omitted_argument val
val[0] = nil
end
val
end
+
+ def pretty_head # :nodoc:
+ "Placed"
+ end
end
end
@@ -503,15 +814,17 @@ class OptionParser
# matching pattern and converter pair. Also provides summary feature.
#
class List
+ # :nodoc:
+
# Map from acceptable argument types to pattern and converter pairs.
attr_reader :atype
-
+
# Map from short style option switches to actual switch objects.
attr_reader :short
-
+
# Map from long style option switches to actual switch objects.
attr_reader :long
-
+
# List of all switches and summary string.
attr_reader :list
@@ -525,13 +838,24 @@ class OptionParser
@list = []
end
+ def pretty_print(q) # :nodoc:
+ q.group(1, "(", ")") do
+ @list.each do |sw|
+ next unless Switch === sw
+ q.group(1, "(" + sw.pretty_head, ")") do
+ sw.pretty_print_contents(q)
+ end
+ end
+ end
+ end
+
#
# See OptionParser.accept.
#
- def accept(t, pat = /.*/nm, &block)
+ def accept(t, pat = /.*/m, &block)
if pat
pat.respond_to?(:match) or
- raise TypeError, "has no `match'", ParseError.filter_backtrace(caller(2))
+ raise TypeError, "has no 'match'", ParseError.filter_backtrace(caller(2))
else
pat = t if t.respond_to?(:match)
end
@@ -556,19 +880,18 @@ class OptionParser
# +lopts+:: Long style option list.
# +nlopts+:: Negated long style options list.
#
- def update(sw, sopts, lopts, nsw = nil, nlopts = nil)
+ private def update(sw, sopts, lopts, nsw = nil, nlopts = nil) # :nodoc:
sopts.each {|o| @short[o] = sw} if sopts
lopts.each {|o| @long[o] = sw} if lopts
nlopts.each {|o| @long[o] = nsw} if nsw and nlopts
used = @short.invert.update(@long.invert)
@list.delete_if {|o| Switch === o and !used[o]}
end
- private :update
#
# Inserts +switch+ at the head of the list, and associates short, long
# and negated long options. Arguments are:
- #
+ #
# +switch+:: OptionParser::Switch instance to be inserted.
# +short_opts+:: List of short style options.
# +long_opts+:: List of long style options.
@@ -584,7 +907,7 @@ class OptionParser
#
# Appends +switch+ at the tail of the list, and associates short, long
# and negated long options. Arguments are:
- #
+ #
# +switch+:: OptionParser::Switch instance to be inserted.
# +short_opts+:: List of short style options.
# +long_opts+:: List of long style options.
@@ -618,6 +941,10 @@ class OptionParser
__send__(id).complete(opt, icase, *pat, &block)
end
+ def get_candidates(id)
+ yield __send__(id).keys
+ end
+
#
# Iterates over each option, passing the option to the +block+.
#
@@ -656,6 +983,14 @@ class OptionParser
end
to
end
+
+ def compsys(*args, &block) # :nodoc:
+ list.each do |opt|
+ if opt.respond_to?(:compsys)
+ opt.compsys(*args, &block)
+ end
+ end
+ end
end
#
@@ -685,7 +1020,7 @@ class OptionParser
# OPTIONAL_ARGUMENT:: The switch requires an optional argument. (:OPTIONAL)
#
# Use like --switch=argument (long style) or -Xargument (short style). For
- # short style, only portion matched to argument pattern is dealed as
+ # short style, only portion matched to argument pattern is treated as
# argument.
#
ArgumentStyle = {}
@@ -702,6 +1037,43 @@ class OptionParser
DefaultList.short['-'] = Switch::NoArgument.new {}
DefaultList.long[''] = Switch::NoArgument.new {throw :terminate}
+ COMPSYS_HEADER = <<'XXX' # :nodoc:
+
+typeset -A opt_args
+local context state line
+
+_arguments -s -S \
+XXX
+
+ def compsys(to, name = File.basename($0)) # :nodoc:
+ to << "#compdef #{name}\n"
+ to << COMPSYS_HEADER
+ visit(:compsys, {}, {}) {|o, d|
+ to << %Q[ "#{o}[#{d.gsub(/[\\\"\[\]]/, '\\\\\&')}]" \\\n]
+ }
+ to << " '*:file:_files' && return 0\n"
+ end
+
+ def help_exit
+ if $stdout.tty? && (pager = ENV.values_at(*%w[RUBY_PAGER PAGER]).find {|e| e && !e.empty?})
+ less = ENV["LESS"]
+ args = [{"LESS" => "#{less} -Fe"}, pager, "w"]
+ print = proc do |f|
+ f.puts help
+ rescue Errno::EPIPE
+ # pager terminated
+ end
+ if Process.respond_to?(:fork) and false
+ IO.popen("-") {|f| f ? Process.exec(*args, in: f) : print.call($stdout)}
+ # unreachable
+ end
+ IO.popen(*args, &print)
+ else
+ puts help
+ end
+ exit
+ end
+
#
# Default options for ARGV, which never appear in option summary.
#
@@ -712,8 +1084,29 @@ class OptionParser
# Shows option summary.
#
Officious['help'] = proc do |parser|
- Switch::NoArgument.new do
- puts parser.help
+ Switch::NoArgument.new do |arg|
+ parser.help_exit
+ end
+ end
+
+ #
+ # --*-completion-bash=WORD
+ # Shows candidates for command line completion.
+ #
+ Officious['*-completion-bash'] = proc do |parser|
+ Switch::RequiredArgument.new do |arg|
+ puts parser.candidate(arg)
+ exit
+ end
+ end
+
+ #
+ # --*-completion-zsh[=NAME:FILE]
+ # Creates zsh completion file.
+ #
+ Officious['*-completion-zsh'] = proc do |parser|
+ Switch::OptionalArgument.new do |arg|
+ parser.compsys($stdout, arg)
exit
end
end
@@ -726,7 +1119,7 @@ class OptionParser
Switch::OptionalArgument.new do |pkg|
if pkg
begin
- require 'optparse/version'
+ require_relative 'optparse/version'
rescue LoadError
else
show_version(*pkg.split(/,/)) or
@@ -750,7 +1143,7 @@ class OptionParser
# Initializes a new instance and evaluates the optional block in context
# of the instance. Arguments +args+ are passed to #new, see there for
# description of parameters.
- #
+ #
# This method is *deprecated*, its behavior corresponds to the older #new
# method.
#
@@ -771,6 +1164,10 @@ class OptionParser
default.to_i + 1
end
end
+
+ #
+ # See self.inc
+ #
def inc(*args)
self.class.inc(*args)
end
@@ -789,6 +1186,8 @@ class OptionParser
@summary_width = width
@summary_indent = indent
@default_argv = ARGV
+ @require_exact = false
+ @raise_unknown = true
add_officious
yield self if block_given?
end
@@ -807,11 +1206,19 @@ class OptionParser
def terminate(arg = nil)
self.class.terminate(arg)
end
+ #
+ # See #terminate.
+ #
def self.terminate(arg = nil)
throw :terminate, arg
end
@stack = [DefaultList]
+ #
+ # Returns the global top option list.
+ #
+ # Do not use directly.
+ #
def self.top() DefaultList end
#
@@ -832,9 +1239,9 @@ class OptionParser
#
# Directs to reject specified class argument.
#
- # +t+:: Argument class specifier, any object including Class.
+ # +type+:: Argument class specifier, any object including Class.
#
- # reject(t)
+ # reject(type)
#
def reject(*args, &blk) top.reject(*args, &blk) end
#
@@ -862,12 +1269,19 @@ class OptionParser
# Strings to be parsed in default.
attr_accessor :default_argv
+ # Whether to require that options match exactly (disallows providing
+ # abbreviated long option as short option).
+ attr_accessor :require_exact
+
+ # Whether to raise at unknown option.
+ attr_accessor :raise_unknown
+
#
# Heading banner preceding summary.
#
def banner
unless @banner
- @banner = "Usage: #{program_name} [options]"
+ @banner = +"Usage: #{program_name} [options]"
visit(:add_banner, @banner)
end
@banner
@@ -878,7 +1292,15 @@ class OptionParser
# to $0.
#
def program_name
- @program_name || File.basename($0, '.*')
+ @program_name || strip_ext(File.basename($0))
+ end
+
+ private def strip_ext(name) # :nodoc:
+ exts = /#{
+ require "rbconfig"
+ Regexp.union(*RbConfig::CONFIG["EXECUTABLE_EXTS"]&.split(" "))
+ }\z/o
+ name.sub(exts, "")
end
# for experimental cascading :-)
@@ -896,14 +1318,14 @@ class OptionParser
# Version
#
def version
- @version || (defined?(::Version) && ::Version)
+ (defined?(@version) && @version) || (defined?(::Version) && ::Version)
end
#
# Release code
#
def release
- @release || (defined?(::Release) && ::Release) || (defined?(::RELEASE) && ::RELEASE)
+ (defined?(@release) && @release) || (defined?(::Release) && ::Release) || (defined?(::RELEASE) && ::RELEASE)
end
#
@@ -911,16 +1333,30 @@ class OptionParser
#
def ver
if v = version
- str = "#{program_name} #{[v].join('.')}"
+ str = +"#{program_name} #{[v].join('.')}"
str << " (#{v})" if v = release
str
end
end
+ #
+ # Shows warning message with the program name
+ #
+ # +mesg+:: Message, defaulted to +$!+.
+ #
+ # See Kernel#warn.
+ #
def warn(mesg = $!)
super("#{program_name}: #{mesg}")
end
+ #
+ # Shows message with the program name then aborts.
+ #
+ # +mesg+:: Message, defaulted to +$!+.
+ #
+ # See Kernel#abort.
+ #
def abort(mesg = $!)
super("#{program_name}: #{mesg}")
end
@@ -942,6 +1378,9 @@ class OptionParser
#
# Pushes a new List.
#
+ # If a block is given, yields +self+ and returns the result of the
+ # block, otherwise returns +self+.
+ #
def new
@stack.push(List.new)
if block_given?
@@ -968,7 +1407,8 @@ class OptionParser
# +indent+:: Indentation, defaults to @summary_indent.
#
def summarize(to = [], width = @summary_width, max = width - 1, indent = @summary_indent, &blk)
- blk ||= proc {|l| to << (l.index($/, -1) ? l : l + $/)}
+ nl = "\n"
+ blk ||= proc {|l| to << (l.index(nl, -1) ? l : l + nl)}
visit(:summarize, {}, {}, width, max, indent, &blk)
to
end
@@ -976,13 +1416,36 @@ class OptionParser
#
# Returns option summary string.
#
- def help; summarize(banner.to_s.sub(/\n?\z/, "\n")) end
+ def help; summarize("#{banner}".sub(/\n?\z/, "\n")) end
alias to_s help
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) do
+ first = true
+ if @stack.size > 2
+ @stack.each_with_index do |s, i|
+ next if i < 2
+ next if s.list.empty?
+ if first
+ first = false
+ q.text ":"
+ end
+ q.breakable
+ s.pretty_print(q)
+ end
+ end
+ end
+ end
+
+ def inspect # :nodoc:
+ require 'pp'
+ pretty_print_inspect
+ end
+
#
# Returns option summary list.
#
- def to_a; summarize(banner.to_a.dup) end
+ def to_a; summarize("#{banner}".split(/^/)) end
#
# Checks if an argument is given twice, in which case an ArgumentError is
@@ -992,73 +1455,20 @@ class OptionParser
# +prv+:: Previously specified argument.
# +msg+:: Exception message.
#
- def notwice(obj, prv, msg)
+ private def notwice(obj, prv, msg) # :nodoc:
unless !prv or prv == obj
raise(ArgumentError, "argument #{msg} given twice: #{obj}",
ParseError.filter_backtrace(caller(2)))
end
obj
end
- private :notwice
-
- SPLAT_PROC = proc {|*a| a.length <= 1 ? a.first : a}
- #
- # Creates an OptionParser::Switch from the parameters. The parsed argument
- # value is passed to the given block, where it can be processed.
- #
- # See at the beginning of OptionParser for some full examples.
- #
- # +opts+ can include the following elements:
- #
- # [Argument style:]
- # One of the following:
- # :NONE, :REQUIRED, :OPTIONAL
- #
- # [Argument pattern:]
- # Acceptable option argument format, must be pre-defined with
- # OptionParser.accept or OptionParser#accept, or Regexp. This can appear
- # once or assigned as String if not present, otherwise causes an
- # ArgumentError. Examples:
- # Float, Time, Array
- #
- # [Possible argument values:]
- # Hash or Array.
- # [:text, :binary, :auto]
- # %w[iso-2022-jp shift_jis euc-jp utf8 binary]
- # { "jis" => "iso-2022-jp", "sjis" => "shift_jis" }
- #
- # [Long style switch:]
- # Specifies a long style switch which takes a mandatory, optional or no
- # argument. It's a string of the following form:
- # "--switch=MANDATORY" or "--switch MANDATORY"
- # "--switch[=OPTIONAL]"
- # "--switch"
- #
- # [Short style switch:]
- # Specifies short style switch which takes a mandatory, optional or no
- # argument. It's a string of the following form:
- # "-xMANDATORY"
- # "-x[OPTIONAL]"
- # "-x"
- # There is also a special form which matches character range (not full
- # set of regular expression):
- # "-[a-z]MANDATORY"
- # "-[a-z][OPTIONAL]"
- # "-[a-z]"
- #
- # [Argument style and description:]
- # Instead of specifying mandatory or optional arguments directly in the
- # switch parameter, this separate parameter can be used.
- # "=MANDATORY"
- # "=[OPTIONAL]"
- #
- # [Description:]
- # Description string for the option.
- # "Run verbosely"
- #
- # [Handler:]
- # Handler for the parsed argument value. Either give a block or pass a
- # Proc or Method as an argument.
+
+ SPLAT_PROC = proc {|*a| a.length <= 1 ? a.first : a} # :nodoc:
+
+ # :call-seq:
+ # make_switch(params, block = nil)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
#
def make_switch(opts, block = nil)
short, long, nolong, style, pattern, conv, not_pattern, not_conv, not_style = [], [], []
@@ -1066,7 +1476,9 @@ class OptionParser
default_style = Switch::NoArgument
default_pattern = nil
klass = nil
- n, q, a = nil
+ q, a = nil
+ has_arg = false
+ values = nil
opts.each do |o|
# argument class
@@ -1080,7 +1492,7 @@ class OptionParser
end
# directly specified pattern(any object possible to match)
- if (!(String === o || Symbol === o)) and o.respond_to?(:match)
+ if !Completion.completable?(o) and o.respond_to?(:match)
pattern = notwice(o, pattern, 'pattern')
if pattern.respond_to?(:convert)
conv = pattern.method(:convert).to_proc
@@ -1094,7 +1506,12 @@ class OptionParser
case o
when Proc, Method
block = notwice(o, block, 'block')
- when Array, Hash
+ when Array, Hash, Set
+ if Array === o
+ o, v = o.partition {|v,| Completion.completable?(v)}
+ values = notwice(v, values, 'values') unless v.empty?
+ next if o.empty?
+ end
case pattern
when CompletingHash
when nil
@@ -1104,11 +1521,13 @@ class OptionParser
raise ArgumentError, "argument pattern given twice"
end
o.each {|pat, *v| pattern[pat] = v.fetch(0) {pat}}
+ when Range
+ values = notwice(o, values, 'values')
when Module
raise ArgumentError, "unsupported argument type: #{o}", ParseError.filter_backtrace(caller(4))
when *ArgumentStyle.keys
style = notwice(ArgumentStyle[o], style, 'style')
- when /^--no-([^\[\]=\s]*)(.+)?/
+ when /\A--no-([^\[\]=\s]*)(.+)?/
q, a = $1, $2
o = notwice(a ? Object : TrueClass, klass, 'type')
not_pattern, not_conv = search(:atype, o) unless not_style
@@ -1116,9 +1535,10 @@ class OptionParser
default_style = Switch::NoArgument
default_pattern, conv = search(:atype, FalseClass) unless default_pattern
ldesc << "--no-#{q}"
- long << 'no-' + (q = q.downcase)
+ (q = q.downcase).tr!('_', '-')
+ long << "no-#{q}"
nolong << q
- when /^--\[no-\]([^\[\]=\s]*)(.+)?/
+ when /\A--\[no-\]([^\[\]=\s]*)(.+)?/
q, a = $1, $2
o = notwice(a ? Object : TrueClass, klass, 'type')
if a
@@ -1126,11 +1546,12 @@ class OptionParser
default_pattern, conv = search(:atype, o) unless default_pattern
end
ldesc << "--[no-]#{q}"
- long << (o = q.downcase)
+ (o = q.downcase).tr!('_', '-')
+ long << o
not_pattern, not_conv = search(:atype, FalseClass) unless not_style
not_style = Switch::NoArgument
- nolong << 'no-' + o
- when /^--([^\[\]=\s]*)(.+)?/
+ nolong << "no-#{o}"
+ when /\A--([^\[\]=\s]*)(.+)?/
q, a = $1, $2
if a
o = notwice(NilClass, klass, 'type')
@@ -1138,17 +1559,20 @@ class OptionParser
default_pattern, conv = search(:atype, o) unless default_pattern
end
ldesc << "--#{q}"
- long << (o = q.downcase)
- when /^-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/
+ (o = q.downcase).tr!('_', '-')
+ long << o
+ when /\A-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/
q, a = $1, $2
o = notwice(Object, klass, 'type')
if a
default_style = default_style.guess(arg = a)
default_pattern, conv = search(:atype, o) unless default_pattern
+ else
+ has_arg = true
end
sdesc << "-#{q}"
short << Regexp.new(q)
- when /^-(.)(.+)?/
+ when /\A-(.)(.+)?/
q, a = $1, $2
if a
o = notwice(NilClass, klass, 'type')
@@ -1157,18 +1581,27 @@ class OptionParser
end
sdesc << "-#{q}"
short << q
- when /^=/
+ when /\A=/
style = notwice(default_style.guess(arg = o), style, 'style')
default_pattern, conv = search(:atype, Object) unless default_pattern
else
- desc.push(o)
+ desc.push(o) if o && !o.empty?
end
end
default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern
+ if Range === values and klass
+ unless (!values.begin or klass === values.begin) and
+ (!values.end or klass === values.end)
+ raise ArgumentError, "range does not match class"
+ end
+ end
if !(short.empty? and long.empty?)
+ if has_arg and default_style == Switch::NoArgument
+ default_style = Switch::RequiredArgument
+ end
s = (style || default_style).new(pattern || default_pattern,
- conv, sdesc, ldesc, arg, desc, block)
+ conv, sdesc, ldesc, arg, desc, block, values)
elsif !block
if style or pattern
raise ArgumentError, "no switch given", ParseError.filter_backtrace(caller)
@@ -1177,21 +1610,33 @@ class OptionParser
else
short << pattern
s = (style || default_style).new(pattern,
- conv, nil, nil, arg, desc, block)
+ conv, nil, nil, arg, desc, block, values)
end
return s, short, long,
(not_style.new(not_pattern, not_conv, sdesc, ldesc, nil, desc, block) if not_style),
nolong
end
+ # ----
+ # Option definition phase methods
+ #
+ # These methods are used to define options, or to construct an
+ # OptionParser instance in other words.
+
+ # :call-seq:
+ # define(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
def define(*opts, &block)
top.append(*(sw = make_switch(opts, block)))
sw[0]
end
+ # :call-seq:
+ # on(*params, &block)
#
- # Add option switch and handler. See #make_switch for an explanation of
- # parameters.
+ # :include: ../doc/optparse/creates_option.rdoc
#
def on(*opts, &block)
define(*opts, &block)
@@ -1199,13 +1644,22 @@ class OptionParser
end
alias def_option define
+ # :call-seq:
+ # define_head(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
def define_head(*opts, &block)
top.prepend(*(sw = make_switch(opts, block)))
sw[0]
end
+ # :call-seq:
+ # on_head(*params, &block)
#
- # Add option switch like with #on, but at head of summary.
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ # The new option is added at the head of the summary.
#
def on_head(*opts, &block)
define_head(*opts, &block)
@@ -1213,13 +1667,23 @@ class OptionParser
end
alias def_head_option define_head
+ # :call-seq:
+ # define_tail(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
def define_tail(*opts, &block)
base.append(*(sw = make_switch(opts, block)))
sw[0]
end
#
- # Add option switch like with #on, but at tail of summary.
+ # :call-seq:
+ # on_tail(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ # The new option is added at the tail of the summary.
#
def on_tail(*opts, &block)
define_tail(*opts, &block)
@@ -1234,58 +1698,83 @@ class OptionParser
top.append(string, nil, nil)
end
+ # ----
+ # Arguments parse phase methods
+ #
+ # These methods parse +argv+, convert, and store the results by
+ # calling handlers. As these methods do not modify +self+, +self+
+ # can be frozen.
+
#
# Parses command line arguments +argv+ in order. When a block is given,
- # each non-option argument is yielded.
+ # each non-option argument is yielded. When optional +into+ keyword
+ # argument is provided, the parsed option values are stored there via
+ # <code>[]=</code> method (so it can be Hash, or OpenStruct, or other
+ # similar object).
#
# Returns the rest of +argv+ left unparsed.
#
- def order(*argv, &block)
+ def order(*argv, **keywords, &nonopt)
argv = argv[0].dup if argv.size == 1 and Array === argv[0]
- order!(argv, &block)
+ order!(argv, **keywords, &nonopt)
end
#
# Same as #order, but removes switches destructively.
+ # Non-option arguments remain in +argv+.
#
- def order!(argv = default_argv, &nonopt)
- parse_in_order(argv, &nonopt)
+ def order!(argv = default_argv, into: nil, **keywords, &nonopt)
+ setter = ->(name, val) {into[name.to_sym] = val} if into
+ parse_in_order(argv, setter, **keywords, &nonopt)
end
- def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc:
+ private def parse_in_order(argv = default_argv, setter = nil, exact: require_exact, **, &nonopt) # :nodoc:
opt, arg, val, rest = nil
nonopt ||= proc {|a| throw :terminate, a}
argv.unshift(arg) if arg = catch(:terminate) {
while arg = argv.shift
case arg
# long option
- when /\A--([^=]*)(?:=(.*))?/nm
+ when /\A--([^=]*)(?:=(.*))?/m
opt, rest = $1, $2
+ opt.tr!('_', '-')
begin
- sw, = complete(:long, opt, true)
+ if exact
+ sw, = search(:long, opt)
+ else
+ sw, = complete(:long, opt, true)
+ end
rescue ParseError
+ throw :terminate, arg unless raise_unknown
raise $!.set_option(arg, true)
+ else
+ unless sw
+ throw :terminate, arg unless raise_unknown
+ raise InvalidOption, arg
+ end
end
begin
opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)}
- val = cb.call(val) if cb
- setter.call(sw.switch_name, val) if setter
+ val = callback!(cb, 1, val) if cb
+ callback!(setter, 2, sw.switch_name, val) if setter
rescue ParseError
raise $!.set_option(arg, rest)
end
# short option
- when /\A-(.)((=).*|.+)?/nm
- opt, has_arg, eq, val, rest = $1, $3, $3, $2, $2
+ when /\A-(.)((=).*|.+)?/m
+ eq, rest, opt = $3, $2, $1
+ has_arg, val = eq, rest
begin
sw, = search(:short, opt)
unless sw
begin
sw, = complete(:short, opt)
# short option matched.
- val = arg.sub(/\A-/, '')
+ val = arg.delete_prefix('-')
has_arg = true
rescue InvalidOption
+ raise if exact
# if no short options match, try completion with long
# options.
sw, = complete(:long, opt)
@@ -1293,14 +1782,20 @@ class OptionParser
end
end
rescue ParseError
+ throw :terminate, arg unless raise_unknown
raise $!.set_option(arg, true)
end
begin
opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq}
+ rescue ParseError
+ raise $!.set_option(arg, arg.length > 2)
+ else
raise InvalidOption, arg if has_arg and !eq and arg == "-#{opt}"
- argv.unshift(opt) if opt and (opt = opt.sub(/\A-*/, '-')) != '-'
- val = cb.call(val) if cb
- setter.call(sw.switch_name, val) if setter
+ end
+ begin
+ argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-')
+ val = callback!(cb, 1, val) if cb
+ callback!(setter, 2, sw.switch_name, val) if setter
rescue ParseError
raise $!.set_option(arg, arg.length > 2)
end
@@ -1324,23 +1819,38 @@ class OptionParser
argv
end
- private :parse_in_order
+
+ # Calls callback with _val_.
+ private def callback!(cb, max_arity, *args) # :nodoc:
+ args.compact!
+
+ if (size = args.size) < max_arity and cb.to_proc.lambda?
+ (arity = cb.arity) < 0 and arity = (1-arity)
+ arity = max_arity if arity > max_arity
+ args[arity - 1] = nil if arity > size
+ end
+ cb.call(*args)
+ end
#
# Parses command line arguments +argv+ in permutation mode and returns
- # list of non-option arguments.
+ # list of non-option arguments. When optional +into+ keyword
+ # argument is provided, the parsed option values are stored there via
+ # <code>[]=</code> method (so it can be Hash, or OpenStruct, or other
+ # similar object).
#
- def permute(*argv)
+ def permute(*argv, **keywords)
argv = argv[0].dup if argv.size == 1 and Array === argv[0]
- permute!(argv)
+ permute!(argv, **keywords)
end
#
# Same as #permute, but removes switches destructively.
+ # Non-option arguments remain in +argv+.
#
- def permute!(argv = default_argv)
+ def permute!(argv = default_argv, **keywords)
nonopts = []
- order!(argv, &nonopts.method(:<<))
+ order!(argv, **keywords) {|nonopt| nonopts << nonopt}
argv[0, 0] = nonopts
argv
end
@@ -1348,128 +1858,215 @@ class OptionParser
#
# Parses command line arguments +argv+ in order when environment variable
# POSIXLY_CORRECT is set, and in permutation mode otherwise.
+ # When optional +into+ keyword argument is provided, the parsed option
+ # values are stored there via <code>[]=</code> method (so it can be Hash,
+ # or OpenStruct, or other similar object).
#
- def parse(*argv)
+ def parse(*argv, **keywords)
argv = argv[0].dup if argv.size == 1 and Array === argv[0]
- parse!(argv)
+ parse!(argv, **keywords)
end
#
# Same as #parse, but removes switches destructively.
+ # Non-option arguments remain in +argv+.
#
- def parse!(argv = default_argv)
+ def parse!(argv = default_argv, **keywords)
if ENV.include?('POSIXLY_CORRECT')
- order!(argv)
+ order!(argv, **keywords)
else
- permute!(argv)
+ permute!(argv, **keywords)
end
end
#
# Wrapper method for getopts.rb.
#
- # params = ARGV.getopts("ab:", "foo", "bar:")
+ # params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option")
+ # # params["a"] = true # -a
+ # # params["b"] = "1" # -b1
+ # # params["foo"] = "1" # --foo
+ # # params["bar"] = "x" # --bar x
+ # # params["zot"] = "z" # --zot Z
+ #
+ # Option +symbolize_names+ (boolean) specifies whether returned Hash keys should be Symbols; defaults to +false+ (use Strings).
+ #
+ # params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option", symbolize_names: true)
# # params[:a] = true # -a
# # params[:b] = "1" # -b1
# # params[:foo] = "1" # --foo
# # params[:bar] = "x" # --bar x
+ # # params[:zot] = "z" # --zot Z
#
- def getopts(*args)
+ def getopts(*args, symbolize_names: false, **keywords)
argv = Array === args.first ? args.shift : default_argv
single_options, *long_options = *args
result = {}
+ setter = (symbolize_names ?
+ ->(name, val) {result[name.to_sym] = val}
+ : ->(name, val) {result[name] = val})
single_options.scan(/(.)(:)?/) do |opt, val|
if val
- result[opt] = nil
+ setter[opt, nil]
define("-#{opt} VAL")
else
- result[opt] = false
+ setter[opt, false]
define("-#{opt}")
end
end if single_options
long_options.each do |arg|
+ arg, desc = arg.split(';', 2)
opt, val = arg.split(':', 2)
if val
- result[opt] = val.empty? ? nil : val
- define("--#{opt} VAL")
+ setter[opt, (val unless val.empty?)]
+ define("--#{opt}=#{result[opt] || "VAL"}", *[desc].compact)
else
- result[opt] = false
- define("--#{opt}")
+ setter[opt, false]
+ define("--#{opt}", *[desc].compact)
end
end
- parse_in_order(argv, result.method(:[]=))
+ parse_in_order(argv, setter, **keywords)
result
end
#
# See #getopts.
#
- def self.getopts(*args)
- new.getopts(*args)
+ def self.getopts(*args, symbolize_names: false)
+ new.getopts(*args, symbolize_names: symbolize_names)
end
#
# Traverses @stack, sending each element method +id+ with +args+ and
# +block+.
#
- def visit(id, *args, &block)
+ private def visit(id, *args, &block) # :nodoc:
@stack.reverse_each do |el|
- el.send(id, *args, &block)
+ el.__send__(id, *args, &block)
end
nil
end
- private :visit
#
# Searches +key+ in @stack for +id+ hash and returns or yields the result.
#
- def search(id, key)
+ private def search(id, key) # :nodoc:
block_given = block_given?
visit(:search, id, key) do |k|
return block_given ? yield(k) : k
end
end
- private :search
#
# Completes shortened long style option switch and returns pair of
# canonical switch and switch descriptor OptionParser::Switch.
#
- # +id+:: Searching table.
+ # +typ+:: Searching table.
# +opt+:: Searching key.
# +icase+:: Search case insensitive if true.
# +pat+:: Optional pattern for completion.
#
- def complete(typ, opt, icase = false, *pat)
+ private def complete(typ, opt, icase = false, *pat) # :nodoc:
if pat.empty?
search(typ, opt) {|sw| return [sw, opt]} # exact match or...
end
- raise AmbiguousOption, catch(:ambiguous) {
+ ambiguous = catch(:ambiguous) {
visit(:complete, typ, opt, icase, *pat) {|o, *sw| return sw}
- raise InvalidOption, opt
}
+ exc = ambiguous ? AmbiguousOption : InvalidOption
+ raise exc.new(opt, additional: proc {|o| additional_message(typ, o)})
+ end
+
+ #
+ # Returns additional info.
+ #
+ def additional_message(typ, opt)
+ return unless typ and opt and defined?(DidYouMean::SpellChecker)
+ all_candidates = []
+ visit(:get_candidates, typ) do |candidates|
+ all_candidates.concat(candidates)
+ end
+ all_candidates.select! {|cand| cand.is_a?(String) }
+ checker = DidYouMean::SpellChecker.new(dictionary: all_candidates)
+ DidYouMean.formatter.message_for(all_candidates & checker.correct(opt))
+ end
+
+ #
+ # Return candidates for +word+.
+ #
+ def candidate(word)
+ list = []
+ case word
+ when '-'
+ long = short = true
+ when /\A--/
+ word, arg = word.split(/=/, 2)
+ argpat = Completion.regexp(arg, false) if arg and !arg.empty?
+ long = true
+ when /\A-/
+ short = true
+ end
+ pat = Completion.regexp(word, long)
+ visit(:each_option) do |opt|
+ next unless Switch === opt
+ opts = (long ? opt.long : []) + (short ? opt.short : [])
+ opts = Completion.candidate(word, true, pat, &opts.method(:each)).map(&:first) if pat
+ if /\A=/ =~ opt.arg
+ opts.map! {|sw| sw + "="}
+ if arg and CompletingHash === opt.pattern
+ if opts = opt.pattern.candidate(arg, false, argpat)
+ opts.map!(&:last)
+ end
+ end
+ end
+ list.concat(opts)
+ end
+ list
end
- private :complete
#
# Loads options from file names as +filename+. Does nothing when the file
# is not present. Returns whether successfully loaded.
#
# +filename+ defaults to basename of the program without suffix in a
- # directory ~/.options.
- #
- def load(filename = nil)
- begin
- filename ||= File.expand_path(File.basename($0, '.*'), '~/.options')
- rescue
- return false
+ # directory ~/.options, then the basename with '.options' suffix
+ # under XDG and Haiku standard places.
+ #
+ # The optional +into+ keyword argument works exactly like that accepted in
+ # method #parse.
+ #
+ def load(filename = nil, **keywords)
+ unless filename
+ basename = File.basename($0, '.*')
+ return true if load(File.expand_path("~/.options/#{basename}"), **keywords) rescue nil
+ basename << ".options"
+ if !(xdg = ENV['XDG_CONFIG_HOME']) or xdg.empty?
+ # https://specifications.freedesktop.org/basedir-spec/latest/#variables
+ #
+ # If $XDG_CONFIG_HOME is either not set or empty, a default
+ # equal to $HOME/.config should be used.
+ xdg = ['~/.config', true]
+ end
+ return [
+ xdg,
+
+ *ENV['XDG_CONFIG_DIRS']&.split(File::PATH_SEPARATOR),
+
+ # Haiku
+ ['~/config/settings', true],
+ ].any? {|dir, expand|
+ next if !dir or dir.empty?
+ filename = File.join(dir, basename)
+ filename = File.expand_path(filename) if expand
+ load(filename, **keywords) rescue nil
+ }
end
begin
- parse(*IO.readlines(filename).each {|s| s.chomp!})
+ parse(*File.readlines(filename, chomp: true), **keywords)
true
rescue Errno::ENOENT, Errno::ENOTDIR
false
@@ -1482,10 +2079,10 @@ class OptionParser
#
# +env+ defaults to the basename of the program.
#
- def environment(env = File.basename($0, '.*'))
+ def environment(env = File.basename($0, '.*'), **keywords)
env = ENV[env] || ENV[env.upcase] or return
require 'shellwords'
- parse(*Shellwords.shellwords(env))
+ parse(*Shellwords.shellwords(env), **keywords)
end
#
@@ -1502,7 +2099,7 @@ class OptionParser
#
# Any non-empty string, and no conversion.
#
- accept(String, /.+/nm) {|s,*|s}
+ accept(String, /.+/m) {|s,*|s}
#
# Ruby/C-like integer, octal for 0-7 sequence, binary for 0b, hexadecimal
@@ -1512,42 +2109,80 @@ class OptionParser
decimal = '\d+(?:_\d+)*'
binary = 'b[01]+(?:_[01]+)*'
hex = 'x[\da-f]+(?:_[\da-f]+)*'
- octal = "0(?:[0-7]*(?:_[0-7]+)*|#{binary}|#{hex})"
+ octal = "0(?:[0-7]+(?:_[0-7]+)*|#{binary}|#{hex})?"
integer = "#{octal}|#{decimal}"
- accept(Integer, %r"\A[-+]?(?:#{integer})"io) {|s,| Integer(s) if s}
+
+ accept(Integer, %r"\A[-+]?(?:#{integer})\z"io) {|s,|
+ begin
+ Integer(s)
+ rescue ArgumentError
+ raise OptionParser::InvalidArgument, s
+ end if s
+ }
#
# Float number format, and converts to Float.
#
- float = "(?:#{decimal}(?:\\.(?:#{decimal})?)?|\\.#{decimal})(?:E[-+]?#{decimal})?"
- floatpat = %r"\A[-+]?#{float}"io
+ float = "(?:#{decimal}(?=(.)?)(?:\\.(?:#{decimal})?)?|\\.#{decimal})(?:E[-+]?#{decimal})?"
+ floatpat = %r"\A[-+]?#{float}\z"io
accept(Float, floatpat) {|s,| s.to_f if s}
#
# Generic numeric format, converts to Integer for integer format, Float
- # for float format.
- #
- accept(Numeric, %r"\A[-+]?(?:#{octal}|#{float})"io) {|s,| eval(s) if s}
+ # for float format, and Rational for rational format.
+ #
+ real = "[-+]?(?:#{octal}|#{float})"
+ accept(Numeric, /\A(#{real})(?:\/(#{real}))?\z/io) {|s, d, f, n,|
+ if n
+ Rational(d, n)
+ elsif f
+ Float(s)
+ else
+ Integer(s)
+ end
+ }
#
# Decimal integer format, to be converted to Integer.
#
- DecimalInteger = /\A[-+]?#{decimal}/io
- accept(DecimalInteger) {|s,| s.to_i if s}
+ DecimalInteger = /\A[-+]?#{decimal}\z/io
+ accept(DecimalInteger, DecimalInteger) {|s,|
+ begin
+ Integer(s, 10)
+ rescue ArgumentError
+ raise OptionParser::InvalidArgument, s
+ end if s
+ }
#
# Ruby/C like octal/hexadecimal/binary integer format, to be converted to
# Integer.
#
- OctalInteger = /\A[-+]?(?:[0-7]+(?:_[0-7]+)*|0(?:#{binary}|#{hex}))/io
- accept(OctalInteger) {|s,| s.oct if s}
+ OctalInteger = /\A[-+]?(?:[0-7]+(?:_[0-7]+)*|0(?:#{binary}|#{hex}))\z/io
+ accept(OctalInteger, OctalInteger) {|s,|
+ begin
+ Integer(s, 8)
+ rescue ArgumentError
+ raise OptionParser::InvalidArgument, s
+ end if s
+ }
#
# Decimal integer/float number format, to be converted to Integer for
# integer format, Float for float format.
#
DecimalNumeric = floatpat # decimal integer is allowed as float also.
- accept(DecimalNumeric) {|s,| eval(s) if s}
+ accept(DecimalNumeric, floatpat) {|s, f|
+ begin
+ if f
+ Float(s)
+ else
+ Integer(s)
+ end
+ rescue ArgumentError
+ raise OptionParser::InvalidArgument, s
+ end if s
+ }
#
# Boolean switch, which means whether it is present or not, whether it is
@@ -1557,7 +2192,7 @@ class OptionParser
yesno = CompletingHash.new
%w[- no false].each {|el| yesno[el] = false}
%w[+ yes true].each {|el| yesno[el] = true}
- yesno['nil'] = false # shoud be nil?
+ yesno['nil'] = false # should be nil?
accept(TrueClass, yesno) {|arg, val| val == nil or val}
#
# Similar to TrueClass, but defaults to false.
@@ -1567,7 +2202,7 @@ class OptionParser
#
# List of strings separated by ",".
#
- accept(Array) do |s,|
+ accept(Array) do |s, |
if s
s = s.split(',').collect {|ss| ss unless ss.empty?}
end
@@ -1583,9 +2218,23 @@ class OptionParser
f |= Regexp::IGNORECASE if /i/ =~ o
f |= Regexp::MULTILINE if /m/ =~ o
f |= Regexp::EXTENDED if /x/ =~ o
- k = o.delete("^imx")
+ case o = o.delete("imx")
+ when ""
+ when "u"
+ s = s.encode(Encoding::UTF_8)
+ when "e"
+ s = s.encode(Encoding::EUC_JP)
+ when "s"
+ s = s.encode(Encoding::SJIS)
+ when "n"
+ f |= Regexp::NOENCODING
+ else
+ raise OptionParser::InvalidArgument, "unknown regexp option - #{o}"
+ end
+ else
+ s ||= all
end
- Regexp.new(s || all, f, k)
+ Regexp.new(s, f)
end
#
@@ -1597,15 +2246,19 @@ class OptionParser
#
class ParseError < RuntimeError
# Reason which caused the error.
- Reason = 'parse error'.freeze
+ Reason = 'parse error'
- def initialize(*args)
+ # :nodoc:
+ def initialize(*args, additional: nil)
+ @additional = additional
+ @arg0, = args
@args = args
@reason = nil
end
attr_reader :args
attr_writer :reason
+ attr_accessor :additional
#
# Pushes back erred argument(s) to +argv+.
@@ -1615,9 +2268,10 @@ class OptionParser
argv
end
+ DIR = File.join(__dir__, '')
def self.filter_backtrace(array)
unless $DEBUG
- array.delete_if(&%r"\A#{Regexp.quote(__FILE__)}:"o.method(:=~))
+ array.delete_if {|bt| bt.start_with?(DIR)}
end
array
end
@@ -1643,14 +2297,14 @@ class OptionParser
end
def inspect
- "#<#{self.class.to_s}: #{args.join(' ')}>"
+ "#<#{self.class}: #{args.join(' ')}>"
end
#
# Default stringizing method to emit standard error message.
#
def message
- reason + ': ' + args.join(' ')
+ "#{reason}: #{args.join(' ')}#{additional[@arg0] if additional}"
end
alias to_s message
@@ -1660,42 +2314,42 @@ class OptionParser
# Raises when ambiguously completable string is encountered.
#
class AmbiguousOption < ParseError
- const_set(:Reason, 'ambiguous option'.freeze)
+ Reason = 'ambiguous option' # :nodoc:
end
#
# Raises when there is an argument for a switch which takes no argument.
#
class NeedlessArgument < ParseError
- const_set(:Reason, 'needless argument'.freeze)
+ Reason = 'needless argument' # :nodoc:
end
#
# Raises when a switch with mandatory argument has no argument.
#
class MissingArgument < ParseError
- const_set(:Reason, 'missing argument'.freeze)
+ Reason = 'missing argument' # :nodoc:
end
#
# Raises when switch is undefined.
#
class InvalidOption < ParseError
- const_set(:Reason, 'invalid option'.freeze)
+ Reason = 'invalid option' # :nodoc:
end
#
# Raises when the given argument does not match required format.
#
class InvalidArgument < ParseError
- const_set(:Reason, 'invalid argument'.freeze)
+ Reason = 'invalid argument' # :nodoc:
end
#
# Raises when the given argument word can't be completed uniquely.
#
class AmbiguousArgument < InvalidArgument
- const_set(:Reason, 'ambiguous argument'.freeze)
+ Reason = 'ambiguous argument' # :nodoc:
end
#
@@ -1746,19 +2400,19 @@ class OptionParser
# Parses +self+ destructively in order and returns +self+ containing the
# rest arguments left unparsed.
#
- def order!(&blk) options.order!(self, &blk) end
+ def order!(**keywords, &blk) options.order!(self, **keywords, &blk) end
#
# Parses +self+ destructively in permutation mode and returns +self+
# containing the rest arguments left unparsed.
#
- def permute!() options.permute!(self) end
+ def permute!(**keywords) options.permute!(self, **keywords) end
#
# Parses +self+ destructively and returns +self+ containing the
# rest arguments left unparsed.
#
- def parse!() options.parse!(self) end
+ def parse!(**keywords) options.parse!(self, **keywords) end
#
# Substitution of getopts is possible as follows. Also see
@@ -1771,8 +2425,8 @@ class OptionParser
# rescue OptionParser::ParseError
# end
#
- def getopts(*args)
- options.getopts(self, *args)
+ def getopts(*args, symbolize_names: false, **keywords)
+ options.getopts(self, *args, symbolize_names: symbolize_names, **keywords)
end
#
@@ -1782,7 +2436,8 @@ class OptionParser
super
obj.instance_eval {@optparse = nil}
end
- def initialize(*args)
+
+ def initialize(*args) # :nodoc:
super
@optparse = nil
end
@@ -1793,18 +2448,16 @@ class OptionParser
# and DecimalNumeric. See Acceptable argument classes (in source code).
#
module Acceptables
- const_set(:DecimalInteger, OptionParser::DecimalInteger)
- const_set(:OctalInteger, OptionParser::OctalInteger)
- const_set(:DecimalNumeric, OptionParser::DecimalNumeric)
+ # :stopdoc:
+ DecimalInteger = OptionParser::DecimalInteger
+ OctalInteger = OptionParser::OctalInteger
+ DecimalNumeric = OptionParser::DecimalNumeric
+ # :startdoc:
end
end
# ARGV is arguable by OptionParser
ARGV.extend(OptionParser::Arguable)
-if $0 == __FILE__
- Version = OptionParser::Version
- ARGV.options {|q|
- q.parse!.empty? or puts "what's #{ARGV.join(' ')}?"
- } or abort(ARGV.options.to_s)
-end
+# An alias for OptionParser.
+OptParse = OptionParser # :nodoc:
diff --git a/lib/optparse/ac.rb b/lib/optparse/ac.rb
new file mode 100644
index 0000000000..23fc740d10
--- /dev/null
+++ b/lib/optparse/ac.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: false
+require_relative '../optparse'
+
+#
+# autoconf-like options.
+#
+class OptionParser::AC < OptionParser
+ # :stopdoc:
+ private
+
+ def _check_ac_args(name, block)
+ unless /\A\w[-\w]*\z/ =~ name
+ raise ArgumentError, name
+ end
+ unless block
+ raise ArgumentError, "no block given", ParseError.filter_backtrace(caller)
+ end
+ end
+
+ ARG_CONV = proc {|val| val.nil? ? true : val}
+ private_constant :ARG_CONV
+
+ def _ac_arg_enable(prefix, name, help_string, block)
+ _check_ac_args(name, block)
+
+ sdesc = []
+ ldesc = ["--#{prefix}-#{name}"]
+ desc = [help_string]
+ q = name.downcase
+ ac_block = proc {|val| block.call(ARG_CONV.call(val))}
+ enable = Switch::PlacedArgument.new(nil, ARG_CONV, sdesc, ldesc, nil, desc, ac_block)
+ disable = Switch::NoArgument.new(nil, proc {false}, sdesc, ldesc, nil, desc, ac_block)
+ top.append(enable, [], ["enable-" + q], disable, ['disable-' + q])
+ enable
+ end
+
+ # :startdoc:
+
+ public
+
+ # Define <tt>--enable</tt> / <tt>--disable</tt> style option
+ #
+ # Appears as <tt>--enable-<i>name</i></tt> in help message.
+ def ac_arg_enable(name, help_string, &block)
+ _ac_arg_enable("enable", name, help_string, block)
+ end
+
+ # Define <tt>--enable</tt> / <tt>--disable</tt> style option
+ #
+ # Appears as <tt>--disable-<i>name</i></tt> in help message.
+ def ac_arg_disable(name, help_string, &block)
+ _ac_arg_enable("disable", name, help_string, block)
+ end
+
+ # Define <tt>--with</tt> / <tt>--without</tt> style option
+ #
+ # Appears as <tt>--with-<i>name</i></tt> in help message.
+ def ac_arg_with(name, help_string, &block)
+ _check_ac_args(name, block)
+
+ sdesc = []
+ ldesc = ["--with-#{name}"]
+ desc = [help_string]
+ q = name.downcase
+ with = Switch::PlacedArgument.new(*search(:atype, String), sdesc, ldesc, nil, desc, block)
+ without = Switch::NoArgument.new(nil, proc {}, sdesc, ldesc, nil, desc, block)
+ top.append(with, [], ["with-" + q], without, ['without-' + q])
+ with
+ end
+end
diff --git a/lib/optparse/date.rb b/lib/optparse/date.rb
index d680559f37..7bbf12b77f 100644
--- a/lib/optparse/date.rb
+++ b/lib/optparse/date.rb
@@ -1,4 +1,5 @@
-require 'optparse'
+# frozen_string_literal: false
+require_relative '../optparse'
require 'date'
OptionParser.accept(DateTime) do |s,|
diff --git a/lib/optparse/kwargs.rb b/lib/optparse/kwargs.rb
new file mode 100644
index 0000000000..59a2f61544
--- /dev/null
+++ b/lib/optparse/kwargs.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+require_relative '../optparse'
+
+class OptionParser
+ # :call-seq:
+ # define_by_keywords(options, method, **params)
+ #
+ # :include: ../../doc/optparse/creates_option.rdoc
+ #
+ # Defines options which set in to _options_ for keyword parameters
+ # of _method_.
+ #
+ # Parameters for each keywords are given as elements of _params_.
+ #
+ def define_by_keywords(options, method, **params)
+ method.parameters.each do |type, name|
+ case type
+ when :key, :keyreq
+ op, cl = *(type == :key ? %w"[ ]" : ["", ""])
+ define("--#{name}=#{op}#{name.upcase}#{cl}", *params[name]) do |o|
+ options[name] = o
+ end
+ end
+ end
+ options
+ end
+end
diff --git a/lib/optparse/optparse.gemspec b/lib/optparse/optparse.gemspec
new file mode 100644
index 0000000000..885b0ec380
--- /dev/null
+++ b/lib/optparse/optparse.gemspec
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1, "..").join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Nobu Nakada"]
+ spec.email = ["nobu@ruby-lang.org"]
+
+ spec.summary = %q{OptionParser is a class for command-line option analysis.}
+ spec.description = File.open(File.join(__dir__, "README.md")) do |readme|
+ readme.gets("") # heading
+ readme.gets("").chomp
+ end rescue spec.summary
+ spec.homepage = "https://github.com/ruby/optparse"
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ dir, gemspec = File.split(__FILE__)
+ excludes = %W[#{gemspec} rakelib test/ Gemfile Rakefile .git* .editor*].map {|n| ":^"+n}
+ spec.files = IO.popen(%w[git ls-files -z --] + excludes, chdir: dir, &:read).split("\x0")
+ spec.bindir = "exe"
+ spec.executables = []
+ spec.require_paths = ["lib"]
+end
diff --git a/lib/optparse/shellwords.rb b/lib/optparse/shellwords.rb
index 0422d7c887..4feb1993d9 100644
--- a/lib/optparse/shellwords.rb
+++ b/lib/optparse/shellwords.rb
@@ -1,6 +1,7 @@
+# frozen_string_literal: false
# -*- ruby -*-
require 'shellwords'
-require 'optparse'
+require_relative '../optparse'
OptionParser.accept(Shellwords) {|s,| Shellwords.shellwords(s)}
diff --git a/lib/optparse/time.rb b/lib/optparse/time.rb
index 402cadcf16..0ce651f6f6 100644
--- a/lib/optparse/time.rb
+++ b/lib/optparse/time.rb
@@ -1,4 +1,5 @@
-require 'optparse'
+# frozen_string_literal: false
+require_relative '../optparse'
require 'time'
OptionParser.accept(Time) do |s,|
diff --git a/lib/optparse/uri.rb b/lib/optparse/uri.rb
index 024dc69eac..31d10593b1 100644
--- a/lib/optparse/uri.rb
+++ b/lib/optparse/uri.rb
@@ -1,6 +1,7 @@
+# frozen_string_literal: false
# -*- ruby -*-
-require 'optparse'
+require_relative '../optparse'
require 'uri'
OptionParser.accept(URI) {|s,| URI.parse(s) if s}
diff --git a/lib/optparse/version.rb b/lib/optparse/version.rb
index 76ed564287..b5ed695146 100644
--- a/lib/optparse/version.rb
+++ b/lib/optparse/version.rb
@@ -1,6 +1,12 @@
+# frozen_string_literal: false
# OptionParser internal utility
class << OptionParser
+ #
+ # Shows version string in packages if Version is defined.
+ #
+ # +pkgs+:: package list
+ #
def show_version(*pkgs)
progname = ARGV.options.program_name
result = false
@@ -46,12 +52,14 @@ class << OptionParser
result
end
+ # :stopdoc:
+
def each_const(path, base = ::Object)
path.split(/::|\//).inject(base) do |klass, name|
raise NameError, path unless Module === klass
klass.constants.grep(/#{name}/i) do |c|
klass.const_defined?(c) or next
- c = klass.const_get(c)
+ klass.const_get(c)
end
end
end
@@ -67,4 +75,6 @@ class << OptionParser
end
end
end
+
+ # :startdoc:
end
diff --git a/lib/ostruct.rb b/lib/ostruct.rb
deleted file mode 100644
index 35a14b4920..0000000000
--- a/lib/ostruct.rb
+++ /dev/null
@@ -1,145 +0,0 @@
-#
-# = ostruct.rb: OpenStruct implementation
-#
-# Author:: Yukihiro Matsumoto
-# Documentation:: Gavin Sinclair
-#
-# OpenStruct allows the creation of data objects with arbitrary attributes.
-# See OpenStruct for an example.
-#
-
-#
-# OpenStruct allows you to create data objects and set arbitrary attributes.
-# For example:
-#
-# require 'ostruct'
-#
-# record = OpenStruct.new
-# record.name = "John Smith"
-# record.age = 70
-# record.pension = 300
-#
-# puts record.name # -> "John Smith"
-# puts record.address # -> nil
-#
-# It is like a hash with a different way to access the data. In fact, it is
-# implemented with a hash, and you can initialize it with one.
-#
-# hash = { "country" => "Australia", :population => 20_000_000 }
-# data = OpenStruct.new(hash)
-#
-# p data # -> <OpenStruct country="Australia" population=20000000>
-#
-class OpenStruct
- #
- # Create a new OpenStruct object. The optional +hash+, if given, will
- # generate attributes and values. For example.
- #
- # require 'ostruct'
- # hash = { "country" => "Australia", :population => 20_000_000 }
- # data = OpenStruct.new(hash)
- #
- # p data # -> <OpenStruct country="Australia" population=20000000>
- #
- # By default, the resulting OpenStruct object will have no attributes.
- #
- def initialize(hash=nil)
- @table = {}
- if hash
- for k,v in hash
- @table[k.to_sym] = v
- new_ostruct_member(k)
- end
- end
- end
-
- # Duplicate an OpenStruct object members.
- def initialize_copy(orig)
- super
- @table = @table.dup
- end
-
- def marshal_dump
- @table
- end
- def marshal_load(x)
- @table = x
- @table.each_key{|key| new_ostruct_member(key)}
- end
-
- def new_ostruct_member(name)
- name = name.to_sym
- unless self.respond_to?(name)
- class << self; self; end.class_eval do
- define_method(name) { @table[name] }
- define_method(:"#{name}=") { |x| @table[name] = x }
- end
- end
- end
-
- def method_missing(mid, *args) # :nodoc:
- mname = mid.id2name
- len = args.length
- if mname =~ /=$/
- if len != 1
- raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1)
- end
- if self.frozen?
- raise TypeError, "can't modify frozen #{self.class}", caller(1)
- end
- mname.chop!
- self.new_ostruct_member(mname)
- @table[mname.intern] = args[0]
- elsif len == 0
- @table[mid]
- else
- raise NoMethodError, "undefined method `#{mname}' for #{self}", caller(1)
- end
- end
-
- #
- # Remove the named field from the object.
- #
- def delete_field(name)
- @table.delete name.to_sym
- end
-
- InspectKey = :__inspect_key__ # :nodoc:
-
- #
- # Returns a string containing a detailed summary of the keys and values.
- #
- def inspect
- str = "#<#{self.class}"
-
- ids = (Thread.current[InspectKey] ||= [])
- if ids.include?(object_id)
- return str << ' ...>'
- end
-
- ids << object_id
- begin
- first = true
- for k,v in @table
- str << "," unless first
- first = false
- str << " #{k}=#{v.inspect}"
- end
- return str << '>'
- ensure
- ids.pop
- end
- end
- alias :to_s :inspect
-
- attr_reader :table # :nodoc:
- protected :table
-
- #
- # Compare this object and +other+ for equality.
- #
- def ==(other)
- return false unless(other.kind_of?(OpenStruct))
- return @table == other.table
- end
-end
diff --git a/lib/pathname.rb b/lib/pathname.rb
index 86f0f54800..0e51e1fdf6 100644
--- a/lib/pathname.rb
+++ b/lib/pathname.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
#
# = pathname.rb
#
@@ -8,1071 +9,143 @@
#
# For documentation, see class Pathname.
#
-# <tt>pathname.rb</tt> is distributed with Ruby since 1.8.0.
-#
-
-#
-# == Pathname
-#
-# Pathname represents a pathname which locates a file in a filesystem.
-# The pathname depends on OS: Unix, Windows, etc.
-# Pathname library works with pathnames of local OS.
-# However non-Unix pathnames are supported experimentally.
-#
-# It does not represent the file itself.
-# A Pathname can be relative or absolute. It's not until you try to
-# reference the file that it even matters whether the file exists or not.
-#
-# Pathname is immutable. It has no method for destructive update.
-#
-# The value of this class is to manipulate file path information in a neater
-# way than standard Ruby provides. The examples below demonstrate the
-# difference. *All* functionality from File, FileTest, and some from Dir and
-# FileUtils is included, in an unsurprising way. It is essentially a facade for
-# all of these, and more.
-#
-# == Examples
-#
-# === Example 1: Using Pathname
-#
-# require 'pathname'
-# p = Pathname.new("/usr/bin/ruby")
-# size = p.size # 27662
-# isdir = p.directory? # false
-# dir = p.dirname # Pathname:/usr/bin
-# base = p.basename # Pathname:ruby
-# dir, base = p.split # [Pathname:/usr/bin, Pathname:ruby]
-# data = p.read
-# p.open { |f| _ }
-# p.each_line { |line| _ }
-#
-# === Example 2: Using standard Ruby
-#
-# p = "/usr/bin/ruby"
-# size = File.size(p) # 27662
-# isdir = File.directory?(p) # false
-# dir = File.dirname(p) # "/usr/bin"
-# base = File.basename(p) # "ruby"
-# dir, base = File.split(p) # ["/usr/bin", "ruby"]
-# data = File.read(p)
-# File.open(p) { |f| _ }
-# File.foreach(p) { |line| _ }
-#
-# === Example 3: Special features
-#
-# p1 = Pathname.new("/usr/lib") # Pathname:/usr/lib
-# p2 = p1 + "ruby/1.8" # Pathname:/usr/lib/ruby/1.8
-# p3 = p1.parent # Pathname:/usr
-# p4 = p2.relative_path_from(p3) # Pathname:lib/ruby/1.8
-# pwd = Pathname.pwd # Pathname:/home/gavin
-# pwd.absolute? # true
-# p5 = Pathname.new "." # Pathname:.
-# p5 = p5 + "music/../articles" # Pathname:music/../articles
-# p5.cleanpath # Pathname:articles
-# p5.realpath # Pathname:/home/gavin/articles
-# p5.children # [Pathname:/home/gavin/articles/linux, ...]
-#
-# == Breakdown of functionality
-#
-# === Core methods
-#
-# These methods are effectively manipulating a String, because that's all a path
-# is. Except for #mountpoint?, #children, and #realpath, they don't access the
-# filesystem.
-#
-# - +
-# - #join
-# - #parent
-# - #root?
-# - #absolute?
-# - #relative?
-# - #relative_path_from
-# - #each_filename
-# - #cleanpath
-# - #realpath
-# - #children
-# - #mountpoint?
-#
-# === File status predicate methods
-#
-# These methods are a facade for FileTest:
-# - #blockdev?
-# - #chardev?
-# - #directory?
-# - #executable?
-# - #executable_real?
-# - #exist?
-# - #file?
-# - #grpowned?
-# - #owned?
-# - #pipe?
-# - #readable?
-# - #world_readable?
-# - #readable_real?
-# - #setgid?
-# - #setuid?
-# - #size
-# - #size?
-# - #socket?
-# - #sticky?
-# - #symlink?
-# - #writable?
-# - #world_writable?
-# - #writable_real?
-# - #zero?
-#
-# === File property and manipulation methods
-#
-# These methods are a facade for File:
-# - #atime
-# - #ctime
-# - #mtime
-# - #chmod(mode)
-# - #lchmod(mode)
-# - #chown(owner, group)
-# - #lchown(owner, group)
-# - #fnmatch(pattern, *args)
-# - #fnmatch?(pattern, *args)
-# - #ftype
-# - #make_link(old)
-# - #open(*args, &block)
-# - #readlink
-# - #rename(to)
-# - #stat
-# - #lstat
-# - #make_symlink(old)
-# - #truncate(length)
-# - #utime(atime, mtime)
-# - #basename(*args)
-# - #dirname
-# - #extname
-# - #expand_path(*args)
-# - #split
-#
-# === Directory methods
-#
-# These methods are a facade for Dir:
-# - Pathname.glob(*args)
-# - Pathname.getwd / Pathname.pwd
-# - #rmdir
-# - #entries
-# - #each_entry(&block)
-# - #mkdir(*args)
-# - #opendir(*args)
-#
-# === IO
-#
-# These methods are a facade for IO:
-# - #each_line(*args, &block)
-# - #read(*args)
-# - #readlines(*args)
-# - #sysopen(*args)
-#
-# === Utilities
-#
-# These methods are a mixture of Find, FileUtils, and others:
-# - #find(&block)
-# - #mkpath
-# - #rmtree
-# - #unlink / #delete
-#
-#
-# == Method documentation
-#
-# As the above section shows, most of the methods in Pathname are facades. The
-# documentation for these methods generally just says, for instance, "See
-# FileTest.writable?", as you should be familiar with the original method
-# anyway, and its documentation (e.g. through +ri+) will contain more
-# information. In some cases, a brief description will follow.
-#
class Pathname
- # :stopdoc:
- if RUBY_VERSION < "1.9"
- TO_PATH = :to_str
- else
- # to_path is implemented so Pathname objects are usable with File.open, etc.
- TO_PATH = :to_path
- end
- # :startdoc:
-
- #
- # Create a Pathname object from the given String (or String-like object).
- # If +path+ contains a NUL character (<tt>\0</tt>), an ArgumentError is raised.
- #
- def initialize(path)
- path = path.__send__(TO_PATH) if path.respond_to? TO_PATH
- @path = path.dup
-
- if /\0/ =~ @path
- raise ArgumentError, "pathname contains \\0: #{@path.inspect}"
- end
-
- self.taint if @path.tainted?
- end
-
- def freeze() super; @path.freeze; self end
- def taint() super; @path.taint; self end
- def untaint() super; @path.untaint; self end
-
- #
- # Compare this pathname with +other+. The comparison is string-based.
- # Be aware that two different paths (<tt>foo.txt</tt> and <tt>./foo.txt</tt>)
- # can refer to the same file.
- #
- def ==(other)
- return false unless Pathname === other
- other.to_s == @path
- end
- alias === ==
- alias eql? ==
-
- # Provides for comparing pathnames, case-sensitively.
- def <=>(other)
- return nil unless Pathname === other
- @path.tr('/', "\0") <=> other.to_s.tr('/', "\0")
- end
-
- def hash # :nodoc:
- @path.hash
- end
-
- # Return the path as a String.
- def to_s
- @path.dup
- end
-
- # to_path is implemented so Pathname objects are usable with File.open, etc.
- alias_method TO_PATH, :to_s
-
- def inspect # :nodoc:
- "#<#{self.class}:#{@path}>"
- end
-
- # Return a pathname which is substituted by String#sub.
- def sub(pattern, *rest, &block)
- self.class.new(@path.sub(pattern, *rest, &block))
- end
-
- if File::ALT_SEPARATOR
- SEPARATOR_LIST = "#{Regexp.quote File::ALT_SEPARATOR}#{Regexp.quote File::SEPARATOR}"
- SEPARATOR_PAT = /[#{SEPARATOR_LIST}]/
- else
- SEPARATOR_LIST = "#{Regexp.quote File::SEPARATOR}"
- SEPARATOR_PAT = /#{Regexp.quote File::SEPARATOR}/
- end
-
- # Return a pathname which the extension of the basename is substituted by
- # <i>repl</i>.
- #
- # If self has no extension part, <i>repl</i> is appended.
- def sub_ext(repl)
- ext = File.extname(@path)
- self.class.new(@path.chomp(ext) + repl)
- end
-
- # chop_basename(path) -> [pre-basename, basename] or nil
- def chop_basename(path)
- base = File.basename(path)
- if /\A#{SEPARATOR_PAT}?\z/ =~ base
- return nil
- else
- return path[0, path.rindex(base)], base
- end
- end
- private :chop_basename
-
- # split_names(path) -> prefix, [name, ...]
- def split_names(path)
- names = []
- while r = chop_basename(path)
- path, basename = r
- names.unshift basename
- end
- return path, names
- end
- private :split_names
-
- def prepend_prefix(prefix, relpath)
- if relpath.empty?
- File.dirname(prefix)
- elsif /#{SEPARATOR_PAT}/ =~ prefix
- prefix = File.dirname(prefix)
- prefix = File.join(prefix, "") if File.basename(prefix + 'a') != 'a'
- prefix + relpath
- else
- prefix + relpath
- end
- end
- private :prepend_prefix
-
- # Returns clean pathname of +self+ with consecutive slashes and useless dots
- # removed. The filesystem is not accessed.
- #
- # If +consider_symlink+ is +true+, then a more conservative algorithm is used
- # to avoid breaking symbolic linkages. This may retain more <tt>..</tt>
- # entries than absolutely necessary, but without accessing the filesystem,
- # this can't be avoided. See #realpath.
- #
- def cleanpath(consider_symlink=false)
- if consider_symlink
- cleanpath_conservative
- else
- cleanpath_aggressive
- end
- end
-
- #
- # Clean the path simply by resolving and removing excess "." and ".." entries.
- # Nothing more, nothing less.
- #
- def cleanpath_aggressive
- path = @path
- names = []
- pre = path
- while r = chop_basename(pre)
- pre, base = r
- case base
- when '.'
- when '..'
- names.unshift base
- else
- if names[0] == '..'
- names.shift
- else
- names.unshift base
- end
- end
- end
- if /#{SEPARATOR_PAT}/o =~ File.basename(pre)
- names.shift while names[0] == '..'
- end
- self.class.new(prepend_prefix(pre, File.join(*names)))
- end
- private :cleanpath_aggressive
-
- # has_trailing_separator?(path) -> bool
- def has_trailing_separator?(path)
- if r = chop_basename(path)
- pre, basename = r
- pre.length + basename.length < path.length
- else
- false
- end
- end
- private :has_trailing_separator?
-
- # add_trailing_separator(path) -> path
- def add_trailing_separator(path)
- if File.basename(path + 'a') == 'a'
- path
- else
- File.join(path, "") # xxx: Is File.join is appropriate to add separator?
- end
- end
- private :add_trailing_separator
-
- def del_trailing_separator(path)
- if r = chop_basename(path)
- pre, basename = r
- pre + basename
- elsif /#{SEPARATOR_PAT}+\z/o =~ path
- $` + File.dirname(path)[/#{SEPARATOR_PAT}*\z/o]
- else
- path
- end
- end
- private :del_trailing_separator
-
- def cleanpath_conservative
- path = @path
- names = []
- pre = path
- while r = chop_basename(pre)
- pre, base = r
- names.unshift base if base != '.'
- end
- if /#{SEPARATOR_PAT}/o =~ File.basename(pre)
- names.shift while names[0] == '..'
- end
- if names.empty?
- self.class.new(File.dirname(pre))
- else
- if names.last != '..' && File.basename(path) == '.'
- names << '.'
- end
- result = prepend_prefix(pre, File.join(*names))
- if /\A(?:\.|\.\.)\z/ !~ names.last && has_trailing_separator?(path)
- self.class.new(add_trailing_separator(result))
- else
- self.class.new(result)
- end
- end
- end
- private :cleanpath_conservative
-
- def realpath_rec(prefix, unresolved, h)
- resolved = []
- until unresolved.empty?
- n = unresolved.shift
- if n == '.'
- next
- elsif n == '..'
- resolved.pop
- else
- path = prepend_prefix(prefix, File.join(*(resolved + [n])))
- if h.include? path
- if h[path] == :resolving
- raise Errno::ELOOP.new(path)
- else
- prefix, *resolved = h[path]
- end
- else
- s = File.lstat(path)
- if s.symlink?
- h[path] = :resolving
- link_prefix, link_names = split_names(File.readlink(path))
- if link_prefix == ''
- prefix, *resolved = h[path] = realpath_rec(prefix, resolved + link_names, h)
- else
- prefix, *resolved = h[path] = realpath_rec(link_prefix, link_names, h)
- end
- else
- resolved << n
- h[path] = [prefix, *resolved]
- end
- end
- end
- end
- return prefix, *resolved
- end
- private :realpath_rec
-
- #
- # Returns a real (absolute) pathname of +self+ in the actual filesystem.
- # The real pathname doesn't contain symlinks or useless dots.
- #
- # No arguments should be given; the old behaviour is *obsoleted*.
- #
- def realpath
- path = @path
- prefix, names = split_names(path)
- if prefix == ''
- prefix, names2 = split_names(Dir.pwd)
- names = names2 + names
- end
- prefix, *names = realpath_rec(prefix, names, {})
- self.class.new(prepend_prefix(prefix, File.join(*names)))
- end
-
- # #parent returns the parent directory.
- #
- # This is same as <tt>self + '..'</tt>.
- def parent
- self + '..'
- end
-
- # #mountpoint? returns +true+ if <tt>self</tt> points to a mountpoint.
- def mountpoint?
- begin
- stat1 = self.lstat
- stat2 = self.parent.lstat
- stat1.dev == stat2.dev && stat1.ino == stat2.ino ||
- stat1.dev != stat2.dev
- rescue Errno::ENOENT
- false
- end
- end
-
- #
- # #root? is a predicate for root directories. I.e. it returns +true+ if the
- # pathname consists of consecutive slashes.
- #
- # It doesn't access actual filesystem. So it may return +false+ for some
- # pathnames which points to roots such as <tt>/usr/..</tt>.
- #
- def root?
- !!(chop_basename(@path) == nil && /#{SEPARATOR_PAT}/o =~ @path)
- end
-
- # Predicate method for testing whether a path is absolute.
- # It returns +true+ if the pathname begins with a slash.
- def absolute?
- !relative?
- end
-
- # The opposite of #absolute?
- def relative?
- path = @path
- while r = chop_basename(path)
- path, basename = r
- end
- path == ''
- end
-
- #
- # Iterates over each component of the path.
- #
- # Pathname.new("/usr/bin/ruby").each_filename {|filename| ... }
- # # yields "usr", "bin", and "ruby".
- #
- def each_filename # :yield: filename
- return to_enum(__method__) unless block_given?
- prefix, names = split_names(@path)
- names.each {|filename| yield filename }
- nil
- end
-
- # Iterates over and yields a new Pathname object
- # for each element in the given path in descending order.
- #
- # Pathname.new('/path/to/some/file.rb').descend {|v| p v}
- # #<Pathname:/>
- # #<Pathname:/path>
- # #<Pathname:/path/to>
- # #<Pathname:/path/to/some>
- # #<Pathname:/path/to/some/file.rb>
- #
- # Pathname.new('path/to/some/file.rb').descend {|v| p v}
- # #<Pathname:path>
- # #<Pathname:path/to>
- # #<Pathname:path/to/some>
- # #<Pathname:path/to/some/file.rb>
- #
- # It doesn't access actual filesystem.
- #
- # This method is available since 1.8.5.
- #
- def descend
- vs = []
- ascend {|v| vs << v }
- vs.reverse_each {|v| yield v }
- nil
- end
-
- # Iterates over and yields a new Pathname object
- # for each element in the given path in ascending order.
- #
- # Pathname.new('/path/to/some/file.rb').ascend {|v| p v}
- # #<Pathname:/path/to/some/file.rb>
- # #<Pathname:/path/to/some>
- # #<Pathname:/path/to>
- # #<Pathname:/path>
- # #<Pathname:/>
- #
- # Pathname.new('path/to/some/file.rb').ascend {|v| p v}
- # #<Pathname:path/to/some/file.rb>
- # #<Pathname:path/to/some>
- # #<Pathname:path/to>
- # #<Pathname:path>
- #
- # It doesn't access actual filesystem.
- #
- # This method is available since 1.8.5.
- #
- def ascend
- path = @path
- yield self
- while r = chop_basename(path)
- path, name = r
- break if path.empty?
- yield self.class.new(del_trailing_separator(path))
- end
- end
-
- #
- # Pathname#+ appends a pathname fragment to this one to produce a new Pathname
- # object.
- #
- # p1 = Pathname.new("/usr") # Pathname:/usr
- # p2 = p1 + "bin/ruby" # Pathname:/usr/bin/ruby
- # p3 = p1 + "/etc/passwd" # Pathname:/etc/passwd
- #
- # This method doesn't access the file system; it is pure string manipulation.
- #
- def +(other)
- other = Pathname.new(other) unless Pathname === other
- Pathname.new(plus(@path, other.to_s))
- end
-
- def plus(path1, path2) # -> path
- prefix2 = path2
- index_list2 = []
- basename_list2 = []
- while r2 = chop_basename(prefix2)
- prefix2, basename2 = r2
- index_list2.unshift prefix2.length
- basename_list2.unshift basename2
- end
- return path2 if prefix2 != ''
- prefix1 = path1
- while true
- while !basename_list2.empty? && basename_list2.first == '.'
- index_list2.shift
- basename_list2.shift
- end
- break unless r1 = chop_basename(prefix1)
- prefix1, basename1 = r1
- next if basename1 == '.'
- if basename1 == '..' || basename_list2.empty? || basename_list2.first != '..'
- prefix1 = prefix1 + basename1
- break
- end
- index_list2.shift
- basename_list2.shift
- end
- r1 = chop_basename(prefix1)
- if !r1 && /#{SEPARATOR_PAT}/o =~ File.basename(prefix1)
- while !basename_list2.empty? && basename_list2.first == '..'
- index_list2.shift
- basename_list2.shift
- end
- end
- if !basename_list2.empty?
- suffix2 = path2[index_list2.first..-1]
- r1 ? File.join(prefix1, suffix2) : prefix1 + suffix2
- else
- r1 ? prefix1 : File.dirname(prefix1)
- end
- end
- private :plus
-
- #
- # Pathname#join joins pathnames.
- #
- # <tt>path0.join(path1, ..., pathN)</tt> is the same as
- # <tt>path0 + path1 + ... + pathN</tt>.
- #
- def join(*args)
- args.unshift self
- result = args.pop
- result = Pathname.new(result) unless Pathname === result
- return result if result.absolute?
- args.reverse_each {|arg|
- arg = Pathname.new(arg) unless Pathname === arg
- result = arg + result
- return result if result.absolute?
- }
- result
- end
-
- #
- # Returns the children of the directory (files and subdirectories, not
- # recursive) as an array of Pathname objects. By default, the returned
- # pathnames will have enough information to access the files. If you set
- # +with_directory+ to +false+, then the returned pathnames will contain the
- # filename only.
- #
- # For example:
- # p = Pathname("/usr/lib/ruby/1.8")
- # p.children
- # # -> [ Pathname:/usr/lib/ruby/1.8/English.rb,
- # Pathname:/usr/lib/ruby/1.8/Env.rb,
- # Pathname:/usr/lib/ruby/1.8/abbrev.rb, ... ]
- # p.children(false)
- # # -> [ Pathname:English.rb, Pathname:Env.rb, Pathname:abbrev.rb, ... ]
- #
- # Note that the result never contain the entries <tt>.</tt> and <tt>..</tt> in
- # the directory because they are not children.
- #
- # This method has existed since 1.8.1.
- #
- def children(with_directory=true)
- with_directory = false if @path == '.'
- result = []
- Dir.foreach(@path) {|e|
- next if e == '.' || e == '..'
- if with_directory
- result << self.class.new(File.join(@path, e))
- else
- result << self.class.new(e)
- end
- }
- result
- end
-
- #
- # #relative_path_from returns a relative path from the argument to the
- # receiver. If +self+ is absolute, the argument must be absolute too. If
- # +self+ is relative, the argument must be relative too.
- #
- # #relative_path_from doesn't access the filesystem. It assumes no symlinks.
- #
- # ArgumentError is raised when it cannot find a relative path.
- #
- # This method has existed since 1.8.1.
- #
- def relative_path_from(base_directory)
- dest_directory = self.cleanpath.to_s
- base_directory = base_directory.cleanpath.to_s
- dest_prefix = dest_directory
- dest_names = []
- while r = chop_basename(dest_prefix)
- dest_prefix, basename = r
- dest_names.unshift basename if basename != '.'
- end
- base_prefix = base_directory
- base_names = []
- while r = chop_basename(base_prefix)
- base_prefix, basename = r
- base_names.unshift basename if basename != '.'
- end
- if dest_prefix != base_prefix
- raise ArgumentError, "different prefix: #{dest_prefix.inspect} and #{base_directory.inspect}"
- end
- while !dest_names.empty? &&
- !base_names.empty? &&
- dest_names.first == base_names.first
- dest_names.shift
- base_names.shift
- end
- if base_names.include? '..'
- raise ArgumentError, "base_directory has ..: #{base_directory.inspect}"
- end
- base_names.fill('..')
- relpath_names = base_names + dest_names
- if relpath_names.empty?
- Pathname.new('.')
- else
- Pathname.new(File.join(*relpath_names))
- end
- end
-end
-
-class Pathname # * IO *
- #
- # #each_line iterates over the line in the file. It yields a String object
- # for each line.
- #
- # This method has existed since 1.8.1.
- #
- def each_line(*args, &block) # :yield: line
- IO.foreach(@path, *args, &block)
- end
-
- # Pathname#foreachline is *obsoleted* at 1.8.1. Use #each_line.
- def foreachline(*args, &block)
- warn "Pathname#foreachline is obsoleted. Use Pathname#each_line."
- each_line(*args, &block)
- end
-
- # See <tt>IO.read</tt>. Returns all the bytes from the file, or the first +N+
- # if specified.
- def read(*args) IO.read(@path, *args) end
-
- # See <tt>IO.readlines</tt>. Returns all the lines from the file.
- def readlines(*args) IO.readlines(@path, *args) end
-
- # See <tt>IO.sysopen</tt>.
- def sysopen(*args) IO.sysopen(@path, *args) end
-end
-
-
-class Pathname # * File *
-
- # See <tt>File.atime</tt>. Returns last access time.
- def atime() File.atime(@path) end
-
- # See <tt>File.ctime</tt>. Returns last (directory entry, not file) change time.
- def ctime() File.ctime(@path) end
-
- # See <tt>File.mtime</tt>. Returns last modification time.
- def mtime() File.mtime(@path) end
-
- # See <tt>File.chmod</tt>. Changes permissions.
- def chmod(mode) File.chmod(mode, @path) end
-
- # See <tt>File.lchmod</tt>.
- def lchmod(mode) File.lchmod(mode, @path) end
-
- # See <tt>File.chown</tt>. Change owner and group of file.
- def chown(owner, group) File.chown(owner, group, @path) end
-
- # See <tt>File.lchown</tt>.
- def lchown(owner, group) File.lchown(owner, group, @path) end
-
- # See <tt>File.fnmatch</tt>. Return +true+ if the receiver matches the given
- # pattern.
- def fnmatch(pattern, *args) File.fnmatch(pattern, @path, *args) end
-
- # See <tt>File.fnmatch?</tt> (same as #fnmatch).
- def fnmatch?(pattern, *args) File.fnmatch?(pattern, @path, *args) end
-
- # See <tt>File.ftype</tt>. Returns "type" of file ("file", "directory",
- # etc).
- def ftype() File.ftype(@path) end
-
- # See <tt>File.link</tt>. Creates a hard link.
- def make_link(old) File.link(old, @path) end
-
- # See <tt>File.open</tt>. Opens the file for reading or writing.
- def open(*args, &block) # :yield: file
- File.open(@path, *args, &block)
- end
-
- # See <tt>File.readlink</tt>. Read symbolic link.
- def readlink() self.class.new(File.readlink(@path)) end
-
- # See <tt>File.rename</tt>. Rename the file.
- def rename(to) File.rename(@path, to) end
-
- # See <tt>File.stat</tt>. Returns a <tt>File::Stat</tt> object.
- def stat() File.stat(@path) end
-
- # See <tt>File.lstat</tt>.
- def lstat() File.lstat(@path) end
-
- # See <tt>File.symlink</tt>. Creates a symbolic link.
- def make_symlink(old) File.symlink(old, @path) end
-
- # See <tt>File.truncate</tt>. Truncate the file to +length+ bytes.
- def truncate(length) File.truncate(@path, length) end
-
- # See <tt>File.utime</tt>. Update the access and modification times.
- def utime(atime, mtime) File.utime(atime, mtime, @path) end
-
- # See <tt>File.basename</tt>. Returns the last component of the path.
- def basename(*args) self.class.new(File.basename(@path, *args)) end
-
- # See <tt>File.dirname</tt>. Returns all but the last component of the path.
- def dirname() self.class.new(File.dirname(@path)) end
-
- # See <tt>File.extname</tt>. Returns the file's extension.
- def extname() File.extname(@path) end
-
- # See <tt>File.expand_path</tt>.
- def expand_path(*args) self.class.new(File.expand_path(@path, *args)) end
-
- # See <tt>File.split</tt>. Returns the #dirname and the #basename in an
- # Array.
- def split() File.split(@path).map {|f| self.class.new(f) } end
-
- # Pathname#link is confusing and *obsoleted* because the receiver/argument
- # order is inverted to corresponding system call.
- def link(old)
- warn 'Pathname#link is obsoleted. Use Pathname#make_link.'
- File.link(old, @path)
- end
-
- # Pathname#symlink is confusing and *obsoleted* because the receiver/argument
- # order is inverted to corresponding system call.
- def symlink(old)
- warn 'Pathname#symlink is obsoleted. Use Pathname#make_symlink.'
- File.symlink(old, @path)
- end
-end
-
-
-class Pathname # * FileTest *
-
- # See <tt>FileTest.blockdev?</tt>.
- def blockdev?() FileTest.blockdev?(@path) end
-
- # See <tt>FileTest.chardev?</tt>.
- def chardev?() FileTest.chardev?(@path) end
-
- # See <tt>FileTest.executable?</tt>.
- def executable?() FileTest.executable?(@path) end
-
- # See <tt>FileTest.executable_real?</tt>.
- def executable_real?() FileTest.executable_real?(@path) end
-
- # See <tt>FileTest.exist?</tt>.
- def exist?() FileTest.exist?(@path) end
-
- # See <tt>FileTest.grpowned?</tt>.
- def grpowned?() FileTest.grpowned?(@path) end
-
- # See <tt>FileTest.directory?</tt>.
- def directory?() FileTest.directory?(@path) end
-
- # See <tt>FileTest.file?</tt>.
- def file?() FileTest.file?(@path) end
-
- # See <tt>FileTest.pipe?</tt>.
- def pipe?() FileTest.pipe?(@path) end
-
- # See <tt>FileTest.socket?</tt>.
- def socket?() FileTest.socket?(@path) end
-
- # See <tt>FileTest.owned?</tt>.
- def owned?() FileTest.owned?(@path) end
-
- # See <tt>FileTest.readable?</tt>.
- def readable?() FileTest.readable?(@path) end
-
- # See <tt>FileTest.world_readable?</tt>.
- def world_readable?() FileTest.world_readable?(@path) end
-
- # See <tt>FileTest.readable_real?</tt>.
- def readable_real?() FileTest.readable_real?(@path) end
-
- # See <tt>FileTest.setuid?</tt>.
- def setuid?() FileTest.setuid?(@path) end
-
- # See <tt>FileTest.setgid?</tt>.
- def setgid?() FileTest.setgid?(@path) end
-
- # See <tt>FileTest.size</tt>.
- def size() FileTest.size(@path) end
-
- # See <tt>FileTest.size?</tt>.
- def size?() FileTest.size?(@path) end
-
- # See <tt>FileTest.sticky?</tt>.
- def sticky?() FileTest.sticky?(@path) end
-
- # See <tt>FileTest.symlink?</tt>.
- def symlink?() FileTest.symlink?(@path) end
-
- # See <tt>FileTest.writable?</tt>.
- def writable?() FileTest.writable?(@path) end
-
- # See <tt>FileTest.world_writable?</tt>.
- def world_writable?() FileTest.world_writable?(@path) end
-
- # See <tt>FileTest.writable_real?</tt>.
- def writable_real?() FileTest.writable_real?(@path) end
-
- # See <tt>FileTest.zero?</tt>.
- def zero?() FileTest.zero?(@path) end
-end
-
-
-class Pathname # * Dir *
- # See <tt>Dir.glob</tt>. Returns or yields Pathname objects.
- def Pathname.glob(*args) # :yield: p
- if block_given?
- Dir.glob(*args) {|f| yield self.new(f) }
- else
- Dir.glob(*args).map {|f| self.new(f) }
- end
- end
-
- # See <tt>Dir.getwd</tt>. Returns the current working directory as a Pathname.
- def Pathname.getwd() self.new(Dir.getwd) end
- class << self; alias pwd getwd end
-
- # Pathname#chdir is *obsoleted* at 1.8.1.
- def chdir(&block)
- warn "Pathname#chdir is obsoleted. Use Dir.chdir."
- Dir.chdir(@path, &block)
- end
-
- # Pathname#chroot is *obsoleted* at 1.8.1.
- def chroot
- warn "Pathname#chroot is obsoleted. Use Dir.chroot."
- Dir.chroot(@path)
- end
-
- # Return the entries (files and subdirectories) in the directory, each as a
- # Pathname object.
- def entries() Dir.entries(@path).map {|f| self.class.new(f) } end
-
- # Iterates over the entries (files and subdirectories) in the directory. It
- # yields a Pathname object for each entry.
- #
- # This method has existed since 1.8.1.
- def each_entry(&block) # :yield: p
- Dir.foreach(@path) {|f| yield self.class.new(f) }
- end
-
- # Pathname#dir_foreach is *obsoleted* at 1.8.1.
- def dir_foreach(*args, &block)
- warn "Pathname#dir_foreach is obsoleted. Use Pathname#each_entry."
- each_entry(*args, &block)
- end
-
- # See <tt>Dir.mkdir</tt>. Create the referenced directory.
- def mkdir(*args) Dir.mkdir(@path, *args) end
-
- # See <tt>Dir.rmdir</tt>. Remove the referenced directory.
- def rmdir() Dir.rmdir(@path) end
-
- # See <tt>Dir.open</tt>.
- def opendir(&block) # :yield: dir
- Dir.open(@path, &block)
- end
-end
-
-
-class Pathname # * Find *
- #
- # Pathname#find is an iterator to traverse a directory tree in a depth first
- # manner. It yields a Pathname for each file under "this" directory.
- #
- # Since it is implemented by <tt>find.rb</tt>, <tt>Find.prune</tt> can be used
- # to control the traverse.
- #
- # If +self+ is <tt>.</tt>, yielded pathnames begin with a filename in the
- # current directory, not <tt>./</tt>.
- #
- def find(&block) # :yield: p
+ # :markup: markdown
+ #
+ # call-seq:
+ # Pathname.find(ignore_error: true) -> nil
+ #
+ # With a block given, performs a depth-first traversal of the path in `self`;
+ # calls the block with each found path:
+ #
+ # ```ruby
+ # paths = []
+ # Pathname('lib').find {|path| paths << path }
+ # paths.size # => 909
+ # paths.take(3)
+ # # =>
+ # # [#<Pathname:lib>,
+ # # #<Pathname:lib/English.gemspec>,
+ # # #<Pathname:lib/English.rb>]
+ # ```
+ #
+ # When `self` contains `'.'`, the found paths omit the leading `'./'`:
+ #
+ # ```ruby
+ # paths = []
+ # Dir.chdir('lib') do
+ # Pathname('.').find {|path| paths << path }
+ # end
+ # paths.take(3)
+ # # # =>
+ # # [#<Pathname:.>,
+ # # #<Pathname:English.gemspec>,
+ # # #<Pathname:English.rb>]
+ # ```
+ #
+ # This method calls method Find.find;
+ # therefore method Find.prune may be used in the block:
+ #
+ # ```ruby
+ # files = []
+ # Pathname('.').find do |path|
+ # Find.prune if File.basename(path) == 'test'
+ # next unless File.file?(path) && File.extname(path) == '.rb'
+ # files << path
+ # end
+ # files.size # => 6690
+ # files.take(3)
+ # # # =>
+ # # [#<Pathname:KNOWNBUGS.rb>,
+ # # #<Pathname:array.rb>,
+ # # #<Pathname:ast.rb>]
+ # ```
+ #
+ # Raises an exception if the path in `self` cannot be read.
+ #
+ # When keyword argument `ignore_error` is given as `true` (the default),
+ # certain exceptions during traversal are ignored (i.e., silently rescued):
+ # Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG, Errno::EINVAL;
+ # when given as `false`, no exceptions are rescued.
+ #
+ # Note that these exceptions may be ignored only in `Pathname#find` traversal code;
+ # an exception raised before traversal begins,
+ # or raised while in the block is not ignored.
+ # Each of the calls below raises an Errno::ENOENT exception that is not ignored:
+ #
+ # ```ruby
+ # Pathname('nosuch').find { }
+ # Pathname('lib').find {|entry| raise Errno::ENOENT }
+ # ```
+ #
+ # With no block given, returns a new Enumerator.
+ def find(ignore_error: true) # :yield: pathname
+ return to_enum(__method__, ignore_error: ignore_error) unless block_given?
require 'find'
if @path == '.'
- Find.find(@path) {|f| yield self.class.new(f.sub(%r{\A\./}, '')) }
+ Find.find(@path, ignore_error: ignore_error) {|f| yield self.class.new(f.delete_prefix('./')) }
else
- Find.find(@path) {|f| yield self.class.new(f) }
+ Find.find(@path, ignore_error: ignore_error) {|f| yield self.class.new(f) }
end
end
end
class Pathname # * FileUtils *
- # See <tt>FileUtils.mkpath</tt>. Creates a full path, including any
- # intermediate directories that don't yet exist.
- def mkpath
- require 'fileutils'
- FileUtils.mkpath(@path)
- nil
- end
-
- # See <tt>FileUtils.rm_r</tt>. Deletes a directory and all beneath it.
- def rmtree
+ # Recursively deletes a directory, including all directories beneath it.
+ #
+ # Note that you need to require 'pathname' to use this method.
+ #
+ # See FileUtils.rm_rf
+ def rmtree(noop: nil, verbose: nil, secure: nil)
# The name "rmtree" is borrowed from File::Path of Perl.
# File::Path provides "mkpath" and "rmtree".
require 'fileutils'
- FileUtils.rm_r(@path)
- nil
+ FileUtils.rm_rf(@path, noop: noop, verbose: verbose, secure: secure)
+ self
end
end
-
-class Pathname # * mixed *
- # Removes a file or directory, using <tt>File.unlink</tt> or
- # <tt>Dir.unlink</tt> as necessary.
- def unlink()
- begin
- Dir.unlink @path
- rescue Errno::ENOTDIR
- File.unlink @path
- end
- end
- alias delete unlink
-
- # This method is *obsoleted* at 1.8.1. Use #each_line or #each_entry.
- def foreach(*args, &block)
- warn "Pathname#foreach is obsoleted. Use each_line or each_entry."
- if FileTest.directory? @path
- # For polymorphism between Dir.foreach and IO.foreach,
- # Pathname#foreach doesn't yield Pathname object.
- Dir.foreach(@path, *args, &block)
+class Pathname # * tmpdir *
+ # call-seq:
+ # Pathname.mktmpdir -> new_pathname
+ # Pathname.mktmpdir {|pathname| ... } -> object
+ #
+ # Creates:
+ #
+ # - A temporary directory via Dir.mktmpdir.
+ # - A \Pathname object that contains the path to that directory.
+ #
+ # With no block given, returns the created pathname;
+ # the caller should delete the created directory when it is no longer needed
+ # (FileUtils.rm_r is a convenient method for the deletion):
+ #
+ # pathname = Pathname.mktmpdir
+ # dirpath = pathname.to_s
+ # Dir.exist?(dirpath) # => true
+ # # Do something with the directory.
+ # require 'fileutils'
+ # FileUtils.rm_r(dirpath)
+ #
+ # With a block given, calls the block with the created pathname;
+ # on block exit, automatically deletes the created directory and all its contents;
+ # returns the block's exit value:
+ #
+ # pathname = Pathname.mktmpdir do |p|
+ # # Do something with the directory.
+ # p
+ # end
+ # Dir.exist?(pathname.to_s) # => false
+ def self.mktmpdir
+ require 'tmpdir' unless defined?(Dir.mktmpdir)
+ if block_given?
+ Dir.mktmpdir do |dir|
+ dir = self.new(dir)
+ yield dir
+ end
else
- IO.foreach(@path, *args, &block)
+ self.new(Dir.mktmpdir)
end
end
end
-
-class Pathname
- undef =~
-end
-
-module Kernel
- # create a pathname object.
- #
- # This method is available since 1.8.5.
- def Pathname(path) # :doc:
- Pathname.new(path)
- end
- private :Pathname
-end
diff --git a/lib/pp.gemspec b/lib/pp.gemspec
new file mode 100644
index 0000000000..15a3b4dc6c
--- /dev/null
+++ b/lib/pp.gemspec
@@ -0,0 +1,35 @@
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1).join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Tanaka Akira"]
+ spec.email = ["akr@fsij.org"]
+
+ spec.summary = %q{Provides a PrettyPrinter for Ruby objects}
+ spec.description = %q{Provides a PrettyPrinter for Ruby objects}
+ spec.homepage = "https://github.com/ruby/pp"
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ spec.files = %w[
+ BSDL
+ COPYING
+ lib/pp.rb
+ pp.gemspec
+ ]
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+
+ spec.add_dependency "prettyprint"
+end
diff --git a/lib/pp.rb b/lib/pp.rb
index 41f51b0046..5fd29a373a 100644
--- a/lib/pp.rb
+++ b/lib/pp.rb
@@ -1,11 +1,17 @@
-# == Pretty-printer for Ruby objects.
+# frozen_string_literal: true
+
+require 'prettyprint'
+
+##
+# A pretty-printer for Ruby objects.
#
-# = Which seems better?
+##
+# == What PP Does
#
-# non-pretty-printed output by #p is:
+# Standard output by #p returns this:
# #<PP:0x81fedf0 @genspace=#<Proc:0x81feda0>, @group_queue=#<PrettyPrint::GroupQueue:0x81fed3c @queue=[[#<PrettyPrint::Group:0x81fed78 @breakables=[], @depth=0, @break=false>], []]>, @buffer=[], @newline="\n", @group_stack=[#<PrettyPrint::Group:0x81fed78 @breakables=[], @depth=0, @break=false>], @buffer_width=0, @indent=0, @maxwidth=79, @output_width=2, @output=#<IO:0x8114ee4>>
#
-# pretty-printed output by #pp is:
+# Pretty-printed output returns this:
# #<PP:0x81fedf0
# @buffer=[],
# @buffer_width=0,
@@ -23,57 +29,72 @@
# @output=#<IO:0x8114ee4>,
# @output_width=2>
#
-# I like the latter. If you do too, this library is for you.
+##
+# == Usage
+#
+# pp(obj) #=> obj
+# pp obj #=> obj
+# pp(obj1, obj2, ...) #=> [obj1, obj2, ...]
+# pp() #=> nil
+#
+# Output <tt>obj(s)</tt> to <tt>$></tt> in pretty printed format.
+#
+# It returns <tt>obj(s)</tt>.
#
-# = Usage
+##
+# == Output Customization
#
-# pp(obj)
+# To define a customized pretty printing function for your classes,
+# redefine method <code>#pretty_print(pp)</code> in the class.
+# Note that <code>require 'pp'</code> is needed before redefining <code>#pretty_print(pp)</code>.
#
-# output +obj+ to +$>+ in pretty printed format.
+# <code>#pretty_print</code> takes the +pp+ argument, which is an instance of the PP class.
+# The method uses #text, #breakable, #nest, #group and #pp to print the
+# object.
#
-# It returns +nil+.
+##
+# == Pretty-Print JSON
#
-# = Output Customization
-# To define your customized pretty printing function for your classes,
-# redefine a method #pretty_print(+pp+) in the class.
-# It takes an argument +pp+ which is an instance of the class PP.
-# The method should use PP#text, PP#breakable, PP#nest, PP#group and
-# PP#pp to print the object.
+# To pretty-print JSON refer to JSON#pretty_generate.
#
-# = Author
-# Tanaka Akira <akr@m17n.org>
+##
+# == Author
+# Tanaka Akira <akr@fsij.org>
-require 'prettyprint'
+class PP < PrettyPrint
-module Kernel
- # returns a pretty printed object as a string.
- def pretty_inspect
- PP.pp(self, '')
- end
+ # The version string
+ VERSION = "0.6.3"
- private
- # prints arguments in pretty form.
+ # Returns the usable width for +out+.
+ # As the width of +out+:
+ # 1. If +out+ is assigned to a tty device, its width is used.
+ # 2. Otherwise, or it could not get the value, the +COLUMN+
+ # environment variable is assumed to be set to the width.
+ # 3. If +COLUMN+ is not set to a non-zero number, 80 is assumed.
#
- # pp returns nil.
- def pp(*objs) # :doc:
- objs.each {|obj|
- PP.pp(obj)
- }
- nil
+ # And finally, returns the above width value - 1.
+ # * This -1 is for Windows command prompt, which moves the cursor to
+ # the next line if it reaches the last column.
+ def PP.width_for(out)
+ begin
+ require 'io/console'
+ _, width = out.winsize
+ rescue LoadError, NoMethodError, SystemCallError
+ end
+ (width || ENV['COLUMNS']&.to_i&.nonzero? || 80) - 1
end
- module_function :pp
-end
-class PP < PrettyPrint
# Outputs +obj+ to +out+ in pretty printed format of
# +width+ columns in width.
#
- # If +out+ is omitted, +$>+ is assumed.
- # If +width+ is omitted, 79 is assumed.
+ # If +out+ is omitted, <code>$></code> is assumed.
+ # If +width+ is omitted, the width of +out+ is assumed (see
+ # width_for).
#
# PP.pp returns +out+.
- def PP.pp(obj, out=$>, width=79)
- q = PP.new(out, width)
+ def PP.pp(obj, out=$>, width=width_for(out))
+ q = new(out, width)
q.guard_inspect_key {q.pp obj}
q.flush
#$pp = q
@@ -93,67 +114,105 @@ class PP < PrettyPrint
# :stopdoc:
def PP.mcall(obj, mod, meth, *args, &block)
- mod.instance_method(meth).bind(obj).call(*args, &block)
+ mod.instance_method(meth).bind_call(obj, *args, &block)
end
# :startdoc:
- @sharing_detection = false
- class << self
- # Returns the sharing detection flag as a boolean value.
- # It is false by default.
- attr_accessor :sharing_detection
- end
-
- module PPMethods
- def guard_inspect_key
- if Thread.current[:__recursive_key__] == nil
- Thread.current[:__recursive_key__] = {}
+ if defined? ::Ractor
+ class << self
+ # Returns the sharing detection flag as a boolean value.
+ # It is false (nil) by default.
+ def sharing_detection
+ Ractor.current[:pp_sharing_detection]
end
-
- if Thread.current[:__recursive_key__][:inspect] == nil
- Thread.current[:__recursive_key__][:inspect] = {}
+ # Sets the sharing detection flag to b.
+ def sharing_detection=(b)
+ Ractor.current[:pp_sharing_detection] = b
end
+ end
+ else
+ @sharing_detection = false
+ class << self
+ # Returns the sharing detection flag as a boolean value.
+ # It is false by default.
+ attr_accessor :sharing_detection
+ end
+ end
- save = Thread.current[:__recursive_key__][:inspect]
+ # Module that defines helper methods for pretty_print.
+ module PPMethods
+ # Yields to a block
+ # and preserves the previous set of objects being printed.
+ def guard_inspect_key
+ recursive_state = Thread.current[:__recursive_key__] ||= {}.compare_by_identity
+ save = recursive_state[:inspect] ||= {}.compare_by_identity
begin
- Thread.current[:__recursive_key__][:inspect] = {}
+ recursive_state[:inspect] = {}.compare_by_identity
yield
ensure
- Thread.current[:__recursive_key__][:inspect] = save
+ recursive_state[:inspect] = save
end
end
+ # Check whether the object_id +id+ is in the current buffer of objects
+ # to be pretty printed. Used to break cycles in chains of objects to be
+ # pretty printed.
def check_inspect_key(id)
- Thread.current[:__recursive_key__] &&
- Thread.current[:__recursive_key__][:inspect] &&
- Thread.current[:__recursive_key__][:inspect].include?(id)
+ recursive_state = Thread.current[:__recursive_key__] or return false
+ recursive_state[:inspect]&.include?(id)
end
+
+ # Adds the object_id +id+ to the set of objects being pretty printed, so
+ # as to not repeat objects.
def push_inspect_key(id)
Thread.current[:__recursive_key__][:inspect][id] = true
end
+
+ # Removes an object from the set of objects being pretty printed.
def pop_inspect_key(id)
Thread.current[:__recursive_key__][:inspect].delete id
end
+ private def guard_inspect(object) # :nodoc:
+ recursive_state = Thread.current[:__recursive_key__]
+
+ if recursive_state&.key?(:inspect)
+ begin
+ push_inspect_key(object)
+ yield
+ ensure
+ pop_inspect_key(object) unless PP.sharing_detection
+ end
+ else
+ guard_inspect_key do
+ push_inspect_key(object)
+ yield
+ end
+ end
+ end
+
# Adds +obj+ to the pretty printing buffer
# using Object#pretty_print or Object#pretty_print_cycle.
#
# Object#pretty_print_cycle is used when +obj+ is already
# printed, a.k.a the object reference chain has a cycle.
def pp(obj)
- id = obj.object_id
+ # If obj is a Delegator then use the object being delegated to for cycle
+ # detection
+ obj = obj.__getobj__ if defined?(::Delegator) and ::Delegator === obj
- if check_inspect_key(id)
+ if check_inspect_key(obj)
group {obj.pretty_print_cycle self}
return
end
- begin
- push_inspect_key(id)
- group {obj.pretty_print self}
- ensure
- pop_inspect_key(id) unless PP.sharing_detection
+ guard_inspect(obj) do
+ group do
+ obj.pretty_print self
+ rescue NoMethodError
+ text Kernel.instance_method(:inspect).bind_call(obj)
+ end
end
end
@@ -164,24 +223,12 @@ class PP < PrettyPrint
group(1, '#<' + obj.class.name, '>', &block)
end
- if 0x100000000.class == Bignum
- # 32bit
- PointerMask = 0xffffffff
- else
- # 64bit
- PointerMask = 0xffffffffffffffff
- end
-
- case Object.new.inspect
- when /\A\#<Object:0x([0-9a-f]+)>\z/
- PointerFormat = "%0#{$1.length}x"
- else
- PointerFormat = "%x"
- end
-
+ # A convenience method, like object_group, but also reformats the Object's
+ # object_id.
def object_address_group(obj, &block)
- id = PointerFormat % (obj.object_id * 2 & PointerMask)
- group(1, "\#<#{obj.class}:0x#{id}", '>', &block)
+ str = Kernel.instance_method(:to_s).bind_call(obj)
+ str.chomp!('>')
+ group(1, str, '>', &block)
end
# A convenience method which is same as follows:
@@ -220,16 +267,22 @@ class PP < PrettyPrint
def seplist(list, sep=nil, iter_method=:each) # :yield: element
sep ||= lambda { comma_breakable }
first = true
+ kwsplat = EMPTY_KWHASH
list.__send__(iter_method) {|*v|
if first
first = false
else
sep.call
end
- yield(*v)
+ kwsplat ? yield(*v, **kwsplat) : yield(*v)
}
end
+ EMPTY_KWHASH = if RUBY_VERSION >= "3.0" # :nodoc:
+ {}.freeze
+ end
+ private_constant :EMPTY_KWHASH
+ # A present standard failsafe for pretty printing any given Object
def pp_object(obj)
object_address_group(obj) {
seplist(obj.pretty_print_instance_variables, lambda { text ',' }) {|v|
@@ -245,33 +298,57 @@ class PP < PrettyPrint
}
end
+ # A pretty print for a Hash
def pp_hash(obj)
group(1, '{', '}') {
seplist(obj, nil, :each_pair) {|k, v|
group {
- pp k
- text '=>'
- group(1) {
- breakable ''
- pp v
- }
+ pp_hash_pair k, v
}
}
}
end
+
+ if RUBY_VERSION >= '3.4.'
+ # A pretty print for a pair of Hash
+ def pp_hash_pair(k, v)
+ if Symbol === k
+ if k.inspect.match?(%r[\A:["$@!]|[%&*+\-\/<=>@\]^`|~]\z])
+ k = k.to_s.inspect
+ end
+ text "#{k}:"
+ else
+ pp k
+ text ' '
+ text '=>'
+ end
+ group(1) {
+ breakable
+ pp v
+ }
+ end
+ else
+ def pp_hash_pair(k, v)
+ pp k
+ text '=>'
+ group(1) {
+ breakable ''
+ pp v
+ }
+ end
+ end
end
include PPMethods
- class SingleLine < PrettyPrint::SingleLine
+ class SingleLine < PrettyPrint::SingleLine # :nodoc:
include PPMethods
end
- module ObjectMixin
+ module ObjectMixin # :nodoc:
# 1. specific pretty_print
# 2. specific inspect
- # 3. specific to_s if instance variable is empty
- # 4. generic pretty_print
+ # 3. generic pretty_print
# A default pretty printing method for general objects.
# It calls #pretty_print_instance_variables to list instance variables.
@@ -283,10 +360,15 @@ class PP < PrettyPrint
# This module provides predefined #pretty_print methods for some of
# the most commonly used built-in classes for convenience.
def pretty_print(q)
- if /\(Kernel\)#/ !~ Object.instance_method(:method).bind(self).call(:inspect).inspect
+ umethod_method = Object.instance_method(:method)
+ begin
+ inspect_method = umethod_method.bind_call(self, :inspect)
+ rescue NameError
+ end
+ if inspect_method && inspect_method.owner != Kernel
+ q.text self.inspect
+ elsif !inspect_method && self.respond_to?(:inspect)
q.text self.inspect
- elsif /\(Kernel\)#/ !~ Object.instance_method(:method).bind(self).call(:to_s).inspect && instance_variables.empty?
- q.text self.to_s
else
q.pp_object(self)
end
@@ -306,7 +388,8 @@ class PP < PrettyPrint
# This method should return an array of names of instance variables as symbols or strings as:
# +[:@a, :@b]+.
def pretty_print_instance_variables
- instance_variables.sort
+ ivars = respond_to?(:instance_variables_to_inspect, true) ? instance_variables_to_inspect || instance_variables : instance_variables
+ ivars.sort
end
# Is #inspect implementation using #pretty_print.
@@ -317,16 +400,16 @@ class PP < PrettyPrint
# However, doing this requires that every class that #inspect is called on
# implement #pretty_print, or a RuntimeError will be raised.
def pretty_print_inspect
- if /\(PP::ObjectMixin\)#/ =~ Object.instance_method(:method).bind(self).call(:pretty_print).inspect
+ if Object.instance_method(:method).bind_call(self, :pretty_print).owner == PP::ObjectMixin
raise "pretty_print is not overridden for #{self.class}"
end
- PP.singleline_pp(self, '')
+ PP.singleline_pp(self, ''.dup)
end
end
end
-class Array
- def pretty_print(q)
+class Array # :nodoc:
+ def pretty_print(q) # :nodoc:
q.group(1, '[', ']') {
q.seplist(self) {|v|
q.pp v
@@ -334,23 +417,45 @@ class Array
}
end
- def pretty_print_cycle(q)
+ def pretty_print_cycle(q) # :nodoc:
q.text(empty? ? '[]' : '[...]')
end
end
-class Hash
- def pretty_print(q)
+class Hash # :nodoc:
+ def pretty_print(q) # :nodoc:
q.pp_hash self
end
- def pretty_print_cycle(q)
+ def pretty_print_cycle(q) # :nodoc:
q.text(empty? ? '{}' : '{...}')
end
end
-class << ENV
- def pretty_print(q)
+if defined?(Set)
+ if set_pp = Set.instance_method(:initialize).source_location
+ set_pp = !set_pp.first.end_with?("/set.rb") # not defined in set.rb
+ else
+ set_pp = true # defined in C
+ end
+end
+class Set # :nodoc:
+ def pretty_print(pp) # :nodoc:
+ pp.group(1, "#{self.class.name}[", ']') {
+ pp.seplist(self) { |o|
+ pp.pp o
+ }
+ }
+ end
+
+ def pretty_print_cycle(pp) # :nodoc:
+ name = self.class.name
+ pp.text(empty? ? "#{name}[]" : "#{name}[...]")
+ end
+end if set_pp
+
+class << ENV # :nodoc:
+ def pretty_print(q) # :nodoc:
h = {}
ENV.keys.sort.each {|k|
h[k] = ENV[k]
@@ -359,9 +464,9 @@ class << ENV
end
end
-class Struct
- def pretty_print(q)
- q.group(1, '#<struct ' + PP.mcall(self, Kernel, :class).name, '>') {
+class Struct # :nodoc:
+ def pretty_print(q) # :nodoc:
+ q.group(1, sprintf("#<struct %s", PP.mcall(self, Kernel, :class).name), '>') {
q.seplist(PP.mcall(self, Struct, :members), lambda { q.text "," }) {|member|
q.breakable
q.text member.to_s
@@ -374,25 +479,83 @@ class Struct
}
end
- def pretty_print_cycle(q)
+ def pretty_print_cycle(q) # :nodoc:
q.text sprintf("#<struct %s:...>", PP.mcall(self, Kernel, :class).name)
end
end
-class Range
- def pretty_print(q)
- q.pp self.begin
+verbose, $VERBOSE = $VERBOSE, nil
+begin
+ has_data_define = defined?(Data.define)
+ensure
+ $VERBOSE = verbose
+end
+
+class Data # :nodoc:
+ def pretty_print(q) # :nodoc:
+ class_name = PP.mcall(self, Kernel, :class).name
+ class_name = " #{class_name}" if class_name
+ q.group(1, "#<data#{class_name}", '>') {
+
+ members = PP.mcall(self, Kernel, :class).members
+ values = []
+ members.select! do |member|
+ begin
+ values << __send__(member)
+ true
+ rescue NoMethodError
+ false
+ end
+ end
+
+ q.seplist(members.zip(values), lambda { q.text "," }) {|(member, value)|
+ q.breakable
+ q.text member.to_s
+ q.text '='
+ q.group(1) {
+ q.breakable ''
+ q.pp value
+ }
+ }
+ }
+ end
+
+ def pretty_print_cycle(q) # :nodoc:
+ q.text sprintf("#<data %s:...>", PP.mcall(self, Kernel, :class).name)
+ end
+end if has_data_define
+
+class Range # :nodoc:
+ def pretty_print(q) # :nodoc:
+ begin_nil = self.begin == nil
+ end_nil = self.end == nil
+ q.pp self.begin if !begin_nil || end_nil
q.breakable ''
q.text(self.exclude_end? ? '...' : '..')
q.breakable ''
- q.pp self.end
+ q.pp self.end if !end_nil || begin_nil
end
end
-class File
- class Stat
- def pretty_print(q)
- require 'etc.so'
+class String # :nodoc:
+ def pretty_print(q) # :nodoc:
+ lines = self.lines
+ if lines.size > 1
+ q.group(0, '', '') do
+ q.seplist(lines, lambda { q.text ' +'; q.breakable }) do |v|
+ q.pp v
+ end
+ end
+ else
+ q.text inspect
+ end
+ end
+end
+
+class File < IO # :nodoc:
+ class Stat # :nodoc:
+ def pretty_print(q) # :nodoc:
+ require 'etc'
q.object_group(self) {
q.breakable
q.text sprintf("dev=0x%x", self.dev); q.comma_breakable
@@ -442,8 +605,10 @@ class File
q.comma_breakable
q.group {
q.text sprintf("rdev=0x%x", self.rdev)
- q.breakable
- q.text sprintf('(%d, %d)', self.rdev_major, self.rdev_minor)
+ if self.rdev_major && self.rdev_minor
+ q.breakable
+ q.text sprintf('(%d, %d)', self.rdev_major, self.rdev_minor)
+ end
}
q.comma_breakable
q.text "size="; q.pp self.size; q.comma_breakable
@@ -471,8 +636,8 @@ class File
end
end
-class MatchData
- def pretty_print(q)
+class MatchData # :nodoc:
+ def pretty_print(q) # :nodoc:
nc = []
self.regexp.named_captures.each {|name, indexes|
indexes.each {|i| nc[i] = name }
@@ -496,7 +661,43 @@ class MatchData
end
end
-class Object
+if defined?(RubyVM::AbstractSyntaxTree)
+ class RubyVM::AbstractSyntaxTree::Node # :nodoc:
+ def pretty_print_children(q, names = [])
+ children.zip(names) do |c, n|
+ if n
+ q.breakable
+ q.text "#{n}:"
+ end
+ q.group(2) do
+ q.breakable
+ q.pp c
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(1, "(#{type}@#{first_lineno}:#{first_column}-#{last_lineno}:#{last_column}", ")") {
+ case type
+ when :SCOPE
+ pretty_print_children(q, %w"tbl args body")
+ when :ARGS
+ pretty_print_children(q, %w[pre_num pre_init opt first_post post_num post_init rest kw kwrest block])
+ when :DEFN
+ pretty_print_children(q, %w[mid body])
+ when :ARYPTN
+ pretty_print_children(q, %w[const pre rest post])
+ when :HSHPTN
+ pretty_print_children(q, %w[const kw kwrest])
+ else
+ pretty_print_children(q)
+ end
+ }
+ end
+ end
+end
+
+class Object < BasicObject # :nodoc:
include PP::ObjectMixin
end
@@ -516,185 +717,22 @@ end
}
}
-# :enddoc:
-if __FILE__ == $0
- require 'test/unit'
-
- class PPTest < Test::Unit::TestCase
- def test_list0123_12
- assert_equal("[0, 1, 2, 3]\n", PP.pp([0,1,2,3], '', 12))
- end
-
- def test_list0123_11
- assert_equal("[0,\n 1,\n 2,\n 3]\n", PP.pp([0,1,2,3], '', 11))
- end
-
- OverriddenStruct = Struct.new("OverriddenStruct", :members, :class)
- def test_struct_override_members # [ruby-core:7865]
- a = OverriddenStruct.new(1,2)
- assert_equal("#<struct Struct::OverriddenStruct members=1, class=2>\n", PP.pp(a, ''))
- end
-
- def test_redefined_method
- o = ""
- def o.method
- end
- assert_equal(%(""\n), PP.pp(o, ""))
- end
- end
-
- class HasInspect
- def initialize(a)
- @a = a
- end
-
- def inspect
- return "<inspect:#{@a.inspect}>"
- end
- end
-
- class HasPrettyPrint
- def initialize(a)
- @a = a
- end
-
- def pretty_print(q)
- q.text "<pretty_print:"
- q.pp @a
- q.text ">"
- end
- end
-
- class HasBoth
- def initialize(a)
- @a = a
- end
-
- def inspect
- return "<inspect:#{@a.inspect}>"
- end
-
- def pretty_print(q)
- q.text "<pretty_print:"
- q.pp @a
- q.text ">"
- end
- end
-
- class PrettyPrintInspect < HasPrettyPrint
- alias inspect pretty_print_inspect
- end
-
- class PrettyPrintInspectWithoutPrettyPrint
- alias inspect pretty_print_inspect
- end
-
- class PPInspectTest < Test::Unit::TestCase
- def test_hasinspect
- a = HasInspect.new(1)
- assert_equal("<inspect:1>\n", PP.pp(a, ''))
- end
-
- def test_hasprettyprint
- a = HasPrettyPrint.new(1)
- assert_equal("<pretty_print:1>\n", PP.pp(a, ''))
- end
-
- def test_hasboth
- a = HasBoth.new(1)
- assert_equal("<pretty_print:1>\n", PP.pp(a, ''))
- end
-
- def test_pretty_print_inspect
- a = PrettyPrintInspect.new(1)
- assert_equal("<pretty_print:1>", a.inspect)
- a = PrettyPrintInspectWithoutPrettyPrint.new
- assert_raise(RuntimeError) { a.inspect }
- end
-
- def test_proc
- a = proc {1}
- assert_equal("#{a.inspect}\n", PP.pp(a, ''))
- end
-
- def test_to_s_with_iv
- a = Object.new
- def a.to_s() "aaa" end
- a.instance_eval { @a = nil }
- result = PP.pp(a, '')
- assert_equal("#{a.inspect}\n", result)
- assert_match(/\A#<Object.*>\n\z/m, result)
- a = 1.0
- a.instance_eval { @a = nil }
- result = PP.pp(a, '')
- assert_equal("#{a.inspect}\n", result)
- end
-
- def test_to_s_without_iv
- a = Object.new
- def a.to_s() "aaa" end
- result = PP.pp(a, '')
- assert_equal("#{a.inspect}\n", result)
- assert_equal("aaa\n", result)
- end
- end
-
- class PPCycleTest < Test::Unit::TestCase
- def test_array
- a = []
- a << a
- assert_equal("[[...]]\n", PP.pp(a, ''))
- assert_equal("#{a.inspect}\n", PP.pp(a, ''))
- end
-
- def test_hash
- a = {}
- a[0] = a
- assert_equal("{0=>{...}}\n", PP.pp(a, ''))
- assert_equal("#{a.inspect}\n", PP.pp(a, ''))
- end
-
- S = Struct.new("S", :a, :b)
- def test_struct
- a = S.new(1,2)
- a.b = a
- assert_equal("#<struct Struct::S a=1, b=#<struct Struct::S:...>>\n", PP.pp(a, ''))
- assert_equal("#{a.inspect}\n", PP.pp(a, ''))
- end
-
- def test_object
- a = Object.new
- a.instance_eval {@a = a}
- assert_equal(a.inspect + "\n", PP.pp(a, ''))
- end
-
- def test_anonymous
- a = Class.new.new
- assert_equal(a.inspect + "\n", PP.pp(a, ''))
- end
-
- def test_withinspect
- a = []
- a << HasInspect.new(a)
- assert_equal("[<inspect:[...]>]\n", PP.pp(a, ''))
- assert_equal("#{a.inspect}\n", PP.pp(a, ''))
- end
-
- def test_share_nil
- begin
- PP.sharing_detection = true
- a = [nil, nil]
- assert_equal("[nil, nil]\n", PP.pp(a, ''))
- ensure
- PP.sharing_detection = false
- end
- end
+module Kernel
+ # Returns a pretty printed object as a string.
+ #
+ # See the PP module for more information.
+ def pretty_inspect
+ PP.pp(self, ''.dup)
end
- class PPSingleLineTest < Test::Unit::TestCase
- def test_hash
- assert_equal("{1=>1}", PP.singleline_pp({ 1 => 1}, '')) # [ruby-core:02699]
- assert_equal("[1#{', 1'*99}]", PP.singleline_pp([1]*100, ''))
- end
+ # prints arguments in pretty form.
+ #
+ # +#pp+ returns argument(s).
+ def pp(*objs)
+ objs.each {|obj|
+ PP.pp(obj)
+ }
+ objs.size <= 1 ? objs.first : objs
end
+ module_function :pp
end
diff --git a/lib/prettyprint.gemspec b/lib/prettyprint.gemspec
new file mode 100644
index 0000000000..a18adb174b
--- /dev/null
+++ b/lib/prettyprint.gemspec
@@ -0,0 +1,29 @@
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1).join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Tanaka Akira"]
+ spec.email = ["akr@fsij.org"]
+
+ spec.summary = %q{Implements a pretty printing algorithm for readable structure.}
+ spec.description = %q{Implements a pretty printing algorithm for readable structure.}
+ spec.homepage = "https://github.com/ruby/prettyprint"
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
+ end
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+end
diff --git a/lib/prettyprint.rb b/lib/prettyprint.rb
index 48f2ebf1e4..44ca5e816f 100644
--- a/lib/prettyprint.rb
+++ b/lib/prettyprint.rb
@@ -1,5 +1,5 @@
-# $Id$
-
+# frozen_string_literal: true
+#
# This class implements a pretty printing algorithm. It finds line breaks and
# nice indentations for grouped structure.
#
@@ -19,18 +19,23 @@
# * Box based formatting?
# * Other (better) model/algorithm?
#
+# Report any bugs at http://bugs.ruby-lang.org
+#
# == References
# Christian Lindig, Strictly Pretty, March 2000,
-# http://www.st.cs.uni-sb.de/~lindig/papers/#pretty
+# https://lindig.github.io/papers/strictly-pretty-2000.pdf
#
# Philip Wadler, A prettier printer, March 1998,
-# http://homepages.inf.ed.ac.uk/wadler/topics/language-design.html#prettier
+# https://homepages.inf.ed.ac.uk/wadler/topics/language-design.html#prettier
#
# == Author
-# Tanaka Akira <akr@m17n.org>
+# Tanaka Akira <akr@fsij.org>
#
class PrettyPrint
+ # The version string
+ VERSION = "0.2.0"
+
# This is a convenience method which is same as follows:
#
# begin
@@ -40,7 +45,7 @@ class PrettyPrint
# output
# end
#
- def PrettyPrint.format(output='', maxwidth=79, newline="\n", genspace=lambda {|n| ' ' * n})
+ def PrettyPrint.format(output=''.dup, maxwidth=79, newline="\n", genspace=lambda {|n| ' ' * n})
q = PrettyPrint.new(output, maxwidth, newline, &genspace)
yield q
q.flush
@@ -54,7 +59,7 @@ class PrettyPrint
# The invocation of +breakable+ in the block doesn't break a line and is
# treated as just an invocation of +text+.
#
- def PrettyPrint.singleline_format(output='', maxwidth=nil, newline=nil, genspace=nil)
+ def PrettyPrint.singleline_format(output=''.dup, maxwidth=nil, newline=nil, genspace=nil)
q = SingleLine.new(output)
yield q
output
@@ -77,7 +82,7 @@ class PrettyPrint
# The block is used to generate spaces. {|width| ' ' * width} is used if it
# is not given.
#
- def initialize(output='', maxwidth=79, newline="\n", &genspace)
+ def initialize(output=''.dup, maxwidth=79, newline="\n", &genspace)
@output = output
@maxwidth = maxwidth
@newline = newline
@@ -92,35 +97,69 @@ class PrettyPrint
@group_queue = GroupQueue.new(root_group)
@indent = 0
end
- attr_reader :output, :maxwidth, :newline, :genspace
- attr_reader :indent, :group_queue
- def current_group
- @group_stack.last
- end
+ # The output object.
+ #
+ # This defaults to '', and should accept the << method
+ attr_reader :output
- # first? is a predicate to test the call is a first call to first? with
- # current group.
+ # The maximum width of a line, before it is separated in to a newline
#
- # It is useful to format comma separated values as:
+ # This defaults to 79, and should be an Integer
+ attr_reader :maxwidth
+
+ # The value that is appended to +output+ to add a new line.
#
- # q.group(1, '[', ']') {
- # xxx.each {|yyy|
- # unless q.first?
- # q.text ','
- # q.breakable
- # end
- # ... pretty printing yyy ...
- # }
- # }
+ # This defaults to "\n", and should be String
+ attr_reader :newline
+
+ # A lambda or Proc, that takes one argument, of an Integer, and returns
+ # the corresponding number of spaces.
#
- # first? is obsoleted in 1.8.2.
+ # By default this is:
+ # lambda {|n| ' ' * n}
+ attr_reader :genspace
+
+ # The number of spaces to be indented
+ attr_reader :indent
+
+ # The PrettyPrint::GroupQueue of groups in stack to be pretty printed
+ attr_reader :group_queue
+
+ # Returns the group most recently added to the stack.
#
- def first?
- warn "PrettyPrint#first? is obsoleted at 1.8.2."
- current_group.first?
+ # Contrived example:
+ # out = ""
+ # => ""
+ # q = PrettyPrint.new(out)
+ # => #<PrettyPrint:0x82f85c0 @output="", @maxwidth=79, @newline="\n", @genspace=#<Proc:0x82f8368@/home/vbatts/.rvm/rubies/ruby-head/lib/ruby/2.0.0/prettyprint.rb:82 (lambda)>, @output_width=0, @buffer_width=0, @buffer=[], @group_stack=[#<PrettyPrint::Group:0x82f8138 @depth=0, @breakables=[], @break=false>], @group_queue=#<PrettyPrint::GroupQueue:0x82fb7c0 @queue=[[#<PrettyPrint::Group:0x82f8138 @depth=0, @breakables=[], @break=false>]]>, @indent=0>
+ # q.group {
+ # q.text q.current_group.inspect
+ # q.text q.newline
+ # q.group(q.current_group.depth + 1) {
+ # q.text q.current_group.inspect
+ # q.text q.newline
+ # q.group(q.current_group.depth + 1) {
+ # q.text q.current_group.inspect
+ # q.text q.newline
+ # q.group(q.current_group.depth + 1) {
+ # q.text q.current_group.inspect
+ # q.text q.newline
+ # }
+ # }
+ # }
+ # }
+ # => 284
+ # puts out
+ # #<PrettyPrint::Group:0x8354758 @depth=1, @breakables=[], @break=false>
+ # #<PrettyPrint::Group:0x8354550 @depth=2, @breakables=[], @break=false>
+ # #<PrettyPrint::Group:0x83541cc @depth=3, @breakables=[], @break=false>
+ # #<PrettyPrint::Group:0x8347e54 @depth=4, @breakables=[], @break=false>
+ def current_group
+ @group_stack.last
end
+ # Breaks the buffer into lines that are shorter than #maxwidth
def break_outmost_groups
while @maxwidth < @output_width + @buffer_width
return unless group = @group_queue.deq
@@ -157,11 +196,27 @@ class PrettyPrint
end
end
+ # This is similar to #breakable except
+ # the decision to break or not is determined individually.
+ #
+ # Two #fill_breakable under a group may cause 4 results:
+ # (break,break), (break,non-break), (non-break,break), (non-break,non-break).
+ # This is different to #breakable because two #breakable under a group
+ # may cause 2 results:
+ # (break,break), (non-break,non-break).
+ #
+ # The text +sep+ is inserted if a line is not broken at this point.
+ #
+ # If +sep+ is not specified, " " is used.
+ #
+ # If +width+ is not specified, +sep.length+ is used. You will have to
+ # specify this when +sep+ is a multibyte character, for example.
+ #
def fill_breakable(sep=' ', width=sep.length)
group { breakable sep, width }
end
- # This tells "you can break a line here if necessary", and a +width+\-column
+ # This says "you can break a line here if necessary", and a +width+\-column
# text +sep+ is inserted if a line is not broken at the point.
#
# If +sep+ is not specified, " " is used.
@@ -204,6 +259,7 @@ class PrettyPrint
text close_obj, close_width
end
+ # Takes a block and queues a new group that is indented 1 level further.
def group_sub
group = Group.new(@group_stack.last.depth + 1)
@group_stack.push group
@@ -240,25 +296,55 @@ class PrettyPrint
@buffer_width = 0
end
- class Text
+ # The Text class is the means by which to collect strings from objects.
+ #
+ # This class is intended for internal use of the PrettyPrint buffers.
+ class Text # :nodoc:
+
+ # Creates a new text object.
+ #
+ # This constructor takes no arguments.
+ #
+ # The workflow is to append a PrettyPrint::Text object to the buffer, and
+ # being able to call the buffer.last() to reference it.
+ #
+ # As there are objects, use PrettyPrint::Text#add to include the objects
+ # and the width to utilized by the String version of this object.
def initialize
@objs = []
@width = 0
end
+
+ # The total width of the objects included in this Text object.
attr_reader :width
+ # Render the String text of the objects that have been added to this Text object.
+ #
+ # Output the text to +out+, and increment the width to +output_width+
def output(out, output_width)
@objs.each {|obj| out << obj}
output_width + @width
end
+ # Include +obj+ in the objects to be pretty printed, and increment
+ # this Text object's total width by +width+
def add(obj, width)
@objs << obj
@width += width
end
end
- class Breakable
+ # The Breakable class is used for breaking up object information
+ #
+ # This class is intended for internal use of the PrettyPrint buffers.
+ class Breakable # :nodoc:
+
+ # Create a new Breakable object.
+ #
+ # Arguments:
+ # * +sep+ String of the separator
+ # * +width+ Integer width of the +sep+
+ # * +q+ parent PrettyPrint object, to base from
def initialize(sep, width, q)
@obj = sep
@width = width
@@ -267,8 +353,24 @@ class PrettyPrint
@group = q.current_group
@group.breakables.push self
end
- attr_reader :obj, :width, :indent
+ # Holds the separator String
+ #
+ # The +sep+ argument from ::new
+ attr_reader :obj
+
+ # The width of +obj+ / +sep+
+ attr_reader :width
+
+ # The number of spaces to indent.
+ #
+ # This is inferred from +q+ within PrettyPrint, passed in ::new
+ attr_reader :indent
+
+ # Render the String text of the objects that have been added to this
+ # Breakable object.
+ #
+ # Output the text to +out+, and increment the width to +output_width+
def output(out, output_width)
@group.breakables.shift
if @group.break?
@@ -283,22 +385,45 @@ class PrettyPrint
end
end
- class Group
+ # The Group class is used for making indentation easier.
+ #
+ # While this class does neither the breaking into newlines nor indentation,
+ # it is used in a stack (as well as a queue) within PrettyPrint, to group
+ # objects.
+ #
+ # For information on using groups, see PrettyPrint#group
+ #
+ # This class is intended for internal use of the PrettyPrint buffers.
+ class Group # :nodoc:
+ # Create a Group object
+ #
+ # Arguments:
+ # * +depth+ - this group's relation to previous groups
def initialize(depth)
@depth = depth
@breakables = []
@break = false
end
- attr_reader :depth, :breakables
+ # This group's relation to previous groups
+ attr_reader :depth
+
+ # Array to hold the Breakable objects for this Group
+ attr_reader :breakables
+
+ # Makes a break for this Group, and returns true
def break
@break = true
end
+ # Boolean of whether this Group has made a break
def break?
@break
end
+ # Boolean of whether this Group has been queried for being first
+ #
+ # This is used as a predicate, and ought to be called first.
def first?
if defined? @first
false
@@ -309,18 +434,33 @@ class PrettyPrint
end
end
- class GroupQueue
+ # The GroupQueue class is used for managing the queue of Group to be pretty
+ # printed.
+ #
+ # This queue groups the Group objects, based on their depth.
+ #
+ # This class is intended for internal use of the PrettyPrint buffers.
+ class GroupQueue # :nodoc:
+ # Create a GroupQueue object
+ #
+ # Arguments:
+ # * +groups+ - one or more PrettyPrint::Group objects
def initialize(*groups)
@queue = []
groups.each {|g| enq g}
end
+ # Enqueue +group+
+ #
+ # This does not strictly append the group to the end of the queue,
+ # but instead adds it in line, base on the +group.depth+
def enq(group)
depth = group.depth
@queue << [] until depth < @queue.length
@queue[depth] << group
end
+ # Returns the outer group of the queue
def deq
@queue.each {|gs|
(gs.length-1).downto(0) {|i|
@@ -336,29 +476,76 @@ class PrettyPrint
return nil
end
+ # Remote +group+ from this queue
def delete(group)
@queue[group.depth].delete(group)
end
end
+ # PrettyPrint::SingleLine is used by PrettyPrint.singleline_format
+ #
+ # It is passed to be similar to a PrettyPrint object itself, by responding to:
+ # * #text
+ # * #breakable
+ # * #fill_breakable
+ # * #nest
+ # * #group
+ # * #group_sub
+ # * #flush
+ # * #first?
+ #
+ # but instead, the output has no line breaks
+ #
class SingleLine
+ # Create a PrettyPrint::SingleLine object
+ #
+ # Arguments:
+ # * +output+ - String (or similar) to store rendered text. Needs to respond to '<<'
+ # * +maxwidth+ - Argument position expected to be here for compatibility.
+ # This argument is a noop.
+ # * +newline+ - Argument position expected to be here for compatibility.
+ # This argument is a noop.
def initialize(output, maxwidth=nil, newline=nil)
@output = output
@first = [true]
end
+ # Add +obj+ to the text to be output.
+ #
+ # +width+ argument is here for compatibility. It is a noop argument.
def text(obj, width=nil)
@output << obj
end
+ # Appends +sep+ to the text to be output. By default +sep+ is ' '
+ #
+ # +width+ argument is here for compatibility. It is a noop argument.
def breakable(sep=' ', width=nil)
@output << sep
end
- def nest(indent)
+ # Appends +sep+ to the text to be output. By default +sep+ is ' '
+ #
+ # +width+ argument is here for compatibility. It is a noop argument.
+ def fill_breakable(sep=' ', width=nil)
+ @output << sep
+ end
+
+ # Takes +indent+ arg, but does nothing with it.
+ #
+ # Yields to a block.
+ def nest(indent) # :nodoc:
yield
end
+ # Opens a block for grouping objects to be pretty printed.
+ #
+ # Arguments:
+ # * +indent+ - noop argument. Present for compatibility.
+ # * +open_obj+ - text appended before the &blok. Default is ''
+ # * +close_obj+ - text appended after the &blok. Default is ''
+ # * +open_width+ - noop argument. Present for compatibility.
+ # * +close_width+ - noop argument. Present for compatibility.
def group(indent=nil, open_obj='', close_obj='', open_width=nil, close_width=nil)
@first.push true
@output << open_obj
@@ -367,530 +554,24 @@ class PrettyPrint
@first.pop
end
- def flush
- end
-
- def first?
- result = @first[-1]
- @first[-1] = false
- result
- end
- end
-end
-
-if __FILE__ == $0
- require 'test/unit'
-
- class WadlerExample < Test::Unit::TestCase # :nodoc:
- def setup
- @tree = Tree.new("aaaa", Tree.new("bbbbb", Tree.new("ccc"),
- Tree.new("dd")),
- Tree.new("eee"),
- Tree.new("ffff", Tree.new("gg"),
- Tree.new("hhh"),
- Tree.new("ii")))
- end
-
- def hello(width)
- PrettyPrint.format('', width) {|hello|
- hello.group {
- hello.group {
- hello.group {
- hello.group {
- hello.text 'hello'
- hello.breakable; hello.text 'a'
- }
- hello.breakable; hello.text 'b'
- }
- hello.breakable; hello.text 'c'
- }
- hello.breakable; hello.text 'd'
- }
- }
- end
-
- def test_hello_00_06
- expected = <<'End'.chomp
-hello
-a
-b
-c
-d
-End
- assert_equal(expected, hello(0))
- assert_equal(expected, hello(6))
- end
-
- def test_hello_07_08
- expected = <<'End'.chomp
-hello a
-b
-c
-d
-End
- assert_equal(expected, hello(7))
- assert_equal(expected, hello(8))
- end
-
- def test_hello_09_10
- expected = <<'End'.chomp
-hello a b
-c
-d
-End
- out = hello(9); assert_equal(expected, out)
- out = hello(10); assert_equal(expected, out)
- end
-
- def test_hello_11_12
- expected = <<'End'.chomp
-hello a b c
-d
-End
- assert_equal(expected, hello(11))
- assert_equal(expected, hello(12))
- end
-
- def test_hello_13
- expected = <<'End'.chomp
-hello a b c d
-End
- assert_equal(expected, hello(13))
- end
-
- def tree(width)
- PrettyPrint.format('', width) {|q| @tree.show(q)}
- end
-
- def test_tree_00_19
- expected = <<'End'.chomp
-aaaa[bbbbb[ccc,
- dd],
- eee,
- ffff[gg,
- hhh,
- ii]]
-End
- assert_equal(expected, tree(0))
- assert_equal(expected, tree(19))
- end
-
- def test_tree_20_22
- expected = <<'End'.chomp
-aaaa[bbbbb[ccc, dd],
- eee,
- ffff[gg,
- hhh,
- ii]]
-End
- assert_equal(expected, tree(20))
- assert_equal(expected, tree(22))
- end
-
- def test_tree_23_43
- expected = <<'End'.chomp
-aaaa[bbbbb[ccc, dd],
- eee,
- ffff[gg, hhh, ii]]
-End
- assert_equal(expected, tree(23))
- assert_equal(expected, tree(43))
- end
-
- def test_tree_44
- assert_equal(<<'End'.chomp, tree(44))
-aaaa[bbbbb[ccc, dd], eee, ffff[gg, hhh, ii]]
-End
- end
-
- def tree_alt(width)
- PrettyPrint.format('', width) {|q| @tree.altshow(q)}
- end
-
- def test_tree_alt_00_18
- expected = <<'End'.chomp
-aaaa[
- bbbbb[
- ccc,
- dd
- ],
- eee,
- ffff[
- gg,
- hhh,
- ii
- ]
-]
-End
- assert_equal(expected, tree_alt(0))
- assert_equal(expected, tree_alt(18))
- end
-
- def test_tree_alt_19_20
- expected = <<'End'.chomp
-aaaa[
- bbbbb[ ccc, dd ],
- eee,
- ffff[
- gg,
- hhh,
- ii
- ]
-]
-End
- assert_equal(expected, tree_alt(19))
- assert_equal(expected, tree_alt(20))
- end
-
- def test_tree_alt_20_49
- expected = <<'End'.chomp
-aaaa[
- bbbbb[ ccc, dd ],
- eee,
- ffff[ gg, hhh, ii ]
-]
-End
- assert_equal(expected, tree_alt(21))
- assert_equal(expected, tree_alt(49))
- end
-
- def test_tree_alt_50
- expected = <<'End'.chomp
-aaaa[ bbbbb[ ccc, dd ], eee, ffff[ gg, hhh, ii ] ]
-End
- assert_equal(expected, tree_alt(50))
- end
-
- class Tree # :nodoc:
- def initialize(string, *children)
- @string = string
- @children = children
- end
-
- def show(q)
- q.group {
- q.text @string
- q.nest(@string.length) {
- unless @children.empty?
- q.text '['
- q.nest(1) {
- first = true
- @children.each {|t|
- if first
- first = false
- else
- q.text ','
- q.breakable
- end
- t.show(q)
- }
- }
- q.text ']'
- end
- }
- }
- end
-
- def altshow(q)
- q.group {
- q.text @string
- unless @children.empty?
- q.text '['
- q.nest(2) {
- q.breakable
- first = true
- @children.each {|t|
- if first
- first = false
- else
- q.text ','
- q.breakable
- end
- t.altshow(q)
- }
- }
- q.breakable
- q.text ']'
- end
- }
- end
-
- end
- end
-
- class StrictPrettyExample < Test::Unit::TestCase # :nodoc:
- def prog(width)
- PrettyPrint.format('', width) {|q|
- q.group {
- q.group {q.nest(2) {
- q.text "if"; q.breakable;
- q.group {
- q.nest(2) {
- q.group {q.text "a"; q.breakable; q.text "=="}
- q.breakable; q.text "b"}}}}
- q.breakable
- q.group {q.nest(2) {
- q.text "then"; q.breakable;
- q.group {
- q.nest(2) {
- q.group {q.text "a"; q.breakable; q.text "<<"}
- q.breakable; q.text "2"}}}}
- q.breakable
- q.group {q.nest(2) {
- q.text "else"; q.breakable;
- q.group {
- q.nest(2) {
- q.group {q.text "a"; q.breakable; q.text "+"}
- q.breakable; q.text "b"}}}}}
- }
- end
-
- def test_00_04
- expected = <<'End'.chomp
-if
- a
- ==
- b
-then
- a
- <<
- 2
-else
- a
- +
- b
-End
- assert_equal(expected, prog(0))
- assert_equal(expected, prog(4))
- end
-
- def test_05
- expected = <<'End'.chomp
-if
- a
- ==
- b
-then
- a
- <<
- 2
-else
- a +
- b
-End
- assert_equal(expected, prog(5))
- end
-
- def test_06
- expected = <<'End'.chomp
-if
- a ==
- b
-then
- a <<
- 2
-else
- a +
- b
-End
- assert_equal(expected, prog(6))
- end
-
- def test_07
- expected = <<'End'.chomp
-if
- a ==
- b
-then
- a <<
- 2
-else
- a + b
-End
- assert_equal(expected, prog(7))
- end
-
- def test_08
- expected = <<'End'.chomp
-if
- a == b
-then
- a << 2
-else
- a + b
-End
- assert_equal(expected, prog(8))
- end
-
- def test_09
- expected = <<'End'.chomp
-if a == b
-then
- a << 2
-else
- a + b
-End
- assert_equal(expected, prog(9))
- end
-
- def test_10
- expected = <<'End'.chomp
-if a == b
-then
- a << 2
-else a + b
-End
- assert_equal(expected, prog(10))
- end
-
- def test_11_31
- expected = <<'End'.chomp
-if a == b
-then a << 2
-else a + b
-End
- assert_equal(expected, prog(11))
- assert_equal(expected, prog(15))
- assert_equal(expected, prog(31))
- end
-
- def test_32
- expected = <<'End'.chomp
-if a == b then a << 2 else a + b
-End
- assert_equal(expected, prog(32))
- end
-
- end
-
- class TailGroup < Test::Unit::TestCase # :nodoc:
- def test_1
- out = PrettyPrint.format('', 10) {|q|
- q.group {
- q.group {
- q.text "abc"
- q.breakable
- q.text "def"
- }
- q.group {
- q.text "ghi"
- q.breakable
- q.text "jkl"
- }
- }
- }
- assert_equal("abc defghi\njkl", out)
- end
- end
-
- class NonString < Test::Unit::TestCase # :nodoc:
- def format(width)
- PrettyPrint.format([], width, 'newline', lambda {|n| "#{n} spaces"}) {|q|
- q.text(3, 3)
- q.breakable(1, 1)
- q.text(3, 3)
- }
- end
-
- def test_6
- assert_equal([3, "newline", "0 spaces", 3], format(6))
- end
-
- def test_7
- assert_equal([3, 1, 3], format(7))
- end
-
- end
-
- class Fill < Test::Unit::TestCase # :nodoc:
- def format(width)
- PrettyPrint.format('', width) {|q|
- q.group {
- q.text 'abc'
- q.fill_breakable
- q.text 'def'
- q.fill_breakable
- q.text 'ghi'
- q.fill_breakable
- q.text 'jkl'
- q.fill_breakable
- q.text 'mno'
- q.fill_breakable
- q.text 'pqr'
- q.fill_breakable
- q.text 'stu'
- }
- }
- end
-
- def test_00_06
- expected = <<'End'.chomp
-abc
-def
-ghi
-jkl
-mno
-pqr
-stu
-End
- assert_equal(expected, format(0))
- assert_equal(expected, format(6))
- end
-
- def test_07_10
- expected = <<'End'.chomp
-abc def
-ghi jkl
-mno pqr
-stu
-End
- assert_equal(expected, format(7))
- assert_equal(expected, format(10))
- end
-
- def test_11_14
- expected = <<'End'.chomp
-abc def ghi
-jkl mno pqr
-stu
-End
- assert_equal(expected, format(11))
- assert_equal(expected, format(14))
- end
-
- def test_15_18
- expected = <<'End'.chomp
-abc def ghi jkl
-mno pqr stu
-End
- assert_equal(expected, format(15))
- assert_equal(expected, format(18))
+ # Yields to a block for compatibility.
+ def group_sub # :nodoc:
+ yield
end
- def test_19_22
- expected = <<'End'.chomp
-abc def ghi jkl mno
-pqr stu
-End
- assert_equal(expected, format(19))
- assert_equal(expected, format(22))
+ # Method present for compatibility, but is a noop
+ def break_outmost_groups # :nodoc:
end
- def test_23_26
- expected = <<'End'.chomp
-abc def ghi jkl mno pqr
-stu
-End
- assert_equal(expected, format(23))
- assert_equal(expected, format(26))
+ # Method present for compatibility, but is a noop
+ def flush # :nodoc:
end
- def test_27
- expected = <<'End'.chomp
-abc def ghi jkl mno pqr stu
-End
- assert_equal(expected, format(27))
+ # This is used as a predicate, and ought to be called first.
+ def first?
+ result = @first[-1]
+ @first[-1] = false
+ result
end
-
end
end
diff --git a/lib/prime.rb b/lib/prime.rb
deleted file mode 100644
index 650d279bc4..0000000000
--- a/lib/prime.rb
+++ /dev/null
@@ -1,461 +0,0 @@
-#
-# = prime.rb
-#
-# Prime numbers and factorization library.
-#
-# Copyright::
-# Copyright (c) 1998-2008 Keiju ISHITSUKA(SHL Japan Inc.)
-# Copyright (c) 2008 Yuki Sonoda (Yugui) <yugui@yugui.jp>
-#
-# Documentation::
-# Yuki Sonoda
-#
-
-require "singleton"
-require "forwardable"
-
-class Integer
- # Re-composes a prime factorization and returns the product.
- #
- # See Prime#int_from_prime_division for more details.
- def Integer.from_prime_division(pd)
- Prime.int_from_prime_division(pd)
- end
-
- # Returns the factorization of +self+.
- #
- # See Prime#prime_division for more details.
- def prime_division(generator = Prime::Generator23.new)
- Prime.prime_division(self, generator)
- end
-
- # Returns true if +self+ is a prime number, false for a composite.
- def prime?
- Prime.prime?(self)
- end
-
- # Iterates the given block over all prime numbers.
- #
- # See +Prime+#each for more details.
- def Integer.each_prime(ubound, &block) # :yields: prime
- Prime.each(ubound, &block)
- end
-end
-
-#
-# The set of all prime numbers.
-#
-# == Example
-# Prime.each(100) do |prime|
-# p prime #=> 2, 3, 5, 7, 11, ...., 97
-# end
-#
-# == Retrieving the instance
-# +Prime+.new is obsolete. Now +Prime+ has the default instance and you can
-# access it as +Prime+.instance.
-#
-# For convenience, each instance method of +Prime+.instance can be accessed
-# as a class method of +Prime+.
-#
-# e.g.
-# Prime.instance.prime?(2) #=> true
-# Prime.prime?(2) #=> true
-#
-# == Generators
-# A "generator" provides an implementation of enumerating pseudo-prime
-# numbers and it remembers the position of enumeration and upper bound.
-# Futhermore, it is a external iterator of prime enumeration which is
-# compatible to an Enumerator.
-#
-# +Prime+::+PseudoPrimeGenerator+ is the base class for generators.
-# There are few implementations of generator.
-#
-# [+Prime+::+EratosthenesGenerator+]
-# Uses eratosthenes's sieve.
-# [+Prime+::+TrialDivisionGenerator+]
-# Uses the trial division method.
-# [+Prime+::+Generator23+]
-# Generates all positive integers which is not divided by 2 nor 3.
-# This sequence is very bad as a pseudo-prime sequence. But this
-# is faster and uses much less memory than other generators. So,
-# it is suitable for factorizing an integer which is not large but
-# has many prime factors. e.g. for Prime#prime? .
-class Prime
- include Enumerable
- @the_instance = Prime.new
-
- # obsolete. Use +Prime+::+instance+ or class methods of +Prime+.
- def initialize
- @generator = EratosthenesGenerator.new
- extend OldCompatibility
- warn "Prime::new is obsolete. use Prime::instance or class methods of Prime."
- end
-
- class<<self
- extend Forwardable
- include Enumerable
- # Returns the default instance of Prime.
- def instance; @the_instance end
-
- def method_added(method) # :nodoc:
- (class<<self;self;end).def_delegator :instance, method
- end
- end
-
- # Iterates the given block over all prime numbers.
- #
- # == Parameters
- # +ubound+::
- # Optional. An arbitrary positive number.
- # The upper bound of enumeration. The method enumerates
- # prime numbers infinitely if +ubound+ is nil.
- # +generator+::
- # Optional. An implementation of pseudo-prime generator.
- #
- # == Return value
- # An evaluated value of the given block at the last time.
- # Or an enumerator which is compatible to an +Enumerator+
- # if no block given.
- #
- # == Description
- # Calls +block+ once for each prime numer, passing the prime as
- # a parameter.
- #
- # +ubound+::
- # Upper bound of prime numbers. The iterator stops after
- # yields all prime numbers p <= +ubound+.
- #
- # == Note
- # +Prime+.+new+ returns a object extended by +Prime+::+OldCompatibility+
- # in order to compatibility to Ruby 1.9, and +Prime+#each is overwritten
- # by +Prime+::+OldCompatibility+#+each+.
- #
- # +Prime+.+new+ is now obsolete. Use +Prime+.+instance+.+each+ or simply
- # +Prime+.+each+.
- def each(ubound = nil, generator = EratosthenesGenerator.new, &block)
- generator.upper_bound = ubound
- generator.each(&block)
- end
-
-
- # Returns true if +value+ is prime, false for a composite.
- #
- # == Parameters
- # +value+:: an arbitrary integer to be checked.
- # +generator+:: optional. A pseudo-prime generator.
- def prime?(value, generator = Prime::Generator23.new)
- for num in generator
- q,r = value.divmod num
- return true if q < num
- return false if r == 0
- end
- end
-
- # Re-composes a prime factorization and returns the product.
- #
- # == Parameters
- # +pd+:: Array of pairs of integers. The each internal
- # pair consists of a prime number -- a prime factor --
- # and a natural number -- an exponent.
- #
- # == Example
- # For [[p_1, e_1], [p_2, e_2], ...., [p_n, e_n]], it returns
- # p_1**e_1 * p_2**e_2 * .... * p_n**e_n.
- #
- # Prime.int_from_prime_division([[2,2], [3,1]]) #=> 12
- def int_from_prime_division(pd)
- pd.inject(1){|value, (prime, index)|
- value *= prime**index
- }
- end
-
- # Returns the factorization of +value+.
- #
- # == Parameters
- # +value+:: An arbitrary integer.
- # +generator+:: Optional. A pseudo-prime generator.
- # +generator+.succ must return the next
- # pseudo-prime number in the ascendent
- # order. It must generate all prime numbers,
- # but may generate non prime numbers.
- #
- # === Exceptions
- # +ZeroDivisionError+:: when +value+ is zero.
- #
- # == Example
- # For an arbitrary integer
- # n = p_1**e_1 * p_2**e_2 * .... * p_n**e_n,
- # prime_division(n) returns
- # [[p_1, e_1], [p_2, e_2], ...., [p_n, e_n]].
- #
- # Prime.prime_division(12) #=> [[2,2], [3,1]]
- #
- def prime_division(value, generator= Prime::Generator23.new)
- raise ZeroDivisionError if value == 0
- pv = []
- for prime in generator
- count = 0
- while (value1, mod = value.divmod(prime)
- mod) == 0
- value = value1
- count += 1
- end
- if count != 0
- pv.push [prime, count]
- end
- break if value1 <= prime
- end
- if value > 1
- pv.push [value, 1]
- end
- return pv
- end
-
- # An abstract class for enumerating pseudo-prime numbers.
- #
- # Concrete subclasses should override succ, next, rewind.
- class PseudoPrimeGenerator
- include Enumerable
-
- def initialize(ubound = nil)
- @ubound = ubound
- end
-
- def upper_bound=(ubound)
- @ubound = ubound
- end
- def upper_bound
- @ubound
- end
-
- # returns the next pseudo-prime number, and move the internal
- # position forward.
- #
- # +PseudoPrimeGenerator+#succ raises +NotImplementedError+.
- def succ
- raise NotImplementedError, "need to define `succ'"
- end
-
- # alias of +succ+.
- def next
- raise NotImplementedError, "need to define `next'"
- end
-
- # Rewinds the internal position for enumeration.
- #
- # See +Enumerator+#rewind.
- def rewind
- raise NotImplementedError, "need to define `rewind'"
- end
-
- # Iterates the given block for each prime numbers.
- def each(&block)
- return self.dup unless block
- if @ubound
- last_value = nil
- loop do
- prime = succ
- break last_value if prime > @ubound
- last_value = block.call(prime)
- end
- else
- loop do
- block.call(succ)
- end
- end
- end
-
- # see +Enumerator+#with_index.
- alias with_index each_with_index
-
- # see +Enumerator+#with_object.
- def with_object(obj)
- return enum_for(:with_object) unless block_given?
- each do |prime|
- yield prime, obj
- end
- end
- end
-
- # An implementation of +PseudoPrimeGenerator+.
- #
- # Uses +EratosthenesSieve+.
- class EratosthenesGenerator < PseudoPrimeGenerator
- def initialize
- @last_prime = nil
- end
-
- def succ
- @last_prime = @last_prime ? EratosthenesSieve.instance.next_to(@last_prime) : 2
- end
- def rewind
- initialize
- end
- alias next succ
- end
-
- # An implementation of +PseudoPrimeGenerator+ which uses
- # a prime table generated by trial division.
- class TrialDivisionGenerator<PseudoPrimeGenerator
- def initialize
- @index = -1
- end
-
- def succ
- TrialDivision.instance[@index += 1]
- end
- def rewind
- initialize
- end
- alias next succ
- end
-
- # Generates all integer which are greater than 2 and
- # are not divided by 2 nor 3.
- #
- # This is a pseudo-prime generator, suitable on
- # checking primality of a integer by brute force
- # method.
- class Generator23<PseudoPrimeGenerator
- def initialize
- @prime = 1
- @step = nil
- end
-
- def succ
- loop do
- if (@step)
- @prime += @step
- @step = 6 - @step
- else
- case @prime
- when 1; @prime = 2
- when 2; @prime = 3
- when 3; @prime = 5; @step = 2
- end
- end
- return @prime
- end
- end
- alias next succ
- def rewind
- initialize
- end
- end
-
-
-
-
- # Internal use. An implementation of prime table by trial division method.
- class TrialDivision
- include Singleton
-
- def initialize # :nodoc:
- # These are included as class variables to cache them for later uses. If memory
- # usage is a problem, they can be put in Prime#initialize as instance variables.
-
- # There must be no primes between @primes[-1] and @next_to_check.
- @primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101]
- # @next_to_check % 6 must be 1.
- @next_to_check = 103 # @primes[-1] - @primes[-1] % 6 + 7
- @ulticheck_index = 3 # @primes.index(@primes.reverse.find {|n|
- # n < Math.sqrt(@@next_to_check) })
- @ulticheck_next_squared = 121 # @primes[@ulticheck_index + 1] ** 2
- end
-
- # Returns the cached prime numbers.
- def cache
- return @primes
- end
- alias primes cache
- alias primes_so_far cache
-
- # Returns the +index+th prime number.
- #
- # +index+ is a 0-based index.
- def [](index)
- while index >= @primes.length
- # Only check for prime factors up to the square root of the potential primes,
- # but without the performance hit of an actual square root calculation.
- if @next_to_check + 4 > @ulticheck_next_squared
- @ulticheck_index += 1
- @ulticheck_next_squared = @primes.at(@ulticheck_index + 1) ** 2
- end
- # Only check numbers congruent to one and five, modulo six. All others
-
- # are divisible by two or three. This also allows us to skip checking against
- # two and three.
- @primes.push @next_to_check if @primes[2..@ulticheck_index].find {|prime| @next_to_check % prime == 0 }.nil?
- @next_to_check += 4
- @primes.push @next_to_check if @primes[2..@ulticheck_index].find {|prime| @next_to_check % prime == 0 }.nil?
- @next_to_check += 2
- end
- return @primes[index]
- end
- end
-
- # Internal use. An implementation of eratosthenes's sieve
- class EratosthenesSieve
- include Singleton
-
- def initialize # :nodoc:
- # bitmap for odd prime numbers less than 256.
- # For an arbitrary odd number n, @table[i][j] is 1 when n is prime where i,j = n.divmod(32) .
- @table = [0xcb6e, 0x64b4, 0x129a, 0x816d, 0x4c32, 0x864a, 0x820d, 0x2196]
- end
-
- # returns the least odd prime number which is greater than +n+.
- def next_to(n)
- n = (n-1).div(2)*2+3 # the next odd number of given n
- i,j = n.divmod(32)
- loop do
- extend_table until @table.length > i
- if !@table[i].zero?
- (j...32).step(2) do |k|
- return 32*i+k if !@table[i][k.div(2)].zero?
- end
- end
- i += 1; j = 1
- end
- end
-
- private
- def extend_table
- orig_len = @table.length
- new_len = [orig_len**2, orig_len+256].min
- lbound = orig_len*32
- ubound = new_len*32
- @table.fill(0xFFFF, orig_len...new_len)
- (3..Integer(Math.sqrt(ubound))).step(2) do |p|
- i, j = p.divmod(32)
- next if @table[i][j.div(2)].zero?
-
- start = (lbound.div(2*p)*2+1)*p # odd multiple of p which is greater than or equal to lbound
- (start...ubound).step(2*p) do |n|
- i, j = n.divmod(32)
- @table[i] &= 0xFFFF ^ (1<<(j.div(2)))
- end
- end
- end
- end
-
- # Provides a +Prime+ object with compatibility to Ruby 1.8 when instanciated via +Prime+.+new+.
- module OldCompatibility
- # Returns the next prime number and forwards internal pointer.
- def succ
- @generator.succ
- end
- alias next succ
-
- # Overwrites Prime#each.
- #
- # Iterates the given block over all prime numbers. Note that enumeration starts from
- # the current position of internal pointer, not rewound.
- def each(&block)
- return @generator.dup unless block_given?
- loop do
- yield succ
- end
- end
- end
-end
diff --git a/lib/prism.rb b/lib/prism.rb
new file mode 100644
index 0000000000..8f0342724a
--- /dev/null
+++ b/lib/prism.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+# The Prism Ruby parser.
+#
+# "Parsing Ruby is suddenly manageable!"
+# - You, hopefully
+#
+module Prism
+ # There are many files in prism that are templated to handle every node type,
+ # which means the files can end up being quite large. We autoload them to make
+ # our require speed faster since consuming libraries are unlikely to use all
+ # of these features.
+
+ autoload :BasicVisitor, "prism/visitor"
+ autoload :Compiler, "prism/compiler"
+ autoload :DesugarCompiler, "prism/desugar_compiler"
+ autoload :Dispatcher, "prism/dispatcher"
+ autoload :DotVisitor, "prism/dot_visitor"
+ autoload :DSL, "prism/dsl"
+ autoload :InspectVisitor, "prism/inspect_visitor"
+ autoload :LexCompat, "prism/lex_compat"
+ autoload :MutationCompiler, "prism/mutation_compiler"
+ autoload :NodeFind, "prism/node_find"
+ autoload :Pattern, "prism/pattern"
+ autoload :Reflection, "prism/reflection"
+ autoload :Relocation, "prism/relocation"
+ autoload :Serialize, "prism/serialize"
+ autoload :StringQuery, "prism/string_query"
+ autoload :Translation, "prism/translation"
+ autoload :Visitor, "prism/visitor"
+
+ # Some of these constants are not meant to be exposed, so marking them as
+ # private here.
+
+ if RUBY_ENGINE != "jruby"
+ private_constant :LexCompat
+ private_constant :NodeFind
+ end
+
+ # Raised when requested to parse as the currently running Ruby version but Prism has no support for it.
+ class CurrentVersionError < ArgumentError
+ # Initialize a new exception for the given ruby version string.
+ #--
+ #: (String version) -> void
+ def initialize(version)
+ message = +"invalid version: Requested to parse as `version: 'current'`; "
+ major, minor, =
+ if version.match?(/\A\d+\.\d+.\d+\z/)
+ version.split(".").map(&:to_i)
+ end
+
+ if major && minor && ((major < 3) || (major == 3 && minor < 3))
+ message << " #{version} is below the minimum supported syntax."
+ else
+ message << " #{version} is unknown. Please update the `prism` gem."
+ end
+
+ super(message)
+ end
+ end
+
+ # :call-seq:
+ # lex_compat(source, **options) -> LexCompat::Result
+ #
+ # Returns a parse result whose value is an array of tokens that closely
+ # resembles the return value of Ripper.lex.
+ #
+ # For supported options, see Prism.parse.
+ #--
+ #: (String source, **untyped options) -> LexCompat::Result
+ def self.lex_compat(source, **options)
+ LexCompat.new(source, **options).result # steep:ignore
+ end
+
+ # :call-seq:
+ # load(source, serialized, freeze) -> ParseResult
+ #
+ # Load the serialized AST using the source as a reference into a tree.
+ #--
+ #: (String source, String serialized, ?bool freeze) -> ParseResult
+ def self.load(source, serialized, freeze = false)
+ Serialize.load_parse(source, serialized, freeze)
+ end
+
+ # Given a Method, UnboundMethod, Proc, or Thread::Backtrace::Location,
+ # returns the Prism node representing it. On CRuby, this uses node_id for
+ # an exact match. On other implementations, it falls back to best-effort
+ # matching by source location line number.
+ #--
+ #: (Method | UnboundMethod | Proc | Thread::Backtrace::Location callable, ?rubyvm: bool) -> Node?
+ def self.find(callable, rubyvm: !!defined?(RubyVM))
+ NodeFind.find(callable, rubyvm)
+ end
+
+ # @rbs!
+ # VERSION: String
+ # BACKEND: :CEXT | :FFI
+ #
+ # interface _Stream
+ # def gets: (?Integer integer) -> (String | nil)
+ # end
+ #
+ # def self.parse: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult
+ # def self.profile: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> void
+ # def self.lex: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> LexResult
+ # def self.parse_lex: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseLexResult
+ # def self.dump: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> String
+ # def self.parse_comments: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> Array[Comment]
+ # def self.parse_success?: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool
+ # def self.parse_failure?: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool
+ # def self.parse_stream: (_Stream stream, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult
+ # def self.parse_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult
+ # def self.profile_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> void
+ # def self.lex_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> LexResult
+ # def self.parse_lex_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseLexResult
+ # def self.dump_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> String
+ # def self.parse_file_comments: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> Array[Comment]
+ # def self.parse_file_success?: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool
+ # def self.parse_file_failure?: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool
+end
+
+require_relative "prism/polyfill/byteindex"
+require_relative "prism/polyfill/warn"
+require_relative "prism/node"
+require_relative "prism/node_ext"
+require_relative "prism/parse_result"
+
+# This is a Ruby implementation of the prism parser. If we're running on CRuby
+# and we haven't explicitly set the PRISM_FFI_BACKEND environment variable, then
+# it's going to require the built library. Otherwise, it's going to require a
+# module that uses FFI to call into the library.
+if RUBY_ENGINE == "ruby" and !ENV["PRISM_FFI_BACKEND"]
+ # The C extension is the default backend on CRuby.
+ Prism::BACKEND = :CEXT
+
+ require "prism/prism"
+else
+ # The FFI backend is used on other Ruby implementations.
+ Prism::BACKEND = :FFI
+
+ require_relative "prism/ffi"
+end
diff --git a/lib/prism/desugar_compiler.rb b/lib/prism/desugar_compiler.rb
new file mode 100644
index 0000000000..c64d03f64a
--- /dev/null
+++ b/lib/prism/desugar_compiler.rb
@@ -0,0 +1,463 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+module Prism
+ class DesugarAndWriteNode # :nodoc:
+ include DSL
+
+ attr_reader :node #: ClassVariableAndWriteNode | ConstantAndWriteNode | GlobalVariableAndWriteNode | InstanceVariableAndWriteNode | LocalVariableAndWriteNode
+ attr_reader :default_source #: Source
+ attr_reader :read_class, :write_class #: Symbol
+ attr_reader :arguments #: Hash[Symbol, untyped]
+
+ #: ((ClassVariableAndWriteNode | ConstantAndWriteNode | GlobalVariableAndWriteNode | InstanceVariableAndWriteNode | LocalVariableAndWriteNode) node, Source default_source, Symbol read_class, Symbol write_class, **untyped arguments) -> void
+ def initialize(node, default_source, read_class, write_class, **arguments)
+ @node = node
+ @default_source = default_source
+ @read_class = read_class
+ @write_class = write_class
+ @arguments = arguments
+ end
+
+ # Desugar `x &&= y` to `x && x = y`
+ #--
+ #: () -> node
+ def compile
+ and_node(
+ location: node.location,
+ left: public_send(read_class, location: node.name_loc, **arguments),
+ right: public_send(
+ write_class,
+ location: node.location,
+ **arguments,
+ name_loc: node.name_loc,
+ value: node.value,
+ operator_loc: node.operator_loc
+ ),
+ operator_loc: node.operator_loc
+ )
+ end
+ end
+
+ class DesugarOrWriteDefinedNode # :nodoc:
+ include DSL
+
+ attr_reader :node #: ClassVariableOrWriteNode | ConstantOrWriteNode | GlobalVariableOrWriteNode
+ attr_reader :default_source #: Source
+ attr_reader :read_class, :write_class #: Symbol
+ attr_reader :arguments #: Hash[Symbol, untyped]
+
+ #: ((ClassVariableOrWriteNode | ConstantOrWriteNode | GlobalVariableOrWriteNode) node, Source default_source, Symbol read_class, Symbol write_class, **untyped arguments) -> void
+ def initialize(node, default_source, read_class, write_class, **arguments)
+ @node = node
+ @default_source = default_source
+ @read_class = read_class
+ @write_class = write_class
+ @arguments = arguments
+ end
+
+ # Desugar `x ||= y` to `defined?(x) ? x : x = y`
+ #--
+ #: () -> node
+ def compile
+ if_node(
+ location: node.location,
+ if_keyword_loc: node.operator_loc,
+ predicate: defined_node(
+ location: node.name_loc,
+ value: public_send(read_class, location: node.name_loc, **arguments),
+ keyword_loc: node.operator_loc
+ ),
+ then_keyword_loc: node.operator_loc,
+ statements: statements_node(
+ location: node.location,
+ body: [public_send(read_class, location: node.name_loc, **arguments)]
+ ),
+ subsequent: else_node(
+ location: node.location,
+ else_keyword_loc: node.operator_loc,
+ statements: statements_node(
+ location: node.location,
+ body: [
+ public_send(
+ write_class,
+ location: node.location,
+ **arguments,
+ name_loc: node.name_loc,
+ value: node.value,
+ operator_loc: node.operator_loc
+ )
+ ]
+ ),
+ end_keyword_loc: node.operator_loc
+ ),
+ end_keyword_loc: node.operator_loc
+ )
+ end
+ end
+
+ class DesugarOperatorWriteNode # :nodoc:
+ include DSL
+
+ attr_reader :node #: ClassVariableOperatorWriteNode | ConstantOperatorWriteNode | GlobalVariableOperatorWriteNode | InstanceVariableOperatorWriteNode | LocalVariableOperatorWriteNode
+ attr_reader :default_source #: Source
+ attr_reader :read_class, :write_class #: Symbol
+ attr_reader :arguments #: Hash[Symbol, untyped]
+
+ #: ((ClassVariableOperatorWriteNode | ConstantOperatorWriteNode | GlobalVariableOperatorWriteNode | InstanceVariableOperatorWriteNode | LocalVariableOperatorWriteNode) node, Source default_source, Symbol read_class, Symbol write_class, **untyped arguments) -> void
+ def initialize(node, default_source, read_class, write_class, **arguments)
+ @node = node
+ @default_source = default_source
+ @read_class = read_class
+ @write_class = write_class
+ @arguments = arguments
+ end
+
+ # Desugar `x += y` to `x = x + y`
+ #--
+ #: () -> node
+ def compile
+ binary_operator_loc = node.binary_operator_loc.chop
+
+ public_send(
+ write_class,
+ location: node.location,
+ **arguments,
+ name_loc: node.name_loc,
+ value: call_node(
+ location: node.location,
+ receiver: public_send(
+ read_class,
+ location: node.name_loc,
+ **arguments
+ ),
+ name: binary_operator_loc.slice.to_sym,
+ message_loc: binary_operator_loc,
+ arguments: arguments_node(
+ location: node.value.location,
+ arguments: [node.value]
+ )
+ ),
+ operator_loc: node.binary_operator_loc.copy(
+ start_offset: node.binary_operator_loc.end_offset - 1,
+ length: 1
+ )
+ )
+ end
+ end
+
+ class DesugarOrWriteNode # :nodoc:
+ include DSL
+
+ attr_reader :node #: InstanceVariableOrWriteNode | LocalVariableOrWriteNode
+ attr_reader :default_source #: Source
+ attr_reader :read_class, :write_class #: Symbol
+ attr_reader :arguments #: Hash[Symbol, untyped]
+
+ #: ((InstanceVariableOrWriteNode | LocalVariableOrWriteNode) node, Source default_source, Symbol read_class, Symbol write_class, **untyped arguments) -> void
+ def initialize(node, default_source, read_class, write_class, **arguments)
+ @node = node
+ @default_source = default_source
+ @read_class = read_class
+ @write_class = write_class
+ @arguments = arguments
+ end
+
+ # Desugar `x ||= y` to `x || x = y`
+ #--
+ #: () -> node
+ def compile
+ or_node(
+ location: node.location,
+ left: public_send(read_class, location: node.name_loc, **arguments),
+ right: public_send(
+ write_class,
+ location: node.location,
+ **arguments,
+ name_loc: node.name_loc,
+ value: node.value,
+ operator_loc: node.operator_loc
+ ),
+ operator_loc: node.operator_loc
+ )
+ end
+ end
+
+ private_constant :DesugarAndWriteNode, :DesugarOrWriteNode, :DesugarOrWriteDefinedNode, :DesugarOperatorWriteNode
+
+ class ClassVariableAndWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarAndWriteNode.new(self, source, :class_variable_read_node, :class_variable_write_node, name: name).compile
+ end
+ end
+
+ class ClassVariableOrWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarOrWriteDefinedNode.new(self, source, :class_variable_read_node, :class_variable_write_node, name: name).compile
+ end
+ end
+
+ class ClassVariableOperatorWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarOperatorWriteNode.new(self, source, :class_variable_read_node, :class_variable_write_node, name: name).compile
+ end
+ end
+
+ class ConstantAndWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarAndWriteNode.new(self, source, :constant_read_node, :constant_write_node, name: name).compile
+ end
+ end
+
+ class ConstantOrWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarOrWriteDefinedNode.new(self, source, :constant_read_node, :constant_write_node, name: name).compile
+ end
+ end
+
+ class ConstantOperatorWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarOperatorWriteNode.new(self, source, :constant_read_node, :constant_write_node, name: name).compile
+ end
+ end
+
+ class GlobalVariableAndWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarAndWriteNode.new(self, source, :global_variable_read_node, :global_variable_write_node, name: name).compile
+ end
+ end
+
+ class GlobalVariableOrWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarOrWriteDefinedNode.new(self, source, :global_variable_read_node, :global_variable_write_node, name: name).compile
+ end
+ end
+
+ class GlobalVariableOperatorWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarOperatorWriteNode.new(self, source, :global_variable_read_node, :global_variable_write_node, name: name).compile
+ end
+ end
+
+ class InstanceVariableAndWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarAndWriteNode.new(self, source, :instance_variable_read_node, :instance_variable_write_node, name: name).compile
+ end
+ end
+
+ class InstanceVariableOrWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarOrWriteNode.new(self, source, :instance_variable_read_node, :instance_variable_write_node, name: name).compile
+ end
+ end
+
+ class InstanceVariableOperatorWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarOperatorWriteNode.new(self, source, :instance_variable_read_node, :instance_variable_write_node, name: name).compile
+ end
+ end
+
+ class LocalVariableAndWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarAndWriteNode.new(self, source, :local_variable_read_node, :local_variable_write_node, name: name, depth: depth).compile
+ end
+ end
+
+ class LocalVariableOrWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarOrWriteNode.new(self, source, :local_variable_read_node, :local_variable_write_node, name: name, depth: depth).compile
+ end
+ end
+
+ class LocalVariableOperatorWriteNode
+ #: () -> node
+ def desugar # :nodoc:
+ DesugarOperatorWriteNode.new(self, source, :local_variable_read_node, :local_variable_write_node, name: name, depth: depth).compile
+ end
+ end
+
+ # DesugarCompiler is a compiler that desugars Ruby code into a more primitive
+ # form. This is useful for consumers that want to deal with fewer node types.
+ class DesugarCompiler < MutationCompiler
+ # `@@foo &&= bar`
+ #
+ # becomes
+ #
+ # `@@foo && @@foo = bar`
+ #--
+ #: (ClassVariableAndWriteNode node) -> node
+ def visit_class_variable_and_write_node(node)
+ node.desugar
+ end
+
+ # `@@foo ||= bar`
+ #
+ # becomes
+ #
+ # `defined?(@@foo) ? @@foo : @@foo = bar`
+ #--
+ #: (ClassVariableOrWriteNode node) -> node
+ def visit_class_variable_or_write_node(node)
+ node.desugar
+ end
+
+ # `@@foo += bar`
+ #
+ # becomes
+ #
+ # `@@foo = @@foo + bar`
+ #--
+ #: (ClassVariableOperatorWriteNode node) -> node
+ def visit_class_variable_operator_write_node(node)
+ node.desugar
+ end
+
+ # `Foo &&= bar`
+ #
+ # becomes
+ #
+ # `Foo && Foo = bar`
+ #--
+ #: (ConstantAndWriteNode node) -> node
+ def visit_constant_and_write_node(node)
+ node.desugar
+ end
+
+ # `Foo ||= bar`
+ #
+ # becomes
+ #
+ # `defined?(Foo) ? Foo : Foo = bar`
+ #--
+ #: (ConstantOrWriteNode node) -> node
+ def visit_constant_or_write_node(node)
+ node.desugar
+ end
+
+ # `Foo += bar`
+ #
+ # becomes
+ #
+ # `Foo = Foo + bar`
+ #--
+ #: (ConstantOperatorWriteNode node) -> node
+ def visit_constant_operator_write_node(node)
+ node.desugar
+ end
+
+ # `$foo &&= bar`
+ #
+ # becomes
+ #
+ # `$foo && $foo = bar`
+ #--
+ #: (GlobalVariableAndWriteNode node) -> node
+ def visit_global_variable_and_write_node(node)
+ node.desugar
+ end
+
+ # `$foo ||= bar`
+ #
+ # becomes
+ #
+ # `defined?($foo) ? $foo : $foo = bar`
+ #--
+ #: (GlobalVariableOrWriteNode node) -> node
+ def visit_global_variable_or_write_node(node)
+ node.desugar
+ end
+
+ # `$foo += bar`
+ #
+ # becomes
+ #
+ # `$foo = $foo + bar`
+ #--
+ #: (GlobalVariableOperatorWriteNode node) -> node
+ def visit_global_variable_operator_write_node(node)
+ node.desugar
+ end
+
+ # `@foo &&= bar`
+ #
+ # becomes
+ #
+ # `@foo && @foo = bar`
+ #--
+ #: (InstanceVariableAndWriteNode node) -> node
+ def visit_instance_variable_and_write_node(node)
+ node.desugar
+ end
+
+ # `@foo ||= bar`
+ #
+ # becomes
+ #
+ # `@foo || @foo = bar`
+ #--
+ #: (InstanceVariableOrWriteNode node) -> node
+ def visit_instance_variable_or_write_node(node)
+ node.desugar
+ end
+
+ # `@foo += bar`
+ #
+ # becomes
+ #
+ # `@foo = @foo + bar`
+ #--
+ #: (InstanceVariableOperatorWriteNode node) -> node
+ def visit_instance_variable_operator_write_node(node)
+ node.desugar
+ end
+
+ # `foo &&= bar`
+ #
+ # becomes
+ #
+ # `foo && foo = bar`
+ #--
+ #: (LocalVariableAndWriteNode node) -> node
+ def visit_local_variable_and_write_node(node)
+ node.desugar
+ end
+
+ # `foo ||= bar`
+ #
+ # becomes
+ #
+ # `foo || foo = bar`
+ #--
+ #: (LocalVariableOrWriteNode node) -> node
+ def visit_local_variable_or_write_node(node)
+ node.desugar
+ end
+
+ # `foo += bar`
+ #
+ # becomes
+ #
+ # `foo = foo + bar`
+ #--
+ #: (LocalVariableOperatorWriteNode node) -> node
+ def visit_local_variable_operator_write_node(node)
+ node.desugar
+ end
+ end
+end
diff --git a/lib/prism/ffi.rb b/lib/prism/ffi.rb
new file mode 100644
index 0000000000..6b9bde51ea
--- /dev/null
+++ b/lib/prism/ffi.rb
@@ -0,0 +1,611 @@
+# frozen_string_literal: true
+# :markup: markdown
+# typed: ignore
+
+# This file is responsible for mirroring the API provided by the C extension by
+# using FFI to call into the shared library.
+
+require "rbconfig"
+require "ffi"
+
+# We want to eagerly load this file if there are Ractors so that it does not get
+# autoloaded from within a non-main Ractor.
+require "prism/serialize" if defined?(Ractor)
+
+module Prism # :nodoc:
+ module LibRubyParser # :nodoc:
+ extend FFI::Library
+
+ # Define the library that we will be pulling functions from. Note that this
+ # must align with the build shared library from make/rake.
+ libprism_in_build = File.expand_path("../../build/libprism.#{RbConfig::CONFIG["SOEXT"]}", __dir__)
+ libprism_in_libdir = "#{RbConfig::CONFIG["libdir"]}/prism/libprism.#{RbConfig::CONFIG["SOEXT"]}"
+
+ if File.exist?(libprism_in_build)
+ INCLUDE_DIR = File.expand_path("../../include", __dir__)
+ ffi_lib libprism_in_build
+ else
+ INCLUDE_DIR = "#{RbConfig::CONFIG["libdir"]}/prism/include"
+ ffi_lib libprism_in_libdir
+ end
+
+ # Convert a native C type declaration into a symbol that FFI understands.
+ # For example:
+ #
+ # const char * -> :pointer
+ # bool -> :bool
+ # size_t -> :size_t
+ # void -> :void
+ #
+ def self.resolve_type(type, callbacks)
+ type = type.strip
+
+ if !type.end_with?("*")
+ type.delete_prefix("const ").to_sym
+ else
+ type = type.delete_suffix("*").rstrip
+ callbacks.include?(type.to_sym) ? type.to_sym : :pointer
+ end
+ end
+
+ # Read through the given header file and find the declaration of each of the
+ # given functions. For each one, define a function with the same name and
+ # signature as the C function.
+ def self.load_exported_functions_from(header, *functions, callbacks)
+ File.foreach("#{INCLUDE_DIR}/#{header}") do |line|
+ # We only want to attempt to load exported functions.
+ next unless line.start_with?("PRISM_EXPORTED_FUNCTION ")
+
+ # We only want to load the functions that we are interested in.
+ next unless functions.any? { |function| line.include?(function) }
+
+ # Strip trailing attributes (PRISM_NODISCARD, PRISM_NONNULL(...), etc.)
+ line = line.sub(/\)(\s+PRISM_\w+(?:\([^)]*\))?)+\s*;/, ");")
+
+ # Parse the function declaration.
+ unless /^PRISM_EXPORTED_FUNCTION (?<return_type>.+) (?<name>\w+)\((?<arg_types>.+)\);$/ =~ line
+ raise "Could not parse #{line}"
+ end
+
+ # Delete the function from the list of functions we are looking for to
+ # mark it as having been found.
+ functions.delete(name)
+
+ # Split up the argument types into an array, ensure we handle the case
+ # where there are no arguments (by explicit void).
+ arg_types = arg_types.split(",").map(&:strip)
+ arg_types = [] if arg_types == %w[void]
+
+ # Resolve the type of the argument by dropping the name of the argument
+ # first if it is present.
+ arg_types.map! { |type| resolve_type(type.sub(/\w+$/, ""), callbacks) }
+
+ # Attach the function using the FFI library.
+ attach_function name, arg_types, resolve_type(return_type, [])
+ end
+
+ # If we didn't find all of the functions, raise an error.
+ raise "Could not find functions #{functions.inspect}" unless functions.empty?
+ end
+
+ callback :pm_source_stream_fgets_t, [:pointer, :int, :pointer], :pointer
+ callback :pm_source_stream_feof_t, [:pointer], :int
+ pm_source_init_result_values = %i[PM_SOURCE_INIT_SUCCESS PM_SOURCE_INIT_ERROR_GENERIC PM_SOURCE_INIT_ERROR_DIRECTORY PM_SOURCE_INIT_ERROR_NON_REGULAR]
+ enum :pm_source_init_result_t, pm_source_init_result_values
+ enum :pm_string_query_t, [:PM_STRING_QUERY_ERROR, -1, :PM_STRING_QUERY_FALSE, :PM_STRING_QUERY_TRUE]
+
+ # Ractor-safe lookup table for pm_source_init_result_t, since FFI's
+ # enum_type accesses module instance variables that are not shareable.
+ SOURCE_INIT_RESULT = pm_source_init_result_values.freeze
+
+ load_exported_functions_from(
+ "prism/version.h",
+ "pm_version",
+ []
+ )
+
+ load_exported_functions_from(
+ "prism/serialize.h",
+ "pm_serialize_parse",
+ "pm_serialize_parse_stream",
+ "pm_serialize_parse_comments",
+ "pm_serialize_lex",
+ "pm_serialize_parse_lex",
+ "pm_serialize_parse_success_p",
+ []
+ )
+
+ load_exported_functions_from(
+ "prism/string_query.h",
+ "pm_string_query_local",
+ "pm_string_query_constant",
+ "pm_string_query_method_name",
+ []
+ )
+
+ load_exported_functions_from(
+ "prism/buffer.h",
+ "pm_buffer_new",
+ "pm_buffer_value",
+ "pm_buffer_length",
+ "pm_buffer_free",
+ []
+ )
+
+ load_exported_functions_from(
+ "prism/source.h",
+ "pm_source_file_new",
+ "pm_source_mapped_new",
+ "pm_source_stream_new",
+ "pm_source_free",
+ "pm_source_source",
+ "pm_source_length",
+ [:pm_source_stream_fgets_t, :pm_source_stream_feof_t]
+ )
+
+ # This object represents a pm_buffer_t. We only use it as an opaque pointer,
+ # so it doesn't need to know the fields of pm_buffer_t.
+ class PrismBuffer # :nodoc:
+ attr_reader :pointer
+
+ def initialize(pointer)
+ @pointer = pointer
+ end
+
+ def value
+ LibRubyParser.pm_buffer_value(pointer)
+ end
+
+ def length
+ LibRubyParser.pm_buffer_length(pointer)
+ end
+
+ def read
+ value.read_string(length)
+ end
+
+ # Initialize a new buffer and yield it to the block. The buffer will be
+ # automatically freed when the block returns.
+ def self.with
+ buffer = LibRubyParser.pm_buffer_new
+ raise unless buffer
+
+ begin
+ yield new(buffer)
+ ensure
+ LibRubyParser.pm_buffer_free(buffer)
+ end
+ end
+ end
+
+ # This object represents source code to be parsed. For strings it wraps a
+ # pointer directly; for files it uses a pm_source_t under the hood.
+ class PrismSource # :nodoc:
+ PLATFORM_EXPECTS_UTF8 =
+ RbConfig::CONFIG["host_os"].match?(/bccwin|cygwin|djgpp|mingw|mswin|wince|darwin/i)
+
+ attr_reader :pointer, :length
+
+ def initialize(pointer, length, from_string)
+ @pointer = pointer
+ @length = length
+ @from_string = from_string
+ end
+
+ def read
+ raise "should use the original String instead" if @from_string
+ @pointer.read_string(@length)
+ end
+
+ # Yields a PrismSource backed by the given string to the block.
+ def self.with_string(string)
+ raise TypeError unless string.is_a?(String)
+
+ length = string.bytesize
+ # + 1 to never get an address of 0, which pm_parser_init() asserts
+ FFI::MemoryPointer.new(:char, length + 1, false) do |pointer|
+ pointer.write_string(string)
+ # since we have the extra byte we might as well \0-terminate
+ pointer.put_char(length, 0)
+ return yield new(pointer, length, true)
+ end
+ end
+
+ # Yields a PrismSource to the given block, backed by a pm_source_t.
+ def self.with_file(filepath)
+ raise TypeError unless filepath.is_a?(String)
+
+ # On Windows and Mac, it's expected that filepaths will be encoded in
+ # UTF-8. If they are not, we need to convert them to UTF-8 before
+ # passing them into pm_source_mapped_new.
+ if PLATFORM_EXPECTS_UTF8 && (encoding = filepath.encoding) != Encoding::ASCII_8BIT && encoding != Encoding::UTF_8
+ filepath = filepath.encode(Encoding::UTF_8)
+ end
+
+ FFI::MemoryPointer.new(:int) do |result_ptr|
+ pm_source = LibRubyParser.pm_source_mapped_new(filepath, 0, result_ptr)
+
+ case SOURCE_INIT_RESULT[result_ptr.read_int]
+ when :PM_SOURCE_INIT_SUCCESS
+ pointer = LibRubyParser.pm_source_source(pm_source)
+ length = LibRubyParser.pm_source_length(pm_source)
+ return yield new(pointer, length, false)
+ when :PM_SOURCE_INIT_ERROR_GENERIC
+ raise SystemCallError.new(filepath, FFI.errno)
+ when :PM_SOURCE_INIT_ERROR_DIRECTORY
+ raise Errno::EISDIR.new(filepath)
+ when :PM_SOURCE_INIT_ERROR_NON_REGULAR
+ # Fall back to reading the file through Ruby IO for non-regular
+ # files (pipes, character devices, etc.)
+ return with_string(File.read(filepath)) { |string| yield string }
+ else
+ raise "Unknown error initializing pm_source_t: #{result_ptr.read_int}"
+ end
+ ensure
+ LibRubyParser.pm_source_free(pm_source) if pm_source && !pm_source.null?
+ end
+ end
+ end
+ end
+
+ # Mark the LibRubyParser module as private as it should only be called through
+ # the prism module.
+ private_constant :LibRubyParser
+
+ # The version constant is set by reading the result of calling pm_version.
+ VERSION = LibRubyParser.pm_version.read_string.freeze
+
+ class << self
+ # Mirror the Prism.dump API by using the serialization API.
+ def dump(source, **options)
+ LibRubyParser::PrismSource.with_string(source) { |string| dump_common(string, options) }
+ end
+
+ # Mirror the Prism.dump_file API by using the serialization API.
+ def dump_file(filepath, **options)
+ options[:filepath] = filepath
+ LibRubyParser::PrismSource.with_file(filepath) { |string| dump_common(string, options) }
+ end
+
+ # Mirror the Prism.lex API by using the serialization API.
+ def lex(code, **options)
+ LibRubyParser::PrismSource.with_string(code) { |string| lex_common(string, code, options) }
+ end
+
+ # Mirror the Prism.lex_file API by using the serialization API.
+ def lex_file(filepath, **options)
+ options[:filepath] = filepath
+ LibRubyParser::PrismSource.with_file(filepath) { |string| lex_common(string, string.read, options) }
+ end
+
+ # Mirror the Prism.parse API by using the serialization API.
+ def parse(code, **options)
+ LibRubyParser::PrismSource.with_string(code) { |string| parse_common(string, code, options) }
+ end
+
+ # Mirror the Prism.parse_file API by using the serialization API. This uses
+ # native strings instead of Ruby strings because it allows us to use mmap
+ # when it is available.
+ def parse_file(filepath, **options)
+ options[:filepath] = filepath
+ LibRubyParser::PrismSource.with_file(filepath) { |string| parse_common(string, string.read, options) }
+ end
+
+ # Mirror the Prism.parse_stream API by using the serialization API.
+ def parse_stream(stream, **options)
+ LibRubyParser::PrismBuffer.with do |buffer|
+ source = +""
+ callback = -> (string, size, _) {
+ raise "Expected size to be >= 0, got: #{size}" if size <= 0
+
+ if !(line = stream.gets(size - 1)).nil?
+ source << line
+ string.write_string("#{line}\x00", line.bytesize + 1)
+ end
+ }
+
+ eof_callback = -> (_) { stream.eof? }
+
+ pm_source = LibRubyParser.pm_source_stream_new(nil, callback, eof_callback)
+ begin
+ LibRubyParser.pm_serialize_parse_stream(buffer.pointer, pm_source, dump_options(options))
+ Prism.load(source, buffer.read, options.fetch(:freeze, false))
+ ensure
+ LibRubyParser.pm_source_free(pm_source) if pm_source && !pm_source.null?
+ end
+ end
+ end
+
+ # Mirror the Prism.parse_comments API by using the serialization API.
+ def parse_comments(code, **options)
+ LibRubyParser::PrismSource.with_string(code) { |string| parse_comments_common(string, code, options) }
+ end
+
+ # Mirror the Prism.parse_file_comments API by using the serialization
+ # API. This uses native strings instead of Ruby strings because it allows us
+ # to use mmap when it is available.
+ def parse_file_comments(filepath, **options)
+ options[:filepath] = filepath
+ LibRubyParser::PrismSource.with_file(filepath) { |string| parse_comments_common(string, string.read, options) }
+ end
+
+ # Mirror the Prism.parse_lex API by using the serialization API.
+ def parse_lex(code, **options)
+ LibRubyParser::PrismSource.with_string(code) { |string| parse_lex_common(string, code, options) }
+ end
+
+ # Mirror the Prism.parse_lex_file API by using the serialization API.
+ def parse_lex_file(filepath, **options)
+ options[:filepath] = filepath
+ LibRubyParser::PrismSource.with_file(filepath) { |string| parse_lex_common(string, string.read, options) }
+ end
+
+ # Mirror the Prism.parse_success? API by using the serialization API.
+ def parse_success?(code, **options)
+ LibRubyParser::PrismSource.with_string(code) { |string| parse_file_success_common(string, options) }
+ end
+
+ # Mirror the Prism.parse_failure? API by using the serialization API.
+ def parse_failure?(code, **options)
+ !parse_success?(code, **options)
+ end
+
+ # Mirror the Prism.parse_file_success? API by using the serialization API.
+ def parse_file_success?(filepath, **options)
+ options[:filepath] = filepath
+ LibRubyParser::PrismSource.with_file(filepath) { |string| parse_file_success_common(string, options) }
+ end
+
+ # Mirror the Prism.parse_file_failure? API by using the serialization API.
+ def parse_file_failure?(filepath, **options)
+ !parse_file_success?(filepath, **options)
+ end
+
+ # Mirror the Prism.profile API by using the serialization API.
+ def profile(source, **options)
+ LibRubyParser::PrismSource.with_string(source) do |string|
+ LibRubyParser::PrismBuffer.with do |buffer|
+ LibRubyParser.pm_serialize_parse(buffer.pointer, string.pointer, string.length, dump_options(options))
+ nil
+ end
+ end
+ end
+
+ # Mirror the Prism.profile_file API by using the serialization API.
+ def profile_file(filepath, **options)
+ LibRubyParser::PrismSource.with_file(filepath) do |string|
+ LibRubyParser::PrismBuffer.with do |buffer|
+ options[:filepath] = filepath
+ LibRubyParser.pm_serialize_parse(buffer.pointer, string.pointer, string.length, dump_options(options))
+ nil
+ end
+ end
+ end
+
+ private
+
+ def dump_common(string, options) # :nodoc:
+ LibRubyParser::PrismBuffer.with do |buffer|
+ LibRubyParser.pm_serialize_parse(buffer.pointer, string.pointer, string.length, dump_options(options))
+
+ dumped = buffer.read
+ dumped.freeze if options.fetch(:freeze, false)
+
+ dumped
+ end
+ end
+
+ def lex_common(string, code, options) # :nodoc:
+ LibRubyParser::PrismBuffer.with do |buffer|
+ LibRubyParser.pm_serialize_lex(buffer.pointer, string.pointer, string.length, dump_options(options))
+ Serialize.load_lex(code, buffer.read, options.fetch(:freeze, false))
+ end
+ end
+
+ def parse_common(string, code, options) # :nodoc:
+ serialized = dump_common(string, options)
+ Serialize.load_parse(code, serialized, options.fetch(:freeze, false))
+ end
+
+ def parse_comments_common(string, code, options) # :nodoc:
+ LibRubyParser::PrismBuffer.with do |buffer|
+ LibRubyParser.pm_serialize_parse_comments(buffer.pointer, string.pointer, string.length, dump_options(options))
+ Serialize.load_parse_comments(code, buffer.read, options.fetch(:freeze, false))
+ end
+ end
+
+ def parse_lex_common(string, code, options) # :nodoc:
+ LibRubyParser::PrismBuffer.with do |buffer|
+ LibRubyParser.pm_serialize_parse_lex(buffer.pointer, string.pointer, string.length, dump_options(options))
+ Serialize.load_parse_lex(code, buffer.read, options.fetch(:freeze, false))
+ end
+ end
+
+ def parse_file_success_common(string, options) # :nodoc:
+ LibRubyParser.pm_serialize_parse_success_p(string.pointer, string.length, dump_options(options))
+ end
+
+ # Return the value that should be dumped for the command_line option.
+ def dump_options_command_line(options)
+ command_line = options.fetch(:command_line, "")
+ raise ArgumentError, "command_line must be a string" unless command_line.is_a?(String)
+
+ command_line.each_char.inject(0) do |value, char|
+ case char
+ when "a" then value | 0b000001
+ when "e" then value | 0b000010
+ when "l" then value | 0b000100
+ when "n" then value | 0b001000
+ when "p" then value | 0b010000
+ when "x" then value | 0b100000
+ else raise ArgumentError, "invalid command_line option: #{char}"
+ end
+ end
+ end
+
+ # Return the value that should be dumped for the version option.
+ def dump_options_version(version)
+ case version
+ when "current"
+ version_string_to_number(RUBY_VERSION) || raise(CurrentVersionError, RUBY_VERSION)
+ when "latest", nil
+ 0 # Handled in pm_parser_init
+ when "nearest"
+ dump = version_string_to_number(RUBY_VERSION)
+ return dump if dump
+ if RUBY_VERSION < "3.3"
+ version_string_to_number("3.3")
+ else
+ 0 # Handled in pm_parser_init
+ end
+ else
+ version_string_to_number(version) || raise(ArgumentError, "invalid version: #{version}")
+ end
+ end
+
+ # Converts a version string like "4.0.0" or "4.0" into a number.
+ # Returns nil if the version is unknown.
+ def version_string_to_number(version)
+ case version
+ when /\A3\.3(\.\d+)?\z/
+ 1
+ when /\A3\.4(\.\d+)?\z/
+ 2
+ when /\A3\.5(\.\d+)?\z/, /\A4\.0(\.\d+)?\z/
+ 3
+ when /\A4\.1(\.\d+)?\z/
+ 4
+ end
+ end
+
+ # Convert the given options into a serialized options string.
+ def dump_options(options)
+ template = +""
+ values = []
+
+ template << "L"
+ if (filepath = options[:filepath])
+ values.push(filepath.bytesize, filepath.b)
+ template << "A*"
+ else
+ values << 0
+ end
+
+ template << "l"
+ values << options.fetch(:line, 1)
+
+ template << "L"
+ if (encoding = options[:encoding])
+ name = encoding.is_a?(Encoding) ? encoding.name : encoding
+ values.push(name.bytesize, name.b)
+ template << "A*"
+ else
+ values << 0
+ end
+
+ template << "C"
+ values << (options.fetch(:frozen_string_literal, false) ? 1 : 0)
+
+ template << "C"
+ values << dump_options_command_line(options)
+
+ template << "C"
+ values << dump_options_version(options[:version])
+
+ template << "C"
+ values << (options[:encoding] == false ? 1 : 0)
+
+ template << "C"
+ values << (options.fetch(:main_script, false) ? 1 : 0)
+
+ template << "C"
+ values << (options.fetch(:partial_script, false) ? 1 : 0)
+
+ template << "C"
+ values << (options.fetch(:freeze, false) ? 1 : 0)
+
+ template << "L"
+ if (scopes = options[:scopes])
+ values << scopes.length
+
+ scopes.each do |scope|
+ locals = nil
+ forwarding = 0
+
+ case scope
+ when Array
+ locals = scope
+ when Scope
+ locals = scope.locals
+
+ scope.forwarding.each do |forward|
+ case forward
+ when :* then forwarding |= 0x1
+ when :** then forwarding |= 0x2
+ when :& then forwarding |= 0x4
+ when :"..." then forwarding |= 0x8
+ else raise ArgumentError, "invalid forwarding value: #{forward}"
+ end
+ end
+ else
+ raise TypeError, "wrong argument type #{scope.class.inspect} (expected Array or Prism::Scope)"
+ end
+
+ template << "L"
+ values << locals.length
+
+ template << "C"
+ values << forwarding
+
+ locals.each do |local|
+ name = local.name
+ template << "L"
+ values << name.bytesize
+
+ template << "A*"
+ values << name.b
+ end
+ end
+ else
+ values << 0
+ end
+
+ values.pack(template)
+ end
+ end
+
+ # Here we are going to patch StringQuery to put in the class-level methods so
+ # that it can maintain a consistent interface
+ class StringQuery # :nodoc:
+ class << self
+ # Mirrors the C extension's StringQuery::local? method.
+ def local?(string)
+ query(LibRubyParser.pm_string_query_local(string, string.bytesize, string.encoding.name))
+ end
+
+ # Mirrors the C extension's StringQuery::constant? method.
+ def constant?(string)
+ query(LibRubyParser.pm_string_query_constant(string, string.bytesize, string.encoding.name))
+ end
+
+ # Mirrors the C extension's StringQuery::method_name? method.
+ def method_name?(string)
+ query(LibRubyParser.pm_string_query_method_name(string, string.bytesize, string.encoding.name))
+ end
+
+ private
+
+ # Parse the enum result and return an appropriate boolean.
+ def query(result)
+ case result
+ when :PM_STRING_QUERY_ERROR
+ raise ArgumentError, "Invalid or non ascii-compatible encoding"
+ when :PM_STRING_QUERY_FALSE
+ false
+ when :PM_STRING_QUERY_TRUE
+ true
+ end
+ end
+ end
+ end
+end
diff --git a/lib/prism/lex_compat.rb b/lib/prism/lex_compat.rb
new file mode 100644
index 0000000000..7aacec037d
--- /dev/null
+++ b/lib/prism/lex_compat.rb
@@ -0,0 +1,906 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+module Prism
+ # @rbs!
+ # module Translation
+ # class Ripper
+ # EXPR_NONE: Integer
+ # EXPR_BEG: Integer
+ # EXPR_MID: Integer
+ # EXPR_END: Integer
+ # EXPR_CLASS: Integer
+ # EXPR_VALUE: Integer
+ # EXPR_ARG: Integer
+ # EXPR_CMDARG: Integer
+ # EXPR_ENDARG: Integer
+ # EXPR_ENDFN: Integer
+ #
+ # class Lexer < Ripper
+ # class State
+ # def self.[]: (Integer value) -> State
+ # end
+ # end
+ #
+ # class LineAndColumnCache
+ # def initialize: (Source source) -> void
+ #
+ # def line_and_column: (Integer byte_offset) -> [Integer, Integer]
+ # end
+ # end
+ # end
+
+ # This class is responsible for lexing the source using prism and then
+ # converting those tokens to be compatible with Ripper. In the vast majority
+ # of cases, this is a one-to-one mapping of the token type. Everything else
+ # generally lines up. However, there are a few cases that require special
+ # handling.
+ class LexCompat # :nodoc:
+ # @rbs!
+ # # A token produced by the Ripper lexer that Prism is replicating.
+ # type lex_compat_token = [[Integer, Integer], Symbol, String, untyped]
+
+ # A result class specialized for holding tokens produced by the lexer.
+ class Result < Prism::Result
+ # The list of tokens that were produced by the lexer.
+ attr_reader :value #: Array[lex_compat_token]
+
+ # Create a new lex compat result object with the given values.
+ #--
+ #: (Array[lex_compat_token] value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, bool continuable, Source source) -> void
+ def initialize(value, comments, magic_comments, data_loc, errors, warnings, continuable, source)
+ @value = value
+ super(comments, magic_comments, data_loc, errors, warnings, continuable, source)
+ end
+
+ # Implement the hash pattern matching interface for Result.
+ #--
+ #: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
+ def deconstruct_keys(keys) # :nodoc:
+ super.merge!(value: value)
+ end
+ end
+
+ # This is a mapping of prism token types to Ripper token types. This is a
+ # many-to-one mapping because we split up our token types, whereas Ripper
+ # tends to group them.
+ RIPPER = {
+ AMPERSAND: :on_op,
+ AMPERSAND_AMPERSAND: :on_op,
+ AMPERSAND_AMPERSAND_EQUAL: :on_op,
+ AMPERSAND_DOT: :on_op,
+ AMPERSAND_EQUAL: :on_op,
+ BACK_REFERENCE: :on_backref,
+ BACKTICK: :on_backtick,
+ BANG: :on_op,
+ BANG_EQUAL: :on_op,
+ BANG_TILDE: :on_op,
+ BRACE_LEFT: :on_lbrace,
+ BRACE_RIGHT: :on_rbrace,
+ BRACKET_LEFT: :on_lbracket,
+ BRACKET_LEFT_ARRAY: :on_lbracket,
+ BRACKET_LEFT_RIGHT: :on_op,
+ BRACKET_LEFT_RIGHT_EQUAL: :on_op,
+ BRACKET_RIGHT: :on_rbracket,
+ CARET: :on_op,
+ CARET_EQUAL: :on_op,
+ CHARACTER_LITERAL: :on_CHAR,
+ CLASS_VARIABLE: :on_cvar,
+ COLON: :on_op,
+ COLON_COLON: :on_op,
+ COMMA: :on_comma,
+ COMMENT: :on_comment,
+ CONSTANT: :on_const,
+ DOT: :on_period,
+ DOT_DOT: :on_op,
+ DOT_DOT_DOT: :on_op,
+ EMBDOC_BEGIN: :on_embdoc_beg,
+ EMBDOC_END: :on_embdoc_end,
+ EMBDOC_LINE: :on_embdoc,
+ EMBEXPR_BEGIN: :on_embexpr_beg,
+ EMBEXPR_END: :on_embexpr_end,
+ EMBVAR: :on_embvar,
+ EOF: :on_eof,
+ EQUAL: :on_op,
+ EQUAL_EQUAL: :on_op,
+ EQUAL_EQUAL_EQUAL: :on_op,
+ EQUAL_GREATER: :on_op,
+ EQUAL_TILDE: :on_op,
+ FLOAT: :on_float,
+ FLOAT_IMAGINARY: :on_imaginary,
+ FLOAT_RATIONAL: :on_rational,
+ FLOAT_RATIONAL_IMAGINARY: :on_imaginary,
+ GREATER: :on_op,
+ GREATER_EQUAL: :on_op,
+ GREATER_GREATER: :on_op,
+ GREATER_GREATER_EQUAL: :on_op,
+ GLOBAL_VARIABLE: :on_gvar,
+ HEREDOC_END: :on_heredoc_end,
+ HEREDOC_START: :on_heredoc_beg,
+ IDENTIFIER: :on_ident,
+ IGNORED_NEWLINE: :on_ignored_nl,
+ INTEGER: :on_int,
+ INTEGER_IMAGINARY: :on_imaginary,
+ INTEGER_RATIONAL: :on_rational,
+ INTEGER_RATIONAL_IMAGINARY: :on_imaginary,
+ INSTANCE_VARIABLE: :on_ivar,
+ INVALID: :INVALID,
+ KEYWORD___ENCODING__: :on_kw,
+ KEYWORD___LINE__: :on_kw,
+ KEYWORD___FILE__: :on_kw,
+ KEYWORD_ALIAS: :on_kw,
+ KEYWORD_AND: :on_kw,
+ KEYWORD_BEGIN: :on_kw,
+ KEYWORD_BEGIN_UPCASE: :on_kw,
+ KEYWORD_BREAK: :on_kw,
+ KEYWORD_CASE: :on_kw,
+ KEYWORD_CLASS: :on_kw,
+ KEYWORD_DEF: :on_kw,
+ KEYWORD_DEFINED: :on_kw,
+ KEYWORD_DO: :on_kw,
+ KEYWORD_DO_BLOCK: :on_kw,
+ KEYWORD_DO_LOOP: :on_kw,
+ KEYWORD_ELSE: :on_kw,
+ KEYWORD_ELSIF: :on_kw,
+ KEYWORD_END: :on_kw,
+ KEYWORD_END_UPCASE: :on_kw,
+ KEYWORD_ENSURE: :on_kw,
+ KEYWORD_FALSE: :on_kw,
+ KEYWORD_FOR: :on_kw,
+ KEYWORD_IF: :on_kw,
+ KEYWORD_IF_MODIFIER: :on_kw,
+ KEYWORD_IN: :on_kw,
+ KEYWORD_MODULE: :on_kw,
+ KEYWORD_NEXT: :on_kw,
+ KEYWORD_NIL: :on_kw,
+ KEYWORD_NOT: :on_kw,
+ KEYWORD_OR: :on_kw,
+ KEYWORD_REDO: :on_kw,
+ KEYWORD_RESCUE: :on_kw,
+ KEYWORD_RESCUE_MODIFIER: :on_kw,
+ KEYWORD_RETRY: :on_kw,
+ KEYWORD_RETURN: :on_kw,
+ KEYWORD_SELF: :on_kw,
+ KEYWORD_SUPER: :on_kw,
+ KEYWORD_THEN: :on_kw,
+ KEYWORD_TRUE: :on_kw,
+ KEYWORD_UNDEF: :on_kw,
+ KEYWORD_UNLESS: :on_kw,
+ KEYWORD_UNLESS_MODIFIER: :on_kw,
+ KEYWORD_UNTIL: :on_kw,
+ KEYWORD_UNTIL_MODIFIER: :on_kw,
+ KEYWORD_WHEN: :on_kw,
+ KEYWORD_WHILE: :on_kw,
+ KEYWORD_WHILE_MODIFIER: :on_kw,
+ KEYWORD_YIELD: :on_kw,
+ LABEL: :on_label,
+ LABEL_END: :on_label_end,
+ LAMBDA_BEGIN: :on_tlambeg,
+ LESS: :on_op,
+ LESS_EQUAL: :on_op,
+ LESS_EQUAL_GREATER: :on_op,
+ LESS_LESS: :on_op,
+ LESS_LESS_EQUAL: :on_op,
+ METHOD_NAME: :on_ident,
+ MINUS: :on_op,
+ MINUS_EQUAL: :on_op,
+ MINUS_GREATER: :on_tlambda,
+ NEWLINE: :on_nl,
+ NUMBERED_REFERENCE: :on_backref,
+ PARENTHESIS_LEFT: :on_lparen,
+ PARENTHESIS_LEFT_PARENTHESES: :on_lparen,
+ PARENTHESIS_RIGHT: :on_rparen,
+ PERCENT: :on_op,
+ PERCENT_EQUAL: :on_op,
+ PERCENT_LOWER_I: :on_qsymbols_beg,
+ PERCENT_LOWER_W: :on_qwords_beg,
+ PERCENT_LOWER_X: :on_backtick,
+ PERCENT_UPPER_I: :on_symbols_beg,
+ PERCENT_UPPER_W: :on_words_beg,
+ PIPE: :on_op,
+ PIPE_EQUAL: :on_op,
+ PIPE_PIPE: :on_op,
+ PIPE_PIPE_EQUAL: :on_op,
+ PLUS: :on_op,
+ PLUS_EQUAL: :on_op,
+ QUESTION_MARK: :on_op,
+ RATIONAL_FLOAT: :on_rational,
+ RATIONAL_INTEGER: :on_rational,
+ REGEXP_BEGIN: :on_regexp_beg,
+ REGEXP_END: :on_regexp_end,
+ SEMICOLON: :on_semicolon,
+ SLASH: :on_op,
+ SLASH_EQUAL: :on_op,
+ STAR: :on_op,
+ STAR_EQUAL: :on_op,
+ STAR_STAR: :on_op,
+ STAR_STAR_EQUAL: :on_op,
+ STRING_BEGIN: :on_tstring_beg,
+ STRING_CONTENT: :on_tstring_content,
+ STRING_END: :on_tstring_end,
+ SYMBOL_BEGIN: :on_symbeg,
+ TILDE: :on_op,
+ UAMPERSAND: :on_op,
+ UCOLON_COLON: :on_op,
+ UDOT_DOT: :on_op,
+ UDOT_DOT_DOT: :on_op,
+ UMINUS: :on_op,
+ UMINUS_NUM: :on_op,
+ UPLUS: :on_op,
+ USTAR: :on_op,
+ USTAR_STAR: :on_op,
+ WORDS_SEP: :on_words_sep,
+ "__END__": :on___end__
+ }.freeze
+
+ # A heredoc in this case is a list of tokens that belong to the body of the
+ # heredoc that should be appended onto the list of tokens when the heredoc
+ # closes.
+ module Heredoc # :nodoc:
+ # Heredocs that are no dash or tilde heredocs are just a list of tokens.
+ # We need to keep them around so that we can insert them in the correct
+ # order back into the token stream and set the state of the last token to
+ # the state that the heredoc was opened in.
+ class PlainHeredoc # :nodoc:
+ attr_reader :tokens #: Array[lex_compat_token]
+
+ #: () -> void
+ def initialize
+ @tokens = []
+ end
+
+ #: (lex_compat_token token) -> void
+ def <<(token)
+ tokens << token
+ end
+
+ #: () -> Array[lex_compat_token]
+ def to_a
+ tokens
+ end
+ end
+
+ # Dash heredocs are a little more complicated. They are a list of tokens
+ # that need to be split on "\\\n" to mimic Ripper's behavior. We also need
+ # to keep track of the state that the heredoc was opened in.
+ class DashHeredoc # :nodoc:
+ attr_reader :split #: bool
+ attr_reader :tokens #: Array[lex_compat_token]
+
+ #: (bool split) -> void
+ def initialize(split)
+ @split = split
+ @tokens = []
+ end
+
+ #: (lex_compat_token token) -> void
+ def <<(token)
+ tokens << token
+ end
+
+ #: () -> Array[lex_compat_token]
+ def to_a
+ embexpr_balance = 0
+
+ tokens.each_with_object([]) do |token, results| #$ Array[lex_compat_token]
+ case token[1]
+ when :on_embexpr_beg
+ embexpr_balance += 1
+ results << token
+ when :on_embexpr_end
+ embexpr_balance -= 1
+ results << token
+ when :on_tstring_content
+ if embexpr_balance == 0
+ lineno = token[0][0]
+ column = token[0][1]
+
+ if split
+ # Split on "\\\n" to mimic Ripper's behavior. Use a lookbehind
+ # to keep the delimiter in the result.
+ token[2].split(/(?<=[^\\]\\\n)|(?<=[^\\]\\\r\n)/).each_with_index do |value, index|
+ column = 0 if index > 0
+ results << [[lineno, column], :on_tstring_content, value, token[3]]
+ lineno += value.count("\n")
+ end
+ else
+ results << token
+ end
+ else
+ results << token
+ end
+ else
+ results << token
+ end
+ end
+ end
+ end
+
+ # Heredocs that are dedenting heredocs are a little more complicated.
+ # Ripper outputs on_ignored_sp tokens for the whitespace that is being
+ # removed from the output. prism only modifies the node itself and keeps
+ # the token the same. This simplifies prism, but makes comparing against
+ # Ripper much harder because there is a length mismatch.
+ #
+ # Fortunately, we already have to pull out the heredoc tokens in order to
+ # insert them into the stream in the correct order. As such, we can do
+ # some extra manipulation on the tokens to make them match Ripper's
+ # output by mirroring the dedent logic that Ripper uses.
+ class DedentingHeredoc # :nodoc:
+ TAB_WIDTH = 8
+
+ attr_reader :tokens #: Array[lex_compat_token]
+ attr_reader :dedent_next #: bool
+ attr_reader :dedent #: Integer?
+ attr_reader :embexpr_balance #: Integer
+ # @rbs @ended_on_newline: bool
+
+ #: () -> void
+ def initialize
+ @tokens = []
+ @dedent_next = true
+ @dedent = nil
+ @embexpr_balance = 0
+ @ended_on_newline = false
+ end
+
+ # As tokens are coming in, we track the minimum amount of common leading
+ # whitespace on plain string content tokens. This allows us to later
+ # remove that amount of whitespace from the beginning of each line.
+ #
+ #: (lex_compat_token token) -> void
+ def <<(token)
+ case token[1]
+ when :on_embexpr_beg, :on_heredoc_beg
+ @embexpr_balance += 1
+ @dedent = 0 if @dedent_next && @ended_on_newline
+ when :on_embexpr_end, :on_heredoc_end
+ @embexpr_balance -= 1
+ when :on_tstring_content
+ if embexpr_balance == 0
+ line = token[2]
+
+ if dedent_next && !(line.strip.empty? && line.end_with?("\n"))
+ leading = line[/\A(\s*)\n?/, 1] #: String
+ next_dedent = 0
+
+ leading.each_char do |char|
+ if char == "\t"
+ next_dedent = next_dedent - (next_dedent % TAB_WIDTH) + TAB_WIDTH
+ else
+ next_dedent += 1
+ end
+ end
+
+ @dedent = [dedent, next_dedent].compact.min
+ @dedent_next = true
+ @ended_on_newline = line.end_with?("\n")
+ tokens << token
+ return
+ end
+ end
+ end
+
+ @dedent_next = token[1] == :on_tstring_content && embexpr_balance == 0
+ @ended_on_newline = false
+ tokens << token
+ end
+
+ #: () -> Array[lex_compat_token]
+ def to_a
+ # If every line in the heredoc is blank, we still need to split up the
+ # string content token into multiple tokens.
+ if dedent.nil?
+ results = [] #: Array[lex_compat_token]
+ embexpr_balance = 0
+
+ tokens.each do |token|
+ case token[1]
+ when :on_embexpr_beg, :on_heredoc_beg
+ embexpr_balance += 1
+ results << token
+ when :on_embexpr_end, :on_heredoc_end
+ embexpr_balance -= 1
+ results << token
+ when :on_tstring_content
+ if embexpr_balance == 0
+ lineno = token[0][0]
+ column = token[0][1]
+
+ token[2].split(/(?<=\n)/).each_with_index do |value, index|
+ column = 0 if index > 0
+ results << [[lineno, column], :on_tstring_content, value, token[3]]
+ lineno += 1
+ end
+ else
+ results << token
+ end
+ else
+ results << token
+ end
+ end
+
+ return results
+ end
+
+ # If the minimum common whitespace is 0, then we need to concatenate
+ # string nodes together that are immediately adjacent.
+ if dedent == 0
+ results = [] #: Array[lex_compat_token]
+ embexpr_balance = 0
+
+ index = 0
+ max_index = tokens.length
+
+ while index < max_index
+ token = tokens[index]
+ results << token
+ index += 1
+
+ case token[1]
+ when :on_embexpr_beg, :on_heredoc_beg
+ embexpr_balance += 1
+ when :on_embexpr_end, :on_heredoc_end
+ embexpr_balance -= 1
+ when :on_tstring_content
+ if embexpr_balance == 0
+ while index < max_index && tokens[index][1] == :on_tstring_content && !token[2].match?(/\\\r?\n\z/)
+ token[2] << tokens[index][2]
+ index += 1
+ end
+ end
+ end
+ end
+
+ return results
+ end
+
+ # Otherwise, we're going to run through each token in the list and
+ # insert on_ignored_sp tokens for the amount of dedent that we need to
+ # perform. We also need to remove the dedent from the beginning of
+ # each line of plain string content tokens.
+ results = [] #: Array[lex_compat_token]
+ dedent_next = true
+ embexpr_balance = 0
+
+ tokens.each do |token|
+ # Notice that the structure of this conditional largely matches the
+ # whitespace calculation we performed above. This is because
+ # checking if the subsequent token needs to be dedented is common to
+ # both the dedent calculation and the ignored_sp insertion.
+ case token[1]
+ when :on_embexpr_beg
+ embexpr_balance += 1
+ results << token
+ when :on_embexpr_end
+ embexpr_balance -= 1
+ results << token
+ when :on_tstring_content
+ if embexpr_balance == 0
+ # Here we're going to split the string on newlines, but maintain
+ # the newlines in the resulting array. We'll do that with a look
+ # behind assertion.
+ splits = token[2].split(/(?<=\n)/)
+ index = 0
+
+ while index < splits.length
+ line = splits[index]
+ lineno = token[0][0] + index
+ column = token[0][1]
+
+ # Blank lines do not count toward common leading whitespace
+ # calculation and do not need to be dedented.
+ if dedent_next || index > 0
+ column = 0
+ end
+
+ # If the dedent is 0 and we're not supposed to dedent the next
+ # line or this line doesn't start with whitespace, then we
+ # should concatenate the rest of the string to match ripper.
+ if dedent == 0 && (!dedent_next || !line.start_with?(/\s/))
+ unjoined = splits[index..] #: Array[String]
+ line = unjoined.join
+ index = splits.length
+ end
+
+ # If we are supposed to dedent this line or if this is not the
+ # first line of the string and this line isn't entirely blank,
+ # then we need to insert an on_ignored_sp token and remove the
+ # dedent from the beginning of the line.
+ if (dedent > 0) && (dedent_next || index > 0)
+ deleting = 0
+ deleted_chars = [] #: Array[String]
+
+ # Gather up all of the characters that we're going to
+ # delete, stopping when you hit a character that would put
+ # you over the dedent amount.
+ line.each_char.with_index do |char, i|
+ case char
+ when "\r"
+ if line[i + 1] == "\n"
+ break
+ end
+ when "\n"
+ break
+ when "\t"
+ deleting = deleting - (deleting % TAB_WIDTH) + TAB_WIDTH
+ else
+ deleting += 1
+ end
+
+ break if deleting > dedent
+ deleted_chars << char
+ end
+
+ # If we have something to delete, then delete it from the
+ # string and insert an on_ignored_sp token.
+ if deleted_chars.any?
+ ignored = deleted_chars.join
+ line.delete_prefix!(ignored)
+
+ results << [[lineno, 0], :on_ignored_sp, ignored, token[3]]
+ column = ignored.length
+ end
+ end
+
+ results << [[lineno, column], token[1], line, token[3]] unless line.empty?
+ index += 1
+ end
+ else
+ results << token
+ end
+ else
+ results << token
+ end
+
+ dedent_next =
+ ((token[1] == :on_tstring_content) || (token[1] == :on_heredoc_end)) &&
+ embexpr_balance == 0
+ end
+
+ results
+ end
+ end
+
+ # Here we will split between the two types of heredocs and return the
+ # object that will store their tokens.
+ #--
+ #: (lex_compat_token opening) -> (PlainHeredoc | DashHeredoc | DedentingHeredoc)
+ def self.build(opening)
+ case opening[2][2]
+ when "~"
+ DedentingHeredoc.new
+ when "-"
+ DashHeredoc.new(opening[2][3] != "'")
+ else
+ PlainHeredoc.new
+ end
+ end
+ end
+
+ private_constant :Heredoc
+
+ # In previous versions of Ruby, Ripper wouldn't flush the bom before the
+ # first token, so we had to have a hack in place to account for that.
+ BOM_FLUSHED = RUBY_VERSION >= "3.3.0"
+ private_constant :BOM_FLUSHED
+
+ attr_reader :options #: Hash[Symbol, untyped]
+ # @rbs @source: String
+
+ #: (String source, **untyped options) -> void
+ def initialize(source, **options)
+ @source = source
+ @options = options
+ end
+
+ #: () -> Result
+ def result
+ tokens = [] #: Array[lex_compat_token]
+
+ state = :default
+ heredoc_stack = [[]] #: Array[Array[Heredoc::PlainHeredoc | Heredoc::DashHeredoc | Heredoc::DedentingHeredoc]]
+
+ result = Prism.lex(@source, **options)
+ source = result.source
+ result_value = result.value
+ previous_state = nil #: Translation::Ripper::Lexer::State?
+ last_heredoc_end = nil #: Integer?
+ eof_token = nil #: Token?
+
+ bom = source.slice(0, 3) == "\xEF\xBB\xBF"
+
+ result_value.each_with_index do |(prism_token, prism_state), index|
+ lineno = prism_token.location.start_line
+ column = prism_token.location.start_column
+
+ event = RIPPER.fetch(prism_token.type)
+ value = prism_token.value
+ lex_state = Translation::Ripper::Lexer::State[prism_state]
+
+ # If there's a UTF-8 byte-order mark as the start of the file, then for
+ # certain tokens ripper sets the first token back by 3 bytes. It also
+ # keeps the byte order mark in the first token's value. This is weird,
+ # and I don't want to mirror that in our parser. So instead, we'll match
+ # up the columns and values here.
+ if bom && lineno == 1
+ column -= 3
+
+ if index == 0 && column == 0 && !BOM_FLUSHED
+ flushed =
+ case prism_token.type
+ when :BACK_REFERENCE, :INSTANCE_VARIABLE, :CLASS_VARIABLE,
+ :GLOBAL_VARIABLE, :NUMBERED_REFERENCE, :PERCENT_LOWER_I,
+ :PERCENT_LOWER_X, :PERCENT_LOWER_W, :PERCENT_UPPER_I,
+ :PERCENT_UPPER_W, :STRING_BEGIN
+ true
+ when :REGEXP_BEGIN, :SYMBOL_BEGIN
+ value.start_with?("%")
+ else
+ false
+ end
+
+ unless flushed
+ column -= 3
+ value.prepend(String.new("\xEF\xBB\xBF", encoding: value.encoding))
+ end
+ end
+ end
+
+ lex_compat_token =
+ case event
+ when :on___end__
+ # Ripper doesn't include the rest of the token in the event, so we need to
+ # trim it down to just the content on the first line.
+ value = value[0..value.index("\n")] #: String
+ [[lineno, column], event, value, lex_state]
+ when :on_comment
+ [[lineno, column], event, value, lex_state]
+ when :on_heredoc_end
+ # Heredoc end tokens can be emitted in an odd order, so we don't
+ # want to bother comparing the state on them.
+ last_heredoc_end = prism_token.location.end_offset
+ [[lineno, column], event, value, lex_state]
+ when :on_embexpr_end
+ [[lineno, column], event, value, lex_state]
+ when :on_words_sep
+ # Ripper emits one token each per line.
+ value.each_line.with_index do |line, index|
+ if index > 0
+ lineno += 1
+ column = 0
+ end
+ tokens << [[lineno, column], event, line, lex_state]
+ end
+ tokens.pop #: lex_compat_token
+ when :on_regexp_end
+ # On regex end, Ripper scans and then sets end state, so the ripper
+ # lexed output is begin, when it should be end. prism sets lex state
+ # correctly to end state, but we want to be able to compare against
+ # Ripper's lexed state. So here, if it's a regexp end token, we
+ # output the state as the previous state, solely for the sake of
+ # comparison.
+ previous_token = result_value[index - 1][0]
+ lex_state =
+ if RIPPER.fetch(previous_token.type) == :on_embexpr_end
+ # If the previous token is embexpr_end, then we have to do even
+ # more processing. The end of an embedded expression sets the
+ # state to the state that it had at the beginning of the
+ # embedded expression. So we have to go and find that state and
+ # set it here.
+ counter = 1
+ current_index = index - 1
+
+ until counter == 0
+ current_index -= 1
+ current_event = RIPPER.fetch(result_value[current_index][0].type)
+ counter += { on_embexpr_beg: -1, on_embexpr_end: 1 }[current_event] || 0
+ end
+
+ Translation::Ripper::Lexer::State[result_value[current_index][1]]
+ else
+ previous_state
+ end
+
+ [[lineno, column], event, value, lex_state]
+ when :on_eof
+ eof_token = prism_token
+ previous_token = result_value[index - 1][0]
+
+ # If we're at the end of the file and the previous token was a
+ # comment and there is still whitespace after the comment, then
+ # Ripper will append a on_nl token (even though there isn't
+ # necessarily a newline). We mirror that here.
+ if previous_token.type == :COMMENT
+ # If the comment is at the start of a heredoc: <<HEREDOC # comment
+ # then the comment's end_offset is up near the heredoc_beg.
+ # This is not the correct offset to use for figuring out if
+ # there is trailing whitespace after the last token.
+ # Use the greater offset of the two to determine the start of
+ # the trailing whitespace.
+ start_offset = [previous_token.location.end_offset, last_heredoc_end].compact.max
+ end_offset = prism_token.location.start_offset
+
+ if start_offset < end_offset
+ if bom
+ start_offset += 3
+ end_offset += 3
+ end
+
+ tokens << [[lineno, 0], :on_nl, source.slice(start_offset, end_offset - start_offset), lex_state]
+ end
+ end
+
+ [[lineno, column], event, value, lex_state]
+ else
+ [[lineno, column], event, value, lex_state]
+ end #: lex_compat_token
+
+ previous_state = lex_state
+
+ # The order in which tokens appear in our lexer is different from the
+ # order that they appear in Ripper. When we hit the declaration of a
+ # heredoc in prism, we skip forward and lex the rest of the content of
+ # the heredoc before going back and lexing at the end of the heredoc
+ # identifier.
+ #
+ # To match up to ripper, we keep a small state variable around here to
+ # track whether we're in the middle of a heredoc or not. In this way we
+ # can shuffle around the token to match Ripper's output.
+ case state
+ when :default
+ # The default state is when there are no heredocs at all. In this
+ # state we can append the token to the list of tokens and move on.
+ tokens << lex_compat_token
+
+ # If we get the declaration of a heredoc, then we open a new heredoc
+ # and move into the heredoc_opened state.
+ if event == :on_heredoc_beg
+ state = :heredoc_opened
+ heredoc_stack.last << Heredoc.build(lex_compat_token)
+ end
+ when :heredoc_opened
+ # The heredoc_opened state is when we've seen the declaration of a
+ # heredoc and are now lexing the body of the heredoc. In this state we
+ # push tokens onto the most recently created heredoc.
+ heredoc_stack.last.last << lex_compat_token
+
+ case event
+ when :on_heredoc_beg
+ # If we receive a heredoc declaration while lexing the body of a
+ # heredoc, this means we have nested heredocs. In this case we'll
+ # push a new heredoc onto the stack and stay in the heredoc_opened
+ # state since we're now lexing the body of the new heredoc.
+ heredoc_stack << [Heredoc.build(lex_compat_token)]
+ when :on_heredoc_end
+ # If we receive the end of a heredoc, then we're done lexing the
+ # body of the heredoc. In this case we now have a completed heredoc
+ # but need to wait for the next newline to push it into the token
+ # stream.
+ state = :heredoc_closed
+ end
+ when :heredoc_closed
+ if %i[on_nl on_ignored_nl on_comment].include?(event) || ((event == :on_tstring_content) && value.end_with?("\n"))
+ if heredoc_stack.size > 1
+ flushing = heredoc_stack.pop #: Array[Heredoc::PlainHeredoc | Heredoc::DashHeredoc | Heredoc::DedentingHeredoc]
+ heredoc_stack.last.last << lex_compat_token
+
+ flushing.each do |heredoc|
+ heredoc.to_a.each do |flushed_token|
+ heredoc_stack.last.last << flushed_token
+ end
+ end
+
+ state = :heredoc_opened
+ next
+ end
+ elsif event == :on_heredoc_beg
+ tokens << lex_compat_token
+ state = :heredoc_opened
+ heredoc_stack.last << Heredoc.build(lex_compat_token)
+ next
+ elsif heredoc_stack.size > 1
+ heredoc_stack[-2].last << lex_compat_token
+ next
+ end
+
+ heredoc_stack.last.each do |heredoc|
+ tokens.concat(heredoc.to_a)
+ end
+
+ heredoc_stack.last.clear
+ state = :default
+
+ tokens << lex_compat_token
+ end
+ end
+
+ # Drop the EOF token from the list. The EOF token may not be
+ # present if the source was syntax invalid
+ if tokens.dig(-1, 1) == :on_eof
+ tokens = tokens[0...-1] #: Array[lex_compat_token]
+ end
+
+ # We sort by location because Ripper.lex sorts.
+ tokens.sort_by! do |token|
+ line, column = token[0]
+ source.byte_offset(line, column)
+ end
+
+ tokens = post_process_tokens(tokens, source, result.data_loc, bom, eof_token)
+
+ Result.new(tokens, result.comments, result.magic_comments, result.data_loc, result.errors, result.warnings, result.continuable?, source)
+ end
+
+ private
+
+ #: (Array[lex_compat_token] tokens, Source source, Location? data_loc, bool bom, Token? eof_token) -> Array[lex_compat_token]
+ def post_process_tokens(tokens, source, data_loc, bom, eof_token)
+ new_tokens = [] #: Array[lex_compat_token]
+
+ prev_token_state = Translation::Ripper::Lexer::State[Translation::Ripper::EXPR_BEG]
+ prev_token_end = bom ? 3 : 0
+
+ cache = Translation::Ripper::LineAndColumnCache.new(source)
+
+ tokens.each do |token|
+ # Skip missing heredoc ends.
+ next if token[1] == :on_heredoc_end && token[2] == ""
+
+ # Add :on_sp tokens.
+ line, column = token[0]
+ start_offset = source.byte_offset(line, column)
+
+ # Ripper reports columns on line 1 without counting the BOM, so we
+ # adjust to get the real offset
+ start_offset += 3 if line == 1 && bom
+
+ if start_offset > prev_token_end
+ sp_value = source.slice(prev_token_end, start_offset - prev_token_end)
+ sp_line, sp_column = cache.line_and_column(prev_token_end)
+ # Ripper reports columns on line 1 without counting the BOM
+ sp_column -= 3 if sp_line == 1 && bom
+ continuation_index = sp_value.byteindex("\\")
+
+ # ripper emits up to three :on_sp tokens when line continuations are used
+ if continuation_index
+ next_whitespace_index = continuation_index + 1
+ next_whitespace_index += 1 if sp_value.byteslice(next_whitespace_index) == "\r"
+ next_whitespace_index += 1
+ first_whitespace = sp_value[0...continuation_index] #: String
+ continuation = sp_value[continuation_index...next_whitespace_index] #: String
+ second_whitespace = sp_value[next_whitespace_index..] || ""
+
+ new_tokens << [[sp_line, sp_column], :on_sp, first_whitespace, prev_token_state] unless first_whitespace.empty?
+ new_tokens << [[sp_line, sp_column + continuation_index], :on_sp, continuation, prev_token_state]
+ new_tokens << [[sp_line + 1, 0], :on_sp, second_whitespace, prev_token_state] unless second_whitespace.empty?
+ else
+ new_tokens << [[sp_line, sp_column], :on_sp, sp_value, prev_token_state]
+ end
+ end
+
+ new_tokens << token
+ prev_token_state = token[3]
+ prev_token_end = start_offset + token[2].bytesize
+ end
+
+ if !data_loc && eof_token # no trailing :on_sp with __END__ as it is always preceded by :on_nl
+ end_offset = eof_token.location.end_offset
+ if prev_token_end < end_offset
+ new_tokens << [
+ [source.line(prev_token_end), source.column(prev_token_end)],
+ :on_sp,
+ source.slice(prev_token_end, end_offset - prev_token_end),
+ prev_token_state
+ ]
+ end
+ end
+
+ new_tokens
+ end
+ end
+
+ private_constant :LexCompat
+end
diff --git a/lib/prism/node_ext.rb b/lib/prism/node_ext.rb
new file mode 100644
index 0000000000..8a6624e76d
--- /dev/null
+++ b/lib/prism/node_ext.rb
@@ -0,0 +1,388 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+#--
+# Here we are reopening the prism module to provide methods on nodes that aren't
+# templated and are meant as convenience methods.
+#++
+module Prism
+ class Node
+ #: (*String replacements) -> void
+ def deprecated(*replacements) # :nodoc:
+ location = caller_locations(1, 1)&.[](0)&.label
+ suggest = replacements.map { |replacement| "#{self.class}##{replacement}" }
+
+ warn(<<~MSG, uplevel: 1, category: :deprecated)
+ [deprecation]: #{self.class}##{location} is deprecated and will be \
+ removed in the next major version. Use #{suggest.join("/")} instead.
+ #{(caller(1, 3) || []).join("\n")}
+ MSG
+ end
+ end
+
+ module RegularExpressionOptions # :nodoc:
+ # Returns a numeric value that represents the flags that were used to create
+ # the regular expression.
+ #--
+ #: (Integer flags) -> Integer
+ def self.options(flags)
+ o = 0
+ o |= Regexp::IGNORECASE if flags.anybits?(RegularExpressionFlags::IGNORE_CASE)
+ o |= Regexp::EXTENDED if flags.anybits?(RegularExpressionFlags::EXTENDED)
+ o |= Regexp::MULTILINE if flags.anybits?(RegularExpressionFlags::MULTI_LINE)
+ o |= Regexp::FIXEDENCODING if flags.anybits?(RegularExpressionFlags::EUC_JP | RegularExpressionFlags::WINDOWS_31J | RegularExpressionFlags::UTF_8)
+ o |= Regexp::NOENCODING if flags.anybits?(RegularExpressionFlags::ASCII_8BIT)
+ o
+ end
+ end
+
+ class InterpolatedMatchLastLineNode < Node
+ # Returns a numeric value that represents the flags that were used to create
+ # the regular expression.
+ #--
+ #: () -> Integer
+ def options
+ RegularExpressionOptions.options(flags)
+ end
+ end
+
+ class InterpolatedRegularExpressionNode < Node
+ # Returns a numeric value that represents the flags that were used to create
+ # the regular expression.
+ #--
+ #: () -> Integer
+ def options
+ RegularExpressionOptions.options(flags)
+ end
+ end
+
+ class MatchLastLineNode < Node
+ # Returns a numeric value that represents the flags that were used to create
+ # the regular expression.
+ #--
+ #: () -> Integer
+ def options
+ RegularExpressionOptions.options(flags)
+ end
+ end
+
+ class RegularExpressionNode < Node
+ # Returns a numeric value that represents the flags that were used to create
+ # the regular expression.
+ #--
+ #: () -> Integer
+ def options
+ RegularExpressionOptions.options(flags)
+ end
+ end
+
+ private_constant :RegularExpressionOptions
+
+ module HeredocQuery # :nodoc:
+ # Returns true if this node was represented as a heredoc in the source code.
+ #--
+ #: (String? opening) -> bool?
+ def self.heredoc?(opening)
+ # @type self: InterpolatedStringNode | InterpolatedXStringNode | StringNode | XStringNode
+ opening&.start_with?("<<")
+ end
+ end
+
+ class InterpolatedStringNode < Node
+ # Returns true if this node was represented as a heredoc in the source code.
+ #--
+ #: () -> bool?
+ def heredoc?
+ HeredocQuery.heredoc?(opening)
+ end
+ end
+
+ class InterpolatedXStringNode < Node
+ # Returns true if this node was represented as a heredoc in the source code.
+ #--
+ #: () -> bool?
+ def heredoc?
+ HeredocQuery.heredoc?(opening)
+ end
+ end
+
+ class StringNode < Node
+ # Returns true if this node was represented as a heredoc in the source code.
+ #--
+ #: () -> bool?
+ def heredoc?
+ HeredocQuery.heredoc?(opening)
+ end
+
+ # Occasionally it's helpful to treat a string as if it were interpolated so
+ # that there's a consistent interface for working with strings.
+ #--
+ #: () -> InterpolatedStringNode
+ def to_interpolated
+ InterpolatedStringNode.new(
+ source,
+ -1,
+ location,
+ frozen? ? InterpolatedStringNodeFlags::FROZEN : 0,
+ opening_loc,
+ [copy(location: content_loc, opening_loc: nil, closing_loc: nil)],
+ closing_loc
+ )
+ end
+ end
+
+ class XStringNode < Node
+ # Returns true if this node was represented as a heredoc in the source code.
+ #--
+ #: () -> bool?
+ def heredoc?
+ HeredocQuery.heredoc?(opening)
+ end
+
+ # Occasionally it's helpful to treat a string as if it were interpolated so
+ # that there's a consistent interface for working with strings.
+ #--
+ #: () -> InterpolatedXStringNode
+ def to_interpolated
+ InterpolatedXStringNode.new(
+ source,
+ -1,
+ location,
+ flags,
+ opening_loc,
+ [StringNode.new(source, node_id, content_loc, 0, nil, content_loc, nil, unescaped)],
+ closing_loc
+ )
+ end
+ end
+
+ private_constant :HeredocQuery
+
+ class ImaginaryNode < Node
+ # Returns the value of the node as a Ruby Complex.
+ #--
+ #: () -> Complex
+ def value
+ Complex(0, numeric.value)
+ end
+ end
+
+ class RationalNode < Node
+ # Returns the value of the node as a Ruby Rational.
+ #--
+ #: () -> Rational
+ def value
+ Rational(numerator, denominator)
+ end
+ end
+
+ class ConstantReadNode < Node
+ # Returns the list of parts for the full name of this constant.
+ # For example: [:Foo]
+ #--
+ #: () -> Array[Symbol]
+ def full_name_parts
+ [name]
+ end
+
+ # Returns the full name of this constant. For example: "Foo"
+ #--
+ #: () -> String
+ def full_name
+ name.to_s
+ end
+ end
+
+ class ConstantWriteNode < Node
+ # Returns the list of parts for the full name of this constant.
+ # For example: [:Foo]
+ #--
+ #: () -> Array[Symbol]
+ def full_name_parts
+ [name]
+ end
+
+ # Returns the full name of this constant. For example: "Foo"
+ #--
+ #: () -> String
+ def full_name
+ name.to_s
+ end
+ end
+
+ class ConstantPathNode < Node
+ # An error class raised when dynamic parts are found while computing a
+ # constant path's full name. For example:
+ # Foo::Bar::Baz -> does not raise because all parts of the constant path are
+ # simple constants
+ # var::Bar::Baz -> raises because the first part of the constant path is a
+ # local variable
+ class DynamicPartsInConstantPathError < StandardError; end
+
+ # An error class raised when error recovery nodes are found while computing a
+ # constant path's full name. For example:
+ # Foo:: -> raises because the constant path is missing the last part
+ class ErrorRecoveryNodesInConstantPathError < StandardError; end
+
+ # Returns the list of parts for the full name of this constant path.
+ # For example: [:Foo, :Bar]
+ #--
+ #: () -> Array[Symbol]
+ def full_name_parts
+ parts = [] #: Array[Symbol]
+ current = self #: node?
+
+ while current.is_a?(ConstantPathNode)
+ name = current.name
+ if name.nil?
+ raise ErrorRecoveryNodesInConstantPathError, "Constant path contains error recovery nodes. Cannot compute full name"
+ end
+
+ parts.unshift(name)
+ current = current.parent
+ end
+
+ if !current.is_a?(ConstantReadNode) && !current.nil?
+ raise DynamicPartsInConstantPathError, "Constant path contains dynamic parts. Cannot compute full name"
+ end
+
+ parts.unshift(current&.name || :"")
+ end
+
+ # Returns the full name of this constant path. For example: "Foo::Bar"
+ #--
+ #: () -> String
+ def full_name
+ full_name_parts.join("::")
+ end
+ end
+
+ class ConstantPathTargetNode < Node
+ # Returns the list of parts for the full name of this constant path.
+ # For example: [:Foo, :Bar]
+ #--
+ #: () -> Array[Symbol]
+ def full_name_parts
+ parts =
+ case (parent = self.parent)
+ when ConstantPathNode, ConstantReadNode
+ parent.full_name_parts
+ when nil
+ [:""]
+ else
+ # e.g. self::Foo, (var)::Bar = baz
+ raise ConstantPathNode::DynamicPartsInConstantPathError, "Constant target path contains dynamic parts. Cannot compute full name"
+ end
+
+ if (name = self.name).nil?
+ raise ConstantPathNode::ErrorRecoveryNodesInConstantPathError, "Constant target path contains error recovery nodes. Cannot compute full name"
+ end
+
+ parts.push(name)
+ end
+
+ # Returns the full name of this constant path. For example: "Foo::Bar"
+ #--
+ #: () -> String
+ def full_name
+ full_name_parts.join("::")
+ end
+ end
+
+ class ConstantTargetNode < Node
+ # Returns the list of parts for the full name of this constant.
+ # For example: [:Foo]
+ #--
+ #: () -> Array[Symbol]
+ def full_name_parts
+ [name]
+ end
+
+ # Returns the full name of this constant. For example: "Foo"
+ #--
+ #: () -> String
+ def full_name
+ name.to_s
+ end
+ end
+
+ class ParametersNode < Node
+ # Mirrors the Method#parameters method.
+ #--
+ #: () -> Array[[Symbol, Symbol] | [Symbol]]
+ def signature
+ names = [] #: Array[[Symbol, Symbol] | [Symbol]]
+
+ requireds.each do |param|
+ names << (param.is_a?(MultiTargetNode) ? [:req] : [:req, param.name])
+ end
+
+ optionals.each { |param| names << [:opt, param.name] }
+
+ if (rest = self.rest).is_a?(RestParameterNode)
+ names << [:rest, rest.name || :*]
+ end
+
+ posts.each do |param|
+ case param
+ when MultiTargetNode
+ names << [:req]
+ when ErrorRecoveryNode
+ raise "Invalid syntax"
+ else
+ names << [:req, param.name]
+ end
+ end
+
+ # Regardless of the order in which the keywords were defined, the required
+ # keywords always come first followed by the optional keywords.
+ keyopt = [] #: Array[OptionalKeywordParameterNode]
+ keywords.each do |param|
+ if param.is_a?(OptionalKeywordParameterNode)
+ keyopt << param
+ else
+ names << [:keyreq, param.name]
+ end
+ end
+
+ keyopt.each { |param| names << [:key, param.name] }
+
+ case (keyword_rest = self.keyword_rest)
+ when ForwardingParameterNode
+ names.concat([[:rest, :*], [:keyrest, :**], [:block, :&]])
+ when KeywordRestParameterNode
+ names << [:keyrest, keyword_rest.name || :**]
+ when NoKeywordsParameterNode
+ names << [:nokey]
+ end
+
+ case (block = self.block)
+ when BlockParameterNode
+ names << [:block, block.name || :&]
+ when NoBlockParameterNode
+ names << [:noblock]
+ end
+
+ names
+ end
+ end
+
+ class CallNode < Node
+ # When a call node has the attribute_write flag set, it means that the call
+ # is using the attribute write syntax. This is either a method call to []=
+ # or a method call to a method that ends with =. Either way, the = sign is
+ # present in the source.
+ #
+ # Prism returns the message_loc _without_ the = sign attached, because there
+ # can be any amount of space between the message and the = sign. However,
+ # sometimes you want the location of the full message including the inner
+ # space and the = sign. This method provides that.
+ #--
+ #: () -> Location?
+ def full_message_loc
+ attribute_write? ? message_loc&.adjoin("=") : message_loc
+ end
+ end
+end
diff --git a/lib/prism/node_find.rb b/lib/prism/node_find.rb
new file mode 100644
index 0000000000..697ee430e8
--- /dev/null
+++ b/lib/prism/node_find.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+module Prism
+ # Finds the Prism AST node corresponding to a given Method, UnboundMethod,
+ # Proc, or Thread::Backtrace::Location. On CRuby, uses node_id from the
+ # instruction sequence for an exact match. On other implementations, falls
+ # back to best-effort matching by source location line number.
+ #
+ # This module is autoloaded so that programs that don't use Prism.find don't
+ # pay for its definition.
+ module NodeFind # :nodoc:
+ # Find the node for the given callable or backtrace location.
+ #--
+ #: (Method | UnboundMethod | Proc | Thread::Backtrace::Location callable, bool rubyvm) -> Node?
+ def self.find(callable, rubyvm)
+ case callable
+ when Proc
+ if rubyvm
+ RubyVMCallableFind.new.find(callable)
+ elsif callable.lambda?
+ LineLambdaFind.new.find(callable)
+ else
+ LineProcFind.new.find(callable)
+ end
+ when Method, UnboundMethod
+ if rubyvm
+ RubyVMCallableFind.new.find(callable)
+ else
+ LineMethodFind.new.find(callable)
+ end
+ when Thread::Backtrace::Location
+ if rubyvm
+ RubyVMBacktraceLocationFind.new.find(callable)
+ else
+ LineBacktraceLocationFind.new.find(callable)
+ end
+ else
+ raise ArgumentError, "Expected a Method, UnboundMethod, Proc, or Thread::Backtrace::Location, got #{callable.class}"
+ end
+ end
+
+ # Base class that handles parsing a file.
+ class Find
+ private
+
+ # Parse the given file path, returning a ParseResult or nil.
+ #--
+ #: (String? file) -> ParseResult?
+ def parse_file(file)
+ return unless file && File.readable?(file)
+ result = Prism.parse_file(file)
+ result if result.success?
+ end
+ end
+
+ # Finds the AST node for a Method, UnboundMethod, or Proc using the node_id
+ # from the instruction sequence.
+ class RubyVMCallableFind < Find
+ # Find the node for the given callable using the ISeq node_id.
+ #--
+ #: (Method | UnboundMethod | Proc callable) -> Node?
+ def find(callable)
+ return unless (source_location = callable.source_location)
+ return unless (result = parse_file(source_location[0]))
+ return unless (iseq = RubyVM::InstructionSequence.of(callable))
+
+ header = iseq.to_a[4]
+ return unless header[:parser] == :prism
+
+ result.value.find { |node| node.node_id == header[:node_id] }
+ end
+ end
+
+ # Finds the AST node for a Thread::Backtrace::Location using the node_id
+ # from the backtrace location.
+ class RubyVMBacktraceLocationFind < Find
+ # Find the node for the given backtrace location using node_id.
+ #--
+ #: (Thread::Backtrace::Location location) -> Node?
+ def find(location)
+ file = location.absolute_path || location.path
+ return unless (result = parse_file(file))
+ return unless RubyVM::AbstractSyntaxTree.respond_to?(:node_id_for_backtrace_location)
+
+ node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(location)
+
+ result.value.find { |node| node.node_id == node_id }
+ end
+ end
+
+ # Finds the AST node for a Method or UnboundMethod using best-effort line
+ # matching. Used on non-CRuby implementations.
+ class LineMethodFind < Find
+ # Find the node for the given method by matching on name and line.
+ #--
+ #: (Method | UnboundMethod callable) -> Node?
+ def find(callable)
+ return unless (source_location = callable.source_location)
+ return unless (result = parse_file(source_location[0]))
+
+ name = callable.name
+ start_line = source_location[1]
+
+ result.value.find do |node|
+ case node
+ when DefNode
+ node.name == name && node.location.start_line == start_line
+ when CallNode
+ node.block.is_a?(BlockNode) && node.location.start_line == start_line
+ else
+ false
+ end
+ end
+ end
+ end
+
+ # Finds the AST node for a lambda using best-effort line matching. Used
+ # on non-CRuby implementations.
+ class LineLambdaFind < Find
+ # Find the node for the given lambda by matching on line.
+ #--
+ #: (Proc callable) -> Node?
+ def find(callable)
+ return unless (source_location = callable.source_location)
+ return unless (result = parse_file(source_location[0]))
+
+ start_line = source_location[1]
+
+ result.value.find do |node|
+ case node
+ when LambdaNode
+ node.location.start_line == start_line
+ when CallNode
+ node.block.is_a?(BlockNode) && node.location.start_line == start_line
+ else
+ false
+ end
+ end
+ end
+ end
+
+ # Finds the AST node for a non-lambda Proc using best-effort line
+ # matching. Used on non-CRuby implementations.
+ class LineProcFind < Find
+ # Find the node for the given proc by matching on line.
+ #--
+ #: (Proc callable) -> Node?
+ def find(callable)
+ return unless (source_location = callable.source_location)
+ return unless (result = parse_file(source_location[0]))
+
+ start_line = source_location[1]
+
+ result.value.find do |node|
+ case node
+ when ForNode
+ node.location.start_line == start_line
+ when CallNode
+ node.block.is_a?(BlockNode) && node.location.start_line == start_line
+ else
+ false
+ end
+ end
+ end
+ end
+
+ # Finds the AST node for a Thread::Backtrace::Location using best-effort
+ # line matching. Used on non-CRuby implementations.
+ class LineBacktraceLocationFind < Find
+ # Find the node for the given backtrace location by matching on line.
+ #--
+ #: (Thread::Backtrace::Location location) -> Node?
+ def find(location)
+ file = location.absolute_path || location.path
+ return unless (result = parse_file(file))
+
+ start_line = location.lineno
+ result.value.find { |node| node.location.start_line == start_line }
+ end
+ end
+ end
+end
diff --git a/lib/prism/parse_result.rb b/lib/prism/parse_result.rb
new file mode 100644
index 0000000000..93d3c006b7
--- /dev/null
+++ b/lib/prism/parse_result.rb
@@ -0,0 +1,1211 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+module Prism
+ # @rbs!
+ # # An internal interface for a cache that can be used to compute code
+ # # units from byte offsets.
+ # interface _CodeUnitsCache
+ # def []: (Integer byte_offset) -> Integer
+ # end
+
+ # This represents a source of Ruby code that has been parsed. It is used in
+ # conjunction with locations to allow them to resolve line numbers and source
+ # ranges.
+ class Source
+ # Create a new source object with the given source code. This method should
+ # be used instead of `new` and it will return either a `Source` or a
+ # specialized and more performant `ASCIISource` if no multibyte characters
+ # are present in the source code.
+ #
+ # Note that if you are calling this method manually, you will need to supply
+ # the start_line and offsets parameters. start_line is the line number that
+ # the source starts on, which is typically 1 but can be different if this
+ # source is a subset of a larger source or if this is an eval. offsets is an
+ # array of byte offsets for the start of each line in the source code, which
+ # can be calculated by iterating through the source code and recording the
+ # byte offset whenever a newline character is encountered. The first
+ # element is always 0 to mark the first line.
+ #--
+ #: (String source, Integer start_line, Array[Integer] offsets) -> Source
+ def self.for(source, start_line, offsets)
+ if source.ascii_only?
+ ASCIISource.new(source, start_line, offsets)
+ elsif source.encoding == Encoding::BINARY
+ source.force_encoding(Encoding::UTF_8)
+
+ if source.valid_encoding?
+ new(source, start_line, offsets)
+ else
+ # This is an extremely niche use case where the file is marked as
+ # binary, contains multi-byte characters, and those characters are not
+ # valid UTF-8. In this case we'll mark it as binary and fall back to
+ # treating everything as a single-byte character. This _may_ cause
+ # problems when asking for code units, but it appears to be the
+ # cleanest solution at the moment.
+ source.force_encoding(Encoding::BINARY)
+ ASCIISource.new(source, start_line, offsets)
+ end
+ else
+ new(source, start_line, offsets)
+ end
+ end
+
+ # The source code that this source object represents.
+ attr_reader :source #: String
+
+ # The line number where this source starts.
+ attr_reader :start_line #: Integer
+
+ # The list of newline byte offsets in the source code. When initialized from
+ # the C extension, this may be a packed binary string of uint32_t values
+ # that is lazily unpacked on first access.
+ #--
+ #: () -> Array[Integer]
+ def offsets
+ offsets = @offsets
+ return offsets if offsets.is_a?(Array)
+ @offsets = offsets.unpack("L*")
+ end
+
+ # Create a new source object with the given source code. The offsets
+ # parameter can be either an Array of Integer byte offsets or a packed
+ # binary string of uint32_t values (from the C extension).
+ #--
+ #: (String source, Integer start_line, Array[Integer] | String offsets) -> void
+ def initialize(source, start_line, offsets)
+ @source = source
+ @start_line = start_line
+ @offsets = offsets
+ end
+
+ # Replace the value of start_line with the given value.
+ #--
+ #: (Integer start_line) -> void
+ def replace_start_line(start_line)
+ @start_line = start_line
+ end
+
+ # Replace the value of offsets with the given value.
+ #--
+ #: (Array[Integer] offsets) -> void
+ def replace_offsets(offsets)
+ @offsets = offsets
+ end
+
+ # Returns the encoding of the source code, which is set by parameters to the
+ # parser or by the encoding magic comment.
+ #--
+ #: () -> Encoding
+ def encoding
+ source.encoding
+ end
+
+ # Returns the lines of the source code as an array of strings.
+ #--
+ #: () -> Array[String]
+ def lines
+ source.lines
+ end
+
+ # Perform a byteslice on the source code using the given byte offset and
+ # byte length.
+ #--
+ #: (Integer byte_offset, Integer length) -> String
+ def slice(byte_offset, length)
+ source.byteslice(byte_offset, length) or raise
+ end
+
+ # Converts the line number and column in bytes to a byte offset.
+ #--
+ #: (Integer line, Integer column) -> Integer
+ def byte_offset(line, column)
+ normal = line - @start_line
+ raise IndexError if normal < 0
+ offsets.fetch(normal) + column
+ rescue IndexError
+ raise ArgumentError, "line #{line} is out of range"
+ end
+
+ # Binary search through the offsets to find the line number for the given
+ # byte offset.
+ #--
+ #: (Integer byte_offset) -> Integer
+ def line(byte_offset)
+ start_line + find_line(byte_offset)
+ end
+
+ # Return the byte offset of the start of the line corresponding to the given
+ # byte offset.
+ #--
+ #: (Integer byte_offset) -> Integer
+ def line_start(byte_offset)
+ offsets[find_line(byte_offset)]
+ end
+
+ # Returns the byte offset of the end of the line corresponding to the given
+ # byte offset.
+ #--
+ #: (Integer byte_offset) -> Integer
+ def line_end(byte_offset)
+ offsets[find_line(byte_offset) + 1] || source.bytesize
+ end
+
+ # Return the column in bytes for the given byte offset.
+ #--
+ #: (Integer byte_offset) -> Integer
+ def column(byte_offset)
+ byte_offset - line_start(byte_offset)
+ end
+
+ # Return the character offset for the given byte offset.
+ #--
+ #: (Integer byte_offset) -> Integer
+ def character_offset(byte_offset)
+ (source.byteslice(0, byte_offset) or raise).length
+ end
+
+ # Return the column in characters for the given byte offset.
+ #--
+ #: (Integer byte_offset) -> Integer
+ def character_column(byte_offset)
+ character_offset(byte_offset) - character_offset(line_start(byte_offset))
+ end
+
+ # Returns the offset from the start of the file for the given byte offset
+ # counting in code units for the given encoding.
+ #
+ # This method is tested with UTF-8, UTF-16, and UTF-32. If there is the
+ # concept of code units that differs from the number of characters in other
+ # encodings, it is not captured here.
+ #
+ # We purposefully replace invalid and undefined characters with replacement
+ # characters in this conversion. This happens for two reasons. First, it's
+ # possible that the given byte offset will not occur on a character
+ # boundary. Second, it's possible that the source code will contain a
+ # character that has no equivalent in the given encoding.
+ #--
+ #: (Integer byte_offset, Encoding encoding) -> Integer
+ def code_units_offset(byte_offset, encoding)
+ return byte_offset if encoding == Encoding::UTF_8
+
+ byteslice = (source.byteslice(0, byte_offset) or raise).encode(encoding, invalid: :replace, undef: :replace)
+
+ if encoding == Encoding::UTF_16LE || encoding == Encoding::UTF_16BE
+ byteslice.bytesize / 2
+ else
+ byteslice.length
+ end
+ end
+
+ # Generate a cache that targets a specific encoding for calculating code
+ # unit offsets.
+ #--
+ #: (Encoding encoding) -> CodeUnitsCache
+ def code_units_cache(encoding)
+ CodeUnitsCache.new(source, encoding)
+ end
+
+ # Returns the column in code units for the given encoding for the
+ # given byte offset.
+ #--
+ #: (Integer byte_offset, Encoding encoding) -> Integer
+ def code_units_column(byte_offset, encoding)
+ code_units_offset(byte_offset, encoding) - code_units_offset(line_start(byte_offset), encoding)
+ end
+
+ # Freeze this object and the objects it contains.
+ #--
+ #: () -> void
+ def deep_freeze
+ source.freeze
+ offsets.freeze
+ freeze
+ end
+
+ # Binary search through the offsets to find the index for the given
+ # byte offset.
+ #--
+ #: (Integer byte_offset) -> Integer
+ def find_line(byte_offset) # :nodoc:
+ index = offsets.bsearch_index { |offset| offset > byte_offset } || offsets.length
+ index - 1
+ end
+ end
+
+ # A cache that can be used to quickly compute code unit offsets from byte
+ # offsets. It purposefully provides only a single #[] method to access the
+ # cache in order to minimize surface area.
+ #
+ # Note that there are some known issues here that may or may not be addressed
+ # in the future:
+ #
+ # * The first is that there are issues when the cache computes values that are
+ # not on character boundaries. This can result in subsequent computations
+ # being off by one or more code units.
+ # * The second is that this cache is currently unbounded. In theory we could
+ # introduce some kind of LRU cache to limit the number of entries, but this
+ # has not yet been implemented.
+ #
+ class CodeUnitsCache
+ # Counter used for UTF-8, where one code unit equals one byte.
+ class UTF8Counter # :nodoc:
+ #: (Integer byte_offset, Integer byte_length) -> Integer
+ def count(byte_offset, byte_length)
+ byte_length
+ end
+ end
+
+ class UTF16Counter # :nodoc:
+ # @rbs @source: String
+ # @rbs @encoding: Encoding
+
+ #: (String source, Encoding encoding) -> void
+ def initialize(source, encoding)
+ @source = source
+ @encoding = encoding
+ end
+
+ #: (Integer byte_offset, Integer byte_length) -> Integer
+ def count(byte_offset, byte_length)
+ (@source.byteslice(byte_offset, byte_length) or raise).encode(@encoding, invalid: :replace, undef: :replace).bytesize / 2
+ end
+ end
+
+ # Counter used for UTF-32, where one code unit equals one code point and
+ # matches String#length. Also used as a best-effort fallback for any other
+ # encoding that does not have a dedicated counter.
+ class UTF32Counter # :nodoc:
+ # @rbs @source: String
+ # @rbs @encoding: Encoding
+
+ #: (String source, Encoding encoding) -> void
+ def initialize(source, encoding)
+ @source = source
+ @encoding = encoding
+ end
+
+ #: (Integer byte_offset, Integer byte_length) -> Integer
+ def count(byte_offset, byte_length)
+ (@source.byteslice(byte_offset, byte_length) or raise).encode(@encoding, invalid: :replace, undef: :replace).length
+ end
+ end
+
+ private_constant :UTF8Counter, :UTF16Counter, :UTF32Counter
+
+ # @rbs @source: String
+ # @rbs @counter: UTF8Counter | UTF16Counter | UTF32Counter
+ # @rbs @cache: Hash[Integer, Integer]
+ # @rbs @offsets: Array[Integer]
+
+ # Initialize a new cache with the given source and encoding.
+ #--
+ #: (String source, Encoding encoding) -> void
+ def initialize(source, encoding)
+ @source = source
+ @counter =
+ case encoding
+ when Encoding::UTF_8
+ UTF8Counter.new
+ when Encoding::UTF_16LE, Encoding::UTF_16BE
+ UTF16Counter.new(source, encoding)
+ else
+ UTF32Counter.new(source, encoding)
+ end
+
+ @cache = {} #: Hash[Integer, Integer]
+ @offsets = [] #: Array[Integer]
+ end
+
+ # Retrieve the code units offset from the given byte offset.
+ #--
+ #: (Integer byte_offset) -> Integer
+ def [](byte_offset)
+ @cache[byte_offset] ||=
+ if (index = @offsets.bsearch_index { |offset| offset > byte_offset }).nil?
+ @offsets << byte_offset
+ @counter.count(0, byte_offset)
+ elsif index == 0
+ @offsets.unshift(byte_offset)
+ @counter.count(0, byte_offset)
+ else
+ @offsets.insert(index, byte_offset)
+ offset = @offsets[index - 1]
+ @cache[offset] + @counter.count(offset, byte_offset - offset)
+ end
+ end
+ end
+
+ # Specialized version of Prism::Source for source code that includes ASCII
+ # characters only. This class is used to apply performance optimizations that
+ # cannot be applied to sources that include multibyte characters.
+ #
+ # In the extremely rare case that a source includes multi-byte characters but
+ # is marked as binary because of a magic encoding comment and it cannot be
+ # eagerly converted to UTF-8, this class will be used as well. This is because
+ # at that point we will treat everything as single-byte characters.
+ class ASCIISource < Source
+ # Return the character offset for the given byte offset.
+ #--
+ #: (Integer byte_offset) -> Integer
+ def character_offset(byte_offset)
+ byte_offset
+ end
+
+ # Return the column in characters for the given byte offset.
+ #--
+ #: (Integer byte_offset) -> Integer
+ def character_column(byte_offset)
+ byte_offset - line_start(byte_offset)
+ end
+
+ # Returns the offset from the start of the file for the given byte offset
+ # counting in code units for the given encoding.
+ #
+ # This method is tested with UTF-8, UTF-16, and UTF-32. If there is the
+ # concept of code units that differs from the number of characters in other
+ # encodings, it is not captured here.
+ #--
+ #: (Integer byte_offset, Encoding encoding) -> Integer
+ def code_units_offset(byte_offset, encoding)
+ byte_offset
+ end
+
+ # Returns a cache that is the identity function in order to maintain the
+ # same interface. We can do this because code units are always equivalent to
+ # byte offsets for ASCII-only sources.
+ #--
+ #: (Encoding encoding) -> _CodeUnitsCache
+ def code_units_cache(encoding)
+ ->(byte_offset) { byte_offset }
+ end
+
+ # Specialized version of `code_units_column` that does not depend on
+ # `code_units_offset`, which is a more expensive operation. This is
+ # essentially the same as `Prism::Source#column`.
+ #--
+ #: (Integer byte_offset, Encoding encoding) -> Integer
+ def code_units_column(byte_offset, encoding)
+ byte_offset - line_start(byte_offset)
+ end
+ end
+
+ # This represents a location in the source.
+ class Location
+ # A Source object that is used to determine more information from the given
+ # offset and length.
+ attr_reader :source #: Source
+ protected :source
+
+ # The byte offset from the beginning of the source where this location
+ # starts.
+ attr_reader :start_offset #: Integer
+
+ # The length of this location in bytes.
+ attr_reader :length #: Integer
+
+ # @rbs @leading_comments: Array[Comment]?
+ # @rbs @trailing_comments: Array[Comment]?
+
+ # Create a new location object with the given source, start byte offset, and
+ # byte length.
+ #--
+ #: (Source source, Integer start_offset, Integer length) -> void
+ def initialize(source, start_offset, length)
+ @source = source
+ @start_offset = start_offset
+ @length = length
+
+ # These are used to store comments that are associated with this location.
+ # They are initialized to `nil` to save on memory when there are no
+ # comments to be attached and/or the comment-related APIs are not used.
+ @leading_comments = nil
+ @trailing_comments = nil
+ end
+
+ # These are the comments that are associated with this location that exist
+ # before the start of this location.
+ #--
+ #: () -> Array[Comment]
+ def leading_comments
+ @leading_comments ||= []
+ end
+
+ # Attach a comment to the leading comments of this location.
+ #--
+ #: (Comment comment) -> void
+ def leading_comment(comment)
+ leading_comments << comment
+ end
+
+ # These are the comments that are associated with this location that exist
+ # after the end of this location.
+ #--
+ #: () -> Array[Comment]
+ def trailing_comments
+ @trailing_comments ||= []
+ end
+
+ # Attach a comment to the trailing comments of this location.
+ #--
+ #: (Comment comment) -> void
+ def trailing_comment(comment)
+ trailing_comments << comment
+ end
+
+ # Returns all comments that are associated with this location (both leading
+ # and trailing comments).
+ #--
+ #: () -> Array[Comment]
+ def comments
+ [*@leading_comments, *@trailing_comments] #: Array[Comment]
+ end
+
+ # Create a new location object with the given options.
+ #--
+ #: (?source: Source, ?start_offset: Integer, ?length: Integer) -> Location
+ def copy(source: self.source, start_offset: self.start_offset, length: self.length)
+ Location.new(source, start_offset, length)
+ end
+
+ # Returns a new location that is the result of chopping off the last byte.
+ #--
+ #: () -> Location
+ def chop
+ copy(length: length == 0 ? length : length - 1)
+ end
+
+ # Returns a string representation of this location.
+ #--
+ #: () -> String
+ def inspect # :nodoc:
+ "#<Prism::Location @start_offset=#{@start_offset} @length=#{@length} start_line=#{start_line}>"
+ end
+
+ # Returns all of the lines of the source code associated with this location.
+ #--
+ #: () -> Array[String]
+ def source_lines
+ source.lines
+ end
+
+ # The source code that this location represents.
+ #--
+ #: () -> String
+ def slice
+ source.slice(start_offset, length)
+ end
+
+ # The source code that this location represents starting from the beginning
+ # of the line that this location starts on to the end of the line that this
+ # location ends on.
+ #--
+ #: () -> String
+ def slice_lines
+ line_start = source.line_start(start_offset)
+ line_end = source.line_end(end_offset)
+ source.slice(line_start, line_end - line_start)
+ end
+
+ # The character offset from the beginning of the source where this location
+ # starts.
+ #--
+ #: () -> Integer
+ def start_character_offset
+ source.character_offset(start_offset)
+ end
+
+ # The offset from the start of the file in code units of the given encoding.
+ #--
+ #: (Encoding encoding) -> Integer
+ def start_code_units_offset(encoding = Encoding::UTF_16LE)
+ source.code_units_offset(start_offset, encoding)
+ end
+
+ # The start offset from the start of the file in code units using the given
+ # cache to fetch or calculate the value.
+ #--
+ #: (_CodeUnitsCache cache) -> Integer
+ def cached_start_code_units_offset(cache)
+ cache[start_offset]
+ end
+
+ # The byte offset from the beginning of the source where this location ends.
+ #--
+ #: () -> Integer
+ def end_offset
+ start_offset + length
+ end
+
+ # The character offset from the beginning of the source where this location
+ # ends.
+ #--
+ #: () -> Integer
+ def end_character_offset
+ source.character_offset(end_offset)
+ end
+
+ # The offset from the start of the file in code units of the given encoding.
+ #--
+ #: (Encoding encoding) -> Integer
+ def end_code_units_offset(encoding = Encoding::UTF_16LE)
+ source.code_units_offset(end_offset, encoding)
+ end
+
+ # The end offset from the start of the file in code units using the given
+ # cache to fetch or calculate the value.
+ #--
+ #: (_CodeUnitsCache cache) -> Integer
+ def cached_end_code_units_offset(cache)
+ cache[end_offset]
+ end
+
+ # The line number where this location starts.
+ #--
+ #: () -> Integer
+ def start_line
+ source.line(start_offset)
+ end
+
+ # The content of the line where this location starts before this location.
+ #--
+ #: () -> String
+ def start_line_slice
+ offset = source.line_start(start_offset)
+ source.slice(offset, start_offset - offset)
+ end
+
+ # The line number where this location ends.
+ #--
+ #: () -> Integer
+ def end_line
+ source.line(end_offset)
+ end
+
+ # The column in bytes where this location starts from the start of
+ # the line.
+ #--
+ #: () -> Integer
+ def start_column
+ source.column(start_offset)
+ end
+
+ # The column in characters where this location ends from the start of
+ # the line.
+ #--
+ #: () -> Integer
+ def start_character_column
+ source.character_column(start_offset)
+ end
+
+ # The column in code units of the given encoding where this location
+ # starts from the start of the line.
+ #--
+ #: (?Encoding encoding) -> Integer
+ def start_code_units_column(encoding = Encoding::UTF_16LE)
+ source.code_units_column(start_offset, encoding)
+ end
+
+ # The start column in code units using the given cache to fetch or calculate
+ # the value.
+ #--
+ #: (_CodeUnitsCache cache) -> Integer
+ def cached_start_code_units_column(cache)
+ cache[start_offset] - cache[source.line_start(start_offset)]
+ end
+
+ # The column in bytes where this location ends from the start of the
+ # line.
+ #--
+ #: () -> Integer
+ def end_column
+ source.column(end_offset)
+ end
+
+ # The column in characters where this location ends from the start of
+ # the line.
+ #--
+ #: () -> Integer
+ def end_character_column
+ source.character_column(end_offset)
+ end
+
+ # The column in code units of the given encoding where this location
+ # ends from the start of the line.
+ #--
+ #: (?Encoding encoding) -> Integer
+ def end_code_units_column(encoding = Encoding::UTF_16LE)
+ source.code_units_column(end_offset, encoding)
+ end
+
+ # The end column in code units using the given cache to fetch or calculate
+ # the value.
+ #--
+ #: (_CodeUnitsCache cache) -> Integer
+ def cached_end_code_units_column(cache)
+ cache[end_offset] - cache[source.line_start(end_offset)]
+ end
+
+ # Implement the hash pattern matching interface for Location.
+ #--
+ #: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
+ def deconstruct_keys(keys) # :nodoc:
+ { start_offset: start_offset, end_offset: end_offset }
+ end
+
+ # Implement the pretty print interface for Location.
+ #--
+ #: (PP q) -> void
+ def pretty_print(q) # :nodoc:
+ q.text("(#{start_line},#{start_column})-(#{end_line},#{end_column})")
+ end
+
+ # Returns true if the given other location is equal to this location.
+ #--
+ #: (untyped other) -> bool
+ def ==(other)
+ Location === other &&
+ other.start_offset == start_offset &&
+ other.end_offset == end_offset
+ end
+
+ # Returns a new location that stretches from this location to the given
+ # other location. Raises an error if this location is not before the other
+ # location or if they don't share the same source.
+ #--
+ #: (Location other) -> Location
+ def join(other)
+ raise "Incompatible sources" if source != other.source
+ raise "Incompatible locations" if start_offset > other.start_offset
+
+ Location.new(source, start_offset, other.end_offset - start_offset)
+ end
+
+ # Join this location with the first occurrence of the string in the source
+ # that occurs after this location on the same line, and return the new
+ # location. This will raise an error if the string does not exist.
+ #--
+ #: (String string) -> Location
+ def adjoin(string)
+ line_suffix = source.slice(end_offset, source.line_end(end_offset) - end_offset)
+
+ line_suffix_index = line_suffix.byteindex(string)
+ raise "Could not find #{string}" if line_suffix_index.nil?
+
+ Location.new(source, start_offset, length + line_suffix_index + string.bytesize)
+ end
+ end
+
+ # This represents a comment that was encountered during parsing. It is the
+ # base class for all comment types.
+ class Comment
+ # The Location of this comment in the source.
+ attr_reader :location #: Location
+
+ # Create a new comment object with the given location.
+ #--
+ #: (Location location) -> void
+ def initialize(location)
+ @location = location
+ end
+
+ # Implement the hash pattern matching interface for Comment.
+ #--
+ #: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
+ def deconstruct_keys(keys) # :nodoc:
+ { location: location }
+ end
+
+ # Returns the content of the comment by slicing it from the source code.
+ #--
+ #: () -> String
+ def slice
+ location.slice
+ end
+
+ # Returns true if this comment happens on the same line as other code and
+ # false if the comment is by itself. This can only be true for inline
+ # comments and should be false for block comments.
+ #--
+ #: () -> bool
+ def trailing?
+ raise NotImplementedError, "trailing? is not implemented for #{self.class}"
+ end
+ end
+
+ # InlineComment objects are the most common. They correspond to comments in
+ # the source file like this one that start with #.
+ class InlineComment < Comment
+ # Returns true if this comment happens on the same line as other code and
+ # false if the comment is by itself.
+ #--
+ #: () -> bool
+ def trailing?
+ !location.start_line_slice.strip.empty?
+ end
+
+ # Returns a string representation of this comment.
+ #--
+ #: () -> String
+ def inspect # :nodoc:
+ "#<Prism::InlineComment @location=#{location.inspect}>"
+ end
+ end
+
+ # EmbDocComment objects correspond to comments that are surrounded by =begin
+ # and =end.
+ class EmbDocComment < Comment
+ # Returns false. This can only be true for inline comments.
+ #--
+ #: () -> bool
+ def trailing?
+ false
+ end
+
+ # Returns a string representation of this comment.
+ #--
+ #: () -> String
+ def inspect # :nodoc:
+ "#<Prism::EmbDocComment @location=#{location.inspect}>"
+ end
+ end
+
+ # This represents a magic comment that was encountered during parsing.
+ class MagicComment
+ # A Location object representing the location of the key in the source.
+ attr_reader :key_loc #: Location
+
+ # A Location object representing the location of the value in the source.
+ attr_reader :value_loc #: Location
+
+ # Create a new magic comment object with the given key and value locations.
+ #--
+ #: (Location key_loc, Location value_loc) -> void
+ def initialize(key_loc, value_loc)
+ @key_loc = key_loc
+ @value_loc = value_loc
+ end
+
+ # Returns the key of the magic comment by slicing it from the source code.
+ #--
+ #: () -> String
+ def key
+ key_loc.slice
+ end
+
+ # Returns the value of the magic comment by slicing it from the source code.
+ #--
+ #: () -> String
+ def value
+ value_loc.slice
+ end
+
+ # Implement the hash pattern matching interface for MagicComment.
+ #--
+ #: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
+ def deconstruct_keys(keys) # :nodoc:
+ { key_loc: key_loc, value_loc: value_loc }
+ end
+
+ # Returns a string representation of this magic comment.
+ #--
+ #: () -> String
+ def inspect # :nodoc:
+ "#<Prism::MagicComment @key=#{key.inspect} @value=#{value.inspect}>"
+ end
+ end
+
+ # This represents an error that was encountered during parsing.
+ class ParseError
+ # The type of error. This is an _internal_ symbol that is used for
+ # communicating with translation layers. It is not meant to be public API.
+ attr_reader :type #: Symbol
+
+ # The message associated with this error.
+ attr_reader :message #: String
+
+ # A Location object representing the location of this error in the source.
+ attr_reader :location #: Location
+
+ # The level of this error.
+ attr_reader :level #: Symbol
+
+ # Create a new error object with the given message and location.
+ #--
+ #: (Symbol type, String message, Location location, Symbol level) -> void
+ def initialize(type, message, location, level)
+ @type = type
+ @message = message
+ @location = location
+ @level = level
+ end
+
+ # Implement the hash pattern matching interface for ParseError.
+ #--
+ #: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
+ def deconstruct_keys(keys) # :nodoc:
+ { type: type, message: message, location: location, level: level }
+ end
+
+ # Returns a string representation of this error.
+ #--
+ #: () -> String
+ def inspect # :nodoc:
+ "#<Prism::ParseError @type=#{@type.inspect} @message=#{@message.inspect} @location=#{@location.inspect} @level=#{@level.inspect}>"
+ end
+ end
+
+ # This represents a warning that was encountered during parsing.
+ class ParseWarning
+ # The type of warning. This is an _internal_ symbol that is used for
+ # communicating with translation layers. It is not meant to be public API.
+ attr_reader :type #: Symbol
+
+ # The message associated with this warning.
+ attr_reader :message #: String
+
+ # A Location object representing the location of this warning in the source.
+ attr_reader :location #: Location
+
+ # The level of this warning.
+ attr_reader :level #: Symbol
+
+ # Create a new warning object with the given message and location.
+ #--
+ #: (Symbol type, String message, Location location, Symbol level) -> void
+ def initialize(type, message, location, level)
+ @type = type
+ @message = message
+ @location = location
+ @level = level
+ end
+
+ # Implement the hash pattern matching interface for ParseWarning.
+ #--
+ #: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
+ def deconstruct_keys(keys) # :nodoc:
+ { type: type, message: message, location: location, level: level }
+ end
+
+ # Returns a string representation of this warning.
+ #--
+ #: () -> String
+ def inspect # :nodoc:
+ "#<Prism::ParseWarning @type=#{@type.inspect} @message=#{@message.inspect} @location=#{@location.inspect} @level=#{@level.inspect}>"
+ end
+ end
+
+ # This represents the result of a call to Prism.parse or Prism.parse_file.
+ # It contains the requested structure, any comments that were encounters,
+ # and any errors that were encountered.
+ class Result
+ # The list of comments that were encountered during parsing.
+ attr_reader :comments #: Array[Comment]
+
+ # The list of magic comments that were encountered during parsing.
+ attr_reader :magic_comments #: Array[MagicComment]
+
+ # An optional location that represents the location of the __END__ marker
+ # and the rest of the content of the file. This content is loaded into the
+ # DATA constant when the file being parsed is the main file being executed.
+ attr_reader :data_loc #: Location?
+
+ # The list of errors that were generated during parsing.
+ attr_reader :errors #: Array[ParseError]
+
+ # The list of warnings that were generated during parsing.
+ attr_reader :warnings #: Array[ParseWarning]
+
+ # A Source instance that represents the source code that was parsed.
+ attr_reader :source #: Source
+
+ # Create a new result object with the given values.
+ #--
+ #: (Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, bool continuable, Source source) -> void
+ def initialize(comments, magic_comments, data_loc, errors, warnings, continuable, source)
+ @comments = comments
+ @magic_comments = magic_comments
+ @data_loc = data_loc
+ @errors = errors
+ @warnings = warnings
+ @continuable = continuable
+ @source = source
+ end
+
+ # Implement the hash pattern matching interface for Result.
+ #--
+ #: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
+ def deconstruct_keys(keys) # :nodoc:
+ { comments: comments, magic_comments: magic_comments, data_loc: data_loc, errors: errors, warnings: warnings }
+ end
+
+ # Returns the encoding of the source code that was parsed.
+ #--
+ #: () -> Encoding
+ def encoding
+ source.encoding
+ end
+
+ # Returns true if there were no errors during parsing and false if there
+ # were.
+ #--
+ #: () -> bool
+ def success?
+ errors.empty?
+ end
+
+ # Returns true if there were errors during parsing and false if there were
+ # not.
+ #--
+ #: () -> bool
+ def failure?
+ !success?
+ end
+
+ # Returns true if the parsed source is an incomplete expression that could
+ # become valid with additional input. This is useful for REPL contexts (such
+ # as IRB) where the user may be entering a multi-line expression one line at
+ # a time and the implementation needs to determine whether to wait for more
+ # input or to evaluate what has been entered so far.
+ #
+ # Concretely, this returns true when every error present is caused by the
+ # parser reaching the end of the input before a construct was closed (e.g.
+ # an unclosed string, array, block, or keyword), and returns false when any
+ # error is caused by a token that makes the input structurally invalid
+ # regardless of what might follow (e.g. a stray `end`, `]`, or `)` with no
+ # matching opener).
+ #
+ # Examples:
+ #
+ # Prism.parse("1 + [").continuable? #=> true (unclosed array)
+ # Prism.parse("1 + ]").continuable? #=> false (stray ])
+ # Prism.parse("tap do").continuable? #=> true (unclosed block)
+ # Prism.parse("end.tap do").continuable? #=> false (stray end)
+ #
+ #--
+ #: () -> bool
+ def continuable?
+ @continuable
+ end
+
+ # Create a code units cache for the given encoding.
+ #--
+ #: (Encoding encoding) -> _CodeUnitsCache
+ def code_units_cache(encoding)
+ source.code_units_cache(encoding)
+ end
+ end
+
+ # This is a result specific to the `parse` and `parse_file` methods.
+ class ParseResult < Result
+ autoload :Comments, "prism/parse_result/comments"
+ autoload :Errors, "prism/parse_result/errors"
+ autoload :Newlines, "prism/parse_result/newlines"
+
+ private_constant :Comments
+ private_constant :Errors
+ private_constant :Newlines
+
+ # The syntax tree that was parsed from the source code.
+ attr_reader :value #: ProgramNode
+
+ # Create a new parse result object with the given values.
+ #--
+ #: (ProgramNode value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, bool continuable, Source source) -> void
+ def initialize(value, comments, magic_comments, data_loc, errors, warnings, continuable, source)
+ @value = value
+ super(comments, magic_comments, data_loc, errors, warnings, continuable, source)
+ end
+
+ # Implement the hash pattern matching interface for ParseResult.
+ #--
+ #: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
+ def deconstruct_keys(keys) # :nodoc:
+ super.merge!(value: value)
+ end
+
+ # Attach the list of comments to their respective locations in the tree.
+ #--
+ #: () -> void
+ def attach_comments!
+ Comments.new(self).attach! # steep:ignore
+ end
+
+ # Walk the tree and mark nodes that are on a new line, loosely emulating
+ # the behavior of CRuby's `:line` tracepoint event.
+ #--
+ #: () -> void
+ def mark_newlines!
+ value.accept(Newlines.new(source.offsets.size)) # steep:ignore
+ end
+
+ # Returns a string representation of the syntax tree with the errors
+ # displayed inline.
+ #--
+ #: () -> String
+ def errors_format
+ Errors.new(self).format
+ end
+ end
+
+ # This is a result specific to the `lex` and `lex_file` methods.
+ class LexResult < Result
+ # The list of tokens that were parsed from the source code.
+ attr_reader :value #: Array[[Token, Integer]]
+
+ # Create a new lex result object with the given values.
+ #--
+ #: (Array[[Token, Integer]] value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, bool continuable, Source source) -> void
+ def initialize(value, comments, magic_comments, data_loc, errors, warnings, continuable, source)
+ @value = value
+ super(comments, magic_comments, data_loc, errors, warnings, continuable, source)
+ end
+
+ # Implement the hash pattern matching interface for LexResult.
+ #--
+ #: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
+ def deconstruct_keys(keys) # :nodoc:
+ super.merge!(value: value)
+ end
+ end
+
+ # This is a result specific to the `parse_lex` and `parse_lex_file` methods.
+ class ParseLexResult < Result
+ # A tuple of the syntax tree and the list of tokens that were parsed from
+ # the source code.
+ attr_reader :value #: [ProgramNode, Array[[Token, Integer]]]
+
+ # Create a new parse lex result object with the given values.
+ #--
+ #: ([ProgramNode, Array[[Token, Integer]]] value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, bool continuable, Source source) -> void
+ def initialize(value, comments, magic_comments, data_loc, errors, warnings, continuable, source)
+ @value = value
+ super(comments, magic_comments, data_loc, errors, warnings, continuable, source)
+ end
+
+ # Implement the hash pattern matching interface for ParseLexResult.
+ #--
+ #: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
+ def deconstruct_keys(keys) # :nodoc:
+ super.merge!(value: value)
+ end
+ end
+
+ # This represents a token from the Ruby source.
+ class Token
+ # The Source object that represents the source this token came from.
+ attr_reader :source #: Source
+ private :source
+
+ # The type of token that this token is.
+ attr_reader :type #: Symbol
+
+ # A byteslice of the source that this token represents.
+ attr_reader :value #: String
+
+ # @rbs @location: Location | Integer
+
+ # Create a new token object with the given type, value, and location.
+ #--
+ #: (Source source, Symbol type, String value, Location | Integer location) -> void
+ def initialize(source, type, value, location)
+ @source = source
+ @type = type
+ @value = value
+ @location = location
+ end
+
+ # Implement the hash pattern matching interface for Token.
+ #--
+ #: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
+ def deconstruct_keys(keys) # :nodoc:
+ { type: type, value: value, location: location }
+ end
+
+ # A Location object representing the location of this token in the source.
+ #--
+ #: () -> Location
+ def location
+ location = @location
+ return location if location.is_a?(Location)
+ @location = Location.new(source, location >> 32, location & 0xFFFFFFFF)
+ end
+
+ # Implement the pretty print interface for Token.
+ #--
+ #: (PP q) -> void
+ def pretty_print(q) # :nodoc:
+ q.group do
+ q.text(type.to_s)
+ self.location.pretty_print(q)
+ q.text("(")
+ q.nest(2) do
+ q.breakable("")
+ q.pp(value)
+ end
+ q.breakable("")
+ q.text(")")
+ end
+ end
+
+ # Returns true if the given other token is equal to this token.
+ #--
+ #: (untyped other) -> bool
+ def ==(other)
+ Token === other &&
+ other.type == type &&
+ other.value == value
+ end
+
+ # Returns a string representation of this token.
+ #--
+ #: () -> String
+ def inspect # :nodoc:
+ location
+ super
+ end
+
+ # Freeze this object and the objects it contains.
+ #--
+ #: () -> void
+ def deep_freeze
+ value.freeze
+ location.freeze
+ freeze
+ end
+ end
+
+ # This object is passed to the various Prism.* methods that accept the
+ # `scopes` option as an element of the list. It defines both the local
+ # variables visible at that scope as well as the forwarding parameters
+ # available at that scope.
+ class Scope
+ # The list of local variables that are defined in this scope. This should be
+ # defined as an array of symbols.
+ attr_reader :locals #: Array[Symbol]
+
+ # The list of local variables that are forwarded to the next scope. This
+ # should by defined as an array of symbols containing the specific values of
+ # :*, :**, :&, or :"...".
+ attr_reader :forwarding #: Array[Symbol]
+
+ # Create a new scope object with the given locals and forwarding.
+ #--
+ #: (Array[Symbol] locals, Array[Symbol] forwarding) -> void
+ def initialize(locals, forwarding)
+ @locals = locals
+ @forwarding = forwarding
+ end
+ end
+
+ # Create a new scope with the given locals and forwarding options that is
+ # suitable for passing into one of the Prism.* methods that accepts the
+ # `scopes` option.
+ #--
+ #: (?locals: Array[Symbol], ?forwarding: Array[Symbol]) -> Scope
+ def self.scope(locals: [], forwarding: [])
+ Scope.new(locals, forwarding)
+ end
+end
diff --git a/lib/prism/parse_result/comments.rb b/lib/prism/parse_result/comments.rb
new file mode 100644
index 0000000000..df80792d39
--- /dev/null
+++ b/lib/prism/parse_result/comments.rb
@@ -0,0 +1,219 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+module Prism
+ class ParseResult < Result
+ # When we've parsed the source, we have both the syntax tree and the list of
+ # comments that we found in the source. This class is responsible for
+ # walking the tree and finding the nearest location to attach each comment.
+ #
+ # It does this by first finding the nearest locations to each comment.
+ # Locations can either come from nodes directly or from location fields on
+ # nodes. For example, a `ClassNode` has an overall location encompassing the
+ # entire class, but it also has a location for the `class` keyword.
+ #
+ # Once the nearest locations are found, it determines which one to attach
+ # to. If it's a trailing comment (a comment on the same line as other source
+ # code), it will favor attaching to the nearest location that occurs before
+ # the comment. Otherwise it will favor attaching to the nearest location
+ # that is after the comment.
+ class Comments
+ # @rbs!
+ # # An internal interface for a target that comments can be attached
+ # # to. This is either going to be a NodeTarget or a CommentTarget.
+ # interface _CommentTarget
+ # def start_offset: () -> Integer
+ # def end_offset: () -> Integer
+ # def encloses?: (Comment) -> bool
+ # def leading_comment: (Comment) -> void
+ # def trailing_comment: (Comment) -> void
+ # end
+
+ # A target for attaching comments that is based on a specific node's
+ # location.
+ class NodeTarget # :nodoc:
+ attr_reader :node #: node
+
+ #: (node node) -> void
+ def initialize(node)
+ @node = node
+ end
+
+ #: () -> Integer
+ def start_offset
+ node.start_offset
+ end
+
+ #: () -> Integer
+ def end_offset
+ node.end_offset
+ end
+
+ #: (Comment comment) -> bool
+ def encloses?(comment)
+ start_offset <= comment.location.start_offset &&
+ comment.location.end_offset <= end_offset
+ end
+
+ #: (Comment comment) -> void
+ def leading_comment(comment)
+ node.location.leading_comment(comment)
+ end
+
+ #: (Comment comment) -> void
+ def trailing_comment(comment)
+ node.location.trailing_comment(comment)
+ end
+ end
+
+ # A target for attaching comments that is based on a location field on a
+ # node. For example, the `end` token of a ClassNode.
+ class LocationTarget # :nodoc:
+ attr_reader :location #: Location
+
+ #: (Location location) -> void
+ def initialize(location)
+ @location = location
+ end
+
+ #: () -> Integer
+ def start_offset
+ location.start_offset
+ end
+
+ #: () -> Integer
+ def end_offset
+ location.end_offset
+ end
+
+ #: (Comment comment) -> bool
+ def encloses?(comment)
+ false
+ end
+
+ #: (Comment comment) -> void
+ def leading_comment(comment)
+ location.leading_comment(comment)
+ end
+
+ #: (Comment comment) -> void
+ def trailing_comment(comment)
+ location.trailing_comment(comment)
+ end
+ end
+
+ # The parse result that we are attaching comments to.
+ attr_reader :parse_result #: ParseResult
+
+ # Create a new Comments object that will attach comments to the given
+ # parse result.
+ #--
+ #: (ParseResult parse_result) -> void
+ def initialize(parse_result)
+ @parse_result = parse_result
+ end
+
+ # Attach the comments to their respective locations in the tree by
+ # mutating the parse result.
+ #--
+ #: () -> void
+ def attach!
+ parse_result.comments.each do |comment|
+ preceding, enclosing, following = nearest_targets(parse_result.value, comment)
+
+ if comment.trailing?
+ if preceding
+ preceding.trailing_comment(comment)
+ else
+ (following || enclosing || NodeTarget.new(parse_result.value)).leading_comment(comment)
+ end
+ else
+ # If a comment exists on its own line, prefer a leading comment.
+ if following
+ following.leading_comment(comment)
+ elsif preceding
+ preceding.trailing_comment(comment)
+ else
+ (enclosing || NodeTarget.new(parse_result.value)).leading_comment(comment)
+ end
+ end
+ end
+ end
+
+ private
+
+ # Responsible for finding the nearest targets to the given comment within
+ # the context of the given encapsulating node.
+ #--
+ #: (node node, Comment comment) -> [_CommentTarget?, _CommentTarget?, _CommentTarget?]
+ def nearest_targets(node, comment)
+ comment_start = comment.location.start_offset
+ comment_end = comment.location.end_offset
+
+ targets = [] #: Array[_CommentTarget]
+ node.comment_targets.map do |value|
+ case value
+ when StatementsNode
+ targets.concat(value.body.map { |node| NodeTarget.new(node) })
+ when Node
+ targets << NodeTarget.new(value)
+ when Location
+ targets << LocationTarget.new(value)
+ end
+ end
+
+ targets.sort_by!(&:start_offset)
+ preceding = nil #: _CommentTarget?
+ following = nil #: _CommentTarget?
+
+ left = 0
+ right = targets.length
+
+ # This is a custom binary search that finds the nearest nodes to the
+ # given comment. When it finds a node that completely encapsulates the
+ # comment, it recurses downward into the tree.
+ while left < right
+ middle = (left + right) / 2
+ target = targets[middle]
+
+ target_start = target.start_offset
+ target_end = target.end_offset
+
+ if target.encloses?(comment)
+ # @type var target: NodeTarget
+ # The comment is completely contained by this target. Abandon the
+ # binary search at this level.
+ return nearest_targets(target.node, comment)
+ end
+
+ if target_end <= comment_start
+ # This target falls completely before the comment. Because we will
+ # never consider this target or any targets before it again, this
+ # target must be the closest preceding target we have encountered so
+ # far.
+ preceding = target
+ left = middle + 1
+ next
+ end
+
+ if comment_end <= target_start
+ # This target falls completely after the comment. Because we will
+ # never consider this target or any targets after it again, this
+ # target must be the closest following target we have encountered so
+ # far.
+ following = target
+ right = middle
+ next
+ end
+
+ # This should only happen if there is a bug in this parser.
+ raise "Comment location overlaps with a target location"
+ end
+
+ [preceding, NodeTarget.new(node), following]
+ end
+ end
+ end
+end
diff --git a/lib/prism/parse_result/errors.rb b/lib/prism/parse_result/errors.rb
new file mode 100644
index 0000000000..388309d23d
--- /dev/null
+++ b/lib/prism/parse_result/errors.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+require "stringio"
+
+module Prism
+ class ParseResult < Result
+ # An object to represent the set of errors on a parse result. This object
+ # can be used to format the errors in a human-readable way.
+ class Errors
+ # The parse result that contains the errors.
+ attr_reader :parse_result #: ParseResult
+
+ # Initialize a new set of errors from the given parse result.
+ #--
+ #: (ParseResult parse_result) -> void
+ def initialize(parse_result)
+ @parse_result = parse_result
+ end
+
+ # Formats the errors in a human-readable way and return them as a string.
+ #--
+ #: () -> String
+ def format
+ error_lines = {} #: Hash[Integer, Array[ParseError]]
+ parse_result.errors.each do |error|
+ location = error.location
+ (location.start_line..location.end_line).each do |line|
+ error_lines[line] ||= []
+ error_lines[line] << error
+ end
+ end
+
+ source_lines = parse_result.source.source.lines
+ source_lines << "" if error_lines.key?(source_lines.size + 1)
+
+ io = StringIO.new
+ source_lines.each.with_index(1) do |line, line_number|
+ io.puts(line)
+
+ (error_lines.delete(line_number) || []).each do |error|
+ location = error.location
+
+ case line_number
+ when location.start_line
+ io.print(" " * location.start_column + "^")
+
+ if location.start_line == location.end_line
+ if location.start_column != location.end_column
+ io.print("~" * (location.end_column - location.start_column - 1))
+ end
+
+ io.puts(" " + error.message)
+ else
+ io.puts("~" * (line.bytesize - location.start_column))
+ end
+ when location.end_line
+ io.puts("~" * location.end_column + " " + error.message)
+ else
+ io.puts("~" * line.bytesize)
+ end
+ end
+ end
+
+ io.puts
+ io.string
+ end
+ end
+ end
+end
diff --git a/lib/prism/parse_result/newlines.rb b/lib/prism/parse_result/newlines.rb
new file mode 100644
index 0000000000..450c790226
--- /dev/null
+++ b/lib/prism/parse_result/newlines.rb
@@ -0,0 +1,204 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+module Prism
+ class ParseResult < Result
+ # The :line tracepoint event gets fired whenever the Ruby VM encounters an
+ # expression on a new line. The types of expressions that can trigger this
+ # event are:
+ #
+ # * if statements
+ # * unless statements
+ # * nodes that are children of statements lists
+ #
+ # In order to keep track of the newlines, we have a list of offsets that
+ # come back from the parser. We assign these offsets to the first nodes that
+ # we find in the tree that are on those lines.
+ #
+ # Note that the logic in this file should be kept in sync with the Java
+ # MarkNewlinesVisitor, since that visitor is responsible for marking the
+ # newlines for JRuby/TruffleRuby.
+ #
+ # This file is autoloaded only when `mark_newlines!` is called, so the
+ # re-opening of the various nodes in this file will only be performed in
+ # that case. We do that to avoid storing the extra `@newline` instance
+ # variable on every node if we don't need it.
+ class Newlines < Visitor
+ # The map of lines indices to whether or not they have been marked as
+ # emitting a newline event.
+ # @rbs @lines: Array[bool]
+
+ # Create a new Newlines visitor with the given newline offsets.
+ #--
+ #: (Integer lines) -> void
+ def initialize(lines)
+ @lines = Array.new(1 + lines, false)
+ end
+
+ # Permit block nodes to mark newlines within themselves.
+ #--
+ #: (BlockNode node) -> void
+ def visit_block_node(node)
+ old_lines = @lines
+ @lines = Array.new(old_lines.size, false)
+
+ begin
+ super(node)
+ ensure
+ @lines = old_lines
+ end
+ end
+
+ # Permit lambda nodes to mark newlines within themselves.
+ #--
+ #: (LambdaNode node) -> void
+ def visit_lambda_node(node)
+ old_lines = @lines
+ @lines = Array.new(old_lines.size, false)
+
+ begin
+ super(node)
+ ensure
+ @lines = old_lines
+ end
+ end
+
+ # Mark if nodes as newlines.
+ #--
+ #: (IfNode node) -> void
+ def visit_if_node(node)
+ node.newline_flag!(@lines)
+ super(node)
+ end
+
+ # Mark unless nodes as newlines.
+ #--
+ #: (UnlessNode node) -> void
+ def visit_unless_node(node)
+ node.newline_flag!(@lines)
+ super(node)
+ end
+
+ # Permit statements lists to mark newlines within themselves.
+ #--
+ #: (StatementsNode node) -> void
+ def visit_statements_node(node)
+ node.body.each do |child|
+ child.newline_flag!(@lines)
+ end
+ super(node)
+ end
+ end
+ end
+
+ class Node
+ # Tracks whether or not this node should emit a newline event when the
+ # instructions that it represents are executed.
+ # @rbs @newline_flag: bool
+
+ #: () -> bool
+ def newline_flag? # :nodoc:
+ !!defined?(@newline_flag)
+ end
+
+ #: (Array[bool] lines) -> void
+ def newline_flag!(lines) # :nodoc:
+ line = location.start_line
+ unless lines[line]
+ lines[line] = true
+ @newline_flag = true
+ end
+ end
+ end
+
+ class BeginNode < Node
+ #: (Array[bool] lines) -> void
+ def newline_flag!(lines) # :nodoc:
+ # Never mark BeginNode with a newline flag, mark children instead.
+ end
+ end
+
+ class ParenthesesNode < Node
+ #: (Array[bool] lines) -> void
+ def newline_flag!(lines) # :nodoc:
+ # Never mark ParenthesesNode with a newline flag, mark children instead.
+ end
+ end
+
+ class IfNode < Node
+ #: (Array[bool] lines) -> void
+ def newline_flag!(lines) # :nodoc:
+ predicate.newline_flag!(lines)
+ end
+ end
+
+ class UnlessNode < Node
+ #: (Array[bool] lines) -> void
+ def newline_flag!(lines) # :nodoc:
+ predicate.newline_flag!(lines)
+ end
+ end
+
+ class UntilNode < Node
+ #: (Array[bool] lines) -> void
+ def newline_flag!(lines) # :nodoc:
+ predicate.newline_flag!(lines)
+ end
+ end
+
+ class WhileNode < Node
+ #: (Array[bool] lines) -> void
+ def newline_flag!(lines) # :nodoc:
+ predicate.newline_flag!(lines)
+ end
+ end
+
+ class RescueModifierNode < Node
+ #: (Array[bool] lines) -> void
+ def newline_flag!(lines) # :nodoc:
+ expression.newline_flag!(lines)
+ end
+ end
+
+ class InterpolatedMatchLastLineNode < Node
+ #: (Array[bool] lines) -> void
+ def newline_flag!(lines) # :nodoc:
+ first = parts.first
+ first.newline_flag!(lines) if first
+ end
+ end
+
+ class InterpolatedRegularExpressionNode < Node
+ #: (Array[bool] lines) -> void
+ def newline_flag!(lines) # :nodoc:
+ first = parts.first
+ first.newline_flag!(lines) if first
+ end
+ end
+
+ class InterpolatedStringNode < Node
+ #: (Array[bool] lines) -> void
+ def newline_flag!(lines) # :nodoc:
+ first = parts.first
+ first.newline_flag!(lines) if first
+ end
+ end
+
+ class InterpolatedSymbolNode < Node
+ #: (Array[bool] lines) -> void
+ def newline_flag!(lines) # :nodoc:
+ first = parts.first
+ first.newline_flag!(lines) if first
+ end
+ end
+
+ class InterpolatedXStringNode < Node
+ #: (Array[bool] lines) -> void
+ def newline_flag!(lines) # :nodoc:
+ first = parts.first
+ first.newline_flag!(lines) if first
+ end
+ end
+end
diff --git a/lib/prism/pattern.rb b/lib/prism/pattern.rb
new file mode 100644
index 0000000000..be0493df05
--- /dev/null
+++ b/lib/prism/pattern.rb
@@ -0,0 +1,314 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+module Prism
+ # A pattern is an object that wraps a Ruby pattern matching expression. The
+ # expression would normally be passed to an `in` clause within a `case`
+ # expression or a rightward assignment expression. For example, in the
+ # following snippet:
+ #
+ # case node
+ # in ConstantPathNode[ConstantReadNode[name: :Prism], ConstantReadNode[name: :Pattern]]
+ # end
+ #
+ # the pattern is the <tt>ConstantPathNode[...]</tt> expression.
+ #
+ # The pattern gets compiled into an object that responds to #call by running
+ # the #compile method. This method itself will run back through Prism to
+ # parse the expression into a tree, then walk the tree to generate the
+ # necessary callable objects. For example, if you wanted to compile the
+ # expression above into a callable, you would:
+ #
+ # callable = Prism::Pattern.new("ConstantPathNode[ConstantReadNode[name: :Prism], ConstantReadNode[name: :Pattern]]").compile
+ # callable.call(node)
+ #
+ # The callable object returned by #compile is guaranteed to respond to #call
+ # with a single argument, which is the node to match against. It also is
+ # guaranteed to respond to #===, which means it itself can be used in a `case`
+ # expression, as in:
+ #
+ # case node
+ # when callable
+ # end
+ #
+ # If the query given to the initializer cannot be compiled into a valid
+ # matcher (either because of a syntax error or because it is using syntax we
+ # do not yet support) then a Prism::Pattern::CompilationError will be
+ # raised.
+ class Pattern
+ # Raised when the query given to a pattern is either invalid Ruby syntax or
+ # is using syntax that we don't yet support.
+ class CompilationError < StandardError
+ # Create a new CompilationError with the given representation of the node
+ # that caused the error.
+ #--
+ #: (String repr) -> void
+ def initialize(repr) # :nodoc:
+ super(<<~ERROR)
+ prism was unable to compile the pattern you provided into a usable
+ expression. It failed on to understand the node represented by:
+
+ #{repr}
+
+ Note that not all syntax supported by Ruby's pattern matching syntax
+ is also supported by prism's patterns. If you're using some syntax
+ that you believe should be supported, please open an issue on
+ GitHub at https://github.com/ruby/prism/issues/new.
+ ERROR
+ end
+ end
+
+ # The query that this pattern was initialized with.
+ attr_reader :query #: String
+ # @rbs @compiled: Proc?
+
+ # Create a new pattern with the given query. The query should be a string
+ # containing a Ruby pattern matching expression.
+ #--
+ #: (String query) -> void
+ def initialize(query)
+ @query = query
+ @compiled = nil
+ end
+
+ # Compile the query into a callable object that can be used to match against
+ # nodes.
+ #--
+ #: () -> Proc
+ def compile
+ result = Prism.parse("case nil\nin #{query}\nend")
+
+ case_match_node = result.value.statements.body.last
+ raise CompilationError, case_match_node.inspect unless case_match_node.is_a?(CaseMatchNode)
+
+ in_node = case_match_node.conditions.last
+ raise CompilationError, in_node.inspect unless in_node.is_a?(InNode)
+
+ compile_node(in_node.pattern)
+ end
+
+ # Scan the given node and all of its children for nodes that match the
+ # pattern. If a block is given, it will be called with each node that
+ # matches the pattern. If no block is given, an enumerator will be returned
+ # that will yield each node that matches the pattern.
+ #--
+ #: (node root) -> Enumerator[node, void]
+ #: (node root) { (node) -> void } -> void
+ def scan(root, &blk)
+ return to_enum(:scan, root) unless block_given?
+
+ @compiled ||= compile
+ queue = [root]
+
+ while (node = queue.shift)
+ yield node if @compiled.call(node) # steep:ignore
+ queue.concat(node.compact_child_nodes)
+ end
+ end
+
+ private
+
+ # Shortcut for combining two procs into one that returns true if both return
+ # true.
+ #--
+ #: (Proc left, Proc right) -> Proc
+ def combine_and(left, right) # :nodoc:
+ ->(other) { left.call(other) && right.call(other) }
+ end
+
+ # Shortcut for combining two procs into one that returns true if either
+ # returns true.
+ #--
+ #: (Proc left, Proc right) -> Proc
+ def combine_or(left, right) # :nodoc:
+ ->(other) { left.call(other) || right.call(other) }
+ end
+
+ # Raise an error because the given node is not supported. Note purposefully
+ # not typing this method since it is a no return method that Steep does not
+ # understand.
+ #--
+ #: (node node) -> bot
+ def compile_error(node) # :nodoc:
+ raise CompilationError, node.inspect
+ end
+
+ # in [foo, bar, baz]
+ #--
+ #: (ArrayPatternNode node) -> Proc
+ def compile_array_pattern_node(node) # :nodoc:
+ compile_error(node) if !node.rest.nil? || node.posts.any?
+
+ constant = node.constant
+ compiled_constant = compile_node(constant) if constant
+
+ preprocessed = node.requireds.map { |required| compile_node(required) }
+
+ compiled_requireds = ->(other) do
+ deconstructed = other.deconstruct
+
+ deconstructed.length == preprocessed.length &&
+ preprocessed
+ .zip(deconstructed)
+ .all? { |(matcher, value)| matcher.call(value) }
+ end
+
+ if compiled_constant
+ combine_and(compiled_constant, compiled_requireds)
+ else
+ compiled_requireds
+ end
+ end
+
+ # in foo | bar
+ #--
+ #: (AlternationPatternNode node) -> Proc
+ def compile_alternation_pattern_node(node) # :nodoc:
+ combine_or(compile_node(node.left), compile_node(node.right))
+ end
+
+ # in Prism::ConstantReadNode
+ #--
+ #: (ConstantPathNode node) -> Proc
+ def compile_constant_path_node(node) # :nodoc:
+ parent = node.parent
+
+ if parent.is_a?(ConstantReadNode) && parent.slice == "Prism"
+ name = node.name
+ raise CompilationError, node.inspect if name.nil?
+
+ compile_constant_name(node, name)
+ else
+ compile_error(node)
+ end
+ end
+
+ # in ConstantReadNode
+ # in String
+ #--
+ #: (ConstantReadNode node) -> Proc
+ def compile_constant_read_node(node) # :nodoc:
+ compile_constant_name(node, node.name)
+ end
+
+ # Compile a name associated with a constant.
+ #--
+ #: ((ConstantPathNode | ConstantReadNode) node, Symbol name) -> Proc
+ def compile_constant_name(node, name) # :nodoc:
+ if Prism.const_defined?(name, false)
+ clazz = Prism.const_get(name)
+
+ ->(other) { clazz === other }
+ elsif Object.const_defined?(name, false)
+ clazz = Object.const_get(name)
+
+ ->(other) { clazz === other }
+ else
+ compile_error(node)
+ end
+ end
+
+ # in InstanceVariableReadNode[name: Symbol]
+ # in { name: Symbol }
+ #--
+ #: (HashPatternNode node) -> Proc
+ def compile_hash_pattern_node(node) # :nodoc:
+ compile_error(node) if node.rest
+
+ if (constant = node.constant)
+ compiled_constant = compile_node(constant)
+ end
+
+ preprocessed =
+ node.elements.to_h do |element|
+ key = element.key
+ if key.is_a?(SymbolNode)
+ [key.unescaped.to_sym, compile_node(element.value)]
+ else
+ raise CompilationError, element.inspect
+ end
+ end
+
+ compiled_keywords = ->(other) do
+ deconstructed = other.deconstruct_keys(preprocessed.keys)
+
+ preprocessed.all? do |keyword, matcher|
+ deconstructed.key?(keyword) && matcher.call(deconstructed[keyword])
+ end
+ end
+
+ if compiled_constant
+ combine_and(compiled_constant, compiled_keywords)
+ else
+ compiled_keywords
+ end
+ end
+
+ # in nil
+ #--
+ #: (NilNode node) -> Proc
+ def compile_nil_node(node) # :nodoc:
+ ->(attribute) { attribute.nil? }
+ end
+
+ # in /foo/
+ #--
+ #: (RegularExpressionNode node) -> Proc
+ def compile_regular_expression_node(node) # :nodoc:
+ regexp = Regexp.new(node.unescaped, node.closing[1..])
+
+ ->(attribute) { regexp === attribute }
+ end
+
+ # in ""
+ # in "foo"
+ #--
+ #: (StringNode node) -> Proc
+ def compile_string_node(node) # :nodoc:
+ string = node.unescaped
+
+ ->(attribute) { string === attribute }
+ end
+
+ # in :+
+ # in :foo
+ #--
+ #: (SymbolNode node) -> Proc
+ def compile_symbol_node(node) # :nodoc:
+ symbol = node.unescaped.to_sym
+
+ ->(attribute) { symbol === attribute }
+ end
+
+ # Compile any kind of node. Dispatch out to the individual compilation
+ # methods based on the type of node.
+ #--
+ #: (node node) -> Proc
+ def compile_node(node) # :nodoc:
+ case node
+ when AlternationPatternNode
+ compile_alternation_pattern_node(node)
+ when ArrayPatternNode
+ compile_array_pattern_node(node)
+ when ConstantPathNode
+ compile_constant_path_node(node)
+ when ConstantReadNode
+ compile_constant_read_node(node)
+ when HashPatternNode
+ compile_hash_pattern_node(node)
+ when NilNode
+ compile_nil_node(node)
+ when RegularExpressionNode
+ compile_regular_expression_node(node)
+ when StringNode
+ compile_string_node(node)
+ when SymbolNode
+ compile_symbol_node(node)
+ else
+ compile_error(node)
+ end
+ end
+ end
+end
diff --git a/lib/prism/polyfill/append_as_bytes.rb b/lib/prism/polyfill/append_as_bytes.rb
new file mode 100644
index 0000000000..24218bd171
--- /dev/null
+++ b/lib/prism/polyfill/append_as_bytes.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# Polyfill for String#append_as_bytes, which didn't exist until Ruby 3.4.
+if !("".respond_to?(:append_as_bytes))
+ String.include(
+ Module.new {
+ def append_as_bytes(*args)
+ args.each do |arg|
+ arg = Integer === arg ? [arg].pack("C") : arg.b
+ self.<<(arg) # steep:ignore
+ end
+ end
+ }
+ )
+end
diff --git a/lib/prism/polyfill/byteindex.rb b/lib/prism/polyfill/byteindex.rb
new file mode 100644
index 0000000000..98c6089f14
--- /dev/null
+++ b/lib/prism/polyfill/byteindex.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# Polyfill for String#byteindex, which didn't exist until Ruby 3.2.
+if !("".respond_to?(:byteindex))
+ String.include(
+ Module.new {
+ def byteindex(needle, offset = 0)
+ charindex = index(needle, offset)
+ slice(0...charindex).bytesize if charindex
+ end
+ }
+ )
+end
diff --git a/lib/prism/polyfill/scan_byte.rb b/lib/prism/polyfill/scan_byte.rb
new file mode 100644
index 0000000000..9276e509fc
--- /dev/null
+++ b/lib/prism/polyfill/scan_byte.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require "strscan"
+
+# Polyfill for StringScanner#scan_byte, which didn't exist until Ruby 3.4.
+if !(StringScanner.method_defined?(:scan_byte))
+ StringScanner.include(
+ Module.new {
+ def scan_byte # :nodoc:
+ get_byte&.b&.ord
+ end
+ }
+ )
+end
diff --git a/lib/prism/polyfill/unpack1.rb b/lib/prism/polyfill/unpack1.rb
new file mode 100644
index 0000000000..3fa9b5a0c5
--- /dev/null
+++ b/lib/prism/polyfill/unpack1.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Polyfill for String#unpack1 with the offset parameter. Not all Ruby engines
+# have Method#parameters implemented, so we check the arity instead if
+# necessary.
+if (unpack1 = String.instance_method(:unpack1)).respond_to?(:parameters) ? unpack1.parameters.none? { |_, name| name == :offset } : (unpack1.arity == 1)
+ String.prepend(
+ Module.new {
+ def unpack1(format, offset: 0) # :nodoc:
+ offset == 0 ? super(format) : self[offset..].unpack1(format) # steep:ignore
+ end
+ }
+ )
+end
diff --git a/lib/prism/polyfill/warn.rb b/lib/prism/polyfill/warn.rb
new file mode 100644
index 0000000000..76a4264623
--- /dev/null
+++ b/lib/prism/polyfill/warn.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+# Polyfill for Kernel#warn with the category parameter. Not all Ruby engines
+# have Method#parameters implemented, so we check the arity instead if
+# necessary.
+if (method = Kernel.instance_method(:warn)).respond_to?(:parameters) ? method.parameters.none? { |_, name| name == :category } : (method.arity == -1)
+ Kernel.prepend(
+ Module.new {
+ def warn(*msgs, uplevel: nil, category: nil) # :nodoc:
+ case uplevel
+ when nil
+ super(*msgs)
+ when Integer
+ super(*msgs, uplevel: uplevel + 1)
+ else
+ super(*msgs, uplevel: uplevel.to_int + 1)
+ end
+ end
+ }
+ )
+
+ Object.prepend(
+ Module.new {
+ def warn(*msgs, uplevel: nil, category: nil) # :nodoc:
+ case uplevel
+ when nil
+ super(*msgs)
+ when Integer
+ super(*msgs, uplevel: uplevel + 1)
+ else
+ super(*msgs, uplevel: uplevel.to_int + 1)
+ end
+ end
+ }
+ )
+end
diff --git a/lib/prism/prism.gemspec b/lib/prism/prism.gemspec
new file mode 100644
index 0000000000..aac056b3f8
--- /dev/null
+++ b/lib/prism/prism.gemspec
@@ -0,0 +1,232 @@
+# frozen_string_literal: true
+
+Gem::Specification.new do |spec|
+ spec.name = "prism"
+ spec.version = "1.9.0"
+ spec.authors = ["Shopify"]
+ spec.email = ["ruby@shopify.com"]
+
+ spec.summary = "Prism Ruby parser"
+ spec.homepage = "https://github.com/ruby/prism"
+ spec.license = "MIT"
+
+ spec.required_ruby_version = ">= 2.7.0"
+
+ spec.require_paths = ["lib"]
+ spec.files = [
+ "BSDmakefile",
+ "CHANGELOG.md",
+ "CODE_OF_CONDUCT.md",
+ "CONTRIBUTING.md",
+ "LICENSE.md",
+ "Makefile",
+ "README.md",
+ "config.yml",
+ "docs/build_system.md",
+ "docs/configuration.md",
+ "docs/cruby_compilation.md",
+ "docs/design.md",
+ "docs/encoding.md",
+ "docs/fuzzing.md",
+ "docs/heredocs.md",
+ "docs/javascript.md",
+ "docs/local_variable_depth.md",
+ "docs/mapping.md",
+ "docs/parser_translation.md",
+ "docs/parsing_rules.md",
+ "docs/releasing.md",
+ "docs/relocation.md",
+ "docs/ripper_translation.md",
+ "docs/ruby_api.md",
+ "docs/ruby_parser_translation.md",
+ "docs/serialization.md",
+ "docs/testing.md",
+ "ext/prism/api_node.c",
+ "ext/prism/extconf.rb",
+ "ext/prism/extension.c",
+ "ext/prism/extension.h",
+ "include/prism.h",
+ "include/prism/compiler/accel.h",
+ "include/prism/compiler/align.h",
+ "include/prism/compiler/exported.h",
+ "include/prism/compiler/fallthrough.h",
+ "include/prism/compiler/filesystem.h",
+ "include/prism/compiler/flex_array.h",
+ "include/prism/compiler/force_inline.h",
+ "include/prism/compiler/format.h",
+ "include/prism/compiler/inline.h",
+ "include/prism/compiler/nodiscard.h",
+ "include/prism/compiler/nonnull.h",
+ "include/prism/compiler/unused.h",
+ "include/prism/internal/allocator.h",
+ "include/prism/internal/allocator_debug.h",
+ "include/prism/internal/arena.h",
+ "include/prism/internal/bit.h",
+ "include/prism/internal/buffer.h",
+ "include/prism/internal/char.h",
+ "include/prism/internal/comments.h",
+ "include/prism/internal/constant_pool.h",
+ "include/prism/internal/diagnostic.h",
+ "include/prism/internal/encoding.h",
+ "include/prism/internal/integer.h",
+ "include/prism/internal/isinf.h",
+ "include/prism/internal/line_offset_list.h",
+ "include/prism/internal/list.h",
+ "include/prism/internal/magic_comments.h",
+ "include/prism/internal/memchr.h",
+ "include/prism/internal/node.h",
+ "include/prism/internal/options.h",
+ "include/prism/internal/parser.h",
+ "include/prism/internal/regexp.h",
+ "include/prism/internal/serialize.h",
+ "include/prism/internal/source.h",
+ "include/prism/internal/static_literals.h",
+ "include/prism/internal/strncasecmp.h",
+ "include/prism/internal/stringy.h",
+ "include/prism/internal/strpbrk.h",
+ "include/prism/internal/tokens.h",
+ "include/prism/arena.h",
+ "include/prism/ast.h",
+ "include/prism/buffer.h",
+ "include/prism/comments.h",
+ "include/prism/constant_pool.h",
+ "include/prism/diagnostic.h",
+ "include/prism/excludes.h",
+ "include/prism/integer.h",
+ "include/prism/json.h",
+ "include/prism/line_offset_list.h",
+ "include/prism/magic_comments.h",
+ "include/prism/node.h",
+ "include/prism/options.h",
+ "include/prism/parser.h",
+ "include/prism/prettyprint.h",
+ "include/prism/serialize.h",
+ "include/prism/source.h",
+ "include/prism/stream.h",
+ "include/prism/string_query.h",
+ "include/prism/stringy.h",
+ "include/prism/version.h",
+ "lib/prism.rb",
+ "lib/prism/compiler.rb",
+ "lib/prism/desugar_compiler.rb",
+ "lib/prism/dispatcher.rb",
+ "lib/prism/dot_visitor.rb",
+ "lib/prism/dsl.rb",
+ "lib/prism/ffi.rb",
+ "lib/prism/inspect_visitor.rb",
+ "lib/prism/lex_compat.rb",
+ "lib/prism/mutation_compiler.rb",
+ "lib/prism/node_ext.rb",
+ "lib/prism/node_find.rb",
+ "lib/prism/node.rb",
+ "lib/prism/parse_result.rb",
+ "lib/prism/parse_result/comments.rb",
+ "lib/prism/parse_result/errors.rb",
+ "lib/prism/parse_result/newlines.rb",
+ "lib/prism/pattern.rb",
+ "lib/prism/polyfill/append_as_bytes.rb",
+ "lib/prism/polyfill/byteindex.rb",
+ "lib/prism/polyfill/scan_byte.rb",
+ "lib/prism/polyfill/unpack1.rb",
+ "lib/prism/polyfill/warn.rb",
+ "lib/prism/reflection.rb",
+ "lib/prism/relocation.rb",
+ "lib/prism/serialize.rb",
+ "lib/prism/string_query.rb",
+ "lib/prism/translation.rb",
+ "lib/prism/translation/parser.rb",
+ "lib/prism/translation/parser_current.rb",
+ "lib/prism/translation/parser_versions.rb",
+ "lib/prism/translation/parser/builder.rb",
+ "lib/prism/translation/parser/compiler.rb",
+ "lib/prism/translation/parser/lexer.rb",
+ "lib/prism/translation/ripper.rb",
+ "lib/prism/translation/ripper/filter.rb",
+ "lib/prism/translation/ripper/lexer.rb",
+ "lib/prism/translation/ripper/sexp.rb",
+ "lib/prism/translation/ripper/shim.rb",
+ "lib/prism/translation/ruby_parser.rb",
+ "lib/prism/visitor.rb",
+ "prism.gemspec",
+ "rbi/generated/prism.rbi",
+ "rbi/generated/prism/compiler.rbi",
+ "rbi/generated/prism/desugar_compiler.rbi",
+ "rbi/generated/prism/dispatcher.rbi",
+ "rbi/generated/prism/dot_visitor.rbi",
+ "rbi/generated/prism/dsl.rbi",
+ "rbi/generated/prism/inspect_visitor.rbi",
+ "rbi/generated/prism/lex_compat.rbi",
+ "rbi/generated/prism/mutation_compiler.rbi",
+ "rbi/generated/prism/node.rbi",
+ "rbi/generated/prism/node_ext.rbi",
+ "rbi/generated/prism/node_find.rbi",
+ "rbi/generated/prism/parse_result.rbi",
+ "rbi/generated/prism/pattern.rbi",
+ "rbi/generated/prism/reflection.rbi",
+ "rbi/generated/prism/relocation.rbi",
+ "rbi/generated/prism/serialize.rbi",
+ "rbi/generated/prism/string_query.rbi",
+ "rbi/generated/prism/translation.rbi",
+ "rbi/generated/prism/visitor.rbi",
+ "rbi/generated/prism/parse_result/comments.rbi",
+ "rbi/generated/prism/parse_result/errors.rbi",
+ "rbi/generated/prism/parse_result/newlines.rbi",
+ "rbi/prism/translation/parser.rbi",
+ "rbi/prism/translation/parser_versions.rbi",
+ "rbi/prism/translation/ripper.rbi",
+ "rbi/rubyvm/node_find.rbi",
+ "sig/generated/prism.rbs",
+ "sig/generated/prism/compiler.rbs",
+ "sig/generated/prism/desugar_compiler.rbs",
+ "sig/generated/prism/dispatcher.rbs",
+ "sig/generated/prism/dot_visitor.rbs",
+ "sig/generated/prism/dsl.rbs",
+ "sig/generated/prism/inspect_visitor.rbs",
+ "sig/generated/prism/lex_compat.rbs",
+ "sig/generated/prism/mutation_compiler.rbs",
+ "sig/generated/prism/node.rbs",
+ "sig/generated/prism/node_ext.rbs",
+ "sig/generated/prism/node_find.rbs",
+ "sig/generated/prism/parse_result.rbs",
+ "sig/generated/prism/pattern.rbs",
+ "sig/generated/prism/reflection.rbs",
+ "sig/generated/prism/relocation.rbs",
+ "sig/generated/prism/serialize.rbs",
+ "sig/generated/prism/string_query.rbs",
+ "sig/generated/prism/translation.rbs",
+ "sig/generated/prism/visitor.rbs",
+ "sig/generated/prism/parse_result/comments.rbs",
+ "sig/generated/prism/parse_result/errors.rbs",
+ "sig/generated/prism/parse_result/newlines.rbs",
+ "src/arena.c",
+ "src/buffer.c",
+ "src/char.c",
+ "src/constant_pool.c",
+ "src/diagnostic.c",
+ "src/encoding.c",
+ "src/integer.c",
+ "src/json.c",
+ "src/line_offset_list.c",
+ "src/list.c",
+ "src/memchr.c",
+ "src/node.c",
+ "src/options.c",
+ "src/parser.c",
+ "src/prettyprint.c",
+ "src/prism.c",
+ "src/regexp.c",
+ "src/serialize.c",
+ "src/source.c",
+ "src/static_literals.c",
+ "src/string_query.c",
+ "src/stringy.c",
+ "src/strncasecmp.c",
+ "src/strpbrk.c",
+ "src/tokens.c"
+ ]
+
+ spec.extensions = ["ext/prism/extconf.rb"]
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
+ spec.metadata["source_code_uri"] = "https://github.com/ruby/prism"
+ spec.metadata["changelog_uri"] = "https://github.com/ruby/prism/blob/main/CHANGELOG.md"
+end
diff --git a/lib/prism/relocation.rb b/lib/prism/relocation.rb
new file mode 100644
index 0000000000..af0f792827
--- /dev/null
+++ b/lib/prism/relocation.rb
@@ -0,0 +1,665 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+module Prism
+ # Prism parses deterministically for the same input. This provides a nice
+ # property that is exposed through the #node_id API on nodes. Effectively this
+ # means that for the same input, these values will remain consistent every
+ # time the source is parsed. This means we can reparse the source same with a
+ # #node_id value and find the exact same node again.
+ #
+ # The Relocation module provides an API around this property. It allows you to
+ # "save" nodes and locations using a minimal amount of memory (just the
+ # node_id and a field identifier) and then reify them later.
+ module Relocation
+ # @rbs!
+ # type entry_value = untyped
+ # type entry_values = Hash[Symbol, entry_value]
+ #
+ # interface _Value
+ # def start_line: () -> Integer
+ # def end_line: () -> Integer
+ # def start_offset: () -> Integer
+ # def end_offset: () -> Integer
+ # def start_character_offset: () -> Integer
+ # def end_character_offset: () -> Integer
+ # def cached_start_code_units_offset: (_CodeUnitsCache cache) -> Integer
+ # def cached_end_code_units_offset: (_CodeUnitsCache cache) -> Integer
+ # def start_column: () -> Integer
+ # def end_column: () -> Integer
+ # def start_character_column: () -> Integer
+ # def end_character_column: () -> Integer
+ # def cached_start_code_units_column: (_CodeUnitsCache cache) -> Integer
+ # def cached_end_code_units_column: (_CodeUnitsCache cache) -> Integer
+ # def leading_comments: () -> Array[Comment]
+ # def trailing_comments: () -> Array[Comment]
+ # end
+ #
+ # interface _Field
+ # def fields: (_Value value) -> entry_values
+ # end
+
+ # An entry in a repository that will lazily reify its values when they are
+ # first accessed.
+ class Entry
+ # Raised if a value that could potentially be on an entry is missing
+ # because it was either not configured on the repository or it has not yet
+ # been fetched.
+ class MissingValueError < StandardError
+ end
+
+ # @rbs @repository: Repository?
+ # @rbs @values: Hash[Symbol, untyped]?
+
+ # Initialize a new entry with the given repository.
+ #--
+ #: (Repository repository) -> void
+ def initialize(repository)
+ @repository = repository
+ @values = nil
+ end
+
+ # Fetch the filepath of the value.
+ #--
+ #: () -> String
+ def filepath
+ fetch_value(:filepath)
+ end
+
+ # Fetch the start line of the value.
+ #--
+ #: () -> Integer
+ def start_line
+ fetch_value(:start_line)
+ end
+
+ # Fetch the end line of the value.
+ #--
+ #: () -> Integer
+ def end_line
+ fetch_value(:end_line)
+ end
+
+ # Fetch the start byte offset of the value.
+ #--
+ #: () -> Integer
+ def start_offset
+ fetch_value(:start_offset)
+ end
+
+ # Fetch the end byte offset of the value.
+ #--
+ #: () -> Integer
+ def end_offset
+ fetch_value(:end_offset)
+ end
+
+ # Fetch the start character offset of the value.
+ #--
+ #: () -> Integer
+ def start_character_offset
+ fetch_value(:start_character_offset)
+ end
+
+ # Fetch the end character offset of the value.
+ #--
+ #: () -> Integer
+ def end_character_offset
+ fetch_value(:end_character_offset)
+ end
+
+ # Fetch the start code units offset of the value, for the encoding that
+ # was configured on the repository.
+ #--
+ #: () -> Integer
+ def start_code_units_offset
+ fetch_value(:start_code_units_offset)
+ end
+
+ # Fetch the end code units offset of the value, for the encoding that was
+ # configured on the repository.
+ #--
+ #: () -> Integer
+ def end_code_units_offset
+ fetch_value(:end_code_units_offset)
+ end
+
+ # Fetch the start byte column of the value.
+ #--
+ #: () -> Integer
+ def start_column
+ fetch_value(:start_column)
+ end
+
+ # Fetch the end byte column of the value.
+ #--
+ #: () -> Integer
+ def end_column
+ fetch_value(:end_column)
+ end
+
+ # Fetch the start character column of the value.
+ #--
+ #: () -> Integer
+ def start_character_column
+ fetch_value(:start_character_column)
+ end
+
+ # Fetch the end character column of the value.
+ #--
+ #: () -> Integer
+ def end_character_column
+ fetch_value(:end_character_column)
+ end
+
+ # Fetch the start code units column of the value, for the encoding that
+ # was configured on the repository.
+ #--
+ #: () -> Integer
+ def start_code_units_column
+ fetch_value(:start_code_units_column)
+ end
+
+ # Fetch the end code units column of the value, for the encoding that was
+ # configured on the repository.
+ #--
+ #: () -> Integer
+ def end_code_units_column
+ fetch_value(:end_code_units_column)
+ end
+
+ # Fetch the leading comments of the value.
+ #--
+ #: () -> Array[CommentsField::Comment]
+ def leading_comments
+ fetch_value(:leading_comments)
+ end
+
+ # Fetch the trailing comments of the value.
+ #--
+ #: () -> Array[CommentsField::Comment]
+ def trailing_comments
+ fetch_value(:trailing_comments)
+ end
+
+ # Fetch the leading and trailing comments of the value.
+ #--
+ #: () -> Array[CommentsField::Comment]
+ def comments
+ [*leading_comments, *trailing_comments]
+ end
+
+ # Reify the values on this entry with the given values. This is an
+ # internal-only API that is called from the repository when it is time to
+ # reify the values.
+ #--
+ #: (entry_values values) -> void
+ def reify!(values) # :nodoc:
+ @repository = nil
+ @values = values
+ end
+
+ private
+
+ # Fetch a value from the entry, raising an error if it is missing.
+ #--
+ #: (Symbol name) -> entry_value
+ def fetch_value(name)
+ values.fetch(name) do
+ raise MissingValueError, "No value for #{name}, make sure the " \
+ "repository has been properly configured"
+ end
+ end
+
+ # Return the values from the repository, reifying them if necessary.
+ #--
+ #: () -> entry_values
+ def values
+ @values || (@repository&.reify!; @values) #: entry_values
+ end
+ end
+
+ # Represents the source of a repository that will be reparsed.
+ class Source
+ # The value that will need to be reparsed.
+ attr_reader :value #: untyped
+
+ # Initialize the source with the given value.
+ #--
+ #: (untyped value) -> void
+ def initialize(value)
+ @value = value
+ end
+
+ # Reparse the value and return the parse result.
+ #--
+ #: () -> ParseResult
+ def result
+ raise NotImplementedError, "Subclasses must implement #result"
+ end
+
+ # Create a code units cache for the given encoding.
+ #--
+ #: (Encoding encoding) -> _CodeUnitsCache
+ def code_units_cache(encoding)
+ result.code_units_cache(encoding)
+ end
+ end
+
+ # A source that is represented by a file path.
+ class SourceFilepath < Source
+ # Reparse the file and return the parse result.
+ #--
+ #: () -> ParseResult
+ def result
+ Prism.parse_file(value)
+ end
+ end
+
+ # A source that is represented by a string.
+ class SourceString < Source
+ # Reparse the string and return the parse result.
+ #--
+ #: () -> ParseResult
+ def result
+ Prism.parse(value)
+ end
+ end
+
+ # A field that represents the file path.
+ class FilepathField
+ # The file path that this field represents.
+ attr_reader :value #: String
+
+ # Initialize a new field with the given file path.
+ #--
+ #: (String value) -> void
+ def initialize(value)
+ @value = value
+ end
+
+ # Fetch the file path.
+ #--
+ #: (_Value _value) -> entry_values
+ def fields(_value)
+ { filepath: value }
+ end
+ end
+
+ # A field representing the start and end lines.
+ class LinesField
+ # Fetches the start and end line of a value.
+ #--
+ #: (_Value value) -> entry_values
+ def fields(value)
+ { start_line: value.start_line, end_line: value.end_line }
+ end
+ end
+
+ # A field representing the start and end byte offsets.
+ class OffsetsField
+ # Fetches the start and end byte offset of a value.
+ #--
+ #: (_Value value) -> entry_values
+ def fields(value)
+ { start_offset: value.start_offset, end_offset: value.end_offset }
+ end
+ end
+
+ # A field representing the start and end character offsets.
+ class CharacterOffsetsField
+ # Fetches the start and end character offset of a value.
+ #--
+ #: (_Value value) -> entry_values
+ def fields(value)
+ {
+ start_character_offset: value.start_character_offset,
+ end_character_offset: value.end_character_offset
+ }
+ end
+ end
+
+ # A field representing the start and end code unit offsets.
+ class CodeUnitOffsetsField
+ # A pointer to the repository object that is used for lazily creating a
+ # code units cache.
+ attr_reader :repository #: Repository
+
+ # The associated encoding for the code units.
+ attr_reader :encoding #: Encoding
+
+ # @rbs @cache: _CodeUnitsCache?
+
+ # Initialize a new field with the associated repository and encoding.
+ #--
+ #: (Repository repository, Encoding encoding) -> void
+ def initialize(repository, encoding)
+ @repository = repository
+ @encoding = encoding
+ @cache = nil
+ end
+
+ # Fetches the start and end code units offset of a value for a particular
+ # encoding.
+ #--
+ #: (_Value value) -> entry_values
+ def fields(value)
+ {
+ start_code_units_offset: value.cached_start_code_units_offset(cache),
+ end_code_units_offset: value.cached_end_code_units_offset(cache)
+ }
+ end
+
+ private
+
+ # Lazily create a code units cache for the associated encoding.
+ #--
+ #: () -> _CodeUnitsCache
+ def cache
+ @cache ||= repository.code_units_cache(encoding)
+ end
+ end
+
+ # A field representing the start and end byte columns.
+ class ColumnsField
+ # Fetches the start and end byte column of a value.
+ #--
+ #: (_Value value) -> entry_values
+ def fields(value)
+ { start_column: value.start_column, end_column: value.end_column }
+ end
+ end
+
+ # A field representing the start and end character columns.
+ class CharacterColumnsField
+ # Fetches the start and end character column of a value.
+ #--
+ #: (_Value value) -> entry_values
+ def fields(value)
+ {
+ start_character_column: value.start_character_column,
+ end_character_column: value.end_character_column
+ }
+ end
+ end
+
+ # A field representing the start and end code unit columns for a specific
+ # encoding.
+ class CodeUnitColumnsField
+ # The repository object that is used for lazily creating a code units
+ # cache.
+ attr_reader :repository #: Repository
+
+ # The associated encoding for the code units.
+ attr_reader :encoding #: Encoding
+
+ # @rbs @cache: _CodeUnitsCache?
+
+ # Initialize a new field with the associated repository and encoding.
+ #--
+ #: (Repository repository, Encoding encoding) -> void
+ def initialize(repository, encoding)
+ @repository = repository
+ @encoding = encoding
+ @cache = nil
+ end
+
+ # Fetches the start and end code units column of a value for a particular
+ # encoding.
+ #--
+ #: (_Value value) -> entry_values
+ def fields(value)
+ {
+ start_code_units_column: value.cached_start_code_units_column(cache),
+ end_code_units_column: value.cached_end_code_units_column(cache)
+ }
+ end
+
+ private
+
+ # Lazily create a code units cache for the associated encoding.
+ #--
+ #: () -> _CodeUnitsCache
+ def cache
+ @cache ||= repository.code_units_cache(encoding)
+ end
+ end
+
+ # An abstract field used as the parent class of the two comments fields.
+ class CommentsField
+ # An object that represents a slice of a comment.
+ class Comment
+ # The slice of the comment.
+ attr_reader :slice #: String
+
+ # Initialize a new comment with the given slice.
+ #
+ #: (String slice) -> void
+ def initialize(slice)
+ @slice = slice
+ end
+ end
+
+ private
+
+ # Create comment objects from the given values.
+ #--
+ #: (entry_value values) -> Array[Comment]
+ def comments(values)
+ values.map { |value| Comment.new(value.slice) }
+ end
+ end
+
+ # A field representing the leading comments.
+ class LeadingCommentsField < CommentsField
+ # Fetches the leading comments of a value.
+ #--
+ #: (_Value value) -> entry_values
+ def fields(value)
+ { leading_comments: comments(value.leading_comments) }
+ end
+ end
+
+ # A field representing the trailing comments.
+ class TrailingCommentsField < CommentsField
+ # Fetches the trailing comments of a value.
+ #--
+ #: (_Value value) -> entry_values
+ def fields(value)
+ { trailing_comments: comments(value.trailing_comments) }
+ end
+ end
+
+ # A repository is a configured collection of fields and a set of entries
+ # that knows how to reparse a source and reify the values.
+ class Repository
+ # Raised when multiple fields of the same type are configured on the same
+ # repository.
+ class ConfigurationError < StandardError
+ end
+
+ # The source associated with this repository. This will be either a
+ # SourceFilepath (the most common use case) or a SourceString.
+ attr_reader :source #: Source
+
+ # The fields that have been configured on this repository.
+ attr_reader :fields #: Hash[Symbol, _Field]
+
+ # The entries that have been saved on this repository.
+ attr_reader :entries #: Hash[Integer, Hash[Symbol, Entry]]
+
+ # Initialize a new repository with the given source.
+ #--
+ #: (Source source) -> void
+ def initialize(source)
+ @source = source
+ @fields = {}
+ @entries = Hash.new { |hash, node_id| hash[node_id] = {} }
+ end
+
+ # Create a code units cache for the given encoding from the source.
+ #--
+ #: (Encoding encoding) -> _CodeUnitsCache
+ def code_units_cache(encoding)
+ source.code_units_cache(encoding)
+ end
+
+ # Configure the filepath field for this repository and return self.
+ #--
+ #: () -> self
+ def filepath
+ raise ConfigurationError, "Can only specify filepath for a filepath source" unless source.is_a?(SourceFilepath)
+ field(:filepath, FilepathField.new(source.value))
+ end
+
+ # Configure the lines field for this repository and return self.
+ #--
+ #: () -> self
+ def lines
+ field(:lines, LinesField.new)
+ end
+
+ # Configure the offsets field for this repository and return self.
+ #--
+ #: () -> self
+ def offsets
+ field(:offsets, OffsetsField.new)
+ end
+
+ # Configure the character offsets field for this repository and return
+ # self.
+ #--
+ #: () -> self
+ def character_offsets
+ field(:character_offsets, CharacterOffsetsField.new)
+ end
+
+ # Configure the code unit offsets field for this repository for a specific
+ # encoding and return self.
+ #--
+ #: (Encoding encoding) -> self
+ def code_unit_offsets(encoding)
+ field(:code_unit_offsets, CodeUnitOffsetsField.new(self, encoding))
+ end
+
+ # Configure the columns field for this repository and return self.
+ #--
+ #: () -> self
+ def columns
+ field(:columns, ColumnsField.new)
+ end
+
+ # Configure the character columns field for this repository and return
+ # self.
+ #--
+ #: () -> self
+ def character_columns
+ field(:character_columns, CharacterColumnsField.new)
+ end
+
+ # Configure the code unit columns field for this repository for a specific
+ # encoding and return self.
+ #--
+ #: (Encoding encoding) -> self
+ def code_unit_columns(encoding)
+ field(:code_unit_columns, CodeUnitColumnsField.new(self, encoding))
+ end
+
+ # Configure the leading comments field for this repository and return
+ # self.
+ #--
+ #: () -> self
+ def leading_comments
+ field(:leading_comments, LeadingCommentsField.new)
+ end
+
+ # Configure the trailing comments field for this repository and return
+ # self.
+ #--
+ #: () -> self
+ def trailing_comments
+ field(:trailing_comments, TrailingCommentsField.new)
+ end
+
+ # Configure both the leading and trailing comment fields for this
+ # repository and return self.
+ #--
+ #: () -> self
+ def comments
+ leading_comments.trailing_comments
+ end
+
+ # This method is called from nodes and locations when they want to enter
+ # themselves into the repository. It it internal-only and meant to be
+ # called from the #save* APIs.
+ #--
+ #: (Integer node_id, Symbol field_name) -> Entry
+ def enter(node_id, field_name) # :nodoc:
+ entry = Entry.new(self)
+ @entries[node_id][field_name] = entry
+ entry
+ end
+
+ # This method is called from the entries in the repository when they need
+ # to reify their values. It is internal-only and meant to be called from
+ # the various value APIs.
+ #--
+ #: () -> void
+ def reify! # :nodoc:
+ result = source.result
+
+ # Attach the comments if they have been requested as part of the
+ # configuration of this repository.
+ if fields.key?(:leading_comments) || fields.key?(:trailing_comments)
+ result.attach_comments!
+ end
+
+ queue = [result.value] #: Array[Prism::node]
+ while (node = queue.shift)
+ @entries[node.node_id].each do |field_name, entry|
+ value = node.public_send(field_name)
+ values = {} #: entry_values
+
+ fields.each_value do |field|
+ values.merge!(field.fields(value))
+ end
+
+ entry.reify!(values)
+ end
+
+ queue.concat(node.compact_child_nodes)
+ end
+
+ @entries.clear
+ end
+
+ private
+
+ # Append the given field to the repository and return the repository so
+ # that these calls can be chained.
+ #--
+ #: (Symbol name, _Field) -> self
+ def field(name, value)
+ raise ConfigurationError, "Cannot specify multiple #{name} fields" if @fields.key?(name)
+ @fields[name] = value
+ self
+ end
+ end
+
+ # Create a new repository for the given filepath.
+ #--
+ #: (String value) -> Repository
+ def self.filepath(value)
+ Repository.new(SourceFilepath.new(value))
+ end
+
+ # Create a new repository for the given string.
+ #--
+ #: (String value) -> Repository
+ def self.string(value)
+ Repository.new(SourceString.new(value))
+ end
+ end
+end
diff --git a/lib/prism/string_query.rb b/lib/prism/string_query.rb
new file mode 100644
index 0000000000..99ce57e5fe
--- /dev/null
+++ b/lib/prism/string_query.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+module Prism
+ # Query methods that allow categorizing strings based on their context for
+ # where they could be valid in a Ruby syntax tree.
+ class StringQuery
+ # @rbs!
+ # def self.local?: (String string) -> bool
+ # def self.constant?: (String string) -> bool
+ # def self.method_name?: (String string) -> bool
+
+ # The string that this query is wrapping.
+ attr_reader :string #: String
+
+ # Initialize a new query with the given string.
+ #--
+ #: (String string) -> void
+ def initialize(string)
+ @string = string
+ end
+
+ # Whether or not this string is a valid local variable name.
+ #--
+ #: () -> bool
+ def local?
+ StringQuery.local?(string)
+ end
+
+ # Whether or not this string is a valid constant name.
+ #--
+ #: () -> bool
+ def constant?
+ StringQuery.constant?(string)
+ end
+
+ # Whether or not this string is a valid method name.
+ #--
+ #: () -> bool
+ def method_name?
+ StringQuery.method_name?(string)
+ end
+ end
+end
diff --git a/lib/prism/translation.rb b/lib/prism/translation.rb
new file mode 100644
index 0000000000..5a086a7542
--- /dev/null
+++ b/lib/prism/translation.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+module Prism
+ # This module is responsible for converting the prism syntax tree into other
+ # syntax trees.
+ module Translation # steep:ignore
+ autoload :Parser, "prism/translation/parser"
+ autoload :ParserCurrent, "prism/translation/parser_current"
+ autoload :Parser33, "prism/translation/parser_versions"
+ autoload :Parser34, "prism/translation/parser_versions"
+ autoload :Parser35, "prism/translation/parser_versions"
+ autoload :Parser40, "prism/translation/parser_versions"
+ autoload :Parser41, "prism/translation/parser_versions"
+ autoload :Ripper, "prism/translation/ripper"
+ autoload :RubyParser, "prism/translation/ruby_parser"
+ end
+end
diff --git a/lib/prism/translation/parser.rb b/lib/prism/translation/parser.rb
new file mode 100644
index 0000000000..70031f133a
--- /dev/null
+++ b/lib/prism/translation/parser.rb
@@ -0,0 +1,376 @@
+# frozen_string_literal: true
+# :markup: markdown
+
+begin
+ required_version = ">= 3.3.7.2"
+ gem "parser", required_version
+ require "parser"
+rescue LoadError
+ warn(<<~MSG)
+ Error: Unable to load parser #{required_version}. \
+ Add `gem "parser"` to your Gemfile or run `bundle update parser`.
+ MSG
+ exit(1)
+end
+
+module Prism
+ module Translation
+ # This class is the entry-point for converting a prism syntax tree into the
+ # whitequark/parser gem's syntax tree. It inherits from the base parser for
+ # the parser gem, and overrides the parse* methods to parse with prism and
+ # then translate.
+ #
+ # Note that this version of the parser always parses using the latest
+ # version of Ruby syntax supported by Prism. If you want specific version
+ # support, use one of the version-specific subclasses, such as
+ # `Prism::Translation::Parser34`. If you want to parse using the same
+ # version of Ruby syntax as the currently running version of Ruby, use
+ # `Prism::Translation::ParserCurrent`.
+ class Parser < ::Parser::Base
+ Diagnostic = ::Parser::Diagnostic # :nodoc:
+ private_constant :Diagnostic
+
+ # The parser gem has a list of diagnostics with a hard-coded set of error
+ # messages. We create our own diagnostic class in order to set our own
+ # error messages.
+ class PrismDiagnostic < Diagnostic # :nodoc:
+ # This is the cached message coming from prism.
+ attr_reader :message
+
+ # Initialize a new diagnostic with the given message and location.
+ def initialize(message, level, reason, location)
+ @message = message
+ super(level, reason, {}, location, [])
+ end
+ end
+
+ Racc_debug_parser = false # :nodoc:
+
+ # The `builder` argument is used to create the parser using our custom builder class by default.
+ #
+ # By using the `:parser` keyword argument, you can translate in a way that is compatible with
+ # the Parser gem using any parser.
+ #
+ # For example, in RuboCop for Ruby LSP, the following approach can be used to improve performance
+ # by reusing a pre-parsed `Prism::ParseLexResult`:
+ #
+ # class PrismPreparsed
+ # def initialize(prism_result)
+ # @prism_result = prism_result
+ # end
+ #
+ # def parse_lex(source, **options)
+ # @prism_result
+ # end
+ # end
+ #
+ # prism_preparsed = PrismPreparsed.new(prism_result)
+ #
+ # Prism::Translation::Ruby34.new(builder, parser: prism_preparsed)
+ #
+ # In an object passed to the `:parser` keyword argument, the `parse` and `parse_lex` methods
+ # should be implemented as needed.
+ #
+ def initialize(builder = Prism::Translation::Parser::Builder.new, parser: Prism)
+ if !builder.is_a?(Prism::Translation::Parser::Builder)
+ warn(<<~MSG, uplevel: 1, category: :deprecated)
+ [deprecation]: The builder passed to `Prism::Translation::Parser.new` is not a \
+ `Prism::Translation::Parser::Builder` subclass. This will raise in the next major version.
+ MSG
+ end
+ @parser = parser
+
+ super(builder)
+ end
+
+ def version # :nodoc:
+ 41
+ end
+
+ # The default encoding for Ruby files is UTF-8.
+ def default_encoding
+ Encoding::UTF_8
+ end
+
+ def yyerror # :nodoc:
+ end
+
+ # Parses a source buffer and returns the AST.
+ def parse(source_buffer)
+ @source_buffer = source_buffer
+ source = source_buffer.source
+
+ offset_cache = build_offset_cache(source)
+ result = unwrap(@parser.parse(source, **prism_options), offset_cache)
+
+ build_ast(result.value, offset_cache)
+ ensure
+ @source_buffer = nil
+ end
+
+ # Parses a source buffer and returns the AST and the source code comments.
+ def parse_with_comments(source_buffer)
+ @source_buffer = source_buffer
+ source = source_buffer.source
+
+ offset_cache = build_offset_cache(source)
+ result = unwrap(@parser.parse(source, **prism_options), offset_cache)
+
+ [
+ build_ast(result.value, offset_cache),
+ build_comments(result.comments, offset_cache)
+ ]
+ ensure
+ @source_buffer = nil
+ end
+
+ # Parses a source buffer and returns the AST, the source code comments,
+ # and the tokens emitted by the lexer.
+ def tokenize(source_buffer, recover = false)
+ @source_buffer = source_buffer
+ source = source_buffer.source
+
+ offset_cache = build_offset_cache(source)
+ result =
+ begin
+ unwrap(@parser.parse_lex(source, **prism_options), offset_cache)
+ rescue ::Parser::SyntaxError
+ raise if !recover
+ end
+
+ program, tokens = result.value
+ ast = build_ast(program, offset_cache) if result.success?
+
+ [
+ ast,
+ build_comments(result.comments, offset_cache),
+ build_tokens(tokens, offset_cache)
+ ]
+ ensure
+ @source_buffer = nil
+ end
+
+ # Since prism resolves num params for us, we don't need to support this
+ # kind of logic here.
+ def try_declare_numparam(node)
+ node.children[0].match?(/\A_[1-9]\z/)
+ end
+
+ private
+
+ # This is a hook to allow consumers to disable some errors if they don't
+ # want them to block creating the syntax tree.
+ def valid_error?(error)
+ true
+ end
+
+ # This is a hook to allow consumers to disable some warnings if they don't
+ # want them to block creating the syntax tree.
+ def valid_warning?(warning)
+ true
+ end
+
+ # Build a diagnostic from the given prism parse error.
+ def error_diagnostic(error, offset_cache)
+ location = error.location
+ diagnostic_location = build_range(location, offset_cache)
+
+ case error.type
+ when :argument_block_multi
+ Diagnostic.new(:error, :block_and_blockarg, {}, diagnostic_location, [])
+ when :argument_formal_constant
+ Diagnostic.new(:error, :argument_const, {}, diagnostic_location, [])
+ when :argument_formal_class
+ Diagnostic.new(:error, :argument_cvar, {}, diagnostic_location, [])
+ when :argument_formal_global
+ Diagnostic.new(:error, :argument_gvar, {}, diagnostic_location, [])
+ when :argument_formal_ivar
+ Diagnostic.new(:error, :argument_ivar, {}, diagnostic_location, [])
+ when :argument_no_forwarding_amp
+ Diagnostic.new(:error, :no_anonymous_blockarg, {}, diagnostic_location, [])
+ when :argument_no_forwarding_star
+ Diagnostic.new(:error, :no_anonymous_restarg, {}, diagnostic_location, [])
+ when :argument_no_forwarding_star_star
+ Diagnostic.new(:error, :no_anonymous_kwrestarg, {}, diagnostic_location, [])
+ when :begin_lonely_else
+ location = location.copy(length: 4)
+ diagnostic_location = build_range(location, offset_cache)
+ Diagnostic.new(:error, :useless_else, {}, diagnostic_location, [])
+ when :class_name, :module_name
+ Diagnostic.new(:error, :module_name_const, {}, diagnostic_location, [])
+ when :class_in_method
+ Diagnostic.new(:error, :class_in_def, {}, diagnostic_location, [])
+ when :def_endless_setter
+ Diagnostic.new(:error, :endless_setter, {}, diagnostic_location, [])
+ when :embdoc_term
+ Diagnostic.new(:error, :embedded_document, {}, diagnostic_location, [])
+ when :incomplete_variable_class, :incomplete_variable_class_3_3
+ location = location.copy(length: location.length + 1)
+ diagnostic_location = build_range(location, offset_cache)
+
+ Diagnostic.new(:error, :cvar_name, { name: location.slice }, diagnostic_location, [])
+ when :incomplete_variable_instance, :incomplete_variable_instance_3_3
+ location = location.copy(length: location.length + 1)
+ diagnostic_location = build_range(location, offset_cache)
+
+ Diagnostic.new(:error, :ivar_name, { name: location.slice }, diagnostic_location, [])
+ when :invalid_variable_global, :invalid_variable_global_3_3
+ Diagnostic.new(:error, :gvar_name, { name: location.slice }, diagnostic_location, [])
+ when :module_in_method
+ Diagnostic.new(:error, :module_in_def, {}, diagnostic_location, [])
+ when :numbered_parameter_ordinary
+ Diagnostic.new(:error, :ordinary_param_defined, {}, diagnostic_location, [])
+ when :numbered_parameter_outer_scope
+ Diagnostic.new(:error, :numparam_used_in_outer_scope, {}, diagnostic_location, [])
+ when :parameter_circular
+ Diagnostic.new(:error, :circular_argument_reference, { var_name: location.slice }, diagnostic_location, [])
+ when :parameter_name_repeat
+ Diagnostic.new(:error, :duplicate_argument, {}, diagnostic_location, [])
+ when :parameter_numbered_reserved
+ Diagnostic.new(:error, :reserved_for_numparam, { name: location.slice }, diagnostic_location, [])
+ when :regexp_unknown_options
+ Diagnostic.new(:error, :regexp_options, { options: location.slice[1..] }, diagnostic_location, [])
+ when :singleton_for_literals
+ Diagnostic.new(:error, :singleton_literal, {}, diagnostic_location, [])
+ when :string_literal_eof
+ Diagnostic.new(:error, :string_eof, {}, diagnostic_location, [])
+ when :unexpected_token_ignore
+ Diagnostic.new(:error, :unexpected_token, { token: location.slice }, diagnostic_location, [])
+ when :write_target_in_method
+ Diagnostic.new(:error, :dynamic_const, {}, diagnostic_location, [])
+ else
+ PrismDiagnostic.new(error.message, :error, error.type, diagnostic_location)
+ end
+ end
+
+ # Build a diagnostic from the given prism parse warning.
+ def warning_diagnostic(warning, offset_cache)
+ diagnostic_location = build_range(warning.location, offset_cache)
+
+ case warning.type
+ when :ambiguous_first_argument_plus
+ Diagnostic.new(:warning, :ambiguous_prefix, { prefix: "+" }, diagnostic_location, [])
+ when :ambiguous_first_argument_minus
+ Diagnostic.new(:warning, :ambiguous_prefix, { prefix: "-" }, diagnostic_location, [])
+ when :ambiguous_prefix_ampersand
+ Diagnostic.new(:warning, :ambiguous_prefix, { prefix: "&" }, diagnostic_location, [])
+ when :ambiguous_prefix_star
+ Diagnostic.new(:warning, :ambiguous_prefix, { prefix: "*" }, diagnostic_location, [])
+ when :ambiguous_prefix_star_star
+ Diagnostic.new(:warning, :ambiguous_prefix, { prefix: "**" }, diagnostic_location, [])
+ when :ambiguous_slash
+ Diagnostic.new(:warning, :ambiguous_regexp, {}, diagnostic_location, [])
+ when :dot_dot_dot_eol
+ Diagnostic.new(:warning, :triple_dot_at_eol, {}, diagnostic_location, [])
+ when :duplicated_hash_key
+ # skip, parser does this on its own
+ else
+ PrismDiagnostic.new(warning.message, :warning, warning.type, diagnostic_location)
+ end
+ end
+
+ # If there was a error generated during the parse, then raise an
+ # appropriate syntax error. Otherwise return the result.
+ def unwrap(result, offset_cache)
+ result.errors.each do |error|
+ next unless valid_error?(error)
+ diagnostics.process(error_diagnostic(error, offset_cache))
+ end
+
+ result.warnings.each do |warning|
+ next unless valid_warning?(warning)
+ diagnostic = warning_diagnostic(warning, offset_cache)
+ diagnostics.process(diagnostic) if diagnostic
+ end
+
+ result
+ end
+
+ # Prism deals with offsets in bytes, while the parser gem deals with
+ # offsets in characters. We need to handle this conversion in order to
+ # build the parser gem AST.
+ #
+ # If the bytesize of the source is the same as the length, then we can
+ # just use the offset directly. Otherwise, we build an array where the
+ # index is the byte offset and the value is the character offset.
+ def build_offset_cache(source)
+ if source.bytesize == source.length
+ -> (offset) { offset }
+ else
+ offset_cache = []
+ offset = 0
+
+ source.each_char do |char|
+ char.bytesize.times { offset_cache << offset }
+ offset += 1
+ end
+
+ offset_cache << offset
+ end
+ end
+
+ # Build the parser gem AST from the prism AST.
+ def build_ast(program, offset_cache)
+ program.accept(Compiler.new(self, offset_cache))
+ end
+
+ # Build the parser gem comments from the prism comments.
+ def build_comments(comments, offset_cache)
+ comments.map do |comment|
+ ::Parser::Source::Comment.new(build_range(comment.location, offset_cache))
+ end
+ end
+
+ # Build the parser gem tokens from the prism tokens.
+ def build_tokens(tokens, offset_cache)
+ Lexer.new(source_buffer, tokens, offset_cache).to_a
+ end
+
+ # Build a range from a prism location.
+ def build_range(location, offset_cache)
+ ::Parser::Source::Range.new(
+ source_buffer,
+ offset_cache[location.start_offset],
+ offset_cache[location.end_offset]
+ )
+ end
+
+ # Options for how prism should parse/lex the source.
+ def prism_options
+ options = {
+ filepath: @source_buffer.name,
+ version: convert_for_prism(version),
+ partial_script: true,
+ }
+ # The parser gem always encodes to UTF-8, unless it is binary.
+ # https://github.com/whitequark/parser/blob/v3.3.6.0/lib/parser/source/buffer.rb#L80-L107
+ options[:encoding] = false if @source_buffer.source.encoding != Encoding::BINARY
+
+ options
+ end
+
+ # Converts the version format handled by Parser to the format handled by Prism.
+ def convert_for_prism(version)
+ case version
+ when 33
+ "3.3.1"
+ when 34
+ "3.4.0"
+ when 35, 40
+ "4.0.0"
+ when 41
+ "4.1.0"
+ else
+ "latest"
+ end
+ end
+
+ require_relative "parser/builder"
+ require_relative "parser/compiler"
+ require_relative "parser/lexer"
+
+ private_constant :Compiler
+ private_constant :Lexer
+ end
+ end
+end
diff --git a/lib/prism/translation/parser/builder.rb b/lib/prism/translation/parser/builder.rb
new file mode 100644
index 0000000000..7fc3bba6b7
--- /dev/null
+++ b/lib/prism/translation/parser/builder.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+# :markup: markdown
+
+module Prism
+ module Translation
+ class Parser
+ # A builder that knows how to convert more modern Ruby syntax
+ # into whitequark/parser gem's syntax tree.
+ class Builder < ::Parser::Builders::Default
+ # It represents the `it` block argument, which is not yet implemented in
+ # the Parser gem.
+ def itarg
+ n(:itarg, [:it], nil)
+ end
+
+ # The following three lines have been added to support the `it` block
+ # parameter syntax in the source code below.
+ #
+ # if args.type == :itarg
+ # block_type = :itblock
+ # args = :it
+ #
+ # https://github.com/whitequark/parser/blob/v3.3.7.1/lib/parser/builders/default.rb#L1122-L1155
+ def block(method_call, begin_t, args, body, end_t)
+ _receiver, _selector, *call_args = *method_call
+
+ if method_call.type == :yield
+ diagnostic :error, :block_given_to_yield, nil, method_call.loc.keyword, [loc(begin_t)]
+ end
+
+ last_arg = call_args.last
+ if last_arg && (last_arg.type == :block_pass || last_arg.type == :forwarded_args)
+ diagnostic :error, :block_and_blockarg, nil, last_arg.loc.expression, [loc(begin_t)]
+ end
+
+ if args.type == :itarg
+ block_type = :itblock
+ args = :it
+ elsif args.type == :numargs
+ block_type = :numblock
+ args = args.children[0]
+ else
+ block_type = :block
+ end
+
+ if [:send, :csend, :index, :super, :zsuper, :lambda].include?(method_call.type)
+ n(block_type, [ method_call, args, body ],
+ block_map(method_call.loc.expression, begin_t, end_t))
+ else
+ # Code like "return foo 1 do end" is reduced in a weird sequence.
+ # Here, method_call is actually (return).
+ actual_send, = *method_call
+ block =
+ n(block_type, [ actual_send, args, body ],
+ block_map(actual_send.loc.expression, begin_t, end_t))
+
+ n(method_call.type, [ block ],
+ method_call.loc.with_expression(join_exprs(method_call, block)))
+ end
+ end
+
+ # def foo(&nil); end
+ # ^^^^
+ def blocknilarg(amper_t, nil_t)
+ n0(:blocknilarg, arg_prefix_map(amper_t, nil_t))
+ end
+ end
+ end
+ end
+end
diff --git a/lib/prism/translation/parser/compiler.rb b/lib/prism/translation/parser/compiler.rb
new file mode 100644
index 0000000000..d11db12ae6
--- /dev/null
+++ b/lib/prism/translation/parser/compiler.rb
@@ -0,0 +1,2219 @@
+# frozen_string_literal: true
+# :markup: markdown
+
+module Prism
+ module Translation
+ class Parser
+ # A visitor that knows how to convert a prism syntax tree into the
+ # whitequark/parser gem's syntax tree.
+ class Compiler < ::Prism::Compiler # :nodoc:
+ # Raised when the tree is malformed or there is a bug in the compiler.
+ class CompilationError < StandardError # :nodoc:
+ end
+
+ # The Parser::Base instance that is being used to build the AST.
+ attr_reader :parser
+
+ # The Parser::Builders::Default instance that is being used to build the
+ # AST.
+ attr_reader :builder
+
+ # The Parser::Source::Buffer instance that is holding a reference to the
+ # source code.
+ attr_reader :source_buffer
+
+ # The offset cache that is used to map between byte and character
+ # offsets in the file.
+ attr_reader :offset_cache
+
+ # The types of values that can be forwarded in the current scope.
+ attr_reader :forwarding
+
+ # Whether or not the current node is in a destructure.
+ attr_reader :in_destructure
+
+ # Whether or not the current node is in a pattern.
+ attr_reader :in_pattern
+
+ # Initialize a new compiler with the given parser, offset cache, and
+ # options.
+ def initialize(parser, offset_cache, forwarding: [], in_destructure: false, in_pattern: false)
+ @parser = parser
+ @builder = parser.builder
+ @source_buffer = parser.source_buffer
+ @offset_cache = offset_cache
+
+ @forwarding = forwarding
+ @in_destructure = in_destructure
+ @in_pattern = in_pattern
+ end
+
+ # alias foo bar
+ # ^^^^^^^^^^^^^
+ def visit_alias_method_node(node)
+ builder.alias(token(node.keyword_loc), visit(node.new_name), visit(node.old_name))
+ end
+
+ # alias $foo $bar
+ # ^^^^^^^^^^^^^^^
+ def visit_alias_global_variable_node(node)
+ builder.alias(token(node.keyword_loc), visit(node.new_name), visit(node.old_name))
+ end
+
+ # foo => bar | baz
+ # ^^^^^^^^^
+ def visit_alternation_pattern_node(node)
+ builder.match_alt(visit(node.left), token(node.operator_loc), visit(node.right))
+ end
+
+ # a and b
+ # ^^^^^^^
+ def visit_and_node(node)
+ builder.logical_op(:and, visit(node.left), token(node.operator_loc), visit(node.right))
+ end
+
+ # []
+ # ^^
+ def visit_array_node(node)
+ if node.opening&.start_with?("%w", "%W", "%i", "%I")
+ elements = node.elements.flat_map do |element|
+ if element.is_a?(StringNode)
+ if element.content.include?("\n")
+ string_nodes_from_line_continuations(element.unescaped, element.content, element.content_loc.start_offset, node.opening)
+ else
+ [builder.string_internal([element.unescaped, srange(element.content_loc)])]
+ end
+ elsif element.is_a?(InterpolatedStringNode)
+ builder.string_compose(
+ token(element.opening_loc),
+ string_nodes_from_interpolation(element, node.opening),
+ token(element.closing_loc)
+ )
+ else
+ [visit(element)]
+ end
+ end
+ else
+ elements = visit_all(node.elements)
+ end
+
+ builder.array(token(node.opening_loc), elements, token(node.closing_loc))
+ end
+
+ # foo => [bar]
+ # ^^^^^
+ def visit_array_pattern_node(node)
+ elements = [*node.requireds]
+ elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode)
+ elements.concat(node.posts)
+ visited = visit_all(elements)
+
+ if node.rest.is_a?(ImplicitRestNode)
+ visited[-1] = builder.match_with_trailing_comma(visited[-1], token(node.rest.location))
+ end
+
+ if node.constant
+ if visited.empty?
+ builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(token(node.opening_loc), visited, token(node.closing_loc)), token(node.closing_loc))
+ else
+ builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(nil, visited, nil), token(node.closing_loc))
+ end
+ else
+ builder.array_pattern(token(node.opening_loc), visited, token(node.closing_loc))
+ end
+ end
+
+ # foo(bar)
+ # ^^^
+ def visit_arguments_node(node)
+ visit_all(node.arguments)
+ end
+
+ # { a: 1 }
+ # ^^^^
+ def visit_assoc_node(node)
+ key = node.key
+
+ if node.value.is_a?(ImplicitNode)
+ if in_pattern
+ if key.is_a?(SymbolNode)
+ if key.opening.nil?
+ builder.match_hash_var([key.unescaped, srange(key.location)])
+ else
+ builder.match_hash_var_from_str(token(key.opening_loc), [builder.string_internal([key.unescaped, srange(key.value_loc)])], token(key.closing_loc))
+ end
+ else
+ builder.match_hash_var_from_str(token(key.opening_loc), visit_all(key.parts), token(key.closing_loc))
+ end
+ else
+ value = node.value.value
+
+ implicit_value = if value.is_a?(CallNode)
+ builder.call_method(nil, nil, [value.name, srange(value.message_loc)])
+ elsif value.is_a?(ConstantReadNode)
+ builder.const([value.name, srange(key.value_loc)])
+ else
+ builder.ident([value.name, srange(key.value_loc)]).updated(:lvar)
+ end
+
+ builder.pair_keyword([key.unescaped, srange(key)], implicit_value)
+ end
+ elsif node.operator_loc
+ builder.pair(visit(key), token(node.operator_loc), visit(node.value))
+ elsif key.is_a?(SymbolNode) && key.opening_loc.nil?
+ builder.pair_keyword([key.unescaped, srange(key.location)], visit(node.value))
+ else
+ parts =
+ if key.is_a?(SymbolNode)
+ [builder.string_internal([key.unescaped, srange(key.value_loc)])]
+ else
+ visit_all(key.parts)
+ end
+
+ builder.pair_quoted(token(key.opening_loc), parts, token(key.closing_loc), visit(node.value))
+ end
+ end
+
+ # def foo(**); bar(**); end
+ # ^^
+ #
+ # { **foo }
+ # ^^^^^
+ def visit_assoc_splat_node(node)
+ if in_pattern
+ builder.match_rest(token(node.operator_loc), token(node.value&.location))
+ elsif node.value.nil? && forwarding.include?(:**)
+ builder.forwarded_kwrestarg(token(node.operator_loc))
+ else
+ builder.kwsplat(token(node.operator_loc), visit(node.value))
+ end
+ end
+
+ # $+
+ # ^^
+ def visit_back_reference_read_node(node)
+ builder.back_ref(token(node.location))
+ end
+
+ # begin end
+ # ^^^^^^^^^
+ def visit_begin_node(node)
+ rescue_bodies = []
+
+ if (rescue_clause = node.rescue_clause)
+ begin
+ find_start_offset = (rescue_clause.reference&.location || rescue_clause.exceptions.last&.location || rescue_clause.keyword_loc).end_offset
+ find_end_offset = (
+ rescue_clause.statements&.location&.start_offset ||
+ rescue_clause.subsequent&.location&.start_offset ||
+ node.else_clause&.location&.start_offset ||
+ node.ensure_clause&.location&.start_offset ||
+ node.end_keyword_loc&.start_offset ||
+ find_start_offset + 1
+ )
+
+ rescue_bodies << builder.rescue_body(
+ token(rescue_clause.keyword_loc),
+ rescue_clause.exceptions.any? ? builder.array(nil, visit_all(rescue_clause.exceptions), nil) : nil,
+ token(rescue_clause.operator_loc),
+ visit(rescue_clause.reference),
+ srange_semicolon(find_start_offset, find_end_offset),
+ visit(rescue_clause.statements)
+ )
+ end until (rescue_clause = rescue_clause.subsequent).nil?
+ end
+
+ begin_body =
+ builder.begin_body(
+ visit(node.statements),
+ rescue_bodies,
+ token(node.else_clause&.else_keyword_loc),
+ visit(node.else_clause),
+ token(node.ensure_clause&.ensure_keyword_loc),
+ visit(node.ensure_clause&.statements)
+ )
+
+ if node.begin_keyword_loc
+ builder.begin_keyword(token(node.begin_keyword_loc), begin_body, token(node.end_keyword_loc))
+ else
+ begin_body
+ end
+ end
+
+ # foo(&bar)
+ # ^^^^
+ def visit_block_argument_node(node)
+ builder.block_pass(token(node.operator_loc), visit(node.expression))
+ end
+
+ # foo { |; bar| }
+ # ^^^
+ def visit_block_local_variable_node(node)
+ builder.shadowarg(token(node.location))
+ end
+
+ # A block on a keyword or method call.
+ def visit_block_node(node)
+ raise CompilationError, "Cannot directly compile block nodes"
+ end
+
+ # def foo(&bar); end
+ # ^^^^
+ def visit_block_parameter_node(node)
+ builder.blockarg(token(node.operator_loc), token(node.name_loc))
+ end
+
+ # A block's parameters.
+ def visit_block_parameters_node(node)
+ [*visit(node.parameters)].concat(visit_all(node.locals))
+ end
+
+ # break
+ # ^^^^^
+ #
+ # break foo
+ # ^^^^^^^^^
+ def visit_break_node(node)
+ builder.keyword_cmd(:break, token(node.keyword_loc), nil, visit(node.arguments) || [], nil)
+ end
+
+ # foo
+ # ^^^
+ #
+ # foo.bar
+ # ^^^^^^^
+ #
+ # foo.bar() {}
+ # ^^^^^^^^^^^^
+ def visit_call_node(node)
+ name = node.name
+ arguments = node.arguments&.arguments || []
+ block = node.block
+
+ if block.is_a?(BlockArgumentNode)
+ arguments = [*arguments, block]
+ block = nil
+ end
+
+ if node.call_operator_loc.nil?
+ case name
+ when :!
+ return visit_block(builder.not_op(token(node.message_loc), token(node.opening_loc), visit(node.receiver), token(node.closing_loc)), block)
+ when :=~
+ if (receiver = node.receiver).is_a?(RegularExpressionNode)
+ return builder.match_op(visit(receiver), token(node.message_loc), visit(node.arguments.arguments.first))
+ end
+ when :[]
+ return visit_block(builder.index(visit(node.receiver), token(node.opening_loc), visit_all(arguments), token(node.closing_loc)), block)
+ when :[]=
+ if node.message != "[]=" && node.arguments && block.nil? && !node.safe_navigation?
+ arguments = node.arguments.arguments[...-1]
+ arguments << node.block if node.block
+
+ return visit_block(
+ builder.assign(
+ builder.index_asgn(
+ visit(node.receiver),
+ token(node.opening_loc),
+ visit_all(arguments),
+ token(node.closing_loc),
+ ),
+ token(node.equal_loc),
+ visit(node.arguments.arguments.last)
+ ),
+ block
+ )
+ end
+ end
+ end
+
+ message_loc = node.message_loc
+ call_operator_loc = node.call_operator_loc
+ call_operator = [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)] if call_operator_loc
+
+ visit_block(
+ if name.end_with?("=") && !message_loc.slice.end_with?("=") && node.arguments && block.nil?
+ builder.assign(
+ builder.attr_asgn(visit(node.receiver), call_operator, token(message_loc)),
+ token(node.equal_loc),
+ visit(node.arguments.arguments.last)
+ )
+ else
+ builder.call_method(
+ visit(node.receiver),
+ call_operator,
+ message_loc ? [node.name, srange(message_loc)] : nil,
+ token(node.opening_loc),
+ visit_all(arguments),
+ token(node.closing_loc)
+ )
+ end,
+ block
+ )
+ end
+
+ # foo.bar += baz
+ # ^^^^^^^^^^^^^^^
+ def visit_call_operator_write_node(node)
+ call_operator_loc = node.call_operator_loc
+
+ builder.op_assign(
+ builder.call_method(
+ visit(node.receiver),
+ call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)],
+ node.message_loc ? [node.read_name, srange(node.message_loc)] : nil,
+ nil,
+ [],
+ nil
+ ),
+ [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # foo.bar &&= baz
+ # ^^^^^^^^^^^^^^^
+ def visit_call_and_write_node(node)
+ call_operator_loc = node.call_operator_loc
+
+ builder.op_assign(
+ builder.call_method(
+ visit(node.receiver),
+ call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)],
+ node.message_loc ? [node.read_name, srange(node.message_loc)] : nil,
+ nil,
+ [],
+ nil
+ ),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # foo.bar ||= baz
+ # ^^^^^^^^^^^^^^^
+ def visit_call_or_write_node(node)
+ call_operator_loc = node.call_operator_loc
+
+ builder.op_assign(
+ builder.call_method(
+ visit(node.receiver),
+ call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)],
+ node.message_loc ? [node.read_name, srange(node.message_loc)] : nil,
+ nil,
+ [],
+ nil
+ ),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # foo.bar, = 1
+ # ^^^^^^^
+ def visit_call_target_node(node)
+ call_operator_loc = node.call_operator_loc
+
+ builder.attr_asgn(
+ visit(node.receiver),
+ call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)],
+ token(node.message_loc)
+ )
+ end
+
+ # foo => bar => baz
+ # ^^^^^^^^^^
+ def visit_capture_pattern_node(node)
+ builder.match_as(visit(node.value), token(node.operator_loc), visit(node.target))
+ end
+
+ # case foo; when bar; end
+ # ^^^^^^^^^^^^^^^^^^^^^^^
+ def visit_case_node(node)
+ builder.case(
+ token(node.case_keyword_loc),
+ visit(node.predicate),
+ visit_all(node.conditions),
+ token(node.else_clause&.else_keyword_loc),
+ visit(node.else_clause),
+ token(node.end_keyword_loc)
+ )
+ end
+
+ # case foo; in bar; end
+ # ^^^^^^^^^^^^^^^^^^^^^
+ def visit_case_match_node(node)
+ builder.case_match(
+ token(node.case_keyword_loc),
+ visit(node.predicate),
+ visit_all(node.conditions),
+ token(node.else_clause&.else_keyword_loc),
+ visit(node.else_clause),
+ token(node.end_keyword_loc)
+ )
+ end
+
+ # class Foo; end
+ # ^^^^^^^^^^^^^^
+ def visit_class_node(node)
+ builder.def_class(
+ token(node.class_keyword_loc),
+ visit(node.constant_path),
+ token(node.inheritance_operator_loc),
+ visit(node.superclass),
+ node.body&.accept(copy_compiler(forwarding: [])),
+ token(node.end_keyword_loc)
+ )
+ end
+
+ # @@foo
+ # ^^^^^
+ def visit_class_variable_read_node(node)
+ builder.cvar(token(node.location))
+ end
+
+ # @@foo = 1
+ # ^^^^^^^^^
+ def visit_class_variable_write_node(node)
+ builder.assign(
+ builder.assignable(builder.cvar(token(node.name_loc))),
+ token(node.operator_loc),
+ visit(node.value)
+ )
+ end
+
+ # @@foo += bar
+ # ^^^^^^^^^^^^
+ def visit_class_variable_operator_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.cvar(token(node.name_loc))),
+ [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # @@foo &&= bar
+ # ^^^^^^^^^^^^^
+ def visit_class_variable_and_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.cvar(token(node.name_loc))),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # @@foo ||= bar
+ # ^^^^^^^^^^^^^
+ def visit_class_variable_or_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.cvar(token(node.name_loc))),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # @@foo, = bar
+ # ^^^^^
+ def visit_class_variable_target_node(node)
+ builder.assignable(builder.cvar(token(node.location)))
+ end
+
+ # Foo
+ # ^^^
+ def visit_constant_read_node(node)
+ builder.const([node.name, srange(node.location)])
+ end
+
+ # Foo = 1
+ # ^^^^^^^
+ #
+ # Foo, Bar = 1
+ # ^^^ ^^^
+ def visit_constant_write_node(node)
+ builder.assign(builder.assignable(builder.const([node.name, srange(node.name_loc)])), token(node.operator_loc), visit(node.value))
+ end
+
+ # Foo += bar
+ # ^^^^^^^^^^^
+ def visit_constant_operator_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.const([node.name, srange(node.name_loc)])),
+ [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # Foo &&= bar
+ # ^^^^^^^^^^^^
+ def visit_constant_and_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.const([node.name, srange(node.name_loc)])),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # Foo ||= bar
+ # ^^^^^^^^^^^^
+ def visit_constant_or_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.const([node.name, srange(node.name_loc)])),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # Foo, = bar
+ # ^^^
+ def visit_constant_target_node(node)
+ builder.assignable(builder.const([node.name, srange(node.location)]))
+ end
+
+ # Foo::Bar
+ # ^^^^^^^^
+ def visit_constant_path_node(node)
+ if node.parent.nil?
+ builder.const_global(
+ token(node.delimiter_loc),
+ [node.name, srange(node.name_loc)]
+ )
+ else
+ builder.const_fetch(
+ visit(node.parent),
+ token(node.delimiter_loc),
+ [node.name, srange(node.name_loc)]
+ )
+ end
+ end
+
+ # Foo::Bar = 1
+ # ^^^^^^^^^^^^
+ #
+ # Foo::Foo, Bar::Bar = 1
+ # ^^^^^^^^ ^^^^^^^^
+ def visit_constant_path_write_node(node)
+ builder.assign(
+ builder.assignable(visit(node.target)),
+ token(node.operator_loc),
+ visit(node.value)
+ )
+ end
+
+ # Foo::Bar += baz
+ # ^^^^^^^^^^^^^^^
+ def visit_constant_path_operator_write_node(node)
+ builder.op_assign(
+ builder.assignable(visit(node.target)),
+ [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # Foo::Bar &&= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_constant_path_and_write_node(node)
+ builder.op_assign(
+ builder.assignable(visit(node.target)),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # Foo::Bar ||= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_constant_path_or_write_node(node)
+ builder.op_assign(
+ builder.assignable(visit(node.target)),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # Foo::Bar, = baz
+ # ^^^^^^^^
+ def visit_constant_path_target_node(node)
+ builder.assignable(visit_constant_path_node(node))
+ end
+
+ # def foo; end
+ # ^^^^^^^^^^^^
+ #
+ # def self.foo; end
+ # ^^^^^^^^^^^^^^^^^
+ def visit_def_node(node)
+ if node.equal_loc
+ if node.receiver
+ builder.def_endless_singleton(
+ token(node.def_keyword_loc),
+ visit(node.receiver.is_a?(ParenthesesNode) ? node.receiver.body : node.receiver),
+ token(node.operator_loc),
+ token(node.name_loc),
+ builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false),
+ token(node.equal_loc),
+ node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters)))
+ )
+ else
+ builder.def_endless_method(
+ token(node.def_keyword_loc),
+ token(node.name_loc),
+ builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false),
+ token(node.equal_loc),
+ node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters)))
+ )
+ end
+ elsif node.receiver
+ builder.def_singleton(
+ token(node.def_keyword_loc),
+ visit(node.receiver.is_a?(ParenthesesNode) ? node.receiver.body : node.receiver),
+ token(node.operator_loc),
+ token(node.name_loc),
+ builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false),
+ node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters))),
+ token(node.end_keyword_loc)
+ )
+ else
+ builder.def_method(
+ token(node.def_keyword_loc),
+ token(node.name_loc),
+ builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false),
+ node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters))),
+ token(node.end_keyword_loc)
+ )
+ end
+ end
+
+ # defined? a
+ # ^^^^^^^^^^
+ #
+ # defined?(a)
+ # ^^^^^^^^^^^
+ def visit_defined_node(node)
+ # Very weird circumstances here where something like:
+ #
+ # defined?
+ # (1)
+ #
+ # gets parsed in Ruby as having only the `1` expression but in parser
+ # it gets parsed as having a begin. In this case we need to synthesize
+ # that begin to match parser's behavior.
+ if node.lparen_loc && node.keyword_loc.join(node.lparen_loc).slice.include?("\n")
+ builder.keyword_cmd(
+ :defined?,
+ token(node.keyword_loc),
+ nil,
+ [
+ builder.begin(
+ token(node.lparen_loc),
+ visit(node.value),
+ token(node.rparen_loc)
+ )
+ ],
+ nil
+ )
+ else
+ builder.keyword_cmd(
+ :defined?,
+ token(node.keyword_loc),
+ token(node.lparen_loc),
+ [visit(node.value)],
+ token(node.rparen_loc)
+ )
+ end
+ end
+
+ # if foo then bar else baz end
+ # ^^^^^^^^^^^^
+ def visit_else_node(node)
+ visit(node.statements)
+ end
+
+ # "foo #{bar}"
+ # ^^^^^^
+ def visit_embedded_statements_node(node)
+ builder.begin(
+ token(node.opening_loc),
+ visit(node.statements),
+ token(node.closing_loc)
+ )
+ end
+
+ # "foo #@bar"
+ # ^^^^^
+ def visit_embedded_variable_node(node)
+ visit(node.variable)
+ end
+
+ # begin; foo; ensure; bar; end
+ # ^^^^^^^^^^^^
+ def visit_ensure_node(node)
+ raise CompilationError, "Cannot directly compile ensure nodes"
+ end
+
+ # false
+ # ^^^^^
+ def visit_false_node(node)
+ builder.false(token(node.location))
+ end
+
+ # foo => [*, bar, *]
+ # ^^^^^^^^^^^
+ def visit_find_pattern_node(node)
+ elements = [node.left, *node.requireds, node.right]
+
+ if node.constant
+ builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.find_pattern(nil, visit_all(elements), nil), token(node.closing_loc))
+ else
+ builder.find_pattern(token(node.opening_loc), visit_all(elements), token(node.closing_loc))
+ end
+ end
+
+ # 1.0
+ # ^^^
+ def visit_float_node(node)
+ visit_numeric(node, builder.float([node.value, srange(node.location)]))
+ end
+
+ # for foo in bar do end
+ # ^^^^^^^^^^^^^^^^^^^^^
+ def visit_for_node(node)
+ builder.for(
+ token(node.for_keyword_loc),
+ visit(node.index),
+ token(node.in_keyword_loc),
+ visit(node.collection),
+ if (do_keyword_loc = node.do_keyword_loc)
+ token(do_keyword_loc)
+ else
+ srange_semicolon(node.collection.location.end_offset, (node.statements&.location || node.end_keyword_loc).start_offset)
+ end,
+ visit(node.statements),
+ token(node.end_keyword_loc)
+ )
+ end
+
+ # def foo(...); bar(...); end
+ # ^^^
+ def visit_forwarding_arguments_node(node)
+ builder.forwarded_args(token(node.location))
+ end
+
+ # def foo(...); end
+ # ^^^
+ def visit_forwarding_parameter_node(node)
+ builder.forward_arg(token(node.location))
+ end
+
+ # super
+ # ^^^^^
+ #
+ # super {}
+ # ^^^^^^^^
+ def visit_forwarding_super_node(node)
+ visit_block(
+ builder.keyword_cmd(
+ :zsuper,
+ ["super", srange_offsets(node.location.start_offset, node.location.start_offset + 5)]
+ ),
+ node.block
+ )
+ end
+
+ # $foo
+ # ^^^^
+ def visit_global_variable_read_node(node)
+ builder.gvar(token(node.location))
+ end
+
+ # $foo = 1
+ # ^^^^^^^^
+ def visit_global_variable_write_node(node)
+ builder.assign(
+ builder.assignable(builder.gvar(token(node.name_loc))),
+ token(node.operator_loc),
+ visit(node.value)
+ )
+ end
+
+ # $foo += bar
+ # ^^^^^^^^^^^
+ def visit_global_variable_operator_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.gvar(token(node.name_loc))),
+ [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # $foo &&= bar
+ # ^^^^^^^^^^^^
+ def visit_global_variable_and_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.gvar(token(node.name_loc))),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # $foo ||= bar
+ # ^^^^^^^^^^^^
+ def visit_global_variable_or_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.gvar(token(node.name_loc))),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # $foo, = bar
+ # ^^^^
+ def visit_global_variable_target_node(node)
+ builder.assignable(builder.gvar([node.slice, srange(node.location)]))
+ end
+
+ # {}
+ # ^^
+ def visit_hash_node(node)
+ builder.associate(
+ token(node.opening_loc),
+ visit_all(node.elements),
+ token(node.closing_loc)
+ )
+ end
+
+ # foo => {}
+ # ^^
+ def visit_hash_pattern_node(node)
+ elements = [*node.elements, *node.rest]
+
+ if node.constant
+ builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.hash_pattern(nil, visit_all(elements), nil), token(node.closing_loc))
+ else
+ builder.hash_pattern(token(node.opening_loc), visit_all(elements), token(node.closing_loc))
+ end
+ end
+
+ # if foo then bar end
+ # ^^^^^^^^^^^^^^^^^^^
+ #
+ # bar if foo
+ # ^^^^^^^^^^
+ #
+ # foo ? bar : baz
+ # ^^^^^^^^^^^^^^^
+ def visit_if_node(node)
+ if !node.if_keyword_loc
+ builder.ternary(
+ visit(node.predicate),
+ token(node.then_keyword_loc),
+ visit(node.statements),
+ token(node.subsequent.else_keyword_loc),
+ visit(node.subsequent)
+ )
+ elsif node.if_keyword_loc.start_offset == node.location.start_offset
+ builder.condition(
+ token(node.if_keyword_loc),
+ visit(node.predicate),
+ if (then_keyword_loc = node.then_keyword_loc)
+ token(then_keyword_loc)
+ else
+ srange_semicolon(node.predicate.location.end_offset, (node.statements&.location || node.subsequent&.location || node.end_keyword_loc).start_offset)
+ end,
+ visit(node.statements),
+ case node.subsequent
+ when IfNode
+ token(node.subsequent.if_keyword_loc)
+ when ElseNode
+ token(node.subsequent.else_keyword_loc)
+ end,
+ visit(node.subsequent),
+ if node.if_keyword != "elsif"
+ token(node.end_keyword_loc)
+ end
+ )
+ else
+ builder.condition_mod(
+ visit(node.statements),
+ visit(node.subsequent),
+ token(node.if_keyword_loc),
+ visit(node.predicate)
+ )
+ end
+ end
+
+ # 1i
+ # ^^
+ def visit_imaginary_node(node)
+ visit_numeric(node, builder.complex([Complex(0, node.numeric.value), srange(node.location)]))
+ end
+
+ # { foo: }
+ # ^^^^
+ def visit_implicit_node(node)
+ raise CompilationError, "Cannot directly compile implicit nodes"
+ end
+
+ # foo { |bar,| }
+ # ^
+ def visit_implicit_rest_node(node)
+ raise CompilationError, "Cannot compile implicit rest nodes"
+ end
+
+ # case foo; in bar; end
+ # ^^^^^^^^^^^^^^^^^^^^^
+ def visit_in_node(node)
+ pattern = nil
+ guard = nil
+
+ case node.pattern
+ when IfNode
+ pattern = within_pattern { |compiler| node.pattern.statements.accept(compiler) }
+ guard = builder.if_guard(token(node.pattern.if_keyword_loc), visit(node.pattern.predicate))
+ when UnlessNode
+ pattern = within_pattern { |compiler| node.pattern.statements.accept(compiler) }
+ guard = builder.unless_guard(token(node.pattern.keyword_loc), visit(node.pattern.predicate))
+ else
+ pattern = within_pattern { |compiler| node.pattern.accept(compiler) }
+ end
+
+ builder.in_pattern(
+ token(node.in_loc),
+ pattern,
+ guard,
+ if (then_loc = node.then_loc)
+ token(then_loc)
+ else
+ srange_semicolon(node.pattern.location.end_offset, node.statements&.location&.start_offset)
+ end,
+ visit(node.statements)
+ )
+ end
+
+ # foo[bar] += baz
+ # ^^^^^^^^^^^^^^^
+ def visit_index_operator_write_node(node)
+ arguments = node.arguments&.arguments || []
+ arguments << node.block if node.block
+
+ builder.op_assign(
+ builder.index(
+ visit(node.receiver),
+ token(node.opening_loc),
+ visit_all(arguments),
+ token(node.closing_loc)
+ ),
+ [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # foo[bar] &&= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_index_and_write_node(node)
+ arguments = node.arguments&.arguments || []
+ arguments << node.block if node.block
+
+ builder.op_assign(
+ builder.index(
+ visit(node.receiver),
+ token(node.opening_loc),
+ visit_all(arguments),
+ token(node.closing_loc)
+ ),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # foo[bar] ||= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_index_or_write_node(node)
+ arguments = node.arguments&.arguments || []
+ arguments << node.block if node.block
+
+ builder.op_assign(
+ builder.index(
+ visit(node.receiver),
+ token(node.opening_loc),
+ visit_all(arguments),
+ token(node.closing_loc)
+ ),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # foo[bar], = 1
+ # ^^^^^^^^
+ def visit_index_target_node(node)
+ builder.index_asgn(
+ visit(node.receiver),
+ token(node.opening_loc),
+ visit_all(node.arguments&.arguments || []),
+ token(node.closing_loc),
+ )
+ end
+
+ # @foo
+ # ^^^^
+ def visit_instance_variable_read_node(node)
+ builder.ivar(token(node.location))
+ end
+
+ # @foo = 1
+ # ^^^^^^^^
+ def visit_instance_variable_write_node(node)
+ builder.assign(
+ builder.assignable(builder.ivar(token(node.name_loc))),
+ token(node.operator_loc),
+ visit(node.value)
+ )
+ end
+
+ # @foo += bar
+ # ^^^^^^^^^^^
+ def visit_instance_variable_operator_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.ivar(token(node.name_loc))),
+ [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # @foo &&= bar
+ # ^^^^^^^^^^^^
+ def visit_instance_variable_and_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.ivar(token(node.name_loc))),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # @foo ||= bar
+ # ^^^^^^^^^^^^
+ def visit_instance_variable_or_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.ivar(token(node.name_loc))),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # @foo, = bar
+ # ^^^^
+ def visit_instance_variable_target_node(node)
+ builder.assignable(builder.ivar(token(node.location)))
+ end
+
+ # 1
+ # ^
+ def visit_integer_node(node)
+ visit_numeric(node, builder.integer([node.value, srange(node.location)]))
+ end
+
+ # /foo #{bar}/
+ # ^^^^^^^^^^^^
+ def visit_interpolated_regular_expression_node(node)
+ builder.regexp_compose(
+ token(node.opening_loc),
+ string_nodes_from_interpolation(node, node.opening),
+ [node.closing[0], srange_offsets(node.closing_loc.start_offset, node.closing_loc.start_offset + 1)],
+ builder.regexp_options([node.closing[1..], srange_offsets(node.closing_loc.start_offset + 1, node.closing_loc.end_offset)])
+ )
+ end
+
+ # if /foo #{bar}/ then end
+ # ^^^^^^^^^^^^
+ alias visit_interpolated_match_last_line_node visit_interpolated_regular_expression_node
+
+ # "foo #{bar}"
+ # ^^^^^^^^^^^^
+ def visit_interpolated_string_node(node)
+ if node.heredoc?
+ return visit_heredoc(node) { |children, closing| builder.string_compose(token(node.opening_loc), children, closing) }
+ end
+
+ builder.string_compose(
+ token(node.opening_loc),
+ string_nodes_from_interpolation(node, node.opening),
+ token(node.closing_loc)
+ )
+ end
+
+ # :"foo #{bar}"
+ # ^^^^^^^^^^^^^
+ def visit_interpolated_symbol_node(node)
+ builder.symbol_compose(
+ token(node.opening_loc),
+ string_nodes_from_interpolation(node, node.opening),
+ token(node.closing_loc)
+ )
+ end
+
+ # `foo #{bar}`
+ # ^^^^^^^^^^^^
+ def visit_interpolated_x_string_node(node)
+ if node.heredoc?
+ return visit_heredoc(node) { |children, closing| builder.xstring_compose(token(node.opening_loc), children, closing) }
+ end
+
+ builder.xstring_compose(
+ token(node.opening_loc),
+ string_nodes_from_interpolation(node, node.opening),
+ token(node.closing_loc)
+ )
+ end
+
+ # -> { it }
+ # ^^
+ def visit_it_local_variable_read_node(node)
+ builder.ident([:it, srange(node.location)]).updated(:lvar)
+ end
+
+ # -> { it }
+ # ^^^^^^^^^
+ def visit_it_parameters_node(node)
+ # FIXME: The builder _should_ always be a subclass of the prism builder.
+ # Currently RuboCop passes in its own builder that always inherits from the
+ # parser builder (which is lacking the `itarg` method). Once rubocop-ast
+ # opts in to use the custom prism builder a warning can be emitted when
+ # it is not the expected class, and eventually raise.
+ # https://github.com/rubocop/rubocop-ast/pull/354
+ if builder.is_a?(Translation::Parser::Builder)
+ builder.itarg
+ else
+ builder.args(nil, [], nil, false)
+ end
+ end
+
+ # foo(bar: baz)
+ # ^^^^^^^^
+ def visit_keyword_hash_node(node)
+ builder.associate(nil, visit_all(node.elements), nil)
+ end
+
+ # def foo(**bar); end
+ # ^^^^^
+ #
+ # def foo(**); end
+ # ^^
+ def visit_keyword_rest_parameter_node(node)
+ builder.kwrestarg(
+ token(node.operator_loc),
+ node.name ? [node.name, srange(node.name_loc)] : nil
+ )
+ end
+
+ # -> {}
+ # ^^^^^
+ def visit_lambda_node(node)
+ parameters = node.parameters
+ implicit_parameters = parameters.is_a?(NumberedParametersNode) || parameters.is_a?(ItParametersNode)
+
+ builder.block(
+ builder.call_lambda(token(node.operator_loc)),
+ [node.opening, srange(node.opening_loc)],
+ if parameters.nil?
+ builder.args(nil, [], nil, false)
+ elsif implicit_parameters
+ visit(node.parameters)
+ else
+ builder.args(
+ token(node.parameters.opening_loc),
+ visit(node.parameters),
+ token(node.parameters.closing_loc),
+ false
+ )
+ end,
+ visit(node.body),
+ [node.closing, srange(node.closing_loc)]
+ )
+ end
+
+ # foo
+ # ^^^
+ def visit_local_variable_read_node(node)
+ builder.ident([node.name, srange(node.location)]).updated(:lvar)
+ end
+
+ # foo = 1
+ # ^^^^^^^
+ def visit_local_variable_write_node(node)
+ builder.assign(
+ builder.assignable(builder.ident(token(node.name_loc))),
+ token(node.operator_loc),
+ visit(node.value)
+ )
+ end
+
+ # foo += bar
+ # ^^^^^^^^^^
+ def visit_local_variable_operator_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.ident(token(node.name_loc))),
+ [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # foo &&= bar
+ # ^^^^^^^^^^^
+ def visit_local_variable_and_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.ident(token(node.name_loc))),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # foo ||= bar
+ # ^^^^^^^^^^^
+ def visit_local_variable_or_write_node(node)
+ builder.op_assign(
+ builder.assignable(builder.ident(token(node.name_loc))),
+ [node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
+ visit(node.value)
+ )
+ end
+
+ # foo, = bar
+ # ^^^
+ def visit_local_variable_target_node(node)
+ if in_pattern
+ builder.assignable(builder.match_var([node.name, srange(node.location)]))
+ else
+ builder.assignable(builder.ident(token(node.location)))
+ end
+ end
+
+ # foo in bar
+ # ^^^^^^^^^^
+ def visit_match_predicate_node(node)
+ builder.match_pattern_p(
+ visit(node.value),
+ token(node.operator_loc),
+ within_pattern { |compiler| node.pattern.accept(compiler) }
+ )
+ end
+
+ # foo => bar
+ # ^^^^^^^^^^
+ def visit_match_required_node(node)
+ builder.match_pattern(
+ visit(node.value),
+ token(node.operator_loc),
+ within_pattern { |compiler| node.pattern.accept(compiler) }
+ )
+ end
+
+ # /(?<foo>foo)/ =~ bar
+ # ^^^^^^^^^^^^^^^^^^^^
+ def visit_match_write_node(node)
+ builder.match_op(
+ visit(node.call.receiver),
+ token(node.call.message_loc),
+ visit(node.call.arguments.arguments.first)
+ )
+ end
+
+ # A node that is missing from the syntax tree. This is only used in the
+ # case of a syntax error. The parser gem doesn't have such a concept, so
+ # we invent our own here.
+ def visit_error_recovery_node(node)
+ ::AST::Node.new(:missing, [], location: ::Parser::Source::Map.new(srange(node.location)))
+ end
+
+ # module Foo; end
+ # ^^^^^^^^^^^^^^^
+ def visit_module_node(node)
+ builder.def_module(
+ token(node.module_keyword_loc),
+ visit(node.constant_path),
+ node.body&.accept(copy_compiler(forwarding: [])),
+ token(node.end_keyword_loc)
+ )
+ end
+
+ # foo, bar = baz
+ # ^^^^^^^^
+ def visit_multi_target_node(node)
+ builder.multi_lhs(
+ token(node.lparen_loc),
+ visit_all(multi_target_elements(node)),
+ token(node.rparen_loc)
+ )
+ end
+
+ # foo, bar = baz
+ # ^^^^^^^^^^^^^^
+ def visit_multi_write_node(node)
+ elements = multi_target_elements(node)
+
+ if elements.length == 1 && elements.first.is_a?(MultiTargetNode) && !node.rest
+ elements = multi_target_elements(elements.first)
+ end
+
+ builder.multi_assign(
+ builder.multi_lhs(
+ token(node.lparen_loc),
+ visit_all(elements),
+ token(node.rparen_loc)
+ ),
+ token(node.operator_loc),
+ visit(node.value)
+ )
+ end
+
+ # next
+ # ^^^^
+ #
+ # next foo
+ # ^^^^^^^^
+ def visit_next_node(node)
+ builder.keyword_cmd(
+ :next,
+ token(node.keyword_loc),
+ nil,
+ visit(node.arguments) || [],
+ nil
+ )
+ end
+
+ # nil
+ # ^^^
+ def visit_nil_node(node)
+ builder.nil(token(node.location))
+ end
+
+ # def foo(&nil); end
+ # ^^^^
+ def visit_no_block_parameter_node(node)
+ builder.blocknilarg(token(node.operator_loc), token(node.keyword_loc))
+ end
+
+ # def foo(**nil); end
+ # ^^^^^
+ def visit_no_keywords_parameter_node(node)
+ if in_pattern
+ builder.match_nil_pattern(token(node.operator_loc), token(node.keyword_loc))
+ else
+ builder.kwnilarg(token(node.operator_loc), token(node.keyword_loc))
+ end
+ end
+
+ # -> { _1 + _2 }
+ # ^^^^^^^^^^^^^^
+ def visit_numbered_parameters_node(node)
+ builder.numargs(node.maximum)
+ end
+
+ # $1
+ # ^^
+ def visit_numbered_reference_read_node(node)
+ builder.nth_ref([node.number, srange(node.location)])
+ end
+
+ # def foo(bar: baz); end
+ # ^^^^^^^^
+ def visit_optional_keyword_parameter_node(node)
+ builder.kwoptarg([node.name, srange(node.name_loc)], visit(node.value))
+ end
+
+ # def foo(bar = 1); end
+ # ^^^^^^^
+ def visit_optional_parameter_node(node)
+ builder.optarg(token(node.name_loc), token(node.operator_loc), visit(node.value))
+ end
+
+ # a or b
+ # ^^^^^^
+ def visit_or_node(node)
+ builder.logical_op(:or, visit(node.left), token(node.operator_loc), visit(node.right))
+ end
+
+ # def foo(bar, *baz); end
+ # ^^^^^^^^^
+ def visit_parameters_node(node)
+ params = []
+
+ if node.requireds.any?
+ node.requireds.each do |required|
+ params <<
+ if required.is_a?(RequiredParameterNode)
+ visit(required)
+ else
+ required.accept(copy_compiler(in_destructure: true))
+ end
+ end
+ end
+
+ params.concat(visit_all(node.optionals)) if node.optionals.any?
+ params << visit(node.rest) if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode)
+
+ if node.posts.any?
+ node.posts.each do |post|
+ params <<
+ if post.is_a?(RequiredParameterNode)
+ visit(post)
+ else
+ post.accept(copy_compiler(in_destructure: true))
+ end
+ end
+ end
+
+ params.concat(visit_all(node.keywords)) if node.keywords.any?
+ params << visit(node.keyword_rest) if !node.keyword_rest.nil?
+ params << visit(node.block) if !node.block.nil?
+ params
+ end
+
+ # ()
+ # ^^
+ #
+ # (1)
+ # ^^^
+ def visit_parentheses_node(node)
+ builder.begin(
+ token(node.opening_loc),
+ visit(node.body),
+ token(node.closing_loc)
+ )
+ end
+
+ # foo => ^(bar)
+ # ^^^^^^
+ def visit_pinned_expression_node(node)
+ parts = node.expression.accept(copy_compiler(in_pattern: false)) # Don't treat * and similar as match_rest
+ expression = builder.begin(token(node.lparen_loc), parts, token(node.rparen_loc))
+ builder.pin(token(node.operator_loc), expression)
+ end
+
+ # foo = 1 and bar => ^foo
+ # ^^^^
+ def visit_pinned_variable_node(node)
+ builder.pin(token(node.operator_loc), visit(node.variable))
+ end
+
+ # END {}
+ def visit_post_execution_node(node)
+ builder.postexe(
+ token(node.keyword_loc),
+ token(node.opening_loc),
+ visit(node.statements),
+ token(node.closing_loc)
+ )
+ end
+
+ # BEGIN {}
+ def visit_pre_execution_node(node)
+ builder.preexe(
+ token(node.keyword_loc),
+ token(node.opening_loc),
+ visit(node.statements),
+ token(node.closing_loc)
+ )
+ end
+
+ # The top-level program node.
+ def visit_program_node(node)
+ visit(node.statements)
+ end
+
+ # 0..5
+ # ^^^^
+ def visit_range_node(node)
+ if node.exclude_end?
+ builder.range_exclusive(
+ visit(node.left),
+ token(node.operator_loc),
+ visit(node.right)
+ )
+ else
+ builder.range_inclusive(
+ visit(node.left),
+ token(node.operator_loc),
+ visit(node.right)
+ )
+ end
+ end
+
+ # if foo .. bar; end
+ # ^^^^^^^^^^
+ alias visit_flip_flop_node visit_range_node
+
+ # 1r
+ # ^^
+ def visit_rational_node(node)
+ visit_numeric(node, builder.rational([node.value, srange(node.location)]))
+ end
+
+ # redo
+ # ^^^^
+ def visit_redo_node(node)
+ builder.keyword_cmd(:redo, token(node.location))
+ end
+
+ # /foo/
+ # ^^^^^
+ def visit_regular_expression_node(node)
+ parts =
+ if node.content == ""
+ []
+ elsif node.content.include?("\n")
+ string_nodes_from_line_continuations(node.unescaped, node.content, node.content_loc.start_offset, node.opening)
+ else
+ [builder.string_internal([node.unescaped, srange(node.content_loc)])]
+ end
+
+ builder.regexp_compose(
+ token(node.opening_loc),
+ parts,
+ [node.closing[0], srange_offsets(node.closing_loc.start_offset, node.closing_loc.start_offset + 1)],
+ builder.regexp_options([node.closing[1..], srange_offsets(node.closing_loc.start_offset + 1, node.closing_loc.end_offset)])
+ )
+ end
+
+ # if /foo/ then end
+ # ^^^^^
+ alias visit_match_last_line_node visit_regular_expression_node
+
+ # def foo(bar:); end
+ # ^^^^
+ def visit_required_keyword_parameter_node(node)
+ builder.kwarg([node.name, srange(node.name_loc)])
+ end
+
+ # def foo(bar); end
+ # ^^^
+ def visit_required_parameter_node(node)
+ builder.arg(token(node.location))
+ end
+
+ # foo rescue bar
+ # ^^^^^^^^^^^^^^
+ def visit_rescue_modifier_node(node)
+ builder.begin_body(
+ visit(node.expression),
+ [
+ builder.rescue_body(
+ token(node.keyword_loc),
+ nil,
+ nil,
+ nil,
+ nil,
+ visit(node.rescue_expression)
+ )
+ ]
+ )
+ end
+
+ # begin; rescue; end
+ # ^^^^^^^
+ def visit_rescue_node(node)
+ raise CompilationError, "Cannot directly compile rescue nodes"
+ end
+
+ # def foo(*bar); end
+ # ^^^^
+ #
+ # def foo(*); end
+ # ^
+ def visit_rest_parameter_node(node)
+ builder.restarg(token(node.operator_loc), token(node.name_loc))
+ end
+
+ # retry
+ # ^^^^^
+ def visit_retry_node(node)
+ builder.keyword_cmd(:retry, token(node.location))
+ end
+
+ # return
+ # ^^^^^^
+ #
+ # return 1
+ # ^^^^^^^^
+ def visit_return_node(node)
+ builder.keyword_cmd(
+ :return,
+ token(node.keyword_loc),
+ nil,
+ visit(node.arguments) || [],
+ nil
+ )
+ end
+
+ # self
+ # ^^^^
+ def visit_self_node(node)
+ builder.self(token(node.location))
+ end
+
+ # A shareable constant.
+ def visit_shareable_constant_node(node)
+ visit(node.write)
+ end
+
+ # class << self; end
+ # ^^^^^^^^^^^^^^^^^^
+ def visit_singleton_class_node(node)
+ builder.def_sclass(
+ token(node.class_keyword_loc),
+ token(node.operator_loc),
+ visit(node.expression),
+ node.body&.accept(copy_compiler(forwarding: [])),
+ token(node.end_keyword_loc)
+ )
+ end
+
+ # __ENCODING__
+ # ^^^^^^^^^^^^
+ def visit_source_encoding_node(node)
+ builder.accessible(builder.__ENCODING__(token(node.location)))
+ end
+
+ # __FILE__
+ # ^^^^^^^^
+ def visit_source_file_node(node)
+ builder.accessible(builder.__FILE__(token(node.location)))
+ end
+
+ # __LINE__
+ # ^^^^^^^^
+ def visit_source_line_node(node)
+ builder.accessible(builder.__LINE__(token(node.location)))
+ end
+
+ # foo(*bar)
+ # ^^^^
+ #
+ # def foo((bar, *baz)); end
+ # ^^^^
+ #
+ # def foo(*); bar(*); end
+ # ^
+ def visit_splat_node(node)
+ if node.expression.nil? && forwarding.include?(:*)
+ builder.forwarded_restarg(token(node.operator_loc))
+ elsif in_destructure
+ builder.restarg(token(node.operator_loc), token(node.expression&.location))
+ elsif in_pattern
+ builder.match_rest(token(node.operator_loc), token(node.expression&.location))
+ else
+ builder.splat(token(node.operator_loc), visit(node.expression))
+ end
+ end
+
+ # A list of statements.
+ def visit_statements_node(node)
+ builder.compstmt(visit_all(node.body))
+ end
+
+ # "foo"
+ # ^^^^^
+ def visit_string_node(node)
+ if node.heredoc?
+ visit_heredoc(node.to_interpolated) { |children, closing| builder.string_compose(token(node.opening_loc), children, closing) }
+ elsif node.opening == "?"
+ builder.character([node.unescaped, srange(node.location)])
+ elsif node.opening&.start_with?("%") && node.unescaped.empty?
+ builder.string_compose(token(node.opening_loc), [], token(node.closing_loc))
+ else
+ parts =
+ if node.content.include?("\n")
+ string_nodes_from_line_continuations(node.unescaped, node.content, node.content_loc.start_offset, node.opening)
+ else
+ [builder.string_internal([node.unescaped, srange(node.content_loc)])]
+ end
+
+ builder.string_compose(
+ token(node.opening_loc),
+ parts,
+ token(node.closing_loc)
+ )
+ end
+ end
+
+ # super(foo)
+ # ^^^^^^^^^^
+ def visit_super_node(node)
+ arguments = node.arguments&.arguments || []
+ block = node.block
+
+ if block.is_a?(BlockArgumentNode)
+ arguments = [*arguments, block]
+ block = nil
+ end
+
+ visit_block(
+ builder.keyword_cmd(
+ :super,
+ token(node.keyword_loc),
+ token(node.lparen_loc),
+ visit_all(arguments),
+ token(node.rparen_loc)
+ ),
+ block
+ )
+ end
+
+ # :foo
+ # ^^^^
+ def visit_symbol_node(node)
+ if node.closing_loc.nil?
+ if node.opening_loc.nil?
+ builder.symbol_internal([node.unescaped, srange(node.location)])
+ else
+ builder.symbol([node.unescaped, srange(node.location)])
+ end
+ else
+ parts =
+ if node.value_loc.nil?
+ []
+ elsif node.value.include?("\n")
+ string_nodes_from_line_continuations(node.unescaped, node.value, node.value_loc.start_offset, node.opening)
+ else
+ [builder.string_internal([node.unescaped, srange(node.value_loc)])]
+ end
+
+ builder.symbol_compose(
+ token(node.opening_loc),
+ parts,
+ token(node.closing_loc)
+ )
+ end
+ end
+
+ # true
+ # ^^^^
+ def visit_true_node(node)
+ builder.true(token(node.location))
+ end
+
+ # undef foo
+ # ^^^^^^^^^
+ def visit_undef_node(node)
+ builder.undef_method(token(node.keyword_loc), visit_all(node.names))
+ end
+
+ # unless foo; bar end
+ # ^^^^^^^^^^^^^^^^^^^
+ #
+ # bar unless foo
+ # ^^^^^^^^^^^^^^
+ def visit_unless_node(node)
+ if node.keyword_loc.start_offset == node.location.start_offset
+ builder.condition(
+ token(node.keyword_loc),
+ visit(node.predicate),
+ if (then_keyword_loc = node.then_keyword_loc)
+ token(then_keyword_loc)
+ else
+ srange_semicolon(node.predicate.location.end_offset, (node.statements&.location || node.else_clause&.location || node.end_keyword_loc).start_offset)
+ end,
+ visit(node.else_clause),
+ token(node.else_clause&.else_keyword_loc),
+ visit(node.statements),
+ token(node.end_keyword_loc)
+ )
+ else
+ builder.condition_mod(
+ visit(node.else_clause),
+ visit(node.statements),
+ token(node.keyword_loc),
+ visit(node.predicate)
+ )
+ end
+ end
+
+ # until foo; bar end
+ # ^^^^^^^^^^^^^^^^^^
+ #
+ # bar until foo
+ # ^^^^^^^^^^^^^
+ def visit_until_node(node)
+ if node.location.start_offset == node.keyword_loc.start_offset
+ builder.loop(
+ :until,
+ token(node.keyword_loc),
+ visit(node.predicate),
+ if (do_keyword_loc = node.do_keyword_loc)
+ token(do_keyword_loc)
+ else
+ srange_semicolon(node.predicate.location.end_offset, (node.statements&.location || node.closing_loc).start_offset)
+ end,
+ visit(node.statements),
+ token(node.closing_loc)
+ )
+ else
+ builder.loop_mod(
+ :until,
+ visit(node.statements),
+ token(node.keyword_loc),
+ visit(node.predicate)
+ )
+ end
+ end
+
+ # case foo; when bar; end
+ # ^^^^^^^^^^^^^
+ def visit_when_node(node)
+ builder.when(
+ token(node.keyword_loc),
+ visit_all(node.conditions),
+ if (then_keyword_loc = node.then_keyword_loc)
+ token(then_keyword_loc)
+ else
+ srange_semicolon(node.conditions.last.location.end_offset, node.statements&.location&.start_offset)
+ end,
+ visit(node.statements)
+ )
+ end
+
+ # while foo; bar end
+ # ^^^^^^^^^^^^^^^^^^
+ #
+ # bar while foo
+ # ^^^^^^^^^^^^^
+ def visit_while_node(node)
+ if node.location.start_offset == node.keyword_loc.start_offset
+ builder.loop(
+ :while,
+ token(node.keyword_loc),
+ visit(node.predicate),
+ if (do_keyword_loc = node.do_keyword_loc)
+ token(do_keyword_loc)
+ else
+ srange_semicolon(node.predicate.location.end_offset, (node.statements&.location || node.closing_loc).start_offset)
+ end,
+ visit(node.statements),
+ token(node.closing_loc)
+ )
+ else
+ builder.loop_mod(
+ :while,
+ visit(node.statements),
+ token(node.keyword_loc),
+ visit(node.predicate)
+ )
+ end
+ end
+
+ # `foo`
+ # ^^^^^
+ def visit_x_string_node(node)
+ if node.heredoc?
+ return visit_heredoc(node.to_interpolated) { |children, closing| builder.xstring_compose(token(node.opening_loc), children, closing) }
+ end
+
+ parts =
+ if node.content == ""
+ []
+ elsif node.content.include?("\n")
+ string_nodes_from_line_continuations(node.unescaped, node.content, node.content_loc.start_offset, node.opening)
+ else
+ [builder.string_internal([node.unescaped, srange(node.content_loc)])]
+ end
+
+ builder.xstring_compose(
+ token(node.opening_loc),
+ parts,
+ token(node.closing_loc)
+ )
+ end
+
+ # yield
+ # ^^^^^
+ #
+ # yield 1
+ # ^^^^^^^
+ def visit_yield_node(node)
+ builder.keyword_cmd(
+ :yield,
+ token(node.keyword_loc),
+ token(node.lparen_loc),
+ visit(node.arguments) || [],
+ token(node.rparen_loc)
+ )
+ end
+
+ private
+
+ # Initialize a new compiler with the given option overrides, used to
+ # visit a subtree with the given options.
+ def copy_compiler(forwarding: self.forwarding, in_destructure: self.in_destructure, in_pattern: self.in_pattern)
+ Compiler.new(parser, offset_cache, forwarding: forwarding, in_destructure: in_destructure, in_pattern: in_pattern)
+ end
+
+ # When *, **, &, or ... are used as an argument in a method call, we
+ # check if they were allowed by the current context. To determine that
+ # we build this lookup table.
+ def find_forwarding(node)
+ return [] if node.nil?
+
+ forwarding = []
+ forwarding << :* if node.rest.is_a?(RestParameterNode) && node.rest.name.nil?
+ forwarding << :** if node.keyword_rest.is_a?(KeywordRestParameterNode) && node.keyword_rest.name.nil?
+ forwarding << :& if !node.block.nil? && node.block.name.nil?
+ forwarding |= [:&, :"..."] if node.keyword_rest.is_a?(ForwardingParameterNode)
+
+ forwarding
+ end
+
+ # Returns the set of targets for a MultiTargetNode or a MultiWriteNode.
+ def multi_target_elements(node)
+ elements = [*node.lefts]
+ elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode)
+ elements.concat(node.rights)
+ elements
+ end
+
+ # Blocks can have a special set of parameters that automatically expand
+ # when given arrays if they have a single required parameter and no
+ # other parameters.
+ def procarg0?(parameters)
+ parameters &&
+ parameters.requireds.length == 1 &&
+ parameters.optionals.empty? &&
+ parameters.rest.nil? &&
+ parameters.posts.empty? &&
+ parameters.keywords.empty? &&
+ parameters.keyword_rest.nil? &&
+ parameters.block.nil?
+ end
+
+ # Locations in the parser gem AST are generated using this class. We
+ # store a reference to its constant to make it slightly faster to look
+ # up.
+ Range = ::Parser::Source::Range
+
+ # Constructs a new source range from the given start and end offsets.
+ def srange(location)
+ Range.new(source_buffer, offset_cache[location.start_offset], offset_cache[location.end_offset]) if location
+ end
+
+ # Constructs a new source range from the given start and end offsets.
+ def srange_offsets(start_offset, end_offset)
+ Range.new(source_buffer, offset_cache[start_offset], offset_cache[end_offset])
+ end
+
+ # Constructs a new source range by finding a semicolon between the given
+ # start offset and end offset. If the semicolon is not found, it returns
+ # nil. Importantly it does not search past newlines or comments.
+ #
+ # Note that end_offset is allowed to be nil, in which case this will
+ # search until the end of the string.
+ def srange_semicolon(start_offset, end_offset)
+ if (match = source_buffer.source.byteslice(start_offset...end_offset)[/\A\s*;/])
+ final_offset = start_offset + match.bytesize
+ [";", Range.new(source_buffer, offset_cache[final_offset - 1], offset_cache[final_offset])]
+ end
+ end
+
+ # Transform a location into a token that the parser gem expects.
+ def token(location)
+ [location.slice, Range.new(source_buffer, offset_cache[location.start_offset], offset_cache[location.end_offset])] if location
+ end
+
+ # Visit a block node on a call.
+ def visit_block(call, block)
+ if block
+ parameters = block.parameters
+ implicit_parameters = parameters.is_a?(NumberedParametersNode) || parameters.is_a?(ItParametersNode)
+
+ builder.block(
+ call,
+ token(block.opening_loc),
+ if parameters.nil?
+ builder.args(nil, [], nil, false)
+ elsif implicit_parameters
+ visit(parameters)
+ else
+ builder.args(
+ token(parameters.opening_loc),
+ if procarg0?(parameters.parameters)
+ parameter = parameters.parameters.requireds.first
+ visited = parameter.is_a?(RequiredParameterNode) ? visit(parameter) : parameter.accept(copy_compiler(in_destructure: true))
+ [builder.procarg0(visited)].concat(visit_all(parameters.locals))
+ else
+ visit(parameters)
+ end,
+ token(parameters.closing_loc),
+ false
+ )
+ end,
+ visit(block.body),
+ token(block.closing_loc)
+ )
+ else
+ call
+ end
+ end
+
+ # Visit a heredoc that can be either a string or an xstring.
+ def visit_heredoc(node)
+ children = Array.new
+ indented = false
+
+ # If this is a dedenting heredoc, then we need to insert the opening
+ # content into the children as well.
+ if node.opening.start_with?("<<~") && node.parts.length > 0 && !node.parts.first.is_a?(StringNode)
+ location = node.parts.first.location
+ location = location.copy(start_offset: location.start_offset - location.start_line_slice.bytesize)
+ children << builder.string_internal(token(location))
+ indented = true
+ end
+
+ node.parts.each do |part|
+ pushing =
+ if part.is_a?(StringNode) && part.content.include?("\n")
+ string_nodes_from_line_continuations(part.unescaped, part.content, part.location.start_offset, node.opening)
+ else
+ [visit(part)]
+ end
+
+ pushing.each do |child|
+ if child.type == :str && child.children.last == ""
+ # nothing
+ elsif child.type == :str && children.last && children.last.type == :str && !children.last.children.first.end_with?("\n")
+ appendee = children[-1]
+
+ location = appendee.loc
+ location = location.with_expression(location.expression.join(child.loc.expression))
+
+ children[-1] = appendee.updated(:str, ["#{appendee.children.first}#{child.children.first}"], location: location)
+ else
+ children << child
+ end
+ end
+ end
+
+ closing = node.closing
+ closing_t = [closing.chomp, srange_offsets(node.closing_loc.start_offset, node.closing_loc.end_offset - (closing[/\s+$/]&.length || 0))]
+ composed = yield children, closing_t
+
+ composed = composed.updated(nil, children[1..-1]) if indented
+ composed
+ end
+
+ # Visit a numeric node and account for the optional sign.
+ def visit_numeric(node, value)
+ if (slice = node.slice).match?(/^[+-]/)
+ builder.unary_num(
+ [slice[0].to_sym, srange_offsets(node.location.start_offset, node.location.start_offset + 1)],
+ value
+ )
+ else
+ value
+ end
+ end
+
+ # Within the given block, track that we're within a pattern.
+ def within_pattern
+ begin
+ parser.pattern_variables.push
+ yield copy_compiler(in_pattern: true)
+ ensure
+ parser.pattern_variables.pop
+ end
+ end
+
+ # When the content of a string node is split across multiple lines, the
+ # parser gem creates individual string nodes for each line the content is part of.
+ def string_nodes_from_interpolation(node, opening)
+ node.parts.flat_map do |part|
+ if part.type == :string_node && part.content.include?("\n") && part.opening_loc.nil?
+ string_nodes_from_line_continuations(part.unescaped, part.content, part.content_loc.start_offset, opening)
+ else
+ visit(part)
+ end
+ end
+ end
+
+ # Create parser string nodes from a single prism node. The parser gem
+ # "glues" strings together when a line continuation is encountered.
+ def string_nodes_from_line_continuations(unescaped, escaped, start_offset, opening)
+ unescaped = unescaped.lines
+ escaped = escaped.lines
+ percent_array = opening&.start_with?("%w", "%W", "%i", "%I")
+ regex = opening == "/" || opening&.start_with?("%r")
+
+ # Non-interpolating strings
+ if opening&.end_with?("'") || opening&.start_with?("%q", "%s", "%w", "%i")
+ current_length = 0
+ current_line = +""
+
+ escaped.filter_map.with_index do |escaped_line, index|
+ unescaped_line = unescaped.fetch(index, "")
+ current_length += escaped_line.bytesize
+ current_line << unescaped_line
+
+ # Glue line continuations together. Only %w and %i arrays can contain these.
+ if percent_array && escaped_line[/(\\)*\n$/, 1]&.length&.odd?
+ next unless index == escaped.count - 1
+ end
+ s = builder.string_internal([current_line, srange_offsets(start_offset, start_offset + current_length)])
+ start_offset += escaped_line.bytesize
+ current_line = +""
+ current_length = 0
+ s
+ end
+ else
+ escaped_lengths = []
+ normalized_lengths = []
+ # Keeps track of where an unescaped line should start a new token. An unescaped
+ # \n would otherwise be indistinguishable from the actual newline at the end of
+ # of the line. The parser gem only emits a new string node at "real" newlines,
+ # line continuations don't start a new node as well.
+ do_next_tokens = []
+
+ escaped
+ .chunk_while { |before, after| before[/(\\*)\r?\n$/, 1]&.length&.odd? || false }
+ .each do |lines|
+ escaped_lengths << lines.sum(&:bytesize)
+
+ unescaped_lines_count =
+ if regex
+ 0 # Will always be preserved as is
+ else
+ lines.sum do |line|
+ count = line.scan(/(\\*)n/).count { |(backslashes)| backslashes&.length&.odd? }
+ count -= 1 if line.match?(/(?:\A|[^\\])(?:\\\\)*\\n\z/) && count > 0
+ count
+ end
+ end
+
+ extra = 1
+ extra = lines.count if percent_array # Account for line continuations in percent arrays
+
+ normalized_lengths.concat(Array.new(unescaped_lines_count + extra, 0))
+ normalized_lengths[-1] = lines.sum { |line| line.bytesize }
+ do_next_tokens.concat(Array.new(unescaped_lines_count + extra, false))
+ do_next_tokens[-1] = true
+ end
+
+ current_line = +""
+ current_normalized_length = 0
+
+ emitted_count = 0
+ unescaped.filter_map.with_index do |unescaped_line, index|
+ current_line << unescaped_line
+ current_normalized_length += normalized_lengths.fetch(index, 0)
+
+ if do_next_tokens[index]
+ inner_part = builder.string_internal([current_line, srange_offsets(start_offset, start_offset + current_normalized_length)])
+ start_offset += escaped_lengths.fetch(emitted_count, 0)
+ current_line = +""
+ current_normalized_length = 0
+ emitted_count += 1
+ inner_part
+ else
+ nil
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/prism/translation/parser/lexer.rb b/lib/prism/translation/parser/lexer.rb
new file mode 100644
index 0000000000..e82042867f
--- /dev/null
+++ b/lib/prism/translation/parser/lexer.rb
@@ -0,0 +1,819 @@
+# frozen_string_literal: true
+# :markup: markdown
+
+require "strscan"
+require_relative "../../polyfill/append_as_bytes"
+require_relative "../../polyfill/scan_byte"
+
+module Prism
+ module Translation
+ class Parser
+ # Accepts a list of prism tokens and converts them into the expected
+ # format for the parser gem.
+ class Lexer # :nodoc:
+ # These tokens are always skipped
+ TYPES_ALWAYS_SKIP = Set.new(%i[IGNORED_NEWLINE __END__ EOF])
+ private_constant :TYPES_ALWAYS_SKIP
+
+ # The direct translating of types between the two lexers.
+ TYPES = {
+ # These tokens should never appear in the output of the lexer.
+ EMBDOC_END: nil,
+ EMBDOC_LINE: nil,
+
+ # These tokens have more or less direct mappings.
+ AMPERSAND: :tAMPER2,
+ AMPERSAND_AMPERSAND: :tANDOP,
+ AMPERSAND_AMPERSAND_EQUAL: :tOP_ASGN,
+ AMPERSAND_DOT: :tANDDOT,
+ AMPERSAND_EQUAL: :tOP_ASGN,
+ BACK_REFERENCE: :tBACK_REF,
+ BACKTICK: :tXSTRING_BEG,
+ BANG: :tBANG,
+ BANG_EQUAL: :tNEQ,
+ BANG_TILDE: :tNMATCH,
+ BRACE_LEFT: :tLCURLY,
+ BRACE_RIGHT: :tRCURLY,
+ BRACKET_LEFT: :tLBRACK2,
+ BRACKET_LEFT_ARRAY: :tLBRACK,
+ BRACKET_LEFT_RIGHT: :tAREF,
+ BRACKET_LEFT_RIGHT_EQUAL: :tASET,
+ BRACKET_RIGHT: :tRBRACK,
+ CARET: :tCARET,
+ CARET_EQUAL: :tOP_ASGN,
+ CHARACTER_LITERAL: :tCHARACTER,
+ CLASS_VARIABLE: :tCVAR,
+ COLON: :tCOLON,
+ COLON_COLON: :tCOLON2,
+ COMMA: :tCOMMA,
+ COMMENT: :tCOMMENT,
+ CONSTANT: :tCONSTANT,
+ DOT: :tDOT,
+ DOT_DOT: :tDOT2,
+ DOT_DOT_DOT: :tDOT3,
+ EMBDOC_BEGIN: :tCOMMENT,
+ EMBEXPR_BEGIN: :tSTRING_DBEG,
+ EMBEXPR_END: :tSTRING_DEND,
+ EMBVAR: :tSTRING_DVAR,
+ EQUAL: :tEQL,
+ EQUAL_EQUAL: :tEQ,
+ EQUAL_EQUAL_EQUAL: :tEQQ,
+ EQUAL_GREATER: :tASSOC,
+ EQUAL_TILDE: :tMATCH,
+ FLOAT: :tFLOAT,
+ FLOAT_IMAGINARY: :tIMAGINARY,
+ FLOAT_RATIONAL: :tRATIONAL,
+ FLOAT_RATIONAL_IMAGINARY: :tIMAGINARY,
+ GLOBAL_VARIABLE: :tGVAR,
+ GREATER: :tGT,
+ GREATER_EQUAL: :tGEQ,
+ GREATER_GREATER: :tRSHFT,
+ GREATER_GREATER_EQUAL: :tOP_ASGN,
+ HEREDOC_START: :tSTRING_BEG,
+ HEREDOC_END: :tSTRING_END,
+ IDENTIFIER: :tIDENTIFIER,
+ INSTANCE_VARIABLE: :tIVAR,
+ INTEGER: :tINTEGER,
+ INTEGER_IMAGINARY: :tIMAGINARY,
+ INTEGER_RATIONAL: :tRATIONAL,
+ INTEGER_RATIONAL_IMAGINARY: :tIMAGINARY,
+ KEYWORD_ALIAS: :kALIAS,
+ KEYWORD_AND: :kAND,
+ KEYWORD_BEGIN: :kBEGIN,
+ KEYWORD_BEGIN_UPCASE: :klBEGIN,
+ KEYWORD_BREAK: :kBREAK,
+ KEYWORD_CASE: :kCASE,
+ KEYWORD_CLASS: :kCLASS,
+ KEYWORD_DEF: :kDEF,
+ KEYWORD_DEFINED: :kDEFINED,
+ KEYWORD_DO: :kDO,
+ KEYWORD_DO_BLOCK: :kDO_BLOCK,
+ KEYWORD_DO_LOOP: :kDO_COND,
+ KEYWORD_END: :kEND,
+ KEYWORD_END_UPCASE: :klEND,
+ KEYWORD_ENSURE: :kENSURE,
+ KEYWORD_ELSE: :kELSE,
+ KEYWORD_ELSIF: :kELSIF,
+ KEYWORD_FALSE: :kFALSE,
+ KEYWORD_FOR: :kFOR,
+ KEYWORD_IF: :kIF,
+ KEYWORD_IF_MODIFIER: :kIF_MOD,
+ KEYWORD_IN: :kIN,
+ KEYWORD_MODULE: :kMODULE,
+ KEYWORD_NEXT: :kNEXT,
+ KEYWORD_NIL: :kNIL,
+ KEYWORD_NOT: :kNOT,
+ KEYWORD_OR: :kOR,
+ KEYWORD_REDO: :kREDO,
+ KEYWORD_RESCUE: :kRESCUE,
+ KEYWORD_RESCUE_MODIFIER: :kRESCUE_MOD,
+ KEYWORD_RETRY: :kRETRY,
+ KEYWORD_RETURN: :kRETURN,
+ KEYWORD_SELF: :kSELF,
+ KEYWORD_SUPER: :kSUPER,
+ KEYWORD_THEN: :kTHEN,
+ KEYWORD_TRUE: :kTRUE,
+ KEYWORD_UNDEF: :kUNDEF,
+ KEYWORD_UNLESS: :kUNLESS,
+ KEYWORD_UNLESS_MODIFIER: :kUNLESS_MOD,
+ KEYWORD_UNTIL: :kUNTIL,
+ KEYWORD_UNTIL_MODIFIER: :kUNTIL_MOD,
+ KEYWORD_WHEN: :kWHEN,
+ KEYWORD_WHILE: :kWHILE,
+ KEYWORD_WHILE_MODIFIER: :kWHILE_MOD,
+ KEYWORD_YIELD: :kYIELD,
+ KEYWORD___ENCODING__: :k__ENCODING__,
+ KEYWORD___FILE__: :k__FILE__,
+ KEYWORD___LINE__: :k__LINE__,
+ LABEL: :tLABEL,
+ LABEL_END: :tLABEL_END,
+ LAMBDA_BEGIN: :tLAMBEG,
+ LESS: :tLT,
+ LESS_EQUAL: :tLEQ,
+ LESS_EQUAL_GREATER: :tCMP,
+ LESS_LESS: :tLSHFT,
+ LESS_LESS_EQUAL: :tOP_ASGN,
+ METHOD_NAME: :tFID,
+ MINUS: :tMINUS,
+ MINUS_EQUAL: :tOP_ASGN,
+ MINUS_GREATER: :tLAMBDA,
+ NEWLINE: :tNL,
+ NUMBERED_REFERENCE: :tNTH_REF,
+ PARENTHESIS_LEFT: :tLPAREN2,
+ PARENTHESIS_LEFT_PARENTHESES: :tLPAREN_ARG,
+ PARENTHESIS_RIGHT: :tRPAREN,
+ PERCENT: :tPERCENT,
+ PERCENT_EQUAL: :tOP_ASGN,
+ PERCENT_LOWER_I: :tQSYMBOLS_BEG,
+ PERCENT_LOWER_W: :tQWORDS_BEG,
+ PERCENT_UPPER_I: :tSYMBOLS_BEG,
+ PERCENT_UPPER_W: :tWORDS_BEG,
+ PERCENT_LOWER_X: :tXSTRING_BEG,
+ PLUS: :tPLUS,
+ PLUS_EQUAL: :tOP_ASGN,
+ PIPE_EQUAL: :tOP_ASGN,
+ PIPE: :tPIPE,
+ PIPE_PIPE: :tOROP,
+ PIPE_PIPE_EQUAL: :tOP_ASGN,
+ QUESTION_MARK: :tEH,
+ REGEXP_BEGIN: :tREGEXP_BEG,
+ REGEXP_END: :tSTRING_END,
+ SEMICOLON: :tSEMI,
+ SLASH: :tDIVIDE,
+ SLASH_EQUAL: :tOP_ASGN,
+ STAR: :tSTAR2,
+ STAR_EQUAL: :tOP_ASGN,
+ STAR_STAR: :tPOW,
+ STAR_STAR_EQUAL: :tOP_ASGN,
+ STRING_BEGIN: :tSTRING_BEG,
+ STRING_CONTENT: :tSTRING_CONTENT,
+ STRING_END: :tSTRING_END,
+ SYMBOL_BEGIN: :tSYMBEG,
+ TILDE: :tTILDE,
+ UAMPERSAND: :tAMPER,
+ UCOLON_COLON: :tCOLON3,
+ UDOT_DOT: :tBDOT2,
+ UDOT_DOT_DOT: :tBDOT3,
+ UMINUS: :tUMINUS,
+ UMINUS_NUM: :tUNARY_NUM,
+ UPLUS: :tUPLUS,
+ USTAR: :tSTAR,
+ USTAR_STAR: :tDSTAR,
+ WORDS_SEP: :tSPACE
+ }
+
+ # These constants represent flags in our lex state. We really, really
+ # don't want to be using them and we really, really don't want to be
+ # exposing them as part of our public API. Unfortunately, we don't have
+ # another way of matching the exact tokens that the parser gem expects
+ # without them. We should find another way to do this, but in the
+ # meantime we'll hide them from the documentation and mark them as
+ # private constants.
+ EXPR_BEG = 0x1
+ EXPR_LABEL = 0x400
+
+ # It is used to determine whether `do` is of the token type `kDO` or `kDO_LAMBDA`.
+ #
+ # NOTE: In edge cases like `-> (foo = -> (bar) {}) do end`, please note that `kDO` is still returned
+ # instead of `kDO_LAMBDA`, which is expected: https://github.com/ruby/prism/pull/3046
+ LAMBDA_TOKEN_TYPES = Set.new([:kDO_LAMBDA, :tLAMBDA, :tLAMBEG])
+
+ # The `PARENTHESIS_LEFT` token in Prism is classified as either `tLPAREN` or `tLPAREN2` in the Parser gem.
+ # The following token types are listed as those classified as `tLPAREN`.
+ LPAREN_CONVERSION_TOKEN_TYPES = Set.new([
+ :kBREAK, :tCARET, :kCASE, :tDIVIDE, :kFOR, :kIF, :kNEXT, :kRETURN, :kUNTIL, :kWHILE, :tAMPER, :tANDOP, :tBANG, :tCOMMA, :tDOT2, :tDOT3,
+ :tEQL, :tLPAREN, :tLPAREN2, :tLPAREN_ARG, :tLSHFT, :tNL, :tOP_ASGN, :tOROP, :tPIPE, :tSEMI, :tSTRING_DBEG, :tUMINUS, :tUPLUS, :tLCURLY
+ ])
+
+ # Types of tokens that are allowed to continue a method call with comments in-between.
+ # For these, the parser gem doesn't emit a newline token after the last comment.
+ COMMENT_CONTINUATION_TYPES = Set.new([:COMMENT, :AMPERSAND_DOT, :DOT])
+ private_constant :COMMENT_CONTINUATION_TYPES
+
+ # Heredocs are complex and require us to keep track of a bit of info to refer to later
+ HeredocData = Struct.new(:identifier, :common_whitespace, keyword_init: true)
+
+ private_constant :TYPES, :EXPR_BEG, :EXPR_LABEL, :LAMBDA_TOKEN_TYPES, :LPAREN_CONVERSION_TOKEN_TYPES, :HeredocData
+
+ # The Parser::Source::Buffer that the tokens were lexed from.
+ attr_reader :source_buffer
+
+ # An array of tuples that contain prism tokens and their associated lex
+ # state when they were lexed.
+ attr_reader :lexed
+
+ # A hash that maps offsets in bytes to offsets in characters.
+ attr_reader :offset_cache
+
+ # Initialize the lexer with the given source buffer, prism tokens, and
+ # offset cache.
+ def initialize(source_buffer, lexed, offset_cache)
+ @source_buffer = source_buffer
+ @lexed = lexed
+ @offset_cache = offset_cache
+ end
+
+ Range = ::Parser::Source::Range
+ private_constant :Range
+
+ # Convert the prism tokens into the expected format for the parser gem.
+ def to_a
+ tokens = []
+
+ index = 0
+ length = lexed.length
+
+ heredoc_stack = []
+ quote_stack = []
+
+ # The parser gem emits the newline tokens for comments out of order. This saves
+ # that token location to emit at a later time to properly line everything up.
+ # https://github.com/whitequark/parser/issues/1025
+ comment_newline_location = nil
+
+ while index < length
+ token, state = lexed[index]
+ index += 1
+ next if TYPES_ALWAYS_SKIP.include?(token.type)
+
+ type = TYPES.fetch(token.type)
+ value = token.value
+ location = range(token.location.start_offset, token.location.end_offset)
+
+ case type
+ when :kDO
+ nearest_lambda_token = tokens.reverse_each.find do |token|
+ LAMBDA_TOKEN_TYPES.include?(token.first)
+ end
+
+ if nearest_lambda_token&.first == :tLAMBDA
+ type = :kDO_LAMBDA
+ end
+ when :tCHARACTER
+ value.delete_prefix!("?")
+ # Character literals behave similar to double-quoted strings. We can use the same escaping mechanism.
+ value = unescape_string(value, "?")
+ when :tCOMMENT
+ if token.type == :EMBDOC_BEGIN
+
+ while !((next_token = lexed[index]&.first) && next_token.type == :EMBDOC_END) && (index < length - 1)
+ value += next_token.value
+ index += 1
+ end
+
+ value += next_token.value
+ location = range(token.location.start_offset, next_token.location.end_offset)
+ index += 1
+ else
+ is_at_eol = value.chomp!.nil?
+ location = range(token.location.start_offset, token.location.end_offset + (is_at_eol ? 0 : -1))
+
+ prev_token, _ = lexed[index - 2] if index - 2 >= 0
+ next_token, _ = lexed[index]
+
+ is_inline_comment = prev_token&.location&.start_line == token.location.start_line
+ if is_inline_comment && !is_at_eol && !COMMENT_CONTINUATION_TYPES.include?(next_token&.type)
+ tokens << [:tCOMMENT, [value, location]]
+
+ nl_location = range(token.location.end_offset - 1, token.location.end_offset)
+ tokens << [:tNL, [nil, nl_location]]
+ next
+ elsif is_inline_comment && next_token&.type == :COMMENT
+ comment_newline_location = range(token.location.end_offset - 1, token.location.end_offset)
+ elsif comment_newline_location && !COMMENT_CONTINUATION_TYPES.include?(next_token&.type)
+ tokens << [:tCOMMENT, [value, location]]
+ tokens << [:tNL, [nil, comment_newline_location]]
+ comment_newline_location = nil
+ next
+ end
+ end
+ when :tNL
+ next_token, _ = lexed[index]
+ # Newlines after comments are emitted out of order.
+ if next_token&.type == :COMMENT
+ comment_newline_location = location
+ next
+ end
+
+ value = nil
+ when :tFLOAT
+ value = parse_float(value)
+ when :tIMAGINARY
+ value = parse_complex(value)
+ when :tINTEGER
+ if value.start_with?("+")
+ tokens << [:tUNARY_NUM, ["+", range(token.location.start_offset, token.location.start_offset + 1)]]
+ location = range(token.location.start_offset + 1, token.location.end_offset)
+ end
+
+ value = parse_integer(value)
+ when :tLABEL
+ value.chomp!(":")
+ when :tLABEL_END
+ value.chomp!(":")
+ when :tLCURLY
+ type = :tLBRACE if state == EXPR_BEG | EXPR_LABEL
+ when :tLPAREN2
+ type = :tLPAREN if tokens.empty? || LPAREN_CONVERSION_TOKEN_TYPES.include?(tokens.dig(-1, 0))
+ when :tNTH_REF
+ value = parse_integer(value.delete_prefix("$"))
+ when :tOP_ASGN
+ value.chomp!("=")
+ when :tRATIONAL
+ value = parse_rational(value)
+ when :tSPACE
+ location = range(token.location.start_offset, token.location.start_offset + percent_array_leading_whitespace(value))
+ value = nil
+ when :tSTRING_BEG
+ next_token, _ = lexed[index]
+ next_next_token, _ = lexed[index + 1]
+ basic_quotes = value == '"' || value == "'"
+
+ if basic_quotes && next_token&.type == :STRING_END
+ next_location = token.location.join(next_token.location)
+ type = :tSTRING
+ value = ""
+ location = range(next_location.start_offset, next_location.end_offset)
+ index += 1
+ elsif value.start_with?("'", '"', "%")
+ if next_token&.type == :STRING_CONTENT && next_next_token&.type == :STRING_END
+ string_value = next_token.value
+ if simplify_string?(string_value, value)
+ next_location = token.location.join(next_next_token.location)
+ if percent_array?(value)
+ value = percent_array_unescape(string_value)
+ else
+ value = unescape_string(string_value, value)
+ end
+ type = :tSTRING
+ location = range(next_location.start_offset, next_location.end_offset)
+ index += 2
+ tokens << [type, [value, location]]
+
+ next
+ end
+ end
+
+ quote_stack.push(value)
+ elsif token.type == :HEREDOC_START
+ quote = value[2] == "-" || value[2] == "~" ? value[3] : value[2]
+ heredoc_type = value[2] == "-" || value[2] == "~" ? value[2] : ""
+ heredoc = HeredocData.new(
+ identifier: value.match(/<<[-~]?["'`]?(?<heredoc_identifier>.*?)["'`]?\z/)[:heredoc_identifier],
+ common_whitespace: 0,
+ )
+
+ if quote == "`"
+ type = :tXSTRING_BEG
+ end
+
+ # The parser gem trims whitespace from squiggly heredocs. We must record
+ # the most common whitespace to later remove.
+ if heredoc_type == "~" || heredoc_type == "`"
+ heredoc.common_whitespace = calculate_heredoc_whitespace(index)
+ end
+
+ if quote == "'" || quote == '"' || quote == "`"
+ value = "<<#{quote}"
+ else
+ value = '<<"'
+ end
+
+ heredoc_stack.push(heredoc)
+ quote_stack.push(value)
+ end
+ when :tSTRING_CONTENT
+ is_percent_array = percent_array?(quote_stack.last)
+
+ if (lines = token.value.lines).one?
+ # Prism usually emits a single token for strings with line continuations.
+ # For squiggly heredocs they are not joined so we do that manually here.
+ current_string = +""
+ current_length = 0
+ start_offset = token.location.start_offset
+ while token.type == :STRING_CONTENT
+ current_length += token.value.bytesize
+ # Heredoc interpolation can have multiple STRING_CONTENT nodes on the same line.
+ prev_token, _ = lexed[index - 2] if index - 2 >= 0
+ is_first_token_on_line = prev_token && token.location.start_line != prev_token.location.start_line
+ # The parser gem only removes indentation when the heredoc is not nested
+ not_nested = heredoc_stack.size == 1
+ if is_percent_array
+ value = percent_array_unescape(token.value)
+ elsif is_first_token_on_line && not_nested && (current_heredoc = heredoc_stack.last).common_whitespace > 0
+ value = trim_heredoc_whitespace(token.value, current_heredoc)
+ end
+
+ current_string << unescape_string(value, quote_stack.last)
+ relevant_backslash_count = if quote_stack.last.start_with?("%W", "%I")
+ 0 # the last backslash escapes the newline
+ else
+ token.value[/(\\{1,})\n/, 1]&.length || 0
+ end
+ if relevant_backslash_count.even? || !interpolation?(quote_stack.last)
+ tokens << [:tSTRING_CONTENT, [current_string, range(start_offset, start_offset + current_length)]]
+ break
+ end
+ token, _ = lexed[index]
+ index += 1
+ end
+ else
+ # When the parser gem encounters a line continuation inside of a multiline string,
+ # it emits a single string node. The backslash (and remaining newline) is removed.
+ current_line = +""
+ adjustment = 0
+ start_offset = token.location.start_offset
+ emit = false
+
+ lines.each.with_index do |line, index|
+ chomped_line = line.chomp
+ backslash_count = chomped_line[/\\{1,}\z/]&.length || 0
+ is_interpolation = interpolation?(quote_stack.last)
+
+ if backslash_count.odd? && (is_interpolation || is_percent_array)
+ if is_percent_array
+ current_line << percent_array_unescape(line)
+ adjustment += 1
+ else
+ chomped_line.delete_suffix!("\\")
+ current_line << chomped_line
+ adjustment += 2
+ end
+ # If the string ends with a line continuation emit the remainder
+ emit = index == lines.count - 1
+ else
+ current_line << line
+ emit = true
+ end
+
+ if emit
+ end_offset = start_offset + current_line.bytesize + adjustment
+ tokens << [:tSTRING_CONTENT, [unescape_string(current_line, quote_stack.last), range(start_offset, end_offset)]]
+ start_offset = end_offset
+ current_line = +""
+ adjustment = 0
+ end
+ end
+ end
+ next
+ when :tSTRING_DVAR
+ value = nil
+ when :tSTRING_END
+ if token.type == :HEREDOC_END && value.end_with?("\n")
+ newline_length = value.end_with?("\r\n") ? 2 : 1
+ value = heredoc_stack.pop.identifier
+ location = range(token.location.start_offset, token.location.end_offset - newline_length)
+ elsif token.type == :REGEXP_END
+ value = value[0]
+ location = range(token.location.start_offset, token.location.start_offset + 1)
+ end
+
+ if percent_array?(quote_stack.pop)
+ prev_token, _ = lexed[index - 2] if index - 2 >= 0
+ empty = %i[PERCENT_LOWER_I PERCENT_LOWER_W PERCENT_UPPER_I PERCENT_UPPER_W].include?(prev_token&.type)
+ ends_with_whitespace = prev_token&.type == :WORDS_SEP
+ # parser always emits a space token after content in a percent array, even if no actual whitespace is present.
+ if !empty && !ends_with_whitespace
+ tokens << [:tSPACE, [nil, range(token.location.start_offset, token.location.start_offset)]]
+ end
+ end
+ when :tSYMBEG
+ if (next_token = lexed[index]&.first) && next_token.type != :STRING_CONTENT && next_token.type != :EMBEXPR_BEGIN && next_token.type != :EMBVAR && next_token.type != :STRING_END
+ next_location = token.location.join(next_token.location)
+ type = :tSYMBOL
+ value = next_token.value
+ value = { "~@" => "~", "!@" => "!" }.fetch(value, value)
+ location = range(next_location.start_offset, next_location.end_offset)
+ index += 1
+ else
+ quote_stack.push(value)
+ end
+ when :tFID
+ if !tokens.empty? && tokens.dig(-1, 0) == :kDEF
+ type = :tIDENTIFIER
+ end
+ when :tXSTRING_BEG
+ if (next_token = lexed[index]&.first) && !%i[STRING_CONTENT STRING_END EMBEXPR_BEGIN].include?(next_token.type)
+ # self.`()
+ type = :tBACK_REF2
+ end
+ quote_stack.push(value)
+ when :tSYMBOLS_BEG, :tQSYMBOLS_BEG, :tWORDS_BEG, :tQWORDS_BEG
+ if (next_token = lexed[index]&.first) && next_token.type == :WORDS_SEP
+ index += 1
+ end
+
+ quote_stack.push(value)
+ when :tREGEXP_BEG
+ quote_stack.push(value)
+ end
+
+ tokens << [type, [value, location]]
+
+ if token.type == :REGEXP_END
+ tokens << [:tREGEXP_OPT, [token.value[1..], range(token.location.start_offset + 1, token.location.end_offset)]]
+ end
+ end
+
+ tokens
+ end
+
+ private
+
+ # Creates a new parser range, taking prisms byte offsets into account
+ def range(start_offset, end_offset)
+ Range.new(source_buffer, offset_cache[start_offset], offset_cache[end_offset])
+ end
+
+ # Parse an integer from the string representation.
+ def parse_integer(value)
+ Integer(value)
+ rescue ArgumentError
+ 0
+ end
+
+ # Parse a float from the string representation.
+ def parse_float(value)
+ Float(value)
+ rescue ArgumentError
+ 0.0
+ end
+
+ # Parse a complex from the string representation.
+ def parse_complex(value)
+ value.chomp!("i")
+
+ if value.end_with?("r")
+ Complex(0, parse_rational(value))
+ elsif value.start_with?(/0[BbOoDdXx]/)
+ Complex(0, parse_integer(value))
+ else
+ Complex(0, value)
+ end
+ rescue ArgumentError
+ 0i
+ end
+
+ # Parse a rational from the string representation.
+ def parse_rational(value)
+ value.chomp!("r")
+
+ if value.start_with?(/0[BbOoDdXx]/)
+ Rational(parse_integer(value))
+ else
+ Rational(value)
+ end
+ rescue ArgumentError
+ 0r
+ end
+
+ # Wonky heredoc tab/spaces rules.
+ # https://github.com/ruby/prism/blob/v1.3.0/src/prism.c#L10548-L10558
+ def calculate_heredoc_whitespace(heredoc_token_index)
+ next_token_index = heredoc_token_index
+ nesting_level = 0
+ previous_line = -1
+ result = Float::MAX
+
+ while (next_token = lexed[next_token_index]&.first)
+ next_token_index += 1
+ next_next_token, _ = lexed[next_token_index]
+ first_token_on_line = next_token.location.start_column == 0
+
+ # String content inside nested heredocs and interpolation is ignored
+ if next_token.type == :HEREDOC_START || next_token.type == :EMBEXPR_BEGIN
+ # When interpolation is the first token of a line there is no string
+ # content to check against. There will be no common whitespace.
+ if nesting_level == 0 && first_token_on_line
+ result = 0
+ end
+ nesting_level += 1
+ elsif next_token.type == :HEREDOC_END || next_token.type == :EMBEXPR_END
+ nesting_level -= 1
+ # When we encountered the matching heredoc end, we can exit
+ break if nesting_level == -1
+ elsif next_token.type == :STRING_CONTENT && nesting_level == 0 && first_token_on_line
+ common_whitespace = 0
+ next_token.value[/^\s*/].each_char do |char|
+ if char == "\t"
+ common_whitespace = (common_whitespace / 8 + 1) * 8;
+ else
+ common_whitespace += 1
+ end
+ end
+
+ is_first_token_on_line = next_token.location.start_line != previous_line
+ # Whitespace is significant if followed by interpolation
+ whitespace_only = common_whitespace == next_token.value.length && next_next_token&.location&.start_line != next_token.location.start_line
+ if is_first_token_on_line && !whitespace_only && common_whitespace < result
+ result = common_whitespace
+ previous_line = next_token.location.start_line
+ end
+ end
+ end
+ result
+ end
+
+ # Wonky heredoc tab/spaces rules.
+ # https://github.com/ruby/prism/blob/v1.3.0/src/prism.c#L16528-L16545
+ def trim_heredoc_whitespace(string, heredoc)
+ trimmed_whitespace = 0
+ trimmed_characters = 0
+ while (string[trimmed_characters] == "\t" || string[trimmed_characters] == " ") && trimmed_whitespace < heredoc.common_whitespace
+ if string[trimmed_characters] == "\t"
+ trimmed_whitespace = (trimmed_whitespace / 8 + 1) * 8;
+ break if trimmed_whitespace > heredoc.common_whitespace
+ else
+ trimmed_whitespace += 1
+ end
+ trimmed_characters += 1
+ end
+
+ string[trimmed_characters..]
+ end
+
+ # Escape sequences that have special and should appear unescaped in the resulting string.
+ ESCAPES = {
+ "a" => "\a", "b" => "\b", "e" => "\e", "f" => "\f",
+ "n" => "\n", "r" => "\r", "s" => "\s", "t" => "\t",
+ "v" => "\v", "\\" => "\\"
+ }.freeze
+ private_constant :ESCAPES
+
+ # When one of these delimiters is encountered, then the other
+ # one is allowed to be escaped as well.
+ DELIMITER_SYMETRY = { "[" => "]", "(" => ")", "{" => "}", "<" => ">" }.freeze
+ private_constant :DELIMITER_SYMETRY
+
+
+ # https://github.com/whitequark/parser/blob/v3.3.6.0/lib/parser/lexer-strings.rl#L14
+ REGEXP_META_CHARACTERS = ["\\", "$", "(", ")", "*", "+", ".", "<", ">", "?", "[", "]", "^", "{", "|", "}"]
+ private_constant :REGEXP_META_CHARACTERS
+
+ # Apply Ruby string escaping rules
+ def unescape_string(string, quote)
+ # In single-quoted heredocs, everything is taken literally.
+ return string if quote == "<<'"
+
+ # OPTIMIZATION: Assume that few strings need escaping to speed up the common case.
+ return string unless string.include?("\\")
+
+ # Enclosing character for the string. `"` for `"foo"`, `{` for `%w{foo}`, etc.
+ delimiter = quote[-1]
+
+ if regexp?(quote)
+ # Should be escaped handled to single-quoted heredocs. The only character that is
+ # allowed to be escaped is the delimiter, except when that also has special meaning
+ # in the regexp. Since all the symetry delimiters have special meaning, they don't need
+ # to be considered separately.
+ if REGEXP_META_CHARACTERS.include?(delimiter)
+ string
+ else
+ # There can never be an even amount of backslashes. It would be a syntax error.
+ string.gsub(/\\(#{Regexp.escape(delimiter)})/, '\1')
+ end
+ elsif interpolation?(quote)
+ # Appending individual escape sequences may force the string out of its intended
+ # encoding. Start out with binary and force it back later.
+ result = "".b
+
+ scanner = StringScanner.new(string)
+ while (skipped = scanner.skip_until(/\\/))
+ # Append what was just skipped over, excluding the found backslash.
+ result.append_as_bytes(string.byteslice(scanner.pos - skipped, skipped - 1))
+ escape_read(result, scanner, false, false)
+ end
+
+ # Add remaining chars
+ result.append_as_bytes(string.byteslice(scanner.pos..))
+ result.force_encoding(source_buffer.source.encoding)
+ else
+ delimiters = Regexp.escape("#{delimiter}#{DELIMITER_SYMETRY[delimiter]}")
+ string.gsub(/\\([\\#{delimiters}])/, '\1')
+ end
+ end
+
+ # Certain strings are merged into a single string token.
+ def simplify_string?(value, quote)
+ case quote
+ when "'"
+ # Only simplify 'foo'
+ !value.include?("\n")
+ when '"'
+ # Simplify when every line ends with a line continuation, or it is the last line
+ value.lines.all? do |line|
+ !line.end_with?("\n") || line[/(\\*)$/, 1]&.length&.odd?
+ end
+ else
+ # %q and similar are never simplified
+ false
+ end
+ end
+
+ # Escape a byte value, given the control and meta flags.
+ def escape_build(value, control, meta)
+ value &= 0x9f if control
+ value |= 0x80 if meta
+ value
+ end
+
+ # Read an escape out of the string scanner, given the control and meta
+ # flags, and push the unescaped value into the result.
+ def escape_read(result, scanner, control, meta)
+ if scanner.skip("\n")
+ # Line continuation
+ elsif (value = ESCAPES[scanner.peek(1)])
+ # Simple single-character escape sequences like \n
+ result.append_as_bytes(value)
+ scanner.pos += 1
+ elsif (value = scanner.scan(/[0-7]{1,3}/))
+ # \nnn
+ result.append_as_bytes(escape_build(value.to_i(8), control, meta))
+ elsif (value = scanner.scan(/x[0-9a-fA-F]{1,2}/))
+ # \xnn
+ result.append_as_bytes(escape_build(value[1..].to_i(16), control, meta))
+ elsif (value = scanner.scan(/u[0-9a-fA-F]{4}/))
+ # \unnnn
+ result.append_as_bytes(value[1..].hex.chr(Encoding::UTF_8))
+ elsif scanner.skip("u{}")
+ # https://github.com/whitequark/parser/issues/856
+ elsif (value = scanner.scan(/u{.*?}/))
+ # \u{nnnn ...}
+ value[2..-2].split.each do |unicode|
+ result.append_as_bytes(unicode.hex.chr(Encoding::UTF_8))
+ end
+ elsif (value = scanner.scan(/c\\?(?=[[:print:]])|C-\\?(?=[[:print:]])/))
+ # \cx or \C-x where x is an ASCII printable character
+ escape_read(result, scanner, true, meta)
+ elsif (value = scanner.scan(/M-\\?(?=[[:print:]])/))
+ # \M-x where x is an ASCII printable character
+ escape_read(result, scanner, control, true)
+ elsif (byte = scanner.scan_byte)
+ # Something else after an escape.
+ if control && byte == 0x3f # ASCII '?'
+ result.append_as_bytes(escape_build(0x7f, false, meta))
+ else
+ result.append_as_bytes(escape_build(byte, control, meta))
+ end
+ end
+ end
+
+ # In a percent array, certain whitespace can be preceeded with a backslash,
+ # causing the following characters to be part of the previous element.
+ def percent_array_unescape(string)
+ string.gsub(/(\\)+[ \f\n\r\t\v]/) do |full_match|
+ full_match.delete_prefix!("\\") if Regexp.last_match[1].length.odd?
+ full_match
+ end
+ end
+
+ # For %-arrays whitespace, the parser gem only considers whitespace before the newline.
+ def percent_array_leading_whitespace(string)
+ return 1 if string.start_with?("\n")
+
+ leading_whitespace = 0
+ string.each_char do |c|
+ break if c == "\n"
+ leading_whitespace += 1
+ end
+ leading_whitespace
+ end
+
+ # Determine if characters preceeded by a backslash should be escaped or not
+ def interpolation?(quote)
+ !quote.end_with?("'") && !quote.start_with?("%q", "%w", "%i", "%s")
+ end
+
+ # Regexp allow interpolation but are handled differently during unescaping
+ def regexp?(quote)
+ quote == "/" || quote.start_with?("%r")
+ end
+
+ # Determine if the string is part of a %-style array.
+ def percent_array?(quote)
+ quote.start_with?("%w", "%W", "%i", "%I")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/prism/translation/parser_current.rb b/lib/prism/translation/parser_current.rb
new file mode 100644
index 0000000000..f7c1070e30
--- /dev/null
+++ b/lib/prism/translation/parser_current.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# typed: ignore
+
+module Prism
+ module Translation
+ case RUBY_VERSION
+ when /^3\.3\./
+ ParserCurrent = Parser33
+ when /^3\.4\./
+ ParserCurrent = Parser34
+ when /^3\.5\./, /^4\.0\./
+ ParserCurrent = Parser40
+ when /^4\.1\./
+ ParserCurrent = Parser41
+ else
+ # Keep this in sync with released Ruby.
+ parser = Parser40
+ major, minor, _patch = Gem::Version.new(RUBY_VERSION).segments
+ warn "warning: `Prism::Translation::Current` is loading #{parser.name}, " \
+ "but you are running #{major}.#{minor}."
+ ParserCurrent = parser
+ end
+ end
+end
diff --git a/lib/prism/translation/parser_versions.rb b/lib/prism/translation/parser_versions.rb
new file mode 100644
index 0000000000..720c7d548c
--- /dev/null
+++ b/lib/prism/translation/parser_versions.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+# :markup: markdown
+
+module Prism
+ module Translation
+ # This class is the entry-point for Ruby 3.3 of `Prism::Translation::Parser`.
+ class Parser33 < Parser
+ def version # :nodoc:
+ 33
+ end
+ end
+
+ # This class is the entry-point for Ruby 3.4 of `Prism::Translation::Parser`.
+ class Parser34 < Parser
+ def version # :nodoc:
+ 34
+ end
+ end
+
+ # This class is the entry-point for Ruby 4.0 of `Prism::Translation::Parser`.
+ class Parser40 < Parser
+ def version # :nodoc:
+ 40
+ end
+ end
+
+ Parser35 = Parser40 # :nodoc:
+
+ # This class is the entry-point for Ruby 4.1 of `Prism::Translation::Parser`.
+ class Parser41 < Parser
+ def version # :nodoc:
+ 41
+ end
+ end
+ end
+end
diff --git a/lib/prism/translation/ripper.rb b/lib/prism/translation/ripper.rb
new file mode 100644
index 0000000000..f179a149a1
--- /dev/null
+++ b/lib/prism/translation/ripper.rb
@@ -0,0 +1,4266 @@
+# frozen_string_literal: true
+# :markup: markdown
+
+module Prism
+ module Translation
+ # This class provides a compatibility layer between prism and Ripper. It
+ # functions by parsing the entire tree first and then walking it and
+ # executing each of the Ripper callbacks as it goes. To use this class, you
+ # treat `Prism::Translation::Ripper` effectively as you would treat the
+ # `Ripper` class.
+ #
+ # Note that this class will serve the most common use cases, but Ripper's
+ # API is extensive and undocumented. It relies on reporting the state of the
+ # parser at any given time. We do our best to replicate that here, but
+ # because it is a different architecture it is not possible to perfectly
+ # replicate the behavior of Ripper.
+ #
+ # The main known difference is that we may omit dispatching some events in
+ # some cases. This impacts the following events:
+ #
+ # - on_assign_error
+ # - on_comma
+ # - on_ignored_nl
+ # - on_ignored_sp
+ # - on_nl
+ # - on_operator_ambiguous
+ # - on_semicolon
+ # - on_sp
+ #
+ class Ripper < Compiler
+ # Parses the given Ruby program read from +src+.
+ # +src+ must be a String or an IO or a object with a #gets method.
+ def self.parse(src, filename = "(ripper)", lineno = 1)
+ new(src, filename, lineno).parse
+ end
+
+ # Tokenizes the Ruby program and returns an array of an array,
+ # which is formatted like
+ # <code>[[lineno, column], type, token, state]</code>.
+ # The +filename+ argument is mostly ignored.
+ # By default, this method does not handle syntax errors in +src+,
+ # use the +raise_errors+ keyword to raise a SyntaxError for an error in +src+.
+ #
+ # require "ripper"
+ # require "pp"
+ #
+ # pp Ripper.lex("def m(a) nil end")
+ # #=> [[[1, 0], :on_kw, "def", FNAME ],
+ # [[1, 3], :on_sp, " ", FNAME ],
+ # [[1, 4], :on_ident, "m", ENDFN ],
+ # [[1, 5], :on_lparen, "(", BEG|LABEL],
+ # [[1, 6], :on_ident, "a", ARG ],
+ # [[1, 7], :on_rparen, ")", ENDFN ],
+ # [[1, 8], :on_sp, " ", BEG ],
+ # [[1, 9], :on_kw, "nil", END ],
+ # [[1, 12], :on_sp, " ", END ],
+ # [[1, 13], :on_kw, "end", END ]]
+ #
+ def self.lex(src, filename = "-", lineno = 1, raise_errors: false)
+ coerced = coerce_source(src)
+ result = Prism.lex_compat(coerced, filepath: filename, line: lineno, version: "current", encoding: coerced.encoding)
+
+ if result.failure? && raise_errors
+ raise SyntaxError, result.errors.first.message
+ else
+ result.value
+ end
+ end
+
+ # Tokenizes the Ruby program and returns an array of strings.
+ # The +filename+ and +lineno+ arguments are mostly ignored, since the
+ # return value is just the tokenized input.
+ # By default, this method does not handle syntax errors in +src+,
+ # use the +raise_errors+ keyword to raise a SyntaxError for an error in +src+.
+ #
+ # p Ripper.tokenize("def m(a) nil end")
+ # # => ["def", " ", "m", "(", "a", ")", " ", "nil", " ", "end"]
+ #
+ def self.tokenize(...)
+ lex(...).map { |token| token[2] }
+ end
+
+ # Mirros the various lex_types that ripper supports
+ def self.coerce_source(source) # :nodoc:
+ if source.is_a?(IO)
+ source.read
+ elsif source.respond_to?(:gets)
+ src = +""
+ while line = source.gets
+ src << line
+ end
+ src
+ else
+ source.to_str
+ end
+ end
+
+ # This contains a table of all of the parser events and their
+ # corresponding arity.
+ PARSER_EVENT_TABLE = {
+ BEGIN: 1,
+ END: 1,
+ alias: 2,
+ alias_error: 2,
+ aref: 2,
+ aref_field: 2,
+ arg_ambiguous: 1,
+ arg_paren: 1,
+ args_add: 2,
+ args_add_block: 2,
+ args_add_star: 2,
+ args_forward: 0,
+ args_new: 0,
+ array: 1,
+ aryptn: 4,
+ assign: 2,
+ assign_error: 2,
+ assoc_new: 2,
+ assoc_splat: 1,
+ assoclist_from_args: 1,
+ bare_assoc_hash: 1,
+ begin: 1,
+ binary: 3,
+ block_var: 2,
+ blockarg: 1,
+ bodystmt: 4,
+ brace_block: 2,
+ break: 1,
+ call: 3,
+ case: 2,
+ class: 3,
+ class_name_error: 2,
+ command: 2,
+ command_call: 4,
+ const_path_field: 2,
+ const_path_ref: 2,
+ const_ref: 1,
+ def: 3,
+ defined: 1,
+ defs: 5,
+ do_block: 2,
+ dot2: 2,
+ dot3: 2,
+ dyna_symbol: 1,
+ else: 1,
+ elsif: 3,
+ ensure: 1,
+ excessed_comma: 0,
+ fcall: 1,
+ field: 3,
+ fndptn: 4,
+ for: 3,
+ hash: 1,
+ heredoc_dedent: 2,
+ hshptn: 3,
+ if: 3,
+ if_mod: 2,
+ ifop: 3,
+ in: 3,
+ kwrest_param: 1,
+ lambda: 2,
+ magic_comment: 2,
+ massign: 2,
+ method_add_arg: 2,
+ method_add_block: 2,
+ mlhs_add: 2,
+ mlhs_add_post: 2,
+ mlhs_add_star: 2,
+ mlhs_new: 0,
+ mlhs_paren: 1,
+ module: 2,
+ mrhs_add: 2,
+ mrhs_add_star: 2,
+ mrhs_new: 0,
+ mrhs_new_from_args: 1,
+ next: 1,
+ nokw_param: 1,
+ opassign: 3,
+ operator_ambiguous: 2,
+ param_error: 2,
+ params: 7,
+ paren: 1,
+ parse_error: 1,
+ program: 1,
+ qsymbols_add: 2,
+ qsymbols_new: 0,
+ qwords_add: 2,
+ qwords_new: 0,
+ redo: 0,
+ regexp_add: 2,
+ regexp_literal: 2,
+ regexp_new: 0,
+ rescue: 4,
+ rescue_mod: 2,
+ rest_param: 1,
+ retry: 0,
+ return: 1,
+ return0: 0,
+ sclass: 2,
+ stmts_add: 2,
+ stmts_new: 0,
+ string_add: 2,
+ string_concat: 2,
+ string_content: 0,
+ string_dvar: 1,
+ string_embexpr: 1,
+ string_literal: 1,
+ super: 1,
+ symbol: 1,
+ symbol_literal: 1,
+ symbols_add: 2,
+ symbols_new: 0,
+ top_const_field: 1,
+ top_const_ref: 1,
+ unary: 2,
+ undef: 1,
+ unless: 3,
+ unless_mod: 2,
+ until: 2,
+ until_mod: 2,
+ var_alias: 2,
+ var_field: 1,
+ var_ref: 1,
+ vcall: 1,
+ void_stmt: 0,
+ when: 3,
+ while: 2,
+ while_mod: 2,
+ word_add: 2,
+ word_new: 0,
+ words_add: 2,
+ words_new: 0,
+ xstring_add: 2,
+ xstring_literal: 1,
+ xstring_new: 0,
+ yield: 1,
+ yield0: 0,
+ zsuper: 0
+ }
+
+ # This contains a table of all of the scanner events and their
+ # corresponding arity.
+ SCANNER_EVENT_TABLE = {
+ CHAR: 1,
+ __end__: 1,
+ backref: 1,
+ backtick: 1,
+ comma: 1,
+ comment: 1,
+ const: 1,
+ cvar: 1,
+ embdoc: 1,
+ embdoc_beg: 1,
+ embdoc_end: 1,
+ embexpr_beg: 1,
+ embexpr_end: 1,
+ embvar: 1,
+ float: 1,
+ gvar: 1,
+ heredoc_beg: 1,
+ heredoc_end: 1,
+ ident: 1,
+ ignored_nl: 1,
+ imaginary: 1,
+ int: 1,
+ ivar: 1,
+ kw: 1,
+ label: 1,
+ label_end: 1,
+ lbrace: 1,
+ lbracket: 1,
+ lparen: 1,
+ nl: 1,
+ op: 1,
+ period: 1,
+ qsymbols_beg: 1,
+ qwords_beg: 1,
+ rational: 1,
+ rbrace: 1,
+ rbracket: 1,
+ regexp_beg: 1,
+ regexp_end: 1,
+ rparen: 1,
+ semicolon: 1,
+ sp: 1,
+ symbeg: 1,
+ symbols_beg: 1,
+ tlambda: 1,
+ tlambeg: 1,
+ tstring_beg: 1,
+ tstring_content: 1,
+ tstring_end: 1,
+ words_beg: 1,
+ words_sep: 1,
+ ignored_sp: 1
+ }
+
+ # This array contains name of parser events.
+ PARSER_EVENTS = PARSER_EVENT_TABLE.keys
+
+ # This array contains name of scanner events.
+ SCANNER_EVENTS = SCANNER_EVENT_TABLE.keys
+
+ # This array contains name of all ripper events.
+ EVENTS = PARSER_EVENTS + SCANNER_EVENTS
+
+ # A list of all of the Ruby keywords.
+ KEYWORDS = [
+ "alias",
+ "and",
+ "begin",
+ "BEGIN",
+ "break",
+ "case",
+ "class",
+ "def",
+ "defined?",
+ "do",
+ "else",
+ "elsif",
+ "end",
+ "END",
+ "ensure",
+ "false",
+ "for",
+ "if",
+ "in",
+ "module",
+ "next",
+ "nil",
+ "not",
+ "or",
+ "redo",
+ "rescue",
+ "retry",
+ "return",
+ "self",
+ "super",
+ "then",
+ "true",
+ "undef",
+ "unless",
+ "until",
+ "when",
+ "while",
+ "yield",
+ "__ENCODING__",
+ "__FILE__",
+ "__LINE__"
+ ].to_set
+
+ # A list of all of the Ruby binary operators.
+ BINARY_OPERATORS = [
+ :!=,
+ :!~,
+ :=~,
+ :==,
+ :===,
+ :<=>,
+ :>,
+ :>=,
+ :<,
+ :<=,
+ :&,
+ :|,
+ :^,
+ :>>,
+ :<<,
+ :-,
+ :+,
+ :%,
+ :/,
+ :*,
+ :**
+ ].to_set
+
+ private_constant :KEYWORDS, :BINARY_OPERATORS
+
+ # Parses +src+ and create S-exp tree.
+ # Returns more readable tree rather than Ripper.sexp_raw.
+ # This method is mainly for developer use.
+ # The +filename+ argument is mostly ignored.
+ # By default, this method does not handle syntax errors in +src+,
+ # returning +nil+ in such cases. Use the +raise_errors+ keyword
+ # to raise a SyntaxError for an error in +src+.
+ #
+ # require "ripper"
+ # require "pp"
+ #
+ # pp Ripper.sexp("def m(a) nil end")
+ # #=> [:program,
+ # [[:def,
+ # [:@ident, "m", [1, 4]],
+ # [:paren, [:params, [[:@ident, "a", [1, 6]]], nil, nil, nil, nil, nil, nil]],
+ # [:bodystmt, [[:var_ref, [:@kw, "nil", [1, 9]]]], nil, nil, nil]]]]
+ #
+ def self.sexp(src, filename = "-", lineno = 1, raise_errors: false)
+ builder = SexpBuilderPP.new(src, filename, lineno)
+ sexp = builder.parse
+ if builder.error?
+ if raise_errors
+ raise SyntaxError, builder.error
+ end
+ else
+ sexp
+ end
+ end
+
+ # Parses +src+ and create S-exp tree.
+ # This method is mainly for developer use.
+ # The +filename+ argument is mostly ignored.
+ # By default, this method does not handle syntax errors in +src+,
+ # returning +nil+ in such cases. Use the +raise_errors+ keyword
+ # to raise a SyntaxError for an error in +src+.
+ #
+ # require "ripper"
+ # require "pp"
+ #
+ # pp Ripper.sexp_raw("def m(a) nil end")
+ # #=> [:program,
+ # [:stmts_add,
+ # [:stmts_new],
+ # [:def,
+ # [:@ident, "m", [1, 4]],
+ # [:paren, [:params, [[:@ident, "a", [1, 6]]], nil, nil, nil]],
+ # [:bodystmt,
+ # [:stmts_add, [:stmts_new], [:var_ref, [:@kw, "nil", [1, 9]]]],
+ # nil,
+ # nil,
+ # nil]]]]
+ #
+ def self.sexp_raw(src, filename = "-", lineno = 1, raise_errors: false)
+ builder = SexpBuilder.new(src, filename, lineno)
+ sexp = builder.parse
+ if builder.error?
+ if raise_errors
+ raise SyntaxError, builder.error
+ end
+ else
+ sexp
+ end
+ end
+
+ autoload :Filter, "prism/translation/ripper/filter"
+ autoload :Lexer, "prism/translation/ripper/lexer"
+ autoload :SexpBuilder, "prism/translation/ripper/sexp"
+ autoload :SexpBuilderPP, "prism/translation/ripper/sexp"
+
+ # Provides optimized access to line and column information.
+ # Ripper bounds are mostly accessed in a linear fashion, so
+ # we can try a linear scan first and fall back to binary search.
+ class LineAndColumnCache # :nodoc:
+ # How many should it look ahead/behind before falling back to binary searching.
+ WINDOW = 8
+ private_constant :WINDOW
+
+ #: (Source source) -> void
+ def initialize(source)
+ @source = source
+ @offsets = source.offsets
+ @hint = 0
+ end
+
+ #: (Integer byte_offset) -> [Integer, Integer]
+ def line_and_column(byte_offset)
+ @hint = new_hint(byte_offset) || @source.find_line(byte_offset)
+ return [@hint + @source.start_line, byte_offset - @offsets[@hint]]
+ end
+
+ private
+
+ def new_hint(byte_offset)
+ if @offsets[@hint] <= byte_offset
+ # Same line?
+ if (@hint + 1 >= @offsets.size || @offsets[@hint + 1] > byte_offset)
+ return @hint
+ end
+
+ # Scan forwards
+ limit = [@hint + WINDOW + 1, @offsets.size].min
+ idx = @hint + 1
+ while idx < limit
+ if @offsets[idx] > byte_offset
+ return idx - 1
+ end
+ if @offsets[idx] == byte_offset
+ return idx
+ end
+ idx += 1
+ end
+ else
+ # Scan backwards
+ limit = @hint > WINDOW ? @hint - WINDOW : 0
+ idx = @hint
+ while idx >= limit + 1
+ if @offsets[idx - 1] <= byte_offset
+ return idx - 1
+ end
+ idx -= 1
+ end
+ end
+
+ nil
+ end
+ end
+
+ # :stopdoc:
+ # This is not part of the public API but used by some gems.
+
+ # Ripper-internal bitflags.
+ LEX_STATE_NAMES = %i[
+ BEG END ENDARG ENDFN ARG CMDARG MID FNAME DOT CLASS LABEL LABELED FITEM
+ ].map.with_index.to_h { |name, i| [2 ** i, name] }.freeze
+ private_constant :LEX_STATE_NAMES
+
+ LEX_STATE_NAMES.each do |value, key|
+ const_set("EXPR_#{key}", value)
+ end
+ EXPR_NONE = 0
+ EXPR_VALUE = EXPR_BEG
+ EXPR_BEG_ANY = EXPR_BEG | EXPR_MID | EXPR_CLASS
+ EXPR_ARG_ANY = EXPR_ARG | EXPR_CMDARG
+ EXPR_END_ANY = EXPR_END | EXPR_ENDARG | EXPR_ENDFN
+
+ def self.lex_state_name(state)
+ LEX_STATE_NAMES.filter_map { |flag, name| name if state & flag != 0 }.join("|")
+ end
+
+ # :startdoc:
+
+ # The source that is being parsed.
+ attr_reader :source
+
+ # The filename of the source being parsed.
+ attr_reader :filename
+
+ # The current line number of the parser.
+ attr_reader :lineno
+
+ # The current column in bytes of the parser.
+ attr_reader :column
+
+ # Create a new Translation::Ripper object with the given source.
+ def initialize(source, filename = "(ripper)", lineno = 1)
+ @source = Ripper.coerce_source(source)
+ @filename = filename
+ @lineno = lineno
+ @column = 0
+ @result = nil
+ @line_and_column_cache = nil
+ end
+
+ ##########################################################################
+ # Public interface
+ ##########################################################################
+
+ # True if the parser encountered an error during parsing.
+ def error?
+ result.failure?
+ end
+
+ # Parse the source and return the result.
+ def parse
+ result.comments.each do |comment|
+ location = comment.location
+ bounds(location)
+
+ if comment.is_a?(InlineComment)
+ # Inline comments always contain a newline if the line itself contains it
+ if result.source.source.bytesize > comment.location.end_offset
+ on_comment("#{comment.slice}\n")
+ else
+ on_comment(comment.slice)
+ end
+ else
+ offset = location.start_offset
+ lines = comment.slice.lines
+
+ lines.each_with_index do |line, index|
+ bounds(location.copy(start_offset: offset))
+
+ if index == 0
+ on_embdoc_beg(line)
+ elsif index == lines.size - 1
+ on_embdoc_end(line)
+ else
+ on_embdoc(line)
+ end
+
+ offset += line.bytesize
+ end
+ end
+ end
+
+ result.magic_comments.each do |magic_comment|
+ on_magic_comment(magic_comment.key, magic_comment.value)
+ end
+
+ unless result.data_loc.nil?
+ on___end__(result.data_loc.slice.each_line.first)
+ end
+
+ result.warnings.each do |warning|
+ bounds(warning.location)
+
+ if warning.level == :default
+ warning(warning.message)
+ else
+ case warning.type
+ when :ambiguous_first_argument_plus
+ on_arg_ambiguous("+")
+ when :ambiguous_first_argument_minus
+ on_arg_ambiguous("-")
+ when :ambiguous_slash
+ on_arg_ambiguous("/")
+ else
+ warn(warning.message)
+ end
+ end
+ end
+
+ if error?
+ result.errors.each do |error|
+ location = error.location
+ bounds(location)
+
+ case error.type
+ when :alias_argument
+ on_alias_error("can't make alias for the number variables", location.slice)
+ when :argument_formal_class
+ on_param_error("formal argument cannot be a class variable", location.slice)
+ when :argument_format_constant
+ on_param_error("formal argument cannot be a constant", location.slice)
+ when :argument_formal_global
+ on_param_error("formal argument cannot be a global variable", location.slice)
+ when :argument_formal_ivar
+ on_param_error("formal argument cannot be an instance variable", location.slice)
+ when :class_name, :module_name
+ on_class_name_error("class/module name must be CONSTANT", location.slice)
+ else
+ on_parse_error(error.message)
+ end
+ end
+
+ nil
+ else
+ result.value.accept(self)
+ end
+ end
+
+ ##########################################################################
+ # Visitor methods
+ ##########################################################################
+
+ # :stopdoc:
+
+ # alias foo bar
+ # ^^^^^^^^^^^^^
+ def visit_alias_method_node(node)
+ bounds(node.keyword_loc)
+ on_kw("alias")
+
+ new_name = visit(node.new_name)
+ old_name = visit(node.old_name)
+
+ bounds(node.location)
+ on_alias(new_name, old_name)
+ end
+
+ # alias $foo $bar
+ # ^^^^^^^^^^^^^^^
+ def visit_alias_global_variable_node(node)
+ bounds(node.keyword_loc)
+ on_kw("alias")
+
+ new_name = visit_alias_global_variable_node_value(node.new_name)
+ old_name = visit_alias_global_variable_node_value(node.old_name)
+
+ bounds(node.location)
+ on_var_alias(new_name, old_name)
+ end
+
+ # Visit one side of an alias global variable node.
+ private def visit_alias_global_variable_node_value(node)
+ bounds(node.location)
+
+ case node
+ when BackReferenceReadNode
+ on_backref(node.slice)
+ when GlobalVariableReadNode
+ on_gvar(node.name.to_s)
+ else
+ raise
+ end
+ end
+
+ # foo => bar | baz
+ # ^^^^^^^^^
+ def visit_alternation_pattern_node(node)
+ left = visit_pattern_node(node.left)
+
+ bounds(node.operator_loc)
+ on_op("|")
+
+ right = visit_pattern_node(node.right)
+
+ bounds(node.location)
+ on_binary(left, :|, right)
+ end
+
+ # Visit a pattern within a pattern match. This is used to bypass the
+ # parenthesis node that can be used to wrap patterns.
+ private def visit_pattern_node(node)
+ if node.is_a?(ParenthesesNode)
+ bounds(node.opening_loc)
+ on_lparen("(")
+ result = visit(node.body)
+ bounds(node.closing_loc)
+ on_rparen(")")
+
+ result
+ else
+ visit(node)
+ end
+ end
+
+ # a and b
+ # ^^^^^^^
+ def visit_and_node(node)
+ left = visit(node.left)
+
+ bounds(node.operator_loc)
+ if node.operator == "and"
+ on_kw("and")
+ else
+ on_op("&&")
+ end
+
+ right = visit(node.right)
+
+ bounds(node.location)
+ on_binary(left, node.operator.to_sym, right)
+ end
+
+ # []
+ # ^^
+ def visit_array_node(node)
+ case (opening = node.opening)
+ when /^%w/
+ opening_loc = node.opening_loc
+ bounds(opening_loc)
+ on_qwords_beg(opening)
+
+ elements = on_qwords_new
+ previous = nil
+
+ node.elements.each do |element|
+ visit_words_sep(opening_loc, previous, element)
+
+ bounds(element.location)
+ elements = on_qwords_add(elements, on_tstring_content(element.content))
+
+ previous = element
+ end
+
+ visit_words_sep(opening_loc, node.elements.last, node.closing_loc)
+
+ bounds(node.closing_loc)
+ on_tstring_end(node.closing)
+ when /^%i/
+ opening_loc = node.opening_loc
+ bounds(opening_loc)
+ on_qsymbols_beg(opening)
+
+ elements = on_qsymbols_new
+ previous = nil
+
+ node.elements.each do |element|
+ visit_words_sep(opening_loc, previous, element)
+
+ bounds(element.location)
+ elements = on_qsymbols_add(elements, on_tstring_content(element.value))
+
+ previous = element
+ end
+
+ visit_words_sep(opening_loc, node.elements.last, node.closing_loc)
+
+ bounds(node.closing_loc)
+ on_tstring_end(node.closing)
+ when /^%W/
+ opening_loc = node.opening_loc
+ bounds(opening_loc)
+ on_words_beg(opening)
+
+ elements = on_words_new
+ previous = nil
+
+ node.elements.each do |element|
+ visit_words_sep(opening_loc, previous, element)
+
+ bounds(element.location)
+ elements =
+ on_words_add(
+ elements,
+ if element.is_a?(StringNode)
+ on_word_add(on_word_new, on_tstring_content(element.content))
+ else
+ element.parts.inject(on_word_new) do |word, part|
+ word_part =
+ if part.is_a?(StringNode)
+ bounds(part.location)
+ on_tstring_content(part.content)
+ else
+ visit(part)
+ end
+
+ on_word_add(word, word_part)
+ end
+ end
+ )
+
+ previous = element
+ end
+
+ visit_words_sep(opening_loc, node.elements.last, node.closing_loc)
+
+ bounds(node.closing_loc)
+ on_tstring_end(node.closing)
+ when /^%I/
+ opening_loc = node.opening_loc
+ bounds(opening_loc)
+ on_symbols_beg(opening)
+
+ elements = on_symbols_new
+ previous = nil
+
+ node.elements.each do |element|
+ visit_words_sep(opening_loc, previous, element)
+
+ bounds(element.location)
+ elements =
+ on_symbols_add(
+ elements,
+ if element.is_a?(SymbolNode)
+ on_word_add(on_word_new, on_tstring_content(element.value))
+ else
+ element.parts.inject(on_word_new) do |word, part|
+ word_part =
+ if part.is_a?(StringNode)
+ bounds(part.location)
+ on_tstring_content(part.content)
+ else
+ visit(part)
+ end
+
+ on_word_add(word, word_part)
+ end
+ end
+ )
+
+ previous = element
+ end
+
+ visit_words_sep(opening_loc, node.elements.last, node.closing_loc)
+
+ bounds(node.closing_loc)
+ on_tstring_end(node.closing)
+ else
+ bounds(node.opening_loc)
+ on_lbracket(opening)
+
+ elements = visit_arguments(node.elements) unless node.elements.empty?
+
+ bounds(node.closing_loc)
+ on_rbracket(node.closing)
+ end
+
+ bounds(node.location)
+ on_array(elements)
+ end
+
+ # Dispatch words_sep events that contains the whitespace between the elements
+ # of list literals.
+ private def visit_words_sep(opening_loc, previous, current)
+ start_offset = (previous.nil? ? opening_loc : previous.location).end_offset
+ end_offset = current.start_offset
+ length = end_offset - start_offset
+
+ if length > 0
+ whitespace = source.byteslice(start_offset, length)
+ current_offset = start_offset
+ whitespace.each_line do |part|
+ bounds(opening_loc.copy(start_offset: current_offset, length: part.bytesize))
+ on_words_sep(part)
+ current_offset += part.bytesize
+ end
+ end
+ end
+
+ # Visit a list of elements, like the elements of an array or arguments.
+ private def visit_arguments(elements)
+ bounds(elements.first.location)
+ elements.inject(on_args_new) do |args, element|
+ arg = visit(element)
+ bounds(element.location)
+
+ case element
+ when BlockArgumentNode
+ on_args_add_block(args, arg)
+ when SplatNode
+ on_args_add_star(args, arg)
+ else
+ on_args_add(args, arg)
+ end
+ end
+ end
+
+ # foo => [bar]
+ # ^^^^^
+ def visit_array_pattern_node(node)
+ constant = visit(node.constant)
+
+ if node.opening_loc
+ bounds(node.opening_loc)
+ node.opening == "[" ? on_lbracket("[") : on_lparen("(")
+ end
+
+ requireds = visit_all(node.requireds) if node.requireds.any?
+ rest =
+ if (rest_node = node.rest).is_a?(SplatNode)
+ bounds(rest_node.operator_loc)
+ on_op("*")
+
+ if rest_node.expression.nil?
+ bounds(rest_node.location)
+ on_var_field(nil)
+ else
+ visit(rest_node.expression)
+ end
+ end
+
+ posts = visit_all(node.posts) if node.posts.any?
+
+ if node.closing_loc
+ bounds(node.closing_loc)
+ node.closing == "]" ? on_rbracket("]") : on_rparen(")")
+ end
+ bounds(node.location)
+ on_aryptn(constant, requireds, rest, posts)
+ end
+
+ # foo(bar)
+ # ^^^
+ def visit_arguments_node(node)
+ arguments, _ = visit_call_node_arguments(node, nil, false)
+ arguments
+ end
+
+ # { a: 1 }
+ # ^^^^
+ def visit_assoc_node(node)
+ key = visit(node.key)
+
+ if node.operator_loc
+ bounds(node.operator_loc)
+ on_op("=>")
+ end
+
+ value = visit(node.value)
+
+ bounds(node.location)
+ on_assoc_new(key, value)
+ end
+
+ # def foo(**); bar(**); end
+ # ^^
+ #
+ # { **foo }
+ # ^^^^^
+ def visit_assoc_splat_node(node)
+ bounds(node.operator_loc)
+ on_op("**")
+
+ value = visit(node.value)
+
+ bounds(node.location)
+ on_assoc_splat(value)
+ end
+
+ # $+
+ # ^^
+ def visit_back_reference_read_node(node)
+ bounds(node.location)
+ on_backref(node.slice)
+ end
+
+ # begin end
+ # ^^^^^^^^^
+ def visit_begin_node(node)
+ if node.begin_keyword_loc
+ bounds(node.begin_keyword_loc)
+ on_kw("begin")
+ end
+
+ clauses = visit_begin_node_clauses(node.begin_keyword_loc, node, false)
+
+ if node.end_keyword_loc
+ bounds(node.end_keyword_loc)
+ on_kw("end")
+ end
+
+ bounds(node.location)
+ on_begin(clauses)
+ end
+
+ # Visit the clauses of a begin node to form an on_bodystmt call.
+ private def visit_begin_node_clauses(location, node, allow_newline)
+ statements =
+ if node.statements.nil?
+ on_stmts_add(on_stmts_new, on_void_stmt)
+ else
+ body = node.statements.body
+ body = [nil, *body] if void_stmt?(location, node.statements.body[0].location, allow_newline)
+
+ bounds(node.statements.location)
+ visit_statements_node_body(body)
+ end
+
+ rescue_clause = visit(node.rescue_clause)
+ else_clause =
+ unless (else_clause_node = node.else_clause).nil?
+ bounds(else_clause_node.else_keyword_loc)
+ on_kw("else")
+
+ else_statements =
+ if else_clause_node.statements.nil?
+ [nil]
+ else
+ body = else_clause_node.statements.body
+ body = [nil, *body] if void_stmt?(else_clause_node.else_keyword_loc, else_clause_node.statements.body[0].location, allow_newline)
+ body
+ end
+
+ bounds(else_clause_node.location)
+ visit_statements_node_body(else_statements)
+ end
+ ensure_clause = visit(node.ensure_clause)
+
+ bounds(node.location)
+ on_bodystmt(statements, rescue_clause, else_clause, ensure_clause)
+ end
+
+ # Visit the body of a structure that can have either a set of statements
+ # or statements wrapped in rescue/else/ensure.
+ private def visit_body_node(location, node, allow_newline = false)
+ case node
+ when nil
+ bounds(location)
+ on_bodystmt(visit_statements_node_body([nil]), nil, nil, nil)
+ when StatementsNode
+ body = [*node.body]
+ body = [nil, *body] if void_stmt?(location, body[0].location, allow_newline)
+ stmts = visit_statements_node_body(body)
+
+ bounds(node.body.first.location)
+ on_bodystmt(stmts, nil, nil, nil)
+ when BeginNode
+ visit_begin_node_clauses(location, node, allow_newline)
+ else
+ raise
+ end
+ end
+
+ # foo(&bar)
+ # ^^^^
+ def visit_block_argument_node(node)
+ bounds(node.operator_loc)
+ on_op("&")
+ visit(node.expression)
+ end
+
+ # foo { |; bar| }
+ # ^^^
+ def visit_block_local_variable_node(node)
+ bounds(node.location)
+ on_ident(node.name.to_s)
+ end
+
+ # Visit a BlockNode.
+ def visit_block_node(node)
+ braces = node.opening == "{"
+ bounds(node.opening_loc)
+ if braces
+ on_lbrace("{")
+ else
+ on_kw("do")
+ end
+
+ parameters = visit(node.parameters)
+
+ body =
+ case node.body
+ when nil
+ bounds(node.location)
+ stmts = on_stmts_add(on_stmts_new, on_void_stmt)
+
+ bounds(node.location)
+ braces ? stmts : on_bodystmt(stmts, nil, nil, nil)
+ when StatementsNode
+ stmts = node.body.body
+ stmts = [nil, *stmts] if void_stmt?(node.parameters&.location || node.opening_loc, node.body.location, false)
+ stmts = visit_statements_node_body(stmts)
+
+ bounds(node.body.location)
+ braces ? stmts : on_bodystmt(stmts, nil, nil, nil)
+ when BeginNode
+ visit_body_node(node.parameters&.location || node.opening_loc, node.body)
+ else
+ raise
+ end
+
+ if braces
+ bounds(node.closing_loc)
+ on_rbrace("}")
+ else
+ bounds(node.closing_loc)
+ on_kw("end")
+ end
+
+ if braces
+ bounds(node.location)
+ on_brace_block(parameters, body)
+ else
+ bounds(node.location)
+ on_do_block(parameters, body)
+ end
+ end
+
+ # def foo(&bar); end
+ # ^^^^
+ def visit_block_parameter_node(node)
+ bounds(node.operator_loc)
+ on_op("&")
+
+ if node.name_loc.nil?
+ bounds(node.location)
+ on_blockarg(nil)
+ else
+ bounds(node.name_loc)
+ name = on_ident(node.name.to_s)
+
+ bounds(node.location)
+ on_blockarg(name)
+ end
+ end
+
+ # A block's parameters.
+ def visit_block_parameters_node(node)
+ bounds(node.opening_loc)
+ on_op("|")
+
+ parameters =
+ if node.parameters.nil?
+ on_params(nil, nil, nil, nil, nil, nil, nil)
+ else
+ visit(node.parameters)
+ end
+
+ locals =
+ if node.locals.any?
+ visit_all(node.locals)
+ else
+ false
+ end
+
+ bounds(node.closing_loc)
+ on_op("|")
+
+ bounds(node.location)
+ on_block_var(parameters, locals)
+ end
+
+ # break
+ # ^^^^^
+ #
+ # break foo
+ # ^^^^^^^^^
+ def visit_break_node(node)
+ bounds(node.keyword_loc)
+ on_kw("break")
+
+ if node.arguments.nil?
+ bounds(node.location)
+ on_break(on_args_new)
+ else
+ arguments = visit(node.arguments)
+
+ bounds(node.location)
+ on_break(arguments)
+ end
+ end
+
+ # foo
+ # ^^^
+ #
+ # foo.bar
+ # ^^^^^^^
+ #
+ # foo.bar() {}
+ # ^^^^^^^^^^^^
+ def visit_call_node(node)
+ if node.call_operator_loc.nil?
+ case node.name
+ when :[]
+ receiver = visit(node.receiver)
+
+ bounds(node.opening_loc)
+ on_lbracket("[")
+
+ arguments, block_node = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc))
+
+ bounds(node.closing_loc)
+ on_rbracket("]")
+
+ block = visit(block_node)
+
+ bounds(node.location)
+ call = on_aref(receiver, arguments)
+
+ if block_node
+ bounds(node.location)
+ on_method_add_block(call, block)
+ else
+ call
+ end
+ when :[]=
+ receiver = visit(node.receiver)
+
+ bounds(node.opening_loc)
+ on_lbracket("[")
+
+ *arguments, last_argument = node.arguments.arguments
+ arguments << node.block if !node.block.nil?
+
+ arguments =
+ if arguments.any?
+ args = visit_arguments(arguments)
+
+ if !node.block.nil?
+ args
+ else
+ bounds(arguments.first.location)
+ on_args_add_block(args, false)
+ end
+ end
+
+ bounds(node.closing_loc)
+ on_rbracket("]")
+ bounds(node.equal_loc)
+ on_op("=")
+
+ bounds(node.location)
+ call = on_aref_field(receiver, arguments)
+ value = visit_write_value(last_argument)
+
+ bounds(last_argument.location)
+ on_assign(call, value)
+ when :-@, :+@, :~
+ bounds(node.message_loc)
+ on_op(node.message)
+
+ receiver = visit(node.receiver)
+ bounds(node.location)
+ on_unary(node.name, receiver)
+ when :!
+ bounds(node.message_loc)
+ if node.message == "not"
+ on_kw("not")
+
+ if node.opening_loc
+ bounds(node.opening_loc)
+ on_lparen("(")
+ end
+
+ receiver =
+ if node.receiver.is_a?(ParenthesesNode) && node.receiver.body.nil?
+ # The parens in `not()` just emit parens and nothing else.
+ bounds(node.receiver.opening_loc)
+ on_lparen("(")
+ bounds(node.receiver.closing_loc)
+ on_rparen(")")
+ nil
+ else
+ visit(node.receiver)
+ end
+
+ if node.closing_loc
+ bounds(node.closing_loc)
+ on_rparen(")")
+ end
+ bounds(node.location)
+ on_unary(:not, receiver)
+ else
+ on_op("!")
+
+ receiver = visit(node.receiver)
+
+ bounds(node.location)
+ on_unary(:!, receiver)
+ end
+ when BINARY_OPERATORS
+ receiver = visit(node.receiver)
+
+ bounds(node.message_loc)
+ on_op(node.message)
+
+ value = visit(node.arguments.arguments.first)
+
+ bounds(node.location)
+ on_binary(receiver, node.name, value)
+ else
+ bounds(node.message_loc)
+ message = visit_token(node.message, false)
+
+ if node.variable_call?
+ on_vcall(message)
+ else
+ if node.opening_loc
+ bounds(node.opening_loc)
+ on_lparen("(")
+ end
+
+ arguments, block_node = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc || node.location))
+
+ if node.closing_loc
+ bounds(node.closing_loc)
+ on_rparen(")")
+ end
+
+ block = visit(block_node)
+ call =
+ if node.opening_loc.nil? && get_arguments_and_block(node.arguments, node.block).first.any?
+ bounds(node.location)
+ on_command(message, arguments)
+ elsif !node.opening_loc.nil?
+ bounds(node.location)
+ on_method_add_arg(on_fcall(message), on_arg_paren(arguments))
+ else
+ bounds(node.location)
+ on_method_add_arg(on_fcall(message), on_args_new)
+ end
+
+ if block_node
+ bounds(node.block.location)
+ on_method_add_block(call, block)
+ else
+ call
+ end
+ end
+ end
+ else
+ receiver = visit(node.receiver)
+
+ bounds(node.call_operator_loc)
+ call_operator = visit_call_operator(node.call_operator)
+
+ message =
+ if node.message_loc.nil?
+ :call
+ else
+ bounds(node.message_loc)
+ visit_token(node.message, false)
+ end
+
+ if node.equal_loc
+ bounds(node.equal_loc)
+ on_op("=")
+ end
+
+ if node.name.end_with?("=") && !node.message.end_with?("=") && !node.arguments.nil? && node.block.nil?
+ value = visit_write_value(node.arguments.arguments.first)
+
+ bounds(node.location)
+ on_assign(on_field(receiver, call_operator, message), value)
+ else
+ if node.opening_loc
+ bounds(node.opening_loc)
+ on_lparen("(")
+ end
+
+ arguments, block_node = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc || node.location))
+
+ if node.closing_loc
+ bounds(node.closing_loc)
+ on_rparen(")")
+ end
+
+ block = visit(block_node)
+ call =
+ if node.opening_loc.nil?
+ bounds(node.location)
+
+ if node.arguments.nil? && !node.block.is_a?(BlockArgumentNode)
+ on_call(receiver, call_operator, message)
+ else
+ on_command_call(receiver, call_operator, message, arguments)
+ end
+ else
+ bounds(node.opening_loc)
+ arguments = on_arg_paren(arguments)
+
+ bounds(node.location)
+ on_method_add_arg(on_call(receiver, call_operator, message), arguments)
+ end
+
+ if block_node
+ bounds(node.block.location)
+ on_method_add_block(call, block)
+ else
+ call
+ end
+ end
+ end
+ end
+
+ # Extract the arguments and block Ripper-style, which means if the block
+ # is like `&b` then it's moved to arguments.
+ private def get_arguments_and_block(arguments_node, block_node)
+ arguments = arguments_node&.arguments || []
+ block = block_node
+
+ if block.is_a?(BlockArgumentNode)
+ arguments += [block]
+ block = nil
+ end
+
+ [arguments, block]
+ end
+
+ # Visit the arguments and block of a call node and return the arguments
+ # and block as they should be used.
+ private def visit_call_node_arguments(arguments_node, block_node, trailing_comma)
+ arguments, block = get_arguments_and_block(arguments_node, block_node)
+
+ [
+ if arguments.length == 1 && arguments.first.is_a?(ForwardingArgumentsNode)
+ visit(arguments.first)
+ elsif arguments.any?
+ args = visit_arguments(arguments)
+
+ if block_node.is_a?(BlockArgumentNode) || arguments.last.is_a?(ForwardingArgumentsNode) || command?(arguments.last) || trailing_comma
+ args
+ else
+ bounds(arguments.first.location)
+ on_args_add_block(args, false)
+ end
+ end,
+ block,
+ ]
+ end
+
+ # Returns true if the given node is a command node.
+ private def command?(node)
+ node.is_a?(CallNode) &&
+ node.opening_loc.nil? &&
+ (!node.arguments.nil? || node.block.is_a?(BlockArgumentNode)) &&
+ !BINARY_OPERATORS.include?(node.name)
+ end
+
+ # foo.bar += baz
+ # ^^^^^^^^^^^^^^^
+ def visit_call_operator_write_node(node)
+ receiver = visit(node.receiver)
+
+ bounds(node.call_operator_loc)
+ call_operator = visit_call_operator(node.call_operator)
+
+ bounds(node.message_loc)
+ message = visit_token(node.message)
+
+ bounds(node.location)
+ target = on_field(receiver, call_operator, message)
+
+ bounds(node.binary_operator_loc)
+ operator = on_op("#{node.binary_operator}=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # foo.bar &&= baz
+ # ^^^^^^^^^^^^^^^
+ def visit_call_and_write_node(node)
+ receiver = visit(node.receiver)
+
+ bounds(node.call_operator_loc)
+ call_operator = visit_call_operator(node.call_operator)
+
+ bounds(node.message_loc)
+ message = visit_token(node.message)
+
+ bounds(node.location)
+ target = on_field(receiver, call_operator, message)
+
+ bounds(node.operator_loc)
+ operator = on_op("&&=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # foo.bar ||= baz
+ # ^^^^^^^^^^^^^^^
+ def visit_call_or_write_node(node)
+ receiver = visit(node.receiver)
+
+ bounds(node.call_operator_loc)
+ call_operator = visit_call_operator(node.call_operator)
+
+ bounds(node.message_loc)
+ message = visit_token(node.message)
+
+ bounds(node.location)
+ target = on_field(receiver, call_operator, message)
+
+ bounds(node.operator_loc)
+ operator = on_op("||=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # foo.bar, = 1
+ # ^^^^^^^
+ def visit_call_target_node(node)
+ if node.call_operator == "::"
+ receiver = visit(node.receiver)
+
+ bounds(node.call_operator_loc)
+ on_op("::")
+
+ bounds(node.message_loc)
+ message = visit_token(node.message)
+
+ bounds(node.location)
+ on_const_path_field(receiver, message)
+ else
+ receiver = visit(node.receiver)
+
+ bounds(node.call_operator_loc)
+ call_operator = visit_call_operator(node.call_operator)
+
+ bounds(node.message_loc)
+ message = visit_token(node.message)
+
+ bounds(node.location)
+ on_field(receiver, call_operator, message)
+ end
+ end
+
+ # foo => bar => baz
+ # ^^^^^^^^^^
+ def visit_capture_pattern_node(node)
+ value = visit(node.value)
+
+ bounds(node.operator_loc)
+ on_op("=>")
+
+ target = visit(node.target)
+
+ bounds(node.location)
+ on_binary(value, :"=>", target)
+ end
+
+ # case foo; when bar; end
+ # ^^^^^^^^^^^^^^^^^^^^^^^
+ def visit_case_node(node)
+ bounds(node.case_keyword_loc)
+ on_kw("case")
+
+ predicate = visit(node.predicate)
+ visited_conditions = node.conditions.map { |condition| visit(condition) }
+ visited_else_clause = visit(node.else_clause)
+
+ if !node.else_clause
+ bounds(node.end_keyword_loc)
+ on_kw("end")
+ end
+
+ clauses =
+ visited_conditions.reverse_each.inject(visited_else_clause) do |current, condition|
+ on_when(*condition, current)
+ end
+
+ bounds(node.location)
+ on_case(predicate, clauses)
+ end
+
+ # case foo; in bar; end
+ # ^^^^^^^^^^^^^^^^^^^^^
+ def visit_case_match_node(node)
+ bounds(node.case_keyword_loc)
+ on_kw("case")
+
+ predicate = visit(node.predicate)
+ visited_conditions = node.conditions.map do | condition|
+ visit(condition)
+ end
+ visited_else_clause = visit(node.else_clause)
+
+ if !node.else_clause
+ bounds(node.end_keyword_loc)
+ on_kw("end")
+ end
+
+ clauses =
+ visited_conditions.reverse_each.inject(visited_else_clause) do |current, condition|
+ on_in(*condition, current)
+ end
+
+ bounds(node.location)
+ on_case(predicate, clauses)
+ end
+
+ # class Foo; end
+ # ^^^^^^^^^^^^^^
+ def visit_class_node(node)
+ bounds(node.class_keyword_loc)
+ on_kw("class")
+
+ constant_path =
+ if node.constant_path.is_a?(ConstantReadNode)
+ bounds(node.constant_path.location)
+ on_const_ref(on_const(node.constant_path.name.to_s))
+ else
+ visit(node.constant_path)
+ end
+
+ if node.inheritance_operator_loc
+ bounds(node.inheritance_operator_loc)
+ on_op("<")
+ end
+
+ superclass = visit(node.superclass)
+ bodystmt = visit_body_node(node.superclass&.location || node.constant_path.location, node.body, node.superclass.nil?)
+
+ bounds(node.end_keyword_loc)
+ on_kw("end")
+
+ bounds(node.location)
+ on_class(constant_path, superclass, bodystmt)
+ end
+
+ # @@foo
+ # ^^^^^
+ def visit_class_variable_read_node(node)
+ bounds(node.location)
+ on_var_ref(on_cvar(node.slice))
+ end
+
+ # @@foo = 1
+ # ^^^^^^^^^
+ def visit_class_variable_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_cvar(node.name.to_s))
+
+ bounds(node.operator_loc)
+ on_op("=")
+
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_assign(target, value)
+ end
+
+ # @@foo += bar
+ # ^^^^^^^^^^^^
+ def visit_class_variable_operator_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_cvar(node.name.to_s))
+
+ bounds(node.binary_operator_loc)
+ operator = on_op("#{node.binary_operator}=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # @@foo &&= bar
+ # ^^^^^^^^^^^^^
+ def visit_class_variable_and_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_cvar(node.name.to_s))
+
+ bounds(node.operator_loc)
+ operator = on_op("&&=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # @@foo ||= bar
+ # ^^^^^^^^^^^^^
+ def visit_class_variable_or_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_cvar(node.name.to_s))
+
+ bounds(node.operator_loc)
+ operator = on_op("||=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # @@foo, = bar
+ # ^^^^^
+ def visit_class_variable_target_node(node)
+ bounds(node.location)
+ on_var_field(on_cvar(node.name.to_s))
+ end
+
+ # Foo
+ # ^^^
+ def visit_constant_read_node(node)
+ bounds(node.location)
+ on_var_ref(on_const(node.name.to_s))
+ end
+
+ # Foo = 1
+ # ^^^^^^^
+ def visit_constant_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_const(node.name.to_s))
+
+ bounds(node.operator_loc)
+ on_op("=")
+
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_assign(target, value)
+ end
+
+ # Foo += bar
+ # ^^^^^^^^^^^
+ def visit_constant_operator_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_const(node.name.to_s))
+
+ bounds(node.binary_operator_loc)
+ operator = on_op("#{node.binary_operator}=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # Foo &&= bar
+ # ^^^^^^^^^^^^
+ def visit_constant_and_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_const(node.name.to_s))
+
+ bounds(node.operator_loc)
+ operator = on_op("&&=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # Foo ||= bar
+ # ^^^^^^^^^^^^
+ def visit_constant_or_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_const(node.name.to_s))
+
+ bounds(node.operator_loc)
+ operator = on_op("||=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # Foo, = bar
+ # ^^^
+ def visit_constant_target_node(node)
+ bounds(node.location)
+ on_var_field(on_const(node.name.to_s))
+ end
+
+ # Foo::Bar
+ # ^^^^^^^^
+ def visit_constant_path_node(node)
+ if node.parent.nil?
+ if node.delimiter_loc
+ bounds(node.delimiter_loc)
+ on_op("::")
+ end
+
+ bounds(node.name_loc)
+ child = on_const(node.name.to_s)
+
+ bounds(node.location)
+ on_top_const_ref(child)
+ else
+ parent = visit(node.parent)
+
+ bounds(node.delimiter_loc)
+ on_op("::")
+
+ bounds(node.name_loc)
+ child = on_const(node.name.to_s)
+
+ bounds(node.location)
+ on_const_path_ref(parent, child)
+ end
+ end
+
+ # Foo::Bar = 1
+ # ^^^^^^^^^^^^
+ def visit_constant_path_write_node(node)
+ target = visit_constant_path_write_node_target(node.target)
+
+ bounds(node.operator_loc)
+ on_op("=")
+
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_assign(target, value)
+ end
+
+ # Visit a constant path that is part of a write node.
+ private def visit_constant_path_write_node_target(node)
+ if node.parent.nil?
+ if node.delimiter_loc
+ bounds(node.delimiter_loc)
+ on_op("::")
+ end
+
+ bounds(node.name_loc)
+ child = on_const(node.name.to_s)
+
+ bounds(node.location)
+ on_top_const_field(child)
+ else
+ parent = visit(node.parent)
+
+ bounds(node.delimiter_loc)
+ on_op("::")
+
+ bounds(node.name_loc)
+ child = on_const(node.name.to_s)
+
+ bounds(node.location)
+ on_const_path_field(parent, child)
+ end
+ end
+
+ # Foo::Bar += baz
+ # ^^^^^^^^^^^^^^^
+ def visit_constant_path_operator_write_node(node)
+ target = visit_constant_path_write_node_target(node.target)
+
+ bounds(node.binary_operator_loc)
+ operator = on_op("#{node.binary_operator}=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # Foo::Bar &&= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_constant_path_and_write_node(node)
+ target = visit_constant_path_write_node_target(node.target)
+
+ bounds(node.operator_loc)
+ operator = on_op("&&=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # Foo::Bar ||= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_constant_path_or_write_node(node)
+ target = visit_constant_path_write_node_target(node.target)
+
+ bounds(node.operator_loc)
+ operator = on_op("||=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # Foo::Bar, = baz
+ # ^^^^^^^^
+ def visit_constant_path_target_node(node)
+ visit_constant_path_write_node_target(node)
+ end
+
+ # def foo; end
+ # ^^^^^^^^^^^^
+ #
+ # def self.foo; end
+ # ^^^^^^^^^^^^^^^^^
+ def visit_def_node(node)
+ bounds(node.def_keyword_loc)
+ on_kw("def")
+
+ receiver = visit(node.receiver)
+ operator =
+ if !node.operator_loc.nil?
+ bounds(node.operator_loc)
+ node.operator == "." ? on_period(".") : on_op("::")
+ end
+
+ bounds(node.name_loc)
+ name = visit_token(node.name_loc.slice)
+
+ if node.lparen_loc
+ bounds(node.lparen_loc)
+ on_lparen("(")
+ end
+
+ parameters =
+ if node.parameters.nil?
+ bounds(node.location)
+ on_params(nil, nil, nil, nil, nil, nil, nil)
+ else
+ visit(node.parameters)
+ end
+
+ if !node.lparen_loc.nil?
+ bounds(node.rparen_loc)
+ on_rparen(")")
+ bounds(node.lparen_loc)
+ parameters = on_paren(parameters)
+ end
+
+ if node.equal_loc
+ bounds(node.equal_loc)
+ on_op("=")
+ end
+
+ bodystmt =
+ if node.equal_loc.nil?
+ visit_body_node(node.rparen_loc || node.end_keyword_loc, node.body)
+ else
+ body = visit(node.body.body.first)
+
+ bounds(node.body.location)
+ on_bodystmt(body, nil, nil, nil)
+ end
+
+ if node.end_keyword_loc
+ bounds(node.end_keyword_loc)
+ on_kw("end")
+ end
+
+ bounds(node.location)
+ if receiver
+ on_defs(receiver, operator, name, parameters, bodystmt)
+ else
+ on_def(name, parameters, bodystmt)
+ end
+ end
+
+ # defined? a
+ # ^^^^^^^^^^
+ #
+ # defined?(a)
+ # ^^^^^^^^^^^
+ def visit_defined_node(node)
+ bounds(node.keyword_loc)
+ on_kw("defined?")
+
+ if node.lparen_loc
+ bounds(node.lparen_loc)
+ on_lparen("(")
+ end
+
+ expression = visit(node.value)
+
+ if node.rparen_loc
+ bounds(node.rparen_loc)
+ on_rparen(")")
+ end
+
+ # Very weird circumstances here where something like:
+ #
+ # defined?
+ # (1)
+ #
+ # gets parsed in Ruby as having only the `1` expression but in Ripper it
+ # gets parsed as having a parentheses node. In this case we need to
+ # synthesize that node to match Ripper's behavior.
+ if node.lparen_loc && node.keyword_loc.join(node.lparen_loc).slice.include?("\n")
+ bounds(node.lparen_loc.join(node.rparen_loc))
+ expression = on_paren(on_stmts_add(on_stmts_new, expression))
+ end
+
+ bounds(node.location)
+ on_defined(expression)
+ end
+
+ # if foo then bar else baz end
+ # ^^^^^^^^^^^^
+ def visit_else_node(node)
+ bounds(node.else_keyword_loc)
+ on_kw("else")
+
+ statements =
+ if node.statements.nil?
+ [nil]
+ else
+ body = node.statements.body
+ body = [nil, *body] if void_stmt?(node.else_keyword_loc, node.statements.body[0].location, false)
+ body
+ end
+
+ else_statements = visit_statements_node_body(statements)
+
+ bounds(node.end_keyword_loc)
+ on_kw("end")
+ bounds(node.location)
+ on_else(else_statements)
+ end
+
+ # "foo #{bar}"
+ # ^^^^^^
+ def visit_embedded_statements_node(node)
+ bounds(node.opening_loc)
+ on_embexpr_beg(node.opening)
+
+ statements =
+ if node.statements.nil?
+ bounds(node.location)
+ on_stmts_add(on_stmts_new, on_void_stmt)
+ else
+ visit(node.statements)
+ end
+
+ bounds(node.closing_loc)
+ on_embexpr_end(node.closing)
+
+ bounds(node.location)
+ on_string_embexpr(statements)
+ end
+
+ # "foo #@bar"
+ # ^^^^^
+ def visit_embedded_variable_node(node)
+ bounds(node.operator_loc)
+ on_embvar(node.operator)
+
+ variable = visit(node.variable)
+
+ bounds(node.location)
+ on_string_dvar(variable)
+ end
+
+ # Visit an EnsureNode node.
+ def visit_ensure_node(node)
+ bounds(node.ensure_keyword_loc)
+ on_kw("ensure")
+
+ statements =
+ if node.statements.nil?
+ [nil]
+ else
+ body = node.statements.body
+ body = [nil, *body] if void_stmt?(node.ensure_keyword_loc, body[0].location, false)
+ body
+ end
+
+ statements = visit_statements_node_body(statements)
+
+ bounds(node.location)
+ on_ensure(statements)
+ end
+
+ # false
+ # ^^^^^
+ def visit_false_node(node)
+ bounds(node.location)
+ on_var_ref(on_kw("false"))
+ end
+
+ # foo => [*, bar, *]
+ # ^^^^^^^^^^^
+ def visit_find_pattern_node(node)
+ constant = visit(node.constant)
+
+ if node.opening_loc
+ bounds(node.opening_loc)
+ node.opening == "[" ? on_lbracket("[") : on_lparen("(")
+ end
+ bounds(node.left.operator_loc)
+ on_op("*")
+
+ left =
+ if node.left.expression.nil?
+ bounds(node.left.location)
+ on_var_field(nil)
+ else
+ visit(node.left.expression)
+ end
+
+ requireds = visit_all(node.requireds) if node.requireds.any?
+
+ bounds(node.right.operator_loc)
+ on_op("*")
+
+ right =
+ if node.right.expression.nil?
+ bounds(node.right.location)
+ on_var_field(nil)
+ else
+ visit(node.right.expression)
+ end
+
+ if node.closing_loc
+ bounds(node.closing_loc)
+ node.closing == "]" ? on_rbracket("]") : on_rparen(")")
+ end
+ bounds(node.location)
+ on_fndptn(constant, left, requireds, right)
+ end
+
+ # if foo .. bar; end
+ # ^^^^^^^^^^
+ def visit_flip_flop_node(node)
+ left = visit(node.left)
+
+ bounds(node.operator_loc)
+ on_op(node.operator)
+
+ right = visit(node.right)
+
+ bounds(node.location)
+ if node.exclude_end?
+ on_dot3(left, right)
+ else
+ on_dot2(left, right)
+ end
+ end
+
+ # 1.0
+ # ^^^
+ def visit_float_node(node)
+ visit_number_node(node) { |text| on_float(text) }
+ end
+
+ # for foo in bar do end
+ # ^^^^^^^^^^^^^^^^^^^^^
+ def visit_for_node(node)
+ bounds(node.for_keyword_loc)
+ on_kw("for")
+
+ index = visit(node.index)
+ bounds(node.in_keyword_loc)
+ on_kw("in")
+
+ collection = visit(node.collection)
+ if node.do_keyword_loc
+ bounds(node.do_keyword_loc)
+ on_kw("do")
+ end
+ statements =
+ if node.statements.nil?
+ bounds(node.location)
+ on_stmts_add(on_stmts_new, on_void_stmt)
+ else
+ visit(node.statements)
+ end
+
+ bounds(node.end_keyword_loc)
+ on_kw("end")
+
+ bounds(node.location)
+ on_for(index, collection, statements)
+ end
+
+ # def foo(...); bar(...); end
+ # ^^^
+ def visit_forwarding_arguments_node(node)
+ bounds(node.location)
+ on_op("...")
+ on_args_forward
+ end
+
+ # def foo(...); end
+ # ^^^
+ def visit_forwarding_parameter_node(node)
+ bounds(node.location)
+ on_op("...")
+ on_args_forward
+ end
+
+ # super
+ # ^^^^^
+ #
+ # super {}
+ # ^^^^^^^^
+ def visit_forwarding_super_node(node)
+ bounds(node.keyword_loc)
+ on_kw("super")
+
+ if node.block.nil?
+ bounds(node.location)
+ on_zsuper
+ else
+ block = visit(node.block)
+
+ bounds(node.location)
+ on_method_add_block(on_zsuper, block)
+ end
+ end
+
+ # $foo
+ # ^^^^
+ def visit_global_variable_read_node(node)
+ bounds(node.location)
+ on_var_ref(on_gvar(node.name.to_s))
+ end
+
+ # $foo = 1
+ # ^^^^^^^^
+ def visit_global_variable_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_gvar(node.name.to_s))
+
+ bounds(node.operator_loc)
+ on_op("=")
+
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_assign(target, value)
+ end
+
+ # $foo += bar
+ # ^^^^^^^^^^^
+ def visit_global_variable_operator_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_gvar(node.name.to_s))
+
+ bounds(node.binary_operator_loc)
+ operator = on_op("#{node.binary_operator}=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # $foo &&= bar
+ # ^^^^^^^^^^^^
+ def visit_global_variable_and_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_gvar(node.name.to_s))
+
+ bounds(node.operator_loc)
+ operator = on_op("&&=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # $foo ||= bar
+ # ^^^^^^^^^^^^
+ def visit_global_variable_or_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_gvar(node.name.to_s))
+
+ bounds(node.operator_loc)
+ operator = on_op("||=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # $foo, = bar
+ # ^^^^
+ def visit_global_variable_target_node(node)
+ bounds(node.location)
+ on_var_field(on_gvar(node.name.to_s))
+ end
+
+ # {}
+ # ^^
+ def visit_hash_node(node)
+ bounds(node.opening_loc)
+ on_lbrace("{")
+
+ elements =
+ if node.elements.any?
+ args = visit_all(node.elements)
+
+ bounds(node.elements.first.location)
+ on_assoclist_from_args(args)
+ end
+
+ bounds(node.closing_loc)
+ on_rbrace("}")
+ bounds(node.location)
+ on_hash(elements)
+ end
+
+ # foo => {}
+ # ^^
+ def visit_hash_pattern_node(node)
+ constant = visit(node.constant)
+
+ if node.constant
+ bounds(node.opening_loc)
+ node.opening == "[" ? on_lbracket("[") : on_lparen("(")
+ elsif node.opening_loc
+ bounds(node.opening_loc)
+ on_lbrace("{")
+ end
+
+ elements =
+ if node.elements.any? || !node.rest.nil?
+ node.elements.map do |element|
+ [
+ if (key = element.key).opening_loc.nil?
+ visit(key)
+ else
+ bounds(key.value_loc)
+ if (value = key.value).empty?
+ on_string_content
+ else
+ on_string_add(on_string_content, on_tstring_content(value))
+ end
+ end,
+ visit(element.value)
+ ]
+ end
+ end
+
+ rest =
+ case node.rest
+ when AssocSplatNode
+ bounds(node.rest.operator_loc)
+ on_op("**")
+ visit(node.rest.value)
+ when NoKeywordsParameterNode
+ bounds(node.rest.location)
+ on_var_field(visit(node.rest))
+ end
+
+ if node.constant
+ bounds(node.closing_loc)
+ node.closing == "]" ? on_rbracket("]") : on_rparen(")")
+ elsif node.closing_loc
+ bounds(node.closing_loc)
+ on_rbrace("}")
+ end
+ bounds(node.location)
+ on_hshptn(constant, elements, rest)
+ end
+
+ # if foo then bar end
+ # ^^^^^^^^^^^^^^^^^^^
+ #
+ # bar if foo
+ # ^^^^^^^^^^
+ #
+ # foo ? bar : baz
+ # ^^^^^^^^^^^^^^^
+ def visit_if_node(node)
+ if node.then_keyword == "?"
+ predicate = visit(node.predicate)
+
+ bounds(node.then_keyword_loc)
+ on_op("?")
+
+ truthy = visit(node.statements.body.first)
+
+ bounds(node.subsequent.else_keyword_loc)
+ on_op(":")
+
+ falsy = visit(node.subsequent.statements.body.first)
+
+ bounds(node.location)
+ on_ifop(predicate, truthy, falsy)
+ elsif node.statements.nil? || (node.predicate.location.start_offset < node.statements.location.start_offset)
+ bounds(node.if_keyword_loc)
+ on_kw(node.if_keyword)
+ predicate = visit(node.predicate)
+ if node.then_keyword_loc && node.then_keyword != "?"
+ bounds(node.then_keyword_loc)
+ on_kw("then")
+ end
+ statements =
+ if node.statements.nil?
+ bounds(node.location)
+ on_stmts_add(on_stmts_new, on_void_stmt)
+ else
+ visit(node.statements)
+ end
+ subsequent = visit(node.subsequent)
+
+ if node.end_keyword_loc && !node.subsequent
+ bounds(node.end_keyword_loc)
+ on_kw("end")
+ end
+
+ bounds(node.location)
+ if node.if_keyword == "if"
+ on_if(predicate, statements, subsequent)
+ else
+ on_elsif(predicate, statements, subsequent)
+ end
+ else
+ statements = visit(node.statements.body.first)
+ bounds(node.if_keyword_loc)
+ on_kw(node.if_keyword)
+ predicate = visit(node.predicate)
+
+ bounds(node.location)
+ on_if_mod(predicate, statements)
+ end
+ end
+
+ # 1i
+ # ^^
+ def visit_imaginary_node(node)
+ visit_number_node(node) { |text| on_imaginary(text) }
+ end
+
+ # { foo: }
+ # ^^^^
+ def visit_implicit_node(node)
+ end
+
+ # foo { |bar,| }
+ # ^
+ def visit_implicit_rest_node(node)
+ bounds(node.location)
+ on_excessed_comma
+ end
+
+ # case foo; in bar; end
+ # ^^^^^^^^^^^^^^^^^^^^^
+ def visit_in_node(node)
+ # This is a special case where we're not going to call on_in directly
+ # because we don't have access to the subsequent. Instead, we'll return
+ # the component parts and let the parent node handle it.
+ bounds(node.in_loc)
+ on_kw("in")
+
+ pattern = visit_pattern_node(node.pattern)
+ if node.then_loc
+ bounds(node.then_loc)
+ on_kw("then")
+ end
+ statements =
+ if node.statements.nil?
+ bounds(node.location)
+ on_stmts_add(on_stmts_new, on_void_stmt)
+ else
+ visit(node.statements)
+ end
+
+ [pattern, statements]
+ end
+
+ # foo[bar] += baz
+ # ^^^^^^^^^^^^^^^
+ def visit_index_operator_write_node(node)
+ receiver = visit(node.receiver)
+
+ bounds(node.opening_loc)
+ on_lbracket("[")
+
+ arguments, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc))
+
+ bounds(node.closing_loc)
+ on_rbracket("]")
+
+ bounds(node.location)
+ target = on_aref_field(receiver, arguments)
+
+ bounds(node.binary_operator_loc)
+ operator = on_op("#{node.binary_operator}=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # foo[bar] &&= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_index_and_write_node(node)
+ receiver = visit(node.receiver)
+
+ bounds(node.opening_loc)
+ on_lbracket("[")
+
+ arguments, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc))
+
+ bounds(node.closing_loc)
+ on_rbracket("]")
+
+ bounds(node.location)
+ target = on_aref_field(receiver, arguments)
+
+ bounds(node.operator_loc)
+ operator = on_op("&&=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # foo[bar] ||= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_index_or_write_node(node)
+ receiver = visit(node.receiver)
+
+ bounds(node.opening_loc)
+ on_lbracket("[")
+
+ arguments, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc))
+
+ bounds(node.closing_loc)
+ on_rbracket("]")
+
+ bounds(node.location)
+ target = on_aref_field(receiver, arguments)
+
+ bounds(node.operator_loc)
+ operator = on_op("||=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # foo[bar], = 1
+ # ^^^^^^^^
+ def visit_index_target_node(node)
+ receiver = visit(node.receiver)
+
+ bounds(node.opening_loc)
+ on_lbracket("[")
+
+ arguments, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc))
+
+ bounds(node.closing_loc)
+ on_rbracket("]")
+
+ bounds(node.location)
+ on_aref_field(receiver, arguments)
+ end
+
+ # @foo
+ # ^^^^
+ def visit_instance_variable_read_node(node)
+ bounds(node.location)
+ on_var_ref(on_ivar(node.name.to_s))
+ end
+
+ # @foo = 1
+ # ^^^^^^^^
+ def visit_instance_variable_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_ivar(node.name.to_s))
+
+ bounds(node.operator_loc)
+ on_op("=")
+
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_assign(target, value)
+ end
+
+ # @foo += bar
+ # ^^^^^^^^^^^
+ def visit_instance_variable_operator_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_ivar(node.name.to_s))
+
+ bounds(node.binary_operator_loc)
+ operator = on_op("#{node.binary_operator}=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # @foo &&= bar
+ # ^^^^^^^^^^^^
+ def visit_instance_variable_and_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_ivar(node.name.to_s))
+
+ bounds(node.operator_loc)
+ operator = on_op("&&=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # @foo ||= bar
+ # ^^^^^^^^^^^^
+ def visit_instance_variable_or_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_ivar(node.name.to_s))
+
+ bounds(node.operator_loc)
+ operator = on_op("||=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # @foo, = bar
+ # ^^^^
+ def visit_instance_variable_target_node(node)
+ bounds(node.location)
+ on_var_field(on_ivar(node.name.to_s))
+ end
+
+ # 1
+ # ^
+ def visit_integer_node(node)
+ visit_number_node(node) { |text| on_int(text) }
+ end
+
+ # if /foo #{bar}/ then end
+ # ^^^^^^^^^^^^
+ def visit_interpolated_match_last_line_node(node)
+ bounds(node.opening_loc)
+ on_regexp_beg(node.opening)
+
+ bounds(node.parts.first.location)
+ parts =
+ node.parts.inject(on_regexp_new) do |content, part|
+ on_regexp_add(content, visit_string_content(part))
+ end
+
+ bounds(node.closing_loc)
+ closing = on_regexp_end(node.closing)
+
+ bounds(node.location)
+ on_regexp_literal(parts, closing)
+ end
+
+ # /foo #{bar}/
+ # ^^^^^^^^^^^^
+ def visit_interpolated_regular_expression_node(node)
+ bounds(node.opening_loc)
+ on_regexp_beg(node.opening)
+
+ bounds(node.parts.first.location)
+ parts =
+ node.parts.inject(on_regexp_new) do |content, part|
+ on_regexp_add(content, visit_string_content(part))
+ end
+
+ bounds(node.closing_loc)
+ closing = on_regexp_end(node.closing)
+
+ bounds(node.location)
+ on_regexp_literal(parts, closing)
+ end
+
+ # "foo #{bar}"
+ # ^^^^^^^^^^^^
+ def visit_interpolated_string_node(node)
+ with_string_bounds(node) do
+ if node.opening&.start_with?("<<~")
+ heredoc = visit_heredoc_string_node(node)
+
+ bounds(node.location)
+ on_string_literal(heredoc)
+ elsif !node.heredoc? && node.parts.length > 1 && node.parts.any? { |part| (part.is_a?(StringNode) || part.is_a?(InterpolatedStringNode)) && !part.opening_loc.nil? }
+ first, *rest = node.parts
+ rest.inject(visit(first)) do |content, part|
+ concat = visit(part)
+
+ bounds(part.location)
+ on_string_concat(content, concat)
+ end
+ else
+ bounds(node.parts.first.location)
+ parts =
+ node.parts.inject(on_string_content) do |content, part|
+ on_string_add(content, visit_string_content(part))
+ end
+
+ bounds(node.location)
+ on_string_literal(parts)
+ end
+ end
+ end
+
+ # :"foo #{bar}"
+ # ^^^^^^^^^^^^^
+ def visit_interpolated_symbol_node(node)
+ with_string_bounds(node) do
+ bounds(node.parts.first.location)
+ parts =
+ node.parts.inject(on_string_content) do |content, part|
+ on_string_add(content, visit_string_content(part))
+ end
+
+ bounds(node.location)
+ on_dyna_symbol(parts)
+ end
+ end
+
+ # `foo #{bar}`
+ # ^^^^^^^^^^^^
+ def visit_interpolated_x_string_node(node)
+ with_string_bounds(node) do
+ if node.opening.start_with?("<<~")
+ heredoc = visit_heredoc_x_string_node(node)
+
+ bounds(node.location)
+ on_xstring_literal(heredoc)
+ else
+ bounds(node.parts.first.location)
+ parts =
+ node.parts.inject(on_xstring_new) do |content, part|
+ on_xstring_add(content, visit_string_content(part))
+ end
+
+ bounds(node.location)
+ on_xstring_literal(parts)
+ end
+ end
+ end
+
+ # Visit an individual part of a string-like node.
+ private def visit_string_content(part)
+ if part.is_a?(StringNode)
+ bounds(part.content_loc)
+ on_tstring_content(part.content)
+ else
+ visit(part)
+ end
+ end
+
+ # -> { it }
+ # ^^
+ def visit_it_local_variable_read_node(node)
+ bounds(node.location)
+ on_vcall(on_ident(node.slice))
+ end
+
+ # -> { it }
+ # ^^^^^^^^^
+ def visit_it_parameters_node(node)
+ end
+
+ # foo(bar: baz)
+ # ^^^^^^^^
+ def visit_keyword_hash_node(node)
+ elements = visit_all(node.elements)
+
+ bounds(node.location)
+ on_bare_assoc_hash(elements)
+ end
+
+ # def foo(**bar); end
+ # ^^^^^
+ #
+ # def foo(**); end
+ # ^^
+ def visit_keyword_rest_parameter_node(node)
+ bounds(node.operator_loc)
+ on_op("**")
+
+ if node.name_loc.nil?
+ bounds(node.location)
+ on_kwrest_param(nil)
+ else
+ bounds(node.name_loc)
+ name = on_ident(node.name.to_s)
+
+ bounds(node.location)
+ on_kwrest_param(name)
+ end
+ end
+
+ # -> {}
+ def visit_lambda_node(node)
+ bounds(node.operator_loc)
+ on_tlambda(node.operator)
+
+ parameters =
+ if node.parameters.is_a?(BlockParametersNode)
+ if node.parameters.opening_loc
+ bounds(node.parameters.opening_loc)
+ on_lparen("(")
+ end
+
+ # Ripper does not track block-locals within lambdas, so we skip
+ # directly to the parameters here.
+ params =
+ if node.parameters.parameters.nil?
+ bounds(node.location)
+ on_params(nil, nil, nil, nil, nil, nil, nil)
+ else
+ visit(node.parameters.parameters)
+ end
+
+ visit_all(node.parameters.locals)
+
+ if node.parameters.closing_loc
+ bounds(node.parameters.closing_loc)
+ on_rparen(")")
+ end
+
+ if node.parameters.opening_loc.nil?
+ params
+ else
+ bounds(node.parameters.opening_loc)
+ on_paren(params)
+ end
+ else
+ bounds(node.location)
+ on_params(nil, nil, nil, nil, nil, nil, nil)
+ end
+
+ braces = node.opening == "{"
+ bounds(node.opening_loc)
+ if braces
+ on_tlambeg(node.opening)
+ else
+ on_kw("do")
+ end
+
+ body =
+ case node.body
+ when nil
+ bounds(node.location)
+ stmts = on_stmts_add(on_stmts_new, on_void_stmt)
+
+ bounds(node.location)
+ braces ? stmts : on_bodystmt(stmts, nil, nil, nil)
+ when StatementsNode
+ stmts = node.body.body
+ stmts = [nil, *stmts] if void_stmt?(node.parameters&.location || node.opening_loc, node.body.location, false)
+ stmts = visit_statements_node_body(stmts)
+
+ bounds(node.body.location)
+ braces ? stmts : on_bodystmt(stmts, nil, nil, nil)
+ when BeginNode
+ visit_body_node(node.opening_loc, node.body)
+ else
+ raise
+ end
+
+ bounds(node.closing_loc)
+ if braces
+ on_rbrace("}")
+ else
+ on_kw("end")
+ end
+
+ bounds(node.location)
+ on_lambda(parameters, body)
+ end
+
+ # foo
+ # ^^^
+ def visit_local_variable_read_node(node)
+ bounds(node.location)
+ on_var_ref(on_ident(node.slice))
+ end
+
+ # foo = 1
+ # ^^^^^^^
+ def visit_local_variable_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_ident(node.name_loc.slice))
+
+ bounds(node.operator_loc)
+ on_op("=")
+
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_assign(target, value)
+ end
+
+ # foo += bar
+ # ^^^^^^^^^^
+ def visit_local_variable_operator_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_ident(node.name_loc.slice))
+
+ bounds(node.binary_operator_loc)
+ operator = on_op("#{node.binary_operator}=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # foo &&= bar
+ # ^^^^^^^^^^^
+ def visit_local_variable_and_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_ident(node.name_loc.slice))
+
+ bounds(node.operator_loc)
+ operator = on_op("&&=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # foo ||= bar
+ # ^^^^^^^^^^^
+ def visit_local_variable_or_write_node(node)
+ bounds(node.name_loc)
+ target = on_var_field(on_ident(node.name_loc.slice))
+
+ bounds(node.operator_loc)
+ operator = on_op("||=")
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_opassign(target, operator, value)
+ end
+
+ # foo, = bar
+ # ^^^
+ def visit_local_variable_target_node(node)
+ bounds(node.location)
+ on_var_field(on_ident(node.name.to_s))
+ end
+
+ # if /foo/ then end
+ # ^^^^^
+ def visit_match_last_line_node(node)
+ bounds(node.opening_loc)
+ on_regexp_beg(node.opening)
+
+ bounds(node.content_loc)
+ tstring_content = on_tstring_content(node.content)
+
+ bounds(node.closing_loc)
+ closing = on_regexp_end(node.closing)
+
+ on_regexp_literal(on_regexp_add(on_regexp_new, tstring_content), closing)
+ end
+
+ # foo in bar
+ # ^^^^^^^^^^
+ def visit_match_predicate_node(node)
+ value = visit(node.value)
+ bounds(node.operator_loc)
+ on_kw("in")
+ pattern = on_in(visit_pattern_node(node.pattern), nil, nil)
+
+ on_case(value, pattern)
+ end
+
+ # foo => bar
+ # ^^^^^^^^^^
+ def visit_match_required_node(node)
+ value = visit(node.value)
+
+ bounds(node.operator_loc)
+ on_op("=>")
+
+ pattern = on_in(visit_pattern_node(node.pattern), nil, nil)
+
+ on_case(value, pattern)
+ end
+
+ # /(?<foo>foo)/ =~ bar
+ # ^^^^^^^^^^^^^^^^^^^^
+ def visit_match_write_node(node)
+ visit(node.call)
+ end
+
+ # A node that is missing from the syntax tree. This is only used in the
+ # case of a syntax error.
+ def visit_error_recovery_node(node)
+ raise "Cannot visit error recovery nodes directly."
+ end
+
+ # module Foo; end
+ # ^^^^^^^^^^^^^^^
+ def visit_module_node(node)
+ bounds(node.module_keyword_loc)
+ on_kw("module")
+
+ constant_path =
+ if node.constant_path.is_a?(ConstantReadNode)
+ bounds(node.constant_path.location)
+ on_const_ref(on_const(node.constant_path.name.to_s))
+ else
+ visit(node.constant_path)
+ end
+
+ bodystmt = visit_body_node(node.constant_path.location, node.body, true)
+
+ bounds(node.end_keyword_loc)
+ on_kw("end")
+
+ bounds(node.location)
+ on_module(constant_path, bodystmt)
+ end
+
+ # (foo, bar), bar = qux
+ # ^^^^^^^^^^
+ def visit_multi_target_node(node)
+ if node.lparen_loc
+ bounds(node.lparen_loc)
+ on_lparen("(")
+ end
+
+ bounds(node.location)
+ targets = visit_multi_target_node_targets(node.lefts, node.rest, node.rights, true)
+
+ if node.rparen_loc
+ bounds(node.rparen_loc)
+ on_rparen(")")
+ end
+
+ if node.lparen_loc.nil?
+ targets
+ else
+ bounds(node.lparen_loc)
+ on_mlhs_paren(targets)
+ end
+ end
+
+ # Visit the targets of a multi-target node.
+ private def visit_multi_target_node_targets(lefts, rest, rights, skippable)
+ if skippable && lefts.length == 1 && lefts.first.is_a?(MultiTargetNode) && rest.nil? && rights.empty?
+ return visit(lefts.first)
+ end
+
+ mlhs = on_mlhs_new
+
+ lefts.each do |left|
+ bounds(left.location)
+ mlhs = on_mlhs_add(mlhs, visit(left))
+ end
+
+ case rest
+ when nil
+ # do nothing
+ when ImplicitRestNode
+ # these do not get put into the generated tree
+ bounds(rest.location)
+ on_excessed_comma
+ else
+ bounds(rest.location)
+ mlhs = on_mlhs_add_star(mlhs, visit(rest))
+ end
+
+ if rights.any?
+ bounds(rights.first.location)
+ post = on_mlhs_new
+
+ rights.each do |right|
+ bounds(right.location)
+ post = on_mlhs_add(post, visit(right))
+ end
+
+ mlhs = on_mlhs_add_post(mlhs, post)
+ end
+
+ mlhs
+ end
+
+ # foo, bar = baz
+ # ^^^^^^^^^^^^^^
+ def visit_multi_write_node(node)
+ if node.lparen_loc
+ bounds(node.lparen_loc)
+ on_lparen("(")
+ end
+
+ bounds(node.location)
+ targets = visit_multi_target_node_targets(node.lefts, node.rest, node.rights, true)
+
+ if node.rparen_loc
+ bounds(node.rparen_loc)
+ on_rparen(")")
+ end
+
+ bounds(node.operator_loc)
+ on_op("=")
+
+ unless node.lparen_loc.nil?
+ bounds(node.lparen_loc)
+ targets = on_mlhs_paren(targets)
+ end
+
+ value = visit_write_value(node.value)
+
+ bounds(node.location)
+ on_massign(targets, value)
+ end
+
+ # next
+ # ^^^^
+ #
+ # next foo
+ # ^^^^^^^^
+ def visit_next_node(node)
+ bounds(node.keyword_loc)
+ on_kw("next")
+
+ if node.arguments.nil?
+ bounds(node.location)
+ on_next(on_args_new)
+ else
+ arguments = visit(node.arguments)
+
+ bounds(node.location)
+ on_next(arguments)
+ end
+ end
+
+ # nil
+ # ^^^
+ def visit_nil_node(node)
+ bounds(node.location)
+ on_var_ref(on_kw("nil"))
+ end
+
+ # def foo(&nil); end
+ # ^^^^
+ def visit_no_block_parameter_node(node)
+ bounds(node.operator_loc)
+ on_op("&")
+ bounds(node.keyword_loc)
+ on_kw("nil")
+ bounds(node.location)
+ on_blockarg(:nil)
+ end
+
+ # def foo(**nil); end
+ # ^^^^^
+ def visit_no_keywords_parameter_node(node)
+ bounds(node.operator_loc)
+ on_op("**")
+ bounds(node.keyword_loc)
+ on_kw("nil")
+ bounds(node.location)
+ on_nokw_param(nil)
+
+ :nil
+ end
+
+ # -> { _1 + _2 }
+ # ^^^^^^^^^^^^^^
+ def visit_numbered_parameters_node(node)
+ end
+
+ # $1
+ # ^^
+ def visit_numbered_reference_read_node(node)
+ bounds(node.location)
+ on_backref(node.slice)
+ end
+
+ # def foo(bar: baz); end
+ # ^^^^^^^^
+ def visit_optional_keyword_parameter_node(node)
+ bounds(node.name_loc)
+ name = on_label("#{node.name}:")
+ value = visit(node.value)
+
+ [name, value]
+ end
+
+ # def foo(bar = 1); end
+ # ^^^^^^^
+ def visit_optional_parameter_node(node)
+ bounds(node.name_loc)
+ name = on_ident(node.name.to_s)
+
+ bounds(node.operator_loc)
+ on_op("=")
+
+ value = visit(node.value)
+
+ [name, value]
+ end
+
+ # a or b
+ # ^^^^^^
+ def visit_or_node(node)
+ left = visit(node.left)
+
+ bounds(node.operator_loc)
+ if node.operator == "or"
+ on_kw("or")
+ else
+ on_op("||")
+ end
+
+ right = visit(node.right)
+
+ bounds(node.location)
+ on_binary(left, node.operator.to_sym, right)
+ end
+
+ # def foo(bar, *baz); end
+ # ^^^^^^^^^
+ def visit_parameters_node(node)
+ requireds = node.requireds.map { |required| required.is_a?(MultiTargetNode) ? visit_destructured_parameter_node(required) : visit(required) } if node.requireds.any?
+ optionals = visit_all(node.optionals) if node.optionals.any?
+ rest = visit(node.rest)
+ posts = node.posts.map { |post| post.is_a?(MultiTargetNode) ? visit_destructured_parameter_node(post) : visit(post) } if node.posts.any?
+ keywords = visit_all(node.keywords) if node.keywords.any?
+ keyword_rest = visit(node.keyword_rest)
+ block = visit(node.block)
+
+ bounds(node.location)
+ on_params(requireds, optionals, rest, posts, keywords, keyword_rest, block)
+ end
+
+ # Visit a destructured positional parameter node.
+ private def visit_destructured_parameter_node(node)
+ if node.lparen_loc
+ bounds(node.lparen_loc)
+ on_lparen("(")
+ end
+
+ bounds(node.location)
+ targets = visit_multi_target_node_targets(node.lefts, node.rest, node.rights, false)
+
+ if node.rparen_loc
+ bounds(node.rparen_loc)
+ on_rparen(")")
+ end
+
+ bounds(node.lparen_loc)
+ on_mlhs_paren(targets)
+ end
+
+ # ()
+ # ^^
+ #
+ # (1)
+ # ^^^
+ def visit_parentheses_node(node)
+ bounds(node.opening_loc)
+ on_lparen("(")
+
+ body =
+ if node.body.nil?
+ on_stmts_add(on_stmts_new, on_void_stmt)
+ else
+ visit(node.body)
+ end
+
+ bounds(node.closing_loc)
+ on_rparen(")")
+ bounds(node.location)
+ on_paren(body)
+ end
+
+ # foo => ^(bar)
+ # ^^^^^^
+ def visit_pinned_expression_node(node)
+ bounds(node.operator_loc)
+ on_op("^")
+ bounds(node.lparen_loc)
+ on_lparen("(")
+
+ expression = visit(node.expression)
+
+ bounds(node.rparen_loc)
+ on_rparen(")")
+ bounds(node.location)
+ on_begin(expression)
+ end
+
+ # foo = 1 and bar => ^foo
+ # ^^^^
+ def visit_pinned_variable_node(node)
+ bounds(node.operator_loc)
+ on_op("^")
+
+ visit(node.variable)
+ end
+
+ # END {}
+ # ^^^^^^
+ def visit_post_execution_node(node)
+ bounds(node.keyword_loc)
+ on_kw("END")
+ bounds(node.opening_loc)
+ on_lbrace("{")
+
+ statements =
+ if node.statements.nil?
+ bounds(node.location)
+ on_stmts_add(on_stmts_new, on_void_stmt)
+ else
+ visit(node.statements)
+ end
+
+ bounds(node.closing_loc)
+ on_rbrace("}")
+ bounds(node.location)
+ on_END(statements)
+ end
+
+ # BEGIN {}
+ # ^^^^^^^^
+ def visit_pre_execution_node(node)
+ bounds(node.keyword_loc)
+ on_kw("BEGIN")
+ bounds(node.opening_loc)
+ on_lbrace("{")
+
+ statements =
+ if node.statements.nil?
+ bounds(node.location)
+ on_stmts_add(on_stmts_new, on_void_stmt)
+ else
+ visit(node.statements)
+ end
+
+ bounds(node.closing_loc)
+ on_rbrace("}")
+ bounds(node.location)
+ on_BEGIN(statements)
+ end
+
+ # The top-level program node.
+ def visit_program_node(node)
+ body = node.statements.body
+ body = [nil] if body.empty?
+ statements = visit_statements_node_body(body)
+
+ bounds(node.location)
+ on_program(statements)
+ end
+
+ # 0..5
+ # ^^^^
+ def visit_range_node(node)
+ left = visit(node.left)
+
+ bounds(node.operator_loc)
+ on_op(node.operator)
+
+ right = visit(node.right)
+
+ bounds(node.location)
+ if node.exclude_end?
+ on_dot3(left, right)
+ else
+ on_dot2(left, right)
+ end
+ end
+
+ # 1r
+ # ^^
+ def visit_rational_node(node)
+ visit_number_node(node) { |text| on_rational(text) }
+ end
+
+ # redo
+ # ^^^^
+ def visit_redo_node(node)
+ bounds(node.location)
+ on_kw("redo")
+ on_redo
+ end
+
+ # /foo/
+ # ^^^^^
+ def visit_regular_expression_node(node)
+ bounds(node.opening_loc)
+ on_regexp_beg(node.opening)
+
+ if node.content.empty?
+ bounds(node.closing_loc)
+ closing = on_regexp_end(node.closing)
+
+ on_regexp_literal(on_regexp_new, closing)
+ else
+ bounds(node.content_loc)
+ tstring_content = on_tstring_content(node.content)
+
+ bounds(node.closing_loc)
+ closing = on_regexp_end(node.closing)
+
+ on_regexp_literal(on_regexp_add(on_regexp_new, tstring_content), closing)
+ end
+ end
+
+ # def foo(bar:); end
+ # ^^^^
+ def visit_required_keyword_parameter_node(node)
+ bounds(node.name_loc)
+ [on_label("#{node.name}:"), false]
+ end
+
+ # def foo(bar); end
+ # ^^^
+ def visit_required_parameter_node(node)
+ bounds(node.location)
+ on_ident(node.name.to_s)
+ end
+
+ # foo rescue bar
+ # ^^^^^^^^^^^^^^
+ def visit_rescue_modifier_node(node)
+ bounds(node.keyword_loc)
+ on_kw("rescue")
+
+ expression = visit_write_value(node.expression)
+ rescue_expression = visit(node.rescue_expression)
+
+ bounds(node.location)
+ on_rescue_mod(expression, rescue_expression)
+ end
+
+ # begin; rescue; end
+ # ^^^^^^^
+ def visit_rescue_node(node)
+ bounds(node.keyword_loc)
+ on_kw("rescue")
+
+ exceptions =
+ case node.exceptions.length
+ when 0
+ nil
+ when 1
+ if (exception = node.exceptions.first).is_a?(SplatNode)
+ bounds(exception.location)
+ on_mrhs_add_star(on_mrhs_new, visit(exception))
+ else
+ [visit(node.exceptions.first)]
+ end
+ else
+ bounds(node.location)
+ length = node.exceptions.length
+
+ node.exceptions.each_with_index.inject(on_args_new) do |mrhs, (exception, index)|
+ arg = visit(exception)
+
+ bounds(exception.location)
+ mrhs = on_mrhs_new_from_args(mrhs) if index == length - 1
+
+ if exception.is_a?(SplatNode)
+ if index == length - 1
+ on_mrhs_add_star(mrhs, arg)
+ else
+ on_args_add_star(mrhs, arg)
+ end
+ else
+ if index == length - 1
+ on_mrhs_add(mrhs, arg)
+ else
+ on_args_add(mrhs, arg)
+ end
+ end
+ end
+ end
+
+ if node.operator_loc
+ bounds(node.operator_loc)
+ on_op("=>")
+ end
+
+ reference = visit(node.reference)
+ statements =
+ if node.statements.nil?
+ bounds(node.location)
+ on_stmts_add(on_stmts_new, on_void_stmt)
+ else
+ visit(node.statements)
+ end
+
+ subsequent = visit(node.subsequent)
+
+ bounds(node.location)
+ on_rescue(exceptions, reference, statements, subsequent)
+ end
+
+ # def foo(*bar); end
+ # ^^^^
+ #
+ # def foo(*); end
+ # ^
+ def visit_rest_parameter_node(node)
+ bounds(node.operator_loc)
+ on_op("*")
+
+ if node.name_loc.nil?
+ bounds(node.location)
+ on_rest_param(nil)
+ else
+ bounds(node.name_loc)
+ on_rest_param(on_ident(node.name.to_s))
+ end
+ end
+
+ # retry
+ # ^^^^^
+ def visit_retry_node(node)
+ bounds(node.location)
+ on_kw("retry")
+ on_retry
+ end
+
+ # return
+ # ^^^^^^
+ #
+ # return 1
+ # ^^^^^^^^
+ def visit_return_node(node)
+ bounds(node.keyword_loc)
+ on_kw("return")
+
+ if node.arguments.nil?
+ bounds(node.location)
+ on_return0
+ else
+ arguments = visit(node.arguments)
+
+ bounds(node.location)
+ on_return(arguments)
+ end
+ end
+
+ # self
+ # ^^^^
+ def visit_self_node(node)
+ bounds(node.location)
+ on_var_ref(on_kw("self"))
+ end
+
+ # A shareable constant.
+ def visit_shareable_constant_node(node)
+ visit(node.write)
+ end
+
+ # class << self; end
+ # ^^^^^^^^^^^^^^^^^^
+ def visit_singleton_class_node(node)
+ bounds(node.class_keyword_loc)
+ on_kw("class")
+ bounds(node.operator_loc)
+ on_op("<<")
+
+ expression = visit(node.expression)
+ bodystmt = visit_body_node(node.body&.location || node.end_keyword_loc, node.body)
+
+ bounds(node.end_keyword_loc)
+ on_kw("end")
+
+ bounds(node.location)
+ on_sclass(expression, bodystmt)
+ end
+
+ # __ENCODING__
+ # ^^^^^^^^^^^^
+ def visit_source_encoding_node(node)
+ bounds(node.location)
+ on_var_ref(on_kw("__ENCODING__"))
+ end
+
+ # __FILE__
+ # ^^^^^^^^
+ def visit_source_file_node(node)
+ bounds(node.location)
+ on_var_ref(on_kw("__FILE__"))
+ end
+
+ # __LINE__
+ # ^^^^^^^^
+ def visit_source_line_node(node)
+ bounds(node.location)
+ on_var_ref(on_kw("__LINE__"))
+ end
+
+ # foo(*bar)
+ # ^^^^
+ #
+ # def foo((bar, *baz)); end
+ # ^^^^
+ #
+ # def foo(*); bar(*); end
+ # ^
+ def visit_splat_node(node)
+ bounds(node.operator_loc)
+ on_op("*")
+ visit(node.expression)
+ end
+
+ # A list of statements.
+ def visit_statements_node(node)
+ bounds(node.location)
+ visit_statements_node_body(node.body)
+ end
+
+ # Visit the list of statements of a statements node. We support nil
+ # statements in the list. This would normally not be allowed by the
+ # structure of the prism parse tree, but we manually add them here so that
+ # we can mirror Ripper's void stmt.
+ private def visit_statements_node_body(body)
+ body.inject(on_stmts_new) do |stmts, stmt|
+ on_stmts_add(stmts, stmt.nil? ? on_void_stmt : visit(stmt))
+ end
+ end
+
+ # "foo"
+ # ^^^^^
+ def visit_string_node(node)
+ with_string_bounds(node) do
+ if (content = node.content).empty?
+ bounds(node.location)
+ on_string_literal(on_string_content)
+ elsif (opening = node.opening) == "?"
+ bounds(node.location)
+ on_CHAR("?#{node.content}")
+ elsif opening.start_with?("<<~")
+ heredoc = visit_heredoc_string_node(node.to_interpolated)
+
+ bounds(node.location)
+ on_string_literal(heredoc)
+ else
+ bounds(node.content_loc)
+ tstring_content = on_tstring_content(content)
+
+ bounds(node.location)
+ on_string_literal(on_string_add(on_string_content, tstring_content))
+ end
+ end
+ end
+
+ # Responsible for emitting the various string-like begin/end events
+ private def with_string_bounds(node)
+ # `foo "bar": baz` doesn't emit the closing location
+ assoc = !(opening = node.opening)&.include?(":") && node.closing&.end_with?(":")
+
+ is_heredoc = opening&.start_with?("<<")
+ if is_heredoc
+ bounds(node.opening_loc)
+ on_heredoc_beg(node.opening)
+ elsif opening&.start_with?(":", "%s")
+ bounds(node.opening_loc)
+ on_symbeg(node.opening)
+ elsif opening&.start_with?("`", "%x")
+ bounds(node.opening_loc)
+ on_backtick(node.opening)
+ elsif opening && !opening.start_with?("?")
+ bounds(node.opening_loc)
+ on_tstring_beg(opening)
+ end
+
+ result = yield
+ if assoc
+ if node.closing != ":"
+ bounds(node.closing_loc)
+ on_label_end(node.closing)
+ end
+ return result
+ end
+
+ if is_heredoc
+ bounds(node.closing_loc)
+ on_heredoc_end(node.closing)
+ elsif node.closing_loc
+ bounds(node.closing_loc)
+ on_tstring_end(node.closing)
+ end
+
+ result
+ end
+
+ # Ripper gives back the escaped string content but strips out the common
+ # leading whitespace. Prism gives back the unescaped string content and
+ # a location for the escaped string content. Unfortunately these don't
+ # work well together, so here we need to re-derive the common leading
+ # whitespace.
+ private def visit_heredoc_node_whitespace(parts)
+ common_whitespace = nil
+ dedent_next = true
+
+ parts.each do |part|
+ if part.is_a?(StringNode)
+ if dedent_next && !(content = part.content).chomp.empty?
+ common_whitespace = [
+ common_whitespace || Float::INFINITY,
+ content[/\A\s*/].each_char.inject(0) do |part_whitespace, char|
+ char == "\t" ? ((part_whitespace / 8 + 1) * 8) : (part_whitespace + 1)
+ end
+ ].min
+ end
+
+ dedent_next = true
+ else
+ dedent_next = false
+ end
+ end
+
+ common_whitespace || 0
+ end
+
+ # Visit a string that is expressed using a <<~ heredoc.
+ private def visit_heredoc_node(parts, base)
+ common_whitespace = visit_heredoc_node_whitespace(parts)
+
+ if common_whitespace == 0
+ bounds(parts.first.location)
+
+ string = []
+ result = base
+
+ parts.each do |part|
+ if part.is_a?(StringNode)
+ if string.empty?
+ string = [part]
+ else
+ string << part
+ end
+ else
+ unless string.empty?
+ bounds(string[0].location)
+ result = yield result, on_tstring_content(string.map(&:content).join)
+ string = []
+ end
+
+ result = yield result, visit(part)
+ end
+ end
+
+ unless string.empty?
+ bounds(string[0].location)
+ result = yield result, on_tstring_content(string.map(&:content).join)
+ end
+
+ result
+ else
+ bounds(parts.first.location)
+ result =
+ parts.inject(base) do |string_content, part|
+ yield string_content, visit_string_content(part)
+ end
+
+ bounds(parts.first.location)
+ on_heredoc_dedent(result, common_whitespace)
+ end
+ end
+
+ # Visit a heredoc node that is representing a string.
+ private def visit_heredoc_string_node(node)
+ bounds(node.location)
+ visit_heredoc_node(node.parts, on_string_content) do |parts, part|
+ on_string_add(parts, part)
+ end
+ end
+
+ # Visit a heredoc node that is representing an xstring.
+ private def visit_heredoc_x_string_node(node)
+ bounds(node.location)
+ visit_heredoc_node(node.parts, on_xstring_new) do |parts, part|
+ on_xstring_add(parts, part)
+ end
+ end
+
+ # super(foo)
+ # ^^^^^^^^^^
+ def visit_super_node(node)
+ bounds(node.keyword_loc)
+ on_kw("super")
+
+ if node.lparen_loc
+ bounds(node.lparen_loc)
+ on_lparen("(")
+ end
+
+ arguments, block_node = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.rparen_loc || node.location))
+
+ if node.rparen_loc
+ bounds(node.rparen_loc)
+ on_rparen(")")
+ end
+
+ block = visit(block_node)
+
+ if !node.lparen_loc.nil?
+ bounds(node.lparen_loc)
+ arguments = on_arg_paren(arguments)
+ end
+
+ bounds(node.location)
+ call = on_super(arguments)
+
+ if block_node
+ bounds(node.block.location)
+ on_method_add_block(call, block)
+ else
+ call
+ end
+ end
+
+ # :foo
+ # ^^^^
+ def visit_symbol_node(node)
+ with_string_bounds(node) do
+ if node.value_loc.nil?
+ bounds(node.location)
+ on_dyna_symbol(on_string_content)
+ elsif (opening = node.opening)&.match?(/^%s|['"]:?$/)
+ bounds(node.value_loc)
+ content = on_string_add(on_string_content, on_tstring_content(node.value))
+ bounds(node.location)
+ on_dyna_symbol(content)
+ elsif (closing = node.closing) == ":"
+ bounds(node.location)
+ on_label("#{node.value}:")
+ elsif opening.nil? && node.closing_loc.nil?
+ bounds(node.value_loc)
+ on_symbol_literal(visit_token(node.value))
+ else
+ bounds(node.value_loc)
+ on_symbol_literal(on_symbol(visit_token(node.value)))
+ end
+ end
+ end
+
+ # true
+ # ^^^^
+ def visit_true_node(node)
+ bounds(node.location)
+ on_var_ref(on_kw("true"))
+ end
+
+ # undef foo
+ # ^^^^^^^^^
+ def visit_undef_node(node)
+ bounds(node.keyword_loc)
+ on_kw("undef")
+
+ names = visit_all(node.names)
+
+ bounds(node.location)
+ on_undef(names)
+ end
+
+ # unless foo; bar end
+ # ^^^^^^^^^^^^^^^^^^^
+ #
+ # bar unless foo
+ # ^^^^^^^^^^^^^^
+ def visit_unless_node(node)
+ if node.statements.nil? || (node.predicate.location.start_offset < node.statements.location.start_offset)
+ bounds(node.keyword_loc)
+ on_kw("unless")
+ predicate = visit(node.predicate)
+ if node.then_keyword_loc
+ bounds(node.then_keyword_loc)
+ on_kw("then")
+ end
+ statements =
+ if node.statements.nil?
+ bounds(node.location)
+ on_stmts_add(on_stmts_new, on_void_stmt)
+ else
+ visit(node.statements)
+ end
+ else_clause = visit(node.else_clause)
+
+ if node.end_keyword_loc && !node.else_clause
+ bounds(node.end_keyword_loc)
+ on_kw("end")
+ end
+
+ bounds(node.location)
+ on_unless(predicate, statements, else_clause)
+ else
+ statements = visit(node.statements.body.first)
+ bounds(node.keyword_loc)
+ on_kw("unless")
+ predicate = visit(node.predicate)
+
+ bounds(node.location)
+ on_unless_mod(predicate, statements)
+ end
+ end
+
+ # until foo; bar end
+ # ^^^^^^^^^^^^^^^^^
+ #
+ # bar until foo
+ # ^^^^^^^^^^^^^
+ def visit_until_node(node)
+ bounds(node.keyword_loc)
+ on_kw("until")
+
+ if node.statements.nil? || (node.predicate.location.start_offset < node.statements.location.start_offset)
+ if node.do_keyword_loc
+ bounds(node.do_keyword_loc)
+ on_kw("do")
+ end
+ predicate = visit(node.predicate)
+ statements =
+ if node.statements.nil?
+ bounds(node.location)
+ on_stmts_add(on_stmts_new, on_void_stmt)
+ else
+ visit(node.statements)
+ end
+
+ if node.closing_loc
+ bounds(node.closing_loc)
+ on_kw("end")
+ end
+
+ bounds(node.location)
+ on_until(predicate, statements)
+ else
+ statements = visit(node.statements.body.first)
+ predicate = visit(node.predicate)
+
+ bounds(node.location)
+ on_until_mod(predicate, statements)
+ end
+ end
+
+ # case foo; when bar; end
+ # ^^^^^^^^^^^^^
+ def visit_when_node(node)
+ # This is a special case where we're not going to call on_when directly
+ # because we don't have access to the subsequent. Instead, we'll return
+ # the component parts and let the parent node handle it.
+ bounds(node.keyword_loc)
+ on_kw("when")
+
+ conditions = visit_arguments(node.conditions)
+ if node.then_keyword_loc
+ bounds(node.then_keyword_loc)
+ on_kw("then")
+ end
+ statements =
+ if node.statements.nil?
+ bounds(node.location)
+ on_stmts_add(on_stmts_new, on_void_stmt)
+ else
+ visit(node.statements)
+ end
+
+ [conditions, statements]
+ end
+
+ # while foo; bar end
+ # ^^^^^^^^^^^^^^^^^^
+ #
+ # bar while foo
+ # ^^^^^^^^^^^^^
+ def visit_while_node(node)
+ if node.statements.nil? || (node.predicate.location.start_offset < node.statements.location.start_offset)
+ bounds(node.keyword_loc)
+ on_kw("while")
+ if node.do_keyword_loc
+ bounds(node.do_keyword_loc)
+ on_kw("do")
+ end
+ predicate = visit(node.predicate)
+ if node.closing_loc
+ bounds(node.closing_loc)
+ on_kw("end")
+ end
+ statements =
+ if node.statements.nil?
+ bounds(node.location)
+ on_stmts_add(on_stmts_new, on_void_stmt)
+ else
+ visit(node.statements)
+ end
+
+ bounds(node.location)
+ on_while(predicate, statements)
+ else
+ statements = visit(node.statements.body.first)
+ bounds(node.keyword_loc)
+ on_kw("while")
+ predicate = visit(node.predicate)
+
+ bounds(node.location)
+ on_while_mod(predicate, statements)
+ end
+ end
+
+ # `foo`
+ # ^^^^^
+ def visit_x_string_node(node)
+ with_string_bounds(node) do
+ if node.unescaped.empty?
+ bounds(node.location)
+ on_xstring_literal(on_xstring_new)
+ elsif node.opening.start_with?("<<~")
+ heredoc = visit_heredoc_x_string_node(node.to_interpolated)
+
+ bounds(node.location)
+ on_xstring_literal(heredoc)
+ else
+ bounds(node.content_loc)
+ content = on_tstring_content(node.content)
+
+ bounds(node.location)
+ on_xstring_literal(on_xstring_add(on_xstring_new, content))
+ end
+ end
+ end
+
+ # yield
+ # ^^^^^
+ #
+ # yield 1
+ # ^^^^^^^
+ def visit_yield_node(node)
+ bounds(node.keyword_loc)
+ on_kw("yield")
+
+ if node.arguments.nil? && node.lparen_loc.nil?
+ bounds(node.location)
+ on_yield0
+ else
+ if node.lparen_loc
+ bounds(node.lparen_loc)
+ on_lparen("(")
+ end
+
+ arguments =
+ if node.arguments.nil?
+ bounds(node.location)
+ on_args_new
+ else
+ visit(node.arguments)
+ end
+
+ unless node.lparen_loc.nil?
+ bounds(node.rparen_loc)
+ on_rparen(")")
+ bounds(node.lparen_loc)
+ arguments = on_paren(arguments)
+ end
+
+ bounds(node.location)
+ on_yield(arguments)
+ end
+ end
+
+ private
+
+ # Lazily initialize the parse result.
+ def result
+ @result ||= Prism.parse(source, partial_script: true, version: "current", freeze: true, encoding: source.encoding)
+ end
+
+ def line_and_column_cache
+ @line_and_column_cache ||= LineAndColumnCache.new(result.source)
+ end
+
+ ##########################################################################
+ # Helpers
+ ##########################################################################
+
+ # Returns true if there is a comma between the two locations.
+ def trailing_comma?(left, right)
+ source.byteslice(left.end_offset...right.start_offset).include?(",")
+ end
+
+ # Returns true if there is a semicolon between the two locations.
+ def void_stmt?(left, right, allow_newline)
+ pattern = allow_newline ? /[;\n]/ : /;/
+ source.byteslice(left.end_offset...right.start_offset).match?(pattern)
+ end
+
+ # Visit the string content of a particular node. This method is used to
+ # split into the various token types.
+ def visit_token(token, allow_keywords = true)
+ if token == "."
+ on_period(token)
+ elsif token == "`"
+ on_backtick(token)
+ elsif allow_keywords && KEYWORDS.include?(token)
+ on_kw(token)
+ elsif token.start_with?("_")
+ on_ident(token)
+ elsif token.match?(/^[[:upper:]]\w*$/)
+ on_const(token)
+ elsif token.start_with?("@@")
+ on_cvar(token)
+ elsif token.start_with?("@")
+ on_ivar(token)
+ elsif token.start_with?("$")
+ on_gvar(token)
+ elsif token.match?(/^[[:punct:]]/)
+ on_op(token)
+ else
+ on_ident(token)
+ end
+ end
+
+ # Visit either `.`, `&.`, or `::`.
+ def visit_call_operator(token)
+ token == "." ? on_period(token) : on_op(token)
+ end
+
+ # Visit a node that represents a number. We need to explicitly handle the
+ # unary - operator.
+ def visit_number_node(node)
+ slice = node.slice
+ location = node.location
+
+ if slice[0] == "-"
+ bounds(location.copy(length: 1))
+ on_op("-")
+
+ bounds(location.copy(start_offset: location.start_offset + 1))
+ value = yield slice[1..-1]
+
+ bounds(node.location)
+ on_unary(:-@, value)
+ else
+ bounds(location)
+ yield slice
+ end
+ end
+
+ # Visit a node that represents a write value. This is used to handle the
+ # special case of an implicit array that is generated without brackets.
+ def visit_write_value(node)
+ if node.is_a?(ArrayNode) && node.opening_loc.nil?
+ elements = node.elements
+ length = elements.length
+
+ bounds(elements.first.location)
+ elements.each_with_index.inject((elements.first.is_a?(SplatNode) && length == 1) ? on_mrhs_new : on_args_new) do |args, (element, index)|
+ arg = visit(element)
+ bounds(element.location)
+
+ if index == length - 1
+ if element.is_a?(SplatNode)
+ mrhs = index == 0 ? args : on_mrhs_new_from_args(args)
+ on_mrhs_add_star(mrhs, arg)
+ else
+ on_mrhs_add(on_mrhs_new_from_args(args), arg)
+ end
+ else
+ case element
+ when BlockArgumentNode
+ on_args_add_block(args, arg)
+ when SplatNode
+ on_args_add_star(args, arg)
+ else
+ on_args_add(args, arg)
+ end
+ end
+ end
+ else
+ visit(node)
+ end
+ end
+
+ # This method is responsible for updating lineno and column information
+ # to reflect the current node.
+ def bounds(location)
+ @lineno, @column = line_and_column_cache.line_and_column(location.start_offset)
+ end
+
+ # :startdoc:
+
+ ##########################################################################
+ # Ripper interface
+ ##########################################################################
+
+ # :stopdoc:
+ def _dispatch_0; end
+ def _dispatch_1(arg); arg end
+ def _dispatch_2(arg, _); arg end
+ def _dispatch_3(arg, _, _); arg end
+ def _dispatch_4(arg, _, _, _); arg end
+ def _dispatch_5(arg, _, _, _, _); arg end
+ def _dispatch_7(arg, _, _, _, _, _, _); arg end
+ # :startdoc:
+
+ #
+ # Parser Events
+ #
+
+ PARSER_EVENT_TABLE.each do |id, arity|
+ alias_method "on_#{id}", "_dispatch_#{arity}"
+ end
+
+ # This method is called when weak warning is produced by the parser.
+ # +fmt+ and +args+ is printf style.
+ def warn(fmt, *args)
+ end
+
+ # This method is called when strong warning is produced by the parser.
+ # +fmt+ and +args+ is printf style.
+ def warning(fmt, *args)
+ end
+
+ # This method is called when the parser found syntax error.
+ def compile_error(msg)
+ end
+
+ #
+ # Scanner Events
+ #
+
+ SCANNER_EVENTS.each do |id|
+ alias_method "on_#{id}", :_dispatch_1
+ end
+
+ # This method is provided by the Ripper C extension. It is called when a
+ # string needs to be dedented because of a tilde heredoc. It is expected
+ # that it will modify the string in place and return the number of bytes
+ # that were removed.
+ def dedent_string(string, width)
+ whitespace = 0
+ cursor = 0
+
+ while cursor < string.length && string[cursor].match?(/\s/) && whitespace < width
+ if string[cursor] == "\t"
+ whitespace = ((whitespace / 8 + 1) * 8)
+ break if whitespace > width
+ else
+ whitespace += 1
+ end
+
+ cursor += 1
+ end
+
+ string.replace(string[cursor..])
+ cursor
+ end
+ end
+ end
+end
diff --git a/lib/prism/translation/ripper/filter.rb b/lib/prism/translation/ripper/filter.rb
new file mode 100644
index 0000000000..19deef2d37
--- /dev/null
+++ b/lib/prism/translation/ripper/filter.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Prism
+ module Translation
+ class Ripper
+ class Filter # :nodoc:
+ # :stopdoc:
+ def initialize(src, filename = '-', lineno = 1)
+ @__lexer = Lexer.new(src, filename, lineno)
+ @__line = nil
+ @__col = nil
+ @__state = nil
+ end
+
+ def filename
+ @__lexer.filename
+ end
+
+ def lineno
+ @__line
+ end
+
+ def column
+ @__col
+ end
+
+ def state
+ @__state
+ end
+
+ def parse(init = nil)
+ data = init
+ @__lexer.lex.each do |pos, event, tok, state|
+ @__line, @__col = *pos
+ @__state = state
+ data = if respond_to?(event, true)
+ then __send__(event, tok, data)
+ else on_default(event, tok, data)
+ end
+ end
+ data
+ end
+
+ private
+
+ def on_default(event, token, data)
+ data
+ end
+ # :startdoc:
+ end
+ end
+ end
+end
diff --git a/lib/prism/translation/ripper/lexer.rb b/lib/prism/translation/ripper/lexer.rb
new file mode 100644
index 0000000000..c6aeae4bd7
--- /dev/null
+++ b/lib/prism/translation/ripper/lexer.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+# :markup: markdown
+
+require_relative "../ripper"
+
+module Prism
+ module Translation
+ class Ripper
+ class Lexer < Ripper # :nodoc:
+ class State # :nodoc:
+ attr_reader :to_int, :to_s
+
+ def initialize(i)
+ @to_int = i
+ @to_s = Ripper.lex_state_name(i)
+ freeze
+ end
+
+ def [](index)
+ case index
+ when 0, :to_int
+ @to_int
+ when 1, :to_s
+ @to_s
+ else
+ nil
+ end
+ end
+
+ alias to_i to_int
+ alias inspect to_s
+ def pretty_print(q) q.text(to_s) end
+ def ==(i) super or to_int == i end
+ def &(i) self.class.new(to_int & i) end
+ def |(i) self.class.new(to_int | i) end
+ def allbits?(i) to_int.allbits?(i) end
+ def anybits?(i) to_int.anybits?(i) end
+ def nobits?(i) to_int.nobits?(i) end
+
+ # Instances are frozen and there are only a handful of them so we
+ # cache them here.
+ STATES = Hash.new { |hash, key| hash[key] = State.new(key) }
+ private_constant :STATES
+
+ def self.[](i)
+ STATES[i]
+ end
+ end
+
+ class Elem # :nodoc:
+ attr_accessor :pos, :event, :tok, :state, :message
+
+ def initialize(pos, event, tok, state, message = nil)
+ @pos = pos
+ @event = event
+ @tok = tok
+ @state = State[state]
+ @message = message
+ end
+
+ def [](index)
+ case index
+ when 0, :pos
+ @pos
+ when 1, :event
+ @event
+ when 2, :tok
+ @tok
+ when 3, :state
+ @state
+ when 4, :message
+ @message
+ else
+ nil
+ end
+ end
+
+ def inspect
+ "#<#{self.class}: #{event}@#{pos[0]}:#{pos[1]}:#{state}: #{tok.inspect}#{": " if message}#{message}>"
+ end
+
+ alias to_s inspect
+
+ def pretty_print(q)
+ q.group(2, "#<#{self.class}:", ">") {
+ q.breakable
+ q.text("#{event}@#{pos[0]}:#{pos[1]}")
+ q.breakable
+ state.pretty_print(q)
+ q.breakable
+ q.text("token: ")
+ tok.pretty_print(q)
+ if message
+ q.breakable
+ q.text("message: ")
+ q.text(message)
+ end
+ }
+ end
+
+ def to_a
+ if @message
+ [@pos, @event, @tok, @state, @message]
+ else
+ [@pos, @event, @tok, @state]
+ end
+ end
+ end
+
+ # Pretty much just the same as Prism.lex_compat.
+ def lex(raise_errors: false)
+ Ripper.lex(@source, filename, lineno, raise_errors: raise_errors)
+ end
+
+ # Returns the lex_compat result wrapped in `Elem`. Errors are omitted.
+ # Since ripper is a streaming parser, tokens are expected to be emitted in the order
+ # that the parser encounters them. This is not implemented.
+ def parse(...)
+ lex(...).map do |position, event, token, state|
+ Elem.new(position, event, token, state.to_int)
+ end
+ end
+
+ # Similar to parse but ripper sorts the elements by position in the source. Also
+ # includes errors. Since prism does error recovery, in cases of syntax errors
+ # the result may differ greatly compared to ripper.
+ def scan(...)
+ parse(...)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/prism/translation/ripper/sexp.rb b/lib/prism/translation/ripper/sexp.rb
new file mode 100644
index 0000000000..46c0333544
--- /dev/null
+++ b/lib/prism/translation/ripper/sexp.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+# :markup: markdown
+
+require_relative "../ripper"
+
+module Prism
+ module Translation
+ class Ripper
+ # This class mirrors the ::Ripper::SexpBuilder subclass of ::Ripper that
+ # returns the arrays of [type, *children].
+ class SexpBuilder < Ripper # :nodoc:
+ attr_reader :error
+
+ private
+
+ def dedent_element(e, width)
+ if (n = dedent_string(e[1], width)) > 0
+ e[2][1] += n
+ end
+ e
+ end
+
+ def on_heredoc_dedent(val, width)
+ sub = proc do |cont|
+ cont.map! do |e|
+ if Array === e
+ case e[0]
+ when :@tstring_content
+ e = dedent_element(e, width)
+ when /_add\z/
+ e[1] = sub[e[1]]
+ end
+ elsif String === e
+ dedent_string(e, width)
+ end
+ e
+ end
+ end
+ sub[val]
+ val
+ end
+
+ events = private_instance_methods(false).grep(/\Aon_/) {$'.to_sym}
+ (PARSER_EVENTS - events).each do |event|
+ module_eval(<<-End, __FILE__, __LINE__ + 1)
+ def on_#{event}(*args)
+ args.unshift :#{event}
+ end
+ End
+ end
+
+ SCANNER_EVENTS.each do |event|
+ module_eval(<<-End, __FILE__, __LINE__ + 1)
+ def on_#{event}(tok)
+ [:@#{event}, tok, [lineno(), column()]]
+ end
+ End
+ end
+
+ def on_error(mesg)
+ @error = mesg
+ end
+ remove_method :on_parse_error
+ alias on_parse_error on_error
+ alias compile_error on_error
+ end
+
+ # This class mirrors the ::Ripper::SexpBuilderPP subclass of ::Ripper that
+ # returns the same values as ::Ripper::SexpBuilder except with a couple of
+ # niceties that flatten linked lists into arrays.
+ class SexpBuilderPP < SexpBuilder # :nodoc:
+ private
+
+ def on_heredoc_dedent(val, width)
+ val.map! do |e|
+ next e if Symbol === e and /_content\z/ =~ e
+ if Array === e and e[0] == :@tstring_content
+ e = dedent_element(e, width)
+ elsif String === e
+ dedent_string(e, width)
+ end
+ e
+ end
+ val
+ end
+
+ def _dispatch_event_new
+ []
+ end
+
+ def _dispatch_event_push(list, item)
+ list.push item
+ list
+ end
+
+ def on_mlhs_paren(list)
+ [:mlhs, *list]
+ end
+
+ def on_mlhs_add_star(list, star)
+ list.push([:rest_param, star])
+ end
+
+ def on_mlhs_add_post(list, post)
+ list.concat(post)
+ end
+
+ PARSER_EVENT_TABLE.each do |event, arity|
+ if /_new\z/ =~ event and arity == 0
+ alias_method "on_#{event}", :_dispatch_event_new
+ elsif /_add\z/ =~ event
+ alias_method "on_#{event}", :_dispatch_event_push
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/prism/translation/ripper/shim.rb b/lib/prism/translation/ripper/shim.rb
new file mode 100644
index 0000000000..00ed625da3
--- /dev/null
+++ b/lib/prism/translation/ripper/shim.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+# This writes the prism ripper translation into the Ripper constant so that
+# users can transparently use Ripper without any changes.
+# :stopdoc:
+Ripper = Prism::Translation::Ripper
+# :startdoc:
diff --git a/lib/prism/translation/ruby_parser.rb b/lib/prism/translation/ruby_parser.rb
new file mode 100644
index 0000000000..42bc5ee658
--- /dev/null
+++ b/lib/prism/translation/ruby_parser.rb
@@ -0,0 +1,1676 @@
+# frozen_string_literal: true
+# :markup: markdown
+
+begin
+ require "sexp"
+rescue LoadError
+ warn(%q{Error: Unable to load sexp. Add `gem "sexp_processor"` to your Gemfile.})
+ exit(1)
+end
+
+class RubyParser # :nodoc:
+ class SyntaxError < RuntimeError # :nodoc:
+ end
+end
+
+module Prism
+ module Translation
+ # This module is the entry-point for converting a prism syntax tree into the
+ # seattlerb/ruby_parser gem's syntax tree.
+ class RubyParser
+ # A prism visitor that builds Sexp objects.
+ class Compiler < ::Prism::Compiler # :nodoc:
+ # This is the name of the file that we are compiling. We set it on every
+ # Sexp object that is generated, and also use it to compile `__FILE__`
+ # nodes.
+ attr_reader :file
+
+ # Class variables will change their type based on if they are inside of
+ # a method definition or not, so we need to track that state.
+ attr_reader :in_def
+
+ # Some nodes will change their representation if they are inside of a
+ # pattern, so we need to track that state.
+ attr_reader :in_pattern
+
+ # Initialize a new compiler with the given file name.
+ def initialize(file, in_def: false, in_pattern: false)
+ @file = file
+ @in_def = in_def
+ @in_pattern = in_pattern
+ end
+
+ # alias foo bar
+ # ^^^^^^^^^^^^^
+ def visit_alias_method_node(node)
+ s(node, :alias, visit(node.new_name), visit(node.old_name))
+ end
+
+ # alias $foo $bar
+ # ^^^^^^^^^^^^^^^
+ def visit_alias_global_variable_node(node)
+ s(node, :valias, node.new_name.name, node.old_name.name)
+ end
+
+ # foo => bar | baz
+ # ^^^^^^^^^
+ def visit_alternation_pattern_node(node)
+ s(node, :or, visit(node.left), visit(node.right))
+ end
+
+ # a and b
+ # ^^^^^^^
+ def visit_and_node(node)
+ left = visit(node.left)
+
+ if left[0] == :and
+ # ruby_parser has the and keyword as right-associative as opposed to
+ # prism which has it as left-associative. We reverse that
+ # associativity here.
+ nest = left
+ nest = nest[2] while nest[2][0] == :and
+ nest[2] = s(node, :and, nest[2], visit(node.right))
+ left
+ else
+ s(node, :and, left, visit(node.right))
+ end
+ end
+
+ # []
+ # ^^
+ def visit_array_node(node)
+ if in_pattern
+ s(node, :array_pat, nil).concat(visit_all(node.elements))
+ else
+ s(node, :array).concat(visit_all(node.elements))
+ end
+ end
+
+ # foo => [bar]
+ # ^^^^^
+ def visit_array_pattern_node(node)
+ if node.constant.nil? && node.requireds.empty? && node.rest.nil? && node.posts.empty?
+ s(node, :array_pat)
+ else
+ result = s(node, :array_pat, visit_pattern_constant(node.constant)).concat(visit_all(node.requireds))
+
+ case node.rest
+ when SplatNode
+ result << :"*#{node.rest.expression&.name}"
+ when ImplicitRestNode
+ result << :*
+
+ # This doesn't make any sense at all, but since we're trying to
+ # replicate the behavior directly, we'll copy it.
+ result.line(666)
+ end
+
+ result.concat(visit_all(node.posts))
+ end
+ end
+
+ # foo(bar)
+ # ^^^
+ def visit_arguments_node(node)
+ raise "Cannot visit arguments directly"
+ end
+
+ # { a: 1 }
+ # ^^^^
+ def visit_assoc_node(node)
+ [visit(node.key), visit(node.value)]
+ end
+
+ # def foo(**); bar(**); end
+ # ^^
+ #
+ # { **foo }
+ # ^^^^^
+ def visit_assoc_splat_node(node)
+ if node.value.nil?
+ [s(node, :kwsplat)]
+ else
+ [s(node, :kwsplat, visit(node.value))]
+ end
+ end
+
+ # $+
+ # ^^
+ def visit_back_reference_read_node(node)
+ s(node, :back_ref, node.name.to_s.delete_prefix("$").to_sym)
+ end
+
+ # begin end
+ # ^^^^^^^^^
+ def visit_begin_node(node)
+ result = node.statements.nil? ? s(node, :nil) : visit(node.statements)
+
+ if !node.rescue_clause.nil?
+ if !node.statements.nil?
+ result = s(node.statements, :rescue, result, visit(node.rescue_clause))
+ else
+ result = s(node.rescue_clause, :rescue, visit(node.rescue_clause))
+ end
+
+ current = node.rescue_clause
+ until (current = current.subsequent).nil?
+ result << visit(current)
+ end
+ end
+
+ if !node.else_clause&.statements.nil?
+ result << visit(node.else_clause)
+ end
+
+ if !node.ensure_clause.nil?
+ if !node.statements.nil? || !node.rescue_clause.nil? || !node.else_clause.nil?
+ result = s(node.statements || node.rescue_clause || node.else_clause || node.ensure_clause, :ensure, result, visit(node.ensure_clause))
+ else
+ result = s(node.ensure_clause, :ensure, visit(node.ensure_clause))
+ end
+ end
+
+ result
+ end
+
+ # foo(&bar)
+ # ^^^^
+ def visit_block_argument_node(node)
+ s(node, :block_pass).tap do |result|
+ result << visit(node.expression) unless node.expression.nil?
+ end
+ end
+
+ # foo { |; bar| }
+ # ^^^
+ def visit_block_local_variable_node(node)
+ node.name
+ end
+
+ # A block on a keyword or method call.
+ def visit_block_node(node)
+ s(node, :block_pass, visit(node.expression))
+ end
+
+ # def foo(&bar); end
+ # ^^^^
+ def visit_block_parameter_node(node)
+ :"&#{node.name}"
+ end
+
+ # A block's parameters.
+ def visit_block_parameters_node(node)
+ # If this block parameters has no parameters and is using pipes, then
+ # it inherits its location from its shadow locals, even if they're not
+ # on the same lines as the pipes.
+ shadow_loc = true
+
+ result =
+ if node.parameters.nil?
+ s(node, :args)
+ else
+ shadow_loc = false
+ visit(node.parameters)
+ end
+
+ if node.opening == "("
+ result.line = node.opening_loc.start_line
+ result.line_max = node.closing_loc.end_line
+ shadow_loc = false
+ end
+
+ if node.locals.any?
+ shadow = s(node, :shadow).concat(visit_all(node.locals))
+ shadow.line = node.locals.first.location.start_line
+ shadow.line_max = node.locals.last.location.end_line
+ result << shadow
+
+ if shadow_loc
+ result.line = shadow.line
+ result.line_max = shadow.line_max
+ end
+ end
+
+ result
+ end
+
+ # break
+ # ^^^^^
+ #
+ # break foo
+ # ^^^^^^^^^
+ def visit_break_node(node)
+ if node.arguments.nil?
+ s(node, :break)
+ elsif node.arguments.arguments.length == 1
+ s(node, :break, visit(node.arguments.arguments.first))
+ else
+ s(node, :break, s(node.arguments, :array).concat(visit_all(node.arguments.arguments)))
+ end
+ end
+
+ # foo
+ # ^^^
+ #
+ # foo.bar
+ # ^^^^^^^
+ #
+ # foo.bar() {}
+ # ^^^^^^^^^^^^
+ def visit_call_node(node)
+ case node.name
+ when :!~
+ return s(node, :not, visit(node.copy(name: :"=~")))
+ when :=~
+ if node.arguments&.arguments&.length == 1 && node.block.nil?
+ case node.receiver
+ when StringNode
+ return s(node, :match3, visit(node.arguments.arguments.first), visit(node.receiver))
+ when RegularExpressionNode, InterpolatedRegularExpressionNode
+ return s(node, :match2, visit(node.receiver), visit(node.arguments.arguments.first))
+ end
+
+ case node.arguments.arguments.first
+ when RegularExpressionNode, InterpolatedRegularExpressionNode
+ return s(node, :match3, visit(node.arguments.arguments.first), visit(node.receiver))
+ end
+ end
+ end
+
+ type = node.attribute_write? ? :attrasgn : :call
+ type = :"safe_#{type}" if node.safe_navigation?
+
+ arguments = node.arguments&.arguments || []
+ write_value = arguments.pop if type == :attrasgn
+ block = node.block
+
+ if block.is_a?(BlockArgumentNode)
+ arguments << block
+ block = nil
+ end
+
+ result = s(node, type, visit(node.receiver), node.name).concat(visit_all(arguments))
+ result << visit_write_value(write_value) unless write_value.nil?
+
+ visit_block(node, result, block)
+ end
+
+ # foo.bar += baz
+ # ^^^^^^^^^^^^^^^
+ def visit_call_operator_write_node(node)
+ if op_asgn?(node)
+ s(node, op_asgn_type(node, :op_asgn), visit(node.receiver), visit_write_value(node.value), node.read_name, node.binary_operator)
+ else
+ s(node, op_asgn_type(node, :op_asgn2), visit(node.receiver), node.write_name, node.binary_operator, visit_write_value(node.value))
+ end
+ end
+
+ # foo.bar &&= baz
+ # ^^^^^^^^^^^^^^^
+ def visit_call_and_write_node(node)
+ if op_asgn?(node)
+ s(node, op_asgn_type(node, :op_asgn), visit(node.receiver), visit_write_value(node.value), node.read_name, :"&&")
+ else
+ s(node, op_asgn_type(node, :op_asgn2), visit(node.receiver), node.write_name, :"&&", visit_write_value(node.value))
+ end
+ end
+
+ # foo.bar ||= baz
+ # ^^^^^^^^^^^^^^^
+ def visit_call_or_write_node(node)
+ if op_asgn?(node)
+ s(node, op_asgn_type(node, :op_asgn), visit(node.receiver), visit_write_value(node.value), node.read_name, :"||")
+ else
+ s(node, op_asgn_type(node, :op_asgn2), visit(node.receiver), node.write_name, :"||", visit_write_value(node.value))
+ end
+ end
+
+ # Call nodes with operators following them will either be op_asgn or
+ # op_asgn2 nodes. That is determined by their call operator and their
+ # right-hand side.
+ private def op_asgn?(node)
+ node.call_operator == "::" || (node.value.is_a?(CallNode) && node.value.opening_loc.nil? && !node.value.arguments.nil?)
+ end
+
+ # Call nodes with operators following them can use &. as an operator,
+ # which changes their type by prefixing "safe_".
+ private def op_asgn_type(node, type)
+ node.safe_navigation? ? :"safe_#{type}" : type
+ end
+
+ # foo.bar, = 1
+ # ^^^^^^^
+ def visit_call_target_node(node)
+ s(node, :attrasgn, visit(node.receiver), node.name)
+ end
+
+ # foo => bar => baz
+ # ^^^^^^^^^^
+ def visit_capture_pattern_node(node)
+ visit(node.target) << visit(node.value)
+ end
+
+ # case foo; when bar; end
+ # ^^^^^^^^^^^^^^^^^^^^^^^
+ def visit_case_node(node)
+ s(node, :case, visit(node.predicate)).concat(visit_all(node.conditions)) << visit(node.else_clause)
+ end
+
+ # case foo; in bar; end
+ # ^^^^^^^^^^^^^^^^^^^^^
+ def visit_case_match_node(node)
+ s(node, :case, visit(node.predicate)).concat(visit_all(node.conditions)) << visit(node.else_clause)
+ end
+
+ # class Foo; end
+ # ^^^^^^^^^^^^^^
+ def visit_class_node(node)
+ name =
+ if node.constant_path.is_a?(ConstantReadNode)
+ node.name
+ else
+ visit(node.constant_path)
+ end
+
+ result =
+ if node.body.nil?
+ s(node, :class, name, visit(node.superclass))
+ elsif node.body.is_a?(StatementsNode)
+ compiler = copy_compiler(in_def: false)
+ s(node, :class, name, visit(node.superclass)).concat(node.body.body.map { |child| child.accept(compiler) })
+ else
+ s(node, :class, name, visit(node.superclass), node.body.accept(copy_compiler(in_def: false)))
+ end
+
+ attach_comments(result, node)
+ result
+ end
+
+ # @@foo
+ # ^^^^^
+ def visit_class_variable_read_node(node)
+ s(node, :cvar, node.name)
+ end
+
+ # @@foo = 1
+ # ^^^^^^^^^
+ def visit_class_variable_write_node(node)
+ s(node, class_variable_write_type, node.name, visit_write_value(node.value))
+ end
+
+ # @@foo += bar
+ # ^^^^^^^^^^^^
+ def visit_class_variable_operator_write_node(node)
+ s(node, class_variable_write_type, node.name, s(node, :call, s(node, :cvar, node.name), node.binary_operator, visit_write_value(node.value)))
+ end
+
+ # @@foo &&= bar
+ # ^^^^^^^^^^^^^
+ def visit_class_variable_and_write_node(node)
+ s(node, :op_asgn_and, s(node, :cvar, node.name), s(node, class_variable_write_type, node.name, visit_write_value(node.value)))
+ end
+
+ # @@foo ||= bar
+ # ^^^^^^^^^^^^^
+ def visit_class_variable_or_write_node(node)
+ s(node, :op_asgn_or, s(node, :cvar, node.name), s(node, class_variable_write_type, node.name, visit_write_value(node.value)))
+ end
+
+ # @@foo, = bar
+ # ^^^^^
+ def visit_class_variable_target_node(node)
+ s(node, class_variable_write_type, node.name)
+ end
+
+ # If a class variable is written within a method definition, it has a
+ # different type than everywhere else.
+ private def class_variable_write_type
+ in_def ? :cvasgn : :cvdecl
+ end
+
+ # Foo
+ # ^^^
+ def visit_constant_read_node(node)
+ s(node, :const, node.name)
+ end
+
+ # Foo = 1
+ # ^^^^^^^
+ #
+ # Foo, Bar = 1
+ # ^^^ ^^^
+ def visit_constant_write_node(node)
+ s(node, :cdecl, node.name, visit_write_value(node.value))
+ end
+
+ # Foo += bar
+ # ^^^^^^^^^^^
+ def visit_constant_operator_write_node(node)
+ s(node, :cdecl, node.name, s(node, :call, s(node, :const, node.name), node.binary_operator, visit_write_value(node.value)))
+ end
+
+ # Foo &&= bar
+ # ^^^^^^^^^^^^
+ def visit_constant_and_write_node(node)
+ s(node, :op_asgn_and, s(node, :const, node.name), s(node, :cdecl, node.name, visit(node.value)))
+ end
+
+ # Foo ||= bar
+ # ^^^^^^^^^^^^
+ def visit_constant_or_write_node(node)
+ s(node, :op_asgn_or, s(node, :const, node.name), s(node, :cdecl, node.name, visit(node.value)))
+ end
+
+ # Foo, = bar
+ # ^^^
+ def visit_constant_target_node(node)
+ s(node, :cdecl, node.name)
+ end
+
+ # Foo::Bar
+ # ^^^^^^^^
+ def visit_constant_path_node(node)
+ if node.parent.nil?
+ s(node, :colon3, node.name)
+ else
+ s(node, :colon2, visit(node.parent), node.name)
+ end
+ end
+
+ # Foo::Bar = 1
+ # ^^^^^^^^^^^^
+ #
+ # Foo::Foo, Bar::Bar = 1
+ # ^^^^^^^^ ^^^^^^^^
+ def visit_constant_path_write_node(node)
+ s(node, :cdecl, visit(node.target), visit_write_value(node.value))
+ end
+
+ # Foo::Bar += baz
+ # ^^^^^^^^^^^^^^^
+ def visit_constant_path_operator_write_node(node)
+ s(node, :op_asgn, visit(node.target), node.binary_operator, visit_write_value(node.value))
+ end
+
+ # Foo::Bar &&= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_constant_path_and_write_node(node)
+ s(node, :op_asgn_and, visit(node.target), visit_write_value(node.value))
+ end
+
+ # Foo::Bar ||= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_constant_path_or_write_node(node)
+ s(node, :op_asgn_or, visit(node.target), visit_write_value(node.value))
+ end
+
+ # Foo::Bar, = baz
+ # ^^^^^^^^
+ def visit_constant_path_target_node(node)
+ inner =
+ if node.parent.nil?
+ s(node, :colon3, node.name)
+ else
+ s(node, :colon2, visit(node.parent), node.name)
+ end
+
+ s(node, :const, inner)
+ end
+
+ # def foo; end
+ # ^^^^^^^^^^^^
+ #
+ # def self.foo; end
+ # ^^^^^^^^^^^^^^^^^
+ def visit_def_node(node)
+ name = node.name_loc.slice.to_sym
+ result =
+ if node.receiver.nil?
+ s(node, :defn, name)
+ else
+ s(node, :defs, visit(node.receiver), name)
+ end
+
+ attach_comments(result, node)
+ result.line(node.name_loc.start_line)
+
+ if node.parameters.nil?
+ result << s(node, :args).line(node.name_loc.start_line)
+ else
+ result << visit(node.parameters)
+ end
+
+ if node.body.nil?
+ result << s(node, :nil)
+ elsif node.body.is_a?(StatementsNode)
+ compiler = copy_compiler(in_def: true)
+ result.concat(node.body.body.map { |child| child.accept(compiler) })
+ else
+ result << node.body.accept(copy_compiler(in_def: true))
+ end
+ end
+
+ # defined? a
+ # ^^^^^^^^^^
+ #
+ # defined?(a)
+ # ^^^^^^^^^^^
+ def visit_defined_node(node)
+ s(node, :defined, visit(node.value))
+ end
+
+ # if foo then bar else baz end
+ # ^^^^^^^^^^^^
+ def visit_else_node(node)
+ visit(node.statements)
+ end
+
+ # "foo #{bar}"
+ # ^^^^^^
+ def visit_embedded_statements_node(node)
+ result = s(node, :evstr)
+ result << visit(node.statements) unless node.statements.nil?
+ result
+ end
+
+ # "foo #@bar"
+ # ^^^^^
+ def visit_embedded_variable_node(node)
+ s(node, :evstr, visit(node.variable))
+ end
+
+ # begin; foo; ensure; bar; end
+ # ^^^^^^^^^^^^
+ def visit_ensure_node(node)
+ node.statements.nil? ? s(node, :nil) : visit(node.statements)
+ end
+
+ # false
+ # ^^^^^
+ def visit_false_node(node)
+ s(node, :false)
+ end
+
+ # foo => [*, bar, *]
+ # ^^^^^^^^^^^
+ def visit_find_pattern_node(node)
+ s(node, :find_pat, visit_pattern_constant(node.constant), :"*#{node.left.expression&.name}", *visit_all(node.requireds), :"*#{node.right.expression&.name}")
+ end
+
+ # if foo .. bar; end
+ # ^^^^^^^^^^
+ def visit_flip_flop_node(node)
+ if node.left.is_a?(IntegerNode) && node.right.is_a?(IntegerNode)
+ s(node, :lit, Range.new(node.left.value, node.right.value, node.exclude_end?))
+ else
+ s(node, node.exclude_end? ? :flip3 : :flip2, visit(node.left), visit(node.right))
+ end
+ end
+
+ # 1.0
+ # ^^^
+ def visit_float_node(node)
+ s(node, :lit, node.value)
+ end
+
+ # for foo in bar do end
+ # ^^^^^^^^^^^^^^^^^^^^^
+ def visit_for_node(node)
+ s(node, :for, visit(node.collection), visit(node.index), visit(node.statements))
+ end
+
+ # def foo(...); bar(...); end
+ # ^^^
+ def visit_forwarding_arguments_node(node)
+ s(node, :forward_args)
+ end
+
+ # def foo(...); end
+ # ^^^
+ def visit_forwarding_parameter_node(node)
+ s(node, :forward_args)
+ end
+
+ # super
+ # ^^^^^
+ #
+ # super {}
+ # ^^^^^^^^
+ def visit_forwarding_super_node(node)
+ visit_block(node, s(node, :zsuper), node.block)
+ end
+
+ # $foo
+ # ^^^^
+ def visit_global_variable_read_node(node)
+ s(node, :gvar, node.name)
+ end
+
+ # $foo = 1
+ # ^^^^^^^^
+ def visit_global_variable_write_node(node)
+ s(node, :gasgn, node.name, visit_write_value(node.value))
+ end
+
+ # $foo += bar
+ # ^^^^^^^^^^^
+ def visit_global_variable_operator_write_node(node)
+ s(node, :gasgn, node.name, s(node, :call, s(node, :gvar, node.name), node.binary_operator, visit(node.value)))
+ end
+
+ # $foo &&= bar
+ # ^^^^^^^^^^^^
+ def visit_global_variable_and_write_node(node)
+ s(node, :op_asgn_and, s(node, :gvar, node.name), s(node, :gasgn, node.name, visit_write_value(node.value)))
+ end
+
+ # $foo ||= bar
+ # ^^^^^^^^^^^^
+ def visit_global_variable_or_write_node(node)
+ s(node, :op_asgn_or, s(node, :gvar, node.name), s(node, :gasgn, node.name, visit_write_value(node.value)))
+ end
+
+ # $foo, = bar
+ # ^^^^
+ def visit_global_variable_target_node(node)
+ s(node, :gasgn, node.name)
+ end
+
+ # {}
+ # ^^
+ def visit_hash_node(node)
+ s(node, :hash).concat(node.elements.flat_map { |element| visit(element) })
+ end
+
+ # foo => {}
+ # ^^
+ def visit_hash_pattern_node(node)
+ result = s(node, :hash_pat, visit_pattern_constant(node.constant)).concat(node.elements.flat_map { |element| visit(element) })
+
+ case node.rest
+ when AssocSplatNode
+ result << s(node.rest, :kwrest, :"**#{node.rest.value&.name}")
+ when NoKeywordsParameterNode
+ result << visit(node.rest)
+ end
+
+ result
+ end
+
+ # if foo then bar end
+ # ^^^^^^^^^^^^^^^^^^^
+ #
+ # bar if foo
+ # ^^^^^^^^^^
+ #
+ # foo ? bar : baz
+ # ^^^^^^^^^^^^^^^
+ def visit_if_node(node)
+ s(node, :if, visit(node.predicate), visit(node.statements), visit(node.subsequent))
+ end
+
+ # 1i
+ def visit_imaginary_node(node)
+ s(node, :lit, node.value)
+ end
+
+ # { foo: }
+ # ^^^^
+ def visit_implicit_node(node)
+ end
+
+ # foo { |bar,| }
+ # ^
+ def visit_implicit_rest_node(node)
+ end
+
+ # case foo; in bar; end
+ # ^^^^^^^^^^^^^^^^^^^^^
+ def visit_in_node(node)
+ pattern =
+ if node.pattern.is_a?(ConstantPathNode)
+ s(node.pattern, :const, visit(node.pattern))
+ else
+ node.pattern.accept(copy_compiler(in_pattern: true))
+ end
+
+ s(node, :in, pattern).concat(node.statements.nil? ? [nil] : visit_all(node.statements.body))
+ end
+
+ # foo[bar] += baz
+ # ^^^^^^^^^^^^^^^
+ def visit_index_operator_write_node(node)
+ arglist = nil
+
+ if !node.arguments.nil? || !node.block.nil?
+ arglist = s(node, :arglist).concat(visit_all(node.arguments&.arguments || []))
+ arglist << visit(node.block) if !node.block.nil?
+ end
+
+ s(node, :op_asgn1, visit(node.receiver), arglist, node.binary_operator, visit_write_value(node.value))
+ end
+
+ # foo[bar] &&= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_index_and_write_node(node)
+ arglist = nil
+
+ if !node.arguments.nil? || !node.block.nil?
+ arglist = s(node, :arglist).concat(visit_all(node.arguments&.arguments || []))
+ arglist << visit(node.block) if !node.block.nil?
+ end
+
+ s(node, :op_asgn1, visit(node.receiver), arglist, :"&&", visit_write_value(node.value))
+ end
+
+ # foo[bar] ||= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_index_or_write_node(node)
+ arglist = nil
+
+ if !node.arguments.nil? || !node.block.nil?
+ arglist = s(node, :arglist).concat(visit_all(node.arguments&.arguments || []))
+ arglist << visit(node.block) if !node.block.nil?
+ end
+
+ s(node, :op_asgn1, visit(node.receiver), arglist, :"||", visit_write_value(node.value))
+ end
+
+ # foo[bar], = 1
+ # ^^^^^^^^
+ def visit_index_target_node(node)
+ arguments = visit_all(node.arguments&.arguments || [])
+ arguments << visit(node.block) unless node.block.nil?
+
+ s(node, :attrasgn, visit(node.receiver), :[]=).concat(arguments)
+ end
+
+ # @foo
+ # ^^^^
+ def visit_instance_variable_read_node(node)
+ s(node, :ivar, node.name)
+ end
+
+ # @foo = 1
+ # ^^^^^^^^
+ def visit_instance_variable_write_node(node)
+ s(node, :iasgn, node.name, visit_write_value(node.value))
+ end
+
+ # @foo += bar
+ # ^^^^^^^^^^^
+ def visit_instance_variable_operator_write_node(node)
+ s(node, :iasgn, node.name, s(node, :call, s(node, :ivar, node.name), node.binary_operator, visit_write_value(node.value)))
+ end
+
+ # @foo &&= bar
+ # ^^^^^^^^^^^^
+ def visit_instance_variable_and_write_node(node)
+ s(node, :op_asgn_and, s(node, :ivar, node.name), s(node, :iasgn, node.name, visit(node.value)))
+ end
+
+ # @foo ||= bar
+ # ^^^^^^^^^^^^
+ def visit_instance_variable_or_write_node(node)
+ s(node, :op_asgn_or, s(node, :ivar, node.name), s(node, :iasgn, node.name, visit(node.value)))
+ end
+
+ # @foo, = bar
+ # ^^^^
+ def visit_instance_variable_target_node(node)
+ s(node, :iasgn, node.name)
+ end
+
+ # 1
+ # ^
+ def visit_integer_node(node)
+ s(node, :lit, node.value)
+ end
+
+ # if /foo #{bar}/ then end
+ # ^^^^^^^^^^^^
+ def visit_interpolated_match_last_line_node(node)
+ parts = visit_interpolated_parts(node.parts)
+ regexp =
+ if parts.length == 1
+ s(node, :lit, Regexp.new(parts.first, node.options))
+ else
+ s(node, :dregx).concat(parts).tap do |result|
+ options = node.options
+ result << options if options != 0
+ end
+ end
+
+ s(node, :match, regexp)
+ end
+
+ # /foo #{bar}/
+ # ^^^^^^^^^^^^
+ def visit_interpolated_regular_expression_node(node)
+ parts = visit_interpolated_parts(node.parts)
+
+ if parts.length == 1
+ s(node, :lit, Regexp.new(parts.first, node.options))
+ else
+ s(node, :dregx).concat(parts).tap do |result|
+ options = node.options
+ result << options if options != 0
+ end
+ end
+ end
+
+ # "foo #{bar}"
+ # ^^^^^^^^^^^^
+ def visit_interpolated_string_node(node)
+ parts = visit_interpolated_parts(node.parts)
+ parts.length == 1 ? s(node, :str, parts.first) : s(node, :dstr).concat(parts)
+ end
+
+ # :"foo #{bar}"
+ # ^^^^^^^^^^^^^
+ def visit_interpolated_symbol_node(node)
+ parts = visit_interpolated_parts(node.parts)
+ parts.length == 1 ? s(node, :lit, parts.first.to_sym) : s(node, :dsym).concat(parts)
+ end
+
+ # `foo #{bar}`
+ # ^^^^^^^^^^^^
+ def visit_interpolated_x_string_node(node)
+ source = node.heredoc? ? node.parts.first : node
+ parts = visit_interpolated_parts(node.parts)
+ parts.length == 1 ? s(source, :xstr, parts.first) : s(source, :dxstr).concat(parts)
+ end
+
+ # Visit the interpolated content of the string-like node.
+ private def visit_interpolated_parts(parts)
+ visited = []
+
+ parts.each do |part|
+ result = visit(part)
+
+ if result[0] == :evstr && result[1]
+ if result[1][0] == :str
+ visited << result[1]
+ elsif result[1][0] == :dstr
+ visited.concat(result[1][1..-1])
+ else
+ visited << result
+ end
+ visited << :space
+ elsif result[0] == :dstr
+ if !visited.empty? && part.parts[0].is_a?(StringNode)
+ # If we are in the middle of an implicitly concatenated string,
+ # we should not have a bare string as the first part. In this
+ # case we need to visit just that first part and then we can
+ # push the rest of the parts onto the visited array.
+ result[1] = visit(part.parts[0])
+ end
+ visited.concat(result[1..-1])
+ else
+ visited << result
+ end
+ end
+
+ state = :beginning #: :beginning | :string_content | :interpolated_content
+ results = []
+
+ visited.each_with_index do |result, index|
+ case state
+ when :beginning
+ if result.is_a?(String)
+ results << result
+ state = :string_content
+ elsif result.is_a?(Array) && result[0] == :str
+ results << result[1]
+ state = :string_content
+ else
+ results << ""
+ results << result
+ state = :interpolated_content
+ end
+ when :string_content
+ if result == :space
+ # continue
+ elsif result.is_a?(String)
+ results[0] = "#{results[0]}#{result}"
+ elsif result.is_a?(Array) && result[0] == :str
+ results[0] = "#{results[0]}#{result[1]}"
+ else
+ results << result
+ state = :interpolated_content
+ end
+ when :interpolated_content
+ if result == :space
+ # continue
+ elsif visited[index - 1] != :space && result.is_a?(Array) && result[0] == :str && results[-1][0] == :str && (results[-1].line_max == result.line)
+ results[-1][1] = "#{results[-1][1]}#{result[1]}"
+ results[-1].line_max = result.line_max
+ else
+ results << result
+ end
+ end
+ end
+
+ results
+ end
+
+ # -> { it }
+ # ^^
+ def visit_it_local_variable_read_node(node)
+ s(node, :call, nil, :it)
+ end
+
+ # foo(bar: baz)
+ # ^^^^^^^^
+ def visit_keyword_hash_node(node)
+ s(node, :hash).concat(node.elements.flat_map { |element| visit(element) })
+ end
+
+ # def foo(**bar); end
+ # ^^^^^
+ #
+ # def foo(**); end
+ # ^^
+ def visit_keyword_rest_parameter_node(node)
+ :"**#{node.name}"
+ end
+
+ # -> {}
+ def visit_lambda_node(node)
+ parameters =
+ case node.parameters
+ when nil, ItParametersNode, NumberedParametersNode
+ 0
+ else
+ visit(node.parameters)
+ end
+
+ if node.body.nil?
+ s(node, :iter, s(node, :lambda), parameters)
+ else
+ s(node, :iter, s(node, :lambda), parameters, visit(node.body))
+ end
+ end
+
+ # foo
+ # ^^^
+ def visit_local_variable_read_node(node)
+ if node.name.match?(/^_\d$/)
+ s(node, :call, nil, node.name)
+ else
+ s(node, :lvar, node.name)
+ end
+ end
+
+ # foo = 1
+ # ^^^^^^^
+ def visit_local_variable_write_node(node)
+ s(node, :lasgn, node.name, visit_write_value(node.value))
+ end
+
+ # foo += bar
+ # ^^^^^^^^^^
+ def visit_local_variable_operator_write_node(node)
+ s(node, :lasgn, node.name, s(node, :call, s(node, :lvar, node.name), node.binary_operator, visit_write_value(node.value)))
+ end
+
+ # foo &&= bar
+ # ^^^^^^^^^^^
+ def visit_local_variable_and_write_node(node)
+ s(node, :op_asgn_and, s(node, :lvar, node.name), s(node, :lasgn, node.name, visit_write_value(node.value)))
+ end
+
+ # foo ||= bar
+ # ^^^^^^^^^^^
+ def visit_local_variable_or_write_node(node)
+ s(node, :op_asgn_or, s(node, :lvar, node.name), s(node, :lasgn, node.name, visit_write_value(node.value)))
+ end
+
+ # foo, = bar
+ # ^^^
+ def visit_local_variable_target_node(node)
+ s(node, :lasgn, node.name)
+ end
+
+ # if /foo/ then end
+ # ^^^^^
+ def visit_match_last_line_node(node)
+ s(node, :match, s(node, :lit, Regexp.new(node.unescaped, node.options)))
+ end
+
+ # foo in bar
+ # ^^^^^^^^^^
+ def visit_match_predicate_node(node)
+ s(node, :case, visit(node.value), s(node, :in, node.pattern.accept(copy_compiler(in_pattern: true)), nil), nil)
+ end
+
+ # foo => bar
+ # ^^^^^^^^^^
+ def visit_match_required_node(node)
+ s(node, :case, visit(node.value), s(node, :in, node.pattern.accept(copy_compiler(in_pattern: true)), nil), nil)
+ end
+
+ # /(?<foo>foo)/ =~ bar
+ # ^^^^^^^^^^^^^^^^^^^^
+ def visit_match_write_node(node)
+ s(node, :match2, visit(node.call.receiver), visit(node.call.arguments.arguments.first))
+ end
+
+ # A node that is missing from the syntax tree. This is only used in the
+ # case of a syntax error. The parser gem doesn't have such a concept, so
+ # we invent our own here.
+ def visit_error_recovery_node(node)
+ raise "Cannot visit error recovery node directly"
+ end
+
+ # module Foo; end
+ # ^^^^^^^^^^^^^^^
+ def visit_module_node(node)
+ name =
+ if node.constant_path.is_a?(ConstantReadNode)
+ node.name
+ else
+ visit(node.constant_path)
+ end
+
+ result =
+ if node.body.nil?
+ s(node, :module, name)
+ elsif node.body.is_a?(StatementsNode)
+ compiler = copy_compiler(in_def: false)
+ s(node, :module, name).concat(node.body.body.map { |child| child.accept(compiler) })
+ else
+ s(node, :module, name, node.body.accept(copy_compiler(in_def: false)))
+ end
+
+ attach_comments(result, node)
+ result
+ end
+
+ # foo, bar = baz
+ # ^^^^^^^^
+ def visit_multi_target_node(node)
+ targets = [*node.lefts]
+ targets << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode)
+ targets.concat(node.rights)
+
+ s(node, :masgn, s(node, :array).concat(visit_all(targets)))
+ end
+
+ # foo, bar = baz
+ # ^^^^^^^^^^^^^^
+ def visit_multi_write_node(node)
+ targets = [*node.lefts]
+ targets << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode)
+ targets.concat(node.rights)
+
+ value =
+ if node.value.is_a?(ArrayNode) && node.value.opening_loc.nil?
+ if node.value.elements.length == 1 && node.value.elements.first.is_a?(SplatNode)
+ visit(node.value.elements.first)
+ else
+ visit(node.value)
+ end
+ else
+ s(node.value, :to_ary, visit(node.value))
+ end
+
+ s(node, :masgn, s(node, :array).concat(visit_all(targets)), value)
+ end
+
+ # next
+ # ^^^^
+ #
+ # next foo
+ # ^^^^^^^^
+ def visit_next_node(node)
+ if node.arguments.nil?
+ s(node, :next)
+ elsif node.arguments.arguments.length == 1
+ argument = node.arguments.arguments.first
+ s(node, :next, argument.is_a?(SplatNode) ? s(node, :svalue, visit(argument)) : visit(argument))
+ else
+ s(node, :next, s(node, :array).concat(visit_all(node.arguments.arguments)))
+ end
+ end
+
+ # nil
+ # ^^^
+ def visit_nil_node(node)
+ s(node, :nil)
+ end
+
+ # def foo(&nil); end
+ # ^^^^
+ def visit_no_block_parameter_node(node)
+ :"&nil"
+ end
+
+ # def foo(**nil); end
+ # ^^^^^
+ def visit_no_keywords_parameter_node(node)
+ in_pattern ? s(node, :kwrest, :"**nil") : :"**nil"
+ end
+
+ # -> { _1 + _2 }
+ # ^^^^^^^^^^^^^^
+ def visit_numbered_parameters_node(node)
+ raise "Cannot visit numbered parameters directly"
+ end
+
+ # $1
+ # ^^
+ def visit_numbered_reference_read_node(node)
+ s(node, :nth_ref, node.number)
+ end
+
+ # def foo(bar: baz); end
+ # ^^^^^^^^
+ def visit_optional_keyword_parameter_node(node)
+ s(node, :kwarg, node.name, visit(node.value))
+ end
+
+ # def foo(bar = 1); end
+ # ^^^^^^^
+ def visit_optional_parameter_node(node)
+ s(node, :lasgn, node.name, visit(node.value))
+ end
+
+ # a or b
+ # ^^^^^^
+ def visit_or_node(node)
+ left = visit(node.left)
+
+ if left[0] == :or
+ # ruby_parser has the or keyword as right-associative as opposed to
+ # prism which has it as left-associative. We reverse that
+ # associativity here.
+ nest = left
+ nest = nest[2] while nest[2][0] == :or
+ nest[2] = s(node, :or, nest[2], visit(node.right))
+ left
+ else
+ s(node, :or, left, visit(node.right))
+ end
+ end
+
+ # def foo(bar, *baz); end
+ # ^^^^^^^^^
+ def visit_parameters_node(node)
+ children =
+ node.each_child_node.map do |element|
+ if element.is_a?(MultiTargetNode)
+ visit_destructured_parameter(element)
+ else
+ visit(element)
+ end
+ end
+
+ s(node, :args).concat(children)
+ end
+
+ # def foo((bar, baz)); end
+ # ^^^^^^^^^^
+ private def visit_destructured_parameter(node)
+ children =
+ [*node.lefts, *node.rest, *node.rights].map do |child|
+ case child
+ when RequiredParameterNode
+ visit(child)
+ when MultiTargetNode
+ visit_destructured_parameter(child)
+ when SplatNode
+ :"*#{child.expression&.name}"
+ else
+ raise
+ end
+ end
+
+ s(node, :masgn).concat(children)
+ end
+
+ # ()
+ # ^^
+ #
+ # (1)
+ # ^^^
+ def visit_parentheses_node(node)
+ if node.body.nil?
+ s(node, :nil)
+ else
+ visit(node.body)
+ end
+ end
+
+ # foo => ^(bar)
+ # ^^^^^^
+ def visit_pinned_expression_node(node)
+ node.expression.accept(copy_compiler(in_pattern: false))
+ end
+
+ # foo = 1 and bar => ^foo
+ # ^^^^
+ def visit_pinned_variable_node(node)
+ if node.variable.is_a?(LocalVariableReadNode) && node.variable.name.match?(/^_\d$/)
+ s(node, :lvar, node.variable.name)
+ else
+ visit(node.variable)
+ end
+ end
+
+ # END {}
+ def visit_post_execution_node(node)
+ s(node, :iter, s(node, :postexe), 0, visit(node.statements))
+ end
+
+ # BEGIN {}
+ def visit_pre_execution_node(node)
+ s(node, :iter, s(node, :preexe), 0, visit(node.statements))
+ end
+
+ # The top-level program node.
+ def visit_program_node(node)
+ visit(node.statements)
+ end
+
+ # 0..5
+ # ^^^^
+ def visit_range_node(node)
+ if !in_pattern && !node.left.nil? && !node.right.nil? && ([node.left.type, node.right.type] - %i[nil_node integer_node]).empty?
+ left = node.left.value if node.left.is_a?(IntegerNode)
+ right = node.right.value if node.right.is_a?(IntegerNode)
+ s(node, :lit, Range.new(left, right, node.exclude_end?))
+ else
+ s(node, node.exclude_end? ? :dot3 : :dot2, visit_range_bounds_node(node.left), visit_range_bounds_node(node.right))
+ end
+ end
+
+ # If the bounds of a range node are empty parentheses, then they do not
+ # get replaced by their usual s(:nil), but instead are s(:begin).
+ private def visit_range_bounds_node(node)
+ if node.is_a?(ParenthesesNode) && node.body.nil?
+ s(node, :begin)
+ else
+ visit(node)
+ end
+ end
+
+ # 1r
+ # ^^
+ def visit_rational_node(node)
+ s(node, :lit, node.value)
+ end
+
+ # redo
+ # ^^^^
+ def visit_redo_node(node)
+ s(node, :redo)
+ end
+
+ # /foo/
+ # ^^^^^
+ def visit_regular_expression_node(node)
+ s(node, :lit, Regexp.new(node.unescaped, node.options))
+ end
+
+ # def foo(bar:); end
+ # ^^^^
+ def visit_required_keyword_parameter_node(node)
+ s(node, :kwarg, node.name)
+ end
+
+ # def foo(bar); end
+ # ^^^
+ def visit_required_parameter_node(node)
+ node.name
+ end
+
+ # foo rescue bar
+ # ^^^^^^^^^^^^^^
+ def visit_rescue_modifier_node(node)
+ s(node, :rescue, visit(node.expression), s(node.rescue_expression, :resbody, s(node.rescue_expression, :array), visit(node.rescue_expression)))
+ end
+
+ # begin; rescue; end
+ # ^^^^^^^
+ def visit_rescue_node(node)
+ exceptions =
+ if node.exceptions.length == 1 && node.exceptions.first.is_a?(SplatNode)
+ visit(node.exceptions.first)
+ else
+ s(node, :array).concat(visit_all(node.exceptions))
+ end
+
+ if !node.reference.nil?
+ exceptions << (visit(node.reference) << s(node.reference, :gvar, :"$!"))
+ end
+
+ s(node, :resbody, exceptions).concat(node.statements.nil? ? [nil] : visit_all(node.statements.body))
+ end
+
+ # def foo(*bar); end
+ # ^^^^
+ #
+ # def foo(*); end
+ # ^
+ def visit_rest_parameter_node(node)
+ :"*#{node.name}"
+ end
+
+ # retry
+ # ^^^^^
+ def visit_retry_node(node)
+ s(node, :retry)
+ end
+
+ # return
+ # ^^^^^^
+ #
+ # return 1
+ # ^^^^^^^^
+ def visit_return_node(node)
+ if node.arguments.nil?
+ s(node, :return)
+ elsif node.arguments.arguments.length == 1
+ argument = node.arguments.arguments.first
+ s(node, :return, argument.is_a?(SplatNode) ? s(node, :svalue, visit(argument)) : visit(argument))
+ else
+ s(node, :return, s(node, :array).concat(visit_all(node.arguments.arguments)))
+ end
+ end
+
+ # self
+ # ^^^^
+ def visit_self_node(node)
+ s(node, :self)
+ end
+
+ # A shareable constant.
+ def visit_shareable_constant_node(node)
+ visit(node.write)
+ end
+
+ # class << self; end
+ # ^^^^^^^^^^^^^^^^^^
+ def visit_singleton_class_node(node)
+ s(node, :sclass, visit(node.expression)).tap do |sexp|
+ sexp << node.body.accept(copy_compiler(in_def: false)) unless node.body.nil?
+ end
+ end
+
+ # __ENCODING__
+ # ^^^^^^^^^^^^
+ def visit_source_encoding_node(node)
+ # TODO
+ s(node, :colon2, s(node, :const, :Encoding), :UTF_8)
+ end
+
+ # __FILE__
+ # ^^^^^^^^
+ def visit_source_file_node(node)
+ s(node, :str, node.filepath)
+ end
+
+ # __LINE__
+ # ^^^^^^^^
+ def visit_source_line_node(node)
+ s(node, :lit, node.location.start_line)
+ end
+
+ # foo(*bar)
+ # ^^^^
+ #
+ # def foo((bar, *baz)); end
+ # ^^^^
+ #
+ # def foo(*); bar(*); end
+ # ^
+ def visit_splat_node(node)
+ if node.expression.nil?
+ s(node, :splat)
+ else
+ s(node, :splat, visit(node.expression))
+ end
+ end
+
+ # A list of statements.
+ def visit_statements_node(node)
+ first, *rest = node.body
+
+ if rest.empty?
+ visit(first)
+ else
+ s(node, :block).concat(visit_all(node.body))
+ end
+ end
+
+ # "foo"
+ # ^^^^^
+ def visit_string_node(node)
+ unescaped = node.unescaped
+
+ if node.forced_binary_encoding?
+ unescaped = unescaped.dup
+ unescaped.force_encoding(Encoding::BINARY)
+ end
+
+ s(node, :str, unescaped)
+ end
+
+ # super(foo)
+ # ^^^^^^^^^^
+ def visit_super_node(node)
+ arguments = node.arguments&.arguments || []
+ block = node.block
+
+ if block.is_a?(BlockArgumentNode)
+ arguments << block
+ block = nil
+ end
+
+ visit_block(node, s(node, :super).concat(visit_all(arguments)), block)
+ end
+
+ # :foo
+ # ^^^^
+ def visit_symbol_node(node)
+ node.value == "!@" ? s(node, :lit, :"!@") : s(node, :lit, node.unescaped.to_sym)
+ end
+
+ # true
+ # ^^^^
+ def visit_true_node(node)
+ s(node, :true)
+ end
+
+ # undef foo
+ # ^^^^^^^^^
+ def visit_undef_node(node)
+ names = node.names.map { |name| s(node, :undef, visit(name)) }
+ names.length == 1 ? names.first : s(node, :block).concat(names)
+ end
+
+ # unless foo; bar end
+ # ^^^^^^^^^^^^^^^^^^^
+ #
+ # bar unless foo
+ # ^^^^^^^^^^^^^^
+ def visit_unless_node(node)
+ s(node, :if, visit(node.predicate), visit(node.else_clause), visit(node.statements))
+ end
+
+ # until foo; bar end
+ # ^^^^^^^^^^^^^^^^^
+ #
+ # bar until foo
+ # ^^^^^^^^^^^^^
+ def visit_until_node(node)
+ s(node, :until, visit(node.predicate), visit(node.statements), !node.begin_modifier?)
+ end
+
+ # case foo; when bar; end
+ # ^^^^^^^^^^^^^
+ def visit_when_node(node)
+ s(node, :when, s(node, :array).concat(visit_all(node.conditions))).concat(node.statements.nil? ? [nil] : visit_all(node.statements.body))
+ end
+
+ # while foo; bar end
+ # ^^^^^^^^^^^^^^^^^^
+ #
+ # bar while foo
+ # ^^^^^^^^^^^^^
+ def visit_while_node(node)
+ s(node, :while, visit(node.predicate), visit(node.statements), !node.begin_modifier?)
+ end
+
+ # `foo`
+ # ^^^^^
+ def visit_x_string_node(node)
+ result = s(node, :xstr, node.unescaped)
+
+ if node.heredoc?
+ result.line = node.content_loc.start_line
+ result.line_max = node.content_loc.end_line
+ end
+
+ result
+ end
+
+ # yield
+ # ^^^^^
+ #
+ # yield 1
+ # ^^^^^^^
+ def visit_yield_node(node)
+ s(node, :yield).concat(visit_all(node.arguments&.arguments || []))
+ end
+
+ private
+
+ # Attach prism comments to the given sexp.
+ def attach_comments(sexp, node)
+ return unless node.comments
+ return if node.comments.empty?
+
+ extra = node.location.start_line - node.comments.last.location.start_line
+ comments = node.comments.map(&:slice)
+ comments.concat([nil] * [0, extra].max)
+ sexp.comments = comments.join("\n")
+ end
+
+ # Create a new compiler with the given options.
+ def copy_compiler(in_def: self.in_def, in_pattern: self.in_pattern)
+ Compiler.new(file, in_def: in_def, in_pattern: in_pattern)
+ end
+
+ # Create a new Sexp object from the given prism node and arguments.
+ def s(node, *arguments)
+ result = Sexp.new(*arguments)
+ result.file = file
+ result.line = node.location.start_line
+ result.line_max = node.location.end_line
+ result
+ end
+
+ # Visit a block node, which will modify the AST by wrapping the given
+ # visited node in an iter node.
+ def visit_block(node, sexp, block)
+ if block.nil?
+ sexp
+ else
+ parameters =
+ case block.parameters
+ when nil, ItParametersNode, NumberedParametersNode
+ 0
+ else
+ visit(block.parameters)
+ end
+
+ if block.body.nil?
+ s(node, :iter, sexp, parameters)
+ else
+ s(node, :iter, sexp, parameters, visit(block.body))
+ end
+ end
+ end
+
+ # Pattern constants get wrapped in another layer of :const.
+ def visit_pattern_constant(node)
+ case node
+ when nil
+ # nothing
+ when ConstantReadNode
+ visit(node)
+ else
+ s(node, :const, visit(node))
+ end
+ end
+
+ # Visit the value of a write, which will be on the right-hand side of
+ # a write operator. Because implicit arrays can have splats, those could
+ # potentially be wrapped in an svalue node.
+ def visit_write_value(node)
+ if node.is_a?(ArrayNode) && node.opening_loc.nil?
+ if node.elements.length == 1 && node.elements.first.is_a?(SplatNode)
+ s(node, :svalue, visit(node.elements.first))
+ else
+ s(node, :svalue, visit(node))
+ end
+ else
+ visit(node)
+ end
+ end
+ end
+
+ private_constant :Compiler
+
+ # Parse the given source and translate it into the seattlerb/ruby_parser
+ # gem's Sexp format.
+ def parse(source, filepath = "(string)")
+ translate(Prism.parse(source, filepath: filepath, partial_script: true), filepath)
+ end
+
+ # Parse the given file and translate it into the seattlerb/ruby_parser
+ # gem's Sexp format.
+ def parse_file(filepath)
+ translate(Prism.parse_file(filepath, partial_script: true), filepath)
+ end
+
+ # Parse the give file and translate it into the
+ # seattlerb/ruby_parser gem's Sexp format. This method is
+ # provided for API compatibility to RubyParser and takes an
+ # optional +timeout+ argument.
+ def process(ruby, file = "(string)", timeout = nil)
+ Timeout.timeout(timeout) { parse(ruby, file) }
+ end
+
+ class << self
+ # Parse the given source and translate it into the seattlerb/ruby_parser
+ # gem's Sexp format.
+ def parse(source, filepath = "(string)")
+ new.parse(source, filepath)
+ end
+
+ # Parse the given file and translate it into the seattlerb/ruby_parser
+ # gem's Sexp format.
+ def parse_file(filepath)
+ new.parse_file(filepath)
+ end
+ end
+
+ private
+
+ # Translate the given parse result and filepath into the
+ # seattlerb/ruby_parser gem's Sexp format.
+ def translate(result, filepath)
+ if result.failure?
+ error = result.errors.first
+ raise ::RubyParser::SyntaxError, "#{filepath}:#{error.location.start_line} :: #{error.message}"
+ end
+
+ result.attach_comments!
+ result.value.accept(Compiler.new(filepath))
+ end
+ end
+ end
+end
diff --git a/lib/profile.rb b/lib/profile.rb
deleted file mode 100644
index 2aeecce908..0000000000
--- a/lib/profile.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'profiler'
-
-RubyVM::InstructionSequence.compile_option = {
- :trace_instruction => true,
- :specialized_instruction => false
-}
-END {
- Profiler__::print_profile(STDERR)
-}
-Profiler__::start_profile
diff --git a/lib/profiler.rb b/lib/profiler.rb
deleted file mode 100644
index a4b8889093..0000000000
--- a/lib/profiler.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-module Profiler__
- # internal values
- @@start = @@stack = @@map = nil
- PROFILE_PROC = proc{|event, file, line, id, binding, klass|
- case event
- when "call", "c-call"
- now = Process.times[0]
- @@stack.push [now, 0.0]
- when "return", "c-return"
- now = Process.times[0]
- key = [klass, id]
- if tick = @@stack.pop
- data = (@@map[key] ||= [0, 0.0, 0.0, key])
- data[0] += 1
- cost = now - tick[0]
- data[1] += cost
- data[2] += cost - tick[1]
- @@stack[-1][1] += cost if @@stack[-1]
- end
- end
- }
-module_function
- def start_profile
- @@start = Process.times[0]
- @@stack = []
- @@map = {}
- set_trace_func PROFILE_PROC
- end
- def stop_profile
- set_trace_func nil
- end
- def print_profile(f)
- stop_profile
- total = Process.times[0] - @@start
- if total == 0 then total = 0.01 end
- data = @@map.values
- data = data.sort_by{|x| -x[2]}
- sum = 0
- f.printf " %% cumulative self self total\n"
- f.printf " time seconds seconds calls ms/call ms/call name\n"
- for d in data
- sum += d[2]
- f.printf "%6.2f %8.2f %8.2f %8d ", d[2]/total*100, sum, d[2], d[0]
- f.printf "%8.2f %8.2f %s\n", d[2]*1000/d[0], d[1]*1000/d[0], get_name(*d[3])
- end
- f.printf "%6.2f %8.2f %8.2f %8d ", 0.0, total, 0.0, 1 # ???
- f.printf "%8.2f %8.2f %s\n", 0.0, total*1000, "#toplevel" # ???
- end
- def get_name(klass, id)
- name = klass.to_s || ""
- if klass.kind_of? Class
- name += "#"
- else
- name += "."
- end
- name + id.id2name
- end
- private :get_name
-end
diff --git a/lib/pstore.rb b/lib/pstore.rb
deleted file mode 100644
index fdc518eaec..0000000000
--- a/lib/pstore.rb
+++ /dev/null
@@ -1,543 +0,0 @@
-# = PStore -- Transactional File Storage for Ruby Objects
-#
-# pstore.rb -
-# originally by matz
-# documentation by Kev Jackson and James Edward Gray II
-# improved by Hongli Lai
-#
-# See PStore for documentation.
-
-
-require "fileutils"
-require "digest/md5"
-require "thread"
-
-#
-# PStore implements a file based persistence mechanism based on a Hash. User
-# code can store hierarchies of Ruby objects (values) into the data store file
-# by name (keys). An object hierarchy may be just a single object. User code
-# may later read values back from the data store or even update data, as needed.
-#
-# The transactional behavior ensures that any changes succeed or fail together.
-# This can be used to ensure that the data store is not left in a transitory
-# state, where some values were updated but others were not.
-#
-# Behind the scenes, Ruby objects are stored to the data store file with
-# Marshal. That carries the usual limitations. Proc objects cannot be
-# marshalled, for example.
-#
-# == Usage example:
-#
-# require "pstore"
-#
-# # a mock wiki object...
-# class WikiPage
-# def initialize( page_name, author, contents )
-# @page_name = page_name
-# @revisions = Array.new
-#
-# add_revision(author, contents)
-# end
-#
-# attr_reader :page_name
-#
-# def add_revision( author, contents )
-# @revisions << { :created => Time.now,
-# :author => author,
-# :contents => contents }
-# end
-#
-# def wiki_page_references
-# [@page_name] + @revisions.last[:contents].scan(/\b(?:[A-Z]+[a-z]+){2,}/)
-# end
-#
-# # ...
-# end
-#
-# # create a new page...
-# home_page = WikiPage.new( "HomePage", "James Edward Gray II",
-# "A page about the JoysOfDocumentation..." )
-#
-# # then we want to update page data and the index together, or not at all...
-# wiki = PStore.new("wiki_pages.pstore")
-# wiki.transaction do # begin transaction; do all of this or none of it
-# # store page...
-# wiki[home_page.page_name] = home_page
-# # ensure that an index has been created...
-# wiki[:wiki_index] ||= Array.new
-# # update wiki index...
-# wiki[:wiki_index].push(*home_page.wiki_page_references)
-# end # commit changes to wiki data store file
-#
-# ### Some time later... ###
-#
-# # read wiki data...
-# wiki.transaction(true) do # begin read-only transaction, no changes allowed
-# wiki.roots.each do |data_root_name|
-# p data_root_name
-# p wiki[data_root_name]
-# end
-# end
-#
-# == Transaction modes
-#
-# By default, file integrity is only ensured as long as the operating system
-# (and the underlying hardware) doesn't raise any unexpected I/O errors. If an
-# I/O error occurs while PStore is writing to its file, then the file will
-# become corrupted.
-#
-# You can prevent this by setting <em>pstore.ultra_safe = true</em>.
-# However, this results in a minor performance loss, and only works on platforms
-# that support atomic file renames. Please consult the documentation for
-# +ultra_safe+ for details.
-#
-# Needless to say, if you're storing valuable data with PStore, then you should
-# backup the PStore files from time to time.
-class PStore
- binmode = defined?(File::BINARY) ? File::BINARY : 0
- RDWR_ACCESS = File::RDWR | File::CREAT | binmode
- RD_ACCESS = File::RDONLY | binmode
- WR_ACCESS = File::WRONLY | File::CREAT | File::TRUNC | binmode
-
- # The error type thrown by all PStore methods.
- class Error < StandardError
- end
-
- # Whether PStore should do its best to prevent file corruptions, even when under
- # unlikely-to-occur error conditions such as out-of-space conditions and other
- # unusual OS filesystem errors. Setting this flag comes at the price in the form
- # of a performance loss.
- #
- # This flag only has effect on platforms on which file renames are atomic (e.g.
- # all POSIX platforms: Linux, MacOS X, FreeBSD, etc). The default value is false.
- attr_accessor :ultra_safe
-
- #
- # To construct a PStore object, pass in the _file_ path where you would like
- # the data to be stored.
- #
- # PStore objects are always reentrant. But if _thread_safe_ is set to true,
- # then it will become thread-safe at the cost of a minor performance hit.
- #
- def initialize(file, thread_safe = false)
- dir = File::dirname(file)
- unless File::directory? dir
- raise PStore::Error, format("directory %s does not exist", dir)
- end
- if File::exist? file and not File::readable? file
- raise PStore::Error, format("file %s not readable", file)
- end
- @transaction = false
- @filename = file
- @abort = false
- @ultra_safe = false
- if @thread_safe
- @lock = Mutex.new
- else
- @lock = DummyMutex.new
- end
- end
-
- # Raises PStore::Error if the calling code is not in a PStore#transaction.
- def in_transaction
- raise PStore::Error, "not in transaction" unless @transaction
- end
- #
- # Raises PStore::Error if the calling code is not in a PStore#transaction or
- # if the code is in a read-only PStore#transaction.
- #
- def in_transaction_wr()
- in_transaction()
- raise PStore::Error, "in read-only transaction" if @rdonly
- end
- private :in_transaction, :in_transaction_wr
-
- #
- # Retrieves a value from the PStore file data, by _name_. The hierarchy of
- # Ruby objects stored under that root _name_ will be returned.
- #
- # *WARNING*: This method is only valid in a PStore#transaction. It will
- # raise PStore::Error if called at any other time.
- #
- def [](name)
- in_transaction
- @table[name]
- end
- #
- # This method is just like PStore#[], save that you may also provide a
- # _default_ value for the object. In the event the specified _name_ is not
- # found in the data store, your _default_ will be returned instead. If you do
- # not specify a default, PStore::Error will be raised if the object is not
- # found.
- #
- # *WARNING*: This method is only valid in a PStore#transaction. It will
- # raise PStore::Error if called at any other time.
- #
- def fetch(name, default=PStore::Error)
- in_transaction
- unless @table.key? name
- if default == PStore::Error
- raise PStore::Error, format("undefined root name `%s'", name)
- else
- return default
- end
- end
- @table[name]
- end
- #
- # Stores an individual Ruby object or a hierarchy of Ruby objects in the data
- # store file under the root _name_. Assigning to a _name_ already in the data
- # store clobbers the old data.
- #
- # == Example:
- #
- # require "pstore"
- #
- # store = PStore.new("data_file.pstore")
- # store.transaction do # begin transaction
- # # load some data into the store...
- # store[:single_object] = "My data..."
- # store[:obj_heirarchy] = { "Kev Jackson" => ["rational.rb", "pstore.rb"],
- # "James Gray" => ["erb.rb", "pstore.rb"] }
- # end # commit changes to data store file
- #
- # *WARNING*: This method is only valid in a PStore#transaction and it cannot
- # be read-only. It will raise PStore::Error if called at any other time.
- #
- def []=(name, value)
- in_transaction_wr()
- @table[name] = value
- end
- #
- # Removes an object hierarchy from the data store, by _name_.
- #
- # *WARNING*: This method is only valid in a PStore#transaction and it cannot
- # be read-only. It will raise PStore::Error if called at any other time.
- #
- def delete(name)
- in_transaction_wr()
- @table.delete name
- end
-
- #
- # Returns the names of all object hierarchies currently in the store.
- #
- # *WARNING*: This method is only valid in a PStore#transaction. It will
- # raise PStore::Error if called at any other time.
- #
- def roots
- in_transaction
- @table.keys
- end
- #
- # Returns true if the supplied _name_ is currently in the data store.
- #
- # *WARNING*: This method is only valid in a PStore#transaction. It will
- # raise PStore::Error if called at any other time.
- #
- def root?(name)
- in_transaction
- @table.key? name
- end
- # Returns the path to the data store file.
- def path
- @filename
- end
-
- #
- # Ends the current PStore#transaction, committing any changes to the data
- # store immediately.
- #
- # == Example:
- #
- # require "pstore"
- #
- # store = PStore.new("data_file.pstore")
- # store.transaction do # begin transaction
- # # load some data into the store...
- # store[:one] = 1
- # store[:two] = 2
- #
- # store.commit # end transaction here, committing changes
- #
- # store[:three] = 3 # this change is never reached
- # end
- #
- # *WARNING*: This method is only valid in a PStore#transaction. It will
- # raise PStore::Error if called at any other time.
- #
- def commit
- in_transaction
- @abort = false
- throw :pstore_abort_transaction
- end
- #
- # Ends the current PStore#transaction, discarding any changes to the data
- # store.
- #
- # == Example:
- #
- # require "pstore"
- #
- # store = PStore.new("data_file.pstore")
- # store.transaction do # begin transaction
- # store[:one] = 1 # this change is not applied, see below...
- # store[:two] = 2 # this change is not applied, see below...
- #
- # store.abort # end transaction here, discard all changes
- #
- # store[:three] = 3 # this change is never reached
- # end
- #
- # *WARNING*: This method is only valid in a PStore#transaction. It will
- # raise PStore::Error if called at any other time.
- #
- def abort
- in_transaction
- @abort = true
- throw :pstore_abort_transaction
- end
-
- #
- # Opens a new transaction for the data store. Code executed inside a block
- # passed to this method may read and write data to and from the data store
- # file.
- #
- # At the end of the block, changes are committed to the data store
- # automatically. You may exit the transaction early with a call to either
- # PStore#commit or PStore#abort. See those methods for details about how
- # changes are handled. Raising an uncaught Exception in the block is
- # equivalent to calling PStore#abort.
- #
- # If _read_only_ is set to +true+, you will only be allowed to read from the
- # data store during the transaction and any attempts to change the data will
- # raise a PStore::Error.
- #
- # Note that PStore does not support nested transactions.
- #
- def transaction(read_only = false, &block) # :yields: pstore
- value = nil
- raise PStore::Error, "nested transaction" if @transaction
- @lock.synchronize do
- @rdonly = read_only
- @transaction = true
- @abort = false
- file = open_and_lock_file(@filename, read_only)
- if file
- begin
- @table, checksum, original_data_size = load_data(file, read_only)
-
- catch(:pstore_abort_transaction) do
- value = yield(self)
- end
-
- if !@abort && !read_only
- save_data(checksum, original_data_size, file)
- end
- ensure
- file.close if !file.closed?
- end
- else
- # This can only occur if read_only == true.
- @table = {}
- catch(:pstore_abort_transaction) do
- value = yield(self)
- end
- end
- end
- value
- ensure
- @transaction = false
- end
-
- private
- # Constant for relieving Ruby's garbage collector.
- EMPTY_STRING = ""
- EMPTY_MARSHAL_DATA = Marshal.dump({})
- EMPTY_MARSHAL_CHECKSUM = Digest::MD5.digest(EMPTY_MARSHAL_DATA)
-
- class DummyMutex
- def synchronize
- yield
- end
- end
-
- #
- # Open the specified filename (either in read-only mode or in
- # read-write mode) and lock it for reading or writing.
- #
- # The opened File object will be returned. If _read_only_ is true,
- # and the file does not exist, then nil will be returned.
- #
- # All exceptions are propagated.
- #
- def open_and_lock_file(filename, read_only)
- if read_only
- begin
- file = File.new(filename, RD_ACCESS)
- begin
- file.flock(File::LOCK_SH)
- return file
- rescue
- file.close
- raise
- end
- rescue Errno::ENOENT
- return nil
- end
- else
- file = File.new(filename, RDWR_ACCESS)
- file.flock(File::LOCK_EX)
- return file
- end
- end
-
- # Load the given PStore file.
- # If +read_only+ is true, the unmarshalled Hash will be returned.
- # If +read_only+ is false, a 3-tuple will be returned: the unmarshalled
- # Hash, an MD5 checksum of the data, and the size of the data.
- def load_data(file, read_only)
- if read_only
- begin
- table = load(file)
- if !table.is_a?(Hash)
- raise Error, "PStore file seems to be corrupted."
- end
- rescue EOFError
- # This seems to be a newly-created file.
- table = {}
- end
- table
- else
- data = file.read
- if data.empty?
- # This seems to be a newly-created file.
- table = {}
- checksum = empty_marshal_checksum
- size = empty_marshal_data.size
- else
- table = load(data)
- checksum = Digest::MD5.digest(data)
- size = data.size
- if !table.is_a?(Hash)
- raise Error, "PStore file seems to be corrupted."
- end
- end
- data.replace(EMPTY_STRING)
- [table, checksum, size]
- end
- end
-
- def on_windows?
- is_windows = RUBY_PLATFORM =~ /mswin/ ||
- RUBY_PLATFORM =~ /mingw/ ||
- RUBY_PLATFORM =~ /bbcwin/ ||
- RUBY_PLATFORM =~ /wince/
- self.class.__send__(:define_method, :on_windows?) do
- is_windows
- end
- is_windows
- end
-
- # Check whether Marshal.dump supports the 'canonical' option. This option
- # makes sure that Marshal.dump always dumps data structures in the same order.
- # This is important because otherwise, the checksums that we generate may differ.
- def marshal_dump_supports_canonical_option?
- begin
- Marshal.dump(nil, -1, true)
- result = true
- rescue
- result = false
- end
- self.class.__send__(:define_method, :marshal_dump_supports_canonical_option?) do
- result
- end
- result
- end
-
- def save_data(original_checksum, original_file_size, file)
- # We only want to save the new data if the size or checksum has changed.
- # This results in less filesystem calls, which is good for performance.
- if marshal_dump_supports_canonical_option?
- new_data = Marshal.dump(@table, -1, true)
- else
- new_data = dump(@table)
- end
- new_checksum = Digest::MD5.digest(new_data)
-
- if new_data.size != original_file_size || new_checksum != original_checksum
- if @ultra_safe && !on_windows?
- # Windows doesn't support atomic file renames.
- save_data_with_atomic_file_rename_strategy(new_data, file)
- else
- save_data_with_fast_strategy(new_data, file)
- end
- end
-
- new_data.replace(EMPTY_STRING)
- end
-
- def save_data_with_atomic_file_rename_strategy(data, file)
- temp_filename = "#{@filename}.tmp.#{Process.pid}.#{rand 1000000}"
- temp_file = File.new(temp_filename, WR_ACCESS)
- begin
- temp_file.flock(File::LOCK_EX)
- temp_file.write(data)
- temp_file.flush
- File.rename(temp_filename, @filename)
- rescue
- File.unlink(temp_file) rescue nil
- raise
- ensure
- temp_file.close
- end
- end
-
- def save_data_with_fast_strategy(data, file)
- file.rewind
- file.truncate(0)
- file.write(data)
- end
-
-
- # This method is just a wrapped around Marshal.dump
- # to allow subclass overriding used in YAML::Store.
- def dump(table) # :nodoc:
- Marshal::dump(table)
- end
-
- # This method is just a wrapped around Marshal.load.
- # to allow subclass overriding used in YAML::Store.
- def load(content) # :nodoc:
- Marshal::load(content)
- end
-
- def empty_marshal_data
- EMPTY_MARSHAL_DATA
- end
- def empty_marshal_checksum
- EMPTY_MARSHAL_CHECKSUM
- end
-end
-
-# :enddoc:
-
-if __FILE__ == $0
- db = PStore.new("/tmp/foo")
- db.transaction do
- p db.roots
- ary = db["root"] = [1,2,3,4]
- ary[1] = [1,1.5]
- end
-
- 1000.times do
- db.transaction do
- db["root"][0] += 1
- p db["root"][0]
- end
- end
-
- db.transaction(true) do
- p db["root"]
- end
-end
diff --git a/lib/racc/parser.rb b/lib/racc/parser.rb
deleted file mode 100644
index e87a250e56..0000000000
--- a/lib/racc/parser.rb
+++ /dev/null
@@ -1,441 +0,0 @@
-#
-# $originalId: parser.rb,v 1.8 2006/07/06 11:42:07 aamine Exp $
-#
-# Copyright (c) 1999-2006 Minero Aoki
-#
-# This program is free software.
-# You can distribute/modify this program under the same terms of ruby.
-#
-# As a special exception, when this code is copied by Racc
-# into a Racc output file, you may use that output file
-# without restriction.
-#
-
-unless defined?(NotImplementedError)
- NotImplementedError = NotImplementError
-end
-
-module Racc
- class ParseError < StandardError; end
-end
-unless defined?(::ParseError)
- ParseError = Racc::ParseError
-end
-
-module Racc
-
- unless defined?(Racc_No_Extentions)
- Racc_No_Extentions = false
- end
-
- class Parser
-
- Racc_Runtime_Version = '1.4.5'
- Racc_Runtime_Revision = '$originalRevision: 1.8 $'.split[1]
-
- Racc_Runtime_Core_Version_R = '1.4.5'
- Racc_Runtime_Core_Revision_R = '$originalRevision: 1.8 $'.split[1]
- begin
- require 'racc/cparse'
- # Racc_Runtime_Core_Version_C = (defined in extention)
- Racc_Runtime_Core_Revision_C = Racc_Runtime_Core_Id_C.split[2]
- unless new.respond_to?(:_racc_do_parse_c, true)
- raise LoadError, 'old cparse.so'
- end
- if Racc_No_Extentions
- raise LoadError, 'selecting ruby version of racc runtime core'
- end
-
- Racc_Main_Parsing_Routine = :_racc_do_parse_c
- Racc_YY_Parse_Method = :_racc_yyparse_c
- Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_C
- Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_C
- Racc_Runtime_Type = 'c'
- rescue LoadError
- Racc_Main_Parsing_Routine = :_racc_do_parse_rb
- Racc_YY_Parse_Method = :_racc_yyparse_rb
- Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_R
- Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_R
- Racc_Runtime_Type = 'ruby'
- end
-
- def Parser.racc_runtime_type
- Racc_Runtime_Type
- end
-
- private
-
- def _racc_setup
- @yydebug = false unless self.class::Racc_debug_parser
- @yydebug = false unless defined?(@yydebug)
- if @yydebug
- @racc_debug_out = $stderr unless defined?(@racc_debug_out)
- @racc_debug_out ||= $stderr
- end
- arg = self.class::Racc_arg
- arg[13] = true if arg.size < 14
- arg
- end
-
- def _racc_init_sysvars
- @racc_state = [0]
- @racc_tstack = []
- @racc_vstack = []
-
- @racc_t = nil
- @racc_val = nil
-
- @racc_read_next = true
-
- @racc_user_yyerror = false
- @racc_error_status = 0
- end
-
- ###
- ### do_parse
- ###
-
- def do_parse
- __send__(Racc_Main_Parsing_Routine, _racc_setup(), false)
- end
-
- def next_token
- raise NotImplementedError, "#{self.class}\#next_token is not defined"
- end
-
- def _racc_do_parse_rb(arg, in_debug)
- action_table, action_check, action_default, action_pointer,
- goto_table, goto_check, goto_default, goto_pointer,
- nt_base, reduce_table, token_table, shift_n,
- reduce_n, use_result, * = arg
-
- _racc_init_sysvars
- tok = act = i = nil
- nerr = 0
-
- catch(:racc_end_parse) {
- while true
- if i = action_pointer[@racc_state[-1]]
- if @racc_read_next
- if @racc_t != 0 # not EOF
- tok, @racc_val = next_token()
- unless tok # EOF
- @racc_t = 0
- else
- @racc_t = (token_table[tok] or 1) # error token
- end
- racc_read_token(@racc_t, tok, @racc_val) if @yydebug
- @racc_read_next = false
- end
- end
- i += @racc_t
- unless i >= 0 and
- act = action_table[i] and
- action_check[i] == @racc_state[-1]
- act = action_default[@racc_state[-1]]
- end
- else
- act = action_default[@racc_state[-1]]
- end
- while act = _racc_evalact(act, arg)
- ;
- end
- end
- }
- end
-
- ###
- ### yyparse
- ###
-
- def yyparse(recv, mid)
- __send__(Racc_YY_Parse_Method, recv, mid, _racc_setup(), true)
- end
-
- def _racc_yyparse_rb(recv, mid, arg, c_debug)
- action_table, action_check, action_default, action_pointer,
- goto_table, goto_check, goto_default, goto_pointer,
- nt_base, reduce_table, token_table, shift_n,
- reduce_n, use_result, * = arg
-
- _racc_init_sysvars
- act = nil
- i = nil
- nerr = 0
-
- catch(:racc_end_parse) {
- until i = action_pointer[@racc_state[-1]]
- while act = _racc_evalact(action_default[@racc_state[-1]], arg)
- ;
- end
- end
- recv.__send__(mid) do |tok, val|
- unless tok
- @racc_t = 0
- else
- @racc_t = (token_table[tok] or 1) # error token
- end
- @racc_val = val
- @racc_read_next = false
-
- i += @racc_t
- unless i >= 0 and
- act = action_table[i] and
- action_check[i] == @racc_state[-1]
- act = action_default[@racc_state[-1]]
- end
- while act = _racc_evalact(act, arg)
- ;
- end
-
- while not(i = action_pointer[@racc_state[-1]]) or
- not @racc_read_next or
- @racc_t == 0 # $
- unless i and i += @racc_t and
- i >= 0 and
- act = action_table[i] and
- action_check[i] == @racc_state[-1]
- act = action_default[@racc_state[-1]]
- end
- while act = _racc_evalact(act, arg)
- ;
- end
- end
- end
- }
- end
-
- ###
- ### common
- ###
-
- def _racc_evalact(act, arg)
- action_table, action_check, action_default, action_pointer,
- goto_table, goto_check, goto_default, goto_pointer,
- nt_base, reduce_table, token_table, shift_n,
- reduce_n, use_result, * = arg
- nerr = 0 # tmp
-
- if act > 0 and act < shift_n
- #
- # shift
- #
- if @racc_error_status > 0
- @racc_error_status -= 1 unless @racc_t == 1 # error token
- end
- @racc_vstack.push @racc_val
- @racc_state.push act
- @racc_read_next = true
- if @yydebug
- @racc_tstack.push @racc_t
- racc_shift @racc_t, @racc_tstack, @racc_vstack
- end
-
- elsif act < 0 and act > -reduce_n
- #
- # reduce
- #
- code = catch(:racc_jump) {
- @racc_state.push _racc_do_reduce(arg, act)
- false
- }
- if code
- case code
- when 1 # yyerror
- @racc_user_yyerror = true # user_yyerror
- return -reduce_n
- when 2 # yyaccept
- return shift_n
- else
- raise '[Racc Bug] unknown jump code'
- end
- end
-
- elsif act == shift_n
- #
- # accept
- #
- racc_accept if @yydebug
- throw :racc_end_parse, @racc_vstack[0]
-
- elsif act == -reduce_n
- #
- # error
- #
- case @racc_error_status
- when 0
- unless arg[21] # user_yyerror
- nerr += 1
- on_error @racc_t, @racc_val, @racc_vstack
- end
- when 3
- if @racc_t == 0 # is $
- throw :racc_end_parse, nil
- end
- @racc_read_next = true
- end
- @racc_user_yyerror = false
- @racc_error_status = 3
- while true
- if i = action_pointer[@racc_state[-1]]
- i += 1 # error token
- if i >= 0 and
- (act = action_table[i]) and
- action_check[i] == @racc_state[-1]
- break
- end
- end
- throw :racc_end_parse, nil if @racc_state.size <= 1
- @racc_state.pop
- @racc_vstack.pop
- if @yydebug
- @racc_tstack.pop
- racc_e_pop @racc_state, @racc_tstack, @racc_vstack
- end
- end
- return act
-
- else
- raise "[Racc Bug] unknown action #{act.inspect}"
- end
-
- racc_next_state(@racc_state[-1], @racc_state) if @yydebug
-
- nil
- end
-
- def _racc_do_reduce(arg, act)
- action_table, action_check, action_default, action_pointer,
- goto_table, goto_check, goto_default, goto_pointer,
- nt_base, reduce_table, token_table, shift_n,
- reduce_n, use_result, * = arg
- state = @racc_state
- vstack = @racc_vstack
- tstack = @racc_tstack
-
- i = act * -3
- len = reduce_table[i]
- reduce_to = reduce_table[i+1]
- method_id = reduce_table[i+2]
- void_array = []
-
- tmp_t = tstack[-len, len] if @yydebug
- tmp_v = vstack[-len, len]
- tstack[-len, len] = void_array if @yydebug
- vstack[-len, len] = void_array
- state[-len, len] = void_array
-
- # tstack must be updated AFTER method call
- if use_result
- vstack.push __send__(method_id, tmp_v, vstack, tmp_v[0])
- else
- vstack.push __send__(method_id, tmp_v, vstack)
- end
- tstack.push reduce_to
-
- racc_reduce(tmp_t, reduce_to, tstack, vstack) if @yydebug
-
- k1 = reduce_to - nt_base
- if i = goto_pointer[k1]
- i += state[-1]
- if i >= 0 and (curstate = goto_table[i]) and goto_check[i] == k1
- return curstate
- end
- end
- goto_default[k1]
- end
-
- def on_error(t, val, vstack)
- raise ParseError, sprintf("\nparse error on value %s (%s)",
- val.inspect, token_to_str(t) || '?')
- end
-
- def yyerror
- throw :racc_jump, 1
- end
-
- def yyaccept
- throw :racc_jump, 2
- end
-
- def yyerrok
- @racc_error_status = 0
- end
-
- #
- # for debugging output
- #
-
- def racc_read_token(t, tok, val)
- @racc_debug_out.print 'read '
- @racc_debug_out.print tok.inspect, '(', racc_token2str(t), ') '
- @racc_debug_out.puts val.inspect
- @racc_debug_out.puts
- end
-
- def racc_shift(tok, tstack, vstack)
- @racc_debug_out.puts "shift #{racc_token2str tok}"
- racc_print_stacks tstack, vstack
- @racc_debug_out.puts
- end
-
- def racc_reduce(toks, sim, tstack, vstack)
- out = @racc_debug_out
- out.print 'reduce '
- if toks.empty?
- out.print ' <none>'
- else
- toks.each {|t| out.print ' ', racc_token2str(t) }
- end
- out.puts " --> #{racc_token2str(sim)}"
-
- racc_print_stacks tstack, vstack
- @racc_debug_out.puts
- end
-
- def racc_accept
- @racc_debug_out.puts 'accept'
- @racc_debug_out.puts
- end
-
- def racc_e_pop(state, tstack, vstack)
- @racc_debug_out.puts 'error recovering mode: pop token'
- racc_print_states state
- racc_print_stacks tstack, vstack
- @racc_debug_out.puts
- end
-
- def racc_next_state(curstate, state)
- @racc_debug_out.puts "goto #{curstate}"
- racc_print_states state
- @racc_debug_out.puts
- end
-
- def racc_print_stacks(t, v)
- out = @racc_debug_out
- out.print ' ['
- t.each_index do |i|
- out.print ' (', racc_token2str(t[i]), ' ', v[i].inspect, ')'
- end
- out.puts ' ]'
- end
-
- def racc_print_states(s)
- out = @racc_debug_out
- out.print ' ['
- s.each {|st| out.print ' ', st }
- out.puts ' ]'
- end
-
- def racc_token2str(tok)
- self.class::Racc_token_to_s_table[tok] or
- raise "[Racc Bug] can't convert token #{tok} to string"
- end
-
- def token_to_str(t)
- self.class::Racc_token_to_s_table[t]
- end
-
- end
-
-end
diff --git a/lib/rake.rb b/lib/rake.rb
deleted file mode 100755
index a0685b4ab2..0000000000
--- a/lib/rake.rb
+++ /dev/null
@@ -1,2465 +0,0 @@
-#!/usr/bin/env ruby
-
-#--
-
-# Copyright (c) 2003, 2004, 2005, 2006, 2007 Jim Weirich
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-#++
-#
-# = Rake -- Ruby Make
-#
-# This is the main file for the Rake application. Normally it is referenced
-# as a library via a require statement, but it can be distributed
-# independently as an application.
-
-RAKEVERSION = '0.8.3'
-
-require 'rbconfig'
-require 'fileutils'
-require 'singleton'
-require 'monitor'
-require 'optparse'
-require 'ostruct'
-
-require 'rake/win32'
-
-######################################################################
-# Rake extensions to Module.
-#
-class Module
- # Check for an existing method in the current class before extending. IF
- # the method already exists, then a warning is printed and the extension is
- # not added. Otherwise the block is yielded and any definitions in the
- # block will take effect.
- #
- # Usage:
- #
- # class String
- # rake_extension("xyz") do
- # def xyz
- # ...
- # end
- # end
- # end
- #
- def rake_extension(method)
- if method_defined?(method)
- $stderr.puts "WARNING: Possible conflict with Rake extension: #{self}##{method} already exists"
- else
- yield
- end
- end
-end # module Module
-
-
-######################################################################
-# User defined methods to be added to String.
-#
-class String
- rake_extension("ext") do
- # Replace the file extension with +newext+. If there is no extenson on
- # the string, append the new extension to the end. If the new extension
- # is not given, or is the empty string, remove any existing extension.
- #
- # +ext+ is a user added method for the String class.
- def ext(newext='')
- return self.dup if ['.', '..'].include? self
- if newext != ''
- newext = (newext =~ /^\./) ? newext : ("." + newext)
- end
- self.chomp(File.extname(self)) << newext
- end
- end
-
- rake_extension("pathmap") do
- # Explode a path into individual components. Used by +pathmap+.
- def pathmap_explode
- head, tail = File.split(self)
- return [self] if head == self
- return [tail] if head == '.' || tail == '/'
- return [head, tail] if head == '/'
- return head.pathmap_explode + [tail]
- end
- protected :pathmap_explode
-
- # Extract a partial path from the path. Include +n+ directories from the
- # front end (left hand side) if +n+ is positive. Include |+n+|
- # directories from the back end (right hand side) if +n+ is negative.
- def pathmap_partial(n)
- dirs = File.dirname(self).pathmap_explode
- partial_dirs =
- if n > 0
- dirs[0...n]
- elsif n < 0
- dirs.reverse[0...-n].reverse
- else
- "."
- end
- File.join(partial_dirs)
- end
- protected :pathmap_partial
-
- # Preform the pathmap replacement operations on the given path. The
- # patterns take the form 'pat1,rep1;pat2,rep2...'.
- def pathmap_replace(patterns, &block)
- result = self
- patterns.split(';').each do |pair|
- pattern, replacement = pair.split(',')
- pattern = Regexp.new(pattern)
- if replacement == '*' && block_given?
- result = result.sub(pattern, &block)
- elsif replacement
- result = result.sub(pattern, replacement)
- else
- result = result.sub(pattern, '')
- end
- end
- result
- end
- protected :pathmap_replace
-
- # Map the path according to the given specification. The specification
- # controls the details of the mapping. The following special patterns are
- # recognized:
- #
- # * <b>%p</b> -- The complete path.
- # * <b>%f</b> -- The base file name of the path, with its file extension,
- # but without any directories.
- # * <b>%n</b> -- The file name of the path without its file extension.
- # * <b>%d</b> -- The directory list of the path.
- # * <b>%x</b> -- The file extension of the path. An empty string if there
- # is no extension.
- # * <b>%X</b> -- Everything *but* the file extension.
- # * <b>%s</b> -- The alternate file separater if defined, otherwise use
- # the standard file separator.
- # * <b>%%</b> -- A percent sign.
- #
- # The %d specifier can also have a numeric prefix (e.g. '%2d'). If the
- # number is positive, only return (up to) +n+ directories in the path,
- # starting from the left hand side. If +n+ is negative, return (up to)
- # |+n+| directories from the right hand side of the path.
- #
- # Examples:
- #
- # 'a/b/c/d/file.txt'.pathmap("%2d") => 'a/b'
- # 'a/b/c/d/file.txt'.pathmap("%-2d") => 'c/d'
- #
- # Also the %d, %p, %f, %n, %x, and %X operators can take a
- # pattern/replacement argument to perform simple string substititions on a
- # particular part of the path. The pattern and replacement are speparated
- # by a comma and are enclosed by curly braces. The replacement spec comes
- # after the % character but before the operator letter. (e.g.
- # "%{old,new}d"). Muliple replacement specs should be separated by
- # semi-colons (e.g. "%{old,new;src,bin}d").
- #
- # Regular expressions may be used for the pattern, and back refs may be
- # used in the replacement text. Curly braces, commas and semi-colons are
- # excluded from both the pattern and replacement text (let's keep parsing
- # reasonable).
- #
- # For example:
- #
- # "src/org/onestepback/proj/A.java".pathmap("%{^src,bin}X.class")
- #
- # returns:
- #
- # "bin/org/onestepback/proj/A.class"
- #
- # If the replacement text is '*', then a block may be provided to perform
- # some arbitrary calculation for the replacement.
- #
- # For example:
- #
- # "/path/to/file.TXT".pathmap("%X%{.*,*}x") { |ext|
- # ext.downcase
- # }
- #
- # Returns:
- #
- # "/path/to/file.txt"
- #
- def pathmap(spec=nil, &block)
- return self if spec.nil?
- result = ''
- spec.scan(/%\{[^}]*\}-?\d*[sdpfnxX%]|%-?\d+d|%.|[^%]+/) do |frag|
- case frag
- when '%f'
- result << File.basename(self)
- when '%n'
- result << File.basename(self, '.*')
- when '%d'
- result << File.dirname(self)
- when '%x'
- result << File.extname(self)
- when '%X'
- result << self.ext
- when '%p'
- result << self
- when '%s'
- result << (File::ALT_SEPARATOR || File::SEPARATOR)
- when '%-'
- # do nothing
- when '%%'
- result << "%"
- when /%(-?\d+)d/
- result << pathmap_partial($1.to_i)
- when /^%\{([^}]*)\}(\d*[dpfnxX])/
- patterns, operator = $1, $2
- result << pathmap('%' + operator).pathmap_replace(patterns, &block)
- when /^%/
- fail ArgumentError, "Unknown pathmap specifier #{frag} in '#{spec}'"
- else
- result << frag
- end
- end
- result
- end
- end
-end # class String
-
-##############################################################################
-module Rake
-
- # Errors -----------------------------------------------------------
-
- # Error indicating an ill-formed task declaration.
- class TaskArgumentError < ArgumentError
- end
-
- # Error indicating a recursion overflow error in task selection.
- class RuleRecursionOverflowError < StandardError
- def initialize(*args)
- super
- @targets = []
- end
-
- def add_target(target)
- @targets << target
- end
-
- def message
- super + ": [" + @targets.reverse.join(' => ') + "]"
- end
- end
-
- # --------------------------------------------------------------------------
- # Rake module singleton methods.
- #
- class << self
- # Current Rake Application
- def application
- @application ||= Rake::Application.new
- end
-
- # Set the current Rake application object.
- def application=(app)
- @application = app
- end
-
- # Return the original directory where the Rake application was started.
- def original_dir
- application.original_dir
- end
-
- end
-
- # ##########################################################################
- # Mixin for creating easily cloned objects.
- #
- module Cloneable
- # Clone an object by making a new object and setting all the instance
- # variables to the same values.
- def dup
- sibling = self.class.new
- instance_variables.each do |ivar|
- value = self.instance_variable_get(ivar)
- new_value = value.clone rescue value
- sibling.instance_variable_set(ivar, new_value)
- end
- sibling.taint if tainted?
- sibling
- end
-
- def clone
- sibling = dup
- sibling.freeze if frozen?
- sibling
- end
- end
-
- ####################################################################
- # TaskAguments manage the arguments passed to a task.
- #
- class TaskArguments
- include Enumerable
-
- attr_reader :names
-
- # Create a TaskArgument object with a list of named arguments
- # (given by :names) and a set of associated values (given by
- # :values). :parent is the parent argument object.
- def initialize(names, values, parent=nil)
- @names = names
- @parent = parent
- @hash = {}
- names.each_with_index { |name, i|
- @hash[name.to_sym] = values[i] unless values[i].nil?
- }
- end
-
- # Create a new argument scope using the prerequisite argument
- # names.
- def new_scope(names)
- values = names.collect { |n| self[n] }
- self.class.new(names, values, self)
- end
-
- # Find an argument value by name or index.
- def [](index)
- lookup(index.to_sym)
- end
-
- # Specify a hash of default values for task arguments. Use the
- # defaults only if there is no specific value for the given
- # argument.
- def with_defaults(defaults)
- @hash = defaults.merge(@hash)
- end
-
- def each(&block)
- @hash.each(&block)
- end
-
- def method_missing(sym, *args, &block)
- lookup(sym.to_sym)
- end
-
- def to_hash
- @hash
- end
-
- def to_s
- @hash.inspect
- end
-
- def inspect
- to_s
- end
-
- protected
-
- def lookup(name)
- if @hash.has_key?(name)
- @hash[name]
- elsif ENV.has_key?(name.to_s)
- ENV[name.to_s]
- elsif ENV.has_key?(name.to_s.upcase)
- ENV[name.to_s.upcase]
- elsif @parent
- @parent.lookup(name)
- end
- end
- end
-
- EMPTY_TASK_ARGS = TaskArguments.new([], [])
-
- ####################################################################
- # InvocationChain tracks the chain of task invocations to detect
- # circular dependencies.
- class InvocationChain
- def initialize(value, tail)
- @value = value
- @tail = tail
- end
-
- def member?(obj)
- @value == obj || @tail.member?(obj)
- end
-
- def append(value)
- if member?(value)
- fail RuntimeError, "Circular dependency detected: #{to_s} => #{value}"
- end
- self.class.new(value, self)
- end
-
- def to_s
- "#{prefix}#{@value}"
- end
-
- def self.append(value, chain)
- chain.append(value)
- end
-
- private
-
- def prefix
- "#{@tail.to_s} => "
- end
-
- class EmptyInvocationChain
- def member?(obj)
- false
- end
- def append(value)
- InvocationChain.new(value, self)
- end
- def to_s
- "TOP"
- end
- end
-
- EMPTY = EmptyInvocationChain.new
-
- end # class InvocationChain
-
-end # module Rake
-
-module Rake
-
- # #########################################################################
- # A Task is the basic unit of work in a Rakefile. Tasks have associated
- # actions (possibly more than one) and a list of prerequisites. When
- # invoked, a task will first ensure that all of its prerequisites have an
- # opportunity to run and then it will execute its own actions.
- #
- # Tasks are not usually created directly using the new method, but rather
- # use the +file+ and +task+ convenience methods.
- #
- class Task
- # List of prerequisites for a task.
- attr_reader :prerequisites
-
- # List of actions attached to a task.
- attr_reader :actions
-
- # Application owning this task.
- attr_accessor :application
-
- # Comment for this task. Restricted to a single line of no more than 50
- # characters.
- attr_reader :comment
-
- # Full text of the (possibly multi-line) comment.
- attr_reader :full_comment
-
- # Array of nested namespaces names used for task lookup by this task.
- attr_reader :scope
-
- # Return task name
- def to_s
- name
- end
-
- def inspect
- "<#{self.class} #{name} => [#{prerequisites.join(', ')}]>"
- end
-
- # List of sources for task.
- attr_writer :sources
- def sources
- @sources ||= []
- end
-
- # First source from a rule (nil if no sources)
- def source
- @sources.first if defined?(@sources)
- end
-
- # Create a task named +task_name+ with no actions or prerequisites. Use
- # +enhance+ to add actions and prerequisites.
- def initialize(task_name, app)
- @name = task_name.to_s
- @prerequisites = []
- @actions = []
- @already_invoked = false
- @full_comment = nil
- @comment = nil
- @lock = Monitor.new
- @application = app
- @scope = app.current_scope
- @arg_names = nil
- end
-
- # Enhance a task with prerequisites or actions. Returns self.
- def enhance(deps=nil, &block)
- @prerequisites |= deps if deps
- @actions << block if block_given?
- self
- end
-
- # Name of the task, including any namespace qualifiers.
- def name
- @name.to_s
- end
-
- # Name of task with argument list description.
- def name_with_args # :nodoc:
- if arg_description
- "#{name}#{arg_description}"
- else
- name
- end
- end
-
- # Argument description (nil if none).
- def arg_description # :nodoc:
- @arg_names ? "[#{(arg_names || []).join(',')}]" : nil
- end
-
- # Name of arguments for this task.
- def arg_names
- @arg_names || []
- end
-
- # Reenable the task, allowing its tasks to be executed if the task
- # is invoked again.
- def reenable
- @already_invoked = false
- end
-
- # Clear the existing prerequisites and actions of a rake task.
- def clear
- clear_prerequisites
- clear_actions
- self
- end
-
- # Clear the existing prerequisites of a rake task.
- def clear_prerequisites
- prerequisites.clear
- self
- end
-
- # Clear the existing actions on a rake task.
- def clear_actions
- actions.clear
- self
- end
-
- # Invoke the task if it is needed. Prerequites are invoked first.
- def invoke(*args)
- task_args = TaskArguments.new(arg_names, args)
- invoke_with_call_chain(task_args, InvocationChain::EMPTY)
- end
-
- # Same as invoke, but explicitly pass a call chain to detect
- # circular dependencies.
- def invoke_with_call_chain(task_args, invocation_chain) # :nodoc:
- new_chain = InvocationChain.append(self, invocation_chain)
- @lock.synchronize do
- if application.options.trace
- puts "** Invoke #{name} #{format_trace_flags}"
- end
- return if @already_invoked
- @already_invoked = true
- invoke_prerequisites(task_args, new_chain)
- execute(task_args) if needed?
- end
- end
- protected :invoke_with_call_chain
-
- # Invoke all the prerequisites of a task.
- def invoke_prerequisites(task_args, invocation_chain) # :nodoc:
- @prerequisites.each { |n|
- prereq = application[n, @scope]
- prereq_args = task_args.new_scope(prereq.arg_names)
- prereq.invoke_with_call_chain(prereq_args, invocation_chain)
- }
- end
-
- # Format the trace flags for display.
- def format_trace_flags
- flags = []
- flags << "first_time" unless @already_invoked
- flags << "not_needed" unless needed?
- flags.empty? ? "" : "(" + flags.join(", ") + ")"
- end
- private :format_trace_flags
-
- # Execute the actions associated with this task.
- def execute(args=nil)
- args ||= EMPTY_TASK_ARGS
- if application.options.dryrun
- puts "** Execute (dry run) #{name}"
- return
- end
- if application.options.trace
- puts "** Execute #{name}"
- end
- application.enhance_with_matching_rule(name) if @actions.empty?
- @actions.each do |act|
- case act.arity
- when 1
- act.call(self)
- else
- act.call(self, args)
- end
- end
- end
-
- # Is this task needed?
- def needed?
- true
- end
-
- # Timestamp for this task. Basic tasks return the current time for their
- # time stamp. Other tasks can be more sophisticated.
- def timestamp
- @prerequisites.collect { |p| application[p].timestamp }.max || Time.now
- end
-
- # Add a description to the task. The description can consist of an option
- # argument list (enclosed brackets) and an optional comment.
- def add_description(description)
- return if ! description
- comment = description.strip
- add_comment(comment) if comment && ! comment.empty?
- end
-
- # Writing to the comment attribute is the same as adding a description.
- def comment=(description)
- add_description(description)
- end
-
- # Add a comment to the task. If a comment alread exists, separate
- # the new comment with " / ".
- def add_comment(comment)
- if @full_comment
- @full_comment << " / "
- else
- @full_comment = ''
- end
- @full_comment << comment
- if @full_comment =~ /\A([^.]+?\.)( |$)/
- @comment = $1
- else
- @comment = @full_comment
- end
- end
- private :add_comment
-
- # Set the names of the arguments for this task. +args+ should be
- # an array of symbols, one for each argument name.
- def set_arg_names(args)
- @arg_names = args.map { |a| a.to_sym }
- end
-
- # Return a string describing the internal state of a task. Useful for
- # debugging.
- def investigation
- result = "------------------------------\n"
- result << "Investigating #{name}\n"
- result << "class: #{self.class}\n"
- result << "task needed: #{needed?}\n"
- result << "timestamp: #{timestamp}\n"
- result << "pre-requisites: \n"
- prereqs = @prerequisites.collect {|name| application[name]}
- prereqs.sort! {|a,b| a.timestamp <=> b.timestamp}
- prereqs.each do |p|
- result << "--#{p.name} (#{p.timestamp})\n"
- end
- latest_prereq = @prerequisites.collect{|n| application[n].timestamp}.max
- result << "latest-prerequisite time: #{latest_prereq}\n"
- result << "................................\n\n"
- return result
- end
-
- # ----------------------------------------------------------------
- # Rake Module Methods
- #
- class << self
-
- # Clear the task list. This cause rake to immediately forget all the
- # tasks that have been assigned. (Normally used in the unit tests.)
- def clear
- Rake.application.clear
- end
-
- # List of all defined tasks.
- def tasks
- Rake.application.tasks
- end
-
- # Return a task with the given name. If the task is not currently
- # known, try to synthesize one from the defined rules. If no rules are
- # found, but an existing file matches the task name, assume it is a file
- # task with no dependencies or actions.
- def [](task_name)
- Rake.application[task_name]
- end
-
- # TRUE if the task name is already defined.
- def task_defined?(task_name)
- Rake.application.lookup(task_name) != nil
- end
-
- # Define a task given +args+ and an option block. If a rule with the
- # given name already exists, the prerequisites and actions are added to
- # the existing task. Returns the defined task.
- def define_task(*args, &block)
- Rake.application.define_task(self, *args, &block)
- end
-
- # Define a rule for synthesizing tasks.
- def create_rule(*args, &block)
- Rake.application.create_rule(*args, &block)
- end
-
- # Apply the scope to the task name according to the rules for
- # this kind of task. Generic tasks will accept the scope as
- # part of the name.
- def scope_name(scope, task_name)
- (scope + [task_name]).join(':')
- end
-
- end # class << Rake::Task
- end # class Rake::Task
-
-
- # #########################################################################
- # A FileTask is a task that includes time based dependencies. If any of a
- # FileTask's prerequisites have a timestamp that is later than the file
- # represented by this task, then the file must be rebuilt (using the
- # supplied actions).
- #
- class FileTask < Task
-
- # Is this file task needed? Yes if it doesn't exist, or if its time stamp
- # is out of date.
- def needed?
- return true unless File.exist?(name)
- return true if out_of_date?(timestamp)
- false
- end
-
- # Time stamp for file task.
- def timestamp
- if File.exist?(name)
- File.mtime(name.to_s)
- else
- Rake::EARLY
- end
- end
-
- private
-
- # Are there any prerequisites with a later time than the given time stamp?
- def out_of_date?(stamp)
- @prerequisites.any? { |n| application[n].timestamp > stamp}
- end
-
- # ----------------------------------------------------------------
- # Task class methods.
- #
- class << self
- # Apply the scope to the task name according to the rules for this kind
- # of task. File based tasks ignore the scope when creating the name.
- def scope_name(scope, task_name)
- task_name
- end
- end
- end # class Rake::FileTask
-
- # #########################################################################
- # A FileCreationTask is a file task that when used as a dependency will be
- # needed if and only if the file has not been created. Once created, it is
- # not re-triggered if any of its dependencies are newer, nor does trigger
- # any rebuilds of tasks that depend on it whenever it is updated.
- #
- class FileCreationTask < FileTask
- # Is this file task needed? Yes if it doesn't exist.
- def needed?
- ! File.exist?(name)
- end
-
- # Time stamp for file creation task. This time stamp is earlier
- # than any other time stamp.
- def timestamp
- Rake::EARLY
- end
- end
-
- # #########################################################################
- # Same as a regular task, but the immediate prerequisites are done in
- # parallel using Ruby threads.
- #
- class MultiTask < Task
- private
- def invoke_prerequisites(args, invocation_chain)
- threads = @prerequisites.collect { |p|
- Thread.new(p) { |r| application[r].invoke_with_call_chain(args, invocation_chain) }
- }
- threads.each { |t| t.join }
- end
- end
-end # module Rake
-
-# ###########################################################################
-# Task Definition Functions ...
-
-# Declare a basic task.
-#
-# Example:
-# task :clobber => [:clean] do
-# rm_rf "html"
-# end
-#
-def task(*args, &block)
- Rake::Task.define_task(*args, &block)
-end
-
-
-# Declare a file task.
-#
-# Example:
-# file "config.cfg" => ["config.template"] do
-# open("config.cfg", "w") do |outfile|
-# open("config.template") do |infile|
-# while line = infile.gets
-# outfile.puts line
-# end
-# end
-# end
-# end
-#
-def file(*args, &block)
- Rake::FileTask.define_task(*args, &block)
-end
-
-# Declare a file creation task.
-# (Mainly used for the directory command).
-def file_create(args, &block)
- Rake::FileCreationTask.define_task(args, &block)
-end
-
-# Declare a set of files tasks to create the given directories on demand.
-#
-# Example:
-# directory "testdata/doc"
-#
-def directory(dir)
- Rake.each_dir_parent(dir) do |d|
- file_create d do |t|
- mkdir_p t.name if ! File.exist?(t.name)
- end
- end
-end
-
-# Declare a task that performs its prerequisites in parallel. Multitasks does
-# *not* guarantee that its prerequisites will execute in any given order
-# (which is obvious when you think about it)
-#
-# Example:
-# multitask :deploy => [:deploy_gem, :deploy_rdoc]
-#
-def multitask(args, &block)
- Rake::MultiTask.define_task(args, &block)
-end
-
-# Create a new rake namespace and use it for evaluating the given block.
-# Returns a NameSpace object that can be used to lookup tasks defined in the
-# namespace.
-#
-# E.g.
-#
-# ns = namespace "nested" do
-# task :run
-# end
-# task_run = ns[:run] # find :run in the given namespace.
-#
-def namespace(name=nil, &block)
- Rake.application.in_namespace(name, &block)
-end
-
-# Declare a rule for auto-tasks.
-#
-# Example:
-# rule '.o' => '.c' do |t|
-# sh %{cc -o #{t.name} #{t.source}}
-# end
-#
-def rule(*args, &block)
- Rake::Task.create_rule(*args, &block)
-end
-
-# Describe the next rake task.
-#
-# Example:
-# desc "Run the Unit Tests"
-# task :test => [:build]
-# runtests
-# end
-#
-def desc(description)
- Rake.application.last_description = description
-end
-
-# Import the partial Rakefiles +fn+. Imported files are loaded _after_ the
-# current file is completely loaded. This allows the import statement to
-# appear anywhere in the importing file, and yet allowing the imported files
-# to depend on objects defined in the importing file.
-#
-# A common use of the import statement is to include files containing
-# dependency declarations.
-#
-# See also the --rakelibdir command line option.
-#
-# Example:
-# import ".depend", "my_rules"
-#
-def import(*fns)
- fns.each do |fn|
- Rake.application.add_import(fn)
- end
-end
-
-# ###########################################################################
-# This a FileUtils extension that defines several additional commands to be
-# added to the FileUtils utility functions.
-#
-module FileUtils
- RUBY = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']).
- sub(/.*\s.*/m, '"\&"')
-
- OPT_TABLE['sh'] = %w(noop verbose)
- OPT_TABLE['ruby'] = %w(noop verbose)
-
- # Run the system command +cmd+. If multiple arguments are given the command
- # is not run with the shell (same semantics as Kernel::exec and
- # Kernel::system).
- #
- # Example:
- # sh %{ls -ltr}
- #
- # sh 'ls', 'file with spaces'
- #
- # # check exit status after command runs
- # sh %{grep pattern file} do |ok, res|
- # if ! ok
- # puts "pattern not found (status = #{res.exitstatus})"
- # end
- # end
- #
- def sh(*cmd, &block)
- options = (Hash === cmd.last) ? cmd.pop : {}
- unless block_given?
- show_command = cmd.join(" ")
- show_command = show_command[0,42] + "..."
- # TODO code application logic heref show_command.length > 45
- block = lambda { |ok, status|
- ok or fail "Command failed with status (#{status.exitstatus}): [#{show_command}]"
- }
- end
- if RakeFileUtils.verbose_flag == :default
- options[:verbose] = false
- else
- options[:verbose] ||= RakeFileUtils.verbose_flag
- end
- options[:noop] ||= RakeFileUtils.nowrite_flag
- rake_check_options options, :noop, :verbose
- rake_output_message cmd.join(" ") if options[:verbose]
- unless options[:noop]
- res = rake_system(*cmd)
- block.call(res, $?)
- end
- end
-
- def rake_system(*cmd)
- if Rake::Win32.windows?
- Rake::Win32.rake_system(*cmd)
- else
- system(*cmd)
- end
- end
- private :rake_system
-
- # Run a Ruby interpreter with the given arguments.
- #
- # Example:
- # ruby %{-pe '$_.upcase!' <README}
- #
- def ruby(*args,&block)
- options = (Hash === args.last) ? args.pop : {}
- if args.length > 1 then
- sh(*([RUBY] + args + [options]), &block)
- else
- sh("#{RUBY} #{args.first}", options, &block)
- end
- end
-
- LN_SUPPORTED = [true]
-
- # Attempt to do a normal file link, but fall back to a copy if the link
- # fails.
- def safe_ln(*args)
- unless LN_SUPPORTED[0]
- cp(*args)
- else
- begin
- ln(*args)
- rescue StandardError, NotImplementedError => ex
- LN_SUPPORTED[0] = false
- cp(*args)
- end
- end
- end
-
- # Split a file path into individual directory names.
- #
- # Example:
- # split_all("a/b/c") => ['a', 'b', 'c']
- #
- def split_all(path)
- head, tail = File.split(path)
- return [tail] if head == '.' || tail == '/'
- return [head, tail] if head == '/'
- return split_all(head) + [tail]
- end
-end
-
-# ###########################################################################
-# RakeFileUtils provides a custom version of the FileUtils methods that
-# respond to the <tt>verbose</tt> and <tt>nowrite</tt> commands.
-#
-module RakeFileUtils
- include FileUtils
-
- class << self
- attr_accessor :verbose_flag, :nowrite_flag
- end
- RakeFileUtils.verbose_flag = :default
- RakeFileUtils.nowrite_flag = false
-
- $fileutils_verbose = true
- $fileutils_nowrite = false
-
- FileUtils::OPT_TABLE.each do |name, opts|
- default_options = []
- if opts.include?(:verbose) || opts.include?("verbose")
- default_options << ':verbose => RakeFileUtils.verbose_flag'
- end
- if opts.include?(:noop) || opts.include?("noop")
- default_options << ':noop => RakeFileUtils.nowrite_flag'
- end
-
- next if default_options.empty?
- module_eval(<<-EOS, __FILE__, __LINE__ + 1)
- def #{name}( *args, &block )
- super(
- *rake_merge_option(args,
- #{default_options.join(', ')}
- ), &block)
- end
- EOS
- end
-
- # Get/set the verbose flag controlling output from the FileUtils utilities.
- # If verbose is true, then the utility method is echoed to standard output.
- #
- # Examples:
- # verbose # return the current value of the verbose flag
- # verbose(v) # set the verbose flag to _v_.
- # verbose(v) { code } # Execute code with the verbose flag set temporarily to _v_.
- # # Return to the original value when code is done.
- def verbose(value=nil)
- oldvalue = RakeFileUtils.verbose_flag
- RakeFileUtils.verbose_flag = value unless value.nil?
- if block_given?
- begin
- yield
- ensure
- RakeFileUtils.verbose_flag = oldvalue
- end
- end
- RakeFileUtils.verbose_flag
- end
-
- # Get/set the nowrite flag controlling output from the FileUtils utilities.
- # If verbose is true, then the utility method is echoed to standard output.
- #
- # Examples:
- # nowrite # return the current value of the nowrite flag
- # nowrite(v) # set the nowrite flag to _v_.
- # nowrite(v) { code } # Execute code with the nowrite flag set temporarily to _v_.
- # # Return to the original value when code is done.
- def nowrite(value=nil)
- oldvalue = RakeFileUtils.nowrite_flag
- RakeFileUtils.nowrite_flag = value unless value.nil?
- if block_given?
- begin
- yield
- ensure
- RakeFileUtils.nowrite_flag = oldvalue
- end
- end
- oldvalue
- end
-
- # Use this function to prevent protentially destructive ruby code from
- # running when the :nowrite flag is set.
- #
- # Example:
- #
- # when_writing("Building Project") do
- # project.build
- # end
- #
- # The following code will build the project under normal conditions. If the
- # nowrite(true) flag is set, then the example will print:
- # DRYRUN: Building Project
- # instead of actually building the project.
- #
- def when_writing(msg=nil)
- if RakeFileUtils.nowrite_flag
- puts "DRYRUN: #{msg}" if msg
- else
- yield
- end
- end
-
- # Merge the given options with the default values.
- def rake_merge_option(args, defaults)
- if Hash === args.last
- defaults.update(args.last)
- args.pop
- end
- args.push defaults
- args
- end
- private :rake_merge_option
-
- # Send the message to the default rake output (which is $stderr).
- def rake_output_message(message)
- $stderr.puts(message)
- end
- private :rake_output_message
-
- # Check that the options do not contain options not listed in +optdecl+. An
- # ArgumentError exception is thrown if non-declared options are found.
- def rake_check_options(options, *optdecl)
- h = options.dup
- optdecl.each do |name|
- h.delete name
- end
- raise ArgumentError, "no such option: #{h.keys.join(' ')}" unless h.empty?
- end
- private :rake_check_options
-
- extend self
-end
-
-# ###########################################################################
-# Include the FileUtils file manipulation functions in the top level module,
-# but mark them private so that they don't unintentionally define methods on
-# other objects.
-
-include RakeFileUtils
-private(*FileUtils.instance_methods(false))
-private(*RakeFileUtils.instance_methods(false))
-
-######################################################################
-module Rake
-
- # #########################################################################
- # A FileList is essentially an array with a few helper methods defined to
- # make file manipulation a bit easier.
- #
- # FileLists are lazy. When given a list of glob patterns for possible files
- # to be included in the file list, instead of searching the file structures
- # to find the files, a FileList holds the pattern for latter use.
- #
- # This allows us to define a number of FileList to match any number of
- # files, but only search out the actual files when then FileList itself is
- # actually used. The key is that the first time an element of the
- # FileList/Array is requested, the pending patterns are resolved into a real
- # list of file names.
- #
- class FileList
-
- include Cloneable
-
- # == Method Delegation
- #
- # The lazy evaluation magic of FileLists happens by implementing all the
- # array specific methods to call +resolve+ before delegating the heavy
- # lifting to an embedded array object (@items).
- #
- # In addition, there are two kinds of delegation calls. The regular kind
- # delegates to the @items array and returns the result directly. Well,
- # almost directly. It checks if the returned value is the @items object
- # itself, and if so will return the FileList object instead.
- #
- # The second kind of delegation call is used in methods that normally
- # return a new Array object. We want to capture the return value of these
- # methods and wrap them in a new FileList object. We enumerate these
- # methods in the +SPECIAL_RETURN+ list below.
-
- # List of array methods (that are not in +Object+) that need to be
- # delegated.
- ARRAY_METHODS = (Array.instance_methods - Object.instance_methods).map { |n| n.to_s }
-
- # List of additional methods that must be delegated.
- MUST_DEFINE = %w[to_a inspect]
-
- # List of methods that should not be delegated here (we define special
- # versions of them explicitly below).
- MUST_NOT_DEFINE = %w[to_a to_ary partition *]
-
- # List of delegated methods that return new array values which need
- # wrapping.
- SPECIAL_RETURN = %w[
- map collect sort sort_by select find_all reject grep
- compact flatten uniq values_at
- + - & |
- ]
-
- DELEGATING_METHODS = (ARRAY_METHODS + MUST_DEFINE - MUST_NOT_DEFINE).collect{ |s| s.to_s }.sort.uniq
-
- # Now do the delegation.
- DELEGATING_METHODS.each_with_index do |sym, i|
- if SPECIAL_RETURN.include?(sym)
- ln = __LINE__+1
- class_eval %{
- def #{sym}(*args, &block)
- resolve
- result = @items.send(:#{sym}, *args, &block)
- FileList.new.import(result)
- end
- }, __FILE__, ln
- else
- ln = __LINE__+1
- class_eval %{
- def #{sym}(*args, &block)
- resolve
- result = @items.send(:#{sym}, *args, &block)
- result.object_id == @items.object_id ? self : result
- end
- }, __FILE__, ln
- end
- end
-
- # Create a file list from the globbable patterns given. If you wish to
- # perform multiple includes or excludes at object build time, use the
- # "yield self" pattern.
- #
- # Example:
- # file_list = FileList.new('lib/**/*.rb', 'test/test*.rb')
- #
- # pkg_files = FileList.new('lib/**/*') do |fl|
- # fl.exclude(/\bCVS\b/)
- # end
- #
- def initialize(*patterns)
- @pending_add = []
- @pending = false
- @exclude_patterns = DEFAULT_IGNORE_PATTERNS.dup
- @exclude_procs = DEFAULT_IGNORE_PROCS.dup
- @exclude_re = nil
- @items = []
- patterns.each { |pattern| include(pattern) }
- yield self if block_given?
- end
-
- # Add file names defined by glob patterns to the file list. If an array
- # is given, add each element of the array.
- #
- # Example:
- # file_list.include("*.java", "*.cfg")
- # file_list.include %w( math.c lib.h *.o )
- #
- def include(*filenames)
- # TODO: check for pending
- filenames.each do |fn|
- if fn.respond_to? :to_ary
- include(*fn.to_ary)
- else
- @pending_add << fn
- end
- end
- @pending = true
- self
- end
- alias :add :include
-
- # Register a list of file name patterns that should be excluded from the
- # list. Patterns may be regular expressions, glob patterns or regular
- # strings. In addition, a block given to exclude will remove entries that
- # return true when given to the block.
- #
- # Note that glob patterns are expanded against the file system. If a file
- # is explicitly added to a file list, but does not exist in the file
- # system, then an glob pattern in the exclude list will not exclude the
- # file.
- #
- # Examples:
- # FileList['a.c', 'b.c'].exclude("a.c") => ['b.c']
- # FileList['a.c', 'b.c'].exclude(/^a/) => ['b.c']
- #
- # If "a.c" is a file, then ...
- # FileList['a.c', 'b.c'].exclude("a.*") => ['b.c']
- #
- # If "a.c" is not a file, then ...
- # FileList['a.c', 'b.c'].exclude("a.*") => ['a.c', 'b.c']
- #
- def exclude(*patterns, &block)
- patterns.each do |pat|
- @exclude_patterns << pat
- end
- if block_given?
- @exclude_procs << block
- end
- resolve_exclude if ! @pending
- self
- end
-
-
- # Clear all the exclude patterns so that we exclude nothing.
- def clear_exclude
- @exclude_patterns = []
- @exclude_procs = []
- calculate_exclude_regexp if ! @pending
- self
- end
-
- # Define equality.
- def ==(array)
- to_ary == array
- end
-
- # Return the internal array object.
- def to_a
- resolve
- @items
- end
-
- # Return the internal array object.
- def to_ary
- to_a
- end
-
- # Lie about our class.
- def is_a?(klass)
- klass == Array || super(klass)
- end
- alias kind_of? is_a?
-
- # Redefine * to return either a string or a new file list.
- def *(other)
- result = @items * other
- case result
- when Array
- FileList.new.import(result)
- else
- result
- end
- end
-
- # Resolve all the pending adds now.
- def resolve
- if @pending
- @pending = false
- @pending_add.each do |fn| resolve_add(fn) end
- @pending_add = []
- resolve_exclude
- end
- self
- end
-
- def calculate_exclude_regexp
- ignores = []
- @exclude_patterns.each do |pat|
- case pat
- when Regexp
- ignores << pat
- when /[*?]/
- Dir[pat].each do |p| ignores << p end
- else
- ignores << Regexp.quote(pat)
- end
- end
- if ignores.empty?
- @exclude_re = /^$/
- else
- re_str = ignores.collect { |p| "(" + p.to_s + ")" }.join("|")
- @exclude_re = Regexp.new(re_str)
- end
- end
-
- def resolve_add(fn)
- case fn
- when %r{[*?\[\{]}
- add_matching(fn)
- else
- self << fn
- end
- end
- private :resolve_add
-
- def resolve_exclude
- calculate_exclude_regexp
- reject! { |fn| exclude?(fn) }
- self
- end
- private :resolve_exclude
-
- # Return a new FileList with the results of running +sub+ against each
- # element of the oringal list.
- #
- # Example:
- # FileList['a.c', 'b.c'].sub(/\.c$/, '.o') => ['a.o', 'b.o']
- #
- def sub(pat, rep)
- inject(FileList.new) { |res, fn| res << fn.sub(pat,rep) }
- end
-
- # Return a new FileList with the results of running +gsub+ against each
- # element of the original list.
- #
- # Example:
- # FileList['lib/test/file', 'x/y'].gsub(/\//, "\\")
- # => ['lib\\test\\file', 'x\\y']
- #
- def gsub(pat, rep)
- inject(FileList.new) { |res, fn| res << fn.gsub(pat,rep) }
- end
-
- # Same as +sub+ except that the oringal file list is modified.
- def sub!(pat, rep)
- each_with_index { |fn, i| self[i] = fn.sub(pat,rep) }
- self
- end
-
- # Same as +gsub+ except that the original file list is modified.
- def gsub!(pat, rep)
- each_with_index { |fn, i| self[i] = fn.gsub(pat,rep) }
- self
- end
-
- # Apply the pathmap spec to each of the included file names, returning a
- # new file list with the modified paths. (See String#pathmap for
- # details.)
- def pathmap(spec=nil)
- collect { |fn| fn.pathmap(spec) }
- end
-
- # Return a new array with <tt>String#ext</tt> method applied to each
- # member of the array.
- #
- # This method is a shortcut for:
- #
- # array.collect { |item| item.ext(newext) }
- #
- # +ext+ is a user added method for the Array class.
- def ext(newext='')
- collect { |fn| fn.ext(newext) }
- end
-
-
- # Grep each of the files in the filelist using the given pattern. If a
- # block is given, call the block on each matching line, passing the file
- # name, line number, and the matching line of text. If no block is given,
- # a standard emac style file:linenumber:line message will be printed to
- # standard out.
- def egrep(pattern, *opt)
- each do |fn|
- open(fn, "rb", *opt) do |inf|
- count = 0
- inf.each do |line|
- count += 1
- if pattern.match(line)
- if block_given?
- yield fn, count, line
- else
- puts "#{fn}:#{count}:#{line}"
- end
- end
- end
- end
- end
- end
-
- # Return a new file list that only contains file names from the current
- # file list that exist on the file system.
- def existing
- select { |fn| File.exist?(fn) }
- end
-
- # Modify the current file list so that it contains only file name that
- # exist on the file system.
- def existing!
- resolve
- @items = @items.select { |fn| File.exist?(fn) }
- self
- end
-
- # FileList version of partition. Needed because the nested arrays should
- # be FileLists in this version.
- def partition(&block) # :nodoc:
- resolve
- result = @items.partition(&block)
- [
- FileList.new.import(result[0]),
- FileList.new.import(result[1]),
- ]
- end
-
- # Convert a FileList to a string by joining all elements with a space.
- def to_s
- resolve
- self.join(' ')
- end
-
- # Add matching glob patterns.
- def add_matching(pattern)
- Dir[pattern].each do |fn|
- self << fn unless exclude?(fn)
- end
- end
- private :add_matching
-
- # Should the given file name be excluded?
- def exclude?(fn)
- calculate_exclude_regexp unless @exclude_re
- fn =~ @exclude_re || @exclude_procs.any? { |p| p.call(fn) }
- end
-
- DEFAULT_IGNORE_PATTERNS = [
- /(^|[\/\\])CVS([\/\\]|$)/,
- /(^|[\/\\])\.svn([\/\\]|$)/,
- /\.bak$/,
- /~$/
- ]
- DEFAULT_IGNORE_PROCS = [
- proc { |fn| fn =~ /(^|[\/\\])core$/ && ! File.directory?(fn) }
- ]
-# @exclude_patterns = DEFAULT_IGNORE_PATTERNS.dup
-
- def import(array)
- @items = array
- self
- end
-
- class << self
- # Create a new file list including the files listed. Similar to:
- #
- # FileList.new(*args)
- def [](*args)
- new(*args)
- end
- end
- end # FileList
-end
-
-module Rake
- class << self
-
- # Yield each file or directory component.
- def each_dir_parent(dir) # :nodoc:
- old_length = nil
- while dir != '.' && dir.length != old_length
- yield(dir)
- old_length = dir.length
- dir = File.dirname(dir)
- end
- end
- end
-end # module Rake
-
-# Alias FileList to be available at the top level.
-FileList = Rake::FileList
-
-# ###########################################################################
-module Rake
-
- # Default Rakefile loader used by +import+.
- class DefaultLoader
- def load(fn)
- Kernel.load(File.expand_path(fn))
- end
- end
-
- # EarlyTime is a fake timestamp that occurs _before_ any other time value.
- class EarlyTime
- include Comparable
- include Singleton
-
- def <=>(other)
- -1
- end
-
- def to_s
- "<EARLY TIME>"
- end
- end
-
- EARLY = EarlyTime.instance
-end # module Rake
-
-# ###########################################################################
-# Extensions to time to allow comparisons with an early time class.
-#
-class Time
- alias rake_original_time_compare :<=>
- def <=>(other)
- if Rake::EarlyTime === other
- - other.<=>(self)
- else
- rake_original_time_compare(other)
- end
- end
-end # class Time
-
-module Rake
-
- ####################################################################
- # The NameSpace class will lookup task names in the the scope
- # defined by a +namespace+ command.
- #
- class NameSpace
-
- # Create a namespace lookup object using the given task manager
- # and the list of scopes.
- def initialize(task_manager, scope_list)
- @task_manager = task_manager
- @scope = scope_list.dup
- end
-
- # Lookup a task named +name+ in the namespace.
- def [](name)
- @task_manager.lookup(name, @scope)
- end
-
- # Return the list of tasks defined in this namespace.
- def tasks
- @task_manager.tasks
- end
- end # NameSpace
-
-
- ####################################################################
- # The TaskManager module is a mixin for managing tasks.
- module TaskManager
- # Track the last comment made in the Rakefile.
- attr_accessor :last_description
- alias :last_comment :last_description # Backwards compatibility
-
- def initialize
- super
- @tasks = Hash.new
- @rules = Array.new
- @scope = Array.new
- @last_description = nil
- end
-
- def create_rule(*args, &block)
- pattern, arg_names, deps = resolve_args(args)
- pattern = Regexp.new(Regexp.quote(pattern) + '$') if String === pattern
- @rules << [pattern, deps, block]
- end
-
- def define_task(task_class, *args, &block)
- task_name, arg_names, deps = resolve_args(args)
- task_name = task_class.scope_name(@scope, task_name)
- deps = [deps] unless deps.respond_to?(:to_ary)
- deps = deps.collect {|d| d.to_s }
- task = intern(task_class, task_name)
- task.set_arg_names(arg_names) unless arg_names.empty?
- task.add_description(@last_description)
- @last_description = nil
- task.enhance(deps, &block)
- task
- end
-
- # Lookup a task. Return an existing task if found, otherwise
- # create a task of the current type.
- def intern(task_class, task_name)
- @tasks[task_name.to_s] ||= task_class.new(task_name, self)
- end
-
- # Find a matching task for +task_name+.
- def [](task_name, scopes=nil)
- task_name = task_name.to_s
- self.lookup(task_name, scopes) or
- enhance_with_matching_rule(task_name) or
- synthesize_file_task(task_name) or
- fail "Don't know how to build task '#{task_name}'"
- end
-
- def synthesize_file_task(task_name)
- return nil unless File.exist?(task_name)
- define_task(Rake::FileTask, task_name)
- end
-
- # Resolve the arguments for a task/rule. Returns a triplet of
- # [task_name, arg_name_list, prerequisites].
- def resolve_args(args)
- if args.last.is_a?(Hash)
- deps = args.pop
- resolve_args_with_dependencies(args, deps)
- else
- resolve_args_without_dependencies(args)
- end
- end
-
- # Resolve task arguments for a task or rule when there are no
- # dependencies declared.
- #
- # The patterns recognized by this argument resolving function are:
- #
- # task :t
- # task :t, [:a]
- # task :t, :a (deprecated)
- #
- def resolve_args_without_dependencies(args)
- task_name = args.shift
- if args.size == 1 && args.first.respond_to?(:to_ary)
- arg_names = args.first.to_ary
- else
- arg_names = args
- end
- [task_name, arg_names, []]
- end
- private :resolve_args_without_dependencies
-
- # Resolve task arguments for a task or rule when there are
- # dependencies declared.
- #
- # The patterns recognized by this argument resolving function are:
- #
- # task :t => [:d]
- # task :t, [a] => [:d]
- # task :t, :needs => [:d] (deprecated)
- # task :t, :a, :needs => [:d] (deprecated)
- #
- def resolve_args_with_dependencies(args, hash) # :nodoc:
- fail "Task Argument Error" if hash.size != 1
- key, value = hash.map { |k, v| [k,v] }.first
- if args.empty?
- task_name = key
- arg_names = []
- deps = value
- elsif key == :needs
- task_name = args.shift
- arg_names = args
- deps = value
- else
- task_name = args.shift
- arg_names = key
- deps = value
- end
- deps = [deps] unless deps.respond_to?(:to_ary)
- [task_name, arg_names, deps]
- end
- private :resolve_args_with_dependencies
-
- # If a rule can be found that matches the task name, enhance the
- # task with the prerequisites and actions from the rule. Set the
- # source attribute of the task appropriately for the rule. Return
- # the enhanced task or nil of no rule was found.
- def enhance_with_matching_rule(task_name, level=0)
- fail Rake::RuleRecursionOverflowError,
- "Rule Recursion Too Deep" if level >= 16
- @rules.each do |pattern, extensions, block|
- if md = pattern.match(task_name)
- task = attempt_rule(task_name, extensions, block, level)
- return task if task
- end
- end
- nil
- rescue Rake::RuleRecursionOverflowError => ex
- ex.add_target(task_name)
- fail ex
- end
-
- # List of all defined tasks in this application.
- def tasks
- @tasks.values.sort_by { |t| t.name }
- end
-
- # Clear all tasks in this application.
- def clear
- @tasks.clear
- @rules.clear
- end
-
- # Lookup a task, using scope and the scope hints in the task name.
- # This method performs straight lookups without trying to
- # synthesize file tasks or rules. Special scope names (e.g. '^')
- # are recognized. If no scope argument is supplied, use the
- # current scope. Return nil if the task cannot be found.
- def lookup(task_name, initial_scope=nil)
- initial_scope ||= @scope
- task_name = task_name.to_s
- if task_name =~ /^rake:/
- scopes = []
- task_name = task_name.sub(/^rake:/, '')
- elsif task_name =~ /^(\^+)/
- scopes = initial_scope[0, initial_scope.size - $1.size]
- task_name = task_name.sub(/^(\^+)/, '')
- else
- scopes = initial_scope
- end
- lookup_in_scope(task_name, scopes)
- end
-
- # Lookup the task name
- def lookup_in_scope(name, scope)
- n = scope.size
- while n >= 0
- tn = (scope[0,n] + [name]).join(':')
- task = @tasks[tn]
- return task if task
- n -= 1
- end
- nil
- end
- private :lookup_in_scope
-
- # Return the list of scope names currently active in the task
- # manager.
- def current_scope
- @scope.dup
- end
-
- # Evaluate the block in a nested namespace named +name+. Create
- # an anonymous namespace if +name+ is nil.
- def in_namespace(name)
- name ||= generate_name
- @scope.push(name)
- ns = NameSpace.new(self, @scope)
- yield(ns)
- ns
- ensure
- @scope.pop
- end
-
- private
-
- # Generate an anonymous namespace name.
- def generate_name
- @seed ||= 0
- @seed += 1
- "_anon_#{@seed}"
- end
-
- def trace_rule(level, message)
- puts "#{" "*level}#{message}" if Rake.application.options.trace_rules
- end
-
- # Attempt to create a rule given the list of prerequisites.
- def attempt_rule(task_name, extensions, block, level)
- sources = make_sources(task_name, extensions)
- prereqs = sources.collect { |source|
- trace_rule level, "Attempting Rule #{task_name} => #{source}"
- if File.exist?(source) || Rake::Task.task_defined?(source)
- trace_rule level, "(#{task_name} => #{source} ... EXIST)"
- source
- elsif parent = enhance_with_matching_rule(source, level+1)
- trace_rule level, "(#{task_name} => #{source} ... ENHANCE)"
- parent.name
- else
- trace_rule level, "(#{task_name} => #{source} ... FAIL)"
- return nil
- end
- }
- task = FileTask.define_task({task_name => prereqs}, &block)
- task.sources = prereqs
- task
- end
-
- # Make a list of sources from the list of file name extensions /
- # translation procs.
- def make_sources(task_name, extensions)
- extensions.collect { |ext|
- case ext
- when /%/
- task_name.pathmap(ext)
- when %r{/}
- ext
- when /^\./
- task_name.ext(ext)
- when String
- ext
- when Proc
- if ext.arity == 1
- ext.call(task_name)
- else
- ext.call
- end
- else
- fail "Don't know how to handle rule dependent: #{ext.inspect}"
- end
- }.flatten
- end
-
- end # TaskManager
-
- ######################################################################
- # Rake main application object. When invoking +rake+ from the
- # command line, a Rake::Application object is created and run.
- #
- class Application
- include TaskManager
-
- # The name of the application (typically 'rake')
- attr_reader :name
-
- # The original directory where rake was invoked.
- attr_reader :original_dir
-
- # Name of the actual rakefile used.
- attr_reader :rakefile
-
- # List of the top level task names (task names from the command line).
- attr_reader :top_level_tasks
-
- DEFAULT_RAKEFILES = ['rakefile', 'Rakefile', 'rakefile.rb', 'Rakefile.rb'].freeze
-
- # Initialize a Rake::Application object.
- def initialize
- super
- @name = 'rake'
- @rakefiles = DEFAULT_RAKEFILES.dup
- @rakefile = nil
- @pending_imports = []
- @imported = []
- @loaders = {}
- @default_loader = Rake::DefaultLoader.new
- @original_dir = Dir.pwd
- @top_level_tasks = []
- add_loader('rb', DefaultLoader.new)
- add_loader('rf', DefaultLoader.new)
- add_loader('rake', DefaultLoader.new)
- @tty_output = STDOUT.tty?
- end
-
- # Run the Rake application. The run method performs the following three steps:
- #
- # * Initialize the command line options (+init+).
- # * Define the tasks (+load_rakefile+).
- # * Run the top level tasks (+run_tasks+).
- #
- # If you wish to build a custom rake command, you should call +init+ on your
- # application. The define any tasks. Finally, call +top_level+ to run your top
- # level tasks.
- def run
- standard_exception_handling do
- init
- load_rakefile
- top_level
- end
- end
-
- # Initialize the command line parameters and app name.
- def init(app_name='rake')
- standard_exception_handling do
- @name = app_name
- collect_tasks handle_options
- end
- end
-
- # Find the rakefile and then load it and any pending imports.
- def load_rakefile
- standard_exception_handling do
- raw_load_rakefile
- end
- end
-
- # Run the top level tasks of a Rake application.
- def top_level
- standard_exception_handling do
- if options.show_tasks
- display_tasks_and_comments
- elsif options.show_prereqs
- display_prerequisites
- else
- top_level_tasks.each { |task_name| invoke_task(task_name) }
- end
- end
- end
-
- # Add a loader to handle imported files ending in the extension
- # +ext+.
- def add_loader(ext, loader)
- ext = ".#{ext}" unless ext =~ /^\./
- @loaders[ext] = loader
- end
-
- # Application options from the command line
- def options
- @options ||= OpenStruct.new
- end
-
- # private ----------------------------------------------------------------
-
- def invoke_task(task_string)
- name, args = parse_task_string(task_string)
- t = self[name]
- t.invoke(*args)
- end
-
- def parse_task_string(string)
- if string =~ /^([^\[]+)(\[(.*)\])$/
- name = $1
- args = $3.split(/\s*,\s*/)
- else
- name = string
- args = []
- end
- [name, args]
- end
-
- # Provide standard execption handling for the given block.
- def standard_exception_handling
- begin
- yield
- rescue SystemExit => ex
- # Exit silently with current status
- raise
- rescue OptionParser::InvalidOption => ex
- # Exit silently
- exit(false)
- rescue Exception => ex
- # Exit with error message
- $stderr.puts "rake aborted!"
- $stderr.puts ex.message
- if options.trace
- $stderr.puts ex.backtrace.join("\n")
- else
- $stderr.puts ex.backtrace.find {|str| str =~ /#{@rakefile}/ } || ""
- $stderr.puts "(See full trace by running task with --trace)"
- end
- exit(false)
- end
- end
-
- # True if one of the files in RAKEFILES is in the current directory.
- # If a match is found, it is copied into @rakefile.
- def have_rakefile
- @rakefiles.each do |fn|
- if File.exist?(fn) || fn == ''
- return fn
- end
- end
- return nil
- end
-
- # True if we are outputting to TTY, false otherwise
- def tty_output?
- @tty_output
- end
-
- # Override the detected TTY output state (mostly for testing)
- def tty_output=( tty_output_state )
- @tty_output = tty_output_state
- end
-
- # We will truncate output if we are outputting to a TTY or if we've been
- # given an explicit column width to honor
- def truncate_output?
- tty_output? || ENV['RAKE_COLUMNS']
- end
-
- # Display the tasks and dependencies.
- def display_tasks_and_comments
- displayable_tasks = tasks.select { |t|
- t.comment && t.name =~ options.show_task_pattern
- }
- if options.full_description
- displayable_tasks.each do |t|
- puts "rake #{t.name_with_args}"
- t.full_comment.split("\n").each do |line|
- puts " #{line}"
- end
- puts
- end
- else
- width = displayable_tasks.collect { |t| t.name_with_args.length }.max || 10
- max_column = truncate_output? ? terminal_width - name.size - width - 7 : nil
- displayable_tasks.each do |t|
- printf "#{name} %-#{width}s # %s\n",
- t.name_with_args, max_column ? truncate(t.comment, max_column) : t.comment
- end
- end
- end
-
- def terminal_width
- if ENV['RAKE_COLUMNS']
- result = ENV['RAKE_COLUMNS'].to_i
- else
- result = unix? ? dynamic_width : 80
- end
- (result < 10) ? 80 : result
- rescue
- 80
- end
-
- # Calculate the dynamic width of the
- def dynamic_width
- @dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput)
- end
-
- def dynamic_width_stty
- %x{stty size 2>/dev/null}.split[1].to_i
- end
-
- def dynamic_width_tput
- %x{tput cols 2>/dev/null}.to_i
- end
-
- def unix?
- RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris|irix|hpux)/i
- end
-
- def windows?
- Win32.windows?
- end
-
- def truncate(string, width)
- if string.length <= width
- string
- else
- ( string[0, width-3] || "" ) + "..."
- end
- end
-
- # Display the tasks and prerequisites
- def display_prerequisites
- tasks.each do |t|
- puts "rake #{t.name}"
- t.prerequisites.each { |pre| puts " #{pre}" }
- end
- end
-
- # A list of all the standard options used in rake, suitable for
- # passing to OptionParser.
- def standard_rake_options
- [
- ['--classic-namespace', '-C', "Put Task and FileTask in the top level namespace",
- lambda { |value|
- require 'rake/classic_namespace'
- options.classic_namespace = true
- }
- ],
- ['--describe', '-D [PATTERN]', "Describe the tasks (matching optional PATTERN), then exit.",
- lambda { |value|
- options.show_tasks = true
- options.full_description = true
- options.show_task_pattern = Regexp.new(value || '')
- }
- ],
- ['--dry-run', '-n', "Do a dry run without executing actions.",
- lambda { |value|
- verbose(true)
- nowrite(true)
- options.dryrun = true
- options.trace = true
- }
- ],
- ['--execute', '-e CODE', "Execute some Ruby code and exit.",
- lambda { |value|
- eval(value)
- exit
- }
- ],
- ['--execute-print', '-p CODE', "Execute some Ruby code, print the result, then exit.",
- lambda { |value|
- puts eval(value)
- exit
- }
- ],
- ['--execute-continue', '-E CODE',
- "Execute some Ruby code, then continue with normal task processing.",
- lambda { |value| eval(value) }
- ],
- ['--libdir', '-I LIBDIR', "Include LIBDIR in the search path for required modules.",
- lambda { |value| $:.push(value) }
- ],
- ['--prereqs', '-P', "Display the tasks and dependencies, then exit.",
- lambda { |value| options.show_prereqs = true }
- ],
- ['--quiet', '-q', "Do not log messages to standard output.",
- lambda { |value| verbose(false) }
- ],
- ['--rakefile', '-f [FILE]', "Use FILE as the rakefile.",
- lambda { |value|
- value ||= ''
- @rakefiles.clear
- @rakefiles << value
- }
- ],
- ['--rakelibdir', '--rakelib', '-R RAKELIBDIR',
- "Auto-import any .rake files in RAKELIBDIR. (default is 'rakelib')",
- lambda { |value| options.rakelib = value.split(':') }
- ],
- ['--require', '-r MODULE', "Require MODULE before executing rakefile.",
- lambda { |value|
- begin
- require value
- rescue LoadError => ex
- begin
- rake_require value
- rescue LoadError => ex2
- raise ex
- end
- end
- }
- ],
- ['--rules', "Trace the rules resolution.",
- lambda { |value| options.trace_rules = true }
- ],
- ['--no-search', '--nosearch', '-N', "Do not search parent directories for the Rakefile.",
- lambda { |value| options.nosearch = true }
- ],
- ['--silent', '-s', "Like --quiet, but also suppresses the 'in directory' announcement.",
- lambda { |value|
- verbose(false)
- options.silent = true
- }
- ],
- ['--system', '-g',
- "Using system wide (global) rakefiles (usually '~/.rake/*.rake').",
- lambda { |value| options.load_system = true }
- ],
- ['--no-system', '--nosystem', '-G',
- "Use standard project Rakefile search paths, ignore system wide rakefiles.",
- lambda { |value| options.ignore_system = true }
- ],
- ['--tasks', '-T [PATTERN]', "Display the tasks (matching optional PATTERN) with descriptions, then exit.",
- lambda { |value|
- options.show_tasks = true
- options.show_task_pattern = Regexp.new(value || '')
- options.full_description = false
- }
- ],
- ['--trace', '-t', "Turn on invoke/execute tracing, enable full backtrace.",
- lambda { |value|
- options.trace = true
- verbose(true)
- }
- ],
- ['--verbose', '-v', "Log message to standard output (default).",
- lambda { |value| verbose(true) }
- ],
- ['--version', '-V', "Display the program version.",
- lambda { |value|
- puts "rake, version #{RAKEVERSION}"
- exit
- }
- ]
- ]
- end
-
- # Read and handle the command line options.
- def handle_options
- options.rakelib = ['rakelib']
-
- opts = OptionParser.new
- opts.banner = "rake [-f rakefile] {options} targets..."
- opts.separator ""
- opts.separator "Options are ..."
-
- opts.on_tail("-h", "--help", "-H", "Display this help message.") do
- puts opts
- exit
- end
-
- standard_rake_options.each { |args| opts.on(*args) }
- parsed_argv = opts.parse(ARGV)
-
- # If class namespaces are requested, set the global options
- # according to the values in the options structure.
- if options.classic_namespace
- $show_tasks = options.show_tasks
- $show_prereqs = options.show_prereqs
- $trace = options.trace
- $dryrun = options.dryrun
- $silent = options.silent
- end
- parsed_argv
- end
-
- # Similar to the regular Ruby +require+ command, but will check
- # for *.rake files in addition to *.rb files.
- def rake_require(file_name, paths=$LOAD_PATH, loaded=$")
- return false if loaded.include?(file_name)
- paths.each do |path|
- fn = file_name + ".rake"
- full_path = File.join(path, fn)
- if File.exist?(full_path)
- load full_path
- loaded << fn
- return true
- end
- end
- fail LoadError, "Can't find #{file_name}"
- end
-
- def find_rakefile_location
- here = Dir.pwd
- while ! (fn = have_rakefile)
- Dir.chdir("..")
- if Dir.pwd == here || options.nosearch
- return nil
- end
- here = Dir.pwd
- end
- [fn, here]
- ensure
- Dir.chdir(Rake.original_dir)
- end
-
- def raw_load_rakefile # :nodoc:
- rakefile, location = find_rakefile_location
- if (! options.ignore_system) &&
- (options.load_system || rakefile.nil?) &&
- system_dir && File.directory?(system_dir)
- puts "(in #{Dir.pwd})" unless options.silent
- glob("#{system_dir}/*.rake") do |name|
- add_import name
- end
- else
- fail "No Rakefile found (looking for: #{@rakefiles.join(', ')})" if
- rakefile.nil?
- @rakefile = rakefile
- Dir.chdir(location)
- puts "(in #{Dir.pwd})" unless options.silent
- $rakefile = @rakefile if options.classic_namespace
- load File.expand_path(@rakefile) if @rakefile && @rakefile != ''
- options.rakelib.each do |rlib|
- glob("#{rlib}/*.rake") do |name|
- add_import name
- end
- end
- end
- load_imports
- end
-
- def glob(path, &block)
- Dir[path.gsub("\\", '/')].each(&block)
- end
- private :glob
-
- # The directory path containing the system wide rakefiles.
- def system_dir
- @system_dir ||=
- begin
- if ENV['RAKE_SYSTEM']
- ENV['RAKE_SYSTEM']
- elsif Win32.windows?
- Win32.win32_system_dir
- else
- standard_system_dir
- end
- end
- end
-
- # The standard directory containing system wide rake files.
- def standard_system_dir #:nodoc:
- File.join(File.expand_path('~'), '.rake')
- end
- private :standard_system_dir
-
- # Collect the list of tasks on the command line. If no tasks are
- # given, return a list containing only the default task.
- # Environmental assignments are processed at this time as well.
- def collect_tasks(argv)
- @top_level_tasks = []
- argv.each do |arg|
- if arg =~ /^(\w+)=(.*)$/
- ENV[$1] = $2
- else
- @top_level_tasks << arg unless arg =~ /^-/
- end
- end
- @top_level_tasks.push("default") if @top_level_tasks.size == 0
- end
-
- # Add a file to the list of files to be imported.
- def add_import(fn)
- @pending_imports << fn
- end
-
- # Load the pending list of imported files.
- def load_imports
- while fn = @pending_imports.shift
- next if @imported.member?(fn)
- if fn_task = lookup(fn)
- fn_task.invoke
- end
- ext = File.extname(fn)
- loader = @loaders[ext] || @default_loader
- loader.load(fn)
- @imported << fn
- end
- end
-
- # Warn about deprecated use of top level constant names.
- def const_warning(const_name)
- @const_warning ||= false
- if ! @const_warning
- $stderr.puts %{WARNING: Deprecated reference to top-level constant '#{const_name}' } +
- %{found at: #{rakefile_location}} # '
- $stderr.puts %{ Use --classic-namespace on rake command}
- $stderr.puts %{ or 'require "rake/classic_namespace"' in Rakefile}
- end
- @const_warning = true
- end
-
- def rakefile_location
- begin
- fail
- rescue RuntimeError => ex
- ex.backtrace.find {|str| str =~ /#{@rakefile}/ } || ""
- end
- end
- end
-end
-
-
-class Module
- # Rename the original handler to make it available.
- alias :rake_original_const_missing :const_missing
-
- # Check for deprecated uses of top level (i.e. in Object) uses of
- # Rake class names. If someone tries to reference the constant
- # name, display a warning and return the proper object. Using the
- # --classic-namespace command line option will define these
- # constants in Object and avoid this handler.
- def const_missing(const_name)
- case const_name
- when :Task
- Rake.application.const_warning(const_name)
- Rake::Task
- when :FileTask
- Rake.application.const_warning(const_name)
- Rake::FileTask
- when :FileCreationTask
- Rake.application.const_warning(const_name)
- Rake::FileCreationTask
- when :RakeApp
- Rake.application.const_warning(const_name)
- Rake::Application
- else
- rake_original_const_missing(const_name)
- end
- end
-end
diff --git a/lib/rake/classic_namespace.rb b/lib/rake/classic_namespace.rb
deleted file mode 100644
index feb7569966..0000000000
--- a/lib/rake/classic_namespace.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# The following classes used to be in the top level namespace.
-# Loading this file enables compatibility with older Rakefile that
-# referenced Task from the top level.
-
-Task = Rake::Task
-FileTask = Rake::FileTask
-FileCreationTask = Rake::FileCreationTask
-RakeApp = Rake::Application
diff --git a/lib/rake/clean.rb b/lib/rake/clean.rb
deleted file mode 100644
index 4ee2c5ac95..0000000000
--- a/lib/rake/clean.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/env ruby
-
-# The 'rake/clean' file defines two file lists (CLEAN and CLOBBER) and
-# two rake tasks (:clean and :clobber).
-#
-# [:clean] Clean up the project by deleting scratch files and backup
-# files. Add files to the CLEAN file list to have the :clean
-# target handle them.
-#
-# [:clobber] Clobber all generated and non-source files in a project.
-# The task depends on :clean, so all the clean files will
-# be deleted as well as files in the CLOBBER file list.
-# The intent of this task is to return a project to its
-# pristine, just unpacked state.
-
-require 'rake'
-
-CLEAN = Rake::FileList["**/*~", "**/*.bak", "**/core"]
-CLEAN.clear_exclude.exclude { |fn|
- fn.pathmap("%f") == 'core' && File.directory?(fn)
-}
-
-desc "Remove any temporary products."
-task :clean do
- CLEAN.each { |fn| rm_r fn rescue nil }
-end
-
-CLOBBER = Rake::FileList.new
-
-desc "Remove any generated file."
-task :clobber => [:clean] do
- CLOBBER.each { |fn| rm_r fn rescue nil }
-end
diff --git a/lib/rake/gempackagetask.rb b/lib/rake/gempackagetask.rb
deleted file mode 100644
index 1e4632a26b..0000000000
--- a/lib/rake/gempackagetask.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-#!/usr/bin/env ruby
-
-# Define a package task library to aid in the definition of GEM
-# packages.
-
-require 'rubygems'
-require 'rake'
-require 'rake/packagetask'
-require 'rubygems/user_interaction'
-require 'rubygems/builder'
-
-module Rake
-
- # Create a package based upon a Gem spec. Gem packages, as well as
- # zip files and tar/gzipped packages can be produced by this task.
- #
- # In addition to the Rake targets generated by PackageTask, a
- # GemPackageTask will also generate the following tasks:
- #
- # [<b>"<em>package_dir</em>/<em>name</em>-<em>version</em>.gem"</b>]
- # Create a Ruby GEM package with the given name and version.
- #
- # Example using a Ruby GEM spec:
- #
- # require 'rubygems'
- #
- # spec = Gem::Specification.new do |s|
- # s.platform = Gem::Platform::RUBY
- # s.summary = "Ruby based make-like utility."
- # s.name = 'rake'
- # s.version = PKG_VERSION
- # s.requirements << 'none'
- # s.require_path = 'lib'
- # s.autorequire = 'rake'
- # s.files = PKG_FILES
- # s.description = <<EOF
- # Rake is a Make-like program implemented in Ruby. Tasks
- # and dependencies are specified in standard Ruby syntax.
- # EOF
- # end
- #
- # Rake::GemPackageTask.new(spec) do |pkg|
- # pkg.need_zip = true
- # pkg.need_tar = true
- # end
- #
- class GemPackageTask < PackageTask
- # Ruby GEM spec containing the metadata for this package. The
- # name, version and package_files are automatically determined
- # from the GEM spec and don't need to be explicitly provided.
- attr_accessor :gem_spec
-
- # Create a GEM Package task library. Automatically define the gem
- # if a block is given. If no block is supplied, then +define+
- # needs to be called to define the task.
- def initialize(gem_spec)
- init(gem_spec)
- yield self if block_given?
- define if block_given?
- end
-
- # Initialization tasks without the "yield self" or define
- # operations.
- def init(gem)
- super(gem.name, gem.version)
- @gem_spec = gem
- @package_files += gem_spec.files if gem_spec.files
- end
-
- # Create the Rake tasks and actions specified by this
- # GemPackageTask. (+define+ is automatically called if a block is
- # given to +new+).
- def define
- super
- task :package => [:gem]
- desc "Build the gem file #{gem_file}"
- task :gem => ["#{package_dir}/#{gem_file}"]
- file "#{package_dir}/#{gem_file}" => [package_dir] + @gem_spec.files do
- when_writing("Creating GEM") {
- Gem::Builder.new(gem_spec).build
- verbose(true) {
- mv gem_file, "#{package_dir}/#{gem_file}"
- }
- }
- end
- end
-
- def gem_file
- if @gem_spec.platform == Gem::Platform::RUBY
- "#{package_name}.gem"
- else
- "#{package_name}-#{@gem_spec.platform}.gem"
- end
- end
-
- end
-end
diff --git a/lib/rake/loaders/makefile.rb b/lib/rake/loaders/makefile.rb
deleted file mode 100644
index 9ade098a1b..0000000000
--- a/lib/rake/loaders/makefile.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/env ruby
-
-module Rake
-
- # Makefile loader to be used with the import file loader.
- class MakefileLoader
-
- # Load the makefile dependencies in +fn+.
- def load(fn)
- open(fn) do |mf|
- lines = mf.read
- lines.gsub!(/#[^\n]*\n/m, "")
- lines.gsub!(/\\\n/, ' ')
- lines.split("\n").each do |line|
- process_line(line)
- end
- end
- end
-
- private
-
- # Process one logical line of makefile data.
- def process_line(line)
- file_tasks, args = line.split(':')
- return if args.nil?
- dependents = args.split
- file_tasks.strip.split.each do |file_task|
- file file_task => dependents
- end
- end
- end
-
- # Install the handler
- Rake.application.add_loader('mf', MakefileLoader.new)
-end
diff --git a/lib/rake/packagetask.rb b/lib/rake/packagetask.rb
deleted file mode 100644
index 6158eaf3f6..0000000000
--- a/lib/rake/packagetask.rb
+++ /dev/null
@@ -1,185 +0,0 @@
-#!/usr/bin/env ruby
-
-# Define a package task libarary to aid in the definition of
-# redistributable package files.
-
-require 'rake'
-require 'rake/tasklib'
-
-module Rake
-
- # Create a packaging task that will package the project into
- # distributable files (e.g zip archive or tar files).
- #
- # The PackageTask will create the following targets:
- #
- # [<b>:package</b>]
- # Create all the requested package files.
- #
- # [<b>:clobber_package</b>]
- # Delete all the package files. This target is automatically
- # added to the main clobber target.
- #
- # [<b>:repackage</b>]
- # Rebuild the package files from scratch, even if they are not out
- # of date.
- #
- # [<b>"<em>package_dir</em>/<em>name</em>-<em>version</em>.tgz"</b>]
- # Create a gzipped tar package (if <em>need_tar</em> is true).
- #
- # [<b>"<em>package_dir</em>/<em>name</em>-<em>version</em>.tar.gz"</b>]
- # Create a gzipped tar package (if <em>need_tar_gz</em> is true).
- #
- # [<b>"<em>package_dir</em>/<em>name</em>-<em>version</em>.tar.bz2"</b>]
- # Create a bzip2'd tar package (if <em>need_tar_bz2</em> is true).
- #
- # [<b>"<em>package_dir</em>/<em>name</em>-<em>version</em>.zip"</b>]
- # Create a zip package archive (if <em>need_zip</em> is true).
- #
- # Example:
- #
- # Rake::PackageTask.new("rake", "1.2.3") do |p|
- # p.need_tar = true
- # p.package_files.include("lib/**/*.rb")
- # end
- #
- class PackageTask < TaskLib
- # Name of the package.
- attr_accessor :name
-
- # Version of the package (e.g. '1.3.2').
- attr_accessor :version
-
- # Directory used to store the package files (default is 'pkg').
- attr_accessor :package_dir
-
- # True if a gzipped tar file (tgz) should be produced (default is false).
- attr_accessor :need_tar
-
- # True if a gzipped tar file (tar.gz) should be produced (default is false).
- attr_accessor :need_tar_gz
-
- # True if a bzip2'd tar file (tar.bz2) should be produced (default is false).
- attr_accessor :need_tar_bz2
-
- # True if a zip file should be produced (default is false)
- attr_accessor :need_zip
-
- # List of files to be included in the package.
- attr_accessor :package_files
-
- # Tar command for gzipped or bzip2ed archives. The default is 'tar'.
- attr_accessor :tar_command
-
- # Zip command for zipped archives. The default is 'zip'.
- attr_accessor :zip_command
-
- # Create a Package Task with the given name and version.
- def initialize(name=nil, version=nil)
- init(name, version)
- yield self if block_given?
- define unless name.nil?
- end
-
- # Initialization that bypasses the "yield self" and "define" step.
- def init(name, version)
- @name = name
- @version = version
- @package_files = Rake::FileList.new
- @package_dir = 'pkg'
- @need_tar = false
- @need_tar_gz = false
- @need_tar_bz2 = false
- @need_zip = false
- @tar_command = 'tar'
- @zip_command = 'zip'
- end
-
- # Create the tasks defined by this task library.
- def define
- fail "Version required (or :noversion)" if @version.nil?
- @version = nil if :noversion == @version
-
- desc "Build all the packages"
- task :package
-
- desc "Force a rebuild of the package files"
- task :repackage => [:clobber_package, :package]
-
- desc "Remove package products"
- task :clobber_package do
- rm_r package_dir rescue nil
- end
-
- task :clobber => [:clobber_package]
-
- [
- [need_tar, tgz_file, "z"],
- [need_tar_gz, tar_gz_file, "z"],
- [need_tar_bz2, tar_bz2_file, "j"]
- ].each do |(need, file, flag)|
- if need
- task :package => ["#{package_dir}/#{file}"]
- file "#{package_dir}/#{file}" => [package_dir_path] + package_files do
- chdir(package_dir) do
- sh %{env}
- sh %{#{@tar_command} #{flag}cvf #{file} #{package_name}}
- end
- end
- end
- end
-
- if need_zip
- task :package => ["#{package_dir}/#{zip_file}"]
- file "#{package_dir}/#{zip_file}" => [package_dir_path] + package_files do
- chdir(package_dir) do
- sh %{#{@zip_command} -r #{zip_file} #{package_name}}
- end
- end
- end
-
- directory package_dir
-
- file package_dir_path => @package_files do
- mkdir_p package_dir rescue nil
- @package_files.each do |fn|
- f = File.join(package_dir_path, fn)
- fdir = File.dirname(f)
- mkdir_p(fdir) if !File.exist?(fdir)
- if File.directory?(fn)
- mkdir_p(f)
- else
- rm_f f
- safe_ln(fn, f)
- end
- end
- end
- self
- end
-
- def package_name
- @version ? "#{@name}-#{@version}" : @name
- end
-
- def package_dir_path
- "#{package_dir}/#{package_name}"
- end
-
- def tgz_file
- "#{package_name}.tgz"
- end
-
- def tar_gz_file
- "#{package_name}.tar.gz"
- end
-
- def tar_bz2_file
- "#{package_name}.tar.bz2"
- end
-
- def zip_file
- "#{package_name}.zip"
- end
- end
-
-end
diff --git a/lib/rake/rake_test_loader.rb b/lib/rake/rake_test_loader.rb
deleted file mode 100644
index 8d7dad3c94..0000000000
--- a/lib/rake/rake_test_loader.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env ruby
-
-# Load the test files from the command line.
-
-ARGV.each { |f| load f unless f =~ /^-/ }
diff --git a/lib/rake/rdoctask.rb b/lib/rake/rdoctask.rb
deleted file mode 100644
index 6cfbda1d6a..0000000000
--- a/lib/rake/rdoctask.rb
+++ /dev/null
@@ -1,147 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'rake'
-require 'rake/tasklib'
-
-module Rake
-
- # Create a documentation task that will generate the RDoc files for
- # a project.
- #
- # The RDocTask will create the following targets:
- #
- # [<b><em>rdoc</em></b>]
- # Main task for this RDOC task.
- #
- # [<b>:clobber_<em>rdoc</em></b>]
- # Delete all the rdoc files. This target is automatically
- # added to the main clobber target.
- #
- # [<b>:re<em>rdoc</em></b>]
- # Rebuild the rdoc files from scratch, even if they are not out
- # of date.
- #
- # Simple Example:
- #
- # Rake::RDocTask.new do |rd|
- # rd.main = "README.rdoc"
- # rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
- # end
- #
- # You may wish to give the task a different name, such as if you are
- # generating two sets of documentation. For instance, if you want to have a
- # development set of documentation including private methods:
- #
- # Rake::RDocTask.new(:rdoc_dev) do |rd|
- # rd.main = "README.doc"
- # rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
- # rd.options << "--all"
- # end
- #
- # The tasks would then be named :<em>rdoc_dev</em>, :clobber_<em>rdoc_dev</em>, and
- # :re<em>rdoc_dev</em>.
- #
- class RDocTask < TaskLib
- # Name of the main, top level task. (default is :rdoc)
- attr_accessor :name
-
- # Name of directory to receive the html output files. (default is "html")
- attr_accessor :rdoc_dir
-
- # Title of RDoc documentation. (default is none)
- attr_accessor :title
-
- # Name of file to be used as the main, top level file of the
- # RDoc. (default is none)
- attr_accessor :main
-
- # Name of template to be used by rdoc. (defaults to rdoc's default)
- attr_accessor :template
-
- # List of files to be included in the rdoc generation. (default is [])
- attr_accessor :rdoc_files
-
- # List of options to be passed rdoc. (default is [])
- attr_accessor :options
-
- # Run the rdoc process as an external shell (default is false)
- attr_accessor :external
-
- # Create an RDoc task named <em>rdoc</em>. Default task name is +rdoc+.
- def initialize(name=:rdoc) # :yield: self
- @name = name
- @rdoc_files = Rake::FileList.new
- @rdoc_dir = 'html'
- @main = nil
- @title = nil
- @template = nil
- @external = false
- @options = []
- yield self if block_given?
- define
- end
-
- # Create the tasks defined by this task lib.
- def define
- if name.to_s != "rdoc"
- desc "Build the RDOC HTML Files"
- end
-
- desc "Build the #{name} HTML Files"
- task name
-
- desc "Force a rebuild of the RDOC files"
- task "re#{name}" => ["clobber_#{name}", name]
-
- desc "Remove rdoc products"
- task "clobber_#{name}" do
- rm_r rdoc_dir rescue nil
- end
-
- task :clobber => ["clobber_#{name}"]
-
- directory @rdoc_dir
- task name => [rdoc_target]
- file rdoc_target => @rdoc_files + [Rake.application.rakefile] do
- rm_r @rdoc_dir rescue nil
- args = option_list + @rdoc_files
- if @external
- argstring = args.join(' ')
- sh %{ruby -Ivendor vender/rd #{argstring}}
- else
- require 'rdoc/rdoc'
- RDoc::RDoc.new.document(args)
- end
- end
- self
- end
-
- def option_list
- result = @options.dup
- result << "-o" << @rdoc_dir
- result << "--main" << quote(main) if main
- result << "--title" << quote(title) if title
- result << "-T" << quote(template) if template
- result
- end
-
- def quote(str)
- if @external
- "'#{str}'"
- else
- str
- end
- end
-
- def option_string
- option_list.join(' ')
- end
-
- private
-
- def rdoc_target
- "#{rdoc_dir}/index.html"
- end
-
- end
-end
diff --git a/lib/rake/runtest.rb b/lib/rake/runtest.rb
deleted file mode 100644
index 3f1d205201..0000000000
--- a/lib/rake/runtest.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'test/unit'
-require 'test/unit/assertions'
-
-module Rake
- include Test::Unit::Assertions
-
- def run_tests(pattern='test/test*.rb', log_enabled=false)
- Dir["#{pattern}"].each { |fn|
- puts fn if log_enabled
- begin
- load fn
- rescue Exception => ex
- puts "Error in #{fn}: #{ex.message}"
- puts ex.backtrace
- assert false
- end
- }
- end
-
- extend self
-end
diff --git a/lib/rake/tasklib.rb b/lib/rake/tasklib.rb
deleted file mode 100644
index c7fd98133c..0000000000
--- a/lib/rake/tasklib.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'rake'
-
-module Rake
-
- # Base class for Task Libraries.
- class TaskLib
- include Cloneable
-
- # Make a symbol by pasting two strings together.
- #
- # NOTE: DEPRECATED! This method is kinda stupid. I don't know why
- # I didn't just use string interpolation. But now other task
- # libraries depend on this so I can't remove it without breaking
- # other people's code. So for now it stays for backwards
- # compatibility. BUT DON'T USE IT.
- def paste(a,b) # :nodoc:
- (a.to_s + b.to_s).intern
- end
- end
-
-end
diff --git a/lib/rake/testtask.rb b/lib/rake/testtask.rb
deleted file mode 100644
index 79154e422b..0000000000
--- a/lib/rake/testtask.rb
+++ /dev/null
@@ -1,161 +0,0 @@
-#!/usr/bin/env ruby
-
-# Define a task library for running unit tests.
-
-require 'rake'
-require 'rake/tasklib'
-
-module Rake
-
- # Create a task that runs a set of tests.
- #
- # Example:
- #
- # Rake::TestTask.new do |t|
- # t.libs << "test"
- # t.test_files = FileList['test/test*.rb']
- # t.verbose = true
- # end
- #
- # If rake is invoked with a "TEST=filename" command line option,
- # then the list of test files will be overridden to include only the
- # filename specified on the command line. This provides an easy way
- # to run just one test.
- #
- # If rake is invoked with a "TESTOPTS=options" command line option,
- # then the given options are passed to the test process after a
- # '--'. This allows Test::Unit options to be passed to the test
- # suite.
- #
- # Examples:
- #
- # rake test # run tests normally
- # rake test TEST=just_one_file.rb # run just one test file.
- # rake test TESTOPTS="-v" # run in verbose mode
- # rake test TESTOPTS="--runner=fox" # use the fox test runner
- #
- class TestTask < TaskLib
-
- # Name of test task. (default is :test)
- attr_accessor :name
-
- # List of directories to added to $LOAD_PATH before running the
- # tests. (default is 'lib')
- attr_accessor :libs
-
- # True if verbose test output desired. (default is false)
- attr_accessor :verbose
-
- # Test options passed to the test suite. An explicit
- # TESTOPTS=opts on the command line will override this. (default
- # is NONE)
- attr_accessor :options
-
- # Request that the tests be run with the warning flag set.
- # E.g. warning=true implies "ruby -w" used to run the tests.
- attr_accessor :warning
-
- # Glob pattern to match test files. (default is 'test/test*.rb')
- attr_accessor :pattern
-
- # Style of test loader to use. Options are:
- #
- # * :rake -- Rake provided test loading script (default).
- # * :testrb -- Ruby provided test loading script.
- # * :direct -- Load tests using command line loader.
- #
- attr_accessor :loader
-
- # Array of commandline options to pass to ruby when running test loader.
- attr_accessor :ruby_opts
-
- # Explicitly define the list of test files to be included in a
- # test. +list+ is expected to be an array of file names (a
- # FileList is acceptable). If both +pattern+ and +test_files+ are
- # used, then the list of test files is the union of the two.
- def test_files=(list)
- @test_files = list
- end
-
- # Create a testing task.
- def initialize(name=:test)
- @name = name
- @libs = ["lib"]
- @pattern = nil
- @options = nil
- @test_files = nil
- @verbose = false
- @warning = false
- @loader = :rake
- @ruby_opts = []
- yield self if block_given?
- @pattern = 'test/test*.rb' if @pattern.nil? && @test_files.nil?
- define
- end
-
- # Create the tasks defined by this task lib.
- def define
- lib_path = @libs.join(File::PATH_SEPARATOR)
- desc "Run tests" + (@name==:test ? "" : " for #{@name}")
- task @name do
- run_code = ''
- RakeFileUtils.verbose(@verbose) do
- run_code =
- case @loader
- when :direct
- "-e 'ARGV.each{|f| load f}'"
- when :testrb
- "-S testrb #{fix}"
- when :rake
- rake_loader
- end
- @ruby_opts.unshift( "-I#{lib_path}" )
- @ruby_opts.unshift( "-w" ) if @warning
- ruby @ruby_opts.join(" ") +
- " \"#{run_code}\" " +
- file_list.collect { |fn| "\"#{fn}\"" }.join(' ') +
- " #{option_list}"
- end
- end
- self
- end
-
- def option_list # :nodoc:
- ENV['TESTOPTS'] || @options || ""
- end
-
- def file_list # :nodoc:
- if ENV['TEST']
- FileList[ ENV['TEST'] ]
- else
- result = []
- result += @test_files.to_a if @test_files
- result += FileList[ @pattern ].to_a if @pattern
- FileList[result]
- end
- end
-
- def fix # :nodoc:
- case RUBY_VERSION
- when '1.8.2'
- find_file 'rake/ruby182_test_unit_fix'
- else
- nil
- end || ''
- end
-
- def rake_loader # :nodoc:
- find_file('rake/rake_test_loader') or
- fail "unable to find rake test loader"
- end
-
- def find_file(fn) # :nodoc:
- $LOAD_PATH.each do |path|
- file_path = File.join(path, "#{fn}.rb")
- return file_path if File.exist? file_path
- end
- nil
- end
-
- end
-end
diff --git a/lib/rake/win32.rb b/lib/rake/win32.rb
deleted file mode 100644
index eadc585a3f..0000000000
--- a/lib/rake/win32.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-module Rake
-
- # Win 32 interface methods for Rake. Windows specific functionality
- # will be placed here to collect that knowledge in one spot.
- module Win32
-
- # Error indicating a problem in locating the home directory on a
- # Win32 system.
- class Win32HomeError < RuntimeError
- end
-
- class << self
- # True if running on a windows system.
- def windows?
- Config::CONFIG['host_os'] =~ /mswin/
- end
-
- # Run a command line on windows.
- def rake_system(*cmd)
- if cmd.size == 1
- system("call #{cmd}")
- else
- system(*cmd)
- end
- end
-
- # The standard directory containing system wide rake files on
- # Win 32 systems. Try the following environment variables (in
- # order):
- #
- # * APPDATA
- # * HOMEDRIVE + HOMEPATH
- # * USERPROFILE
- #
- # If the above are not defined, the return nil.
- def win32_system_dir #:nodoc:
- win32_shared_path = ENV['APPDATA']
- if win32_shared_path.nil? && ENV['HOMEDRIVE'] && ENV['HOMEPATH']
- win32_shared_path = ENV['HOMEDRIVE'] + ENV['HOMEPATH']
- end
- win32_shared_path ||= ENV['USERPROFILE']
- raise Win32HomeError, "Unable to determine home path environment variable." if
- win32_shared_path.nil? or win32_shared_path.empty?
- normalize(File.join(win32_shared_path, 'Rake'))
- end
-
- # Normalize a win32 path so that the slashes are all forward slashes.
- def normalize(path)
- path.gsub(/\\/, '/')
- end
-
- end
- end
-end
diff --git a/lib/random/formatter.rb b/lib/random/formatter.rb
new file mode 100644
index 0000000000..4ecd6ad027
--- /dev/null
+++ b/lib/random/formatter.rb
@@ -0,0 +1,372 @@
+# -*- coding: us-ascii -*-
+# frozen_string_literal: true
+
+# == \Random number formatter.
+#
+# Formats generated random numbers in many manners. When <tt>'random/formatter'</tt>
+# is required, several methods are added to empty core module <tt>Random::Formatter</tt>,
+# making them available as Random's instance and module methods.
+#
+# Standard library SecureRandom is also extended with the module, and the methods
+# described below are available as a module methods in it.
+#
+# === Examples
+#
+# Generate random hexadecimal strings:
+#
+# require 'random/formatter'
+#
+# prng = Random.new
+# prng.hex(10) #=> "52750b30ffbc7de3b362"
+# prng.hex(10) #=> "92b15d6c8dc4beb5f559"
+# prng.hex(13) #=> "39b290146bea6ce975c37cfc23"
+# # or just
+# Random.hex #=> "1aed0c631e41be7f77365415541052ee"
+#
+# Generate random base64 strings:
+#
+# prng.base64(10) #=> "EcmTPZwWRAozdA=="
+# prng.base64(10) #=> "KO1nIU+p9DKxGg=="
+# prng.base64(12) #=> "7kJSM/MzBJI+75j8"
+# Random.base64(4) #=> "bsQ3fQ=="
+#
+# Generate random binary strings:
+#
+# prng.random_bytes(10) #=> "\016\t{\370g\310pbr\301"
+# prng.random_bytes(10) #=> "\323U\030TO\234\357\020\a\337"
+# Random.random_bytes(6) #=> "\xA1\xE6Lr\xC43"
+#
+# Generate alphanumeric strings:
+#
+# prng.alphanumeric(10) #=> "S8baxMJnPl"
+# prng.alphanumeric(10) #=> "aOxAg8BAJe"
+# Random.alphanumeric #=> "TmP9OsJHJLtaZYhP"
+#
+# Generate UUIDs:
+#
+# prng.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594"
+# prng.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab"
+# Random.uuid #=> "f14e0271-de96-45cc-8911-8910292a42cd"
+#
+# All methods are available in the standard library SecureRandom, too:
+#
+# SecureRandom.hex #=> "05b45376a30c67238eb93b16499e50cf"
+
+module Random::Formatter
+
+ # Generate a random binary string.
+ #
+ # The argument _n_ specifies the length of the result string.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in future.
+ #
+ # The result may contain any byte: "\x00" - "\xff".
+ #
+ # require 'random/formatter'
+ #
+ # Random.random_bytes #=> "\xD8\\\xE0\xF4\r\xB2\xFC*WM\xFF\x83\x18\xF45\xB6"
+ # # or
+ # prng = Random.new
+ # prng.random_bytes #=> "m\xDC\xFC/\a\x00Uf\xB2\xB2P\xBD\xFF6S\x97"
+ def random_bytes(n=nil)
+ n = n ? n.to_int : 16
+ gen_random(n)
+ end
+
+ # Generate a random hexadecimal string.
+ #
+ # The argument _n_ specifies the length, in bytes, of the random number to be generated.
+ # The length of the resulting hexadecimal string is twice of _n_.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in the future.
+ #
+ # The result may contain 0-9 and a-f.
+ #
+ # require 'random/formatter'
+ #
+ # Random.hex #=> "eb693ec8252cd630102fd0d0fb7c3485"
+ # # or
+ # prng = Random.new
+ # prng.hex #=> "91dc3bfb4de5b11d029d376634589b61"
+ def hex(n=nil)
+ random_bytes(n).unpack1("H*")
+ end
+
+ # Generate a random base64 string.
+ #
+ # The argument _n_ specifies the length, in bytes, of the random number
+ # to be generated. The length of the result string is about 4/3 of _n_.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in the future.
+ #
+ # The result may contain A-Z, a-z, 0-9, "+", "/" and "=".
+ #
+ # require 'random/formatter'
+ #
+ # Random.base64 #=> "/2BuBuLf3+WfSKyQbRcc/A=="
+ # # or
+ # prng = Random.new
+ # prng.base64 #=> "6BbW0pxO0YENxn38HMUbcQ=="
+ #
+ # See RFC 3548 for the definition of base64.
+ def base64(n=nil)
+ [random_bytes(n)].pack("m0")
+ end
+
+ # Generate a random URL-safe base64 string.
+ #
+ # The argument _n_ specifies the length, in bytes, of the random number
+ # to be generated. The length of the result string is about 4/3 of _n_.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in the future.
+ #
+ # The boolean argument _padding_ specifies the padding.
+ # If it is false or nil, padding is not generated.
+ # Otherwise padding is generated.
+ # By default, padding is not generated because "=" may be used as a URL delimiter.
+ #
+ # The result may contain A-Z, a-z, 0-9, "-" and "_".
+ # "=" is also used if _padding_ is true.
+ #
+ # require 'random/formatter'
+ #
+ # Random.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
+ # # or
+ # prng = Random.new
+ # prng.urlsafe_base64 #=> "UZLdOkzop70Ddx-IJR0ABg"
+ #
+ # prng.urlsafe_base64(nil, true) #=> "i0XQ-7gglIsHGV2_BNPrdQ=="
+ # prng.urlsafe_base64(nil, true) #=> "-M8rLhr7JEpJlqFGUMmOxg=="
+ #
+ # See RFC 3548 for the definition of URL-safe base64.
+ def urlsafe_base64(n=nil, padding=false)
+ s = [random_bytes(n)].pack("m0")
+ s.tr!("+/", "-_")
+ s.delete!("=") unless padding
+ s
+ end
+
+ # Generate a random v4 UUID (Universally Unique IDentifier).
+ #
+ # require 'random/formatter'
+ #
+ # Random.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594"
+ # Random.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab"
+ # # or
+ # prng = Random.new
+ # prng.uuid #=> "62936e70-1815-439b-bf89-8492855a7e6b"
+ #
+ # The version 4 UUID is purely random (except the version).
+ # It doesn't contain meaningful information such as MAC addresses, timestamps, etc.
+ #
+ # The result contains 122 random bits (15.25 random bytes).
+ #
+ # See RFC9562[https://www.rfc-editor.org/rfc/rfc9562] for details of UUIDv4.
+ #
+ def uuid
+ ary = random_bytes(16)
+ ary.setbyte(6, (ary.getbyte(6) & 0x0f) | 0x40)
+ ary.setbyte(8, (ary.getbyte(8) & 0x3f) | 0x80)
+ ary.unpack("H8H4H4H4H12").join(?-)
+ end
+
+ alias uuid_v4 uuid
+
+ # Generate a random v7 UUID (Universally Unique IDentifier).
+ #
+ # require 'random/formatter'
+ #
+ # Random.uuid_v7 # => "0188d4c3-1311-7f96-85c7-242a7aa58f1e"
+ # Random.uuid_v7 # => "0188d4c3-16fe-744f-86af-38fa04c62bb5"
+ # Random.uuid_v7 # => "0188d4c3-1af8-764f-b049-c204ce0afa23"
+ # Random.uuid_v7 # => "0188d4c3-1e74-7085-b14f-ef6415dc6f31"
+ # # |<--sorted-->| |<----- random ---->|
+ #
+ # # or
+ # prng = Random.new
+ # prng.uuid_v7 # => "0188ca51-5e72-7950-a11d-def7ff977c98"
+ #
+ # The version 7 UUID starts with the least significant 48 bits of a 64 bit
+ # Unix timestamp (milliseconds since the epoch) and fills the remaining bits
+ # with random data, excluding the version and variant bits.
+ #
+ # This allows version 7 UUIDs to be sorted by creation time. Time ordered
+ # UUIDs can be used for better database index locality of newly inserted
+ # records, which may have a significant performance benefit compared to random
+ # data inserts.
+ #
+ # The result contains 74 random bits (9.25 random bytes).
+ #
+ # Note that this method cannot be made reproducible because its output
+ # includes not only random bits but also timestamp.
+ #
+ # See RFC9562[https://www.rfc-editor.org/rfc/rfc9562] for details of UUIDv7.
+ #
+ # ==== Monotonicity
+ #
+ # UUIDv7 has millisecond precision by default, so multiple UUIDs created
+ # within the same millisecond are not issued in monotonically increasing
+ # order. To create UUIDs that are time-ordered with sub-millisecond
+ # precision, up to 12 bits of additional timestamp may added with
+ # +extra_timestamp_bits+. The extra timestamp precision comes at the expense
+ # of random bits. Setting <tt>extra_timestamp_bits: 12</tt> provides ~244ns
+ # of precision, but only 62 random bits (7.75 random bytes).
+ #
+ # prng = Random.new
+ # Array.new(4) { prng.uuid_v7(extra_timestamp_bits: 12) }
+ # # =>
+ # ["0188d4c7-13da-74f9-8b53-22a786ffdd5a",
+ # "0188d4c7-13da-753b-83a5-7fb9b2afaeea",
+ # "0188d4c7-13da-754a-88ea-ac0baeedd8db",
+ # "0188d4c7-13da-7557-83e1-7cad9cda0d8d"]
+ # # |<--- sorted --->| |<-- random --->|
+ #
+ # Array.new(4) { prng.uuid_v7(extra_timestamp_bits: 8) }
+ # # =>
+ # ["0188d4c7-3333-7a95-850a-de6edb858f7e",
+ # "0188d4c7-3333-7ae8-842e-bc3a8b7d0cf9", # <- out of order
+ # "0188d4c7-3333-7ae2-995a-9f135dc44ead", # <- out of order
+ # "0188d4c7-3333-7af9-87c3-8f612edac82e"]
+ # # |<--- sorted -->||<---- random --->|
+ #
+ # Any rollbacks of the system clock will break monotonicity. UUIDv7 is based
+ # on UTC, which excludes leap seconds and can rollback the clock. To avoid
+ # this, the system clock can synchronize with an NTP server configured to use
+ # a "leap smear" approach. NTP or PTP will also be needed to synchronize
+ # across distributed nodes.
+ #
+ # Counters and other mechanisms for stronger guarantees of monotonicity are
+ # not implemented. Applications with stricter requirements should follow
+ # {Section 6.2}[https://www.rfc-editor.org/rfc/rfc9562.html#name-monotonicity-and-counters]
+ # of the specification.
+ #
+ def uuid_v7(extra_timestamp_bits: 0)
+ case (extra_timestamp_bits = Integer(extra_timestamp_bits))
+ when 0 # min timestamp precision
+ ms = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
+ rand = random_bytes(10)
+ rand.setbyte(0, rand.getbyte(0) & 0x0f | 0x70) # version
+ rand.setbyte(2, rand.getbyte(2) & 0x3f | 0x80) # variant
+ "%08x-%04x-%s" % [
+ (ms & 0x0000_ffff_ffff_0000) >> 16,
+ (ms & 0x0000_0000_0000_ffff),
+ rand.unpack("H4H4H12").join("-")
+ ]
+
+ when 12 # max timestamp precision
+ ms, ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
+ .divmod(1_000_000)
+ extra_bits = ns * 4096 / 1_000_000
+ rand = random_bytes(8)
+ rand.setbyte(0, rand.getbyte(0) & 0x3f | 0x80) # variant
+ "%08x-%04x-7%03x-%s" % [
+ (ms & 0x0000_ffff_ffff_0000) >> 16,
+ (ms & 0x0000_0000_0000_ffff),
+ extra_bits,
+ rand.unpack("H4H12").join("-")
+ ]
+
+ when (0..12) # the generic version is slower than the special cases above
+ rand_a, rand_b1, rand_b2, rand_b3 = random_bytes(10).unpack("nnnN")
+ rand_mask_bits = 12 - extra_timestamp_bits
+ ms, ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
+ .divmod(1_000_000)
+ "%08x-%04x-%04x-%04x-%04x%08x" % [
+ (ms & 0x0000_ffff_ffff_0000) >> 16,
+ (ms & 0x0000_0000_0000_ffff),
+ 0x7000 |
+ ((ns * (1 << extra_timestamp_bits) / 1_000_000) << rand_mask_bits) |
+ rand_a & ((1 << rand_mask_bits) - 1),
+ 0x8000 | (rand_b1 & 0x3fff),
+ rand_b2,
+ rand_b3
+ ]
+
+ else
+ raise ArgumentError, "extra_timestamp_bits must be in 0..12"
+ end
+ end
+
+ # Internal interface to Random; Generate random data _n_ bytes.
+ private def gen_random(n)
+ self.bytes(n)
+ end
+
+ # Generate a string that randomly draws from a
+ # source array of characters.
+ #
+ # The argument _source_ specifies the array of characters from which
+ # to generate the string.
+ # The argument _n_ specifies the length, in characters, of the string to be
+ # generated.
+ #
+ # The result may contain whatever characters are in the source array.
+ #
+ # require 'random/formatter'
+ #
+ # prng.choose([*'l'..'r'], 16) #=> "lmrqpoonmmlqlron"
+ # prng.choose([*'0'..'9'], 5) #=> "27309"
+ private def choose(source, n)
+ size = source.size
+ m = 1
+ limit = size
+ while limit * size <= 0x100000000
+ limit *= size
+ m += 1
+ end
+ result = ''.dup
+ while m <= n
+ rs = random_number(limit)
+ is = rs.digits(size)
+ (m-is.length).times { is << 0 }
+ result << source.values_at(*is).join('')
+ n -= m
+ end
+ if 0 < n
+ rs = random_number(limit)
+ is = rs.digits(size)
+ if is.length < n
+ (n-is.length).times { is << 0 }
+ else
+ is.pop while n < is.length
+ end
+ result.concat source.values_at(*is).join('')
+ end
+ result
+ end
+
+ # The default character list for #alphanumeric.
+ ALPHANUMERIC = [*'A'..'Z', *'a'..'z', *'0'..'9'].map(&:freeze).freeze
+
+ # Generate a random alphanumeric string.
+ #
+ # The argument _n_ specifies the length, in characters, of the alphanumeric
+ # string to be generated.
+ # The argument _chars_ specifies the character list which the result is
+ # consist of.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in the future.
+ #
+ # The result may contain A-Z, a-z and 0-9, unless _chars_ is specified.
+ #
+ # require 'random/formatter'
+ #
+ # Random.alphanumeric #=> "2BuBuLf3WfSKyQbR"
+ # # or
+ # prng = Random.new
+ # prng.alphanumeric(10) #=> "i6K93NdqiH"
+ #
+ # Random.alphanumeric(4, chars: [*"0".."9"]) #=> "2952"
+ # # or
+ # prng = Random.new
+ # prng.alphanumeric(10, chars: [*"!".."/"]) #=> ",.,++%/''."
+ def alphanumeric(n = nil, chars: ALPHANUMERIC)
+ n = 16 if n.nil?
+ choose(chars, n)
+ end
+end
diff --git a/lib/rational.rb b/lib/rational.rb
deleted file mode 100644
index 5acfa5433d..0000000000
--- a/lib/rational.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class Fixnum
-
- alias quof fdiv
- alias rdiv quo
-
- alias power! ** unless defined?(0.power!)
- alias rpower **
-
-end
-
-class Bignum
-
- alias quof fdiv
- alias rdiv quo
-
- alias power! ** unless defined?(0.power!)
- alias rpower **
-
-end
diff --git a/lib/rbconfig/datadir.rb b/lib/rbconfig/datadir.rb
deleted file mode 100644
index 5b8f07754a..0000000000
--- a/lib/rbconfig/datadir.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env ruby
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-
-module Config
-
- # Only define datadir if it doesn't already exist.
- unless Config.respond_to?(:datadir)
-
- # Return the path to the data directory associated with the given
- # package name. Normally this is just
- # "#{Config::CONFIG['datadir']}/#{package_name}", but may be
- # modified by packages like RubyGems to handle versioned data
- # directories.
- def Config.datadir(package_name)
- File.join(CONFIG['datadir'], package_name)
- end
-
- end
-end
diff --git a/lib/rdoc.rb b/lib/rdoc.rb
deleted file mode 100644
index f4fc3867cf..0000000000
--- a/lib/rdoc.rb
+++ /dev/null
@@ -1,395 +0,0 @@
-$DEBUG_RDOC = nil
-
-##
-# = \RDoc - Ruby Documentation System
-#
-# This package contains RDoc and RDoc::Markup. RDoc is an application that
-# produces documentation for one or more Ruby source files. It works similarly
-# to JavaDoc, parsing the source, and extracting the definition for classes,
-# modules, and methods (along with includes and requires). It associates with
-# these optional documentation contained in the immediately preceding comment
-# block, and then renders the result using a pluggable output formatter.
-# RDoc::Markup is a library that converts plain text into various output
-# formats. The markup library is used to interpret the comment blocks that
-# RDoc uses to document methods, classes, and so on.
-#
-# == Roadmap
-#
-# * If you want to use RDoc to create documentation for your Ruby source files,
-# read on.
-# * If you want to include extensions written in C, see RDoc::Parser::C
-# * If you want to drive RDoc programmatically, see RDoc::RDoc.
-# * If you want to use the library to format text blocks into HTML, have a look
-# at RDoc::Markup.
-# * If you want to try writing your own HTML output template, see
-# RDoc::Generator::HTML
-#
-# == Summary
-#
-# Once installed, you can create documentation using the +rdoc+ command
-#
-# % rdoc [options] [names...]
-#
-# For an up-to-date option summary, type
-# % rdoc --help
-#
-# A typical use might be to generate documentation for a package of Ruby
-# source (such as RDoc itself).
-#
-# % rdoc
-#
-# This command generates documentation for all the Ruby and C source
-# files in and below the current directory. These will be stored in a
-# documentation tree starting in the subdirectory +doc+.
-#
-# You can make this slightly more useful for your readers by having the
-# index page contain the documentation for the primary file. In our
-# case, we could type
-#
-# % rdoc --main rdoc.rb
-#
-# You'll find information on the various formatting tricks you can use
-# in comment blocks in the documentation this generates.
-#
-# RDoc uses file extensions to determine how to process each file. File names
-# ending +.rb+ and +.rbw+ are assumed to be Ruby source. Files
-# ending +.c+ are parsed as C files. All other files are assumed to
-# contain just Markup-style markup (with or without leading '#' comment
-# markers). If directory names are passed to RDoc, they are scanned
-# recursively for C and Ruby source files only.
-#
-# == \Options
-# rdoc can be passed a variety of command-line options. In addition,
-# options can be specified via the +RDOCOPT+ environment variable, which
-# functions similarly to the +RUBYOPT+ environment variable.
-#
-# % export RDOCOPT="-S"
-#
-# will make rdoc default to inline method source code. Command-line options
-# always will override those in +RDOCOPT+.
-#
-# Run
-#
-# % rdoc --help
-#
-# for full details on rdoc's options.
-#
-# Here are some of the most commonly used options.
-# [-d, --diagram]
-# Generate diagrams showing modules and
-# classes. You need dot V1.8.6 or later to
-# use the --diagram option correctly. Dot is
-# available from http://graphviz.org
-#
-# [-S, --inline-source]
-# Show method source code inline, rather than via a popup link.
-#
-# [-T, --template=NAME]
-# Set the template used when generating output.
-#
-# == Documenting Source Code
-#
-# Comment blocks can be written fairly naturally, either using +#+ on
-# successive lines of the comment, or by including the comment in
-# a =begin/=end block. If you use the latter form, the =begin line must be
-# flagged with an RDoc tag:
-#
-# =begin rdoc
-# Documentation to be processed by RDoc.
-#
-# ...
-# =end
-#
-# RDoc stops processing comments if it finds a comment line containing
-# a <tt>--</tt>. This can be used to separate external from internal
-# comments, or to stop a comment being associated with a method, class, or
-# module. Commenting can be turned back on with a line that starts with a
-# <tt>++</tt>.
-#
-# ##
-# # Extract the age and calculate the date-of-birth.
-# #--
-# # FIXME: fails if the birthday falls on February 29th
-# #++
-# # The DOB is returned as a Time object.
-#
-# def get_dob(person)
-# # ...
-# end
-#
-# Names of classes, files, and any method names containing an
-# underscore or preceded by a hash character are automatically hyperlinked
-# from comment text to their description.
-#
-# Method parameter lists are extracted and displayed with the method
-# description. If a method calls +yield+, then the parameters passed to yield
-# will also be displayed:
-#
-# def fred
-# ...
-# yield line, address
-#
-# This will get documented as:
-#
-# fred() { |line, address| ... }
-#
-# You can override this using a comment containing ':yields: ...' immediately
-# after the method definition
-#
-# def fred # :yields: index, position
-# # ...
-#
-# yield line, address
-#
-# which will get documented as
-#
-# fred() { |index, position| ... }
-#
-# +:yields:+ is an example of a documentation directive. These appear
-# immediately after the start of the document element they are modifying.
-#
-# == \Markup
-#
-# * The markup engine looks for a document's natural left margin. This is
-# used as the initial margin for the document.
-#
-# * Consecutive lines starting at this margin are considered to be a
-# paragraph.
-#
-# * If a paragraph starts with a "*", "-", or with "<digit>.", then it is
-# taken to be the start of a list. The margin in increased to be the first
-# non-space following the list start flag. Subsequent lines should be
-# indented to this new margin until the list ends. For example:
-#
-# * this is a list with three paragraphs in
-# the first item. This is the first paragraph.
-#
-# And this is the second paragraph.
-#
-# 1. This is an indented, numbered list.
-# 2. This is the second item in that list
-#
-# This is the third conventional paragraph in the
-# first list item.
-#
-# * This is the second item in the original list
-#
-# * You can also construct labeled lists, sometimes called description
-# or definition lists. Do this by putting the label in square brackets
-# and indenting the list body:
-#
-# [cat] a small furry mammal
-# that seems to sleep a lot
-#
-# [ant] a little insect that is known
-# to enjoy picnics
-#
-# A minor variation on labeled lists uses two colons to separate the
-# label from the list body:
-#
-# cat:: a small furry mammal
-# that seems to sleep a lot
-#
-# ant:: a little insect that is known
-# to enjoy picnics
-#
-# This latter style guarantees that the list bodies' left margins are
-# aligned: think of them as a two column table.
-#
-# * Any line that starts to the right of the current margin is treated
-# as verbatim text. This is useful for code listings. The example of a
-# list above is also verbatim text.
-#
-# * A line starting with an equals sign (=) is treated as a
-# heading. Level one headings have one equals sign, level two headings
-# have two,and so on.
-#
-# * A line starting with three or more hyphens (at the current indent)
-# generates a horizontal rule. The more hyphens, the thicker the rule
-# (within reason, and if supported by the output device)
-#
-# * You can use markup within text (except verbatim) to change the
-# appearance of parts of that text. Out of the box, RDoc::Markup
-# supports word-based and general markup.
-#
-# Word-based markup uses flag characters around individual words:
-#
-# [\*word*] displays word in a *bold* font
-# [\_word_] displays word in an _emphasized_ font
-# [\+word+] displays word in a +code+ font
-#
-# General markup affects text between a start delimiter and and end
-# delimiter. Not surprisingly, these delimiters look like HTML markup.
-#
-# [\<b>text...</b>] displays word in a *bold* font
-# [\<em>text...</em>] displays word in an _emphasized_ font
-# [\\<i>text...</i>] displays word in an <i>italicized</i> font
-# [\<tt>text...</tt>] displays word in a +code+ font
-#
-# Unlike conventional Wiki markup, general markup can cross line
-# boundaries. You can turn off the interpretation of markup by
-# preceding the first character with a backslash. This only works for
-# simple markup, not HTML-style markup.
-#
-# * Hyperlinks to the web starting http:, mailto:, ftp:, or www. are
-# recognized. An HTTP url that references an external image file is
-# converted into an inline <IMG..>. Hyperlinks starting 'link:' are
-# assumed to refer to local files whose path is relative to the --op
-# directory.
-#
-# Hyperlinks can also be of the form <tt>label</tt>[url], in which
-# case the label is used in the displayed text, and +url+ is
-# used as the target. If +label+ contains multiple words,
-# put it in braces: <em>{multi word label}[</em>url<em>]</em>.
-#
-# == Directives
-#
-# [+:nodoc:+ / +:nodoc:+ all]
-# This directive prevents documentation for the element from
-# being generated. For classes and modules, the methods, aliases,
-# constants, and attributes directly within the affected class or
-# module also will be omitted. By default, though, modules and
-# classes within that class of module _will_ be documented. This is
-# turned off by adding the +all+ modifier.
-#
-# module MyModule # :nodoc:
-# class Input
-# end
-# end
-#
-# module OtherModule # :nodoc: all
-# class Output
-# end
-# end
-#
-# In the above code, only class <tt>MyModule::Input</tt> will be documented.
-# The +:nodoc:+ directive is global across all files for the class or module
-# to which it applies, so use +:stopdoc:+/+:startdoc:+ to suppress
-# documentation only for a particular set of methods, etc.
-#
-# [+:doc:+]
-# Forces a method or attribute to be documented even if it wouldn't be
-# otherwise. Useful if, for example, you want to include documentation of a
-# particular private method.
-#
-# [+:notnew:+]
-# Only applicable to the +initialize+ instance method. Normally RDoc
-# assumes that the documentation and parameters for +initialize+ are
-# actually for the +new+ method, and so fakes out a +new+ for the class.
-# The +:notnew:+ modifier stops this. Remember that +initialize+ is private,
-# so you won't see the documentation unless you use the +-a+ command line
-# option.
-#
-# Comment blocks can contain other directives:
-#
-# [<tt>:section: title</tt>]
-# Starts a new section in the output. The title following +:section:+ is
-# used as the section heading, and the remainder of the comment containing
-# the section is used as introductory text. Subsequent methods, aliases,
-# attributes, and classes will be documented in this section. A :section:
-# comment block may have one or more lines before the :section: directive.
-# These will be removed, and any identical lines at the end of the block are
-# also removed. This allows you to add visual cues such as:
-#
-# # ----------------------------------------
-# # :section: My Section
-# # This is the section that I wrote.
-# # See it glisten in the noon-day sun.
-# # ----------------------------------------
-#
-# [+:call-seq:+]
-# Lines up to the next blank line in the comment are treated as the method's
-# calling sequence, overriding the default parsing of method parameters and
-# yield arguments.
-#
-# [+:include:+ _filename_]
-# \Include the contents of the named file at this point. The file will be
-# searched for in the directories listed by the +--include+ option, or in
-# the current directory by default. The contents of the file will be
-# shifted to have the same indentation as the ':' at the start of
-# the :include: directive.
-#
-# [+:title:+ _text_]
-# Sets the title for the document. Equivalent to the <tt>--title</tt>
-# command line parameter. (The command line parameter overrides any :title:
-# directive in the source).
-#
-# [+:enddoc:+]
-# Document nothing further at the current level.
-#
-# [+:main:+ _name_]
-# Equivalent to the <tt>--main</tt> command line parameter.
-#
-# [+:stopdoc:+ / +:startdoc:+]
-# Stop and start adding new documentation elements to the current container.
-# For example, if a class has a number of constants that you don't want to
-# document, put a +:stopdoc:+ before the first, and a +:startdoc:+ after the
-# last. If you don't specify a +:startdoc:+ by the end of the container,
-# disables documentation for the entire class or module.
-#
-# == Other stuff
-#
-# RDoc is currently being maintained by Eric Hodel <drbrain@segment7.net>
-#
-# Dave Thomas <dave@pragmaticprogrammer.com> is the original author of RDoc.
-#
-# == Credits
-#
-# * The Ruby parser in rdoc/parse.rb is based heavily on the outstanding
-# work of Keiju ISHITSUKA of Nippon Rational Inc, who produced the Ruby
-# parser for irb and the rtags package.
-#
-# * Code to diagram classes and modules was written by Sergey A Yanovitsky
-# (Jah) of Enticla.
-#
-# * Charset patch from MoonWolf.
-#
-# * Rich Kilmer wrote the kilmer.rb output template.
-#
-# * Dan Brickley led the design of the RDF format.
-#
-# == License
-#
-# RDoc is Copyright (c) 2001-2003 Dave Thomas, The Pragmatic Programmers. It
-# is free software, and may be redistributed under the terms specified
-# in the README file of the Ruby distribution.
-#
-# == Warranty
-#
-# This software is provided "as is" and without any express or implied
-# warranties, including, without limitation, the implied warranties of
-# merchantibility and fitness for a particular purpose.
-
-module RDoc
-
- ##
- # Exception thrown by any rdoc error.
-
- class Error < RuntimeError; end
-
- RDocError = Error # :nodoc:
-
- ##
- # RDoc version you are using
-
- VERSION = "2.2.2"
-
- ##
- # Name of the dotfile that contains the description of files to be processed
- # in the current directory
-
- DOT_DOC_FILENAME = ".document"
-
- GENERAL_MODIFIERS = %w[nodoc].freeze
-
- CLASS_MODIFIERS = GENERAL_MODIFIERS
-
- ATTR_MODIFIERS = GENERAL_MODIFIERS
-
- CONSTANT_MODIFIERS = GENERAL_MODIFIERS
-
- METHOD_MODIFIERS = GENERAL_MODIFIERS +
- %w[arg args yield yields notnew not-new not_new doc]
-
-end
-
diff --git a/lib/rdoc/README b/lib/rdoc/README
deleted file mode 100644
index f183c61f8d..0000000000
--- a/lib/rdoc/README
+++ /dev/null
@@ -1,232 +0,0 @@
-= RDOC - Ruby Documentation System
-
-This package contains RDoc and RDoc::Markup. RDoc is an application that
-produces documentation for one or more Ruby source files. We work similarly to
-JavaDoc, parsing the source, and extracting the definition for classes,
-modules, and methods (along with includes and requires). We associate with
-these optional documentation contained in the immediately preceding comment
-block, and then render the result using a pluggable output formatter.
-RDoc::Markup is a library that converts plain text into various output formats.
-The markup library is used to interpret the comment blocks that RDoc uses to
-document methods, classes, and so on.
-
-== Roadmap
-
-* If you want to use RDoc to create documentation for your Ruby source files,
- read on.
-* If you want to include extensions written in C, see RDoc::C_Parser
-* For information on the various markups available in comment blocks, see
- RDoc::Markup.
-* If you want to drive RDoc programmatically, see RDoc::RDoc.
-* If you want to use the library to format text blocks into HTML, have a look
- at RDoc::Markup.
-* If you want to try writing your own HTML output template, see
- RDoc::Generator::HTML
-
-== Summary
-
-Once installed, you can create documentation using the 'rdoc' command
-(the command is 'rdoc.bat' under Windows)
-
- % rdoc [options] [names...]
-
-Type "rdoc --help" for an up-to-date option summary.
-
-A typical use might be to generate documentation for a package of Ruby
-source (such as rdoc itself).
-
- % rdoc
-
-This command generates documentation for all the Ruby and C source
-files in and below the current directory. These will be stored in a
-documentation tree starting in the subdirectory 'doc'.
-
-You can make this slightly more useful for your readers by having the
-index page contain the documentation for the primary file. In our
-case, we could type
-
- % rdoc --main rdoc.rb
-
-You'll find information on the various formatting tricks you can use
-in comment blocks in the documentation this generates.
-
-RDoc uses file extensions to determine how to process each file. File names
-ending +.rb+ and <tt>.rbw</tt> are assumed to be Ruby source. Files
-ending +.c+ are parsed as C files. All other files are assumed to
-contain just Markup-style markup (with or without leading '#' comment markers).
-If directory names are passed to RDoc, they are scanned recursively for C and
-Ruby source files only.
-
-= Markup
-
-For information on how to make lists, hyperlinks, & etc. with RDoc, see
-RDoc::Markup.
-
-Comment blocks can be written fairly naturally, either using '#' on successive
-lines of the comment, or by including the comment in an =begin/=end block. If
-you use the latter form, the =begin line must be flagged with an RDoc tag:
-
- =begin rdoc
- Documentation to be processed by RDoc.
-
- ...
- =end
-
-RDoc stops processing comments if it finds a comment line containing '+#--+'.
-This can be used to separate external from internal comments, or to stop a
-comment being associated with a method, class, or module. Commenting can be
-turned back on with a line that starts '+#+++'.
-
- ##
- # Extract the age and calculate the date-of-birth.
- #--
- # FIXME: fails if the birthday falls on February 29th
- #++
- # The DOB is returned as a Time object.
-
- def get_dob(person)
- # ...
- end
-
-Names of classes, source files, and any method names containing an underscore
-or preceded by a hash character are automatically hyperlinked from comment text
-to their description.
-
-Method parameter lists are extracted and displayed with the method description.
-If a method calls +yield+, then the parameters passed to yield will also be
-displayed:
-
- def fred
- ...
- yield line, address
-
-This will get documented as:
-
- fred() { |line, address| ... }
-
-You can override this using a comment containing ':yields: ...' immediately
-after the method definition
-
- def fred # :yields: index, position
- # ...
-
- yield line, address
-
-which will get documented as
-
- fred() { |index, position| ... }
-
-+:yields:+ is an example of a documentation directive. These appear immediately
-after the start of the document element they are modifying.
-
-== Directives
-
-[+:nodoc:+ / +:nodoc:+ all]
- Don't include this element in the documentation. For classes
- and modules, the methods, aliases, constants, and attributes
- directly within the affected class or module will also be
- omitted. By default, though, modules and classes within that
- class of module _will_ be documented. This is turned off by
- adding the +all+ modifier.
-
- module MyModule # :nodoc:
- class Input
- end
- end
-
- module OtherModule # :nodoc: all
- class Output
- end
- end
-
- In the above code, only class +MyModule::Input+ will be documented.
-
-[+:doc:+]
- Force a method or attribute to be documented even if it wouldn't otherwise
- be. Useful if, for example, you want to include documentation of a
- particular private method.
-
-[+:notnew:+]
- Only applicable to the +initialize+ instance method. Normally RDoc assumes
- that the documentation and parameters for #initialize are actually for the
- ::new method, and so fakes out a ::new for the class. The :notnew: modifier
- stops this. Remember that #initialize is protected, so you won't see the
- documentation unless you use the -a command line option.
-
-Comment blocks can contain other directives:
-
-[+:section: title+]
- Starts a new section in the output. The title following +:section:+ is used
- as the section heading, and the remainder of the comment containing the
- section is used as introductory text. Subsequent methods, aliases,
- attributes, and classes will be documented in this section. A :section:
- comment block may have one or more lines before the :section: directive.
- These will be removed, and any identical lines at the end of the block are
- also removed. This allows you to add visual cues such as:
-
- # ----------------------------------------
- # :section: My Section
- # This is the section that I wrote.
- # See it glisten in the noon-day sun.
- # ----------------------------------------
-
-[+:call-seq:+]
- Lines up to the next blank line in the comment are treated as the method's
- calling sequence, overriding the default parsing of method parameters and
- yield arguments.
-
-[+:include:+ _filename_]
- Include the contents of the named file at this point. The file will be
- searched for in the directories listed by the +--include+ option, or in the
- current directory by default. The contents of the file will be shifted to
- have the same indentation as the ':' at the start of the :include: directive.
-
-[+:title:+ _text_]
- Sets the title for the document. Equivalent to the --title command line
- parameter. (The command line parameter overrides any :title: directive in
- the source).
-
-[+:enddoc:+]
- Document nothing further at the current level.
-
-[+:main:+ _name_]
- Equivalent to the --main command line parameter.
-
-[+:stopdoc:+ / +:startdoc:+]
- Stop and start adding new documentation elements to the current container.
- For example, if a class has a number of constants that you don't want to
- document, put a +:stopdoc:+ before the first, and a +:startdoc:+ after the
- last. If you don't specify a +:startdoc:+ by the end of the container,
- disables documentation for the entire class or module.
-
-= Other stuff
-
-Author:: Dave Thomas <dave@pragmaticprogrammer.com>
-
-== Credits
-
-* The Ruby parser in rdoc/parse.rb is based heavily on the outstanding
- work of Keiju ISHITSUKA of Nippon Rational Inc, who produced the Ruby
- parser for irb and the rtags package.
-
-* Code to diagram classes and modules was written by Sergey A Yanovitsky
- (Jah) of Enticla.
-
-* Charset patch from MoonWolf.
-
-* Rich Kilmer wrote the kilmer.rb output template.
-
-* Dan Brickley led the design of the RDF format.
-
-== License
-
-RDoc is Copyright (c) 2001-2003 Dave Thomas, The Pragmatic Programmers. It
-is free software, and may be redistributed under the terms specified
-in the README file of the Ruby distribution.
-
-== Warranty
-
-This software is provided "as is" and without any express or implied
-warranties, including, without limitation, the implied warranties of
-merchantibility and fitness for a particular purpose.
-
diff --git a/lib/rdoc/code_objects.rb b/lib/rdoc/code_objects.rb
deleted file mode 100644
index 0916b03398..0000000000
--- a/lib/rdoc/code_objects.rb
+++ /dev/null
@@ -1,1061 +0,0 @@
-# We represent the various high-level code constructs that appear
-# in Ruby programs: classes, modules, methods, and so on.
-
-require 'rdoc/tokenstream'
-
-module RDoc
-
- ##
- # We contain the common stuff for contexts (which are containers) and other
- # elements (methods, attributes and so on)
-
- class CodeObject
-
- attr_accessor :parent
-
- # We are the model of the code, but we know that at some point
- # we will be worked on by viewers. By implementing the Viewable
- # protocol, viewers can associated themselves with these objects.
-
- attr_accessor :viewer
-
- # are we done documenting (ie, did we come across a :enddoc:)?
-
- attr_accessor :done_documenting
-
- # Which section are we in
-
- attr_accessor :section
-
- # do we document ourselves?
-
- attr_reader :document_self
-
- def initialize
- @document_self = true
- @document_children = true
- @force_documentation = false
- @done_documenting = false
- end
-
- def document_self=(val)
- @document_self = val
- if !val
- remove_methods_etc
- end
- end
-
- # set and cleared by :startdoc: and :enddoc:, this is used to toggle
- # the capturing of documentation
- def start_doc
- @document_self = true
- @document_children = true
- end
-
- def stop_doc
- @document_self = false
- @document_children = false
- end
-
- # do we document ourselves and our children
-
- attr_reader :document_children
-
- def document_children=(val)
- @document_children = val
- if !val
- remove_classes_and_modules
- end
- end
-
- # Do we _force_ documentation, even is we wouldn't normally show the entity
- attr_accessor :force_documentation
-
- def parent_file_name
- @parent ? @parent.file_base_name : '(unknown)'
- end
-
- def parent_name
- @parent ? @parent.name : '(unknown)'
- end
-
- # Default callbacks to nothing, but this is overridden for classes
- # and modules
- def remove_classes_and_modules
- end
-
- def remove_methods_etc
- end
-
- # Access the code object's comment
- attr_reader :comment
-
- # Update the comment, but don't overwrite a real comment with an empty one
- def comment=(comment)
- @comment = comment unless comment.empty?
- end
-
- # There's a wee trick we pull. Comment blocks can have directives that
- # override the stuff we extract during the parse. So, we have a special
- # class method, attr_overridable, that lets code objects list
- # those directives. Wehn a comment is assigned, we then extract
- # out any matching directives and update our object
-
- def self.attr_overridable(name, *aliases)
- @overridables ||= {}
-
- attr_accessor name
-
- aliases.unshift name
- aliases.each do |directive_name|
- @overridables[directive_name.to_s] = name
- end
- end
-
- end
-
- ##
- # A Context is something that can hold modules, classes, methods,
- # attributes, aliases, requires, and includes. Classes, modules, and files
- # are all Contexts.
-
- class Context < CodeObject
-
- attr_reader :aliases
- attr_reader :attributes
- attr_reader :constants
- attr_reader :current_section
- attr_reader :in_files
- attr_reader :includes
- attr_reader :method_list
- attr_reader :name
- attr_reader :requires
- attr_reader :sections
- attr_reader :visibility
-
- class Section
- attr_reader :title, :comment, :sequence
-
- @@sequence = "SEC00000"
-
- def initialize(title, comment)
- @title = title
- @@sequence.succ!
- @sequence = @@sequence.dup
- @comment = nil
- set_comment(comment)
- end
-
- def ==(other)
- self.class === other and @sequence == other.sequence
- end
-
- def inspect
- "#<%s:0x%x %s %p>" % [
- self.class, object_id,
- @sequence, title
- ]
- end
-
- ##
- # Set the comment for this section from the original comment block If
- # the first line contains :section:, strip it and use the rest.
- # Otherwise remove lines up to the line containing :section:, and look
- # for those lines again at the end and remove them. This lets us write
- #
- # # ---------------------
- # # :SECTION: The title
- # # The body
- # # ---------------------
-
- def set_comment(comment)
- return unless comment
-
- if comment =~ /^#[ \t]*:section:.*\n/
- start = $`
- rest = $'
-
- if start.empty?
- @comment = rest
- else
- @comment = rest.sub(/#{start.chomp}\Z/, '')
- end
- else
- @comment = comment
- end
- @comment = nil if @comment.empty?
- end
-
- end
-
- def initialize
- super
-
- @in_files = []
-
- @name ||= "unknown"
- @comment ||= ""
- @parent = nil
- @visibility = :public
-
- @current_section = Section.new(nil, nil)
- @sections = [ @current_section ]
-
- initialize_methods_etc
- initialize_classes_and_modules
- end
-
- ##
- # map the class hash to an array externally
-
- def classes
- @classes.values
- end
-
- ##
- # map the module hash to an array externally
-
- def modules
- @modules.values
- end
-
- ##
- # return the classes Hash (only to be used internally)
-
- def classes_hash
- @classes
- end
- protected :classes_hash
-
- ##
- # return the modules Hash (only to be used internally)
-
- def modules_hash
- @modules
- end
- protected :modules_hash
-
- ##
- # Change the default visibility for new methods
-
- def ongoing_visibility=(vis)
- @visibility = vis
- end
-
- ##
- # Yields Method and Attr entries matching the list of names in +methods+.
- # Attributes are only returned when +singleton+ is false.
-
- def methods_matching(methods, singleton = false)
- count = 0
-
- @method_list.each do |m|
- if methods.include? m.name and m.singleton == singleton then
- yield m
- count += 1
- end
- end
-
- return if count == methods.size || singleton
-
- # perhaps we need to look at attributes
-
- @attributes.each do |a|
- yield a if methods.include? a.name
- end
- end
-
- ##
- # Given an array +methods+ of method names, set the visibility of the
- # corresponding AnyMethod object
-
- def set_visibility_for(methods, vis, singleton = false)
- methods_matching methods, singleton do |m|
- m.visibility = vis
- end
- end
-
- ##
- # Record the file that we happen to find it in
-
- def record_location(toplevel)
- @in_files << toplevel unless @in_files.include?(toplevel)
- end
-
- # Return true if at least part of this thing was defined in +file+
- def defined_in?(file)
- @in_files.include?(file)
- end
-
- def add_class(class_type, name, superclass)
- klass = add_class_or_module @classes, class_type, name, superclass
-
- #
- # If the parser encounters Container::Item before encountering
- # Container, then it assumes that Container is a module. This
- # may not be the case, so remove Container from the module list
- # if present and transfer any contained classes and modules to
- # the new class.
- #
- mod = @modules.delete(name)
-
- if mod then
- klass.classes_hash.update(mod.classes_hash)
- klass.modules_hash.update(mod.modules_hash)
- klass.method_list.concat(mod.method_list)
- end
-
- return klass
- end
-
- def add_module(class_type, name)
- add_class_or_module(@modules, class_type, name, nil)
- end
-
- def add_method(a_method)
- a_method.visibility = @visibility
- add_to(@method_list, a_method)
-
- unmatched_alias_list = @unmatched_alias_lists[a_method.name]
- if unmatched_alias_list then
- unmatched_alias_list.each do |unmatched_alias|
- add_alias_impl unmatched_alias, a_method
- @aliases.delete unmatched_alias
- end
-
- @unmatched_alias_lists.delete a_method.name
- end
- end
-
- def add_attribute(an_attribute)
- add_to(@attributes, an_attribute)
- end
-
- def add_alias_impl(an_alias, meth)
- new_meth = AnyMethod.new(an_alias.text, an_alias.new_name)
- new_meth.is_alias_for = meth
- new_meth.singleton = meth.singleton
- new_meth.params = meth.params
- new_meth.comment = "Alias for \##{meth.name}"
- meth.add_alias(new_meth)
- add_method(new_meth)
- end
-
- def add_alias(an_alias)
- meth = find_instance_method_named(an_alias.old_name)
-
- if meth then
- add_alias_impl(an_alias, meth)
- else
- add_to(@aliases, an_alias)
- unmatched_alias_list = @unmatched_alias_lists[an_alias.old_name] ||= []
- unmatched_alias_list.push(an_alias)
- end
-
- an_alias
- end
-
- def add_include(an_include)
- add_to(@includes, an_include)
- end
-
- def add_constant(const)
- add_to(@constants, const)
- end
-
- # Requires always get added to the top-level (file) context
- def add_require(a_require)
- if TopLevel === self then
- add_to @requires, a_require
- else
- parent.add_require a_require
- end
- end
-
- def add_class_or_module(collection, class_type, name, superclass=nil)
- cls = collection[name]
-
- if cls then
- cls.superclass = superclass unless cls.module?
- puts "Reusing class/module #{name}" if $DEBUG_RDOC
- else
- cls = class_type.new(name, superclass)
-# collection[name] = cls if @document_self && !@done_documenting
- collection[name] = cls if !@done_documenting
- cls.parent = self
- cls.section = @current_section
- end
- cls
- end
-
- def add_to(array, thing)
- array << thing if @document_self and not @done_documenting
- thing.parent = self
- thing.section = @current_section
- end
-
- # If a class's documentation is turned off after we've started
- # collecting methods etc., we need to remove the ones
- # we have
-
- def remove_methods_etc
- initialize_methods_etc
- end
-
- def initialize_methods_etc
- @method_list = []
- @attributes = []
- @aliases = []
- @requires = []
- @includes = []
- @constants = []
-
- # This Hash maps a method name to a list of unmatched
- # aliases (aliases of a method not yet encountered).
- @unmatched_alias_lists = {}
- end
-
- # and remove classes and modules when we see a :nodoc: all
- def remove_classes_and_modules
- initialize_classes_and_modules
- end
-
- def initialize_classes_and_modules
- @classes = {}
- @modules = {}
- end
-
- # Find a named module
- def find_module_named(name)
- # First check the enclosed modules, then check the module itself,
- # then check the enclosing modules (this mirrors the check done by
- # the Ruby parser)
- res = @modules[name] || @classes[name]
- return res if res
- return self if self.name == name
- find_enclosing_module_named(name)
- end
-
- # find a module at a higher scope
- def find_enclosing_module_named(name)
- parent && parent.find_module_named(name)
- end
-
- # Iterate over all the classes and modules in
- # this object
-
- def each_classmodule
- @modules.each_value {|m| yield m}
- @classes.each_value {|c| yield c}
- end
-
- def each_method
- @method_list.each {|m| yield m}
- end
-
- def each_attribute
- @attributes.each {|a| yield a}
- end
-
- def each_constant
- @constants.each {|c| yield c}
- end
-
- # Return the toplevel that owns us
-
- def toplevel
- return @toplevel if defined? @toplevel
- @toplevel = self
- @toplevel = @toplevel.parent until TopLevel === @toplevel
- @toplevel
- end
-
- # allow us to sort modules by name
- def <=>(other)
- name <=> other.name
- end
-
- ##
- # Look up +symbol+. If +method+ is non-nil, then we assume the symbol
- # references a module that contains that method.
-
- def find_symbol(symbol, method = nil)
- result = nil
-
- case symbol
- when /^::(.*)/ then
- result = toplevel.find_symbol($1)
- when /::/ then
- modules = symbol.split(/::/)
-
- unless modules.empty? then
- module_name = modules.shift
- result = find_module_named(module_name)
-
- if result then
- modules.each do |name|
- result = result.find_module_named(name)
- break unless result
- end
- end
- end
-
- else
- # if a method is specified, then we're definitely looking for
- # a module, otherwise it could be any symbol
- if method
- result = find_module_named(symbol)
- else
- result = find_local_symbol(symbol)
- if result.nil?
- if symbol =~ /^[A-Z]/
- result = parent
- while result && result.name != symbol
- result = result.parent
- end
- end
- end
- end
- end
-
- if result and method then
- fail unless result.respond_to? :find_local_symbol
- result = result.find_local_symbol(method)
- end
-
- result
- end
-
- def find_local_symbol(symbol)
- res = find_method_named(symbol) ||
- find_constant_named(symbol) ||
- find_attribute_named(symbol) ||
- find_module_named(symbol) ||
- find_file_named(symbol)
- end
-
- # Handle sections
-
- def set_current_section(title, comment)
- @current_section = Section.new(title, comment)
- @sections << @current_section
- end
-
- private
-
- # Find a named method, or return nil
- def find_method_named(name)
- @method_list.find {|meth| meth.name == name}
- end
-
- # Find a named instance method, or return nil
- def find_instance_method_named(name)
- @method_list.find {|meth| meth.name == name && !meth.singleton}
- end
-
- # Find a named constant, or return nil
- def find_constant_named(name)
- @constants.find {|m| m.name == name}
- end
-
- # Find a named attribute, or return nil
- def find_attribute_named(name)
- @attributes.find {|m| m.name == name}
- end
-
- ##
- # Find a named file, or return nil
-
- def find_file_named(name)
- toplevel.class.find_file_named(name)
- end
-
- end
-
- ##
- # A TopLevel context is a source file
-
- class TopLevel < Context
- attr_accessor :file_stat
- attr_accessor :file_relative_name
- attr_accessor :file_absolute_name
- attr_accessor :diagram
-
- @@all_classes = {}
- @@all_modules = {}
- @@all_files = {}
-
- def self.reset
- @@all_classes = {}
- @@all_modules = {}
- @@all_files = {}
- end
-
- def initialize(file_name)
- super()
- @name = "TopLevel"
- @file_relative_name = file_name
- @file_absolute_name = file_name
- @file_stat = File.stat(file_name)
- @diagram = nil
- @@all_files[file_name] = self
- end
-
- def file_base_name
- File.basename @file_absolute_name
- end
-
- def full_name
- nil
- end
-
- ##
- # Adding a class or module to a TopLevel is special, as we only want one
- # copy of a particular top-level class. For example, if both file A and
- # file B implement class C, we only want one ClassModule object for C.
- # This code arranges to share classes and modules between files.
-
- def add_class_or_module(collection, class_type, name, superclass)
- cls = collection[name]
-
- if cls then
- cls.superclass = superclass unless cls.module?
- puts "Reusing class/module #{cls.full_name}" if $DEBUG_RDOC
- else
- if class_type == NormalModule then
- all = @@all_modules
- else
- all = @@all_classes
- end
-
- cls = all[name]
-
- if !cls then
- cls = class_type.new name, superclass
- all[name] = cls unless @done_documenting
- else
- # If the class has been encountered already, check that its
- # superclass has been set (it may not have been, depending on
- # the context in which it was encountered).
- if class_type == NormalClass
- if !cls.superclass then
- cls.superclass = superclass
- end
- end
- end
-
- collection[name] = cls unless @done_documenting
-
- cls.parent = self
- end
-
- cls
- end
-
- def self.all_classes_and_modules
- @@all_classes.values + @@all_modules.values
- end
-
- def self.find_class_named(name)
- @@all_classes.each_value do |c|
- res = c.find_class_named(name)
- return res if res
- end
- nil
- end
-
- def self.find_file_named(name)
- @@all_files[name]
- end
-
- def find_local_symbol(symbol)
- find_class_or_module_named(symbol) || super
- end
-
- def find_class_or_module_named(symbol)
- @@all_classes.each_value {|c| return c if c.name == symbol}
- @@all_modules.each_value {|m| return m if m.name == symbol}
- nil
- end
-
- ##
- # Find a named module
-
- def find_module_named(name)
- find_class_or_module_named(name) || find_enclosing_module_named(name)
- end
-
- def inspect
- "#<%s:0x%x %p modules: %p classes: %p>" % [
- self.class, object_id,
- file_base_name,
- @modules.map { |n,m| m },
- @classes.map { |n,c| c }
- ]
- end
-
- end
-
- ##
- # ClassModule is the base class for objects representing either a class or a
- # module.
-
- class ClassModule < Context
-
- attr_accessor :diagram
-
- def initialize(name, superclass = nil)
- @name = name
- @diagram = nil
- @superclass = superclass
- @comment = ""
- super()
- end
-
- def find_class_named(name)
- return self if full_name == name
- @classes.each_value {|c| return c if c.find_class_named(name) }
- nil
- end
-
- ##
- # Return the fully qualified name of this class or module
-
- def full_name
- if @parent && @parent.full_name
- @parent.full_name + "::" + @name
- else
- @name
- end
- end
-
- def http_url(prefix)
- path = full_name.split("::")
- File.join(prefix, *path) + ".html"
- end
-
- ##
- # Does this object represent a module?
-
- def module?
- false
- end
-
- ##
- # Get the superclass of this class. Attempts to retrieve the superclass'
- # real name by following module nesting.
-
- def superclass
- raise NoMethodError, "#{full_name} is a module" if module?
-
- scope = self
-
- begin
- superclass = scope.classes.find { |c| c.name == @superclass }
-
- return superclass.full_name if superclass
- scope = scope.parent
- end until scope.nil? or TopLevel === scope
-
- @superclass
- end
-
- ##
- # Set the superclass of this class
-
- def superclass=(superclass)
- raise NoMethodError, "#{full_name} is a module" if module?
-
- if @superclass.nil? or @superclass == 'Object' then
- @superclass = superclass
- end
- end
-
- def to_s
- "#{self.class}: #{@name} #{@comment} #{super}"
- end
-
- end
-
- ##
- # Anonymous classes
-
- class AnonClass < ClassModule
- end
-
- ##
- # Normal classes
-
- class NormalClass < ClassModule
-
- def inspect
- superclass = @superclass ? " < #{@superclass}" : nil
- "<%s:0x%x class %s%s includes: %p attributes: %p methods: %p aliases: %p>" % [
- self.class, object_id,
- @name, superclass, @includes, @attributes, @method_list, @aliases
- ]
- end
-
- end
-
- ##
- # Singleton classes
-
- class SingleClass < ClassModule
- end
-
- ##
- # Module
-
- class NormalModule < ClassModule
-
- def comment=(comment)
- return if comment.empty?
- comment = @comment << "# ---\n" << comment unless @comment.empty?
-
- super
- end
-
- def inspect
- "#<%s:0x%x module %s includes: %p attributes: %p methods: %p aliases: %p>" % [
- self.class, object_id,
- @name, @includes, @attributes, @method_list, @aliases
- ]
- end
-
- def module?
- true
- end
-
- end
-
- ##
- # AnyMethod is the base class for objects representing methods
-
- class AnyMethod < CodeObject
-
- attr_accessor :name
- attr_accessor :visibility
- attr_accessor :block_params
- attr_accessor :dont_rename_initialize
- attr_accessor :singleton
- attr_reader :text
-
- # list of other names for this method
- attr_reader :aliases
-
- # method we're aliasing
- attr_accessor :is_alias_for
-
- attr_overridable :params, :param, :parameters, :parameter
-
- attr_accessor :call_seq
-
- include TokenStream
-
- def initialize(text, name)
- super()
- @text = text
- @name = name
- @token_stream = nil
- @visibility = :public
- @dont_rename_initialize = false
- @block_params = nil
- @aliases = []
- @is_alias_for = nil
- @comment = ""
- @call_seq = nil
- end
-
- def <=>(other)
- @name <=> other.name
- end
-
- def add_alias(method)
- @aliases << method
- end
-
- def inspect
- alias_for = @is_alias_for ? " (alias for #{@is_alias_for.name})" : nil
- "#<%s:0x%x %s%s%s (%s)%s>" % [
- self.class, object_id,
- parent_name,
- singleton ? '::' : '#',
- name,
- visibility,
- alias_for,
- ]
- end
-
- def param_seq
- params = params.gsub(/\s*\#.*/, '')
- params = params.tr("\n", " ").squeeze(" ")
- params = "(#{params})" unless p[0] == ?(
-
- if block = block_params then # yes, =
- # If this method has explicit block parameters, remove any explicit
- # &block
- params.sub!(/,?\s*&\w+/)
-
- block.gsub!(/\s*\#.*/, '')
- block = block.tr("\n", " ").squeeze(" ")
- if block[0] == ?(
- block.sub!(/^\(/, '').sub!(/\)/, '')
- end
- params << " { |#{block}| ... }"
- end
-
- params
- end
-
- def to_s
- res = self.class.name + ": " + @name + " (" + @text + ")\n"
- res << @comment.to_s
- res
- end
-
- end
-
- ##
- # GhostMethod represents a method referenced only by a comment
-
- class GhostMethod < AnyMethod
- end
-
- ##
- # MetaMethod represents a meta-programmed method
-
- class MetaMethod < AnyMethod
- end
-
- ##
- # Represent an alias, which is an old_name/ new_name pair associated with a
- # particular context
-
- class Alias < CodeObject
-
- attr_accessor :text, :old_name, :new_name, :comment
-
- def initialize(text, old_name, new_name, comment)
- super()
- @text = text
- @old_name = old_name
- @new_name = new_name
- self.comment = comment
- end
-
- def inspect
- "#<%s:0x%x %s.alias_method %s, %s>" % [
- self.class, object_id,
- parent.name, @old_name, @new_name,
- ]
- end
-
- def to_s
- "alias: #{self.old_name} -> #{self.new_name}\n#{self.comment}"
- end
-
- end
-
- ##
- # Represent a constant
-
- class Constant < CodeObject
- attr_accessor :name, :value
-
- def initialize(name, value, comment)
- super()
- @name = name
- @value = value
- self.comment = comment
- end
- end
-
- ##
- # Represent attributes
-
- class Attr < CodeObject
- attr_accessor :text, :name, :rw, :visibility
-
- def initialize(text, name, rw, comment)
- super()
- @text = text
- @name = name
- @rw = rw
- @visibility = :public
- self.comment = comment
- end
-
- def <=>(other)
- self.name <=> other.name
- end
-
- def inspect
- attr = case rw
- when 'RW' then :attr_accessor
- when 'R' then :attr_reader
- when 'W' then :attr_writer
- else
- " (#{rw})"
- end
-
- "#<%s:0x%x %s.%s :%s>" % [
- self.class, object_id,
- parent_name, attr, @name,
- ]
- end
-
- def to_s
- "attr: #{self.name} #{self.rw}\n#{self.comment}"
- end
-
- end
-
- ##
- # A required file
-
- class Require < CodeObject
- attr_accessor :name
-
- def initialize(name, comment)
- super()
- @name = name.gsub(/'|"/, "") #'
- self.comment = comment
- end
-
- def inspect
- "#<%s:0x%x require '%s' in %s>" % [
- self.class,
- object_id,
- @name,
- parent_file_name,
- ]
- end
-
- end
-
- ##
- # An included module
-
- class Include < CodeObject
-
- attr_accessor :name
-
- def initialize(name, comment)
- super()
- @name = name
- self.comment = comment
-
- end
-
- def inspect
- "#<%s:0x%x %s.include %s>" % [
- self.class,
- object_id,
- parent_name, @name,
- ]
- end
-
- end
-
-end
diff --git a/lib/rdoc/diagram.rb b/lib/rdoc/diagram.rb
deleted file mode 100644
index 4aa2ec5656..0000000000
--- a/lib/rdoc/diagram.rb
+++ /dev/null
@@ -1,340 +0,0 @@
-# A wonderful hack by to draw package diagrams using the dot package.
-# Originally written by Jah, team Enticla.
-#
-# You must have the V1.7 or later in your path
-# http://www.research.att.com/sw/tools/graphviz/
-
-require 'rdoc/dot'
-
-module RDoc
-
- ##
- # Draw a set of diagrams representing the modules and classes in the
- # system. We draw one diagram for each file, and one for each toplevel
- # class or module. This means there will be overlap. However, it also
- # means that you'll get better context for objects.
- #
- # To use, simply
- #
- # d = Diagram.new(info) # pass in collection of top level infos
- # d.draw
- #
- # The results will be written to the +dot+ subdirectory. The process
- # also sets the +diagram+ attribute in each object it graphs to
- # the name of the file containing the image. This can be used
- # by output generators to insert images.
-
- class Diagram
-
- FONT = "Arial"
-
- DOT_PATH = "dot"
-
- ##
- # Pass in the set of top level objects. The method also creates the
- # subdirectory to hold the images
-
- def initialize(info, options)
- @info = info
- @options = options
- @counter = 0
- FileUtils.mkdir_p(DOT_PATH)
- @diagram_cache = {}
- end
-
- ##
- # Draw the diagrams. We traverse the files, drawing a diagram for each. We
- # also traverse each top-level class and module in that file drawing a
- # diagram for these too.
-
- def draw
- unless @options.quiet
- $stderr.print "Diagrams: "
- $stderr.flush
- end
-
- @info.each_with_index do |i, file_count|
- @done_modules = {}
- @local_names = find_names(i)
- @global_names = []
- @global_graph = graph = DOT::Digraph.new('name' => 'TopLevel',
- 'fontname' => FONT,
- 'fontsize' => '8',
- 'bgcolor' => 'lightcyan1',
- 'compound' => 'true')
-
- # it's a little hack %) i'm too lazy to create a separate class
- # for default node
- graph << DOT::Node.new('name' => 'node',
- 'fontname' => FONT,
- 'color' => 'black',
- 'fontsize' => 8)
-
- i.modules.each do |mod|
- draw_module(mod, graph, true, i.file_relative_name)
- end
- add_classes(i, graph, i.file_relative_name)
-
- i.diagram = convert_to_png("f_#{file_count}", graph)
-
- # now go through and document each top level class and
- # module independently
- i.modules.each_with_index do |mod, count|
- @done_modules = {}
- @local_names = find_names(mod)
- @global_names = []
-
- @global_graph = graph = DOT::Digraph.new('name' => 'TopLevel',
- 'fontname' => FONT,
- 'fontsize' => '8',
- 'bgcolor' => 'lightcyan1',
- 'compound' => 'true')
-
- graph << DOT::Node.new('name' => 'node',
- 'fontname' => FONT,
- 'color' => 'black',
- 'fontsize' => 8)
- draw_module(mod, graph, true)
- mod.diagram = convert_to_png("m_#{file_count}_#{count}",
- graph)
- end
- end
- $stderr.puts unless @options.quiet
- end
-
- private
-
- def find_names(mod)
- return [mod.full_name] + mod.classes.collect{|cl| cl.full_name} +
- mod.modules.collect{|m| find_names(m)}.flatten
- end
-
- def find_full_name(name, mod)
- full_name = name.dup
- return full_name if @local_names.include?(full_name)
- mod_path = mod.full_name.split('::')[0..-2]
- unless mod_path.nil?
- until mod_path.empty?
- full_name = mod_path.pop + '::' + full_name
- return full_name if @local_names.include?(full_name)
- end
- end
- return name
- end
-
- def draw_module(mod, graph, toplevel = false, file = nil)
- return if @done_modules[mod.full_name] and not toplevel
-
- @counter += 1
- url = mod.http_url("classes")
- m = DOT::Subgraph.new('name' => "cluster_#{mod.full_name.gsub( /:/,'_' )}",
- 'label' => mod.name,
- 'fontname' => FONT,
- 'color' => 'blue',
- 'style' => 'filled',
- 'URL' => %{"#{url}"},
- 'fillcolor' => toplevel ? 'palegreen1' : 'palegreen3')
-
- @done_modules[mod.full_name] = m
- add_classes(mod, m, file)
- graph << m
-
- unless mod.includes.empty?
- mod.includes.each do |inc|
- m_full_name = find_full_name(inc.name, mod)
- if @local_names.include?(m_full_name)
- @global_graph << DOT::Edge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
- 'to' => "#{mod.full_name.gsub( /:/,'_' )}",
- 'ltail' => "cluster_#{m_full_name.gsub( /:/,'_' )}",
- 'lhead' => "cluster_#{mod.full_name.gsub( /:/,'_' )}")
- else
- unless @global_names.include?(m_full_name)
- path = m_full_name.split("::")
- url = File.join('classes', *path) + ".html"
- @global_graph << DOT::Node.new('name' => "#{m_full_name.gsub( /:/,'_' )}",
- 'shape' => 'box',
- 'label' => "#{m_full_name}",
- 'URL' => %{"#{url}"})
- @global_names << m_full_name
- end
- @global_graph << DOT::Edge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
- 'to' => "#{mod.full_name.gsub( /:/,'_' )}",
- 'lhead' => "cluster_#{mod.full_name.gsub( /:/,'_' )}")
- end
- end
- end
- end
-
- def add_classes(container, graph, file = nil )
-
- use_fileboxes = @options.fileboxes
-
- files = {}
-
- # create dummy node (needed if empty and for module includes)
- if container.full_name
- graph << DOT::Node.new('name' => "#{container.full_name.gsub( /:/,'_' )}",
- 'label' => "",
- 'width' => (container.classes.empty? and
- container.modules.empty?) ?
- '0.75' : '0.01',
- 'height' => '0.01',
- 'shape' => 'plaintext')
- end
-
- container.classes.each_with_index do |cl, cl_index|
- last_file = cl.in_files[-1].file_relative_name
-
- if use_fileboxes && !files.include?(last_file)
- @counter += 1
- files[last_file] =
- DOT::Subgraph.new('name' => "cluster_#{@counter}",
- 'label' => "#{last_file}",
- 'fontname' => FONT,
- 'color'=>
- last_file == file ? 'red' : 'black')
- end
-
- next if cl.name == 'Object' || cl.name[0,2] == "<<"
-
- url = cl.http_url("classes")
-
- label = cl.name.dup
- if use_fileboxes && cl.in_files.length > 1
- label << '\n[' +
- cl.in_files.collect {|i|
- i.file_relative_name
- }.sort.join( '\n' ) +
- ']'
- end
-
- attrs = {
- 'name' => "#{cl.full_name.gsub( /:/, '_' )}",
- 'fontcolor' => 'black',
- 'style'=>'filled',
- 'color'=>'palegoldenrod',
- 'label' => label,
- 'shape' => 'ellipse',
- 'URL' => %{"#{url}"}
- }
-
- c = DOT::Node.new(attrs)
-
- if use_fileboxes
- files[last_file].push c
- else
- graph << c
- end
- end
-
- if use_fileboxes
- files.each_value do |val|
- graph << val
- end
- end
-
- unless container.classes.empty?
- container.classes.each_with_index do |cl, cl_index|
- cl.includes.each do |m|
- m_full_name = find_full_name(m.name, cl)
- if @local_names.include?(m_full_name)
- @global_graph << DOT::Edge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
- 'to' => "#{cl.full_name.gsub( /:/,'_' )}",
- 'ltail' => "cluster_#{m_full_name.gsub( /:/,'_' )}")
- else
- unless @global_names.include?(m_full_name)
- path = m_full_name.split("::")
- url = File.join('classes', *path) + ".html"
- @global_graph << DOT::Node.new('name' => "#{m_full_name.gsub( /:/,'_' )}",
- 'shape' => 'box',
- 'label' => "#{m_full_name}",
- 'URL' => %{"#{url}"})
- @global_names << m_full_name
- end
- @global_graph << DOT::Edge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
- 'to' => "#{cl.full_name.gsub( /:/, '_')}")
- end
- end
-
- sclass = cl.superclass
- next if sclass.nil? || sclass == 'Object'
- sclass_full_name = find_full_name(sclass,cl)
- unless @local_names.include?(sclass_full_name) or @global_names.include?(sclass_full_name)
- path = sclass_full_name.split("::")
- url = File.join('classes', *path) + ".html"
- @global_graph << DOT::Node.new('name' => "#{sclass_full_name.gsub( /:/, '_' )}",
- 'label' => sclass_full_name,
- 'URL' => %{"#{url}"})
- @global_names << sclass_full_name
- end
- @global_graph << DOT::Edge.new('from' => "#{sclass_full_name.gsub( /:/,'_' )}",
- 'to' => "#{cl.full_name.gsub( /:/, '_')}")
- end
- end
-
- container.modules.each do |submod|
- draw_module(submod, graph)
- end
-
- end
-
- def convert_to_png(file_base, graph)
- str = graph.to_s
- return @diagram_cache[str] if @diagram_cache[str]
- op_type = @options.image_format
- dotfile = File.join(DOT_PATH, file_base)
- src = dotfile + ".dot"
- dot = dotfile + "." + op_type
-
- unless @options.quiet
- $stderr.print "."
- $stderr.flush
- end
-
- File.open(src, 'w+' ) do |f|
- f << str << "\n"
- end
-
- system "dot", "-T#{op_type}", src, "-o", dot
-
- # Now construct the imagemap wrapper around
- # that png
-
- ret = wrap_in_image_map(src, dot)
- @diagram_cache[str] = ret
- return ret
- end
-
- ##
- # Extract the client-side image map from dot, and use it to generate the
- # imagemap proper. Return the whole <map>..<img> combination, suitable for
- # inclusion on the page
-
- def wrap_in_image_map(src, dot)
- res = ""
- dot_map = `dot -Tismap #{src}`
-
- if(!dot_map.empty?)
- res << %{<map id="map" name="map">\n}
- dot_map.split($/).each do |area|
- unless area =~ /^rectangle \((\d+),(\d+)\) \((\d+),(\d+)\) ([\/\w.]+)\s*(.*)/
- $stderr.puts "Unexpected output from dot:\n#{area}"
- return nil
- end
-
- xs, ys = [$1.to_i, $3.to_i], [$2.to_i, $4.to_i]
- url, area_name = $5, $6
-
- res << %{ <area shape="rect" coords="#{xs.min},#{ys.min},#{xs.max},#{ys.max}" }
- res << %{ href="#{url}" alt="#{area_name}" />\n}
- end
- res << "</map>\n"
- end
-
- res << %{<img src="#{dot}" usemap="#map" alt="#{dot}" />}
- return res
- end
-
- end
-
-end
diff --git a/lib/rdoc/dot.rb b/lib/rdoc/dot.rb
deleted file mode 100644
index fbd2cfba02..0000000000
--- a/lib/rdoc/dot.rb
+++ /dev/null
@@ -1,249 +0,0 @@
-module RDoc; end
-
-module RDoc::DOT
-
- TAB = ' '
- TAB2 = TAB * 2
-
- # options for node declaration
- NODE_OPTS = [
- 'bgcolor',
- 'color',
- 'fontcolor',
- 'fontname',
- 'fontsize',
- 'height',
- 'width',
- 'label',
- 'layer',
- 'rank',
- 'shape',
- 'shapefile',
- 'style',
- 'URL',
- ]
-
- # options for edge declaration
- EDGE_OPTS = [
- 'color',
- 'decorate',
- 'dir',
- 'fontcolor',
- 'fontname',
- 'fontsize',
- 'id',
- 'label',
- 'layer',
- 'lhead',
- 'ltail',
- 'minlen',
- 'style',
- 'weight'
- ]
-
- # options for graph declaration
- GRAPH_OPTS = [
- 'bgcolor',
- 'center',
- 'clusterrank',
- 'color',
- 'compound',
- 'concentrate',
- 'fillcolor',
- 'fontcolor',
- 'fontname',
- 'fontsize',
- 'label',
- 'layerseq',
- 'margin',
- 'mclimit',
- 'nodesep',
- 'nslimit',
- 'ordering',
- 'orientation',
- 'page',
- 'rank',
- 'rankdir',
- 'ranksep',
- 'ratio',
- 'size',
- 'style',
- 'URL'
- ]
-
- # a root class for any element in dot notation
- class SimpleElement
- attr_accessor :name
-
- def initialize( params = {} )
- @label = params['name'] ? params['name'] : ''
- end
-
- def to_s
- @name
- end
- end
-
- # an element that has options ( node, edge or graph )
- class Element < SimpleElement
- #attr_reader :parent
- attr_accessor :name, :options
-
- def initialize( params = {}, option_list = [] )
- super( params )
- @name = params['name'] ? params['name'] : nil
- @parent = params['parent'] ? params['parent'] : nil
- @options = {}
- option_list.each{ |i|
- @options[i] = params[i] if params[i]
- }
- @options['label'] ||= @name if @name != 'node'
- end
-
- def each_option
- @options.each{ |i| yield i }
- end
-
- def each_option_pair
- @options.each_pair{ |key, val| yield key, val }
- end
-
- #def parent=( thing )
- # @parent.delete( self ) if defined?( @parent ) and @parent
- # @parent = thing
- #end
- end
-
-
- # this is used when we build nodes that have shape=record
- # ports don't have options :)
- class Port < SimpleElement
- attr_accessor :label
-
- def initialize( params = {} )
- super( params )
- @name = params['label'] ? params['label'] : ''
- end
- def to_s
- ( @name && @name != "" ? "<#{@name}>" : "" ) + "#{@label}"
- end
- end
-
- # node element
- class Node < Element
-
- def initialize( params = {}, option_list = NODE_OPTS )
- super( params, option_list )
- @ports = params['ports'] ? params['ports'] : []
- end
-
- def each_port
- @ports.each{ |i| yield i }
- end
-
- def << ( thing )
- @ports << thing
- end
-
- def push ( thing )
- @ports.push( thing )
- end
-
- def pop
- @ports.pop
- end
-
- def to_s( t = '' )
-
- label = @options['shape'] != 'record' && @ports.length == 0 ?
- @options['label'] ?
- t + TAB + "label = \"#{@options['label']}\"\n" :
- '' :
- t + TAB + 'label = "' + " \\\n" +
- t + TAB2 + "#{@options['label']}| \\\n" +
- @ports.collect{ |i|
- t + TAB2 + i.to_s
- }.join( "| \\\n" ) + " \\\n" +
- t + TAB + '"' + "\n"
-
- t + "#{@name} [\n" +
- @options.to_a.collect{ |i|
- i[1] && i[0] != 'label' ?
- t + TAB + "#{i[0]} = #{i[1]}" : nil
- }.compact.join( ",\n" ) + ( label != '' ? ",\n" : "\n" ) +
- label +
- t + "]\n"
- end
- end
-
- # subgraph element is the same to graph, but has another header in dot
- # notation
- class Subgraph < Element
-
- def initialize( params = {}, option_list = GRAPH_OPTS )
- super( params, option_list )
- @nodes = params['nodes'] ? params['nodes'] : []
- @dot_string = 'subgraph'
- end
-
- def each_node
- @nodes.each{ |i| yield i }
- end
-
- def << ( thing )
- @nodes << thing
- end
-
- def push( thing )
- @nodes.push( thing )
- end
-
- def pop
- @nodes.pop
- end
-
- def to_s( t = '' )
- hdr = t + "#{@dot_string} #{@name} {\n"
-
- options = @options.to_a.collect{ |name, val|
- val && name != 'label' ?
- t + TAB + "#{name} = #{val}" :
- name ? t + TAB + "#{name} = \"#{val}\"" : nil
- }.compact.join( "\n" ) + "\n"
-
- nodes = @nodes.collect{ |i|
- i.to_s( t + TAB )
- }.join( "\n" ) + "\n"
- hdr + options + nodes + t + "}\n"
- end
- end
-
- # this is graph
- class Digraph < Subgraph
- def initialize( params = {}, option_list = GRAPH_OPTS )
- super( params, option_list )
- @dot_string = 'digraph'
- end
- end
-
- # this is edge
- class Edge < Element
- attr_accessor :from, :to
- def initialize( params = {}, option_list = EDGE_OPTS )
- super( params, option_list )
- @from = params['from'] ? params['from'] : nil
- @to = params['to'] ? params['to'] : nil
- end
-
- def to_s( t = '' )
- t + "#{@from} -> #{to} [\n" +
- @options.to_a.collect{ |i|
- i[1] && i[0] != 'label' ?
- t + TAB + "#{i[0]} = #{i[1]}" :
- i[1] ? t + TAB + "#{i[0]} = \"#{i[1]}\"" : nil
- }.compact.join( "\n" ) + "\n" + t + "]\n"
- end
- end
-
-end
-
diff --git a/lib/rdoc/generator.rb b/lib/rdoc/generator.rb
deleted file mode 100644
index d695e661d1..0000000000
--- a/lib/rdoc/generator.rb
+++ /dev/null
@@ -1,1082 +0,0 @@
-require 'cgi'
-require 'rdoc'
-require 'rdoc/options'
-require 'rdoc/markup/to_html_crossref'
-require 'rdoc/template'
-
-module RDoc::Generator
-
- ##
- # Name of sub-directory that holds file descriptions
-
- FILE_DIR = "files"
-
- ##
- # Name of sub-directory that holds class descriptions
-
- CLASS_DIR = "classes"
-
- ##
- # Name of the RDoc CSS file
-
- CSS_NAME = "rdoc-style.css"
-
- ##
- # Build a hash of all items that can be cross-referenced. This is used when
- # we output required and included names: if the names appear in this hash,
- # we can generate an html cross reference to the appropriate description.
- # We also use this when parsing comment blocks: any decorated words matching
- # an entry in this list are hyperlinked.
-
- class AllReferences
- @@refs = {}
-
- def AllReferences::reset
- @@refs = {}
- end
-
- def AllReferences.add(name, html_class)
- @@refs[name] = html_class
- end
-
- def AllReferences.[](name)
- @@refs[name]
- end
-
- def AllReferences.keys
- @@refs.keys
- end
- end
-
- ##
- # Handle common markup tasks for the various Context subclasses
-
- module MarkUp
-
- ##
- # Convert a string in markup format into HTML.
-
- def markup(str, remove_para = false)
- return '' unless str
-
- # Convert leading comment markers to spaces, but only if all non-blank
- # lines have them
- if str =~ /^(?>\s*)[^\#]/ then
- content = str
- else
- content = str.gsub(/^\s*(#+)/) { $1.tr '#', ' ' }
- end
-
- res = formatter.convert content
-
- if remove_para then
- res.sub!(/^<p>/, '')
- res.sub!(/<\/p>$/, '')
- end
-
- res
- end
-
- ##
- # Qualify a stylesheet URL; if if +css_name+ does not begin with '/' or
- # 'http[s]://', prepend a prefix relative to +path+. Otherwise, return it
- # unmodified.
-
- def style_url(path, css_name=nil)
-# $stderr.puts "style_url( #{path.inspect}, #{css_name.inspect} )"
- css_name ||= CSS_NAME
- if %r{^(https?:/)?/} =~ css_name
- css_name
- else
- RDoc::Markup::ToHtml.gen_relative_url path, css_name
- end
- end
-
- ##
- # Build a webcvs URL with the given 'url' argument. URLs with a '%s' in them
- # get the file's path sprintfed into them; otherwise they're just catenated
- # together.
-
- def cvs_url(url, full_path)
- if /%s/ =~ url
- return sprintf( url, full_path )
- else
- return url + full_path
- end
- end
-
- end
-
- ##
- # A Context is built by the parser to represent a container: contexts hold
- # classes, modules, methods, require lists and include lists. ClassModule
- # and TopLevel are the context objects we process here
-
- class Context
-
- include MarkUp
-
- attr_reader :context
-
- ##
- # Generate:
- #
- # * a list of RDoc::Generator::File objects for each TopLevel object
- # * a list of RDoc::Generator::Class objects for each first level class or
- # module in the TopLevel objects
- # * a complete list of all hyperlinkable terms (file, class, module, and
- # method names)
-
- def self.build_indices(toplevels, options)
- files = []
- classes = []
-
- toplevels.each do |toplevel|
- files << RDoc::Generator::File.new(toplevel, options,
- RDoc::Generator::FILE_DIR)
- end
-
- RDoc::TopLevel.all_classes_and_modules.each do |cls|
- build_class_list(classes, options, cls, files[0],
- RDoc::Generator::CLASS_DIR)
- end
-
- return files, classes
- end
-
- def self.build_class_list(classes, options, from, html_file, class_dir)
- classes << RDoc::Generator::Class.new(from, html_file, class_dir, options)
-
- from.each_classmodule do |mod|
- build_class_list(classes, options, mod, html_file, class_dir)
- end
- end
-
- def initialize(context, options)
- @context = context
- @options = options
-
- # HACK ugly
- @template = options.template_class
- end
-
- def formatter
- @formatter ||= @options.formatter ||
- RDoc::Markup::ToHtmlCrossref.new(path, self, @options.show_hash)
- end
-
- ##
- # convenience method to build a hyperlink
-
- def href(link, cls, name)
- %{<a href="#{link}" class="#{cls}">#{name}</a>} #"
- end
-
- ##
- # Returns a reference to outselves to be used as an href= the form depends
- # on whether we're all in one file or in multiple files
-
- def as_href(from_path)
- if @options.all_one_file
- "#" + path
- else
- RDoc::Markup::ToHtml.gen_relative_url from_path, path
- end
- end
-
- ##
- # Create a list of Method objects for each method in the corresponding
- # context object. If the @options.show_all variable is set (corresponding
- # to the <tt>--all</tt> option, we include all methods, otherwise just the
- # public ones.
-
- def collect_methods
- list = @context.method_list
-
- unless @options.show_all then
- list = list.select do |m|
- m.visibility == :public or
- m.visibility == :protected or
- m.force_documentation
- end
- end
-
- @methods = list.collect do |m|
- RDoc::Generator::Method.new m, self, @options
- end
- end
-
- ##
- # Build a summary list of all the methods in this context
-
- def build_method_summary_list(path_prefix = "")
- collect_methods unless @methods
-
- @methods.sort.map do |meth|
- {
- "name" => CGI.escapeHTML(meth.name),
- "aref" => "##{meth.aref}"
- }
- end
- end
-
- ##
- # Build a list of aliases for which we couldn't find a
- # corresponding method
-
- def build_alias_summary_list(section)
- @context.aliases.map do |al|
- next unless al.section == section
-
- res = {
- 'old_name' => al.old_name,
- 'new_name' => al.new_name,
- }
-
- if al.comment and not al.comment.empty? then
- res['desc'] = markup al.comment, true
- end
-
- res
- end.compact
- end
-
- ##
- # Build a list of constants
-
- def build_constants_summary_list(section)
- @context.constants.map do |co|
- next unless co.section == section
-
- res = {
- 'name' => co.name,
- 'value' => CGI.escapeHTML(co.value)
- }
-
- if co.comment and not co.comment.empty? then
- res['desc'] = markup co.comment, true
- end
-
- res
- end.compact
- end
-
- def build_requires_list(context)
- potentially_referenced_list(context.requires) {|fn| [fn + ".rb"] }
- end
-
- def build_include_list(context)
- potentially_referenced_list(context.includes)
- end
-
- ##
- # Build a list from an array of Context items. Look up each in the
- # AllReferences hash: if we find a corresponding entry, we generate a
- # hyperlink to it, otherwise just output the name. However, some names
- # potentially need massaging. For example, you may require a Ruby file
- # without the .rb extension, but the file names we know about may have it.
- # To deal with this, we pass in a block which performs the massaging,
- # returning an array of alternative names to match
-
- def potentially_referenced_list(array)
- res = []
- array.each do |i|
- ref = AllReferences[i.name]
-# if !ref
-# container = @context.parent
-# while !ref && container
-# name = container.name + "::" + i.name
-# ref = AllReferences[name]
-# container = container.parent
-# end
-# end
-
- ref = @context.find_symbol(i.name)
- ref = ref.viewer if ref
-
- if !ref && block_given?
- possibles = yield(i.name)
- while !ref and !possibles.empty?
- ref = AllReferences[possibles.shift]
- end
- end
- h_name = CGI.escapeHTML(i.name)
- if ref and ref.document_self
- path = url(ref.path)
- res << { "name" => h_name, "aref" => path }
- else
- res << { "name" => h_name }
- end
- end
- res
- end
-
- ##
- # Build an array of arrays of method details. The outer array has up
- # to six entries, public, private, and protected for both class
- # methods, the other for instance methods. The inner arrays contain
- # a hash for each method
-
- def build_method_detail_list(section)
- outer = []
-
- methods = @methods.sort.select do |m|
- m.document_self and m.section == section
- end
-
- for singleton in [true, false]
- for vis in [ :public, :protected, :private ]
- res = []
- methods.each do |m|
- next unless m.visibility == vis and m.singleton == singleton
-
- row = {}
-
- if m.call_seq then
- row["callseq"] = m.call_seq.gsub(/->/, '&rarr;')
- else
- row["name"] = CGI.escapeHTML(m.name)
- row["params"] = m.params
- end
-
- desc = m.description.strip
- row["m_desc"] = desc unless desc.empty?
- row["aref"] = m.aref
- row["visibility"] = m.visibility.to_s
-
- alias_names = []
-
- m.aliases.each do |other|
- if other.viewer then # won't be if the alias is private
- alias_names << {
- 'name' => other.name,
- 'aref' => other.viewer.as_href(path)
- }
- end
- end
-
- row["aka"] = alias_names unless alias_names.empty?
-
- if @options.inline_source then
- code = m.source_code
- row["sourcecode"] = code if code
- else
- code = m.src_url
- if code then
- row["codeurl"] = code
- row["imgurl"] = m.img_url
- end
- end
-
- res << row
- end
-
- if res.size > 0 then
- outer << {
- "type" => vis.to_s.capitalize,
- "category" => singleton ? "Class" : "Instance",
- "methods" => res
- }
- end
- end
- end
-
- outer
- end
-
- ##
- # Build the structured list of classes and modules contained
- # in this context.
-
- def build_class_list(level, from, section, infile=nil)
- prefix = '&nbsp;&nbsp;::' * level;
- res = ''
-
- from.modules.sort.each do |mod|
- next unless mod.section == section
- next if infile && !mod.defined_in?(infile)
- if mod.document_self
- res <<
- prefix <<
- 'Module ' <<
- href(url(mod.viewer.path), 'link', mod.full_name) <<
- "<br />\n" <<
- build_class_list(level + 1, mod, section, infile)
- end
- end
-
- from.classes.sort.each do |cls|
- next unless cls.section == section
- next if infile and not cls.defined_in?(infile)
-
- if cls.document_self
- res <<
- prefix <<
- 'Class ' <<
- href(url(cls.viewer.path), 'link', cls.full_name) <<
- "<br />\n" <<
- build_class_list(level + 1, cls, section, infile)
- end
- end
-
- res
- end
-
- def url(target)
- RDoc::Markup::ToHtml.gen_relative_url path, target
- end
-
- def aref_to(target)
- if @options.all_one_file
- "#" + target
- else
- url(target)
- end
- end
-
- def document_self
- @context.document_self
- end
-
- def diagram_reference(diagram)
- res = diagram.gsub(/((?:src|href)=")(.*?)"/) {
- $1 + url($2) + '"'
- }
- res
- end
-
- ##
- # Find a symbol in ourselves or our parent
-
- def find_symbol(symbol, method=nil)
- res = @context.find_symbol(symbol, method)
- if res
- res = res.viewer
- end
- res
- end
-
- ##
- # create table of contents if we contain sections
-
- def add_table_of_sections
- toc = []
- @context.sections.each do |section|
- if section.title then
- toc << {
- 'secname' => section.title,
- 'href' => section.sequence
- }
- end
- end
-
- @values['toc'] = toc unless toc.empty?
- end
-
- end
-
- ##
- # Wrap a ClassModule context
-
- class Class < Context
-
- attr_reader :methods
- attr_reader :path
- attr_reader :values
-
- def initialize(context, html_file, prefix, options)
- super context, options
-
- @html_file = html_file
- @html_class = self
- @is_module = context.module?
- @values = {}
-
- context.viewer = self
-
- if options.all_one_file
- @path = context.full_name
- else
- @path = http_url(context.full_name, prefix)
- end
-
- collect_methods
-
- AllReferences.add(name, self)
- end
-
- ##
- # Returns the relative file name to store this class in, which is also its
- # url
-
- def http_url(full_name, prefix)
- path = full_name.dup
-
- path.gsub!(/<<\s*(\w*)/, 'from-\1') if path['<<']
-
- ::File.join(prefix, path.split("::")) + ".html"
- end
-
- def name
- @context.full_name
- end
-
- def parent_name
- @context.parent.full_name
- end
-
- def index_name
- name
- end
-
- def write_on(f, file_list, class_list, method_list, overrides = {})
- value_hash
-
- @values['file_list'] = file_list
- @values['class_list'] = class_list
- @values['method_list'] = method_list
-
- @values.update overrides
-
- template = RDoc::TemplatePage.new(@template::BODY,
- @template::CLASS_PAGE,
- @template::METHOD_LIST)
-
- template.write_html_on(f, @values)
- end
-
- def value_hash
- class_attribute_values
- add_table_of_sections
-
- @values["charset"] = @options.charset
- @values["style_url"] = style_url(path, @options.css)
-
- d = markup(@context.comment)
- @values["description"] = d unless d.empty?
-
- ml = build_method_summary_list @path
- @values["methods"] = ml unless ml.empty?
-
- il = build_include_list @context
- @values["includes"] = il unless il.empty?
-
- @values["sections"] = @context.sections.map do |section|
- secdata = {
- "sectitle" => section.title,
- "secsequence" => section.sequence,
- "seccomment" => markup(section.comment),
- }
-
- al = build_alias_summary_list section
- secdata["aliases"] = al unless al.empty?
-
- co = build_constants_summary_list section
- secdata["constants"] = co unless co.empty?
-
- al = build_attribute_list section
- secdata["attributes"] = al unless al.empty?
-
- cl = build_class_list 0, @context, section
- secdata["classlist"] = cl unless cl.empty?
-
- mdl = build_method_detail_list section
- secdata["method_list"] = mdl unless mdl.empty?
-
- secdata
- end
-
- @values
- end
-
- def build_attribute_list(section)
- @context.attributes.sort.map do |att|
- next unless att.section == section
-
- if att.visibility == :public or att.visibility == :protected or
- @options.show_all then
-
- entry = {
- "name" => CGI.escapeHTML(att.name),
- "rw" => att.rw,
- "a_desc" => markup(att.comment, true)
- }
-
- unless att.visibility == :public or att.visibility == :protected then
- entry["rw"] << "-"
- end
-
- entry
- end
- end.compact
- end
-
- def class_attribute_values
- h_name = CGI.escapeHTML(name)
-
- @values["href"] = @path
- @values["classmod"] = @is_module ? "Module" : "Class"
- @values["title"] = "#{@values['classmod']}: #{h_name} [#{@options.title}]"
-
- c = @context
- c = c.parent while c and not c.diagram
-
- if c and c.diagram then
- @values["diagram"] = diagram_reference(c.diagram)
- end
-
- @values["full_name"] = h_name
-
- if not @context.module? and @context.superclass then
- parent_class = @context.superclass
- @values["parent"] = CGI.escapeHTML(parent_class)
-
- if parent_name
- lookup = parent_name + "::" + parent_class
- else
- lookup = parent_class
- end
-
- parent_url = AllReferences[lookup] || AllReferences[parent_class]
-
- if parent_url and parent_url.document_self
- @values["par_url"] = aref_to(parent_url.path)
- end
- end
-
- files = []
- @context.in_files.each do |f|
- res = {}
- full_path = CGI.escapeHTML(f.file_absolute_name)
-
- res["full_path"] = full_path
- res["full_path_url"] = aref_to(f.viewer.path) if f.document_self
-
- if @options.webcvs
- res["cvsurl"] = cvs_url( @options.webcvs, full_path )
- end
-
- files << res
- end
-
- @values['infiles'] = files
- end
-
- def <=>(other)
- self.name <=> other.name
- end
-
- end
-
- ##
- # Handles the mapping of a file's information to HTML. In reality, a file
- # corresponds to a +TopLevel+ object, containing modules, classes, and
- # top-level methods. In theory it _could_ contain attributes and aliases,
- # but we ignore these for now.
-
- class File < Context
-
- attr_reader :path
- attr_reader :name
- attr_reader :values
-
- def initialize(context, options, file_dir)
- super context, options
-
- @values = {}
-
- if options.all_one_file
- @path = filename_to_label
- else
- @path = http_url(file_dir)
- end
-
- @name = @context.file_relative_name
-
- collect_methods
- AllReferences.add(name, self)
- context.viewer = self
- end
-
- def http_url(file_dir)
- ::File.join file_dir, "#{@context.file_relative_name.tr '.', '_'}.html"
- end
-
- def filename_to_label
- @context.file_relative_name.gsub(/%|\/|\?|\#/) do
- ('%%%x' % $&[0]).unpack('C')
- end
- end
-
- def index_name
- name
- end
-
- def parent_name
- nil
- end
-
- def value_hash
- file_attribute_values
- add_table_of_sections
-
- @values["charset"] = @options.charset
- @values["href"] = path
- @values["style_url"] = style_url(path, @options.css)
-
- if @context.comment
- d = markup(@context.comment)
- @values["description"] = d if d.size > 0
- end
-
- ml = build_method_summary_list
- @values["methods"] = ml unless ml.empty?
-
- il = build_include_list(@context)
- @values["includes"] = il unless il.empty?
-
- rl = build_requires_list(@context)
- @values["requires"] = rl unless rl.empty?
-
- if @options.promiscuous
- file_context = nil
- else
- file_context = @context
- end
-
-
- @values["sections"] = @context.sections.map do |section|
-
- secdata = {
- "sectitle" => section.title,
- "secsequence" => section.sequence,
- "seccomment" => markup(section.comment)
- }
-
- cl = build_class_list(0, @context, section, file_context)
- secdata["classlist"] = cl unless cl.empty?
-
- mdl = build_method_detail_list(section)
- secdata["method_list"] = mdl unless mdl.empty?
-
- al = build_alias_summary_list(section)
- secdata["aliases"] = al unless al.empty?
-
- co = build_constants_summary_list(section)
- secdata["constants"] = co unless co.empty?
-
- secdata
- end
-
- @values
- end
-
- def write_on(f, file_list, class_list, method_list, overrides = {})
- value_hash
-
- @values['file_list'] = file_list
- @values['class_list'] = class_list
- @values['method_list'] = method_list
-
- @values.update overrides
-
- template = RDoc::TemplatePage.new(@template::BODY,
- @template::FILE_PAGE,
- @template::METHOD_LIST)
-
- template.write_html_on(f, @values)
- end
-
- def file_attribute_values
- full_path = @context.file_absolute_name
- short_name = ::File.basename full_path
-
- @values["title"] = CGI.escapeHTML("File: #{short_name} [#{@options.title}]")
-
- if @context.diagram then
- @values["diagram"] = diagram_reference(@context.diagram)
- end
-
- @values["short_name"] = CGI.escapeHTML(short_name)
- @values["full_path"] = CGI.escapeHTML(full_path)
- @values["dtm_modified"] = @context.file_stat.mtime.to_s
-
- if @options.webcvs then
- @values["cvsurl"] = cvs_url @options.webcvs, @values["full_path"]
- end
- end
-
- def <=>(other)
- self.name <=> other.name
- end
-
- end
-
- class Method
-
- include MarkUp
-
- attr_reader :context
- attr_reader :src_url
- attr_reader :img_url
- attr_reader :source_code
-
- def self.all_methods
- @@all_methods
- end
-
- def self.reset
- @@all_methods = []
- @@seq = "M000000"
- end
-
- # Initialize the class variables.
- self.reset
-
- def initialize(context, html_class, options)
- # TODO: rethink the class hierarchy here...
- @context = context
- @html_class = html_class
- @options = options
-
- @@seq = @@seq.succ
- @seq = @@seq
-
- # HACK ugly
- @template = options.template_class
-
- @@all_methods << self
-
- context.viewer = self
-
- if (ts = @context.token_stream)
- @source_code = markup_code(ts)
- unless @options.inline_source
- @src_url = create_source_code_file(@source_code)
- @img_url = RDoc::Markup::ToHtml.gen_relative_url path, 'source.png'
- end
- end
-
- AllReferences.add(name, self)
- end
-
- ##
- # Returns a reference to outselves to be used as an href= the form depends
- # on whether we're all in one file or in multiple files
-
- def as_href(from_path)
- if @options.all_one_file
- "#" + path
- else
- RDoc::Markup::ToHtml.gen_relative_url from_path, path
- end
- end
-
- def formatter
- @formatter ||= @options.formatter ||
- RDoc::Markup::ToHtmlCrossref.new(path, self, @options.show_hash)
- end
-
- def inspect
- alias_for = if @context.is_alias_for then
- " (alias_for #{@context.is_alias_for})"
- else
- nil
- end
-
- "#<%s:0x%x %s%s%s (%s)%s>" % [
- self.class, object_id,
- @context.parent.name,
- @context.singleton ? '::' : '#',
- name,
- @context.visibility,
- alias_for
- ]
- end
-
- def name
- @context.name
- end
-
- def section
- @context.section
- end
-
- def index_name
- "#{@context.name} (#{@html_class.name})"
- end
-
- def parent_name
- if @context.parent.parent
- @context.parent.parent.full_name
- else
- nil
- end
- end
-
- def aref
- @seq
- end
-
- def path
- if @options.all_one_file
- aref
- else
- @html_class.path + "#" + aref
- end
- end
-
- def description
- markup(@context.comment)
- end
-
- def visibility
- @context.visibility
- end
-
- def singleton
- @context.singleton
- end
-
- def call_seq
- cs = @context.call_seq
- if cs
- cs.gsub(/\n/, "<br />\n")
- else
- nil
- end
- end
-
- def params
- # params coming from a call-seq in 'C' will start with the
- # method name
- params = @context.params
- if params !~ /^\w/
- params = @context.params.gsub(/\s*\#.*/, '')
- params = params.tr("\n", " ").squeeze(" ")
- params = "(" + params + ")" unless params[0] == ?(
-
- if (block = @context.block_params)
- # If this method has explicit block parameters, remove any
- # explicit &block
-
- params.sub!(/,?\s*&\w+/, '')
-
- block.gsub!(/\s*\#.*/, '')
- block = block.tr("\n", " ").squeeze(" ")
- if block[0] == ?(
- block.sub!(/^\(/, '').sub!(/\)/, '')
- end
- params << " {|#{block.strip}| ...}"
- end
- end
- CGI.escapeHTML(params)
- end
-
- def create_source_code_file(code_body)
- meth_path = @html_class.path.sub(/\.html$/, '.src')
- FileUtils.mkdir_p(meth_path)
- file_path = ::File.join meth_path, "#{@seq}.html"
-
- template = RDoc::TemplatePage.new(@template::SRC_PAGE)
-
- open file_path, 'w' do |f|
- values = {
- 'title' => CGI.escapeHTML(index_name),
- 'code' => code_body,
- 'style_url' => style_url(file_path, @options.css),
- 'charset' => @options.charset
- }
- template.write_html_on(f, values)
- end
-
- RDoc::Markup::ToHtml.gen_relative_url path, file_path
- end
-
- def <=>(other)
- @context <=> other.context
- end
-
- ##
- # Given a sequence of source tokens, mark up the source code
- # to make it look purty.
-
- def markup_code(tokens)
- src = ""
- tokens.each do |t|
- next unless t
-# style = STYLE_MAP[t.class]
- style = case t
- when RDoc::RubyToken::TkCONSTANT then "ruby-constant"
- when RDoc::RubyToken::TkKW then "ruby-keyword kw"
- when RDoc::RubyToken::TkIVAR then "ruby-ivar"
- when RDoc::RubyToken::TkOp then "ruby-operator"
- when RDoc::RubyToken::TkId then "ruby-identifier"
- when RDoc::RubyToken::TkNode then "ruby-node"
- when RDoc::RubyToken::TkCOMMENT then "ruby-comment cmt"
- when RDoc::RubyToken::TkREGEXP then "ruby-regexp re"
- when RDoc::RubyToken::TkSTRING then "ruby-value str"
- when RDoc::RubyToken::TkVal then "ruby-value"
- else
- nil
- end
-
- text = CGI.escapeHTML(t.text)
-
- if style
- src << "<span class=\"#{style}\">#{text}</span>"
- else
- src << text
- end
- end
-
- add_line_numbers(src) if @options.include_line_numbers
- src
- end
-
- ##
- # We rely on the fact that the first line of a source code listing has
- # # File xxxxx, line dddd
-
- def add_line_numbers(src)
- if src =~ /\A.*, line (\d+)/
- first = $1.to_i - 1
- last = first + src.count("\n")
- size = last.to_s.length
- fmt = "%#{size}d: "
- is_first_line = true
- line_num = first
- src.gsub!(/^/) do
- if is_first_line then
- is_first_line = false
- res = " " * (size+2)
- else
- res = sprintf(fmt, line_num)
- end
-
- line_num += 1
- res
- end
- end
- end
-
- def document_self
- @context.document_self
- end
-
- def aliases
- @context.aliases
- end
-
- def find_symbol(symbol, method=nil)
- res = @context.parent.find_symbol(symbol, method)
- if res
- res = res.viewer
- end
- res
- end
-
- end
-
-end
-
diff --git a/lib/rdoc/generator/chm.rb b/lib/rdoc/generator/chm.rb
deleted file mode 100644
index 7537365842..0000000000
--- a/lib/rdoc/generator/chm.rb
+++ /dev/null
@@ -1,113 +0,0 @@
-require 'rdoc/generator/html'
-
-class RDoc::Generator::CHM < RDoc::Generator::HTML
-
- HHC_PATH = "c:/Program Files/HTML Help Workshop/hhc.exe"
-
- ##
- # Standard generator factory
-
- def self.for(options)
- new(options)
- end
-
- def initialize(*args)
- super
- @op_name = @options.op_name || "rdoc"
- check_for_html_help_workshop
- end
-
- def check_for_html_help_workshop
- stat = File.stat(HHC_PATH)
- rescue
- $stderr <<
- "\n.chm output generation requires that Microsoft's Html Help\n" <<
- "Workshop is installed. RDoc looks for it in:\n\n " <<
- HHC_PATH <<
- "\n\nYou can download a copy for free from:\n\n" <<
- " http://msdn.microsoft.com/library/default.asp?" <<
- "url=/library/en-us/htmlhelp/html/hwMicrosoftHTMLHelpDownloads.asp\n\n"
- end
-
- ##
- # Generate the html as normal, then wrap it in a help project
-
- def generate(info)
- super
- @project_name = @op_name + ".hhp"
- create_help_project
- end
-
- ##
- # The project contains the project file, a table of contents and an index
-
- def create_help_project
- create_project_file
- create_contents_and_index
- compile_project
- end
-
- ##
- # The project file links together all the various
- # files that go to make up the help.
-
- def create_project_file
- template = RDoc::TemplatePage.new @template::HPP_FILE
- values = { "title" => @options.title, "opname" => @op_name }
- files = []
- @files.each do |f|
- files << { "html_file_name" => f.path }
- end
-
- values['all_html_files'] = files
-
- File.open(@project_name, "w") do |f|
- template.write_html_on(f, values)
- end
- end
-
- ##
- # The contents is a list of all files and modules.
- # For each we include as sub-entries the list
- # of methods they contain. As we build the contents
- # we also build an index file
-
- def create_contents_and_index
- contents = []
- index = []
-
- (@files+@classes).sort.each do |entry|
- content_entry = { "c_name" => entry.name, "ref" => entry.path }
- index << { "name" => entry.name, "aref" => entry.path }
-
- internals = []
-
- methods = entry.build_method_summary_list(entry.path)
-
- content_entry["methods"] = methods unless methods.empty?
- contents << content_entry
- index.concat methods
- end
-
- values = { "contents" => contents }
- template = RDoc::TemplatePage.new @template::CONTENTS
- File.open("contents.hhc", "w") do |f|
- template.write_html_on(f, values)
- end
-
- values = { "index" => index }
- template = RDoc::TemplatePage.new @template::CHM_INDEX
- File.open("index.hhk", "w") do |f|
- template.write_html_on(f, values)
- end
- end
-
- ##
- # Invoke the windows help compiler to compiler the project
-
- def compile_project
- system(HHC_PATH, @project_name)
- end
-
-end
-
diff --git a/lib/rdoc/generator/chm/chm.rb b/lib/rdoc/generator/chm/chm.rb
deleted file mode 100644
index cceeca5dfc..0000000000
--- a/lib/rdoc/generator/chm/chm.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-require 'rdoc/generator/chm'
-require 'rdoc/generator/html/html'
-
-module RDoc::Generator::CHM::CHM
-
- HTML = RDoc::Generator::HTML::HTML
-
- INDEX = HTML::INDEX
-
- STYLE = HTML::STYLE
-
- CLASS_INDEX = HTML::CLASS_INDEX
- CLASS_PAGE = HTML::CLASS_PAGE
- FILE_INDEX = HTML::FILE_INDEX
- FILE_PAGE = HTML::FILE_PAGE
- METHOD_INDEX = HTML::METHOD_INDEX
- METHOD_LIST = HTML::METHOD_LIST
-
- FR_INDEX_BODY = HTML::FR_INDEX_BODY
-
- # This is a nasty little hack, but hhc doesn't support the <?xml tag, so...
- BODY = HTML::BODY.sub!(/<\?xml.*\?>/, '')
- SRC_PAGE = HTML::SRC_PAGE.sub!(/<\?xml.*\?>/, '')
-
- HPP_FILE = <<-EOF
-[OPTIONS]
-Auto Index = Yes
-Compatibility=1.1 or later
-Compiled file=<%= values["opname"] %>.chm
-Contents file=contents.hhc
-Full-text search=Yes
-Index file=index.hhk
-Language=0x409 English(United States)
-Title=<%= values["title"] %>
-
-[FILES]
-<% values["all_html_files"].each do |all_html_files| %>
-<%= all_html_files["html_file_name"] %>
-<% end # values["all_html_files"] %>
- EOF
-
- CONTENTS = <<-EOF
-<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
-<HTML>
-<HEAD>
-<meta name="GENERATOR" content="Microsoft&reg; HTML Help Workshop 4.1">
-<!-- Sitemap 1.0 -->
-</HEAD><BODY>
-<OBJECT type="text/site properties">
- <param name="Foreground" value="0x80">
- <param name="Window Styles" value="0x800025">
- <param name="ImageType" value="Folder">
-</OBJECT>
-<UL>
-<% values["contents"].each do |contents| %>
- <LI> <OBJECT type="text/sitemap">
- <param name="Name" value="<%= contents["c_name"] %>">
- <param name="Local" value="<%= contents["ref"] %>">
- </OBJECT>
-<% if contents["methods"] then %>
-<ul>
-<% contents["methods"].each do |methods| %>
- <LI> <OBJECT type="text/sitemap">
- <param name="Name" value="<%= methods["name"] %>">
- <param name="Local" value="<%= methods["aref"] %>">
- </OBJECT>
-<% end # contents["methods"] %>
-</ul>
-<% end %>
- </LI>
-<% end # values["contents"] %>
-</UL>
-</BODY></HTML>
- EOF
-
- CHM_INDEX = <<-EOF
-<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
-<HTML>
-<HEAD>
-<meta name="GENERATOR" content="Microsoft&reg; HTML Help Workshop 4.1">
-<!-- Sitemap 1.0 -->
-</HEAD><BODY>
-<OBJECT type="text/site properties">
- <param name="Foreground" value="0x80">
- <param name="Window Styles" value="0x800025">
- <param name="ImageType" value="Folder">
-</OBJECT>
-<UL>
-<% values["index"].each do |index| %>
- <LI> <OBJECT type="text/sitemap">
- <param name="Name" value="<%= index["name"] %>">
- <param name="Local" value="<%= index["aref"] %>">
- </OBJECT>
-<% end # values["index"] %>
-</UL>
-</BODY></HTML>
- EOF
-
-end
-
diff --git a/lib/rdoc/generator/html.rb b/lib/rdoc/generator/html.rb
deleted file mode 100644
index d136de7b00..0000000000
--- a/lib/rdoc/generator/html.rb
+++ /dev/null
@@ -1,445 +0,0 @@
-require 'fileutils'
-
-require 'rdoc/generator'
-require 'rdoc/markup/to_html'
-
-##
-# We're responsible for generating all the HTML files from the object tree
-# defined in code_objects.rb. We generate:
-#
-# [files] an html file for each input file given. These
-# input files appear as objects of class
-# TopLevel
-#
-# [classes] an html file for each class or module encountered.
-# These classes are not grouped by file: if a file
-# contains four classes, we'll generate an html
-# file for the file itself, and four html files
-# for the individual classes.
-#
-# [indices] we generate three indices for files, classes,
-# and methods. These are displayed in a browser
-# like window with three index panes across the
-# top and the selected description below
-#
-# Method descriptions appear in whatever entity (file, class, or module) that
-# contains them.
-#
-# We generate files in a structure below a specified subdirectory, normally
-# +doc+.
-#
-# opdir
-# |
-# |___ files
-# | |__ per file summaries
-# |
-# |___ classes
-# |__ per class/module descriptions
-#
-# HTML is generated using the Template class.
-
-class RDoc::Generator::HTML
-
- include RDoc::Generator::MarkUp
-
- ##
- # Generator may need to return specific subclasses depending on the
- # options they are passed. Because of this we create them using a factory
-
- def self.for(options)
- RDoc::Generator::AllReferences.reset
- RDoc::Generator::Method.reset
-
- if options.all_one_file
- RDoc::Generator::HTMLInOne.new options
- else
- new options
- end
- end
-
- class << self
- protected :new
- end
-
- ##
- # Set up a new HTML generator. Basically all we do here is load up the
- # correct output temlate
-
- def initialize(options) #:not-new:
- @options = options
- load_html_template
- end
-
- ##
- # Build the initial indices and output objects
- # based on an array of TopLevel objects containing
- # the extracted information.
-
- def generate(toplevels)
- @toplevels = toplevels
- @files = []
- @classes = []
-
- write_style_sheet
- gen_sub_directories
- build_indices
- generate_html
- end
-
- private
-
- ##
- # Load up the HTML template specified in the options.
- # If the template name contains a slash, use it literally
-
- def load_html_template
- #
- # If the template is not a path, first look for it
- # in rdoc's HTML template directory. Perhaps this behavior should
- # be reversed (first try to include the template and, only if that
- # fails, try to include it in the default template directory).
- # One danger with reversing the behavior, however, is that
- # if something like require 'html' could load up an
- # unrelated file in the standard library or in a gem.
- #
- template = @options.template
-
- unless template =~ %r{/|\\} then
- template = File.join('rdoc', 'generator', @options.generator.key,
- template)
- end
-
- begin
- require template
-
- @template = self.class.const_get @options.template.upcase
- @options.template_class = @template
- rescue LoadError => e
- #
- # The template did not exist in the default template directory, so
- # see if require can find the template elsewhere (in a gem, for
- # instance).
- #
- if(e.message[template] && template != @options.template)
- template = @options.template
- retry
- end
-
- $stderr.puts "Could not find HTML template '#{template}': #{e.message}"
- exit 99
- end
- end
-
- ##
- # Write out the style sheet used by the main frames
-
- def write_style_sheet
- return unless @template.constants.include? :STYLE or
- @template.constants.include? 'STYLE'
-
- template = RDoc::TemplatePage.new @template::STYLE
-
- unless @options.css then
- open RDoc::Generator::CSS_NAME, 'w' do |f|
- values = {}
-
- if @template.constants.include? :FONTS or
- @template.constants.include? 'FONTS' then
- values["fonts"] = @template::FONTS
- end
-
- template.write_html_on(f, values)
- end
- end
- end
-
- ##
- # See the comments at the top for a description of the directory structure
-
- def gen_sub_directories
- FileUtils.mkdir_p RDoc::Generator::FILE_DIR
- FileUtils.mkdir_p RDoc::Generator::CLASS_DIR
- rescue
- $stderr.puts $!.message
- exit 1
- end
-
- def build_indices
- @files, @classes = RDoc::Generator::Context.build_indices(@toplevels,
- @options)
- end
-
- ##
- # Generate all the HTML
-
- def generate_html
- @main_url = main_url
-
- # the individual descriptions for files and classes
- gen_into(@files)
- gen_into(@classes)
-
- # and the index files
- gen_file_index
- gen_class_index
- gen_method_index
- gen_main_index
-
- # this method is defined in the template file
- values = {
- 'title_suffix' => CGI.escapeHTML("[#{@options.title}]"),
- 'charset' => @options.charset,
- 'style_url' => style_url('', @options.css),
- }
-
- @template.write_extra_pages(values) if @template.respond_to?(:write_extra_pages)
- end
-
- def gen_into(list)
- #
- # The file, class, and method lists technically should be regenerated
- # for every output file, in order that the relative links be correct
- # (we are worried here about frameless templates, which need this
- # information for every generated page). Doing this is a bit slow,
- # however. For a medium-sized gem, this increased rdoc's runtime by
- # about 5% (using the 'time' command-line utility). While this is not
- # necessarily a problem, I do not want to pessimize rdoc for large
- # projects, however, and so we only regenerate the lists when the
- # directory of the output file changes, which seems like a reasonable
- # optimization.
- #
- file_list = {}
- class_list = {}
- method_list = {}
- prev_op_dir = nil
-
- list.each do |item|
- next unless item.document_self
-
- op_file = item.path
- op_dir = File.dirname(op_file)
-
- if(op_dir != prev_op_dir)
- file_list = index_to_links op_file, @files
- class_list = index_to_links op_file, @classes
- method_list = index_to_links op_file, RDoc::Generator::Method.all_methods
- end
- prev_op_dir = op_dir
-
- FileUtils.mkdir_p op_dir
-
- open op_file, 'w' do |io|
- item.write_on io, file_list, class_list, method_list
- end
- end
- end
-
- def gen_file_index
- gen_an_index @files, 'Files', @template::FILE_INDEX, "fr_file_index.html"
- end
-
- def gen_class_index
- gen_an_index(@classes, 'Classes', @template::CLASS_INDEX,
- "fr_class_index.html")
- end
-
- def gen_method_index
- gen_an_index(RDoc::Generator::Method.all_methods, 'Methods',
- @template::METHOD_INDEX, "fr_method_index.html")
- end
-
- def gen_an_index(collection, title, template, filename)
- template = RDoc::TemplatePage.new @template::FR_INDEX_BODY, template
- res = []
- collection.sort.each do |f|
- if f.document_self
- res << { "href" => f.path, "name" => f.index_name }
- end
- end
-
- values = {
- "entries" => res,
- 'title' => CGI.escapeHTML("#{title} [#{@options.title}]"),
- 'list_title' => CGI.escapeHTML(title),
- 'index_url' => @main_url,
- 'charset' => @options.charset,
- 'style_url' => style_url('', @options.css),
- }
-
- open filename, 'w' do |f|
- template.write_html_on(f, values)
- end
- end
-
- ##
- # The main index page is mostly a template frameset, but includes the
- # initial page. If the <tt>--main</tt> option was given, we use this as
- # our main page, otherwise we use the first file specified on the command
- # line.
-
- def gen_main_index
- if @template.const_defined? :FRAMELESS then
- #
- # If we're using a template without frames, then just redirect
- # to it from index.html.
- #
- # One alternative to this, expanding the main page's template into
- # index.html, is tricky because the relative URLs will be different
- # (since index.html is located in at the site's root,
- # rather than within a files or a classes subdirectory).
- #
- open 'index.html', 'w' do |f|
- f.puts(%{<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">})
- f.puts(%{<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
- lang="en">})
- f.puts(%{<head>})
- f.puts(%{<title>#{CGI.escapeHTML(@options.title)}</title>})
- f.puts(%{<meta http-equiv="refresh" content="0; url=#{@main_url}" />})
- f.puts(%{</head>})
- f.puts(%{<body></body>})
- f.puts(%{</html>})
- end
- else
- main = RDoc::TemplatePage.new @template::INDEX
-
- open 'index.html', 'w' do |f|
- style_url = style_url '', @options.css
-
- classes = @classes.sort.map { |klass| klass.value_hash }
-
- values = {
- 'initial_page' => @main_url,
- 'style_url' => style_url('', @options.css),
- 'title' => CGI.escapeHTML(@options.title),
- 'charset' => @options.charset,
- 'classes' => classes,
- }
-
- values['inline_source'] = @options.inline_source
-
- main.write_html_on f, values
- end
- end
- end
-
- def index_to_links(output_path, collection)
- collection.sort.map do |f|
- next unless f.document_self
- { "href" => RDoc::Markup::ToHtml.gen_relative_url(output_path, f.path),
- "name" => f.index_name }
- end.compact
- end
-
- ##
- # Returns the url of the main page
-
- def main_url
- main_page = @options.main_page
-
- #
- # If a main page has been specified (--main), then search for it
- # in the AllReferences array. This allows either files or classes
- # to be used for the main page.
- #
- if main_page then
- main_page_ref = RDoc::Generator::AllReferences[main_page]
-
- if main_page_ref then
- return main_page_ref.path
- else
- $stderr.puts "Could not find main page #{main_page}"
- end
- end
-
- #
- # No main page has been specified, so just use the README.
- #
- @files.each do |file|
- if file.name =~ /^README/ then
- return file.path
- end
- end
-
- #
- # There's no README (shame! shame!). Just use the first file
- # that will be documented.
- #
- @files.each do |file|
- if file.document_self then
- return file.path
- end
- end
-
- #
- # There are no files to be documented... Something seems very wrong.
- #
- raise RDoc::Error, "Couldn't find anything to document (perhaps :stopdoc: has been used in all classes)!"
- end
- private :main_url
-
-end
-
-class RDoc::Generator::HTMLInOne < RDoc::Generator::HTML
-
- def initialize(*args)
- super
- end
-
- ##
- # Build the initial indices and output objects
- # based on an array of TopLevel objects containing
- # the extracted information.
-
- def generate(info)
- @toplevels = info
- @hyperlinks = {}
-
- build_indices
- generate_xml
- end
-
- ##
- # Generate:
- #
- # * a list of RDoc::Generator::File objects for each TopLevel object.
- # * a list of RDoc::Generator::Class objects for each first level
- # class or module in the TopLevel objects
- # * a complete list of all hyperlinkable terms (file,
- # class, module, and method names)
-
- def build_indices
- @files, @classes = RDoc::Generator::Context.build_indices(@toplevels,
- @options)
- end
-
- ##
- # Generate all the HTML. For the one-file case, we generate
- # all the information in to one big hash
-
- def generate_xml
- values = {
- 'charset' => @options.charset,
- 'files' => gen_into(@files),
- 'classes' => gen_into(@classes),
- 'title' => CGI.escapeHTML(@options.title),
- }
-
- template = RDoc::TemplatePage.new @template::ONE_PAGE
-
- if @options.op_name
- opfile = open @options.op_name, 'w'
- else
- opfile = $stdout
- end
- template.write_html_on(opfile, values)
- end
-
- def gen_into(list)
- res = []
- list.each do |item|
- res << item.value_hash
- end
- res
- end
-end
diff --git a/lib/rdoc/generator/html/common.rb b/lib/rdoc/generator/html/common.rb
deleted file mode 100644
index b25f009a72..0000000000
--- a/lib/rdoc/generator/html/common.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-#
-# The templates require further refactoring. In particular,
-# * Some kind of HTML generation library should be used.
-#
-# Also, all of the templates require some TLC from a designer.
-#
-# Right now, this file contains some constants that are used by all
-# of the templates.
-#
-module RDoc::Generator::HTML::Common
- XHTML_STRICT_PREAMBLE = <<-EOF
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-EOF
-
- XHTML_FRAME_PREAMBLE = <<-EOF
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"
-"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
-EOF
-
- HTML_ELEMENT = <<-EOF
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-EOF
-end
diff --git a/lib/rdoc/generator/html/frameless.rb b/lib/rdoc/generator/html/frameless.rb
deleted file mode 100644
index 0375fee313..0000000000
--- a/lib/rdoc/generator/html/frameless.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-require 'rdoc/generator/html/html'
-
-##
-# = CSS2 RDoc HTML template
-#
-# This is a template for RDoc that uses XHTML 1.0 Strict and dictates a
-# bit more of the appearance of the output to cascading stylesheets than the
-# default. It was designed for clean inline code display, and uses DHTMl to
-# toggle the visbility of each method's source with each click on the '[source]'
-# link.
-#
-# Frameless basically is the html template without frames.
-#
-# == Authors
-#
-# * Michael Granger <ged@FaerieMUD.org>
-#
-# Copyright (c) 2002, 2003 The FaerieMUD Consortium. Some rights reserved.
-#
-# This work is licensed under the Creative Commons Attribution License. To view
-# a copy of this license, visit http://creativecommons.org/licenses/by/1.0/ or
-# send a letter to Creative Commons, 559 Nathan Abbott Way, Stanford, California
-# 94305, USA.
-
-module RDoc::Generator::HTML::FRAMELESS
-
- FRAMELESS = true
-
- FONTS = RDoc::Generator::HTML::HTML::FONTS
-
- STYLE = RDoc::Generator::HTML::HTML::STYLE
-
- HEADER = RDoc::Generator::HTML::HTML::HEADER
-
- FOOTER = <<-EOF
- <div id="popupmenu" class="index">
- <br />
- <h1 class="index-entries section-bar">Files</h1>
- <ul>
-<% values["file_list"].each do |file| %>
- <li><a href="<%= file["href"] %>"><%= file["name"] %></a></li>
-<% end %>
- </ul>
-
- <br />
- <h1 class="index-entries section-bar">Classes</h1>
- <ul>
-<% values["class_list"].each do |klass| %>
- <li><a href="<%= klass["href"] %>"><%= klass["name"] %></a></li>
-<% end %>
- </ul>
-
- <br />
- <h1 class="index-entries section-bar">Methods</h1>
- <ul>
-<% values["method_list"].each do |method| %>
- <li><a href="<%= method["href"] %>"><%= method["name"] %></a></li>
-<% end %>
- </ul>
- </div>
-</body>
-</html>
- EOF
-
- FILE_PAGE = RDoc::Generator::HTML::HTML::FILE_PAGE
-
- CLASS_PAGE = RDoc::Generator::HTML::HTML::CLASS_PAGE
-
- METHOD_LIST = RDoc::Generator::HTML::HTML::METHOD_LIST
-
- BODY = HEADER + %{
-
-<%= template_include %> <!-- banner header -->
-
- <div id="bodyContent">
-
-} + METHOD_LIST + %{
-
- </div>
-
-} + FOOTER
-
- SRC_PAGE = RDoc::Generator::HTML::HTML::SRC_PAGE
-
- FR_INDEX_BODY = RDoc::Generator::HTML::HTML::FR_INDEX_BODY
-
- FILE_INDEX = RDoc::Generator::HTML::HTML::FILE_INDEX
-
- CLASS_INDEX = RDoc::Generator::HTML::HTML::CLASS_INDEX
-
- METHOD_INDEX = RDoc::Generator::HTML::HTML::METHOD_INDEX
-end
diff --git a/lib/rdoc/generator/html/hefss.rb b/lib/rdoc/generator/html/hefss.rb
deleted file mode 100644
index 540c23d869..0000000000
--- a/lib/rdoc/generator/html/hefss.rb
+++ /dev/null
@@ -1,150 +0,0 @@
-require 'rdoc/generator/html'
-require 'rdoc/generator/html/kilmerfactory'
-
-module RDoc::Generator::HTML::HEFSS
-
- FONTS = "Verdana, Arial, Helvetica, sans-serif"
-
- CENTRAL_STYLE = <<-EOF
-body,p { font-family: <%= values["fonts"] %>;
- color: #000040; background: #BBBBBB;
-}
-
-td { font-family: <%= values["fonts"] %>;
- color: #000040;
-}
-
-.attr-rw { font-size: small; color: #444488 }
-
-.title-row {color: #eeeeff;
- background: #BBBBDD;
-}
-
-.big-title-font { color: white;
- font-family: <%= values["fonts"] %>;
- font-size: large;
- height: 50px}
-
-.small-title-font { color: purple;
- font-family: <%= values["fonts"] %>;
- font-size: small; }
-
-.aqua { color: purple }
-
-#diagram img {
- border: 0;
-}
-
-.method-name, attr-name {
- font-family: monospace; font-weight: bold;
-}
-
-.tablesubtitle {
- width: 100%;
- margin-top: 1ex;
- margin-bottom: .5ex;
- padding: 5px 0px 5px 20px;
- font-size: large;
- color: purple;
- background: #BBBBCC;
-}
-
-.tablesubsubtitle {
- width: 100%;
- margin-top: 1ex;
- margin-bottom: .5ex;
- padding: 5px 0px 5px 20px;
- font-size: medium;
- color: white;
- background: #BBBBCC;
-}
-
-.name-list {
- font-family: monospace;
- margin-left: 40px;
- margin-bottom: 2ex;
- line-height: 140%;
-}
-
-.description {
- margin-left: 40px;
- margin-bottom: 2ex;
- line-height: 140%;
-}
-
-.methodtitle {
- font-size: medium;
- text_decoration: none;
- padding: 3px 3px 3px 20px;
- color: #0000AA;
-}
-
-.ruby-comment { color: green; font-style: italic }
-.ruby-constant { color: #4433aa; font-weight: bold; }
-.ruby-identifier { color: #222222; }
-.ruby-ivar { color: #2233dd; }
-.ruby-keyword { color: #3333FF; font-weight: bold }
-.ruby-node { color: #777777; }
-.ruby-operator { color: #111111; }
-.ruby-regexp { color: #662222; }
-.ruby-value { color: #662222; font-style: italic }
-
-.srcbut { float: right }
- EOF
-
- INDEX_STYLE = <<-EOF
-body {
- background-color: #bbbbbb;
- font-family: #{FONTS};
- font-size: 11px;
- font-style: normal;
- line-height: 14px;
- color: #000040;
-}
-
-div.banner {
- background: #bbbbcc;
- color: white;
- padding: 1;
- margin: 0;
- font-size: 90%;
- font-weight: bold;
- line-height: 1.1;
- text-align: center;
- width: 100%;
-}
-EOF
-
- FACTORY = RDoc::Generator::HTML::
- KilmerFactory.new(:central_css => CENTRAL_STYLE,
- :index_css => INDEX_STYLE,
- :method_list_heading => "Subroutines and Functions",
- :class_and_module_list_heading => "Classes and Modules",
- :attribute_list_heading => "Arguments")
-
- STYLE = FACTORY.get_STYLE()
-
- METHOD_LIST = FACTORY.get_METHOD_LIST()
-
- BODY = FACTORY.get_BODY()
-
- FILE_PAGE = FACTORY.get_FILE_PAGE()
-
- CLASS_PAGE = FACTORY.get_CLASS_PAGE()
-
- SRC_PAGE = FACTORY.get_SRC_PAGE()
-
- FR_INDEX_BODY = FACTORY.get_FR_INDEX_BODY()
-
- FILE_INDEX = FACTORY.get_FILE_INDEX()
-
- CLASS_INDEX = FACTORY.get_CLASS_INDEX()
-
- METHOD_INDEX = FACTORY.get_METHOD_INDEX()
-
- INDEX = FACTORY.get_INDEX()
-
- def self.write_extra_pages(values)
- FACTORY.write_extra_pages(values)
- end
-end
diff --git a/lib/rdoc/generator/html/html.rb b/lib/rdoc/generator/html/html.rb
deleted file mode 100644
index 823d8056e7..0000000000
--- a/lib/rdoc/generator/html/html.rb
+++ /dev/null
@@ -1,769 +0,0 @@
-require 'rdoc/generator/html'
-require 'rdoc/generator/html/common'
-
-##
-# = CSS2 RDoc HTML template
-#
-# This is a template for RDoc that uses XHTML 1.0 Strict and dictates a
-# bit more of the appearance of the output to cascading stylesheets than the
-# default. It was designed for clean inline code display, and uses DHTMl to
-# toggle the visibility of each method's source with each click on the
-# '[source]' link.
-#
-# This template *also* forms the basis of the frameless template.
-#
-# == Authors
-#
-# * Michael Granger <ged@FaerieMUD.org>
-#
-# Copyright (c) 2002, 2003 The FaerieMUD Consortium. Some rights reserved.
-#
-# This work is licensed under the Creative Commons Attribution License. To
-# view a copy of this license, visit
-# http://creativecommons.org/licenses/by/1.0/ or send a letter to Creative
-# Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA.
-
-module RDoc::Generator::HTML::HTML
-
- include RDoc::Generator::HTML::Common
-
- FONTS = "Verdana,Arial,Helvetica,sans-serif"
-
- STYLE = <<-EOF
-body {
- font-family: #{FONTS};
- font-size: 90%;
- margin: 0;
- margin-left: 40px;
- padding: 0;
- background: white;
- color: black;
-}
-
-h1, h2, h3, h4 {
- margin: 0;
- background: transparent;
-}
-
-h1 {
- font-size: 150%;
-}
-
-h2,h3,h4 {
- margin-top: 1em;
-}
-
-:link, :visited {
- background: #eef;
- color: #039;
- text-decoration: none;
-}
-
-:link:hover, :visited:hover {
- background: #039;
- color: #eef;
-}
-
-/* Override the base stylesheet's Anchor inside a table cell */
-td > :link, td > :visited {
- background: transparent;
- color: #039;
- text-decoration: none;
-}
-
-/* and inside a section title */
-.section-title > :link, .section-title > :visited {
- background: transparent;
- color: #eee;
- text-decoration: none;
-}
-
-/* === Structural elements =================================== */
-
-.index {
- margin: 0;
- margin-left: -40px;
- padding: 0;
- font-size: 90%;
-}
-
-.index :link, .index :visited {
- margin-left: 0.7em;
-}
-
-.index .section-bar {
- margin-left: 0px;
- padding-left: 0.7em;
- background: #ccc;
- font-size: small;
-}
-
-#classHeader, #fileHeader {
- width: auto;
- color: white;
- padding: 0.5em 1.5em 0.5em 1.5em;
- margin: 0;
- margin-left: -40px;
- border-bottom: 3px solid #006;
-}
-
-#classHeader :link, #fileHeader :link,
-#classHeader :visited, #fileHeader :visited {
- background: inherit;
- color: white;
-}
-
-#classHeader td, #fileHeader td {
- background: inherit;
- color: white;
-}
-
-#fileHeader {
- background: #057;
-}
-
-#classHeader {
- background: #048;
-}
-
-.class-name-in-header {
- font-size: 180%;
- font-weight: bold;
-}
-
-#bodyContent {
- padding: 0 1.5em 0 1.5em;
-}
-
-#description {
- padding: 0.5em 1.5em;
- background: #efefef;
- border: 1px dotted #999;
-}
-
-#description h1, #description h2, #description h3,
-#description h4, #description h5, #description h6 {
- color: #125;
- background: transparent;
-}
-
-#validator-badges {
- text-align: center;
-}
-
-#validator-badges img {
- border: 0;
-}
-
-#copyright {
- color: #333;
- background: #efefef;
- font: 0.75em sans-serif;
- margin-top: 5em;
- margin-bottom: 0;
- padding: 0.5em 2em;
-}
-
-/* === Classes =================================== */
-
-table.header-table {
- color: white;
- font-size: small;
-}
-
-.type-note {
- font-size: small;
- color: #dedede;
-}
-
-.section-bar {
- color: #333;
- border-bottom: 1px solid #999;
- margin-left: -20px;
-}
-
-.section-title {
- background: #79a;
- color: #eee;
- padding: 3px;
- margin-top: 2em;
- margin-left: -30px;
- border: 1px solid #999;
-}
-
-.top-aligned-row {
- vertical-align: top
-}
-
-.bottom-aligned-row {
- vertical-align: bottom
-}
-
-#diagram img {
- border: 0;
-}
-
-/* --- Context section classes ----------------------- */
-
-.context-row { }
-
-.context-item-name {
- font-family: monospace;
- font-weight: bold;
- color: black;
-}
-
-.context-item-value {
- font-size: small;
- color: #448;
-}
-
-.context-item-desc {
- color: #333;
- padding-left: 2em;
-}
-
-/* --- Method classes -------------------------- */
-
-.method-detail {
- background: #efefef;
- padding: 0;
- margin-top: 0.5em;
- margin-bottom: 1em;
- border: 1px dotted #ccc;
-}
-
-.method-heading {
- color: black;
- background: #ccc;
- border-bottom: 1px solid #666;
- padding: 0.2em 0.5em 0 0.5em;
-}
-
-.method-signature {
- color: black;
- background: inherit;
-}
-
-.method-name {
- font-weight: bold;
-}
-
-.method-args {
- font-style: italic;
-}
-
-.method-description {
- padding: 0 0.5em 0 0.5em;
-}
-
-/* --- Source code sections -------------------- */
-
-:link.source-toggle, :visited.source-toggle {
- font-size: 90%;
-}
-
-div.method-source-code {
- background: #262626;
- color: #ffdead;
- margin: 1em;
- padding: 0.5em;
- border: 1px dashed #999;
- overflow: auto;
-}
-
-div.method-source-code pre {
- color: #ffdead;
-}
-
-/* --- Ruby keyword styles --------------------- */
-
-.standalone-code {
- background: #221111;
- color: #ffdead;
- overflow: auto;
-}
-
-.ruby-constant {
- color: #7fffd4;
- background: transparent;
-}
-
-.ruby-keyword {
- color: #00ffff;
- background: transparent;
-}
-
-.ruby-ivar {
- color: #eedd82;
- background: transparent;
-}
-
-.ruby-operator {
- color: #00ffee;
- background: transparent;
-}
-
-.ruby-identifier {
- color: #ffdead;
- background: transparent;
-}
-
-.ruby-node {
- color: #ffa07a;
- background: transparent;
-}
-
-.ruby-comment {
- color: #b22222;
- font-weight: bold;
- background: transparent;
-}
-
-.ruby-regexp {
- color: #ffa07a;
- background: transparent;
-}
-
-.ruby-value {
- color: #7fffd4;
- background: transparent;
-}
-EOF
-
-
-#####################################################################
-### H E A D E R T E M P L A T E
-#####################################################################
-
- HEADER = XHTML_STRICT_PREAMBLE + HTML_ELEMENT + <<-EOF
-<head>
- <title><%= values["title"] %></title>
- <meta http-equiv="Content-Type" content="text/html; charset=<%= values["charset"] %>" />
- <meta http-equiv="Content-Script-Type" content="text/javascript" />
- <link rel="stylesheet" href="<%= values["style_url"] %>" type="text/css" media="screen" />
- <script type="text/javascript">
- // <![CDATA[
-
- function popupCode( url ) {
- window.open(url, "Code", "resizable=yes,scrollbars=yes,toolbar=no,status=no,height=150,width=400")
- }
-
- function toggleCode( id ) {
- if ( document.getElementById )
- elem = document.getElementById( id );
- else if ( document.all )
- elem = eval( "document.all." + id );
- else
- return false;
-
- elemStyle = elem.style;
-
- if ( elemStyle.display != "block" ) {
- elemStyle.display = "block"
- } else {
- elemStyle.display = "none"
- }
-
- return true;
- }
-
- // Make codeblocks hidden by default
- document.writeln( "<style type=\\"text/css\\">div.method-source-code { display: none }<\\/style>" )
-
- // ]]>
- </script>
-
-</head>
-<body>
-EOF
-
-#####################################################################
-### F O O T E R T E M P L A T E
-#####################################################################
-
- FOOTER = <<-EOF
-<div id="validator-badges">
- <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
-</div>
-
-</body>
-</html>
- EOF
-
-
-#####################################################################
-### F I L E P A G E H E A D E R T E M P L A T E
-#####################################################################
-
- FILE_PAGE = <<-EOF
- <div id="fileHeader">
- <h1><%= values["short_name"] %></h1>
- <table class="header-table">
- <tr class="top-aligned-row">
- <td><strong>Path:</strong></td>
- <td><%= values["full_path"] %>
-<% if values["cvsurl"] then %>
- &nbsp;(<a href="<%= values["cvsurl"] %>"><acronym title="Concurrent Versioning System">CVS</acronym></a>)
-<% end %>
- </td>
- </tr>
- <tr class="top-aligned-row">
- <td><strong>Last Update:</strong></td>
- <td><%= values["dtm_modified"] %></td>
- </tr>
- </table>
- </div>
- EOF
-
-#####################################################################
-### C L A S S P A G E H E A D E R T E M P L A T E
-#####################################################################
-
- CLASS_PAGE = <<-EOF
- <div id="classHeader">
- <table class="header-table">
- <tr class="top-aligned-row">
- <td><strong><%= values["classmod"] %></strong></td>
- <td class="class-name-in-header"><%= values["full_name"] %></td>
- </tr>
- <tr class="top-aligned-row">
- <td><strong>In:</strong></td>
- <td>
-<% values["infiles"].each do |infiles| %>
-<% if infiles["full_path_url"] then %>
- <a href="<%= infiles["full_path_url"] %>">
-<% end %>
- <%= infiles["full_path"] %>
-<% if infiles["full_path_url"] then %>
- </a>
-<% end %>
-<% if infiles["cvsurl"] then %>
- &nbsp;(<a href="<%= infiles["cvsurl"] %>"><acronym title="Concurrent Versioning System">CVS</acronym></a>)
-<% end %>
- <br />
-<% end %><%# values["infiles"] %>
- </td>
- </tr>
-
-<% if values["parent"] then %>
- <tr class="top-aligned-row">
- <td><strong>Parent:</strong></td>
- <td>
-<% if values["par_url"] then %>
- <a href="<%= values["par_url"] %>">
-<% end %>
- <%= values["parent"] %>
-<% if values["par_url"] then %>
- </a>
-<% end %>
- </td>
- </tr>
-<% end %>
- </table>
- </div>
- EOF
-
-#####################################################################
-### M E T H O D L I S T T E M P L A T E
-#####################################################################
-
- METHOD_LIST = <<-EOF
- <div id="contextContent">
-<% if values["diagram"] then %>
- <div id="diagram">
- <%= values["diagram"] %>
- </div>
-<% end
-
- if values["description"] then %>
- <div id="description">
- <%= values["description"] %>
- </div>
-<% end
-
- if values["requires"] then %>
- <div id="requires-list">
- <h3 class="section-bar">Required files</h3>
-
- <div class="name-list">
-<% values["requires"].each do |requires| %>
- <%= href requires["aref"], requires["name"] %>&nbsp;&nbsp;
-<% end %><%# values["requires"] %>
- </div>
- </div>
-<% end
-
- if values["toc"] then %>
- <div id="contents-list">
- <h3 class="section-bar">Contents</h3>
- <ul>
-<% values["toc"].each do |toc| %>
- <li><a href="#<%= toc["href"] %>"><%= toc["secname"] %></a></li>
-<% end %><%# values["toc"] %>
- </ul>
-<% end %>
- </div>
-
-<% if values["methods"] then %>
- <div id="method-list">
- <h3 class="section-bar">Methods</h3>
-
- <div class="name-list">
-<% values["methods"].each do |methods| %>
- <%= href methods["aref"], methods["name"] %>&nbsp;&nbsp;
-<% end %><%# values["methods"] %>
- </div>
- </div>
-<% end %>
- </div>
-
- <!-- if includes -->
-<% if values["includes"] then %>
- <div id="includes">
- <h3 class="section-bar">Included Modules</h3>
-
- <div id="includes-list">
-<% values["includes"].each do |includes| %>
- <span class="include-name"><%= href includes["aref"], includes["name"] %></span>
-<% end %><%# values["includes"] %>
- </div>
- </div>
-<% end
-
- values["sections"].each do |sections| %>
- <div id="section">
-<% if sections["sectitle"] then %>
- <h2 class="section-title"><a name="<%= sections["secsequence"] %>"><%= sections["sectitle"] %></a></h2>
-<% if sections["seccomment"] then %>
- <div class="section-comment">
- <%= sections["seccomment"] %>
- </div>
-<% end
- end
-
- if sections["classlist"] then %>
- <div id="class-list">
- <h3 class="section-bar">Classes and Modules</h3>
-
- <%= sections["classlist"] %>
- </div>
-<% end
-
- if sections["constants"] then %>
- <div id="constants-list">
- <h3 class="section-bar">Constants</h3>
-
- <div class="name-list">
- <table summary="Constants">
-<% sections["constants"].each do |constants| %>
- <tr class="top-aligned-row context-row">
- <td class="context-item-name"><%= constants["name"] %></td>
- <td>=</td>
- <td class="context-item-value"><%= constants["value"] %></td>
-<% if constants["desc"] then %>
- <td>&nbsp;</td>
- <td class="context-item-desc"><%= constants["desc"] %></td>
-<% end %>
- </tr>
-<% end %><%# sections["constants"] %>
- </table>
- </div>
- </div>
-<% end
-
- if sections["aliases"] then %>
- <div id="aliases-list">
- <h3 class="section-bar">External Aliases</h3>
-
- <div class="name-list">
- <table summary="aliases">
-<% sections["aliases"].each do |aliases| %>
- <tr class="top-aligned-row context-row">
- <td class="context-item-name"><%= aliases["old_name"] %></td>
- <td>-&gt;</td>
- <td class="context-item-value"><%= aliases["new_name"] %></td>
- </tr>
-<% if aliases["desc"] then %>
- <tr class="top-aligned-row context-row">
- <td>&nbsp;</td>
- <td colspan="2" class="context-item-desc"><%= aliases["desc"] %></td>
- </tr>
-<% end
- end %><%# sections["aliases"] %>
- </table>
- </div>
- </div>
-<% end %>
-
-<% if sections["attributes"] then %>
- <div id="attribute-list">
- <h3 class="section-bar">Attributes</h3>
-
- <div class="name-list">
- <table>
-<% sections["attributes"].each do |attribute| %>
- <tr class="top-aligned-row context-row">
- <td class="context-item-name"><%= attribute["name"] %></td>
-<% if attribute["rw"] then %>
- <td class="context-item-value">&nbsp;[<%= attribute["rw"] %>]&nbsp;</td>
-<% end
- unless attribute["rw"] then %>
- <td class="context-item-value">&nbsp;&nbsp;</td>
-<% end %>
- <td class="context-item-desc"><%= attribute["a_desc"] %></td>
- </tr>
-<% end %><%# sections["attributes"] %>
- </table>
- </div>
- </div>
-<% end %>
-
- <!-- if method_list -->
-<% if sections["method_list"] then %>
- <div id="methods">
-<% sections["method_list"].each do |method_list|
- if method_list["methods"] then %>
- <h3 class="section-bar"><%= method_list["type"] %> <%= method_list["category"] %> methods</h3>
-
-<% method_list["methods"].each do |methods| %>
- <div id="method-<%= methods["aref"] %>" class="method-detail">
- <a name="<%= methods["aref"] %>"></a>
-
- <div class="method-heading">
-<% if methods["codeurl"] then %>
- <a href="<%= methods["codeurl"] %>" target="Code" class="method-signature"
- onclick="popupCode('<%= methods["codeurl"] %>');return false;">
-<% end
- if methods["sourcecode"] then %>
- <a href="#<%= methods["aref"] %>" class="method-signature">
-<% end
- if methods["callseq"] then %>
- <span class="method-name"><%= methods["callseq"] %></span>
-<% end
- unless methods["callseq"] then %>
- <span class="method-name"><%= methods["name"] %></span><span class="method-args"><%= methods["params"] %></span>
-<% end
- if methods["codeurl"] then %>
- </a>
-<% end
- if methods["sourcecode"] then %>
- </a>
-<% end %>
- </div>
-
- <div class="method-description">
-<% if methods["m_desc"] then %>
- <%= methods["m_desc"] %>
-<% end
- if methods["sourcecode"] then %>
- <p><a class="source-toggle" href="#"
- onclick="toggleCode('<%= methods["aref"] %>-source');return false;">[Source]</a></p>
- <div class="method-source-code" id="<%= methods["aref"] %>-source">
-<pre>
-<%= methods["sourcecode"] %>
-</pre>
- </div>
-<% end %>
- </div>
- </div>
-
-<% end %><%# method_list["methods"] %><%
- end
- end %><%# sections["method_list"] %>
-
- </div>
-<% end %>
-<% end %><%# values["sections"] %>
- EOF
-
-#####################################################################
-### B O D Y T E M P L A T E
-#####################################################################
-
- BODY = HEADER + %{
-
-<%= template_include %> <!-- banner header -->
-
- <div id="bodyContent">
-
-} + METHOD_LIST + %{
-
- </div>
-
-} + FOOTER
-
-#####################################################################
-### S O U R C E C O D E T E M P L A T E
-#####################################################################
-
- SRC_PAGE = XHTML_STRICT_PREAMBLE + HTML_ELEMENT + <<-EOF
-<head>
- <title><%= values["title"] %></title>
- <meta http-equiv="Content-Type" content="text/html; charset=<%= values["charset"] %>" />
- <link rel="stylesheet" href="<%= values["style_url"] %>" type="text/css" media="screen" />
-</head>
-<body class="standalone-code">
- <pre><%= values["code"] %></pre>
-</body>
-</html>
- EOF
-
-
-#####################################################################
-### I N D E X F I L E T E M P L A T E S
-#####################################################################
-
- FR_INDEX_BODY = %{<%= template_include %>}
-
- FILE_INDEX = XHTML_STRICT_PREAMBLE + HTML_ELEMENT + <<-EOF
-<!--
-
- <%= values["title"] %>
-
- -->
-<head>
- <title><%= values["title"] %></title>
- <meta http-equiv="Content-Type" content="text/html; charset=<%= values["charset"] %>" />
- <link rel="stylesheet" href="<%= values["style_url"] %>" type="text/css" />
- <base target="docwin" />
-</head>
-<body>
-<div class="index">
- <h1 class="section-bar"><%= values["list_title"] %></h1>
- <div id="index-entries">
-<% values["entries"].each do |entries| %>
- <a href="<%= entries["href"] %>"><%= entries["name"] %></a><br />
-<% end %><%# values["entries"] %>
- </div>
-</div>
-</body>
-</html>
- EOF
-
- CLASS_INDEX = FILE_INDEX
- METHOD_INDEX = FILE_INDEX
-
- INDEX = XHTML_FRAME_PREAMBLE + HTML_ELEMENT + <<-EOF
-<!--
-
- <%= values["title"] %>
-
- -->
-<head>
- <title><%= values["title"] %></title>
- <meta http-equiv="Content-Type" content="text/html; charset=<%= values["charset"] %>" />
-</head>
-<frameset rows="20%, 80%">
- <frameset cols="25%,35%,45%">
- <frame src="fr_file_index.html" title="Files" name="Files" />
- <frame src="fr_class_index.html" name="Classes" />
- <frame src="fr_method_index.html" name="Methods" />
- </frameset>
- <frame src="<%= values["initial_page"] %>" name="docwin" />
-</frameset>
-</html>
- EOF
-
-end
-
diff --git a/lib/rdoc/generator/html/kilmer.rb b/lib/rdoc/generator/html/kilmer.rb
deleted file mode 100644
index 4c5a9ee8b0..0000000000
--- a/lib/rdoc/generator/html/kilmer.rb
+++ /dev/null
@@ -1,151 +0,0 @@
-require 'rdoc/generator/html'
-require 'rdoc/generator/html/kilmerfactory'
-
-module RDoc::Generator::HTML::KILMER
-
- FONTS = "Verdana, Arial, Helvetica, sans-serif"
-
- CENTRAL_STYLE = <<-EOF
-body,td,p { font-family: <%= values["fonts"] %>;
- color: #000040;
-}
-
-.attr-rw { font-size: xx-small; color: #444488 }
-
-.title-row { background-color: #CCCCFF;
- color: #000010;
-}
-
-.big-title-font {
- color: black;
- font-weight: bold;
- font-family: <%= values["fonts"] %>;
- font-size: large;
- height: 60px;
- padding: 10px 3px 10px 3px;
-}
-
-.small-title-font { color: black;
- font-family: <%= values["fonts"] %>;
- font-size:10; }
-
-.aqua { color: black }
-
-#diagram img {
- border: 0;
-}
-
-.method-name, .attr-name {
- font-family: font-family: <%= values["fonts"] %>;
- font-weight: bold;
- font-size: small;
- margin-left: 20px;
- color: #000033;
-}
-
-.tablesubtitle, .tablesubsubtitle {
- width: 100%;
- margin-top: 1ex;
- margin-bottom: .5ex;
- padding: 5px 0px 5px 3px;
- font-size: large;
- color: black;
- background-color: #CCCCFF;
- border: thin;
-}
-
-.name-list {
- margin-left: 5px;
- margin-bottom: 2ex;
- line-height: 105%;
-}
-
-.description {
- margin-left: 5px;
- margin-bottom: 2ex;
- line-height: 105%;
- font-size: small;
-}
-
-.methodtitle {
- font-size: small;
- font-weight: bold;
- text-decoration: none;
- color: #000033;
- background: #ccc;
-}
-
-.srclink {
- font-size: small;
- font-weight: bold;
- text-decoration: none;
- color: #0000DD;
- background-color: white;
-}
-
-.srcbut { float: right }
-
-.ruby-comment { color: green; font-style: italic }
-.ruby-constant { color: #4433aa; font-weight: bold; }
-.ruby-identifier { color: #222222; }
-.ruby-ivar { color: #2233dd; }
-.ruby-keyword { color: #3333FF; font-weight: bold }
-.ruby-node { color: #777777; }
-.ruby-operator { color: #111111; }
-.ruby-regexp { color: #662222; }
-.ruby-value { color: #662222; font-style: italic }
- EOF
-
- INDEX_STYLE = <<-EOF
-body {
- background-color: #ddddff;
- font-family: #{FONTS};
- font-size: 11px;
- font-style: normal;
- line-height: 14px;
- color: #000040;
-}
-
-div.banner {
- background: #0000aa;
- color: white;
- padding: 1;
- margin: 0;
- font-size: 90%;
- font-weight: bold;
- line-height: 1.1;
- text-align: center;
- width: 100%;
-}
-EOF
-
- FACTORY = RDoc::Generator::HTML::
- KilmerFactory.new(:central_css => CENTRAL_STYLE,
- :index_css => INDEX_STYLE)
-
- STYLE = FACTORY.get_STYLE()
-
- METHOD_LIST = FACTORY.get_METHOD_LIST()
-
- BODY = FACTORY.get_BODY()
-
- FILE_PAGE = FACTORY.get_FILE_PAGE()
-
- CLASS_PAGE = FACTORY.get_CLASS_PAGE()
-
- SRC_PAGE = FACTORY.get_SRC_PAGE()
-
- FR_INDEX_BODY = FACTORY.get_FR_INDEX_BODY()
-
- FILE_INDEX = FACTORY.get_FILE_INDEX()
-
- CLASS_INDEX = FACTORY.get_CLASS_INDEX()
-
- METHOD_INDEX = FACTORY.get_METHOD_INDEX()
-
- INDEX = FACTORY.get_INDEX()
-
- def self.write_extra_pages(values)
- FACTORY.write_extra_pages(values)
- end
-end
diff --git a/lib/rdoc/generator/html/kilmerfactory.rb b/lib/rdoc/generator/html/kilmerfactory.rb
deleted file mode 100644
index ef6f3f3b4d..0000000000
--- a/lib/rdoc/generator/html/kilmerfactory.rb
+++ /dev/null
@@ -1,427 +0,0 @@
-require 'rdoc/generator/html'
-require 'rdoc/generator/html/common'
-
-#
-# This class generates Kilmer-style templates. Right now,
-# rdoc is shipped with two such templates:
-# * kilmer
-# * hefss
-#
-# Kilmer-style templates use frames. The left side of the page has
-# three frames stacked on top of each other: one lists
-# files, one lists classes, and one lists methods. If source code
-# is not inlined, an additional frame runs across the bottom of
-# the page and will be used to display method source code.
-# The central (and largest frame) display class and file
-# pages.
-#
-# The constructor of this class accepts a Hash containing stylistic
-# attributes. Then, a get_BLAH instance method of this class returns a
-# value for the template's BLAH constant. get_BODY, for instance, returns
-# the value of the template's BODY constant.
-#
-class RDoc::Generator::HTML::KilmerFactory
-
- include RDoc::Generator::HTML::Common
-
- #
- # The contents of the stylesheet that should be used for the
- # central frame (for the class and file pages).
- #
- # This must be specified in the Hash passed to the constructor.
- #
- attr_reader :central_css
-
- #
- # The contents of the stylesheet that should be used for the
- # index pages.
- #
- # This must be specified in the Hash passed to the constructor.
- #
- attr_reader :index_css
-
- #
- # The heading that should be displayed before listing methods.
- #
- # If not supplied, this defaults to "Methods".
- #
- attr_reader :method_list_heading
-
- #
- # The heading that should be displayed before listing classes and
- # modules.
- #
- # If not supplied, this defaults to "Classes and Modules".
- #
- attr_reader :class_and_module_list_heading
-
- #
- # The heading that should be displayed before listing attributes.
- #
- # If not supplied, this defaults to "Attributes".
- #
- attr_reader :attribute_list_heading
-
- #
- # ====Description:
- # This method constructs a KilmerFactory instance, which
- # can be used to build Kilmer-style template classes.
- # The +style_attributes+ argument is a Hash that contains the
- # values of the classes attributes (Symbols mapped to Strings).
- #
- # ====Parameters:
- # [style_attributes]
- # A Hash describing the appearance of the Kilmer-style.
- #
- def initialize(style_attributes)
- @central_css = style_attributes[:central_css]
- if(!@central_css)
- raise ArgumentError, "did not specify a value for :central_css"
- end
-
- @index_css = style_attributes[:index_css]
- if(!@index_css)
- raise ArgumentError, "did not specify a value for :index_css"
- end
-
- @method_list_heading = style_attributes[:method_list_heading]
- if(!@method_list_heading)
- @method_list_heading = "Methods"
- end
-
- @class_and_module_list_heading = style_attributes[:class_and_module_list_heading]
- if(!@class_and_module_list_heading)
- @class_and_module_list_heading = "Classes and Modules"
- end
-
- @attribute_list_heading = style_attributes[:attribute_list_heading]
- if(!@attribute_list_heading)
- @attribute_list_heading = "Attributes"
- end
- end
-
- def get_STYLE
- return @central_css
- end
-
- def get_METHOD_LIST
- return %{
-<% if values["diagram"] then %>
-<div id="diagram">
-<table width="100%"><tr><td align="center">
-<%= values["diagram"] %>
-</td></tr></table>
-</div>
-<% end %>
-
-<% if values["description"] then %>
-<div class="description"><%= values["description"] %></div>
-<% end %>
-
-<% if values["requires"] then %>
-<table cellpadding="5" width="100%">
-<tr><td class="tablesubtitle">Required files</td></tr>
-</table><br />
-<div class="name-list">
-<% values["requires"].each do |requires| %>
-<%= href requires["aref"], requires["name"] %>
-<% end %><%# values["requires"] %>
-</div>
-<% end %>
-
-<% if values["methods"] then %>
-<table cellpadding="5" width="100%">
-<tr><td class="tablesubtitle">#{@method_list_heading}</td></tr>
-</table><br />
-<div class="name-list">
-<% values["methods"].each do |methods| %>
-<%= href methods["aref"], methods["name"] %>,
-<% end %><%# values["methods"] %>
-</div>
-<% end %>
-
-<% if values["includes"] then %>
-<div class="tablesubsubtitle">Included modules</div><br />
-<div class="name-list">
-<% values["includes"].each do |includes| %>
- <span class="method-name"><%= href includes["aref"], includes["name"] %></span>
-<% end %><%# values["includes"] %>
-</div>
-<% end %>
-
-<% values["sections"].each do |sections| %>
- <div id="section">
-<% if sections["sectitle"] then %>
- <h2 class="section-title"><a name="<%= sections["secsequence"] %>"><%= sections["sectitle"] %></a></h2>
-<% if sections["seccomment"] then %>
- <div class="section-comment">
- <%= sections["seccomment"] %>
- </div>
-<% end %>
-<% end %>
-<% if sections["attributes"] then %>
-<table cellpadding="5" width="100%">
-<tr><td class="tablesubtitle">#{@attribute_list_heading}</td></tr>
-</table><br />
-<table cellspacing="5">
-<% sections["attributes"].each do |attributes| %>
- <tr valign="top">
-<% if attributes["rw"] then %>
- <td align="center" class="attr-rw">&nbsp;[<%= attributes["rw"] %>]&nbsp;</td>
-<% end %>
-<% unless attributes["rw"] then %>
- <td></td>
-<% end %>
- <td class="attr-name"><%= attributes["name"] %></td>
- <td><%= attributes["a_desc"] %></td>
- </tr>
-<% end %><%# sections["attributes"] %>
-</table>
-<% end %>
-
-<% if sections["classlist"] then %>
-<table cellpadding="5" width="100%">
-<tr><td class="tablesubtitle">#{@class_and_module_list_heading}</td></tr>
-</table><br />
-<%= sections["classlist"] %><br />
-<% end %>
-
-<% if sections["method_list"] then %>
-<% sections["method_list"].each do |method_list| %>
-<% if method_list["methods"] then %>
-<table cellpadding="5" width="100%">
-<tr><td class="tablesubtitle"><%= method_list["type"] %> <%= method_list["category"] %> methods</td></tr>
-</table>
-<% method_list["methods"].each do |methods| %>
-<table width="100%" cellspacing="0" cellpadding="5" border="0">
-<tr><td class="methodtitle">
-<a name="<%= methods["aref"] %>">
-<% if methods["callseq"] then %>
-<b><%= methods["callseq"] %></b>
-<% end %>
-<% unless methods["callseq"] then %>
- <b><%= methods["name"] %></b><%= methods["params"] %>
-<% end %>
-</a>
-<% if methods["codeurl"] then %>
-<a href="<%= methods["codeurl"] %>" target="source" class="srclink">src</a>
-<% end %>
-</td></tr>
-</table>
-<% if methods["m_desc"] then %>
-<div class="description">
-<%= methods["m_desc"] %>
-</div>
-<% end %>
-<% if methods["aka"] then %>
-<div class="aka">
-This method is also aliased as
-<% methods["aka"].each do |aka| %>
-<a href="<%= methods["aref"] %>"><%= methods["name"] %></a>
-<% end %><%# methods["aka"] %>
-</div>
-<% end %>
-<% if methods["sourcecode"] then %>
-<pre class="source">
-<%= methods["sourcecode"] %>
-</pre>
-<% end %>
-<% end %><%# method_list["methods"] %>
-<% end %>
-<% end %><%# sections["method_list"] %>
-<% end %>
-
-<% end %><%# values["sections"] %>
-</div>
-}
- end
-
- def get_BODY
- return XHTML_STRICT_PREAMBLE + HTML_ELEMENT + %{
-<head>
- <title><%= values["title"] %></title>
- <meta http-equiv="Content-Type" content="text/html; charset=<%= values["charset"] %>" />
- <link rel="stylesheet" href="<%= values["style_url"] %>" type="text/css" media="screen" />
- <script type="text/javascript">
- <!--
- function popCode(url) {
- parent.frames.source.location = url
- }
- //-->
- </script>
-</head>
-<body>
-<div class="bodyContent">
-<%= template_include %> <!-- banner header -->
-
-#{get_METHOD_LIST()}
-</div>
-</body>
-</html>
-}
- end
-
-def get_FILE_PAGE
- return %{
-<table width="100%">
- <tr class="title-row">
- <td><table width="100%"><tr>
- <td class="big-title-font" colspan="2">File<br /><%= values["short_name"] %></td>
- <td align="right"><table cellspacing="0" cellpadding="2">
- <tr>
- <td class="small-title-font">Path:</td>
- <td class="small-title-font"><%= values["full_path"] %>
-<% if values["cvsurl"] then %>
- &nbsp;(<a href="<%= values["cvsurl"] %>"><acronym title="Concurrent Versioning System">CVS</acronym></a>)
-<% end %>
- </td>
- </tr>
- <tr>
- <td class="small-title-font">Modified:</td>
- <td class="small-title-font"><%= values["dtm_modified"] %></td>
- </tr>
- </table>
- </td></tr></table></td>
- </tr>
-</table><br />
-}
-end
-
-def get_CLASS_PAGE
- return %{
-<table width="100%" border="0" cellspacing="0">
- <tr class="title-row">
- <td class="big-title-font">
- <%= values["classmod"] %><br /><%= values["full_name"] %>
- </td>
- <td align="right">
- <table cellspacing="0" cellpadding="2">
- <tr valign="top">
- <td class="small-title-font">In:</td>
- <td class="small-title-font">
-<% values["infiles"].each do |infiles| %>
-<%= href infiles["full_path_url"], infiles["full_path"] %>
-<% if infiles["cvsurl"] then %>
-&nbsp;(<a href="<%= infiles["cvsurl"] %>"><acronym title="Concurrent Versioning System">CVS</acronym></a>)
-<% end %>
-<% end %><%# values["infiles"] %>
- </td>
- </tr>
-<% if values["parent"] then %>
- <tr>
- <td class="small-title-font">Parent:</td>
- <td class="small-title-font">
-<% if values["par_url"] then %>
- <a href="<%= values["par_url"] %>" class="cyan">
-<% end %>
-<%= values["parent"] %>
-<% if values["par_url"] then %>
- </a>
-<% end %>
- </td>
- </tr>
-<% end %>
- </table>
- </td>
- </tr>
-</table><br />
-}
-end
-
-def get_SRC_PAGE
- return XHTML_STRICT_PREAMBLE + HTML_ELEMENT + %{
-<head><title><%= values["title"] %></title>
-<meta http-equiv="Content-Type" content="text/html; charset=<%= values["charset"] %>" />
-<link rel="stylesheet" href="<%= values["style_url"] %>" type="text/css" media="screen" />
-</head>
-<body>
-<pre><%= values["code"] %></pre>
-</body>
-</html>
-}
-end
-
-def get_FR_INDEX_BODY
- return %{<%= template_include %>}
-end
-
-def get_FILE_INDEX
- return XHTML_STRICT_PREAMBLE + HTML_ELEMENT + %{
-<head>
-<title><%= values["title"] %></title>
-<meta http-equiv="Content-Type" content="text/html; charset=<%= values["charset"] %>" />
-<style type="text/css">
-<!--
-#{@index_css}
--->
-</style>
-<base target="docwin" />
-</head>
-<body>
-<div class="index">
-<div class="banner"><%= values["list_title"] %></div>
-<% values["entries"].each do |entries| %>
-<a href="<%= entries["href"] %>"><%= entries["name"] %></a><br />
-<% end %><%# values["entries"] %>
-</div>
-</body></html>
-}
-end
-
-def get_CLASS_INDEX
- return get_FILE_INDEX
-end
-
-def get_METHOD_INDEX
- return get_FILE_INDEX
-end
-
-def get_INDEX
- return XHTML_FRAME_PREAMBLE + HTML_ELEMENT + %{
-<head>
- <title><%= values["title"] %></title>
- <meta http-equiv="Content-Type" content="text/html; charset=<%= values["charset"] %>" />
-</head>
-
-<frameset cols="20%,*">
- <frameset rows="15%,35%,50%">
- <frame src="fr_file_index.html" title="Files" name="Files" />
- <frame src="fr_class_index.html" name="Classes" />
- <frame src="fr_method_index.html" name="Methods" />
- </frameset>
-<% if values["inline_source"] then %>
- <frame src="<%= values["initial_page"] %>" name="docwin" />
-<% end %>
-<% unless values["inline_source"] then %>
- <frameset rows="80%,20%">
- <frame src="<%= values["initial_page"] %>" name="docwin" />
- <frame src="blank.html" name="source" />
- </frameset>
-<% end %>
-</frameset>
-
-</html>
-}
-end
-
-def get_BLANK
- # This will be displayed in the source code frame before
- # any source code has been selected.
- return XHTML_STRICT_PREAMBLE + HTML_ELEMENT + %{
-<head>
- <title>Source Code Frame <%= values["title_suffix"] %></title>
- <meta http-equiv="Content-Type" content="text/html; charset=<%= values["charset"] %>" />
- <link rel="stylesheet" href="<%= values["style_url"] %>" type="text/css" media="screen" />
-</head>
-<body>
-</body>
-</html>
-}
-end
-
-def write_extra_pages(values)
- template = RDoc::TemplatePage.new(get_BLANK())
- File.open("blank.html", "w") { |f| template.write_html_on(f, values) }
-end
-
-end
diff --git a/lib/rdoc/generator/html/one_page_html.rb b/lib/rdoc/generator/html/one_page_html.rb
deleted file mode 100644
index 51ae32351a..0000000000
--- a/lib/rdoc/generator/html/one_page_html.rb
+++ /dev/null
@@ -1,122 +0,0 @@
-require 'rdoc/generator/html'
-require 'rdoc/generator/html/common'
-
-module RDoc::Generator::HTML::ONE_PAGE_HTML
-
- include RDoc::Generator::HTML::Common
-
- CONTENTS_XML = <<-EOF
-<% if defined? classes and classes["description"] then %>
-<%= classes["description"] %>
-<% end %>
-
-<% if defined? files and files["requires"] then %>
-<h4>Requires:</h4>
-<ul>
-<% files["requires"].each do |requires| %>
-<% if requires["aref"] then %>
-<li><a href="<%= requires["aref"] %>"><%= requires["name"] %></a></li>
-<% end %>
-<% unless requires["aref"] then %>
-<li><%= requires["name"] %></li>
-<% end %>
-<% end %><%# files["requires"] %>
-</ul>
-<% end %>
-
-<% if defined? classes and classes["includes"] then %>
-<h4>Includes</h4>
-<ul>
-<% classes["includes"].each do |includes| %>
-<% if includes["aref"] then %>
-<li><a href="<%= includes["aref"] %>"><%= includes["name"] %></a></li>
-<% end %>
-<% unless includes["aref"] then %>
-<li><%= includes["name"] %></li>
-<% end %>
-<% end %><%# classes["includes"] %>
-</ul>
-<% end %>
-
-<% if defined? classes and classes["sections"] then %>
-<% classes["sections"].each do |sections| %>
-<% if sections["attributes"] then %>
-<h4>Attributes</h4>
-<table>
-<% sections["attributes"].each do |attributes| %>
-<tr><td><%= attributes["name"] %></td><td><%= attributes["rw"] %></td><td><%= attributes["a_desc"] %></td></tr>
-<% end %><%# sections["attributes"] %>
-</table>
-<% end %>
-
-<% if sections["method_list"] then %>
-<h3>Methods</h3>
-<% sections["method_list"].each do |method_list| %>
-<% if method_list["methods"] then %>
-<% method_list["methods"].each do |methods| %>
-<h4><%= methods["type"] %> <%= methods["category"] %> method:
-<% if methods["callseq"] then %>
-<a name="<%= methods["aref"] %>"><%= methods["callseq"] %></a>
-<% end %>
-<% unless methods["callseq"] then %>
-<a name="<%= methods["aref"] %>"><%= methods["name"] %><%= methods["params"] %></a></h4>
-<% end %>
-
-<% if methods["m_desc"] then %>
-<%= methods["m_desc"] %>
-<% end %>
-
-<% if methods["sourcecode"] then %>
-<blockquote><pre>
-<%= methods["sourcecode"] %>
-</pre></blockquote>
-<% end %>
-<% end %><%# method_list["methods"] %>
-<% end %>
-<% end %><%# sections["method_list"] %>
-<% end %>
-<% end %><%# classes["sections"] %>
-<% end %>
- EOF
-
- ONE_PAGE = XHTML_STRICT_PREAMBLE + HTML_ELEMENT + %{
-<head>
- <title><%= values["title"] %></title>
- <meta http-equiv="Content-Type" content="text/html; charset=<%= values["charset"] %>" />
-</head>
-<body>
-<% values["files"].each do |files| %>
-<h2>File: <a name="<%= files["href"] %>"><%= files["short_name"] %></a></h2>
-<table>
- <tr><td>Path:</td><td><%= files["full_path"] %></td></tr>
- <tr><td>Modified:</td><td><%= files["dtm_modified"] %></td></tr>
-</table>
-} + CONTENTS_XML + %{
-<% end %><%# values["files"] %>
-
-<% if values["classes"] then %>
-<h2>Classes</h2>
-<% values["classes"].each do |classes| %>
-<% if classes["parent"] then %>
-<h3><%= classes["classmod"] %> <a name="<%= classes["href"] %>"><%= classes["full_name"] %></a> &lt; <%= href classes["par_url"], classes["parent"] %></h3>
-<% end %>
-<% unless classes["parent"] then %>
-<h3><%= classes["classmod"] %> <%= classes["full_name"] %></h3>
-<% end %>
-
-<% if classes["infiles"] then %>
-(in files
-<% classes["infiles"].each do |infiles| %>
-<%= href infiles["full_path_url"], infiles["full_path"] %>
-<% end %><%# classes["infiles"] %>
-)
-<% end %>
-} + CONTENTS_XML + %{
-<% end %><%# values["classes"] %>
-<% end %>
-</body>
-</html>
-}
-
-end
-
diff --git a/lib/rdoc/generator/ri.rb b/lib/rdoc/generator/ri.rb
deleted file mode 100644
index 6b7a5932f8..0000000000
--- a/lib/rdoc/generator/ri.rb
+++ /dev/null
@@ -1,226 +0,0 @@
-require 'rdoc/generator'
-require 'rdoc/markup/to_flow'
-
-require 'rdoc/ri/cache'
-require 'rdoc/ri/reader'
-require 'rdoc/ri/writer'
-require 'rdoc/ri/descriptions'
-
-class RDoc::Generator::RI
-
- ##
- # Generator may need to return specific subclasses depending on the
- # options they are passed. Because of this we create them using a factory
-
- def self.for(options)
- new(options)
- end
-
- ##
- # Set up a new ri generator
-
- def initialize(options) #:not-new:
- @options = options
- @ri_writer = RDoc::RI::Writer.new "."
- @markup = RDoc::Markup.new
- @to_flow = RDoc::Markup::ToFlow.new
-
- @generated = {}
- end
-
- ##
- # Build the initial indices and output objects based on an array of
- # TopLevel objects containing the extracted information.
-
- def generate(toplevels)
- RDoc::TopLevel.all_classes_and_modules.each do |cls|
- process_class cls
- end
- end
-
- def process_class(from_class)
- generate_class_info(from_class)
-
- # now recurse into this class' constituent classes
- from_class.each_classmodule do |mod|
- process_class(mod)
- end
- end
-
- def generate_class_info(cls)
- case cls
- when RDoc::NormalModule then
- cls_desc = RDoc::RI::ModuleDescription.new
- else
- cls_desc = RDoc::RI::ClassDescription.new
- cls_desc.superclass = cls.superclass
- end
-
- cls_desc.name = cls.name
- cls_desc.full_name = cls.full_name
- cls_desc.comment = markup(cls.comment)
-
- cls_desc.attributes = cls.attributes.sort.map do |a|
- RDoc::RI::Attribute.new(a.name, a.rw, markup(a.comment))
- end
-
- cls_desc.constants = cls.constants.map do |c|
- RDoc::RI::Constant.new(c.name, c.value, markup(c.comment))
- end
-
- cls_desc.includes = cls.includes.map do |i|
- RDoc::RI::IncludedModule.new(i.name)
- end
-
- class_methods, instance_methods = method_list(cls)
-
- cls_desc.class_methods = class_methods.map do |m|
- RDoc::RI::MethodSummary.new(m.name)
- end
-
- cls_desc.instance_methods = instance_methods.map do |m|
- RDoc::RI::MethodSummary.new(m.name)
- end
-
- update_or_replace(cls_desc)
-
- class_methods.each do |m|
- generate_method_info(cls_desc, m)
- end
-
- instance_methods.each do |m|
- generate_method_info(cls_desc, m)
- end
- end
-
- def generate_method_info(cls_desc, method)
- meth_desc = RDoc::RI::MethodDescription.new
- meth_desc.name = method.name
- meth_desc.full_name = cls_desc.full_name
- if method.singleton
- meth_desc.full_name += "::"
- else
- meth_desc.full_name += "#"
- end
- meth_desc.full_name << method.name
-
- meth_desc.comment = markup(method.comment)
- meth_desc.params = params_of(method)
- meth_desc.visibility = method.visibility.to_s
- meth_desc.is_singleton = method.singleton
- meth_desc.block_params = method.block_params
-
- meth_desc.aliases = method.aliases.map do |a|
- RDoc::RI::AliasName.new(a.name)
- end
-
- @ri_writer.add_method(cls_desc, meth_desc)
- end
-
- private
-
- ##
- # Returns a list of class and instance methods that we'll be documenting
-
- def method_list(cls)
- list = cls.method_list
- unless @options.show_all
- list = list.find_all do |m|
- m.visibility == :public || m.visibility == :protected || m.force_documentation
- end
- end
-
- c = []
- i = []
- list.sort.each do |m|
- if m.singleton
- c << m
- else
- i << m
- end
- end
- return c,i
- end
-
- def params_of(method)
- if method.call_seq
- method.call_seq
- else
- params = method.params || ""
-
- p = params.gsub(/\s*\#.*/, '')
- p = p.tr("\n", " ").squeeze(" ")
- p = "(" + p + ")" unless p[0] == ?(
-
- if (block = method.block_params)
- block.gsub!(/\s*\#.*/, '')
- block = block.tr("\n", " ").squeeze(" ")
- if block[0] == ?(
- block.sub!(/^\(/, '').sub!(/\)/, '')
- end
- p << " {|#{block.strip}| ...}"
- end
- p
- end
- end
-
- def markup(comment)
- return nil if !comment || comment.empty?
-
- # Convert leading comment markers to spaces, but only
- # if all non-blank lines have them
-
- if comment =~ /^(?>\s*)[^\#]/
- content = comment
- else
- content = comment.gsub(/^\s*(#+)/) { $1.tr('#',' ') }
- end
- @markup.convert(content, @to_flow)
- end
-
- ##
- # By default we replace existing classes with the same name. If the
- # --merge option was given, we instead merge this definition into an
- # existing class. We add our methods, aliases, etc to that class, but do
- # not change the class's description.
-
- def update_or_replace(cls_desc)
- old_cls = nil
-
- if @options.merge
- rdr = RDoc::RI::Reader.new RDoc::RI::Cache.new(@options.op_dir)
-
- namespace = rdr.top_level_namespace
- namespace = rdr.lookup_namespace_in(cls_desc.name, namespace)
- if namespace.empty?
- $stderr.puts "You asked me to merge this source into existing "
- $stderr.puts "documentation. This file references a class or "
- $stderr.puts "module called #{cls_desc.name} which I don't"
- $stderr.puts "have existing documentation for."
- $stderr.puts
- $stderr.puts "Perhaps you need to generate its documentation first"
- exit 1
- else
- old_cls = namespace[0]
- end
- end
-
- prev_cls = @generated[cls_desc.full_name]
-
- if old_cls and not prev_cls then
- old_desc = rdr.get_class old_cls
- cls_desc.merge_in old_desc
- end
-
- if prev_cls then
- cls_desc.merge_in prev_cls
- end
-
- @generated[cls_desc.full_name] = cls_desc
-
- @ri_writer.remove_class cls_desc
- @ri_writer.add_class cls_desc
- end
-
-end
-
diff --git a/lib/rdoc/generator/texinfo.rb b/lib/rdoc/generator/texinfo.rb
deleted file mode 100644
index 70db875af9..0000000000
--- a/lib/rdoc/generator/texinfo.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-require 'rdoc/rdoc'
-require 'rdoc/generator'
-require 'rdoc/markup/to_texinfo'
-
-module RDoc
- module Generator
- # This generates Texinfo files for viewing with GNU Info or Emacs
- # from RDoc extracted from Ruby source files.
- class TEXINFO
- # What should the .info file be named by default?
- DEFAULT_INFO_FILENAME = 'rdoc.info'
-
- include Generator::MarkUp
-
- # Accept some options
- def initialize(options)
- @options = options
- @options.inline_source = true
- @options.op_name ||= 'rdoc.texinfo'
- @options.formatter = ::RDoc::Markup::ToTexInfo.new
- end
-
- # Generate the +texinfo+ files
- def generate(toplevels)
- @toplevels = toplevels
- @files, @classes = ::RDoc::Generator::Context.build_indices(@toplevels,
- @options)
-
- (@files + @classes).each { |x| x.value_hash }
-
- open(@options.op_name, 'w') do |f|
- f.puts TexinfoTemplate.new('files' => @files,
- 'classes' => @classes,
- 'filename' => @options.op_name.gsub(/texinfo/, 'info'),
- 'title' => @options.title).render
- end
- # TODO: create info files and install?
- end
-
- class << self
- # Factory? We don't need no stinkin' factory!
- alias_method :for, :new
- end
- end
-
- # Basically just a wrapper around ERB.
- # Should probably use RDoc::TemplatePage instead
- class TexinfoTemplate
- BASE_DIR = ::File.expand_path(::File.dirname(__FILE__)) # have to calculate this when the file's loaded.
-
- def initialize(values, file = 'texinfo.erb')
- @v, @file = [values, file]
- end
-
- def template
- ::File.read(::File.join(BASE_DIR, 'texinfo', @file))
- end
-
- # Go!
- def render
- ERB.new(template).result binding
- end
-
- def href(location, text)
- text # TODO: how does texinfo do hyperlinks?
- end
-
- def target(name, text)
- text # TODO: how do hyperlink targets work?
- end
-
- # TODO: this is probably implemented elsewhere?
- def method_prefix(section)
- { 'Class' => '.',
- 'Module' => '::',
- 'Instance' => '#',
- }[section['category']]
- end
- end
- end
-end
diff --git a/lib/rdoc/generator/texinfo/class.texinfo.erb b/lib/rdoc/generator/texinfo/class.texinfo.erb
deleted file mode 100644
index 74ecc59f7d..0000000000
--- a/lib/rdoc/generator/texinfo/class.texinfo.erb
+++ /dev/null
@@ -1,44 +0,0 @@
-@node <%= @v['class']['full_name'].gsub(/::/, '-') %>
-@chapter <%= @v['class']["classmod"] %> <%= @v['class']['full_name'] %>
-
-<% if @v['class']["parent"] and @v['class']['par_url'] %>
-Inherits <%= href @v['class']["par_url"], @v['class']["parent"] %><% end %>
-
-<%= @v['class']["description"] %>
-
-<% if @v['class']["includes"] %>
-Includes
-<% @v['class']["includes"].each do |include| %>
-* <%= href include["aref"], include["name"] %>
-<% end # @v['class']["includes"] %>
-<% end %>
-
-<% if @v['class']["sections"] %>
-<% @v['class']["sections"].each do |section| %>
-<% if section["attributes"] %>
-Attributes
-<% section["attributes"].each do |attributes| %>
-* <%= attributes["name"] %> <%= attributes["rw"] %> <%= attributes["a_desc"] %>
-<% end # section["attributes"] %>
-<% end %>
-<% end %>
-
-<% @v['class']["sections"].each do |section| %>
-<% if section["method_list"] %>
-Methods
-@menu
-<% section["method_list"].each_with_index do |method_list, i| %>
-<%= i %>
-<% (method_list["methods"] || []).each do |method| %>
-* <%= @v['class']['full_name'].gsub(/::/, '-') %><%= method_prefix method_list %><%= method['name'] %>::<% end %>
-<% end %>
-@end menu
-
-<% section["method_list"].each do |method_list| %>
-<% (method_list["methods"] || []).uniq.each do |method| %>
-<%= TexinfoTemplate.new(@v.merge({'method' => method, 'list' => method_list}),
- 'method.texinfo.erb').render %><% end %>
-<% end %>
-<% end # if section["method_list"] %>
-<% end # @v['class']["sections"] %>
-<% end %>
diff --git a/lib/rdoc/generator/texinfo/file.texinfo.erb b/lib/rdoc/generator/texinfo/file.texinfo.erb
deleted file mode 100644
index b619b94bd2..0000000000
--- a/lib/rdoc/generator/texinfo/file.texinfo.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-<% if false %>
-<h2>File: <%= @v['file']["short_name"] %></h2>
-Path: <%= @v['file']["full_path"] %>
-
-<%= TexinfoTemplate.new(@v, 'content.texinfo.erb').render %>
-<% end %>
diff --git a/lib/rdoc/generator/texinfo/method.texinfo.erb b/lib/rdoc/generator/texinfo/method.texinfo.erb
deleted file mode 100644
index f5c2b73a4b..0000000000
--- a/lib/rdoc/generator/texinfo/method.texinfo.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-@node <%= @v['class']['full_name'].gsub(/::/, '-') %><%= method_prefix @v['list'] %><%= @v['method']['name'] %>
-@section <%= @v['class']["classmod"] %> <%= @v['class']['full_name'] %><%= method_prefix @v['list'] %><%= @v['method']['name'] %>
-<%= @v['method']["type"] %> <%= @v['method']["category"] %> method:
-<%= target @v['method']["aref"], @v['method']['callseq'] ||
- @v['method']["name"] + @v['method']["params"] %>
-<%= @v['method']["m_desc"] %>
diff --git a/lib/rdoc/generator/texinfo/texinfo.erb b/lib/rdoc/generator/texinfo/texinfo.erb
deleted file mode 100644
index 235f63d73c..0000000000
--- a/lib/rdoc/generator/texinfo/texinfo.erb
+++ /dev/null
@@ -1,28 +0,0 @@
-\input texinfo @c -*-texinfo-*-
-@c %**start of header
-@setfilename <%= @v['filename'] %>
-@settitle <%= @v['title'] %>
-@c %**end of header
-
-@contents @c TODO: whitespace is a mess... =\
-
-@ifnottex
-@node Top
-
-@top <%= @v['title'] %>
-@end ifnottex
-
-<% if @f = @v['files'].detect { |f| f.name =~ /Readme/i } %>
-<%= @f.values['description'] %><% end %>
-
-@menu
-<% @v['classes'].each do |klass| %>
-* <%= klass.name.gsub(/::/, '-') %>::<% end %>
-@c TODO: add files
-@end menu
-
-<% (@v['classes'] || []).each_with_index do |klass, i| %>
-<%= TexinfoTemplate.new(@v.merge('class' => klass.values),
- 'class.texinfo.erb').render %><% end %>
-
-@bye
diff --git a/lib/rdoc/generator/xml.rb b/lib/rdoc/generator/xml.rb
deleted file mode 100644
index 0d4c5a7ea1..0000000000
--- a/lib/rdoc/generator/xml.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-require 'rdoc/generator/html'
-
-##
-# Generate XML output as one big file
-
-class RDoc::Generator::XML < RDoc::Generator::HTML
-
- ##
- # Standard generator factory
-
- def self.for(options)
- new(options)
- end
-
- def initialize(*args)
- super
- end
-
- ##
- # Build the initial indices and output objects
- # based on an array of TopLevel objects containing
- # the extracted information.
-
- def generate(info)
- @info = info
- @files = []
- @classes = []
- @hyperlinks = {}
-
- build_indices
- generate_xml
- end
-
- ##
- # Generate:
- #
- # * a list of File objects for each TopLevel object.
- # * a list of Class objects for each first level
- # class or module in the TopLevel objects
- # * a complete list of all hyperlinkable terms (file,
- # class, module, and method names)
-
- def build_indices
- @info.each do |toplevel|
- @files << RDoc::Generator::File.new(toplevel, @options, RDoc::Generator::FILE_DIR)
- end
-
- RDoc::TopLevel.all_classes_and_modules.each do |cls|
- build_class_list(cls, @files[0], RDoc::Generator::CLASS_DIR)
- end
- end
-
- def build_class_list(from, html_file, class_dir)
- @classes << RDoc::Generator::Class.new(from, html_file, class_dir, @options)
- from.each_classmodule do |mod|
- build_class_list(mod, html_file, class_dir)
- end
- end
-
- ##
- # Generate all the HTML. For the one-file case, we generate
- # all the information in to one big hash
-
- def generate_xml
- values = {
- 'charset' => @options.charset,
- 'files' => gen_into(@files),
- 'classes' => gen_into(@classes)
- }
-
- template = RDoc::TemplatePage.new @template::ONE_PAGE
-
- if @options.op_name
- opfile = File.open(@options.op_name, "w")
- else
- opfile = $stdout
- end
- template.write_html_on(opfile, values)
- end
-
- def gen_into(list)
- res = []
- list.each do |item|
- res << item.value_hash
- end
- res
- end
-
- def gen_file_index
- gen_an_index(@files, 'Files')
- end
-
- def gen_class_index
- gen_an_index(@classes, 'Classes')
- end
-
- def gen_method_index
- gen_an_index(RDoc::Generator::HtmlMethod.all_methods, 'Methods')
- end
-
- def gen_an_index(collection, title)
- res = []
- collection.sort.each do |f|
- if f.document_self
- res << { "href" => f.path, "name" => f.index_name }
- end
- end
-
- return {
- "entries" => res,
- 'list_title' => title,
- 'index_url' => main_url,
- }
- end
-
-end
-
diff --git a/lib/rdoc/generator/xml/rdf.rb b/lib/rdoc/generator/xml/rdf.rb
deleted file mode 100644
index 7b15c69a18..0000000000
--- a/lib/rdoc/generator/xml/rdf.rb
+++ /dev/null
@@ -1,113 +0,0 @@
-require 'rdoc/generator/xml'
-
-module RDoc::Generator::XML::RDF
-
- CONTENTS_RDF = <<-EOF
-<% if defined? classes and classes["description"] then %>
- <description rd:parseType="Literal">
-<%= classes["description"] %>
- </description>
-<% end %>
-
-<% if defined? files and files["requires"] then %>
-<% files["requires"].each do |requires| %>
- <rd:required-file rd:name="<%= requires["name"] %>" />
-<% end # files["requires"] %>
-<% end %>
-
-<% if defined? classes and classes["includes"] then %>
- <IncludedModuleList>
-<% classes["includes"].each do |includes| %>
- <included-module rd:name="<%= includes["name"] %>" />
-<% end # includes["includes"] %>
- </IncludedModuleList>
-<% end %>
-
-<% if defined? classes and classes["sections"] then %>
-<% classes["sections"].each do |sections| %>
-<% if sections["attributes"] then %>
-<% sections["attributes"].each do |attributes| %>
- <contents>
- <Attribute rd:name="<%= attributes["name"] %>">
-<% if attributes["rw"] then %>
- <attribute-rw><%= attributes["rw"] %></attribute-rw>
-<% end %>
- <description rdf:parseType="Literal"><%= attributes["a_desc"] %></description>
- </Attribute>
- </contents>
-<% end # sections["attributes"] %>
-<% end %>
-
-<% if sections["method_list"] then %>
-<% sections["method_list"].each do |method_list| %>
-<% if method_list["methods"] then %>
-<% method_list["methods"].each do |methods| %>
- <contents>
- <Method rd:name="<%= methods["name"] %>" rd:visibility="<%= methods["type"] %>"
- rd:category="<%= methods["category"] %>" rd:id="<%= methods["aref"] %>">
- <parameters><%= methods["params"] %></parameters>
-<% if methods["m_desc"] then %>
- <description rdf:parseType="Literal">
-<%= methods["m_desc"] %>
- </description>
-<% end %>
-<% if methods["sourcecode"] then %>
- <source-code-listing rdf:parseType="Literal">
-<%= methods["sourcecode"] %>
- </source-code-listing>
-<% end %>
- </Method>
- </contents>
-<% end # method_list["methods"] %>
-<% end %>
-<% end # sections["method_list"] %>
-<% end %>
- <!-- end method list -->
-<% end # classes["sections"] %>
-<% end %>
- EOF
-
-########################################################################
-
- ONE_PAGE = %{<?xml version="1.0" encoding="utf-8"?>
-<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns="http://pragprog.com/rdoc/rdoc.rdf#"
- xmlns:rd="http://pragprog.com/rdoc/rdoc.rdf#">
-
-<!-- RDoc -->
-<% values["files"].each do |files| %>
- <rd:File rd:name="<%= files["short_name"] %>" rd:id="<%= files["href"] %>">
- <path><%= files["full_path"] %></path>
- <dtm-modified><%= files["dtm_modified"] %></dtm-modified>
-} + CONTENTS_RDF + %{
- </rd:File>
-<% end # values["files"] %>
-<% values["classes"].each do |classes| %>
- <<%= values["classmod"] %> rd:name="<%= classes["full_name"] %>" rd:id="<%= classes["full_name"] %>">
- <classmod-info>
-<% if classes["infiles"] then %>
- <InFiles>
-<% classes["infiles"].each do |infiles| %>
- <infile>
- <File rd:name="<%= infiles["full_path"] %>"
-<% if infiles["full_path_url"] then %>
- rdf:about="<%= infiles["full_path_url"] %>"
-<% end %>
- />
- </infile>
-<% end # classes["infiles"] %>
- </InFiles>
-<% end %>
-<% if classes["parent"] then %>
- <superclass><%= href classes["par_url"], classes["parent"] %></superclass>
-<% end %>
- </classmod-info>
-} + CONTENTS_RDF + %{
- </<%= classes["classmod"] %>>
-<% end # values["classes"] %>
-<!-- /RDoc -->
-</rdf:RDF>
-}
-
-end
-
diff --git a/lib/rdoc/generator/xml/xml.rb b/lib/rdoc/generator/xml/xml.rb
deleted file mode 100644
index 4b54e7350f..0000000000
--- a/lib/rdoc/generator/xml/xml.rb
+++ /dev/null
@@ -1,123 +0,0 @@
-require 'rdoc/generator/xml'
-
-module RDoc::Generator::XML::XML
-
- CONTENTS_XML = <<-EOF
-<% if defined? classes and classes["description"] then %>
- <description>
-<%= classes["description"] %>
- </description>
-<% end %>
- <contents>
-<% if defined? files and files["requires"] then %>
- <required-file-list>
-<% files["requires"].each do |requires| %>
- <required-file name="<%= requires["name"] %>"
-<% if requires["aref"] then %>
- href="<%= requires["aref"] %>"
-<% end %>
- />
-<% end %><%# files["requires"] %>
- </required-file-list>
-<% end %>
-<% if defined? classes and classes["sections"] then %>
-<% classes["sections"].each do |sections| %>
-<% if sections["constants"] then %>
- <constant-list>
-<% sections["constants"].each do |constant| %>
- <constant name="<%= constant["name"] %>">
-<% if constant["value"] then %>
- <value><%= constant["value"] %></value>
-<% end %>
- <description><%= constant["a_desc"] %></description>
- </constant>
-<% end %><%# sections["constants"] %>
- </constant-list>
-<% end %>
-<% if sections["attributes"] then %>
- <attribute-list>
-<% sections["attributes"].each do |attributes| %>
- <attribute name="<%= attributes["name"] %>">
-<% if attributes["rw"] then %>
- <attribute-rw><%= attributes["rw"] %></attribute-rw>
-<% end %>
- <description><%= attributes["a_desc"] %></description>
- </attribute>
-<% end %><%# sections["attributes"] %>
- </attribute-list>
-<% end %>
-<% if sections["method_list"] then %>
- <method-list>
-<% sections["method_list"].each do |method_list| %>
-<% if method_list["methods"] then %>
-<% method_list["methods"].each do |methods| %>
- <method name="<%= methods["name"] %>" type="<%= methods["type"] %>" category="<%= methods["category"] %>" id="<%= methods["aref"] %>">
- <parameters><%= methods["params"] %></parameters>
-<% if methods["m_desc"] then %>
- <description>
-<%= methods["m_desc"] %>
- </description>
-<% end %>
-<% if methods["sourcecode"] then %>
- <source-code-listing>
-<%= methods["sourcecode"] %>
- </source-code-listing>
-<% end %>
- </method>
-<% end %><%# method_list["methods"] %>
-<% end %>
-<% end %><%# sections["method_list"] %>
- </method-list>
-<% end %>
-<% end %><%# classes["sections"] %>
-<% end %>
-<% if defined? classes and classes["includes"] then %>
- <included-module-list>
-<% classes["includes"].each do |includes| %>
- <included-module name="<%= includes["name"] %>"
-<% if includes["aref"] then %>
- href="<%= includes["aref"] %>"
-<% end %>
- />
-<% end %><%# classes["includes"] %>
- </included-module-list>
-<% end %>
- </contents>
- EOF
-
- ONE_PAGE = %{<?xml version="1.0" encoding="utf-8"?>
-<rdoc>
-<file-list>
-<% values["files"].each do |files| %>
- <file name="<%= files["short_name"] %>" id="<%= files["href"] %>">
- <file-info>
- <path><%= files["full_path"] %></path>
- <dtm-modified><%= files["dtm_modified"] %></dtm-modified>
- </file-info>
-} + CONTENTS_XML + %{
- </file>
-<% end %><%# values["files"] %>
-</file-list>
-<class-module-list>
-<% values["classes"].each do |classes| %>
- <<%= classes["classmod"] %> name="<%= classes["full_name"] %>" id="<%= classes["full_name"] %>">
- <classmod-info>
-<% if classes["infiles"] then %>
- <infiles>
-<% classes["infiles"].each do |infiles| %>
- <infile><%= href infiles["full_path_url"], infiles["full_path"] %></infile>
-<% end %><%# classes["infiles"] %>
- </infiles>
-<% end %>
-<% if classes["parent"] then %>
- <superclass><%= href classes["par_url"], classes["parent"] %></superclass>
-<% end %>
- </classmod-info>
-} + CONTENTS_XML + %{
- </<%= classes["classmod"] %>>
-<% end %><%# values["classes"] %>
-</class-module-list>
-</rdoc>
-}
-
-end
diff --git a/lib/rdoc/known_classes.rb b/lib/rdoc/known_classes.rb
deleted file mode 100644
index dbb1802f5a..0000000000
--- a/lib/rdoc/known_classes.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-module RDoc
-
- ##
- # Ruby's built-in classes, modules and exceptions
-
- KNOWN_CLASSES = {
- "rb_cArray" => "Array",
- "rb_cBignum" => "Bignum",
- "rb_cClass" => "Class",
- "rb_cData" => "Data",
- "rb_cDir" => "Dir",
- "rb_cFalseClass" => "FalseClass",
- "rb_cFile" => "File",
- "rb_cFixnum" => "Fixnum",
- "rb_cFloat" => "Float",
- "rb_cHash" => "Hash",
- "rb_cIO" => "IO",
- "rb_cInteger" => "Integer",
- "rb_cModule" => "Module",
- "rb_cNilClass" => "NilClass",
- "rb_cNumeric" => "Numeric",
- "rb_cObject" => "Object",
- "rb_cProc" => "Proc",
- "rb_cRange" => "Range",
- "rb_cRegexp" => "Regexp",
- "rb_cRubyVM" => "RubyVM",
- "rb_cString" => "String",
- "rb_cStruct" => "Struct",
- "rb_cSymbol" => "Symbol",
- "rb_cThread" => "Thread",
- "rb_cTime" => "Time",
- "rb_cTrueClass" => "TrueClass",
-
- "rb_eArgError" => "ArgError",
- "rb_eEOFError" => "EOFError",
- "rb_eException" => "Exception",
- "rb_eFatal" => "Fatal",
- "rb_eFloatDomainError" => "FloatDomainError",
- "rb_eIOError" => "IOError",
- "rb_eIndexError" => "IndexError",
- "rb_eInterrupt" => "Interrupt",
- "rb_eLoadError" => "LoadError",
- "rb_eNameError" => "NameError",
- "rb_eNoMemError" => "NoMemError",
- "rb_eNotImpError" => "NotImpError",
- "rb_eRangeError" => "RangeError",
- "rb_eRuntimeError" => "RuntimeError",
- "rb_eScriptError" => "ScriptError",
- "rb_eSecurityError" => "SecurityError",
- "rb_eSignal" => "Signal",
- "rb_eStandardError" => "StandardError",
- "rb_eSyntaxError" => "SyntaxError",
- "rb_eSystemCallError" => "SystemCallError",
- "rb_eSystemExit" => "SystemExit",
- "rb_eTypeError" => "TypeError",
- "rb_eZeroDivError" => "ZeroDivError",
-
- "rb_mComparable" => "Comparable",
- "rb_mEnumerable" => "Enumerable",
- "rb_mErrno" => "Errno",
- "rb_mFileTest" => "FileTest",
- "rb_mGC" => "GC",
- "rb_mKernel" => "Kernel",
- "rb_mMath" => "Math",
- "rb_mProcess" => "Process"
- }
-
-end
diff --git a/lib/rdoc/markup.rb b/lib/rdoc/markup.rb
deleted file mode 100644
index 9d22b38946..0000000000
--- a/lib/rdoc/markup.rb
+++ /dev/null
@@ -1,378 +0,0 @@
-require 'rdoc'
-
-##
-# RDoc::Markup parses plain text documents and attempts to decompose them into
-# their constituent parts. Some of these parts are high-level: paragraphs,
-# chunks of verbatim text, list entries and the like. Other parts happen at
-# the character level: a piece of bold text, a word in code font. This markup
-# is similar in spirit to that used on WikiWiki webs, where folks create web
-# pages using a simple set of formatting rules.
-#
-# RDoc::Markup itself does no output formatting: this is left to a different
-# set of classes.
-#
-# RDoc::Markup is extendable at runtime: you can add \new markup elements to
-# be recognised in the documents that RDoc::Markup parses.
-#
-# RDoc::Markup is intended to be the basis for a family of tools which share
-# the common requirement that simple, plain-text should be rendered in a
-# variety of different output formats and media. It is envisaged that
-# RDoc::Markup could be the basis for formatting RDoc style comment blocks,
-# Wiki entries, and online FAQs.
-#
-# == Synopsis
-#
-# This code converts +input_string+ to HTML. The conversion takes place in
-# the +convert+ method, so you can use the same RDoc::Markup converter to
-# convert multiple input strings.
-#
-# require 'rdoc/markup/to_html'
-#
-# h = RDoc::Markup::ToHtml.new
-#
-# puts h.convert(input_string)
-#
-# You can extend the RDoc::Markup parser to recognise new markup
-# sequences, and to add special processing for text that matches a
-# regular expression. Here we make WikiWords significant to the parser,
-# and also make the sequences {word} and \<no>text...</no> signify
-# strike-through text. When then subclass the HTML output class to deal
-# with these:
-#
-# require 'rdoc/markup'
-# require 'rdoc/markup/to_html'
-#
-# class WikiHtml < RDoc::Markup::ToHtml
-# def handle_special_WIKIWORD(special)
-# "<font color=red>" + special.text + "</font>"
-# end
-# end
-#
-# m = RDoc::Markup.new
-# m.add_word_pair("{", "}", :STRIKE)
-# m.add_html("no", :STRIKE)
-#
-# m.add_special(/\b([A-Z][a-z]+[A-Z]\w+)/, :WIKIWORD)
-#
-# wh = WikiHtml.new
-# wh.add_tag(:STRIKE, "<strike>", "</strike>")
-#
-# puts "<body>#{wh.convert ARGF.read}</body>"
-#
-#--
-# Author:: Dave Thomas, dave@pragmaticprogrammer.com
-# License:: Ruby license
-
-class RDoc::Markup
-
- SPACE = ?\s
-
- # List entries look like:
- # * text
- # 1. text
- # [label] text
- # label:: text
- #
- # Flag it as a list entry, and work out the indent for subsequent lines
-
- SIMPLE_LIST_RE = /^(
- ( \* (?# bullet)
- |- (?# bullet)
- |\d+\. (?# numbered )
- |[A-Za-z]\. (?# alphabetically numbered )
- )
- \s+
- )\S/x
-
- LABEL_LIST_RE = /^(
- ( \[.*?\] (?# labeled )
- |\S.*:: (?# note )
- )(?:\s+|$)
- )/x
-
- ##
- # Take a block of text and use various heuristics to determine it's
- # structure (paragraphs, lists, and so on). Invoke an event handler as we
- # identify significant chunks.
-
- def initialize
- @am = RDoc::Markup::AttributeManager.new
- @output = nil
- end
-
- ##
- # Add to the sequences used to add formatting to an individual word (such
- # as *bold*). Matching entries will generate attributes that the output
- # formatters can recognize by their +name+.
-
- def add_word_pair(start, stop, name)
- @am.add_word_pair(start, stop, name)
- end
-
- ##
- # Add to the sequences recognized as general markup.
-
- def add_html(tag, name)
- @am.add_html(tag, name)
- end
-
- ##
- # Add to other inline sequences. For example, we could add WikiWords using
- # something like:
- #
- # parser.add_special(/\b([A-Z][a-z]+[A-Z]\w+)/, :WIKIWORD)
- #
- # Each wiki word will be presented to the output formatter via the
- # accept_special method.
-
- def add_special(pattern, name)
- @am.add_special(pattern, name)
- end
-
- ##
- # We take a string, split it into lines, work out the type of each line,
- # and from there deduce groups of lines (for example all lines in a
- # paragraph). We then invoke the output formatter using a Visitor to
- # display the result.
-
- def convert(str, op)
- lines = str.split(/\r?\n/).map { |line| Line.new line }
- @lines = Lines.new lines
-
- return "" if @lines.empty?
- @lines.normalize
- assign_types_to_lines
- group = group_lines
- # call the output formatter to handle the result
- #group.each { |line| p line }
- group.accept @am, op
- end
-
- private
-
- ##
- # Look through the text at line indentation. We flag each line as being
- # Blank, a paragraph, a list element, or verbatim text.
-
- def assign_types_to_lines(margin = 0, level = 0)
- while line = @lines.next
- if line.blank? then
- line.stamp :BLANK, level
- next
- end
-
- # if a line contains non-blanks before the margin, then it must belong
- # to an outer level
-
- text = line.text
-
- for i in 0...margin
- if text[i] != SPACE
- @lines.unget
- return
- end
- end
-
- active_line = text[margin..-1]
-
- # Rules (horizontal lines) look like
- #
- # --- (three or more hyphens)
- #
- # The more hyphens, the thicker the rule
- #
-
- if /^(---+)\s*$/ =~ active_line
- line.stamp :RULE, level, $1.length-2
- next
- end
-
- # Then look for list entries. First the ones that have to have
- # text following them (* xxx, - xxx, and dd. xxx)
-
- if SIMPLE_LIST_RE =~ active_line
- offset = margin + $1.length
- prefix = $2
- prefix_length = prefix.length
-
- flag = case prefix
- when "*","-" then :BULLET
- when /^\d/ then :NUMBER
- when /^[A-Z]/ then :UPPERALPHA
- when /^[a-z]/ then :LOWERALPHA
- else raise "Invalid List Type: #{self.inspect}"
- end
-
- line.stamp :LIST, level+1, prefix, flag
- text[margin, prefix_length] = " " * prefix_length
- assign_types_to_lines(offset, level + 1)
- next
- end
-
- if LABEL_LIST_RE =~ active_line
- offset = margin + $1.length
- prefix = $2
- prefix_length = prefix.length
-
- next if handled_labeled_list(line, level, margin, offset, prefix)
- end
-
- # Headings look like
- # = Main heading
- # == Second level
- # === Third
- #
- # Headings reset the level to 0
-
- if active_line[0] == ?= and active_line =~ /^(=+)\s*(.*)/
- prefix_length = $1.length
- prefix_length = 6 if prefix_length > 6
- line.stamp :HEADING, 0, prefix_length
- line.strip_leading(margin + prefix_length)
- next
- end
-
- # If the character's a space, then we have verbatim text,
- # otherwise
-
- if active_line[0] == SPACE
- line.strip_leading(margin) if margin > 0
- line.stamp :VERBATIM, level
- else
- line.stamp :PARAGRAPH, level
- end
- end
- end
-
- ##
- # Handle labeled list entries, We have a special case to deal with.
- # Because the labels can be long, they force the remaining block of text
- # over the to right:
- #
- # this is a long label that I wrote:: and here is the
- # block of text with
- # a silly margin
- #
- # So we allow the special case. If the label is followed by nothing, and
- # if the following line is indented, then we take the indent of that line
- # as the new margin.
- #
- # this is a long label that I wrote::
- # here is a more reasonably indented block which
- # will be attached to the label.
- #
-
- def handled_labeled_list(line, level, margin, offset, prefix)
- prefix_length = prefix.length
- text = line.text
- flag = nil
-
- case prefix
- when /^\[/ then
- flag = :LABELED
- prefix = prefix[1, prefix.length-2]
- when /:$/ then
- flag = :NOTE
- prefix.chop!
- else
- raise "Invalid List Type: #{self.inspect}"
- end
-
- # body is on the next line
- if text.length <= offset then
- original_line = line
- line = @lines.next
- return false unless line
- text = line.text
-
- for i in 0..margin
- if text[i] != SPACE
- @lines.unget
- return false
- end
- end
-
- i = margin
- i += 1 while text[i] == SPACE
-
- if i >= text.length then
- @lines.unget
- return false
- else
- offset = i
- prefix_length = 0
-
- if text[offset..-1] =~ SIMPLE_LIST_RE then
- @lines.unget
- line = original_line
- line.text = ''
- else
- @lines.delete original_line
- end
- end
- end
-
- line.stamp :LIST, level+1, prefix, flag
- text[margin, prefix_length] = " " * prefix_length
- assign_types_to_lines(offset, level + 1)
- return true
- end
-
- ##
- # Return a block consisting of fragments which are paragraphs, list
- # entries or verbatim text. We merge consecutive lines of the same type
- # and level together. We are also slightly tricky with lists: the lines
- # following a list introduction look like paragraph lines at the next
- # level, and we remap them into list entries instead.
-
- def group_lines
- @lines.rewind
-
- in_list = false
- wanted_type = wanted_level = nil
-
- block = LineCollection.new
- group = nil
-
- while line = @lines.next
- if line.level == wanted_level and line.type == wanted_type
- group.add_text(line.text)
- else
- group = block.fragment_for(line)
- block.add(group)
-
- if line.type == :LIST
- wanted_type = :PARAGRAPH
- else
- wanted_type = line.type
- end
-
- wanted_level = line.type == :HEADING ? line.param : line.level
- end
- end
-
- block.normalize
- block
- end
-
- ##
- # For debugging, we allow access to our line contents as text.
-
- def content
- @lines.as_text
- end
- public :content
-
- ##
- # For debugging, return the list of line types.
-
- def get_line_types
- @lines.line_types
- end
- public :get_line_types
-
-end
-
-require 'rdoc/markup/fragments'
-require 'rdoc/markup/inline'
-require 'rdoc/markup/lines'
diff --git a/lib/rdoc/markup/attribute_manager.rb b/lib/rdoc/markup/attribute_manager.rb
deleted file mode 100644
index d13b79376c..0000000000
--- a/lib/rdoc/markup/attribute_manager.rb
+++ /dev/null
@@ -1,265 +0,0 @@
-require 'rdoc/markup/inline'
-
-class RDoc::Markup::AttributeManager
-
- NULL = "\000".freeze
-
- ##
- # We work by substituting non-printing characters in to the text. For now
- # I'm assuming that I can substitute a character in the range 0..8 for a 7
- # bit character without damaging the encoded string, but this might be
- # optimistic
-
- A_PROTECT = 004
- PROTECT_ATTR = A_PROTECT.chr
-
- ##
- # This maps delimiters that occur around words (such as *bold* or +tt+)
- # where the start and end delimiters and the same. This lets us optimize
- # the regexp
-
- MATCHING_WORD_PAIRS = {}
-
- ##
- # And this is used when the delimiters aren't the same. In this case the
- # hash maps a pattern to the attribute character
-
- WORD_PAIR_MAP = {}
-
- ##
- # This maps HTML tags to the corresponding attribute char
-
- HTML_TAGS = {}
-
- ##
- # And this maps _special_ sequences to a name. A special sequence is
- # something like a WikiWord
-
- SPECIAL = {}
-
- ##
- # Return an attribute object with the given turn_on and turn_off bits set
-
- def attribute(turn_on, turn_off)
- RDoc::Markup::AttrChanger.new turn_on, turn_off
- end
-
- def change_attribute(current, new)
- diff = current ^ new
- attribute(new & diff, current & diff)
- end
-
- def changed_attribute_by_name(current_set, new_set)
- current = new = 0
- current_set.each do |name|
- current |= RDoc::Markup::Attribute.bitmap_for(name)
- end
-
- new_set.each do |name|
- new |= RDoc::Markup::Attribute.bitmap_for(name)
- end
-
- change_attribute(current, new)
- end
-
- def copy_string(start_pos, end_pos)
- res = @str[start_pos...end_pos]
- res.gsub!(/\000/, '')
- res
- end
-
- ##
- # Map attributes like <b>text</b>to the sequence
- # \001\002<char>\001\003<char>, where <char> is a per-attribute specific
- # character
-
- def convert_attrs(str, attrs)
- # first do matching ones
- tags = MATCHING_WORD_PAIRS.keys.join("")
-
- re = /(^|\W)([#{tags}])([#:\\]?[\w.\/-]+?\S?)\2(\W|$)/
-
- 1 while str.gsub!(re) do
- attr = MATCHING_WORD_PAIRS[$2]
- attrs.set_attrs($`.length + $1.length + $2.length, $3.length, attr)
- $1 + NULL * $2.length + $3 + NULL * $2.length + $4
- end
-
- # then non-matching
- unless WORD_PAIR_MAP.empty? then
- WORD_PAIR_MAP.each do |regexp, attr|
- str.gsub!(regexp) {
- attrs.set_attrs($`.length + $1.length, $2.length, attr)
- NULL * $1.length + $2 + NULL * $3.length
- }
- end
- end
- end
-
- def convert_html(str, attrs)
- tags = HTML_TAGS.keys.join '|'
-
- 1 while str.gsub!(/<(#{tags})>(.*?)<\/\1>/i) {
- attr = HTML_TAGS[$1.downcase]
- html_length = $1.length + 2
- seq = NULL * html_length
- attrs.set_attrs($`.length + html_length, $2.length, attr)
- seq + $2 + seq + NULL
- }
- end
-
- def convert_specials(str, attrs)
- unless SPECIAL.empty?
- SPECIAL.each do |regexp, attr|
- str.scan(regexp) do
- attrs.set_attrs($`.length, $&.length,
- attr | RDoc::Markup::Attribute::SPECIAL)
- end
- end
- end
- end
-
- ##
- # A \ in front of a character that would normally be processed turns off
- # processing. We do this by turning \< into <#{PROTECT}
-
- PROTECTABLE = %w[<\\]
-
- def mask_protected_sequences
- protect_pattern = Regexp.new("\\\\([#{Regexp.escape(PROTECTABLE.join(''))}])")
- @str.gsub!(protect_pattern, "\\1#{PROTECT_ATTR}")
- end
-
- def unmask_protected_sequences
- @str.gsub!(/(.)#{PROTECT_ATTR}/, "\\1\000")
- end
-
- def initialize
- add_word_pair("*", "*", :BOLD)
- add_word_pair("_", "_", :EM)
- add_word_pair("+", "+", :TT)
-
- add_html("em", :EM)
- add_html("i", :EM)
- add_html("b", :BOLD)
- add_html("tt", :TT)
- add_html("code", :TT)
- end
-
- def add_word_pair(start, stop, name)
- raise ArgumentError, "Word flags may not start with '<'" if
- start[0,1] == '<'
-
- bitmap = RDoc::Markup::Attribute.bitmap_for name
-
- if start == stop then
- MATCHING_WORD_PAIRS[start] = bitmap
- else
- pattern = /(#{Regexp.escape start})(\S+)(#{Regexp.escape stop})/
- WORD_PAIR_MAP[pattern] = bitmap
- end
-
- PROTECTABLE << start[0,1]
- PROTECTABLE.uniq!
- end
-
- def add_html(tag, name)
- HTML_TAGS[tag.downcase] = RDoc::Markup::Attribute.bitmap_for name
- end
-
- def add_special(pattern, name)
- SPECIAL[pattern] = RDoc::Markup::Attribute.bitmap_for name
- end
-
- def flow(str)
- @str = str
-
- mask_protected_sequences
-
- @attrs = RDoc::Markup::AttrSpan.new @str.length
-
- convert_attrs(@str, @attrs)
- convert_html(@str, @attrs)
- convert_specials(str, @attrs)
-
- unmask_protected_sequences
-
- return split_into_flow
- end
-
- def display_attributes
- puts
- puts @str.tr(NULL, "!")
- bit = 1
- 16.times do |bno|
- line = ""
- @str.length.times do |i|
- if (@attrs[i] & bit) == 0
- line << " "
- else
- if bno.zero?
- line << "S"
- else
- line << ("%d" % (bno+1))
- end
- end
- end
- puts(line) unless line =~ /^ *$/
- bit <<= 1
- end
- end
-
- def split_into_flow
- res = []
- current_attr = 0
- str = ""
-
- str_len = @str.length
-
- # skip leading invisible text
- i = 0
- i += 1 while i < str_len and @str[i].chr == "\0"
- start_pos = i
-
- # then scan the string, chunking it on attribute changes
- while i < str_len
- new_attr = @attrs[i]
- if new_attr != current_attr
- if i > start_pos
- res << copy_string(start_pos, i)
- start_pos = i
- end
-
- res << change_attribute(current_attr, new_attr)
- current_attr = new_attr
-
- if (current_attr & RDoc::Markup::Attribute::SPECIAL) != 0 then
- i += 1 while
- i < str_len and (@attrs[i] & RDoc::Markup::Attribute::SPECIAL) != 0
-
- res << RDoc::Markup::Special.new(current_attr,
- copy_string(start_pos, i))
- start_pos = i
- next
- end
- end
-
- # move on, skipping any invisible characters
- begin
- i += 1
- end while i < str_len and @str[i].chr == "\0"
- end
-
- # tidy up trailing text
- if start_pos < str_len
- res << copy_string(start_pos, str_len)
- end
-
- # and reset to all attributes off
- res << change_attribute(current_attr, 0) if current_attr != 0
-
- return res
- end
-
-end
-
diff --git a/lib/rdoc/markup/formatter.rb b/lib/rdoc/markup/formatter.rb
deleted file mode 100644
index 14cbae59f9..0000000000
--- a/lib/rdoc/markup/formatter.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-require 'rdoc/markup'
-
-class RDoc::Markup::Formatter
-
- def initialize
- @markup = RDoc::Markup.new
- end
-
- def convert(content)
- @markup.convert content, self
- end
-
-end
-
diff --git a/lib/rdoc/markup/fragments.rb b/lib/rdoc/markup/fragments.rb
deleted file mode 100644
index b7f9b605c8..0000000000
--- a/lib/rdoc/markup/fragments.rb
+++ /dev/null
@@ -1,337 +0,0 @@
-require 'rdoc/markup'
-require 'rdoc/markup/lines'
-
-class RDoc::Markup
-
- ##
- # A Fragment is a chunk of text, subclassed as a paragraph, a list
- # entry, or verbatim text.
-
- class Fragment
- attr_reader :level, :param, :txt
- attr_accessor :type
-
- ##
- # This is a simple factory system that lets us associate fragement
- # types (a string) with a subclass of fragment
-
- TYPE_MAP = {}
-
- def self.type_name(name)
- TYPE_MAP[name] = self
- end
-
- def self.for(line)
- klass = TYPE_MAP[line.type] ||
- raise("Unknown line type: '#{line.type.inspect}:' '#{line.text}'")
- return klass.new(line.level, line.param, line.flag, line.text)
- end
-
- def initialize(level, param, type, txt)
- @level = level
- @param = param
- @type = type
- @txt = ""
- add_text(txt) if txt
- end
-
- def add_text(txt)
- @txt << " " if @txt.length > 0
- @txt << txt.tr_s("\n ", " ").strip
- end
-
- def to_s
- "L#@level: #{self.class.name.split('::')[-1]}\n#@txt"
- end
-
- end
-
- ##
- # A paragraph is a fragment which gets wrapped to fit. We remove all
- # newlines when we're created, and have them put back on output.
-
- class Paragraph < Fragment
- type_name :PARAGRAPH
- end
-
- class BlankLine < Paragraph
- type_name :BLANK
- end
-
- class Heading < Paragraph
- type_name :HEADING
-
- def head_level
- @param.to_i
- end
- end
-
- ##
- # A List is a fragment with some kind of label
-
- class ListBase < Paragraph
- LIST_TYPES = [
- :BULLET,
- :NUMBER,
- :UPPERALPHA,
- :LOWERALPHA,
- :LABELED,
- :NOTE,
- ]
- end
-
- class ListItem < ListBase
- type_name :LIST
-
- def to_s
- text = if [:NOTE, :LABELED].include? type then
- "#{@param}: #{@txt}"
- else
- @txt
- end
-
- "L#@level: #{type} #{self.class.name.split('::')[-1]}\n#{text}"
- end
-
- end
-
- class ListStart < ListBase
- def initialize(level, param, type)
- super(level, param, type, nil)
- end
- end
-
- class ListEnd < ListBase
- def initialize(level, type)
- super(level, "", type, nil)
- end
- end
-
- ##
- # Verbatim code contains lines that don't get wrapped.
-
- class Verbatim < Fragment
- type_name :VERBATIM
-
- def add_text(txt)
- @txt << txt.chomp << "\n"
- end
-
- end
-
- ##
- # A horizontal rule
-
- class Rule < Fragment
- type_name :RULE
- end
-
- ##
- # Collect groups of lines together. Each group will end up containing a flow
- # of text.
-
- class LineCollection
-
- def initialize
- @fragments = []
- end
-
- def add(fragment)
- @fragments << fragment
- end
-
- def each(&b)
- @fragments.each(&b)
- end
-
- def to_a # :nodoc:
- @fragments.map {|fragment| fragment.to_s}
- end
-
- ##
- # Factory for different fragment types
-
- def fragment_for(*args)
- Fragment.for(*args)
- end
-
- ##
- # Tidy up at the end
-
- def normalize
- change_verbatim_blank_lines
- add_list_start_and_ends
- add_list_breaks
- tidy_blank_lines
- end
-
- def to_s
- @fragments.join("\n----\n")
- end
-
- def accept(am, visitor)
- visitor.start_accepting
-
- @fragments.each do |fragment|
- case fragment
- when Verbatim
- visitor.accept_verbatim(am, fragment)
- when Rule
- visitor.accept_rule(am, fragment)
- when ListStart
- visitor.accept_list_start(am, fragment)
- when ListEnd
- visitor.accept_list_end(am, fragment)
- when ListItem
- visitor.accept_list_item(am, fragment)
- when BlankLine
- visitor.accept_blank_line(am, fragment)
- when Heading
- visitor.accept_heading(am, fragment)
- when Paragraph
- visitor.accept_paragraph(am, fragment)
- end
- end
-
- visitor.end_accepting
- end
-
- private
-
- # If you have:
- #
- # normal paragraph text.
- #
- # this is code
- #
- # and more code
- #
- # You'll end up with the fragments Paragraph, BlankLine, Verbatim,
- # BlankLine, Verbatim, BlankLine, etc.
- #
- # The BlankLine in the middle of the verbatim chunk needs to be changed to
- # a real verbatim newline, and the two verbatim blocks merged
-
- def change_verbatim_blank_lines
- frag_block = nil
- blank_count = 0
- @fragments.each_with_index do |frag, i|
- if frag_block.nil?
- frag_block = frag if Verbatim === frag
- else
- case frag
- when Verbatim
- blank_count.times { frag_block.add_text("\n") }
- blank_count = 0
- frag_block.add_text(frag.txt)
- @fragments[i] = nil # remove out current fragment
- when BlankLine
- if frag_block
- blank_count += 1
- @fragments[i] = nil
- end
- else
- frag_block = nil
- blank_count = 0
- end
- end
- end
- @fragments.compact!
- end
-
- ##
- # List nesting is implicit given the level of indentation. Make it
- # explicit, just to make life a tad easier for the output processors
-
- def add_list_start_and_ends
- level = 0
- res = []
- type_stack = []
-
- @fragments.each do |fragment|
- # $stderr.puts "#{level} : #{fragment.class.name} : #{fragment.level}"
- new_level = fragment.level
- while (level < new_level)
- level += 1
- type = fragment.type
- res << ListStart.new(level, fragment.param, type) if type
- type_stack.push type
- # $stderr.puts "Start: #{level}"
- end
-
- while level > new_level
- type = type_stack.pop
- res << ListEnd.new(level, type) if type
- level -= 1
- # $stderr.puts "End: #{level}, #{type}"
- end
-
- res << fragment
- level = fragment.level
- end
- level.downto(1) do |i|
- type = type_stack.pop
- res << ListEnd.new(i, type) if type
- end
-
- @fragments = res
- end
-
- ##
- # Inserts start/ends between list entries at the same level that have
- # different element types
-
- def add_list_breaks
- res = @fragments
-
- @fragments = []
- list_stack = []
-
- res.each do |fragment|
- case fragment
- when ListStart
- list_stack.push fragment
- when ListEnd
- start = list_stack.pop
- fragment.type = start.type
- when ListItem
- l = list_stack.last
- if fragment.type != l.type
- @fragments << ListEnd.new(l.level, l.type)
- start = ListStart.new(l.level, fragment.param, fragment.type)
- @fragments << start
- list_stack.pop
- list_stack.push start
- end
- else
- ;
- end
- @fragments << fragment
- end
- end
-
- ##
- # Tidy up the blank lines:
- # * change Blank/ListEnd into ListEnd/Blank
- # * remove blank lines at the front
-
- def tidy_blank_lines
- (@fragments.size - 1).times do |i|
- if BlankLine === @fragments[i] and ListEnd === @fragments[i+1] then
- @fragments[i], @fragments[i+1] = @fragments[i+1], @fragments[i]
- end
- end
-
- # remove leading blanks
- @fragments.each_with_index do |f, i|
- break unless f.kind_of? BlankLine
- @fragments[i] = nil
- end
-
- @fragments.compact!
- end
-
- end
-
-end
-
diff --git a/lib/rdoc/markup/inline.rb b/lib/rdoc/markup/inline.rb
deleted file mode 100644
index 46c9b5822c..0000000000
--- a/lib/rdoc/markup/inline.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-require 'rdoc/markup'
-
-class RDoc::Markup
-
- ##
- # We manage a set of attributes. Each attribute has a symbol name and a bit
- # value.
-
- class Attribute
- SPECIAL = 1
-
- @@name_to_bitmap = { :_SPECIAL_ => SPECIAL }
- @@next_bitmap = 2
-
- def self.bitmap_for(name)
- bitmap = @@name_to_bitmap[name]
- unless bitmap then
- bitmap = @@next_bitmap
- @@next_bitmap <<= 1
- @@name_to_bitmap[name] = bitmap
- end
- bitmap
- end
-
- def self.as_string(bitmap)
- return "none" if bitmap.zero?
- res = []
- @@name_to_bitmap.each do |name, bit|
- res << name if (bitmap & bit) != 0
- end
- res.join(",")
- end
-
- def self.each_name_of(bitmap)
- @@name_to_bitmap.each do |name, bit|
- next if bit == SPECIAL
- yield name.to_s if (bitmap & bit) != 0
- end
- end
- end
-
- AttrChanger = Struct.new(:turn_on, :turn_off)
-
- ##
- # An AttrChanger records a change in attributes. It contains a bitmap of the
- # attributes to turn on, and a bitmap of those to turn off.
-
- class AttrChanger
- def to_s
- "Attr: +#{Attribute.as_string(turn_on)}/-#{Attribute.as_string(turn_on)}"
- end
- end
-
- ##
- # An array of attributes which parallels the characters in a string.
-
- class AttrSpan
- def initialize(length)
- @attrs = Array.new(length, 0)
- end
-
- def set_attrs(start, length, bits)
- for i in start ... (start+length)
- @attrs[i] |= bits
- end
- end
-
- def [](n)
- @attrs[n]
- end
- end
-
- ##
- # Hold details of a special sequence
-
- class Special
- attr_reader :type
- attr_accessor :text
-
- def initialize(type, text)
- @type, @text = type, text
- end
-
- def ==(o)
- self.text == o.text && self.type == o.type
- end
-
- def inspect
- "#<RDoc::Markup::Special:0x%x @type=%p, name=%p @text=%p>" % [
- object_id, @type, RDoc::Markup::Attribute.as_string(type), text.dump]
- end
-
- def to_s
- "Special: type=#{type}, name=#{RDoc::Markup::Attribute.as_string type}, text=#{text.dump}"
- end
-
- end
-
-end
-
-require 'rdoc/markup/attribute_manager'
diff --git a/lib/rdoc/markup/lines.rb b/lib/rdoc/markup/lines.rb
deleted file mode 100644
index 069492122f..0000000000
--- a/lib/rdoc/markup/lines.rb
+++ /dev/null
@@ -1,152 +0,0 @@
-class RDoc::Markup
-
- ##
- # We store the lines we're working on as objects of class Line. These
- # contain the text of the line, along with a flag indicating the line type,
- # and an indentation level.
-
- class Line
- INFINITY = 9999
-
- LINE_TYPES = [
- :BLANK,
- :HEADING,
- :LIST,
- :PARAGRAPH,
- :RULE,
- :VERBATIM,
- ]
-
- # line type
- attr_accessor :type
-
- # The indentation nesting level
- attr_accessor :level
-
- # The contents
- attr_accessor :text
-
- # A prefix or parameter. For LIST lines, this is
- # the text that introduced the list item (the label)
- attr_accessor :param
-
- # A flag. For list lines, this is the type of the list
- attr_accessor :flag
-
- # the number of leading spaces
- attr_accessor :leading_spaces
-
- # true if this line has been deleted from the list of lines
- attr_accessor :deleted
-
- def initialize(text)
- @text = text.dup
- @deleted = false
-
- # expand tabs
- 1 while @text.gsub!(/\t+/) { ' ' * (8*$&.length - $`.length % 8)} && $~ #`
-
- # Strip trailing whitespace
- @text.sub!(/\s+$/, '')
-
- # and look for leading whitespace
- if @text.length > 0
- @text =~ /^(\s*)/
- @leading_spaces = $1.length
- else
- @leading_spaces = INFINITY
- end
- end
-
- # Return true if this line is blank
- def blank?
- @text.empty?
- end
-
- # stamp a line with a type, a level, a prefix, and a flag
- def stamp(type, level, param="", flag=nil)
- @type, @level, @param, @flag = type, level, param, flag
- end
-
- ##
- # Strip off the leading margin
-
- def strip_leading(size)
- if @text.size > size
- @text[0,size] = ""
- else
- @text = ""
- end
- end
-
- def to_s
- "#@type#@level: #@text"
- end
- end
-
- ##
- # A container for all the lines.
-
- class Lines
-
- include Enumerable
-
- attr_reader :lines # :nodoc:
-
- def initialize(lines)
- @lines = lines
- rewind
- end
-
- def empty?
- @lines.size.zero?
- end
-
- def each
- @lines.each do |line|
- yield line unless line.deleted
- end
- end
-
-# def [](index)
-# @lines[index]
-# end
-
- def rewind
- @nextline = 0
- end
-
- def next
- begin
- res = @lines[@nextline]
- @nextline += 1 if @nextline < @lines.size
- end while res and res.deleted and @nextline < @lines.size
- res
- end
-
- def unget
- @nextline -= 1
- end
-
- def delete(a_line)
- a_line.deleted = true
- end
-
- def normalize
- margin = @lines.collect{|l| l.leading_spaces}.min
- margin = 0 if margin == :INFINITY
- @lines.each {|line| line.strip_leading(margin) } if margin > 0
- end
-
- def as_text
- @lines.map {|l| l.text}.join("\n")
- end
-
- def line_types
- @lines.map {|l| l.type }
- end
-
- end
-
-end
-
diff --git a/lib/rdoc/markup/preprocess.rb b/lib/rdoc/markup/preprocess.rb
deleted file mode 100644
index 00dd4be4ad..0000000000
--- a/lib/rdoc/markup/preprocess.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-require 'rdoc/markup'
-
-##
-# Handle common directives that can occur in a block of text:
-#
-# : include : filename
-
-class RDoc::Markup::PreProcess
-
- def initialize(input_file_name, include_path)
- @input_file_name = input_file_name
- @include_path = include_path
- end
-
- ##
- # Look for common options in a chunk of text. Options that we don't handle
- # are yielded to the caller.
-
- def handle(text)
- text.gsub!(/^([ \t]*#?[ \t]*):(\w+):([ \t]*)(.+)?\n/) do
- next $& if $3.empty? and $4 and $4[0, 1] == ':'
-
- prefix = $1
- directive = $2.downcase
- param = $4
-
- case directive
- when 'include' then
- filename = param.split[0]
- include_file filename, prefix
-
- else
- result = yield directive, param
- result = "#{prefix}:#{directive}: #{param}\n" unless result
- result
- end
- end
- end
-
- private
-
- ##
- # Include a file, indenting it correctly.
-
- def include_file(name, indent)
- if full_name = find_include_file(name) then
- content = File.open(full_name) {|f| f.read}
- # strip leading '#'s, but only if all lines start with them
- if content =~ /^[^#]/
- content.gsub(/^/, indent)
- else
- content.gsub(/^#?/, indent)
- end
- else
- $stderr.puts "Couldn't find file to include: '#{name}'"
- ''
- end
- end
-
- ##
- # Look for the given file in the directory containing the current file,
- # and then in each of the directories specified in the RDOC_INCLUDE path
-
- def find_include_file(name)
- to_search = [ File.dirname(@input_file_name) ].concat @include_path
- to_search.each do |dir|
- full_name = File.join(dir, name)
- stat = File.stat(full_name) rescue next
- return full_name if stat.readable?
- end
- nil
- end
-
-end
-
diff --git a/lib/rdoc/markup/to_flow.rb b/lib/rdoc/markup/to_flow.rb
deleted file mode 100644
index 3d87b3e9c3..0000000000
--- a/lib/rdoc/markup/to_flow.rb
+++ /dev/null
@@ -1,185 +0,0 @@
-require 'rdoc/markup/formatter'
-require 'rdoc/markup/fragments'
-require 'rdoc/markup/inline'
-require 'cgi'
-
-class RDoc::Markup
-
- module Flow
- P = Struct.new(:body)
- VERB = Struct.new(:body)
- RULE = Struct.new(:width)
- class LIST
- attr_reader :type, :contents
- def initialize(type)
- @type = type
- @contents = []
- end
- def <<(stuff)
- @contents << stuff
- end
- end
- LI = Struct.new(:label, :body)
- H = Struct.new(:level, :text)
- end
-
- class ToFlow < RDoc::Markup::Formatter
- LIST_TYPE_TO_HTML = {
- :BULLET => [ "<ul>", "</ul>" ],
- :NUMBER => [ "<ol>", "</ol>" ],
- :UPPERALPHA => [ "<ol>", "</ol>" ],
- :LOWERALPHA => [ "<ol>", "</ol>" ],
- :LABELED => [ "<dl>", "</dl>" ],
- :NOTE => [ "<table>", "</table>" ],
- }
-
- InlineTag = Struct.new(:bit, :on, :off)
-
- def initialize
- super
-
- init_tags
- end
-
- ##
- # Set up the standard mapping of attributes to HTML tags
-
- def init_tags
- @attr_tags = [
- InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:BOLD), "<b>", "</b>"),
- InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:TT), "<tt>", "</tt>"),
- InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:EM), "<em>", "</em>"),
- ]
- end
-
- ##
- # Add a new set of HTML tags for an attribute. We allow separate start and
- # end tags for flexibility
-
- def add_tag(name, start, stop)
- @attr_tags << InlineTag.new(RDoc::Markup::Attribute.bitmap_for(name), start, stop)
- end
-
- ##
- # Given an HTML tag, decorate it with class information and the like if
- # required. This is a no-op in the base class, but is overridden in HTML
- # output classes that implement style sheets
-
- def annotate(tag)
- tag
- end
-
- ##
- # Here's the client side of the visitor pattern
-
- def start_accepting
- @res = []
- @list_stack = []
- end
-
- def end_accepting
- @res
- end
-
- def accept_paragraph(am, fragment)
- @res << Flow::P.new((convert_flow(am.flow(fragment.txt))))
- end
-
- def accept_verbatim(am, fragment)
- @res << Flow::VERB.new((convert_flow(am.flow(fragment.txt))))
- end
-
- def accept_rule(am, fragment)
- size = fragment.param
- size = 10 if size > 10
- @res << Flow::RULE.new(size)
- end
-
- def accept_list_start(am, fragment)
- @list_stack.push(@res)
- list = Flow::LIST.new(fragment.type)
- @res << list
- @res = list
- end
-
- def accept_list_end(am, fragment)
- @res = @list_stack.pop
- end
-
- def accept_list_item(am, fragment)
- @res << Flow::LI.new(fragment.param, convert_flow(am.flow(fragment.txt)))
- end
-
- def accept_blank_line(am, fragment)
- # @res << annotate("<p />") << "\n"
- end
-
- def accept_heading(am, fragment)
- @res << Flow::H.new(fragment.head_level, convert_flow(am.flow(fragment.txt)))
- end
-
- private
-
- def on_tags(res, item)
- attr_mask = item.turn_on
- return if attr_mask.zero?
-
- @attr_tags.each do |tag|
- if attr_mask & tag.bit != 0
- res << annotate(tag.on)
- end
- end
- end
-
- def off_tags(res, item)
- attr_mask = item.turn_off
- return if attr_mask.zero?
-
- @attr_tags.reverse_each do |tag|
- if attr_mask & tag.bit != 0
- res << annotate(tag.off)
- end
- end
- end
-
- def convert_flow(flow)
- res = ""
- flow.each do |item|
- case item
- when String
- res << convert_string(item)
- when AttrChanger
- off_tags(res, item)
- on_tags(res, item)
- when Special
- res << convert_special(item)
- else
- raise "Unknown flow element: #{item.inspect}"
- end
- end
- res
- end
-
- def convert_string(item)
- CGI.escapeHTML(item)
- end
-
- def convert_special(special)
- handled = false
- Attribute.each_name_of(special.type) do |name|
- method_name = "handle_special_#{name}"
- if self.respond_to? method_name
- special.text = send(method_name, special)
- handled = true
- end
- end
-
- raise "Unhandled special: #{special}" unless handled
-
- special.text
- end
-
- end
-
-end
-
diff --git a/lib/rdoc/markup/to_html.rb b/lib/rdoc/markup/to_html.rb
deleted file mode 100644
index dce7a69b12..0000000000
--- a/lib/rdoc/markup/to_html.rb
+++ /dev/null
@@ -1,403 +0,0 @@
-require 'rdoc/markup/formatter'
-require 'rdoc/markup/fragments'
-require 'rdoc/markup/inline'
-
-require 'cgi'
-
-class RDoc::Markup::ToHtml < RDoc::Markup::Formatter
-
- LIST_TYPE_TO_HTML = {
- :BULLET => %w[<ul> </ul>],
- :NUMBER => %w[<ol> </ol>],
- :UPPERALPHA => %w[<ol> </ol>],
- :LOWERALPHA => %w[<ol> </ol>],
- :LABELED => %w[<dl> </dl>],
- :NOTE => %w[<table> </table>],
- }
-
- InlineTag = Struct.new(:bit, :on, :off)
-
- def initialize
- super
-
- # @in_tt - tt nested levels count
- # @tt_bit - cache
- @in_tt = 0
- @tt_bit = RDoc::Markup::Attribute.bitmap_for :TT
-
- # external hyperlinks
- @markup.add_special(/((link:|https?:|mailto:|ftp:|www\.)\S+\w)/, :HYPERLINK)
-
- # and links of the form <text>[<url>]
- @markup.add_special(/(((\{.*?\})|\b\S+?)\[\S+?\.\S+?\])/, :TIDYLINK)
-
- init_tags
- end
-
- ##
- # Converts a target url to one that is relative to a given path
-
- def self.gen_relative_url(path, target)
- from = File.dirname path
- to, to_file = File.split target
-
- from = from.split "/"
- to = to.split "/"
-
- while from.size > 0 and to.size > 0 and from[0] == to[0] do
- from.shift
- to.shift
- end
-
- from.fill ".."
- from.concat to
- from << to_file
- File.join(*from)
- end
-
- ##
- # Generate a hyperlink for url, labeled with text. Handle the
- # special cases for img: and link: described under handle_special_HYPERLINK
-
- def gen_url(url, text)
- if url =~ /([A-Za-z]+):(.*)/ then
- type = $1
- path = $2
- else
- type = "http"
- path = url
- url = "http://#{url}"
- end
-
- if type == "link" then
- url = if path[0, 1] == '#' then # is this meaningful?
- path
- else
- self.class.gen_relative_url @from_path, path
- end
- end
-
- if (type == "http" or type == "link") and
- url =~ /\.(gif|png|jpg|jpeg|bmp)$/ then
- "<img src=\"#{url}\" />"
- else
- "<a href=\"#{url}\">#{text.sub(%r{^#{type}:/*}, '')}</a>"
- end
- end
-
- ##
- # And we're invoked with a potential external hyperlink mailto:
- # just gets inserted. http: links are checked to see if they
- # reference an image. If so, that image gets inserted using an
- # <img> tag. Otherwise a conventional <a href> is used. We also
- # support a special type of hyperlink, link:, which is a reference
- # to a local file whose path is relative to the --op directory.
-
- def handle_special_HYPERLINK(special)
- url = special.text
- gen_url url, url
- end
-
- ##
- # Here's a hypedlink where the label is different to the URL
- # <label>[url] or {long label}[url]
-
- def handle_special_TIDYLINK(special)
- text = special.text
-
- return text unless text =~ /\{(.*?)\}\[(.*?)\]/ or text =~ /(\S+)\[(.*?)\]/
-
- label = $1
- url = $2
- gen_url url, label
- end
-
- ##
- # are we currently inside <tt> tags?
-
- def in_tt?
- @in_tt > 0
- end
-
- ##
- # is +tag+ a <tt> tag?
-
- def tt?(tag)
- tag.bit == @tt_bit
- end
-
- ##
- # Set up the standard mapping of attributes to HTML tags
-
- def init_tags
- @attr_tags = [
- InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:BOLD), "<b>", "</b>"),
- InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:TT), "<tt>", "</tt>"),
- InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:EM), "<em>", "</em>"),
- ]
- end
-
- ##
- # Add a new set of HTML tags for an attribute. We allow separate start and
- # end tags for flexibility.
-
- def add_tag(name, start, stop)
- @attr_tags << InlineTag.new(RDoc::Markup::Attribute.bitmap_for(name), start, stop)
- end
-
- ##
- # Given an HTML tag, decorate it with class information and the like if
- # required. This is a no-op in the base class, but is overridden in HTML
- # output classes that implement style sheets.
-
- def annotate(tag)
- tag
- end
-
- ##
- # Here's the client side of the visitor pattern
-
- def start_accepting
- @res = ""
- @in_list_entry = []
- end
-
- def end_accepting
- @res
- end
-
- def accept_paragraph(am, fragment)
- @res << annotate("<p>") + "\n"
- @res << wrap(convert_flow(am.flow(fragment.txt)))
- @res << annotate("</p>") + "\n"
- end
-
- def accept_verbatim(am, fragment)
- @res << annotate("<pre>") + "\n"
- @res << CGI.escapeHTML(fragment.txt)
- @res << annotate("</pre>") << "\n"
- end
-
- def accept_rule(am, fragment)
- size = fragment.param
- size = 10 if size > 10
- @res << "<hr size=\"#{size}\"></hr>"
- end
-
- def accept_list_start(am, fragment)
- @res << html_list_name(fragment.type, true) << "\n"
- @in_list_entry.push false
- end
-
- def accept_list_end(am, fragment)
- if tag = @in_list_entry.pop
- @res << annotate(tag) << "\n"
- end
- @res << html_list_name(fragment.type, false) << "\n"
- end
-
- def accept_list_item(am, fragment)
- if tag = @in_list_entry.last
- @res << annotate(tag) << "\n"
- end
-
- @res << list_item_start(am, fragment)
-
- @res << wrap(convert_flow(am.flow(fragment.txt))) << "\n"
-
- @in_list_entry[-1] = list_end_for(fragment.type)
- end
-
- def accept_blank_line(am, fragment)
- # @res << annotate("<p />") << "\n"
- end
-
- def accept_heading(am, fragment)
- @res << convert_heading(fragment.head_level, am.flow(fragment.txt))
- end
-
- ##
- # This is a higher speed (if messier) version of wrap
-
- def wrap(txt, line_len = 76)
- res = ""
- sp = 0
- ep = txt.length
- while sp < ep
- # scan back for a space
- p = sp + line_len - 1
- if p >= ep
- p = ep
- else
- while p > sp and txt[p] != ?\s
- p -= 1
- end
- if p <= sp
- p = sp + line_len
- while p < ep and txt[p] != ?\s
- p += 1
- end
- end
- end
- res << txt[sp...p] << "\n"
- sp = p
- sp += 1 while sp < ep and txt[sp] == ?\s
- end
- res
- end
-
- private
-
- def on_tags(res, item)
- attr_mask = item.turn_on
- return if attr_mask.zero?
-
- @attr_tags.each do |tag|
- if attr_mask & tag.bit != 0
- res << annotate(tag.on)
- @in_tt += 1 if tt?(tag)
- end
- end
- end
-
- def off_tags(res, item)
- attr_mask = item.turn_off
- return if attr_mask.zero?
-
- @attr_tags.reverse_each do |tag|
- if attr_mask & tag.bit != 0
- @in_tt -= 1 if tt?(tag)
- res << annotate(tag.off)
- end
- end
- end
-
- def convert_flow(flow)
- res = ""
-
- flow.each do |item|
- case item
- when String
- res << convert_string(item)
- when RDoc::Markup::AttrChanger
- off_tags(res, item)
- on_tags(res, item)
- when RDoc::Markup::Special
- res << convert_special(item)
- else
- raise "Unknown flow element: #{item.inspect}"
- end
- end
-
- res
- end
-
- def convert_string(item)
- in_tt? ? convert_string_simple(item) : convert_string_fancy(item)
- end
-
- def convert_string_simple(item)
- CGI.escapeHTML item
- end
-
- ##
- # some of these patterns are taken from SmartyPants...
-
- def convert_string_fancy(item)
- # convert ampersand before doing anything else
- item.gsub(/&/, '&amp;').
-
- # convert -- to em-dash, (-- to en-dash)
- gsub(/---?/, '&#8212;'). #gsub(/--/, '&#8211;').
-
- # convert ... to elipsis (and make sure .... becomes .<elipsis>)
- gsub(/\.\.\.\./, '.&#8230;').gsub(/\.\.\./, '&#8230;').
-
- # convert single closing quote
- gsub(%r{([^ \t\r\n\[\{\(])\'}, '\1&#8217;'). # }
- gsub(%r{\'(?=\W|s\b)}, '&#8217;').
-
- # convert single opening quote
- gsub(/'/, '&#8216;').
-
- # convert double closing quote
- gsub(%r{([^ \t\r\n\[\{\(])\"(?=\W)}, '\1&#8221;'). # }
-
- # convert double opening quote
- gsub(/"/, '&#8220;').
-
- # convert copyright
- gsub(/\(c\)/, '&#169;').
-
- # convert registered trademark
- gsub(/\(r\)/, '&#174;')
- end
-
- def convert_special(special)
- handled = false
- RDoc::Markup::Attribute.each_name_of(special.type) do |name|
- method_name = "handle_special_#{name}"
- if self.respond_to? method_name
- special.text = send(method_name, special)
- handled = true
- end
- end
- raise "Unhandled special: #{special}" unless handled
- special.text
- end
-
- def convert_heading(level, flow)
- res =
- annotate("<h#{level}>") +
- convert_flow(flow) +
- annotate("</h#{level}>\n")
- end
-
- def html_list_name(list_type, is_open_tag)
- tags = LIST_TYPE_TO_HTML[list_type] || raise("Invalid list type: #{list_type.inspect}")
- annotate(tags[ is_open_tag ? 0 : 1])
- end
-
- def list_item_start(am, fragment)
- case fragment.type
- when :BULLET, :NUMBER then
- annotate("<li>")
-
- when :UPPERALPHA then
- annotate("<li type=\"A\">")
-
- when :LOWERALPHA then
- annotate("<li type=\"a\">")
-
- when :LABELED then
- annotate("<dt>") +
- convert_flow(am.flow(fragment.param)) +
- annotate("</dt>") +
- annotate("<dd>")
-
- when :NOTE then
- annotate("<tr>") +
- annotate("<td valign=\"top\">") +
- convert_flow(am.flow(fragment.param)) +
- annotate("</td>") +
- annotate("<td>")
- else
- raise "Invalid list type"
- end
- end
-
- def list_end_for(fragment_type)
- case fragment_type
- when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA then
- "</li>"
- when :LABELED then
- "</dd>"
- when :NOTE then
- "</td></tr>"
- else
- raise "Invalid list type"
- end
- end
-
-end
-
diff --git a/lib/rdoc/markup/to_html_crossref.rb b/lib/rdoc/markup/to_html_crossref.rb
deleted file mode 100644
index dc64b30da1..0000000000
--- a/lib/rdoc/markup/to_html_crossref.rb
+++ /dev/null
@@ -1,148 +0,0 @@
-require 'rdoc/markup/to_html'
-
-##
-# Subclass of the RDoc::Markup::ToHtml class that supports looking up words in
-# the AllReferences list. Those that are found (like AllReferences in this
-# comment) will be hyperlinked
-
-class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml
-
- attr_accessor :context
-
- # Regular expressions to match class and method references.
- #
- # 1.) There can be a '\' in front of text to suppress
- # any cross-references (note, however, that the single '\'
- # is written as '\\\\' in order to escape it twice, once
- # in the Ruby String literal and once in the regexp).
- # 2.) There can be a '::' in front of class names to reference
- # from the top-level namespace.
- # 3.) The method can be followed by parenthesis,
- # which may or may not have things inside (this
- # apparently is allowed for Fortran 95, but I also think that this
- # is a good idea for Ruby, as it is very reasonable to want to
- # reference a call with arguments).
- #
- # NOTE: In order to support Fortran 95 properly, the [A-Z] below
- # should be changed to [A-Za-z]. This slows down rdoc significantly,
- # however, and the Fortran 95 support is broken in any case due to
- # the return in handle_special_CROSSREF if the token consists
- # entirely of lowercase letters.
- #
- # The markup/cross-referencing engine needs a rewrite for
- # Fortran 95 to be supported properly.
- CLASS_REGEXP_STR = '\\\\?((?:\:{2})?[A-Z]\w*(?:\:\:\w+)*)'
- METHOD_REGEXP_STR = '(\w+[!?=]?)(?:\([\.\w+\*\/\+\-\=\<\>]*\))?'
-
- # Regular expressions matching text that should potentially have
- # cross-reference links generated are passed to add_special.
- # Note that these expressions are meant to pick up text for which
- # cross-references have been suppressed, since the suppression
- # characters are removed by the code that is triggered.
- CROSSREF_REGEXP = /(
- # A::B::C.meth
- #{CLASS_REGEXP_STR}[\.\#]#{METHOD_REGEXP_STR}
-
- # Stand-alone method (proceeded by a #)
- | \\?\##{METHOD_REGEXP_STR}
-
- # A::B::C
- # The stuff after CLASS_REGEXP_STR is a
- # nasty hack. CLASS_REGEXP_STR unfortunately matches
- # words like dog and cat (these are legal "class"
- # names in Fortran 95). When a word is flagged as a
- # potential cross-reference, limitations in the markup
- # engine suppress other processing, such as typesetting.
- # This is particularly noticeable for contractions.
- # In order that words like "can't" not
- # be flagged as potential cross-references, only
- # flag potential class cross-references if the character
- # after the cross-referece is a space or sentence
- # punctuation.
- | #{CLASS_REGEXP_STR}(?=[\s\)\.\?\!\,\;]|\z)
-
- # Things that look like filenames
- # The key thing is that there must be at least
- # one special character (period, slash, or
- # underscore).
- | [\/\w]+[_\/\.][\w\/\.]+
-
- # Things that have markup suppressed
- | \\[^\s]
- )/x
-
- ##
- # We need to record the html path of our caller so we can generate
- # correct relative paths for any hyperlinks that we find
-
- def initialize(from_path, context, show_hash)
- raise ArgumentError, 'from_path cannot be nil' if from_path.nil?
- super()
-
- @markup.add_special(CROSSREF_REGEXP, :CROSSREF)
-
- @from_path = from_path
- @context = context
- @show_hash = show_hash
-
- @seen = {}
- end
-
- ##
- # We're invoked when any text matches the CROSSREF pattern
- # (defined in MarkUp). If we fine the corresponding reference,
- # generate a hyperlink. If the name we're looking for contains
- # no punctuation, we look for it up the module/class chain. For
- # example, HyperlinkHtml is found, even without the Generator::
- # prefix, because we look for it in module Generator first.
-
- def handle_special_CROSSREF(special)
- name = special.text
-
- # This ensures that words entirely consisting of lowercase letters will
- # not have cross-references generated (to suppress lots of
- # erroneous cross-references to "new" in text, for instance)
- return name if name =~ /\A[a-z]*\z/
-
- return @seen[name] if @seen.include? name
-
- if name[0, 1] == '#' then
- lookup = name[1..-1]
- name = lookup unless @show_hash
- else
- lookup = name
- end
-
-
- # Find class, module, or method in class or module.
- #
- # Do not, however, use an if/elsif/else chain to do so. Instead, test
- # each possible pattern until one matches. The reason for this is that a
- # string like "YAML.txt" could be the txt() class method of class YAML (in
- # which case it would match the first pattern, which splits the string
- # into container and method components and looks up both) or a filename
- # (in which case it would match the last pattern, which just checks
- # whether the string as a whole is a known symbol).
-
- if /#{CLASS_REGEXP_STR}[\.\#]#{METHOD_REGEXP_STR}/ =~ lookup then
- container = $1
- method = $2
- ref = @context.find_symbol container, method
- end
-
- ref = @context.find_symbol lookup unless ref
-
- out = if lookup =~ /^\\/ then
- $'
- elsif ref and ref.document_self then
- "<a href=\"#{ref.as_href(@from_path)}\">#{name}</a>"
- else
- name
- end
-
- @seen[name] = out
-
- out
- end
-
-end
diff --git a/lib/rdoc/markup/to_latex.rb b/lib/rdoc/markup/to_latex.rb
deleted file mode 100644
index bbf958f2ed..0000000000
--- a/lib/rdoc/markup/to_latex.rb
+++ /dev/null
@@ -1,328 +0,0 @@
-require 'rdoc/markup/formatter'
-require 'rdoc/markup/fragments'
-require 'rdoc/markup/inline'
-
-require 'cgi'
-
-##
-# Convert SimpleMarkup to basic LaTeX report format.
-
-class RDoc::Markup::ToLaTeX < RDoc::Markup::Formatter
-
- BS = "\020" # \
- OB = "\021" # {
- CB = "\022" # }
- DL = "\023" # Dollar
-
- BACKSLASH = "#{BS}symbol#{OB}92#{CB}"
- HAT = "#{BS}symbol#{OB}94#{CB}"
- BACKQUOTE = "#{BS}symbol#{OB}0#{CB}"
- TILDE = "#{DL}#{BS}sim#{DL}"
- LESSTHAN = "#{DL}<#{DL}"
- GREATERTHAN = "#{DL}>#{DL}"
-
- def self.l(str)
- str.tr('\\', BS).tr('{', OB).tr('}', CB).tr('$', DL)
- end
-
- def l(arg)
- RDoc::Markup::ToLaTeX.l(arg)
- end
-
- LIST_TYPE_TO_LATEX = {
- :BULLET => [ l("\\begin{itemize}"), l("\\end{itemize}") ],
- :NUMBER => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\arabic" ],
- :UPPERALPHA => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\Alph" ],
- :LOWERALPHA => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\alph" ],
- :LABELED => [ l("\\begin{description}"), l("\\end{description}") ],
- :NOTE => [
- l("\\begin{tabularx}{\\linewidth}{@{} l X @{}}"),
- l("\\end{tabularx}") ],
- }
-
- InlineTag = Struct.new(:bit, :on, :off)
-
- def initialize
- init_tags
- @list_depth = 0
- @prev_list_types = []
- end
-
- ##
- # Set up the standard mapping of attributes to LaTeX
-
- def init_tags
- @attr_tags = [
- InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:BOLD), l("\\textbf{"), l("}")),
- InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:TT), l("\\texttt{"), l("}")),
- InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:EM), l("\\emph{"), l("}")),
- ]
- end
-
- ##
- # Escape a LaTeX string
-
- def escape(str)
- $stderr.print "FE: ", str if $DEBUG_RDOC
- s = str.
- sub(/\s+$/, '').
- gsub(/([_\${}&%#])/, "#{BS}\\1").
- gsub(/\\/, BACKSLASH).
- gsub(/\^/, HAT).
- gsub(/~/, TILDE).
- gsub(/</, LESSTHAN).
- gsub(/>/, GREATERTHAN).
- gsub(/,,/, ",{},").
- gsub(/\`/, BACKQUOTE)
- $stderr.print "-> ", s, "\n" if $DEBUG_RDOC
- s
- end
-
- ##
- # Add a new set of LaTeX tags for an attribute. We allow
- # separate start and end tags for flexibility
-
- def add_tag(name, start, stop)
- @attr_tags << InlineTag.new(RDoc::Markup::Attribute.bitmap_for(name), start, stop)
- end
-
- ##
- # Here's the client side of the visitor pattern
-
- def start_accepting
- @res = ""
- @in_list_entry = []
- end
-
- def end_accepting
- @res.tr(BS, '\\').tr(OB, '{').tr(CB, '}').tr(DL, '$')
- end
-
- def accept_paragraph(am, fragment)
- @res << wrap(convert_flow(am.flow(fragment.txt)))
- @res << "\n"
- end
-
- def accept_verbatim(am, fragment)
- @res << "\n\\begin{code}\n"
- @res << fragment.txt.sub(/[\n\s]+\Z/, '')
- @res << "\n\\end{code}\n\n"
- end
-
- def accept_rule(am, fragment)
- size = fragment.param
- size = 10 if size > 10
- @res << "\n\n\\rule{\\linewidth}{#{size}pt}\n\n"
- end
-
- def accept_list_start(am, fragment)
- @res << list_name(fragment.type, true) << "\n"
- @in_list_entry.push false
- end
-
- def accept_list_end(am, fragment)
- if tag = @in_list_entry.pop
- @res << tag << "\n"
- end
- @res << list_name(fragment.type, false) << "\n"
- end
-
- def accept_list_item(am, fragment)
- if tag = @in_list_entry.last
- @res << tag << "\n"
- end
- @res << list_item_start(am, fragment)
- @res << wrap(convert_flow(am.flow(fragment.txt))) << "\n"
- @in_list_entry[-1] = list_end_for(fragment.type)
- end
-
- def accept_blank_line(am, fragment)
- # @res << "\n"
- end
-
- def accept_heading(am, fragment)
- @res << convert_heading(fragment.head_level, am.flow(fragment.txt))
- end
-
- ##
- # This is a higher speed (if messier) version of wrap
-
- def wrap(txt, line_len = 76)
- res = ""
- sp = 0
- ep = txt.length
- while sp < ep
- # scan back for a space
- p = sp + line_len - 1
- if p >= ep
- p = ep
- else
- while p > sp and txt[p] != ?\s
- p -= 1
- end
- if p <= sp
- p = sp + line_len
- while p < ep and txt[p] != ?\s
- p += 1
- end
- end
- end
- res << txt[sp...p] << "\n"
- sp = p
- sp += 1 while sp < ep and txt[sp] == ?\s
- end
- res
- end
-
- private
-
- def on_tags(res, item)
- attr_mask = item.turn_on
- return if attr_mask.zero?
-
- @attr_tags.each do |tag|
- if attr_mask & tag.bit != 0
- res << tag.on
- end
- end
- end
-
- def off_tags(res, item)
- attr_mask = item.turn_off
- return if attr_mask.zero?
-
- @attr_tags.reverse_each do |tag|
- if attr_mask & tag.bit != 0
- res << tag.off
- end
- end
- end
-
- def convert_flow(flow)
- res = ""
- flow.each do |item|
- case item
- when String
- $stderr.puts "Converting '#{item}'" if $DEBUG_RDOC
- res << convert_string(item)
- when AttrChanger
- off_tags(res, item)
- on_tags(res, item)
- when Special
- res << convert_special(item)
- else
- raise "Unknown flow element: #{item.inspect}"
- end
- end
- res
- end
-
- ##
- # some of these patterns are taken from SmartyPants...
-
- def convert_string(item)
- escape(item).
-
- # convert ... to elipsis (and make sure .... becomes .<elipsis>)
- gsub(/\.\.\.\./, '.\ldots{}').gsub(/\.\.\./, '\ldots{}').
-
- # convert single closing quote
- gsub(%r{([^ \t\r\n\[\{\(])\'}, '\1\'').
- gsub(%r{\'(?=\W|s\b)}, "'" ).
-
- # convert single opening quote
- gsub(/'/, '`').
-
- # convert double closing quote
- gsub(%r{([^ \t\r\n\[\{\(])\"(?=\W)}, "\\1''").
-
- # convert double opening quote
- gsub(/"/, "``").
-
- # convert copyright
- gsub(/\(c\)/, '\copyright{}')
-
- end
-
- def convert_special(special)
- handled = false
- Attribute.each_name_of(special.type) do |name|
- method_name = "handle_special_#{name}"
- if self.respond_to? method_name
- special.text = send(method_name, special)
- handled = true
- end
- end
- raise "Unhandled special: #{special}" unless handled
- special.text
- end
-
- def convert_heading(level, flow)
- res =
- case level
- when 1 then "\\chapter{"
- when 2 then "\\section{"
- when 3 then "\\subsection{"
- when 4 then "\\subsubsection{"
- else "\\paragraph{"
- end +
- convert_flow(flow) +
- "}\n"
- end
-
- def list_name(list_type, is_open_tag)
- tags = LIST_TYPE_TO_LATEX[list_type] || raise("Invalid list type: #{list_type.inspect}")
- if tags[2] # enumerate
- if is_open_tag
- @list_depth += 1
- if @prev_list_types[@list_depth] != tags[2]
- case @list_depth
- when 1
- roman = "i"
- when 2
- roman = "ii"
- when 3
- roman = "iii"
- when 4
- roman = "iv"
- else
- raise("Too deep list: level #{@list_depth}")
- end
- @prev_list_types[@list_depth] = tags[2]
- return l("\\renewcommand{\\labelenum#{roman}}{#{tags[2]}{enum#{roman}}}") + "\n" + tags[0]
- end
- else
- @list_depth -= 1
- end
- end
- tags[ is_open_tag ? 0 : 1]
- end
-
- def list_item_start(am, fragment)
- case fragment.type
- when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA then
- "\\item "
-
- when :LABELED then
- "\\item[" + convert_flow(am.flow(fragment.param)) + "] "
-
- when :NOTE then
- convert_flow(am.flow(fragment.param)) + " & "
- else
- raise "Invalid list type"
- end
- end
-
- def list_end_for(fragment_type)
- case fragment_type
- when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA, :LABELED then
- ""
- when :NOTE
- "\\\\\n"
- else
- raise "Invalid list type"
- end
- end
-
-end
-
diff --git a/lib/rdoc/markup/to_test.rb b/lib/rdoc/markup/to_test.rb
deleted file mode 100644
index ce6aff6e9a..0000000000
--- a/lib/rdoc/markup/to_test.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'rdoc/markup'
-require 'rdoc/markup/formatter'
-
-##
-# This Markup outputter is used for testing purposes.
-
-class RDoc::Markup::ToTest < RDoc::Markup::Formatter
-
- def start_accepting
- @res = []
- end
-
- def end_accepting
- @res
- end
-
- def accept_paragraph(am, fragment)
- @res << fragment.to_s
- end
-
- def accept_verbatim(am, fragment)
- @res << fragment.to_s
- end
-
- def accept_list_start(am, fragment)
- @res << fragment.to_s
- end
-
- def accept_list_end(am, fragment)
- @res << fragment.to_s
- end
-
- def accept_list_item(am, fragment)
- @res << fragment.to_s
- end
-
- def accept_blank_line(am, fragment)
- @res << fragment.to_s
- end
-
- def accept_heading(am, fragment)
- @res << fragment.to_s
- end
-
- def accept_rule(am, fragment)
- @res << fragment.to_s
- end
-
-end
-
diff --git a/lib/rdoc/markup/to_texinfo.rb b/lib/rdoc/markup/to_texinfo.rb
deleted file mode 100644
index 65a1608c4d..0000000000
--- a/lib/rdoc/markup/to_texinfo.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'rdoc/markup/formatter'
-require 'rdoc/markup/fragments'
-require 'rdoc/markup/inline'
-
-require 'rdoc/markup'
-require 'rdoc/markup/formatter'
-
-##
-# Convert SimpleMarkup to basic TexInfo format
-#
-# TODO: WTF is AttributeManager for?
-#
-class RDoc::Markup::ToTexInfo < RDoc::Markup::Formatter
-
- def start_accepting
- @text = []
- end
-
- def end_accepting
- @text.join("\n")
- end
-
- def accept_paragraph(attributes, text)
- @text << format(text)
- end
-
- def accept_verbatim(attributes, text)
- @text << "@verb{|#{format(text)}|}"
- end
-
- def accept_heading(attributes, text)
- heading = ['@majorheading', '@chapheading'][text.head_level - 1] || '@heading'
- @text << "#{heading} #{format(text)}"
- end
-
- def accept_list_start(attributes, text)
- @text << '@itemize @bullet'
- end
-
- def accept_list_end(attributes, text)
- @text << '@end itemize'
- end
-
- def accept_list_item(attributes, text)
- @text << "@item\n#{format(text)}"
- end
-
- def accept_blank_line(attributes, text)
- @text << "\n"
- end
-
- def accept_rule(attributes, text)
- @text << '-----'
- end
-
- def format(text)
- text.txt.
- gsub(/@/, "@@").
- gsub(/\{/, "@{").
- gsub(/\}/, "@}").
- # gsub(/,/, "@,"). # technically only required in cross-refs
- gsub(/\+([\w]+)\+/, "@code{\\1}").
- gsub(/\<tt\>([^<]+)\<\/tt\>/, "@code{\\1}").
- gsub(/\*([\w]+)\*/, "@strong{\\1}").
- gsub(/\<b\>([^<]+)\<\/b\>/, "@strong{\\1}").
- gsub(/_([\w]+)_/, "@emph{\\1}").
- gsub(/\<em\>([^<]+)\<\/em\>/, "@emph{\\1}")
- end
-end
diff --git a/lib/rdoc/options.rb b/lib/rdoc/options.rb
deleted file mode 100644
index 1d92bd4748..0000000000
--- a/lib/rdoc/options.rb
+++ /dev/null
@@ -1,638 +0,0 @@
-# We handle the parsing of options, and subsequently as a singleton
-# object to be queried for option values
-
-require "rdoc/ri/paths"
-require 'optparse'
-
-class RDoc::Options
-
- ##
- # Should the output be placed into a single file
-
- attr_reader :all_one_file
-
- ##
- # Character-set
-
- attr_reader :charset
-
- ##
- # URL of stylesheet
-
- attr_reader :css
-
- ##
- # Should diagrams be drawn
-
- attr_reader :diagram
-
- ##
- # Files matching this pattern will be excluded
-
- attr_accessor :exclude
-
- ##
- # Additional attr_... style method flags
-
- attr_reader :extra_accessor_flags
-
- ##
- # Pattern for additional attr_... style methods
-
- attr_accessor :extra_accessors
-
- ##
- # Should we draw fileboxes in diagrams
-
- attr_reader :fileboxes
-
- ##
- # The list of files to be processed
-
- attr_accessor :files
-
- ##
- # Scan newer sources than the flag file if true.
-
- attr_reader :force_update
-
- ##
- # Description of the output generator (set with the <tt>-fmt</tt> option)
-
- attr_accessor :generator
-
- ##
- # Formatter to mark up text with
-
- attr_accessor :formatter
-
- ##
- # image format for diagrams
-
- attr_reader :image_format
-
- ##
- # Include line numbers in the source listings
-
- attr_reader :include_line_numbers
-
- ##
- # Should source code be included inline, or displayed in a popup
-
- attr_accessor :inline_source
-
- ##
- # Name of the file, class or module to display in the initial index page (if
- # not specified the first file we encounter is used)
-
- attr_accessor :main_page
-
- ##
- # Merge into classes of the same name when generating ri
-
- attr_reader :merge
-
- ##
- # The name of the output directory
-
- attr_accessor :op_dir
-
- ##
- # The name to use for the output
-
- attr_accessor :op_name
-
- ##
- # Are we promiscuous about showing module contents across multiple files
-
- attr_reader :promiscuous
-
- ##
- # Array of directories to search for files to satisfy an :include:
-
- attr_reader :rdoc_include
-
- ##
- # Include private and protected methods in the output
-
- attr_accessor :show_all
-
- ##
- # Include the '#' at the front of hyperlinked instance method names
-
- attr_reader :show_hash
-
- ##
- # The number of columns in a tab
-
- attr_reader :tab_width
-
- ##
- # template to be used when generating output
-
- attr_reader :template
-
- ##
- # Template class for file generation
- #--
- # HACK around dependencies in lib/rdoc/generator/html.rb
-
- attr_accessor :template_class # :nodoc:
-
- ##
- # Documentation title
-
- attr_reader :title
-
- ##
- # Verbosity, zero means quiet
-
- attr_accessor :verbosity
-
- ##
- # URL of web cvs frontend
-
- attr_reader :webcvs
-
- def initialize(generators = {}) # :nodoc:
- @op_dir = "doc"
- @op_name = nil
- @show_all = false
- @main_page = nil
- @merge = false
- @exclude = []
- @generators = generators
- @generator_name = 'html'
- @generator = @generators[@generator_name]
- @rdoc_include = []
- @title = nil
- @template = nil
- @template_class = nil
- @diagram = false
- @fileboxes = false
- @show_hash = false
- @image_format = 'png'
- @inline_source = false
- @all_one_file = false
- @tab_width = 8
- @include_line_numbers = false
- @extra_accessor_flags = {}
- @promiscuous = false
- @force_update = false
- @verbosity = 1
-
- @css = nil
- @webcvs = nil
-
- @charset = 'utf-8'
- end
-
- ##
- # Parse command line options.
-
- def parse(argv)
- accessors = []
-
- opts = OptionParser.new do |opt|
- opt.program_name = File.basename $0
- opt.version = RDoc::VERSION
- opt.release = nil
- opt.summary_indent = ' ' * 4
- opt.banner = <<-EOF
-Usage: #{opt.program_name} [options] [names...]
-
- Files are parsed, and the information they contain collected, before any
- output is produced. This allows cross references between all files to be
- resolved. If a name is a directory, it is traversed. If no names are
- specified, all Ruby files in the current directory (and subdirectories) are
- processed.
-
- How RDoc generates output depends on the output formatter being used, and on
- the options you give.
-
- - HTML output is normally produced into a number of separate files
- (one per class, module, and file, along with various indices).
- These files will appear in the directory given by the --op
- option (doc/ by default).
-
- - XML output by default is written to standard output. If a
- --opname option is given, the output will instead be written
- to a file with that name in the output directory.
-
- - .chm files (Windows help files) are written in the --op directory.
- If an --opname parameter is present, that name is used, otherwise
- the file will be called rdoc.chm.
- EOF
-
- opt.separator nil
- opt.separator "Options:"
- opt.separator nil
-
- opt.on("--accessor=ACCESSORS", "-A", Array,
- "A comma separated list of additional class",
- "methods that should be treated like",
- "'attr_reader' and friends.",
- " ",
- "Option may be repeated.",
- " ",
- "Each accessorname may have '=text'",
- "appended, in which case that text appears",
- "where the r/w/rw appears for normal.",
- "accessors") do |value|
- value.each do |accessor|
- if accessor =~ /^(\w+)(=(.*))?$/
- accessors << $1
- @extra_accessor_flags[$1] = $3
- end
- end
- end
-
- opt.separator nil
-
- opt.on("--all", "-a",
- "Include all methods (not just public) in",
- "the output.") do |value|
- @show_all = value
- end
-
- opt.separator nil
-
- opt.on("--charset=CHARSET", "-c",
- "Specifies the output HTML character-set.") do |value|
- @charset = value
- end
-
- opt.separator nil
-
- opt.on("--debug", "-D",
- "Displays lots on internal stuff.") do |value|
- $DEBUG_RDOC = value
- end
-
- opt.separator nil
-
- opt.on("--diagram", "-d",
- "Generate diagrams showing modules and",
- "classes. You need dot V1.8.6 or later to",
- "use the --diagram option correctly. Dot is",
- "available from http://graphviz.org") do |value|
- check_diagram
- @diagram = true
- end
-
- opt.separator nil
-
- opt.on("--exclude=PATTERN", "-x", Regexp,
- "Do not process files or directories",
- "matching PATTERN.") do |value|
- @exclude << value
- end
-
- opt.separator nil
-
- opt.on("--extension=NEW=OLD", "-E",
- "Treat files ending with .new as if they",
- "ended with .old. Using '-E cgi=rb' will",
- "cause xxx.cgi to be parsed as a Ruby file.") do |value|
- new, old = value.split(/=/, 2)
-
- unless new and old then
- raise OptionParser::InvalidArgument, "Invalid parameter to '-E'"
- end
-
- unless RDoc::ParserFactory.alias_extension old, new then
- raise OptionParser::InvalidArgument, "Unknown extension .#{old} to -E"
- end
- end
-
- opt.separator nil
-
- opt.on("--fileboxes", "-F",
- "Classes are put in boxes which represents",
- "files, where these classes reside. Classes",
- "shared between more than one file are",
- "shown with list of files that are sharing",
- "them. Silently discarded if --diagram is",
- "not given.") do |value|
- @fileboxes = value
- end
-
- opt.separator nil
-
- opt.on("--force-update", "-U",
- "Forces rdoc to scan all sources even if",
- "newer than the flag file.") do |value|
- @force_update = value
- end
-
- opt.separator nil
-
- opt.on("--fmt=FORMAT", "--format=FORMAT", "-f", @generators.keys,
- "Set the output formatter.") do |value|
- @generator_name = value.downcase
- setup_generator
- end
-
- opt.separator nil
-
- image_formats = %w[gif png jpg jpeg]
- opt.on("--image-format=FORMAT", "-I", image_formats,
- "Sets output image format for diagrams. Can",
- "be #{image_formats.join ', '}. If this option",
- "is omitted, png is used. Requires",
- "diagrams.") do |value|
- @image_format = value
- end
-
- opt.separator nil
-
- opt.on("--include=DIRECTORIES", "-i", Array,
- "set (or add to) the list of directories to",
- "be searched when satisfying :include:",
- "requests. Can be used more than once.") do |value|
- @rdoc_include.concat value.map { |dir| dir.strip }
- end
-
- opt.separator nil
-
- opt.on("--inline-source", "-S",
- "Show method source code inline, rather than",
- "via a popup link.") do |value|
- @inline_source = value
- end
-
- opt.separator nil
-
- opt.on("--line-numbers", "-N",
- "Include line numbers in the source code.") do |value|
- @include_line_numbers = value
- end
-
- opt.separator nil
-
- opt.on("--main=NAME", "-m",
- "NAME will be the initial page displayed.") do |value|
- @main_page = value
- end
-
- opt.separator nil
-
- opt.on("--merge", "-M",
- "When creating ri output, merge previously",
- "processed classes into previously",
- "documented classes of the same name.") do |value|
- @merge = value
- end
-
- opt.separator nil
-
- opt.on("--one-file", "-1",
- "Put all the output into a single file.") do |value|
- @all_one_file = value
- @inline_source = value if value
- @template = 'one_page_html'
- end
-
- opt.separator nil
-
- opt.on("--op=DIR", "-o",
- "Set the output directory.") do |value|
- @op_dir = value
- end
-
- opt.separator nil
-
- opt.on("--opname=NAME", "-n",
- "Set the NAME of the output. Has no effect",
- "for HTML.") do |value|
- @op_name = value
- end
-
- opt.separator nil
-
- opt.on("--promiscuous", "-p",
- "When documenting a file that contains a",
- "module or class also defined in other",
- "files, show all stuff for that module or",
- "class in each files page. By default, only",
- "show stuff defined in that particular file.") do |value|
- @promiscuous = value
- end
-
- opt.separator nil
-
- opt.on("--quiet", "-q",
- "Don't show progress as we parse.") do |value|
- @verbosity = 0
- end
-
- opt.on("--verbose", "-v",
- "Display extra progress as we parse.") do |value|
- @verbosity = 2
- end
-
-
- opt.separator nil
-
- opt.on("--ri", "-r",
- "Generate output for use by `ri`. The files",
- "are stored in the '.rdoc' directory under",
- "your home directory unless overridden by a",
- "subsequent --op parameter, so no special",
- "privileges are needed.") do |value|
- @generator_name = "ri"
- @op_dir = RDoc::RI::Paths::HOMEDIR
- setup_generator
- end
-
- opt.separator nil
-
- opt.on("--ri-site", "-R",
- "Generate output for use by `ri`. The files",
- "are stored in a site-wide directory,",
- "making them accessible to others, so",
- "special privileges are needed.") do |value|
- @generator_name = "ri"
- @op_dir = RDoc::RI::Paths::SITEDIR
- setup_generator
- end
-
- opt.separator nil
-
- opt.on("--ri-system", "-Y",
- "Generate output for use by `ri`. The files",
- "are stored in a site-wide directory,",
- "making them accessible to others, so",
- "special privileges are needed. This",
- "option is intended to be used during Ruby",
- "installation.") do |value|
- @generator_name = "ri"
- @op_dir = RDoc::RI::Paths::SYSDIR
- setup_generator
- end
-
- opt.separator nil
-
- opt.on("--show-hash", "-H",
- "A name of the form #name in a comment is a",
- "possible hyperlink to an instance method",
- "name. When displayed, the '#' is removed",
- "unless this option is specified.") do |value|
- @show_hash = value
- end
-
- opt.separator nil
-
- opt.on("--style=URL", "-s",
- "Specifies the URL of a separate stylesheet.") do |value|
- @css = value
- end
-
- opt.separator nil
-
- opt.on("--tab-width=WIDTH", "-w", OptionParser::DecimalInteger,
- "Set the width of tab characters.") do |value|
- @tab_width = value
- end
-
- opt.separator nil
-
- opt.on("--template=NAME", "-T",
- "Set the template used when generating",
- "output.") do |value|
- @template = value
- end
-
- opt.separator nil
-
- opt.on("--title=TITLE", "-t",
- "Set TITLE as the title for HTML output.") do |value|
- @title = value
- end
-
- opt.separator nil
-
- opt.on("--webcvs=URL", "-W",
- "Specify a URL for linking to a web frontend",
- "to CVS. If the URL contains a '\%s', the",
- "name of the current file will be",
- "substituted; if the URL doesn't contain a",
- "'\%s', the filename will be appended to it.") do |value|
- @webcvs = value
- end
- end
-
- argv.insert(0, *ENV['RDOCOPT'].split) if ENV['RDOCOPT']
-
- opts.parse! argv
-
- @files = argv.dup
-
- @rdoc_include << "." if @rdoc_include.empty?
-
- if @exclude.empty? then
- @exclude = nil
- else
- @exclude = Regexp.new(@exclude.join("|"))
- end
-
- check_files
-
- # If no template was specified, use the default template for the output
- # formatter
-
- @template ||= @generator_name
-
- # Generate a regexp from the accessors
- unless accessors.empty? then
- re = '^(' + accessors.map { |a| Regexp.quote a }.join('|') + ')$'
- @extra_accessors = Regexp.new re
- end
-
- rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e
- puts opts
- puts
- puts e
- exit 1
- end
-
- ##
- # Set the title, but only if not already set. This means that a title set
- # from the command line trumps one set in a source file
-
- def title=(string)
- @title ||= string
- end
-
- ##
- # Don't display progress as we process the files
-
- def quiet
- @verbosity.zero?
- end
-
- def quiet=(bool)
- @verbosity = bool ? 0 : 1
- end
-
- private
-
- ##
- # Set up an output generator for the format in @generator_name
-
- def setup_generator
- @generator = @generators[@generator_name]
-
- unless @generator then
- raise OptionParser::InvalidArgument, "Invalid output formatter"
- end
-
- if @generator_name == "xml" then
- @all_one_file = true
- @inline_source = true
- end
- end
-
- # Check that the right version of 'dot' is available. Unfortunately this
- # doesn't work correctly under Windows NT, so we'll bypass the test under
- # Windows.
-
- def check_diagram
- return if RUBY_PLATFORM =~ /mswin|cygwin|mingw|bccwin/
-
- ok = false
- ver = nil
-
- IO.popen "dot -V 2>&1" do |io|
- ver = io.read
- if ver =~ /dot.+version(?:\s+gviz)?\s+(\d+)\.(\d+)/ then
- ok = ($1.to_i > 1) || ($1.to_i == 1 && $2.to_i >= 8)
- end
- end
-
- unless ok then
- if ver =~ /^dot.+version/ then
- $stderr.puts "Warning: You may need dot V1.8.6 or later to use\n",
- "the --diagram option correctly. You have:\n\n ",
- ver,
- "\nDiagrams might have strange background colors.\n\n"
- else
- $stderr.puts "You need the 'dot' program to produce diagrams.",
- "(see http://www.research.att.com/sw/tools/graphviz/)\n\n"
- exit
- end
- end
- end
-
- ##
- # Check that the files on the command line exist
-
- def check_files
- @files.each do |f|
- stat = File.stat f
- raise RDoc::Error, "file '#{f}' not readable" unless stat.readable?
- end
- end
-
-end
-
diff --git a/lib/rdoc/parser.rb b/lib/rdoc/parser.rb
deleted file mode 100644
index 6b1233c62d..0000000000
--- a/lib/rdoc/parser.rb
+++ /dev/null
@@ -1,142 +0,0 @@
-require 'rdoc'
-require 'rdoc/code_objects'
-require 'rdoc/markup/preprocess'
-require 'rdoc/stats'
-
-##
-# A parser is simple a class that implements
-#
-# #initialize(file_name, body, options)
-#
-# and
-#
-# #scan
-#
-# The initialize method takes a file name to be used, the body of the file,
-# and an RDoc::Options object. The scan method is then called to return an
-# appropriately parsed TopLevel code object.
-#
-# The ParseFactory is used to redirect to the correct parser given a
-# filename extension. This magic works because individual parsers have to
-# register themselves with us as they are loaded in. The do this using the
-# following incantation
-#
-# require "rdoc/parser"
-#
-# class RDoc::Parser::Xyz < RDoc::Parser
-# parse_files_matching /\.xyz$/ # <<<<
-#
-# def initialize(file_name, body, options)
-# ...
-# end
-#
-# def scan
-# ...
-# end
-# end
-#
-# Just to make life interesting, if we suspect a plain text file, we also
-# look for a shebang line just in case it's a potential shell script
-
-class RDoc::Parser
-
- @parsers = []
-
- class << self
- attr_reader :parsers
- end
-
- ##
- # Alias an extension to another extension. After this call, files ending
- # "new_ext" will be parsed using the same parser as "old_ext"
-
- def self.alias_extension(old_ext, new_ext)
- old_ext = old_ext.sub(/^\.(.*)/, '\1')
- new_ext = new_ext.sub(/^\.(.*)/, '\1')
-
- parser = can_parse "xxx.#{old_ext}"
- return false unless parser
-
- RDoc::Parser.parsers.unshift [/\.#{new_ext}$/, parser]
-
- true
- end
-
- ##
- # Shamelessly stolen from the ptools gem (since RDoc cannot depend on
- # the gem).
-
- def self.binary?(file)
- s = (File.read(file, File.stat(file).blksize, 0, :mode => "rb") || "").split(//)
-
- if s.size > 0 then
- ((s.size - s.grep(" ".."~").size) / s.size.to_f) > 0.30
- else
- false
- end
- end
- private_class_method :binary?
-
- ##
- # Return a parser that can handle a particular extension
-
- def self.can_parse(file_name)
- parser = RDoc::Parser.parsers.find { |regexp,| regexp =~ file_name }.last
-
- #
- # The default parser should *NOT* parse binary files.
- #
- if parser == RDoc::Parser::Simple then
- if binary? file_name then
- return nil
- end
- end
-
- return parser
- end
-
- ##
- # Find the correct parser for a particular file name. Return a SimpleParser
- # for ones that we don't know
-
- def self.for(top_level, file_name, body, options, stats)
- # If no extension, look for shebang
- if file_name !~ /\.\w+$/ && body =~ %r{\A#!(.+)} then
- shebang = $1
- case shebang
- when %r{env\s+ruby}, %r{/ruby}
- file_name = "dummy.rb"
- end
- end
-
- parser = can_parse file_name
-
- #
- # This method must return a parser.
- #
- if !parser then
- parser = RDoc::Parser::Simple
- end
-
- parser.new top_level, file_name, body, options, stats
- end
-
- ##
- # Record which file types this parser can understand.
-
- def self.parse_files_matching(regexp)
- RDoc::Parser.parsers.unshift [regexp, self]
- end
-
- def initialize(top_level, file_name, content, options, stats)
- @top_level = top_level
- @file_name = file_name
- @content = content
- @options = options
- @stats = stats
- end
-
-end
-
-require 'rdoc/parser/simple'
-
diff --git a/lib/rdoc/parser/c.rb b/lib/rdoc/parser/c.rb
deleted file mode 100644
index 933838debd..0000000000
--- a/lib/rdoc/parser/c.rb
+++ /dev/null
@@ -1,661 +0,0 @@
-require 'rdoc/parser'
-require 'rdoc/parser/ruby'
-require 'rdoc/known_classes'
-
-##
-# We attempt to parse C extension files. Basically we look for
-# the standard patterns that you find in extensions: <tt>rb_define_class,
-# rb_define_method</tt> and so on. We also try to find the corresponding
-# C source for the methods and extract comments, but if we fail
-# we don't worry too much.
-#
-# The comments associated with a Ruby method are extracted from the C
-# comment block associated with the routine that _implements_ that
-# method, that is to say the method whose name is given in the
-# <tt>rb_define_method</tt> call. For example, you might write:
-#
-# /*
-# * Returns a new array that is a one-dimensional flattening of this
-# * array (recursively). That is, for every element that is an array,
-# * extract its elements into the new array.
-# *
-# * s = [ 1, 2, 3 ] #=> [1, 2, 3]
-# * t = [ 4, 5, 6, [7, 8] ] #=> [4, 5, 6, [7, 8]]
-# * a = [ s, t, 9, 10 ] #=> [[1, 2, 3], [4, 5, 6, [7, 8]], 9, 10]
-# * a.flatten #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
-# */
-# static VALUE
-# rb_ary_flatten(ary)
-# VALUE ary;
-# {
-# ary = rb_obj_dup(ary);
-# rb_ary_flatten_bang(ary);
-# return ary;
-# }
-#
-# ...
-#
-# void
-# Init_Array()
-# {
-# ...
-# rb_define_method(rb_cArray, "flatten", rb_ary_flatten, 0);
-#
-# Here RDoc will determine from the rb_define_method line that there's a
-# method called "flatten" in class Array, and will look for the implementation
-# in the method rb_ary_flatten. It will then use the comment from that
-# method in the HTML output. This method must be in the same source file
-# as the rb_define_method.
-#
-# C classes can be diagrammed (see /tc/dl/ruby/ruby/error.c), and RDoc
-# integrates C and Ruby source into one tree
-#
-# The comment blocks may include special directives:
-#
-# [Document-class: <i>name</i>]
-# This comment block is documentation for the given class. Use this
-# when the <tt>Init_xxx</tt> method is not named after the class.
-#
-# [Document-method: <i>name</i>]
-# This comment documents the named method. Use when RDoc cannot
-# automatically find the method from it's declaration
-#
-# [call-seq: <i>text up to an empty line</i>]
-# Because C source doesn't give descripive names to Ruby-level parameters,
-# you need to document the calling sequence explicitly
-#
-# In addition, RDoc assumes by default that the C method implementing a
-# Ruby function is in the same source file as the rb_define_method call.
-# If this isn't the case, add the comment:
-#
-# rb_define_method(....); // in: filename
-#
-# As an example, we might have an extension that defines multiple classes
-# in its Init_xxx method. We could document them using
-#
-# /*
-# * Document-class: MyClass
-# *
-# * Encapsulate the writing and reading of the configuration
-# * file. ...
-# */
-#
-# /*
-# * Document-method: read_value
-# *
-# * call-seq:
-# * cfg.read_value(key) -> value
-# * cfg.read_value(key} { |key| } -> value
-# *
-# * Return the value corresponding to +key+ from the configuration.
-# * In the second form, if the key isn't found, invoke the
-# * block and return its value.
-# */
-
-class RDoc::Parser::C < RDoc::Parser
-
- parse_files_matching(/\.(?:([CcHh])\1?|c([+xp])\2|y)\z/)
-
- @@enclosure_classes = {}
- @@known_bodies = {}
-
- ##
- # Prepare to parse a C file
-
- def initialize(top_level, file_name, content, options, stats)
- super
-
- @known_classes = RDoc::KNOWN_CLASSES.dup
- @content = handle_tab_width handle_ifdefs_in(@content)
- @classes = Hash.new
- @file_dir = File.dirname(@file_name)
- end
-
- def do_aliases
- @content.scan(%r{rb_define_alias\s*\(\s*(\w+),\s*"([^"]+)",\s*"([^"]+)"\s*\)}m) do
- |var_name, new_name, old_name|
- class_name = @known_classes[var_name] || var_name
- class_obj = find_class(var_name, class_name)
-
- as = class_obj.add_alias RDoc::Alias.new("", old_name, new_name, "")
-
- @stats.add_alias as
- end
- end
-
- def do_classes
- @content.scan(/(\w+)\s* = \s*rb_define_module\s*\(\s*"(\w+)"\s*\)/mx) do
- |var_name, class_name|
- handle_class_module(var_name, "module", class_name, nil, nil)
- end
-
- # The '.' lets us handle SWIG-generated files
- @content.scan(/([\w\.]+)\s* = \s*rb_define_class\s*
- \(
- \s*"(\w+)",
- \s*(\w+)\s*
- \)/mx) do |var_name, class_name, parent|
- handle_class_module(var_name, "class", class_name, parent, nil)
- end
-
- @content.scan(/(\w+)\s*=\s*boot_defclass\s*\(\s*"(\w+?)",\s*(\w+?)\s*\)/) do
- |var_name, class_name, parent|
- parent = nil if parent == "0"
- handle_class_module(var_name, "class", class_name, parent, nil)
- end
-
- @content.scan(/(\w+)\s* = \s*rb_define_module_under\s*
- \(
- \s*(\w+),
- \s*"(\w+)"
- \s*\)/mx) do |var_name, in_module, class_name|
- handle_class_module(var_name, "module", class_name, nil, in_module)
- end
-
- @content.scan(/([\w\.]+)\s* = \s*rb_define_class_under\s*
- \(
- \s*(\w+),
- \s*"(\w+)",
- \s*([\w\*\s\(\)\.\->]+)\s* # for SWIG
- \s*\)/mx) do |var_name, in_module, class_name, parent|
- handle_class_module(var_name, "class", class_name, parent, in_module)
- end
- end
-
- def do_constants
- @content.scan(%r{\Wrb_define_
- (
- variable |
- readonly_variable |
- const |
- global_const |
- )
- \s*\(
- (?:\s*(\w+),)?
- \s*"(\w+)",
- \s*(.*?)\s*\)\s*;
- }xm) do |type, var_name, const_name, definition|
- var_name = "rb_cObject" if !var_name or var_name == "rb_mKernel"
- handle_constants(type, var_name, const_name, definition)
- end
- end
-
- ##
- # Look for includes of the form:
- #
- # rb_include_module(rb_cArray, rb_mEnumerable);
-
- def do_includes
- @content.scan(/rb_include_module\s*\(\s*(\w+?),\s*(\w+?)\s*\)/) do |c,m|
- if cls = @classes[c]
- m = @known_classes[m] || m
- cls.add_include RDoc::Include.new(m, "")
- end
- end
- end
-
- def do_methods
- @content.scan(%r{rb_define_
- (
- singleton_method |
- method |
- module_function |
- private_method
- )
- \s*\(\s*([\w\.]+),
- \s*"([^"]+)",
- \s*(?:RUBY_METHOD_FUNC\(|VALUEFUNC\()?(\w+)\)?,
- \s*(-?\w+)\s*\)
- (?:;\s*/[*/]\s+in\s+(\w+?\.[cy]))?
- }xm) do
- |type, var_name, meth_name, meth_body, param_count, source_file|
-
- # Ignore top-object and weird struct.c dynamic stuff
- next if var_name == "ruby_top_self"
- next if var_name == "nstr"
- next if var_name == "envtbl"
- next if var_name == "argf" # it'd be nice to handle this one
-
- var_name = "rb_cObject" if var_name == "rb_mKernel"
- handle_method(type, var_name, meth_name,
- meth_body, param_count, source_file)
- end
-
- @content.scan(%r{rb_define_attr\(
- \s*([\w\.]+),
- \s*"([^"]+)",
- \s*(\d+),
- \s*(\d+)\s*\);
- }xm) do |var_name, attr_name, attr_reader, attr_writer|
- #var_name = "rb_cObject" if var_name == "rb_mKernel"
- handle_attr(var_name, attr_name,
- attr_reader.to_i != 0,
- attr_writer.to_i != 0)
- end
-
- @content.scan(%r{rb_define_global_function\s*\(
- \s*"([^"]+)",
- \s*(?:RUBY_METHOD_FUNC\(|VALUEFUNC\()?(\w+)\)?,
- \s*(-?\w+)\s*\)
- (?:;\s*/[*/]\s+in\s+(\w+?\.[cy]))?
- }xm) do |meth_name, meth_body, param_count, source_file|
- handle_method("method", "rb_mKernel", meth_name,
- meth_body, param_count, source_file)
- end
-
- @content.scan(/define_filetest_function\s*\(
- \s*"([^"]+)",
- \s*(?:RUBY_METHOD_FUNC\(|VALUEFUNC\()?(\w+)\)?,
- \s*(-?\w+)\s*\)/xm) do
- |meth_name, meth_body, param_count|
-
- handle_method("method", "rb_mFileTest", meth_name, meth_body, param_count)
- handle_method("singleton_method", "rb_cFile", meth_name, meth_body, param_count)
- end
- end
-
- def find_attr_comment(attr_name)
- if @content =~ %r{((?>/\*.*?\*/\s+))
- rb_define_attr\((?:\s*(\w+),)?\s*"#{attr_name}"\s*,.*?\)\s*;}xmi
- $1
- elsif @content =~ %r{Document-attr:\s#{attr_name}\s*?\n((?>.*?\*/))}m
- $1
- else
- ''
- end
- end
-
- ##
- # Find the C code corresponding to a Ruby method
-
- def find_body(class_name, meth_name, meth_obj, body, quiet = false)
- case body
- when %r"((?>/\*.*?\*/\s*))(?:(?:static|SWIGINTERN)\s+)?(?:intern\s+)?VALUE\s+#{meth_name}
- \s*(\([^)]*\))([^;]|$)"xm
- comment, params = $1, $2
- body_text = $&
-
- remove_private_comments(comment) if comment
-
- # see if we can find the whole body
-
- re = Regexp.escape(body_text) + '[^(]*^\{.*?^\}'
- body_text = $& if /#{re}/m =~ body
-
- # The comment block may have been overridden with a 'Document-method'
- # block. This happens in the interpreter when multiple methods are
- # vectored through to the same C method but those methods are logically
- # distinct (for example Kernel.hash and Kernel.object_id share the same
- # implementation
-
- override_comment = find_override_comment(class_name, meth_obj.name)
- comment = override_comment if override_comment
-
- find_modifiers(comment, meth_obj) if comment
-
-# meth_obj.params = params
- meth_obj.start_collecting_tokens
- meth_obj.add_token(RDoc::RubyToken::Token.new(1,1).set_text(body_text))
- meth_obj.comment = mangle_comment(comment)
- when %r{((?>/\*.*?\*/\s*))^\s*\#\s*define\s+#{meth_name}\s+(\w+)}m
- comment = $1
- find_body(class_name, $2, meth_obj, body, true)
- find_modifiers(comment, meth_obj)
- meth_obj.comment = mangle_comment(comment) + meth_obj.comment
- when %r{^\s*\#\s*define\s+#{meth_name}\s+(\w+)}m
- unless find_body(class_name, $1, meth_obj, body, true)
- warn "No definition for #{meth_name}" unless @options.quiet
- return false
- end
- else
-
- # No body, but might still have an override comment
- comment = find_override_comment(class_name, meth_obj.name)
-
- if comment
- find_modifiers(comment, meth_obj)
- meth_obj.comment = mangle_comment(comment)
- else
- warn "No definition for #{meth_name}" unless @options.quiet
- return false
- end
- end
- true
- end
-
- def find_class(raw_name, name)
- unless @classes[raw_name]
- if raw_name =~ /^rb_m/
- container = @top_level.add_module RDoc::NormalModule, name
- else
- container = @top_level.add_class RDoc::NormalClass, name, nil
- end
-
- container.record_location @top_level
- @classes[raw_name] = container
- end
- @classes[raw_name]
- end
-
- ##
- # Look for class or module documentation above Init_+class_name+(void),
- # in a Document-class +class_name+ (or module) comment or above an
- # rb_define_class (or module). If a comment is supplied above a matching
- # Init_ and a rb_define_class the Init_ comment is used.
- #
- # /*
- # * This is a comment for Foo
- # */
- # Init_Foo(void) {
- # VALUE cFoo = rb_define_class("Foo", rb_cObject);
- # }
- #
- # /*
- # * Document-class: Foo
- # * This is a comment for Foo
- # */
- # Init_foo(void) {
- # VALUE cFoo = rb_define_class("Foo", rb_cObject);
- # }
- #
- # /*
- # * This is a comment for Foo
- # */
- # VALUE cFoo = rb_define_class("Foo", rb_cObject);
-
- def find_class_comment(class_name, class_meth)
- comment = nil
- if @content =~ %r{((?>/\*.*?\*/\s+))
- (static\s+)?void\s+Init_#{class_name}\s*(?:_\(\s*)?\(\s*(?:void\s*)\)}xmi then
- comment = $1
- elsif @content =~ %r{Document-(?:class|module):\s#{class_name}\s*?(?:<\s+[:,\w]+)?\n((?>.*?\*/))}m
- comment = $1
- else
- if @content =~ /rb_define_(class|module)/m then
- class_name = class_name.split("::").last
- comments = []
- @content.split(/(\/\*.*?\*\/)\s*?\n/m).each_with_index do |chunk, index|
- comments[index] = chunk
- if chunk =~ /rb_define_(class|module).*?"(#{class_name})"/m then
- comment = comments[index-1]
- break
- end
- end
- end
- end
- class_meth.comment = mangle_comment(comment) if comment
- end
-
- ##
- # Finds a comment matching +type+ and +const_name+ either above the
- # comment or in the matching Document- section.
-
- def find_const_comment(type, const_name)
- if @content =~ %r{((?>^\s*/\*.*?\*/\s+))
- rb_define_#{type}\((?:\s*(\w+),)?\s*"#{const_name}"\s*,.*?\)\s*;}xmi
- $1
- elsif @content =~ %r{Document-(?:const|global|variable):\s#{const_name}\s*?\n((?>.*?\*/))}m
- $1
- else
- ''
- end
- end
-
- ##
- # If the comment block contains a section that looks like:
- #
- # call-seq:
- # Array.new
- # Array.new(10)
- #
- # use it for the parameters.
-
- def find_modifiers(comment, meth_obj)
- if comment.sub!(/:nodoc:\s*^\s*\*?\s*$/m, '') or
- comment.sub!(/\A\/\*\s*:nodoc:\s*\*\/\Z/, '')
- meth_obj.document_self = false
- end
- if comment.sub!(/call-seq:(.*?)^\s*\*?\s*$/m, '') or
- comment.sub!(/\A\/\*\s*call-seq:(.*?)\*\/\Z/, '')
- seq = $1
- seq.gsub!(/^\s*\*\s*/, '')
- meth_obj.call_seq = seq
- end
- end
-
- def find_override_comment(class_name, meth_name)
- name = Regexp.escape(meth_name)
- if @content =~ %r{Document-method:\s+#{class_name}(?:\.|::|#)#{name}\s*?\n((?>.*?\*/))}m then
- $1
- elsif @content =~ %r{Document-method:\s#{name}\s*?\n((?>.*?\*/))}m then
- $1
- end
- end
-
- def handle_attr(var_name, attr_name, reader, writer)
- rw = ''
- if reader
- #@stats.num_methods += 1
- rw << 'R'
- end
- if writer
- #@stats.num_methods += 1
- rw << 'W'
- end
-
- class_name = @known_classes[var_name]
-
- return unless class_name
-
- class_obj = find_class(var_name, class_name)
-
- if class_obj
- comment = find_attr_comment(attr_name)
- unless comment.empty?
- comment = mangle_comment(comment)
- end
- att = RDoc::Attr.new '', attr_name, rw, comment
- class_obj.add_attribute(att)
- end
- end
-
- def handle_class_module(var_name, class_mod, class_name, parent, in_module)
- parent_name = @known_classes[parent] || parent
-
- if in_module
- enclosure = @classes[in_module] || @@enclosure_classes[in_module]
- unless enclosure
- if enclosure = @known_classes[in_module]
- handle_class_module(in_module, (/^rb_m/ =~ in_module ? "module" : "class"),
- enclosure, nil, nil)
- enclosure = @classes[in_module]
- end
- end
- unless enclosure
- warn("Enclosing class/module '#{in_module}' for " +
- "#{class_mod} #{class_name} not known")
- return
- end
- else
- enclosure = @top_level
- end
-
- if class_mod == "class" then
- full_name = enclosure.full_name.to_s + "::#{class_name}"
- if @content =~ %r{Document-class:\s+#{full_name}\s*<\s+([:,\w]+)} then
- parent_name = $1
- end
- cm = enclosure.add_class RDoc::NormalClass, class_name, parent_name
- @stats.add_class cm
- else
- cm = enclosure.add_module RDoc::NormalModule, class_name
- @stats.add_module cm
- end
-
- cm.record_location(enclosure.toplevel)
-
- find_class_comment(cm.full_name, cm)
- @classes[var_name] = cm
- @@enclosure_classes[var_name] = cm
- @known_classes[var_name] = cm.full_name
- end
-
- ##
- # Adds constant comments. By providing some_value: at the start ofthe
- # comment you can override the C value of the comment to give a friendly
- # definition.
- #
- # /* 300: The perfect score in bowling */
- # rb_define_const(cFoo, "PERFECT", INT2FIX(300);
- #
- # Will override +INT2FIX(300)+ with the value +300+ in the output RDoc.
- # Values may include quotes and escaped colons (\:).
-
- def handle_constants(type, var_name, const_name, definition)
- #@stats.num_constants += 1
- class_name = @known_classes[var_name]
-
- return unless class_name
-
- class_obj = find_class(var_name, class_name)
-
- unless class_obj
- warn("Enclosing class/module '#{const_name}' for not known")
- return
- end
-
- comment = find_const_comment(type, const_name)
-
- # In the case of rb_define_const, the definition and comment are in
- # "/* definition: comment */" form. The literal ':' and '\' characters
- # can be escaped with a backslash.
- if type.downcase == 'const' then
- elements = mangle_comment(comment).split(':')
- if elements.nil? or elements.empty? then
- con = RDoc::Constant.new(const_name, definition,
- mangle_comment(comment))
- else
- new_definition = elements[0..-2].join(':')
- if new_definition.empty? then # Default to literal C definition
- new_definition = definition
- else
- new_definition.gsub!("\:", ":")
- new_definition.gsub!("\\", '\\')
- end
- new_definition.sub!(/\A(\s+)/, '')
- new_comment = $1.nil? ? elements.last : "#{$1}#{elements.last.lstrip}"
- con = RDoc::Constant.new(const_name, new_definition,
- mangle_comment(new_comment))
- end
- else
- con = RDoc::Constant.new const_name, definition, mangle_comment(comment)
- end
-
- class_obj.add_constant(con)
- end
-
- ##
- # Removes #ifdefs that would otherwise confuse us
-
- def handle_ifdefs_in(body)
- body.gsub(/^#ifdef HAVE_PROTOTYPES.*?#else.*?\n(.*?)#endif.*?\n/m, '\1')
- end
-
- def handle_method(type, var_name, meth_name, meth_body, param_count,
- source_file = nil)
- class_name = @known_classes[var_name]
-
- return unless class_name
-
- class_obj = find_class var_name, class_name
-
- if class_obj then
- if meth_name == "initialize" then
- meth_name = "new"
- type = "singleton_method"
- end
-
- meth_obj = RDoc::AnyMethod.new '', meth_name
- meth_obj.singleton = %w[singleton_method module_function].include? type
-
- p_count = (Integer(param_count) rescue -1)
-
- if p_count < 0
- meth_obj.params = "(...)"
- elsif p_count == 0
- meth_obj.params = "()"
- else
- meth_obj.params = "(" + (1..p_count).map{|i| "p#{i}"}.join(", ") + ")"
- end
-
- if source_file then
- file_name = File.join(@file_dir, source_file)
- body = (@@known_bodies[source_file] ||= File.read(file_name))
- else
- body = @content
- end
-
- if find_body(class_name, meth_body, meth_obj, body) and meth_obj.document_self then
- class_obj.add_method meth_obj
- @stats.add_method meth_obj
- end
- end
- end
-
- def handle_tab_width(body)
- if /\t/ =~ body
- tab_width = @options.tab_width
- body.split(/\n/).map do |line|
- 1 while line.gsub!(/\t+/) { ' ' * (tab_width*$&.length - $`.length % tab_width)} && $~ #`
- line
- end .join("\n")
- else
- body
- end
- end
-
- ##
- # Remove the /*'s and leading asterisks from C comments
-
- def mangle_comment(comment)
- comment.sub!(%r{/\*+}) { " " * $&.length }
- comment.sub!(%r{\*+/}) { " " * $&.length }
- comment.gsub!(/^[ \t]*\*/m) { " " * $&.length }
- comment
- end
-
- ##
- # Removes lines that are commented out that might otherwise get picked up
- # when scanning for classes and methods
-
- def remove_commented_out_lines
- @content.gsub!(%r{//.*rb_define_}, '//')
- end
-
- def remove_private_comments(comment)
- comment.gsub!(/\/?\*--\n(.*?)\/?\*\+\+/m, '')
- comment.sub!(/\/?\*--\n.*/m, '')
- end
-
- ##
- # Extract the classes/modules and methods from a C file and return the
- # corresponding top-level object
-
- def scan
- remove_commented_out_lines
- do_classes
- do_constants
- do_methods
- do_includes
- do_aliases
- @top_level
- end
-
- def warn(msg)
- $stderr.puts
- $stderr.puts msg
- $stderr.flush
- end
-
-end
-
diff --git a/lib/rdoc/parser/f95.rb b/lib/rdoc/parser/f95.rb
deleted file mode 100644
index fd372b098b..0000000000
--- a/lib/rdoc/parser/f95.rb
+++ /dev/null
@@ -1,1835 +0,0 @@
-require 'rdoc/parser'
-
-##
-# = Fortran95 RDoc Parser
-#
-# == Overview
-#
-# This parser parses Fortran95 files with suffixes "f90", "F90", "f95" and
-# "F95". Fortran95 files are expected to be conformed to Fortran95 standards.
-#
-# == Rules
-#
-# Fundamental rules are same as that of the Ruby parser. But comment markers
-# are '!' not '#'.
-#
-# === Correspondence between RDoc documentation and Fortran95 programs
-#
-# F95 parses main programs, modules, subroutines, functions, derived-types,
-# public variables, public constants, defined operators and defined
-# assignments. These components are described in items of RDoc documentation,
-# as follows.
-#
-# Files :: Files (same as Ruby)
-# Classes:: Modules
-# Methods:: Subroutines, functions, variables, constants, derived-types,
-# defined operators, defined assignments
-# Required files:: Files in which imported modules, external subroutines and
-# external functions are defined.
-# Included Modules:: List of imported modules
-# Attributes:: List of derived-types, List of imported modules all of whose
-# components are published again
-#
-# Components listed in 'Methods' (subroutines, functions, ...) defined in
-# modules are described in the item of 'Classes'. On the other hand,
-# components defined in main programs or as external procedures are described
-# in the item of 'Files'.
-#
-# === Components parsed by default
-#
-# By default, documentation on public components (subroutines, functions,
-# variables, constants, derived-types, defined operators, defined assignments)
-# are generated.
-#
-# With "--all" option, documentation on all components are generated (almost
-# same as the Ruby parser).
-#
-# === Information parsed automatically
-#
-# The following information is automatically parsed.
-#
-# * Types of arguments
-# * Types of variables and constants
-# * Types of variables in the derived types, and initial values
-# * NAMELISTs and types of variables in them, and initial values
-#
-# Aliases by interface statement are described in the item of 'Methods'.
-#
-# Components which are imported from other modules and published again are
-# described in the item of 'Methods'.
-#
-# === Format of comment blocks
-#
-# Comment blocks should be written as follows.
-#
-# Comment blocks are considered to be ended when the line without '!' appears.
-#
-# The indentation is not necessary.
-#
-# ! (Top of file)
-# !
-# ! Comment blocks for the files.
-# !
-# !--
-# ! The comment described in the part enclosed by
-# ! "!--" and "!++" is ignored.
-# !++
-# !
-# module hogehoge
-# !
-# ! Comment blocks for the modules (or the programs).
-# !
-#
-# private
-#
-# logical :: a ! a private variable
-# real, public :: b ! a public variable
-# integer, parameter :: c = 0 ! a public constant
-#
-# public :: c
-# public :: MULTI_ARRAY
-# public :: hoge, foo
-#
-# type MULTI_ARRAY
-# !
-# ! Comment blocks for the derived-types.
-# !
-# real, pointer :: var(:) =>null() ! Comments block for the variables.
-# integer :: num = 0
-# end type MULTI_ARRAY
-#
-# contains
-#
-# subroutine hoge( in, & ! Comment blocks between continuation lines are ignored.
-# & out )
-# !
-# ! Comment blocks for the subroutines or functions
-# !
-# character(*),intent(in):: in ! Comment blocks for the arguments.
-# character(*),intent(out),allocatable,target :: in
-# ! Comment blocks can be
-# ! written under Fortran statements.
-#
-# character(32) :: file ! This comment parsed as a variable in below NAMELIST.
-# integer :: id
-#
-# namelist /varinfo_nml/ file, id
-# !
-# ! Comment blocks for the NAMELISTs.
-# ! Information about variables are described above.
-# !
-#
-# ....
-#
-# end subroutine hoge
-#
-# integer function foo( in )
-# !
-# ! This part is considered as comment block.
-#
-# ! Comment blocks under blank lines are ignored.
-# !
-# integer, intent(in):: inA ! This part is considered as comment block.
-#
-# ! This part is ignored.
-#
-# end function foo
-#
-# subroutine hide( in, &
-# & out ) !:nodoc:
-# !
-# ! If "!:nodoc:" is described at end-of-line in subroutine
-# ! statement as above, the subroutine is ignored.
-# ! This assignment can be used to modules, subroutines,
-# ! functions, variables, constants, derived-types,
-# ! defined operators, defined assignments,
-# ! list of imported modules ("use" statement).
-# !
-#
-# ....
-#
-# end subroutine hide
-#
-# end module hogehoge
-
-class RDoc::Parser::F95 < RDoc::Parser
-
- parse_files_matching(/\.((f|F)9(0|5)|F)$/)
-
- class Token
-
- NO_TEXT = "??".freeze
-
- def initialize(line_no, char_no)
- @line_no = line_no
- @char_no = char_no
- @text = NO_TEXT
- end
- # Because we're used in contexts that expect to return a token,
- # we set the text string and then return ourselves
- def set_text(text)
- @text = text
- self
- end
-
- attr_reader :line_no, :char_no, :text
-
- end
-
- @@external_aliases = []
- @@public_methods = []
-
- ##
- # "false":: Comments are below source code
- # "true" :: Comments are upper source code
-
- COMMENTS_ARE_UPPER = false
-
- ##
- # Internal alias message
-
- INTERNAL_ALIAS_MES = "Alias for"
-
- ##
- # External alias message
-
- EXTERNAL_ALIAS_MES = "The entity is"
-
- ##
- # Define code constructs
-
- def scan
- # remove private comment
- remaining_code = remove_private_comments(@content)
-
- # continuation lines are united to one line
- remaining_code = united_to_one_line(remaining_code)
-
- # semicolons are replaced to line feed
- remaining_code = semicolon_to_linefeed(remaining_code)
-
- # collect comment for file entity
- whole_comment, remaining_code = collect_first_comment(remaining_code)
- @top_level.comment = whole_comment
-
- # String "remaining_code" is converted to Array "remaining_lines"
- remaining_lines = remaining_code.split("\n")
-
- # "module" or "program" parts are parsed (new)
- #
- level_depth = 0
- block_searching_flag = nil
- block_searching_lines = []
- pre_comment = []
- module_program_trailing = ""
- module_program_name = ""
- other_block_level_depth = 0
- other_block_searching_flag = nil
- remaining_lines.collect!{|line|
- if !block_searching_flag && !other_block_searching_flag
- if line =~ /^\s*?module\s+(\w+)\s*?(!.*?)?$/i
- block_searching_flag = :module
- block_searching_lines << line
- module_program_name = $1
- module_program_trailing = find_comments($2)
- next false
- elsif line =~ /^\s*?program\s+(\w+)\s*?(!.*?)?$/i ||
- line =~ /^\s*?\w/ && !block_start?(line)
- block_searching_flag = :program
- block_searching_lines << line
- module_program_name = $1 || ""
- module_program_trailing = find_comments($2)
- next false
-
- elsif block_start?(line)
- other_block_searching_flag = true
- next line
-
- elsif line =~ /^\s*?!\s?(.*)/
- pre_comment << line
- next line
- else
- pre_comment = []
- next line
- end
- elsif other_block_searching_flag
- other_block_level_depth += 1 if block_start?(line)
- other_block_level_depth -= 1 if block_end?(line)
- if other_block_level_depth < 0
- other_block_level_depth = 0
- other_block_searching_flag = nil
- end
- next line
- end
-
- block_searching_lines << line
- level_depth += 1 if block_start?(line)
- level_depth -= 1 if block_end?(line)
- if level_depth >= 0
- next false
- end
-
- # "module_program_code" is formatted.
- # ":nodoc:" flag is checked.
- #
- module_program_code = block_searching_lines.join("\n")
- module_program_code = remove_empty_head_lines(module_program_code)
- if module_program_trailing =~ /^:nodoc:/
- # next loop to search next block
- level_depth = 0
- block_searching_flag = false
- block_searching_lines = []
- pre_comment = []
- next false
- end
-
- # NormalClass is created, and added to @top_level
- #
- if block_searching_flag == :module
- module_name = module_program_name
- module_code = module_program_code
- module_trailing = module_program_trailing
-
- f9x_module = @top_level.add_module NormalClass, module_name
- f9x_module.record_location @top_level
-
- @stats.add_module f9x_module
-
- f9x_comment = COMMENTS_ARE_UPPER ?
- find_comments(pre_comment.join("\n")) + "\n" + module_trailing :
- module_trailing + "\n" + find_comments(module_code.sub(/^.*$\n/i, ''))
- f9x_module.comment = f9x_comment
- parse_program_or_module(f9x_module, module_code)
-
- TopLevel.all_files.each do |name, toplevel|
- if toplevel.include_includes?(module_name, @options.ignore_case)
- if !toplevel.include_requires?(@file_name, @options.ignore_case)
- toplevel.add_require(Require.new(@file_name, ""))
- end
- end
- toplevel.each_classmodule{|m|
- if m.include_includes?(module_name, @options.ignore_case)
- if !m.include_requires?(@file_name, @options.ignore_case)
- m.add_require(Require.new(@file_name, ""))
- end
- end
- }
- end
- elsif block_searching_flag == :program
- program_name = module_program_name
- program_code = module_program_code
- program_trailing = module_program_trailing
- # progress "p" # HACK what stats thingy does this correspond to?
- program_comment = COMMENTS_ARE_UPPER ?
- find_comments(pre_comment.join("\n")) + "\n" + program_trailing :
- program_trailing + "\n" + find_comments(program_code.sub(/^.*$\n/i, ''))
- program_comment = "\n\n= <i>Program</i> <tt>#{program_name}</tt>\n\n" \
- + program_comment
- @top_level.comment << program_comment
- parse_program_or_module(@top_level, program_code, :private)
- end
-
- # next loop to search next block
- level_depth = 0
- block_searching_flag = false
- block_searching_lines = []
- pre_comment = []
- next false
- }
-
- remaining_lines.delete_if{ |line|
- line == false
- }
-
- # External subprograms and functions are parsed
- #
- parse_program_or_module(@top_level, remaining_lines.join("\n"),
- :public, true)
-
- @top_level
- end # End of scan
-
- private
-
- def parse_program_or_module(container, code,
- visibility=:public, external=nil)
- return unless container
- return unless code
- remaining_lines = code.split("\n")
- remaining_code = "#{code}"
-
- #
- # Parse variables before "contains" in module
- #
- level_depth = 0
- before_contains_lines = []
- before_contains_code = nil
- before_contains_flag = nil
- remaining_lines.each{ |line|
- if !before_contains_flag
- if line =~ /^\s*?module\s+\w+\s*?(!.*?)?$/i
- before_contains_flag = true
- end
- else
- break if line =~ /^\s*?contains\s*?(!.*?)?$/i
- level_depth += 1 if block_start?(line)
- level_depth -= 1 if block_end?(line)
- break if level_depth < 0
- before_contains_lines << line
- end
- }
- before_contains_code = before_contains_lines.join("\n")
- if before_contains_code
- before_contains_code.gsub!(/^\s*?interface\s+.*?\s+end\s+interface.*?$/im, "")
- before_contains_code.gsub!(/^\s*?type[\s\,]+.*?\s+end\s+type.*?$/im, "")
- end
-
- #
- # Parse global "use"
- #
- use_check_code = "#{before_contains_code}"
- cascaded_modules_list = []
- while use_check_code =~ /^\s*?use\s+(\w+)(.*?)(!.*?)?$/i
- use_check_code = $~.pre_match
- use_check_code << $~.post_match
- used_mod_name = $1.strip.chomp
- used_list = $2 || ""
- used_trailing = $3 || ""
- next if used_trailing =~ /!:nodoc:/
- if !container.include_includes?(used_mod_name, @options.ignore_case)
- # progress "." # HACK what stats thingy does this correspond to?
- container.add_include Include.new(used_mod_name, "")
- end
- if ! (used_list =~ /\,\s*?only\s*?:/i )
- cascaded_modules_list << "\#" + used_mod_name
- end
- end
-
- #
- # Parse public and private, and store information.
- # This information is used when "add_method" and
- # "set_visibility_for" are called.
- #
- visibility_default, visibility_info =
- parse_visibility(remaining_lines.join("\n"), visibility, container)
- @@public_methods.concat visibility_info
- if visibility_default == :public
- if !cascaded_modules_list.empty?
- cascaded_modules =
- Attr.new("Cascaded Modules",
- "Imported modules all of whose components are published again",
- "",
- cascaded_modules_list.join(", "))
- container.add_attribute(cascaded_modules)
- end
- end
-
- #
- # Check rename elements
- #
- use_check_code = "#{before_contains_code}"
- while use_check_code =~ /^\s*?use\s+(\w+)\s*?\,(.+)$/i
- use_check_code = $~.pre_match
- use_check_code << $~.post_match
- used_mod_name = $1.strip.chomp
- used_elements = $2.sub(/\s*?only\s*?:\s*?/i, '')
- used_elements.split(",").each{ |used|
- if /\s*?(\w+)\s*?=>\s*?(\w+)\s*?/ =~ used
- local = $1
- org = $2
- @@public_methods.collect!{ |pub_meth|
- if local == pub_meth["name"] ||
- local.upcase == pub_meth["name"].upcase &&
- @options.ignore_case
- pub_meth["name"] = org
- pub_meth["local_name"] = local
- end
- pub_meth
- }
- end
- }
- end
-
- #
- # Parse private "use"
- #
- use_check_code = remaining_lines.join("\n")
- while use_check_code =~ /^\s*?use\s+(\w+)(.*?)(!.*?)?$/i
- use_check_code = $~.pre_match
- use_check_code << $~.post_match
- used_mod_name = $1.strip.chomp
- used_trailing = $3 || ""
- next if used_trailing =~ /!:nodoc:/
- if !container.include_includes?(used_mod_name, @options.ignore_case)
- # progress "." # HACK what stats thingy does this correspond to?
- container.add_include Include.new(used_mod_name, "")
- end
- end
-
- container.each_includes{ |inc|
- TopLevel.all_files.each do |name, toplevel|
- indicated_mod = toplevel.find_symbol(inc.name,
- nil, @options.ignore_case)
- if indicated_mod
- indicated_name = indicated_mod.parent.file_relative_name
- if !container.include_requires?(indicated_name, @options.ignore_case)
- container.add_require(Require.new(indicated_name, ""))
- end
- break
- end
- end
- }
-
- #
- # Parse derived-types definitions
- #
- derived_types_comment = ""
- remaining_code = remaining_lines.join("\n")
- while remaining_code =~ /^\s*?
- type[\s\,]+(public|private)?\s*?(::)?\s*?
- (\w+)\s*?(!.*?)?$
- (.*?)
- ^\s*?end\s+type.*?$
- /imx
- remaining_code = $~.pre_match
- remaining_code << $~.post_match
- typename = $3.chomp.strip
- type_elements = $5 || ""
- type_code = remove_empty_head_lines($&)
- type_trailing = find_comments($4)
- next if type_trailing =~ /^:nodoc:/
- type_visibility = $1
- type_comment = COMMENTS_ARE_UPPER ?
- find_comments($~.pre_match) + "\n" + type_trailing :
- type_trailing + "\n" + find_comments(type_code.sub(/^.*$\n/i, ''))
- type_element_visibility_public = true
- type_code.split("\n").each{ |line|
- if /^\s*?private\s*?$/ =~ line
- type_element_visibility_public = nil
- break
- end
- } if type_code
-
- args_comment = ""
- type_args_info = nil
-
- if @options.show_all
- args_comment = find_arguments(nil, type_code, true)
- else
- type_public_args_list = []
- type_args_info = definition_info(type_code)
- type_args_info.each{ |arg|
- arg_is_public = type_element_visibility_public
- arg_is_public = true if arg.include_attr?("public")
- arg_is_public = nil if arg.include_attr?("private")
- type_public_args_list << arg.varname if arg_is_public
- }
- args_comment = find_arguments(type_public_args_list, type_code)
- end
-
- type = AnyMethod.new("type #{typename}", typename)
- type.singleton = false
- type.params = ""
- type.comment = "<b><em> Derived Type </em></b> :: <tt></tt>\n"
- type.comment << args_comment if args_comment
- type.comment << type_comment if type_comment
-
- @stats.add_method type
-
- container.add_method type
-
- set_visibility(container, typename, visibility_default, @@public_methods)
-
- if type_visibility
- type_visibility.gsub!(/\s/,'')
- type_visibility.gsub!(/\,/,'')
- type_visibility.gsub!(/:/,'')
- type_visibility.downcase!
- if type_visibility == "public"
- container.set_visibility_for([typename], :public)
- elsif type_visibility == "private"
- container.set_visibility_for([typename], :private)
- end
- end
-
- check_public_methods(type, container.name)
-
- if @options.show_all
- derived_types_comment << ", " unless derived_types_comment.empty?
- derived_types_comment << typename
- else
- if type.visibility == :public
- derived_types_comment << ", " unless derived_types_comment.empty?
- derived_types_comment << typename
- end
- end
-
- end
-
- if !derived_types_comment.empty?
- derived_types_table =
- Attr.new("Derived Types", "Derived_Types", "",
- derived_types_comment)
- container.add_attribute(derived_types_table)
- end
-
- #
- # move interface scope
- #
- interface_code = ""
- while remaining_code =~ /^\s*?
- interface(
- \s+\w+ |
- \s+operator\s*?\(.*?\) |
- \s+assignment\s*?\(\s*?=\s*?\)
- )?\s*?$
- (.*?)
- ^\s*?end\s+interface.*?$
- /imx
- interface_code << remove_empty_head_lines($&) + "\n"
- remaining_code = $~.pre_match
- remaining_code << $~.post_match
- end
-
- #
- # Parse global constants or variables in modules
- #
- const_var_defs = definition_info(before_contains_code)
- const_var_defs.each{|defitem|
- next if defitem.nodoc
- const_or_var_type = "Variable"
- const_or_var_progress = "v"
- if defitem.include_attr?("parameter")
- const_or_var_type = "Constant"
- const_or_var_progress = "c"
- end
- const_or_var = AnyMethod.new(const_or_var_type, defitem.varname)
- const_or_var.singleton = false
- const_or_var.params = ""
- self_comment = find_arguments([defitem.varname], before_contains_code)
- const_or_var.comment = "<b><em>" + const_or_var_type + "</em></b> :: <tt></tt>\n"
- const_or_var.comment << self_comment if self_comment
-
- @stats.add_method const_or_var_progress
-
- container.add_method const_or_var
-
- set_visibility(container, defitem.varname, visibility_default, @@public_methods)
-
- if defitem.include_attr?("public")
- container.set_visibility_for([defitem.varname], :public)
- elsif defitem.include_attr?("private")
- container.set_visibility_for([defitem.varname], :private)
- end
-
- check_public_methods(const_or_var, container.name)
-
- } if const_var_defs
-
- remaining_lines = remaining_code.split("\n")
-
- # "subroutine" or "function" parts are parsed (new)
- #
- level_depth = 0
- block_searching_flag = nil
- block_searching_lines = []
- pre_comment = []
- procedure_trailing = ""
- procedure_name = ""
- procedure_params = ""
- procedure_prefix = ""
- procedure_result_arg = ""
- procedure_type = ""
- contains_lines = []
- contains_flag = nil
- remaining_lines.collect!{|line|
- if !block_searching_flag
- # subroutine
- if line =~ /^\s*?
- (recursive|pure|elemental)?\s*?
- subroutine\s+(\w+)\s*?(\(.*?\))?\s*?(!.*?)?$
- /ix
- block_searching_flag = :subroutine
- block_searching_lines << line
-
- procedure_name = $2.chomp.strip
- procedure_params = $3 || ""
- procedure_prefix = $1 || ""
- procedure_trailing = $4 || "!"
- next false
-
- # function
- elsif line =~ /^\s*?
- (recursive|pure|elemental)?\s*?
- (
- character\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- | type\s*?\([\w\s]+?\)\s+
- | integer\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- | real\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- | double\s+precision\s+
- | logical\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- | complex\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- )?
- function\s+(\w+)\s*?
- (\(.*?\))?(\s+result\((.*?)\))?\s*?(!.*?)?$
- /ix
- block_searching_flag = :function
- block_searching_lines << line
-
- procedure_prefix = $1 || ""
- procedure_type = $2 ? $2.chomp.strip : nil
- procedure_name = $8.chomp.strip
- procedure_params = $9 || ""
- procedure_result_arg = $11 ? $11.chomp.strip : procedure_name
- procedure_trailing = $12 || "!"
- next false
- elsif line =~ /^\s*?!\s?(.*)/
- pre_comment << line
- next line
- else
- pre_comment = []
- next line
- end
- end
- contains_flag = true if line =~ /^\s*?contains\s*?(!.*?)?$/
- block_searching_lines << line
- contains_lines << line if contains_flag
-
- level_depth += 1 if block_start?(line)
- level_depth -= 1 if block_end?(line)
- if level_depth >= 0
- next false
- end
-
- # "procedure_code" is formatted.
- # ":nodoc:" flag is checked.
- #
- procedure_code = block_searching_lines.join("\n")
- procedure_code = remove_empty_head_lines(procedure_code)
- if procedure_trailing =~ /^!:nodoc:/
- # next loop to search next block
- level_depth = 0
- block_searching_flag = nil
- block_searching_lines = []
- pre_comment = []
- procedure_trailing = ""
- procedure_name = ""
- procedure_params = ""
- procedure_prefix = ""
- procedure_result_arg = ""
- procedure_type = ""
- contains_lines = []
- contains_flag = nil
- next false
- end
-
- # AnyMethod is created, and added to container
- #
- subroutine_function = nil
- if block_searching_flag == :subroutine
- subroutine_prefix = procedure_prefix
- subroutine_name = procedure_name
- subroutine_params = procedure_params
- subroutine_trailing = procedure_trailing
- subroutine_code = procedure_code
-
- subroutine_comment = COMMENTS_ARE_UPPER ?
- pre_comment.join("\n") + "\n" + subroutine_trailing :
- subroutine_trailing + "\n" + subroutine_code.sub(/^.*$\n/i, '')
- subroutine = AnyMethod.new("subroutine", subroutine_name)
- parse_subprogram(subroutine, subroutine_params,
- subroutine_comment, subroutine_code,
- before_contains_code, nil, subroutine_prefix)
-
- @stats.add_method subroutine
-
- container.add_method subroutine
- subroutine_function = subroutine
-
- elsif block_searching_flag == :function
- function_prefix = procedure_prefix
- function_type = procedure_type
- function_name = procedure_name
- function_params_org = procedure_params
- function_result_arg = procedure_result_arg
- function_trailing = procedure_trailing
- function_code_org = procedure_code
-
- function_comment = COMMENTS_ARE_UPPER ?
- pre_comment.join("\n") + "\n" + function_trailing :
- function_trailing + "\n " + function_code_org.sub(/^.*$\n/i, '')
-
- function_code = "#{function_code_org}"
- if function_type
- function_code << "\n" + function_type + " :: " + function_result_arg
- end
-
- function_params =
- function_params_org.sub(/^\(/, "\(#{function_result_arg}, ")
-
- function = AnyMethod.new("function", function_name)
- parse_subprogram(function, function_params,
- function_comment, function_code,
- before_contains_code, true, function_prefix)
-
- # Specific modification due to function
- function.params.sub!(/\(\s*?#{function_result_arg}\s*?,\s*?/, "\( ")
- function.params << " result(" + function_result_arg + ")"
- function.start_collecting_tokens
- function.add_token Token.new(1,1).set_text(function_code_org)
-
- @stats.add_method function
-
- container.add_method function
- subroutine_function = function
-
- end
-
- # The visibility of procedure is specified
- #
- set_visibility(container, procedure_name,
- visibility_default, @@public_methods)
-
- # The alias for this procedure from external modules
- #
- check_external_aliases(procedure_name,
- subroutine_function.params,
- subroutine_function.comment, subroutine_function) if external
- check_public_methods(subroutine_function, container.name)
-
-
- # contains_lines are parsed as private procedures
- if contains_flag
- parse_program_or_module(container,
- contains_lines.join("\n"), :private)
- end
-
- # next loop to search next block
- level_depth = 0
- block_searching_flag = nil
- block_searching_lines = []
- pre_comment = []
- procedure_trailing = ""
- procedure_name = ""
- procedure_params = ""
- procedure_prefix = ""
- procedure_result_arg = ""
- contains_lines = []
- contains_flag = nil
- next false
- } # End of remaining_lines.collect!{|line|
-
- # Array remains_lines is converted to String remains_code again
- #
- remaining_code = remaining_lines.join("\n")
-
- #
- # Parse interface
- #
- interface_scope = false
- generic_name = ""
- interface_code.split("\n").each{ |line|
- if /^\s*?
- interface(
- \s+\w+|
- \s+operator\s*?\(.*?\)|
- \s+assignment\s*?\(\s*?=\s*?\)
- )?
- \s*?(!.*?)?$
- /ix =~ line
- generic_name = $1 ? $1.strip.chomp : nil
- interface_trailing = $2 || "!"
- interface_scope = true
- interface_scope = false if interface_trailing =~ /!:nodoc:/
-# if generic_name =~ /operator\s*?\((.*?)\)/i
-# operator_name = $1
-# if operator_name && !operator_name.empty?
-# generic_name = "#{operator_name}"
-# end
-# end
-# if generic_name =~ /assignment\s*?\((.*?)\)/i
-# assignment_name = $1
-# if assignment_name && !assignment_name.empty?
-# generic_name = "#{assignment_name}"
-# end
-# end
- end
- if /^\s*?end\s+interface/i =~ line
- interface_scope = false
- generic_name = nil
- end
- # internal alias
- if interface_scope && /^\s*?module\s+procedure\s+(.*?)(!.*?)?$/i =~ line
- procedures = $1.strip.chomp
- procedures_trailing = $2 || "!"
- next if procedures_trailing =~ /!:nodoc:/
- procedures.split(",").each{ |proc|
- proc.strip!
- proc.chomp!
- next if generic_name == proc || !generic_name
- old_meth = container.find_symbol(proc, nil, @options.ignore_case)
- next if !old_meth
- nolink = old_meth.visibility == :private ? true : nil
- nolink = nil if @options.show_all
- new_meth =
- initialize_external_method(generic_name, proc,
- old_meth.params, nil,
- old_meth.comment,
- old_meth.clone.token_stream[0].text,
- true, nolink)
- new_meth.singleton = old_meth.singleton
-
- @stats.add_method new_meth
-
- container.add_method new_meth
-
- set_visibility(container, generic_name, visibility_default, @@public_methods)
-
- check_public_methods(new_meth, container.name)
-
- }
- end
-
- # external aliases
- if interface_scope
- # subroutine
- proc = nil
- params = nil
- procedures_trailing = nil
- if line =~ /^\s*?
- (recursive|pure|elemental)?\s*?
- subroutine\s+(\w+)\s*?(\(.*?\))?\s*?(!.*?)?$
- /ix
- proc = $2.chomp.strip
- generic_name = proc unless generic_name
- params = $3 || ""
- procedures_trailing = $4 || "!"
-
- # function
- elsif line =~ /^\s*?
- (recursive|pure|elemental)?\s*?
- (
- character\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- | type\s*?\([\w\s]+?\)\s+
- | integer\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- | real\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- | double\s+precision\s+
- | logical\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- | complex\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- )?
- function\s+(\w+)\s*?
- (\(.*?\))?(\s+result\((.*?)\))?\s*?(!.*?)?$
- /ix
- proc = $8.chomp.strip
- generic_name = proc unless generic_name
- params = $9 || ""
- procedures_trailing = $12 || "!"
- else
- next
- end
- next if procedures_trailing =~ /!:nodoc:/
- indicated_method = nil
- indicated_file = nil
- TopLevel.all_files.each do |name, toplevel|
- indicated_method = toplevel.find_local_symbol(proc, @options.ignore_case)
- indicated_file = name
- break if indicated_method
- end
-
- if indicated_method
- external_method =
- initialize_external_method(generic_name, proc,
- indicated_method.params,
- indicated_file,
- indicated_method.comment)
-
- @stats.add_method external_method
-
- container.add_method external_method
- set_visibility(container, generic_name, visibility_default, @@public_methods)
- if !container.include_requires?(indicated_file, @options.ignore_case)
- container.add_require(Require.new(indicated_file, ""))
- end
- check_public_methods(external_method, container.name)
-
- else
- @@external_aliases << {
- "new_name" => generic_name,
- "old_name" => proc,
- "file_or_module" => container,
- "visibility" => find_visibility(container, generic_name, @@public_methods) || visibility_default
- }
- end
- end
-
- } if interface_code # End of interface_code.split("\n").each ...
-
- #
- # Already imported methods are removed from @@public_methods.
- # Remainders are assumed to be imported from other modules.
- #
- @@public_methods.delete_if{ |method| method["entity_is_discovered"]}
-
- @@public_methods.each{ |pub_meth|
- next unless pub_meth["file_or_module"].name == container.name
- pub_meth["used_modules"].each{ |used_mod|
- TopLevel.all_classes_and_modules.each{ |modules|
- if modules.name == used_mod ||
- modules.name.upcase == used_mod.upcase &&
- @options.ignore_case
- modules.method_list.each{ |meth|
- if meth.name == pub_meth["name"] ||
- meth.name.upcase == pub_meth["name"].upcase &&
- @options.ignore_case
- new_meth = initialize_public_method(meth,
- modules.name)
- if pub_meth["local_name"]
- new_meth.name = pub_meth["local_name"]
- end
-
- @stats.add_method new_meth
-
- container.add_method new_meth
- end
- }
- end
- }
- }
- }
-
- container
- end # End of parse_program_or_module
-
- ##
- # Parse arguments, comment, code of subroutine and function. Return
- # AnyMethod object.
-
- def parse_subprogram(subprogram, params, comment, code,
- before_contains=nil, function=nil, prefix=nil)
- subprogram.singleton = false
- prefix = "" if !prefix
- arguments = params.sub(/\(/, "").sub(/\)/, "").split(",") if params
- args_comment, params_opt =
- find_arguments(arguments, code.sub(/^s*?contains\s*?(!.*?)?$.*/im, ""),
- nil, nil, true)
- params_opt = "( " + params_opt + " ) " if params_opt
- subprogram.params = params_opt || ""
- namelist_comment = find_namelists(code, before_contains)
-
- block_comment = find_comments comment
- if function
- subprogram.comment = "<b><em> Function </em></b> :: <em>#{prefix}</em>\n"
- else
- subprogram.comment = "<b><em> Subroutine </em></b> :: <em>#{prefix}</em>\n"
- end
- subprogram.comment << args_comment if args_comment
- subprogram.comment << block_comment if block_comment
- subprogram.comment << namelist_comment if namelist_comment
-
- # For output source code
- subprogram.start_collecting_tokens
- subprogram.add_token Token.new(1,1).set_text(code)
-
- subprogram
- end
-
- ##
- # Collect comment for file entity
-
- def collect_first_comment(body)
- comment = ""
- not_comment = ""
- comment_start = false
- comment_end = false
- body.split("\n").each{ |line|
- if comment_end
- not_comment << line
- not_comment << "\n"
- elsif /^\s*?!\s?(.*)$/i =~ line
- comment_start = true
- comment << $1
- comment << "\n"
- elsif /^\s*?$/i =~ line
- comment_end = true if comment_start && COMMENTS_ARE_UPPER
- else
- comment_end = true
- not_comment << line
- not_comment << "\n"
- end
- }
- return comment, not_comment
- end
-
-
- ##
- # Return comments of definitions of arguments
- #
- # If "all" argument is true, information of all arguments are returned.
- #
- # If "modified_params" is true, list of arguments are decorated, for
- # example, optional arguments are parenthetic as "[arg]".
-
- def find_arguments(args, text, all=nil, indent=nil, modified_params=nil)
- return unless args || all
- indent = "" unless indent
- args = ["all"] if all
- params = "" if modified_params
- comma = ""
- return unless text
- args_rdocforms = "\n"
- remaining_lines = "#{text}"
- definitions = definition_info(remaining_lines)
- args.each{ |arg|
- arg.strip!
- arg.chomp!
- definitions.each { |defitem|
- if arg == defitem.varname.strip.chomp || all
- args_rdocforms << <<-"EOF"
-
-#{indent}<tt><b>#{defitem.varname.chomp.strip}#{defitem.arraysuffix}</b> #{defitem.inivalue}</tt> ::
-#{indent} <tt>#{defitem.types.chomp.strip}</tt>
-EOF
- if !defitem.comment.chomp.strip.empty?
- comment = ""
- defitem.comment.split("\n").each{ |line|
- comment << " " + line + "\n"
- }
- args_rdocforms << <<-"EOF"
-
-#{indent} <tt></tt> ::
-#{indent} <tt></tt>
-#{indent} #{comment.chomp.strip}
-EOF
- end
-
- if modified_params
- if defitem.include_attr?("optional")
- params << "#{comma}[#{arg}]"
- else
- params << "#{comma}#{arg}"
- end
- comma = ", "
- end
- end
- }
- }
- if modified_params
- return args_rdocforms, params
- else
- return args_rdocforms
- end
- end
-
- ##
- # Return comments of definitions of namelists
-
- def find_namelists(text, before_contains=nil)
- return nil if !text
- result = ""
- lines = "#{text}"
- before_contains = "" if !before_contains
- while lines =~ /^\s*?namelist\s+\/\s*?(\w+)\s*?\/([\s\w\,]+)$/i
- lines = $~.post_match
- nml_comment = COMMENTS_ARE_UPPER ?
- find_comments($~.pre_match) : find_comments($~.post_match)
- nml_name = $1
- nml_args = $2.split(",")
- result << "\n\n=== NAMELIST <tt><b>" + nml_name + "</tt></b>\n\n"
- result << nml_comment + "\n" if nml_comment
- if lines.split("\n")[0] =~ /^\//i
- lines = "namelist " + lines
- end
- result << find_arguments(nml_args, "#{text}" + "\n" + before_contains)
- end
- return result
- end
-
- ##
- # Comments just after module or subprogram, or arguments are returned. If
- # "COMMENTS_ARE_UPPER" is true, comments just before modules or subprograms
- # are returnd
-
- def find_comments text
- return "" unless text
- lines = text.split("\n")
- lines.reverse! if COMMENTS_ARE_UPPER
- comment_block = Array.new
- lines.each do |line|
- break if line =~ /^\s*?\w/ || line =~ /^\s*?$/
- if COMMENTS_ARE_UPPER
- comment_block.unshift line.sub(/^\s*?!\s?/,"")
- else
- comment_block.push line.sub(/^\s*?!\s?/,"")
- end
- end
- nice_lines = comment_block.join("\n").split "\n\s*?\n"
- nice_lines[0] ||= ""
- nice_lines.shift
- end
-
- ##
- # Create method for internal alias
-
- def initialize_public_method(method, parent)
- return if !method || !parent
-
- new_meth = AnyMethod.new("External Alias for module", method.name)
- new_meth.singleton = method.singleton
- new_meth.params = method.params.clone
- new_meth.comment = remove_trailing_alias(method.comment.clone)
- new_meth.comment << "\n\n#{EXTERNAL_ALIAS_MES} #{parent.strip.chomp}\##{method.name}"
-
- return new_meth
- end
-
- ##
- # Create method for external alias
- #
- # If argument "internal" is true, file is ignored.
-
- def initialize_external_method(new, old, params, file, comment, token=nil,
- internal=nil, nolink=nil)
- return nil unless new || old
-
- if internal
- external_alias_header = "#{INTERNAL_ALIAS_MES} "
- external_alias_text = external_alias_header + old
- elsif file
- external_alias_header = "#{EXTERNAL_ALIAS_MES} "
- external_alias_text = external_alias_header + file + "#" + old
- else
- return nil
- end
- external_meth = AnyMethod.new(external_alias_text, new)
- external_meth.singleton = false
- external_meth.params = params
- external_comment = remove_trailing_alias(comment) + "\n\n" if comment
- external_meth.comment = external_comment || ""
- if nolink && token
- external_meth.start_collecting_tokens
- external_meth.add_token Token.new(1,1).set_text(token)
- else
- external_meth.comment << external_alias_text
- end
-
- return external_meth
- end
-
- ##
- # Parse visibility
-
- def parse_visibility(code, default, container)
- result = []
- visibility_default = default || :public
-
- used_modules = []
- container.includes.each{|i| used_modules << i.name} if container
-
- remaining_code = code.gsub(/^\s*?type[\s\,]+.*?\s+end\s+type.*?$/im, "")
- remaining_code.split("\n").each{ |line|
- if /^\s*?private\s*?$/ =~ line
- visibility_default = :private
- break
- end
- } if remaining_code
-
- remaining_code.split("\n").each{ |line|
- if /^\s*?private\s*?(::)?\s+(.*)\s*?(!.*?)?/i =~ line
- methods = $2.sub(/!.*$/, '')
- methods.split(",").each{ |meth|
- meth.sub!(/!.*$/, '')
- meth.gsub!(/:/, '')
- result << {
- "name" => meth.chomp.strip,
- "visibility" => :private,
- "used_modules" => used_modules.clone,
- "file_or_module" => container,
- "entity_is_discovered" => nil,
- "local_name" => nil
- }
- }
- elsif /^\s*?public\s*?(::)?\s+(.*)\s*?(!.*?)?/i =~ line
- methods = $2.sub(/!.*$/, '')
- methods.split(",").each{ |meth|
- meth.sub!(/!.*$/, '')
- meth.gsub!(/:/, '')
- result << {
- "name" => meth.chomp.strip,
- "visibility" => :public,
- "used_modules" => used_modules.clone,
- "file_or_module" => container,
- "entity_is_discovered" => nil,
- "local_name" => nil
- }
- }
- end
- } if remaining_code
-
- if container
- result.each{ |vis_info|
- vis_info["parent"] = container.name
- }
- end
-
- return visibility_default, result
- end
-
- ##
- # Set visibility
- #
- # "subname" element of "visibility_info" is deleted.
-
- def set_visibility(container, subname, visibility_default, visibility_info)
- return unless container || subname || visibility_default || visibility_info
- not_found = true
- visibility_info.collect!{ |info|
- if info["name"] == subname ||
- @options.ignore_case && info["name"].upcase == subname.upcase
- if info["file_or_module"].name == container.name
- container.set_visibility_for([subname], info["visibility"])
- info["entity_is_discovered"] = true
- not_found = false
- end
- end
- info
- }
- if not_found
- return container.set_visibility_for([subname], visibility_default)
- else
- return container
- end
- end
-
- ##
- # Find visibility
-
- def find_visibility(container, subname, visibility_info)
- return nil if !subname || !visibility_info
- visibility_info.each{ |info|
- if info["name"] == subname ||
- @options.ignore_case && info["name"].upcase == subname.upcase
- if info["parent"] == container.name
- return info["visibility"]
- end
- end
- }
- return nil
- end
-
- ##
- # Check external aliases
-
- def check_external_aliases(subname, params, comment, test=nil)
- @@external_aliases.each{ |alias_item|
- if subname == alias_item["old_name"] ||
- subname.upcase == alias_item["old_name"].upcase &&
- @options.ignore_case
-
- new_meth = initialize_external_method(alias_item["new_name"],
- subname, params, @file_name,
- comment)
- new_meth.visibility = alias_item["visibility"]
-
- @stats.add_method new_meth
-
- alias_item["file_or_module"].add_method(new_meth)
-
- if !alias_item["file_or_module"].include_requires?(@file_name, @options.ignore_case)
- alias_item["file_or_module"].add_require(Require.new(@file_name, ""))
- end
- end
- }
- end
-
- ##
- # Check public_methods
-
- def check_public_methods(method, parent)
- return if !method || !parent
- @@public_methods.each{ |alias_item|
- parent_is_used_module = nil
- alias_item["used_modules"].each{ |used_module|
- if used_module == parent ||
- used_module.upcase == parent.upcase &&
- @options.ignore_case
- parent_is_used_module = true
- end
- }
- next if !parent_is_used_module
-
- if method.name == alias_item["name"] ||
- method.name.upcase == alias_item["name"].upcase &&
- @options.ignore_case
-
- new_meth = initialize_public_method(method, parent)
- if alias_item["local_name"]
- new_meth.name = alias_item["local_name"]
- end
-
- @stats.add_method new_meth
-
- alias_item["file_or_module"].add_method new_meth
- end
- }
- end
-
- ##
- # Continuous lines are united.
- #
- # Comments in continuous lines are removed.
-
- def united_to_one_line(f90src)
- return "" unless f90src
- lines = f90src.split("\n")
- previous_continuing = false
- now_continuing = false
- body = ""
- lines.each{ |line|
- words = line.split("")
- next if words.empty? && previous_continuing
- commentout = false
- brank_flag = true ; brank_char = ""
- squote = false ; dquote = false
- ignore = false
- words.collect! { |char|
- if previous_continuing && brank_flag
- now_continuing = true
- ignore = true
- case char
- when "!" ; break
- when " " ; brank_char << char ; next ""
- when "&"
- brank_flag = false
- now_continuing = false
- next ""
- else
- brank_flag = false
- now_continuing = false
- ignore = false
- next brank_char + char
- end
- end
- ignore = false
-
- if now_continuing
- next ""
- elsif !(squote) && !(dquote) && !(commentout)
- case char
- when "!" ; commentout = true ; next char
- when "\""; dquote = true ; next char
- when "\'"; squote = true ; next char
- when "&" ; now_continuing = true ; next ""
- else next char
- end
- elsif commentout
- next char
- elsif squote
- case char
- when "\'"; squote = false ; next char
- else next char
- end
- elsif dquote
- case char
- when "\""; dquote = false ; next char
- else next char
- end
- end
- }
- if !ignore && !previous_continuing || !brank_flag
- if previous_continuing
- body << words.join("")
- else
- body << "\n" + words.join("")
- end
- end
- previous_continuing = now_continuing ? true : nil
- now_continuing = nil
- }
- return body
- end
-
-
- ##
- # Continuous line checker
-
- def continuous_line?(line)
- continuous = false
- if /&\s*?(!.*)?$/ =~ line
- continuous = true
- if comment_out?($~.pre_match)
- continuous = false
- end
- end
- return continuous
- end
-
- ##
- # Comment out checker
-
- def comment_out?(line)
- return nil unless line
- commentout = false
- squote = false ; dquote = false
- line.split("").each { |char|
- if !(squote) && !(dquote)
- case char
- when "!" ; commentout = true ; break
- when "\""; dquote = true
- when "\'"; squote = true
- else next
- end
- elsif squote
- case char
- when "\'"; squote = false
- else next
- end
- elsif dquote
- case char
- when "\""; dquote = false
- else next
- end
- end
- }
- return commentout
- end
-
- ##
- # Semicolons are replaced to line feed.
-
- def semicolon_to_linefeed(text)
- return "" unless text
- lines = text.split("\n")
- lines.collect!{ |line|
- words = line.split("")
- commentout = false
- squote = false ; dquote = false
- words.collect! { |char|
- if !(squote) && !(dquote) && !(commentout)
- case char
- when "!" ; commentout = true ; next char
- when "\""; dquote = true ; next char
- when "\'"; squote = true ; next char
- when ";" ; "\n"
- else next char
- end
- elsif commentout
- next char
- elsif squote
- case char
- when "\'"; squote = false ; next char
- else next char
- end
- elsif dquote
- case char
- when "\""; dquote = false ; next char
- else next char
- end
- end
- }
- words.join("")
- }
- return lines.join("\n")
- end
-
- ##
- # Which "line" is start of block (module, program, block data, subroutine,
- # function) statement ?
-
- def block_start?(line)
- return nil if !line
-
- if line =~ /^\s*?module\s+(\w+)\s*?(!.*?)?$/i ||
- line =~ /^\s*?program\s+(\w+)\s*?(!.*?)?$/i ||
- line =~ /^\s*?block\s+data(\s+\w+)?\s*?(!.*?)?$/i ||
- line =~ \
- /^\s*?
- (recursive|pure|elemental)?\s*?
- subroutine\s+(\w+)\s*?(\(.*?\))?\s*?(!.*?)?$
- /ix ||
- line =~ \
- /^\s*?
- (recursive|pure|elemental)?\s*?
- (
- character\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- | type\s*?\([\w\s]+?\)\s+
- | integer\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- | real\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- | double\s+precision\s+
- | logical\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- | complex\s*?(\([\w\s\=\(\)\*]+?\))?\s+
- )?
- function\s+(\w+)\s*?
- (\(.*?\))?(\s+result\((.*?)\))?\s*?(!.*?)?$
- /ix
- return true
- end
-
- return nil
- end
-
- ##
- # Which "line" is end of block (module, program, block data, subroutine,
- # function) statement ?
-
- def block_end?(line)
- return nil if !line
-
- if line =~ /^\s*?end\s*?(!.*?)?$/i ||
- line =~ /^\s*?end\s+module(\s+\w+)?\s*?(!.*?)?$/i ||
- line =~ /^\s*?end\s+program(\s+\w+)?\s*?(!.*?)?$/i ||
- line =~ /^\s*?end\s+block\s+data(\s+\w+)?\s*?(!.*?)?$/i ||
- line =~ /^\s*?end\s+subroutine(\s+\w+)?\s*?(!.*?)?$/i ||
- line =~ /^\s*?end\s+function(\s+\w+)?\s*?(!.*?)?$/i
- return true
- end
-
- return nil
- end
-
- ##
- # Remove "Alias for" in end of comments
-
- def remove_trailing_alias(text)
- return "" if !text
- lines = text.split("\n").reverse
- comment_block = Array.new
- checked = false
- lines.each do |line|
- if !checked
- if /^\s?#{INTERNAL_ALIAS_MES}/ =~ line ||
- /^\s?#{EXTERNAL_ALIAS_MES}/ =~ line
- checked = true
- next
- end
- end
- comment_block.unshift line
- end
- nice_lines = comment_block.join("\n")
- nice_lines ||= ""
- return nice_lines
- end
-
- ##
- # Empty lines in header are removed
-
- def remove_empty_head_lines(text)
- return "" unless text
- lines = text.split("\n")
- header = true
- lines.delete_if{ |line|
- header = false if /\S/ =~ line
- header && /^\s*?$/ =~ line
- }
- lines.join("\n")
- end
-
- ##
- # header marker "=", "==", ... are removed
-
- def remove_header_marker(text)
- return text.gsub(/^\s?(=+)/, '<tt></tt>\1')
- end
-
- def remove_private_comments(body)
- body.gsub!(/^\s*!--\s*?$.*?^\s*!\+\+\s*?$/m, '')
- return body
- end
-
- ##
- # Information of arguments of subroutines and functions in Fortran95
-
- class Fortran95Definition
-
- # Name of variable
- #
- attr_reader :varname
-
- # Types of variable
- #
- attr_reader :types
-
- # Initial Value
- #
- attr_reader :inivalue
-
- # Suffix of array
- #
- attr_reader :arraysuffix
-
- # Comments
- #
- attr_accessor :comment
-
- # Flag of non documentation
- #
- attr_accessor :nodoc
-
- def initialize(varname, types, inivalue, arraysuffix, comment,
- nodoc=false)
- @varname = varname
- @types = types
- @inivalue = inivalue
- @arraysuffix = arraysuffix
- @comment = comment
- @nodoc = nodoc
- end
-
- def to_s
- return <<-EOF
-<Fortran95Definition:
-varname=#{@varname}, types=#{types},
-inivalue=#{@inivalue}, arraysuffix=#{@arraysuffix}, nodoc=#{@nodoc},
-comment=
-#{@comment}
->
-EOF
- end
-
- #
- # If attr is included, true is returned
- #
- def include_attr?(attr)
- return if !attr
- @types.split(",").each{ |type|
- return true if type.strip.chomp.upcase == attr.strip.chomp.upcase
- }
- return nil
- end
-
- end # End of Fortran95Definition
-
- ##
- # Parse string argument "text", and Return Array of Fortran95Definition
- # object
-
- def definition_info(text)
- return nil unless text
- lines = "#{text}"
- defs = Array.new
- comment = ""
- trailing_comment = ""
- under_comment_valid = false
- lines.split("\n").each{ |line|
- if /^\s*?!\s?(.*)/ =~ line
- if COMMENTS_ARE_UPPER
- comment << remove_header_marker($1)
- comment << "\n"
- elsif defs[-1] && under_comment_valid
- defs[-1].comment << "\n"
- defs[-1].comment << remove_header_marker($1)
- end
- next
- elsif /^\s*?$/ =~ line
- comment = ""
- under_comment_valid = false
- next
- end
- type = ""
- characters = ""
- if line =~ /^\s*?
- (
- character\s*?(\([\w\s\=\(\)\*]+?\))?[\s\,]*
- | type\s*?\([\w\s]+?\)[\s\,]*
- | integer\s*?(\([\w\s\=\(\)\*]+?\))?[\s\,]*
- | real\s*?(\([\w\s\=\(\)\*]+?\))?[\s\,]*
- | double\s+precision[\s\,]*
- | logical\s*?(\([\w\s\=\(\)\*]+?\))?[\s\,]*
- | complex\s*?(\([\w\s\=\(\)\*]+?\))?[\s\,]*
- )
- (.*?::)?
- (.+)$
- /ix
- characters = $8
- type = $1
- type << $7.gsub(/::/, '').gsub(/^\s*?\,/, '') if $7
- else
- under_comment_valid = false
- next
- end
- squote = false ; dquote = false ; bracket = 0
- iniflag = false; commentflag = false
- varname = "" ; arraysuffix = "" ; inivalue = ""
- start_pos = defs.size
- characters.split("").each { |char|
- if !(squote) && !(dquote) && bracket <= 0 && !(iniflag) && !(commentflag)
- case char
- when "!" ; commentflag = true
- when "(" ; bracket += 1 ; arraysuffix = char
- when "\""; dquote = true
- when "\'"; squote = true
- when "=" ; iniflag = true ; inivalue << char
- when ","
- defs << Fortran95Definition.new(varname, type, inivalue, arraysuffix, comment)
- varname = "" ; arraysuffix = "" ; inivalue = ""
- under_comment_valid = true
- when " " ; next
- else ; varname << char
- end
- elsif commentflag
- comment << remove_header_marker(char)
- trailing_comment << remove_header_marker(char)
- elsif iniflag
- if dquote
- case char
- when "\"" ; dquote = false ; inivalue << char
- else ; inivalue << char
- end
- elsif squote
- case char
- when "\'" ; squote = false ; inivalue << char
- else ; inivalue << char
- end
- elsif bracket > 0
- case char
- when "(" ; bracket += 1 ; inivalue << char
- when ")" ; bracket -= 1 ; inivalue << char
- else ; inivalue << char
- end
- else
- case char
- when ","
- defs << Fortran95Definition.new(varname, type, inivalue, arraysuffix, comment)
- varname = "" ; arraysuffix = "" ; inivalue = ""
- iniflag = false
- under_comment_valid = true
- when "(" ; bracket += 1 ; inivalue << char
- when "\""; dquote = true ; inivalue << char
- when "\'"; squote = true ; inivalue << char
- when "!" ; commentflag = true
- else ; inivalue << char
- end
- end
- elsif !(squote) && !(dquote) && bracket > 0
- case char
- when "(" ; bracket += 1 ; arraysuffix << char
- when ")" ; bracket -= 1 ; arraysuffix << char
- else ; arraysuffix << char
- end
- elsif squote
- case char
- when "\'"; squote = false ; inivalue << char
- else ; inivalue << char
- end
- elsif dquote
- case char
- when "\""; dquote = false ; inivalue << char
- else ; inivalue << char
- end
- end
- }
- defs << Fortran95Definition.new(varname, type, inivalue, arraysuffix, comment)
- if trailing_comment =~ /^:nodoc:/
- defs[start_pos..-1].collect!{ |defitem|
- defitem.nodoc = true
- }
- end
- varname = "" ; arraysuffix = "" ; inivalue = ""
- comment = ""
- under_comment_valid = true
- trailing_comment = ""
- }
- return defs
- end
-
-end
-
diff --git a/lib/rdoc/parser/perl.rb b/lib/rdoc/parser/perl.rb
deleted file mode 100644
index 43d1e9ff69..0000000000
--- a/lib/rdoc/parser/perl.rb
+++ /dev/null
@@ -1,165 +0,0 @@
-require 'rdoc/parser'
-
-##
-#
-# This is an attamept to write a basic parser for Perl's
-# POD (Plain old Documentation) format. Ruby code must
-# co-exist with Perl, and some tasks are easier in Perl
-# than Ruby because of existing libraries.
-#
-# One difficult is that Perl POD has no means of identifying
-# the classes (packages) and methods (subs) with which it
-# is associated, it is more like literate programming in so
-# far as it just happens to be in the same place as the code,
-# but need not be.
-#
-# We would like to support all the markup the POD provides
-# so that it will convert happily to HTML. At the moment
-# I don't think I can do that: time constraints.
-#
-
-class RDoc::Parser::PerlPOD < RDoc::Parser
-
- parse_files_matching(/.p[lm]$/)
-
- ##
- # Prepare to parse a perl file
-
- def initialize(top_level, file_name, content, options, stats)
- super
-
- preprocess = RDoc::Markup::PreProcess.new @file_name, @options.rdoc_include
-
- preprocess.handle @content do |directive, param|
- warn "Unrecognized directive '#{directive}' in #{@file_name}"
- end
- end
-
- ##
- # Extract the Pod(-like) comments from the code.
- # At its most basic there will ne no need to distinguish
- # between the different types of header, etc.
- #
- # This uses a simple finite state machine, in a very
- # procedural pattern. I could "replace case with polymorphism"
- # but I think it would obscure the intent, scatter the
- # code all over tha place. This machine is necessary
- # because POD requires that directives be preceded by
- # blank lines, so reading line by line is necessary,
- # and preserving state about what is seen is necesary.
-
- def scan
-
- @top_level.comment ||= ""
- state=:code_blank
- line_number = 0
- line = nil
-
- # This started out as a really long nested case statement,
- # which also led to repetitive code. I'd like to avoid that
- # so I'm using a "table" instead.
-
- # Firstly we need some procs to do the transition and processing
- # work. Because these are procs they are closures, and they can
- # use variables in the local scope.
- #
- # First, the "nothing to see here" stuff.
- code_noop = lambda do
- if line =~ /^\s+$/
- state = :code_blank
- end
- end
-
- pod_noop = lambda do
- if line =~ /^\s+$/
- state = :pod_blank
- end
- @top_level.comment += filter(line)
- end
-
- begin_noop = lambda do
- if line =~ /^\s+$/
- state = :begin_blank
- end
- @top_level.comment += filter(line)
- end
-
- # Now for the blocks that process code and comments...
-
- transit_to_pod = lambda do
- case line
- when /^=(?:pod|head\d+)/
- state = :pod_no_blank
- @top_level.comment += filter(line)
- when /^=over/
- state = :over_no_blank
- @top_level.comment += filter(line)
- when /^=(?:begin|for)/
- state = :begin_no_blank
- end
- end
-
- process_pod = lambda do
- case line
- when /^\s*$/
- state = :pod_blank
- @top_level.comment += filter(line)
- when /^=cut/
- state = :code_no_blank
- when /^=end/
- $stderr.puts "'=end' unexpected at #{line_number} in #{@file_name}"
- else
- @top_level.comment += filter(line)
- end
- end
-
-
- process_begin = lambda do
- case line
- when /^\s*$/
- state = :begin_blank
- @top_level.comment += filter(line)
- when /^=end/
- state = :code_no_blank
- when /^=cut/
- $stderr.puts "'=cut' unexpected at #{line_number} in #{@file_name}"
- else
- @top_level.comment += filter(line)
- end
-
- end
-
-
- transitions = { :code_no_blank => code_noop,
- :code_blank => transit_to_pod,
- :pod_no_blank => pod_noop,
- :pod_blank => process_pod,
- :begin_no_blank => begin_noop,
- :begin_blank => process_begin}
- @content.each_line do |l|
- line = l
- line_number += 1
- transitions[state].call
- end # each line
-
- @top_level
- end
-
- # Filter the perl markup that does the same as the rdoc
- # filtering. Only basic for now. Will probably need a
- # proper parser to cope with C<<...>> etc
- def filter(comment)
- return '' if comment =~ /^=pod\s*$/
- comment.gsub!(/^=pod/, '==')
- comment.gsub!(/^=head(\d+)/) do
- "=" * $1.to_i
- end
- comment.gsub!(/=item/, '');
- comment.gsub!(/C<(.*?)>/, '<tt>\1</tt>');
- comment.gsub!(/I<(.*?)>/, '<i>\1</i>');
- comment.gsub!(/B<(.*?)>/, '<b>\1</b>');
- comment
- end
-
-end
-
diff --git a/lib/rdoc/parser/ruby.rb b/lib/rdoc/parser/ruby.rb
deleted file mode 100644
index 865cb79d39..0000000000
--- a/lib/rdoc/parser/ruby.rb
+++ /dev/null
@@ -1,2829 +0,0 @@
-##
-# This file contains stuff stolen outright from:
-#
-# rtags.rb -
-# ruby-lex.rb - ruby lexcal analyzer
-# ruby-token.rb - ruby tokens
-# by Keiju ISHITSUKA (Nippon Rational Inc.)
-#
-
-require 'e2mmap'
-require 'irb/slex'
-
-require 'rdoc/code_objects'
-require 'rdoc/tokenstream'
-require 'rdoc/markup/preprocess'
-require 'rdoc/parser'
-
-$TOKEN_DEBUG ||= nil
-#$TOKEN_DEBUG = $DEBUG_RDOC
-
-##
-# Definitions of all tokens involved in the lexical analysis
-
-module RDoc::RubyToken
-
- EXPR_BEG = :EXPR_BEG
- EXPR_MID = :EXPR_MID
- EXPR_END = :EXPR_END
- EXPR_ARG = :EXPR_ARG
- EXPR_FNAME = :EXPR_FNAME
- EXPR_DOT = :EXPR_DOT
- EXPR_CLASS = :EXPR_CLASS
-
- class Token
- NO_TEXT = "??".freeze
-
- attr_accessor :text
- attr_reader :line_no
- attr_reader :char_no
-
- def initialize(line_no, char_no)
- @line_no = line_no
- @char_no = char_no
- @text = NO_TEXT
- end
-
- def ==(other)
- self.class == other.class and
- other.line_no == @line_no and
- other.char_no == @char_no and
- other.text == @text
- end
-
- ##
- # Because we're used in contexts that expect to return a token, we set the
- # text string and then return ourselves
-
- def set_text(text)
- @text = text
- self
- end
-
- end
-
- class TkNode < Token
- attr :node
- end
-
- class TkId < Token
- def initialize(line_no, char_no, name)
- super(line_no, char_no)
- @name = name
- end
- attr :name
- end
-
- class TkKW < TkId
- end
-
- class TkVal < Token
- def initialize(line_no, char_no, value = nil)
- super(line_no, char_no)
- set_text(value)
- end
- end
-
- class TkOp < Token
- def name
- self.class.op_name
- end
- end
-
- class TkOPASGN < TkOp
- def initialize(line_no, char_no, op)
- super(line_no, char_no)
- op = TkReading2Token[op] unless Symbol === op
- @op = op
- end
- attr :op
- end
-
- class TkUnknownChar < Token
- def initialize(line_no, char_no, id)
- super(line_no, char_no)
- @name = char_no.chr
- end
- attr :name
- end
-
- class TkError < Token
- end
-
- def set_token_position(line, char)
- @prev_line_no = line
- @prev_char_no = char
- end
-
- def Token(token, value = nil)
- tk = nil
- case token
- when String, Symbol
- source = String === token ? TkReading2Token : TkSymbol2Token
- raise TkReading2TokenNoKey, token if (tk = source[token]).nil?
- tk = Token(tk[0], value)
- else
- tk = if (token.ancestors & [TkId, TkVal, TkOPASGN, TkUnknownChar]).empty?
- token.new(@prev_line_no, @prev_char_no)
- else
- token.new(@prev_line_no, @prev_char_no, value)
- end
- end
- tk
- end
-
- TokenDefinitions = [
- [:TkCLASS, TkKW, "class", EXPR_CLASS],
- [:TkMODULE, TkKW, "module", EXPR_CLASS],
- [:TkDEF, TkKW, "def", EXPR_FNAME],
- [:TkUNDEF, TkKW, "undef", EXPR_FNAME],
- [:TkBEGIN, TkKW, "begin", EXPR_BEG],
- [:TkRESCUE, TkKW, "rescue", EXPR_MID],
- [:TkENSURE, TkKW, "ensure", EXPR_BEG],
- [:TkEND, TkKW, "end", EXPR_END],
- [:TkIF, TkKW, "if", EXPR_BEG, :TkIF_MOD],
- [:TkUNLESS, TkKW, "unless", EXPR_BEG, :TkUNLESS_MOD],
- [:TkTHEN, TkKW, "then", EXPR_BEG],
- [:TkELSIF, TkKW, "elsif", EXPR_BEG],
- [:TkELSE, TkKW, "else", EXPR_BEG],
- [:TkCASE, TkKW, "case", EXPR_BEG],
- [:TkWHEN, TkKW, "when", EXPR_BEG],
- [:TkWHILE, TkKW, "while", EXPR_BEG, :TkWHILE_MOD],
- [:TkUNTIL, TkKW, "until", EXPR_BEG, :TkUNTIL_MOD],
- [:TkFOR, TkKW, "for", EXPR_BEG],
- [:TkBREAK, TkKW, "break", EXPR_END],
- [:TkNEXT, TkKW, "next", EXPR_END],
- [:TkREDO, TkKW, "redo", EXPR_END],
- [:TkRETRY, TkKW, "retry", EXPR_END],
- [:TkIN, TkKW, "in", EXPR_BEG],
- [:TkDO, TkKW, "do", EXPR_BEG],
- [:TkRETURN, TkKW, "return", EXPR_MID],
- [:TkYIELD, TkKW, "yield", EXPR_END],
- [:TkSUPER, TkKW, "super", EXPR_END],
- [:TkSELF, TkKW, "self", EXPR_END],
- [:TkNIL, TkKW, "nil", EXPR_END],
- [:TkTRUE, TkKW, "true", EXPR_END],
- [:TkFALSE, TkKW, "false", EXPR_END],
- [:TkAND, TkKW, "and", EXPR_BEG],
- [:TkOR, TkKW, "or", EXPR_BEG],
- [:TkNOT, TkKW, "not", EXPR_BEG],
- [:TkIF_MOD, TkKW],
- [:TkUNLESS_MOD, TkKW],
- [:TkWHILE_MOD, TkKW],
- [:TkUNTIL_MOD, TkKW],
- [:TkALIAS, TkKW, "alias", EXPR_FNAME],
- [:TkDEFINED, TkKW, "defined?", EXPR_END],
- [:TklBEGIN, TkKW, "BEGIN", EXPR_END],
- [:TklEND, TkKW, "END", EXPR_END],
- [:Tk__LINE__, TkKW, "__LINE__", EXPR_END],
- [:Tk__FILE__, TkKW, "__FILE__", EXPR_END],
-
- [:TkIDENTIFIER, TkId],
- [:TkFID, TkId],
- [:TkGVAR, TkId],
- [:TkIVAR, TkId],
- [:TkCONSTANT, TkId],
-
- [:TkINTEGER, TkVal],
- [:TkFLOAT, TkVal],
- [:TkSTRING, TkVal],
- [:TkXSTRING, TkVal],
- [:TkREGEXP, TkVal],
- [:TkCOMMENT, TkVal],
-
- [:TkDSTRING, TkNode],
- [:TkDXSTRING, TkNode],
- [:TkDREGEXP, TkNode],
- [:TkNTH_REF, TkId],
- [:TkBACK_REF, TkId],
-
- [:TkUPLUS, TkOp, "+@"],
- [:TkUMINUS, TkOp, "-@"],
- [:TkPOW, TkOp, "**"],
- [:TkCMP, TkOp, "<=>"],
- [:TkEQ, TkOp, "=="],
- [:TkEQQ, TkOp, "==="],
- [:TkNEQ, TkOp, "!="],
- [:TkGEQ, TkOp, ">="],
- [:TkLEQ, TkOp, "<="],
- [:TkANDOP, TkOp, "&&"],
- [:TkOROP, TkOp, "||"],
- [:TkMATCH, TkOp, "=~"],
- [:TkNMATCH, TkOp, "!~"],
- [:TkDOT2, TkOp, ".."],
- [:TkDOT3, TkOp, "..."],
- [:TkAREF, TkOp, "[]"],
- [:TkASET, TkOp, "[]="],
- [:TkLSHFT, TkOp, "<<"],
- [:TkRSHFT, TkOp, ">>"],
- [:TkCOLON2, TkOp],
- [:TkCOLON3, TkOp],
-# [:OPASGN, TkOp], # +=, -= etc. #
- [:TkASSOC, TkOp, "=>"],
- [:TkQUESTION, TkOp, "?"], #?
- [:TkCOLON, TkOp, ":"], #:
-
- [:TkfLPAREN], # func( #
- [:TkfLBRACK], # func[ #
- [:TkfLBRACE], # func{ #
- [:TkSTAR], # *arg
- [:TkAMPER], # &arg #
- [:TkSYMBOL, TkId], # :SYMBOL
- [:TkSYMBEG, TkId],
- [:TkGT, TkOp, ">"],
- [:TkLT, TkOp, "<"],
- [:TkPLUS, TkOp, "+"],
- [:TkMINUS, TkOp, "-"],
- [:TkMULT, TkOp, "*"],
- [:TkDIV, TkOp, "/"],
- [:TkMOD, TkOp, "%"],
- [:TkBITOR, TkOp, "|"],
- [:TkBITXOR, TkOp, "^"],
- [:TkBITAND, TkOp, "&"],
- [:TkBITNOT, TkOp, "~"],
- [:TkNOTOP, TkOp, "!"],
-
- [:TkBACKQUOTE, TkOp, "`"],
-
- [:TkASSIGN, Token, "="],
- [:TkDOT, Token, "."],
- [:TkLPAREN, Token, "("], #(exp)
- [:TkLBRACK, Token, "["], #[arry]
- [:TkLBRACE, Token, "{"], #{hash}
- [:TkRPAREN, Token, ")"],
- [:TkRBRACK, Token, "]"],
- [:TkRBRACE, Token, "}"],
- [:TkCOMMA, Token, ","],
- [:TkSEMICOLON, Token, ";"],
-
- [:TkRD_COMMENT],
- [:TkSPACE],
- [:TkNL],
- [:TkEND_OF_SCRIPT],
-
- [:TkBACKSLASH, TkUnknownChar, "\\"],
- [:TkAT, TkUnknownChar, "@"],
- [:TkDOLLAR, TkUnknownChar, "\$"], #"
- ]
-
- # {reading => token_class}
- # {reading => [token_class, *opt]}
- TkReading2Token = {}
- TkSymbol2Token = {}
-
- def self.def_token(token_n, super_token = Token, reading = nil, *opts)
- token_n = token_n.id2name unless String === token_n
-
- fail AlreadyDefinedToken, token_n if const_defined?(token_n)
-
- token_c = Class.new super_token
- const_set token_n, token_c
-# token_c.inspect
-
- if reading
- if TkReading2Token[reading]
- fail TkReading2TokenDuplicateError, token_n, reading
- end
- if opts.empty?
- TkReading2Token[reading] = [token_c]
- else
- TkReading2Token[reading] = [token_c].concat(opts)
- end
- end
- TkSymbol2Token[token_n.intern] = token_c
-
- if token_c <= TkOp
- token_c.class_eval %{
- def self.op_name; "#{reading}"; end
- }
- end
- end
-
- for defs in TokenDefinitions
- def_token(*defs)
- end
-
- NEWLINE_TOKEN = TkNL.new(0,0)
- NEWLINE_TOKEN.set_text("\n")
-
-end
-
-##
-# Lexical analyzer for Ruby source
-
-class RDoc::RubyLex
-
- ##
- # Read an input stream character by character. We allow for unlimited
- # ungetting of characters just read.
- #
- # We simplify the implementation greatly by reading the entire input
- # into a buffer initially, and then simply traversing it using
- # pointers.
- #
- # We also have to allow for the <i>here document diversion</i>. This
- # little gem comes about when the lexer encounters a here
- # document. At this point we effectively need to split the input
- # stream into two parts: one to read the body of the here document,
- # the other to read the rest of the input line where the here
- # document was initially encountered. For example, we might have
- #
- # do_something(<<-A, <<-B)
- # stuff
- # for
- # A
- # stuff
- # for
- # B
- #
- # When the lexer encounters the <<A, it reads until the end of the
- # line, and keeps it around for later. It then reads the body of the
- # here document. Once complete, it needs to read the rest of the
- # original line, but then skip the here document body.
- #
-
- class BufferedReader
-
- attr_reader :line_num
-
- def initialize(content, options)
- @options = options
-
- if /\t/ =~ content
- tab_width = @options.tab_width
- content = content.split(/\n/).map do |line|
- 1 while line.gsub!(/\t+/) { ' ' * (tab_width*$&.length - $`.length % tab_width)} && $~ #`
- line
- end .join("\n")
- end
- @content = content
- @content << "\n" unless @content[-1,1] == "\n"
- @size = @content.size
- @offset = 0
- @hwm = 0
- @line_num = 1
- @read_back_offset = 0
- @last_newline = 0
- @newline_pending = false
- end
-
- def column
- @offset - @last_newline
- end
-
- def getc
- return nil if @offset >= @size
- ch = @content[@offset, 1]
-
- @offset += 1
- @hwm = @offset if @hwm < @offset
-
- if @newline_pending
- @line_num += 1
- @last_newline = @offset - 1
- @newline_pending = false
- end
-
- if ch == "\n"
- @newline_pending = true
- end
- ch
- end
-
- def getc_already_read
- getc
- end
-
- def ungetc(ch)
- raise "unget past beginning of file" if @offset <= 0
- @offset -= 1
- if @content[@offset] == ?\n
- @newline_pending = false
- end
- end
-
- def get_read
- res = @content[@read_back_offset...@offset]
- @read_back_offset = @offset
- res
- end
-
- def peek(at)
- pos = @offset + at
- if pos >= @size
- nil
- else
- @content[pos, 1]
- end
- end
-
- def peek_equal(str)
- @content[@offset, str.length] == str
- end
-
- def divert_read_from(reserve)
- @content[@offset, 0] = reserve
- @size = @content.size
- end
- end
-
- # end of nested class BufferedReader
-
- extend Exception2MessageMapper
- def_exception(:AlreadyDefinedToken, "Already defined token(%s)")
- def_exception(:TkReading2TokenNoKey, "key nothing(key='%s')")
- def_exception(:TkSymbol2TokenNoKey, "key nothing(key='%s')")
- def_exception(:TkReading2TokenDuplicateError,
- "key duplicate(token_n='%s', key='%s')")
- def_exception(:SyntaxError, "%s")
-
- include RDoc::RubyToken
- include IRB
-
- attr_reader :continue
- attr_reader :lex_state
-
- def self.debug?
- false
- end
-
- def initialize(content, options)
- lex_init
-
- @options = options
-
- @reader = BufferedReader.new content, @options
-
- @exp_line_no = @line_no = 1
- @base_char_no = 0
- @indent = 0
-
- @ltype = nil
- @quoted = nil
- @lex_state = EXPR_BEG
- @space_seen = false
-
- @continue = false
- @line = ""
-
- @skip_space = false
- @read_auto_clean_up = false
- @exception_on_syntax_error = true
- end
-
- attr_accessor :skip_space
- attr_accessor :read_auto_clean_up
- attr_accessor :exception_on_syntax_error
- attr_reader :indent
-
- # io functions
- def line_no
- @reader.line_num
- end
-
- def char_no
- @reader.column
- end
-
- def get_read
- @reader.get_read
- end
-
- def getc
- @reader.getc
- end
-
- def getc_of_rests
- @reader.getc_already_read
- end
-
- def gets
- c = getc or return
- l = ""
- begin
- l.concat c unless c == "\r"
- break if c == "\n"
- end while c = getc
- l
- end
-
-
- def ungetc(c = nil)
- @reader.ungetc(c)
- end
-
- def peek_equal?(str)
- @reader.peek_equal(str)
- end
-
- def peek(i = 0)
- @reader.peek(i)
- end
-
- def lex
- until (TkNL === (tk = token) or TkEND_OF_SCRIPT === tk) and
- not @continue or tk.nil?
- end
-
- line = get_read
-
- if line == "" and TkEND_OF_SCRIPT === tk or tk.nil? then
- nil
- else
- line
- end
- end
-
- def token
- set_token_position(line_no, char_no)
- begin
- begin
- tk = @OP.match(self)
- @space_seen = TkSPACE === tk
- rescue SyntaxError => e
- raise RDoc::Error, "syntax error: #{e.message}" if
- @exception_on_syntax_error
-
- tk = TkError.new(line_no, char_no)
- end
- end while @skip_space and TkSPACE === tk
- if @read_auto_clean_up
- get_read
- end
-# throw :eof unless tk
- tk
- end
-
- ENINDENT_CLAUSE = [
- "case", "class", "def", "do", "for", "if",
- "module", "unless", "until", "while", "begin" #, "when"
- ]
- DEINDENT_CLAUSE = ["end" #, "when"
- ]
-
- PERCENT_LTYPE = {
- "q" => "\'",
- "Q" => "\"",
- "x" => "\`",
- "r" => "/",
- "w" => "]"
- }
-
- PERCENT_PAREN = {
- "{" => "}",
- "[" => "]",
- "<" => ">",
- "(" => ")"
- }
-
- Ltype2Token = {
- "\'" => TkSTRING,
- "\"" => TkSTRING,
- "\`" => TkXSTRING,
- "/" => TkREGEXP,
- "]" => TkDSTRING
- }
- Ltype2Token.default = TkSTRING
-
- DLtype2Token = {
- "\"" => TkDSTRING,
- "\`" => TkDXSTRING,
- "/" => TkDREGEXP,
- }
-
- def lex_init()
- @OP = IRB::SLex.new
- @OP.def_rules("\0", "\004", "\032") do |chars, io|
- Token(TkEND_OF_SCRIPT).set_text(chars)
- end
-
- @OP.def_rules(" ", "\t", "\f", "\r", "\13") do |chars, io|
- @space_seen = TRUE
- while (ch = getc) =~ /[ \t\f\r\13]/
- chars << ch
- end
- ungetc
- Token(TkSPACE).set_text(chars)
- end
-
- @OP.def_rule("#") do
- |op, io|
- identify_comment
- end
-
- @OP.def_rule("=begin", proc{@prev_char_no == 0 && peek(0) =~ /\s/}) do
- |op, io|
- str = op
- @ltype = "="
-
-
- begin
- line = ""
- begin
- ch = getc
- line << ch
- end until ch == "\n"
- str << line
- end until line =~ /^=end/
-
- ungetc
-
- @ltype = nil
-
- if str =~ /\A=begin\s+rdoc/i
- str.sub!(/\A=begin.*\n/, '')
- str.sub!(/^=end.*/m, '')
- Token(TkCOMMENT).set_text(str)
- else
- Token(TkRD_COMMENT)#.set_text(str)
- end
- end
-
- @OP.def_rule("\n") do
- print "\\n\n" if RDoc::RubyLex.debug?
- case @lex_state
- when EXPR_BEG, EXPR_FNAME, EXPR_DOT
- @continue = TRUE
- else
- @continue = FALSE
- @lex_state = EXPR_BEG
- end
- Token(TkNL).set_text("\n")
- end
-
- @OP.def_rules("*", "**",
- "!", "!=", "!~",
- "=", "==", "===",
- "=~", "<=>",
- "<", "<=",
- ">", ">=", ">>") do
- |op, io|
- @lex_state = EXPR_BEG
- Token(op).set_text(op)
- end
-
- @OP.def_rules("<<") do
- |op, io|
- tk = nil
- if @lex_state != EXPR_END && @lex_state != EXPR_CLASS &&
- (@lex_state != EXPR_ARG || @space_seen)
- c = peek(0)
- if /[-\w_\"\'\`]/ =~ c
- tk = identify_here_document
- end
- end
- if !tk
- @lex_state = EXPR_BEG
- tk = Token(op).set_text(op)
- end
- tk
- end
-
- @OP.def_rules("'", '"') do
- |op, io|
- identify_string(op)
- end
-
- @OP.def_rules("`") do
- |op, io|
- if @lex_state == EXPR_FNAME
- Token(op).set_text(op)
- else
- identify_string(op)
- end
- end
-
- @OP.def_rules('?') do
- |op, io|
- if @lex_state == EXPR_END
- @lex_state = EXPR_BEG
- Token(TkQUESTION).set_text(op)
- else
- ch = getc
- if @lex_state == EXPR_ARG && ch !~ /\s/
- ungetc
- @lex_state = EXPR_BEG
- Token(TkQUESTION).set_text(op)
- else
- str = op
- str << ch
- if (ch == '\\') #'
- str << read_escape
- end
- @lex_state = EXPR_END
- Token(TkINTEGER).set_text(str)
- end
- end
- end
-
- @OP.def_rules("&", "&&", "|", "||") do
- |op, io|
- @lex_state = EXPR_BEG
- Token(op).set_text(op)
- end
-
- @OP.def_rules("+=", "-=", "*=", "**=",
- "&=", "|=", "^=", "<<=", ">>=", "||=", "&&=") do
- |op, io|
- @lex_state = EXPR_BEG
- op =~ /^(.*)=$/
- Token(TkOPASGN, $1).set_text(op)
- end
-
- @OP.def_rule("+@", proc{@lex_state == EXPR_FNAME}) do |op, io|
- Token(TkUPLUS).set_text(op)
- end
-
- @OP.def_rule("-@", proc{@lex_state == EXPR_FNAME}) do |op, io|
- Token(TkUMINUS).set_text(op)
- end
-
- @OP.def_rules("+", "-") do
- |op, io|
- catch(:RET) do
- if @lex_state == EXPR_ARG
- if @space_seen and peek(0) =~ /[0-9]/
- throw :RET, identify_number(op)
- else
- @lex_state = EXPR_BEG
- end
- elsif @lex_state != EXPR_END and peek(0) =~ /[0-9]/
- throw :RET, identify_number(op)
- else
- @lex_state = EXPR_BEG
- end
- Token(op).set_text(op)
- end
- end
-
- @OP.def_rule(".") do
- @lex_state = EXPR_BEG
- if peek(0) =~ /[0-9]/
- ungetc
- identify_number("")
- else
- # for obj.if
- @lex_state = EXPR_DOT
- Token(TkDOT).set_text(".")
- end
- end
-
- @OP.def_rules("..", "...") do
- |op, io|
- @lex_state = EXPR_BEG
- Token(op).set_text(op)
- end
-
- lex_int2
- end
-
- def lex_int2
- @OP.def_rules("]", "}", ")") do
- |op, io|
- @lex_state = EXPR_END
- @indent -= 1
- Token(op).set_text(op)
- end
-
- @OP.def_rule(":") do
- if @lex_state == EXPR_END || peek(0) =~ /\s/
- @lex_state = EXPR_BEG
- tk = Token(TkCOLON)
- else
- @lex_state = EXPR_FNAME
- tk = Token(TkSYMBEG)
- end
- tk.set_text(":")
- end
-
- @OP.def_rule("::") do
- if @lex_state == EXPR_BEG or @lex_state == EXPR_ARG && @space_seen
- @lex_state = EXPR_BEG
- tk = Token(TkCOLON3)
- else
- @lex_state = EXPR_DOT
- tk = Token(TkCOLON2)
- end
- tk.set_text("::")
- end
-
- @OP.def_rule("/") do
- |op, io|
- if @lex_state == EXPR_BEG || @lex_state == EXPR_MID
- identify_string(op)
- elsif peek(0) == '='
- getc
- @lex_state = EXPR_BEG
- Token(TkOPASGN, :/).set_text("/=") #")
- elsif @lex_state == EXPR_ARG and @space_seen and peek(0) !~ /\s/
- identify_string(op)
- else
- @lex_state = EXPR_BEG
- Token("/").set_text(op)
- end
- end
-
- @OP.def_rules("^") do
- @lex_state = EXPR_BEG
- Token("^").set_text("^")
- end
-
- @OP.def_rules(",", ";") do
- |op, io|
- @lex_state = EXPR_BEG
- Token(op).set_text(op)
- end
-
- @OP.def_rule("~") do
- @lex_state = EXPR_BEG
- Token("~").set_text("~")
- end
-
- @OP.def_rule("~@", proc{@lex_state = EXPR_FNAME}) do
- @lex_state = EXPR_BEG
- Token("~").set_text("~@")
- end
-
- @OP.def_rule("(") do
- @indent += 1
- if @lex_state == EXPR_BEG || @lex_state == EXPR_MID
- @lex_state = EXPR_BEG
- tk = Token(TkfLPAREN)
- else
- @lex_state = EXPR_BEG
- tk = Token(TkLPAREN)
- end
- tk.set_text("(")
- end
-
- @OP.def_rule("[]", proc{@lex_state == EXPR_FNAME}) do
- Token("[]").set_text("[]")
- end
-
- @OP.def_rule("[]=", proc{@lex_state == EXPR_FNAME}) do
- Token("[]=").set_text("[]=")
- end
-
- @OP.def_rule("[") do
- @indent += 1
- if @lex_state == EXPR_FNAME
- t = Token(TkfLBRACK)
- else
- if @lex_state == EXPR_BEG || @lex_state == EXPR_MID
- t = Token(TkLBRACK)
- elsif @lex_state == EXPR_ARG && @space_seen
- t = Token(TkLBRACK)
- else
- t = Token(TkfLBRACK)
- end
- @lex_state = EXPR_BEG
- end
- t.set_text("[")
- end
-
- @OP.def_rule("{") do
- @indent += 1
- if @lex_state != EXPR_END && @lex_state != EXPR_ARG
- t = Token(TkLBRACE)
- else
- t = Token(TkfLBRACE)
- end
- @lex_state = EXPR_BEG
- t.set_text("{")
- end
-
- @OP.def_rule('\\') do #'
- if getc == "\n"
- @space_seen = true
- @continue = true
- Token(TkSPACE).set_text("\\\n")
- else
- ungetc
- Token("\\").set_text("\\") #"
- end
- end
-
- @OP.def_rule('%') do
- |op, io|
- if @lex_state == EXPR_BEG || @lex_state == EXPR_MID
- identify_quotation('%')
- elsif peek(0) == '='
- getc
- Token(TkOPASGN, "%").set_text("%=")
- elsif @lex_state == EXPR_ARG and @space_seen and peek(0) !~ /\s/
- identify_quotation('%')
- else
- @lex_state = EXPR_BEG
- Token("%").set_text("%")
- end
- end
-
- @OP.def_rule('$') do #'
- identify_gvar
- end
-
- @OP.def_rule('@') do
- if peek(0) =~ /[@\w_]/
- ungetc
- identify_identifier
- else
- Token("@").set_text("@")
- end
- end
-
- @OP.def_rule("__END__", proc{@prev_char_no == 0 && peek(0) =~ /[\r\n]/}) do
- throw :eof
- end
-
- @OP.def_rule("") do
- |op, io|
- printf "MATCH: start %s: %s\n", op, io.inspect if RDoc::RubyLex.debug?
- if peek(0) =~ /[0-9]/
- t = identify_number("")
- elsif peek(0) =~ /[\w_]/
- t = identify_identifier
- end
- printf "MATCH: end %s: %s\n", op, io.inspect if RDoc::RubyLex.debug?
- t
- end
- end
-
- def identify_gvar
- @lex_state = EXPR_END
- str = "$"
-
- tk = case ch = getc
- when /[~_*$?!@\/\\;,=:<>".]/ #"
- str << ch
- Token(TkGVAR, str)
-
- when "-"
- str << "-" << getc
- Token(TkGVAR, str)
-
- when "&", "`", "'", "+"
- str << ch
- Token(TkBACK_REF, str)
-
- when /[1-9]/
- str << ch
- while (ch = getc) =~ /[0-9]/
- str << ch
- end
- ungetc
- Token(TkNTH_REF)
- when /\w/
- ungetc
- ungetc
- return identify_identifier
- else
- ungetc
- Token("$")
- end
- tk.set_text(str)
- end
-
- def identify_identifier
- token = ""
- token.concat getc if peek(0) =~ /[$@]/
- token.concat getc if peek(0) == "@"
-
- while (ch = getc) =~ /\w|_/
- print ":", ch, ":" if RDoc::RubyLex.debug?
- token.concat ch
- end
- ungetc
-
- if ch == "!" or ch == "?"
- token.concat getc
- end
- # fix token
-
- # $stderr.puts "identifier - #{token}, state = #@lex_state"
-
- case token
- when /^\$/
- return Token(TkGVAR, token).set_text(token)
- when /^\@/
- @lex_state = EXPR_END
- return Token(TkIVAR, token).set_text(token)
- end
-
- if @lex_state != EXPR_DOT
- print token, "\n" if RDoc::RubyLex.debug?
-
- token_c, *trans = TkReading2Token[token]
- if token_c
- # reserved word?
-
- if (@lex_state != EXPR_BEG &&
- @lex_state != EXPR_FNAME &&
- trans[1])
- # modifiers
- token_c = TkSymbol2Token[trans[1]]
- @lex_state = trans[0]
- else
- if @lex_state != EXPR_FNAME
- if ENINDENT_CLAUSE.include?(token)
- @indent += 1
- elsif DEINDENT_CLAUSE.include?(token)
- @indent -= 1
- end
- @lex_state = trans[0]
- else
- @lex_state = EXPR_END
- end
- end
- return Token(token_c, token).set_text(token)
- end
- end
-
- if @lex_state == EXPR_FNAME
- @lex_state = EXPR_END
- if peek(0) == '='
- token.concat getc
- end
- elsif @lex_state == EXPR_BEG || @lex_state == EXPR_DOT
- @lex_state = EXPR_ARG
- else
- @lex_state = EXPR_END
- end
-
- if token[0, 1] =~ /[A-Z]/
- return Token(TkCONSTANT, token).set_text(token)
- elsif token[token.size - 1, 1] =~ /[!?]/
- return Token(TkFID, token).set_text(token)
- else
- return Token(TkIDENTIFIER, token).set_text(token)
- end
- end
-
- def identify_here_document
- ch = getc
- if ch == "-"
- ch = getc
- indent = true
- end
- if /['"`]/ =~ ch # '
- lt = ch
- quoted = ""
- while (c = getc) && c != lt
- quoted.concat c
- end
- else
- lt = '"'
- quoted = ch.dup
- while (c = getc) && c =~ /\w/
- quoted.concat c
- end
- ungetc
- end
-
- ltback, @ltype = @ltype, lt
- reserve = ""
-
- while ch = getc
- reserve << ch
- if ch == "\\" #"
- ch = getc
- reserve << ch
- elsif ch == "\n"
- break
- end
- end
-
- str = ""
- while (l = gets)
- l.chomp!
- l.strip! if indent
- break if l == quoted
- str << l.chomp << "\n"
- end
-
- @reader.divert_read_from(reserve)
-
- @ltype = ltback
- @lex_state = EXPR_END
- Token(Ltype2Token[lt], str).set_text(str.dump)
- end
-
- def identify_quotation(initial_char)
- ch = getc
- if lt = PERCENT_LTYPE[ch]
- initial_char += ch
- ch = getc
- elsif ch =~ /\W/
- lt = "\""
- else
- fail SyntaxError, "unknown type of %string ('#{ch}')"
- end
-# if ch !~ /\W/
-# ungetc
-# next
-# end
- #@ltype = lt
- @quoted = ch unless @quoted = PERCENT_PAREN[ch]
- identify_string(lt, @quoted, ch, initial_char)
- end
-
- def identify_number(start)
- str = start.dup
-
- if start == "+" or start == "-" or start == ""
- start = getc
- str << start
- end
-
- @lex_state = EXPR_END
-
- if start == "0"
- if peek(0) == "x"
- ch = getc
- str << ch
- match = /[0-9a-f_]/
- else
- match = /[0-7_]/
- end
- while ch = getc
- if ch !~ match
- ungetc
- break
- else
- str << ch
- end
- end
- return Token(TkINTEGER).set_text(str)
- end
-
- type = TkINTEGER
- allow_point = TRUE
- allow_e = TRUE
- while ch = getc
- case ch
- when /[0-9_]/
- str << ch
-
- when allow_point && "."
- type = TkFLOAT
- if peek(0) !~ /[0-9]/
- ungetc
- break
- end
- str << ch
- allow_point = false
-
- when allow_e && "e", allow_e && "E"
- str << ch
- type = TkFLOAT
- if peek(0) =~ /[+-]/
- str << getc
- end
- allow_e = false
- allow_point = false
- else
- ungetc
- break
- end
- end
- Token(type).set_text(str)
- end
-
- def identify_string(ltype, quoted = ltype, opener=nil, initial_char = nil)
- @ltype = ltype
- @quoted = quoted
- subtype = nil
-
- str = ""
- str << initial_char if initial_char
- str << (opener||quoted)
-
- nest = 0
- begin
- while ch = getc
- str << ch
- if @quoted == ch
- if nest == 0
- break
- else
- nest -= 1
- end
- elsif opener == ch
- nest += 1
- elsif @ltype != "'" && @ltype != "]" and ch == "#"
- ch = getc
- if ch == "{"
- subtype = true
- str << ch << skip_inner_expression
- else
- ungetc(ch)
- end
- elsif ch == '\\' #'
- str << read_escape
- end
- end
- if @ltype == "/"
- if peek(0) =~ /i|o|n|e|s/
- str << getc
- end
- end
- if subtype
- Token(DLtype2Token[ltype], str)
- else
- Token(Ltype2Token[ltype], str)
- end.set_text(str)
- ensure
- @ltype = nil
- @quoted = nil
- @lex_state = EXPR_END
- end
- end
-
- def skip_inner_expression
- res = ""
- nest = 0
- while (ch = getc)
- res << ch
- if ch == '}'
- break if nest.zero?
- nest -= 1
- elsif ch == '{'
- nest += 1
- end
- end
- res
- end
-
- def identify_comment
- @ltype = "#"
- comment = "#"
- while ch = getc
- if ch == "\\"
- ch = getc
- if ch == "\n"
- ch = " "
- else
- comment << "\\"
- end
- else
- if ch == "\n"
- @ltype = nil
- ungetc
- break
- end
- end
- comment << ch
- end
- return Token(TkCOMMENT).set_text(comment)
- end
-
- def read_escape
- res = ""
- case ch = getc
- when /[0-7]/
- ungetc ch
- 3.times do
- case ch = getc
- when /[0-7]/
- when nil
- break
- else
- ungetc
- break
- end
- res << ch
- end
-
- when "x"
- res << ch
- 2.times do
- case ch = getc
- when /[0-9a-fA-F]/
- when nil
- break
- else
- ungetc
- break
- end
- res << ch
- end
-
- when "M"
- res << ch
- if (ch = getc) != '-'
- ungetc
- else
- res << ch
- if (ch = getc) == "\\" #"
- res << ch
- res << read_escape
- else
- res << ch
- end
- end
-
- when "C", "c" #, "^"
- res << ch
- if ch == "C" and (ch = getc) != "-"
- ungetc
- else
- res << ch
- if (ch = getc) == "\\" #"
- res << ch
- res << read_escape
- else
- res << ch
- end
- end
- else
- res << ch
- end
- res
- end
-end
-
-##
-# Extracts code elements from a source file returning a TopLevel object
-# containing the constituent file elements.
-#
-# This file is based on rtags
-#
-# RubyParser understands how to document:
-# * classes
-# * modules
-# * methods
-# * constants
-# * aliases
-# * private, public, protected
-# * private_class_function, public_class_function
-# * module_function
-# * attr, attr_reader, attr_writer, attr_accessor
-# * extra accessors given on the command line
-# * metaprogrammed methods
-# * require
-# * include
-#
-# == Method Arguments
-#
-#--
-# NOTE: I don't think this works, needs tests, remove the paragraph following
-# this block when known to work
-#
-# The parser extracts the arguments from the method definition. You can
-# override this with a custom argument definition using the :args: directive:
-#
-# ##
-# # This method tries over and over until it is tired
-#
-# def go_go_go(thing_to_try, tries = 10) # :args: thing_to_try
-# puts thing_to_try
-# go_go_go thing_to_try, tries - 1
-# end
-#
-# If you have a more-complex set of overrides you can use the :call-seq:
-# directive:
-#++
-#
-# The parser extracts the arguments from the method definition. You can
-# override this with a custom argument definition using the :call-seq:
-# directive:
-#
-# ##
-# # This method can be called with a range or an offset and length
-# #
-# # :call-seq:
-# # my_method(Range)
-# # my_method(offset, length)
-#
-# def my_method(*args)
-# end
-#
-# The parser extracts +yield+ expressions from method bodies to gather the
-# yielded argument names. If your method manually calls a block instead of
-# yielding or you want to override the discovered argument names use
-# the :yields: directive:
-#
-# ##
-# # My method is awesome
-#
-# def my_method(&block) # :yields: happy, times
-# block.call 1, 2
-# end
-#
-# == Metaprogrammed Methods
-#
-# To pick up a metaprogrammed method, the parser looks for a comment starting
-# with '##' before an identifier:
-#
-# ##
-# # This is a meta-programmed method!
-#
-# add_my_method :meta_method, :arg1, :arg2
-#
-# The parser looks at the token after the identifier to determine the name, in
-# this example, :meta_method. If a name cannot be found, a warning is printed
-# and 'unknown is used.
-#
-# You can force the name of a method using the :method: directive:
-#
-# ##
-# # :method: woo_hoo!
-#
-# By default, meta-methods are instance methods. To indicate that a method is
-# a singleton method instead use the :singleton-method: directive:
-#
-# ##
-# # :singleton-method:
-#
-# You can also use the :singleton-method: directive with a name:
-#
-# ##
-# # :singleton-method: woo_hoo!
-#
-# == Hidden methods
-#
-# You can provide documentation for methods that don't appear using
-# the :method: and :singleton-method: directives:
-#
-# ##
-# # :method: ghost_method
-# # There is a method here, but you can't see it!
-#
-# ##
-# # this is a comment for a regular method
-#
-# def regular_method() end
-#
-# Note that by default, the :method: directive will be ignored if there is a
-# standard rdocable item following it.
-
-class RDoc::Parser::Ruby < RDoc::Parser
-
- parse_files_matching(/\.rbw?$/)
-
- include RDoc::RubyToken
- include RDoc::TokenStream
-
- NORMAL = "::"
- SINGLE = "<<"
-
- def initialize(top_level, file_name, content, options, stats)
- super
-
- @size = 0
- @token_listeners = nil
- @scanner = RDoc::RubyLex.new content, @options
- @scanner.exception_on_syntax_error = false
-
- reset
- end
-
- def add_token_listener(obj)
- @token_listeners ||= []
- @token_listeners << obj
- end
-
- ##
- # Look for the first comment in a file that isn't a shebang line.
-
- def collect_first_comment
- skip_tkspace
- res = ''
- first_line = true
-
- tk = get_tk
-
- while TkCOMMENT === tk
- if first_line and tk.text =~ /\A#!/ then
- skip_tkspace
- tk = get_tk
- elsif first_line and tk.text =~ /\A#\s*-\*-/ then
- first_line = false
- skip_tkspace
- tk = get_tk
- else
- first_line = false
- res << tk.text << "\n"
- tk = get_tk
-
- if TkNL === tk then
- skip_tkspace false
- tk = get_tk
- end
- end
- end
-
- unget_tk tk
-
- res
- end
-
- def error(msg)
- msg = make_message msg
- $stderr.puts msg
- exit(1)
- end
-
- ##
- # Look for a 'call-seq' in the comment, and override the normal parameter
- # stuff
-
- def extract_call_seq(comment, meth)
- if comment.sub!(/:?call-seq:(.*?)^\s*\#?\s*$/m, '') then
- seq = $1
- seq.gsub!(/^\s*\#\s*/, '')
- meth.call_seq = seq
- end
-
- meth
- end
-
- def get_bool
- skip_tkspace
- tk = get_tk
- case tk
- when TkTRUE
- true
- when TkFALSE, TkNIL
- false
- else
- unget_tk tk
- true
- end
- end
-
- ##
- # Look for the name of a class of module (optionally with a leading :: or
- # with :: separated named) and return the ultimate name and container
-
- def get_class_or_module(container)
- skip_tkspace
- name_t = get_tk
-
- # class ::A -> A is in the top level
- if TkCOLON2 === name_t then
- name_t = get_tk
- container = @top_level
- end
-
- skip_tkspace(false)
-
- while TkCOLON2 === peek_tk do
- prev_container = container
- container = container.find_module_named(name_t.name)
- if !container
-# warn("Couldn't find module #{name_t.name}")
- container = prev_container.add_module RDoc::NormalModule, name_t.name
- end
- get_tk
- name_t = get_tk
- end
- skip_tkspace(false)
- return [container, name_t]
- end
-
- ##
- # Return a superclass, which can be either a constant of an expression
-
- def get_class_specification
- tk = get_tk
- return "self" if TkSELF === tk
-
- res = ""
- while TkCOLON2 === tk or TkCOLON3 === tk or TkCONSTANT === tk do
- res += tk.text
- tk = get_tk
- end
-
- unget_tk(tk)
- skip_tkspace(false)
-
- get_tkread # empty out read buffer
-
- tk = get_tk
-
- case tk
- when TkNL, TkCOMMENT, TkSEMICOLON then
- unget_tk(tk)
- return res
- end
-
- res += parse_call_parameters(tk)
- res
- end
-
- ##
- # Parse a constant, which might be qualified by one or more class or module
- # names
-
- def get_constant
- res = ""
- skip_tkspace(false)
- tk = get_tk
-
- while TkCOLON2 === tk or TkCOLON3 === tk or TkCONSTANT === tk do
- res += tk.text
- tk = get_tk
- end
-
-# if res.empty?
-# warn("Unexpected token #{tk} in constant")
-# end
- unget_tk(tk)
- res
- end
-
- ##
- # Get a constant that may be surrounded by parens
-
- def get_constant_with_optional_parens
- skip_tkspace(false)
- nest = 0
- while TkLPAREN === (tk = peek_tk) or TkfLPAREN === tk do
- get_tk
- skip_tkspace(true)
- nest += 1
- end
-
- name = get_constant
-
- while nest > 0
- skip_tkspace(true)
- tk = get_tk
- nest -= 1 if TkRPAREN === tk
- end
- name
- end
-
- def get_symbol_or_name
- tk = get_tk
- case tk
- when TkSYMBOL
- tk.text.sub(/^:/, '')
- when TkId, TkOp
- tk.name
- when TkSTRING
- tk.text
- else
- raise "Name or symbol expected (got #{tk})"
- end
- end
-
- def get_tk
- tk = nil
- if @tokens.empty?
- tk = @scanner.token
- @read.push @scanner.get_read
- puts "get_tk1 => #{tk.inspect}" if $TOKEN_DEBUG
- else
- @read.push @unget_read.shift
- tk = @tokens.shift
- puts "get_tk2 => #{tk.inspect}" if $TOKEN_DEBUG
- end
-
- if TkSYMBEG === tk then
- set_token_position(tk.line_no, tk.char_no)
- tk1 = get_tk
- if TkId === tk1 or TkOp === tk1 or TkSTRING === tk1 then
- if tk1.respond_to?(:name)
- tk = Token(TkSYMBOL).set_text(":" + tk1.name)
- else
- tk = Token(TkSYMBOL).set_text(":" + tk1.text)
- end
- # remove the identifier we just read (we're about to
- # replace it with a symbol)
- @token_listeners.each do |obj|
- obj.pop_token
- end if @token_listeners
- else
- warn("':' not followed by identifier or operator")
- tk = tk1
- end
- end
-
- # inform any listeners of our shiny new token
- @token_listeners.each do |obj|
- obj.add_token(tk)
- end if @token_listeners
-
- tk
- end
-
- def get_tkread
- read = @read.join("")
- @read = []
- read
- end
-
- ##
- # Look for directives in a normal comment block:
- #
- # #-- - don't display comment from this point forward
- #
- # This routine modifies it's parameter
-
- def look_for_directives_in(context, comment)
- preprocess = RDoc::Markup::PreProcess.new(@file_name,
- @options.rdoc_include)
-
- preprocess.handle(comment) do |directive, param|
- case directive
- when 'enddoc' then
- throw :enddoc
- when 'main' then
- @options.main_page = param
- ''
- when 'method', 'singleton-method' then
- false # ignore
- when 'section' then
- context.set_current_section(param, comment)
- comment.replace ''
- break
- when 'startdoc' then
- context.start_doc
- context.force_documentation = true
- ''
- when 'stopdoc' then
- context.stop_doc
- ''
- when 'title' then
- @options.title = param
- ''
- else
- warn "Unrecognized directive '#{directive}'"
- false
- end
- end
-
- remove_private_comments(comment)
- end
-
- def make_message(msg)
- prefix = "\n" + @file_name + ":"
- if @scanner
- prefix << "#{@scanner.line_no}:#{@scanner.char_no}: "
- end
- return prefix + msg
- end
-
- def parse_attr(context, single, tk, comment)
- args = parse_symbol_arg(1)
- if args.size > 0
- name = args[0]
- rw = "R"
- skip_tkspace(false)
- tk = get_tk
- if TkCOMMA === tk then
- rw = "RW" if get_bool
- else
- unget_tk tk
- end
- att = RDoc::Attr.new get_tkread, name, rw, comment
- read_documentation_modifiers att, RDoc::ATTR_MODIFIERS
- if att.document_self
- context.add_attribute(att)
- end
- else
- warn("'attr' ignored - looks like a variable")
- end
- end
-
- def parse_attr_accessor(context, single, tk, comment)
- args = parse_symbol_arg
- read = get_tkread
- rw = "?"
-
- # If nodoc is given, don't document any of them
-
- tmp = RDoc::CodeObject.new
- read_documentation_modifiers tmp, RDoc::ATTR_MODIFIERS
- return unless tmp.document_self
-
- case tk.name
- when "attr_reader" then rw = "R"
- when "attr_writer" then rw = "W"
- when "attr_accessor" then rw = "RW"
- else
- rw = @options.extra_accessor_flags[tk.name]
- rw = '?' if rw.nil?
- end
-
- for name in args
- att = RDoc::Attr.new get_tkread, name, rw, comment
- context.add_attribute att
- end
- end
-
- def parse_alias(context, single, tk, comment)
- skip_tkspace
- if TkLPAREN === peek_tk then
- get_tk
- skip_tkspace
- end
- new_name = get_symbol_or_name
- @scanner.instance_eval{@lex_state = EXPR_FNAME}
- skip_tkspace
- if TkCOMMA === peek_tk then
- get_tk
- skip_tkspace
- end
- old_name = get_symbol_or_name
-
- al = RDoc::Alias.new get_tkread, old_name, new_name, comment
- read_documentation_modifiers al, RDoc::ATTR_MODIFIERS
- if al.document_self
- context.add_alias(al)
- end
- end
-
- def parse_call_parameters(tk)
- end_token = case tk
- when TkLPAREN, TkfLPAREN
- TkRPAREN
- when TkRPAREN
- return ""
- else
- TkNL
- end
- nest = 0
-
- loop do
- case tk
- when TkSEMICOLON
- break
- when TkLPAREN, TkfLPAREN
- nest += 1
- when end_token
- if end_token == TkRPAREN
- nest -= 1
- break if @scanner.lex_state == EXPR_END and nest <= 0
- else
- break unless @scanner.continue
- end
- when TkCOMMENT
- unget_tk(tk)
- break
- end
- tk = get_tk
- end
- res = get_tkread.tr("\n", " ").strip
- res = "" if res == ";"
- res
- end
-
- def parse_class(container, single, tk, comment)
- container, name_t = get_class_or_module(container)
-
- case name_t
- when TkCONSTANT
- name = name_t.name
- superclass = "Object"
-
- if TkLT === peek_tk then
- get_tk
- skip_tkspace(true)
- superclass = get_class_specification
- superclass = "<unknown>" if superclass.empty?
- end
-
- cls_type = single == SINGLE ? RDoc::SingleClass : RDoc::NormalClass
- cls = container.add_class cls_type, name, superclass
-
- @stats.add_class cls
-
- read_documentation_modifiers cls, RDoc::CLASS_MODIFIERS
- cls.record_location @top_level
-
- parse_statements cls
- cls.comment = comment
-
- when TkLSHFT
- case name = get_class_specification
- when "self", container.name
- parse_statements(container, SINGLE)
- else
- other = RDoc::TopLevel.find_class_named(name)
- unless other
- # other = @top_level.add_class(NormalClass, name, nil)
- # other.record_location(@top_level)
- # other.comment = comment
- other = RDoc::NormalClass.new "Dummy", nil
- end
-
- @stats.add_class other
-
- read_documentation_modifiers other, RDoc::CLASS_MODIFIERS
- parse_statements(other, SINGLE)
- end
-
- else
- warn("Expected class name or '<<'. Got #{name_t.class}: #{name_t.text.inspect}")
- end
- end
-
- def parse_constant(container, single, tk, comment)
- name = tk.name
- skip_tkspace(false)
- eq_tk = get_tk
-
- unless TkASSIGN === eq_tk then
- unget_tk(eq_tk)
- return
- end
-
-
- nest = 0
- get_tkread
-
- tk = get_tk
- if TkGT === tk then
- unget_tk(tk)
- unget_tk(eq_tk)
- return
- end
-
- loop do
- case tk
- when TkSEMICOLON
- break
- when TkLPAREN, TkfLPAREN, TkLBRACE, TkLBRACK, TkDO
- nest += 1
- when TkRPAREN, TkRBRACE, TkRBRACK, TkEND
- nest -= 1
- when TkCOMMENT
- if nest <= 0 && @scanner.lex_state == EXPR_END
- unget_tk(tk)
- break
- end
- when TkNL
- if (nest <= 0) && ((@scanner.lex_state == EXPR_END) || (!@scanner.continue))
- unget_tk(tk)
- break
- end
- end
- tk = get_tk
- end
-
- res = get_tkread.tr("\n", " ").strip
- res = "" if res == ";"
-
- con = RDoc::Constant.new name, res, comment
- read_documentation_modifiers con, RDoc::CONSTANT_MODIFIERS
-
- if con.document_self
- container.add_constant(con)
- end
- end
-
- def parse_comment(container, tk, comment)
- line_no = tk.line_no
- column = tk.char_no
-
- singleton = !!comment.sub!(/(^# +:?)(singleton-)(method:)/, '\1\3')
-
- if comment.sub!(/^# +:?method: *(\S*).*?\n/i, '') then
- name = $1 unless $1.empty?
- else
- return nil
- end
-
- meth = RDoc::GhostMethod.new get_tkread, name
- meth.singleton = singleton
-
- @stats.add_method meth
-
- meth.start_collecting_tokens
- indent = TkSPACE.new 1, 1
- indent.set_text " " * column
-
- position_comment = TkCOMMENT.new(line_no, 1, "# File #{@top_level.file_absolute_name}, line #{line_no}")
- meth.add_tokens [position_comment, NEWLINE_TOKEN, indent]
-
- meth.params = ''
-
- extract_call_seq comment, meth
-
- container.add_method meth if meth.document_self
-
- meth.comment = comment
- end
-
- def parse_include(context, comment)
- loop do
- skip_tkspace_comment
-
- name = get_constant_with_optional_parens
- context.add_include RDoc::Include.new(name, comment) unless name.empty?
-
- return unless TkCOMMA === peek_tk
- get_tk
- end
- end
-
- ##
- # Parses a meta-programmed method
-
- def parse_meta_method(container, single, tk, comment)
- line_no = tk.line_no
- column = tk.char_no
-
- start_collecting_tokens
- add_token tk
- add_token_listener self
-
- skip_tkspace false
-
- singleton = !!comment.sub!(/(^# +:?)(singleton-)(method:)/, '\1\3')
-
- if comment.sub!(/^# +:?method: *(\S*).*?\n/i, '') then
- name = $1 unless $1.empty?
- end
-
- if name.nil? then
- name_t = get_tk
- case name_t
- when TkSYMBOL then
- name = name_t.text[1..-1]
- when TkSTRING then
- name = name_t.text[1..-2]
- else
- warn "#{container.top_level.file_relative_name}:#{name_t.line_no} unknown name token #{name_t.inspect} for meta-method"
- name = 'unknown'
- end
- end
-
- meth = RDoc::MetaMethod.new get_tkread, name
- meth.singleton = singleton
-
- @stats.add_method meth
-
- remove_token_listener self
-
- meth.start_collecting_tokens
- indent = TkSPACE.new 1, 1
- indent.set_text " " * column
-
- position_comment = TkCOMMENT.new(line_no, 1, "# File #{@top_level.file_absolute_name}, line #{line_no}")
- meth.add_tokens [position_comment, NEWLINE_TOKEN, indent]
- meth.add_tokens @token_stream
-
- add_token_listener meth
-
- meth.params = ''
-
- extract_call_seq comment, meth
-
- container.add_method meth if meth.document_self
-
- last_tk = tk
-
- while tk = get_tk do
- case tk
- when TkSEMICOLON then
- break
- when TkNL then
- break unless last_tk and TkCOMMA === last_tk
- when TkSPACE then
- # expression continues
- else
- last_tk = tk
- end
- end
-
- remove_token_listener meth
-
- meth.comment = comment
- end
-
- ##
- # Parses a method
-
- def parse_method(container, single, tk, comment)
- line_no = tk.line_no
- column = tk.char_no
-
- start_collecting_tokens
- add_token(tk)
- add_token_listener(self)
-
- @scanner.instance_eval do @lex_state = EXPR_FNAME end
-
- skip_tkspace(false)
- name_t = get_tk
- back_tk = skip_tkspace
- meth = nil
- added_container = false
-
- dot = get_tk
- if TkDOT === dot or TkCOLON2 === dot then
- @scanner.instance_eval do @lex_state = EXPR_FNAME end
- skip_tkspace
- name_t2 = get_tk
-
- case name_t
- when TkSELF then
- name = name_t2.name
- when TkCONSTANT then
- name = name_t2.name
- prev_container = container
- container = container.find_module_named(name_t.name)
- unless container then
- added_container = true
- obj = name_t.name.split("::").inject(Object) do |state, item|
- state.const_get(item)
- end rescue nil
-
- type = obj.class == Class ? RDoc::NormalClass : RDoc::NormalModule
-
- unless [Class, Module].include?(obj.class) then
- warn("Couldn't find #{name_t.name}. Assuming it's a module")
- end
-
- if type == RDoc::NormalClass then
- container = prev_container.add_class(type, name_t.name, obj.superclass.name)
- else
- container = prev_container.add_module(type, name_t.name)
- end
-
- container.record_location @top_level
- end
- else
- # warn("Unexpected token '#{name_t2.inspect}'")
- # break
- skip_method(container)
- return
- end
-
- meth = RDoc::AnyMethod.new(get_tkread, name)
- meth.singleton = true
- else
- unget_tk dot
- back_tk.reverse_each do |token|
- unget_tk token
- end
- name = name_t.name
-
- meth = RDoc::AnyMethod.new get_tkread, name
- meth.singleton = (single == SINGLE)
- end
-
- @stats.add_method meth
-
- remove_token_listener self
-
- meth.start_collecting_tokens
- indent = TkSPACE.new 1, 1
- indent.set_text " " * column
-
- token = TkCOMMENT.new(line_no, 1, "# File #{@top_level.file_absolute_name}, line #{line_no}")
- meth.add_tokens [token, NEWLINE_TOKEN, indent]
- meth.add_tokens @token_stream
-
- add_token_listener meth
-
- @scanner.instance_eval do @continue = false end
- parse_method_parameters meth
-
- if meth.document_self then
- container.add_method meth
- elsif added_container then
- container.document_self = false
- end
-
- # Having now read the method parameters and documentation modifiers, we
- # now know whether we have to rename #initialize to ::new
-
- if name == "initialize" && !meth.singleton then
- if meth.dont_rename_initialize then
- meth.visibility = :protected
- else
- meth.singleton = true
- meth.name = "new"
- meth.visibility = :public
- end
- end
-
- parse_statements(container, single, meth)
-
- remove_token_listener(meth)
-
- extract_call_seq comment, meth
-
- meth.comment = comment
- end
-
- def parse_method_or_yield_parameters(method = nil,
- modifiers = RDoc::METHOD_MODIFIERS)
- skip_tkspace(false)
- tk = get_tk
-
- # Little hack going on here. In the statement
- # f = 2*(1+yield)
- # We see the RPAREN as the next token, so we need
- # to exit early. This still won't catch all cases
- # (such as "a = yield + 1"
- end_token = case tk
- when TkLPAREN, TkfLPAREN
- TkRPAREN
- when TkRPAREN
- return ""
- else
- TkNL
- end
- nest = 0
-
- loop do
- case tk
- when TkSEMICOLON
- break
- when TkLBRACE
- nest += 1
- when TkRBRACE
- # we might have a.each {|i| yield i }
- unget_tk(tk) if nest.zero?
- nest -= 1
- break if nest <= 0
- when TkLPAREN, TkfLPAREN
- nest += 1
- when end_token
- if end_token == TkRPAREN
- nest -= 1
- break if @scanner.lex_state == EXPR_END and nest <= 0
- else
- break unless @scanner.continue
- end
- when method && method.block_params.nil? && TkCOMMENT
- unget_tk(tk)
- read_documentation_modifiers(method, modifiers)
- end
- tk = get_tk
- end
- res = get_tkread.tr("\n", " ").strip
- res = "" if res == ";"
- res
- end
-
- ##
- # Capture the method's parameters. Along the way, look for a comment
- # containing:
- #
- # # yields: ....
- #
- # and add this as the block_params for the method
-
- def parse_method_parameters(method)
- res = parse_method_or_yield_parameters(method)
- res = "(" + res + ")" unless res[0] == ?(
- method.params = res unless method.params
- if method.block_params.nil?
- skip_tkspace(false)
- read_documentation_modifiers method, RDoc::METHOD_MODIFIERS
- end
- end
-
- def parse_module(container, single, tk, comment)
- container, name_t = get_class_or_module(container)
-
- name = name_t.name
-
- mod = container.add_module RDoc::NormalModule, name
- mod.record_location @top_level
-
- @stats.add_module mod
-
- read_documentation_modifiers mod, RDoc::CLASS_MODIFIERS
- parse_statements(mod)
- mod.comment = comment
- end
-
- def parse_require(context, comment)
- skip_tkspace_comment
- tk = get_tk
- if TkLPAREN === tk then
- skip_tkspace_comment
- tk = get_tk
- end
-
- name = nil
- case tk
- when TkSTRING
- name = tk.text
- # when TkCONSTANT, TkIDENTIFIER, TkIVAR, TkGVAR
- # name = tk.name
- when TkDSTRING
- warn "Skipping require of dynamic string: #{tk.text}"
- # else
- # warn "'require' used as variable"
- end
- if name
- context.add_require RDoc::Require.new(name, comment)
- else
- unget_tk(tk)
- end
- end
-
- def parse_statements(container, single = NORMAL, current_method = nil,
- comment = '')
- nest = 1
- save_visibility = container.visibility
-
- non_comment_seen = true
-
- while tk = get_tk do
- keep_comment = false
-
- non_comment_seen = true unless TkCOMMENT === tk
-
- case tk
- when TkNL then
- skip_tkspace true # Skip blanks and newlines
- tk = get_tk
-
- if TkCOMMENT === tk then
- if non_comment_seen then
- # Look for RDoc in a comment about to be thrown away
- parse_comment container, tk, comment unless comment.empty?
-
- comment = ''
- non_comment_seen = false
- end
-
- while TkCOMMENT === tk do
- comment << tk.text << "\n"
- tk = get_tk # this is the newline
- skip_tkspace(false) # leading spaces
- tk = get_tk
- end
-
- unless comment.empty? then
- look_for_directives_in container, comment
-
- if container.done_documenting then
- container.ongoing_visibility = save_visibility
- end
- end
-
- keep_comment = true
- else
- non_comment_seen = true
- end
-
- unget_tk tk
- keep_comment = true
-
- when TkCLASS then
- if container.document_children then
- parse_class container, single, tk, comment
- else
- nest += 1
- end
-
- when TkMODULE then
- if container.document_children then
- parse_module container, single, tk, comment
- else
- nest += 1
- end
-
- when TkDEF then
- if container.document_self then
- parse_method container, single, tk, comment
- else
- nest += 1
- end
-
- when TkCONSTANT then
- if container.document_self then
- parse_constant container, single, tk, comment
- end
-
- when TkALIAS then
- if container.document_self then
- parse_alias container, single, tk, comment
- end
-
- when TkYIELD then
- if current_method.nil? then
- warn "Warning: yield outside of method" if container.document_self
- else
- parse_yield container, single, tk, current_method
- end
-
- # Until and While can have a 'do', which shouldn't increase the nesting.
- # We can't solve the general case, but we can handle most occurrences by
- # ignoring a do at the end of a line.
-
- when TkUNTIL, TkWHILE then
- nest += 1
- skip_optional_do_after_expression
-
- # 'for' is trickier
- when TkFOR then
- nest += 1
- skip_for_variable
- skip_optional_do_after_expression
-
- when TkCASE, TkDO, TkIF, TkUNLESS, TkBEGIN then
- nest += 1
-
- when TkIDENTIFIER then
- if nest == 1 and current_method.nil? then
- case tk.name
- when 'private', 'protected', 'public', 'private_class_method',
- 'public_class_method', 'module_function' then
- parse_visibility container, single, tk
- keep_comment = true
- when 'attr' then
- parse_attr container, single, tk, comment
- when /^attr_(reader|writer|accessor)$/, @options.extra_accessors then
- parse_attr_accessor container, single, tk, comment
- when 'alias_method' then
- if container.document_self then
- parse_alias container, single, tk, comment
- end
- else
- if container.document_self and comment =~ /\A#\#$/ then
- parse_meta_method container, single, tk, comment
- end
- end
- end
-
- case tk.name
- when "require" then
- parse_require container, comment
- when "include" then
- parse_include container, comment
- end
-
- when TkEND then
- nest -= 1
- if nest == 0 then
- read_documentation_modifiers container, RDoc::CLASS_MODIFIERS
- container.ongoing_visibility = save_visibility
- return
- end
-
- end
-
- comment = '' unless keep_comment
-
- begin
- get_tkread
- skip_tkspace(false)
- end while peek_tk == TkNL
- end
- end
-
- def parse_symbol_arg(no = nil)
- args = []
- skip_tkspace_comment
- case tk = get_tk
- when TkLPAREN
- loop do
- skip_tkspace_comment
- if tk1 = parse_symbol_in_arg
- args.push tk1
- break if no and args.size >= no
- end
-
- skip_tkspace_comment
- case tk2 = get_tk
- when TkRPAREN
- break
- when TkCOMMA
- else
- warn("unexpected token: '#{tk2.inspect}'") if $DEBUG_RDOC
- break
- end
- end
- else
- unget_tk tk
- if tk = parse_symbol_in_arg
- args.push tk
- return args if no and args.size >= no
- end
-
- loop do
- skip_tkspace(false)
-
- tk1 = get_tk
- unless TkCOMMA === tk1 then
- unget_tk tk1
- break
- end
-
- skip_tkspace_comment
- if tk = parse_symbol_in_arg
- args.push tk
- break if no and args.size >= no
- end
- end
- end
- args
- end
-
- def parse_symbol_in_arg
- case tk = get_tk
- when TkSYMBOL
- tk.text.sub(/^:/, '')
- when TkSTRING
- eval @read[-1]
- else
- warn("Expected symbol or string, got #{tk.inspect}") if $DEBUG_RDOC
- nil
- end
- end
-
- def parse_toplevel_statements(container)
- comment = collect_first_comment
- look_for_directives_in(container, comment)
- container.comment = comment unless comment.empty?
- parse_statements container, NORMAL, nil, comment
- end
-
- def parse_visibility(container, single, tk)
- singleton = (single == SINGLE)
-
- vis_type = tk.name
-
- vis = case vis_type
- when 'private' then :private
- when 'protected' then :protected
- when 'public' then :public
- when 'private_class_method' then
- singleton = true
- :private
- when 'public_class_method' then
- singleton = true
- :public
- when 'module_function' then
- singleton = true
- :public
- else
- raise "Invalid visibility: #{tk.name}"
- end
-
- skip_tkspace_comment false
-
- case peek_tk
- # Ryan Davis suggested the extension to ignore modifiers, because he
- # often writes
- #
- # protected unless $TESTING
- #
- when TkNL, TkUNLESS_MOD, TkIF_MOD, TkSEMICOLON then
- container.ongoing_visibility = vis
- else
- if vis_type == 'module_function' then
- args = parse_symbol_arg
- container.set_visibility_for args, :private, false
-
- module_functions = []
-
- container.methods_matching args do |m|
- s_m = m.dup
- s_m.singleton = true if RDoc::AnyMethod === s_m
- s_m.visibility = :public
- module_functions << s_m
- end
-
- module_functions.each do |s_m|
- case s_m
- when RDoc::AnyMethod then
- container.add_method s_m
- when RDoc::Attr then
- container.add_attribute s_m
- end
- end
- else
- args = parse_symbol_arg
- container.set_visibility_for args, vis, singleton
- end
- end
- end
-
- def parse_yield_parameters
- parse_method_or_yield_parameters
- end
-
- def parse_yield(context, single, tk, method)
- if method.block_params.nil?
- get_tkread
- @scanner.instance_eval{@continue = false}
- method.block_params = parse_yield_parameters
- end
- end
-
- def peek_read
- @read.join('')
- end
-
- ##
- # Peek at the next token, but don't remove it from the stream
-
- def peek_tk
- unget_tk(tk = get_tk)
- tk
- end
-
- ##
- # Directives are modifier comments that can appear after class, module, or
- # method names. For example:
- #
- # def fred # :yields: a, b
- #
- # or:
- #
- # class MyClass # :nodoc:
- #
- # We return the directive name and any parameters as a two element array
-
- def read_directive(allowed)
- tk = get_tk
- result = nil
- if TkCOMMENT === tk
- if tk.text =~ /\s*:?(\w+):\s*(.*)/
- directive = $1.downcase
- if allowed.include?(directive)
- result = [directive, $2]
- end
- end
- else
- unget_tk(tk)
- end
- result
- end
-
- def read_documentation_modifiers(context, allow)
- dir = read_directive(allow)
-
- case dir[0]
- when "notnew", "not_new", "not-new" then
- context.dont_rename_initialize = true
-
- when "nodoc" then
- context.document_self = false
- if dir[1].downcase == "all"
- context.document_children = false
- end
-
- when "doc" then
- context.document_self = true
- context.force_documentation = true
-
- when "yield", "yields" then
- unless context.params.nil?
- context.params.sub!(/(,|)\s*&\w+/,'') # remove parameter &proc
- end
-
- context.block_params = dir[1]
-
- when "arg", "args" then
- context.params = dir[1]
- end if dir
- end
-
- def remove_private_comments(comment)
- comment.gsub!(/^#--\n.*?^#\+\+/m, '')
- comment.sub!(/^#--\n.*/m, '')
- end
-
- def remove_token_listener(obj)
- @token_listeners.delete(obj)
- end
-
- def reset
- @tokens = []
- @unget_read = []
- @read = []
- end
-
- def scan
- reset
-
- catch(:eof) do
- catch(:enddoc) do
- begin
- parse_toplevel_statements(@top_level)
- rescue Exception => e
- $stderr.puts <<-EOF
-
-
-RDoc failure in #{@file_name} at or around line #{@scanner.line_no} column
-#{@scanner.char_no}
-
-Before reporting this, could you check that the file you're documenting
-compiles cleanly--RDoc is not a full Ruby parser, and gets confused easily if
-fed invalid programs.
-
-The internal error was:
-
- EOF
-
- e.set_backtrace(e.backtrace[0,4])
- raise
- end
- end
- end
-
- @top_level
- end
-
- ##
- # while, until, and for have an optional do
-
- def skip_optional_do_after_expression
- skip_tkspace(false)
- tk = get_tk
- case tk
- when TkLPAREN, TkfLPAREN
- end_token = TkRPAREN
- else
- end_token = TkNL
- end
-
- nest = 0
- @scanner.instance_eval{@continue = false}
-
- loop do
- case tk
- when TkSEMICOLON
- break
- when TkLPAREN, TkfLPAREN
- nest += 1
- when TkDO
- break if nest.zero?
- when end_token
- if end_token == TkRPAREN
- nest -= 1
- break if @scanner.lex_state == EXPR_END and nest.zero?
- else
- break unless @scanner.continue
- end
- end
- tk = get_tk
- end
- skip_tkspace(false)
-
- get_tk if TkDO === peek_tk
- end
-
- ##
- # skip the var [in] part of a 'for' statement
-
- def skip_for_variable
- skip_tkspace(false)
- tk = get_tk
- skip_tkspace(false)
- tk = get_tk
- unget_tk(tk) unless TkIN === tk
- end
-
- def skip_method(container)
- meth = RDoc::AnyMethod.new "", "anon"
- parse_method_parameters(meth)
- parse_statements(container, false, meth)
- end
-
- ##
- # Skip spaces
-
- def skip_tkspace(skip_nl = true)
- tokens = []
-
- while TkSPACE === (tk = get_tk) or (skip_nl and TkNL === tk) do
- tokens.push tk
- end
-
- unget_tk(tk)
- tokens
- end
-
- ##
- # Skip spaces until a comment is found
-
- def skip_tkspace_comment(skip_nl = true)
- loop do
- skip_tkspace(skip_nl)
- return unless TkCOMMENT === peek_tk
- get_tk
- end
- end
-
- def unget_tk(tk)
- @tokens.unshift tk
- @unget_read.unshift @read.pop
-
- # Remove this token from any listeners
- @token_listeners.each do |obj|
- obj.pop_token
- end if @token_listeners
- end
-
- def warn(msg)
- return if @options.quiet
- msg = make_message msg
- $stderr.puts msg
- end
-
-end
-
diff --git a/lib/rdoc/parser/simple.rb b/lib/rdoc/parser/simple.rb
deleted file mode 100644
index cdfe686718..0000000000
--- a/lib/rdoc/parser/simple.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-require 'rdoc/parser'
-
-##
-# Parse a non-source file. We basically take the whole thing as one big
-# comment. If the first character in the file is '#', we strip leading pound
-# signs.
-
-class RDoc::Parser::Simple < RDoc::Parser
-
- parse_files_matching(//)
-
- ##
- # Prepare to parse a plain file
-
- def initialize(top_level, file_name, content, options, stats)
- super
-
- preprocess = RDoc::Markup::PreProcess.new @file_name, @options.rdoc_include
-
- preprocess.handle @content do |directive, param|
- warn "Unrecognized directive '#{directive}' in #{@file_name}"
- end
- end
-
- ##
- # Extract the file contents and attach them to the toplevel as a comment
-
- def scan
- @top_level.comment = remove_private_comments(@content)
- @top_level
- end
-
- def remove_private_comments(comment)
- comment.gsub(/^--\n.*?^\+\+/m, '').sub(/^--\n.*/m, '')
- end
-
-end
-
diff --git a/lib/rdoc/rdoc.rb b/lib/rdoc/rdoc.rb
deleted file mode 100644
index ce1cb1a93f..0000000000
--- a/lib/rdoc/rdoc.rb
+++ /dev/null
@@ -1,293 +0,0 @@
-require 'rdoc'
-
-require 'rdoc/parser'
-
-# Simple must come first
-require 'rdoc/parser/simple'
-require 'rdoc/parser/ruby'
-require 'rdoc/parser/c'
-require 'rdoc/parser/f95'
-require 'rdoc/parser/perl'
-
-require 'rdoc/stats'
-require 'rdoc/options'
-
-require 'rdoc/diagram'
-
-require 'find'
-require 'fileutils'
-require 'time'
-
-module RDoc
-
- ##
- # Encapsulate the production of rdoc documentation. Basically you can use
- # this as you would invoke rdoc from the command line:
- #
- # rdoc = RDoc::RDoc.new
- # rdoc.document(args)
- #
- # Where +args+ is an array of strings, each corresponding to an argument
- # you'd give rdoc on the command line. See rdoc/rdoc.rb for details.
-
- class RDoc
-
- Generator = Struct.new(:file_name, :class_name, :key)
-
- ##
- # Accessor for statistics. Available after each call to parse_files
-
- attr_reader :stats
-
- ##
- # This is the list of output generator that we support
-
- GENERATORS = {}
-
- $LOAD_PATH.collect do |d|
- File.expand_path d
- end.find_all do |d|
- File.directory? "#{d}/rdoc/generator"
- end.each do |dir|
- Dir.entries("#{dir}/rdoc/generator").each do |gen|
- next unless /(\w+)\.rb$/ =~ gen
- type = $1
- unless GENERATORS.has_key? type
- GENERATORS[type] = Generator.new("rdoc/generator/#{gen}",
- "#{type.upcase}".intern,
- type)
- end
- end
- end
-
- def initialize
- @stats = nil
- end
-
- ##
- # Report an error message and exit
-
- def error(msg)
- raise ::RDoc::Error, msg
- end
-
- ##
- # Create an output dir if it doesn't exist. If it does exist, but doesn't
- # contain the flag file <tt>created.rid</tt> then we refuse to use it, as
- # we may clobber some manually generated documentation
-
- def setup_output_dir(op_dir, force)
- flag_file = output_flag_file(op_dir)
- if File.exist?(op_dir)
- unless File.directory?(op_dir)
- error "'#{op_dir}' exists, and is not a directory"
- end
- begin
- created = File.read(flag_file)
- rescue SystemCallError
- error "\nDirectory #{op_dir} already exists, but it looks like it\n" +
- "isn't an RDoc directory. Because RDoc doesn't want to risk\n" +
- "destroying any of your existing files, you'll need to\n" +
- "specify a different output directory name (using the\n" +
- "--op <dir> option).\n\n"
- else
- last = (Time.parse(created) unless force rescue nil)
- end
- else
- FileUtils.mkdir_p(op_dir)
- end
- last
- end
-
- ##
- # Update the flag file in an output directory.
-
- def update_output_dir(op_dir, time)
- File.open(output_flag_file(op_dir), "w") {|f| f.puts time.rfc2822 }
- end
-
- ##
- # Return the path name of the flag file in an output directory.
-
- def output_flag_file(op_dir)
- File.join(op_dir, "created.rid")
- end
-
- ##
- # The .document file contains a list of file and directory name patterns,
- # representing candidates for documentation. It may also contain comments
- # (starting with '#')
-
- def parse_dot_doc_file(in_dir, filename, options)
- # read and strip comments
- patterns = File.read(filename).gsub(/#.*/, '')
-
- result = []
-
- patterns.split.each do |patt|
- candidates = Dir.glob(File.join(in_dir, patt))
- result.concat(normalized_file_list(options, candidates))
- end
- result
- end
-
- ##
- # Given a list of files and directories, create a list of all the Ruby
- # files they contain.
- #
- # If +force_doc+ is true we always add the given files, if false, only
- # add files that we guarantee we can parse. It is true when looking at
- # files given on the command line, false when recursing through
- # subdirectories.
- #
- # The effect of this is that if you want a file with a non-standard
- # extension parsed, you must name it explicitly.
-
- def normalized_file_list(options, relative_files, force_doc = false,
- exclude_pattern = nil)
- file_list = []
-
- relative_files.each do |rel_file_name|
- next if exclude_pattern && exclude_pattern =~ rel_file_name
- stat = File.stat(rel_file_name)
- case type = stat.ftype
- when "file"
- next if @last_created and stat.mtime < @last_created
-
- if force_doc or ::RDoc::Parser.can_parse(rel_file_name) then
- file_list << rel_file_name.sub(/^\.\//, '')
- end
- when "directory"
- next if rel_file_name == "CVS" || rel_file_name == ".svn"
- dot_doc = File.join(rel_file_name, DOT_DOC_FILENAME)
- if File.file?(dot_doc)
- file_list.concat(parse_dot_doc_file(rel_file_name, dot_doc, options))
- else
- file_list.concat(list_files_in_directory(rel_file_name, options))
- end
- else
- raise RDoc::Error, "I can't deal with a #{type} #{rel_file_name}"
- end
- end
-
- file_list
- end
-
- ##
- # Return a list of the files to be processed in a directory. We know that
- # this directory doesn't have a .document file, so we're looking for real
- # files. However we may well contain subdirectories which must be tested
- # for .document files.
-
- def list_files_in_directory(dir, options)
- files = Dir.glob File.join(dir, "*")
-
- normalized_file_list options, files, false, options.exclude
- end
-
- ##
- # Parse each file on the command line, recursively entering directories.
-
- def parse_files(options)
- @stats = Stats.new options.verbosity
-
- files = options.files
- files = ["."] if files.empty?
-
- file_list = normalized_file_list(options, files, true, options.exclude)
-
- return [] if file_list.empty?
-
- file_info = []
-
- file_list.each do |filename|
- @stats.add_file filename
-
- content = if RUBY_VERSION >= '1.9' then
- File.open(filename, "r:ascii-8bit") { |f| f.read }
- else
- File.read filename
- end
-
- if defined? Encoding then
- if /coding:\s*(\S+)/ =~ content[/\A(?:.*\n){0,2}/]
- if enc = ::Encoding.find($1)
- content.force_encoding(enc)
- end
- end
- end
-
- top_level = ::RDoc::TopLevel.new filename
-
- parser = ::RDoc::Parser.for top_level, filename, content, options,
- @stats
-
- file_info << parser.scan
- end
-
- file_info
- end
-
- ##
- # Format up one or more files according to the given arguments.
- #
- # For simplicity, _argv_ is an array of strings, equivalent to the strings
- # that would be passed on the command line. (This isn't a coincidence, as
- # we _do_ pass in ARGV when running interactively). For a list of options,
- # see rdoc/rdoc.rb. By default, output will be stored in a directory
- # called +doc+ below the current directory, so make sure you're somewhere
- # writable before invoking.
- #
- # Throws: RDoc::Error on error
-
- def document(argv)
- TopLevel::reset
-
- @options = Options.new GENERATORS
- @options.parse argv
-
- @last_created = nil
-
- unless @options.all_one_file then
- @last_created = setup_output_dir @options.op_dir, @options.force_update
- end
-
- start_time = Time.now
-
- file_info = parse_files @options
-
- @options.title = "RDoc Documentation"
-
- if file_info.empty?
- $stderr.puts "\nNo newer files." unless @options.quiet
- else
- @gen = @options.generator
-
- $stderr.puts "\nGenerating #{@gen.key.upcase}..." unless @options.quiet
-
- require @gen.file_name
-
- gen_class = ::RDoc::Generator.const_get @gen.class_name
- @gen = gen_class.for @options
-
- pwd = Dir.pwd
-
- Dir.chdir @options.op_dir unless @options.all_one_file
-
- begin
- Diagram.new(file_info, @options).draw if @options.diagram
- @gen.generate(file_info)
- update_output_dir(".", start_time)
- ensure
- Dir.chdir(pwd)
- end
- end
-
- unless @options.quiet
- puts
- @stats.print
- end
- end
- end
-end
-
diff --git a/lib/rdoc/ri.rb b/lib/rdoc/ri.rb
deleted file mode 100644
index a3a858e673..0000000000
--- a/lib/rdoc/ri.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require 'rdoc'
-
-module RDoc::RI
-
- class Error < RDoc::Error; end
-
-end
-
diff --git a/lib/rdoc/ri/cache.rb b/lib/rdoc/ri/cache.rb
deleted file mode 100644
index 06177a00de..0000000000
--- a/lib/rdoc/ri/cache.rb
+++ /dev/null
@@ -1,187 +0,0 @@
-require 'rdoc/ri'
-
-class RDoc::RI::ClassEntry
-
- attr_reader :name
- attr_reader :path_names
-
- def initialize(path_name, name, in_class)
- @path_names = [ path_name ]
- @name = name
- @in_class = in_class
- @class_methods = []
- @instance_methods = []
- @inferior_classes = []
- end
-
- # We found this class in more than one place, so add
- # in the name from there.
- def add_path(path)
- @path_names << path
- end
-
- # read in our methods and any classes
- # and modules in our namespace. Methods are
- # stored in files called name-c|i.yaml,
- # where the 'name' portion is the external
- # form of the method name and the c|i is a class|instance
- # flag
-
- def load_from(dir)
- Dir.foreach(dir) do |name|
- next if name =~ /^\./
-
- # convert from external to internal form, and
- # extract the instance/class flag
-
- if name =~ /^(.*?)-(c|i).yaml$/
- external_name = $1
- is_class_method = $2 == "c"
- internal_name = RDoc::RI::Writer.external_to_internal(external_name)
- list = is_class_method ? @class_methods : @instance_methods
- path = File.join(dir, name)
- list << RDoc::RI::MethodEntry.new(path, internal_name, is_class_method, self)
- else
- full_name = File.join(dir, name)
- if File.directory?(full_name)
- inf_class = @inferior_classes.find {|c| c.name == name }
- if inf_class
- inf_class.add_path(full_name)
- else
- inf_class = RDoc::RI::ClassEntry.new(full_name, name, self)
- @inferior_classes << inf_class
- end
- inf_class.load_from(full_name)
- end
- end
- end
- end
-
- # Return a list of any classes or modules that we contain
- # that match a given string
-
- def contained_modules_matching(name)
- @inferior_classes.find_all {|c| c.name[name]}
- end
-
- def classes_and_modules
- @inferior_classes
- end
-
- # Return an exact match to a particular name
- def contained_class_named(name)
- @inferior_classes.find {|c| c.name == name}
- end
-
- # return the list of local methods matching name
- # We're split into two because we need distinct behavior
- # when called from the _toplevel_
- def methods_matching(name, is_class_method)
- local_methods_matching(name, is_class_method)
- end
-
- # Find methods matching 'name' in ourselves and in
- # any classes we contain
- def recursively_find_methods_matching(name, is_class_method)
- res = local_methods_matching(name, is_class_method)
- @inferior_classes.each do |c|
- res.concat(c.recursively_find_methods_matching(name, is_class_method))
- end
- res
- end
-
-
- # Return our full name
- def full_name
- res = @in_class.full_name
- res << "::" unless res.empty?
- res << @name
- end
-
- # Return a list of all out method names
- def all_method_names
- res = @class_methods.map {|m| m.full_name }
- @instance_methods.each {|m| res << m.full_name}
- res
- end
-
- private
-
- # Return a list of all our methods matching a given string.
- # Is +is_class_methods+ if 'nil', we don't care if the method
- # is a class method or not, otherwise we only return
- # those methods that match
- def local_methods_matching(name, is_class_method)
-
- list = case is_class_method
- when nil then @class_methods + @instance_methods
- when true then @class_methods
- when false then @instance_methods
- else fail "Unknown is_class_method: #{is_class_method.inspect}"
- end
-
- list.find_all {|m| m.name; m.name[name]}
- end
-end
-
-##
-# A TopLevelEntry is like a class entry, but when asked to search for methods
-# searches all classes, not just itself
-
-class RDoc::RI::TopLevelEntry < RDoc::RI::ClassEntry
- def methods_matching(name, is_class_method)
- res = recursively_find_methods_matching(name, is_class_method)
- end
-
- def full_name
- ""
- end
-
- def module_named(name)
-
- end
-
-end
-
-class RDoc::RI::MethodEntry
- attr_reader :name
- attr_reader :path_name
-
- def initialize(path_name, name, is_class_method, in_class)
- @path_name = path_name
- @name = name
- @is_class_method = is_class_method
- @in_class = in_class
- end
-
- def full_name
- res = @in_class.full_name
- unless res.empty?
- if @is_class_method
- res << "::"
- else
- res << "#"
- end
- end
- res << @name
- end
-end
-
-##
-# We represent everything known about all 'ri' files accessible to this program
-
-class RDoc::RI::Cache
-
- attr_reader :toplevel
-
- def initialize(dirs)
- # At the top level we have a dummy module holding the
- # overall namespace
- @toplevel = RDoc::RI::TopLevelEntry.new('', '::', nil)
-
- dirs.each do |dir|
- @toplevel.load_from(dir)
- end
- end
-
-end
diff --git a/lib/rdoc/ri/descriptions.rb b/lib/rdoc/ri/descriptions.rb
deleted file mode 100644
index 467b7de2a9..0000000000
--- a/lib/rdoc/ri/descriptions.rb
+++ /dev/null
@@ -1,156 +0,0 @@
-require 'yaml'
-require 'rdoc/markup/fragments'
-require 'rdoc/ri'
-
-##
-# Descriptions are created by RDoc (in ri_generator) and written out in
-# serialized form into the documentation tree. ri then reads these to generate
-# the documentation
-
-class RDoc::RI::NamedThing
- attr_reader :name
- def initialize(name)
- @name = name
- end
-
- def <=>(other)
- @name <=> other.name
- end
-
- def hash
- @name.hash
- end
-
- def eql?(other)
- @name.eql?(other)
- end
-end
-
-class RDoc::RI::AliasName < RDoc::RI::NamedThing; end
-
-class RDoc::RI::Attribute < RDoc::RI::NamedThing
- attr_reader :rw, :comment
-
- def initialize(name, rw, comment)
- super(name)
- @rw = rw
- @comment = comment
- end
-end
-
-class RDoc::RI::Constant < RDoc::RI::NamedThing
- attr_reader :value, :comment
-
- def initialize(name, value, comment)
- super(name)
- @value = value
- @comment = comment
- end
-end
-
-class RDoc::RI::IncludedModule < RDoc::RI::NamedThing; end
-
-class RDoc::RI::MethodSummary < RDoc::RI::NamedThing
- def initialize(name="")
- super
- end
-end
-
-class RDoc::RI::Description
- attr_accessor :name
- attr_accessor :full_name
- attr_accessor :comment
-
- def serialize
- self.to_yaml
- end
-
- def self.deserialize(from)
- YAML.load(from)
- end
-
- def <=>(other)
- @name <=> other.name
- end
-end
-
-class RDoc::RI::ModuleDescription < RDoc::RI::Description
-
- attr_accessor :class_methods
- attr_accessor :class_method_extensions
- attr_accessor :instance_methods
- attr_accessor :instance_method_extensions
- attr_accessor :attributes
- attr_accessor :constants
- attr_accessor :includes
-
- # merge in another class description into this one
- def merge_in(old)
- merge(@class_methods, old.class_methods)
- merge(@instance_methods, old.instance_methods)
- merge(@attributes, old.attributes)
- merge(@constants, old.constants)
- merge(@includes, old.includes)
- if @comment.nil? || @comment.empty?
- @comment = old.comment
- else
- unless old.comment.nil? or old.comment.empty? then
- if @comment.nil? or @comment.empty? then
- @comment = old.comment
- else
- @comment << RDoc::Markup::Flow::RULE.new
- @comment.concat old.comment
- end
- end
- end
- end
-
- def display_name
- "Module"
- end
-
- # the 'ClassDescription' subclass overrides this
- # to format up the name of a parent
- def superclass_string
- nil
- end
-
- private
-
- def merge(into, from)
- names = {}
- into.each {|i| names[i.name] = i }
- from.each {|i| names[i.name] = i }
- into.replace(names.keys.sort.map {|n| names[n]})
- end
-end
-
-class RDoc::RI::ClassDescription < RDoc::RI::ModuleDescription
- attr_accessor :superclass
-
- def display_name
- "Class"
- end
-
- def superclass_string
- if @superclass && @superclass != "Object"
- @superclass
- else
- nil
- end
- end
-end
-
-class RDoc::RI::MethodDescription < RDoc::RI::Description
-
- attr_accessor :is_class_method
- attr_accessor :visibility
- attr_accessor :block_params
- attr_accessor :is_singleton
- attr_accessor :aliases
- attr_accessor :is_alias_for
- attr_accessor :params
- attr_accessor :source_path
-
-end
-
diff --git a/lib/rdoc/ri/display.rb b/lib/rdoc/ri/display.rb
deleted file mode 100644
index 7b0158c18a..0000000000
--- a/lib/rdoc/ri/display.rb
+++ /dev/null
@@ -1,392 +0,0 @@
-require 'rdoc/ri'
-
-# readline support might not be present, so be careful
-# when requiring it.
-begin
- require('readline')
- require('abbrev')
- CAN_USE_READLINE = true # HACK use an RDoc namespace constant
-rescue LoadError
- CAN_USE_READLINE = false
-end
-
-##
-# This is a kind of 'flag' module. If you want to write your own 'ri' display
-# module (perhaps because you're writing an IDE), you write a class which
-# implements the various 'display' methods in RDoc::RI::DefaultDisplay, and
-# include the RDoc::RI::Display module in that class.
-#
-# To access your class from the command line, you can do
-#
-# ruby -r <your source file> ../ri ....
-
-module RDoc::RI::Display
-
- @@display_class = nil
-
- def self.append_features(display_class)
- @@display_class = display_class
- end
-
- def self.new(*args)
- @@display_class.new(*args)
- end
-
-end
-
-##
-# A paging display module. Uses the RDoc::RI::Formatter class to do the actual
-# presentation.
-
-class RDoc::RI::DefaultDisplay
-
- include RDoc::RI::Display
-
- def initialize(formatter, width, use_stdout, output = $stdout)
- @use_stdout = use_stdout
- @formatter = formatter.new output, width, " "
- end
-
- ##
- # Display information about +klass+. Fetches additional information from
- # +ri_reader+ as necessary.
-
- def display_class_info(klass)
- page do
- superclass = klass.superclass
-
- if superclass
- superclass = " < " + superclass
- else
- superclass = ""
- end
-
- @formatter.draw_line(klass.display_name + ": " +
- klass.full_name + superclass)
-
- display_flow(klass.comment)
- @formatter.draw_line
-
- unless klass.includes.empty?
- @formatter.blankline
- @formatter.display_heading("Includes:", 2, "")
- incs = []
-
- klass.includes.each do |inc|
- incs << inc.name
- end
-
- @formatter.wrap(incs.sort.join(', '))
- end
-
- unless klass.constants.empty?
- @formatter.blankline
- @formatter.display_heading("Constants:", 2, "")
-
- constants = klass.constants.sort_by { |constant| constant.name }
-
- constants.each do |constant|
- @formatter.wrap "#{constant.name} = #{constant.value}"
- if constant.comment then
- @formatter.indent do
- @formatter.display_flow constant.comment
- end
- else
- @formatter.break_to_newline
- end
- end
- end
-
- unless klass.attributes.empty? then
- @formatter.blankline
- @formatter.display_heading 'Attributes:', 2, ''
-
- attributes = klass.attributes.sort_by { |attribute| attribute.name }
-
- attributes.each do |attribute|
- if attribute.comment then
- @formatter.wrap "#{attribute.name} (#{attribute.rw}):"
- @formatter.indent do
- @formatter.display_flow attribute.comment
- end
- else
- @formatter.wrap "#{attribute.name} (#{attribute.rw})"
- @formatter.break_to_newline
- end
- end
- end
-
- return display_class_method_list(klass)
- end
- end
-
- ##
- # Given a Hash mapping a class' methods to method types (returned by
- # display_class_method_list), this method allows the user to
- # choose one of the methods.
-
- def get_class_method_choice(method_map)
- if CAN_USE_READLINE
- # prepare abbreviations for tab completion
- abbreviations = method_map.keys.abbrev
- Readline.completion_proc = proc do |string|
- abbreviations.values.uniq.grep(/^#{string}/)
- end
- end
-
- @formatter.raw_print_line "\nEnter the method name you want.\n"
- @formatter.raw_print_line "Class methods can be preceeded by '::' and instance methods by '#'.\n"
-
- if CAN_USE_READLINE
- @formatter.raw_print_line "You can use tab to autocomplete.\n"
- @formatter.raw_print_line "Enter a blank line to exit.\n"
-
- choice_string = Readline.readline(">> ").strip
- else
- @formatter.raw_print_line "Enter a blank line to exit.\n"
- @formatter.raw_print_line ">> "
- choice_string = $stdin.gets.strip
- end
-
- if choice_string == ''
- return nil
- else
- class_or_instance = method_map[choice_string]
-
- if class_or_instance
- # If the user's choice is not preceeded by a '::' or a '#', figure
- # out whether they want a class or an instance method and decorate
- # the choice appropriately.
- if(choice_string =~ /^[a-zA-Z]/)
- if(class_or_instance == :class)
- choice_string = "::#{choice_string}"
- else
- choice_string = "##{choice_string}"
- end
- end
-
- return choice_string
- else
- @formatter.raw_print_line "No method matched '#{choice_string}'.\n"
- return nil
- end
- end
- end
-
-
- ##
- # Display methods on +klass+
- # Returns a hash mapping method name to method contents (HACK?)
-
- def display_class_method_list(klass)
- method_map = {}
-
- class_data = [
- :class_methods,
- :class_method_extensions,
- :instance_methods,
- :instance_method_extensions,
- ]
-
- class_data.each do |data_type|
- data = klass.send data_type
-
- unless data.nil? or data.empty? then
- @formatter.blankline
-
- heading = data_type.to_s.split('_').join(' ').capitalize << ':'
- @formatter.display_heading heading, 2, ''
-
- method_names = []
- data.each do |item|
- method_names << item.name
-
- if(data_type == :class_methods ||
- data_type == :class_method_extensions) then
- method_map["::#{item.name}"] = :class
- method_map[item.name] = :class
- else
- #
- # Since we iterate over instance methods after class methods,
- # an instance method always will overwrite the unqualified
- # class method entry for a class method of the same name.
- #
- method_map["##{item.name}"] = :instance
- method_map[item.name] = :instance
- end
- end
- method_names.sort!
-
- @formatter.wrap method_names.join(', ')
- end
- end
-
- method_map
- end
- private :display_class_method_list
-
- ##
- # Display an Array of RDoc::Markup::Flow objects, +flow+.
-
- def display_flow(flow)
- if flow and not flow.empty? then
- @formatter.display_flow flow
- else
- @formatter.wrap '[no description]'
- end
- end
-
- ##
- # Display information about +method+.
-
- def display_method_info(method)
- page do
- @formatter.draw_line(method.full_name)
- display_params(method)
-
- @formatter.draw_line
- display_flow(method.comment)
-
- if method.aliases and not method.aliases.empty? then
- @formatter.blankline
- aka = "(also known as #{method.aliases.map { |a| a.name }.join(', ')})"
- @formatter.wrap aka
- end
- end
- end
-
- ##
- # Display the list of +methods+.
-
- def display_method_list(methods)
- page do
- @formatter.wrap "More than one method matched your request. You can refine your search by asking for information on one of:"
- @formatter.blankline
-
- methods.each do |method|
- @formatter.raw_print_line "#{method.full_name} [#{method.source_path}]\n"
- end
- end
- end
-
- ##
- # Display a list of +methods+ and allow the user to select one of them.
-
- def display_method_list_choice(methods)
- page do
- @formatter.wrap "More than one method matched your request. Please choose one of the possible matches."
- @formatter.blankline
-
- methods.each_with_index do |method, index|
- @formatter.raw_print_line "%3d %s [%s]\n" % [index + 1, method.full_name, method.source_path]
- end
-
- @formatter.raw_print_line ">> "
-
- choice = $stdin.gets.strip!
-
- if(choice == '')
- return
- end
-
- choice = choice.to_i
-
- if ((choice == 0) || (choice > methods.size)) then
- @formatter.raw_print_line "Invalid choice!\n"
- else
- method = methods[choice - 1]
- display_method_info(method)
- end
- end
- end
-
- ##
- # Display the params for +method+.
-
- def display_params(method)
- params = method.params
-
- if params[0,1] == "(" then
- if method.is_singleton
- params = method.full_name + params
- else
- params = method.name + params
- end
- end
-
- params.split(/\n/).each do |param|
- @formatter.wrap param
- @formatter.break_to_newline
- end
-
- @formatter.blankline
- @formatter.wrap("From #{method.source_path}")
- end
-
- ##
- # List the classes in +classes+.
-
- def list_known_classes(classes)
- if classes.empty?
- warn_no_database
- else
- page do
- @formatter.draw_line "Known classes and modules"
- @formatter.blankline
-
- @formatter.wrap classes.sort.join(', ')
- end
- end
- end
-
- ##
- # Paginates output through a pager program.
-
- def page
- if pager = setup_pager then
- begin
- orig_output = @formatter.output
- @formatter.output = pager
- yield
- ensure
- @formatter.output = orig_output
- pager.close
- end
- else
- yield
- end
- rescue Errno::EPIPE
- end
-
- ##
- # Sets up a pager program to pass output through.
-
- def setup_pager
- unless @use_stdout then
- for pager in [ ENV['PAGER'], "less", "more", 'pager' ].compact.uniq
- return IO.popen(pager, "w") rescue nil
- end
- @use_stdout = true
- nil
- end
- end
-
- ##
- # Displays a message that describes how to build RI data.
-
- def warn_no_database
- output = @formatter.output
-
- output.puts "No ri data found"
- output.puts
- output.puts "If you've installed Ruby yourself, you need to generate documentation using:"
- output.puts
- output.puts " make install-doc"
- output.puts
- output.puts "from the same place you ran `make` to build ruby."
- output.puts
- output.puts "If you installed Ruby from a packaging system, then you may need to"
- output.puts "install an additional package, or ask the packager to enable ri generation."
- end
-
-end
diff --git a/lib/rdoc/ri/driver.rb b/lib/rdoc/ri/driver.rb
deleted file mode 100644
index 0c91232b70..0000000000
--- a/lib/rdoc/ri/driver.rb
+++ /dev/null
@@ -1,669 +0,0 @@
-require 'optparse'
-require 'yaml'
-
-require 'rdoc/ri'
-require 'rdoc/ri/paths'
-require 'rdoc/ri/formatter'
-require 'rdoc/ri/display'
-require 'fileutils'
-require 'rdoc/markup'
-require 'rdoc/markup/to_flow'
-
-class RDoc::RI::Driver
-
- #
- # This class offers both Hash and OpenStruct functionality.
- # We convert from the Core Hash to this before calling any of
- # the display methods, in order to give the display methods
- # a cleaner API for accessing the data.
- #
- class OpenStructHash < Hash
- #
- # This method converts from a Hash to an OpenStructHash.
- #
- def self.convert(object)
- case object
- when Hash then
- new_hash = new # Convert Hash -> OpenStructHash
-
- object.each do |key, value|
- new_hash[key] = convert(value)
- end
-
- new_hash
- when Array then
- object.map do |element|
- convert(element)
- end
- else
- object
- end
- end
-
- def merge_enums(other)
- other.each do |k, v|
- if self[k] then
- case v
- when Array then
- # HACK dunno
- if String === self[k] and self[k].empty? then
- self[k] = v
- else
- self[k] += v
- end
- when Hash then
- self[k].update v
- else
- # do nothing
- end
- else
- self[k] = v
- end
- end
- end
-
- def method_missing method, *args
- self[method.to_s]
- end
- end
-
- class Error < RDoc::RI::Error; end
-
- class NotFoundError < Error
- def message
- "Nothing known about #{super}"
- end
- end
-
- attr_accessor :homepath # :nodoc:
-
- def self.default_options
- options = {}
- options[:use_stdout] = !$stdout.tty?
- options[:width] = 72
- options[:formatter] = RDoc::RI::Formatter.for 'plain'
- options[:interactive] = false
- options[:use_cache] = true
-
- # By default all standard paths are used.
- options[:use_system] = true
- options[:use_site] = true
- options[:use_home] = true
- options[:use_gems] = true
- options[:extra_doc_dirs] = []
-
- return options
- end
-
- def self.process_args(argv)
- options = default_options
-
- opts = OptionParser.new do |opt|
- opt.program_name = File.basename $0
- opt.version = RDoc::VERSION
- opt.release = nil
- opt.summary_indent = ' ' * 4
-
- directories = [
- RDoc::RI::Paths::SYSDIR,
- RDoc::RI::Paths::SITEDIR,
- RDoc::RI::Paths::HOMEDIR
- ]
-
- if RDoc::RI::Paths::GEMDIRS then
- Gem.path.each do |dir|
- directories << "#{dir}/doc/*/ri"
- end
- end
-
- opt.banner = <<-EOT
-Usage: #{opt.program_name} [options] [names...]
-
-Where name can be:
-
- Class | Class::method | Class#method | Class.method | method
-
-All class names may be abbreviated to their minimum unambiguous form. If a name
-is ambiguous, all valid options will be listed.
-
-The form '.' method matches either class or instance methods, while #method
-matches only instance and ::method matches only class methods.
-
-For example:
-
- #{opt.program_name} Fil
- #{opt.program_name} File
- #{opt.program_name} File.new
- #{opt.program_name} zip
-
-Note that shell quoting may be required for method names containing
-punctuation:
-
- #{opt.program_name} 'Array.[]'
- #{opt.program_name} compact\\!
-
-By default ri searches for documentation in the following directories:
-
- #{directories.join "\n "}
-
-Specifying the --system, --site, --home, --gems or --doc-dir options will
-limit ri to searching only the specified directories.
-
-Options may also be set in the 'RI' environment variable.
- EOT
-
- opt.separator nil
- opt.separator "Options:"
- opt.separator nil
-
- opt.on("--fmt=FORMAT", "--format=FORMAT", "-f",
- RDoc::RI::Formatter::FORMATTERS.keys,
- "Format to use when displaying output:",
- " #{RDoc::RI::Formatter.list}",
- "Use 'bs' (backspace) with most pager",
- "programs. To use ANSI, either disable the",
- "pager or tell the pager to allow control",
- "characters.") do |value|
- options[:formatter] = RDoc::RI::Formatter.for value
- end
-
- opt.separator nil
-
- opt.on("--doc-dir=DIRNAME", "-d", Array,
- "List of directories from which to source",
- "documentation in addition to the standard",
- "directories. May be repeated.") do |value|
- value.each do |dir|
- unless File.directory? dir then
- raise OptionParser::InvalidArgument, "#{dir} is not a directory"
- end
-
- options[:extra_doc_dirs] << File.expand_path(dir)
- end
- end
-
- opt.separator nil
-
- opt.on("--[no-]use-cache",
- "Whether or not to use ri's cache.",
- "True by default.") do |value|
- options[:use_cache] = value
- end
-
- opt.separator nil
-
- opt.on("--no-standard-docs",
- "Do not include documentation from",
- "the Ruby standard library, site_lib,",
- "installed gems, or ~/.rdoc.",
- "Equivalent to specifying",
- "the options --no-system, --no-site, --no-gems,",
- "and --no-home") do
- options[:use_system] = false
- options[:use_site] = false
- options[:use_gems] = false
- options[:use_home] = false
- end
-
- opt.separator nil
-
- opt.on("--[no-]system",
- "Include documentation from Ruby's standard",
- "library. Defaults to true.") do |value|
- options[:use_system] = value
- end
-
- opt.separator nil
-
- opt.on("--[no-]site",
- "Include documentation from libraries",
- "installed in site_lib.",
- "Defaults to true.") do |value|
- options[:use_site] = value
- end
-
- opt.separator nil
-
- opt.on("--[no-]gems",
- "Include documentation from RubyGems.",
- "Defaults to true.") do |value|
- options[:use_gems] = value
- end
-
- opt.separator nil
-
- opt.on("--[no-]home",
- "Include documentation stored in ~/.rdoc.",
- "Defaults to true.") do |value|
- options[:use_home] = value
- end
-
- opt.separator nil
-
- opt.on("--list-doc-dirs",
- "List the directories from which ri will",
- "source documentation on stdout and exit.") do
- options[:list_doc_dirs] = true
- end
-
- opt.separator nil
-
- opt.on("--no-pager", "-T",
- "Send output directly to stdout,",
- "rather than to a pager.") do
- options[:use_stdout] = true
- end
-
- opt.on("--interactive", "-i",
- "This makes ri go into interactive mode.",
- "When ri is in interactive mode it will",
- "allow the user to disambiguate lists of",
- "methods in case multiple methods match",
- "against a method search string. It also",
- "will allow the user to enter in a method",
- "name (with auto-completion, if readline",
- "is supported) when viewing a class.") do
- options[:interactive] = true
- end
-
- opt.separator nil
-
- opt.on("--width=WIDTH", "-w", OptionParser::DecimalInteger,
- "Set the width of the output.") do |value|
- options[:width] = value
- end
- end
-
- argv = ENV['RI'].to_s.split.concat argv
-
- opts.parse! argv
-
- options[:names] = argv
-
- options[:formatter] ||= RDoc::RI::Formatter.for('plain')
- options[:use_stdout] ||= !$stdout.tty?
- options[:use_stdout] ||= options[:interactive]
- options[:width] ||= 72
-
- options
-
- rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e
- puts opts
- puts
- puts e
- exit 1
- end
-
- def self.run(argv = ARGV)
- options = process_args argv
- ri = new options
- ri.run
- end
-
- def initialize(initial_options={})
- options = self.class.default_options.update(initial_options)
-
- @names = options[:names]
- @class_cache_name = 'classes'
-
- @doc_dirs = RDoc::RI::Paths.path(options[:use_system],
- options[:use_site],
- options[:use_home],
- options[:use_gems],
- options[:extra_doc_dirs])
-
- @homepath = RDoc::RI::Paths.raw_path(false, false, true, false).first
- @homepath = @homepath.sub(/\.rdoc/, '.ri')
- @sys_dir = RDoc::RI::Paths.raw_path(true, false, false, false).first
- @list_doc_dirs = options[:list_doc_dirs]
-
- FileUtils.mkdir_p cache_file_path unless File.directory? cache_file_path
- @cache_doc_dirs_path = File.join cache_file_path, ".doc_dirs"
-
- @use_cache = options[:use_cache]
- @class_cache = nil
-
- @interactive = options[:interactive]
- @display = RDoc::RI::DefaultDisplay.new(options[:formatter],
- options[:width],
- options[:use_stdout])
- end
-
- def class_cache
- return @class_cache if @class_cache
-
- # Get the documentation directories used to make the cache in order to see
- # whether the cache is valid for the current ri instantiation.
- if(File.readable?(@cache_doc_dirs_path))
- cache_doc_dirs = IO.read(@cache_doc_dirs_path).split("\n")
- else
- cache_doc_dirs = []
- end
-
- newest = map_dirs('created.rid') do |f|
- File.mtime f if test ?f, f
- end.max
-
- # An up to date cache file must have been created more recently than
- # the last modification of any of the documentation directories. It also
- # must have been created with the same documentation directories
- # as those from which ri currently is sourcing documentation.
- up_to_date = (File.exist?(class_cache_file_path) and
- newest and newest < File.mtime(class_cache_file_path) and
- (cache_doc_dirs == @doc_dirs))
-
- if up_to_date and @use_cache then
- open class_cache_file_path, 'rb' do |fp|
- begin
- @class_cache = Marshal.load fp.read
- rescue
- #
- # This shouldn't be necessary, since the up_to_date logic above
- # should force the cache to be recreated when a new version of
- # rdoc is installed. This seems like a worthwhile enhancement
- # to ri's robustness, however.
- #
- $stderr.puts "Error reading the class cache; recreating the class cache!"
- @class_cache = create_class_cache
- end
- end
- else
- @class_cache = create_class_cache
- end
-
- @class_cache
- end
-
- def create_class_cache
- class_cache = OpenStructHash.new
-
- if(@use_cache)
- # Dump the documentation directories to a file in the cache, so that
- # we only will use the cache for future instantiations with identical
- # documentation directories.
- File.open @cache_doc_dirs_path, "wb" do |fp|
- fp << @doc_dirs.join("\n")
- end
- end
-
- classes = map_dirs('**/cdesc*.yaml') { |f| Dir[f] }
- warn "Updating class cache with #{classes.size} classes..."
- populate_class_cache class_cache, classes
-
- write_cache class_cache, class_cache_file_path
-
- class_cache
- end
-
- def populate_class_cache(class_cache, classes, extension = false)
- classes.each do |cdesc|
- desc = read_yaml cdesc
- klassname = desc["full_name"]
-
- unless class_cache.has_key? klassname then
- desc["display_name"] = "Class"
- desc["sources"] = [cdesc]
- desc["instance_method_extensions"] = []
- desc["class_method_extensions"] = []
- class_cache[klassname] = desc
- else
- klass = class_cache[klassname]
-
- if extension then
- desc["instance_method_extensions"] = desc.delete "instance_methods"
- desc["class_method_extensions"] = desc.delete "class_methods"
- end
-
- klass.merge_enums desc
- klass["sources"] << cdesc
- end
- end
- end
-
- def class_cache_file_path
- File.join cache_file_path, @class_cache_name
- end
-
- def cache_file_for(klassname)
- File.join cache_file_path, klassname.gsub(/:+/, "-")
- end
-
- def cache_file_path
- File.join @homepath, 'cache'
- end
-
- def display_class(name)
- klass = class_cache[name]
- @display.display_class_info klass
- end
-
- def display_method(method)
- @display.display_method_info method
- end
-
- def get_info_for(arg)
- @names = [arg]
- run
- end
-
- def load_cache_for(klassname)
- path = cache_file_for klassname
-
- cache = nil
-
- if File.exist? path and
- File.mtime(path) >= File.mtime(class_cache_file_path) and
- @use_cache then
- open path, 'rb' do |fp|
- begin
- cache = Marshal.load fp.read
- rescue
- #
- # The cache somehow is bad. Recreate the cache.
- #
- $stderr.puts "Error reading the cache for #{klassname}; recreating the cache!"
- cache = create_cache_for klassname, path
- end
- end
- else
- cache = create_cache_for klassname, path
- end
-
- cache
- end
-
- def create_cache_for(klassname, path)
- klass = class_cache[klassname]
- return nil unless klass
-
- method_files = klass["sources"]
- cache = OpenStructHash.new
-
- method_files.each do |f|
- system_file = f.index(@sys_dir) == 0
- Dir[File.join(File.dirname(f), "*")].each do |yaml|
- next unless yaml =~ /yaml$/
- next if yaml =~ /cdesc-[^\/]+yaml$/
-
- method = read_yaml yaml
-
- if system_file then
- method["source_path"] = "Ruby #{RDoc::RI::Paths::VERSION}"
- else
- if(f =~ %r%gems/[\d.]+/doc/([^/]+)%) then
- ext_path = "gem #{$1}"
- else
- ext_path = f
- end
-
- method["source_path"] = ext_path
- end
-
- name = method["full_name"]
- cache[name] = method
- end
- end
-
- write_cache cache, path
- end
-
- ##
- # Finds the next ancestor of +orig_klass+ after +klass+.
-
- def lookup_ancestor(klass, orig_klass)
- # This is a bit hacky, but ri will go into an infinite
- # loop otherwise, since Object has an Object ancestor
- # for some reason. Depending on the documentation state, I've seen
- # Kernel as an ancestor of Object and not as an ancestor of Object.
- if ((orig_klass == "Object") &&
- ((klass == "Kernel") || (klass == "Object")))
- return nil
- end
-
- cache = class_cache[orig_klass]
-
- return nil unless cache
-
- ancestors = [orig_klass]
- ancestors.push(*cache.includes.map { |inc| inc['name'] })
- ancestors << cache.superclass
-
- ancestor_index = ancestors.index(klass)
-
- if ancestor_index
- ancestor = ancestors[ancestors.index(klass) + 1]
- return ancestor if ancestor
- end
-
- lookup_ancestor klass, cache.superclass
- end
-
- ##
- # Finds the method
-
- def lookup_method(name, klass)
- cache = load_cache_for klass
- return nil unless cache
-
- method = cache[name.gsub('.', '#')]
- method = cache[name.gsub('.', '::')] unless method
- method
- end
-
- def map_dirs(file_name)
- @doc_dirs.map { |dir| yield File.join(dir, file_name) }.flatten.compact
- end
-
- ##
- # Extract the class and method name parts from +name+ like Foo::Bar#baz
-
- def parse_name(name)
- parts = name.split(/(::|\#|\.)/)
-
- if parts[-2] != '::' or parts.last !~ /^[A-Z]/ then
- meth = parts.pop
- parts.pop
- end
-
- klass = parts.join
-
- [klass, meth]
- end
-
- def read_yaml(path)
- data = File.read path
-
- # Necessary to be backward-compatible with documentation generated
- # by earliar RDoc versions.
- data = data.gsub(/ \!ruby\/(object|struct):(RDoc::RI|RI).*/, '')
- data = data.gsub(/ \!ruby\/(object|struct):SM::(\S+)/,
- ' !ruby/\1:RDoc::Markup::\2')
- OpenStructHash.convert(YAML.load(data))
- end
-
- def run
- if(@list_doc_dirs)
- puts @doc_dirs.join("\n")
- elsif @names.empty? then
- @display.list_known_classes class_cache.keys.sort
- else
- @names.each do |name|
- if class_cache.key? name then
- method_map = display_class name
- if(@interactive)
- method_name = @display.get_class_method_choice(method_map)
-
- if(method_name != nil)
- method = lookup_method "#{name}#{method_name}", name
- display_method method
- end
- end
- elsif name =~ /::|\#|\./ then
- klass, = parse_name name
-
- orig_klass = klass
- orig_name = name
-
- loop do
- method = lookup_method name, klass
-
- break method if method
-
- ancestor = lookup_ancestor klass, orig_klass
-
- break unless ancestor
-
- name = name.sub klass, ancestor
- klass = ancestor
- end
-
- raise NotFoundError, orig_name unless method
-
- display_method method
- else
- methods = select_methods(/#{name}/)
-
- if methods.size == 0
- raise NotFoundError, name
- elsif methods.size == 1
- display_method methods[0]
- else
- if(@interactive)
- @display.display_method_list_choice methods
- else
- @display.display_method_list methods
- end
- end
- end
- end
- end
- rescue NotFoundError => e
- abort e.message
- end
-
- def select_methods(pattern)
- methods = []
- class_cache.keys.sort.each do |klass|
- class_cache[klass]["instance_methods"].map{|h|h["name"]}.grep(pattern) do |name|
- method = load_cache_for(klass)[klass+'#'+name]
- methods << method if method
- end
- class_cache[klass]["class_methods"].map{|h|h["name"]}.grep(pattern) do |name|
- method = load_cache_for(klass)[klass+'::'+name]
- methods << method if method
- end
- end
- methods
- end
-
- def write_cache(cache, path)
- if(@use_cache)
- File.open path, "wb" do |cache_file|
- Marshal.dump cache, cache_file
- end
- end
-
- cache
- end
-
-end
diff --git a/lib/rdoc/ri/formatter.rb b/lib/rdoc/ri/formatter.rb
deleted file mode 100644
index 933882abc4..0000000000
--- a/lib/rdoc/ri/formatter.rb
+++ /dev/null
@@ -1,616 +0,0 @@
-require 'rdoc/ri'
-require 'rdoc/markup'
-
-class RDoc::RI::Formatter
-
- attr_writer :indent
- attr_accessor :output
-
- FORMATTERS = { }
-
- def self.for(name)
- FORMATTERS[name.downcase]
- end
-
- def self.list
- FORMATTERS.keys.sort.join ", "
- end
-
- def initialize(output, width, indent)
- @output = output
- @width = width
- @indent = indent
- @original_indent = indent.dup
- end
-
- def draw_line(label=nil)
- len = @width
- len -= (label.size + 1) if label
-
- if len > 0 then
- @output.print '-' * len
- if label
- @output.print ' '
- bold_print label
- end
-
- @output.puts
- else
- @output.print '-' * @width
- @output.puts
-
- @output.puts label
- end
- end
-
- def indent
- return @indent unless block_given?
-
- begin
- indent = @indent.dup
- @indent += @original_indent
- yield
- ensure
- @indent = indent
- end
- end
-
- def wrap(txt, prefix=@indent, linelen=@width)
- return unless txt && !txt.empty?
-
- work = conv_markup(txt)
- textLen = linelen - prefix.length
- patt = Regexp.new("^(.{0,#{textLen}})[ \n]")
- next_prefix = prefix.tr("^ ", " ")
-
- res = []
-
- while work.length > textLen
- if work =~ patt
- res << $1
- work.slice!(0, $&.length)
- else
- res << work.slice!(0, textLen)
- end
- end
- res << work if work.length.nonzero?
- @output.puts(prefix + res.join("\n" + next_prefix))
- end
-
- def blankline
- @output.puts
- end
-
- ##
- # Called when we want to ensure a new 'wrap' starts on a newline. Only
- # needed for HtmlFormatter, because the rest do their own line breaking.
-
- def break_to_newline
- end
-
- def bold_print(txt)
- @output.print txt
- end
-
- def raw_print_line(txt)
- @output.print txt
- end
-
- ##
- # Convert HTML entities back to ASCII
-
- def conv_html(txt)
- txt = txt.gsub(/&gt;/, '>')
- txt.gsub!(/&lt;/, '<')
- txt.gsub!(/&quot;/, '"')
- txt.gsub!(/&amp;/, '&')
- txt
- end
-
- ##
- # Convert markup into display form
-
- def conv_markup(txt)
- txt = txt.gsub(%r{<tt>(.*?)</tt>}, '+\1+')
- txt.gsub!(%r{<code>(.*?)</code>}, '+\1+')
- txt.gsub!(%r{<b>(.*?)</b>}, '*\1*')
- txt.gsub!(%r{<em>(.*?)</em>}, '_\1_')
- txt
- end
-
- def display_list(list)
- case list.type
- when :BULLET
- prefixer = proc { |ignored| @indent + "* " }
-
- when :NUMBER, :UPPERALPHA, :LOWERALPHA then
- start = case list.type
- when :NUMBER then 1
- when :UPPERALPHA then 'A'
- when :LOWERALPHA then 'a'
- end
-
- prefixer = proc do |ignored|
- res = @indent + "#{start}.".ljust(4)
- start = start.succ
- res
- end
-
- when :LABELED, :NOTE then
- longest = 0
-
- list.contents.each do |item|
- if RDoc::Markup::Flow::LI === item and item.label.length > longest then
- longest = item.label.length
- end
- end
-
- longest += 1
-
- prefixer = proc { |li| @indent + li.label.ljust(longest) }
-
- else
- raise ArgumentError, "unknown list type #{list.type}"
- end
-
- list.contents.each do |item|
- if RDoc::Markup::Flow::LI === item then
- prefix = prefixer.call item
- display_flow_item item, prefix
- else
- display_flow_item item
- end
- end
- end
-
- def display_flow_item(item, prefix = @indent)
- case item
- when RDoc::Markup::Flow::P, RDoc::Markup::Flow::LI
- wrap(conv_html(item.body), prefix)
- blankline
-
- when RDoc::Markup::Flow::LIST
- display_list(item)
-
- when RDoc::Markup::Flow::VERB
- display_verbatim_flow_item(item, @indent)
-
- when RDoc::Markup::Flow::H
- display_heading(conv_html(item.text), item.level, @indent)
-
- when RDoc::Markup::Flow::RULE
- draw_line
-
- else
- raise RDoc::Error, "Unknown flow element: #{item.class}"
- end
- end
-
- def display_verbatim_flow_item(item, prefix=@indent)
- item.body.split(/\n/).each do |line|
- @output.print @indent, conv_html(line), "\n"
- end
- blankline
- end
-
- def display_heading(text, level, indent)
- text = strip_attributes text
-
- case level
- when 1 then
- ul = "=" * text.length
- @output.puts
- @output.puts text.upcase
- @output.puts ul
-
- when 2 then
- ul = "-" * text.length
- @output.puts
- @output.puts text
- @output.puts ul
- else
- @output.print indent, text, "\n"
- end
-
- @output.puts
- end
-
- def display_flow(flow)
- flow.each do |f|
- display_flow_item(f)
- end
- end
-
- def strip_attributes(text)
- text.gsub(/(<\/?(?:b|code|em|i|tt)>)/, '')
- end
-
-end
-
-##
-# Handle text with attributes. We're a base class: there are different
-# presentation classes (one, for example, uses overstrikes to handle bold and
-# underlining, while another using ANSI escape sequences.
-
-class RDoc::RI::AttributeFormatter < RDoc::RI::Formatter
-
- BOLD = 1
- ITALIC = 2
- CODE = 4
-
- ATTR_MAP = {
- "b" => BOLD,
- "code" => CODE,
- "em" => ITALIC,
- "i" => ITALIC,
- "tt" => CODE
- }
-
- AttrChar = Struct.new :char, :attr
-
- class AttributeString
- attr_reader :txt
-
- def initialize
- @txt = []
- @optr = 0
- end
-
- def <<(char)
- @txt << char
- end
-
- def empty?
- @optr >= @txt.length
- end
-
- # accept non space, then all following spaces
- def next_word
- start = @optr
- len = @txt.length
-
- while @optr < len && @txt[@optr].char != " "
- @optr += 1
- end
-
- while @optr < len && @txt[@optr].char == " "
- @optr += 1
- end
-
- @txt[start...@optr]
- end
- end
-
- ##
- # Overrides base class. Looks for <tt>...</tt> etc sequences and generates
- # an array of AttrChars. This array is then used as the basis for the
- # split.
-
- def wrap(txt, prefix=@indent, linelen=@width)
- return unless txt && !txt.empty?
-
- txt = add_attributes_to(txt)
- next_prefix = prefix.tr("^ ", " ")
- linelen -= prefix.size
-
- line = []
-
- until txt.empty?
- word = txt.next_word
- if word.size + line.size > linelen
- write_attribute_text(prefix, line)
- prefix = next_prefix
- line = []
- end
- line.concat(word)
- end
-
- write_attribute_text(prefix, line) if line.length > 0
- end
-
- protected
-
- def write_attribute_text(prefix, line)
- @output.print prefix
- line.each do |achar|
- @output.print achar.char
- end
- @output.puts
- end
-
- def bold_print(txt)
- @output.print txt
- end
-
- private
-
- def add_attributes_to(txt)
- tokens = txt.split(%r{(</?(?:b|code|em|i|tt)>)})
- text = AttributeString.new
- attributes = 0
- tokens.each do |tok|
- case tok
- when %r{^</(\w+)>$} then attributes &= ~(ATTR_MAP[$1]||0)
- when %r{^<(\w+)>$} then attributes |= (ATTR_MAP[$1]||0)
- else
- tok.split(//).each {|ch| text << AttrChar.new(ch, attributes)}
- end
- end
- text
- end
-
-end
-
-##
-# This formatter generates overstrike-style formatting, which works with
-# pagers such as man and less.
-
-class RDoc::RI::OverstrikeFormatter < RDoc::RI::AttributeFormatter
-
- BS = "\C-h"
-
- def write_attribute_text(prefix, line)
- @output.print prefix
-
- line.each do |achar|
- attr = achar.attr
- @output.print "_", BS if (attr & (ITALIC + CODE)) != 0
- @output.print achar.char, BS if (attr & BOLD) != 0
- @output.print achar.char
- end
-
- @output.puts
- end
-
- ##
- # Draw a string in bold
-
- def bold_print(text)
- text.split(//).each do |ch|
- @output.print ch, BS, ch
- end
- end
-
-end
-
-##
-# This formatter uses ANSI escape sequences to colorize stuff works with
-# pagers such as man and less.
-
-class RDoc::RI::AnsiFormatter < RDoc::RI::AttributeFormatter
-
- def initialize(*args)
- super
- @output.print "\033[0m"
- end
-
- def write_attribute_text(prefix, line)
- @output.print prefix
- curr_attr = 0
- line.each do |achar|
- attr = achar.attr
- if achar.attr != curr_attr
- update_attributes(achar.attr)
- curr_attr = achar.attr
- end
- @output.print achar.char
- end
- update_attributes(0) unless curr_attr.zero?
- @output.puts
- end
-
- def bold_print(txt)
- @output.print "\033[1m#{txt}\033[m"
- end
-
- HEADINGS = {
- 1 => ["\033[1;32m", "\033[m"],
- 2 => ["\033[4;32m", "\033[m"],
- 3 => ["\033[32m", "\033[m"],
- }
-
- def display_heading(text, level, indent)
- level = 3 if level > 3
- heading = HEADINGS[level]
- @output.print indent
- @output.print heading[0]
- @output.print strip_attributes(text)
- @output.puts heading[1]
- end
-
- private
-
- ATTR_MAP = {
- BOLD => "1",
- ITALIC => "33",
- CODE => "36"
- }
-
- def update_attributes(attr)
- str = "\033["
- for quality in [ BOLD, ITALIC, CODE]
- unless (attr & quality).zero?
- str << ATTR_MAP[quality]
- end
- end
- @output.print str, "m"
- end
-
-end
-
-##
-# This formatter uses HTML.
-
-class RDoc::RI::HtmlFormatter < RDoc::RI::AttributeFormatter
-
- def write_attribute_text(prefix, line)
- curr_attr = 0
- line.each do |achar|
- attr = achar.attr
- if achar.attr != curr_attr
- update_attributes(curr_attr, achar.attr)
- curr_attr = achar.attr
- end
- @output.print(escape(achar.char))
- end
- update_attributes(curr_attr, 0) unless curr_attr.zero?
- end
-
- def draw_line(label=nil)
- if label != nil
- bold_print(label)
- end
- @output.puts("<hr>")
- end
-
- def bold_print(txt)
- tag("b") { txt }
- end
-
- def blankline()
- @output.puts("<p>")
- end
-
- def break_to_newline
- @output.puts("<br>")
- end
-
- def display_heading(text, level, indent)
- level = 4 if level > 4
- tag("h#{level}") { text }
- @output.puts
- end
-
- def display_list(list)
- case list.type
- when :BULLET then
- list_type = "ul"
- prefixer = proc { |ignored| "<li>" }
-
- when :NUMBER, :UPPERALPHA, :LOWERALPHA then
- list_type = "ol"
- prefixer = proc { |ignored| "<li>" }
-
- when :LABELED then
- list_type = "dl"
- prefixer = proc do |li|
- "<dt><b>" + escape(li.label) + "</b><dd>"
- end
-
- when :NOTE then
- list_type = "table"
- prefixer = proc do |li|
- %{<tr valign="top"><td>#{li.label.gsub(/ /, '&nbsp;')}</td><td>}
- end
- else
- fail "unknown list type"
- end
-
- @output.print "<#{list_type}>"
- list.contents.each do |item|
- if item.kind_of? RDoc::Markup::Flow::LI
- prefix = prefixer.call(item)
- @output.print prefix
- display_flow_item(item, prefix)
- else
- display_flow_item(item)
- end
- end
- @output.print "</#{list_type}>"
- end
-
- def display_verbatim_flow_item(item, prefix=@indent)
- @output.print("<pre>")
- item.body.split(/\n/).each do |line|
- @output.puts conv_html(line)
- end
- @output.puts("</pre>")
- end
-
- private
-
- ATTR_MAP = {
- BOLD => "b>",
- ITALIC => "i>",
- CODE => "tt>"
- }
-
- def update_attributes(current, wanted)
- str = ""
- # first turn off unwanted ones
- off = current & ~wanted
- for quality in [ BOLD, ITALIC, CODE]
- if (off & quality) > 0
- str << "</" + ATTR_MAP[quality]
- end
- end
-
- # now turn on wanted
- for quality in [ BOLD, ITALIC, CODE]
- unless (wanted & quality).zero?
- str << "<" << ATTR_MAP[quality]
- end
- end
- @output.print str
- end
-
- def tag(code)
- @output.print("<#{code}>")
- @output.print(yield)
- @output.print("</#{code}>")
- end
-
- def escape(str)
- str = str.gsub(/&/n, '&amp;')
- str.gsub!(/\"/n, '&quot;')
- str.gsub!(/>/n, '&gt;')
- str.gsub!(/</n, '&lt;')
- str
- end
-
-end
-
-##
-# This formatter reduces extra lines for a simpler output. It improves way
-# output looks for tools like IRC bots.
-
-class RDoc::RI::SimpleFormatter < RDoc::RI::Formatter
-
- ##
- # No extra blank lines
-
- def blankline
- end
-
- ##
- # Display labels only, no lines
-
- def draw_line(label=nil)
- unless label.nil? then
- bold_print(label)
- @output.puts
- end
- end
-
- ##
- # Place heading level indicators inline with heading.
-
- def display_heading(text, level, indent)
- text = strip_attributes(text)
- case level
- when 1
- @output.puts "= " + text.upcase
- when 2
- @output.puts "-- " + text
- else
- @output.print indent, text, "\n"
- end
- end
-
-end
-
-RDoc::RI::Formatter::FORMATTERS['plain'] = RDoc::RI::Formatter
-RDoc::RI::Formatter::FORMATTERS['simple'] = RDoc::RI::SimpleFormatter
-RDoc::RI::Formatter::FORMATTERS['bs'] = RDoc::RI::OverstrikeFormatter
-RDoc::RI::Formatter::FORMATTERS['ansi'] = RDoc::RI::AnsiFormatter
-RDoc::RI::Formatter::FORMATTERS['html'] = RDoc::RI::HtmlFormatter
diff --git a/lib/rdoc/ri/paths.rb b/lib/rdoc/ri/paths.rb
deleted file mode 100644
index 6279723529..0000000000
--- a/lib/rdoc/ri/paths.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-require 'rdoc/ri'
-
-##
-# Encapsulate all the strangeness to do with finding out where to find RDoc
-# files
-#
-# We basically deal with three directories:
-#
-# 1. The 'system' documentation directory, which holds the documentation
-# distributed with Ruby, and which is managed by the Ruby install process
-# 2. The 'site' directory, which contains site-wide documentation added
-# locally.
-# 3. The 'user' documentation directory, stored under the user's own home
-# directory.
-#
-# There's contention about all this, but for now:
-#
-# system:: $datadir/ri/<ver>/system/...
-# site:: $datadir/ri/<ver>/site/...
-# user:: ~/.rdoc
-
-module RDoc::RI::Paths
-
- #:stopdoc:
- require 'rbconfig'
-
- DOC_DIR = "doc/rdoc"
-
- VERSION = RbConfig::CONFIG['ruby_version']
-
- base = File.join(RbConfig::CONFIG['datadir'], "ri", VERSION)
- SYSDIR = File.join(base, "system")
- SITEDIR = File.join(base, "site")
- homedir = ENV['HOME'] || ENV['USERPROFILE'] || ENV['HOMEPATH']
-
- if homedir then
- HOMEDIR = File.join(homedir, ".rdoc")
- else
- HOMEDIR = nil
- end
-
- begin
- require 'rubygems' unless defined?(Gem)
-
- # HACK dup'd from Gem.latest_partials and friends
- all_paths = []
-
- all_paths = Gem.path.map do |dir|
- Dir[File.join(dir, 'doc', '*', 'ri')]
- end.flatten
-
- ri_paths = {}
-
- all_paths.each do |dir|
- base = File.basename File.dirname(dir)
- if base =~ /(.*)-((\d+\.)*\d+)/ then
- name, version = $1, $2
- ver = Gem::Version.new version
- if ri_paths[name].nil? or ver > ri_paths[name][0] then
- ri_paths[name] = [ver, dir]
- end
- end
- end
-
- GEMDIRS = ri_paths.map { |k,v| v.last }.sort
- rescue LoadError
- GEMDIRS = []
- end
-
- # Returns the selected documentation directories as an Array, or PATH if no
- # overriding directories were given.
-
- def self.path(use_system, use_site, use_home, use_gems, *extra_dirs)
- path = raw_path(use_system, use_site, use_home, use_gems, *extra_dirs)
- return path.select { |directory| File.directory? directory }
- end
-
- # Returns the selected documentation directories including nonexistent
- # directories. Used to print out what paths were searched if no ri was
- # found.
-
- def self.raw_path(use_system, use_site, use_home, use_gems, *extra_dirs)
- path = []
- path << extra_dirs unless extra_dirs.empty?
- path << SYSDIR if use_system
- path << SITEDIR if use_site
- path << HOMEDIR if use_home
- path << GEMDIRS if use_gems
-
- return path.flatten.compact
- end
-end
diff --git a/lib/rdoc/ri/reader.rb b/lib/rdoc/ri/reader.rb
deleted file mode 100644
index de3c8d9afa..0000000000
--- a/lib/rdoc/ri/reader.rb
+++ /dev/null
@@ -1,106 +0,0 @@
-require 'rdoc/ri'
-require 'rdoc/ri/descriptions'
-require 'rdoc/ri/writer'
-require 'rdoc/markup/to_flow'
-
-class RDoc::RI::Reader
-
- def initialize(ri_cache)
- @cache = ri_cache
- end
-
- def top_level_namespace
- [ @cache.toplevel ]
- end
-
- def lookup_namespace_in(target, namespaces)
- result = []
- for n in namespaces
- result.concat(n.contained_modules_matching(target))
- end
- result
- end
-
- def find_class_by_name(full_name)
- names = full_name.split(/::/)
- ns = @cache.toplevel
- for name in names
- ns = ns.contained_class_named(name)
- return nil if ns.nil?
- end
- get_class(ns)
- end
-
- def find_methods(name, is_class_method, namespaces)
- result = []
- namespaces.each do |ns|
- result.concat ns.methods_matching(name, is_class_method)
- end
- result
- end
-
- ##
- # Return the MethodDescription for a given MethodEntry by deserializing the
- # YAML
-
- def get_method(method_entry)
- path = method_entry.path_name
- File.open(path) { |f| RDoc::RI::Description.deserialize(f) }
- end
-
- ##
- # Return a class description
-
- def get_class(class_entry)
- result = nil
- for path in class_entry.path_names
- path = RDoc::RI::Writer.class_desc_path(path, class_entry)
- desc = File.open(path) {|f| RDoc::RI::Description.deserialize(f) }
- if result
- result.merge_in(desc)
- else
- result = desc
- end
- end
- result
- end
-
- ##
- # Return the names of all classes and modules
-
- def full_class_names
- res = []
- find_classes_in(res, @cache.toplevel)
- end
-
- ##
- # Return a list of all classes, modules, and methods
-
- def all_names
- res = []
- find_names_in(res, @cache.toplevel)
- end
-
- private
-
- def find_classes_in(res, klass)
- classes = klass.classes_and_modules
- for c in classes
- res << c.full_name
- find_classes_in(res, c)
- end
- res
- end
-
- def find_names_in(res, klass)
- classes = klass.classes_and_modules
- for c in classes
- res << c.full_name
- res.concat c.all_method_names
- find_names_in(res, c)
- end
- res
- end
-
-end
-
diff --git a/lib/rdoc/ri/util.rb b/lib/rdoc/ri/util.rb
deleted file mode 100644
index 4e91eb978d..0000000000
--- a/lib/rdoc/ri/util.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-require 'rdoc/ri'
-
-##
-# Break argument into its constituent class or module names, an
-# optional method type, and a method name
-
-class RDoc::RI::NameDescriptor
-
- attr_reader :class_names
- attr_reader :method_name
-
- ##
- # true and false have the obvious meaning. nil means we don't care
-
- attr_reader :is_class_method
-
- ##
- # +arg+ may be
- #
- # 1. A class or module name (optionally qualified with other class or module
- # names (Kernel, File::Stat etc)
- # 2. A method name
- # 3. A method name qualified by a optionally fully qualified class or module
- # name
- #
- # We're fairly casual about delimiters: folks can say Kernel::puts,
- # Kernel.puts, or Kernel\#puts for example. There's one exception: if you
- # say IO::read, we look for a class method, but if you say IO.read, we look
- # for an instance method
-
- def initialize(arg)
- @class_names = []
- separator = nil
-
- tokens = arg.split(/(\.|::|#)/)
-
- # Skip leading '::', '#' or '.', but remember it might
- # be a method name qualifier
- separator = tokens.shift if tokens[0] =~ /^(\.|::|#)/
-
- # Skip leading '::', but remember we potentially have an inst
-
- # leading stuff must be class names
-
- while tokens[0] =~ /^[A-Z]/
- @class_names << tokens.shift
- unless tokens.empty?
- separator = tokens.shift
- break unless separator == "::"
- end
- end
-
- # Now must have a single token, the method name, or an empty array
- unless tokens.empty?
- @method_name = tokens.shift
- # We may now have a trailing !, ?, or = to roll into
- # the method name
- if !tokens.empty? && tokens[0] =~ /^[!?=]$/
- @method_name << tokens.shift
- end
-
- if @method_name =~ /::|\.|#/ or !tokens.empty?
- raise RDoc::RI::Error.new("Bad argument: #{arg}")
- end
- if separator && separator != '.'
- @is_class_method = separator == "::"
- end
- end
- end
-
- # Return the full class name (with '::' between the components) or "" if
- # there's no class name
-
- def full_class_name
- @class_names.join("::")
- end
-
-end
-
diff --git a/lib/rdoc/ri/writer.rb b/lib/rdoc/ri/writer.rb
deleted file mode 100644
index 92aaa1c2da..0000000000
--- a/lib/rdoc/ri/writer.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-require 'fileutils'
-require 'rdoc/ri'
-
-class RDoc::RI::Writer
-
- def self.class_desc_path(dir, class_desc)
- File.join(dir, "cdesc-" + class_desc.name + ".yaml")
- end
-
- ##
- # Convert a name from internal form (containing punctuation) to an external
- # form (where punctuation is replaced by %xx)
-
- def self.internal_to_external(name)
- if ''.respond_to? :ord then
- name.gsub(/\W/) { "%%%02x" % $&[0].ord }
- else
- name.gsub(/\W/) { "%%%02x" % $&[0] }
- end
- end
-
- ##
- # And the reverse operation
-
- def self.external_to_internal(name)
- name.gsub(/%([0-9a-f]{2,2})/) { $1.to_i(16).chr }
- end
-
- def initialize(base_dir)
- @base_dir = base_dir
- end
-
- def remove_class(class_desc)
- FileUtils.rm_rf(path_to_dir(class_desc.full_name))
- end
-
- def add_class(class_desc)
- dir = path_to_dir(class_desc.full_name)
- FileUtils.mkdir_p(dir)
- class_file_name = self.class.class_desc_path(dir, class_desc)
- File.open(class_file_name, "w") do |f|
- f.write(class_desc.serialize)
- end
- end
-
- def add_method(class_desc, method_desc)
- dir = path_to_dir(class_desc.full_name)
- file_name = self.class.internal_to_external(method_desc.name)
- meth_file_name = File.join(dir, file_name)
- if method_desc.is_singleton
- meth_file_name += "-c.yaml"
- else
- meth_file_name += "-i.yaml"
- end
-
- File.open(meth_file_name, "w") do |f|
- f.write(method_desc.serialize)
- end
- end
-
- private
-
- def path_to_dir(class_name)
- File.join(@base_dir, *class_name.split('::'))
- end
-
-end
-
diff --git a/lib/rdoc/stats.rb b/lib/rdoc/stats.rb
deleted file mode 100644
index e18e3c23d7..0000000000
--- a/lib/rdoc/stats.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-require 'rdoc'
-
-##
-# Simple stats collector
-
-class RDoc::Stats
-
- attr_reader :num_classes
- attr_reader :num_files
- attr_reader :num_methods
- attr_reader :num_modules
-
- def initialize(verbosity = 1)
- @num_classes = 0
- @num_files = 0
- @num_methods = 0
- @num_modules = 0
-
- @start = Time.now
-
- @display = case verbosity
- when 0 then Quiet.new
- when 1 then Normal.new
- else Verbose.new
- end
- end
-
- def add_alias(as)
- @display.print_alias as
- @num_methods += 1
- end
-
- def add_class(klass)
- @display.print_class klass
- @num_classes += 1
- end
-
- def add_file(file)
- @display.print_file file
- @num_files += 1
- end
-
- def add_method(method)
- @display.print_method method
- @num_methods += 1
- end
-
- def add_module(mod)
- @display.print_module mod
- @num_modules += 1
- end
-
- def print
- puts "Files: #@num_files"
- puts "Classes: #@num_classes"
- puts "Modules: #@num_modules"
- puts "Methods: #@num_methods"
- puts "Elapsed: " + sprintf("%0.1fs", Time.now - @start)
- end
-
- class Quiet
- def print_alias(*) end
- def print_class(*) end
- def print_file(*) end
- def print_method(*) end
- def print_module(*) end
- end
-
- class Normal
- def print_alias(as)
- print 'a'
- end
-
- def print_class(klass)
- print 'C'
- end
-
- def print_file(file)
- print "\n#{file}: "
- end
-
- def print_method(method)
- print 'm'
- end
-
- def print_module(mod)
- print 'M'
- end
- end
-
- class Verbose
- def print_alias(as)
- puts "\t\talias #{as.new_name} #{as.old_name}"
- end
-
- def print_class(klass)
- puts "\tclass #{klass.full_name}"
- end
-
- def print_file(file)
- puts file
- end
-
- def print_method(method)
- puts "\t\t#{method.singleton ? '::' : '#'}#{method.name}"
- end
-
- def print_module(mod)
- puts "\tmodule #{mod.full_name}"
- end
- end
-
-end
-
-
diff --git a/lib/rdoc/template.rb b/lib/rdoc/template.rb
deleted file mode 100644
index 53d0e3ce68..0000000000
--- a/lib/rdoc/template.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-require 'erb'
-
-module RDoc; end
-
-##
-# An ERb wrapper that allows nesting of one ERb template inside another.
-#
-# This TemplatePage operates similarly to RDoc 1.x's TemplatePage, but uses
-# ERb instead of a custom template language.
-#
-# Converting from a RDoc 1.x template to an RDoc 2.x template is fairly easy.
-#
-# * %blah% becomes <%= values["blah"] %>
-# * !INCLUDE! becomes <%= template_include %>
-# * HREF:aref:name becomes <%= href values["aref"], values["name"] %>
-# * IF:blah becomes <% if values["blah"] then %>
-# * IFNOT:blah becomes <% unless values["blah"] then %>
-# * ENDIF:blah becomes <% end %>
-# * START:blah becomes <% values["blah"].each do |blah| %>
-# * END:blah becomes <% end %>
-#
-# To make nested loops easier to convert, start by converting START statements
-# to:
-#
-# <% values["blah"].each do |blah| $stderr.puts blah.keys %>
-#
-# So you can see what is being used inside which loop.
-
-class RDoc::TemplatePage
-
- ##
- # Create a new TemplatePage that will use +templates+.
-
- def initialize(*templates)
- @templates = templates
- end
-
- ##
- # Returns "<a href=\"#{ref}\">#{name}</a>"
-
- def href(ref, name)
- if ref then
- "<a href=\"#{ref}\">#{name}</a>"
- else
- name
- end
- end
-
- ##
- # Process the template using +values+, writing the result to +io+.
-
- def write_html_on(io, values)
- b = binding
- template_include = ""
-
- @templates.reverse_each do |template|
- template_include = ERB.new(template).result b
- end
-
- io.write template_include
- end
-
-end
-
diff --git a/lib/rdoc/tokenstream.rb b/lib/rdoc/tokenstream.rb
deleted file mode 100644
index 0a1eb9130b..0000000000
--- a/lib/rdoc/tokenstream.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module RDoc; end
-
-##
-# A TokenStream is a list of tokens, gathered during the parse of some entity
-# (say a method). Entities populate these streams by being registered with the
-# lexer. Any class can collect tokens by including TokenStream. From the
-# outside, you use such an object by calling the start_collecting_tokens
-# method, followed by calls to add_token and pop_token.
-
-module RDoc::TokenStream
-
- def token_stream
- @token_stream
- end
-
- def start_collecting_tokens
- @token_stream = []
- end
-
- def add_token(tk)
- @token_stream << tk
- end
-
- def add_tokens(tks)
- tks.each {|tk| add_token(tk)}
- end
-
- def pop_token
- @token_stream.pop
- end
-
-end
-
diff --git a/lib/resolv-replace.rb b/lib/resolv-replace.rb
deleted file mode 100644
index 63d58cea27..0000000000
--- a/lib/resolv-replace.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-require 'socket'
-require 'resolv'
-
-class << IPSocket
- alias original_resolv_getaddress getaddress
- def getaddress(host)
- begin
- return Resolv.getaddress(host).to_s
- rescue Resolv::ResolvError
- raise SocketError, "Hostname not known: #{host}"
- end
- end
-end
-
-class TCPSocket
- alias original_resolv_initialize initialize
- def initialize(host, serv, *rest)
- rest[0] = IPSocket.getaddress(rest[0]) unless rest.empty?
- original_resolv_initialize(IPSocket.getaddress(host), serv, *rest)
- end
-end
-
-class UDPSocket
- alias original_resolv_bind bind
- def bind(host, port)
- host = IPSocket.getaddress(host) if host != ""
- original_resolv_bind(host, port)
- end
-
- alias original_resolv_connect connect
- def connect(host, port)
- original_resolv_connect(IPSocket.getaddress(host), port)
- end
-
- alias original_resolv_send send
- def send(mesg, flags, *rest)
- if rest.length == 2
- host, port = rest
- begin
- addrs = Resolv.getaddresses(host)
- rescue Resolv::ResolvError
- raise SocketError, "Hostname not known: #{host}"
- end
- err = nil
- addrs[0...-1].each {|addr|
- begin
- return original_resolv_send(mesg, flags, addr, port)
- rescue SystemCallError
- end
- }
- original_resolv_send(mesg, flags, addrs[-1], port)
- else
- original_resolv_send(mesg, flags, *rest)
- end
- end
-end
-
-class SOCKSSocket
- alias original_resolv_initialize initialize
- def initialize(host, serv)
- original_resolv_initialize(IPSocket.getaddress(host), port)
- end
-end if defined? SOCKSSocket
diff --git a/lib/resolv.gemspec b/lib/resolv.gemspec
new file mode 100644
index 0000000000..66aed34e01
--- /dev/null
+++ b/lib/resolv.gemspec
@@ -0,0 +1,29 @@
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1).join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Tanaka Akira"]
+ spec.email = ["akr@fsij.org"]
+
+ spec.summary = %q{Thread-aware DNS resolver library in Ruby.}
+ spec.description = %q{Thread-aware DNS resolver library in Ruby.}
+ spec.homepage = "https://github.com/ruby/resolv"
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+ spec.extensions << "ext/win32/resolv/extconf.rb"
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ excludes = %W[/.git* /bin /test /*file /#{File.basename(__FILE__)}]
+ spec.files = IO.popen(%W[git -C #{__dir__} ls-files -z --] + excludes.map {|e| ":^#{e}"}, &:read).split("\x0")
+ spec.bindir = "exe"
+ spec.executables = []
+ spec.require_paths = ["lib"]
+end
diff --git a/lib/resolv.rb b/lib/resolv.rb
index fc3c78215b..6b58f92813 100644
--- a/lib/resolv.rb
+++ b/lib/resolv.rb
@@ -1,18 +1,16 @@
+# frozen_string_literal: true
+
require 'socket'
-require 'fcntl'
require 'timeout'
-require 'thread'
-
-begin
- require 'securerandom'
-rescue LoadError
-end
+require 'io/wait'
+require 'securerandom'
+require 'rbconfig'
# Resolv is a thread-aware DNS resolver library written in Ruby. Resolv can
-# handle multiple DNS requests concurrently without blocking. The ruby
+# handle multiple DNS requests concurrently without blocking the entire Ruby
# interpreter.
#
-# See also resolv-replace.rb to replace the libc resolver with # Resolv.
+# See also resolv-replace.rb to replace the libc resolver with Resolv.
#
# Resolv can look up various DNS resources using the DNS module directly.
#
@@ -23,7 +21,7 @@ end
#
# Resolv::DNS.open do |dns|
# ress = dns.getresources "www.ruby-lang.org", Resolv::DNS::Resource::IN::A
-# p ress.map { |r| r.address }
+# p ress.map(&:address)
# ress = dns.getresources "ruby-lang.org", Resolv::DNS::Resource::IN::MX
# p ress.map { |r| [r.exchange.to_s, r.preference] }
# end
@@ -36,6 +34,9 @@ end
class Resolv
+ # The version string
+ VERSION = "0.7.1"
+
##
# Looks up the first IP address for +name+.
@@ -80,9 +81,22 @@ class Resolv
##
# Creates a new Resolv using +resolvers+.
+ #
+ # If +resolvers+ is not given, a hash, or +nil+, uses a Hosts resolver and
+ # and a DNS resolver. If +resolvers+ is a hash, uses the hash as
+ # configuration for the DNS resolver.
- def initialize(resolvers=[Hosts.new, DNS.new])
- @resolvers = resolvers
+ def initialize(resolvers=(arg_not_set = true; nil), use_ipv6: (keyword_not_set = true; nil))
+ if !keyword_not_set && !arg_not_set
+ warn "Support for separate use_ipv6 keyword is deprecated, as it is ignored if an argument is provided. Do not provide a positional argument if using the use_ipv6 keyword argument.", uplevel: 1
+ end
+
+ @resolvers = case resolvers
+ when Hash, nil
+ [Hosts.new, DNS.new(DNS::Config.default_config_hash.merge(resolvers || {}))]
+ else
+ resolvers
+ end
end
##
@@ -159,25 +173,28 @@ class Resolv
##
# Indicates a timeout resolving a name or address.
- class ResolvTimeout < TimeoutError; end
+ class ResolvTimeout < Timeout::Error; end
##
- # DNS::Hosts is a hostname resolver that uses the system hosts file.
+ # Resolv::Hosts is a hostname resolver that uses the system hosts file.
class Hosts
- if /mswin32|mingw|bccwin/ =~ RUBY_PLATFORM
- require 'win32/resolv'
- DefaultFileName = Win32::Resolv.get_hosts_path
- else
- DefaultFileName = '/etc/hosts'
+ if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM || ::RbConfig::CONFIG['host_os'] =~ /mswin/
+ begin
+ require 'win32/resolv' unless defined?(Win32::Resolv)
+ hosts = Win32::Resolv.get_hosts_path || IO::NULL
+ rescue LoadError
+ end
end
+ # The default file name for host names
+ DefaultFileName = hosts || '/etc/hosts'
##
- # Creates a new DNS::Hosts, using +filename+ for its data source.
+ # Creates a new Resolv::Hosts, using +filename+ for its data source.
def initialize(filename = DefaultFileName)
@filename = filename
- @mutex = Mutex.new
+ @mutex = Thread::Mutex.new
@initialized = nil
end
@@ -186,23 +203,13 @@ class Resolv
unless @initialized
@name2addr = {}
@addr2name = {}
- open(@filename) {|f|
+ File.open(@filename, 'rb') {|f|
f.each {|line|
line.sub!(/#.*/, '')
- addr, hostname, *aliases = line.split(/\s+/)
+ addr, *hostnames = line.split(/\s+/)
next unless addr
- addr.untaint
- hostname.untaint
- @addr2name[addr] = [] unless @addr2name.include? addr
- @addr2name[addr] << hostname
- @addr2name[addr] += aliases
- @name2addr[hostname] = [] unless @name2addr.include? hostname
- @name2addr[hostname] << addr
- aliases.each {|n|
- n.untaint
- @name2addr[n] = [] unless @name2addr.include? n
- @name2addr[n] << addr
- }
+ (@addr2name[addr] ||= []).concat(hostnames)
+ hostnames.each {|hostname| (@name2addr[hostname] ||= []) << addr}
}
}
@name2addr.each {|name, arr| arr.reverse!}
@@ -234,9 +241,7 @@ class Resolv
def each_address(name, &proc)
lazy_initialize
- if @name2addr.include?(name)
- @name2addr[name].each(&proc)
- end
+ @name2addr[name]&.each(&proc)
end
##
@@ -261,9 +266,7 @@ class Resolv
def each_name(address, &proc)
lazy_initialize
- if @addr2name.include?(address)
- @addr2name[address].each(&proc)
- end
+ @addr2name[address]&.each(&proc)
end
end
@@ -313,6 +316,18 @@ class Resolv
# nil:: Uses /etc/resolv.conf.
# String:: Path to a file using /etc/resolv.conf's format.
# Hash:: Must contain :nameserver, :search and :ndots keys.
+ # :nameserver_port can be used to specify port number of nameserver address.
+ # :raise_timeout_errors can be used to raise timeout errors
+ # as exceptions instead of treating the same as an NXDOMAIN response.
+ #
+ # The value of :nameserver should be an address string or
+ # an array of address strings.
+ # - :nameserver => '8.8.8.8'
+ # - :nameserver => ['8.8.8.8', '8.8.4.4']
+ #
+ # The value of :nameserver_port should be an array of
+ # pair of nameserver address and port number.
+ # - :nameserver_port => [['8.8.8.8', 53], ['8.8.4.4', 53]]
#
# Example:
#
@@ -321,11 +336,26 @@ class Resolv
# :ndots => 1)
def initialize(config_info=nil)
- @mutex = Mutex.new
+ @mutex = Thread::Mutex.new
@config = Config.new(config_info)
@initialized = nil
end
+ # Sets the resolver timeouts. This may be a single positive number
+ # or an array of positive numbers representing timeouts in seconds.
+ # If an array is specified, a DNS request will retry and wait for
+ # each successive interval in the array until a successful response
+ # is received. Specifying +nil+ reverts to the default timeouts:
+ # [ 5, second = 5 * 2 / nameserver_count, 2 * second, 4 * second ]
+ #
+ # Example:
+ #
+ # dns.timeouts = 3
+ #
+ def timeouts=(values)
+ @config.timeouts = values
+ end
+
def lazy_initialize # :nodoc:
@mutex.synchronize {
unless @initialized
@@ -378,10 +408,29 @@ class Resolv
# be a Resolv::IPv4 or Resolv::IPv6
def each_address(name)
+ if use_ipv6?
+ each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address}
+ end
each_resource(name, Resource::IN::A) {|resource| yield resource.address}
- each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address}
end
+ def use_ipv6? # :nodoc:
+ @config.lazy_initialize unless @config.instance_variable_get(:@initialized)
+
+ use_ipv6 = @config.use_ipv6?
+ unless use_ipv6.nil?
+ return use_ipv6
+ end
+
+ begin
+ list = Socket.ip_address_list
+ rescue NotImplementedError
+ return true
+ end
+ list.any? {|a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
+ end
+ private :use_ipv6?
+
##
# Gets the hostname for +address+ from the DNS resolver.
#
@@ -416,6 +465,8 @@ class Resolv
case address
when Name
ptr = address
+ when IPv4, IPv6
+ ptr = address.to_name
when IPv4::Regex
ptr = IPv4.create(address).to_name
when IPv6::Regex
@@ -436,13 +487,18 @@ class Resolv
# * Resolv::DNS::Resource::IN::A
# * Resolv::DNS::Resource::IN::AAAA
# * Resolv::DNS::Resource::IN::ANY
+ # * Resolv::DNS::Resource::IN::CAA
# * Resolv::DNS::Resource::IN::CNAME
# * Resolv::DNS::Resource::IN::HINFO
+ # * Resolv::DNS::Resource::IN::HTTPS
+ # * Resolv::DNS::Resource::IN::LOC
# * Resolv::DNS::Resource::IN::MINFO
# * Resolv::DNS::Resource::IN::MX
# * Resolv::DNS::Resource::IN::NS
# * Resolv::DNS::Resource::IN::PTR
# * Resolv::DNS::Resource::IN::SOA
+ # * Resolv::DNS::Resource::IN::SRV
+ # * Resolv::DNS::Resource::IN::SVCB
# * Resolv::DNS::Resource::IN::TXT
# * Resolv::DNS::Resource::IN::WKS
#
@@ -469,52 +525,94 @@ class Resolv
# #getresource for argument details.
def each_resource(name, typeclass, &proc)
+ fetch_resource(name, typeclass) {|reply, reply_name|
+ extract_resources(reply, reply_name, typeclass, &proc)
+ }
+ end
+
+ # :stopdoc:
+
+ def fetch_resource(name, typeclass)
lazy_initialize
- requester = make_requester
+ truncated = {}
+ requesters = {}
+ udp_requester = begin
+ make_udp_requester
+ rescue Errno::EACCES
+ # fall back to TCP
+ end
senders = {}
+
begin
- @config.resolv(name) {|candidate, tout, nameserver|
+ @config.resolv(name) do |candidate, tout, nameserver, port|
msg = Message.new
msg.rd = 1
msg.add_question(candidate, typeclass)
- unless sender = senders[[candidate, nameserver]]
- sender = senders[[candidate, nameserver]] =
- requester.sender(msg, candidate, nameserver)
+
+ requester = requesters.fetch([nameserver, port]) do
+ if !truncated[candidate] && udp_requester
+ udp_requester
+ else
+ requesters[[nameserver, port]] = make_tcp_requester(nameserver, port)
+ end
+ end
+
+ unless sender = senders[[candidate, requester, nameserver, port]]
+ sender = requester.sender(msg, candidate, nameserver, port)
+ next if !sender
+ senders[[candidate, requester, nameserver, port]] = sender
end
reply, reply_name = requester.request(sender, tout)
case reply.rcode
when RCode::NoError
- extract_resources(reply, reply_name, typeclass, &proc)
+ if reply.tc == 1 and not Requester::TCP === requester
+ # Retry via TCP:
+ truncated[candidate] = true
+ redo
+ else
+ yield(reply, reply_name)
+ end
return
when RCode::NXDomain
raise Config::NXDomain.new(reply_name.to_s)
else
raise Config::OtherResolvError.new(reply_name.to_s)
end
- }
+ end
ensure
- requester.close
+ udp_requester&.close
+ requesters.each_value { |requester| requester&.close }
end
end
- def make_requester # :nodoc:
- if nameserver = @config.single?
- Requester::ConnectedUDP.new(nameserver)
+ def make_udp_requester # :nodoc:
+ nameserver_port = @config.nameserver_port
+ if nameserver_port.length == 1
+ Requester::ConnectedUDP.new(*nameserver_port[0])
else
- Requester::UnconnectedUDP.new
+ Requester::UnconnectedUDP.new(*nameserver_port)
end
end
+ def make_tcp_requester(host, port) # :nodoc:
+ return Requester::TCP.new(host, port)
+ rescue Errno::ECONNREFUSED
+ # Treat a refused TCP connection attempt to a nameserver like a timeout,
+ # as Resolv::DNS::Config#resolv considers ResolvTimeout exceptions as a
+ # hint to try the next nameserver:
+ raise ResolvTimeout
+ end
+
def extract_resources(msg, name, typeclass) # :nodoc:
if typeclass < Resource::ANY
n0 = Name.create(name)
- msg.each_answer {|n, ttl, data|
+ msg.each_resource {|n, ttl, data|
yield data if n0 == n
}
end
yielded = false
n0 = Name.create(name)
- msg.each_answer {|n, ttl, data|
+ msg.each_resource {|n, ttl, data|
if n0 == n
case data
when typeclass
@@ -526,7 +624,7 @@ class Resolv
end
}
return if yielded
- msg.each_answer {|n, ttl, data|
+ msg.each_resource {|n, ttl, data|
if n0 == n
case data
when typeclass
@@ -536,39 +634,23 @@ class Resolv
}
end
- if defined? SecureRandom
- def self.random(arg) # :nodoc:
- begin
- SecureRandom.random_number(arg)
- rescue NotImplementedError
- rand(arg)
- end
- end
- else
- def self.random(arg) # :nodoc:
+ def self.random(arg) # :nodoc:
+ begin
+ SecureRandom.random_number(arg)
+ rescue NotImplementedError
rand(arg)
end
end
-
- def self.rangerand(range) # :nodoc:
- base = range.begin
- len = range.end - range.begin
- if !range.exclude_end?
- len += 1
- end
- base + random(len)
- end
-
- RequestID = {}
- RequestIDMutex = Mutex.new
+ RequestID = {} # :nodoc:
+ RequestIDMutex = Thread::Mutex.new # :nodoc:
def self.allocate_request_id(host, port) # :nodoc:
id = nil
RequestIDMutex.synchronize {
h = (RequestID[[host, port]] ||= {})
begin
- id = rangerand(0x0000..0xffff)
+ id = random(0x0000..0xffff)
end while h[id]
h[id] = true
}
@@ -587,11 +669,25 @@ class Resolv
}
end
- def self.bind_random_port(udpsock) # :nodoc:
- begin
- port = rangerand(1024..65535)
- udpsock.bind("", port)
- rescue Errno::EADDRINUSE
+ case RUBY_PLATFORM
+ when *[
+ # https://www.rfc-editor.org/rfc/rfc6056.txt
+ # Appendix A. Survey of the Algorithms in Use by Some Popular Implementations
+ /freebsd/, /linux/, /netbsd/, /openbsd/, /solaris/,
+ /darwin/, # the same as FreeBSD
+ ] then
+ def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc:
+ udpsock.bind(bind_host, 0)
+ end
+ else
+ # Sequential port assignment
+ def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc:
+ # Ephemeral port number range recommended by RFC 6056
+ port = random(1024..65535)
+ udpsock.bind(bind_host, port)
+ rescue Errno::EADDRINUSE, # POSIX
+ Errno::EACCES, # SunOS: See PRIV_SYS_NFS in privileges(5)
+ Errno::EPERM # FreeBSD: security.mac.portacl.port_high is configurable. See mac_portacl(4).
retry
end
end
@@ -599,36 +695,65 @@ class Resolv
class Requester # :nodoc:
def initialize
@senders = {}
- @sock = nil
+ @socks = nil
end
def request(sender, tout)
- timelimit = Time.now + tout
- sender.send
- while (now = Time.now) < timelimit
- timeout = timelimit - now
- if !IO.select([@sock], nil, nil, timeout)
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ timelimit = start + tout
+ begin
+ sender.send
+ rescue Errno::EHOSTUNREACH, # multi-homed IPv6 may generate this
+ Errno::ENETUNREACH
+ raise ResolvTimeout
+ end
+ while true
+ before_select = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ timeout = timelimit - before_select
+ if timeout <= 0
+ raise ResolvTimeout
+ end
+ if @socks.size == 1
+ select_result = @socks[0].wait_readable(timeout) ? [ @socks ] : nil
+ else
+ select_result = IO.select(@socks, nil, nil, timeout)
+ end
+ if !select_result
+ after_select = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ next if after_select < timelimit
+ raise ResolvTimeout
+ end
+ begin
+ reply, from = recv_reply(select_result[0])
+ rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD
+ Errno::ECONNRESET, # Windows
+ EOFError
+ # No name server running on the server?
+ # Don't wait anymore.
raise ResolvTimeout
end
- reply, from = recv_reply
begin
msg = Message.decode(reply)
rescue DecodeError
next # broken DNS message ignored
end
- if s = @senders[[from,msg.id]]
+ if sender == sender_for(from, msg)
break
else
# unexpected DNS message ignored
end
end
- return msg, s.data
+ return msg, sender.data
+ end
+
+ def sender_for(addr, msg)
+ @senders[[addr,msg.id]]
end
def close
- sock = @sock
- @sock = nil
- sock.close if sock
+ socks = @socks
+ @socks = nil
+ socks&.each(&:close)
end
class Sender # :nodoc:
@@ -640,31 +765,70 @@ class Resolv
end
class UnconnectedUDP < Requester # :nodoc:
- def initialize
+ def initialize(*nameserver_port)
super()
- @sock = UDPSocket.new
- @sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined? Fcntl::F_SETFD
- DNS.bind_random_port(@sock)
+ @nameserver_port = nameserver_port
+ @initialized = false
+ @mutex = Thread::Mutex.new
+ end
+
+ def lazy_initialize
+ @mutex.synchronize {
+ next if @initialized
+ @initialized = true
+ @socks_hash = {}
+ @socks = []
+ @nameserver_port.each {|host, port|
+ if host.index(':')
+ bind_host = "::"
+ af = Socket::AF_INET6
+ else
+ bind_host = "0.0.0.0"
+ af = Socket::AF_INET
+ end
+ next if @socks_hash[bind_host]
+ begin
+ sock = UDPSocket.new(af)
+ rescue Errno::EAFNOSUPPORT, Errno::EPROTONOSUPPORT
+ next # The kernel doesn't support the address family.
+ end
+ @socks << sock
+ @socks_hash[bind_host] = sock
+ sock.do_not_reverse_lookup = true
+ DNS.bind_random_port(sock, bind_host)
+ }
+ }
+ self
end
- def recv_reply
- reply, from = @sock.recvfrom(UDPSize)
+ def recv_reply(readable_socks)
+ lazy_initialize
+ reply, from = readable_socks[0].recvfrom(UDPSize)
return reply, [from[3],from[1]]
end
def sender(msg, data, host, port=Port)
+ host = Addrinfo.ip(host).ip_address
+ lazy_initialize
+ sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"]
+ return nil if !sock
service = [host, port]
id = DNS.allocate_request_id(host, port)
request = msg.encode
request[0,2] = [id].pack('n')
return @senders[[service, id]] =
- Sender.new(request, data, @sock, host, port)
+ Sender.new(request, data, sock, host, port)
end
def close
- super
- @senders.each_key {|service, id|
- DNS.free_request_id(service[0], service[1], id)
+ @mutex.synchronize {
+ if @initialized
+ super
+ @senders.each_key {|service, id|
+ DNS.free_request_id(service[0], service[1], id)
+ }
+ @initialized = false
+ end
}
end
@@ -677,6 +841,7 @@ class Resolv
attr_reader :data
def send
+ raise "@sock is nil." if @sock.nil?
@sock.send(@msg, 0, @host, @port)
end
end
@@ -687,55 +852,95 @@ class Resolv
super()
@host = host
@port = port
- @sock = UDPSocket.new(host.index(':') ? Socket::AF_INET6 : Socket::AF_INET)
- DNS.bind_random_port(@sock)
- @sock.connect(host, port)
- @sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined? Fcntl::F_SETFD
+ @mutex = Thread::Mutex.new
+ @initialized = false
+ end
+
+ def lazy_initialize
+ @mutex.synchronize {
+ next if @initialized
+ @initialized = true
+ is_ipv6 = @host.index(':')
+ sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET)
+ @socks = [sock]
+ sock.do_not_reverse_lookup = true
+ DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0")
+ sock.connect(@host, @port)
+ }
+ self
end
- def recv_reply
- reply = @sock.recv(UDPSize)
+ def recv_reply(readable_socks)
+ lazy_initialize
+ reply = readable_socks[0].recv(UDPSize)
return reply, nil
end
def sender(msg, data, host=@host, port=@port)
+ lazy_initialize
unless host == @host && port == @port
raise RequestError.new("host/port don't match: #{host}:#{port}")
end
id = DNS.allocate_request_id(@host, @port)
request = msg.encode
request[0,2] = [id].pack('n')
- return @senders[[nil,id]] = Sender.new(request, data, @sock)
+ return @senders[[nil,id]] = Sender.new(request, data, @socks[0])
end
def close
- super
- @senders.each_key {|from, id|
- DNS.free_request_id(@host, @port, id)
- }
+ @mutex.synchronize do
+ if @initialized
+ super
+ @senders.each_key {|from, id|
+ DNS.free_request_id(@host, @port, id)
+ }
+ @initialized = false
+ end
+ end
end
class Sender < Requester::Sender # :nodoc:
def send
+ raise "@sock is nil." if @sock.nil?
@sock.send(@msg, 0)
end
attr_reader :data
end
end
+ class MDNSOneShot < UnconnectedUDP # :nodoc:
+ def sender(msg, data, host, port=Port)
+ lazy_initialize
+ id = DNS.allocate_request_id(host, port)
+ request = msg.encode
+ request[0,2] = [id].pack('n')
+ sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"]
+ return @senders[id] =
+ UnconnectedUDP::Sender.new(request, data, sock, host, port)
+ end
+
+ def sender_for(addr, msg)
+ lazy_initialize
+ @senders[msg.id]
+ end
+ end
+
class TCP < Requester # :nodoc:
def initialize(host, port=Port)
super()
@host = host
@port = port
- @sock = TCPSocket.new(@host, @port)
- @sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined? Fcntl::F_SETFD
+ sock = TCPSocket.new(@host, @port)
+ @socks = [sock]
@senders = {}
end
- def recv_reply
- len = @sock.read(2).unpack('n')[0]
- reply = @sock.read(len)
+ def recv_reply(readable_socks)
+ len_data = readable_socks[0].read(2)
+ raise EOFError if len_data.nil? || len_data.bytesize != 2
+ len = len_data.unpack('n')[0]
+ reply = @socks[0].read(len)
+ raise EOFError if reply.nil? || reply.bytesize != len
return reply, nil
end
@@ -746,7 +951,7 @@ class Resolv
id = DNS.allocate_request_id(@host, @port)
request = msg.encode
request[0,2] = [request.length, id].pack('nn')
- return @senders[[nil,id]] = Sender.new(request, data, @sock)
+ return @senders[[nil,id]] = Sender.new(request, data, @socks[0])
end
class Sender < Requester::Sender # :nodoc:
@@ -774,32 +979,43 @@ class Resolv
class Config # :nodoc:
def initialize(config_info=nil)
- @mutex = Mutex.new
+ @mutex = Thread::Mutex.new
@config_info = config_info
@initialized = nil
+ @timeouts = nil
+ end
+
+ def timeouts=(values)
+ if values
+ values = Array(values)
+ values.each do |t|
+ Numeric === t or raise ArgumentError, "#{t.inspect} is not numeric"
+ t > 0.0 or raise ArgumentError, "timeout=#{t} must be positive"
+ end
+ @timeouts = values
+ else
+ @timeouts = nil
+ end
end
def Config.parse_resolv_conf(filename)
nameserver = []
search = nil
ndots = 1
- open(filename) {|f|
+ File.open(filename, 'rb') {|f|
f.each {|line|
line.sub!(/[#;].*/, '')
keyword, *args = line.split(/\s+/)
- args.each { |arg|
- arg.untaint
- }
next unless keyword
case keyword
when 'nameserver'
- nameserver += args
+ nameserver.concat(args.each(&:freeze))
when 'domain'
next if args.empty?
- search = [args[0]]
+ search = [args[0].freeze]
when 'search'
next if args.empty?
- search = args
+ search = args.each(&:freeze)
when 'options'
args.each {|arg|
case arg
@@ -810,28 +1026,28 @@ class Resolv
end
}
}
- return { :nameserver => nameserver, :search => search, :ndots => ndots }
+ return { :nameserver => nameserver.freeze, :search => search.freeze, :ndots => ndots.freeze }.freeze
end
def Config.default_config_hash(filename="/etc/resolv.conf")
if File.exist? filename
- config_hash = Config.parse_resolv_conf(filename)
+ Config.parse_resolv_conf(filename)
+ elsif defined?(Win32::Resolv)
+ search, nameserver = Win32::Resolv.get_resolv_info
+ config_hash = {}
+ config_hash[:nameserver] = nameserver if nameserver
+ config_hash[:search] = [search].flatten if search
+ config_hash
else
- if /mswin32|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM
- require 'win32/resolv'
- search, nameserver = Win32::Resolv.get_resolv_info
- config_hash = {}
- config_hash[:nameserver] = nameserver if nameserver
- config_hash[:search] = [search].flatten if search
- end
+ {}
end
- config_hash
end
def lazy_initialize
@mutex.synchronize {
unless @initialized
- @nameserver = []
+ @nameserver_port = []
+ @use_ipv6 = nil
@search = nil
@ndots = 1
case @config_info
@@ -850,11 +1066,22 @@ class Resolv
else
raise ArgumentError.new("invalid resolv configuration: #{@config_info.inspect}")
end
- @nameserver = config_hash[:nameserver] if config_hash.include? :nameserver
+ if config_hash.include? :nameserver
+ @nameserver_port = config_hash[:nameserver].map {|ns| [ns, Port] }
+ end
+ if config_hash.include? :nameserver_port
+ @nameserver_port = config_hash[:nameserver_port].map {|ns, port| [ns, (port || Port)] }
+ end
+ if config_hash.include? :use_ipv6
+ @use_ipv6 = config_hash[:use_ipv6]
+ end
@search = config_hash[:search] if config_hash.include? :search
@ndots = config_hash[:ndots] if config_hash.include? :ndots
+ @raise_timeout_errors = config_hash[:raise_timeout_errors]
- @nameserver = ['0.0.0.0'] if @nameserver.empty?
+ if @nameserver_port.empty?
+ @nameserver_port << ['0.0.0.0', Port]
+ end
if @search
@search = @search.map {|arg| Label.split(arg) }
else
@@ -866,9 +1093,14 @@ class Resolv
end
end
- if !@nameserver.kind_of?(Array) ||
- !@nameserver.all? {|ns| String === ns }
- raise ArgumentError.new("invalid nameserver config: #{@nameserver.inspect}")
+ if !@nameserver_port.kind_of?(Array) ||
+ @nameserver_port.any? {|ns_port|
+ !(Array === ns_port) ||
+ ns_port.length != 2
+ !(String === ns_port[0]) ||
+ !(Integer === ns_port[1])
+ }
+ raise ArgumentError.new("invalid nameserver config: #{@nameserver_port.inspect}")
end
if !@search.kind_of?(Array) ||
@@ -888,13 +1120,21 @@ class Resolv
def single?
lazy_initialize
- if @nameserver.length == 1
- return @nameserver[0]
+ if @nameserver_port.length == 1
+ return @nameserver_port[0]
else
return nil
end
end
+ def nameserver_port
+ @nameserver_port
+ end
+
+ def use_ipv6?
+ @use_ipv6
+ end
+
def generate_candidates(name)
candidates = nil
name = Name.create(name)
@@ -907,6 +1147,10 @@ class Resolv
candidates = []
end
candidates.concat(@search.map {|domain| Name.new(name.to_a + domain)})
+ fname = Name.create("#{name}.")
+ if !candidates.include?(fname)
+ candidates << fname
+ end
end
return candidates
end
@@ -915,7 +1159,7 @@ class Resolv
def generate_timeouts
ts = [InitialTimeout]
- ts << ts[-1] * 2 / @nameserver.length
+ ts << ts[-1] * 2 / @nameserver_port.length
ts << ts[-1] * 2
ts << ts[-1] * 2
return ts
@@ -923,23 +1167,26 @@ class Resolv
def resolv(name)
candidates = generate_candidates(name)
- timeouts = generate_timeouts
+ timeouts = @timeouts || generate_timeouts
+ timeout_error = false
begin
candidates.each {|candidate|
begin
timeouts.each {|tout|
- @nameserver.each {|nameserver|
+ @nameserver_port.each {|nameserver, port|
begin
- yield candidate, tout, nameserver
+ yield candidate, tout, nameserver, port
rescue ResolvTimeout
end
}
}
+ timeout_error = true
raise ResolvError.new("DNS resolv timeout: #{name}")
rescue NXDomain
end
}
rescue ResolvError
+ raise if @raise_timeout_errors && timeout_error
end
end
@@ -1007,7 +1254,9 @@ class Resolv
class Str # :nodoc:
def initialize(string)
@string = string
- @downcase = string.downcase
+ # case insensivity of DNS labels doesn't apply non-ASCII characters. [RFC 4343]
+ # This assumes @string is given in ASCII compatible encoding.
+ @downcase = string.b.downcase
end
attr_reader :string, :downcase
@@ -1016,11 +1265,11 @@ class Resolv
end
def inspect
- return "#<#{self.class} #{self.to_s}>"
+ return "#<#{self.class} #{self}>"
end
def ==(other)
- return @downcase == other.downcase
+ return self.class == other.class && @downcase == other.downcase
end
def eql?(other)
@@ -1056,12 +1305,20 @@ class Resolv
end
def initialize(labels, absolute=true) # :nodoc:
+ labels = labels.map {|label|
+ case label
+ when String then Label::Str.new(label)
+ when Label::Str then label
+ else
+ raise ArgumentError, "unexpected label: #{label.inspect}"
+ end
+ }
@labels = labels
@absolute = absolute
end
def inspect # :nodoc:
- "#<#{self.class}: #{self.to_s}#{@absolute ? '.' : ''}>"
+ "#<#{self.class}: #{self}#{@absolute ? '.' : ''}>"
end
##
@@ -1073,7 +1330,8 @@ class Resolv
def ==(other) # :nodoc:
return false unless Name === other
- return @labels.join == other.to_a.join && @absolute == other.absolute?
+ return false unless @absolute == other.absolute?
+ return @labels == other.to_a
end
alias eql? == # :nodoc:
@@ -1247,7 +1505,7 @@ class Resolv
class MessageEncoder # :nodoc:
def initialize
- @data = ''
+ @data = ''.dup
@names = {}
yield self
end
@@ -1284,18 +1542,20 @@ class Resolv
}
end
- def put_name(d)
- put_labels(d.to_a)
+ def put_name(d, compress: true)
+ put_labels(d.to_a, compress: compress)
end
- def put_labels(d)
+ def put_labels(d, compress: true)
d.each_index {|i|
domain = d[i..-1]
- if idx = @names[domain]
+ if compress && idx = @names[domain]
self.put_pack("n", 0xc000 | idx)
return
else
- @names[domain] = @data.length
+ if @data.length < 0x4000
+ @names[domain] = @data.length
+ end
self.put_label(d[i])
end
}
@@ -1313,13 +1573,15 @@ class Resolv
id, flag, qdcount, ancount, nscount, arcount =
msg.get_unpack('nnnnnn')
o.id = id
+ o.tc = (flag >> 9) & 1
+ o.rcode = flag & 15
+ return o unless o.tc.zero?
+
o.qr = (flag >> 15) & 1
o.opcode = (flag >> 11) & 15
o.aa = (flag >> 10) & 1
- o.tc = (flag >> 9) & 1
o.rd = (flag >> 8) & 1
o.ra = (flag >> 7) & 1
- o.rcode = flag & 15
(1..qdcount).each {
name, typeclass = msg.get_question
o.add_question(name, typeclass)
@@ -1344,10 +1606,14 @@ class Resolv
def initialize(data)
@data = data
@index = 0
- @limit = data.length
+ @limit = data.bytesize
yield self
end
+ def inspect
+ "\#<#{self.class}: #{@data.byteslice(0, @index).inspect} #{@data.byteslice(@index..-1).inspect}>"
+ end
+
def get_length16
len, = self.get_unpack('n')
save_limit = @limit
@@ -1363,7 +1629,8 @@ class Resolv
end
def get_bytes(len = @limit - @index)
- d = @data[@index, len]
+ raise DecodeError.new("limit exceeded") if @limit < @index + len
+ d = @data.byteslice(@index, len)
@index += len
return d
end
@@ -1390,9 +1657,10 @@ class Resolv
end
def get_string
- len = @data[@index].ord
+ raise DecodeError.new("limit exceeded") if @limit <= @index
+ len = @data.getbyte(@index)
raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len
- d = @data[@index + 1, len]
+ d = @data.byteslice(@index + 1, len)
@index += 1 + len
return d
end
@@ -1405,33 +1673,49 @@ class Resolv
strings
end
+ def get_list
+ [].tap do |values|
+ while @index < @limit
+ values << yield
+ end
+ end
+ end
+
def get_name
return Name.new(self.get_labels)
end
- def get_labels(limit=nil)
- limit = @index if !limit || @index < limit
+ def get_labels
+ prev_index = @index
+ save_index = nil
d = []
+ size = -1
while true
- case @data[@index].ord
+ raise DecodeError.new("limit exceeded") if @limit <= @index
+ case @data.getbyte(@index)
when 0
@index += 1
+ if save_index
+ @index = save_index
+ end
return d
when 192..255
idx = self.get_unpack('n')[0] & 0x3fff
- if limit <= idx
+ if prev_index <= idx
raise DecodeError.new("non-backward name pointer")
end
- save_index = @index
+ prev_index = idx
+ if !save_index
+ save_index = @index
+ end
@index = idx
- d += self.get_labels(limit)
- @index = save_index
- return d
else
- d << self.get_label
+ l = self.get_label
+ d << l
+ size += 1 + l.string.bytesize
+ raise DecodeError.new("name label data exceed 255 octets") if size > 255
end
end
- return d
end
def get_label
@@ -1448,7 +1732,13 @@ class Resolv
name = self.get_name
type, klass, ttl = self.get_unpack('nnN')
typeclass = Resource.get_class(type, klass)
- res = self.get_length16 { typeclass.decode_rdata self }
+ res = self.get_length16 do
+ begin
+ typeclass.decode_rdata self
+ rescue => e
+ raise DecodeError, e.message, e.backtrace
+ end
+ end
res.instance_variable_set :@ttl, ttl
return name, ttl, res
end
@@ -1456,6 +1746,377 @@ class Resolv
end
##
+ # SvcParams for service binding RRs. [RFC9460]
+
+ class SvcParams
+ include Enumerable
+
+ ##
+ # Create a list of SvcParams with the given initial content.
+ #
+ # +params+ has to be an enumerable of +SvcParam+s.
+ # If its content has +SvcParam+s with the duplicate key,
+ # the one appears last takes precedence.
+
+ def initialize(params = [])
+ @params = {}
+
+ params.each do |param|
+ add param
+ end
+ end
+
+ ##
+ # Get SvcParam for the given +key+ in this list.
+
+ def [](key)
+ @params[canonical_key(key)]
+ end
+
+ ##
+ # Get the number of SvcParams in this list.
+
+ def count
+ @params.count
+ end
+
+ ##
+ # Get whether this list is empty.
+
+ def empty?
+ @params.empty?
+ end
+
+ ##
+ # Add the SvcParam +param+ to this list, overwriting the existing one with the same key.
+
+ def add(param)
+ @params[param.class.key_number] = param
+ end
+
+ ##
+ # Remove the +SvcParam+ with the given +key+ and return it.
+
+ def delete(key)
+ @params.delete(canonical_key(key))
+ end
+
+ ##
+ # Enumerate the +SvcParam+s in this list.
+
+ def each(&block)
+ return enum_for(:each) unless block
+ @params.each_value(&block)
+ end
+
+ def encode(msg) # :nodoc:
+ @params.keys.sort.each do |key|
+ msg.put_pack('n', key)
+ msg.put_length16 do
+ @params.fetch(key).encode(msg)
+ end
+ end
+ end
+
+ def self.decode(msg) # :nodoc:
+ params = msg.get_list do
+ key, = msg.get_unpack('n')
+ msg.get_length16 do
+ SvcParam::ClassHash[key].decode(msg)
+ end
+ end
+
+ return self.new(params)
+ end
+
+ private
+
+ def canonical_key(key) # :nodoc:
+ case key
+ when Integer
+ key
+ when /\Akey(\d+)\z/
+ Integer($1)
+ when Symbol
+ SvcParam::ClassHash[key].key_number
+ else
+ raise TypeError, 'key must be either String or Symbol'
+ end
+ end
+ end
+
+ ##
+ # Base class for SvcParam. [RFC9460]
+
+ class SvcParam
+
+ ##
+ # Get the presentation name of the SvcParamKey.
+
+ def self.key_name
+ const_get(:KeyName)
+ end
+
+ ##
+ # Get the registered number of the SvcParamKey.
+
+ def self.key_number
+ const_get(:KeyNumber)
+ end
+
+ ClassHash = Hash.new do |h, key| # :nodoc:
+ case key
+ when Integer
+ Generic.create(key)
+ when /\Akey(?<key>\d+)\z/
+ Generic.create(key.to_int)
+ when Symbol
+ raise KeyError, "unknown key #{key}"
+ else
+ raise TypeError, 'key must be either String or Symbol'
+ end
+ end
+
+ ##
+ # Generic SvcParam abstract class.
+
+ class Generic < SvcParam
+
+ ##
+ # SvcParamValue in wire-format byte string.
+
+ attr_reader :value
+
+ ##
+ # Create generic SvcParam
+
+ def initialize(value)
+ @value = value
+ end
+
+ def encode(msg) # :nodoc:
+ msg.put_bytes(@value)
+ end
+
+ def self.decode(msg) # :nodoc:
+ return self.new(msg.get_bytes)
+ end
+
+ def self.create(key_number)
+ c = Class.new(Generic)
+ key_name = :"key#{key_number}"
+ c.const_set(:KeyName, key_name)
+ c.const_set(:KeyNumber, key_number)
+ self.const_set(:"Key#{key_number}", c)
+ ClassHash[key_name] = ClassHash[key_number] = c
+ return c
+ end
+ end
+
+ ##
+ # "mandatory" SvcParam -- Mandatory keys in service binding RR
+
+ class Mandatory < SvcParam
+ KeyName = :mandatory
+ KeyNumber = 0
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Mandatory keys.
+
+ attr_reader :keys
+
+ ##
+ # Initialize "mandatory" ScvParam.
+
+ def initialize(keys)
+ @keys = keys.map(&:to_int)
+ end
+
+ def encode(msg) # :nodoc:
+ @keys.sort.each do |key|
+ msg.put_pack('n', key)
+ end
+ end
+
+ def self.decode(msg) # :nodoc:
+ keys = msg.get_list { msg.get_unpack('n')[0] }
+ return self.new(keys)
+ end
+ end
+
+ ##
+ # "alpn" SvcParam -- Additional supported protocols
+
+ class ALPN < SvcParam
+ KeyName = :alpn
+ KeyNumber = 1
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Supported protocol IDs.
+
+ attr_reader :protocol_ids
+
+ ##
+ # Initialize "alpn" ScvParam.
+
+ def initialize(protocol_ids)
+ @protocol_ids = protocol_ids.map(&:to_str)
+ end
+
+ def encode(msg) # :nodoc:
+ msg.put_string_list(@protocol_ids)
+ end
+
+ def self.decode(msg) # :nodoc:
+ return self.new(msg.get_string_list)
+ end
+ end
+
+ ##
+ # "no-default-alpn" SvcParam -- No support for default protocol
+
+ class NoDefaultALPN < SvcParam
+ KeyName = :'no-default-alpn'
+ KeyNumber = 2
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ def encode(msg) # :nodoc:
+ # no payload
+ end
+
+ def self.decode(msg) # :nodoc:
+ return self.new
+ end
+ end
+
+ ##
+ # "port" SvcParam -- Port for alternative endpoint
+
+ class Port < SvcParam
+ KeyName = :port
+ KeyNumber = 3
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Port number.
+
+ attr_reader :port
+
+ ##
+ # Initialize "port" ScvParam.
+
+ def initialize(port)
+ @port = port.to_int
+ end
+
+ def encode(msg) # :nodoc:
+ msg.put_pack('n', @port)
+ end
+
+ def self.decode(msg) # :nodoc:
+ port, = msg.get_unpack('n')
+ return self.new(port)
+ end
+ end
+
+ ##
+ # "ipv4hint" SvcParam -- IPv4 address hints
+
+ class IPv4Hint < SvcParam
+ KeyName = :ipv4hint
+ KeyNumber = 4
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Set of IPv4 addresses.
+
+ attr_reader :addresses
+
+ ##
+ # Initialize "ipv4hint" ScvParam.
+
+ def initialize(addresses)
+ @addresses = addresses.map {|address| IPv4.create(address) }
+ end
+
+ def encode(msg) # :nodoc:
+ @addresses.each do |address|
+ msg.put_bytes(address.address)
+ end
+ end
+
+ def self.decode(msg) # :nodoc:
+ addresses = msg.get_list { IPv4.new(msg.get_bytes(4)) }
+ return self.new(addresses)
+ end
+ end
+
+ ##
+ # "ipv6hint" SvcParam -- IPv6 address hints
+
+ class IPv6Hint < SvcParam
+ KeyName = :ipv6hint
+ KeyNumber = 6
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Set of IPv6 addresses.
+
+ attr_reader :addresses
+
+ ##
+ # Initialize "ipv6hint" ScvParam.
+
+ def initialize(addresses)
+ @addresses = addresses.map {|address| IPv6.create(address) }
+ end
+
+ def encode(msg) # :nodoc:
+ @addresses.each do |address|
+ msg.put_bytes(address.address)
+ end
+ end
+
+ def self.decode(msg) # :nodoc:
+ addresses = msg.get_list { IPv6.new(msg.get_bytes(16)) }
+ return self.new(addresses)
+ end
+ end
+
+ ##
+ # "dohpath" SvcParam -- DNS over HTTPS path template [RFC9461]
+
+ class DoHPath < SvcParam
+ KeyName = :dohpath
+ KeyNumber = 7
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # URI template for DoH queries.
+
+ attr_reader :template
+
+ ##
+ # Initialize "dohpath" ScvParam.
+
+ def initialize(template)
+ @template = template.encode('utf-8')
+ end
+
+ def encode(msg) # :nodoc:
+ msg.put_bytes(@template)
+ end
+
+ def self.decode(msg) # :nodoc:
+ template = msg.get_bytes.force_encoding('utf-8')
+ return self.new(template)
+ end
+ end
+ end
+
+ ##
# A DNS query abstract class.
class Query
@@ -1478,7 +2139,14 @@ class Resolv
attr_reader :ttl
- ClassHash = {} # :nodoc:
+ ClassHash = Module.new do
+ module_function
+
+ def []=(type_class_value, klass)
+ type_value, class_value = type_class_value
+ Resource.const_set(:"Type#{type_value}_Class#{class_value}", klass)
+ end
+ end
def encode_rdata(msg) # :nodoc:
raise NotImplementedError.new
@@ -1492,10 +2160,10 @@ class Resolv
return false unless self.class == other.class
s_ivars = self.instance_variables
s_ivars.sort!
- s_ivars.delete "@ttl"
+ s_ivars.delete :@ttl
o_ivars = other.instance_variables
o_ivars.sort!
- o_ivars.delete "@ttl"
+ o_ivars.delete :@ttl
return s_ivars == o_ivars &&
s_ivars.collect {|name| self.instance_variable_get name} ==
o_ivars.collect {|name| other.instance_variable_get name}
@@ -1508,7 +2176,7 @@ class Resolv
def hash # :nodoc:
h = 0
vars = self.instance_variables
- vars.delete "@ttl"
+ vars.delete :@ttl
vars.each {|name|
h ^= self.instance_variable_get(name).hash
}
@@ -1516,7 +2184,9 @@ class Resolv
end
def self.get_class(type_value, class_value) # :nodoc:
- return ClassHash[[type_value, class_value]] ||
+ cache = :"Type#{type_value}_Class#{class_value}"
+
+ return (const_defined?(cache) && const_get(cache)) ||
Generic.create(type_value, class_value)
end
@@ -1806,10 +2476,10 @@ class Resolv
attr_reader :strings
##
- # Returns the first string from +strings+.
+ # Returns the concatenated string from +strings+.
def data
- @strings[0]
+ @strings.join("")
end
def encode_rdata(msg) # :nodoc:
@@ -1823,14 +2493,166 @@ class Resolv
end
##
+ # Location resource
+
+ class LOC < Resource
+
+ TypeValue = 29 # :nodoc:
+
+ def initialize(version, ssize, hprecision, vprecision, latitude, longitude, altitude)
+ @version = version
+ @ssize = Resolv::LOC::Size.create(ssize)
+ @hprecision = Resolv::LOC::Size.create(hprecision)
+ @vprecision = Resolv::LOC::Size.create(vprecision)
+ @latitude = Resolv::LOC::Coord.create(latitude)
+ @longitude = Resolv::LOC::Coord.create(longitude)
+ @altitude = Resolv::LOC::Alt.create(altitude)
+ end
+
+ ##
+ # Returns the version value for this LOC record which should always be 00
+
+ attr_reader :version
+
+ ##
+ # The spherical size of this LOC
+ # in meters using scientific notation as 2 integers of XeY
+
+ attr_reader :ssize
+
+ ##
+ # The horizontal precision using ssize type values
+ # in meters using scientific notation as 2 integers of XeY
+ # for precision use value/2 e.g. 2m = +/-1m
+
+ attr_reader :hprecision
+
+ ##
+ # The vertical precision using ssize type values
+ # in meters using scientific notation as 2 integers of XeY
+ # for precision use value/2 e.g. 2m = +/-1m
+
+ attr_reader :vprecision
+
+ ##
+ # The latitude for this LOC where 2**31 is the equator
+ # in thousandths of an arc second as an unsigned 32bit integer
+
+ attr_reader :latitude
+
+ ##
+ # The longitude for this LOC where 2**31 is the prime meridian
+ # in thousandths of an arc second as an unsigned 32bit integer
+
+ attr_reader :longitude
+
+ ##
+ # The altitude of the LOC above a reference sphere whose surface sits 100km below the WGS84 spheroid
+ # in centimeters as an unsigned 32bit integer
+
+ attr_reader :altitude
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_bytes(@version)
+ msg.put_bytes(@ssize.scalar)
+ msg.put_bytes(@hprecision.scalar)
+ msg.put_bytes(@vprecision.scalar)
+ msg.put_bytes(@latitude.coordinates)
+ msg.put_bytes(@longitude.coordinates)
+ msg.put_bytes(@altitude.altitude)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ version = msg.get_bytes(1)
+ ssize = msg.get_bytes(1)
+ hprecision = msg.get_bytes(1)
+ vprecision = msg.get_bytes(1)
+ latitude = msg.get_bytes(4)
+ longitude = msg.get_bytes(4)
+ altitude = msg.get_bytes(4)
+ return self.new(
+ version,
+ Resolv::LOC::Size.new(ssize),
+ Resolv::LOC::Size.new(hprecision),
+ Resolv::LOC::Size.new(vprecision),
+ Resolv::LOC::Coord.new(latitude,"lat"),
+ Resolv::LOC::Coord.new(longitude,"lon"),
+ Resolv::LOC::Alt.new(altitude)
+ )
+ end
+ end
+
+ ##
# A Query type requesting any RR.
class ANY < Query
TypeValue = 255 # :nodoc:
end
+ ##
+ # CAA resource record defined in RFC 8659
+ #
+ # These records identify certificate authority allowed to issue
+ # certificates for the given domain.
+
+ class CAA < Resource
+ TypeValue = 257
+
+ ##
+ # Creates a new CAA for +flags+, +tag+ and +value+.
+
+ def initialize(flags, tag, value)
+ unless (0..255) === flags
+ raise ArgumentError.new('flags must be an Integer between 0 and 255')
+ end
+ unless (1..15) === tag.bytesize
+ raise ArgumentError.new('length of tag must be between 1 and 15')
+ end
+
+ @flags = flags
+ @tag = tag
+ @value = value
+ end
+
+ ##
+ # Flags for this property:
+ # - Bit 0 : 0 = not critical, 1 = critical
+
+ attr_reader :flags
+
+ ##
+ # Property tag ("issue", "issuewild", "iodef"...).
+
+ attr_reader :tag
+
+ ##
+ # Property value.
+
+ attr_reader :value
+
+ ##
+ # Whether the critical flag is set on this property.
+
+ def critical?
+ flags & 0x80 != 0
+ end
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_pack('C', @flags)
+ msg.put_string(@tag)
+ msg.put_bytes(@value)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ flags, = msg.get_unpack('C')
+ tag = msg.get_string
+ value = msg.get_bytes
+ self.new flags, tag, value
+ end
+ end
+
ClassInsensitiveTypes = [ # :nodoc:
- NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, ANY
+ NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA
]
##
@@ -2016,7 +2838,7 @@ class Resolv
msg.put_pack("n", @priority)
msg.put_pack("n", @weight)
msg.put_pack("n", @port)
- msg.put_name(@target)
+ msg.put_name(@target, compress: false)
end
def self.decode_rdata(msg) # :nodoc:
@@ -2027,6 +2849,84 @@ class Resolv
return self.new(priority, weight, port, target)
end
end
+
+ ##
+ # Common implementation for SVCB-compatible resource records.
+
+ class ServiceBinding
+
+ ##
+ # Create a service binding resource record.
+
+ def initialize(priority, target, params = [])
+ @priority = priority.to_int
+ @target = Name.create(target)
+ @params = SvcParams.new(params)
+ end
+
+ ##
+ # The priority of this target host.
+ #
+ # The range is 0-65535.
+ # If set to 0, this RR is in AliasMode. Otherwise, it is in ServiceMode.
+
+ attr_reader :priority
+
+ ##
+ # The domain name of the target host.
+
+ attr_reader :target
+
+ ##
+ # The service parameters for the target host.
+
+ attr_reader :params
+
+ ##
+ # Whether this RR is in AliasMode.
+
+ def alias_mode?
+ self.priority == 0
+ end
+
+ ##
+ # Whether this RR is in ServiceMode.
+
+ def service_mode?
+ !alias_mode?
+ end
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_pack("n", @priority)
+ msg.put_name(@target, compress: false)
+ @params.encode(msg)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ priority, = msg.get_unpack("n")
+ target = msg.get_name
+ params = SvcParams.decode(msg)
+ return self.new(priority, target, params)
+ end
+ end
+
+ ##
+ # SVCB resource record [RFC9460]
+
+ class SVCB < ServiceBinding
+ TypeValue = 64
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+ end
+
+ ##
+ # HTTPS resource record [RFC9460]
+
+ class HTTPS < ServiceBinding
+ TypeValue = 65
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+ end
end
end
end
@@ -2036,10 +2936,20 @@ class Resolv
class IPv4
+ Regex256 = /0
+ |1(?:[0-9][0-9]?)?
+ |2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
+ |[3-9][0-9]?/x # :nodoc:
+
##
# Regular expression IPv4 addresses must match.
+ Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/
- Regex = /\A(\d+)\.(\d+)\.(\d+)\.(\d+)\z/
+ ##
+ # Creates a new IPv4 address from +arg+ which may be:
+ #
+ # IPv4:: returns +arg+.
+ # String:: +arg+ must match the IPv4::Regex constant
def self.create(arg)
case arg
@@ -2060,8 +2970,11 @@ class Resolv
end
def initialize(address) # :nodoc:
- unless address.kind_of?(String) && address.length == 4
- raise ArgumentError.new('IPv4 address must be 4 bytes')
+ unless address.kind_of?(String)
+ raise ArgumentError, 'IPv4 address must be a string'
+ end
+ unless address.length == 4
+ raise ArgumentError, "IPv4 address expects 4 bytes but #{address.length} bytes"
end
@address = address
end
@@ -2079,7 +2992,7 @@ class Resolv
end
def inspect # :nodoc:
- return "#<#{self.class} #{self.to_s}>"
+ return "#<#{self.class} #{self}>"
end
##
@@ -2141,13 +3054,38 @@ class Resolv
\z/x
##
+ # IPv6 link local address format fe80:b:c:d:e:f:g:h%em1
+ Regex_8HexLinkLocal = /\A
+ [Ff][Ee]80
+ (?::[0-9A-Fa-f]{1,4}){7}
+ %[-0-9A-Za-z._~]+
+ \z/x
+
+ ##
+ # Compressed IPv6 link local address format fe80::b%em1
+
+ Regex_CompressedHexLinkLocal = /\A
+ [Ff][Ee]80:
+ (?:
+ ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
+ ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
+ |
+ :((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
+ )?
+ :[0-9A-Fa-f]{1,4}%[-0-9A-Za-z._~]+
+ \z/x
+
+ ##
# A composite IPv6 address Regexp.
Regex = /
(?:#{Regex_8Hex}) |
(?:#{Regex_CompressedHex}) |
(?:#{Regex_6Hex4Dec}) |
- (?:#{Regex_CompressedHex4Dec})/x
+ (?:#{Regex_CompressedHex4Dec}) |
+ (?:#{Regex_8HexLinkLocal}) |
+ (?:#{Regex_CompressedHexLinkLocal})
+ /x
##
# Creates a new IPv6 address from +arg+ which may be:
@@ -2160,14 +3098,14 @@ class Resolv
when IPv6
return arg
when String
- address = ''
+ address = ''.b
if Regex_8Hex =~ arg
arg.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')}
elsif Regex_CompressedHex =~ arg
prefix = $1
suffix = $2
- a1 = ''
- a2 = ''
+ a1 = ''.b
+ a2 = ''.b
prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')}
suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')}
omitlen = 16 - a1.length - a2.length
@@ -2183,8 +3121,8 @@ class Resolv
elsif Regex_CompressedHex4Dec =~ arg
prefix, suffix, a, b, c, d = $1, $2, $3.to_i, $4.to_i, $5.to_i, $6.to_i
if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d
- a1 = ''
- a2 = ''
+ a1 = ''.b
+ a2 = ''.b
prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')}
suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')}
omitlen = 12 - a1.length - a2.length
@@ -2214,15 +3152,11 @@ class Resolv
attr_reader :address
def to_s # :nodoc:
- address = sprintf("%X:%X:%X:%X:%X:%X:%X:%X", *@address.unpack("nnnnnnnn"))
- unless address.sub!(/(^|:)0(:0)+(:|$)/, '::')
- address.sub!(/(^|:)0(:|$)/, '::')
- end
- return address
+ sprintf("%x:%x:%x:%x:%x:%x:%x:%x", *@address.unpack("nnnnnnnn")).sub(/(^|:)0(:0)+(:|$)/, '::')
end
def inspect # :nodoc:
- return "#<#{self.class} #{self.to_s}>"
+ return "#<#{self.class} #{self}>"
end
##
@@ -2249,14 +3183,331 @@ class Resolv
end
##
+ # Resolv::MDNS is a one-shot Multicast DNS (mDNS) resolver. It blindly
+ # makes queries to the mDNS addresses without understanding anything about
+ # multicast ports.
+ #
+ # Information taken form the following places:
+ #
+ # * RFC 6762
+
+ class MDNS < DNS
+
+ ##
+ # Default mDNS Port
+
+ Port = 5353
+
+ ##
+ # Default IPv4 mDNS address
+
+ AddressV4 = '224.0.0.251'
+
+ ##
+ # Default IPv6 mDNS address
+
+ AddressV6 = 'ff02::fb'
+
+ ##
+ # Default mDNS addresses
+
+ Addresses = [
+ [AddressV4, Port],
+ [AddressV6, Port],
+ ]
+
+ ##
+ # Creates a new one-shot Multicast DNS (mDNS) resolver.
+ #
+ # +config_info+ can be:
+ #
+ # nil::
+ # Uses the default mDNS addresses
+ #
+ # Hash::
+ # Must contain :nameserver or :nameserver_port like
+ # Resolv::DNS#initialize.
+
+ def initialize(config_info=nil)
+ if config_info then
+ super({ nameserver_port: Addresses }.merge(config_info))
+ else
+ super(nameserver_port: Addresses)
+ end
+ end
+
+ ##
+ # Iterates over all IP addresses for +name+ retrieved from the mDNS
+ # resolver, provided name ends with "local". If the name does not end in
+ # "local" no records will be returned.
+ #
+ # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will
+ # be a Resolv::IPv4 or Resolv::IPv6
+
+ def each_address(name)
+ name = Resolv::DNS::Name.create(name)
+
+ return unless name[-1].to_s == 'local'
+
+ super(name)
+ end
+
+ def make_udp_requester # :nodoc:
+ nameserver_port = @config.nameserver_port
+ Requester::MDNSOneShot.new(*nameserver_port)
+ end
+
+ end
+
+ module LOC # :nodoc:
+
+ ##
+ # A Resolv::LOC::Size
+
+ class Size
+
+ # Regular expression LOC size must match.
+
+ Regex = /\A0*(\d{1,8}(?:\.\d+)?)m\z/
+
+ ##
+ # Creates a new LOC::Size from +arg+ which may be:
+ #
+ # LOC::Size:: returns +arg+.
+ # String:: +arg+ must match the LOC::Size::Regex constant
+
+ def self.create(arg)
+ case arg
+ when Size
+ return arg
+ when String
+ unless Regex =~ arg
+ raise ArgumentError.new("not a properly formed Size string: " + arg)
+ end
+ unless (0.0...1e8) === (scalar = $1.to_f)
+ raise ArgumentError.new("out of range as Size: #{arg}")
+ end
+ str = (scalar * 100).to_i.to_s
+ return new([(str[0].to_i << 4) + (str.bytesize-1)].pack("C"))
+ else
+ raise ArgumentError.new("cannot interpret as Size: #{arg.inspect}")
+ end
+ end
+
+ # Internal use; use self.create.
+ def initialize(scalar)
+ @scalar = scalar
+ end
+
+ ##
+ # The raw size
+
+ attr_reader :scalar
+
+ def to_s # :nodoc:
+ s, = @scalar.unpack("C")
+ return "#{(s >> 4) * (10.0 ** ((s & 0xf) - 2))}m"
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
+ end
+
+ def ==(other) # :nodoc:
+ return @scalar == other.scalar
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @scalar.hash
+ end
+
+ end
+
+ ##
+ # A Resolv::LOC::Coord
+
+ class Coord
+
+ # Regular expression LOC Coord must match.
+
+ Regex = /\A0*(\d{1,3})\s([0-5]?\d)\s([0-5]?\d(?:\.\d+)?)\s([NESW])\z/
+
+ # Bias for the equator/prime meridian, in thousandths of a second of arc.
+ Bias = 1 << 31
+
+ ##
+ # Creates a new LOC::Coord from +arg+ which may be:
+ #
+ # LOC::Coord:: returns +arg+.
+ # String:: +arg+ must match the LOC::Coord::Regex constant
+
+ def self.create(arg)
+ case arg
+ when Coord
+ return arg
+ when String
+ unless m = Regex.match(arg)
+ raise ArgumentError.new("not a properly formed Coord string: " + arg)
+ end
+
+ arc = (m[1].to_i * 3_600_000) + (m[2].to_i * 60_000) + (m[3].to_f * 1_000).to_i
+ dir = m[4]
+ lat = dir[/[NS]/]
+ unless arc <= (lat ? 324_000_000 : 648_000_000) # (lat ? 90 : 180) * 3_600_000
+ raise ArgumentError.new("out of range as Coord: #{arg}")
+ end
+
+ hemi = dir[/[NE]/] ? 1 : -1
+ return new([arc * hemi + Bias].pack("N"), lat ? "lat" : "lon")
+ else
+ raise ArgumentError.new("cannot interpret as Coord: #{arg.inspect}")
+ end
+ end
+
+ # Internal use; use self.create.
+ def initialize(coordinates,orientation)
+ unless coordinates.kind_of?(String) and coordinates.bytesize == 4
+ raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}")
+ end
+ unless orientation == "lon" || orientation == "lat"
+ raise ArgumentError.new('Coord expects orientation to be a String argument of "lat" or "lon"')
+ end
+ @coordinates = coordinates
+ @orientation = orientation
+ end
+
+ ##
+ # The raw coordinates
+
+ attr_reader :coordinates
+
+ ## The orientation of the hemisphere as 'lat' or 'lon'
+
+ attr_reader :orientation
+
+ def to_s # :nodoc:
+ c, = @coordinates.unpack("N")
+ val = (c -= Bias).abs
+ val, fracsecs = val.divmod(1000)
+ val, secs = val.divmod(60)
+ degs, mins = val.divmod(60)
+ hemi = if c.negative?
+ @orientation == "lon" ? "W" : "S"
+ else
+ @orientation == "lat" ? "N" : "E"
+ end
+ format("%d %02d %02d.%03d %s", degs, mins, secs, fracsecs, hemi)
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
+ end
+
+ def ==(other) # :nodoc:
+ return @coordinates == other.coordinates
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @coordinates.hash
+ end
+
+ end
+
+ ##
+ # A Resolv::LOC::Alt
+
+ class Alt
+
+ # Regular expression LOC Alt must match.
+
+ Regex = /\A([+-]?0*\d{1,8}(?:\.\d+)?)m\z/
+
+ # Bias to a base of 100,000m below the WGS 84 reference spheroid.
+ Bias = 100_000_00
+
+ ##
+ # Creates a new LOC::Alt from +arg+ which may be:
+ #
+ # LOC::Alt:: returns +arg+.
+ # String:: +arg+ must match the LOC::Alt::Regex constant
+
+ def self.create(arg)
+ case arg
+ when Alt
+ return arg
+ when String
+ unless Regex =~ arg
+ raise ArgumentError.new("not a properly formed Alt string: " + arg)
+ end
+ altitude = ($1.to_f * 100).to_i + Bias
+ unless (0...0x1_0000_0000) === altitude
+ raise ArgumentError.new("out of raise as Alt: #{arg}")
+ end
+ return new([altitude].pack("N"))
+ else
+ raise ArgumentError.new("cannot interpret as Alt: #{arg.inspect}")
+ end
+ end
+
+ # Internal use; use self.create.
+ def initialize(altitude)
+ @altitude = altitude
+ end
+
+ ##
+ # The raw altitude
+
+ attr_reader :altitude
+
+ def to_s # :nodoc:
+ a, = @altitude.unpack("N")
+ return "#{(a - Bias).fdiv(100)}m"
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
+ end
+
+ def ==(other) # :nodoc:
+ return @altitude == other.altitude
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @altitude.hash
+ end
+
+ end
+
+ end
+
+ ##
# Default resolver to use for Resolv class methods.
DefaultResolver = self.new
##
+ # Replaces the resolvers in the default resolver with +new_resolvers+. This
+ # allows resolvers to be changed for resolv-replace.
+
+ def DefaultResolver.replace_resolvers new_resolvers
+ @resolvers = new_resolvers
+ end
+
+ ##
# Address Regexp to use for matching IP addresses.
AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/
end
-
diff --git a/lib/rexml/attlistdecl.rb b/lib/rexml/attlistdecl.rb
deleted file mode 100644
index ea5a98b69e..0000000000
--- a/lib/rexml/attlistdecl.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-#vim:ts=2 sw=2 noexpandtab:
-require 'rexml/child'
-require 'rexml/source'
-
-module REXML
- # This class needs:
- # * Documentation
- # * Work! Not all types of attlists are intelligently parsed, so we just
- # spew back out what we get in. This works, but it would be better if
- # we formatted the output ourselves.
- #
- # AttlistDecls provide *just* enough support to allow namespace
- # declarations. If you need some sort of generalized support, or have an
- # interesting idea about how to map the hideous, terrible design of DTD
- # AttlistDecls onto an intuitive Ruby interface, let me know. I'm desperate
- # for anything to make DTDs more palateable.
- class AttlistDecl < Child
- include Enumerable
-
- # What is this? Got me.
- attr_reader :element_name
-
- # Create an AttlistDecl, pulling the information from a Source. Notice
- # that this isn't very convenient; to create an AttlistDecl, you basically
- # have to format it yourself, and then have the initializer parse it.
- # Sorry, but for the forseeable future, DTD support in REXML is pretty
- # weak on convenience. Have I mentioned how much I hate DTDs?
- def initialize(source)
- super()
- if (source.kind_of? Array)
- @element_name, @pairs, @contents = *source
- end
- end
-
- # Access the attlist attribute/value pairs.
- # value = attlist_decl[ attribute_name ]
- def [](key)
- @pairs[key]
- end
-
- # Whether an attlist declaration includes the given attribute definition
- # if attlist_decl.include? "xmlns:foobar"
- def include?(key)
- @pairs.keys.include? key
- end
-
- # Iterate over the key/value pairs:
- # attlist_decl.each { |attribute_name, attribute_value| ... }
- def each(&block)
- @pairs.each(&block)
- end
-
- # Write out exactly what we got in.
- def write out, indent=-1
- out << @contents
- end
-
- def node_type
- :attlistdecl
- end
- end
-end
diff --git a/lib/rexml/attribute.rb b/lib/rexml/attribute.rb
deleted file mode 100644
index febcc288b1..0000000000
--- a/lib/rexml/attribute.rb
+++ /dev/null
@@ -1,188 +0,0 @@
-require "rexml/namespace"
-require 'rexml/text'
-
-module REXML
- # Defines an Element Attribute; IE, a attribute=value pair, as in:
- # <element attribute="value"/>. Attributes can be in their own
- # namespaces. General users of REXML will not interact with the
- # Attribute class much.
- class Attribute
- include Node
- include Namespace
-
- # The element to which this attribute belongs
- attr_reader :element
- # The normalized value of this attribute. That is, the attribute with
- # entities intact.
- attr_writer :normalized
- PATTERN = /\s*(#{NAME_STR})\s*=\s*(["'])(.*?)\2/um
-
- NEEDS_A_SECOND_CHECK = /(<|&((#{Entity::NAME});|(#0*((?:\d+)|(?:x[a-fA-F0-9]+)));)?)/um
-
- # Constructor.
- # FIXME: The parser doesn't catch illegal characters in attributes
- #
- # first::
- # Either: an Attribute, which this new attribute will become a
- # clone of; or a String, which is the name of this attribute
- # second::
- # If +first+ is an Attribute, then this may be an Element, or nil.
- # If nil, then the Element parent of this attribute is the parent
- # of the +first+ Attribute. If the first argument is a String,
- # then this must also be a String, and is the content of the attribute.
- # If this is the content, it must be fully normalized (contain no
- # illegal characters).
- # parent::
- # Ignored unless +first+ is a String; otherwise, may be the Element
- # parent of this attribute, or nil.
- #
- #
- # Attribute.new( attribute_to_clone )
- # Attribute.new( attribute_to_clone, parent_element )
- # Attribute.new( "attr", "attr_value" )
- # Attribute.new( "attr", "attr_value", parent_element )
- def initialize( first, second=nil, parent=nil )
- @normalized = @unnormalized = @element = nil
- if first.kind_of? Attribute
- self.name = first.expanded_name
- @unnormalized = first.value
- if second.kind_of? Element
- @element = second
- else
- @element = first.element
- end
- elsif first.kind_of? String
- @element = parent
- self.name = first
- @normalized = second.to_s
- else
- raise "illegal argument #{first.class.name} to Attribute constructor"
- end
- end
-
- # Returns the namespace of the attribute.
- #
- # e = Element.new( "elns:myelement" )
- # e.add_attribute( "nsa:a", "aval" )
- # e.add_attribute( "b", "bval" )
- # e.attributes.get_attribute( "a" ).prefix # -> "nsa"
- # e.attributes.get_attribute( "b" ).prefix # -> "elns"
- # a = Attribute.new( "x", "y" )
- # a.prefix # -> ""
- def prefix
- pf = super
- if pf == ""
- pf = @element.prefix if @element
- end
- pf
- end
-
- # Returns the namespace URL, if defined, or nil otherwise
- #
- # e = Element.new("el")
- # e.add_attributes({"xmlns:ns", "http://url"})
- # e.namespace( "ns" ) # -> "http://url"
- def namespace arg=nil
- arg = prefix if arg.nil?
- @element.namespace arg
- end
-
- # Returns true if other is an Attribute and has the same name and value,
- # false otherwise.
- def ==( other )
- other.kind_of?(Attribute) and other.name==name and other.value==value
- end
-
- # Creates (and returns) a hash from both the name and value
- def hash
- name.hash + value.hash
- end
-
- # Returns this attribute out as XML source, expanding the name
- #
- # a = Attribute.new( "x", "y" )
- # a.to_string # -> "x='y'"
- # b = Attribute.new( "ns:x", "y" )
- # b.to_string # -> "ns:x='y'"
- def to_string
- if @element and @element.context and @element.context[:attribute_quote] == :quote
- %Q^#@expanded_name="#{to_s().gsub(/"/, '&quote;')}"^
- else
- "#@expanded_name='#{to_s().gsub(/'/, '&apos;')}'"
- end
- end
-
- def doctype
- if @element
- doc = @element.document
- doctype = doc.doctype if doc
- end
- end
-
- # Returns the attribute value, with entities replaced
- def to_s
- return @normalized if @normalized
-
- @normalized = Text::normalize( @unnormalized, doctype )
- @unnormalized = nil
- @normalized
- end
-
- # Returns the UNNORMALIZED value of this attribute. That is, entities
- # have been expanded to their values
- def value
- return @unnormalized if @unnormalized
- @unnormalized = Text::unnormalize( @normalized, doctype )
- @normalized = nil
- @unnormalized
- end
-
- # Returns a copy of this attribute
- def clone
- Attribute.new self
- end
-
- # Sets the element of which this object is an attribute. Normally, this
- # is not directly called.
- #
- # Returns this attribute
- def element=( element )
- @element = element
-
- if @normalized
- Text.check( @normalized, NEEDS_A_SECOND_CHECK, doctype )
- end
-
- self
- end
-
- # Removes this Attribute from the tree, and returns true if successfull
- #
- # This method is usually not called directly.
- def remove
- @element.attributes.delete self.name unless @element.nil?
- end
-
- # Writes this attribute (EG, puts 'key="value"' to the output)
- def write( output, indent=-1 )
- output << to_string
- end
-
- def node_type
- :attribute
- end
-
- def inspect
- rv = ""
- write( rv )
- rv
- end
-
- def xpath
- path = @element.xpath
- path += "/@#{self.expanded_name}"
- return path
- end
- end
-end
-#vim:ts=2 sw=2 noexpandtab:
diff --git a/lib/rexml/cdata.rb b/lib/rexml/cdata.rb
deleted file mode 100644
index 123a7c3d82..0000000000
--- a/lib/rexml/cdata.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require "rexml/text"
-
-module REXML
- class CData < Text
- START = '<![CDATA['
- STOP = ']]>'
- ILLEGAL = /(\]\]>)/
-
- # Constructor. CData is data between <![CDATA[ ... ]]>
- #
- # _Examples_
- # CData.new( source )
- # CData.new( "Here is some CDATA" )
- # CData.new( "Some unprocessed data", respect_whitespace_TF, parent_element )
- def initialize( first, whitespace=true, parent=nil )
- super( first, whitespace, parent, false, true, ILLEGAL )
- end
-
- # Make a copy of this object
- #
- # _Examples_
- # c = CData.new( "Some text" )
- # d = c.clone
- # d.to_s # -> "Some text"
- def clone
- CData.new self
- end
-
- # Returns the content of this CData object
- #
- # _Examples_
- # c = CData.new( "Some text" )
- # c.to_s # -> "Some text"
- def to_s
- @string
- end
-
- def value
- @string
- end
-
- # == DEPRECATED
- # See the rexml/formatters package
- #
- # Generates XML output of this object
- #
- # output::
- # Where to write the string. Defaults to $stdout
- # indent::
- # The amount to indent this node by
- # transitive::
- # Ignored
- # ie_hack::
- # Ignored
- #
- # _Examples_
- # c = CData.new( " Some text " )
- # c.write( $stdout ) #-> <![CDATA[ Some text ]]>
- def write( output=$stdout, indent=-1, transitive=false, ie_hack=false )
- Kernel.warn( "#{self.class.name}.write is deprecated" )
- indent( output, indent )
- output << START
- output << @string
- output << STOP
- end
- end
-end
diff --git a/lib/rexml/child.rb b/lib/rexml/child.rb
deleted file mode 100644
index 033057da55..0000000000
--- a/lib/rexml/child.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-require "rexml/node"
-
-module REXML
- ##
- # A Child object is something contained by a parent, and this class
- # contains methods to support that. Most user code will not use this
- # class directly.
- class Child
- include Node
- attr_reader :parent # The Parent of this object
-
- # Constructor. Any inheritors of this class should call super to make
- # sure this method is called.
- # parent::
- # if supplied, the parent of this child will be set to the
- # supplied value, and self will be added to the parent
- def initialize( parent = nil )
- @parent = nil
- # Declare @parent, but don't define it. The next line sets the
- # parent.
- parent.add( self ) if parent
- end
-
- # Replaces this object with another object. Basically, calls
- # Parent.replace_child
- #
- # Returns:: self
- def replace_with( child )
- @parent.replace_child( self, child )
- self
- end
-
- # Removes this child from the parent.
- #
- # Returns:: self
- def remove
- unless @parent.nil?
- @parent.delete self
- end
- self
- end
-
- # Sets the parent of this child to the supplied argument.
- #
- # other::
- # Must be a Parent object. If this object is the same object as the
- # existing parent of this child, no action is taken. Otherwise, this
- # child is removed from the current parent (if one exists), and is added
- # to the new parent.
- # Returns:: The parent added
- def parent=( other )
- return @parent if @parent == other
- @parent.delete self if defined? @parent and @parent
- @parent = other
- end
-
- alias :next_sibling :next_sibling_node
- alias :previous_sibling :previous_sibling_node
-
- # Sets the next sibling of this child. This can be used to insert a child
- # after some other child.
- # a = Element.new("a")
- # b = a.add_element("b")
- # c = Element.new("c")
- # b.next_sibling = c
- # # => <a><b/><c/></a>
- def next_sibling=( other )
- parent.insert_after self, other
- end
-
- # Sets the previous sibling of this child. This can be used to insert a
- # child before some other child.
- # a = Element.new("a")
- # b = a.add_element("b")
- # c = Element.new("c")
- # b.previous_sibling = c
- # # => <a><b/><c/></a>
- def previous_sibling=(other)
- parent.insert_before self, other
- end
-
- # Returns:: the document this child belongs to, or nil if this child
- # belongs to no document
- def document
- return parent.document unless parent.nil?
- nil
- end
-
- # This doesn't yet handle encodings
- def bytes
- encoding = document.encoding
-
- to_s
- end
- end
-end
diff --git a/lib/rexml/comment.rb b/lib/rexml/comment.rb
deleted file mode 100644
index d5be89b652..0000000000
--- a/lib/rexml/comment.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-require "rexml/child"
-
-module REXML
- ##
- # Represents an XML comment; that is, text between \<!-- ... -->
- class Comment < Child
- include Comparable
- START = "<!--"
- STOP = "-->"
-
- # The content text
-
- attr_accessor :string
-
- ##
- # Constructor. The first argument can be one of three types:
- # @param first If String, the contents of this comment are set to the
- # argument. If Comment, the argument is duplicated. If
- # Source, the argument is scanned for a comment.
- # @param second If the first argument is a Source, this argument
- # should be nil, not supplied, or a Parent to be set as the parent
- # of this object
- def initialize( first, second = nil )
- #puts "IN COMMENT CONSTRUCTOR; SECOND IS #{second.type}"
- super(second)
- if first.kind_of? String
- @string = first
- elsif first.kind_of? Comment
- @string = first.string
- end
- end
-
- def clone
- Comment.new self
- end
-
- # == DEPRECATED
- # See REXML::Formatters
- #
- # output::
- # Where to write the string
- # indent::
- # An integer. If -1, no indenting will be used; otherwise, the
- # indentation will be this number of spaces, and children will be
- # indented an additional amount.
- # transitive::
- # Ignored by this class. The contents of comments are never modified.
- # ie_hack::
- # Needed for conformity to the child API, but not used by this class.
- def write( output, indent=-1, transitive=false, ie_hack=false )
- Kernel.warn("Comment.write is deprecated. See REXML::Formatters")
- indent( output, indent )
- output << START
- output << @string
- output << STOP
- end
-
- alias :to_s :string
-
- ##
- # Compares this Comment to another; the contents of the comment are used
- # in the comparison.
- def <=>(other)
- other.to_s <=> @string
- end
-
- ##
- # Compares this Comment to another; the contents of the comment are used
- # in the comparison.
- def ==( other )
- other.kind_of? Comment and
- (other <=> self) == 0
- end
-
- def node_type
- :comment
- end
- end
-end
-#vim:ts=2 sw=2 noexpandtab:
diff --git a/lib/rexml/doctype.rb b/lib/rexml/doctype.rb
deleted file mode 100644
index 35beabc566..0000000000
--- a/lib/rexml/doctype.rb
+++ /dev/null
@@ -1,270 +0,0 @@
-require "rexml/parent"
-require "rexml/parseexception"
-require "rexml/namespace"
-require 'rexml/entity'
-require 'rexml/attlistdecl'
-require 'rexml/xmltokens'
-
-module REXML
- # Represents an XML DOCTYPE declaration; that is, the contents of <!DOCTYPE
- # ... >. DOCTYPES can be used to declare the DTD of a document, as well as
- # being used to declare entities used in the document.
- class DocType < Parent
- include XMLTokens
- START = "<!DOCTYPE"
- STOP = ">"
- SYSTEM = "SYSTEM"
- PUBLIC = "PUBLIC"
- DEFAULT_ENTITIES = {
- 'gt'=>EntityConst::GT,
- 'lt'=>EntityConst::LT,
- 'quot'=>EntityConst::QUOT,
- "apos"=>EntityConst::APOS
- }
-
- # name is the name of the doctype
- # external_id is the referenced DTD, if given
- attr_reader :name, :external_id, :entities, :namespaces
-
- # Constructor
- #
- # dt = DocType.new( 'foo', '-//I/Hate/External/IDs' )
- # # <!DOCTYPE foo '-//I/Hate/External/IDs'>
- # dt = DocType.new( doctype_to_clone )
- # # Incomplete. Shallow clone of doctype
- #
- # +Note+ that the constructor:
- #
- # Doctype.new( Source.new( "<!DOCTYPE foo 'bar'>" ) )
- #
- # is _deprecated_. Do not use it. It will probably disappear.
- def initialize( first, parent=nil )
- @entities = DEFAULT_ENTITIES
- @long_name = @uri = nil
- if first.kind_of? String
- super()
- @name = first
- @external_id = parent
- elsif first.kind_of? DocType
- super( parent )
- @name = first.name
- @external_id = first.external_id
- elsif first.kind_of? Array
- super( parent )
- @name = first[0]
- @external_id = first[1]
- @long_name = first[2]
- @uri = first[3]
- elsif first.kind_of? Source
- super( parent )
- parser = Parsers::BaseParser.new( first )
- event = parser.pull
- if event[0] == :start_doctype
- @name, @external_id, @long_name, @uri, = event[1..-1]
- end
- else
- super()
- end
- end
-
- def node_type
- :doctype
- end
-
- def attributes_of element
- rv = []
- each do |child|
- child.each do |key,val|
- rv << Attribute.new(key,val)
- end if child.kind_of? AttlistDecl and child.element_name == element
- end
- rv
- end
-
- def attribute_of element, attribute
- att_decl = find do |child|
- child.kind_of? AttlistDecl and
- child.element_name == element and
- child.include? attribute
- end
- return nil unless att_decl
- att_decl[attribute]
- end
-
- def clone
- DocType.new self
- end
-
- # output::
- # Where to write the string
- # indent::
- # An integer. If -1, no indentation will be used; otherwise, the
- # indentation will be this number of spaces, and children will be
- # indented an additional amount.
- # transitive::
- # Ignored
- # ie_hack::
- # Ignored
- def write( output, indent=0, transitive=false, ie_hack=false )
- f = REXML::Formatters::Default.new
- indent( output, indent )
- output << START
- output << ' '
- output << @name
- output << " #@external_id" if @external_id
- output << " #{@long_name.inspect}" if @long_name
- output << " #{@uri.inspect}" if @uri
- unless @children.empty?
- next_indent = indent + 1
- output << ' ['
- @children.each { |child|
- output << "\n"
- f.write( child, output )
- }
- output << "\n]"
- end
- output << STOP
- end
-
- def context
- @parent.context
- end
-
- def entity( name )
- @entities[name].unnormalized if @entities[name]
- end
-
- def add child
- super(child)
- @entities = DEFAULT_ENTITIES.clone if @entities == DEFAULT_ENTITIES
- @entities[ child.name ] = child if child.kind_of? Entity
- end
-
- # This method retrieves the public identifier identifying the document's
- # DTD.
- #
- # Method contributed by Henrik Martensson
- def public
- case @external_id
- when "SYSTEM"
- nil
- when "PUBLIC"
- strip_quotes(@long_name)
- end
- end
-
- # This method retrieves the system identifier identifying the document's DTD
- #
- # Method contributed by Henrik Martensson
- def system
- case @external_id
- when "SYSTEM"
- strip_quotes(@long_name)
- when "PUBLIC"
- @uri.kind_of?(String) ? strip_quotes(@uri) : nil
- end
- end
-
- # This method returns a list of notations that have been declared in the
- # _internal_ DTD subset. Notations in the external DTD subset are not
- # listed.
- #
- # Method contributed by Henrik Martensson
- def notations
- children().select {|node| node.kind_of?(REXML::NotationDecl)}
- end
-
- # Retrieves a named notation. Only notations declared in the internal
- # DTD subset can be retrieved.
- #
- # Method contributed by Henrik Martensson
- def notation(name)
- notations.find { |notation_decl|
- notation_decl.name == name
- }
- end
-
- private
-
- # Method contributed by Henrik Martensson
- def strip_quotes(quoted_string)
- quoted_string =~ /^[\'\"].*[\'\"]$/ ?
- quoted_string[1, quoted_string.length-2] :
- quoted_string
- end
- end
-
- # We don't really handle any of these since we're not a validating
- # parser, so we can be pretty dumb about them. All we need to be able
- # to do is spew them back out on a write()
-
- # This is an abstract class. You never use this directly; it serves as a
- # parent class for the specific declarations.
- class Declaration < Child
- def initialize src
- super()
- @string = src
- end
-
- def to_s
- @string+'>'
- end
-
- # == DEPRECATED
- # See REXML::Formatters
- #
- def write( output, indent )
- output << to_s
- end
- end
-
- public
- class ElementDecl < Declaration
- def initialize( src )
- super
- end
- end
-
- class ExternalEntity < Child
- def initialize( src )
- super()
- @entity = src
- end
- def to_s
- @entity
- end
- def write( output, indent )
- output << @entity
- end
- end
-
- class NotationDecl < Child
- attr_accessor :public, :system
- def initialize name, middle, pub, sys
- super(nil)
- @name = name
- @middle = middle
- @public = pub
- @system = sys
- end
-
- def to_s
- "<!NOTATION #@name #@middle#{
- @public ? ' ' + public.inspect : ''
- }#{
- @system ? ' ' +@system.inspect : ''
- }>"
- end
-
- def write( output, indent=-1 )
- output << to_s
- end
-
- # This method retrieves the name of the notation.
- #
- # Method contributed by Henrik Martensson
- def name
- @name
- end
- end
-end
diff --git a/lib/rexml/document.rb b/lib/rexml/document.rb
deleted file mode 100644
index 48f1a0ec6c..0000000000
--- a/lib/rexml/document.rb
+++ /dev/null
@@ -1,231 +0,0 @@
-require "rexml/element"
-require "rexml/xmldecl"
-require "rexml/source"
-require "rexml/comment"
-require "rexml/doctype"
-require "rexml/instruction"
-require "rexml/rexml"
-require "rexml/parseexception"
-require "rexml/output"
-require "rexml/parsers/baseparser"
-require "rexml/parsers/streamparser"
-require "rexml/parsers/treeparser"
-
-module REXML
- # Represents a full XML document, including PIs, a doctype, etc. A
- # Document has a single child that can be accessed by root().
- # Note that if you want to have an XML declaration written for a document
- # you create, you must add one; REXML documents do not write a default
- # declaration for you. See |DECLARATION| and |write|.
- class Document < Element
- # A convenient default XML declaration. If you want an XML declaration,
- # the easiest way to add one is mydoc << Document::DECLARATION
- # +DEPRECATED+
- # Use: mydoc << XMLDecl.default
- DECLARATION = XMLDecl.default
-
- # Constructor
- # @param source if supplied, must be a Document, String, or IO.
- # Documents have their context and Element attributes cloned.
- # Strings are expected to be valid XML documents. IOs are expected
- # to be sources of valid XML documents.
- # @param context if supplied, contains the context of the document;
- # this should be a Hash.
- def initialize( source = nil, context = {} )
- @entity_expansion_count = 0
- super()
- @context = context
- return if source.nil?
- if source.kind_of? Document
- @context = source.context
- super source
- else
- build( source )
- end
- end
-
- def node_type
- :document
- end
-
- # Should be obvious
- def clone
- Document.new self
- end
-
- # According to the XML spec, a root node has no expanded name
- def expanded_name
- ''
- #d = doc_type
- #d ? d.name : "UNDEFINED"
- end
-
- alias :name :expanded_name
-
- # We override this, because XMLDecls and DocTypes must go at the start
- # of the document
- def add( child )
- if child.kind_of? XMLDecl
- @children.unshift child
- child.parent = self
- elsif child.kind_of? DocType
- # Find first Element or DocType node and insert the decl right
- # before it. If there is no such node, just insert the child at the
- # end. If there is a child and it is an DocType, then replace it.
- insert_before_index = 0
- @children.find { |x|
- insert_before_index += 1
- x.kind_of?(Element) || x.kind_of?(DocType)
- }
- if @children[ insert_before_index ] # Not null = not end of list
- if @children[ insert_before_index ].kind_of DocType
- @children[ insert_before_index ] = child
- else
- @children[ index_before_index-1, 0 ] = child
- end
- else # Insert at end of list
- @children[insert_before_index] = child
- end
- child.parent = self
- else
- rv = super
- raise "attempted adding second root element to document" if @elements.size > 1
- rv
- end
- end
- alias :<< :add
-
- def add_element(arg=nil, arg2=nil)
- rv = super
- raise "attempted adding second root element to document" if @elements.size > 1
- rv
- end
-
- # @return the root Element of the document, or nil if this document
- # has no children.
- def root
- elements[1]
- #self
- #@children.find { |item| item.kind_of? Element }
- end
-
- # @return the DocType child of the document, if one exists,
- # and nil otherwise.
- def doctype
- @children.find { |item| item.kind_of? DocType }
- end
-
- # @return the XMLDecl of this document; if no XMLDecl has been
- # set, the default declaration is returned.
- def xml_decl
- rv = @children[0]
- return rv if rv.kind_of? XMLDecl
- rv = @children.unshift(XMLDecl.default)[0]
- end
-
- # @return the XMLDecl version of this document as a String.
- # If no XMLDecl has been set, returns the default version.
- def version
- xml_decl().version
- end
-
- # @return the XMLDecl encoding of this document as a String.
- # If no XMLDecl has been set, returns the default encoding.
- def encoding
- xml_decl().encoding
- end
-
- # @return the XMLDecl standalone value of this document as a String.
- # If no XMLDecl has been set, returns the default setting.
- def stand_alone?
- xml_decl().stand_alone?
- end
-
- # Write the XML tree out, optionally with indent. This writes out the
- # entire XML document, including XML declarations, doctype declarations,
- # and processing instructions (if any are given).
- #
- # A controversial point is whether Document should always write the XML
- # declaration (<?xml version='1.0'?>) whether or not one is given by the
- # user (or source document). REXML does not write one if one was not
- # specified, because it adds unnecessary bandwidth to applications such
- # as XML-RPC.
- #
- # See also the classes in the rexml/formatters package for the proper way
- # to change the default formatting of XML output
- #
- # _Examples_
- # Document.new("<a><b/></a>").serialize
- #
- # output_string = ""
- # tr = Transitive.new( output_string )
- # Document.new("<a><b/></a>").serialize( tr )
- #
- # output::
- # output an object which supports '<< string'; this is where the
- # document will be written.
- # indent::
- # An integer. If -1, no indenting will be used; otherwise, the
- # indentation will be twice this number of spaces, and children will be
- # indented an additional amount. For a value of 3, every item will be
- # indented 3 more levels, or 6 more spaces (2 * 3). Defaults to -1
- # transitive::
- # If transitive is true and indent is >= 0, then the output will be
- # pretty-printed in such a way that the added whitespace does not affect
- # the absolute *value* of the document -- that is, it leaves the value
- # and number of Text nodes in the document unchanged.
- # ie_hack::
- # Internet Explorer is the worst piece of crap to have ever been
- # written, with the possible exception of Windows itself. Since IE is
- # unable to parse proper XML, we have to provide a hack to generate XML
- # that IE's limited abilities can handle. This hack inserts a space
- # before the /> on empty tags. Defaults to false
- def write( output=$stdout, indent=-1, transitive=false, ie_hack=false )
- if xml_decl.encoding != "UTF-8" && !output.kind_of?(Output)
- output = Output.new( output, xml_decl.encoding )
- end
- formatter = if indent > -1
- if transitive
- require "rexml/formatters/transitive"
- REXML::Formatters::Transitive.new( indent, ie_hack )
- else
- REXML::Formatters::Pretty.new( indent, ie_hack )
- end
- else
- REXML::Formatters::Default.new( ie_hack )
- end
- formatter.write( self, output )
- end
-
-
- def Document::parse_stream( source, listener )
- Parsers::StreamParser.new( source, listener ).parse
- end
-
- @@entity_expansion_limit = 10_000
-
- # Set the entity expansion limit. By default the limit is set to 10000.
- def Document::entity_expansion_limit=( val )
- @@entity_expansion_limit = val
- end
-
- # Get the entity expansion limit. By default the limit is set to 10000.
- def Document::entity_expansion_limit
- return @@entity_expansion_limit
- end
-
- attr_reader :entity_expansion_count
-
- def record_entity_expansion
- @entity_expansion_count += 1
- if @entity_expansion_count > @@entity_expansion_limit
- raise "number of entity expansions exceeded, processing aborted."
- end
- end
-
- private
- def build( source )
- Parsers::TreeParser.new( source, self ).parse
- end
- end
-end
diff --git a/lib/rexml/dtd/attlistdecl.rb b/lib/rexml/dtd/attlistdecl.rb
deleted file mode 100644
index 25955ee274..0000000000
--- a/lib/rexml/dtd/attlistdecl.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require "rexml/child"
-module REXML
- module DTD
- class AttlistDecl < Child
- START = "<!ATTLIST"
- START_RE = /^\s*#{START}/um
- PATTERN_RE = /\s*(#{START}.*?>)/um
- end
- end
-end
diff --git a/lib/rexml/dtd/dtd.rb b/lib/rexml/dtd/dtd.rb
deleted file mode 100644
index 966e39ea57..0000000000
--- a/lib/rexml/dtd/dtd.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require "rexml/dtd/elementdecl"
-require "rexml/dtd/entitydecl"
-require "rexml/comment"
-require "rexml/dtd/notationdecl"
-require "rexml/dtd/attlistdecl"
-require "rexml/parent"
-
-module REXML
- module DTD
- class Parser
- def Parser.parse( input )
- case input
- when String
- parse_helper input
- when File
- parse_helper input.read
- end
- end
-
- # Takes a String and parses it out
- def Parser.parse_helper( input )
- contents = Parent.new
- while input.size > 0
- case input
- when ElementDecl.PATTERN_RE
- match = $&
- source = $'
- contents << ElementDecl.new( match )
- when AttlistDecl.PATTERN_RE
- matchdata = $~
- source = $'
- contents << AttlistDecl.new( matchdata )
- when EntityDecl.PATTERN_RE
- matchdata = $~
- source = $'
- contents << EntityDecl.new( matchdata )
- when Comment.PATTERN_RE
- matchdata = $~
- source = $'
- contents << Comment.new( matchdata )
- when NotationDecl.PATTERN_RE
- matchdata = $~
- source = $'
- contents << NotationDecl.new( matchdata )
- end
- end
- contents
- end
- end
- end
-end
diff --git a/lib/rexml/dtd/elementdecl.rb b/lib/rexml/dtd/elementdecl.rb
deleted file mode 100644
index a0bf641300..0000000000
--- a/lib/rexml/dtd/elementdecl.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require "rexml/child"
-module REXML
- module DTD
- class ElementDecl < Child
- START = "<!ELEMENT"
- START_RE = /^\s*#{START}/um
- PATTERN_RE = /^\s*(#{START}.*?)>/um
- PATTERN_RE = /^\s*#{START}\s+((?:[:\w_][-\.\w_]*:)?[-!\*\.\w_]*)(.*?)>/
- #\s*((((["']).*?\5)|[^\/'">]*)*?)(\/)?>/um, true)
-
- def initialize match
- @name = match[1]
- @rest = match[2]
- end
- end
- end
-end
diff --git a/lib/rexml/dtd/entitydecl.rb b/lib/rexml/dtd/entitydecl.rb
deleted file mode 100644
index 0adda6f7b9..0000000000
--- a/lib/rexml/dtd/entitydecl.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-require "rexml/child"
-module REXML
- module DTD
- class EntityDecl < Child
- START = "<!ENTITY"
- START_RE = /^\s*#{START}/um
- PUBLIC = /^\s*#{START}\s+(?:%\s+)?(\w+)\s+PUBLIC\s+((["']).*?\3)\s+((["']).*?\5)\s*>/um
- SYSTEM = /^\s*#{START}\s+(?:%\s+)?(\w+)\s+SYSTEM\s+((["']).*?\3)(?:\s+NDATA\s+\w+)?\s*>/um
- PLAIN = /^\s*#{START}\s+(\w+)\s+((["']).*?\3)\s*>/um
- PERCENT = /^\s*#{START}\s+%\s+(\w+)\s+((["']).*?\3)\s*>/um
- # <!ENTITY name SYSTEM "...">
- # <!ENTITY name "...">
- def initialize src
- super()
- md = nil
- if src.match( PUBLIC )
- md = src.match( PUBLIC, true )
- @middle = "PUBLIC"
- @content = "#{md[2]} #{md[4]}"
- elsif src.match( SYSTEM )
- md = src.match( SYSTEM, true )
- @middle = "SYSTEM"
- @content = md[2]
- elsif src.match( PLAIN )
- md = src.match( PLAIN, true )
- @middle = ""
- @content = md[2]
- elsif src.match( PERCENT )
- md = src.match( PERCENT, true )
- @middle = ""
- @content = md[2]
- end
- raise ParseException.new("failed Entity match", src) if md.nil?
- @name = md[1]
- end
-
- def to_s
- rv = "<!ENTITY #@name "
- rv << "#@middle " if @middle.size > 0
- rv << @content
- rv
- end
-
- def write( output, indent )
- indent( output, indent )
- output << to_s
- end
-
- def EntityDecl.parse_source source, listener
- md = source.match( PATTERN_RE, true )
- thing = md[0].squeeze(" \t\n\r")
- listener.send inspect.downcase, thing
- end
- end
- end
-end
diff --git a/lib/rexml/dtd/notationdecl.rb b/lib/rexml/dtd/notationdecl.rb
deleted file mode 100644
index eae71f2e52..0000000000
--- a/lib/rexml/dtd/notationdecl.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require "rexml/child"
-module REXML
- module DTD
- class NotationDecl < Child
- START = "<!NOTATION"
- START_RE = /^\s*#{START}/um
- PUBLIC = /^\s*#{START}\s+(\w[\w-]*)\s+(PUBLIC)\s+((["']).*?\4)\s*>/um
- SYSTEM = /^\s*#{START}\s+(\w[\w-]*)\s+(SYSTEM)\s+((["']).*?\4)\s*>/um
- def initialize src
- super()
- if src.match( PUBLIC )
- md = src.match( PUBLIC, true )
- elsif src.match( SYSTEM )
- md = src.match( SYSTEM, true )
- else
- raise ParseException.new( "error parsing notation: no matching pattern", src )
- end
- @name = md[1]
- @middle = md[2]
- @rest = md[3]
- end
-
- def to_s
- "<!NOTATION #@name #@middle #@rest>"
- end
-
- def write( output, indent )
- indent( output, indent )
- output << to_s
- end
-
- def NotationDecl.parse_source source, listener
- md = source.match( PATTERN_RE, true )
- thing = md[0].squeeze(" \t\n\r")
- listener.send inspect.downcase, thing
- end
- end
- end
-end
diff --git a/lib/rexml/element.rb b/lib/rexml/element.rb
deleted file mode 100644
index 92308a5c99..0000000000
--- a/lib/rexml/element.rb
+++ /dev/null
@@ -1,1246 +0,0 @@
-require "rexml/parent"
-require "rexml/namespace"
-require "rexml/attribute"
-require "rexml/cdata"
-require "rexml/xpath"
-require "rexml/parseexception"
-
-module REXML
- # An implementation note about namespaces:
- # As we parse, when we find namespaces we put them in a hash and assign
- # them a unique ID. We then convert the namespace prefix for the node
- # to the unique ID. This makes namespace lookup much faster for the
- # cost of extra memory use. We save the namespace prefix for the
- # context node and convert it back when we write it.
- @@namespaces = {}
-
- # Represents a tagged XML element. Elements are characterized by
- # having children, attributes, and names, and can themselves be
- # children.
- class Element < Parent
- include Namespace
-
- UNDEFINED = "UNDEFINED"; # The default name
-
- # Mechanisms for accessing attributes and child elements of this
- # element.
- attr_reader :attributes, :elements
- # The context holds information about the processing environment, such as
- # whitespace handling.
- attr_accessor :context
-
- # Constructor
- # arg::
- # if not supplied, will be set to the default value.
- # If a String, the name of this object will be set to the argument.
- # If an Element, the object will be shallowly cloned; name,
- # attributes, and namespaces will be copied. Children will +not+ be
- # copied.
- # parent::
- # if supplied, must be a Parent, and will be used as
- # the parent of this object.
- # context::
- # If supplied, must be a hash containing context items. Context items
- # include:
- # * <tt>:respect_whitespace</tt> the value of this is :+all+ or an array of
- # strings being the names of the elements to respect
- # whitespace for. Defaults to :+all+.
- # * <tt>:compress_whitespace</tt> the value can be :+all+ or an array of
- # strings being the names of the elements to ignore whitespace on.
- # Overrides :+respect_whitespace+.
- # * <tt>:ignore_whitespace_nodes</tt> the value can be :+all+ or an array
- # of strings being the names of the elements in which to ignore
- # whitespace-only nodes. If this is set, Text nodes which contain only
- # whitespace will not be added to the document tree.
- # * <tt>:raw</tt> can be :+all+, or an array of strings being the names of
- # the elements to process in raw mode. In raw mode, special
- # characters in text is not converted to or from entities.
- def initialize( arg = UNDEFINED, parent=nil, context=nil )
- super(parent)
-
- @elements = Elements.new(self)
- @attributes = Attributes.new(self)
- @context = context
-
- if arg.kind_of? String
- self.name = arg
- elsif arg.kind_of? Element
- self.name = arg.expanded_name
- arg.attributes.each_attribute{ |attribute|
- @attributes << Attribute.new( attribute )
- }
- @context = arg.context
- end
- end
-
- def inspect
- rv = "<#@expanded_name"
-
- @attributes.each_attribute do |attr|
- rv << " "
- attr.write( rv, 0 )
- end
-
- if children.size > 0
- rv << "> ... </>"
- else
- rv << "/>"
- end
- end
-
-
- # Creates a shallow copy of self.
- # d = Document.new "<a><b/><b/><c><d/></c></a>"
- # new_a = d.root.clone
- # puts new_a # => "<a/>"
- def clone
- self.class.new self
- end
-
- # Evaluates to the root node of the document that this element
- # belongs to. If this element doesn't belong to a document, but does
- # belong to another Element, the parent's root will be returned, until the
- # earliest ancestor is found.
- #
- # Note that this is not the same as the document element.
- # In the following example, <a> is the document element, and the root
- # node is the parent node of the document element. You may ask yourself
- # why the root node is useful: consider the doctype and XML declaration,
- # and any processing instructions before the document element... they
- # are children of the root node, or siblings of the document element.
- # The only time this isn't true is when an Element is created that is
- # not part of any Document. In this case, the ancestor that has no
- # parent acts as the root node.
- # d = Document.new '<a><b><c/></b></a>'
- # a = d[1] ; c = a[1][1]
- # d.root_node == d # TRUE
- # a.root_node # namely, d
- # c.root_node # again, d
- def root_node
- parent.nil? ? self : parent.root_node
- end
-
- def root
- return elements[1] if self.kind_of? Document
- return self if parent.kind_of? Document or parent.nil?
- return parent.root
- end
-
- # Evaluates to the document to which this element belongs, or nil if this
- # element doesn't belong to a document.
- def document
- rt = root
- rt.parent if rt
- end
-
- # Evaluates to +true+ if whitespace is respected for this element. This
- # is the case if:
- # 1. Neither :+respect_whitespace+ nor :+compress_whitespace+ has any value
- # 2. The context has :+respect_whitespace+ set to :+all+ or
- # an array containing the name of this element, and
- # :+compress_whitespace+ isn't set to :+all+ or an array containing the
- # name of this element.
- # The evaluation is tested against +expanded_name+, and so is namespace
- # sensitive.
- def whitespace
- @whitespace = nil
- if @context
- if @context[:respect_whitespace]
- @whitespace = (@context[:respect_whitespace] == :all or
- @context[:respect_whitespace].include? expanded_name)
- end
- @whitespace = false if (@context[:compress_whitespace] and
- (@context[:compress_whitespace] == :all or
- @context[:compress_whitespace].include? expanded_name)
- )
- end
- @whitespace = true unless @whitespace == false
- @whitespace
- end
-
- def ignore_whitespace_nodes
- @ignore_whitespace_nodes = false
- if @context
- if @context[:ignore_whitespace_nodes]
- @ignore_whitespace_nodes =
- (@context[:ignore_whitespace_nodes] == :all or
- @context[:ignore_whitespace_nodes].include? expanded_name)
- end
- end
- end
-
- # Evaluates to +true+ if raw mode is set for this element. This
- # is the case if the context has :+raw+ set to :+all+ or
- # an array containing the name of this element.
- #
- # The evaluation is tested against +expanded_name+, and so is namespace
- # sensitive.
- def raw
- @raw = (@context and @context[:raw] and
- (@context[:raw] == :all or
- @context[:raw].include? expanded_name))
- @raw
- end
-
- #once :whitespace, :raw, :ignore_whitespace_nodes
-
- #################################################
- # Namespaces #
- #################################################
-
- # Evaluates to an +Array+ containing the prefixes (names) of all defined
- # namespaces at this context node.
- # doc = Document.new("<a xmlns:x='1' xmlns:y='2'><b/><c xmlns:z='3'/></a>")
- # doc.elements['//b'].prefixes # -> ['x', 'y']
- def prefixes
- prefixes = []
- prefixes = parent.prefixes if parent
- prefixes |= attributes.prefixes
- return prefixes
- end
-
- def namespaces
- namespaces = {}
- namespaces = parent.namespaces if parent
- namespaces = namespaces.merge( attributes.namespaces )
- return namespaces
- end
-
- # Evalutas to the URI for a prefix, or the empty string if no such
- # namespace is declared for this element. Evaluates recursively for
- # ancestors. Returns the default namespace, if there is one.
- # prefix::
- # the prefix to search for. If not supplied, returns the default
- # namespace if one exists
- # Returns::
- # the namespace URI as a String, or nil if no such namespace
- # exists. If the namespace is undefined, returns an empty string
- # doc = Document.new("<a xmlns='1' xmlns:y='2'><b/><c xmlns:z='3'/></a>")
- # b = doc.elements['//b']
- # b.namespace # -> '1'
- # b.namespace("y") # -> '2'
- def namespace(prefix=nil)
- if prefix.nil?
- prefix = prefix()
- end
- if prefix == ''
- prefix = "xmlns"
- else
- prefix = "xmlns:#{prefix}" unless prefix[0,5] == 'xmlns'
- end
- ns = attributes[ prefix ]
- ns = parent.namespace(prefix) if ns.nil? and parent
- ns = '' if ns.nil? and prefix == 'xmlns'
- return ns
- end
-
- # Adds a namespace to this element.
- # prefix::
- # the prefix string, or the namespace URI if +uri+ is not
- # supplied
- # uri::
- # the namespace URI. May be nil, in which +prefix+ is used as
- # the URI
- # Evaluates to: this Element
- # a = Element.new("a")
- # a.add_namespace("xmlns:foo", "bar" )
- # a.add_namespace("foo", "bar") # shorthand for previous line
- # a.add_namespace("twiddle")
- # puts a #-> <a xmlns:foo='bar' xmlns='twiddle'/>
- def add_namespace( prefix, uri=nil )
- unless uri
- @attributes["xmlns"] = prefix
- else
- prefix = "xmlns:#{prefix}" unless prefix =~ /^xmlns:/
- @attributes[ prefix ] = uri
- end
- self
- end
-
- # Removes a namespace from this node. This only works if the namespace is
- # actually declared in this node. If no argument is passed, deletes the
- # default namespace.
- #
- # Evaluates to: this element
- # doc = Document.new "<a xmlns:foo='bar' xmlns='twiddle'/>"
- # doc.root.delete_namespace
- # puts doc # -> <a xmlns:foo='bar'/>
- # doc.root.delete_namespace 'foo'
- # puts doc # -> <a/>
- def delete_namespace namespace="xmlns"
- namespace = "xmlns:#{namespace}" unless namespace == 'xmlns'
- attribute = attributes.get_attribute(namespace)
- attribute.remove unless attribute.nil?
- self
- end
-
- #################################################
- # Elements #
- #################################################
-
- # Adds a child to this element, optionally setting attributes in
- # the element.
- # element::
- # optional. If Element, the element is added.
- # Otherwise, a new Element is constructed with the argument (see
- # Element.initialize).
- # attrs::
- # If supplied, must be a Hash containing String name,value
- # pairs, which will be used to set the attributes of the new Element.
- # Returns:: the Element that was added
- # el = doc.add_element 'my-tag'
- # el = doc.add_element 'my-tag', {'attr1'=>'val1', 'attr2'=>'val2'}
- # el = Element.new 'my-tag'
- # doc.add_element el
- def add_element element, attrs=nil
- raise "First argument must be either an element name, or an Element object" if element.nil?
- el = @elements.add(element)
- attrs.each do |key, value|
- el.attributes[key]=value
- end if attrs.kind_of? Hash
- el
- end
-
- # Deletes a child element.
- # element::
- # Must be an +Element+, +String+, or +Integer+. If Element,
- # the element is removed. If String, the element is found (via XPath)
- # and removed. <em>This means that any parent can remove any
- # descendant.<em> If Integer, the Element indexed by that number will be
- # removed.
- # Returns:: the element that was removed.
- # doc.delete_element "/a/b/c[@id='4']"
- # doc.delete_element doc.elements["//k"]
- # doc.delete_element 1
- def delete_element element
- @elements.delete element
- end
-
- # Evaluates to +true+ if this element has at least one child Element
- # doc = Document.new "<a><b/><c>Text</c></a>"
- # doc.root.has_elements # -> true
- # doc.elements["/a/b"].has_elements # -> false
- # doc.elements["/a/c"].has_elements # -> false
- def has_elements?
- !@elements.empty?
- end
-
- # Iterates through the child elements, yielding for each Element that
- # has a particular attribute set.
- # key::
- # the name of the attribute to search for
- # value::
- # the value of the attribute
- # max::
- # (optional) causes this method to return after yielding
- # for this number of matching children
- # name::
- # (optional) if supplied, this is an XPath that filters
- # the children to check.
- #
- # doc = Document.new "<a><b @id='1'/><c @id='2'/><d @id='1'/><e/></a>"
- # # Yields b, c, d
- # doc.root.each_element_with_attribute( 'id' ) {|e| p e}
- # # Yields b, d
- # doc.root.each_element_with_attribute( 'id', '1' ) {|e| p e}
- # # Yields b
- # doc.root.each_element_with_attribute( 'id', '1', 1 ) {|e| p e}
- # # Yields d
- # doc.root.each_element_with_attribute( 'id', '1', 0, 'd' ) {|e| p e}
- def each_element_with_attribute( key, value=nil, max=0, name=nil, &block ) # :yields: Element
- each_with_something( proc {|child|
- if value.nil?
- child.attributes[key] != nil
- else
- child.attributes[key]==value
- end
- }, max, name, &block )
- end
-
- # Iterates through the children, yielding for each Element that
- # has a particular text set.
- # text::
- # the text to search for. If nil, or not supplied, will iterate
- # over all +Element+ children that contain at least one +Text+ node.
- # max::
- # (optional) causes this method to return after yielding
- # for this number of matching children
- # name::
- # (optional) if supplied, this is an XPath that filters
- # the children to check.
- #
- # doc = Document.new '<a><b>b</b><c>b</c><d>d</d><e/></a>'
- # # Yields b, c, d
- # doc.each_element_with_text {|e|p e}
- # # Yields b, c
- # doc.each_element_with_text('b'){|e|p e}
- # # Yields b
- # doc.each_element_with_text('b', 1){|e|p e}
- # # Yields d
- # doc.each_element_with_text(nil, 0, 'd'){|e|p e}
- def each_element_with_text( text=nil, max=0, name=nil, &block ) # :yields: Element
- each_with_something( proc {|child|
- if text.nil?
- child.has_text?
- else
- child.text == text
- end
- }, max, name, &block )
- end
-
- # Synonym for Element.elements.each
- def each_element( xpath=nil, &block ) # :yields: Element
- @elements.each( xpath, &block )
- end
-
- # Synonym for Element.to_a
- # This is a little slower than calling elements.each directly.
- # xpath:: any XPath by which to search for elements in the tree
- # Returns:: an array of Elements that match the supplied path
- def get_elements( xpath )
- @elements.to_a( xpath )
- end
-
- # Returns the next sibling that is an element, or nil if there is
- # no Element sibling after this one
- # doc = Document.new '<a><b/>text<c/></a>'
- # doc.root.elements['b'].next_element #-> <c/>
- # doc.root.elements['c'].next_element #-> nil
- def next_element
- element = next_sibling
- element = element.next_sibling until element.nil? or element.kind_of? Element
- return element
- end
-
- # Returns the previous sibling that is an element, or nil if there is
- # no Element sibling prior to this one
- # doc = Document.new '<a><b/>text<c/></a>'
- # doc.root.elements['c'].previous_element #-> <b/>
- # doc.root.elements['b'].previous_element #-> nil
- def previous_element
- element = previous_sibling
- element = element.previous_sibling until element.nil? or element.kind_of? Element
- return element
- end
-
-
- #################################################
- # Text #
- #################################################
-
- # Evaluates to +true+ if this element has at least one Text child
- def has_text?
- not text().nil?
- end
-
- # A convenience method which returns the String value of the _first_
- # child text element, if one exists, and +nil+ otherwise.
- #
- # <em>Note that an element may have multiple Text elements, perhaps
- # separated by other children</em>. Be aware that this method only returns
- # the first Text node.
- #
- # This method returns the +value+ of the first text child node, which
- # ignores the +raw+ setting, so always returns normalized text. See
- # the Text::value documentation.
- #
- # doc = Document.new "<p>some text <b>this is bold!</b> more text</p>"
- # # The element 'p' has two text elements, "some text " and " more text".
- # doc.root.text #-> "some text "
- def text( path = nil )
- rv = get_text(path)
- return rv.value unless rv.nil?
- nil
- end
-
- # Returns the first child Text node, if any, or +nil+ otherwise.
- # This method returns the actual +Text+ node, rather than the String content.
- # doc = Document.new "<p>some text <b>this is bold!</b> more text</p>"
- # # The element 'p' has two text elements, "some text " and " more text".
- # doc.root.get_text.value #-> "some text "
- def get_text path = nil
- rv = nil
- if path
- element = @elements[ path ]
- rv = element.get_text unless element.nil?
- else
- rv = @children.find { |node| node.kind_of? Text }
- end
- return rv
- end
-
- # Sets the first Text child of this object. See text() for a
- # discussion about Text children.
- #
- # If a Text child already exists, the child is replaced by this
- # content. This means that Text content can be deleted by calling
- # this method with a nil argument. In this case, the next Text
- # child becomes the first Text child. In no case is the order of
- # any siblings disturbed.
- # text::
- # If a String, a new Text child is created and added to
- # this Element as the first Text child. If Text, the text is set
- # as the first Child element. If nil, then any existing first Text
- # child is removed.
- # Returns:: this Element.
- # doc = Document.new '<a><b/></a>'
- # doc.root.text = 'Sean' #-> '<a><b/>Sean</a>'
- # doc.root.text = 'Elliott' #-> '<a><b/>Elliott</a>'
- # doc.root.add_element 'c' #-> '<a><b/>Elliott<c/></a>'
- # doc.root.text = 'Russell' #-> '<a><b/>Russell<c/></a>'
- # doc.root.text = nil #-> '<a><b/><c/></a>'
- def text=( text )
- if text.kind_of? String
- text = Text.new( text, whitespace(), nil, raw() )
- elsif text and !text.kind_of? Text
- text = Text.new( text.to_s, whitespace(), nil, raw() )
- end
- old_text = get_text
- if text.nil?
- old_text.remove unless old_text.nil?
- else
- if old_text.nil?
- self << text
- else
- old_text.replace_with( text )
- end
- end
- return self
- end
-
- # A helper method to add a Text child. Actual Text instances can
- # be added with regular Parent methods, such as add() and <<()
- # text::
- # if a String, a new Text instance is created and added
- # to the parent. If Text, the object is added directly.
- # Returns:: this Element
- # e = Element.new('a') #-> <e/>
- # e.add_text 'foo' #-> <e>foo</e>
- # e.add_text Text.new(' bar') #-> <e>foo bar</e>
- # Note that at the end of this example, the branch has <b>3</b> nodes; the 'e'
- # element and <b>2</b> Text node children.
- def add_text( text )
- if text.kind_of? String
- if @children[-1].kind_of? Text
- @children[-1] << text
- return
- end
- text = Text.new( text, whitespace(), nil, raw() )
- end
- self << text unless text.nil?
- return self
- end
-
- def node_type
- :element
- end
-
- def xpath
- path_elements = []
- cur = self
- path_elements << __to_xpath_helper( self )
- while cur.parent
- cur = cur.parent
- path_elements << __to_xpath_helper( cur )
- end
- return path_elements.reverse.join( "/" )
- end
-
- #################################################
- # Attributes #
- #################################################
-
- def attribute( name, namespace=nil )
- prefix = nil
- if namespaces.respond_to? :key
- prefix = namespaces.key(namespace) if namespace
- else
- prefix = namespaces.index(namespace) if namespace
- end
- prefix = nil if prefix == 'xmlns'
-
- ret_val =
- attributes.get_attribute( "#{prefix ? prefix + ':' : ''}#{name}" )
-
- return ret_val unless ret_val.nil?
- return nil if prefix.nil?
-
- # now check that prefix'es namespace is not the same as the
- # default namespace
- return nil unless ( namespaces[ prefix ] == namespaces[ 'xmlns' ] )
-
- attributes.get_attribute( name )
-
- end
-
- # Evaluates to +true+ if this element has any attributes set, false
- # otherwise.
- def has_attributes?
- return !@attributes.empty?
- end
-
- # Adds an attribute to this element, overwriting any existing attribute
- # by the same name.
- # key::
- # can be either an Attribute or a String. If an Attribute,
- # the attribute is added to the list of Element attributes. If String,
- # the argument is used as the name of the new attribute, and the value
- # parameter must be supplied.
- # value::
- # Required if +key+ is a String, and ignored if the first argument is
- # an Attribute. This is a String, and is used as the value
- # of the new Attribute. This should be the unnormalized value of the
- # attribute (without entities).
- # Returns:: the Attribute added
- # e = Element.new 'e'
- # e.add_attribute( 'a', 'b' ) #-> <e a='b'/>
- # e.add_attribute( 'x:a', 'c' ) #-> <e a='b' x:a='c'/>
- # e.add_attribute Attribute.new('b', 'd') #-> <e a='b' x:a='c' b='d'/>
- def add_attribute( key, value=nil )
- if key.kind_of? Attribute
- @attributes << key
- else
- @attributes[key] = value
- end
- end
-
- # Add multiple attributes to this element.
- # hash:: is either a hash, or array of arrays
- # el.add_attributes( {"name1"=>"value1", "name2"=>"value2"} )
- # el.add_attributes( [ ["name1","value1"], ["name2"=>"value2"] ] )
- def add_attributes hash
- if hash.kind_of? Hash
- hash.each_pair {|key, value| @attributes[key] = value }
- elsif hash.kind_of? Array
- hash.each { |value| @attributes[ value[0] ] = value[1] }
- end
- end
-
- # Removes an attribute
- # key::
- # either an Attribute or a String. In either case, the
- # attribute is found by matching the attribute name to the argument,
- # and then removed. If no attribute is found, no action is taken.
- # Returns::
- # the attribute removed, or nil if this Element did not contain
- # a matching attribute
- # e = Element.new('E')
- # e.add_attribute( 'name', 'Sean' ) #-> <E name='Sean'/>
- # r = e.add_attribute( 'sur:name', 'Russell' ) #-> <E name='Sean' sur:name='Russell'/>
- # e.delete_attribute( 'name' ) #-> <E sur:name='Russell'/>
- # e.delete_attribute( r ) #-> <E/>
- def delete_attribute(key)
- attr = @attributes.get_attribute(key)
- attr.remove unless attr.nil?
- end
-
- #################################################
- # Other Utilities #
- #################################################
-
- # Get an array of all CData children.
- # IMMUTABLE
- def cdatas
- find_all { |child| child.kind_of? CData }.freeze
- end
-
- # Get an array of all Comment children.
- # IMMUTABLE
- def comments
- find_all { |child| child.kind_of? Comment }.freeze
- end
-
- # Get an array of all Instruction children.
- # IMMUTABLE
- def instructions
- find_all { |child| child.kind_of? Instruction }.freeze
- end
-
- # Get an array of all Text children.
- # IMMUTABLE
- def texts
- find_all { |child| child.kind_of? Text }.freeze
- end
-
- # == DEPRECATED
- # See REXML::Formatters
- #
- # Writes out this element, and recursively, all children.
- # output::
- # output an object which supports '<< string'; this is where the
- # document will be written.
- # indent::
- # An integer. If -1, no indenting will be used; otherwise, the
- # indentation will be this number of spaces, and children will be
- # indented an additional amount. Defaults to -1
- # transitive::
- # If transitive is true and indent is >= 0, then the output will be
- # pretty-printed in such a way that the added whitespace does not affect
- # the parse tree of the document
- # ie_hack::
- # Internet Explorer is the worst piece of crap to have ever been
- # written, with the possible exception of Windows itself. Since IE is
- # unable to parse proper XML, we have to provide a hack to generate XML
- # that IE's limited abilities can handle. This hack inserts a space
- # before the /> on empty tags. Defaults to false
- #
- # out = ''
- # doc.write( out ) #-> doc is written to the string 'out'
- # doc.write( $stdout ) #-> doc written to the console
- def write(output=$stdout, indent=-1, transitive=false, ie_hack=false)
- Kernel.warn("#{self.class.name}.write is deprecated. See REXML::Formatters")
- formatter = if indent > -1
- if transitive
- require "rexml/formatters/transitive"
- REXML::Formatters::Transitive.new( indent, ie_hack )
- else
- REXML::Formatters::Pretty.new( indent, ie_hack )
- end
- else
- REXML::Formatters::Default.new( ie_hack )
- end
- formatter.write( self, output )
- end
-
-
- private
- def __to_xpath_helper node
- rv = node.expanded_name.clone
- if node.parent
- results = node.parent.find_all {|n|
- n.kind_of?(REXML::Element) and n.expanded_name == node.expanded_name
- }
- if results.length > 1
- idx = results.index( node )
- rv << "[#{idx+1}]"
- end
- end
- rv
- end
-
- # A private helper method
- def each_with_something( test, max=0, name=nil )
- num = 0
- @elements.each( name ){ |child|
- yield child if test.call(child) and num += 1
- return if max>0 and num == max
- }
- end
- end
-
- ########################################################################
- # ELEMENTS #
- ########################################################################
-
- # A class which provides filtering of children for Elements, and
- # XPath search support. You are expected to only encounter this class as
- # the <tt>element.elements</tt> object. Therefore, you are
- # _not_ expected to instantiate this yourself.
- class Elements
- include Enumerable
- # Constructor
- # parent:: the parent Element
- def initialize parent
- @element = parent
- end
-
- # Fetches a child element. Filters only Element children, regardless of
- # the XPath match.
- # index::
- # the search parameter. This is either an Integer, which
- # will be used to find the index'th child Element, or an XPath,
- # which will be used to search for the Element. <em>Because
- # of the nature of XPath searches, any element in the connected XML
- # document can be fetched through any other element.</em> <b>The
- # Integer index is 1-based, not 0-based.</b> This means that the first
- # child element is at index 1, not 0, and the +n+th element is at index
- # +n+, not <tt>n-1</tt>. This is because XPath indexes element children
- # starting from 1, not 0, and the indexes should be the same.
- # name::
- # optional, and only used in the first argument is an
- # Integer. In that case, the index'th child Element that has the
- # supplied name will be returned. Note again that the indexes start at 1.
- # Returns:: the first matching Element, or nil if no child matched
- # doc = Document.new '<a><b/><c id="1"/><c id="2"/><d/></a>'
- # doc.root.elements[1] #-> <b/>
- # doc.root.elements['c'] #-> <c id="1"/>
- # doc.root.elements[2,'c'] #-> <c id="2"/>
- def []( index, name=nil)
- if index.kind_of? Integer
- raise "index (#{index}) must be >= 1" if index < 1
- name = literalize(name) if name
- num = 0
- @element.find { |child|
- child.kind_of? Element and
- (name.nil? ? true : child.has_name?( name )) and
- (num += 1) == index
- }
- else
- return XPath::first( @element, index )
- #{ |element|
- # return element if element.kind_of? Element
- #}
- #return nil
- end
- end
-
- # Sets an element, replacing any previous matching element. If no
- # existing element is found ,the element is added.
- # index:: Used to find a matching element to replace. See []().
- # element::
- # The element to replace the existing element with
- # the previous element
- # Returns:: nil if no previous element was found.
- #
- # doc = Document.new '<a/>'
- # doc.root.elements[10] = Element.new('b') #-> <a><b/></a>
- # doc.root.elements[1] #-> <b/>
- # doc.root.elements[1] = Element.new('c') #-> <a><c/></a>
- # doc.root.elements['c'] = Element.new('d') #-> <a><d/></a>
- def []=( index, element )
- previous = self[index]
- if previous.nil?
- @element.add element
- else
- previous.replace_with element
- end
- return previous
- end
-
- # Returns +true+ if there are no +Element+ children, +false+ otherwise
- def empty?
- @element.find{ |child| child.kind_of? Element}.nil?
- end
-
- # Returns the index of the supplied child (starting at 1), or -1 if
- # the element is not a child
- # element:: an +Element+ child
- def index element
- rv = 0
- found = @element.find do |child|
- child.kind_of? Element and
- (rv += 1) and
- child == element
- end
- return rv if found == element
- return -1
- end
-
- # Deletes a child Element
- # element::
- # Either an Element, which is removed directly; an
- # xpath, where the first matching child is removed; or an Integer,
- # where the n'th Element is removed.
- # Returns:: the removed child
- # doc = Document.new '<a><b/><c/><c id="1"/></a>'
- # b = doc.root.elements[1]
- # doc.root.elements.delete b #-> <a><c/><c id="1"/></a>
- # doc.elements.delete("a/c[@id='1']") #-> <a><c/></a>
- # doc.root.elements.delete 1 #-> <a/>
- def delete element
- if element.kind_of? Element
- @element.delete element
- else
- el = self[element]
- el.remove if el
- end
- end
-
- # Removes multiple elements. Filters for Element children, regardless of
- # XPath matching.
- # xpath:: all elements matching this String path are removed.
- # Returns:: an Array of Elements that have been removed
- # doc = Document.new '<a><c/><c/><c/><c/></a>'
- # deleted = doc.elements.delete_all 'a/c' #-> [<c/>, <c/>, <c/>, <c/>]
- def delete_all( xpath )
- rv = []
- XPath::each( @element, xpath) {|element|
- rv << element if element.kind_of? Element
- }
- rv.each do |element|
- @element.delete element
- element.remove
- end
- return rv
- end
-
- # Adds an element
- # element::
- # if supplied, is either an Element, String, or
- # Source (see Element.initialize). If not supplied or nil, a
- # new, default Element will be constructed
- # Returns:: the added Element
- # a = Element.new('a')
- # a.elements.add(Element.new('b')) #-> <a><b/></a>
- # a.elements.add('c') #-> <a><b/><c/></a>
- def add element=nil
- rv = nil
- if element.nil?
- Element.new("", self, @element.context)
- elsif not element.kind_of?(Element)
- Element.new(element, self, @element.context)
- else
- @element << element
- element.context = @element.context
- element
- end
- end
-
- alias :<< :add
-
- # Iterates through all of the child Elements, optionally filtering
- # them by a given XPath
- # xpath::
- # optional. If supplied, this is a String XPath, and is used to
- # filter the children, so that only matching children are yielded. Note
- # that XPaths are automatically filtered for Elements, so that
- # non-Element children will not be yielded
- # doc = Document.new '<a><b/><c/><d/>sean<b/><c/><d/></a>'
- # doc.root.each {|e|p e} #-> Yields b, c, d, b, c, d elements
- # doc.root.each('b') {|e|p e} #-> Yields b, b elements
- # doc.root.each('child::node()') {|e|p e}
- # #-> Yields <b/>, <c/>, <d/>, <b/>, <c/>, <d/>
- # XPath.each(doc.root, 'child::node()', &block)
- # #-> Yields <b/>, <c/>, <d/>, sean, <b/>, <c/>, <d/>
- def each( xpath=nil, &block)
- XPath::each( @element, xpath ) {|e| yield e if e.kind_of? Element }
- end
-
- def collect( xpath=nil, &block )
- collection = []
- XPath::each( @element, xpath ) {|e|
- collection << yield(e) if e.kind_of?(Element)
- }
- collection
- end
-
- def inject( xpath=nil, initial=nil, &block )
- first = true
- XPath::each( @element, xpath ) {|e|
- if (e.kind_of? Element)
- if (first and initial == nil)
- initial = e
- first = false
- else
- initial = yield( initial, e ) if e.kind_of? Element
- end
- end
- }
- initial
- end
-
- # Returns the number of +Element+ children of the parent object.
- # doc = Document.new '<a>sean<b/>elliott<b/>russell<b/></a>'
- # doc.root.size #-> 6, 3 element and 3 text nodes
- # doc.root.elements.size #-> 3
- def size
- count = 0
- @element.each {|child| count+=1 if child.kind_of? Element }
- count
- end
-
- # Returns an Array of Element children. An XPath may be supplied to
- # filter the children. Only Element children are returned, even if the
- # supplied XPath matches non-Element children.
- # doc = Document.new '<a>sean<b/>elliott<c/></a>'
- # doc.root.elements.to_a #-> [ <b/>, <c/> ]
- # doc.root.elements.to_a("child::node()") #-> [ <b/>, <c/> ]
- # XPath.match(doc.root, "child::node()") #-> [ sean, <b/>, elliott, <c/> ]
- def to_a( xpath=nil )
- rv = XPath.match( @element, xpath )
- return rv.find_all{|e| e.kind_of? Element} if xpath
- rv
- end
-
- private
- # Private helper class. Removes quotes from quoted strings
- def literalize name
- name = name[1..-2] if name[0] == ?' or name[0] == ?" #'
- name
- end
- end
-
- ########################################################################
- # ATTRIBUTES #
- ########################################################################
-
- # A class that defines the set of Attributes of an Element and provides
- # operations for accessing elements in that set.
- class Attributes < Hash
- # Constructor
- # element:: the Element of which this is an Attribute
- def initialize element
- @element = element
- end
-
- # Fetches an attribute value. If you want to get the Attribute itself,
- # use get_attribute()
- # name:: an XPath attribute name. Namespaces are relevant here.
- # Returns::
- # the String value of the matching attribute, or +nil+ if no
- # matching attribute was found. This is the unnormalized value
- # (with entities expanded).
- #
- # doc = Document.new "<a foo:att='1' bar:att='2' att='&lt;'/>"
- # doc.root.attributes['att'] #-> '<'
- # doc.root.attributes['bar:att'] #-> '2'
- def [](name)
- attr = get_attribute(name)
- return attr.value unless attr.nil?
- return nil
- end
-
- def to_a
- values.flatten
- end
-
- # Returns the number of attributes the owning Element contains.
- # doc = Document "<a x='1' y='2' foo:x='3'/>"
- # doc.root.attributes.length #-> 3
- def length
- c = 0
- each_attribute { c+=1 }
- c
- end
- alias :size :length
-
- # Iterates over the attributes of an Element. Yields actual Attribute
- # nodes, not String values.
- #
- # doc = Document.new '<a x="1" y="2"/>'
- # doc.root.attributes.each_attribute {|attr|
- # p attr.expanded_name+" => "+attr.value
- # }
- def each_attribute # :yields: attribute
- each_value do |val|
- if val.kind_of? Attribute
- yield val
- else
- val.each_value { |atr| yield atr }
- end
- end
- end
-
- # Iterates over each attribute of an Element, yielding the expanded name
- # and value as a pair of Strings.
- #
- # doc = Document.new '<a x="1" y="2"/>'
- # doc.root.attributes.each {|name, value| p name+" => "+value }
- def each
- each_attribute do |attr|
- yield [attr.expanded_name, attr.value]
- end
- end
-
- # Fetches an attribute
- # name::
- # the name by which to search for the attribute. Can be a
- # <tt>prefix:name</tt> namespace name.
- # Returns:: The first matching attribute, or nil if there was none. This
- # value is an Attribute node, not the String value of the attribute.
- # doc = Document.new '<a x:foo="1" foo="2" bar="3"/>'
- # doc.root.attributes.get_attribute("foo").value #-> "2"
- # doc.root.attributes.get_attribute("x:foo").value #-> "1"
- def get_attribute( name )
- attr = fetch( name, nil )
- if attr.nil?
- return nil if name.nil?
- # Look for prefix
- name =~ Namespace::NAMESPLIT
- prefix, n = $1, $2
- if prefix
- attr = fetch( n, nil )
- # check prefix
- if attr == nil
- elsif attr.kind_of? Attribute
- return attr if prefix == attr.prefix
- else
- attr = attr[ prefix ]
- return attr
- end
- end
- element_document = @element.document
- if element_document and element_document.doctype
- expn = @element.expanded_name
- expn = element_document.doctype.name if expn.size == 0
- attr_val = element_document.doctype.attribute_of(expn, name)
- return Attribute.new( name, attr_val ) if attr_val
- end
- return nil
- end
- if attr.kind_of? Hash
- attr = attr[ @element.prefix ]
- end
- return attr
- end
-
- # Sets an attribute, overwriting any existing attribute value by the
- # same name. Namespace is significant.
- # name:: the name of the attribute
- # value::
- # (optional) If supplied, the value of the attribute. If
- # nil, any existing matching attribute is deleted.
- # Returns::
- # Owning element
- # doc = Document.new "<a x:foo='1' foo='3'/>"
- # doc.root.attributes['y:foo'] = '2'
- # doc.root.attributes['foo'] = '4'
- # doc.root.attributes['x:foo'] = nil
- def []=( name, value )
- if value.nil? # Delete the named attribute
- attr = get_attribute(name)
- delete attr
- return
- end
- element_document = @element.document
- unless value.kind_of? Attribute
- if @element.document and @element.document.doctype
- value = Text::normalize( value, @element.document.doctype )
- else
- value = Text::normalize( value, nil )
- end
- value = Attribute.new(name, value)
- end
- value.element = @element
- old_attr = fetch(value.name, nil)
- if old_attr.nil?
- store(value.name, value)
- elsif old_attr.kind_of? Hash
- old_attr[value.prefix] = value
- elsif old_attr.prefix != value.prefix
- # Check for conflicting namespaces
- raise ParseException.new(
- "Namespace conflict in adding attribute \"#{value.name}\": "+
- "Prefix \"#{old_attr.prefix}\" = "+
- "\"#{@element.namespace(old_attr.prefix)}\" and prefix "+
- "\"#{value.prefix}\" = \"#{@element.namespace(value.prefix)}\"") if
- value.prefix != "xmlns" and old_attr.prefix != "xmlns" and
- @element.namespace( old_attr.prefix ) ==
- @element.namespace( value.prefix )
- store value.name, { old_attr.prefix => old_attr,
- value.prefix => value }
- else
- store value.name, value
- end
- return @element
- end
-
- # Returns an array of Strings containing all of the prefixes declared
- # by this set of # attributes. The array does not include the default
- # namespace declaration, if one exists.
- # doc = Document.new("<a xmlns='foo' xmlns:x='bar' xmlns:y='twee' "+
- # "z='glorp' p:k='gru'/>")
- # prefixes = doc.root.attributes.prefixes #-> ['x', 'y']
- def prefixes
- ns = []
- each_attribute do |attribute|
- ns << attribute.name if attribute.prefix == 'xmlns'
- end
- if @element.document and @element.document.doctype
- expn = @element.expanded_name
- expn = @element.document.doctype.name if expn.size == 0
- @element.document.doctype.attributes_of(expn).each {
- |attribute|
- ns << attribute.name if attribute.prefix == 'xmlns'
- }
- end
- ns
- end
-
- def namespaces
- namespaces = {}
- each_attribute do |attribute|
- namespaces[attribute.name] = attribute.value if attribute.prefix == 'xmlns' or attribute.name == 'xmlns'
- end
- if @element.document and @element.document.doctype
- expn = @element.expanded_name
- expn = @element.document.doctype.name if expn.size == 0
- @element.document.doctype.attributes_of(expn).each {
- |attribute|
- namespaces[attribute.name] = attribute.value if attribute.prefix == 'xmlns' or attribute.name == 'xmlns'
- }
- end
- namespaces
- end
-
- # Removes an attribute
- # attribute::
- # either a String, which is the name of the attribute to remove --
- # namespaces are significant here -- or the attribute to remove.
- # Returns:: the owning element
- # doc = Document.new "<a y:foo='0' x:foo='1' foo='3' z:foo='4'/>"
- # doc.root.attributes.delete 'foo' #-> <a y:foo='0' x:foo='1' z:foo='4'/>"
- # doc.root.attributes.delete 'x:foo' #-> <a y:foo='0' z:foo='4'/>"
- # attr = doc.root.attributes.get_attribute('y:foo')
- # doc.root.attributes.delete attr #-> <a z:foo='4'/>"
- def delete( attribute )
- name = nil
- prefix = nil
- if attribute.kind_of? Attribute
- name = attribute.name
- prefix = attribute.prefix
- else
- attribute =~ Namespace::NAMESPLIT
- prefix, name = $1, $2
- prefix = '' unless prefix
- end
- old = fetch(name, nil)
- attr = nil
- if old.kind_of? Hash # the supplied attribute is one of many
- attr = old.delete(prefix)
- if old.size == 1
- repl = nil
- old.each_value{|v| repl = v}
- store name, repl
- end
- elsif old.nil?
- return @element
- else # the supplied attribute is a top-level one
- attr = old
- res = super(name)
- end
- @element
- end
-
- # Adds an attribute, overriding any existing attribute by the
- # same name. Namespaces are significant.
- # attribute:: An Attribute
- def add( attribute )
- self[attribute.name] = attribute
- end
-
- alias :<< :add
-
- # Deletes all attributes matching a name. Namespaces are significant.
- # name::
- # A String; all attributes that match this path will be removed
- # Returns:: an Array of the Attributes that were removed
- def delete_all( name )
- rv = []
- each_attribute { |attribute|
- rv << attribute if attribute.expanded_name == name
- }
- rv.each{ |attr| attr.remove }
- return rv
- end
-
- # The +get_attribute_ns+ method retrieves a method by its namespace
- # and name. Thus it is possible to reliably identify an attribute
- # even if an XML processor has changed the prefix.
- #
- # Method contributed by Henrik Martensson
- def get_attribute_ns(namespace, name)
- result = nil
- each_attribute() { |attribute|
- if name == attribute.name &&
- namespace == attribute.namespace() &&
- ( !namespace.empty? || !attribute.fully_expanded_name.index(':') )
- # foo will match xmlns:foo, but only if foo isn't also an attribute
- result = attribute if !result or !namespace.empty? or
- !attribute.fully_expanded_name.index(':')
- end
- }
- result
- end
- end
-end
diff --git a/lib/rexml/encoding.rb b/lib/rexml/encoding.rb
deleted file mode 100644
index 608c69cd65..0000000000
--- a/lib/rexml/encoding.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# -*- mode: ruby; ruby-indent-level: 2; indent-tabs-mode: t; tab-width: 2 -*- vim: sw=2 ts=2
-module REXML
- module Encoding
- @encoding_methods = {}
- def self.register(enc, &block)
- @encoding_methods[enc] = block
- end
- def self.apply(obj, enc)
- @encoding_methods[enc][obj]
- end
- def self.encoding_method(enc)
- @encoding_methods[enc]
- end
-
- # Native, default format is UTF-8, so it is declared here rather than in
- # an encodings/ definition.
- UTF_8 = 'UTF-8'
- UTF_16 = 'UTF-16'
- UNILE = 'UNILE'
-
- # ID ---> Encoding name
- attr_reader :encoding
- def encoding=( enc )
- old_verbosity = $VERBOSE
- begin
- $VERBOSE = false
- enc = enc.nil? ? nil : enc.upcase
- return false if defined? @encoding and enc == @encoding
- if enc and enc != UTF_8
- @encoding = enc
- raise ArgumentError, "Bad encoding name #@encoding" unless @encoding =~ /^[\w-]+$/
- @encoding.untaint
- begin
- require 'rexml/encodings/ICONV.rb'
- Encoding.apply(self, "ICONV")
- rescue LoadError, Exception
- begin
- enc_file = File.join( "rexml", "encodings", "#@encoding.rb" )
- require enc_file
- Encoding.apply(self, @encoding)
- rescue LoadError => err
- puts err.message
- raise ArgumentError, "No decoder found for encoding #@encoding. Please install iconv."
- end
- end
- else
- @encoding = UTF_8
- require 'rexml/encodings/UTF-8.rb'
- Encoding.apply(self, @encoding)
- end
- ensure
- $VERBOSE = old_verbosity
- end
- true
- end
-
- def check_encoding str
- # We have to recognize UTF-16, LSB UTF-16, and UTF-8
- if str[0,2] == "\xfe\xff"
- str[0,2] = ""
- return UTF_16
- elsif str[0,2] == "\xff\xfe"
- str[0,2] = ""
- return UNILE
- end
- str =~ /^\s*<\?xml\s+version\s*=\s*(['"]).*?\1\s+encoding\s*=\s*(["'])(.*?)\2/m
- return $3.upcase if $3
- return UTF_8
- end
- end
-end
diff --git a/lib/rexml/encodings/CP-1252.rb b/lib/rexml/encodings/CP-1252.rb
deleted file mode 100644
index 2ef6a1a291..0000000000
--- a/lib/rexml/encodings/CP-1252.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-#
-# This class was contributed by Mikko Tiihonen mikko DOT tiihonen AT hut DOT fi
-#
-module REXML
- module Encoding
- register( "CP-1252" ) do |o|
- class << o
- alias encode encode_cp1252
- alias decode decode_cp1252
- end
- end
-
- # Convert from UTF-8
- def encode_cp1252(content)
- array_utf8 = content.unpack('U*')
- array_enc = []
- array_utf8.each do |num|
- case num
- # shortcut first bunch basic characters
- when 0..0xFF; array_enc << num
- # characters added compared to iso-8859-1
- when 0x20AC; array_enc << 0x80 # 0xe2 0x82 0xac
- when 0x201A; array_enc << 0x82 # 0xe2 0x82 0x9a
- when 0x0192; array_enc << 0x83 # 0xc6 0x92
- when 0x201E; array_enc << 0x84 # 0xe2 0x82 0x9e
- when 0x2026; array_enc << 0x85 # 0xe2 0x80 0xa6
- when 0x2020; array_enc << 0x86 # 0xe2 0x80 0xa0
- when 0x2021; array_enc << 0x87 # 0xe2 0x80 0xa1
- when 0x02C6; array_enc << 0x88 # 0xcb 0x86
- when 0x2030; array_enc << 0x89 # 0xe2 0x80 0xb0
- when 0x0160; array_enc << 0x8A # 0xc5 0xa0
- when 0x2039; array_enc << 0x8B # 0xe2 0x80 0xb9
- when 0x0152; array_enc << 0x8C # 0xc5 0x92
- when 0x017D; array_enc << 0x8E # 0xc5 0xbd
- when 0x2018; array_enc << 0x91 # 0xe2 0x80 0x98
- when 0x2019; array_enc << 0x92 # 0xe2 0x80 0x99
- when 0x201C; array_enc << 0x93 # 0xe2 0x80 0x9c
- when 0x201D; array_enc << 0x94 # 0xe2 0x80 0x9d
- when 0x2022; array_enc << 0x95 # 0xe2 0x80 0xa2
- when 0x2013; array_enc << 0x96 # 0xe2 0x80 0x93
- when 0x2014; array_enc << 0x97 # 0xe2 0x80 0x94
- when 0x02DC; array_enc << 0x98 # 0xcb 0x9c
- when 0x2122; array_enc << 0x99 # 0xe2 0x84 0xa2
- when 0x0161; array_enc << 0x9A # 0xc5 0xa1
- when 0x203A; array_enc << 0x9B # 0xe2 0x80 0xba
- when 0x0152; array_enc << 0x9C # 0xc5 0x93
- when 0x017E; array_enc << 0x9E # 0xc5 0xbe
- when 0x0178; array_enc << 0x9F # 0xc5 0xb8
- else
- # all remaining basic characters can be used directly
- if num <= 0xFF
- array_enc << num
- else
- # Numeric entity (&#nnnn;); shard by Stefan Scholl
- array_enc.concat "&\##{num};".unpack('C*')
- end
- end
- end
- array_enc.pack('C*')
- end
-
- # Convert to UTF-8
- def decode_cp1252(str)
- array_latin9 = str.unpack('C*')
- array_enc = []
- array_latin9.each do |num|
- case num
- # characters that added compared to iso-8859-1
- when 0x80; array_enc << 0x20AC # 0xe2 0x82 0xac
- when 0x82; array_enc << 0x201A # 0xe2 0x82 0x9a
- when 0x83; array_enc << 0x0192 # 0xc6 0x92
- when 0x84; array_enc << 0x201E # 0xe2 0x82 0x9e
- when 0x85; array_enc << 0x2026 # 0xe2 0x80 0xa6
- when 0x86; array_enc << 0x2020 # 0xe2 0x80 0xa0
- when 0x87; array_enc << 0x2021 # 0xe2 0x80 0xa1
- when 0x88; array_enc << 0x02C6 # 0xcb 0x86
- when 0x89; array_enc << 0x2030 # 0xe2 0x80 0xb0
- when 0x8A; array_enc << 0x0160 # 0xc5 0xa0
- when 0x8B; array_enc << 0x2039 # 0xe2 0x80 0xb9
- when 0x8C; array_enc << 0x0152 # 0xc5 0x92
- when 0x8E; array_enc << 0x017D # 0xc5 0xbd
- when 0x91; array_enc << 0x2018 # 0xe2 0x80 0x98
- when 0x92; array_enc << 0x2019 # 0xe2 0x80 0x99
- when 0x93; array_enc << 0x201C # 0xe2 0x80 0x9c
- when 0x94; array_enc << 0x201D # 0xe2 0x80 0x9d
- when 0x95; array_enc << 0x2022 # 0xe2 0x80 0xa2
- when 0x96; array_enc << 0x2013 # 0xe2 0x80 0x93
- when 0x97; array_enc << 0x2014 # 0xe2 0x80 0x94
- when 0x98; array_enc << 0x02DC # 0xcb 0x9c
- when 0x99; array_enc << 0x2122 # 0xe2 0x84 0xa2
- when 0x9A; array_enc << 0x0161 # 0xc5 0xa1
- when 0x9B; array_enc << 0x203A # 0xe2 0x80 0xba
- when 0x9C; array_enc << 0x0152 # 0xc5 0x93
- when 0x9E; array_enc << 0x017E # 0xc5 0xbe
- when 0x9F; array_enc << 0x0178 # 0xc5 0xb8
- else
- array_enc << num
- end
- end
- array_enc.pack('U*')
- end
- end
-end
diff --git a/lib/rexml/encodings/EUC-JP.rb b/lib/rexml/encodings/EUC-JP.rb
deleted file mode 100644
index db37b6bf0d..0000000000
--- a/lib/rexml/encodings/EUC-JP.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-module REXML
- module Encoding
- begin
- require 'uconv'
-
- def decode_eucjp(str)
- Uconv::euctou8(str)
- end
-
- def encode_eucjp content
- Uconv::u8toeuc(content)
- end
- rescue LoadError
- require 'nkf'
-
- EUCTOU8 = '-Ewm0'
- U8TOEUC = '-Wem0'
-
- def decode_eucjp(str)
- NKF.nkf(EUCTOU8, str)
- end
-
- def encode_eucjp content
- NKF.nkf(U8TOEUC, content)
- end
- end
-
- register("EUC-JP") do |obj|
- class << obj
- alias decode decode_eucjp
- alias encode encode_eucjp
- end
- end
- end
-end
diff --git a/lib/rexml/encodings/ICONV.rb b/lib/rexml/encodings/ICONV.rb
deleted file mode 100644
index 172fba7cd1..0000000000
--- a/lib/rexml/encodings/ICONV.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-require "iconv"
-raise LoadError unless defined? Iconv
-
-module REXML
- module Encoding
- def decode_iconv(str)
- Iconv.conv(UTF_8, @encoding, str)
- end
-
- def encode_iconv(content)
- Iconv.conv(@encoding, UTF_8, content)
- end
-
- register("ICONV") do |obj|
- Iconv.conv(UTF_8, obj.encoding, nil)
- class << obj
- alias decode decode_iconv
- alias encode encode_iconv
- end
- end
- end
-end
diff --git a/lib/rexml/encodings/ISO-8859-1.rb b/lib/rexml/encodings/ISO-8859-1.rb
deleted file mode 100644
index 2873d13bf0..0000000000
--- a/lib/rexml/encodings/ISO-8859-1.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'rexml/encodings/US-ASCII'
-
-module REXML
- module Encoding
- register("ISO-8859-1", &encoding_method("US-ASCII"))
- end
-end
diff --git a/lib/rexml/encodings/ISO-8859-15.rb b/lib/rexml/encodings/ISO-8859-15.rb
deleted file mode 100644
index 953267250e..0000000000
--- a/lib/rexml/encodings/ISO-8859-15.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-#
-# This class was contributed by Mikko Tiihonen mikko DOT tiihonen AT hut DOT fi
-#
-module REXML
- module Encoding
- register("ISO-8859-15") do |o|
- alias encode to_iso_8859_15
- alias decode from_iso_8859_15
- end
-
- # Convert from UTF-8
- def to_iso_8859_15(content)
- array_utf8 = content.unpack('U*')
- array_enc = []
- array_utf8.each do |num|
- case num
- # shortcut first bunch basic characters
- when 0..0xA3; array_enc << num
- # characters removed compared to iso-8859-1
- when 0xA4; array_enc << '&#164;'
- when 0xA6; array_enc << '&#166;'
- when 0xA8; array_enc << '&#168;'
- when 0xB4; array_enc << '&#180;'
- when 0xB8; array_enc << '&#184;'
- when 0xBC; array_enc << '&#188;'
- when 0xBD; array_enc << '&#189;'
- when 0xBE; array_enc << '&#190;'
- # characters added compared to iso-8859-1
- when 0x20AC; array_enc << 0xA4 # 0xe2 0x82 0xac
- when 0x0160; array_enc << 0xA6 # 0xc5 0xa0
- when 0x0161; array_enc << 0xA8 # 0xc5 0xa1
- when 0x017D; array_enc << 0xB4 # 0xc5 0xbd
- when 0x017E; array_enc << 0xB8 # 0xc5 0xbe
- when 0x0152; array_enc << 0xBC # 0xc5 0x92
- when 0x0153; array_enc << 0xBD # 0xc5 0x93
- when 0x0178; array_enc << 0xBE # 0xc5 0xb8
- else
- # all remaining basic characters can be used directly
- if num <= 0xFF
- array_enc << num
- else
- # Numeric entity (&#nnnn;); shard by Stefan Scholl
- array_enc.concat "&\##{num};".unpack('C*')
- end
- end
- end
- array_enc.pack('C*')
- end
-
- # Convert to UTF-8
- def from_iso_8859_15(str)
- array_latin9 = str.unpack('C*')
- array_enc = []
- array_latin9.each do |num|
- case num
- # characters that differ compared to iso-8859-1
- when 0xA4; array_enc << 0x20AC
- when 0xA6; array_enc << 0x0160
- when 0xA8; array_enc << 0x0161
- when 0xB4; array_enc << 0x017D
- when 0xB8; array_enc << 0x017E
- when 0xBC; array_enc << 0x0152
- when 0xBD; array_enc << 0x0153
- when 0xBE; array_enc << 0x0178
- else
- array_enc << num
- end
- end
- array_enc.pack('U*')
- end
- end
-end
diff --git a/lib/rexml/encodings/SHIFT-JIS.rb b/lib/rexml/encodings/SHIFT-JIS.rb
deleted file mode 100644
index 9e0f4af20e..0000000000
--- a/lib/rexml/encodings/SHIFT-JIS.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-module REXML
- module Encoding
- begin
- require 'uconv'
-
- def decode_sjis content
- Uconv::sjistou8(content)
- end
-
- def encode_sjis(str)
- Uconv::u8tosjis(str)
- end
- rescue LoadError
- require 'nkf'
-
- SJISTOU8 = '-Swm0x'
- U8TOSJIS = '-Wsm0x'
-
- def decode_sjis(str)
- NKF.nkf(SJISTOU8, str)
- end
-
- def encode_sjis content
- NKF.nkf(U8TOSJIS, content)
- end
- end
-
- b = proc do |obj|
- class << obj
- alias decode decode_sjis
- alias encode encode_sjis
- end
- end
- register("SHIFT-JIS", &b)
- register("SHIFT_JIS", &b)
- end
-end
diff --git a/lib/rexml/encodings/SHIFT_JIS.rb b/lib/rexml/encodings/SHIFT_JIS.rb
deleted file mode 100644
index e355704a7c..0000000000
--- a/lib/rexml/encodings/SHIFT_JIS.rb
+++ /dev/null
@@ -1 +0,0 @@
-require 'rexml/encodings/SHIFT-JIS'
diff --git a/lib/rexml/encodings/UNILE.rb b/lib/rexml/encodings/UNILE.rb
deleted file mode 100644
index d054140c40..0000000000
--- a/lib/rexml/encodings/UNILE.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module REXML
- module Encoding
- def encode_unile content
- array_utf8 = content.unpack("U*")
- array_enc = []
- array_utf8.each do |num|
- if ((num>>16) > 0)
- array_enc << ??
- array_enc << 0
- else
- array_enc << (num & 0xFF)
- array_enc << (num >> 8)
- end
- end
- array_enc.pack('C*')
- end
-
- def decode_unile(str)
- array_enc=str.unpack('C*')
- array_utf8 = []
- 0.step(array_enc.size-1, 2){|i|
- array_utf8 << (array_enc.at(i) + array_enc.at(i+1)*0x100)
- }
- array_utf8.pack('U*')
- end
-
- register(UNILE) do |obj|
- class << obj
- alias decode decode_unile
- alias encode encode_unile
- end
- end
- end
-end
diff --git a/lib/rexml/encodings/US-ASCII.rb b/lib/rexml/encodings/US-ASCII.rb
deleted file mode 100644
index fb4c217074..0000000000
--- a/lib/rexml/encodings/US-ASCII.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module REXML
- module Encoding
- # Convert from UTF-8
- def encode_ascii content
- array_utf8 = content.unpack('U*')
- array_enc = []
- array_utf8.each do |num|
- if num <= 0x7F
- array_enc << num
- else
- # Numeric entity (&#nnnn;); shard by Stefan Scholl
- array_enc.concat "&\##{num};".unpack('C*')
- end
- end
- array_enc.pack('C*')
- end
-
- # Convert to UTF-8
- def decode_ascii(str)
- str.unpack('C*').pack('U*')
- end
-
- register("US-ASCII") do |obj|
- class << obj
- alias decode decode_ascii
- alias encode encode_ascii
- end
- end
- end
-end
diff --git a/lib/rexml/encodings/UTF-16.rb b/lib/rexml/encodings/UTF-16.rb
deleted file mode 100644
index 007c493d9c..0000000000
--- a/lib/rexml/encodings/UTF-16.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-module REXML
- module Encoding
- def encode_utf16 content
- array_utf8 = content.unpack("U*")
- array_enc = []
- array_utf8.each do |num|
- if ((num>>16) > 0)
- array_enc << 0
- array_enc << ??
- else
- array_enc << (num >> 8)
- array_enc << (num & 0xFF)
- end
- end
- array_enc.pack('C*')
- end
-
- def decode_utf16(str)
- str = str[2..-1] if /^\376\377/n =~ str
- array_enc=str.unpack('C*')
- array_utf8 = []
- 0.step(array_enc.size-1, 2){|i|
- array_utf8 << (array_enc.at(i+1) + array_enc.at(i)*0x100)
- }
- array_utf8.pack('U*')
- end
-
- register(UTF_16) do |obj|
- class << obj
- alias decode decode_utf16
- alias encode encode_utf16
- end
- end
- end
-end
diff --git a/lib/rexml/encodings/UTF-8.rb b/lib/rexml/encodings/UTF-8.rb
deleted file mode 100644
index bb08f44100..0000000000
--- a/lib/rexml/encodings/UTF-8.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module REXML
- module Encoding
- def encode_utf8 content
- content
- end
-
- def decode_utf8(str)
- str
- end
-
- register(UTF_8) do |obj|
- class << obj
- alias decode decode_utf8
- alias encode encode_utf8
- end
- end
- end
-end
diff --git a/lib/rexml/entity.rb b/lib/rexml/entity.rb
deleted file mode 100644
index d2f27ecd44..0000000000
--- a/lib/rexml/entity.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-require 'rexml/child'
-require 'rexml/source'
-require 'rexml/xmltokens'
-
-module REXML
- # God, I hate DTDs. I really do. Why this idiot standard still
- # plagues us is beyond me.
- class Entity < Child
- include XMLTokens
- PUBIDCHAR = "\x20\x0D\x0Aa-zA-Z0-9\\-()+,./:=?;!*@$_%#"
- SYSTEMLITERAL = %Q{((?:"[^"]*")|(?:'[^']*'))}
- PUBIDLITERAL = %Q{("[#{PUBIDCHAR}']*"|'[#{PUBIDCHAR}]*')}
- EXTERNALID = "(?:(?:(SYSTEM)\\s+#{SYSTEMLITERAL})|(?:(PUBLIC)\\s+#{PUBIDLITERAL}\\s+#{SYSTEMLITERAL}))"
- NDATADECL = "\\s+NDATA\\s+#{NAME}"
- PEREFERENCE = "%#{NAME};"
- ENTITYVALUE = %Q{((?:"(?:[^%&"]|#{PEREFERENCE}|#{REFERENCE})*")|(?:'([^%&']|#{PEREFERENCE}|#{REFERENCE})*'))}
- PEDEF = "(?:#{ENTITYVALUE}|#{EXTERNALID})"
- ENTITYDEF = "(?:#{ENTITYVALUE}|(?:#{EXTERNALID}(#{NDATADECL})?))"
- PEDECL = "<!ENTITY\\s+(%)\\s+#{NAME}\\s+#{PEDEF}\\s*>"
- GEDECL = "<!ENTITY\\s+#{NAME}\\s+#{ENTITYDEF}\\s*>"
- ENTITYDECL = /\s*(?:#{GEDECL})|(?:#{PEDECL})/um
-
- attr_reader :name, :external, :ref, :ndata, :pubid
-
- # Create a new entity. Simple entities can be constructed by passing a
- # name, value to the constructor; this creates a generic, plain entity
- # reference. For anything more complicated, you have to pass a Source to
- # the constructor with the entity definiton, or use the accessor methods.
- # +WARNING+: There is no validation of entity state except when the entity
- # is read from a stream. If you start poking around with the accessors,
- # you can easily create a non-conformant Entity. The best thing to do is
- # dump the stupid DTDs and use XMLSchema instead.
- #
- # e = Entity.new( 'amp', '&' )
- def initialize stream, value=nil, parent=nil, reference=false
- super(parent)
- @ndata = @pubid = @value = @external = nil
- if stream.kind_of? Array
- @name = stream[1]
- if stream[-1] == '%'
- @reference = true
- stream.pop
- else
- @reference = false
- end
- if stream[2] =~ /SYSTEM|PUBLIC/
- @external = stream[2]
- if @external == 'SYSTEM'
- @ref = stream[3]
- @ndata = stream[4] if stream.size == 5
- else
- @pubid = stream[3]
- @ref = stream[4]
- end
- else
- @value = stream[2]
- end
- else
- @reference = reference
- @external = nil
- @name = stream
- @value = value
- end
- end
-
- # Evaluates whether the given string matchs an entity definition,
- # returning true if so, and false otherwise.
- def Entity::matches? string
- (ENTITYDECL =~ string) == 0
- end
-
- # Evaluates to the unnormalized value of this entity; that is, replacing
- # all entities -- both %ent; and &ent; entities. This differs from
- # +value()+ in that +value+ only replaces %ent; entities.
- def unnormalized
- document.record_entity_expansion unless document.nil?
- v = value()
- return nil if v.nil?
- @unnormalized = Text::unnormalize(v, parent)
- @unnormalized
- end
-
- #once :unnormalized
-
- # Returns the value of this entity unprocessed -- raw. This is the
- # normalized value; that is, with all %ent; and &ent; entities intact
- def normalized
- @value
- end
-
- # Write out a fully formed, correct entity definition (assuming the Entity
- # object itself is valid.)
- #
- # out::
- # An object implementing <TT>&lt;&lt;<TT> to which the entity will be
- # output
- # indent::
- # *DEPRECATED* and ignored
- def write out, indent=-1
- out << '<!ENTITY '
- out << '% ' if @reference
- out << @name
- out << ' '
- if @external
- out << @external << ' '
- if @pubid
- q = @pubid.include?('"')?"'":'"'
- out << q << @pubid << q << ' '
- end
- q = @ref.include?('"')?"'":'"'
- out << q << @ref << q
- out << ' NDATA ' << @ndata if @ndata
- else
- q = @value.include?('"')?"'":'"'
- out << q << @value << q
- end
- out << '>'
- end
-
- # Returns this entity as a string. See write().
- def to_s
- rv = ''
- write rv
- rv
- end
-
- PEREFERENCE_RE = /#{PEREFERENCE}/um
- # Returns the value of this entity. At the moment, only internal entities
- # are processed. If the value contains internal references (IE,
- # %blah;), those are replaced with their values. IE, if the doctype
- # contains:
- # <!ENTITY % foo "bar">
- # <!ENTITY yada "nanoo %foo; nanoo>
- # then:
- # doctype.entity('yada').value #-> "nanoo bar nanoo"
- def value
- if @value
- matches = @value.scan(PEREFERENCE_RE)
- rv = @value.clone
- if @parent
- matches.each do |entity_reference|
- entity_value = @parent.entity( entity_reference[0] )
- rv.gsub!( /%#{entity_reference.join};/um, entity_value )
- end
- end
- return rv
- end
- nil
- end
- end
-
- # This is a set of entity constants -- the ones defined in the XML
- # specification. These are +gt+, +lt+, +amp+, +quot+ and +apos+.
- module EntityConst
- # +>+
- GT = Entity.new( 'gt', '>' )
- # +<+
- LT = Entity.new( 'lt', '<' )
- # +&+
- AMP = Entity.new( 'amp', '&' )
- # +"+
- QUOT = Entity.new( 'quot', '"' )
- # +'+
- APOS = Entity.new( 'apos', "'" )
- end
-end
diff --git a/lib/rexml/formatters/default.rb b/lib/rexml/formatters/default.rb
deleted file mode 100644
index b4d63bc5b5..0000000000
--- a/lib/rexml/formatters/default.rb
+++ /dev/null
@@ -1,109 +0,0 @@
-module REXML
- module Formatters
- class Default
- # Prints out the XML document with no formatting -- except if id_hack is
- # set.
- #
- # ie_hack::
- # If set to true, then inserts whitespace before the close of an empty
- # tag, so that IE's bad XML parser doesn't choke.
- def initialize( ie_hack=false )
- @ie_hack = ie_hack
- end
-
- # Writes the node to some output.
- #
- # node::
- # The node to write
- # output::
- # A class implementing <TT>&lt;&lt;</TT>. Pass in an Output object to
- # change the output encoding.
- def write( node, output )
- case node
-
- when Document
- if node.xml_decl.encoding != "UTF-8" && !output.kind_of?(Output)
- output = Output.new( output, node.xml_decl.encoding )
- end
- write_document( node, output )
-
- when Element
- write_element( node, output )
-
- when Declaration, ElementDecl, NotationDecl, ExternalEntity, Entity,
- Attribute, AttlistDecl
- node.write( output,-1 )
-
- when Instruction
- write_instruction( node, output )
-
- when DocType, XMLDecl
- node.write( output )
-
- when Comment
- write_comment( node, output )
-
- when CData
- write_cdata( node, output )
-
- when Text
- write_text( node, output )
-
- else
- raise Exception.new("XML FORMATTING ERROR")
-
- end
- end
-
- protected
- def write_document( node, output )
- node.children.each { |child| write( child, output ) }
- end
-
- def write_element( node, output )
- output << "<#{node.expanded_name}"
-
- node.attributes.to_a.sort_by {|attr| attr.name}.each do |attr|
- output << " "
- attr.write( output )
- end unless node.attributes.empty?
-
- if node.children.empty?
- output << " " if @ie_hack
- output << "/"
- else
- output << ">"
- node.children.each { |child|
- write( child, output )
- }
- output << "</#{node.expanded_name}"
- end
- output << ">"
- end
-
- def write_text( node, output )
- output << node.to_s()
- end
-
- def write_comment( node, output )
- output << Comment::START
- output << node.to_s
- output << Comment::STOP
- end
-
- def write_cdata( node, output )
- output << CData::START
- output << node.to_s
- output << CData::STOP
- end
-
- def write_instruction( node, output )
- output << Instruction::START.sub(/\\/u, '')
- output << node.target
- output << ' '
- output << node.content
- output << Instruction::STOP.sub(/\\/u, '')
- end
- end
- end
-end
diff --git a/lib/rexml/formatters/pretty.rb b/lib/rexml/formatters/pretty.rb
deleted file mode 100644
index 84c442e8bb..0000000000
--- a/lib/rexml/formatters/pretty.rb
+++ /dev/null
@@ -1,139 +0,0 @@
-require 'rexml/formatters/default'
-
-module REXML
- module Formatters
- # Pretty-prints an XML document. This destroys whitespace in text nodes
- # and will insert carriage returns and indentations.
- #
- # TODO: Add an option to print attributes on new lines
- class Pretty < Default
-
- # If compact is set to true, then the formatter will attempt to use as
- # little space as possible
- attr_accessor :compact
- # The width of a page. Used for formatting text
- attr_accessor :width
-
- # Create a new pretty printer.
- #
- # output::
- # An object implementing '<<(String)', to which the output will be written.
- # indentation::
- # An integer greater than 0. The indentation of each level will be
- # this number of spaces. If this is < 1, the behavior of this object
- # is undefined. Defaults to 2.
- # ie_hack::
- # If true, the printer will insert whitespace before closing empty
- # tags, thereby allowing Internet Explorer's feeble XML parser to
- # function. Defaults to false.
- def initialize( indentation=2, ie_hack=false )
- @indentation = indentation
- @level = 0
- @ie_hack = ie_hack
- @width = 80
- @compact = false
- end
-
- protected
- def write_element(node, output)
- output << ' '*@level
- output << "<#{node.expanded_name}"
-
- node.attributes.each_attribute do |attr|
- output << " "
- attr.write( output )
- end unless node.attributes.empty?
-
- if node.children.empty?
- if @ie_hack
- output << " "
- end
- output << "/"
- else
- output << ">"
- # If compact and all children are text, and if the formatted output
- # is less than the specified width, then try to print everything on
- # one line
- skip = false
- if compact
- if node.children.inject(true) {|s,c| s & c.kind_of?(Text)}
- string = ""
- old_level = @level
- @level = 0
- node.children.each { |child| write( child, string ) }
- @level = old_level
- if string.length < @width
- output << string
- skip = true
- end
- end
- end
- unless skip
- output << "\n"
- @level += @indentation
- node.children.each { |child|
- next if child.kind_of?(Text) and child.to_s.strip.length == 0
- write( child, output )
- output << "\n"
- }
- @level -= @indentation
- output << ' '*@level
- end
- output << "</#{node.expanded_name}"
- end
- output << ">"
- end
-
- def write_text( node, output )
- s = node.to_s()
- s.gsub!(/\s/,' ')
- s.squeeze!(" ")
- s = wrap(s, 80-@level)
- s = indent_text(s, @level, " ", true)
- output << (' '*@level + s)
- end
-
- def write_comment( node, output)
- output << ' ' * @level
- super
- end
-
- def write_cdata( node, output)
- output << ' ' * @level
- super
- end
-
- def write_document( node, output )
- # Ok, this is a bit odd. All XML documents have an XML declaration,
- # but it may not write itself if the user didn't specifically add it,
- # either through the API or in the input document. If it doesn't write
- # itself, then we don't need a carriage return... which makes this
- # logic more complex.
- node.children.each { |child|
- next if child == node.children[-1] and child.instance_of?(Text)
- unless child == node.children[0] or child.instance_of?(Text) or
- (child == node.children[1] and !node.children[0].writethis)
- output << "\n"
- end
- write( child, output )
- }
- end
-
- private
- def indent_text(string, level=1, style="\t", indentfirstline=true)
- return string if level < 0
- string.gsub(/\n/, "\n#{style*level}")
- end
-
- def wrap(string, width)
- # Recursively wrap string at width.
- return string if string.length <= width
- place = string.rindex(' ', width) # Position in string with last ' ' before cutoff
- return string if place.nil?
- return string[0,place] + "\n" + wrap(string[place+1..-1], width)
- end
-
- end
- end
-end
-
diff --git a/lib/rexml/formatters/transitive.rb b/lib/rexml/formatters/transitive.rb
deleted file mode 100644
index 6083f0390b..0000000000
--- a/lib/rexml/formatters/transitive.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-require 'rexml/formatters/pretty'
-
-module REXML
- module Formatters
- # The Transitive formatter writes an XML document that parses to an
- # identical document as the source document. This means that no extra
- # whitespace nodes are inserted, and whitespace within text nodes is
- # preserved. Within these constraints, the document is pretty-printed,
- # with whitespace inserted into the metadata to introduce formatting.
- #
- # Note that this is only useful if the original XML is not already
- # formatted. Since this formatter does not alter whitespace nodes, the
- # results of formatting already formatted XML will be odd.
- class Transitive < Default
- def initialize( indentation=2, ie_hack=false )
- @indentation = indentation
- @level = 0
- @ie_hack = ie_hack
- end
-
- protected
- def write_element( node, output )
- output << "<#{node.expanded_name}"
-
- node.attributes.each_attribute do |attr|
- output << " "
- attr.write( output )
- end unless node.attributes.empty?
-
- output << "\n"
- output << ' '*@level
- if node.children.empty?
- output << " " if @ie_hack
- output << "/"
- else
- output << ">"
- # If compact and all children are text, and if the formatted output
- # is less than the specified width, then try to print everything on
- # one line
- skip = false
- @level += @indentation
- node.children.each { |child|
- write( child, output )
- }
- @level -= @indentation
- output << "</#{node.expanded_name}"
- output << "\n"
- output << ' '*@level
- end
- output << ">"
- end
-
- def write_text( node, output )
- output << node.to_s()
- end
- end
- end
-end
diff --git a/lib/rexml/functions.rb b/lib/rexml/functions.rb
deleted file mode 100644
index fc9c4701c4..0000000000
--- a/lib/rexml/functions.rb
+++ /dev/null
@@ -1,388 +0,0 @@
-module REXML
- # If you add a method, keep in mind two things:
- # (1) the first argument will always be a list of nodes from which to
- # filter. In the case of context methods (such as position), the function
- # should return an array with a value for each child in the array.
- # (2) all method calls from XML will have "-" replaced with "_".
- # Therefore, in XML, "local-name()" is identical (and actually becomes)
- # "local_name()"
- module Functions
- @@context = nil
- @@namespace_context = {}
- @@variables = {}
-
- def Functions::namespace_context=(x) ; @@namespace_context=x ; end
- def Functions::variables=(x) ; @@variables=x ; end
- def Functions::namespace_context ; @@namespace_context ; end
- def Functions::variables ; @@variables ; end
-
- def Functions::context=(value); @@context = value; end
-
- def Functions::text( )
- if @@context[:node].node_type == :element
- return @@context[:node].find_all{|n| n.node_type == :text}.collect{|n| n.value}
- elsif @@context[:node].node_type == :text
- return @@context[:node].value
- else
- return false
- end
- end
-
- def Functions::last( )
- @@context[:size]
- end
-
- def Functions::position( )
- @@context[:index]
- end
-
- def Functions::count( node_set )
- node_set.size
- end
-
- # Since REXML is non-validating, this method is not implemented as it
- # requires a DTD
- def Functions::id( object )
- end
-
- # UNTESTED
- def Functions::local_name( node_set=nil )
- get_namespace( node_set ) do |node|
- return node.local_name
- end
- end
-
- def Functions::namespace_uri( node_set=nil )
- get_namespace( node_set ) {|node| node.namespace}
- end
-
- def Functions::name( node_set=nil )
- get_namespace( node_set ) do |node|
- node.expanded_name
- end
- end
-
- # Helper method.
- def Functions::get_namespace( node_set = nil )
- if node_set == nil
- yield @@context[:node] if defined? @@context[:node].namespace
- else
- if node_set.respond_to? :each
- node_set.each { |node| yield node if defined? node.namespace }
- elsif node_set.respond_to? :namespace
- yield node_set
- end
- end
- end
-
- # A node-set is converted to a string by returning the string-value of the
- # node in the node-set that is first in document order. If the node-set is
- # empty, an empty string is returned.
- #
- # A number is converted to a string as follows
- #
- # NaN is converted to the string NaN
- #
- # positive zero is converted to the string 0
- #
- # negative zero is converted to the string 0
- #
- # positive infinity is converted to the string Infinity
- #
- # negative infinity is converted to the string -Infinity
- #
- # if the number is an integer, the number is represented in decimal form
- # as a Number with no decimal point and no leading zeros, preceded by a
- # minus sign (-) if the number is negative
- #
- # otherwise, the number is represented in decimal form as a Number
- # including a decimal point with at least one digit before the decimal
- # point and at least one digit after the decimal point, preceded by a
- # minus sign (-) if the number is negative; there must be no leading zeros
- # before the decimal point apart possibly from the one required digit
- # immediately before the decimal point; beyond the one required digit
- # after the decimal point there must be as many, but only as many, more
- # digits as are needed to uniquely distinguish the number from all other
- # IEEE 754 numeric values.
- #
- # The boolean false value is converted to the string false. The boolean
- # true value is converted to the string true.
- #
- # An object of a type other than the four basic types is converted to a
- # string in a way that is dependent on that type.
- def Functions::string( object=nil )
- #object = @context unless object
- if object.instance_of? Array
- string( object[0] )
- elsif defined? object.node_type
- if object.node_type == :attribute
- object.value
- elsif object.node_type == :element || object.node_type == :document
- string_value(object)
- else
- object.to_s
- end
- elsif object.nil?
- return ""
- else
- object.to_s
- end
- end
-
- def Functions::string_value( o )
- rv = ""
- o.children.each { |e|
- if e.node_type == :text
- rv << e.to_s
- elsif e.node_type == :element
- rv << string_value( e )
- end
- }
- rv
- end
-
- # UNTESTED
- def Functions::concat( *objects )
- objects.join
- end
-
- # Fixed by Mike Stok
- def Functions::starts_with( string, test )
- string(string).index(string(test)) == 0
- end
-
- # Fixed by Mike Stok
- def Functions::contains( string, test )
- string(string).include?(string(test))
- end
-
- # Kouhei fixed this
- def Functions::substring_before( string, test )
- ruby_string = string(string)
- ruby_index = ruby_string.index(string(test))
- if ruby_index.nil?
- ""
- else
- ruby_string[ 0...ruby_index ]
- end
- end
-
- # Kouhei fixed this too
- def Functions::substring_after( string, test )
- ruby_string = string(string)
- test_string = string(test)
- return $1 if ruby_string =~ /#{test}(.*)/
- ""
- end
-
- # Take equal portions of Mike Stok and Sean Russell; mix
- # vigorously, and pour into a tall, chilled glass. Serves 10,000.
- def Functions::substring( string, start, length=nil )
- ruby_string = string(string)
- ruby_length = if length.nil?
- ruby_string.length.to_f
- else
- number(length)
- end
- ruby_start = number(start)
-
- # Handle the special cases
- return '' if (
- ruby_length.nan? or
- ruby_start.nan? or
- ruby_start.infinite?
- )
-
- infinite_length = ruby_length.infinite? == 1
- ruby_length = ruby_string.length if infinite_length
-
- # Now, get the bounds. The XPath bounds are 1..length; the ruby bounds
- # are 0..length. Therefore, we have to offset the bounds by one.
- ruby_start = ruby_start.round - 1
- ruby_length = ruby_length.round
-
- if ruby_start < 0
- ruby_length += ruby_start unless infinite_length
- ruby_start = 0
- end
- return '' if ruby_length <= 0
- ruby_string[ruby_start,ruby_length]
- end
-
- # UNTESTED
- def Functions::string_length( string )
- string(string).length
- end
-
- # UNTESTED
- def Functions::normalize_space( string=nil )
- string = string(@@context[:node]) if string.nil?
- if string.kind_of? Array
- string.collect{|x| string.to_s.strip.gsub(/\s+/um, ' ') if string}
- else
- string.to_s.strip.gsub(/\s+/um, ' ')
- end
- end
-
- # This is entirely Mike Stok's beast
- def Functions::translate( string, tr1, tr2 )
- from = string(tr1)
- to = string(tr2)
-
- # the map is our translation table.
- #
- # if a character occurs more than once in the
- # from string then we ignore the second &
- # subsequent mappings
- #
- # if a character maps to nil then we delete it
- # in the output. This happens if the from
- # string is longer than the to string
- #
- # there's nothing about - or ^ being special in
- # http://www.w3.org/TR/xpath#function-translate
- # so we don't build ranges or negated classes
-
- map = Hash.new
- 0.upto(from.length - 1) { |pos|
- from_char = from[pos]
- unless map.has_key? from_char
- map[from_char] =
- if pos < to.length
- to[pos]
- else
- nil
- end
- end
- }
-
- if ''.respond_to? :chars
- string(string).chars.collect { |c|
- if map.has_key? c then map[c] else c end
- }.compact.join
- else
- string(string).unpack('U*').collect { |c|
- if map.has_key? c then map[c] else c end
- }.compact.pack('U*')
- end
- end
-
- # UNTESTED
- def Functions::boolean( object=nil )
- if object.kind_of? String
- if object =~ /\d+/u
- return object.to_f != 0
- else
- return object.size > 0
- end
- elsif object.kind_of? Array
- object = object.find{|x| x and true}
- end
- return object ? true : false
- end
-
- # UNTESTED
- def Functions::not( object )
- not boolean( object )
- end
-
- # UNTESTED
- def Functions::true( )
- true
- end
-
- # UNTESTED
- def Functions::false( )
- false
- end
-
- # UNTESTED
- def Functions::lang( language )
- lang = false
- node = @@context[:node]
- attr = nil
- until node.nil?
- if node.node_type == :element
- attr = node.attributes["xml:lang"]
- unless attr.nil?
- lang = compare_language(string(language), attr)
- break
- else
- end
- end
- node = node.parent
- end
- lang
- end
-
- def Functions::compare_language lang1, lang2
- lang2.downcase.index(lang1.downcase) == 0
- end
-
- # a string that consists of optional whitespace followed by an optional
- # minus sign followed by a Number followed by whitespace is converted to
- # the IEEE 754 number that is nearest (according to the IEEE 754
- # round-to-nearest rule) to the mathematical value represented by the
- # string; any other string is converted to NaN
- #
- # boolean true is converted to 1; boolean false is converted to 0
- #
- # a node-set is first converted to a string as if by a call to the string
- # function and then converted in the same way as a string argument
- #
- # an object of a type other than the four basic types is converted to a
- # number in a way that is dependent on that type
- def Functions::number( object=nil )
- object = @@context[:node] unless object
- case object
- when true
- Float(1)
- when false
- Float(0)
- when Array
- number(string( object ))
- when Numeric
- object.to_f
- else
- str = string( object )
- # If XPath ever gets scientific notation...
- #if str =~ /^\s*-?(\d*\.?\d+|\d+\.)([Ee]\d*)?\s*$/
- if str =~ /^\s*-?(\d*\.?\d+|\d+\.)\s*$/
- str.to_f
- else
- (0.0 / 0.0)
- end
- end
- end
-
- def Functions::sum( nodes )
- nodes = [nodes] unless nodes.kind_of? Array
- nodes.inject(0) { |r,n| r += number(string(n)) }
- end
-
- def Functions::floor( number )
- number(number).floor
- end
-
- def Functions::ceiling( number )
- number(number).ceil
- end
-
- def Functions::round( number )
- begin
- number(number).round
- rescue FloatDomainError
- number(number)
- end
- end
-
- def Functions::processing_instruction( node )
- node.node_type == :processing_instruction
- end
-
- def Functions::method_missing( id )
- puts "METHOD MISSING #{id.id2name}"
- XPath.match( @@context[:node], id.id2name )
- end
- end
-end
diff --git a/lib/rexml/instruction.rb b/lib/rexml/instruction.rb
deleted file mode 100644
index 50bf95d17a..0000000000
--- a/lib/rexml/instruction.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-require "rexml/child"
-require "rexml/source"
-
-module REXML
- # Represents an XML Instruction; IE, <? ... ?>
- # TODO: Add parent arg (3rd arg) to constructor
- class Instruction < Child
- START = '<\?'
- STOP = '\?>'
-
- # target is the "name" of the Instruction; IE, the "tag" in <?tag ...?>
- # content is everything else.
- attr_accessor :target, :content
-
- # Constructs a new Instruction
- # @param target can be one of a number of things. If String, then
- # the target of this instruction is set to this. If an Instruction,
- # then the Instruction is shallowly cloned (target and content are
- # copied). If a Source, then the source is scanned and parsed for
- # an Instruction declaration.
- # @param content Must be either a String, or a Parent. Can only
- # be a Parent if the target argument is a Source. Otherwise, this
- # String is set as the content of this instruction.
- def initialize(target, content=nil)
- if target.kind_of? String
- super()
- @target = target
- @content = content
- elsif target.kind_of? Instruction
- super(content)
- @target = target.target
- @content = target.content
- end
- @content.strip! if @content
- end
-
- def clone
- Instruction.new self
- end
-
- # == DEPRECATED
- # See the rexml/formatters package
- #
- def write writer, indent=-1, transitive=false, ie_hack=false
- Kernel.warn( "#{self.class.name}.write is deprecated" )
- indent(writer, indent)
- writer << START.sub(/\\/u, '')
- writer << @target
- writer << ' '
- writer << @content
- writer << STOP.sub(/\\/u, '')
- end
-
- # @return true if other is an Instruction, and the content and target
- # of the other matches the target and content of this object.
- def ==( other )
- other.kind_of? Instruction and
- other.target == @target and
- other.content == @content
- end
-
- def node_type
- :processing_instruction
- end
-
- def inspect
- "<?p-i #{target} ...?>"
- end
- end
-end
diff --git a/lib/rexml/light/node.rb b/lib/rexml/light/node.rb
deleted file mode 100644
index 9c90148c05..0000000000
--- a/lib/rexml/light/node.rb
+++ /dev/null
@@ -1,196 +0,0 @@
-require 'rexml/xmltokens'
-require 'rexml/light/node'
-
-# [ :element, parent, name, attributes, children* ]
- # a = Node.new
- # a << "B" # => <a>B</a>
- # a.b # => <a>B<b/></a>
- # a.b[1] # => <a>B<b/><b/><a>
- # a.b[1]["x"] = "y" # => <a>B<b/><b x="y"/></a>
- # a.b[0].c # => <a>B<b><c/></b><b x="y"/></a>
- # a.b.c << "D" # => <a>B<b><c>D</c></b><b x="y"/></a>
-module REXML
- module Light
- # Represents a tagged XML element. Elements are characterized by
- # having children, attributes, and names, and can themselves be
- # children.
- class Node
- NAMESPLIT = /^(?:(#{XMLTokens::NCNAME_STR}):)?(#{XMLTokens::NCNAME_STR})/u
- PARENTS = [ :element, :document, :doctype ]
- # Create a new element.
- def initialize node=nil
- @node = node
- if node.kind_of? String
- node = [ :text, node ]
- elsif node.nil?
- node = [ :document, nil, nil ]
- elsif node[0] == :start_element
- node[0] = :element
- elsif node[0] == :start_doctype
- node[0] = :doctype
- elsif node[0] == :start_document
- node[0] = :document
- end
- end
-
- def size
- if PARENTS.include? @node[0]
- @node[-1].size
- else
- 0
- end
- end
-
- def each( &block )
- size.times { |x| yield( at(x+4) ) }
- end
-
- def name
- at(2)
- end
-
- def name=( name_str, ns=nil )
- pfx = ''
- pfx = "#{prefix(ns)}:" if ns
- _old_put(2, "#{pfx}#{name_str}")
- end
-
- def parent=( node )
- _old_put(1,node)
- end
-
- def local_name
- namesplit
- @name
- end
-
- def local_name=( name_str )
- _old_put( 1, "#@prefix:#{name_str}" )
- end
-
- def prefix( namespace=nil )
- prefix_of( self, namespace )
- end
-
- def namespace( prefix=prefix() )
- namespace_of( self, prefix )
- end
-
- def namespace=( namespace )
- @prefix = prefix( namespace )
- pfx = ''
- pfx = "#@prefix:" if @prefix.size > 0
- _old_put(1, "#{pfx}#@name")
- end
-
- def []( reference, ns=nil )
- if reference.kind_of? String
- pfx = ''
- pfx = "#{prefix(ns)}:" if ns
- at(3)["#{pfx}#{reference}"]
- elsif reference.kind_of? Range
- _old_get( Range.new(4+reference.begin, reference.end, reference.exclude_end?) )
- else
- _old_get( 4+reference )
- end
- end
-
- def =~( path )
- XPath.match( self, path )
- end
-
- # Doesn't handle namespaces yet
- def []=( reference, ns, value=nil )
- if reference.kind_of? String
- value = ns unless value
- at( 3 )[reference] = value
- elsif reference.kind_of? Range
- _old_put( Range.new(3+reference.begin, reference.end, reference.exclude_end?), ns )
- else
- if value
- _old_put( 4+reference, ns, value )
- else
- _old_put( 4+reference, ns )
- end
- end
- end
-
- # Append a child to this element, optionally under a provided namespace.
- # The namespace argument is ignored if the element argument is an Element
- # object. Otherwise, the element argument is a string, the namespace (if
- # provided) is the namespace the element is created in.
- def << element
- if node_type() == :text
- at(-1) << element
- else
- newnode = Node.new( element )
- newnode.parent = self
- self.push( newnode )
- end
- at(-1)
- end
-
- def node_type
- _old_get(0)
- end
-
- def text=( foo )
- replace = at(4).kind_of?(String)? 1 : 0
- self._old_put(4,replace, normalizefoo)
- end
-
- def root
- context = self
- context = context.at(1) while context.at(1)
- end
-
- def has_name?( name, namespace = '' )
- at(3) == name and namespace() == namespace
- end
-
- def children
- self
- end
-
- def parent
- at(1)
- end
-
- def to_s
-
- end
-
- private
-
- def namesplit
- return if @name.defined?
- at(2) =~ NAMESPLIT
- @prefix = '' || $1
- @name = $2
- end
-
- def namespace_of( node, prefix=nil )
- if not prefix
- name = at(2)
- name =~ NAMESPLIT
- prefix = $1
- end
- to_find = 'xmlns'
- to_find = "xmlns:#{prefix}" if not prefix.nil?
- ns = at(3)[ to_find ]
- ns ? ns : namespace_of( @node[0], prefix )
- end
-
- def prefix_of( node, namespace=nil )
- if not namespace
- name = node.name
- name =~ NAMESPLIT
- $1
- else
- ns = at(3).find { |k,v| v == namespace }
- ns ? ns : prefix_of( node.parent, namespace )
- end
- end
- end
- end
-end
diff --git a/lib/rexml/namespace.rb b/lib/rexml/namespace.rb
deleted file mode 100644
index 8d43fc85ad..0000000000
--- a/lib/rexml/namespace.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require 'rexml/xmltokens'
-
-module REXML
- # Adds named attributes to an object.
- module Namespace
- # The name of the object, valid if set
- attr_reader :name, :expanded_name
- # The expanded name of the object, valid if name is set
- attr_accessor :prefix
- include XMLTokens
- NAMESPLIT = /^(?:(#{NCNAME_STR}):)?(#{NCNAME_STR})/u
-
- # Sets the name and the expanded name
- def name=( name )
- @expanded_name = name
- name =~ NAMESPLIT
- if $1
- @prefix = $1
- else
- @prefix = ""
- @namespace = ""
- end
- @name = $2
- end
-
- # Compares names optionally WITH namespaces
- def has_name?( other, ns=nil )
- if ns
- return (namespace() == ns and name() == other)
- elsif other.include? ":"
- return fully_expanded_name == other
- else
- return name == other
- end
- end
-
- alias :local_name :name
-
- # Fully expand the name, even if the prefix wasn't specified in the
- # source file.
- def fully_expanded_name
- ns = prefix
- return "#{ns}:#@name" if ns.size > 0
- return @name
- end
- end
-end
diff --git a/lib/rexml/node.rb b/lib/rexml/node.rb
deleted file mode 100644
index eb39141944..0000000000
--- a/lib/rexml/node.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-require "rexml/parseexception"
-require "rexml/formatters/pretty"
-require "rexml/formatters/default"
-
-module REXML
- # Represents a node in the tree. Nodes are never encountered except as
- # superclasses of other objects. Nodes have siblings.
- module Node
- # @return the next sibling (nil if unset)
- def next_sibling_node
- return nil if @parent.nil?
- @parent[ @parent.index(self) + 1 ]
- end
-
- # @return the previous sibling (nil if unset)
- def previous_sibling_node
- return nil if @parent.nil?
- ind = @parent.index(self)
- return nil if ind == 0
- @parent[ ind - 1 ]
- end
-
- # indent::
- # *DEPRECATED* This parameter is now ignored. See the formatters in the
- # REXML::Formatters package for changing the output style.
- def to_s indent=nil
- unless indent.nil?
- Kernel.warn( "#{self.class.name}.to_s(indent) parameter is deprecated" )
- f = REXML::Formatters::Pretty.new( indent )
- f.write( self, rv = "" )
- else
- f = REXML::Formatters::Default.new
- f.write( self, rv = "" )
- end
- return rv
- end
-
- def indent to, ind
- if @parent and @parent.context and not @parent.context[:indentstyle].nil? then
- indentstyle = @parent.context[:indentstyle]
- else
- indentstyle = ' '
- end
- to << indentstyle*ind unless ind<1
- end
-
- def parent?
- false;
- end
-
-
- # Visit all subnodes of +self+ recursively
- def each_recursive(&block) # :yields: node
- self.elements.each {|node|
- block.call(node)
- node.each_recursive(&block)
- }
- end
-
- # Find (and return) first subnode (recursively) for which the block
- # evaluates to true. Returns +nil+ if none was found.
- def find_first_recursive(&block) # :yields: node
- each_recursive {|node|
- return node if block.call(node)
- }
- return nil
- end
-
- # Returns the position that +self+ holds in its parent's array, indexed
- # from 1.
- def index_in_parent
- parent.index(self)+1
- end
- end
-end
diff --git a/lib/rexml/output.rb b/lib/rexml/output.rb
deleted file mode 100644
index 997f2b117d..0000000000
--- a/lib/rexml/output.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require 'rexml/encoding'
-
-module REXML
- class Output
- include Encoding
-
- attr_reader :encoding
-
- def initialize real_IO, encd="iso-8859-1"
- @output = real_IO
- self.encoding = encd
-
- @to_utf = encd == UTF_8 ? false : true
- end
-
- def <<( content )
- @output << (@to_utf ? self.encode(content) : content)
- end
-
- def to_s
- "Output[#{encoding}]"
- end
- end
-end
diff --git a/lib/rexml/parent.rb b/lib/rexml/parent.rb
deleted file mode 100644
index a20aaaef6b..0000000000
--- a/lib/rexml/parent.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-require "rexml/child"
-
-module REXML
- # A parent has children, and has methods for accessing them. The Parent
- # class is never encountered except as the superclass for some other
- # object.
- class Parent < Child
- include Enumerable
-
- # Constructor
- # @param parent if supplied, will be set as the parent of this object
- def initialize parent=nil
- super(parent)
- @children = []
- end
-
- def add( object )
- #puts "PARENT GOTS #{size} CHILDREN"
- object.parent = self
- @children << object
- #puts "PARENT NOW GOTS #{size} CHILDREN"
- object
- end
-
- alias :push :add
- alias :<< :push
-
- def unshift( object )
- object.parent = self
- @children.unshift object
- end
-
- def delete( object )
- found = false
- @children.delete_if {|c| c.equal?(object) and found = true }
- object.parent = nil if found
- end
-
- def each(&block)
- @children.each(&block)
- end
-
- def delete_if( &block )
- @children.delete_if(&block)
- end
-
- def delete_at( index )
- @children.delete_at index
- end
-
- def each_index( &block )
- @children.each_index(&block)
- end
-
- # Fetches a child at a given index
- # @param index the Integer index of the child to fetch
- def []( index )
- @children[index]
- end
-
- alias :each_child :each
-
-
-
- # Set an index entry. See Array.[]=
- # @param index the index of the element to set
- # @param opt either the object to set, or an Integer length
- # @param child if opt is an Integer, this is the child to set
- # @return the parent (self)
- def []=( *args )
- args[-1].parent = self
- @children[*args[0..-2]] = args[-1]
- end
-
- # Inserts an child before another child
- # @param child1 this is either an xpath or an Element. If an Element,
- # child2 will be inserted before child1 in the child list of the parent.
- # If an xpath, child2 will be inserted before the first child to match
- # the xpath.
- # @param child2 the child to insert
- # @return the parent (self)
- def insert_before( child1, child2 )
- if child1.kind_of? String
- child1 = XPath.first( self, child1 )
- child1.parent.insert_before child1, child2
- else
- ind = index(child1)
- child2.parent.delete(child2) if child2.parent
- @children[ind,0] = child2
- child2.parent = self
- end
- self
- end
-
- # Inserts an child after another child
- # @param child1 this is either an xpath or an Element. If an Element,
- # child2 will be inserted after child1 in the child list of the parent.
- # If an xpath, child2 will be inserted after the first child to match
- # the xpath.
- # @param child2 the child to insert
- # @return the parent (self)
- def insert_after( child1, child2 )
- if child1.kind_of? String
- child1 = XPath.first( self, child1 )
- child1.parent.insert_after child1, child2
- else
- ind = index(child1)+1
- child2.parent.delete(child2) if child2.parent
- @children[ind,0] = child2
- child2.parent = self
- end
- self
- end
-
- def to_a
- @children.dup
- end
-
- # Fetches the index of a given child
- # @param child the child to get the index of
- # @return the index of the child, or nil if the object is not a child
- # of this parent.
- def index( child )
- count = -1
- @children.find { |i| count += 1 ; i.hash == child.hash }
- count
- end
-
- # @return the number of children of this parent
- def size
- @children.size
- end
-
- alias :length :size
-
- # Replaces one child with another, making sure the nodelist is correct
- # @param to_replace the child to replace (must be a Child)
- # @param replacement the child to insert into the nodelist (must be a
- # Child)
- def replace_child( to_replace, replacement )
- @children.map! {|c| c.equal?( to_replace ) ? replacement : c }
- to_replace.parent = nil
- replacement.parent = self
- end
-
- # Deeply clones this object. This creates a complete duplicate of this
- # Parent, including all descendants.
- def deep_clone
- cl = clone()
- each do |child|
- if child.kind_of? Parent
- cl << child.deep_clone
- else
- cl << child.clone
- end
- end
- cl
- end
-
- alias :children :to_a
-
- def parent?
- true
- end
- end
-end
diff --git a/lib/rexml/parseexception.rb b/lib/rexml/parseexception.rb
deleted file mode 100644
index feb7a7e638..0000000000
--- a/lib/rexml/parseexception.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-module REXML
- class ParseException < RuntimeError
- attr_accessor :source, :parser, :continued_exception
-
- def initialize( message, source=nil, parser=nil, exception=nil )
- super(message)
- @source = source
- @parser = parser
- @continued_exception = exception
- end
-
- def to_s
- # Quote the original exception, if there was one
- if @continued_exception
- err = @continued_exception.inspect
- err << "\n"
- err << @continued_exception.backtrace.join("\n")
- err << "\n...\n"
- else
- err = ""
- end
-
- # Get the stack trace and error message
- err << super
-
- # Add contextual information
- if @source
- err << "\nLine: #{line}\n"
- err << "Position: #{position}\n"
- err << "Last 80 unconsumed characters:\n"
- err << @source.buffer[0..80].gsub(/\n/, ' ')
- end
-
- err
- end
-
- def position
- @source.current_line[0] if @source and defined? @source.current_line and
- @source.current_line
- end
-
- def line
- @source.current_line[2] if @source and defined? @source.current_line and
- @source.current_line
- end
-
- def context
- @source.current_line
- end
- end
-end
diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb
deleted file mode 100644
index 162d029a62..0000000000
--- a/lib/rexml/parsers/baseparser.rb
+++ /dev/null
@@ -1,530 +0,0 @@
-require 'rexml/parseexception'
-require 'rexml/undefinednamespaceexception'
-require 'rexml/source'
-require 'set'
-
-module REXML
- module Parsers
- # = Using the Pull Parser
- # <em>This API is experimental, and subject to change.</em>
- # parser = PullParser.new( "<a>text<b att='val'/>txet</a>" )
- # while parser.has_next?
- # res = parser.next
- # puts res[1]['att'] if res.start_tag? and res[0] == 'b'
- # end
- # See the PullEvent class for information on the content of the results.
- # The data is identical to the arguments passed for the various events to
- # the StreamListener API.
- #
- # Notice that:
- # parser = PullParser.new( "<a>BAD DOCUMENT" )
- # while parser.has_next?
- # res = parser.next
- # raise res[1] if res.error?
- # end
- #
- # Nat Price gave me some good ideas for the API.
- class BaseParser
- if String.method_defined? :encode
- # Oniguruma / POSIX [understands unicode]
- LETTER = '[[:alpha:]]'
- DIGIT = '[[:digit:]]'
- else
- # Ruby < 1.9 [doesn't understand unicode]
- LETTER = 'a-zA-Z'
- DIGIT = '\d'
- end
-
- COMBININGCHAR = '' # TODO
- EXTENDER = '' # TODO
-
- NCNAME_STR= "[#{LETTER}_:][-#{LETTER}#{DIGIT}._:#{COMBININGCHAR}#{EXTENDER}]*"
- NAME_STR= "(?:(#{NCNAME_STR}):)?(#{NCNAME_STR})"
- UNAME_STR= "(?:#{NCNAME_STR}:)?#{NCNAME_STR}"
-
- NAMECHAR = '[\-\w\d\.:]'
- NAME = "([\\w:]#{NAMECHAR}*)"
- NMTOKEN = "(?:#{NAMECHAR})+"
- NMTOKENS = "#{NMTOKEN}(\\s+#{NMTOKEN})*"
- REFERENCE = "&(?:#{NAME};|#\\d+;|#x[0-9a-fA-F]+;)"
- REFERENCE_RE = /#{REFERENCE}/
-
- DOCTYPE_START = /\A\s*<!DOCTYPE\s/um
- DOCTYPE_PATTERN = /\s*<!DOCTYPE\s+(.*?)(\[|>)/um
- ATTRIBUTE_PATTERN = /\s*(#{NAME_STR})\s*=\s*(["'])(.*?)\4/um
- COMMENT_START = /\A<!--/u
- COMMENT_PATTERN = /<!--(.*?)-->/um
- CDATA_START = /\A<!\[CDATA\[/u
- CDATA_END = /^\s*\]\s*>/um
- CDATA_PATTERN = /<!\[CDATA\[(.*?)\]\]>/um
- XMLDECL_START = /\A<\?xml\s/u;
- XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>/um
- INSTRUCTION_START = /\A<\?/u
- INSTRUCTION_PATTERN = /<\?(.*?)(\s+.*?)?\?>/um
- TAG_MATCH = /^<((?>#{NAME_STR}))\s*((?>\s+#{UNAME_STR}\s*=\s*(["']).*?\5)*)\s*(\/)?>/um
- CLOSE_MATCH = /^\s*<\/(#{NAME_STR})\s*>/um
-
- VERSION = /\bversion\s*=\s*["'](.*?)['"]/um
- ENCODING = /\bencoding\s*=\s*["'](.*?)['"]/um
- STANDALONE = /\bstandalone\s*=\s["'](.*?)['"]/um
-
- ENTITY_START = /^\s*<!ENTITY/
- IDENTITY = /^([!\*\w\-]+)(\s+#{NCNAME_STR})?(\s+["'](.*?)['"])?(\s+['"](.*?)["'])?/u
- ELEMENTDECL_START = /^\s*<!ELEMENT/um
- ELEMENTDECL_PATTERN = /^\s*(<!ELEMENT.*?)>/um
- SYSTEMENTITY = /^\s*(%.*?;)\s*$/um
- ENUMERATION = "\\(\\s*#{NMTOKEN}(?:\\s*\\|\\s*#{NMTOKEN})*\\s*\\)"
- NOTATIONTYPE = "NOTATION\\s+\\(\\s*#{NAME}(?:\\s*\\|\\s*#{NAME})*\\s*\\)"
- ENUMERATEDTYPE = "(?:(?:#{NOTATIONTYPE})|(?:#{ENUMERATION}))"
- ATTTYPE = "(CDATA|ID|IDREF|IDREFS|ENTITY|ENTITIES|NMTOKEN|NMTOKENS|#{ENUMERATEDTYPE})"
- ATTVALUE = "(?:\"((?:[^<&\"]|#{REFERENCE})*)\")|(?:'((?:[^<&']|#{REFERENCE})*)')"
- DEFAULTDECL = "(#REQUIRED|#IMPLIED|(?:(#FIXED\\s+)?#{ATTVALUE}))"
- ATTDEF = "\\s+#{NAME}\\s+#{ATTTYPE}\\s+#{DEFAULTDECL}"
- ATTDEF_RE = /#{ATTDEF}/
- ATTLISTDECL_START = /^\s*<!ATTLIST/um
- ATTLISTDECL_PATTERN = /^\s*<!ATTLIST\s+#{NAME}(?:#{ATTDEF})*\s*>/um
- NOTATIONDECL_START = /^\s*<!NOTATION/um
- PUBLIC = /^\s*<!NOTATION\s+(\w[\-\w]*)\s+(PUBLIC)\s+(["'])(.*?)\3(?:\s+(["'])(.*?)\5)?\s*>/um
- SYSTEM = /^\s*<!NOTATION\s+(\w[\-\w]*)\s+(SYSTEM)\s+(["'])(.*?)\3\s*>/um
-
- TEXT_PATTERN = /\A([^<]*)/um
-
- # Entity constants
- PUBIDCHAR = "\x20\x0D\x0Aa-zA-Z0-9\\-()+,./:=?;!*@$_%#"
- SYSTEMLITERAL = %Q{((?:"[^"]*")|(?:'[^']*'))}
- PUBIDLITERAL = %Q{("[#{PUBIDCHAR}']*"|'[#{PUBIDCHAR}]*')}
- EXTERNALID = "(?:(?:(SYSTEM)\\s+#{SYSTEMLITERAL})|(?:(PUBLIC)\\s+#{PUBIDLITERAL}\\s+#{SYSTEMLITERAL}))"
- NDATADECL = "\\s+NDATA\\s+#{NAME}"
- PEREFERENCE = "%#{NAME};"
- ENTITYVALUE = %Q{((?:"(?:[^%&"]|#{PEREFERENCE}|#{REFERENCE})*")|(?:'([^%&']|#{PEREFERENCE}|#{REFERENCE})*'))}
- PEDEF = "(?:#{ENTITYVALUE}|#{EXTERNALID})"
- ENTITYDEF = "(?:#{ENTITYVALUE}|(?:#{EXTERNALID}(#{NDATADECL})?))"
- PEDECL = "<!ENTITY\\s+(%)\\s+#{NAME}\\s+#{PEDEF}\\s*>"
- GEDECL = "<!ENTITY\\s+#{NAME}\\s+#{ENTITYDEF}\\s*>"
- ENTITYDECL = /\s*(?:#{GEDECL})|(?:#{PEDECL})/um
-
- EREFERENCE = /&(?!#{NAME};)/
-
- DEFAULT_ENTITIES = {
- 'gt' => [/&gt;/, '&gt;', '>', />/],
- 'lt' => [/&lt;/, '&lt;', '<', /</],
- 'quot' => [/&quot;/, '&quot;', '"', /"/],
- "apos" => [/&apos;/, "&apos;", "'", /'/]
- }
-
-
- ######################################################################
- # These are patterns to identify common markup errors, to make the
- # error messages more informative.
- ######################################################################
- MISSING_ATTRIBUTE_QUOTES = /^<#{NAME_STR}\s+#{NAME_STR}\s*=\s*[^"']/um
-
- def initialize( source )
- self.stream = source
- end
-
- def add_listener( listener )
- if !defined?(@listeners) or !@listeners
- @listeners = []
- instance_eval <<-EOL
- alias :_old_pull :pull
- def pull
- event = _old_pull
- @listeners.each do |listener|
- listener.receive event
- end
- event
- end
- EOL
- end
- @listeners << listener
- end
-
- attr_reader :source
-
- def stream=( source )
- @source = SourceFactory.create_from( source )
- @closed = nil
- @document_status = nil
- @tags = []
- @stack = []
- @entities = []
- @nsstack = []
- end
-
- def position
- if @source.respond_to? :position
- @source.position
- else
- # FIXME
- 0
- end
- end
-
- # Returns true if there are no more events
- def empty?
- return (@source.empty? and @stack.empty?)
- end
-
- # Returns true if there are more events. Synonymous with !empty?
- def has_next?
- return !(@source.empty? and @stack.empty?)
- end
-
- # Push an event back on the head of the stream. This method
- # has (theoretically) infinite depth.
- def unshift token
- @stack.unshift(token)
- end
-
- # Peek at the +depth+ event in the stack. The first element on the stack
- # is at depth 0. If +depth+ is -1, will parse to the end of the input
- # stream and return the last event, which is always :end_document.
- # Be aware that this causes the stream to be parsed up to the +depth+
- # event, so you can effectively pre-parse the entire document (pull the
- # entire thing into memory) using this method.
- def peek depth=0
- raise %Q[Illegal argument "#{depth}"] if depth < -1
- temp = []
- if depth == -1
- temp.push(pull()) until empty?
- else
- while @stack.size+temp.size < depth+1
- temp.push(pull())
- end
- end
- @stack += temp if temp.size > 0
- @stack[depth]
- end
-
- # Returns the next event. This is a +PullEvent+ object.
- def pull
- if @closed
- x, @closed = @closed, nil
- return [ :end_element, x ]
- end
- return [ :end_document ] if empty?
- return @stack.shift if @stack.size > 0
- #STDERR.puts @source.encoding
- @source.read if @source.buffer.size<2
- #STDERR.puts "BUFFER = #{@source.buffer.inspect}"
- if @document_status == nil
- #@source.consume( /^\s*/um )
- word = @source.match( /^((?:\s+)|(?:<[^>]*>))/um )
- word = word[1] unless word.nil?
- #STDERR.puts "WORD = #{word.inspect}"
- case word
- when COMMENT_START
- return [ :comment, @source.match( COMMENT_PATTERN, true )[1] ]
- when XMLDECL_START
- #STDERR.puts "XMLDECL"
- results = @source.match( XMLDECL_PATTERN, true )[1]
- version = VERSION.match( results )
- version = version[1] unless version.nil?
- encoding = ENCODING.match(results)
- encoding = encoding[1] unless encoding.nil?
- @source.encoding = encoding
- standalone = STANDALONE.match(results)
- standalone = standalone[1] unless standalone.nil?
- return [ :xmldecl, version, encoding, standalone ]
- when INSTRUCTION_START
- return [ :processing_instruction, *@source.match(INSTRUCTION_PATTERN, true)[1,2] ]
- when DOCTYPE_START
- md = @source.match( DOCTYPE_PATTERN, true )
- @nsstack.unshift(curr_ns=Set.new)
- identity = md[1]
- close = md[2]
- identity =~ IDENTITY
- name = $1
- raise REXML::ParseException.new("DOCTYPE is missing a name") if name.nil?
- pub_sys = $2.nil? ? nil : $2.strip
- long_name = $4.nil? ? nil : $4.strip
- uri = $6.nil? ? nil : $6.strip
- args = [ :start_doctype, name, pub_sys, long_name, uri ]
- if close == ">"
- @document_status = :after_doctype
- @source.read if @source.buffer.size<2
- md = @source.match(/^\s*/um, true)
- @stack << [ :end_doctype ]
- else
- @document_status = :in_doctype
- end
- return args
- when /^\s+/
- else
- @document_status = :after_doctype
- @source.read if @source.buffer.size<2
- md = @source.match(/\s*/um, true)
- if @source.encoding == "UTF-8"
- if @source.buffer.respond_to? :force_encoding
- @source.buffer.force_encoding(Encoding::UTF_8)
- end
- end
- end
- end
- if @document_status == :in_doctype
- md = @source.match(/\s*(.*?>)/um)
- case md[1]
- when SYSTEMENTITY
- match = @source.match( SYSTEMENTITY, true )[1]
- return [ :externalentity, match ]
-
- when ELEMENTDECL_START
- return [ :elementdecl, @source.match( ELEMENTDECL_PATTERN, true )[1] ]
-
- when ENTITY_START
- match = @source.match( ENTITYDECL, true ).to_a.compact
- match[0] = :entitydecl
- ref = false
- if match[1] == '%'
- ref = true
- match.delete_at 1
- end
- # Now we have to sort out what kind of entity reference this is
- if match[2] == 'SYSTEM'
- # External reference
- match[3] = match[3][1..-2] # PUBID
- match.delete_at(4) if match.size > 4 # Chop out NDATA decl
- # match is [ :entity, name, SYSTEM, pubid(, ndata)? ]
- elsif match[2] == 'PUBLIC'
- # External reference
- match[3] = match[3][1..-2] # PUBID
- match[4] = match[4][1..-2] # HREF
- # match is [ :entity, name, PUBLIC, pubid, href ]
- else
- match[2] = match[2][1..-2]
- match.pop if match.size == 4
- # match is [ :entity, name, value ]
- end
- match << '%' if ref
- return match
- when ATTLISTDECL_START
- md = @source.match( ATTLISTDECL_PATTERN, true )
- raise REXML::ParseException.new( "Bad ATTLIST declaration!", @source ) if md.nil?
- element = md[1]
- contents = md[0]
-
- pairs = {}
- values = md[0].scan( ATTDEF_RE )
- values.each do |attdef|
- unless attdef[3] == "#IMPLIED"
- attdef.compact!
- val = attdef[3]
- val = attdef[4] if val == "#FIXED "
- pairs[attdef[0]] = val
- if attdef[0] =~ /^xmlns:(.*)/
- @nsstack[0] << $1
- end
- end
- end
- return [ :attlistdecl, element, pairs, contents ]
- when NOTATIONDECL_START
- md = nil
- if @source.match( PUBLIC )
- md = @source.match( PUBLIC, true )
- vals = [md[1],md[2],md[4],md[6]]
- elsif @source.match( SYSTEM )
- md = @source.match( SYSTEM, true )
- vals = [md[1],md[2],nil,md[4]]
- else
- raise REXML::ParseException.new( "error parsing notation: no matching pattern", @source )
- end
- return [ :notationdecl, *vals ]
- when CDATA_END
- @document_status = :after_doctype
- @source.match( CDATA_END, true )
- return [ :end_doctype ]
- end
- end
- begin
- if @source.buffer[0] == ?<
- if @source.buffer[1] == ?/
- @nsstack.shift
- last_tag = @tags.pop
- #md = @source.match_to_consume( '>', CLOSE_MATCH)
- md = @source.match( CLOSE_MATCH, true )
- raise REXML::ParseException.new( "Missing end tag for "+
- "'#{last_tag}' (got \"#{md[1]}\")",
- @source) unless last_tag == md[1]
- return [ :end_element, last_tag ]
- elsif @source.buffer[1] == ?!
- md = @source.match(/\A(\s*[^>]*>)/um)
- #STDERR.puts "SOURCE BUFFER = #{source.buffer}, #{source.buffer.size}"
- raise REXML::ParseException.new("Malformed node", @source) unless md
- if md[0][2] == ?-
- md = @source.match( COMMENT_PATTERN, true )
-
- case md[1]
- when /--/, /-$/
- raise REXML::ParseException.new("Malformed comment", @source)
- end
-
- return [ :comment, md[1] ] if md
- else
- md = @source.match( CDATA_PATTERN, true )
- return [ :cdata, md[1] ] if md
- end
- raise REXML::ParseException.new( "Declarations can only occur "+
- "in the doctype declaration.", @source)
- elsif @source.buffer[1] == ??
- md = @source.match( INSTRUCTION_PATTERN, true )
- return [ :processing_instruction, md[1], md[2] ] if md
- raise REXML::ParseException.new( "Bad instruction declaration",
- @source)
- else
- # Get the next tag
- md = @source.match(TAG_MATCH, true)
- unless md
- # Check for missing attribute quotes
- raise REXML::ParseException.new("missing attribute quote", @source) if @source.match(MISSING_ATTRIBUTE_QUOTES )
- raise REXML::ParseException.new("malformed XML: missing tag start", @source)
- end
- attributes = {}
- prefixes = Set.new
- prefixes << md[2] if md[2]
- @nsstack.unshift(curr_ns=Set.new)
- if md[4].size > 0
- attrs = md[4].scan( ATTRIBUTE_PATTERN )
- raise REXML::ParseException.new( "error parsing attributes: [#{attrs.join ', '}], excess = \"#$'\"", @source) if $' and $'.strip.size > 0
- attrs.each { |a,b,c,d,e|
- if b == "xmlns"
- if c == "xml"
- if d != "http://www.w3.org/XML/1998/namespace"
- msg = "The 'xml' prefix must not be bound to any other namespace "+
- "(http://www.w3.org/TR/REC-xml-names/#ns-decl)"
- raise REXML::ParseException.new( msg, @source, self )
- end
- elsif c == "xmlns"
- msg = "The 'xmlns' prefix must not be declared "+
- "(http://www.w3.org/TR/REC-xml-names/#ns-decl)"
- raise REXML::ParseException.new( msg, @source, self)
- end
- curr_ns << c
- elsif b
- prefixes << b unless b == "xml"
- end
-
- if attributes.has_key? a
- msg = "Duplicate attribute #{a.inspect}"
- raise REXML::ParseException.new( msg, @source, self)
- end
-
- attributes[a] = e
- }
- end
-
- # Verify that all of the prefixes have been defined
- for prefix in prefixes
- unless @nsstack.find{|k| k.member?(prefix)}
- raise UndefinedNamespaceException.new(prefix,@source,self)
- end
- end
-
- if md[6]
- @closed = md[1]
- @nsstack.shift
- else
- @tags.push( md[1] )
- end
- return [ :start_element, md[1], attributes ]
- end
- else
- md = @source.match( TEXT_PATTERN, true )
- if md[0].length == 0
- @source.match( /(\s+)/, true )
- end
- #STDERR.puts "GOT #{md[1].inspect}" unless md[0].length == 0
- #return [ :text, "" ] if md[0].length == 0
- # unnormalized = Text::unnormalize( md[1], self )
- # return PullEvent.new( :text, md[1], unnormalized )
- return [ :text, md[1] ]
- end
- rescue REXML::UndefinedNamespaceException
- raise
- rescue REXML::ParseException
- raise
- rescue Exception, NameError => error
- raise REXML::ParseException.new( "Exception parsing",
- @source, self, (error ? error : $!) )
- end
- return [ :dummy ]
- end
-
- def entity( reference, entities )
- value = nil
- value = entities[ reference ] if entities
- if not value
- value = DEFAULT_ENTITIES[ reference ]
- value = value[2] if value
- end
- unnormalize( value, entities ) if value
- end
-
- # Escapes all possible entities
- def normalize( input, entities=nil, entity_filter=nil )
- copy = input.clone
- # Doing it like this rather than in a loop improves the speed
- copy.gsub!( EREFERENCE, '&amp;' )
- entities.each do |key, value|
- copy.gsub!( value, "&#{key};" ) unless entity_filter and
- entity_filter.include?(entity)
- end if entities
- copy.gsub!( EREFERENCE, '&amp;' )
- DEFAULT_ENTITIES.each do |key, value|
- copy.gsub!( value[3], value[1] )
- end
- copy
- end
-
- # Unescapes all possible entities
- def unnormalize( string, entities=nil, filter=nil )
- rv = string.clone
- rv.gsub!( /\r\n?/, "\n" )
- matches = rv.scan( REFERENCE_RE )
- return rv if matches.size == 0
- rv.gsub!( /&#0*((?:\d+)|(?:x[a-fA-F0-9]+));/ ) {
- m=$1
- m = "0#{m}" if m[0] == ?x
- [Integer(m)].pack('U*')
- }
- matches.collect!{|x|x[0]}.compact!
- if matches.size > 0
- matches.each do |entity_reference|
- unless filter and filter.include?(entity_reference)
- entity_value = entity( entity_reference, entities )
- if entity_value
- re = /&#{entity_reference};/
- rv.gsub!( re, entity_value )
- else
- er = DEFAULT_ENTITIES[entity_reference]
- rv.gsub!( er[0], er[2] ) if er
- end
- end
- end
- rv.gsub!( /&amp;/, '&' )
- end
- rv
- end
- end
- end
-end
-
-=begin
- case event[0]
- when :start_element
- when :text
- when :end_element
- when :processing_instruction
- when :cdata
- when :comment
- when :xmldecl
- when :start_doctype
- when :end_doctype
- when :externalentity
- when :elementdecl
- when :entity
- when :attlistdecl
- when :notationdecl
- when :end_doctype
- end
-=end
diff --git a/lib/rexml/parsers/lightparser.rb b/lib/rexml/parsers/lightparser.rb
deleted file mode 100644
index ca9692c449..0000000000
--- a/lib/rexml/parsers/lightparser.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-require 'rexml/parsers/streamparser'
-require 'rexml/parsers/baseparser'
-require 'rexml/light/node'
-
-module REXML
- module Parsers
- class LightParser
- def initialize stream
- @stream = stream
- @parser = REXML::Parsers::BaseParser.new( stream )
- end
-
- def add_listener( listener )
- @parser.add_listener( listener )
- end
-
- def rewind
- @stream.rewind
- @parser.stream = @stream
- end
-
- def parse
- root = context = [ :document ]
- while true
- event = @parser.pull
- case event[0]
- when :end_document
- break
- when :start_element, :start_doctype
- new_node = event
- context << new_node
- new_node[1,0] = [context]
- context = new_node
- when :end_element, :end_doctype
- context = context[1]
- else
- new_node = event
- context << new_node
- new_node[1,0] = [context]
- end
- end
- root
- end
- end
-
- # An element is an array. The array contains:
- # 0 The parent element
- # 1 The tag name
- # 2 A hash of attributes
- # 3..-1 The child elements
- # An element is an array of size > 3
- # Text is a String
- # PIs are [ :processing_instruction, target, data ]
- # Comments are [ :comment, data ]
- # DocTypes are DocType structs
- # The root is an array with XMLDecls, Text, DocType, Array, Text
- end
-end
diff --git a/lib/rexml/parsers/pullparser.rb b/lib/rexml/parsers/pullparser.rb
deleted file mode 100644
index 36dc7160c3..0000000000
--- a/lib/rexml/parsers/pullparser.rb
+++ /dev/null
@@ -1,196 +0,0 @@
-require 'forwardable'
-
-require 'rexml/parseexception'
-require 'rexml/parsers/baseparser'
-require 'rexml/xmltokens'
-
-module REXML
- module Parsers
- # = Using the Pull Parser
- # <em>This API is experimental, and subject to change.</em>
- # parser = PullParser.new( "<a>text<b att='val'/>txet</a>" )
- # while parser.has_next?
- # res = parser.next
- # puts res[1]['att'] if res.start_tag? and res[0] == 'b'
- # end
- # See the PullEvent class for information on the content of the results.
- # The data is identical to the arguments passed for the various events to
- # the StreamListener API.
- #
- # Notice that:
- # parser = PullParser.new( "<a>BAD DOCUMENT" )
- # while parser.has_next?
- # res = parser.next
- # raise res[1] if res.error?
- # end
- #
- # Nat Price gave me some good ideas for the API.
- class PullParser
- include XMLTokens
- extend Forwardable
-
- def_delegators( :@parser, :has_next? )
- def_delegators( :@parser, :entity )
- def_delegators( :@parser, :empty? )
- def_delegators( :@parser, :source )
-
- def initialize stream
- @entities = {}
- @listeners = nil
- @parser = BaseParser.new( stream )
- @my_stack = []
- end
-
- def add_listener( listener )
- @listeners = [] unless @listeners
- @listeners << listener
- end
-
- def each
- while has_next?
- yield self.pull
- end
- end
-
- def peek depth=0
- if @my_stack.length <= depth
- (depth - @my_stack.length + 1).times {
- e = PullEvent.new(@parser.pull)
- @my_stack.push(e)
- }
- end
- @my_stack[depth]
- end
-
- def pull
- return @my_stack.shift if @my_stack.length > 0
-
- event = @parser.pull
- case event[0]
- when :entitydecl
- @entities[ event[1] ] =
- event[2] unless event[2] =~ /PUBLIC|SYSTEM/
- when :text
- unnormalized = @parser.unnormalize( event[1], @entities )
- event << unnormalized
- end
- PullEvent.new( event )
- end
-
- def unshift token
- @my_stack.unshift token
- end
- end
-
- # A parsing event. The contents of the event are accessed as an +Array?,
- # and the type is given either by the ...? methods, or by accessing the
- # +type+ accessor. The contents of this object vary from event to event,
- # but are identical to the arguments passed to +StreamListener+s for each
- # event.
- class PullEvent
- # The type of this event. Will be one of :tag_start, :tag_end, :text,
- # :processing_instruction, :comment, :doctype, :attlistdecl, :entitydecl,
- # :notationdecl, :entity, :cdata, :xmldecl, or :error.
- def initialize(arg)
- @contents = arg
- end
-
- def []( start, endd=nil)
- if start.kind_of? Range
- @contents.slice( start.begin+1 .. start.end )
- elsif start.kind_of? Numeric
- if endd.nil?
- @contents.slice( start+1 )
- else
- @contents.slice( start+1, endd )
- end
- else
- raise "Illegal argument #{start.inspect} (#{start.class})"
- end
- end
-
- def event_type
- @contents[0]
- end
-
- # Content: [ String tag_name, Hash attributes ]
- def start_element?
- @contents[0] == :start_element
- end
-
- # Content: [ String tag_name ]
- def end_element?
- @contents[0] == :end_element
- end
-
- # Content: [ String raw_text, String unnormalized_text ]
- def text?
- @contents[0] == :text
- end
-
- # Content: [ String text ]
- def instruction?
- @contents[0] == :processing_instruction
- end
-
- # Content: [ String text ]
- def comment?
- @contents[0] == :comment
- end
-
- # Content: [ String name, String pub_sys, String long_name, String uri ]
- def doctype?
- @contents[0] == :start_doctype
- end
-
- # Content: [ String text ]
- def attlistdecl?
- @contents[0] == :attlistdecl
- end
-
- # Content: [ String text ]
- def elementdecl?
- @contents[0] == :elementdecl
- end
-
- # Due to the wonders of DTDs, an entity declaration can be just about
- # anything. There's no way to normalize it; you'll have to interpret the
- # content yourself. However, the following is true:
- #
- # * If the entity declaration is an internal entity:
- # [ String name, String value ]
- # Content: [ String text ]
- def entitydecl?
- @contents[0] == :entitydecl
- end
-
- # Content: [ String text ]
- def notationdecl?
- @contents[0] == :notationdecl
- end
-
- # Content: [ String text ]
- def entity?
- @contents[0] == :entity
- end
-
- # Content: [ String text ]
- def cdata?
- @contents[0] == :cdata
- end
-
- # Content: [ String version, String encoding, String standalone ]
- def xmldecl?
- @contents[0] == :xmldecl
- end
-
- def error?
- @contents[0] == :error
- end
-
- def inspect
- @contents[0].to_s + ": " + @contents[1..-1].inspect
- end
- end
- end
-end
diff --git a/lib/rexml/parsers/sax2parser.rb b/lib/rexml/parsers/sax2parser.rb
deleted file mode 100644
index 72131401c3..0000000000
--- a/lib/rexml/parsers/sax2parser.rb
+++ /dev/null
@@ -1,247 +0,0 @@
-require 'rexml/parsers/baseparser'
-require 'rexml/parseexception'
-require 'rexml/namespace'
-require 'rexml/text'
-
-module REXML
- module Parsers
- # SAX2Parser
- class SAX2Parser
- def initialize source
- @parser = BaseParser.new(source)
- @listeners = []
- @procs = []
- @namespace_stack = []
- @has_listeners = false
- @tag_stack = []
- @entities = {}
- end
-
- def source
- @parser.source
- end
-
- def add_listener( listener )
- @parser.add_listener( listener )
- end
-
- # Listen arguments:
- #
- # Symbol, Array, Block
- # Listen to Symbol events on Array elements
- # Symbol, Block
- # Listen to Symbol events
- # Array, Listener
- # Listen to all events on Array elements
- # Array, Block
- # Listen to :start_element events on Array elements
- # Listener
- # Listen to All events
- #
- # Symbol can be one of: :start_element, :end_element,
- # :start_prefix_mapping, :end_prefix_mapping, :characters,
- # :processing_instruction, :doctype, :attlistdecl, :elementdecl,
- # :entitydecl, :notationdecl, :cdata, :xmldecl, :comment
- #
- # There is an additional symbol that can be listened for: :progress.
- # This will be called for every event generated, passing in the current
- # stream position.
- #
- # Array contains regular expressions or strings which will be matched
- # against fully qualified element names.
- #
- # Listener must implement the methods in SAX2Listener
- #
- # Block will be passed the same arguments as a SAX2Listener method would
- # be, where the method name is the same as the matched Symbol.
- # See the SAX2Listener for more information.
- def listen( *args, &blok )
- if args[0].kind_of? Symbol
- if args.size == 2
- args[1].each { |match| @procs << [args[0], match, blok] }
- else
- add( [args[0], nil, blok] )
- end
- elsif args[0].kind_of? Array
- if args.size == 2
- args[0].each { |match| add( [nil, match, args[1]] ) }
- else
- args[0].each { |match| add( [ :start_element, match, blok ] ) }
- end
- else
- add([nil, nil, args[0]])
- end
- end
-
- def deafen( listener=nil, &blok )
- if listener
- @listeners.delete_if {|item| item[-1] == listener }
- @has_listeners = false if @listeners.size == 0
- else
- @procs.delete_if {|item| item[-1] == blok }
- end
- end
-
- def parse
- @procs.each { |sym,match,block| block.call if sym == :start_document }
- @listeners.each { |sym,match,block|
- block.start_document if sym == :start_document or sym.nil?
- }
- root = context = []
- while true
- event = @parser.pull
- case event[0]
- when :end_document
- handle( :end_document )
- break
- when :start_doctype
- handle( :doctype, *event[1..-1])
- when :end_doctype
- context = context[1]
- when :start_element
- @tag_stack.push(event[1])
- # find the observers for namespaces
- procs = get_procs( :start_prefix_mapping, event[1] )
- listeners = get_listeners( :start_prefix_mapping, event[1] )
- if procs or listeners
- # break out the namespace declarations
- # The attributes live in event[2]
- event[2].each {|n, v| event[2][n] = @parser.normalize(v)}
- nsdecl = event[2].find_all { |n, value| n =~ /^xmlns(:|$)/ }
- nsdecl.collect! { |n, value| [ n[6..-1], value ] }
- @namespace_stack.push({})
- nsdecl.each do |n,v|
- @namespace_stack[-1][n] = v
- # notify observers of namespaces
- procs.each { |ob| ob.call( n, v ) } if procs
- listeners.each { |ob| ob.start_prefix_mapping(n, v) } if listeners
- end
- end
- event[1] =~ Namespace::NAMESPLIT
- prefix = $1
- local = $2
- uri = get_namespace(prefix)
- # find the observers for start_element
- procs = get_procs( :start_element, event[1] )
- listeners = get_listeners( :start_element, event[1] )
- # notify observers
- procs.each { |ob| ob.call( uri, local, event[1], event[2] ) } if procs
- listeners.each { |ob|
- ob.start_element( uri, local, event[1], event[2] )
- } if listeners
- when :end_element
- @tag_stack.pop
- event[1] =~ Namespace::NAMESPLIT
- prefix = $1
- local = $2
- uri = get_namespace(prefix)
- # find the observers for start_element
- procs = get_procs( :end_element, event[1] )
- listeners = get_listeners( :end_element, event[1] )
- # notify observers
- procs.each { |ob| ob.call( uri, local, event[1] ) } if procs
- listeners.each { |ob|
- ob.end_element( uri, local, event[1] )
- } if listeners
-
- namespace_mapping = @namespace_stack.pop
- # find the observers for namespaces
- procs = get_procs( :end_prefix_mapping, event[1] )
- listeners = get_listeners( :end_prefix_mapping, event[1] )
- if procs or listeners
- namespace_mapping.each do |ns_prefix, ns_uri|
- # notify observers of namespaces
- procs.each { |ob| ob.call( ns_prefix ) } if procs
- listeners.each { |ob| ob.end_prefix_mapping(ns_prefix) } if listeners
- end
- end
- when :text
- #normalized = @parser.normalize( event[1] )
- #handle( :characters, normalized )
- copy = event[1].clone
-
- esub = proc { |match|
- if @entities.has_key?($1)
- @entities[$1].gsub(Text::REFERENCE, &esub)
- else
- match
- end
- }
-
- copy.gsub!( Text::REFERENCE, &esub )
- copy.gsub!( Text::NUMERICENTITY ) {|m|
- m=$1
- m = "0#{m}" if m[0] == ?x
- [Integer(m)].pack('U*')
- }
- handle( :characters, copy )
- when :entitydecl
- @entities[ event[1] ] = event[2] if event.size == 3
- handle( *event )
- when :processing_instruction, :comment, :attlistdecl,
- :elementdecl, :cdata, :notationdecl, :xmldecl
- handle( *event )
- end
- handle( :progress, @parser.position )
- end
- end
-
- private
- def handle( symbol, *arguments )
- tag = @tag_stack[-1]
- procs = get_procs( symbol, tag )
- listeners = get_listeners( symbol, tag )
- # notify observers
- procs.each { |ob| ob.call( *arguments ) } if procs
- listeners.each { |l|
- l.send( symbol.to_s, *arguments )
- } if listeners
- end
-
- # The following methods are duplicates, but it is faster than using
- # a helper
- def get_procs( symbol, name )
- return nil if @procs.size == 0
- @procs.find_all do |sym, match, block|
- #puts sym.inspect+"=="+symbol.inspect+ "\t"+match.inspect+"=="+name.inspect+ "\t"+( (sym.nil? or symbol == sym) and ((name.nil? and match.nil?) or match.nil? or ( (name == match) or (match.kind_of? Regexp and name =~ match)))).to_s
- (
- (sym.nil? or symbol == sym) and
- ((name.nil? and match.nil?) or match.nil? or (
- (name == match) or
- (match.kind_of? Regexp and name =~ match)
- )
- )
- )
- end.collect{|x| x[-1]}
- end
- def get_listeners( symbol, name )
- return nil if @listeners.size == 0
- @listeners.find_all do |sym, match, block|
- (
- (sym.nil? or symbol == sym) and
- ((name.nil? and match.nil?) or match.nil? or (
- (name == match) or
- (match.kind_of? Regexp and name =~ match)
- )
- )
- )
- end.collect{|x| x[-1]}
- end
-
- def add( pair )
- if pair[-1].respond_to? :call
- @procs << pair unless @procs.include? pair
- else
- @listeners << pair unless @listeners.include? pair
- @has_listeners = true
- end
- end
-
- def get_namespace( prefix )
- uris = (@namespace_stack.find_all { |ns| not ns[prefix].nil? }) ||
- (@namespace_stack.find { |ns| not ns[nil].nil? })
- uris[-1][prefix] unless uris.nil? or 0 == uris.size
- end
- end
- end
-end
diff --git a/lib/rexml/parsers/streamparser.rb b/lib/rexml/parsers/streamparser.rb
deleted file mode 100644
index 256d0f611c..0000000000
--- a/lib/rexml/parsers/streamparser.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-module REXML
- module Parsers
- class StreamParser
- def initialize source, listener
- @listener = listener
- @parser = BaseParser.new( source )
- end
-
- def add_listener( listener )
- @parser.add_listener( listener )
- end
-
- def parse
- # entity string
- while true
- event = @parser.pull
- case event[0]
- when :end_document
- return
- when :start_element
- attrs = event[2].each do |n, v|
- event[2][n] = @parser.unnormalize( v )
- end
- @listener.tag_start( event[1], attrs )
- when :end_element
- @listener.tag_end( event[1] )
- when :text
- normalized = @parser.unnormalize( event[1] )
- @listener.text( normalized )
- when :processing_instruction
- @listener.instruction( *event[1,2] )
- when :start_doctype
- @listener.doctype( *event[1..-1] )
- when :end_doctype
- # FIXME: remove this condition for milestone:3.2
- @listener.doctype_end if @listener.respond_to? :doctype_end
- when :comment, :attlistdecl, :cdata, :xmldecl, :elementdecl
- @listener.send( event[0].to_s, *event[1..-1] )
- when :entitydecl, :notationdecl
- @listener.send( event[0].to_s, event[1..-1] )
- end
- end
- end
- end
- end
-end
diff --git a/lib/rexml/parsers/treeparser.rb b/lib/rexml/parsers/treeparser.rb
deleted file mode 100644
index 30327d0dfd..0000000000
--- a/lib/rexml/parsers/treeparser.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-require 'rexml/validation/validationexception'
-require 'rexml/undefinednamespaceexception'
-
-module REXML
- module Parsers
- class TreeParser
- def initialize( source, build_context = Document.new )
- @build_context = build_context
- @parser = Parsers::BaseParser.new( source )
- end
-
- def add_listener( listener )
- @parser.add_listener( listener )
- end
-
- def parse
- tag_stack = []
- in_doctype = false
- entities = nil
- begin
- while true
- event = @parser.pull
- #STDERR.puts "TREEPARSER GOT #{event.inspect}"
- case event[0]
- when :end_document
- unless tag_stack.empty?
- #raise ParseException.new("No close tag for #{tag_stack.inspect}")
- raise ParseException.new("No close tag for #{@build_context.xpath}")
- end
- return
- when :start_element
- tag_stack.push(event[1])
- el = @build_context = @build_context.add_element( event[1] )
- event[2].each do |key, value|
- el.attributes[key]=Attribute.new(key,value,self)
- end
- when :end_element
- tag_stack.pop
- @build_context = @build_context.parent
- when :text
- if not in_doctype
- if @build_context[-1].instance_of? Text
- @build_context[-1] << event[1]
- else
- @build_context.add(
- Text.new(event[1], @build_context.whitespace, nil, true)
- ) unless (
- @build_context.ignore_whitespace_nodes and
- event[1].strip.size==0
- )
- end
- end
- when :comment
- c = Comment.new( event[1] )
- @build_context.add( c )
- when :cdata
- c = CData.new( event[1] )
- @build_context.add( c )
- when :processing_instruction
- @build_context.add( Instruction.new( event[1], event[2] ) )
- when :end_doctype
- in_doctype = false
- entities.each { |k,v| entities[k] = @build_context.entities[k].value }
- @build_context = @build_context.parent
- when :start_doctype
- doctype = DocType.new( event[1..-1], @build_context )
- @build_context = doctype
- entities = {}
- in_doctype = true
- when :attlistdecl
- n = AttlistDecl.new( event[1..-1] )
- @build_context.add( n )
- when :externalentity
- n = ExternalEntity.new( event[1] )
- @build_context.add( n )
- when :elementdecl
- n = ElementDecl.new( event[1] )
- @build_context.add(n)
- when :entitydecl
- entities[ event[1] ] = event[2] unless event[2] =~ /PUBLIC|SYSTEM/
- @build_context.add(Entity.new(event))
- when :notationdecl
- n = NotationDecl.new( *event[1..-1] )
- @build_context.add( n )
- when :xmldecl
- x = XMLDecl.new( event[1], event[2], event[3] )
- @build_context.add( x )
- end
- end
- rescue REXML::Validation::ValidationException
- raise
- rescue REXML::UndefinedNamespaceException
- raise
- rescue
- raise ParseException.new( $!.message, @parser.source, @parser, $! )
- end
- end
- end
- end
-end
diff --git a/lib/rexml/parsers/ultralightparser.rb b/lib/rexml/parsers/ultralightparser.rb
deleted file mode 100644
index 96c55d837e..0000000000
--- a/lib/rexml/parsers/ultralightparser.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-require 'rexml/parsers/streamparser'
-require 'rexml/parsers/baseparser'
-
-module REXML
- module Parsers
- class UltraLightParser
- def initialize stream
- @stream = stream
- @parser = REXML::Parsers::BaseParser.new( stream )
- end
-
- def add_listener( listener )
- @parser.add_listener( listener )
- end
-
- def rewind
- @stream.rewind
- @parser.stream = @stream
- end
-
- def parse
- root = context = []
- while true
- event = @parser.pull
- case event[0]
- when :end_document
- break
- when :end_doctype
- context = context[1]
- when :start_element, :doctype
- context << event
- event[1,0] = [context]
- context = event
- when :end_element
- context = context[1]
- else
- context << event
- end
- end
- root
- end
- end
-
- # An element is an array. The array contains:
- # 0 The parent element
- # 1 The tag name
- # 2 A hash of attributes
- # 3..-1 The child elements
- # An element is an array of size > 3
- # Text is a String
- # PIs are [ :processing_instruction, target, data ]
- # Comments are [ :comment, data ]
- # DocTypes are DocType structs
- # The root is an array with XMLDecls, Text, DocType, Array, Text
- end
-end
diff --git a/lib/rexml/parsers/xpathparser.rb b/lib/rexml/parsers/xpathparser.rb
deleted file mode 100644
index 49450b4aef..0000000000
--- a/lib/rexml/parsers/xpathparser.rb
+++ /dev/null
@@ -1,698 +0,0 @@
-require 'rexml/namespace'
-require 'rexml/xmltokens'
-
-module REXML
- module Parsers
- # You don't want to use this class. Really. Use XPath, which is a wrapper
- # for this class. Believe me. You don't want to poke around in here.
- # There is strange, dark magic at work in this code. Beware. Go back! Go
- # back while you still can!
- class XPathParser
- include XMLTokens
- LITERAL = /^'([^']*)'|^"([^"]*)"/u
-
- def namespaces=( namespaces )
- Functions::namespace_context = namespaces
- @namespaces = namespaces
- end
-
- def parse path
- path.gsub!(/([\(\[])\s+/, '\1') # Strip ignorable spaces
- path.gsub!( /\s+([\]\)])/, '\1' )
- parsed = []
- path = OrExpr(path, parsed)
- parsed
- end
-
- def predicate path
- parsed = []
- Predicate( "[#{path}]", parsed )
- parsed
- end
-
- def abbreviate( path )
- path = path.kind_of?(String) ? parse( path ) : path
- string = ""
- document = false
- while path.size > 0
- op = path.shift
- case op
- when :node
- when :attribute
- string << "/" if string.size > 0
- string << "@"
- when :child
- string << "/" if string.size > 0
- when :descendant_or_self
- string << "/"
- when :self
- string << "."
- when :parent
- string << ".."
- when :any
- string << "*"
- when :text
- string << "text()"
- when :following, :following_sibling,
- :ancestor, :ancestor_or_self, :descendant,
- :namespace, :preceding, :preceding_sibling
- string << "/" unless string.size == 0
- string << op.to_s.tr("_", "-")
- string << "::"
- when :qname
- prefix = path.shift
- name = path.shift
- string << prefix+":" if prefix.size > 0
- string << name
- when :predicate
- string << '['
- string << predicate_to_string( path.shift ) {|x| abbreviate( x ) }
- string << ']'
- when :document
- document = true
- when :function
- string << path.shift
- string << "( "
- string << predicate_to_string( path.shift[0] ) {|x| abbreviate( x )}
- string << " )"
- when :literal
- string << %Q{ "#{path.shift}" }
- else
- string << "/" unless string.size == 0
- string << "UNKNOWN("
- string << op.inspect
- string << ")"
- end
- end
- string = "/"+string if document
- return string
- end
-
- def expand( path )
- path = path.kind_of?(String) ? parse( path ) : path
- string = ""
- document = false
- while path.size > 0
- op = path.shift
- case op
- when :node
- string << "node()"
- when :attribute, :child, :following, :following_sibling,
- :ancestor, :ancestor_or_self, :descendant, :descendant_or_self,
- :namespace, :preceding, :preceding_sibling, :self, :parent
- string << "/" unless string.size == 0
- string << op.to_s.tr("_", "-")
- string << "::"
- when :any
- string << "*"
- when :qname
- prefix = path.shift
- name = path.shift
- string << prefix+":" if prefix.size > 0
- string << name
- when :predicate
- string << '['
- string << predicate_to_string( path.shift ) { |x| expand(x) }
- string << ']'
- when :document
- document = true
- else
- string << "/" unless string.size == 0
- string << "UNKNOWN("
- string << op.inspect
- string << ")"
- end
- end
- string = "/"+string if document
- return string
- end
-
- def predicate_to_string( path, &block )
- string = ""
- case path[0]
- when :and, :or, :mult, :plus, :minus, :neq, :eq, :lt, :gt, :lteq, :gteq, :div, :mod, :union
- op = path.shift
- case op
- when :eq
- op = "="
- when :lt
- op = "<"
- when :gt
- op = ">"
- when :lteq
- op = "<="
- when :gteq
- op = ">="
- when :neq
- op = "!="
- when :union
- op = "|"
- end
- left = predicate_to_string( path.shift, &block )
- right = predicate_to_string( path.shift, &block )
- string << " "
- string << left
- string << " "
- string << op.to_s
- string << " "
- string << right
- string << " "
- when :function
- path.shift
- name = path.shift
- string << name
- string << "( "
- string << predicate_to_string( path.shift, &block )
- string << " )"
- when :literal
- path.shift
- string << " "
- string << path.shift.inspect
- string << " "
- else
- string << " "
- string << yield( path )
- string << " "
- end
- return string.squeeze(" ")
- end
-
- private
- #LocationPath
- # | RelativeLocationPath
- # | '/' RelativeLocationPath?
- # | '//' RelativeLocationPath
- def LocationPath path, parsed
- #puts "LocationPath '#{path}'"
- path = path.strip
- if path[0] == ?/
- parsed << :document
- if path[1] == ?/
- parsed << :descendant_or_self
- parsed << :node
- path = path[2..-1]
- else
- path = path[1..-1]
- end
- end
- #puts parsed.inspect
- return RelativeLocationPath( path, parsed ) if path.size > 0
- end
-
- #RelativeLocationPath
- # | Step
- # | (AXIS_NAME '::' | '@' | '') AxisSpecifier
- # NodeTest
- # Predicate
- # | '.' | '..' AbbreviatedStep
- # | RelativeLocationPath '/' Step
- # | RelativeLocationPath '//' Step
- AXIS = /^(ancestor|ancestor-or-self|attribute|child|descendant|descendant-or-self|following|following-sibling|namespace|parent|preceding|preceding-sibling|self)::/
- def RelativeLocationPath path, parsed
- #puts "RelativeLocationPath #{path}"
- while path.size > 0
- # (axis or @ or <child::>) nodetest predicate >
- # OR > / Step
- # (. or ..) >
- if path[0] == ?.
- if path[1] == ?.
- parsed << :parent
- parsed << :node
- path = path[2..-1]
- else
- parsed << :self
- parsed << :node
- path = path[1..-1]
- end
- else
- if path[0] == ?@
- #puts "ATTRIBUTE"
- parsed << :attribute
- path = path[1..-1]
- # Goto Nodetest
- elsif path =~ AXIS
- parsed << $1.tr('-','_').intern
- path = $'
- # Goto Nodetest
- else
- parsed << :child
- end
-
- #puts "NODETESTING '#{path}'"
- n = []
- path = NodeTest( path, n)
- #puts "NODETEST RETURNED '#{path}'"
-
- if path[0] == ?[
- path = Predicate( path, n )
- end
-
- parsed.concat(n)
- end
-
- if path.size > 0
- if path[0] == ?/
- if path[1] == ?/
- parsed << :descendant_or_self
- parsed << :node
- path = path[2..-1]
- else
- path = path[1..-1]
- end
- else
- return path
- end
- end
- end
- return path
- end
-
- # Returns a 1-1 map of the nodeset
- # The contents of the resulting array are either:
- # true/false, if a positive match
- # String, if a name match
- #NodeTest
- # | ('*' | NCNAME ':' '*' | QNAME) NameTest
- # | NODE_TYPE '(' ')' NodeType
- # | PI '(' LITERAL ')' PI
- # | '[' expr ']' Predicate
- NCNAMETEST= /^(#{NCNAME_STR}):\*/u
- QNAME = Namespace::NAMESPLIT
- NODE_TYPE = /^(comment|text|node)\(\s*\)/m
- PI = /^processing-instruction\(/
- def NodeTest path, parsed
- #puts "NodeTest with #{path}"
- res = nil
- case path
- when /^\*/
- path = $'
- parsed << :any
- when NODE_TYPE
- type = $1
- path = $'
- parsed << type.tr('-', '_').intern
- when PI
- path = $'
- literal = nil
- if path !~ /^\s*\)/
- path =~ LITERAL
- literal = $1
- path = $'
- raise ParseException.new("Missing ')' after processing instruction") if path[0] != ?)
- path = path[1..-1]
- end
- parsed << :processing_instruction
- parsed << (literal || '')
- when NCNAMETEST
- #puts "NCNAMETEST"
- prefix = $1
- path = $'
- parsed << :namespace
- parsed << prefix
- when QNAME
- #puts "QNAME"
- prefix = $1
- name = $2
- path = $'
- prefix = "" unless prefix
- parsed << :qname
- parsed << prefix
- parsed << name
- end
- return path
- end
-
- # Filters the supplied nodeset on the predicate(s)
- def Predicate path, parsed
- #puts "PREDICATE with #{path}"
- return nil unless path[0] == ?[
- predicates = []
- while path[0] == ?[
- path, expr = get_group(path)
- predicates << expr[1..-2] if expr
- end
- #puts "PREDICATES = #{predicates.inspect}"
- predicates.each{ |pred|
- #puts "ORING #{pred}"
- preds = []
- parsed << :predicate
- parsed << preds
- OrExpr(pred, preds)
- }
- #puts "PREDICATES = #{predicates.inspect}"
- path
- end
-
- # The following return arrays of true/false, a 1-1 mapping of the
- # supplied nodeset, except for axe(), which returns a filtered
- # nodeset
-
- #| OrExpr S 'or' S AndExpr
- #| AndExpr
- def OrExpr path, parsed
- #puts "OR >>> #{path}"
- n = []
- rest = AndExpr( path, n )
- #puts "OR <<< #{rest}"
- if rest != path
- while rest =~ /^\s*( or )/
- n = [ :or, n, [] ]
- rest = AndExpr( $', n[-1] )
- end
- end
- if parsed.size == 0 and n.size != 0
- parsed.replace(n)
- elsif n.size > 0
- parsed << n
- end
- rest
- end
-
- #| AndExpr S 'and' S EqualityExpr
- #| EqualityExpr
- def AndExpr path, parsed
- #puts "AND >>> #{path}"
- n = []
- rest = EqualityExpr( path, n )
- #puts "AND <<< #{rest}"
- if rest != path
- while rest =~ /^\s*( and )/
- n = [ :and, n, [] ]
- #puts "AND >>> #{rest}"
- rest = EqualityExpr( $', n[-1] )
- #puts "AND <<< #{rest}"
- end
- end
- if parsed.size == 0 and n.size != 0
- parsed.replace(n)
- elsif n.size > 0
- parsed << n
- end
- rest
- end
-
- #| EqualityExpr ('=' | '!=') RelationalExpr
- #| RelationalExpr
- def EqualityExpr path, parsed
- #puts "EQUALITY >>> #{path}"
- n = []
- rest = RelationalExpr( path, n )
- #puts "EQUALITY <<< #{rest}"
- if rest != path
- while rest =~ /^\s*(!?=)\s*/
- if $1[0] == ?!
- n = [ :neq, n, [] ]
- else
- n = [ :eq, n, [] ]
- end
- rest = RelationalExpr( $', n[-1] )
- end
- end
- if parsed.size == 0 and n.size != 0
- parsed.replace(n)
- elsif n.size > 0
- parsed << n
- end
- rest
- end
-
- #| RelationalExpr ('<' | '>' | '<=' | '>=') AdditiveExpr
- #| AdditiveExpr
- def RelationalExpr path, parsed
- #puts "RELATION >>> #{path}"
- n = []
- rest = AdditiveExpr( path, n )
- #puts "RELATION <<< #{rest}"
- if rest != path
- while rest =~ /^\s*([<>]=?)\s*/
- if $1[0] == ?<
- sym = "lt"
- else
- sym = "gt"
- end
- sym << "eq" if $1[-1] == ?=
- n = [ sym.intern, n, [] ]
- rest = AdditiveExpr( $', n[-1] )
- end
- end
- if parsed.size == 0 and n.size != 0
- parsed.replace(n)
- elsif n.size > 0
- parsed << n
- end
- rest
- end
-
- #| AdditiveExpr ('+' | S '-') MultiplicativeExpr
- #| MultiplicativeExpr
- def AdditiveExpr path, parsed
- #puts "ADDITIVE >>> #{path}"
- n = []
- rest = MultiplicativeExpr( path, n )
- #puts "ADDITIVE <<< #{rest}"
- if rest != path
- while rest =~ /^\s*(\+| -)\s*/
- if $1[0] == ?+
- n = [ :plus, n, [] ]
- else
- n = [ :minus, n, [] ]
- end
- rest = MultiplicativeExpr( $', n[-1] )
- end
- end
- if parsed.size == 0 and n.size != 0
- parsed.replace(n)
- elsif n.size > 0
- parsed << n
- end
- rest
- end
-
- #| MultiplicativeExpr ('*' | S ('div' | 'mod') S) UnaryExpr
- #| UnaryExpr
- def MultiplicativeExpr path, parsed
- #puts "MULT >>> #{path}"
- n = []
- rest = UnaryExpr( path, n )
- #puts "MULT <<< #{rest}"
- if rest != path
- while rest =~ /^\s*(\*| div | mod )\s*/
- if $1[0] == ?*
- n = [ :mult, n, [] ]
- elsif $1.include?( "div" )
- n = [ :div, n, [] ]
- else
- n = [ :mod, n, [] ]
- end
- rest = UnaryExpr( $', n[-1] )
- end
- end
- if parsed.size == 0 and n.size != 0
- parsed.replace(n)
- elsif n.size > 0
- parsed << n
- end
- rest
- end
-
- #| '-' UnaryExpr
- #| UnionExpr
- def UnaryExpr path, parsed
- path =~ /^(\-*)/
- path = $'
- if $1 and (($1.size % 2) != 0)
- mult = -1
- else
- mult = 1
- end
- parsed << :neg if mult < 0
-
- #puts "UNARY >>> #{path}"
- n = []
- path = UnionExpr( path, n )
- #puts "UNARY <<< #{path}"
- parsed.concat( n )
- path
- end
-
- #| UnionExpr '|' PathExpr
- #| PathExpr
- def UnionExpr path, parsed
- #puts "UNION >>> #{path}"
- n = []
- rest = PathExpr( path, n )
- #puts "UNION <<< #{rest}"
- if rest != path
- while rest =~ /^\s*(\|)\s*/
- n = [ :union, n, [] ]
- rest = PathExpr( $', n[-1] )
- end
- end
- if parsed.size == 0 and n.size != 0
- parsed.replace( n )
- elsif n.size > 0
- parsed << n
- end
- rest
- end
-
- #| LocationPath
- #| FilterExpr ('/' | '//') RelativeLocationPath
- def PathExpr path, parsed
- path =~ /^\s*/
- path = $'
- #puts "PATH >>> #{path}"
- n = []
- rest = FilterExpr( path, n )
- #puts "PATH <<< '#{rest}'"
- if rest != path
- if rest and rest[0] == ?/
- return RelativeLocationPath(rest, n)
- end
- end
- #puts "BEFORE WITH '#{rest}'"
- rest = LocationPath(rest, n) if rest =~ /\A[\/\.\@\[\w_*]/
- parsed.concat(n)
- return rest
- end
-
- #| FilterExpr Predicate
- #| PrimaryExpr
- def FilterExpr path, parsed
- #puts "FILTER >>> #{path}"
- n = []
- path = PrimaryExpr( path, n )
- #puts "FILTER <<< #{path}"
- path = Predicate(path, n) if path and path[0] == ?[
- #puts "FILTER <<< #{path}"
- parsed.concat(n)
- path
- end
-
- #| VARIABLE_REFERENCE
- #| '(' expr ')'
- #| LITERAL
- #| NUMBER
- #| FunctionCall
- VARIABLE_REFERENCE = /^\$(#{NAME_STR})/u
- NUMBER = /^(\d*\.?\d+)/
- NT = /^comment|text|processing-instruction|node$/
- def PrimaryExpr path, parsed
- arry = []
- case path
- when VARIABLE_REFERENCE
- varname = $1
- path = $'
- parsed << :variable
- parsed << varname
- #arry << @variables[ varname ]
- when /^(\w[-\w]*)(?:\()/
- #puts "PrimaryExpr :: Function >>> #$1 -- '#$''"
- fname = $1
- tmp = $'
- #puts "#{fname} =~ #{NT.inspect}"
- return path if fname =~ NT
- path = tmp
- parsed << :function
- parsed << fname
- path = FunctionCall(path, parsed)
- when NUMBER
- #puts "LITERAL or NUMBER: #$1"
- varname = $1.nil? ? $2 : $1
- path = $'
- parsed << :literal
- parsed << (varname.include?('.') ? varname.to_f : varname.to_i)
- when LITERAL
- #puts "LITERAL or NUMBER: #$1"
- varname = $1.nil? ? $2 : $1
- path = $'
- parsed << :literal
- parsed << varname
- when /^\(/ #/
- path, contents = get_group(path)
- contents = contents[1..-2]
- n = []
- OrExpr( contents, n )
- parsed.concat(n)
- end
- path
- end
-
- #| FUNCTION_NAME '(' ( expr ( ',' expr )* )? ')'
- def FunctionCall rest, parsed
- path, arguments = parse_args(rest)
- argset = []
- for argument in arguments
- args = []
- OrExpr( argument, args )
- argset << args
- end
- parsed << argset
- path
- end
-
- # get_group( '[foo]bar' ) -> ['bar', '[foo]']
- def get_group string
- ind = 0
- depth = 0
- st = string[0,1]
- en = (st == "(" ? ")" : "]")
- begin
- case string[ind,1]
- when st
- depth += 1
- when en
- depth -= 1
- end
- ind += 1
- end while depth > 0 and ind < string.length
- return nil unless depth==0
- [string[ind..-1], string[0..ind-1]]
- end
-
- def parse_args( string )
- arguments = []
- ind = 0
- inquot = false
- inapos = false
- depth = 1
- begin
- case string[ind]
- when ?"
- inquot = !inquot unless inapos
- when ?'
- inapos = !inapos unless inquot
- else
- unless inquot or inapos
- case string[ind]
- when ?(
- depth += 1
- if depth == 1
- string = string[1..-1]
- ind -= 1
- end
- when ?)
- depth -= 1
- if depth == 0
- s = string[0,ind].strip
- arguments << s unless s == ""
- string = string[ind+1..-1]
- end
- when ?,
- if depth == 1
- s = string[0,ind].strip
- arguments << s unless s == ""
- string = string[ind+1..-1]
- ind = -1
- end
- end
- end
- end
- ind += 1
- end while depth > 0 and ind < string.length
- return nil unless depth==0
- [string,arguments]
- end
- end
- end
-end
diff --git a/lib/rexml/quickpath.rb b/lib/rexml/quickpath.rb
deleted file mode 100644
index fd2ebdd0ca..0000000000
--- a/lib/rexml/quickpath.rb
+++ /dev/null
@@ -1,263 +0,0 @@
-require 'rexml/functions'
-require 'rexml/xmltokens'
-
-module REXML
- class QuickPath
- include Functions
- include XMLTokens
-
- EMPTY_HASH = {}
-
- def QuickPath::first element, path, namespaces=EMPTY_HASH
- match(element, path, namespaces)[0]
- end
-
- def QuickPath::each element, path, namespaces=EMPTY_HASH, &block
- path = "*" unless path
- match(element, path, namespaces).each( &block )
- end
-
- def QuickPath::match element, path, namespaces=EMPTY_HASH
- raise "nil is not a valid xpath" unless path
- results = nil
- Functions::namespace_context = namespaces
- case path
- when /^\/([^\/]|$)/u
- # match on root
- path = path[1..-1]
- return [element.root.parent] if path == ''
- results = filter([element.root], path)
- when /^[-\w]*::/u
- results = filter([element], path)
- when /^\*/u
- results = filter(element.to_a, path)
- when /^[\[!\w:]/u
- # match on child
- matches = []
- children = element.to_a
- results = filter(children, path)
- else
- results = filter([element], path)
- end
- return results
- end
-
- # Given an array of nodes it filters the array based on the path. The
- # result is that when this method returns, the array will contain elements
- # which match the path
- def QuickPath::filter elements, path
- return elements if path.nil? or path == '' or elements.size == 0
- case path
- when /^\/\//u # Descendant
- return axe( elements, "descendant-or-self", $' )
- when /^\/?\b(\w[-\w]*)\b::/u # Axe
- axe_name = $1
- rest = $'
- return axe( elements, $1, $' )
- when /^\/(?=\b([:!\w][-\.\w]*:)?[-!\*\.\w]*\b([^:(]|$)|\*)/u # Child
- rest = $'
- results = []
- elements.each do |element|
- results |= filter( element.to_a, rest )
- end
- return results
- when /^\/?(\w[-\w]*)\(/u # / Function
- return function( elements, $1, $' )
- when Namespace::NAMESPLIT # Element name
- name = $2
- ns = $1
- rest = $'
- elements.delete_if do |element|
- !(element.kind_of? Element and
- (element.expanded_name == name or
- (element.name == name and
- element.namespace == Functions.namespace_context[ns])))
- end
- return filter( elements, rest )
- when /^\/\[/u
- matches = []
- elements.each do |element|
- matches |= predicate( element.to_a, path[1..-1] ) if element.kind_of? Element
- end
- return matches
- when /^\[/u # Predicate
- return predicate( elements, path )
- when /^\/?\.\.\./u # Ancestor
- return axe( elements, "ancestor", $' )
- when /^\/?\.\./u # Parent
- return filter( elements.collect{|e|e.parent}, $' )
- when /^\/?\./u # Self
- return filter( elements, $' )
- when /^\*/u # Any
- results = []
- elements.each do |element|
- results |= filter( [element], $' ) if element.kind_of? Element
- #if element.kind_of? Element
- # children = element.to_a
- # children.delete_if { |child| !child.kind_of?(Element) }
- # results |= filter( children, $' )
- #end
- end
- return results
- end
- return []
- end
-
- def QuickPath::axe( elements, axe_name, rest )
- matches = []
- matches = filter( elements.dup, rest ) if axe_name =~ /-or-self$/u
- case axe_name
- when /^descendant/u
- elements.each do |element|
- matches |= filter( element.to_a, "descendant-or-self::#{rest}" ) if element.kind_of? Element
- end
- when /^ancestor/u
- elements.each do |element|
- while element.parent
- matches << element.parent
- element = element.parent
- end
- end
- matches = filter( matches, rest )
- when "self"
- matches = filter( elements, rest )
- when "child"
- elements.each do |element|
- matches |= filter( element.to_a, rest ) if element.kind_of? Element
- end
- when "attribute"
- elements.each do |element|
- matches << element.attributes[ rest ] if element.kind_of? Element
- end
- when "parent"
- matches = filter(elements.collect{|element| element.parent}.uniq, rest)
- when "following-sibling"
- matches = filter(elements.collect{|element| element.next_sibling}.uniq,
- rest)
- when "previous-sibling"
- matches = filter(elements.collect{|element|
- element.previous_sibling}.uniq, rest )
- end
- return matches.uniq
- end
-
- # A predicate filters a node-set with respect to an axis to produce a
- # new node-set. For each node in the node-set to be filtered, the
- # PredicateExpr is evaluated with that node as the context node, with
- # the number of nodes in the node-set as the context size, and with the
- # proximity position of the node in the node-set with respect to the
- # axis as the context position; if PredicateExpr evaluates to true for
- # that node, the node is included in the new node-set; otherwise, it is
- # not included.
- #
- # A PredicateExpr is evaluated by evaluating the Expr and converting
- # the result to a boolean. If the result is a number, the result will
- # be converted to true if the number is equal to the context position
- # and will be converted to false otherwise; if the result is not a
- # number, then the result will be converted as if by a call to the
- # boolean function. Thus a location path para[3] is equivalent to
- # para[position()=3].
- def QuickPath::predicate( elements, path )
- ind = 1
- bcount = 1
- while bcount > 0
- bcount += 1 if path[ind] == ?[
- bcount -= 1 if path[ind] == ?]
- ind += 1
- end
- ind -= 1
- predicate = path[1..ind-1]
- rest = path[ind+1..-1]
-
- # have to change 'a [=<>] b [=<>] c' into 'a [=<>] b and b [=<>] c'
- predicate.gsub!( /([^\s(and)(or)<>=]+)\s*([<>=])\s*([^\s(and)(or)<>=]+)\s*([<>=])\s*([^\s(and)(or)<>=]+)/u,
- '\1 \2 \3 and \3 \4 \5' )
- # Let's do some Ruby trickery to avoid some work:
- predicate.gsub!( /&/u, "&&" )
- predicate.gsub!( /=/u, "==" )
- predicate.gsub!( /@(\w[-\w.]*)/u, 'attribute("\1")' )
- predicate.gsub!( /\bmod\b/u, "%" )
- predicate.gsub!( /\b(\w[-\w.]*\()/u ) {
- fname = $1
- fname.gsub( /-/u, "_" )
- }
-
- Functions.pair = [ 0, elements.size ]
- results = []
- elements.each do |element|
- Functions.pair[0] += 1
- Functions.node = element
- res = eval( predicate )
- case res
- when true
- results << element
- when Fixnum
- results << element if Functions.pair[0] == res
- when String
- results << element
- end
- end
- return filter( results, rest )
- end
-
- def QuickPath::attribute( name )
- return Functions.node.attributes[name] if Functions.node.kind_of? Element
- end
-
- def QuickPath::name()
- return Functions.node.name if Functions.node.kind_of? Element
- end
-
- def QuickPath::method_missing( id, *args )
- begin
- Functions.send( id.id2name, *args )
- rescue Exception
- raise "METHOD: #{id.id2name}(#{args.join ', '})\n#{$!.message}"
- end
- end
-
- def QuickPath::function( elements, fname, rest )
- args = parse_args( elements, rest )
- Functions.pair = [0, elements.size]
- results = []
- elements.each do |element|
- Functions.pair[0] += 1
- Functions.node = element
- res = Functions.send( fname, *args )
- case res
- when true
- results << element
- when Fixnum
- results << element if Functions.pair[0] == res
- end
- end
- return results
- end
-
- def QuickPath::parse_args( element, string )
- # /.*?(?:\)|,)/
- arguments = []
- buffer = ""
- while string and string != ""
- c = string[0]
- string.sub!(/^./u, "")
- case c
- when ?,
- # if depth = 1, then we start a new argument
- arguments << evaluate( buffer )
- #arguments << evaluate( string[0..count] )
- when ?(
- # start a new method call
- function( element, buffer, string )
- buffer = ""
- when ?)
- # close the method call and return arguments
- return arguments
- else
- buffer << c
- end
- end
- ""
- end
- end
-end
diff --git a/lib/rexml/rexml.rb b/lib/rexml/rexml.rb
deleted file mode 100644
index 810af31356..0000000000
--- a/lib/rexml/rexml.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# -*- encoding: utf-8 -*-
-# REXML is an XML toolkit for Ruby[http://www.ruby-lang.org], in Ruby.
-#
-# REXML is a _pure_ Ruby, XML 1.0 conforming,
-# non-validating[http://www.w3.org/TR/2004/REC-xml-20040204/#sec-conformance]
-# toolkit with an intuitive API. REXML passes 100% of the non-validating Oasis
-# tests[http://www.oasis-open.org/committees/xml-conformance/xml-test-suite.shtml],
-# and provides tree, stream, SAX2, pull, and lightweight APIs. REXML also
-# includes a full XPath[http://www.w3c.org/tr/xpath] 1.0 implementation. Since
-# Ruby 1.8, REXML is included in the standard Ruby distribution.
-#
-# Main page:: http://www.germane-software.com/software/rexml
-# Author:: Sean Russell <serATgermaneHYPHENsoftwareDOTcom>
-# Date:: 2008/019
-# Version:: 3.1.7.3
-#
-# This API documentation can be downloaded from the REXML home page, or can
-# be accessed online[http://www.germane-software.com/software/rexml_doc]
-#
-# A tutorial is available in the REXML distribution in docs/tutorial.html,
-# or can be accessed
-# online[http://www.germane-software.com/software/rexml/docs/tutorial.html]
-module REXML
- COPYRIGHT = "Copyright © 2001-2008 Sean Russell <ser@germane-software.com>"
- DATE = "2008/019"
- VERSION = "3.1.7.3"
- REVISION = "$Revision$".gsub(/\$Revision:|\$/,'').strip
-
- Copyright = COPYRIGHT
- Version = VERSION
-end
diff --git a/lib/rexml/sax2listener.rb b/lib/rexml/sax2listener.rb
deleted file mode 100644
index 9545b08a93..0000000000
--- a/lib/rexml/sax2listener.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-module REXML
- # A template for stream parser listeners.
- # Note that the declarations (attlistdecl, elementdecl, etc) are trivially
- # processed; REXML doesn't yet handle doctype entity declarations, so you
- # have to parse them out yourself.
- # === Missing methods from SAX2
- # ignorable_whitespace
- # === Methods extending SAX2
- # +WARNING+
- # These methods are certainly going to change, until DTDs are fully
- # supported. Be aware of this.
- # start_document
- # end_document
- # doctype
- # elementdecl
- # attlistdecl
- # entitydecl
- # notationdecl
- # cdata
- # xmldecl
- # comment
- module SAX2Listener
- def start_document
- end
- def end_document
- end
- def start_prefix_mapping prefix, uri
- end
- def end_prefix_mapping prefix
- end
- def start_element uri, localname, qname, attributes
- end
- def end_element uri, localname, qname
- end
- def characters text
- end
- def processing_instruction target, data
- end
- # Handles a doctype declaration. Any attributes of the doctype which are
- # not supplied will be nil. # EG, <!DOCTYPE me PUBLIC "foo" "bar">
- # @p name the name of the doctype; EG, "me"
- # @p pub_sys "PUBLIC", "SYSTEM", or nil. EG, "PUBLIC"
- # @p long_name the supplied long name, or nil. EG, "foo"
- # @p uri the uri of the doctype, or nil. EG, "bar"
- def doctype name, pub_sys, long_name, uri
- end
- # If a doctype includes an ATTLIST declaration, it will cause this
- # method to be called. The content is the declaration itself, unparsed.
- # EG, <!ATTLIST el attr CDATA #REQUIRED> will come to this method as "el
- # attr CDATA #REQUIRED". This is the same for all of the .*decl
- # methods.
- def attlistdecl(element, pairs, contents)
- end
- # <!ELEMENT ...>
- def elementdecl content
- end
- # <!ENTITY ...>
- # The argument passed to this method is an array of the entity
- # declaration. It can be in a number of formats, but in general it
- # returns (example, result):
- # <!ENTITY % YN '"Yes"'>
- # ["%", "YN", "'\"Yes\"'", "\""]
- # <!ENTITY % YN 'Yes'>
- # ["%", "YN", "'Yes'", "s"]
- # <!ENTITY WhatHeSaid "He said %YN;">
- # ["WhatHeSaid", "\"He said %YN;\"", "YN"]
- # <!ENTITY open-hatch SYSTEM "http://www.textuality.com/boilerplate/OpenHatch.xml">
- # ["open-hatch", "SYSTEM", "\"http://www.textuality.com/boilerplate/OpenHatch.xml\""]
- # <!ENTITY open-hatch PUBLIC "-//Textuality//TEXT Standard open-hatch boilerplate//EN" "http://www.textuality.com/boilerplate/OpenHatch.xml">
- # ["open-hatch", "PUBLIC", "\"-//Textuality//TEXT Standard open-hatch boilerplate//EN\"", "\"http://www.textuality.com/boilerplate/OpenHatch.xml\""]
- # <!ENTITY hatch-pic SYSTEM "../grafix/OpenHatch.gif" NDATA gif>
- # ["hatch-pic", "SYSTEM", "\"../grafix/OpenHatch.gif\"", "\n\t\t\t\t\t\t\tNDATA gif", "gif"]
- def entitydecl name, decl
- end
- # <!NOTATION ...>
- def notationdecl content
- end
- # Called when <![CDATA[ ... ]]> is encountered in a document.
- # @p content "..."
- def cdata content
- end
- # Called when an XML PI is encountered in the document.
- # EG: <?xml version="1.0" encoding="utf"?>
- # @p version the version attribute value. EG, "1.0"
- # @p encoding the encoding attribute value, or nil. EG, "utf"
- # @p standalone the standalone attribute value, or nil. EG, nil
- # @p spaced the declaration is followed by a line break
- def xmldecl version, encoding, standalone
- end
- # Called when a comment is encountered.
- # @p comment The content of the comment
- def comment comment
- end
- def progress position
- end
- end
-end
diff --git a/lib/rexml/source.rb b/lib/rexml/source.rb
deleted file mode 100644
index d4335138a1..0000000000
--- a/lib/rexml/source.rb
+++ /dev/null
@@ -1,258 +0,0 @@
-require 'rexml/encoding'
-
-module REXML
- # Generates Source-s. USE THIS CLASS.
- class SourceFactory
- # Generates a Source object
- # @param arg Either a String, or an IO
- # @return a Source, or nil if a bad argument was given
- def SourceFactory::create_from(arg)
- if arg.respond_to? :read and
- arg.respond_to? :readline and
- arg.respond_to? :nil? and
- arg.respond_to? :eof?
- IOSource.new(arg)
- elsif arg.respond_to? :to_str
- require 'stringio'
- IOSource.new(StringIO.new(arg))
- elsif arg.kind_of? Source
- arg
- else
- raise "#{arg.class} is not a valid input stream. It must walk \n"+
- "like either a String, an IO, or a Source."
- end
- end
- end
-
- # A Source can be searched for patterns, and wraps buffers and other
- # objects and provides consumption of text
- class Source
- include Encoding
- # The current buffer (what we're going to read next)
- attr_reader :buffer
- # The line number of the last consumed text
- attr_reader :line
- attr_reader :encoding
-
- # Constructor
- # @param arg must be a String, and should be a valid XML document
- # @param encoding if non-null, sets the encoding of the source to this
- # value, overriding all encoding detection
- def initialize(arg, encoding=nil)
- @orig = @buffer = arg
- if encoding
- self.encoding = encoding
- else
- self.encoding = check_encoding( @buffer )
- end
- @line = 0
- end
-
-
- # Inherited from Encoding
- # Overridden to support optimized en/decoding
- def encoding=(enc)
- return unless super
- @line_break = encode( '>' )
- if enc != UTF_8
- @buffer = decode(@buffer)
- @to_utf = true
- else
- @to_utf = false
- if @buffer.respond_to? :force_encoding
- @buffer.force_encoding Encoding::UTF_8
- end
- end
- end
-
- # Scans the source for a given pattern. Note, that this is not your
- # usual scan() method. For one thing, the pattern argument has some
- # requirements; for another, the source can be consumed. You can easily
- # confuse this method. Originally, the patterns were easier
- # to construct and this method more robust, because this method
- # generated search regexes on the fly; however, this was
- # computationally expensive and slowed down the entire REXML package
- # considerably, since this is by far the most commonly called method.
- # @param pattern must be a Regexp, and must be in the form of
- # /^\s*(#{your pattern, with no groups})(.*)/. The first group
- # will be returned; the second group is used if the consume flag is
- # set.
- # @param consume if true, the pattern returned will be consumed, leaving
- # everything after it in the Source.
- # @return the pattern, if found, or nil if the Source is empty or the
- # pattern is not found.
- def scan(pattern, cons=false)
- return nil if @buffer.nil?
- rv = @buffer.scan(pattern)
- @buffer = $' if cons and rv.size>0
- rv
- end
-
- def read
- end
-
- def consume( pattern )
- @buffer = $' if pattern.match( @buffer )
- end
-
- def match_to( char, pattern )
- return pattern.match(@buffer)
- end
-
- def match_to_consume( char, pattern )
- md = pattern.match(@buffer)
- @buffer = $'
- return md
- end
-
- def match(pattern, cons=false)
- md = pattern.match(@buffer)
- @buffer = $' if cons and md
- return md
- end
-
- # @return true if the Source is exhausted
- def empty?
- @buffer == ""
- end
-
- def position
- @orig.index( @buffer )
- end
-
- # @return the current line in the source
- def current_line
- lines = @orig.split
- res = lines.grep @buffer[0..30]
- res = res[-1] if res.kind_of? Array
- lines.index( res ) if res
- end
- end
-
- # A Source that wraps an IO. See the Source class for method
- # documentation
- class IOSource < Source
- #attr_reader :block_size
-
- # block_size has been deprecated
- def initialize(arg, block_size=500, encoding=nil)
- @er_source = @source = arg
- @to_utf = false
-
- # Determining the encoding is a deceptively difficult issue to resolve.
- # First, we check the first two bytes for UTF-16. Then we
- # assume that the encoding is at least ASCII enough for the '>', and
- # we read until we get one of those. This gives us the XML declaration,
- # if there is one. If there isn't one, the file MUST be UTF-8, as per
- # the XML spec. If there is one, we can determine the encoding from
- # it.
- @buffer = ""
- str = @source.read( 2 ) || ''
- if encoding
- self.encoding = encoding
- elsif str[0,2] == "\xfe\xff"
- @line_break = "\000>"
- elsif str[0,2] == "\xff\xfe"
- @line_break = ">\000"
- elsif str[0,2] == "\xef\xbb"
- str += @source.read(1)
- str = '' if (str[2,1] == "\xBF")
- @line_break = ">"
- else
- @line_break = ">"
- end
- super( @source.eof? ? str : str+@source.readline( @line_break ) )
- end
-
- def scan(pattern, cons=false)
- rv = super
- # You'll notice that this next section is very similar to the same
- # section in match(), but just a liiittle different. This is
- # because it is a touch faster to do it this way with scan()
- # than the way match() does it; enough faster to warrent duplicating
- # some code
- if rv.size == 0
- until @buffer =~ pattern or @source.nil?
- begin
- # READLINE OPT
- #str = @source.read(@block_size)
- str = @source.readline(@line_break)
- str = decode(str) if @to_utf and str
- @buffer << str
- rescue Iconv::IllegalSequence
- raise
- rescue
- @source = nil
- end
- end
- rv = super
- end
- rv.taint
- rv
- end
-
- def read
- begin
- str = @source.readline(@line_break)
- str = decode(str) if @to_utf and str
- @buffer << str
- if not @to_utf and @buffer.respond_to? :force_encoding
- @buffer.force_encoding Encoding::UTF_8
- end
- rescue Exception, NameError
- @source = nil
- end
- end
-
- def consume( pattern )
- match( pattern, true )
- end
-
- def match( pattern, cons=false )
- rv = pattern.match(@buffer)
- @buffer = $' if cons and rv
- while !rv and @source
- begin
- str = @source.readline(@line_break)
- str = decode(str) if @to_utf and str
- @buffer << str
- rv = pattern.match(@buffer)
- @buffer = $' if cons and rv
- rescue
- @source = nil
- end
- end
- rv.taint
- rv
- end
-
- def empty?
- super and ( @source.nil? || @source.eof? )
- end
-
- def position
- @er_source.pos rescue 0
- end
-
- # @return the current line in the source
- def current_line
- begin
- pos = @er_source.pos # The byte position in the source
- lineno = @er_source.lineno # The XML < position in the source
- @er_source.rewind
- line = 0 # The \r\n position in the source
- begin
- while @er_source.pos < pos
- @er_source.readline
- line += 1
- end
- rescue
- end
- rescue IOError
- pos = -1
- line = -1
- end
- [pos, lineno, line]
- end
- end
-end
diff --git a/lib/rexml/streamlistener.rb b/lib/rexml/streamlistener.rb
deleted file mode 100644
index 3a4ef9f769..0000000000
--- a/lib/rexml/streamlistener.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-module REXML
- # A template for stream parser listeners.
- # Note that the declarations (attlistdecl, elementdecl, etc) are trivially
- # processed; REXML doesn't yet handle doctype entity declarations, so you
- # have to parse them out yourself.
- module StreamListener
- # Called when a tag is encountered.
- # @p name the tag name
- # @p attrs an array of arrays of attribute/value pairs, suitable for
- # use with assoc or rassoc. IE, <tag attr1="value1" attr2="value2">
- # will result in
- # tag_start( "tag", # [["attr1","value1"],["attr2","value2"]])
- def tag_start name, attrs
- end
- # Called when the end tag is reached. In the case of <tag/>, tag_end
- # will be called immidiately after tag_start
- # @p the name of the tag
- def tag_end name
- end
- # Called when text is encountered in the document
- # @p text the text content.
- def text text
- end
- # Called when an instruction is encountered. EG: <?xsl sheet='foo'?>
- # @p name the instruction name; in the example, "xsl"
- # @p instruction the rest of the instruction. In the example,
- # "sheet='foo'"
- def instruction name, instruction
- end
- # Called when a comment is encountered.
- # @p comment The content of the comment
- def comment comment
- end
- # Handles a doctype declaration. Any attributes of the doctype which are
- # not supplied will be nil. # EG, <!DOCTYPE me PUBLIC "foo" "bar">
- # @p name the name of the doctype; EG, "me"
- # @p pub_sys "PUBLIC", "SYSTEM", or nil. EG, "PUBLIC"
- # @p long_name the supplied long name, or nil. EG, "foo"
- # @p uri the uri of the doctype, or nil. EG, "bar"
- def doctype name, pub_sys, long_name, uri
- end
- # Called when the doctype is done
- def doctype_end
- end
- # If a doctype includes an ATTLIST declaration, it will cause this
- # method to be called. The content is the declaration itself, unparsed.
- # EG, <!ATTLIST el attr CDATA #REQUIRED> will come to this method as "el
- # attr CDATA #REQUIRED". This is the same for all of the .*decl
- # methods.
- def attlistdecl element_name, attributes, raw_content
- end
- # <!ELEMENT ...>
- def elementdecl content
- end
- # <!ENTITY ...>
- # The argument passed to this method is an array of the entity
- # declaration. It can be in a number of formats, but in general it
- # returns (example, result):
- # <!ENTITY % YN '"Yes"'>
- # ["%", "YN", "'\"Yes\"'", "\""]
- # <!ENTITY % YN 'Yes'>
- # ["%", "YN", "'Yes'", "s"]
- # <!ENTITY WhatHeSaid "He said %YN;">
- # ["WhatHeSaid", "\"He said %YN;\"", "YN"]
- # <!ENTITY open-hatch SYSTEM "http://www.textuality.com/boilerplate/OpenHatch.xml">
- # ["open-hatch", "SYSTEM", "\"http://www.textuality.com/boilerplate/OpenHatch.xml\""]
- # <!ENTITY open-hatch PUBLIC "-//Textuality//TEXT Standard open-hatch boilerplate//EN" "http://www.textuality.com/boilerplate/OpenHatch.xml">
- # ["open-hatch", "PUBLIC", "\"-//Textuality//TEXT Standard open-hatch boilerplate//EN\"", "\"http://www.textuality.com/boilerplate/OpenHatch.xml\""]
- # <!ENTITY hatch-pic SYSTEM "../grafix/OpenHatch.gif" NDATA gif>
- # ["hatch-pic", "SYSTEM", "\"../grafix/OpenHatch.gif\"", "\n\t\t\t\t\t\t\tNDATA gif", "gif"]
- def entitydecl content
- end
- # <!NOTATION ...>
- def notationdecl content
- end
- # Called when %foo; is encountered in a doctype declaration.
- # @p content "foo"
- def entity content
- end
- # Called when <![CDATA[ ... ]]> is encountered in a document.
- # @p content "..."
- def cdata content
- end
- # Called when an XML PI is encountered in the document.
- # EG: <?xml version="1.0" encoding="utf"?>
- # @p version the version attribute value. EG, "1.0"
- # @p encoding the encoding attribute value, or nil. EG, "utf"
- # @p standalone the standalone attribute value, or nil. EG, nil
- def xmldecl version, encoding, standalone
- end
- end
-end
diff --git a/lib/rexml/syncenumerator.rb b/lib/rexml/syncenumerator.rb
deleted file mode 100644
index 11609bdf3d..0000000000
--- a/lib/rexml/syncenumerator.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-module REXML
- class SyncEnumerator
- include Enumerable
-
- # Creates a new SyncEnumerator which enumerates rows of given
- # Enumerable objects.
- def initialize(*enums)
- @gens = enums
- @length = @gens.collect {|x| x.size }.max
- end
-
- # Returns the number of enumerated Enumerable objects, i.e. the size
- # of each row.
- def size
- @gens.size
- end
-
- # Returns the number of enumerated Enumerable objects, i.e. the size
- # of each row.
- def length
- @gens.length
- end
-
- # Enumerates rows of the Enumerable objects.
- def each
- @length.times {|i|
- yield @gens.collect {|x| x[i]}
- }
- self
- end
- end
-end
diff --git a/lib/rexml/text.rb b/lib/rexml/text.rb
deleted file mode 100644
index fac5ac3e41..0000000000
--- a/lib/rexml/text.rb
+++ /dev/null
@@ -1,404 +0,0 @@
-require 'rexml/entity'
-require 'rexml/doctype'
-require 'rexml/child'
-require 'rexml/doctype'
-require 'rexml/parseexception'
-
-module REXML
- # Represents text nodes in an XML document
- class Text < Child
- include Comparable
- # The order in which the substitutions occur
- SPECIALS = [ /&(?!#?[\w-]+;)/u, /</u, />/u, /"/u, /'/u, /\r/u ]
- SUBSTITUTES = ['&amp;', '&lt;', '&gt;', '&quot;', '&apos;', '&#13;']
- # Characters which are substituted in written strings
- SLAICEPS = [ '<', '>', '"', "'", '&' ]
- SETUTITSBUS = [ /&lt;/u, /&gt;/u, /&quot;/u, /&apos;/u, /&amp;/u ]
-
- # If +raw+ is true, then REXML leaves the value alone
- attr_accessor :raw
-
- NEEDS_A_SECOND_CHECK = /(<|&((#{Entity::NAME});|(#0*((?:\d+)|(?:x[a-fA-F0-9]+)));)?)/um
- NUMERICENTITY = /&#0*((?:\d+)|(?:x[a-fA-F0-9]+));/
- VALID_CHAR = [
- 0x9, 0xA, 0xD,
- (0x20..0xD7FF),
- (0xE000..0xFFFD),
- (0x10000..0x10FFFF)
- ]
-
- if String.method_defined? :encode
- VALID_XML_CHARS = Regexp.new('^['+
- VALID_CHAR.map { |item|
- case item
- when Fixnum
- [item].pack('U').force_encoding('utf-8')
- when Range
- [item.first, '-'.ord, item.last].pack('UUU').force_encoding('utf-8')
- end
- }.join +
- ']*$')
- else
- VALID_XML_CHARS = /^(
- [\x09\x0A\x0D\x20-\x7E] # ASCII
- | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
- | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
- | [\xE1-\xEC\xEE][\x80-\xBF]{2} # straight 3-byte
- | \xEF[\x80-\xBE]{2} #
- | \xEF\xBF[\x80-\xBD] # excluding U+fffe and U+ffff
- | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
- | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
- | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
- | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
- )*$/nx;
- end
-
- # Constructor
- # +arg+ if a String, the content is set to the String. If a Text,
- # the object is shallowly cloned.
- #
- # +respect_whitespace+ (boolean, false) if true, whitespace is
- # respected
- #
- # +parent+ (nil) if this is a Parent object, the parent
- # will be set to this.
- #
- # +raw+ (nil) This argument can be given three values.
- # If true, then the value of used to construct this object is expected to
- # contain no unescaped XML markup, and REXML will not change the text. If
- # this value is false, the string may contain any characters, and REXML will
- # escape any and all defined entities whose values are contained in the
- # text. If this value is nil (the default), then the raw value of the
- # parent will be used as the raw value for this node. If there is no raw
- # value for the parent, and no value is supplied, the default is false.
- # Use this field if you have entities defined for some text, and you don't
- # want REXML to escape that text in output.
- # Text.new( "<&", false, nil, false ) #-> "&lt;&amp;"
- # Text.new( "&lt;&amp;", false, nil, false ) #-> "&amp;lt;&amp;amp;"
- # Text.new( "<&", false, nil, true ) #-> Parse exception
- # Text.new( "&lt;&amp;", false, nil, true ) #-> "&lt;&amp;"
- # # Assume that the entity "s" is defined to be "sean"
- # # and that the entity "r" is defined to be "russell"
- # Text.new( "sean russell" ) #-> "&s; &r;"
- # Text.new( "sean russell", false, nil, true ) #-> "sean russell"
- #
- # +entity_filter+ (nil) This can be an array of entities to match in the
- # supplied text. This argument is only useful if +raw+ is set to false.
- # Text.new( "sean russell", false, nil, false, ["s"] ) #-> "&s; russell"
- # Text.new( "sean russell", false, nil, true, ["s"] ) #-> "sean russell"
- # In the last example, the +entity_filter+ argument is ignored.
- #
- # +pattern+ INTERNAL USE ONLY
- def initialize(arg, respect_whitespace=false, parent=nil, raw=nil,
- entity_filter=nil, illegal=NEEDS_A_SECOND_CHECK )
-
- @raw = false
-
- if parent
- super( parent )
- @raw = parent.raw
- else
- @parent = nil
- end
-
- @raw = raw unless raw.nil?
- @entity_filter = entity_filter
- @normalized = @unnormalized = nil
-
- if arg.kind_of? String
- @string = arg.clone
- @string.squeeze!(" \n\t") unless respect_whitespace
- elsif arg.kind_of? Text
- @string = arg.to_s
- @raw = arg.raw
- elsif
- raise "Illegal argument of type #{arg.type} for Text constructor (#{arg})"
- end
-
- @string.gsub!( /\r\n?/, "\n" )
-
- Text.check(@string, NEEDS_A_SECOND_CHECK, doctype) if @raw and @parent
- end
-
- def parent= parent
- super(parent)
- Text.check(@string, NEEDS_A_SECOND_CHECK, doctype) if @raw and @parent
- end
-
- # check for illegal characters
- def Text.check string, pattern, doctype
-
- # illegal anywhere
- if string !~ VALID_XML_CHARS
- if String.method_defined? :encode
- string.chars.each do |c|
- case c.ord
- when *VALID_CHAR
- else
- raise "Illegal character #{c.inspect} in raw string \"#{string}\""
- end
- end
- else
- string.scan(/[\x00-\x7F]|[\x80-\xBF][\xC0-\xF0]*|[\xC0-\xF0]/n) do |c|
- case c.unpack('U')
- when *VALID_CHAR
- else
- raise "Illegal character #{c.inspect} in raw string \"#{string}\""
- end
- end
- end
- end
-
- # context sensitive
- string.scan(pattern) do
- if $1[-1] != ?;
- raise "Illegal character '#{$1}' in raw string \"#{string}\""
- elsif $1[0] == ?&
- if $5 and $5[0] == ?#
- case ($5[1] == ?x ? $5[2..-1].to_i(16) : $5[1..-1].to_i)
- when *VALID_CHAR
- else
- raise "Illegal character '#{$1}' in raw string \"#{string}\""
- end
- elsif $3 and !SUBSTITUTES.include?($1)
- if !doctype or !doctype.entities.has_key?($3)
- raise "Undeclared entity '#{$1}' in raw string \"#{string}\""
- end
- end
- end
- end
- end
-
- def node_type
- :text
- end
-
- def empty?
- @string.size==0
- end
-
-
- def clone
- return Text.new(self)
- end
-
-
- # Appends text to this text node. The text is appended in the +raw+ mode
- # of this text node.
- def <<( to_append )
- @string << to_append.gsub( /\r\n?/, "\n" )
- end
-
-
- # +other+ a String or a Text
- # +returns+ the result of (to_s <=> arg.to_s)
- def <=>( other )
- to_s() <=> other.to_s
- end
-
- def doctype
- if @parent
- doc = @parent.document
- doc.doctype if doc
- end
- end
-
- REFERENCE = /#{Entity::REFERENCE}/
- # Returns the string value of this text node. This string is always
- # escaped, meaning that it is a valid XML text node string, and all
- # entities that can be escaped, have been inserted. This method respects
- # the entity filter set in the constructor.
- #
- # # Assume that the entity "s" is defined to be "sean", and that the
- # # entity "r" is defined to be "russell"
- # t = Text.new( "< & sean russell", false, nil, false, ['s'] )
- # t.to_s #-> "&lt; &amp; &s; russell"
- # t = Text.new( "< & &s; russell", false, nil, false )
- # t.to_s #-> "&lt; &amp; &s; russell"
- # u = Text.new( "sean russell", false, nil, true )
- # u.to_s #-> "sean russell"
- def to_s
- return @string if @raw
- return @normalized if @normalized
-
- @normalized = Text::normalize( @string, doctype, @entity_filter )
- end
-
- def inspect
- @string.inspect
- end
-
- # Returns the string value of this text. This is the text without
- # entities, as it might be used programmatically, or printed to the
- # console. This ignores the 'raw' attribute setting, and any
- # entity_filter.
- #
- # # Assume that the entity "s" is defined to be "sean", and that the
- # # entity "r" is defined to be "russell"
- # t = Text.new( "< & sean russell", false, nil, false, ['s'] )
- # t.value #-> "< & sean russell"
- # t = Text.new( "< & &s; russell", false, nil, false )
- # t.value #-> "< & sean russell"
- # u = Text.new( "sean russell", false, nil, true )
- # u.value #-> "sean russell"
- def value
- return @unnormalized if @unnormalized
- @unnormalized = Text::unnormalize( @string, doctype )
- end
-
- # Sets the contents of this text node. This expects the text to be
- # unnormalized. It returns self.
- #
- # e = Element.new( "a" )
- # e.add_text( "foo" ) # <a>foo</a>
- # e[0].value = "bar" # <a>bar</a>
- # e[0].value = "<a>" # <a>&lt;a&gt;</a>
- def value=( val )
- @string = val.gsub( /\r\n?/, "\n" )
- @unnormalized = nil
- @normalized = nil
- @raw = false
- end
-
- def wrap(string, width, addnewline=false)
- # Recursively wrap string at width.
- return string if string.length <= width
- place = string.rindex(' ', width) # Position in string with last ' ' before cutoff
- if addnewline then
- return "\n" + string[0,place] + "\n" + wrap(string[place+1..-1], width)
- else
- return string[0,place] + "\n" + wrap(string[place+1..-1], width)
- end
- end
-
- def indent_text(string, level=1, style="\t", indentfirstline=true)
- return string if level < 0
- new_string = ''
- string.each { |line|
- indent_string = style * level
- new_line = (indent_string + line).sub(/[\s]+$/,'')
- new_string << new_line
- }
- new_string.strip! unless indentfirstline
- return new_string
- end
-
- # == DEPRECATED
- # See REXML::Formatters
- #
- def write( writer, indent=-1, transitive=false, ie_hack=false )
- Kernel.warn("#{self.class.name}.write is deprecated. See REXML::Formatters")
- formatter = if indent > -1
- REXML::Formatters::Pretty.new( indent )
- else
- REXML::Formatters::Default.new
- end
- formatter.write( self, writer )
- end
-
- # FIXME
- # This probably won't work properly
- def xpath
- path = @parent.xpath
- path += "/text()"
- return path
- end
-
- # Writes out text, substituting special characters beforehand.
- # +out+ A String, IO, or any other object supporting <<( String )
- # +input+ the text to substitute and the write out
- #
- # z=utf8.unpack("U*")
- # ascOut=""
- # z.each{|r|
- # if r < 0x100
- # ascOut.concat(r.chr)
- # else
- # ascOut.concat(sprintf("&#x%x;", r))
- # end
- # }
- # puts ascOut
- def write_with_substitution out, input
- copy = input.clone
- # Doing it like this rather than in a loop improves the speed
- copy.gsub!( SPECIALS[0], SUBSTITUTES[0] )
- copy.gsub!( SPECIALS[1], SUBSTITUTES[1] )
- copy.gsub!( SPECIALS[2], SUBSTITUTES[2] )
- copy.gsub!( SPECIALS[3], SUBSTITUTES[3] )
- copy.gsub!( SPECIALS[4], SUBSTITUTES[4] )
- copy.gsub!( SPECIALS[5], SUBSTITUTES[5] )
- out << copy
- end
-
- # Reads text, substituting entities
- def Text::read_with_substitution( input, illegal=nil )
- copy = input.clone
-
- if copy =~ illegal
- raise ParseException.new( "malformed text: Illegal character #$& in \"#{copy}\"" )
- end if illegal
-
- copy.gsub!( /\r\n?/, "\n" )
- if copy.include? ?&
- copy.gsub!( SETUTITSBUS[0], SLAICEPS[0] )
- copy.gsub!( SETUTITSBUS[1], SLAICEPS[1] )
- copy.gsub!( SETUTITSBUS[2], SLAICEPS[2] )
- copy.gsub!( SETUTITSBUS[3], SLAICEPS[3] )
- copy.gsub!( SETUTITSBUS[4], SLAICEPS[4] )
- copy.gsub!( /&#0*((?:\d+)|(?:x[a-f0-9]+));/ ) {
- m=$1
- #m='0' if m==''
- m = "0#{m}" if m[0] == ?x
- [Integer(m)].pack('U*')
- }
- end
- copy
- end
-
- EREFERENCE = /&(?!#{Entity::NAME};)/
- # Escapes all possible entities
- def Text::normalize( input, doctype=nil, entity_filter=nil )
- copy = input.to_s
- # Doing it like this rather than in a loop improves the speed
- #copy = copy.gsub( EREFERENCE, '&amp;' )
- copy = copy.gsub( "&", "&amp;" )
- if doctype
- # Replace all ampersands that aren't part of an entity
- doctype.entities.each_value do |entity|
- copy = copy.gsub( entity.value,
- "&#{entity.name};" ) if entity.value and
- not( entity_filter and entity_filter.include?(entity) )
- end
- else
- # Replace all ampersands that aren't part of an entity
- DocType::DEFAULT_ENTITIES.each_value do |entity|
- copy = copy.gsub(entity.value, "&#{entity.name};" )
- end
- end
- copy
- end
-
- # Unescapes all possible entities
- def Text::unnormalize( string, doctype=nil, filter=nil, illegal=nil )
- string.gsub( /\r\n?/, "\n" ).gsub( REFERENCE ) {
- ref = $&
- if ref[1] == ?#
- if ref[2] == ?x
- [ref[3...-1].to_i(16)].pack('U*')
- else
- [ref[2...-1].to_i].pack('U*')
- end
- elsif ref == '&amp;'
- '&'
- elsif filter and filter.include?( ref[1...-1] )
- ref
- elsif doctype
- doctype.entity( ref[1...-1] ) or ref
- else
- entity_value = DocType::DEFAULT_ENTITIES[ ref[1...-1] ]
- entity_value ? entity_value.value : ref
- end
- }
- end
- end
-end
diff --git a/lib/rexml/undefinednamespaceexception.rb b/lib/rexml/undefinednamespaceexception.rb
deleted file mode 100644
index 8ebfdfd0a9..0000000000
--- a/lib/rexml/undefinednamespaceexception.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require 'rexml/parseexception'
-module REXML
- class UndefinedNamespaceException < ParseException
- def initialize( prefix, source, parser )
- super( "Undefined prefix #{prefix} found" )
- end
- end
-end
diff --git a/lib/rexml/validation/relaxng.rb b/lib/rexml/validation/relaxng.rb
deleted file mode 100644
index 2b863710b4..0000000000
--- a/lib/rexml/validation/relaxng.rb
+++ /dev/null
@@ -1,559 +0,0 @@
-require "rexml/validation/validation"
-require "rexml/parsers/baseparser"
-
-module REXML
- module Validation
- # Implemented:
- # * empty
- # * element
- # * attribute
- # * text
- # * optional
- # * choice
- # * oneOrMore
- # * zeroOrMore
- # * group
- # * value
- # * interleave
- # * mixed
- # * ref
- # * grammar
- # * start
- # * define
- #
- # Not implemented:
- # * data
- # * param
- # * include
- # * externalRef
- # * notAllowed
- # * anyName
- # * nsName
- # * except
- # * name
- class RelaxNG
- include Validator
-
- INFINITY = 1.0 / 0.0
- EMPTY = Event.new( nil )
- TEXT = [:start_element, "text"]
- attr_accessor :current
- attr_accessor :count
- attr_reader :references
-
- # FIXME: Namespaces
- def initialize source
- parser = REXML::Parsers::BaseParser.new( source )
-
- @count = 0
- @references = {}
- @root = @current = Sequence.new(self)
- @root.previous = true
- states = [ @current ]
- begin
- event = parser.pull
- case event[0]
- when :start_element
- case event[1]
- when "empty"
- when "element", "attribute", "text", "value"
- states[-1] << event
- when "optional"
- states << Optional.new( self )
- states[-2] << states[-1]
- when "choice"
- states << Choice.new( self )
- states[-2] << states[-1]
- when "oneOrMore"
- states << OneOrMore.new( self )
- states[-2] << states[-1]
- when "zeroOrMore"
- states << ZeroOrMore.new( self )
- states[-2] << states[-1]
- when "group"
- states << Sequence.new( self )
- states[-2] << states[-1]
- when "interleave"
- states << Interleave.new( self )
- states[-2] << states[-1]
- when "mixed"
- states << Interleave.new( self )
- states[-2] << states[-1]
- states[-1] << TEXT
- when "define"
- states << [ event[2]["name"] ]
- when "ref"
- states[-1] << Ref.new( event[2]["name"] )
- when "anyName"
- states << AnyName.new( self )
- states[-2] << states[-1]
- when "nsName"
- when "except"
- when "name"
- when "data"
- when "param"
- when "include"
- when "grammar"
- when "start"
- when "externalRef"
- when "notAllowed"
- end
- when :end_element
- case event[1]
- when "element", "attribute"
- states[-1] << event
- when "zeroOrMore", "oneOrMore", "choice", "optional",
- "interleave", "group", "mixed"
- states.pop
- when "define"
- ref = states.pop
- @references[ ref.shift ] = ref
- #when "empty"
- end
- when :end_document
- states[-1] << event
- when :text
- states[-1] << event
- end
- end while event[0] != :end_document
- end
-
- def receive event
- validate( event )
- end
- end
-
- class State
- def initialize( context )
- @previous = []
- @events = []
- @current = 0
- @count = context.count += 1
- @references = context.references
- @value = false
- end
-
- def reset
- return if @current == 0
- @current = 0
- @events.each {|s| s.reset if s.kind_of? State }
- end
-
- def previous=( previous )
- @previous << previous
- end
-
- def next( event )
- #print "In next with #{event.inspect}. "
- #puts "Next (#@current) is #{@events[@current]}"
- #p @previous
- return @previous.pop.next( event ) if @events[@current].nil?
- expand_ref_in( @events, @current ) if @events[@current].class == Ref
- if ( @events[@current].kind_of? State )
- @current += 1
- @events[@current-1].previous = self
- return @events[@current-1].next( event )
- end
- #puts "Current isn't a state"
- if ( @events[@current].matches?(event) )
- @current += 1
- if @events[@current].nil?
- #puts "#{inspect[0,5]} 1RETURNING #{@previous.inspect[0,5]}"
- return @previous.pop
- elsif @events[@current].kind_of? State
- @current += 1
- #puts "#{inspect[0,5]} 2RETURNING (#{@current-1}) #{@events[@current-1].inspect[0,5]}; on return, next is #{@events[@current]}"
- @events[@current-1].previous = self
- return @events[@current-1]
- else
- #puts "#{inspect[0,5]} RETURNING self w/ next(#@current) = #{@events[@current]}"
- return self
- end
- else
- return nil
- end
- end
-
- def to_s
- # Abbreviated:
- self.class.name =~ /(?:::)(\w)\w+$/
- # Full:
- #self.class.name =~ /(?:::)(\w+)$/
- "#$1.#@count"
- end
-
- def inspect
- "< #{to_s} #{@events.collect{|e|
- pre = e == @events[@current] ? '#' : ''
- pre + e.inspect unless self == e
- }.join(', ')} >"
- end
-
- def expected
- return [@events[@current]]
- end
-
- def <<( event )
- add_event_to_arry( @events, event )
- end
-
-
- protected
- def expand_ref_in( arry, ind )
- new_events = []
- @references[ arry[ind].to_s ].each{ |evt|
- add_event_to_arry(new_events,evt)
- }
- arry[ind,1] = new_events
- end
-
- def add_event_to_arry( arry, evt )
- evt = generate_event( evt )
- if evt.kind_of? String
- arry[-1].event_arg = evt if arry[-1].kind_of? Event and @value
- @value = false
- else
- arry << evt
- end
- end
-
- def generate_event( event )
- return event if event.kind_of? State or event.class == Ref
- evt = nil
- arg = nil
- case event[0]
- when :start_element
- case event[1]
- when "element"
- evt = :start_element
- arg = event[2]["name"]
- when "attribute"
- evt = :start_attribute
- arg = event[2]["name"]
- when "text"
- evt = :text
- when "value"
- evt = :text
- @value = true
- end
- when :text
- return event[1]
- when :end_document
- return Event.new( event[0] )
- else # then :end_element
- case event[1]
- when "element"
- evt = :end_element
- when "attribute"
- evt = :end_attribute
- end
- end
- return Event.new( evt, arg )
- end
- end
-
-
- class Sequence < State
- def matches?(event)
- @events[@current].matches?( event )
- end
- end
-
-
- class Optional < State
- def next( event )
- if @current == 0
- rv = super
- return rv if rv
- @prior = @previous.pop
- return @prior.next( event )
- end
- super
- end
-
- def matches?(event)
- @events[@current].matches?(event) ||
- (@current == 0 and @previous[-1].matches?(event))
- end
-
- def expected
- return [ @prior.expected, @events[0] ].flatten if @current == 0
- return [@events[@current]]
- end
- end
-
-
- class ZeroOrMore < Optional
- def next( event )
- expand_ref_in( @events, @current ) if @events[@current].class == Ref
- if ( @events[@current].matches?(event) )
- @current += 1
- if @events[@current].nil?
- @current = 0
- return self
- elsif @events[@current].kind_of? State
- @current += 1
- @events[@current-1].previous = self
- return @events[@current-1]
- else
- return self
- end
- else
- @prior = @previous.pop
- return @prior.next( event ) if @current == 0
- return nil
- end
- end
-
- def expected
- return [ @prior.expected, @events[0] ].flatten if @current == 0
- return [@events[@current]]
- end
- end
-
-
- class OneOrMore < State
- def initialize context
- super
- @ord = 0
- end
-
- def reset
- super
- @ord = 0
- end
-
- def next( event )
- expand_ref_in( @events, @current ) if @events[@current].class == Ref
- if ( @events[@current].matches?(event) )
- @current += 1
- @ord += 1
- if @events[@current].nil?
- @current = 0
- return self
- elsif @events[@current].kind_of? State
- @current += 1
- @events[@current-1].previous = self
- return @events[@current-1]
- else
- return self
- end
- else
- return @previous.pop.next( event ) if @current == 0 and @ord > 0
- return nil
- end
- end
-
- def matches?( event )
- @events[@current].matches?(event) ||
- (@current == 0 and @ord > 0 and @previous[-1].matches?(event))
- end
-
- def expected
- if @current == 0 and @ord > 0
- return [@previous[-1].expected, @events[0]].flatten
- else
- return [@events[@current]]
- end
- end
- end
-
-
- class Choice < State
- def initialize context
- super
- @choices = []
- end
-
- def reset
- super
- @events = []
- @choices.each { |c| c.each { |s| s.reset if s.kind_of? State } }
- end
-
- def <<( event )
- add_event_to_arry( @choices, event )
- end
-
- def next( event )
- # Make the choice if we haven't
- if @events.size == 0
- c = 0 ; max = @choices.size
- while c < max
- if @choices[c][0].class == Ref
- expand_ref_in( @choices[c], 0 )
- @choices += @choices[c]
- @choices.delete( @choices[c] )
- max -= 1
- else
- c += 1
- end
- end
- @events = @choices.find { |evt| evt[0].matches? event }
- # Remove the references
- # Find the events
- end
- #puts "In next with #{event.inspect}."
- #puts "events is #{@events.inspect}"
- unless @events
- @events = []
- return nil
- end
- #puts "current = #@current"
- super
- end
-
- def matches?( event )
- return @events[@current].matches?( event ) if @events.size > 0
- !@choices.find{|evt| evt[0].matches?(event)}.nil?
- end
-
- def expected
- #puts "IN CHOICE EXPECTED"
- #puts "EVENTS = #{@events.inspect}"
- return [@events[@current]] if @events.size > 0
- return @choices.collect do |x|
- if x[0].kind_of? State
- x[0].expected
- else
- x[0]
- end
- end.flatten
- end
-
- def inspect
- "< #{to_s} #{@choices.collect{|e| e.collect{|f|f.to_s}.join(', ')}.join(' or ')} >"
- end
-
- protected
- def add_event_to_arry( arry, evt )
- if evt.kind_of? State or evt.class == Ref
- arry << [evt]
- elsif evt[0] == :text
- if arry[-1] and
- arry[-1][-1].kind_of?( Event ) and
- arry[-1][-1].event_type == :text and @value
-
- arry[-1][-1].event_arg = evt[1]
- @value = false
- end
- else
- arry << [] if evt[0] == :start_element
- arry[-1] << generate_event( evt )
- end
- end
- end
-
-
- class Interleave < Choice
- def initialize context
- super
- @choice = 0
- end
-
- def reset
- @choice = 0
- end
-
- def next_current( event )
- # Expand references
- c = 0 ; max = @choices.size
- while c < max
- if @choices[c][0].class == Ref
- expand_ref_in( @choices[c], 0 )
- @choices += @choices[c]
- @choices.delete( @choices[c] )
- max -= 1
- else
- c += 1
- end
- end
- @events = @choices[@choice..-1].find { |evt| evt[0].matches? event }
- @current = 0
- if @events
- # reorder the choices
- old = @choices[@choice]
- idx = @choices.index( @events )
- @choices[@choice] = @events
- @choices[idx] = old
- @choice += 1
- end
-
- #puts "In next with #{event.inspect}."
- #puts "events is #{@events.inspect}"
- @events = [] unless @events
- end
-
-
- def next( event )
- # Find the next series
- next_current(event) unless @events[@current]
- return nil unless @events[@current]
-
- expand_ref_in( @events, @current ) if @events[@current].class == Ref
- #puts "In next with #{event.inspect}."
- #puts "Next (#@current) is #{@events[@current]}"
- if ( @events[@current].kind_of? State )
- @current += 1
- @events[@current-1].previous = self
- return @events[@current-1].next( event )
- end
- #puts "Current isn't a state"
- return @previous.pop.next( event ) if @events[@current].nil?
- if ( @events[@current].matches?(event) )
- @current += 1
- if @events[@current].nil?
- #puts "#{inspect[0,5]} 1RETURNING self" unless @choices[@choice].nil?
- return self unless @choices[@choice].nil?
- #puts "#{inspect[0,5]} 1RETURNING #{@previous[-1].inspect[0,5]}"
- return @previous.pop
- elsif @events[@current].kind_of? State
- @current += 1
- #puts "#{inspect[0,5]} 2RETURNING (#{@current-1}) #{@events[@current-1].inspect[0,5]}; on return, next is #{@events[@current]}"
- @events[@current-1].previous = self
- return @events[@current-1]
- else
- #puts "#{inspect[0,5]} RETURNING self w/ next(#@current) = #{@events[@current]}"
- return self
- end
- else
- return nil
- end
- end
-
- def matches?( event )
- return @events[@current].matches?( event ) if @events[@current]
- !@choices[@choice..-1].find{|evt| evt[0].matches?(event)}.nil?
- end
-
- def expected
- #puts "IN CHOICE EXPECTED"
- #puts "EVENTS = #{@events.inspect}"
- return [@events[@current]] if @events[@current]
- return @choices[@choice..-1].collect do |x|
- if x[0].kind_of? State
- x[0].expected
- else
- x[0]
- end
- end.flatten
- end
-
- def inspect
- "< #{to_s} #{@choices.collect{|e| e.collect{|f|f.to_s}.join(', ')}.join(' and ')} >"
- end
- end
-
- class Ref
- def initialize value
- @value = value
- end
- def to_s
- @value
- end
- def inspect
- "{#{to_s}}"
- end
- end
- end
-end
diff --git a/lib/rexml/validation/validation.rb b/lib/rexml/validation/validation.rb
deleted file mode 100644
index 93f5bfb329..0000000000
--- a/lib/rexml/validation/validation.rb
+++ /dev/null
@@ -1,155 +0,0 @@
-require 'rexml/validation/validationexception'
-
-module REXML
- module Validation
- module Validator
- NILEVENT = [ nil ]
- def reset
- @current = @root
- @root.reset
- @root.previous = true
- @attr_stack = []
- self
- end
- def dump
- puts @root.inspect
- end
- def validate( event )
- #puts "Current: #@current"
- #puts "Event: #{event.inspect}"
- @attr_stack = [] unless defined? @attr_stack
- match = @current.next(event)
- raise ValidationException.new( "Validation error. Expected: "+
- @current.expected.join( " or " )+" from #{@current.inspect} "+
- " but got #{Event.new( event[0], event[1] ).inspect}" ) unless match
- @current = match
-
- # Check for attributes
- case event[0]
- when :start_element
- #puts "Checking attributes"
- @attr_stack << event[2]
- begin
- sattr = [:start_attribute, nil]
- eattr = [:end_attribute]
- text = [:text, nil]
- k,v = event[2].find { |key,value|
- sattr[1] = key
- #puts "Looking for #{sattr.inspect}"
- m = @current.next( sattr )
- #puts "Got #{m.inspect}"
- if m
- # If the state has text children...
- #puts "Looking for #{eattr.inspect}"
- #puts "Expect #{m.expected}"
- if m.matches?( eattr )
- #puts "Got end"
- @current = m
- else
- #puts "Didn't get end"
- text[1] = value
- #puts "Looking for #{text.inspect}"
- m = m.next( text )
- #puts "Got #{m.inspect}"
- text[1] = nil
- return false unless m
- @current = m if m
- end
- m = @current.next( eattr )
- if m
- @current = m
- true
- else
- false
- end
- else
- false
- end
- }
- event[2].delete(k) if k
- end while k
- when :end_element
- attrs = @attr_stack.pop
- raise ValidationException.new( "Validation error. Illegal "+
- " attributes: #{attrs.inspect}") if attrs.length > 0
- end
- end
- end
-
- class Event
- def initialize(event_type, event_arg=nil )
- @event_type = event_type
- @event_arg = event_arg
- end
-
- attr_reader :event_type
- attr_accessor :event_arg
-
- def done?
- @done
- end
-
- def single?
- return (@event_type != :start_element and @event_type != :start_attribute)
- end
-
- def matches?( event )
- #puts "#@event_type =? #{event[0]} && #@event_arg =? #{event[1]} "
- return false unless event[0] == @event_type
- case event[0]
- when nil
- return true
- when :start_element
- return true if event[1] == @event_arg
- when :end_element
- return true
- when :start_attribute
- return true if event[1] == @event_arg
- when :end_attribute
- return true
- when :end_document
- return true
- when :text
- return (@event_arg.nil? or @event_arg == event[1])
-=begin
- when :processing_instruction
- false
- when :xmldecl
- false
- when :start_doctype
- false
- when :end_doctype
- false
- when :externalentity
- false
- when :elementdecl
- false
- when :entity
- false
- when :attlistdecl
- false
- when :notationdecl
- false
- when :end_doctype
- false
-=end
- else
- false
- end
- end
-
- def ==( other )
- return false unless other.kind_of? Event
- @event_type == other.event_type and @event_arg == other.event_arg
- end
-
- def to_s
- inspect
- end
-
- def inspect
- "#{@event_type.inspect}( #@event_arg )"
- end
- end
- end
-end
diff --git a/lib/rexml/validation/validationexception.rb b/lib/rexml/validation/validationexception.rb
deleted file mode 100644
index 4723d9e4d3..0000000000
--- a/lib/rexml/validation/validationexception.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module REXML
- module Validation
- class ValidationException < RuntimeError
- def initialize msg
- super
- end
- end
- end
-end
diff --git a/lib/rexml/xmldecl.rb b/lib/rexml/xmldecl.rb
deleted file mode 100644
index 361e4b7106..0000000000
--- a/lib/rexml/xmldecl.rb
+++ /dev/null
@@ -1,119 +0,0 @@
-require 'rexml/encoding'
-require 'rexml/source'
-
-module REXML
- # NEEDS DOCUMENTATION
- class XMLDecl < Child
- include Encoding
-
- DEFAULT_VERSION = "1.0";
- DEFAULT_ENCODING = "UTF-8";
- DEFAULT_STANDALONE = "no";
- START = '<\?xml';
- STOP = '\?>';
-
- attr_accessor :version, :standalone
- attr_reader :writeencoding, :writethis
-
- def initialize(version=DEFAULT_VERSION, encoding=nil, standalone=nil)
- @writethis = true
- @writeencoding = !encoding.nil?
- if version.kind_of? XMLDecl
- super()
- @version = version.version
- self.encoding = version.encoding
- @writeencoding = version.writeencoding
- @standalone = version.standalone
- else
- super()
- @version = version
- self.encoding = encoding
- @standalone = standalone
- end
- @version = DEFAULT_VERSION if @version.nil?
- end
-
- def clone
- XMLDecl.new(self)
- end
-
- # indent::
- # Ignored. There must be no whitespace before an XML declaration
- # transitive::
- # Ignored
- # ie_hack::
- # Ignored
- def write(writer, indent=-1, transitive=false, ie_hack=false)
- return nil unless @writethis or writer.kind_of? Output
- writer << START.sub(/\\/u, '')
- if writer.kind_of? Output
- writer << " #{content writer.encoding}"
- else
- writer << " #{content encoding}"
- end
- writer << STOP.sub(/\\/u, '')
- end
-
- def ==( other )
- other.kind_of?(XMLDecl) and
- other.version == @version and
- other.encoding == self.encoding and
- other.standalone == @standalone
- end
-
- def xmldecl version, encoding, standalone
- @version = version
- self.encoding = encoding
- @standalone = standalone
- end
-
- def node_type
- :xmldecl
- end
-
- alias :stand_alone? :standalone
- alias :old_enc= :encoding=
-
- def encoding=( enc )
- if enc.nil?
- self.old_enc = "UTF-8"
- @writeencoding = false
- else
- self.old_enc = enc
- @writeencoding = true
- end
- self.dowrite
- end
-
- # Only use this if you do not want the XML declaration to be written;
- # this object is ignored by the XML writer. Otherwise, instantiate your
- # own XMLDecl and add it to the document.
- #
- # Note that XML 1.1 documents *must* include an XML declaration
- def XMLDecl.default
- rv = XMLDecl.new( "1.0" )
- rv.nowrite
- rv
- end
-
- def nowrite
- @writethis = false
- end
-
- def dowrite
- @writethis = true
- end
-
- def inspect
- START.sub(/\\/u, '') + " ... " + STOP.sub(/\\/u, '')
- end
-
- private
- def content(enc)
- rv = "version='#@version'"
- rv << " encoding='#{enc}'" if @writeencoding || enc !~ /utf-8/i
- rv << " standalone='#@standalone'" if @standalone
- rv
- end
- end
-end
diff --git a/lib/rexml/xmltokens.rb b/lib/rexml/xmltokens.rb
deleted file mode 100644
index 83efeb0e44..0000000000
--- a/lib/rexml/xmltokens.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module REXML
- # Defines a number of tokens used for parsing XML. Not for general
- # consumption.
- module XMLTokens
- NCNAME_STR= '[\w:][\-\w\d.]*'
- NAME_STR= "(?:#{NCNAME_STR}:)?#{NCNAME_STR}"
-
- NAMECHAR = '[\-\w\d\.:]'
- NAME = "([\\w:]#{NAMECHAR}*)"
- NMTOKEN = "(?:#{NAMECHAR})+"
- NMTOKENS = "#{NMTOKEN}(\\s+#{NMTOKEN})*"
- REFERENCE = "(?:&#{NAME};|&#\\d+;|&#x[0-9a-fA-F]+;)"
-
- #REFERENCE = "(?:#{ENTITYREF}|#{CHARREF})"
- #ENTITYREF = "&#{NAME};"
- #CHARREF = "&#\\d+;|&#x[0-9a-fA-F]+;"
- end
-end
diff --git a/lib/rexml/xpath.rb b/lib/rexml/xpath.rb
deleted file mode 100644
index b22969ec8c..0000000000
--- a/lib/rexml/xpath.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'rexml/functions'
-require 'rexml/xpath_parser'
-
-module REXML
- # Wrapper class. Use this class to access the XPath functions.
- class XPath
- include Functions
- EMPTY_HASH = {}
-
- # Finds and returns the first node that matches the supplied xpath.
- # element::
- # The context element
- # path::
- # The xpath to search for. If not supplied or nil, returns the first
- # node matching '*'.
- # namespaces::
- # If supplied, a Hash which defines a namespace mapping.
- # variables::
- # If supplied, a Hash which maps $variables in the query
- # to values. This can be used to avoid XPath injection attacks
- # or to automatically handle escaping string values.
- #
- # XPath.first( node )
- # XPath.first( doc, "//b"} )
- # XPath.first( node, "a/x:b", { "x"=>"http://doofus" } )
- # XPath.first( node, '/book/publisher/text()=$publisher', {}, {"publisher"=>"O'Reilly"})
- def XPath::first element, path=nil, namespaces=nil, variables={}
- raise "The namespaces argument, if supplied, must be a hash object." unless namespaces.nil? or namespaces.kind_of?(Hash)
- raise "The variables argument, if supplied, must be a hash object." unless variables.kind_of?(Hash)
- parser = XPathParser.new
- parser.namespaces = namespaces
- parser.variables = variables
- path = "*" unless path
- element = [element] unless element.kind_of? Array
- parser.parse(path, element).flatten[0]
- end
-
- # Iterates over nodes that match the given path, calling the supplied
- # block with the match.
- # element::
- # The context element
- # path::
- # The xpath to search for. If not supplied or nil, defaults to '*'
- # namespaces::
- # If supplied, a Hash which defines a namespace mapping
- # variables::
- # If supplied, a Hash which maps $variables in the query
- # to values. This can be used to avoid XPath injection attacks
- # or to automatically handle escaping string values.
- #
- # XPath.each( node ) { |el| ... }
- # XPath.each( node, '/*[@attr='v']' ) { |el| ... }
- # XPath.each( node, 'ancestor::x' ) { |el| ... }
- # XPath.each( node, '/book/publisher/text()=$publisher', {}, {"publisher"=>"O'Reilly"}) \
- # {|el| ... }
- def XPath::each element, path=nil, namespaces=nil, variables={}, &block
- raise "The namespaces argument, if supplied, must be a hash object." unless namespaces.nil? or namespaces.kind_of?(Hash)
- raise "The variables argument, if supplied, must be a hash object." unless variables.kind_of?(Hash)
- parser = XPathParser.new
- parser.namespaces = namespaces
- parser.variables = variables
- path = "*" unless path
- element = [element] unless element.kind_of? Array
- parser.parse(path, element).each( &block )
- end
-
- # Returns an array of nodes matching a given XPath.
- def XPath::match element, path=nil, namespaces=nil, variables={}
- parser = XPathParser.new
- parser.namespaces = namespaces
- parser.variables = variables
- path = "*" unless path
- element = [element] unless element.kind_of? Array
- parser.parse(path,element)
- end
- end
-end
diff --git a/lib/rexml/xpath_parser.rb b/lib/rexml/xpath_parser.rb
deleted file mode 100644
index ead5adaf7f..0000000000
--- a/lib/rexml/xpath_parser.rb
+++ /dev/null
@@ -1,792 +0,0 @@
-require 'rexml/namespace'
-require 'rexml/xmltokens'
-require 'rexml/attribute'
-require 'rexml/syncenumerator'
-require 'rexml/parsers/xpathparser'
-
-class Object
- def dclone
- clone
- end
-end
-class Symbol
- def dclone ; self ; end
-end
-class Fixnum
- def dclone ; self ; end
-end
-class Float
- def dclone ; self ; end
-end
-class Array
- def dclone
- klone = self.clone
- klone.clear
- self.each{|v| klone << v.dclone}
- klone
- end
-end
-
-module REXML
- # You don't want to use this class. Really. Use XPath, which is a wrapper
- # for this class. Believe me. You don't want to poke around in here.
- # There is strange, dark magic at work in this code. Beware. Go back! Go
- # back while you still can!
- class XPathParser
- include XMLTokens
- LITERAL = /^'([^']*)'|^"([^"]*)"/u
-
- def initialize( )
- @parser = REXML::Parsers::XPathParser.new
- @namespaces = nil
- @variables = {}
- end
-
- def namespaces=( namespaces={} )
- Functions::namespace_context = namespaces
- @namespaces = namespaces
- end
-
- def variables=( vars={} )
- Functions::variables = vars
- @variables = vars
- end
-
- def parse path, nodeset
- #puts "#"*40
- path_stack = @parser.parse( path )
- #puts "PARSE: #{path} => #{path_stack.inspect}"
- #puts "PARSE: nodeset = #{nodeset.inspect}"
- match( path_stack, nodeset )
- end
-
- def get_first path, nodeset
- #puts "#"*40
- path_stack = @parser.parse( path )
- #puts "PARSE: #{path} => #{path_stack.inspect}"
- #puts "PARSE: nodeset = #{nodeset.inspect}"
- first( path_stack, nodeset )
- end
-
- def predicate path, nodeset
- path_stack = @parser.parse( path )
- expr( path_stack, nodeset )
- end
-
- def []=( variable_name, value )
- @variables[ variable_name ] = value
- end
-
-
- # Performs a depth-first (document order) XPath search, and returns the
- # first match. This is the fastest, lightest way to return a single result.
- #
- # FIXME: This method is incomplete!
- def first( path_stack, node )
- #puts "#{depth}) Entering match( #{path.inspect}, #{tree.inspect} )"
- return nil if path.size == 0
-
- case path[0]
- when :document
- # do nothing
- return first( path[1..-1], node )
- when :child
- for c in node.children
- #puts "#{depth}) CHILD checking #{name(c)}"
- r = first( path[1..-1], c )
- #puts "#{depth}) RETURNING #{r.inspect}" if r
- return r if r
- end
- when :qname
- name = path[2]
- #puts "#{depth}) QNAME #{name(tree)} == #{name} (path => #{path.size})"
- if node.name == name
- #puts "#{depth}) RETURNING #{tree.inspect}" if path.size == 3
- return node if path.size == 3
- return first( path[3..-1], node )
- else
- return nil
- end
- when :descendant_or_self
- r = first( path[1..-1], node )
- return r if r
- for c in node.children
- r = first( path, c )
- return r if r
- end
- when :node
- return first( path[1..-1], node )
- when :any
- return first( path[1..-1], node )
- end
- return nil
- end
-
-
- def match( path_stack, nodeset )
- #puts "MATCH: path_stack = #{path_stack.inspect}"
- #puts "MATCH: nodeset = #{nodeset.inspect}"
- r = expr( path_stack, nodeset )
- #puts "MAIN EXPR => #{r.inspect}"
- r
- end
-
- private
-
-
- # Returns a String namespace for a node, given a prefix
- # The rules are:
- #
- # 1. Use the supplied namespace mapping first.
- # 2. If no mapping was supplied, use the context node to look up the namespace
- def get_namespace( node, prefix )
- if @namespaces
- return @namespaces[prefix] || ''
- else
- return node.namespace( prefix ) if node.node_type == :element
- return ''
- end
- end
-
-
- # Expr takes a stack of path elements and a set of nodes (either a Parent
- # or an Array and returns an Array of matching nodes
- ALL = [ :attribute, :element, :text, :processing_instruction, :comment ]
- ELEMENTS = [ :element ]
- def expr( path_stack, nodeset, context=nil )
- #puts "#"*15
- #puts "In expr with #{path_stack.inspect}"
- #puts "Returning" if path_stack.length == 0 || nodeset.length == 0
- node_types = ELEMENTS
- return nodeset if path_stack.length == 0 || nodeset.length == 0
- while path_stack.length > 0
- #puts "#"*5
- #puts "Path stack = #{path_stack.inspect}"
- #puts "Nodeset is #{nodeset.inspect}"
- if nodeset.length == 0
- path_stack.clear
- return []
- end
- case (op = path_stack.shift)
- when :document
- nodeset = [ nodeset[0].root_node ]
- #puts ":document, nodeset = #{nodeset.inspect}"
-
- when :qname
- #puts "IN QNAME"
- prefix = path_stack.shift
- name = path_stack.shift
- nodeset.delete_if do |node|
- # FIXME: This DOUBLES the time XPath searches take
- ns = get_namespace( node, prefix )
- #puts "NS = #{ns.inspect}"
- #puts "node.node_type == :element => #{node.node_type == :element}"
- if node.node_type == :element
- #puts "node.name == #{name} => #{node.name == name}"
- if node.name == name
- #puts "node.namespace == #{ns.inspect} => #{node.namespace == ns}"
- end
- end
- !(node.node_type == :element and
- node.name == name and
- node.namespace == ns )
- end
- node_types = ELEMENTS
-
- when :any
- #puts "ANY 1: nodeset = #{nodeset.inspect}"
- #puts "ANY 1: node_types = #{node_types.inspect}"
- nodeset.delete_if { |node| !node_types.include?(node.node_type) }
- #puts "ANY 2: nodeset = #{nodeset.inspect}"
-
- when :self
- # This space left intentionally blank
-
- when :processing_instruction
- target = path_stack.shift
- nodeset.delete_if do |node|
- (node.node_type != :processing_instruction) or
- ( target!='' and ( node.target != target ) )
- end
-
- when :text
- nodeset.delete_if { |node| node.node_type != :text }
-
- when :comment
- nodeset.delete_if { |node| node.node_type != :comment }
-
- when :node
- # This space left intentionally blank
- node_types = ALL
-
- when :child
- new_nodeset = []
- nt = nil
- nodeset.each do |node|
- nt = node.node_type
- new_nodeset += node.children if nt == :element or nt == :document
- end
- nodeset = new_nodeset
- node_types = ELEMENTS
-
- when :literal
- return path_stack.shift
-
- when :attribute
- new_nodeset = []
- case path_stack.shift
- when :qname
- prefix = path_stack.shift
- name = path_stack.shift
- for element in nodeset
- if element.node_type == :element
- #puts "Element name = #{element.name}"
- #puts "get_namespace( #{element.inspect}, #{prefix} ) = #{get_namespace(element, prefix)}"
- attrib = element.attribute( name, get_namespace(element, prefix) )
- #puts "attrib = #{attrib.inspect}"
- new_nodeset << attrib if attrib
- end
- end
- when :any
- #puts "ANY"
- for element in nodeset
- if element.node_type == :element
- new_nodeset += element.attributes.to_a
- end
- end
- end
- nodeset = new_nodeset
-
- when :parent
- #puts "PARENT 1: nodeset = #{nodeset}"
- nodeset = nodeset.collect{|n| n.parent}.compact
- #nodeset = expr(path_stack.dclone, nodeset.collect{|n| n.parent}.compact)
- #puts "PARENT 2: nodeset = #{nodeset.inspect}"
- node_types = ELEMENTS
-
- when :ancestor
- new_nodeset = []
- nodeset.each do |node|
- while node.parent
- node = node.parent
- new_nodeset << node unless new_nodeset.include? node
- end
- end
- nodeset = new_nodeset
- node_types = ELEMENTS
-
- when :ancestor_or_self
- new_nodeset = []
- nodeset.each do |node|
- if node.node_type == :element
- new_nodeset << node
- while ( node.parent )
- node = node.parent
- new_nodeset << node unless new_nodeset.include? node
- end
- end
- end
- nodeset = new_nodeset
- node_types = ELEMENTS
-
- when :predicate
- new_nodeset = []
- subcontext = { :size => nodeset.size }
- pred = path_stack.shift
- nodeset.each_with_index { |node, index|
- subcontext[ :node ] = node
- #puts "PREDICATE SETTING CONTEXT INDEX TO #{index+1}"
- subcontext[ :index ] = index+1
- pc = pred.dclone
- #puts "#{node.hash}) Recursing with #{pred.inspect} and [#{node.inspect}]"
- result = expr( pc, [node], subcontext )
- result = result[0] if result.kind_of? Array and result.length == 1
- #puts "#{node.hash}) Result = #{result.inspect} (#{result.class.name})"
- if result.kind_of? Numeric
- #puts "Adding node #{node.inspect}" if result == (index+1)
- new_nodeset << node if result == (index+1)
- elsif result.instance_of? Array
- if result.size > 0 and result.inject(false) {|k,s| s or k}
- #puts "Adding node #{node.inspect}" if result.size > 0
- new_nodeset << node if result.size > 0
- end
- else
- #puts "Adding node #{node.inspect}" if result
- new_nodeset << node if result
- end
- }
- #puts "New nodeset = #{new_nodeset.inspect}"
- #puts "Path_stack = #{path_stack.inspect}"
- nodeset = new_nodeset
-=begin
- predicate = path_stack.shift
- ns = nodeset.clone
- result = expr( predicate, ns )
- #puts "Result = #{result.inspect} (#{result.class.name})"
- #puts "nodeset = #{nodeset.inspect}"
- if result.kind_of? Array
- nodeset = result.zip(ns).collect{|m,n| n if m}.compact
- else
- nodeset = result ? nodeset : []
- end
- #puts "Outgoing NS = #{nodeset.inspect}"
-=end
-
- when :descendant_or_self
- rv = descendant_or_self( path_stack, nodeset )
- path_stack.clear
- nodeset = rv
- node_types = ELEMENTS
-
- when :descendant
- results = []
- nt = nil
- nodeset.each do |node|
- nt = node.node_type
- results += expr( path_stack.dclone.unshift( :descendant_or_self ),
- node.children ) if nt == :element or nt == :document
- end
- nodeset = results
- node_types = ELEMENTS
-
- when :following_sibling
- #puts "FOLLOWING_SIBLING 1: nodeset = #{nodeset}"
- results = []
- nodeset.each do |node|
- next if node.parent.nil?
- all_siblings = node.parent.children
- current_index = all_siblings.index( node )
- following_siblings = all_siblings[ current_index+1 .. -1 ]
- results += expr( path_stack.dclone, following_siblings )
- end
- #puts "FOLLOWING_SIBLING 2: nodeset = #{nodeset}"
- nodeset = results
-
- when :preceding_sibling
- results = []
- nodeset.each do |node|
- next if node.parent.nil?
- all_siblings = node.parent.children
- current_index = all_siblings.index( node )
- preceding_siblings = all_siblings[ 0, current_index ].reverse
- results += preceding_siblings
- end
- nodeset = results
- node_types = ELEMENTS
-
- when :preceding
- new_nodeset = []
- nodeset.each do |node|
- new_nodeset += preceding( node )
- end
- #puts "NEW NODESET => #{new_nodeset.inspect}"
- nodeset = new_nodeset
- node_types = ELEMENTS
-
- when :following
- new_nodeset = []
- nodeset.each do |node|
- new_nodeset += following( node )
- end
- nodeset = new_nodeset
- node_types = ELEMENTS
-
- when :namespace
- #puts "In :namespace"
- new_nodeset = []
- prefix = path_stack.shift
- nodeset.each do |node|
- if (node.node_type == :element or node.node_type == :attribute)
- if @namespaces
- namespaces = @namespaces
- elsif (node.node_type == :element)
- namespaces = node.namespaces
- else
- namespaces = node.element.namesapces
- end
- #puts "Namespaces = #{namespaces.inspect}"
- #puts "Prefix = #{prefix.inspect}"
- #puts "Node.namespace = #{node.namespace}"
- if (node.namespace == namespaces[prefix])
- new_nodeset << node
- end
- end
- end
- nodeset = new_nodeset
-
- when :variable
- var_name = path_stack.shift
- return @variables[ var_name ]
-
- # :and, :or, :eq, :neq, :lt, :lteq, :gt, :gteq
- # TODO: Special case for :or and :and -- not evaluate the right
- # operand if the left alone determines result (i.e. is true for
- # :or and false for :and).
- when :eq, :neq, :lt, :lteq, :gt, :gteq, :or
- left = expr( path_stack.shift, nodeset.dup, context )
- #puts "LEFT => #{left.inspect} (#{left.class.name})"
- right = expr( path_stack.shift, nodeset.dup, context )
- #puts "RIGHT => #{right.inspect} (#{right.class.name})"
- res = equality_relational_compare( left, op, right )
- #puts "RES => #{res.inspect}"
- return res
-
- when :and
- left = expr( path_stack.shift, nodeset.dup, context )
- #puts "LEFT => #{left.inspect} (#{left.class.name})"
- if left == false || left.nil? || !left.inject(false) {|a,b| a | b}
- return []
- end
- right = expr( path_stack.shift, nodeset.dup, context )
- #puts "RIGHT => #{right.inspect} (#{right.class.name})"
- res = equality_relational_compare( left, op, right )
- #puts "RES => #{res.inspect}"
- return res
-
- when :div
- left = Functions::number(expr(path_stack.shift, nodeset, context)).to_f
- right = Functions::number(expr(path_stack.shift, nodeset, context)).to_f
- return (left / right)
-
- when :mod
- left = Functions::number(expr(path_stack.shift, nodeset, context )).to_f
- right = Functions::number(expr(path_stack.shift, nodeset, context )).to_f
- return (left % right)
-
- when :mult
- left = Functions::number(expr(path_stack.shift, nodeset, context )).to_f
- right = Functions::number(expr(path_stack.shift, nodeset, context )).to_f
- return (left * right)
-
- when :plus
- left = Functions::number(expr(path_stack.shift, nodeset, context )).to_f
- right = Functions::number(expr(path_stack.shift, nodeset, context )).to_f
- return (left + right)
-
- when :minus
- left = Functions::number(expr(path_stack.shift, nodeset, context )).to_f
- right = Functions::number(expr(path_stack.shift, nodeset, context )).to_f
- return (left - right)
-
- when :union
- left = expr( path_stack.shift, nodeset, context )
- right = expr( path_stack.shift, nodeset, context )
- return (left | right)
-
- when :neg
- res = expr( path_stack, nodeset, context )
- return -(res.to_f)
-
- when :not
- when :function
- func_name = path_stack.shift.tr('-','_')
- arguments = path_stack.shift
- #puts "FUNCTION 0: #{func_name}(#{arguments.collect{|a|a.inspect}.join(', ')})"
- subcontext = context ? nil : { :size => nodeset.size }
-
- res = []
- cont = context
- nodeset.each_with_index { |n, i|
- if subcontext
- subcontext[:node] = n
- subcontext[:index] = i
- cont = subcontext
- end
- arg_clone = arguments.dclone
- args = arg_clone.collect { |arg|
- #puts "FUNCTION 1: Calling expr( #{arg.inspect}, [#{n.inspect}] )"
- expr( arg, [n], cont )
- }
- #puts "FUNCTION 2: #{func_name}(#{args.collect{|a|a.inspect}.join(', ')})"
- Functions.context = cont
- res << Functions.send( func_name, *args )
- #puts "FUNCTION 3: #{res[-1].inspect}"
- }
- return res
-
- end
- end # while
- #puts "EXPR returning #{nodeset.inspect}"
- return nodeset
- end
-
-
- ##########################################################
- # FIXME
- # The next two methods are BAD MOJO!
- # This is my achilles heel. If anybody thinks of a better
- # way of doing this, be my guest. This really sucks, but
- # it is a wonder it works at all.
- # ########################################################
-
- def descendant_or_self( path_stack, nodeset )
- rs = []
- #puts "#"*80
- #puts "PATH_STACK = #{path_stack.inspect}"
- #puts "NODESET = #{nodeset.collect{|n|n.inspect}.inspect}"
- d_o_s( path_stack, nodeset, rs )
- #puts "RS = #{rs.collect{|n|n.inspect}.inspect}"
- document_order(rs.flatten.compact)
- #rs.flatten.compact
- end
-
- def d_o_s( p, ns, r )
- #puts "IN DOS with #{ns.inspect}; ALREADY HAVE #{r.inspect}"
- nt = nil
- ns.each_index do |i|
- n = ns[i]
- #puts "P => #{p.inspect}"
- x = expr( p.dclone, [ n ] )
- nt = n.node_type
- d_o_s( p, n.children, x ) if nt == :element or nt == :document and n.children.size > 0
- r.concat(x) if x.size > 0
- end
- end
-
-
- # Reorders an array of nodes so that they are in document order
- # It tries to do this efficiently.
- #
- # FIXME: I need to get rid of this, but the issue is that most of the XPath
- # interpreter functions as a filter, which means that we lose context going
- # in and out of function calls. If I knew what the index of the nodes was,
- # I wouldn't have to do this. Maybe add a document IDX for each node?
- # Problems with mutable documents. Or, rewrite everything.
- def document_order( array_of_nodes )
- new_arry = []
- array_of_nodes.each { |node|
- node_idx = []
- np = node.node_type == :attribute ? node.element : node
- while np.parent and np.parent.node_type == :element
- node_idx << np.parent.index( np )
- np = np.parent
- end
- new_arry << [ node_idx.reverse, node ]
- }
- #puts "new_arry = #{new_arry.inspect}"
- new_arry.sort{ |s1, s2| s1[0] <=> s2[0] }.collect{ |s| s[1] }
- end
-
-
- def recurse( nodeset, &block )
- for node in nodeset
- yield node
- recurse( node, &block ) if node.node_type == :element
- end
- end
-
-
-
- # Builds a nodeset of all of the preceding nodes of the supplied node,
- # in reverse document order
- # preceding:: includes every element in the document that precedes this node,
- # except for ancestors
- def preceding( node )
- #puts "IN PRECEDING"
- ancestors = []
- p = node.parent
- while p
- ancestors << p
- p = p.parent
- end
-
- acc = []
- p = preceding_node_of( node )
- #puts "P = #{p.inspect}"
- while p
- if ancestors.include? p
- ancestors.delete(p)
- else
- acc << p
- end
- p = preceding_node_of( p )
- #puts "P = #{p.inspect}"
- end
- acc
- end
-
- def preceding_node_of( node )
- #puts "NODE: #{node.inspect}"
- #puts "PREVIOUS NODE: #{node.previous_sibling_node.inspect}"
- #puts "PARENT NODE: #{node.parent}"
- psn = node.previous_sibling_node
- if psn.nil?
- if node.parent.nil? or node.parent.class == Document
- return nil
- end
- return node.parent
- #psn = preceding_node_of( node.parent )
- end
- while psn and psn.kind_of? Element and psn.children.size > 0
- psn = psn.children[-1]
- end
- psn
- end
-
- def following( node )
- #puts "IN PRECEDING"
- acc = []
- p = next_sibling_node( node )
- #puts "P = #{p.inspect}"
- while p
- acc << p
- p = following_node_of( p )
- #puts "P = #{p.inspect}"
- end
- acc
- end
-
- def following_node_of( node )
- #puts "NODE: #{node.inspect}"
- #puts "PREVIOUS NODE: #{node.previous_sibling_node.inspect}"
- #puts "PARENT NODE: #{node.parent}"
- if node.kind_of? Element and node.children.size > 0
- return node.children[0]
- end
- return next_sibling_node(node)
- end
-
- def next_sibling_node(node)
- psn = node.next_sibling_node
- while psn.nil?
- if node.parent.nil? or node.parent.class == Document
- return nil
- end
- node = node.parent
- psn = node.next_sibling_node
- #puts "psn = #{psn.inspect}"
- end
- return psn
- end
-
- def norm b
- case b
- when true, false
- return b
- when 'true', 'false'
- return Functions::boolean( b )
- when /^\d+(\.\d+)?$/
- return Functions::number( b )
- else
- return Functions::string( b )
- end
- end
-
- def equality_relational_compare( set1, op, set2 )
- #puts "EQ_REL_COMP(#{set1.inspect} #{op.inspect} #{set2.inspect})"
- if set1.kind_of? Array and set2.kind_of? Array
- #puts "#{set1.size} & #{set2.size}"
- if set1.size == 1 and set2.size == 1
- set1 = set1[0]
- set2 = set2[0]
- elsif set1.size == 0 or set2.size == 0
- nd = set1.size==0 ? set2 : set1
- rv = nd.collect { |il| compare( il, op, nil ) }
- #puts "RV = #{rv.inspect}"
- return rv
- else
- res = []
- enum = SyncEnumerator.new( set1, set2 ).each { |i1, i2|
- #puts "i1 = #{i1.inspect} (#{i1.class.name})"
- #puts "i2 = #{i2.inspect} (#{i2.class.name})"
- i1 = norm( i1 )
- i2 = norm( i2 )
- res << compare( i1, op, i2 )
- }
- return res
- end
- end
- #puts "EQ_REL_COMP: #{set1.inspect} (#{set1.class.name}), #{op}, #{set2.inspect} (#{set2.class.name})"
- #puts "COMPARING VALUES"
- # If one is nodeset and other is number, compare number to each item
- # in nodeset s.t. number op number(string(item))
- # If one is nodeset and other is string, compare string to each item
- # in nodeset s.t. string op string(item)
- # If one is nodeset and other is boolean, compare boolean to each item
- # in nodeset s.t. boolean op boolean(item)
- if set1.kind_of? Array or set2.kind_of? Array
- #puts "ISA ARRAY"
- if set1.kind_of? Array
- a = set1
- b = set2
- else
- a = set2
- b = set1
- end
-
- case b
- when true, false
- return a.collect {|v| compare( Functions::boolean(v), op, b ) }
- when Numeric
- return a.collect {|v| compare( Functions::number(v), op, b )}
- when /^\d+(\.\d+)?$/
- b = Functions::number( b )
- #puts "B = #{b.inspect}"
- return a.collect {|v| compare( Functions::number(v), op, b )}
- else
- #puts "Functions::string( #{b}(#{b.class.name}) ) = #{Functions::string(b)}"
- b = Functions::string( b )
- return a.collect { |v| compare( Functions::string(v), op, b ) }
- end
- else
- # If neither is nodeset,
- # If op is = or !=
- # If either boolean, convert to boolean
- # If either number, convert to number
- # Else, convert to string
- # Else
- # Convert both to numbers and compare
- s1 = set1.to_s
- s2 = set2.to_s
- #puts "EQ_REL_COMP: #{set1}=>#{s1}, #{set2}=>#{s2}"
- if s1 == 'true' or s1 == 'false' or s2 == 'true' or s2 == 'false'
- #puts "Functions::boolean(#{set1})=>#{Functions::boolean(set1)}"
- #puts "Functions::boolean(#{set2})=>#{Functions::boolean(set2)}"
- set1 = Functions::boolean( set1 )
- set2 = Functions::boolean( set2 )
- else
- if op == :eq or op == :neq
- if s1 =~ /^\d+(\.\d+)?$/ or s2 =~ /^\d+(\.\d+)?$/
- set1 = Functions::number( s1 )
- set2 = Functions::number( s2 )
- else
- set1 = Functions::string( set1 )
- set2 = Functions::string( set2 )
- end
- else
- set1 = Functions::number( set1 )
- set2 = Functions::number( set2 )
- end
- end
- #puts "EQ_REL_COMP: #{set1} #{op} #{set2}"
- #puts ">>> #{compare( set1, op, set2 )}"
- return compare( set1, op, set2 )
- end
- return false
- end
-
- def compare a, op, b
- #puts "COMPARE #{a.inspect}(#{a.class.name}) #{op} #{b.inspect}(#{b.class.name})"
- case op
- when :eq
- a == b
- when :neq
- a != b
- when :lt
- a < b
- when :lteq
- a <= b
- when :gt
- a > b
- when :gteq
- a >= b
- when :and
- a and b
- when :or
- a or b
- else
- false
- end
- end
- end
-end
diff --git a/lib/rinda/.document b/lib/rinda/.document
deleted file mode 100644
index 598977af68..0000000000
--- a/lib/rinda/.document
+++ /dev/null
@@ -1,3 +0,0 @@
-rinda.rb
-ring.rb
-tuplespace.rb
diff --git a/lib/rinda/rinda.rb b/lib/rinda/rinda.rb
deleted file mode 100644
index 6c59e68654..0000000000
--- a/lib/rinda/rinda.rb
+++ /dev/null
@@ -1,283 +0,0 @@
-require 'drb/drb'
-require 'thread'
-
-##
-# A module to implement the Linda distributed computing paradigm in Ruby.
-#
-# Rinda is part of DRb (dRuby).
-#
-# == Example(s)
-#
-# See the sample/drb/ directory in the Ruby distribution, from 1.8.2 onwards.
-#
-#--
-# TODO
-# == Introduction to Linda/rinda?
-#
-# == Why is this library separate from DRb?
-
-module Rinda
-
- ##
- # Rinda error base class
-
- class RindaError < RuntimeError; end
-
- ##
- # Raised when a hash-based tuple has an invalid key.
-
- class InvalidHashTupleKey < RindaError; end
-
- ##
- # Raised when trying to use a canceled tuple.
-
- class RequestCanceledError < ThreadError; end
-
- ##
- # Raised when trying to use an expired tuple.
-
- class RequestExpiredError < ThreadError; end
-
- ##
- # A tuple is the elementary object in Rinda programming.
- # Tuples may be matched against templates if the tuple and
- # the template are the same size.
-
- class Tuple
-
- ##
- # Creates a new Tuple from +ary_or_hash+ which must be an Array or Hash.
-
- def initialize(ary_or_hash)
- if hash?(ary_or_hash)
- init_with_hash(ary_or_hash)
- else
- init_with_ary(ary_or_hash)
- end
- end
-
- ##
- # The number of elements in the tuple.
-
- def size
- @tuple.size
- end
-
- ##
- # Accessor method for elements of the tuple.
-
- def [](k)
- @tuple[k]
- end
-
- ##
- # Fetches item +k+ from the tuple.
-
- def fetch(k)
- @tuple.fetch(k)
- end
-
- ##
- # Iterate through the tuple, yielding the index or key, and the
- # value, thus ensuring arrays are iterated similarly to hashes.
-
- def each # FIXME
- if Hash === @tuple
- @tuple.each { |k, v| yield(k, v) }
- else
- @tuple.each_with_index { |v, k| yield(k, v) }
- end
- end
-
- ##
- # Return the tuple itself
- def value
- @tuple
- end
-
- private
-
- def hash?(ary_or_hash)
- ary_or_hash.respond_to?(:keys)
- end
-
- ##
- # Munges +ary+ into a valid Tuple.
-
- def init_with_ary(ary)
- @tuple = Array.new(ary.size)
- @tuple.size.times do |i|
- @tuple[i] = ary[i]
- end
- end
-
- ##
- # Ensures +hash+ is a valid Tuple.
-
- def init_with_hash(hash)
- @tuple = Hash.new
- hash.each do |k, v|
- raise InvalidHashTupleKey unless String === k
- @tuple[k] = v
- end
- end
-
- end
-
- ##
- # Templates are used to match tuples in Rinda.
-
- class Template < Tuple
-
- ##
- # Matches this template against +tuple+. The +tuple+ must be the same
- # size as the template. An element with a +nil+ value in a template acts
- # as a wildcard, matching any value in the corresponding position in the
- # tuple. Elements of the template match the +tuple+ if the are #== or
- # #===.
- #
- # Template.new([:foo, 5]).match Tuple.new([:foo, 5]) # => true
- # Template.new([:foo, nil]).match Tuple.new([:foo, 5]) # => true
- # Template.new([String]).match Tuple.new(['hello']) # => true
- #
- # Template.new([:foo]).match Tuple.new([:foo, 5]) # => false
- # Template.new([:foo, 6]).match Tuple.new([:foo, 5]) # => false
- # Template.new([:foo, nil]).match Tuple.new([:foo]) # => false
- # Template.new([:foo, 6]).match Tuple.new([:foo]) # => false
-
- def match(tuple)
- return false unless tuple.respond_to?(:size)
- return false unless tuple.respond_to?(:fetch)
- return false unless self.size == tuple.size
- each do |k, v|
- begin
- it = tuple.fetch(k)
- rescue
- return false
- end
- next if v.nil?
- next if v == it
- next if v === it
- return false
- end
- return true
- end
-
- ##
- # Alias for #match.
-
- def ===(tuple)
- match(tuple)
- end
-
- end
-
- ##
- # <i>Documentation?</i>
-
- class DRbObjectTemplate
-
- ##
- # Creates a new DRbObjectTemplate that will match against +uri+ and +ref+.
-
- def initialize(uri=nil, ref=nil)
- @drb_uri = uri
- @drb_ref = ref
- end
-
- ##
- # This DRbObjectTemplate matches +ro+ if the remote object's drburi and
- # drbref are the same. +nil+ is used as a wildcard.
-
- def ===(ro)
- return true if super(ro)
- unless @drb_uri.nil?
- return false unless (@drb_uri === ro.__drburi rescue false)
- end
- unless @drb_ref.nil?
- return false unless (@drb_ref === ro.__drbref rescue false)
- end
- true
- end
-
- end
-
- ##
- # TupleSpaceProxy allows a remote Tuplespace to appear as local.
-
- class TupleSpaceProxy
-
- ##
- # Creates a new TupleSpaceProxy to wrap +ts+.
-
- def initialize(ts)
- @ts = ts
- end
-
- ##
- # Adds +tuple+ to the proxied TupleSpace. See TupleSpace#write.
-
- def write(tuple, sec=nil)
- @ts.write(tuple, sec)
- end
-
- ##
- # Takes +tuple+ from the proxied TupleSpace. See TupleSpace#take.
-
- def take(tuple, sec=nil, &block)
- port = []
- @ts.move(DRbObject.new(port), tuple, sec, &block)
- port[0]
- end
-
- ##
- # Reads +tuple+ from the proxied TupleSpace. See TupleSpace#read.
-
- def read(tuple, sec=nil, &block)
- @ts.read(tuple, sec, &block)
- end
-
- ##
- # Reads all tuples matching +tuple+ from the proxied TupleSpace. See
- # TupleSpace#read_all.
-
- def read_all(tuple)
- @ts.read_all(tuple)
- end
-
- ##
- # Registers for notifications of event +ev+ on the proxied TupleSpace.
- # See TupleSpace#notify
-
- def notify(ev, tuple, sec=nil)
- @ts.notify(ev, tuple, sec)
- end
-
- end
-
- ##
- # An SimpleRenewer allows a TupleSpace to check if a TupleEntry is still
- # alive.
-
- class SimpleRenewer
-
- include DRbUndumped
-
- ##
- # Creates a new SimpleRenewer that keeps an object alive for another +sec+
- # seconds.
-
- def initialize(sec=180)
- @sec = sec
- end
-
- ##
- # Called by the TupleSpace to check if the object is still alive.
-
- def renew
- @sec
- end
- end
-
-end
-
diff --git a/lib/rinda/ring.rb b/lib/rinda/ring.rb
deleted file mode 100644
index 4dc7c7d79a..0000000000
--- a/lib/rinda/ring.rb
+++ /dev/null
@@ -1,271 +0,0 @@
-#
-# Note: Rinda::Ring API is unstable.
-#
-require 'drb/drb'
-require 'rinda/rinda'
-require 'thread'
-
-module Rinda
-
- ##
- # The default port Ring discovery will use.
-
- Ring_PORT = 7647
-
- ##
- # A RingServer allows a Rinda::TupleSpace to be located via UDP broadcasts.
- # Service location uses the following steps:
- #
- # 1. A RingServer begins listening on the broadcast UDP address.
- # 2. A RingFinger sends a UDP packet containing the DRb URI where it will
- # listen for a reply.
- # 3. The RingServer receives the UDP packet and connects back to the
- # provided DRb URI with the DRb service.
-
- class RingServer
-
- include DRbUndumped
-
- ##
- # Advertises +ts+ on the UDP broadcast address at +port+.
-
- def initialize(ts, port=Ring_PORT)
- @ts = ts
- @soc = UDPSocket.open
- @soc.bind('', port)
- @w_service = write_service
- @r_service = reply_service
- end
-
- ##
- # Creates a thread that picks up UDP packets and passes them to do_write
- # for decoding.
-
- def write_service
- Thread.new do
- loop do
- msg = @soc.recv(1024)
- do_write(msg)
- end
- end
- end
-
- ##
- # Extracts the response URI from +msg+ and adds it to TupleSpace where it
- # will be picked up by +reply_service+ for notification.
-
- def do_write(msg)
- Thread.new do
- begin
- tuple, sec = Marshal.load(msg)
- @ts.write(tuple, sec)
- rescue
- end
- end
- end
-
- ##
- # Creates a thread that notifies waiting clients from the TupleSpace.
-
- def reply_service
- Thread.new do
- loop do
- do_reply
- end
- end
- end
-
- ##
- # Pulls lookup tuples out of the TupleSpace and sends their DRb object the
- # address of the local TupleSpace.
-
- def do_reply
- tuple = @ts.take([:lookup_ring, DRbObject])
- Thread.new { tuple[1].call(@ts) rescue nil}
- rescue
- end
-
- end
-
- ##
- # RingFinger is used by RingServer clients to discover the RingServer's
- # TupleSpace. Typically, all a client needs to do is call
- # RingFinger.primary to retrieve the remote TupleSpace, which it can then
- # begin using.
-
- class RingFinger
-
- @@broadcast_list = ['<broadcast>', 'localhost']
-
- @@finger = nil
-
- ##
- # Creates a singleton RingFinger and looks for a RingServer. Returns the
- # created RingFinger.
-
- def self.finger
- unless @@finger
- @@finger = self.new
- @@finger.lookup_ring_any
- end
- @@finger
- end
-
- ##
- # Returns the first advertised TupleSpace.
-
- def self.primary
- finger.primary
- end
-
- ##
- # Contains all discovered TupleSpaces except for the primary.
-
- def self.to_a
- finger.to_a
- end
-
- ##
- # The list of addresses where RingFinger will send query packets.
-
- attr_accessor :broadcast_list
-
- ##
- # The port that RingFinger will send query packets to.
-
- attr_accessor :port
-
- ##
- # Contain the first advertised TupleSpace after lookup_ring_any is called.
-
- attr_accessor :primary
-
- ##
- # Creates a new RingFinger that will look for RingServers at +port+ on
- # the addresses in +broadcast_list+.
-
- def initialize(broadcast_list=@@broadcast_list, port=Ring_PORT)
- @broadcast_list = broadcast_list || ['localhost']
- @port = port
- @primary = nil
- @rings = []
- end
-
- ##
- # Contains all discovered TupleSpaces except for the primary.
-
- def to_a
- @rings
- end
-
- ##
- # Iterates over all discovered TupleSpaces starting with the primary.
-
- def each
- lookup_ring_any unless @primary
- return unless @primary
- yield(@primary)
- @rings.each { |x| yield(x) }
- end
-
- ##
- # Looks up RingServers waiting +timeout+ seconds. RingServers will be
- # given +block+ as a callback, which will be called with the remote
- # TupleSpace.
-
- def lookup_ring(timeout=5, &block)
- return lookup_ring_any(timeout) unless block_given?
-
- msg = Marshal.dump([[:lookup_ring, DRbObject.new(block)], timeout])
- @broadcast_list.each do |it|
- soc = UDPSocket.open
- begin
- soc.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
- soc.send(msg, 0, it, @port)
- rescue
- nil
- ensure
- soc.close
- end
- end
- sleep(timeout)
- end
-
- ##
- # Returns the first found remote TupleSpace. Any further recovered
- # TupleSpaces can be found by calling +to_a+.
-
- def lookup_ring_any(timeout=5)
- queue = Queue.new
-
- th = Thread.new do
- self.lookup_ring(timeout) do |ts|
- queue.push(ts)
- end
- queue.push(nil)
- while it = queue.pop
- @rings.push(it)
- end
- end
-
- @primary = queue.pop
- raise('RingNotFound') if @primary.nil?
- @primary
- end
-
- end
-
- ##
- # RingProvider uses a RingServer advertised TupleSpace as a name service.
- # TupleSpace clients can register themselves with the remote TupleSpace and
- # look up other provided services via the remote TupleSpace.
- #
- # Services are registered with a tuple of the format [:name, klass,
- # DRbObject, description].
-
- class RingProvider
-
- ##
- # Creates a RingProvider that will provide a +klass+ service running on
- # +front+, with a +description+. +renewer+ is optional.
-
- def initialize(klass, front, desc, renewer = nil)
- @tuple = [:name, klass, front, desc]
- @renewer = renewer || Rinda::SimpleRenewer.new
- end
-
- ##
- # Advertises this service on the primary remote TupleSpace.
-
- def provide
- ts = Rinda::RingFinger.primary
- ts.write(@tuple, @renewer)
- end
-
- end
-
-end
-
-if __FILE__ == $0
- DRb.start_service
- case ARGV.shift
- when 's'
- require 'rinda/tuplespace'
- ts = Rinda::TupleSpace.new
- place = Rinda::RingServer.new(ts)
- $stdin.gets
- when 'w'
- finger = Rinda::RingFinger.new(nil)
- finger.lookup_ring do |ts2|
- p ts2
- ts2.write([:hello, :world])
- end
- when 'r'
- finger = Rinda::RingFinger.new(nil)
- finger.lookup_ring do |ts2|
- p ts2
- p ts2.take([nil, nil])
- end
- end
-end
-
diff --git a/lib/rinda/tuplespace.rb b/lib/rinda/tuplespace.rb
deleted file mode 100644
index 6ca30a7b4b..0000000000
--- a/lib/rinda/tuplespace.rb
+++ /dev/null
@@ -1,642 +0,0 @@
-require 'monitor'
-require 'thread'
-require 'drb/drb'
-require 'rinda/rinda'
-require 'enumerator'
-require 'forwardable'
-
-module Rinda
-
- ##
- # A TupleEntry is a Tuple (i.e. a possible entry in some Tuplespace)
- # together with expiry and cancellation data.
-
- class TupleEntry
-
- include DRbUndumped
-
- attr_accessor :expires
-
- ##
- # Creates a TupleEntry based on +ary+ with an optional renewer or expiry
- # time +sec+.
- #
- # A renewer must implement the +renew+ method which returns a Numeric,
- # nil, or true to indicate when the tuple has expired.
-
- def initialize(ary, sec=nil)
- @cancel = false
- @expires = nil
- @tuple = make_tuple(ary)
- @renewer = nil
- renew(sec)
- end
-
- ##
- # Marks this TupleEntry as canceled.
-
- def cancel
- @cancel = true
- end
-
- ##
- # A TupleEntry is dead when it is canceled or expired.
-
- def alive?
- !canceled? && !expired?
- end
-
- ##
- # Return the object which makes up the tuple itself: the Array
- # or Hash.
-
- def value; @tuple.value; end
-
- ##
- # Returns the canceled status.
-
- def canceled?; @cancel; end
-
- ##
- # Has this tuple expired? (true/false).
- #
- # A tuple has expired when its expiry timer based on the +sec+ argument to
- # #initialize runs out.
-
- def expired?
- return true unless @expires
- return false if @expires > Time.now
- return true if @renewer.nil?
- renew(@renewer)
- return true unless @expires
- return @expires < Time.now
- end
-
- ##
- # Reset the expiry time according to +sec_or_renewer+.
- #
- # +nil+:: it is set to expire in the far future.
- # +false+:: it has expired.
- # Numeric:: it will expire in that many seconds.
- #
- # Otherwise the argument refers to some kind of renewer object
- # which will reset its expiry time.
-
- def renew(sec_or_renewer)
- sec, @renewer = get_renewer(sec_or_renewer)
- @expires = make_expires(sec)
- end
-
- ##
- # Returns an expiry Time based on +sec+ which can be one of:
- # Numeric:: +sec+ seconds into the future
- # +true+:: the expiry time is the start of 1970 (i.e. expired)
- # +nil+:: it is Tue Jan 19 03:14:07 GMT Standard Time 2038 (i.e. when
- # UNIX clocks will die)
-
- def make_expires(sec=nil)
- case sec
- when Numeric
- Time.now + sec
- when true
- Time.at(1)
- when nil
- Time.at(2**31-1)
- end
- end
-
- ##
- # Retrieves +key+ from the tuple.
-
- def [](key)
- @tuple[key]
- end
-
- ##
- # Fetches +key+ from the tuple.
-
- def fetch(key)
- @tuple.fetch(key)
- end
-
- ##
- # The size of the tuple.
-
- def size
- @tuple.size
- end
-
- ##
- # Creates a Rinda::Tuple for +ary+.
-
- def make_tuple(ary)
- Rinda::Tuple.new(ary)
- end
-
- private
-
- ##
- # Returns a valid argument to make_expires and the renewer or nil.
- #
- # Given +true+, +nil+, or Numeric, returns that value and +nil+ (no actual
- # renewer). Otherwise it returns an expiry value from calling +it.renew+
- # and the renewer.
-
- def get_renewer(it)
- case it
- when Numeric, true, nil
- return it, nil
- else
- begin
- return it.renew, it
- rescue Exception
- return it, nil
- end
- end
- end
-
- end
-
- ##
- # A TemplateEntry is a Template together with expiry and cancellation data.
-
- class TemplateEntry < TupleEntry
- ##
- # Matches this TemplateEntry against +tuple+. See Template#match for
- # details on how a Template matches a Tuple.
-
- def match(tuple)
- @tuple.match(tuple)
- end
-
- alias === match
-
- def make_tuple(ary) # :nodoc:
- Rinda::Template.new(ary)
- end
-
- end
-
- ##
- # <i>Documentation?</i>
-
- class WaitTemplateEntry < TemplateEntry
-
- attr_reader :found
-
- def initialize(place, ary, expires=nil)
- super(ary, expires)
- @place = place
- @cond = place.new_cond
- @found = nil
- end
-
- def cancel
- super
- signal
- end
-
- def wait
- @cond.wait
- end
-
- def read(tuple)
- @found = tuple
- signal
- end
-
- def signal
- @place.synchronize do
- @cond.signal
- end
- end
-
- end
-
- ##
- # A NotifyTemplateEntry is returned by TupleSpace#notify and is notified of
- # TupleSpace changes. You may receive either your subscribed event or the
- # 'close' event when iterating over notifications.
- #
- # See TupleSpace#notify_event for valid notification types.
- #
- # == Example
- #
- # ts = Rinda::TupleSpace.new
- # observer = ts.notify 'write', [nil]
- #
- # Thread.start do
- # observer.each { |t| p t }
- # end
- #
- # 3.times { |i| ts.write [i] }
- #
- # Outputs:
- #
- # ['write', [0]]
- # ['write', [1]]
- # ['write', [2]]
-
- class NotifyTemplateEntry < TemplateEntry
-
- ##
- # Creates a new NotifyTemplateEntry that watches +place+ for +event+s that
- # match +tuple+.
-
- def initialize(place, event, tuple, expires=nil)
- ary = [event, Rinda::Template.new(tuple)]
- super(ary, expires)
- @queue = Queue.new
- @done = false
- end
-
- ##
- # Called by TupleSpace to notify this NotifyTemplateEntry of a new event.
-
- def notify(ev)
- @queue.push(ev)
- end
-
- ##
- # Retrieves a notification. Raises RequestExpiredError when this
- # NotifyTemplateEntry expires.
-
- def pop
- raise RequestExpiredError if @done
- it = @queue.pop
- @done = true if it[0] == 'close'
- return it
- end
-
- ##
- # Yields event/tuple pairs until this NotifyTemplateEntry expires.
-
- def each # :yields: event, tuple
- while !@done
- it = pop
- yield(it)
- end
- rescue
- ensure
- cancel
- end
-
- end
-
- ##
- # TupleBag is an unordered collection of tuples. It is the basis
- # of Tuplespace.
-
- class TupleBag
- class TupleBin
- extend Forwardable
- def_delegators '@bin', :find_all, :delete_if, :each, :empty?
-
- def initialize
- @bin = []
- end
-
- def add(tuple)
- @bin.push(tuple)
- end
-
- def delete(tuple)
- idx = @bin.rindex(tuple)
- @bin.delete_at(idx) if idx
- end
-
- def find(&blk)
- @bin.reverse_each do |x|
- return x if yield(x)
- end
- nil
- end
- end
-
- def initialize # :nodoc:
- @hash = {}
- @enum = enum_for(:each_entry)
- end
-
- ##
- # +true+ if the TupleBag to see if it has any expired entries.
-
- def has_expires?
- @enum.find do |tuple|
- tuple.expires
- end
- end
-
- ##
- # Add +tuple+ to the TupleBag.
-
- def push(tuple)
- key = bin_key(tuple)
- @hash[key] ||= TupleBin.new
- @hash[key].add(tuple)
- end
-
- ##
- # Removes +tuple+ from the TupleBag.
-
- def delete(tuple)
- key = bin_key(tuple)
- bin = @hash[key]
- return nil unless bin
- bin.delete(tuple)
- @hash.delete(key) if bin.empty?
- tuple
- end
-
- ##
- # Finds all live tuples that match +template+.
- def find_all(template)
- bin_for_find(template).find_all do |tuple|
- tuple.alive? && template.match(tuple)
- end
- end
-
- ##
- # Finds a live tuple that matches +template+.
-
- def find(template)
- bin_for_find(template).find do |tuple|
- tuple.alive? && template.match(tuple)
- end
- end
-
- ##
- # Finds all tuples in the TupleBag which when treated as templates, match
- # +tuple+ and are alive.
-
- def find_all_template(tuple)
- @enum.find_all do |template|
- template.alive? && template.match(tuple)
- end
- end
-
- ##
- # Delete tuples which dead tuples from the TupleBag, returning the deleted
- # tuples.
-
- def delete_unless_alive
- deleted = []
- @hash.each do |key, bin|
- bin.delete_if do |tuple|
- if tuple.alive?
- false
- else
- deleted.push(tuple)
- true
- end
- end
- end
- deleted
- end
-
- private
- def each_entry(&blk)
- @hash.each do |k, v|
- v.each(&blk)
- end
- end
-
- def bin_key(tuple)
- head = tuple[0]
- if head.class == Symbol
- return head
- else
- false
- end
- end
-
- def bin_for_find(template)
- key = bin_key(template)
- key ? @hash.fetch(key, []) : @enum
- end
- end
-
- ##
- # The Tuplespace manages access to the tuples it contains,
- # ensuring mutual exclusion requirements are met.
- #
- # The +sec+ option for the write, take, move, read and notify methods may
- # either be a number of seconds or a Renewer object.
-
- class TupleSpace
-
- include DRbUndumped
- include MonitorMixin
-
- ##
- # Creates a new TupleSpace. +period+ is used to control how often to look
- # for dead tuples after modifications to the TupleSpace.
- #
- # If no dead tuples are found +period+ seconds after the last
- # modification, the TupleSpace will stop looking for dead tuples.
-
- def initialize(period=60)
- super()
- @bag = TupleBag.new
- @read_waiter = TupleBag.new
- @take_waiter = TupleBag.new
- @notify_waiter = TupleBag.new
- @period = period
- @keeper = nil
- end
-
- ##
- # Adds +tuple+
-
- def write(tuple, sec=nil)
- entry = create_entry(tuple, sec)
- synchronize do
- if entry.expired?
- @read_waiter.find_all_template(entry).each do |template|
- template.read(tuple)
- end
- notify_event('write', entry.value)
- notify_event('delete', entry.value)
- else
- @bag.push(entry)
- start_keeper if entry.expires
- @read_waiter.find_all_template(entry).each do |template|
- template.read(tuple)
- end
- @take_waiter.find_all_template(entry).each do |template|
- template.signal
- end
- notify_event('write', entry.value)
- end
- end
- entry
- end
-
- ##
- # Removes +tuple+
-
- def take(tuple, sec=nil, &block)
- move(nil, tuple, sec, &block)
- end
-
- ##
- # Moves +tuple+ to +port+.
-
- def move(port, tuple, sec=nil)
- template = WaitTemplateEntry.new(self, tuple, sec)
- yield(template) if block_given?
- synchronize do
- entry = @bag.find(template)
- if entry
- port.push(entry.value) if port
- @bag.delete(entry)
- notify_event('take', entry.value)
- return entry.value
- end
- raise RequestExpiredError if template.expired?
-
- begin
- @take_waiter.push(template)
- start_keeper if template.expires
- while true
- raise RequestCanceledError if template.canceled?
- raise RequestExpiredError if template.expired?
- entry = @bag.find(template)
- if entry
- port.push(entry.value) if port
- @bag.delete(entry)
- notify_event('take', entry.value)
- return entry.value
- end
- template.wait
- end
- ensure
- @take_waiter.delete(template)
- end
- end
- end
-
- ##
- # Reads +tuple+, but does not remove it.
-
- def read(tuple, sec=nil)
- template = WaitTemplateEntry.new(self, tuple, sec)
- yield(template) if block_given?
- synchronize do
- entry = @bag.find(template)
- return entry.value if entry
- raise RequestExpiredError if template.expired?
-
- begin
- @read_waiter.push(template)
- start_keeper if template.expires
- template.wait
- raise RequestCanceledError if template.canceled?
- raise RequestExpiredError if template.expired?
- return template.found
- ensure
- @read_waiter.delete(template)
- end
- end
- end
-
- ##
- # Returns all tuples matching +tuple+. Does not remove the found tuples.
-
- def read_all(tuple)
- template = WaitTemplateEntry.new(self, tuple, nil)
- synchronize do
- entry = @bag.find_all(template)
- entry.collect do |e|
- e.value
- end
- end
- end
-
- ##
- # Registers for notifications of +event+. Returns a NotifyTemplateEntry.
- # See NotifyTemplateEntry for examples of how to listen for notifications.
- #
- # +event+ can be:
- # 'write':: A tuple was added
- # 'take':: A tuple was taken or moved
- # 'delete':: A tuple was lost after being overwritten or expiring
- #
- # The TupleSpace will also notify you of the 'close' event when the
- # NotifyTemplateEntry has expired.
-
- def notify(event, tuple, sec=nil)
- template = NotifyTemplateEntry.new(self, event, tuple, sec)
- synchronize do
- @notify_waiter.push(template)
- end
- template
- end
-
- private
-
- def create_entry(tuple, sec)
- TupleEntry.new(tuple, sec)
- end
-
- ##
- # Removes dead tuples.
-
- def keep_clean
- synchronize do
- @read_waiter.delete_unless_alive.each do |e|
- e.signal
- end
- @take_waiter.delete_unless_alive.each do |e|
- e.signal
- end
- @notify_waiter.delete_unless_alive.each do |e|
- e.notify(['close'])
- end
- @bag.delete_unless_alive.each do |e|
- notify_event('delete', e.value)
- end
- end
- end
-
- ##
- # Notifies all registered listeners for +event+ of a status change of
- # +tuple+.
-
- def notify_event(event, tuple)
- ev = [event, tuple]
- @notify_waiter.find_all_template(ev).each do |template|
- template.notify(ev)
- end
- end
-
- ##
- # Creates a thread that scans the tuplespace for expired tuples.
-
- def start_keeper
- return if @keeper && @keeper.alive?
- @keeper = Thread.new do
- while true
- sleep(@period)
- synchronize do
- break unless need_keeper?
- keep_clean
- end
- end
- end
- end
-
- ##
- # Checks the tuplespace to see if it needs cleaning.
-
- def need_keeper?
- return true if @bag.has_expires?
- return true if @read_waiter.has_expires?
- return true if @take_waiter.has_expires?
- return true if @notify_waiter.has_expires?
- end
-
- end
-
-end
-
diff --git a/lib/rss.rb b/lib/rss.rb
deleted file mode 100644
index a1d0f76ba1..0000000000
--- a/lib/rss.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2003-2007 Kouhei Sutou. You can redistribute it and/or
-# modify it under the same terms as Ruby.
-#
-# Author:: Kouhei Sutou <kou@cozmixng.org>
-# Tutorial:: http://www.cozmixng.org/~rwiki/?cmd=view;name=RSS+Parser%3A%3ATutorial.en
-
-require 'rss/1.0'
-require 'rss/2.0'
-require 'rss/atom'
-require 'rss/content'
-require 'rss/dublincore'
-require 'rss/image'
-require 'rss/itunes'
-require 'rss/slash'
-require 'rss/syndication'
-require 'rss/taxonomy'
-require 'rss/trackback'
-
-require "rss/maker"
diff --git a/lib/rss/0.9.rb b/lib/rss/0.9.rb
deleted file mode 100644
index 7b24e7596d..0000000000
--- a/lib/rss/0.9.rb
+++ /dev/null
@@ -1,428 +0,0 @@
-require "rss/parser"
-
-module RSS
-
- module RSS09
- NSPOOL = {}
- ELEMENTS = []
-
- def self.append_features(klass)
- super
-
- klass.install_must_call_validator('', "")
- end
- end
-
- class Rss < Element
-
- include RSS09
- include RootElementMixin
-
- %w(channel).each do |name|
- install_have_child_element(name, "", nil)
- end
-
- attr_writer :feed_version
- alias_method(:rss_version, :feed_version)
- alias_method(:rss_version=, :feed_version=)
-
- def initialize(feed_version, version=nil, encoding=nil, standalone=nil)
- super
- @feed_type = "rss"
- end
-
- def items
- if @channel
- @channel.items
- else
- []
- end
- end
-
- def image
- if @channel
- @channel.image
- else
- nil
- end
- end
-
- def textinput
- if @channel
- @channel.textInput
- else
- nil
- end
- end
-
- def setup_maker_elements(maker)
- super
- items.each do |item|
- item.setup_maker(maker.items)
- end
- image.setup_maker(maker) if image
- textinput.setup_maker(maker) if textinput
- end
-
- private
- def _attrs
- [
- ["version", true, "feed_version"],
- ]
- end
-
- class Channel < Element
-
- include RSS09
-
- [
- ["title", nil, :text],
- ["link", nil, :text],
- ["description", nil, :text],
- ["language", nil, :text],
- ["copyright", "?", :text],
- ["managingEditor", "?", :text],
- ["webMaster", "?", :text],
- ["rating", "?", :text],
- ["pubDate", "?", :date, :rfc822],
- ["lastBuildDate", "?", :date, :rfc822],
- ["docs", "?", :text],
- ["cloud", "?", :have_attribute],
- ["skipDays", "?", :have_child],
- ["skipHours", "?", :have_child],
- ["image", nil, :have_child],
- ["item", "*", :have_children],
- ["textInput", "?", :have_child],
- ].each do |name, occurs, type, *args|
- __send__("install_#{type}_element", name, "", occurs, name, *args)
- end
- alias date pubDate
- alias date= pubDate=
-
- private
- def maker_target(maker)
- maker.channel
- end
-
- def setup_maker_elements(channel)
- super
- [
- [skipDays, "day"],
- [skipHours, "hour"],
- ].each do |skip, key|
- if skip
- skip.__send__("#{key}s").each do |val|
- target_skips = channel.__send__("skip#{key.capitalize}s")
- new_target = target_skips.__send__("new_#{key}")
- new_target.content = val.content
- end
- end
- end
- end
-
- def not_need_to_call_setup_maker_variables
- %w(image textInput)
- end
-
- class SkipDays < Element
- include RSS09
-
- [
- ["day", "*"]
- ].each do |name, occurs|
- install_have_children_element(name, "", occurs)
- end
-
- class Day < Element
- include RSS09
-
- content_setup
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.content = args[0]
- end
- end
-
- end
-
- end
-
- class SkipHours < Element
- include RSS09
-
- [
- ["hour", "*"]
- ].each do |name, occurs|
- install_have_children_element(name, "", occurs)
- end
-
- class Hour < Element
- include RSS09
-
- content_setup(:integer)
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.content = args[0]
- end
- end
- end
-
- end
-
- class Image < Element
-
- include RSS09
-
- %w(url title link).each do |name|
- install_text_element(name, "", nil)
- end
- [
- ["width", :integer],
- ["height", :integer],
- ["description"],
- ].each do |name, type|
- install_text_element(name, "", "?", name, type)
- end
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.url = args[0]
- self.title = args[1]
- self.link = args[2]
- self.width = args[3]
- self.height = args[4]
- self.description = args[5]
- end
- end
-
- private
- def maker_target(maker)
- maker.image
- end
- end
-
- class Cloud < Element
-
- include RSS09
-
- [
- ["domain", "", true],
- ["port", "", true, :integer],
- ["path", "", true],
- ["registerProcedure", "", true],
- ["protocol", "", true],
- ].each do |name, uri, required, type|
- install_get_attribute(name, uri, required, type)
- end
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.domain = args[0]
- self.port = args[1]
- self.path = args[2]
- self.registerProcedure = args[3]
- self.protocol = args[4]
- end
- end
- end
-
- class Item < Element
-
- include RSS09
-
- [
- ["title", '?', :text],
- ["link", '?', :text],
- ["description", '?', :text],
- ["category", '*', :have_children, "categories"],
- ["source", '?', :have_child],
- ["enclosure", '?', :have_child],
- ].each do |tag, occurs, type, *args|
- __send__("install_#{type}_element", tag, "", occurs, tag, *args)
- end
-
- private
- def maker_target(items)
- if items.respond_to?("items")
- # For backward compatibility
- items = items.items
- end
- items.new_item
- end
-
- def setup_maker_element(item)
- super
- @enclosure.setup_maker(item) if @enclosure
- @source.setup_maker(item) if @source
- end
-
- class Source < Element
-
- include RSS09
-
- [
- ["url", "", true]
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required)
- end
-
- content_setup
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.url = args[0]
- self.content = args[1]
- end
- end
-
- private
- def maker_target(item)
- item.source
- end
-
- def setup_maker_attributes(source)
- source.url = url
- source.content = content
- end
- end
-
- class Enclosure < Element
-
- include RSS09
-
- [
- ["url", "", true],
- ["length", "", true, :integer],
- ["type", "", true],
- ].each do |name, uri, required, type|
- install_get_attribute(name, uri, required, type)
- end
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.url = args[0]
- self.length = args[1]
- self.type = args[2]
- end
- end
-
- private
- def maker_target(item)
- item.enclosure
- end
-
- def setup_maker_attributes(enclosure)
- enclosure.url = url
- enclosure.length = length
- enclosure.type = type
- end
- end
-
- class Category < Element
-
- include RSS09
-
- [
- ["domain", "", false]
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required)
- end
-
- content_setup
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.domain = args[0]
- self.content = args[1]
- end
- end
-
- private
- def maker_target(item)
- item.new_category
- end
-
- def setup_maker_attributes(category)
- category.domain = domain
- category.content = content
- end
-
- end
-
- end
-
- class TextInput < Element
-
- include RSS09
-
- %w(title description name link).each do |name|
- install_text_element(name, "", nil)
- end
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.title = args[0]
- self.description = args[1]
- self.name = args[2]
- self.link = args[3]
- end
- end
-
- private
- def maker_target(maker)
- maker.textinput
- end
- end
-
- end
-
- end
-
- RSS09::ELEMENTS.each do |name|
- BaseListener.install_get_text_element("", name, name)
- end
-
- module ListenerMixin
- private
- def initial_start_rss(tag_name, prefix, attrs, ns)
- check_ns(tag_name, prefix, ns, "")
-
- @rss = Rss.new(attrs['version'], @version, @encoding, @standalone)
- @rss.do_validate = @do_validate
- @rss.xml_stylesheets = @xml_stylesheets
- @last_element = @rss
- pr = Proc.new do |text, tags|
- @rss.validate_for_stream(tags, @ignore_unknown_element) if @do_validate
- end
- @proc_stack.push(pr)
- end
-
- end
-
-end
diff --git a/lib/rss/1.0.rb b/lib/rss/1.0.rb
deleted file mode 100644
index f04e61c5eb..0000000000
--- a/lib/rss/1.0.rb
+++ /dev/null
@@ -1,452 +0,0 @@
-require "rss/parser"
-
-module RSS
-
- module RSS10
- NSPOOL = {}
- ELEMENTS = []
-
- def self.append_features(klass)
- super
-
- klass.install_must_call_validator('', ::RSS::URI)
- end
-
- end
-
- class RDF < Element
-
- include RSS10
- include RootElementMixin
-
- class << self
-
- def required_uri
- URI
- end
-
- end
-
- @tag_name = 'RDF'
-
- PREFIX = 'rdf'
- URI = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-
- install_ns('', ::RSS::URI)
- install_ns(PREFIX, URI)
-
- [
- ["channel", nil],
- ["image", "?"],
- ["item", "+", :children],
- ["textinput", "?"],
- ].each do |tag, occurs, type|
- type ||= :child
- __send__("install_have_#{type}_element", tag, ::RSS::URI, occurs)
- end
-
- alias_method(:rss_version, :feed_version)
- def initialize(version=nil, encoding=nil, standalone=nil)
- super('1.0', version, encoding, standalone)
- @feed_type = "rss"
- end
-
- def full_name
- tag_name_with_prefix(PREFIX)
- end
-
- class Li < Element
-
- include RSS10
-
- class << self
- def required_uri
- URI
- end
- end
-
- [
- ["resource", [URI, ""], true]
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required)
- end
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.resource = args[0]
- end
- end
-
- def full_name
- tag_name_with_prefix(PREFIX)
- end
- end
-
- class Seq < Element
-
- include RSS10
-
- Li = ::RSS::RDF::Li
-
- class << self
- def required_uri
- URI
- end
- end
-
- @tag_name = 'Seq'
-
- install_have_children_element("li", URI, "*")
- install_must_call_validator('rdf', ::RSS::RDF::URI)
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- @li = args[0] if args[0]
- end
- end
-
- def full_name
- tag_name_with_prefix(PREFIX)
- end
-
- def setup_maker(target)
- lis.each do |li|
- target << li.resource
- end
- end
- end
-
- class Bag < Element
-
- include RSS10
-
- Li = ::RSS::RDF::Li
-
- class << self
- def required_uri
- URI
- end
- end
-
- @tag_name = 'Bag'
-
- install_have_children_element("li", URI, "*")
- install_must_call_validator('rdf', URI)
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- @li = args[0] if args[0]
- end
- end
-
- def full_name
- tag_name_with_prefix(PREFIX)
- end
-
- def setup_maker(target)
- lis.each do |li|
- target << li.resource
- end
- end
- end
-
- class Channel < Element
-
- include RSS10
-
- class << self
-
- def required_uri
- ::RSS::URI
- end
-
- end
-
- [
- ["about", URI, true]
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required, nil, nil,
- "#{PREFIX}:#{name}")
- end
-
- [
- ['title', nil, :text],
- ['link', nil, :text],
- ['description', nil, :text],
- ['image', '?', :have_child],
- ['items', nil, :have_child],
- ['textinput', '?', :have_child],
- ].each do |tag, occurs, type|
- __send__("install_#{type}_element", tag, ::RSS::URI, occurs)
- end
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.about = args[0]
- end
- end
-
- private
- def maker_target(maker)
- maker.channel
- end
-
- def setup_maker_attributes(channel)
- channel.about = about
- end
-
- class Image < Element
-
- include RSS10
-
- class << self
-
- def required_uri
- ::RSS::URI
- end
-
- end
-
- [
- ["resource", URI, true]
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required, nil, nil,
- "#{PREFIX}:#{name}")
- end
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.resource = args[0]
- end
- end
- end
-
- class Textinput < Element
-
- include RSS10
-
- class << self
-
- def required_uri
- ::RSS::URI
- end
-
- end
-
- [
- ["resource", URI, true]
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required, nil, nil,
- "#{PREFIX}:#{name}")
- end
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.resource = args[0]
- end
- end
- end
-
- class Items < Element
-
- include RSS10
-
- Seq = ::RSS::RDF::Seq
-
- class << self
-
- def required_uri
- ::RSS::URI
- end
-
- end
-
- install_have_child_element("Seq", URI, nil)
- install_must_call_validator('rdf', URI)
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.Seq = args[0]
- end
- self.Seq ||= Seq.new
- end
-
- def resources
- if @Seq
- @Seq.lis.collect do |li|
- li.resource
- end
- else
- []
- end
- end
- end
- end
-
- class Image < Element
-
- include RSS10
-
- class << self
-
- def required_uri
- ::RSS::URI
- end
-
- end
-
- [
- ["about", URI, true]
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required, nil, nil,
- "#{PREFIX}:#{name}")
- end
-
- %w(title url link).each do |name|
- install_text_element(name, ::RSS::URI, nil)
- end
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.about = args[0]
- end
- end
-
- private
- def maker_target(maker)
- maker.image
- end
- end
-
- class Item < Element
-
- include RSS10
-
- class << self
-
- def required_uri
- ::RSS::URI
- end
-
- end
-
-
- [
- ["about", URI, true]
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required, nil, nil,
- "#{PREFIX}:#{name}")
- end
-
- [
- ["title", nil],
- ["link", nil],
- ["description", "?"],
- ].each do |tag, occurs|
- install_text_element(tag, ::RSS::URI, occurs)
- end
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.about = args[0]
- end
- end
-
- private
- def maker_target(items)
- if items.respond_to?("items")
- # For backward compatibility
- items = items.items
- end
- items.new_item
- end
- end
-
- class Textinput < Element
-
- include RSS10
-
- class << self
-
- def required_uri
- ::RSS::URI
- end
-
- end
-
- [
- ["about", URI, true]
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required, nil, nil,
- "#{PREFIX}:#{name}")
- end
-
- %w(title description name link).each do |name|
- install_text_element(name, ::RSS::URI, nil)
- end
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.about = args[0]
- end
- end
-
- private
- def maker_target(maker)
- maker.textinput
- end
- end
-
- end
-
- RSS10::ELEMENTS.each do |name|
- BaseListener.install_get_text_element(URI, name, name)
- end
-
- module ListenerMixin
- private
- def initial_start_RDF(tag_name, prefix, attrs, ns)
- check_ns(tag_name, prefix, ns, RDF::URI)
-
- @rss = RDF.new(@version, @encoding, @standalone)
- @rss.do_validate = @do_validate
- @rss.xml_stylesheets = @xml_stylesheets
- @last_element = @rss
- pr = Proc.new do |text, tags|
- @rss.validate_for_stream(tags, @ignore_unknown_element) if @do_validate
- end
- @proc_stack.push(pr)
- end
- end
-
-end
diff --git a/lib/rss/2.0.rb b/lib/rss/2.0.rb
deleted file mode 100644
index 3798da4eb7..0000000000
--- a/lib/rss/2.0.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-require "rss/0.9"
-
-module RSS
-
- class Rss
-
- class Channel
-
- [
- ["generator"],
- ["ttl", :integer],
- ].each do |name, type|
- install_text_element(name, "", "?", name, type)
- end
-
- [
- %w(category categories),
- ].each do |name, plural_name|
- install_have_children_element(name, "", "*", name, plural_name)
- end
-
- [
- ["image", "?"],
- ["language", "?"],
- ].each do |name, occurs|
- install_model(name, "", occurs)
- end
-
- Category = Item::Category
-
- class Item
-
- [
- ["comments", "?"],
- ["author", "?"],
- ].each do |name, occurs|
- install_text_element(name, "", occurs)
- end
-
- [
- ["pubDate", '?'],
- ].each do |name, occurs|
- install_date_element(name, "", occurs, name, 'rfc822')
- end
- alias date pubDate
- alias date= pubDate=
-
- [
- ["guid", '?'],
- ].each do |name, occurs|
- install_have_child_element(name, "", occurs)
- end
-
- private
- alias _setup_maker_element setup_maker_element
- def setup_maker_element(item)
- _setup_maker_element(item)
- @guid.setup_maker(item) if @guid
- end
-
- class Guid < Element
-
- include RSS09
-
- [
- ["isPermaLink", "", false, :boolean]
- ].each do |name, uri, required, type|
- install_get_attribute(name, uri, required, type)
- end
-
- content_setup
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.isPermaLink = args[0]
- self.content = args[1]
- end
- end
-
- alias_method :_PermaLink?, :PermaLink?
- private :_PermaLink?
- def PermaLink?
- perma = _PermaLink?
- perma or perma.nil?
- end
-
- private
- def maker_target(item)
- item.guid
- end
-
- def setup_maker_attributes(guid)
- guid.isPermaLink = isPermaLink
- guid.content = content
- end
- end
-
- end
-
- end
-
- end
-
- RSS09::ELEMENTS.each do |name|
- BaseListener.install_get_text_element("", name, name)
- end
-
-end
diff --git a/lib/rss/atom.rb b/lib/rss/atom.rb
deleted file mode 100644
index 10282a8743..0000000000
--- a/lib/rss/atom.rb
+++ /dev/null
@@ -1,748 +0,0 @@
-require 'rss/parser'
-
-module RSS
- module Atom
- URI = "http://www.w3.org/2005/Atom"
- XHTML_URI = "http://www.w3.org/1999/xhtml"
-
- module CommonModel
- NSPOOL = {}
- ELEMENTS = []
-
- def self.append_features(klass)
- super
- klass.install_must_call_validator("atom", URI)
- [
- ["lang", :xml],
- ["base", :xml],
- ].each do |name, uri, required|
- klass.install_get_attribute(name, uri, required, [nil, :inherit])
- end
- klass.class_eval do
- class << self
- def required_uri
- URI
- end
-
- def need_parent?
- true
- end
- end
- end
- end
- end
-
- module ContentModel
- module ClassMethods
- def content_type
- @content_type ||= nil
- end
- end
-
- class << self
- def append_features(klass)
- super
- klass.extend(ClassMethods)
- klass.content_setup(klass.content_type, klass.tag_name)
- end
- end
-
- def maker_target(target)
- target
- end
-
- private
- def setup_maker_element_writer
- "#{self.class.name.split(/::/).last.downcase}="
- end
-
- def setup_maker_element(target)
- target.__send__(setup_maker_element_writer, content)
- super
- end
- end
-
- module URIContentModel
- class << self
- def append_features(klass)
- super
- klass.class_eval do
- @content_type = [nil, :uri]
- include(ContentModel)
- end
- end
- end
- end
-
- module TextConstruct
- def self.append_features(klass)
- super
- klass.class_eval do
- [
- ["type", ""],
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required, :text_type)
- end
-
- content_setup
- add_need_initialize_variable("xhtml")
-
- class << self
- def xml_getter
- "xhtml"
- end
-
- def xml_setter
- "xhtml="
- end
- end
- end
- end
-
- attr_writer :xhtml
- def xhtml
- return @xhtml if @xhtml.nil?
- if @xhtml.is_a?(XML::Element) and
- [@xhtml.name, @xhtml.uri] == ["div", XHTML_URI]
- return @xhtml
- end
-
- children = @xhtml
- children = [children] unless children.is_a?(Array)
- XML::Element.new("div", nil, XHTML_URI,
- {"xmlns" => XHTML_URI}, children)
- end
-
- def have_xml_content?
- @type == "xhtml"
- end
-
- def atom_validate(ignore_unknown_element, tags, uri)
- if have_xml_content?
- if @xhtml.nil?
- raise MissingTagError.new("div", tag_name)
- end
- unless [@xhtml.name, @xhtml.uri] == ["div", XHTML_URI]
- raise NotExpectedTagError.new(@xhtml.name, @xhtml.uri, tag_name)
- end
- end
- end
-
- private
- def maker_target(target)
- target.__send__(self.class.name.split(/::/).last.downcase) {|x| x}
- end
-
- def setup_maker_attributes(target)
- target.type = type
- target.content = content
- target.xml_content = @xhtml
- end
- end
-
- module PersonConstruct
- def self.append_features(klass)
- super
- klass.class_eval do
- [
- ["name", nil],
- ["uri", "?"],
- ["email", "?"],
- ].each do |tag, occurs|
- install_have_attribute_element(tag, URI, occurs, nil, :content)
- end
- end
- end
-
- def maker_target(target)
- target.__send__("new_#{self.class.name.split(/::/).last.downcase}")
- end
-
- class Name < RSS::Element
- include CommonModel
- include ContentModel
- end
-
- class Uri < RSS::Element
- include CommonModel
- include URIContentModel
- end
-
- class Email < RSS::Element
- include CommonModel
- include ContentModel
- end
- end
-
- module DateConstruct
- def self.append_features(klass)
- super
- klass.class_eval do
- @content_type = :w3cdtf
- include(ContentModel)
- end
- end
-
- def atom_validate(ignore_unknown_element, tags, uri)
- raise NotAvailableValueError.new(tag_name, "") if content.nil?
- end
- end
-
- module DuplicateLinkChecker
- def validate_duplicate_links(links)
- link_infos = {}
- links.each do |link|
- rel = link.rel || "alternate"
- next unless rel == "alternate"
- key = [link.hreflang, link.type]
- if link_infos.has_key?(key)
- raise TooMuchTagError.new("link", tag_name)
- end
- link_infos[key] = true
- end
- end
- end
-
- class Feed < RSS::Element
- include RootElementMixin
- include CommonModel
- include DuplicateLinkChecker
-
- install_ns('', URI)
-
- [
- ["author", "*", :children],
- ["category", "*", :children, "categories"],
- ["contributor", "*", :children],
- ["generator", "?"],
- ["icon", "?", nil, :content],
- ["id", nil, nil, :content],
- ["link", "*", :children],
- ["logo", "?"],
- ["rights", "?"],
- ["subtitle", "?", nil, :content],
- ["title", nil, nil, :content],
- ["updated", nil, nil, :content],
- ["entry", "*", :children, "entries"],
- ].each do |tag, occurs, type, *args|
- type ||= :child
- __send__("install_have_#{type}_element",
- tag, URI, occurs, tag, *args)
- end
-
- def initialize(version=nil, encoding=nil, standalone=nil)
- super("1.0", version, encoding, standalone)
- @feed_type = "atom"
- @feed_subtype = "feed"
- end
-
- alias_method :items, :entries
-
- def have_author?
- authors.any? {|author| !author.to_s.empty?} or
- entries.any? {|entry| entry.have_author?(false)}
- end
-
- private
- def atom_validate(ignore_unknown_element, tags, uri)
- unless have_author?
- raise MissingTagError.new("author", tag_name)
- end
- validate_duplicate_links(links)
- end
-
- def have_required_elements?
- super and have_author?
- end
-
- def maker_target(maker)
- maker.channel
- end
-
- def setup_maker_element(channel)
- prev_dc_dates = channel.dc_dates.to_a.dup
- super
- channel.about = id.content if id
- channel.dc_dates.replace(prev_dc_dates)
- end
-
- def setup_maker_elements(channel)
- super
- items = channel.maker.items
- entries.each do |entry|
- entry.setup_maker(items)
- end
- end
-
- class Author < RSS::Element
- include CommonModel
- include PersonConstruct
- end
-
- class Category < RSS::Element
- include CommonModel
-
- [
- ["term", "", true],
- ["scheme", "", false, [nil, :uri]],
- ["label", ""],
- ].each do |name, uri, required, type|
- install_get_attribute(name, uri, required, type)
- end
-
- private
- def maker_target(target)
- target.new_category
- end
- end
-
- class Contributor < RSS::Element
- include CommonModel
- include PersonConstruct
- end
-
- class Generator < RSS::Element
- include CommonModel
- include ContentModel
-
- [
- ["uri", "", false, [nil, :uri]],
- ["version", ""],
- ].each do |name, uri, required, type|
- install_get_attribute(name, uri, required, type)
- end
-
- private
- def setup_maker_attributes(target)
- target.generator do |generator|
- generator.uri = uri if uri
- generator.version = version if version
- end
- end
- end
-
- class Icon < RSS::Element
- include CommonModel
- include URIContentModel
- end
-
- class Id < RSS::Element
- include CommonModel
- include URIContentModel
- end
-
- class Link < RSS::Element
- include CommonModel
-
- [
- ["href", "", true, [nil, :uri]],
- ["rel", ""],
- ["type", ""],
- ["hreflang", ""],
- ["title", ""],
- ["length", ""],
- ].each do |name, uri, required, type|
- install_get_attribute(name, uri, required, type)
- end
-
- private
- def maker_target(target)
- target.new_link
- end
- end
-
- class Logo < RSS::Element
- include CommonModel
- include URIContentModel
-
- def maker_target(target)
- target.maker.image
- end
-
- private
- def setup_maker_element_writer
- "url="
- end
- end
-
- class Rights < RSS::Element
- include CommonModel
- include TextConstruct
- end
-
- class Subtitle < RSS::Element
- include CommonModel
- include TextConstruct
- end
-
- class Title < RSS::Element
- include CommonModel
- include TextConstruct
- end
-
- class Updated < RSS::Element
- include CommonModel
- include DateConstruct
- end
-
- class Entry < RSS::Element
- include CommonModel
- include DuplicateLinkChecker
-
- [
- ["author", "*", :children],
- ["category", "*", :children, "categories"],
- ["content", "?", :child],
- ["contributor", "*", :children],
- ["id", nil, nil, :content],
- ["link", "*", :children],
- ["published", "?", :child, :content],
- ["rights", "?", :child],
- ["source", "?"],
- ["summary", "?", :child],
- ["title", nil],
- ["updated", nil, :child, :content],
- ].each do |tag, occurs, type, *args|
- type ||= :attribute
- __send__("install_have_#{type}_element",
- tag, URI, occurs, tag, *args)
- end
-
- def have_author?(check_parent=true)
- authors.any? {|author| !author.to_s.empty?} or
- (check_parent and @parent and @parent.have_author?) or
- (source and source.have_author?)
- end
-
- private
- def atom_validate(ignore_unknown_element, tags, uri)
- unless have_author?
- raise MissingTagError.new("author", tag_name)
- end
- validate_duplicate_links(links)
- end
-
- def have_required_elements?
- super and have_author?
- end
-
- def maker_target(items)
- if items.respond_to?("items")
- # For backward compatibility
- items = items.items
- end
- items.new_item
- end
-
- Author = Feed::Author
- Category = Feed::Category
-
- class Content < RSS::Element
- include CommonModel
-
- class << self
- def xml_setter
- "xml="
- end
-
- def xml_getter
- "xml"
- end
- end
-
- [
- ["type", ""],
- ["src", "", false, [nil, :uri]],
- ].each do |name, uri, required, type|
- install_get_attribute(name, uri, required, type)
- end
-
- content_setup
- add_need_initialize_variable("xml")
-
- attr_writer :xml
- def have_xml_content?
- inline_xhtml? or inline_other_xml?
- end
-
- def xml
- return @xml unless inline_xhtml?
- return @xml if @xml.nil?
- if @xml.is_a?(XML::Element) and
- [@xml.name, @xml.uri] == ["div", XHTML_URI]
- return @xml
- end
-
- children = @xml
- children = [children] unless children.is_a?(Array)
- XML::Element.new("div", nil, XHTML_URI,
- {"xmlns" => XHTML_URI}, children)
- end
-
- def xhtml
- if inline_xhtml?
- xml
- else
- nil
- end
- end
-
- def atom_validate(ignore_unknown_element, tags, uri)
- if out_of_line?
- raise MissingAttributeError.new(tag_name, "type") if @type.nil?
- unless (content.nil? or content.empty?)
- raise NotAvailableValueError.new(tag_name, content)
- end
- elsif inline_xhtml?
- if @xml.nil?
- raise MissingTagError.new("div", tag_name)
- end
- unless @xml.name == "div" and @xml.uri == XHTML_URI
- raise NotExpectedTagError.new(@xml.name, @xml.uri, tag_name)
- end
- end
- end
-
- def inline_text?
- !out_of_line? and [nil, "text", "html"].include?(@type)
- end
-
- def inline_html?
- return false if out_of_line?
- @type == "html" or mime_split == ["text", "html"]
- end
-
- def inline_xhtml?
- !out_of_line? and @type == "xhtml"
- end
-
- def inline_other?
- return false if out_of_line?
- media_type, subtype = mime_split
- return false if media_type.nil? or subtype.nil?
- true
- end
-
- def inline_other_text?
- return false unless inline_other?
- return false if inline_other_xml?
-
- media_type, subtype = mime_split
- return true if "text" == media_type.downcase
- false
- end
-
- def inline_other_xml?
- return false unless inline_other?
-
- media_type, subtype = mime_split
- normalized_mime_type = "#{media_type}/#{subtype}".downcase
- if /(?:\+xml|^xml)$/ =~ subtype or
- %w(text/xml-external-parsed-entity
- application/xml-external-parsed-entity
- application/xml-dtd).find {|x| x == normalized_mime_type}
- return true
- end
- false
- end
-
- def inline_other_base64?
- inline_other? and !inline_other_text? and !inline_other_xml?
- end
-
- def out_of_line?
- not @src.nil?
- end
-
- def mime_split
- media_type = subtype = nil
- if /\A\s*([a-z]+)\/([a-z\+]+)\s*(?:;.*)?\z/i =~ @type.to_s
- media_type = $1.downcase
- subtype = $2.downcase
- end
- [media_type, subtype]
- end
-
- def need_base64_encode?
- inline_other_base64?
- end
-
- private
- def empty_content?
- out_of_line? or super
- end
- end
-
- Contributor = Feed::Contributor
- Id = Feed::Id
- Link = Feed::Link
-
- class Published < RSS::Element
- include CommonModel
- include DateConstruct
- end
-
- Rights = Feed::Rights
-
- class Source < RSS::Element
- include CommonModel
-
- [
- ["author", "*", :children],
- ["category", "*", :children, "categories"],
- ["contributor", "*", :children],
- ["generator", "?"],
- ["icon", "?"],
- ["id", "?", nil, :content],
- ["link", "*", :children],
- ["logo", "?"],
- ["rights", "?"],
- ["subtitle", "?"],
- ["title", "?"],
- ["updated", "?", nil, :content],
- ].each do |tag, occurs, type, *args|
- type ||= :attribute
- __send__("install_have_#{type}_element",
- tag, URI, occurs, tag, *args)
- end
-
- def have_author?
- !author.to_s.empty?
- end
-
- Author = Feed::Author
- Category = Feed::Category
- Contributor = Feed::Contributor
- Generator = Feed::Generator
- Icon = Feed::Icon
- Id = Feed::Id
- Link = Feed::Link
- Logo = Feed::Logo
- Rights = Feed::Rights
- Subtitle = Feed::Subtitle
- Title = Feed::Title
- Updated = Feed::Updated
- end
-
- class Summary < RSS::Element
- include CommonModel
- include TextConstruct
- end
-
- Title = Feed::Title
- Updated = Feed::Updated
- end
- end
-
- class Entry < RSS::Element
- include RootElementMixin
- include CommonModel
- include DuplicateLinkChecker
-
- [
- ["author", "*", :children],
- ["category", "*", :children, "categories"],
- ["content", "?"],
- ["contributor", "*", :children],
- ["id", nil, nil, :content],
- ["link", "*", :children],
- ["published", "?", :child, :content],
- ["rights", "?"],
- ["source", "?"],
- ["summary", "?"],
- ["title", nil],
- ["updated", nil, nil, :content],
- ].each do |tag, occurs, type, *args|
- type ||= :attribute
- __send__("install_have_#{type}_element",
- tag, URI, occurs, tag, *args)
- end
-
- def initialize(version=nil, encoding=nil, standalone=nil)
- super("1.0", version, encoding, standalone)
- @feed_type = "atom"
- @feed_subtype = "entry"
- end
-
- def items
- [self]
- end
-
- def setup_maker(maker)
- maker = maker.maker if maker.respond_to?("maker")
- super(maker)
- end
-
- def have_author?
- authors.any? {|author| !author.to_s.empty?} or
- (source and source.have_author?)
- end
-
- private
- def atom_validate(ignore_unknown_element, tags, uri)
- unless have_author?
- raise MissingTagError.new("author", tag_name)
- end
- validate_duplicate_links(links)
- end
-
- def have_required_elements?
- super and have_author?
- end
-
- def maker_target(maker)
- maker.items.new_item
- end
-
- Author = Feed::Entry::Author
- Category = Feed::Entry::Category
- Content = Feed::Entry::Content
- Contributor = Feed::Entry::Contributor
- Id = Feed::Entry::Id
- Link = Feed::Entry::Link
- Published = Feed::Entry::Published
- Rights = Feed::Entry::Rights
- Source = Feed::Entry::Source
- Summary = Feed::Entry::Summary
- Title = Feed::Entry::Title
- Updated = Feed::Entry::Updated
- end
- end
-
- Atom::CommonModel::ELEMENTS.each do |name|
- BaseListener.install_get_text_element(Atom::URI, name, "#{name}=")
- end
-
- module ListenerMixin
- private
- def initial_start_feed(tag_name, prefix, attrs, ns)
- check_ns(tag_name, prefix, ns, Atom::URI)
-
- @rss = Atom::Feed.new(@version, @encoding, @standalone)
- @rss.do_validate = @do_validate
- @rss.xml_stylesheets = @xml_stylesheets
- @rss.lang = attrs["xml:lang"]
- @rss.base = attrs["xml:base"]
- @last_element = @rss
- pr = Proc.new do |text, tags|
- @rss.validate_for_stream(tags) if @do_validate
- end
- @proc_stack.push(pr)
- end
-
- def initial_start_entry(tag_name, prefix, attrs, ns)
- check_ns(tag_name, prefix, ns, Atom::URI)
-
- @rss = Atom::Entry.new(@version, @encoding, @standalone)
- @rss.do_validate = @do_validate
- @rss.xml_stylesheets = @xml_stylesheets
- @rss.lang = attrs["xml:lang"]
- @rss.base = attrs["xml:base"]
- @last_element = @rss
- pr = Proc.new do |text, tags|
- @rss.validate_for_stream(tags) if @do_validate
- end
- @proc_stack.push(pr)
- end
- end
-end
diff --git a/lib/rss/content.rb b/lib/rss/content.rb
deleted file mode 100644
index b12ee918aa..0000000000
--- a/lib/rss/content.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-require "rss/rss"
-
-module RSS
- CONTENT_PREFIX = 'content'
- CONTENT_URI = "http://purl.org/rss/1.0/modules/content/"
-
- module ContentModel
- extend BaseModel
-
- ELEMENTS = ["#{CONTENT_PREFIX}_encoded"]
-
- def self.append_features(klass)
- super
-
- klass.install_must_call_validator(CONTENT_PREFIX, CONTENT_URI)
- ELEMENTS.each do |full_name|
- name = full_name[(CONTENT_PREFIX.size + 1)..-1]
- klass.install_text_element(name, CONTENT_URI, "?", full_name)
- end
- end
- end
-
- prefix_size = CONTENT_PREFIX.size + 1
- ContentModel::ELEMENTS.each do |full_name|
- name = full_name[prefix_size..-1]
- BaseListener.install_get_text_element(CONTENT_URI, name, full_name)
- end
-end
-
-require 'rss/content/1.0'
-require 'rss/content/2.0'
diff --git a/lib/rss/content/1.0.rb b/lib/rss/content/1.0.rb
deleted file mode 100644
index e7c0c19685..0000000000
--- a/lib/rss/content/1.0.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'rss/1.0'
-require 'rss/content'
-
-module RSS
- RDF.install_ns(CONTENT_PREFIX, CONTENT_URI)
-
- class RDF
- class Item; include ContentModel; end
- end
-end
diff --git a/lib/rss/content/2.0.rb b/lib/rss/content/2.0.rb
deleted file mode 100644
index 8671b5b1a6..0000000000
--- a/lib/rss/content/2.0.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require "rss/2.0"
-require "rss/content"
-
-module RSS
- Rss.install_ns(CONTENT_PREFIX, CONTENT_URI)
-
- class Rss
- class Channel
- class Item; include ContentModel; end
- end
- end
-end
diff --git a/lib/rss/converter.rb b/lib/rss/converter.rb
deleted file mode 100644
index 745d6de965..0000000000
--- a/lib/rss/converter.rb
+++ /dev/null
@@ -1,170 +0,0 @@
-require "rss/utils"
-
-module RSS
-
- class Converter
-
- include Utils
-
- def initialize(to_enc, from_enc=nil)
- if "".respond_to?(:encode)
- @to_encoding = to_enc
- return
- end
- normalized_to_enc = to_enc.downcase.gsub(/-/, '_')
- from_enc ||= 'utf-8'
- normalized_from_enc = from_enc.downcase.gsub(/-/, '_')
- if normalized_to_enc == normalized_from_enc
- def_same_enc()
- else
- def_diff_enc = "def_to_#{normalized_to_enc}_from_#{normalized_from_enc}"
- if respond_to?(def_diff_enc)
- __send__(def_diff_enc)
- else
- def_else_enc(to_enc, from_enc)
- end
- end
- end
-
- def convert(value)
- if value.is_a?(String) and value.respond_to?(:encode)
- value.encode(@to_encoding)
- else
- value
- end
- end
-
- def def_convert(depth=0)
- instance_eval(<<-EOC, *get_file_and_line_from_caller(depth))
- def convert(value)
- if value.kind_of?(String)
- #{yield('value')}
- else
- value
- end
- end
- EOC
- end
-
- def def_iconv_convert(to_enc, from_enc, depth=0)
- begin
- require "iconv"
- @iconv = Iconv.new(to_enc, from_enc)
- def_convert(depth+1) do |value|
- <<-EOC
- begin
- @iconv.iconv(#{value})
- rescue Iconv::Failure
- raise ConversionError.new(#{value}, "#{to_enc}", "#{from_enc}")
- end
- EOC
- end
- rescue LoadError, ArgumentError, SystemCallError
- raise UnknownConversionMethodError.new(to_enc, from_enc)
- end
- end
-
- def def_else_enc(to_enc, from_enc)
- def_iconv_convert(to_enc, from_enc, 0)
- end
-
- def def_same_enc()
- def_convert do |value|
- value
- end
- end
-
- def def_uconv_convert_if_can(meth, to_enc, from_enc, nkf_arg)
- begin
- require "uconv"
- def_convert(1) do |value|
- <<-EOC
- begin
- Uconv.#{meth}(#{value})
- rescue Uconv::Error
- raise ConversionError.new(#{value}, "#{to_enc}", "#{from_enc}")
- end
- EOC
- end
- rescue LoadError
- require 'nkf'
- if NKF.const_defined?(:UTF8)
- def_convert(1) do |value|
- "NKF.nkf(#{nkf_arg.dump}, #{value})"
- end
- else
- def_iconv_convert(to_enc, from_enc, 1)
- end
- end
- end
-
- def def_to_euc_jp_from_utf_8
- def_uconv_convert_if_can('u8toeuc', 'EUC-JP', 'UTF-8', '-We')
- end
-
- def def_to_utf_8_from_euc_jp
- def_uconv_convert_if_can('euctou8', 'UTF-8', 'EUC-JP', '-Ew')
- end
-
- def def_to_shift_jis_from_utf_8
- def_uconv_convert_if_can('u8tosjis', 'Shift_JIS', 'UTF-8', '-Ws')
- end
-
- def def_to_utf_8_from_shift_jis
- def_uconv_convert_if_can('sjistou8', 'UTF-8', 'Shift_JIS', '-Sw')
- end
-
- def def_to_euc_jp_from_shift_jis
- require "nkf"
- def_convert do |value|
- "NKF.nkf('-Se', #{value})"
- end
- end
-
- def def_to_shift_jis_from_euc_jp
- require "nkf"
- def_convert do |value|
- "NKF.nkf('-Es', #{value})"
- end
- end
-
- def def_to_euc_jp_from_iso_2022_jp
- require "nkf"
- def_convert do |value|
- "NKF.nkf('-Je', #{value})"
- end
- end
-
- def def_to_iso_2022_jp_from_euc_jp
- require "nkf"
- def_convert do |value|
- "NKF.nkf('-Ej', #{value})"
- end
- end
-
- def def_to_utf_8_from_iso_8859_1
- def_convert do |value|
- "#{value}.unpack('C*').pack('U*')"
- end
- end
-
- def def_to_iso_8859_1_from_utf_8
- def_convert do |value|
- <<-EOC
- array_utf8 = #{value}.unpack('U*')
- array_enc = []
- array_utf8.each do |num|
- if num <= 0xFF
- array_enc << num
- else
- array_enc.concat "&\#\#{num};".unpack('C*')
- end
- end
- array_enc.pack('C*')
- EOC
- end
- end
-
- end
-
-end
diff --git a/lib/rss/dublincore.rb b/lib/rss/dublincore.rb
deleted file mode 100644
index 7ba239f8f1..0000000000
--- a/lib/rss/dublincore.rb
+++ /dev/null
@@ -1,161 +0,0 @@
-require "rss/rss"
-
-module RSS
- DC_PREFIX = 'dc'
- DC_URI = "http://purl.org/dc/elements/1.1/"
-
- module BaseDublinCoreModel
- def append_features(klass)
- super
-
- return if klass.instance_of?(Module)
- DublinCoreModel::ELEMENT_NAME_INFOS.each do |name, plural_name|
- plural = plural_name || "#{name}s"
- full_name = "#{DC_PREFIX}_#{name}"
- full_plural_name = "#{DC_PREFIX}_#{plural}"
- klass_name = "DublinCore#{Utils.to_class_name(name)}"
- klass.install_must_call_validator(DC_PREFIX, DC_URI)
- klass.install_have_children_element(name, DC_URI, "*",
- full_name, full_plural_name)
- klass.module_eval(<<-EOC, *get_file_and_line_from_caller(0))
- remove_method :#{full_name}
- remove_method :#{full_name}=
- remove_method :set_#{full_name}
-
- def #{full_name}
- @#{full_name}.first and @#{full_name}.first.value
- end
-
- def #{full_name}=(new_value)
- @#{full_name}[0] = Utils.new_with_value_if_need(#{klass_name}, new_value)
- end
- alias set_#{full_name} #{full_name}=
- EOC
- end
- klass.module_eval(<<-EOC, *get_file_and_line_from_caller(0))
- if method_defined?(:date)
- alias date_without_#{DC_PREFIX}_date= date=
-
- def date=(value)
- self.date_without_#{DC_PREFIX}_date = value
- self.#{DC_PREFIX}_date = value
- end
- else
- alias date #{DC_PREFIX}_date
- alias date= #{DC_PREFIX}_date=
- end
-
- # For backward compatibility
- alias #{DC_PREFIX}_rightses #{DC_PREFIX}_rights_list
- EOC
- end
- end
-
- module DublinCoreModel
-
- extend BaseModel
- extend BaseDublinCoreModel
-
- TEXT_ELEMENTS = {
- "title" => nil,
- "description" => nil,
- "creator" => nil,
- "subject" => nil,
- "publisher" => nil,
- "contributor" => nil,
- "type" => nil,
- "format" => nil,
- "identifier" => nil,
- "source" => nil,
- "language" => nil,
- "relation" => nil,
- "coverage" => nil,
- "rights" => "rights_list"
- }
-
- DATE_ELEMENTS = {
- "date" => "w3cdtf",
- }
-
- ELEMENT_NAME_INFOS = DublinCoreModel::TEXT_ELEMENTS.to_a
- DublinCoreModel::DATE_ELEMENTS.each do |name, |
- ELEMENT_NAME_INFOS << [name, nil]
- end
-
- ELEMENTS = TEXT_ELEMENTS.keys + DATE_ELEMENTS.keys
-
- ELEMENTS.each do |name, plural_name|
- module_eval(<<-EOC, *get_file_and_line_from_caller(0))
- class DublinCore#{Utils.to_class_name(name)} < Element
- include RSS10
-
- content_setup
-
- class << self
- def required_prefix
- DC_PREFIX
- end
-
- def required_uri
- DC_URI
- end
- end
-
- @tag_name = #{name.dump}
-
- alias_method(:value, :content)
- alias_method(:value=, :content=)
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.content = args[0]
- end
- end
-
- def full_name
- tag_name_with_prefix(DC_PREFIX)
- end
-
- def maker_target(target)
- target.new_#{name}
- end
-
- def setup_maker_attributes(#{name})
- #{name}.content = content
- end
- end
- EOC
- end
-
- DATE_ELEMENTS.each do |name, type|
- tag_name = "#{DC_PREFIX}:#{name}"
- module_eval(<<-EOC, *get_file_and_line_from_caller(0))
- class DublinCore#{Utils.to_class_name(name)} < Element
- remove_method(:content=)
- remove_method(:value=)
-
- date_writer("content", #{type.dump}, #{tag_name.dump})
-
- alias_method(:value=, :content=)
- end
- EOC
- end
- end
-
- # For backward compatibility
- DublincoreModel = DublinCoreModel
-
- DublinCoreModel::ELEMENTS.each do |name|
- class_name = Utils.to_class_name(name)
- BaseListener.install_class_name(DC_URI, name, "DublinCore#{class_name}")
- end
-
- DublinCoreModel::ELEMENTS.collect! {|name| "#{DC_PREFIX}_#{name}"}
-end
-
-require 'rss/dublincore/1.0'
-require 'rss/dublincore/2.0'
-require 'rss/dublincore/atom'
diff --git a/lib/rss/dublincore/1.0.rb b/lib/rss/dublincore/1.0.rb
deleted file mode 100644
index e193c6d2c2..0000000000
--- a/lib/rss/dublincore/1.0.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require "rss/1.0"
-require "rss/dublincore"
-
-module RSS
- RDF.install_ns(DC_PREFIX, DC_URI)
-
- class RDF
- class Channel; include DublinCoreModel; end
- class Image; include DublinCoreModel; end
- class Item; include DublinCoreModel; end
- class Textinput; include DublinCoreModel; end
- end
-end
diff --git a/lib/rss/dublincore/2.0.rb b/lib/rss/dublincore/2.0.rb
deleted file mode 100644
index 82ed1888c5..0000000000
--- a/lib/rss/dublincore/2.0.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require "rss/2.0"
-require "rss/dublincore"
-
-module RSS
- Rss.install_ns(DC_PREFIX, DC_URI)
-
- class Rss
- class Channel
- include DublinCoreModel
- class Item; include DublinCoreModel; end
- end
- end
-end
diff --git a/lib/rss/dublincore/atom.rb b/lib/rss/dublincore/atom.rb
deleted file mode 100644
index e78df4821b..0000000000
--- a/lib/rss/dublincore/atom.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require "rss/atom"
-require "rss/dublincore"
-
-module RSS
- module Atom
- Feed.install_ns(DC_PREFIX, DC_URI)
-
- class Feed
- include DublinCoreModel
- class Entry; include DublinCoreModel; end
- end
-
- class Entry
- include DublinCoreModel
- end
- end
-end
diff --git a/lib/rss/image.rb b/lib/rss/image.rb
deleted file mode 100644
index c4714aea12..0000000000
--- a/lib/rss/image.rb
+++ /dev/null
@@ -1,193 +0,0 @@
-require 'rss/1.0'
-require 'rss/dublincore'
-
-module RSS
-
- IMAGE_PREFIX = 'image'
- IMAGE_URI = 'http://purl.org/rss/1.0/modules/image/'
-
- RDF.install_ns(IMAGE_PREFIX, IMAGE_URI)
-
- IMAGE_ELEMENTS = []
-
- %w(item favicon).each do |name|
- class_name = Utils.to_class_name(name)
- BaseListener.install_class_name(IMAGE_URI, name, "Image#{class_name}")
- IMAGE_ELEMENTS << "#{IMAGE_PREFIX}_#{name}"
- end
-
- module ImageModelUtils
- def validate_one_tag_name(ignore_unknown_element, name, tags)
- if !ignore_unknown_element
- invalid = tags.find {|tag| tag != name}
- raise UnknownTagError.new(invalid, IMAGE_URI) if invalid
- end
- raise TooMuchTagError.new(name, tag_name) if tags.size > 1
- end
- end
-
- module ImageItemModel
- include ImageModelUtils
- extend BaseModel
-
- def self.append_features(klass)
- super
-
- klass.install_have_child_element("item", IMAGE_URI, "?",
- "#{IMAGE_PREFIX}_item")
- klass.install_must_call_validator(IMAGE_PREFIX, IMAGE_URI)
- end
-
- class ImageItem < Element
- include RSS10
- include DublinCoreModel
-
- @tag_name = "item"
-
- class << self
- def required_prefix
- IMAGE_PREFIX
- end
-
- def required_uri
- IMAGE_URI
- end
- end
-
- install_must_call_validator(IMAGE_PREFIX, IMAGE_URI)
-
- [
- ["about", ::RSS::RDF::URI, true],
- ["resource", ::RSS::RDF::URI, false],
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required, nil, nil,
- "#{::RSS::RDF::PREFIX}:#{name}")
- end
-
- %w(width height).each do |tag|
- full_name = "#{IMAGE_PREFIX}_#{tag}"
- disp_name = "#{IMAGE_PREFIX}:#{tag}"
- install_text_element(tag, IMAGE_URI, "?",
- full_name, :integer, disp_name)
- BaseListener.install_get_text_element(IMAGE_URI, tag, full_name)
- end
-
- alias width= image_width=
- alias width image_width
- alias height= image_height=
- alias height image_height
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.about = args[0]
- self.resource = args[1]
- end
- end
-
- def full_name
- tag_name_with_prefix(IMAGE_PREFIX)
- end
-
- private
- def maker_target(target)
- target.image_item
- end
-
- def setup_maker_attributes(item)
- item.about = self.about
- item.resource = self.resource
- end
- end
- end
-
- module ImageFaviconModel
- include ImageModelUtils
- extend BaseModel
-
- def self.append_features(klass)
- super
-
- unless klass.class == Module
- klass.install_have_child_element("favicon", IMAGE_URI, "?",
- "#{IMAGE_PREFIX}_favicon")
- klass.install_must_call_validator(IMAGE_PREFIX, IMAGE_URI)
- end
- end
-
- class ImageFavicon < Element
- include RSS10
- include DublinCoreModel
-
- @tag_name = "favicon"
-
- class << self
- def required_prefix
- IMAGE_PREFIX
- end
-
- def required_uri
- IMAGE_URI
- end
- end
-
- [
- ["about", ::RSS::RDF::URI, true, ::RSS::RDF::PREFIX],
- ["size", IMAGE_URI, true, IMAGE_PREFIX],
- ].each do |name, uri, required, prefix|
- install_get_attribute(name, uri, required, nil, nil,
- "#{prefix}:#{name}")
- end
-
- AVAILABLE_SIZES = %w(small medium large)
- alias_method :set_size, :size=
- private :set_size
- def size=(new_value)
- if @do_validate and !new_value.nil?
- new_value = new_value.strip
- unless AVAILABLE_SIZES.include?(new_value)
- attr_name = "#{IMAGE_PREFIX}:size"
- raise NotAvailableValueError.new(full_name, new_value, attr_name)
- end
- end
- set_size(new_value)
- end
-
- alias image_size= size=
- alias image_size size
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.about = args[0]
- self.size = args[1]
- end
- end
-
- def full_name
- tag_name_with_prefix(IMAGE_PREFIX)
- end
-
- private
- def maker_target(target)
- target.image_favicon
- end
-
- def setup_maker_attributes(favicon)
- favicon.about = self.about
- favicon.size = self.size
- end
- end
-
- end
-
- class RDF
- class Channel; include ImageFaviconModel; end
- class Item; include ImageItemModel; end
- end
-
-end
diff --git a/lib/rss/itunes.rb b/lib/rss/itunes.rb
deleted file mode 100644
index f95ca7aa2e..0000000000
--- a/lib/rss/itunes.rb
+++ /dev/null
@@ -1,410 +0,0 @@
-require 'rss/2.0'
-
-module RSS
- ITUNES_PREFIX = 'itunes'
- ITUNES_URI = 'http://www.itunes.com/dtds/podcast-1.0.dtd'
-
- Rss.install_ns(ITUNES_PREFIX, ITUNES_URI)
-
- module ITunesModelUtils
- include Utils
-
- def def_class_accessor(klass, name, type, *args)
- normalized_name = name.gsub(/-/, "_")
- full_name = "#{ITUNES_PREFIX}_#{normalized_name}"
- klass_name = "ITunes#{Utils.to_class_name(normalized_name)}"
-
- case type
- when :element, :attribute
- klass::ELEMENTS << full_name
- def_element_class_accessor(klass, name, full_name, klass_name, *args)
- when :elements
- klass::ELEMENTS << full_name
- def_elements_class_accessor(klass, name, full_name, klass_name, *args)
- else
- klass.install_must_call_validator(ITUNES_PREFIX, ITUNES_URI)
- klass.install_text_element(normalized_name, ITUNES_URI, "?",
- full_name, type, name)
- end
- end
-
- def def_element_class_accessor(klass, name, full_name, klass_name,
- recommended_attribute_name=nil)
- klass.install_have_child_element(name, ITUNES_PREFIX, "?", full_name)
- end
-
- def def_elements_class_accessor(klass, name, full_name, klass_name,
- plural_name, recommended_attribute_name=nil)
- full_plural_name = "#{ITUNES_PREFIX}_#{plural_name}"
- klass.install_have_children_element(name, ITUNES_PREFIX, "*",
- full_name, full_plural_name)
- end
- end
-
- module ITunesBaseModel
- extend ITunesModelUtils
-
- ELEMENTS = []
-
- ELEMENT_INFOS = [["author"],
- ["block", :yes_other],
- ["explicit", :yes_clean_other],
- ["keywords", :csv],
- ["subtitle"],
- ["summary"]]
- end
-
- module ITunesChannelModel
- extend BaseModel
- extend ITunesModelUtils
- include ITunesBaseModel
-
- ELEMENTS = []
-
- class << self
- def append_features(klass)
- super
-
- return if klass.instance_of?(Module)
- ELEMENT_INFOS.each do |name, type, *additional_infos|
- def_class_accessor(klass, name, type, *additional_infos)
- end
- end
- end
-
- ELEMENT_INFOS = [
- ["category", :elements, "categories", "text"],
- ["image", :attribute, "href"],
- ["owner", :element],
- ["new-feed-url"],
- ] + ITunesBaseModel::ELEMENT_INFOS
-
- class ITunesCategory < Element
- include RSS09
-
- @tag_name = "category"
-
- class << self
- def required_prefix
- ITUNES_PREFIX
- end
-
- def required_uri
- ITUNES_URI
- end
- end
-
- [
- ["text", "", true]
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required)
- end
-
- ITunesCategory = self
- install_have_children_element("category", ITUNES_URI, "*",
- "#{ITUNES_PREFIX}_category",
- "#{ITUNES_PREFIX}_categories")
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.text = args[0]
- end
- end
-
- def full_name
- tag_name_with_prefix(ITUNES_PREFIX)
- end
-
- private
- def maker_target(categories)
- if text or !itunes_categories.empty?
- categories.new_category
- else
- nil
- end
- end
-
- def setup_maker_attributes(category)
- category.text = text if text
- end
-
- def setup_maker_elements(category)
- super(category)
- itunes_categories.each do |sub_category|
- sub_category.setup_maker(category)
- end
- end
- end
-
- class ITunesImage < Element
- include RSS09
-
- @tag_name = "image"
-
- class << self
- def required_prefix
- ITUNES_PREFIX
- end
-
- def required_uri
- ITUNES_URI
- end
- end
-
- [
- ["href", "", true]
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required)
- end
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.href = args[0]
- end
- end
-
- def full_name
- tag_name_with_prefix(ITUNES_PREFIX)
- end
-
- private
- def maker_target(target)
- if href
- target.itunes_image {|image| image}
- else
- nil
- end
- end
-
- def setup_maker_attributes(image)
- image.href = href
- end
- end
-
- class ITunesOwner < Element
- include RSS09
-
- @tag_name = "owner"
-
- class << self
- def required_prefix
- ITUNES_PREFIX
- end
-
- def required_uri
- ITUNES_URI
- end
- end
-
- install_must_call_validator(ITUNES_PREFIX, ITUNES_URI)
- [
- ["name"],
- ["email"],
- ].each do |name,|
- ITunesBaseModel::ELEMENT_INFOS << name
- install_text_element(name, ITUNES_URI, nil, "#{ITUNES_PREFIX}_#{name}")
- end
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.itunes_name = args[0]
- self.itunes_email = args[1]
- end
- end
-
- def full_name
- tag_name_with_prefix(ITUNES_PREFIX)
- end
-
- private
- def maker_target(target)
- target.itunes_owner
- end
-
- def setup_maker_element(owner)
- super(owner)
- owner.itunes_name = itunes_name
- owner.itunes_email = itunes_email
- end
- end
- end
-
- module ITunesItemModel
- extend BaseModel
- extend ITunesModelUtils
- include ITunesBaseModel
-
- class << self
- def append_features(klass)
- super
-
- return if klass.instance_of?(Module)
- ELEMENT_INFOS.each do |name, type|
- def_class_accessor(klass, name, type)
- end
- end
- end
-
- ELEMENT_INFOS = ITunesBaseModel::ELEMENT_INFOS +
- [["duration", :element, "content"]]
-
- class ITunesDuration < Element
- include RSS09
-
- @tag_name = "duration"
-
- class << self
- def required_prefix
- ITUNES_PREFIX
- end
-
- def required_uri
- ITUNES_URI
- end
-
- def parse(duration, do_validate=true)
- if do_validate and /\A(?:
- \d?\d:[0-5]\d:[0-5]\d|
- [0-5]?\d:[0-5]\d
- )\z/x !~ duration
- raise ArgumentError,
- "must be one of HH:MM:SS, H:MM:SS, MM::SS, M:SS: " +
- duration.inspect
- end
-
- components = duration.split(':')
- components[3..-1] = nil if components.size > 3
-
- components.unshift("00") until components.size == 3
-
- components.collect do |component|
- component.to_i
- end
- end
-
- def construct(hour, minute, second)
- components = [minute, second]
- if components.include?(nil)
- nil
- else
- components.unshift(hour) if hour and hour > 0
- components.collect do |component|
- "%02d" % component
- end.join(":")
- end
- end
- end
-
- content_setup
- alias_method(:value, :content)
- remove_method(:content=)
-
- attr_reader :hour, :minute, :second
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- args = args[0] if args.size == 1 and args[0].is_a?(Array)
- if args.size == 1
- self.content = args[0]
- elsif args.size > 3
- raise ArgumentError,
- "must be (do_validate, params), (content), " +
- "(minute, second), ([minute, second]), " +
- "(hour, minute, second) or ([hour, minute, second]): " +
- args.inspect
- else
- @second, @minute, @hour = args.reverse
- update_content
- end
- end
- end
-
- def content=(value)
- if value.nil?
- @content = nil
- elsif value.is_a?(self.class)
- self.content = value.content
- else
- begin
- @hour, @minute, @second = self.class.parse(value, @do_validate)
- rescue ArgumentError
- raise NotAvailableValueError.new(tag_name, value)
- end
- @content = value
- end
- end
- alias_method(:value=, :content=)
-
- def hour=(hour)
- @hour = @do_validate ? Integer(hour) : hour.to_i
- update_content
- hour
- end
-
- def minute=(minute)
- @minute = @do_validate ? Integer(minute) : minute.to_i
- update_content
- minute
- end
-
- def second=(second)
- @second = @do_validate ? Integer(second) : second.to_i
- update_content
- second
- end
-
- def full_name
- tag_name_with_prefix(ITUNES_PREFIX)
- end
-
- private
- def update_content
- @content = self.class.construct(hour, minute, second)
- end
-
- def maker_target(target)
- if @content
- target.itunes_duration {|duration| duration}
- else
- nil
- end
- end
-
- def setup_maker_element(duration)
- super(duration)
- duration.content = @content
- end
- end
- end
-
- class Rss
- class Channel
- include ITunesChannelModel
- class Item; include ITunesItemModel; end
- end
- end
-
- element_infos =
- ITunesChannelModel::ELEMENT_INFOS + ITunesItemModel::ELEMENT_INFOS
- element_infos.each do |name, type|
- case type
- when :element, :elements, :attribute
- class_name = Utils.to_class_name(name)
- BaseListener.install_class_name(ITUNES_URI, name, "ITunes#{class_name}")
- else
- accessor_base = "#{ITUNES_PREFIX}_#{name.gsub(/-/, '_')}"
- BaseListener.install_get_text_element(ITUNES_URI, name, accessor_base)
- end
- end
-end
diff --git a/lib/rss/maker.rb b/lib/rss/maker.rb
deleted file mode 100644
index bcba1aaff3..0000000000
--- a/lib/rss/maker.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-require "rss/rss"
-
-module RSS
- module Maker
- MAKERS = {}
-
- class << self
- def make(version, &block)
- m = maker(version)
- raise UnsupportedMakerVersionError.new(version) if m.nil?
- m[:maker].make(m[:version], &block)
- end
-
- def maker(version)
- MAKERS[version]
- end
-
- def add_maker(version, normalized_version, maker)
- MAKERS[version] = {:maker => maker, :version => normalized_version}
- end
-
- def versions
- MAKERS.keys.uniq.sort
- end
-
- def makers
- MAKERS.values.collect {|info| info[:maker]}.uniq
- end
- end
- end
-end
-
-require "rss/maker/1.0"
-require "rss/maker/2.0"
-require "rss/maker/feed"
-require "rss/maker/entry"
-require "rss/maker/content"
-require "rss/maker/dublincore"
-require "rss/maker/slash"
-require "rss/maker/syndication"
-require "rss/maker/taxonomy"
-require "rss/maker/trackback"
-require "rss/maker/image"
-require "rss/maker/itunes"
diff --git a/lib/rss/maker/0.9.rb b/lib/rss/maker/0.9.rb
deleted file mode 100644
index 72b14dc977..0000000000
--- a/lib/rss/maker/0.9.rb
+++ /dev/null
@@ -1,467 +0,0 @@
-require "rss/0.9"
-
-require "rss/maker/base"
-
-module RSS
- module Maker
-
- class RSS09 < RSSBase
-
- def initialize(feed_version="0.92")
- super
- @feed_type = "rss"
- end
-
- private
- def make_feed
- Rss.new(@feed_version, @version, @encoding, @standalone)
- end
-
- def setup_elements(rss)
- setup_channel(rss)
- end
-
- class Channel < ChannelBase
- def to_feed(rss)
- channel = Rss::Channel.new
- set = setup_values(channel)
- _not_set_required_variables = not_set_required_variables
- if _not_set_required_variables.empty?
- rss.channel = channel
- set_parent(channel, rss)
- setup_items(rss)
- setup_image(rss)
- setup_textinput(rss)
- setup_other_elements(rss, channel)
- rss
- else
- raise NotSetError.new("maker.channel", _not_set_required_variables)
- end
- end
-
- private
- def setup_items(rss)
- @maker.items.to_feed(rss)
- end
-
- def setup_image(rss)
- @maker.image.to_feed(rss)
- end
-
- def setup_textinput(rss)
- @maker.textinput.to_feed(rss)
- end
-
- def variables
- super + ["pubDate"]
- end
-
- def required_variable_names
- %w(link language)
- end
-
- def not_set_required_variables
- vars = super
- vars << "description" unless description {|d| d.have_required_values?}
- vars << "title" unless title {|t| t.have_required_values?}
- vars
- end
-
- class SkipDays < SkipDaysBase
- def to_feed(rss, channel)
- unless @days.empty?
- skipDays = Rss::Channel::SkipDays.new
- channel.skipDays = skipDays
- set_parent(skipDays, channel)
- @days.each do |day|
- day.to_feed(rss, skipDays.days)
- end
- end
- end
-
- class Day < DayBase
- def to_feed(rss, days)
- day = Rss::Channel::SkipDays::Day.new
- set = setup_values(day)
- if set
- days << day
- set_parent(day, days)
- setup_other_elements(rss, day)
- end
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
- end
-
- class SkipHours < SkipHoursBase
- def to_feed(rss, channel)
- unless @hours.empty?
- skipHours = Rss::Channel::SkipHours.new
- channel.skipHours = skipHours
- set_parent(skipHours, channel)
- @hours.each do |hour|
- hour.to_feed(rss, skipHours.hours)
- end
- end
- end
-
- class Hour < HourBase
- def to_feed(rss, hours)
- hour = Rss::Channel::SkipHours::Hour.new
- set = setup_values(hour)
- if set
- hours << hour
- set_parent(hour, hours)
- setup_other_elements(rss, hour)
- end
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
- end
-
- class Cloud < CloudBase
- def to_feed(*args)
- end
- end
-
- class Categories < CategoriesBase
- def to_feed(*args)
- end
-
- class Category < CategoryBase
- end
- end
-
- class Links < LinksBase
- def to_feed(rss, channel)
- return if @links.empty?
- @links.first.to_feed(rss, channel)
- end
-
- class Link < LinkBase
- def to_feed(rss, channel)
- if have_required_values?
- channel.link = href
- else
- raise NotSetError.new("maker.channel.link",
- not_set_required_variables)
- end
- end
-
- private
- def required_variable_names
- %w(href)
- end
- end
- end
-
- class Authors < AuthorsBase
- def to_feed(rss, channel)
- end
-
- class Author < AuthorBase
- def to_feed(rss, channel)
- end
- end
- end
-
- class Contributors < ContributorsBase
- def to_feed(rss, channel)
- end
-
- class Contributor < ContributorBase
- end
- end
-
- class Generator < GeneratorBase
- def to_feed(rss, channel)
- end
- end
-
- class Copyright < CopyrightBase
- def to_feed(rss, channel)
- channel.copyright = content if have_required_values?
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
-
- class Description < DescriptionBase
- def to_feed(rss, channel)
- channel.description = content if have_required_values?
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
-
- class Title < TitleBase
- def to_feed(rss, channel)
- channel.title = content if have_required_values?
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
- end
-
- class Image < ImageBase
- def to_feed(rss)
- image = Rss::Channel::Image.new
- set = setup_values(image)
- if set
- image.link = link
- rss.channel.image = image
- set_parent(image, rss.channel)
- setup_other_elements(rss, image)
- elsif required_element?
- raise NotSetError.new("maker.image", not_set_required_variables)
- end
- end
-
- private
- def required_variable_names
- %w(url title link)
- end
-
- def required_element?
- true
- end
- end
-
- class Items < ItemsBase
- def to_feed(rss)
- if rss.channel
- normalize.each do |item|
- item.to_feed(rss)
- end
- setup_other_elements(rss, rss.items)
- end
- end
-
- class Item < ItemBase
- def to_feed(rss)
- item = Rss::Channel::Item.new
- set = setup_values(item)
- _not_set_required_variables = not_set_required_variables
- if _not_set_required_variables.empty?
- rss.items << item
- set_parent(item, rss.channel)
- setup_other_elements(rss, item)
- elsif variable_is_set?
- raise NotSetError.new("maker.items", _not_set_required_variables)
- end
- end
-
- private
- def required_variable_names
- []
- end
-
- def not_set_required_variables
- vars = super
- if @maker.feed_version == "0.91"
- vars << "title" unless title {|t| t.have_required_values?}
- vars << "link" unless link {|l| l.have_required_values?}
- end
- vars
- end
-
- class Guid < GuidBase
- def to_feed(*args)
- end
- end
-
- class Enclosure < EnclosureBase
- def to_feed(*args)
- end
- end
-
- class Source < SourceBase
- def to_feed(*args)
- end
-
- class Authors < AuthorsBase
- def to_feed(*args)
- end
-
- class Author < AuthorBase
- end
- end
-
- class Categories < CategoriesBase
- def to_feed(*args)
- end
-
- class Category < CategoryBase
- end
- end
-
- class Contributors < ContributorsBase
- def to_feed(*args)
- end
-
- class Contributor < ContributorBase
- end
- end
-
- class Generator < GeneratorBase
- def to_feed(*args)
- end
- end
-
- class Icon < IconBase
- def to_feed(*args)
- end
- end
-
- class Links < LinksBase
- def to_feed(*args)
- end
-
- class Link < LinkBase
- end
- end
-
- class Logo < LogoBase
- def to_feed(*args)
- end
- end
-
- class Rights < RightsBase
- def to_feed(*args)
- end
- end
-
- class Subtitle < SubtitleBase
- def to_feed(*args)
- end
- end
-
- class Title < TitleBase
- def to_feed(*args)
- end
- end
- end
-
- class Categories < CategoriesBase
- def to_feed(*args)
- end
-
- class Category < CategoryBase
- end
- end
-
- class Authors < AuthorsBase
- def to_feed(*args)
- end
-
- class Author < AuthorBase
- end
- end
-
- class Links < LinksBase
- def to_feed(rss, item)
- return if @links.empty?
- @links.first.to_feed(rss, item)
- end
-
- class Link < LinkBase
- def to_feed(rss, item)
- if have_required_values?
- item.link = href
- else
- raise NotSetError.new("maker.link",
- not_set_required_variables)
- end
- end
-
- private
- def required_variable_names
- %w(href)
- end
- end
- end
-
- class Contributors < ContributorsBase
- def to_feed(rss, item)
- end
-
- class Contributor < ContributorBase
- end
- end
-
- class Rights < RightsBase
- def to_feed(rss, item)
- end
- end
-
- class Description < DescriptionBase
- def to_feed(rss, item)
- item.description = content if have_required_values?
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
-
- class Content < ContentBase
- def to_feed(rss, item)
- end
- end
-
- class Title < TitleBase
- def to_feed(rss, item)
- item.title = content if have_required_values?
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
- end
- end
-
- class Textinput < TextinputBase
- def to_feed(rss)
- textInput = Rss::Channel::TextInput.new
- set = setup_values(textInput)
- if set
- rss.channel.textInput = textInput
- set_parent(textInput, rss.channel)
- setup_other_elements(rss, textInput)
- end
- end
-
- private
- def required_variable_names
- %w(title description name link)
- end
- end
- end
-
- add_maker("0.9", "0.92", RSS09)
- add_maker("0.91", "0.91", RSS09)
- add_maker("0.92", "0.92", RSS09)
- add_maker("rss0.91", "0.91", RSS09)
- add_maker("rss0.92", "0.92", RSS09)
- end
-end
diff --git a/lib/rss/maker/1.0.rb b/lib/rss/maker/1.0.rb
deleted file mode 100644
index a1e2594f70..0000000000
--- a/lib/rss/maker/1.0.rb
+++ /dev/null
@@ -1,434 +0,0 @@
-require "rss/1.0"
-
-require "rss/maker/base"
-
-module RSS
- module Maker
-
- class RSS10 < RSSBase
-
- def initialize(feed_version="1.0")
- super
- @feed_type = "rss"
- end
-
- private
- def make_feed
- RDF.new(@version, @encoding, @standalone)
- end
-
- def setup_elements(rss)
- setup_channel(rss)
- setup_image(rss)
- setup_items(rss)
- setup_textinput(rss)
- end
-
- class Channel < ChannelBase
-
- def to_feed(rss)
- set_default_values do
- _not_set_required_variables = not_set_required_variables
- if _not_set_required_variables.empty?
- channel = RDF::Channel.new(@about)
- set = setup_values(channel)
- channel.dc_dates.clear
- rss.channel = channel
- set_parent(channel, rss)
- setup_items(rss)
- setup_image(rss)
- setup_textinput(rss)
- setup_other_elements(rss, channel)
- else
- raise NotSetError.new("maker.channel", _not_set_required_variables)
- end
- end
- end
-
- private
- def setup_items(rss)
- items = RDF::Channel::Items.new
- seq = items.Seq
- set_parent(items, seq)
- target_items = @maker.items.normalize
- raise NotSetError.new("maker", ["items"]) if target_items.empty?
- target_items.each do |item|
- li = RDF::Channel::Items::Seq::Li.new(item.link)
- seq.lis << li
- set_parent(li, seq)
- end
- rss.channel.items = items
- set_parent(rss.channel, items)
- end
-
- def setup_image(rss)
- if @maker.image.have_required_values?
- image = RDF::Channel::Image.new(@maker.image.url)
- rss.channel.image = image
- set_parent(image, rss.channel)
- end
- end
-
- def setup_textinput(rss)
- if @maker.textinput.have_required_values?
- textinput = RDF::Channel::Textinput.new(@maker.textinput.link)
- rss.channel.textinput = textinput
- set_parent(textinput, rss.channel)
- end
- end
-
- def required_variable_names
- %w(about link)
- end
-
- def not_set_required_variables
- vars = super
- vars << "description" unless description {|d| d.have_required_values?}
- vars << "title" unless title {|t| t.have_required_values?}
- vars
- end
-
- class SkipDays < SkipDaysBase
- def to_feed(*args)
- end
-
- class Day < DayBase
- end
- end
-
- class SkipHours < SkipHoursBase
- def to_feed(*args)
- end
-
- class Hour < HourBase
- end
- end
-
- class Cloud < CloudBase
- def to_feed(*args)
- end
- end
-
- class Categories < CategoriesBase
- def to_feed(*args)
- end
-
- class Category < CategoryBase
- end
- end
-
- class Links < LinksBase
- def to_feed(rss, channel)
- return if @links.empty?
- @links.first.to_feed(rss, channel)
- end
-
- class Link < LinkBase
- def to_feed(rss, channel)
- if have_required_values?
- channel.link = href
- else
- raise NotSetError.new("maker.channel.link",
- not_set_required_variables)
- end
- end
-
- private
- def required_variable_names
- %w(href)
- end
- end
- end
-
- class Authors < AuthorsBase
- def to_feed(rss, channel)
- end
-
- class Author < AuthorBase
- def to_feed(rss, channel)
- end
- end
- end
-
- class Contributors < ContributorsBase
- def to_feed(rss, channel)
- end
-
- class Contributor < ContributorBase
- end
- end
-
- class Generator < GeneratorBase
- def to_feed(rss, channel)
- end
- end
-
- class Copyright < CopyrightBase
- def to_feed(rss, channel)
- end
- end
-
- class Description < DescriptionBase
- def to_feed(rss, channel)
- channel.description = content if have_required_values?
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
-
- class Title < TitleBase
- def to_feed(rss, channel)
- channel.title = content if have_required_values?
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
- end
-
- class Image < ImageBase
- def to_feed(rss)
- if @url
- image = RDF::Image.new(@url)
- set = setup_values(image)
- if set
- rss.image = image
- set_parent(image, rss)
- setup_other_elements(rss, image)
- end
- end
- end
-
- def have_required_values?
- super and @maker.channel.have_required_values?
- end
-
- private
- def variables
- super + ["link"]
- end
-
- def required_variable_names
- %w(url title link)
- end
- end
-
- class Items < ItemsBase
- def to_feed(rss)
- if rss.channel
- normalize.each do |item|
- item.to_feed(rss)
- end
- setup_other_elements(rss, rss.items)
- end
- end
-
- class Item < ItemBase
- def to_feed(rss)
- set_default_values do
- item = RDF::Item.new(link)
- set = setup_values(item)
- if set
- item.dc_dates.clear
- rss.items << item
- set_parent(item, rss)
- setup_other_elements(rss, item)
- elsif !have_required_values?
- raise NotSetError.new("maker.item", not_set_required_variables)
- end
- end
- end
-
- private
- def required_variable_names
- %w(link)
- end
-
- def variables
- super + %w(link)
- end
-
- def not_set_required_variables
- set_default_values do
- vars = super
- vars << "title" unless title {|t| t.have_required_values?}
- vars
- end
- end
-
- class Guid < GuidBase
- def to_feed(*args)
- end
- end
-
- class Enclosure < EnclosureBase
- def to_feed(*args)
- end
- end
-
- class Source < SourceBase
- def to_feed(*args)
- end
-
- class Authors < AuthorsBase
- def to_feed(*args)
- end
-
- class Author < AuthorBase
- end
- end
-
- class Categories < CategoriesBase
- def to_feed(*args)
- end
-
- class Category < CategoryBase
- end
- end
-
- class Contributors < ContributorsBase
- def to_feed(*args)
- end
-
- class Contributor < ContributorBase
- end
- end
-
- class Generator < GeneratorBase
- def to_feed(*args)
- end
- end
-
- class Icon < IconBase
- def to_feed(*args)
- end
- end
-
- class Links < LinksBase
- def to_feed(*args)
- end
-
- class Link < LinkBase
- end
- end
-
- class Logo < LogoBase
- def to_feed(*args)
- end
- end
-
- class Rights < RightsBase
- def to_feed(*args)
- end
- end
-
- class Subtitle < SubtitleBase
- def to_feed(*args)
- end
- end
-
- class Title < TitleBase
- def to_feed(*args)
- end
- end
- end
-
- class Categories < CategoriesBase
- def to_feed(*args)
- end
-
- class Category < CategoryBase
- end
- end
-
- class Authors < AuthorsBase
- def to_feed(*args)
- end
-
- class Author < AuthorBase
- end
- end
-
- class Links < LinksBase
- def to_feed(*args)
- end
-
- class Link < LinkBase
- end
- end
-
- class Contributors < ContributorsBase
- def to_feed(rss, item)
- end
-
- class Contributor < ContributorBase
- end
- end
-
- class Rights < RightsBase
- def to_feed(rss, item)
- end
- end
-
- class Description < DescriptionBase
- def to_feed(rss, item)
- item.description = content if have_required_values?
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
-
- class Content < ContentBase
- def to_feed(rss, item)
- end
- end
-
- class Title < TitleBase
- def to_feed(rss, item)
- item.title = content if have_required_values?
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
- end
- end
-
- class Textinput < TextinputBase
- def to_feed(rss)
- if @link
- textinput = RDF::Textinput.new(@link)
- set = setup_values(textinput)
- if set
- rss.textinput = textinput
- set_parent(textinput, rss)
- setup_other_elements(rss, textinput)
- end
- end
- end
-
- def have_required_values?
- super and @maker.channel.have_required_values?
- end
-
- private
- def required_variable_names
- %w(title description name link)
- end
- end
- end
-
- add_maker("1.0", "1.0", RSS10)
- add_maker("rss1.0", "1.0", RSS10)
- end
-end
diff --git a/lib/rss/maker/2.0.rb b/lib/rss/maker/2.0.rb
deleted file mode 100644
index 67d68126ac..0000000000
--- a/lib/rss/maker/2.0.rb
+++ /dev/null
@@ -1,223 +0,0 @@
-require "rss/2.0"
-
-require "rss/maker/0.9"
-
-module RSS
- module Maker
-
- class RSS20 < RSS09
-
- def initialize(feed_version="2.0")
- super
- end
-
- class Channel < RSS09::Channel
-
- private
- def required_variable_names
- %w(link)
- end
-
- class SkipDays < RSS09::Channel::SkipDays
- class Day < RSS09::Channel::SkipDays::Day
- end
- end
-
- class SkipHours < RSS09::Channel::SkipHours
- class Hour < RSS09::Channel::SkipHours::Hour
- end
- end
-
- class Cloud < RSS09::Channel::Cloud
- def to_feed(rss, channel)
- cloud = Rss::Channel::Cloud.new
- set = setup_values(cloud)
- if set
- channel.cloud = cloud
- set_parent(cloud, channel)
- setup_other_elements(rss, cloud)
- end
- end
-
- private
- def required_variable_names
- %w(domain port path registerProcedure protocol)
- end
- end
-
- class Categories < RSS09::Channel::Categories
- def to_feed(rss, channel)
- @categories.each do |category|
- category.to_feed(rss, channel)
- end
- end
-
- class Category < RSS09::Channel::Categories::Category
- def to_feed(rss, channel)
- category = Rss::Channel::Category.new
- set = setup_values(category)
- if set
- channel.categories << category
- set_parent(category, channel)
- setup_other_elements(rss, category)
- end
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
- end
-
- class Generator < GeneratorBase
- def to_feed(rss, channel)
- channel.generator = content
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
- end
-
- class Image < RSS09::Image
- private
- def required_element?
- false
- end
- end
-
- class Items < RSS09::Items
- class Item < RSS09::Items::Item
- private
- def required_variable_names
- []
- end
-
- def not_set_required_variables
- vars = super
- if !title {|t| t.have_required_values?} and
- !description {|d| d.have_required_values?}
- vars << "title or description"
- end
- vars
- end
-
- def variables
- super + ["pubDate"]
- end
-
- class Guid < RSS09::Items::Item::Guid
- def to_feed(rss, item)
- guid = Rss::Channel::Item::Guid.new
- set = setup_values(guid)
- if set
- item.guid = guid
- set_parent(guid, item)
- setup_other_elements(rss, guid)
- end
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
-
- class Enclosure < RSS09::Items::Item::Enclosure
- def to_feed(rss, item)
- enclosure = Rss::Channel::Item::Enclosure.new
- set = setup_values(enclosure)
- if set
- item.enclosure = enclosure
- set_parent(enclosure, item)
- setup_other_elements(rss, enclosure)
- end
- end
-
- private
- def required_variable_names
- %w(url length type)
- end
- end
-
- class Source < RSS09::Items::Item::Source
- def to_feed(rss, item)
- source = Rss::Channel::Item::Source.new
- set = setup_values(source)
- if set
- item.source = source
- set_parent(source, item)
- setup_other_elements(rss, source)
- end
- end
-
- private
- def required_variable_names
- %w(url content)
- end
-
- class Links < RSS09::Items::Item::Source::Links
- def to_feed(rss, source)
- return if @links.empty?
- @links.first.to_feed(rss, source)
- end
-
- class Link < RSS09::Items::Item::Source::Links::Link
- def to_feed(rss, source)
- source.url = href
- end
- end
- end
- end
-
- class Categories < RSS09::Items::Item::Categories
- def to_feed(rss, item)
- @categories.each do |category|
- category.to_feed(rss, item)
- end
- end
-
- class Category < RSS09::Items::Item::Categories::Category
- def to_feed(rss, item)
- category = Rss::Channel::Item::Category.new
- set = setup_values(category)
- if set
- item.categories << category
- set_parent(category, item)
- setup_other_elements(rss)
- end
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
- end
-
- class Authors < RSS09::Items::Item::Authors
- def to_feed(rss, item)
- return if @authors.empty?
- @authors.first.to_feed(rss, item)
- end
-
- class Author < RSS09::Items::Item::Authors::Author
- def to_feed(rss, item)
- item.author = name
- end
- end
- end
- end
- end
-
- class Textinput < RSS09::Textinput
- end
- end
-
- add_maker("2.0", "2.0", RSS20)
- add_maker("rss2.0", "2.0", RSS20)
- end
-end
diff --git a/lib/rss/maker/atom.rb b/lib/rss/maker/atom.rb
deleted file mode 100644
index fd3198cd9e..0000000000
--- a/lib/rss/maker/atom.rb
+++ /dev/null
@@ -1,172 +0,0 @@
-require "rss/atom"
-
-require "rss/maker/base"
-
-module RSS
- module Maker
- module AtomPersons
- module_function
- def def_atom_persons(klass, name, maker_name, plural=nil)
- plural ||= "#{name}s"
- klass_name = Utils.to_class_name(name)
- plural_klass_name = Utils.to_class_name(plural)
-
- klass.class_eval(<<-EOC, __FILE__, __LINE__ + 1)
- class #{plural_klass_name} < #{plural_klass_name}Base
- class #{klass_name} < #{klass_name}Base
- def to_feed(feed, current)
- #{name} = feed.class::#{klass_name}.new
- set = setup_values(#{name})
- unless set
- raise NotSetError.new(#{maker_name.dump},
- not_set_required_variables)
- end
- current.#{plural} << #{name}
- set_parent(#{name}, current)
- setup_other_elements(#{name})
- end
-
- private
- def required_variable_names
- %w(name)
- end
- end
- end
-EOC
- end
- end
-
- module AtomTextConstruct
- class << self
- def def_atom_text_construct(klass, name, maker_name, klass_name=nil,
- atom_klass_name=nil)
- klass_name ||= Utils.to_class_name(name)
- atom_klass_name ||= Utils.to_class_name(name)
-
- klass.class_eval(<<-EOC, __FILE__, __LINE__ + 1)
- class #{klass_name} < #{klass_name}Base
- include #{self.name}
- def to_feed(feed, current)
- #{name} = current.class::#{atom_klass_name}.new
- if setup_values(#{name})
- current.#{name} = #{name}
- set_parent(#{name}, current)
- setup_other_elements(feed)
- elsif variable_is_set?
- raise NotSetError.new(#{maker_name.dump},
- not_set_required_variables)
- end
- end
- end
- EOC
- end
- end
-
- private
- def required_variable_names
- if type == "xhtml"
- %w(xml_content)
- else
- %w(content)
- end
- end
-
- def variables
- if type == "xhtml"
- super + %w(xhtml)
- else
- super
- end
- end
- end
-
- module AtomCategory
- def to_feed(feed, current)
- category = feed.class::Category.new
- set = setup_values(category)
- if set
- current.categories << category
- set_parent(category, current)
- setup_other_elements(feed)
- else
- raise NotSetError.new(self.class.not_set_name,
- not_set_required_variables)
- end
- end
-
- private
- def required_variable_names
- %w(term)
- end
-
- def variables
- super + ["term", "scheme"]
- end
- end
-
- module AtomLink
- def to_feed(feed, current)
- link = feed.class::Link.new
- set = setup_values(link)
- if set
- current.links << link
- set_parent(link, current)
- setup_other_elements(feed)
- else
- raise NotSetError.new(self.class.not_set_name,
- not_set_required_variables)
- end
- end
-
- private
- def required_variable_names
- %w(href)
- end
- end
-
- module AtomGenerator
- def to_feed(feed, current)
- generator = current.class::Generator.new
- if setup_values(generator)
- current.generator = generator
- set_parent(generator, current)
- setup_other_elements(feed)
- elsif variable_is_set?
- raise NotSetError.new(self.class.not_set_name,
- not_set_required_variables)
- end
- end
-
- private
- def required_variable_names
- %w(content)
- end
- end
-
- module AtomLogo
- def to_feed(feed, current)
- logo = current.class::Logo.new
- class << logo
- alias_method(:uri=, :content=)
- end
- set = setup_values(logo)
- class << logo
- remove_method(:uri=)
- end
- if set
- current.logo = logo
- set_parent(logo, current)
- setup_other_elements(feed)
- elsif variable_is_set?
- raise NotSetError.new(self.class.not_set_name,
- not_set_required_variables)
- end
- end
-
- private
- def required_variable_names
- %w(uri)
- end
- end
- end
-end
diff --git a/lib/rss/maker/base.rb b/lib/rss/maker/base.rb
deleted file mode 100644
index 2262a764ec..0000000000
--- a/lib/rss/maker/base.rb
+++ /dev/null
@@ -1,880 +0,0 @@
-require 'forwardable'
-
-require 'rss/rss'
-
-module RSS
- module Maker
- class Base
- extend Utils::InheritedReader
-
- OTHER_ELEMENTS = []
- NEED_INITIALIZE_VARIABLES = []
-
- class << self
- def other_elements
- inherited_array_reader("OTHER_ELEMENTS")
- end
- def need_initialize_variables
- inherited_array_reader("NEED_INITIALIZE_VARIABLES")
- end
-
- def inherited_base
- ::RSS::Maker::Base
- end
-
- def inherited(subclass)
- subclass.const_set("OTHER_ELEMENTS", [])
- subclass.const_set("NEED_INITIALIZE_VARIABLES", [])
- end
-
- def add_other_element(variable_name)
- self::OTHER_ELEMENTS << variable_name
- end
-
- def add_need_initialize_variable(variable_name, init_value=nil,
- &init_block)
- init_value ||= init_block
- self::NEED_INITIALIZE_VARIABLES << [variable_name, init_value]
- end
-
- def def_array_element(name, plural=nil, klass_name=nil)
- include Enumerable
- extend Forwardable
-
- plural ||= "#{name}s"
- klass_name ||= Utils.to_class_name(name)
- def_delegators("@#{plural}", :<<, :[], :[]=, :first, :last)
- def_delegators("@#{plural}", :push, :pop, :shift, :unshift)
- def_delegators("@#{plural}", :each, :size, :empty?, :clear)
-
- add_need_initialize_variable(plural) {[]}
-
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- def new_#{name}
- #{name} = self.class::#{klass_name}.new(@maker)
- @#{plural} << #{name}
- if block_given?
- yield #{name}
- else
- #{name}
- end
- end
- alias new_child new_#{name}
-
- def to_feed(*args)
- @#{plural}.each do |#{name}|
- #{name}.to_feed(*args)
- end
- end
-
- def replace(elements)
- @#{plural}.replace(elements.to_a)
- end
- EOC
- end
-
- def def_classed_element_without_accessor(name, class_name=nil)
- class_name ||= Utils.to_class_name(name)
- add_other_element(name)
- add_need_initialize_variable(name) do |object|
- object.send("make_#{name}")
- end
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- private
- def setup_#{name}(feed, current)
- @#{name}.to_feed(feed, current)
- end
-
- def make_#{name}
- self.class::#{class_name}.new(@maker)
- end
- EOC
- end
-
- def def_classed_element(name, class_name=nil, attribute_name=nil)
- def_classed_element_without_accessor(name, class_name)
- if attribute_name
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- def #{name}
- if block_given?
- yield(@#{name})
- else
- @#{name}.#{attribute_name}
- end
- end
-
- def #{name}=(new_value)
- @#{name}.#{attribute_name} = new_value
- end
- EOC
- else
- attr_reader name
- end
- end
-
- def def_classed_elements(name, attribute, plural_class_name=nil,
- plural_name=nil, new_name=nil)
- plural_name ||= "#{name}s"
- new_name ||= name
- def_classed_element(plural_name, plural_class_name)
- local_variable_name = "_#{name}"
- new_value_variable_name = "new_value"
- additional_setup_code = nil
- if block_given?
- additional_setup_code = yield(local_variable_name,
- new_value_variable_name)
- end
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- def #{name}
- #{local_variable_name} = #{plural_name}.first
- #{local_variable_name} ? #{local_variable_name}.#{attribute} : nil
- end
-
- def #{name}=(#{new_value_variable_name})
- #{local_variable_name} =
- #{plural_name}.first || #{plural_name}.new_#{new_name}
- #{additional_setup_code}
- #{local_variable_name}.#{attribute} = #{new_value_variable_name}
- end
- EOC
- end
-
- def def_other_element(name)
- attr_accessor name
- def_other_element_without_accessor(name)
- end
-
- def def_other_element_without_accessor(name)
- add_need_initialize_variable(name)
- add_other_element(name)
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- def setup_#{name}(feed, current)
- if !@#{name}.nil? and current.respond_to?(:#{name}=)
- current.#{name} = @#{name}
- end
- end
- EOC
- end
-
- def def_csv_element(name, type=nil)
- def_other_element_without_accessor(name)
- attr_reader(name)
- converter = ""
- if type == :integer
- converter = "{|v| Integer(v)}"
- end
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- def #{name}=(value)
- @#{name} = Utils::CSV.parse(value)#{converter}
- end
- EOC
- end
- end
-
- attr_reader :maker
- def initialize(maker)
- @maker = maker
- @default_values_are_set = false
- initialize_variables
- end
-
- def have_required_values?
- not_set_required_variables.empty?
- end
-
- def variable_is_set?
- variables.any? {|var| not __send__(var).nil?}
- end
-
- private
- def initialize_variables
- self.class.need_initialize_variables.each do |variable_name, init_value|
- if init_value.nil?
- value = nil
- else
- if init_value.respond_to?(:call)
- value = init_value.call(self)
- elsif init_value.is_a?(String)
- # just for backward compatibility
- value = instance_eval(init_value, __FILE__, __LINE__)
- else
- value = init_value
- end
- end
- instance_variable_set("@#{variable_name}", value)
- end
- end
-
- def setup_other_elements(feed, current=nil)
- current ||= current_element(feed)
- self.class.other_elements.each do |element|
- __send__("setup_#{element}", feed, current)
- end
- end
-
- def current_element(feed)
- feed
- end
-
- def set_default_values(&block)
- return yield if @default_values_are_set
-
- begin
- @default_values_are_set = true
- _set_default_values(&block)
- ensure
- @default_values_are_set = false
- end
- end
-
- def _set_default_values(&block)
- yield
- end
-
- def setup_values(target)
- set = false
- if have_required_values?
- variables.each do |var|
- setter = "#{var}="
- if target.respond_to?(setter)
- value = __send__(var)
- if value
- target.__send__(setter, value)
- set = true
- end
- end
- end
- end
- set
- end
-
- def set_parent(target, parent)
- target.parent = parent if target.class.need_parent?
- end
-
- def variables
- self.class.need_initialize_variables.find_all do |name, init|
- # init == "nil" is just for backward compatibility
- init.nil? or init == "nil"
- end.collect do |name, init|
- name
- end
- end
-
- def not_set_required_variables
- required_variable_names.find_all do |var|
- __send__(var).nil?
- end
- end
-
- def required_variables_are_set?
- required_variable_names.each do |var|
- return false if __send__(var).nil?
- end
- true
- end
- end
-
- module AtomPersonConstructBase
- def self.append_features(klass)
- super
-
- klass.class_eval(<<-EOC, __FILE__, __LINE__ + 1)
- %w(name uri email).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
- EOC
- end
- end
-
- module AtomTextConstructBase
- module EnsureXMLContent
- class << self
- def included(base)
- super
- base.class_eval do
- %w(type content xml_content).each do |element|
- attr_reader element
- attr_writer element if element != "xml_content"
- add_need_initialize_variable(element)
- end
-
- alias_method(:xhtml, :xml_content)
- end
- end
- end
-
- def ensure_xml_content(content)
- xhtml_uri = ::RSS::Atom::XHTML_URI
- unless content.is_a?(RSS::XML::Element) and
- ["div", xhtml_uri] == [content.name, content.uri]
- children = content
- children = [children] unless content.is_a?(Array)
- children = set_xhtml_uri_as_default_uri(children)
- content = RSS::XML::Element.new("div", nil, xhtml_uri,
- {"xmlns" => xhtml_uri},
- children)
- end
- content
- end
-
- def xml_content=(content)
- @xml_content = ensure_xml_content(content)
- end
-
- def xhtml=(content)
- self.xml_content = content
- end
-
- private
- def set_xhtml_uri_as_default_uri(children)
- children.collect do |child|
- if child.is_a?(RSS::XML::Element) and
- child.prefix.nil? and child.uri.nil?
- RSS::XML::Element.new(child.name, nil, ::RSS::Atom::XHTML_URI,
- child.attributes.dup,
- set_xhtml_uri_as_default_uri(child.children))
- else
- child
- end
- end
- end
- end
-
- def self.append_features(klass)
- super
-
- klass.class_eval do
- include EnsureXMLContent
- end
- end
- end
-
- module SetupDefaultDate
- private
- def _set_default_values(&block)
- keep = {
- :date => date,
- :dc_dates => dc_dates.to_a.dup,
- }
- _date = date
- if _date and !dc_dates.any? {|dc_date| dc_date.value == _date}
- dc_date = self.class::DublinCoreDates::DublinCoreDate.new(self)
- dc_date.value = _date.dup
- dc_dates.unshift(dc_date)
- end
- self.date ||= self.dc_date
- super(&block)
- ensure
- date = keep[:date]
- dc_dates.replace(keep[:dc_dates])
- end
- end
-
- class RSSBase < Base
- class << self
- def make(version, &block)
- new(version).make(&block)
- end
- end
-
- %w(xml_stylesheets channel image items textinput).each do |element|
- attr_reader element
- add_need_initialize_variable(element) do |object|
- object.send("make_#{element}")
- end
- module_eval(<<-EOC, __FILE__, __LINE__)
- private
- def setup_#{element}(feed)
- @#{element}.to_feed(feed)
- end
-
- def make_#{element}
- self.class::#{Utils.to_class_name(element)}.new(self)
- end
- EOC
- end
-
- attr_reader :feed_version
- alias_method(:rss_version, :feed_version)
- attr_accessor :version, :encoding, :standalone
-
- def initialize(feed_version)
- super(self)
- @feed_type = nil
- @feed_subtype = nil
- @feed_version = feed_version
- @version = "1.0"
- @encoding = "UTF-8"
- @standalone = nil
- end
-
- def make
- yield(self)
- to_feed
- end
-
- def to_feed
- feed = make_feed
- setup_xml_stylesheets(feed)
- setup_elements(feed)
- setup_other_elements(feed)
- feed.validate
- feed
- end
-
- private
- remove_method :make_xml_stylesheets
- def make_xml_stylesheets
- XMLStyleSheets.new(self)
- end
- end
-
- class XMLStyleSheets < Base
- def_array_element("xml_stylesheet", nil, "XMLStyleSheet")
-
- class XMLStyleSheet < Base
-
- ::RSS::XMLStyleSheet::ATTRIBUTES.each do |attribute|
- attr_accessor attribute
- add_need_initialize_variable(attribute)
- end
-
- def to_feed(feed)
- xss = ::RSS::XMLStyleSheet.new
- guess_type_if_need(xss)
- set = setup_values(xss)
- if set
- feed.xml_stylesheets << xss
- end
- end
-
- private
- def guess_type_if_need(xss)
- if @type.nil?
- xss.href = @href
- @type = xss.type
- end
- end
-
- def required_variable_names
- %w(href type)
- end
- end
- end
-
- class ChannelBase < Base
- include SetupDefaultDate
-
- %w(cloud categories skipDays skipHours).each do |name|
- def_classed_element(name)
- end
-
- %w(generator copyright description title).each do |name|
- def_classed_element(name, nil, "content")
- end
-
- [
- ["link", "href", Proc.new {|target,| "#{target}.href = 'self'"}],
- ["author", "name"],
- ["contributor", "name"],
- ].each do |name, attribute, additional_setup_maker|
- def_classed_elements(name, attribute, &additional_setup_maker)
- end
-
- %w(id about language
- managingEditor webMaster rating docs date
- lastBuildDate ttl).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
-
- def pubDate
- date
- end
-
- def pubDate=(date)
- self.date = date
- end
-
- def updated
- date
- end
-
- def updated=(date)
- self.date = date
- end
-
- alias_method(:rights, :copyright)
- alias_method(:rights=, :copyright=)
-
- alias_method(:subtitle, :description)
- alias_method(:subtitle=, :description=)
-
- def icon
- image_favicon.about
- end
-
- def icon=(url)
- image_favicon.about = url
- end
-
- def logo
- maker.image.url
- end
-
- def logo=(url)
- maker.image.url = url
- end
-
- class SkipDaysBase < Base
- def_array_element("day")
-
- class DayBase < Base
- %w(content).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
- end
- end
-
- class SkipHoursBase < Base
- def_array_element("hour")
-
- class HourBase < Base
- %w(content).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
- end
- end
-
- class CloudBase < Base
- %w(domain port path registerProcedure protocol).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
- end
-
- class CategoriesBase < Base
- def_array_element("category", "categories")
-
- class CategoryBase < Base
- %w(domain content label).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
-
- alias_method(:term, :domain)
- alias_method(:term=, :domain=)
- alias_method(:scheme, :content)
- alias_method(:scheme=, :content=)
- end
- end
-
- class LinksBase < Base
- def_array_element("link")
-
- class LinkBase < Base
- %w(href rel type hreflang title length).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
- end
- end
-
- class AuthorsBase < Base
- def_array_element("author")
-
- class AuthorBase < Base
- include AtomPersonConstructBase
- end
- end
-
- class ContributorsBase < Base
- def_array_element("contributor")
-
- class ContributorBase < Base
- include AtomPersonConstructBase
- end
- end
-
- class GeneratorBase < Base
- %w(uri version content).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
- end
-
- class CopyrightBase < Base
- include AtomTextConstructBase
- end
-
- class DescriptionBase < Base
- include AtomTextConstructBase
- end
-
- class TitleBase < Base
- include AtomTextConstructBase
- end
- end
-
- class ImageBase < Base
- %w(title url width height description).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
-
- def link
- @maker.channel.link
- end
- end
-
- class ItemsBase < Base
- def_array_element("item")
-
- attr_accessor :do_sort, :max_size
-
- def initialize(maker)
- super
- @do_sort = false
- @max_size = -1
- end
-
- def normalize
- if @max_size >= 0
- sort_if_need[0...@max_size]
- else
- sort_if_need[0..@max_size]
- end
- end
-
- private
- def sort_if_need
- if @do_sort.respond_to?(:call)
- @items.sort do |x, y|
- @do_sort.call(x, y)
- end
- elsif @do_sort
- @items.sort do |x, y|
- y <=> x
- end
- else
- @items
- end
- end
-
- class ItemBase < Base
- include SetupDefaultDate
-
- %w(guid enclosure source categories content).each do |name|
- def_classed_element(name)
- end
-
- %w(rights description title).each do |name|
- def_classed_element(name, nil, "content")
- end
-
- [
- ["author", "name"],
- ["link", "href", Proc.new {|target,| "#{target}.href = 'alternate'"}],
- ["contributor", "name"],
- ].each do |name, attribute|
- def_classed_elements(name, attribute)
- end
-
- %w(date comments id published).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
-
- def pubDate
- date
- end
-
- def pubDate=(date)
- self.date = date
- end
-
- def updated
- date
- end
-
- def updated=(date)
- self.date = date
- end
-
- alias_method(:summary, :description)
- alias_method(:summary=, :description=)
-
- def <=>(other)
- _date = date || dc_date
- _other_date = other.date || other.dc_date
- if _date and _other_date
- _date <=> _other_date
- elsif _date
- 1
- elsif _other_date
- -1
- else
- 0
- end
- end
-
- class GuidBase < Base
- %w(isPermaLink content).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
- end
-
- class EnclosureBase < Base
- %w(url length type).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
- end
-
- class SourceBase < Base
- %w(authors categories contributors generator icon
- logo rights subtitle title).each do |name|
- def_classed_element(name)
- end
-
- [
- ["link", "href"],
- ].each do |name, attribute|
- def_classed_elements(name, attribute)
- end
-
- %w(id content date).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
-
- alias_method(:url, :link)
- alias_method(:url=, :link=)
-
- def updated
- date
- end
-
- def updated=(date)
- self.date = date
- end
-
- private
- AuthorsBase = ChannelBase::AuthorsBase
- CategoriesBase = ChannelBase::CategoriesBase
- ContributorsBase = ChannelBase::ContributorsBase
- GeneratorBase = ChannelBase::GeneratorBase
-
- class IconBase < Base
- %w(url).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
- end
-
- LinksBase = ChannelBase::LinksBase
-
- class LogoBase < Base
- %w(uri).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
- end
-
- class RightsBase < Base
- include AtomTextConstructBase
- end
-
- class SubtitleBase < Base
- include AtomTextConstructBase
- end
-
- class TitleBase < Base
- include AtomTextConstructBase
- end
- end
-
- CategoriesBase = ChannelBase::CategoriesBase
- AuthorsBase = ChannelBase::AuthorsBase
- LinksBase = ChannelBase::LinksBase
- ContributorsBase = ChannelBase::ContributorsBase
-
- class RightsBase < Base
- include AtomTextConstructBase
- end
-
- class DescriptionBase < Base
- include AtomTextConstructBase
- end
-
- class ContentBase < Base
- include AtomTextConstructBase::EnsureXMLContent
-
- %w(src).each do |element|
- attr_accessor(element)
- add_need_initialize_variable(element)
- end
-
- def xml_content=(content)
- content = ensure_xml_content(content) if inline_xhtml?
- @xml_content = content
- end
-
- alias_method(:xml, :xml_content)
- alias_method(:xml=, :xml_content=)
-
- def inline_text?
- [nil, "text", "html"].include?(@type)
- end
-
- def inline_html?
- @type == "html"
- end
-
- def inline_xhtml?
- @type == "xhtml"
- end
-
- def inline_other?
- !out_of_line? and ![nil, "text", "html", "xhtml"].include?(@type)
- end
-
- def inline_other_text?
- return false if @type.nil? or out_of_line?
- /\Atext\//i.match(@type) ? true : false
- end
-
- def inline_other_xml?
- return false if @type.nil? or out_of_line?
- /[\+\/]xml\z/i.match(@type) ? true : false
- end
-
- def inline_other_base64?
- return false if @type.nil? or out_of_line?
- @type.include?("/") and !inline_other_text? and !inline_other_xml?
- end
-
- def out_of_line?
- not @src.nil? and @content.nil?
- end
- end
-
- class TitleBase < Base
- include AtomTextConstructBase
- end
- end
- end
-
- class TextinputBase < Base
- %w(title description name link).each do |element|
- attr_accessor element
- add_need_initialize_variable(element)
- end
- end
- end
-end
diff --git a/lib/rss/maker/content.rb b/lib/rss/maker/content.rb
deleted file mode 100644
index 46c4911f73..0000000000
--- a/lib/rss/maker/content.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'rss/content'
-require 'rss/maker/1.0'
-require 'rss/maker/2.0'
-
-module RSS
- module Maker
- module ContentModel
- def self.append_features(klass)
- super
-
- ::RSS::ContentModel::ELEMENTS.each do |name|
- klass.def_other_element(name)
- end
- end
- end
-
- class ItemsBase
- class ItemBase; include ContentModel; end
- end
- end
-end
diff --git a/lib/rss/maker/dublincore.rb b/lib/rss/maker/dublincore.rb
deleted file mode 100644
index ff4813fe19..0000000000
--- a/lib/rss/maker/dublincore.rb
+++ /dev/null
@@ -1,124 +0,0 @@
-require 'rss/dublincore'
-require 'rss/maker/1.0'
-
-module RSS
- module Maker
- module DublinCoreModel
- def self.append_features(klass)
- super
-
- ::RSS::DublinCoreModel::ELEMENT_NAME_INFOS.each do |name, plural_name|
- plural_name ||= "#{name}s"
- full_name = "#{RSS::DC_PREFIX}_#{name}"
- full_plural_name = "#{RSS::DC_PREFIX}_#{plural_name}"
- klass_name = Utils.to_class_name(name)
- plural_klass_name = "DublinCore#{Utils.to_class_name(plural_name)}"
- full_plural_klass_name = "self.class::#{plural_klass_name}"
- full_klass_name = "#{full_plural_klass_name}::#{klass_name}"
- klass.def_classed_elements(full_name, "value", plural_klass_name,
- full_plural_name, name)
- klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- def new_#{full_name}(value=nil)
- _#{full_name} = #{full_plural_name}.new_#{name}
- _#{full_name}.value = value
- if block_given?
- yield _#{full_name}
- else
- _#{full_name}
- end
- end
- EOC
- end
-
- klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- # For backward compatibility
- alias #{DC_PREFIX}_rightses #{DC_PREFIX}_rights_list
- EOC
- end
-
- ::RSS::DublinCoreModel::ELEMENT_NAME_INFOS.each do |name, plural_name|
- plural_name ||= "#{name}s"
- full_name ||= "#{DC_PREFIX}_#{name}"
- full_plural_name ||= "#{DC_PREFIX}_#{plural_name}"
- klass_name = Utils.to_class_name(name)
- full_klass_name = "DublinCore#{klass_name}"
- plural_klass_name = "DublinCore#{Utils.to_class_name(plural_name)}"
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- class #{plural_klass_name}Base < Base
- def_array_element(#{name.dump}, #{full_plural_name.dump},
- #{full_klass_name.dump})
-
- class #{full_klass_name}Base < Base
- attr_accessor :value
- add_need_initialize_variable("value")
- alias_method(:content, :value)
- alias_method(:content=, :value=)
-
- def have_required_values?
- @value
- end
-
- def to_feed(feed, current)
- if value and current.respond_to?(:#{full_name})
- new_item = current.class::#{full_klass_name}.new(value)
- current.#{full_plural_name} << new_item
- end
- end
- end
- #{klass_name}Base = #{full_klass_name}Base
- end
- EOC
- end
-
- def self.install_dublin_core(klass)
- ::RSS::DublinCoreModel::ELEMENT_NAME_INFOS.each do |name, plural_name|
- plural_name ||= "#{name}s"
- klass_name = Utils.to_class_name(name)
- full_klass_name = "DublinCore#{klass_name}"
- plural_klass_name = "DublinCore#{Utils.to_class_name(plural_name)}"
- klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- class #{plural_klass_name} < #{plural_klass_name}Base
- class #{full_klass_name} < #{full_klass_name}Base
- end
- #{klass_name} = #{full_klass_name}
- end
-EOC
- end
- end
- end
-
- class ChannelBase
- include DublinCoreModel
- end
-
- class ImageBase; include DublinCoreModel; end
- class ItemsBase
- class ItemBase
- include DublinCoreModel
- end
- end
- class TextinputBase; include DublinCoreModel; end
-
- makers.each do |maker|
- maker.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- class Channel
- DublinCoreModel.install_dublin_core(self)
- end
-
- class Image
- DublinCoreModel.install_dublin_core(self)
- end
-
- class Items
- class Item
- DublinCoreModel.install_dublin_core(self)
- end
- end
-
- class Textinput
- DublinCoreModel.install_dublin_core(self)
- end
- EOC
- end
- end
-end
diff --git a/lib/rss/maker/entry.rb b/lib/rss/maker/entry.rb
deleted file mode 100644
index edaa31ec06..0000000000
--- a/lib/rss/maker/entry.rb
+++ /dev/null
@@ -1,163 +0,0 @@
-require "rss/maker/atom"
-require "rss/maker/feed"
-
-module RSS
- module Maker
- module Atom
- class Entry < RSSBase
- def initialize(feed_version="1.0")
- super
- @feed_type = "atom"
- @feed_subtype = "entry"
- end
-
- private
- def make_feed
- ::RSS::Atom::Entry.new(@version, @encoding, @standalone)
- end
-
- def setup_elements(entry)
- setup_items(entry)
- end
-
- class Channel < ChannelBase
- class SkipDays < SkipDaysBase
- class Day < DayBase
- end
- end
-
- class SkipHours < SkipHoursBase
- class Hour < HourBase
- end
- end
-
- class Cloud < CloudBase
- end
-
- Categories = Feed::Channel::Categories
- Links = Feed::Channel::Links
- Authors = Feed::Channel::Authors
- Contributors = Feed::Channel::Contributors
-
- class Generator < GeneratorBase
- include AtomGenerator
-
- def self.not_set_name
- "maker.channel.generator"
- end
- end
-
- Copyright = Feed::Channel::Copyright
-
- class Description < DescriptionBase
- end
-
- Title = Feed::Channel::Title
- end
-
- class Image < ImageBase
- end
-
- class Items < ItemsBase
- def to_feed(entry)
- (normalize.first || Item.new(@maker)).to_feed(entry)
- end
-
- class Item < ItemBase
- def to_feed(entry)
- set_default_values do
- setup_values(entry)
- entry.dc_dates.clear
- setup_other_elements(entry)
- unless have_required_values?
- raise NotSetError.new("maker.item", not_set_required_variables)
- end
- end
- end
-
- private
- def required_variable_names
- %w(id updated)
- end
-
- def variables
- super + ["updated"]
- end
-
- def variable_is_set?
- super or !authors.empty?
- end
-
- def not_set_required_variables
- set_default_values do
- vars = super
- if authors.all? {|author| !author.have_required_values?}
- vars << "author"
- end
- vars << "title" unless title {|t| t.have_required_values?}
- vars
- end
- end
-
- def _set_default_values(&block)
- keep = {
- :authors => authors.to_a.dup,
- :contributors => contributors.to_a.dup,
- :categories => categories.to_a.dup,
- :id => id,
- :links => links.to_a.dup,
- :rights => @rights,
- :title => @title,
- :updated => updated,
- }
- authors.replace(@maker.channel.authors) if keep[:authors].empty?
- if keep[:contributors].empty?
- contributors.replace(@maker.channel.contributors)
- end
- if keep[:categories].empty?
- categories.replace(@maker.channel.categories)
- end
- self.id ||= link || @maker.channel.id
- links.replace(@maker.channel.links) if keep[:links].empty?
- unless keep[:rights].variable_is_set?
- @maker.channel.rights {|r| @rights = r}
- end
- unless keep[:title].variable_is_set?
- @maker.channel.title {|t| @title = t}
- end
- self.updated ||= @maker.channel.updated
- super(&block)
- ensure
- authors.replace(keep[:authors])
- contributors.replace(keep[:contributors])
- categories.replace(keep[:categories])
- links.replace(keep[:links])
- self.id = keep[:id]
- @rights = keep[:rights]
- @title = keep[:title]
- self.updated = keep[:prev_updated]
- end
-
- Guid = Feed::Items::Item::Guid
- Enclosure = Feed::Items::Item::Enclosure
- Source = Feed::Items::Item::Source
- Categories = Feed::Items::Item::Categories
- Authors = Feed::Items::Item::Authors
- Contributors = Feed::Items::Item::Contributors
- Links = Feed::Items::Item::Links
- Rights = Feed::Items::Item::Rights
- Description = Feed::Items::Item::Description
- Title = Feed::Items::Item::Title
- Content = Feed::Items::Item::Content
- end
- end
-
- class Textinput < TextinputBase
- end
- end
- end
-
- add_maker("atom:entry", "1.0", Atom::Entry)
- add_maker("atom1.0:entry", "1.0", Atom::Entry)
- end
-end
diff --git a/lib/rss/maker/feed.rb b/lib/rss/maker/feed.rb
deleted file mode 100644
index 3a30ad4287..0000000000
--- a/lib/rss/maker/feed.rb
+++ /dev/null
@@ -1,429 +0,0 @@
-require "rss/maker/atom"
-
-module RSS
- module Maker
- module Atom
- class Feed < RSSBase
- def initialize(feed_version="1.0")
- super
- @feed_type = "atom"
- @feed_subtype = "feed"
- end
-
- private
- def make_feed
- ::RSS::Atom::Feed.new(@version, @encoding, @standalone)
- end
-
- def setup_elements(feed)
- setup_channel(feed)
- setup_image(feed)
- setup_items(feed)
- end
-
- class Channel < ChannelBase
- def to_feed(feed)
- set_default_values do
- setup_values(feed)
- feed.dc_dates.clear
- setup_other_elements(feed)
- if image_favicon.about
- icon = feed.class::Icon.new
- icon.content = image_favicon.about
- feed.icon = icon
- end
- unless have_required_values?
- raise NotSetError.new("maker.channel",
- not_set_required_variables)
- end
- end
- end
-
- def have_required_values?
- super and
- (!authors.empty? or
- @maker.items.any? {|item| !item.authors.empty?})
- end
-
- private
- def required_variable_names
- %w(id updated)
- end
-
- def variables
- super + %w(id updated)
- end
-
- def variable_is_set?
- super or !authors.empty?
- end
-
- def not_set_required_variables
- vars = super
- if authors.empty? and
- @maker.items.all? {|item| item.author.to_s.empty?}
- vars << "author"
- end
- vars << "title" unless title {|t| t.have_required_values?}
- vars
- end
-
- def _set_default_values(&block)
- keep = {
- :id => id,
- :updated => updated,
- }
- self.id ||= about
- self.updated ||= dc_date
- super(&block)
- ensure
- self.id = keep[:id]
- self.updated = keep[:updated]
- end
-
- class SkipDays < SkipDaysBase
- def to_feed(*args)
- end
-
- class Day < DayBase
- end
- end
-
- class SkipHours < SkipHoursBase
- def to_feed(*args)
- end
-
- class Hour < HourBase
- end
- end
-
- class Cloud < CloudBase
- def to_feed(*args)
- end
- end
-
- class Categories < CategoriesBase
- class Category < CategoryBase
- include AtomCategory
-
- def self.not_set_name
- "maker.channel.category"
- end
- end
- end
-
- class Links < LinksBase
- class Link < LinkBase
- include AtomLink
-
- def self.not_set_name
- "maker.channel.link"
- end
- end
- end
-
- AtomPersons.def_atom_persons(self, "author", "maker.channel.author")
- AtomPersons.def_atom_persons(self, "contributor",
- "maker.channel.contributor")
-
- class Generator < GeneratorBase
- include AtomGenerator
-
- def self.not_set_name
- "maker.channel.generator"
- end
- end
-
- AtomTextConstruct.def_atom_text_construct(self, "rights",
- "maker.channel.copyright",
- "Copyright")
- AtomTextConstruct.def_atom_text_construct(self, "subtitle",
- "maker.channel.description",
- "Description")
- AtomTextConstruct.def_atom_text_construct(self, "title",
- "maker.channel.title")
- end
-
- class Image < ImageBase
- def to_feed(feed)
- logo = feed.class::Logo.new
- class << logo
- alias_method(:url=, :content=)
- end
- set = setup_values(logo)
- class << logo
- remove_method(:url=)
- end
- if set
- feed.logo = logo
- set_parent(logo, feed)
- setup_other_elements(feed, logo)
- elsif variable_is_set?
- raise NotSetError.new("maker.image", not_set_required_variables)
- end
- end
-
- private
- def required_variable_names
- %w(url)
- end
- end
-
- class Items < ItemsBase
- def to_feed(feed)
- normalize.each do |item|
- item.to_feed(feed)
- end
- setup_other_elements(feed, feed.entries)
- end
-
- class Item < ItemBase
- def to_feed(feed)
- set_default_values do
- entry = feed.class::Entry.new
- set = setup_values(entry)
- setup_other_elements(feed, entry)
- if set
- feed.entries << entry
- set_parent(entry, feed)
- elsif variable_is_set?
- raise NotSetError.new("maker.item", not_set_required_variables)
- end
- end
- end
-
- def have_required_values?
- set_default_values do
- super and title {|t| t.have_required_values?}
- end
- end
-
- private
- def required_variable_names
- %w(id updated)
- end
-
- def variables
- super + ["updated"]
- end
-
- def not_set_required_variables
- vars = super
- vars << "title" unless title {|t| t.have_required_values?}
- vars
- end
-
- def _set_default_values(&block)
- keep = {
- :id => id,
- :updated => updated,
- }
- self.id ||= link
- self.updated ||= dc_date
- super(&block)
- ensure
- self.id = keep[:id]
- self.updated = keep[:updated]
- end
-
- class Guid < GuidBase
- def to_feed(feed, current)
- end
- end
-
- class Enclosure < EnclosureBase
- def to_feed(feed, current)
- end
- end
-
- class Source < SourceBase
- def to_feed(feed, current)
- source = current.class::Source.new
- setup_values(source)
- current.source = source
- set_parent(source, current)
- setup_other_elements(feed, source)
- current.source = nil if source.to_s == "<source/>"
- end
-
- private
- def required_variable_names
- []
- end
-
- def variables
- super + ["updated"]
- end
-
- AtomPersons.def_atom_persons(self, "author",
- "maker.item.source.author")
- AtomPersons.def_atom_persons(self, "contributor",
- "maker.item.source.contributor")
-
- class Categories < CategoriesBase
- class Category < CategoryBase
- include AtomCategory
-
- def self.not_set_name
- "maker.item.source.category"
- end
- end
- end
-
- class Generator < GeneratorBase
- include AtomGenerator
-
- def self.not_set_name
- "maker.item.source.generator"
- end
- end
-
- class Icon < IconBase
- def to_feed(feed, current)
- icon = current.class::Icon.new
- class << icon
- alias_method(:url=, :content=)
- end
- set = setup_values(icon)
- class << icon
- remove_method(:url=)
- end
- if set
- current.icon = icon
- set_parent(icon, current)
- setup_other_elements(feed, icon)
- elsif variable_is_set?
- raise NotSetError.new("maker.item.source.icon",
- not_set_required_variables)
- end
- end
-
- private
- def required_variable_names
- %w(url)
- end
- end
-
- class Links < LinksBase
- class Link < LinkBase
- include AtomLink
-
- def self.not_set_name
- "maker.item.source.link"
- end
- end
- end
-
- class Logo < LogoBase
- include AtomLogo
-
- def self.not_set_name
- "maker.item.source.logo"
- end
- end
-
- maker_name_base = "maker.item.source."
- maker_name = "#{maker_name_base}rights"
- AtomTextConstruct.def_atom_text_construct(self, "rights",
- maker_name)
- maker_name = "#{maker_name_base}subtitle"
- AtomTextConstruct.def_atom_text_construct(self, "subtitle",
- maker_name)
- maker_name = "#{maker_name_base}title"
- AtomTextConstruct.def_atom_text_construct(self, "title",
- maker_name)
- end
-
- class Categories < CategoriesBase
- class Category < CategoryBase
- include AtomCategory
-
- def self.not_set_name
- "maker.item.category"
- end
- end
- end
-
- AtomPersons.def_atom_persons(self, "author", "maker.item.author")
- AtomPersons.def_atom_persons(self, "contributor",
- "maker.item.contributor")
-
- class Links < LinksBase
- class Link < LinkBase
- include AtomLink
-
- def self.not_set_name
- "maker.item.link"
- end
- end
- end
-
- AtomTextConstruct.def_atom_text_construct(self, "rights",
- "maker.item.rights")
- AtomTextConstruct.def_atom_text_construct(self, "summary",
- "maker.item.description",
- "Description")
- AtomTextConstruct.def_atom_text_construct(self, "title",
- "maker.item.title")
-
- class Content < ContentBase
- def to_feed(feed, current)
- content = current.class::Content.new
- if setup_values(content)
- content.src = nil if content.src and content.content
- current.content = content
- set_parent(content, current)
- setup_other_elements(feed, content)
- elsif variable_is_set?
- raise NotSetError.new("maker.item.content",
- not_set_required_variables)
- end
- end
-
- alias_method(:xml, :xml_content)
-
- private
- def required_variable_names
- if out_of_line?
- %w(type)
- elsif xml_type?
- %w(xml_content)
- else
- %w(content)
- end
- end
-
- def variables
- if out_of_line?
- super
- elsif xml_type?
- super + %w(xml)
- else
- super
- end
- end
-
- def xml_type?
- _type = type
- return false if _type.nil?
- _type == "xhtml" or
- /(?:\+xml|\/xml)$/i =~ _type or
- %w(text/xml-external-parsed-entity
- application/xml-external-parsed-entity
- application/xml-dtd).include?(_type.downcase)
- end
- end
- end
- end
-
- class Textinput < TextinputBase
- end
- end
- end
-
- add_maker("atom", "1.0", Atom::Feed)
- add_maker("atom:feed", "1.0", Atom::Feed)
- add_maker("atom1.0", "1.0", Atom::Feed)
- add_maker("atom1.0:feed", "1.0", Atom::Feed)
- end
-end
diff --git a/lib/rss/maker/image.rb b/lib/rss/maker/image.rb
deleted file mode 100644
index b95cf4c714..0000000000
--- a/lib/rss/maker/image.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-require 'rss/image'
-require 'rss/maker/1.0'
-require 'rss/maker/dublincore'
-
-module RSS
- module Maker
- module ImageItemModel
- def self.append_features(klass)
- super
-
- name = "#{RSS::IMAGE_PREFIX}_item"
- klass.def_classed_element(name)
- end
-
- def self.install_image_item(klass)
- klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- class ImageItem < ImageItemBase
- DublinCoreModel.install_dublin_core(self)
- end
-EOC
- end
-
- class ImageItemBase < Base
- include Maker::DublinCoreModel
-
- attr_accessor :about, :resource, :image_width, :image_height
- add_need_initialize_variable("about")
- add_need_initialize_variable("resource")
- add_need_initialize_variable("image_width")
- add_need_initialize_variable("image_height")
- alias width= image_width=
- alias width image_width
- alias height= image_height=
- alias height image_height
-
- def have_required_values?
- @about
- end
-
- def to_feed(feed, current)
- if current.respond_to?(:image_item=) and have_required_values?
- item = current.class::ImageItem.new
- setup_values(item)
- setup_other_elements(item)
- current.image_item = item
- end
- end
- end
- end
-
- module ImageFaviconModel
- def self.append_features(klass)
- super
-
- name = "#{RSS::IMAGE_PREFIX}_favicon"
- klass.def_classed_element(name)
- end
-
- def self.install_image_favicon(klass)
- klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- class ImageFavicon < ImageFaviconBase
- DublinCoreModel.install_dublin_core(self)
- end
- EOC
- end
-
- class ImageFaviconBase < Base
- include Maker::DublinCoreModel
-
- attr_accessor :about, :image_size
- add_need_initialize_variable("about")
- add_need_initialize_variable("image_size")
- alias size image_size
- alias size= image_size=
-
- def have_required_values?
- @about and @image_size
- end
-
- def to_feed(feed, current)
- if current.respond_to?(:image_favicon=) and have_required_values?
- favicon = current.class::ImageFavicon.new
- setup_values(favicon)
- setup_other_elements(favicon)
- current.image_favicon = favicon
- end
- end
- end
- end
-
- class ChannelBase; include Maker::ImageFaviconModel; end
-
- class ItemsBase
- class ItemBase; include Maker::ImageItemModel; end
- end
-
- makers.each do |maker|
- maker.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- class Channel
- ImageFaviconModel.install_image_favicon(self)
- end
-
- class Items
- class Item
- ImageItemModel.install_image_item(self)
- end
- end
- EOC
- end
- end
-end
diff --git a/lib/rss/maker/itunes.rb b/lib/rss/maker/itunes.rb
deleted file mode 100644
index 8b7420da3c..0000000000
--- a/lib/rss/maker/itunes.rb
+++ /dev/null
@@ -1,242 +0,0 @@
-require 'rss/itunes'
-require 'rss/maker/2.0'
-
-module RSS
- module Maker
- module ITunesBaseModel
- def def_class_accessor(klass, name, type, *args)
- name = name.gsub(/-/, "_").gsub(/^itunes_/, '')
- full_name = "#{RSS::ITUNES_PREFIX}_#{name}"
- case type
- when nil
- klass.def_other_element(full_name)
- when :yes_other
- def_yes_other_accessor(klass, full_name)
- when :yes_clean_other
- def_yes_clean_other_accessor(klass, full_name)
- when :csv
- def_csv_accessor(klass, full_name)
- when :element, :attribute
- recommended_attribute_name, = *args
- klass_name = "ITunes#{Utils.to_class_name(name)}"
- klass.def_classed_element(full_name, klass_name,
- recommended_attribute_name)
- when :elements
- plural_name, recommended_attribute_name = args
- plural_name ||= "#{name}s"
- full_plural_name = "#{RSS::ITUNES_PREFIX}_#{plural_name}"
- klass_name = "ITunes#{Utils.to_class_name(name)}"
- plural_klass_name = "ITunes#{Utils.to_class_name(plural_name)}"
- def_elements_class_accessor(klass, name, full_name, full_plural_name,
- klass_name, plural_klass_name,
- recommended_attribute_name)
- end
- end
-
- def def_yes_other_accessor(klass, full_name)
- klass.def_other_element(full_name)
- klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- def #{full_name}?
- Utils::YesOther.parse(@#{full_name})
- end
- EOC
- end
-
- def def_yes_clean_other_accessor(klass, full_name)
- klass.def_other_element(full_name)
- klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- def #{full_name}?
- Utils::YesCleanOther.parse(#{full_name})
- end
- EOC
- end
-
- def def_csv_accessor(klass, full_name)
- klass.def_csv_element(full_name)
- end
-
- def def_elements_class_accessor(klass, name, full_name, full_plural_name,
- klass_name, plural_klass_name,
- recommended_attribute_name=nil)
- if recommended_attribute_name
- klass.def_classed_elements(full_name, recommended_attribute_name,
- plural_klass_name, full_plural_name)
- else
- klass.def_classed_element(full_plural_name, plural_klass_name)
- end
- klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- def new_#{full_name}(text=nil)
- #{full_name} = @#{full_plural_name}.new_#{name}
- #{full_name}.text = text
- if block_given?
- yield #{full_name}
- else
- #{full_name}
- end
- end
- EOC
- end
- end
-
- module ITunesChannelModel
- extend ITunesBaseModel
-
- class << self
- def append_features(klass)
- super
-
- ::RSS::ITunesChannelModel::ELEMENT_INFOS.each do |name, type, *args|
- def_class_accessor(klass, name, type, *args)
- end
- end
- end
-
- class ITunesCategoriesBase < Base
- def_array_element("category", "itunes_categories",
- "ITunesCategory")
- class ITunesCategoryBase < Base
- attr_accessor :text
- add_need_initialize_variable("text")
- def_array_element("category", "itunes_categories",
- "ITunesCategory")
-
- def have_required_values?
- text
- end
-
- alias_method :to_feed_for_categories, :to_feed
- def to_feed(feed, current)
- if text and current.respond_to?(:itunes_category)
- new_item = current.class::ITunesCategory.new(text)
- to_feed_for_categories(feed, new_item)
- current.itunes_categories << new_item
- end
- end
- end
- end
-
- class ITunesImageBase < Base
- add_need_initialize_variable("href")
- attr_accessor("href")
-
- def to_feed(feed, current)
- if @href and current.respond_to?(:itunes_image)
- current.itunes_image ||= current.class::ITunesImage.new
- current.itunes_image.href = @href
- end
- end
- end
-
- class ITunesOwnerBase < Base
- %w(itunes_name itunes_email).each do |name|
- add_need_initialize_variable(name)
- attr_accessor(name)
- end
-
- def to_feed(feed, current)
- if current.respond_to?(:itunes_owner=)
- _not_set_required_variables = not_set_required_variables
- if (required_variable_names - _not_set_required_variables).empty?
- return
- end
-
- unless have_required_values?
- raise NotSetError.new("maker.channel.itunes_owner",
- _not_set_required_variables)
- end
- current.itunes_owner ||= current.class::ITunesOwner.new
- current.itunes_owner.itunes_name = @itunes_name
- current.itunes_owner.itunes_email = @itunes_email
- end
- end
-
- private
- def required_variable_names
- %w(itunes_name itunes_email)
- end
- end
- end
-
- module ITunesItemModel
- extend ITunesBaseModel
-
- class << self
- def append_features(klass)
- super
-
- ::RSS::ITunesItemModel::ELEMENT_INFOS.each do |name, type, *args|
- def_class_accessor(klass, name, type, *args)
- end
- end
- end
-
- class ITunesDurationBase < Base
- attr_reader :content
- add_need_initialize_variable("content")
-
- %w(hour minute second).each do |name|
- attr_reader(name)
- add_need_initialize_variable(name, 0)
- end
-
- def content=(content)
- if content.nil?
- @hour, @minute, @second, @content = nil
- else
- @hour, @minute, @second =
- ::RSS::ITunesItemModel::ITunesDuration.parse(content)
- @content = content
- end
- end
-
- def hour=(hour)
- @hour = Integer(hour)
- update_content
- end
-
- def minute=(minute)
- @minute = Integer(minute)
- update_content
- end
-
- def second=(second)
- @second = Integer(second)
- update_content
- end
-
- def to_feed(feed, current)
- if @content and current.respond_to?(:itunes_duration=)
- current.itunes_duration ||= current.class::ITunesDuration.new
- current.itunes_duration.content = @content
- end
- end
-
- private
- def update_content
- components = [@hour, @minute, @second]
- @content =
- ::RSS::ITunesItemModel::ITunesDuration.construct(*components)
- end
- end
- end
-
- class ChannelBase
- include Maker::ITunesChannelModel
- class ITunesCategories < ITunesCategoriesBase
- class ITunesCategory < ITunesCategoryBase
- ITunesCategory = self
- end
- end
-
- class ITunesImage < ITunesImageBase; end
- class ITunesOwner < ITunesOwnerBase; end
- end
-
- class ItemsBase
- class ItemBase
- include Maker::ITunesItemModel
- class ITunesDuration < ITunesDurationBase; end
- end
- end
- end
-end
diff --git a/lib/rss/maker/slash.rb b/lib/rss/maker/slash.rb
deleted file mode 100644
index 27adef3832..0000000000
--- a/lib/rss/maker/slash.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-require 'rss/slash'
-require 'rss/maker/1.0'
-
-module RSS
- module Maker
- module SlashModel
- def self.append_features(klass)
- super
-
- ::RSS::SlashModel::ELEMENT_INFOS.each do |name, type|
- full_name = "#{RSS::SLASH_PREFIX}_#{name}"
- case type
- when :csv_integer
- klass.def_csv_element(full_name, :integer)
- else
- klass.def_other_element(full_name)
- end
- end
-
- klass.module_eval do
- alias_method(:slash_hit_parades, :slash_hit_parade)
- alias_method(:slash_hit_parades=, :slash_hit_parade=)
- end
- end
- end
-
- class ItemsBase
- class ItemBase
- include SlashModel
- end
- end
- end
-end
diff --git a/lib/rss/maker/syndication.rb b/lib/rss/maker/syndication.rb
deleted file mode 100644
index b81230457c..0000000000
--- a/lib/rss/maker/syndication.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'rss/syndication'
-require 'rss/maker/1.0'
-
-module RSS
- module Maker
- module SyndicationModel
- def self.append_features(klass)
- super
-
- ::RSS::SyndicationModel::ELEMENTS.each do |name|
- klass.def_other_element(name)
- end
- end
- end
-
- class ChannelBase; include SyndicationModel; end
- end
-end
diff --git a/lib/rss/maker/taxonomy.rb b/lib/rss/maker/taxonomy.rb
deleted file mode 100644
index 211603840f..0000000000
--- a/lib/rss/maker/taxonomy.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-require 'rss/taxonomy'
-require 'rss/maker/1.0'
-require 'rss/maker/dublincore'
-
-module RSS
- module Maker
- module TaxonomyTopicsModel
- def self.append_features(klass)
- super
-
- klass.def_classed_element("#{RSS::TAXO_PREFIX}_topics",
- "TaxonomyTopics")
- end
-
- def self.install_taxo_topics(klass)
- klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- class TaxonomyTopics < TaxonomyTopicsBase
- def to_feed(feed, current)
- if current.respond_to?(:taxo_topics)
- topics = current.class::TaxonomyTopics.new
- bag = topics.Bag
- @resources.each do |resource|
- bag.lis << RDF::Bag::Li.new(resource)
- end
- current.taxo_topics = topics
- end
- end
- end
-EOC
- end
-
- class TaxonomyTopicsBase < Base
- attr_reader :resources
- def_array_element("resource")
- remove_method :new_resource
- end
- end
-
- module TaxonomyTopicModel
- def self.append_features(klass)
- super
-
- class_name = "TaxonomyTopics"
- klass.def_classed_elements("#{TAXO_PREFIX}_topic", "value", class_name)
- end
-
- def self.install_taxo_topic(klass)
- klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- class TaxonomyTopics < TaxonomyTopicsBase
- class TaxonomyTopic < TaxonomyTopicBase
- DublinCoreModel.install_dublin_core(self)
- TaxonomyTopicsModel.install_taxo_topics(self)
-
- def to_feed(feed, current)
- if current.respond_to?(:taxo_topics)
- topic = current.class::TaxonomyTopic.new(value)
- topic.taxo_link = value
- taxo_topics.to_feed(feed, topic) if taxo_topics
- current.taxo_topics << topic
- setup_other_elements(feed, topic)
- end
- end
- end
- end
-EOC
- end
-
- class TaxonomyTopicsBase < Base
- def_array_element("topic", nil, "TaxonomyTopic")
- alias_method(:new_taxo_topic, :new_topic) # For backward compatibility
-
- class TaxonomyTopicBase < Base
- include DublinCoreModel
- include TaxonomyTopicsModel
-
- attr_accessor :value
- add_need_initialize_variable("value")
- alias_method(:taxo_link, :value)
- alias_method(:taxo_link=, :value=)
-
- def have_required_values?
- @value
- end
- end
- end
- end
-
- class RSSBase
- include TaxonomyTopicModel
- end
-
- class ChannelBase
- include TaxonomyTopicsModel
- end
-
- class ItemsBase
- class ItemBase
- include TaxonomyTopicsModel
- end
- end
-
- makers.each do |maker|
- maker.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- TaxonomyTopicModel.install_taxo_topic(self)
-
- class Channel
- TaxonomyTopicsModel.install_taxo_topics(self)
- end
-
- class Items
- class Item
- TaxonomyTopicsModel.install_taxo_topics(self)
- end
- end
- EOC
- end
- end
-end
diff --git a/lib/rss/maker/trackback.rb b/lib/rss/maker/trackback.rb
deleted file mode 100644
index 278fe53ebe..0000000000
--- a/lib/rss/maker/trackback.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-require 'rss/trackback'
-require 'rss/maker/1.0'
-require 'rss/maker/2.0'
-
-module RSS
- module Maker
- module TrackBackModel
- def self.append_features(klass)
- super
-
- klass.def_other_element("#{RSS::TRACKBACK_PREFIX}_ping")
- klass.def_classed_elements("#{RSS::TRACKBACK_PREFIX}_about", "value",
- "TrackBackAbouts")
- end
-
- class TrackBackAboutsBase < Base
- def_array_element("about", nil, "TrackBackAbout")
-
- class TrackBackAboutBase < Base
- attr_accessor :value
- add_need_initialize_variable("value")
-
- alias_method(:resource, :value)
- alias_method(:resource=, :value=)
- alias_method(:content, :value)
- alias_method(:content=, :value=)
-
- def have_required_values?
- @value
- end
-
- def to_feed(feed, current)
- if current.respond_to?(:trackback_abouts) and have_required_values?
- about = current.class::TrackBackAbout.new
- setup_values(about)
- setup_other_elements(about)
- current.trackback_abouts << about
- end
- end
- end
- end
- end
-
- class ItemsBase
- class ItemBase; include TrackBackModel; end
- end
-
- makers.each do |maker|
- maker.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- class Items
- class Item
- class TrackBackAbouts < TrackBackAboutsBase
- class TrackBackAbout < TrackBackAboutBase
- end
- end
- end
- end
- EOC
- end
- end
-end
diff --git a/lib/rss/parser.rb b/lib/rss/parser.rb
deleted file mode 100644
index 9b28f0fa8a..0000000000
--- a/lib/rss/parser.rb
+++ /dev/null
@@ -1,551 +0,0 @@
-require "forwardable"
-require "open-uri"
-
-require "rss/rss"
-require "rss/xml"
-
-module RSS
-
- class NotWellFormedError < Error
- attr_reader :line, :element
-
- # Create a new NotWellFormedError for an error at +line+
- # in +element+. If a block is given the return value of
- # the block ends up in the error message.
- def initialize(line=nil, element=nil)
- message = "This is not well formed XML"
- if element or line
- message << "\nerror occurred"
- message << " in #{element}" if element
- message << " at about #{line} line" if line
- end
- message << "\n#{yield}" if block_given?
- super(message)
- end
- end
-
- class XMLParserNotFound < Error
- def initialize
- super("available XML parser was not found in " <<
- "#{AVAILABLE_PARSER_LIBRARIES.inspect}.")
- end
- end
-
- class NotValidXMLParser < Error
- def initialize(parser)
- super("#{parser} is not an available XML parser. " <<
- "Available XML parser" <<
- (AVAILABLE_PARSERS.size > 1 ? "s are " : " is ") <<
- "#{AVAILABLE_PARSERS.inspect}.")
- end
- end
-
- class NSError < InvalidRSSError
- attr_reader :tag, :prefix, :uri
- def initialize(tag, prefix, require_uri)
- @tag, @prefix, @uri = tag, prefix, require_uri
- super("prefix <#{prefix}> doesn't associate uri " <<
- "<#{require_uri}> in tag <#{tag}>")
- end
- end
-
- class Parser
-
- extend Forwardable
-
- class << self
-
- @@default_parser = nil
-
- def default_parser
- @@default_parser || AVAILABLE_PARSERS.first
- end
-
- # Set @@default_parser to new_value if it is one of the
- # available parsers. Else raise NotValidXMLParser error.
- def default_parser=(new_value)
- if AVAILABLE_PARSERS.include?(new_value)
- @@default_parser = new_value
- else
- raise NotValidXMLParser.new(new_value)
- end
- end
-
- def parse(rss, do_validate=true, ignore_unknown_element=true,
- parser_class=default_parser)
- parser = new(rss, parser_class)
- parser.do_validate = do_validate
- parser.ignore_unknown_element = ignore_unknown_element
- parser.parse
- end
- end
-
- def_delegators(:@parser, :parse, :rss,
- :ignore_unknown_element,
- :ignore_unknown_element=, :do_validate,
- :do_validate=)
-
- def initialize(rss, parser_class=self.class.default_parser)
- @parser = parser_class.new(normalize_rss(rss))
- end
-
- private
-
- # Try to get the XML associated with +rss+.
- # Return +rss+ if it already looks like XML, or treat it as a URI,
- # or a file to get the XML,
- def normalize_rss(rss)
- return rss if maybe_xml?(rss)
-
- uri = to_uri(rss)
-
- if uri.respond_to?(:read)
- uri.read
- elsif !rss.tainted? and File.readable?(rss)
- File.open(rss) {|f| f.read}
- else
- rss
- end
- end
-
- # maybe_xml? tests if source is a string that looks like XML.
- def maybe_xml?(source)
- source.is_a?(String) and /</ =~ source
- end
-
- # Attempt to convert rss to a URI, but just return it if
- # there's a ::URI::Error
- def to_uri(rss)
- return rss if rss.is_a?(::URI::Generic)
-
- begin
- ::URI.parse(rss)
- rescue ::URI::Error
- rss
- end
- end
- end
-
- class BaseParser
-
- class << self
- def raise_for_undefined_entity?
- listener.raise_for_undefined_entity?
- end
- end
-
- def initialize(rss)
- @listener = self.class.listener.new
- @rss = rss
- end
-
- def rss
- @listener.rss
- end
-
- def ignore_unknown_element
- @listener.ignore_unknown_element
- end
-
- def ignore_unknown_element=(new_value)
- @listener.ignore_unknown_element = new_value
- end
-
- def do_validate
- @listener.do_validate
- end
-
- def do_validate=(new_value)
- @listener.do_validate = new_value
- end
-
- def parse
- if @listener.rss.nil?
- _parse
- end
- @listener.rss
- end
-
- end
-
- class BaseListener
-
- extend Utils
-
- class << self
-
- @@accessor_bases = {}
- @@registered_uris = {}
- @@class_names = {}
-
- # return the setter for the uri, tag_name pair, or nil.
- def setter(uri, tag_name)
- _getter = getter(uri, tag_name)
- if _getter
- "#{_getter}="
- else
- nil
- end
- end
-
- def getter(uri, tag_name)
- (@@accessor_bases[uri] || {})[tag_name]
- end
-
- # return the tag_names for setters associated with uri
- def available_tags(uri)
- (@@accessor_bases[uri] || {}).keys
- end
-
- # register uri against this name.
- def register_uri(uri, name)
- @@registered_uris[name] ||= {}
- @@registered_uris[name][uri] = nil
- end
-
- # test if this uri is registered against this name
- def uri_registered?(uri, name)
- @@registered_uris[name].has_key?(uri)
- end
-
- # record class_name for the supplied uri and tag_name
- def install_class_name(uri, tag_name, class_name)
- @@class_names[uri] ||= {}
- @@class_names[uri][tag_name] = class_name
- end
-
- # retrieve class_name for the supplied uri and tag_name
- # If it doesn't exist, capitalize the tag_name
- def class_name(uri, tag_name)
- name = (@@class_names[uri] || {})[tag_name]
- return name if name
-
- tag_name = tag_name.gsub(/[_\-]([a-z]?)/) {$1.upcase}
- tag_name[0, 1].upcase + tag_name[1..-1]
- end
-
- def install_get_text_element(uri, name, accessor_base)
- install_accessor_base(uri, name, accessor_base)
- def_get_text_element(uri, name, *get_file_and_line_from_caller(1))
- end
-
- def raise_for_undefined_entity?
- true
- end
-
- private
- # set the accessor for the uri, tag_name pair
- def install_accessor_base(uri, tag_name, accessor_base)
- @@accessor_bases[uri] ||= {}
- @@accessor_bases[uri][tag_name] = accessor_base.chomp("=")
- end
-
- def def_get_text_element(uri, element_name, file, line)
- register_uri(uri, element_name)
- method_name = "start_#{element_name}"
- unless private_method_defined?(method_name)
- define_method(method_name) do |name, prefix, attrs, ns|
- uri = _ns(ns, prefix)
- if self.class.uri_registered?(uri, element_name)
- start_get_text_element(name, prefix, ns, uri)
- else
- start_else_element(name, prefix, attrs, ns)
- end
- end
- private(method_name)
- end
- end
- end
- end
-
- module ListenerMixin
- attr_reader :rss
-
- attr_accessor :ignore_unknown_element
- attr_accessor :do_validate
-
- def initialize
- @rss = nil
- @ignore_unknown_element = true
- @do_validate = true
- @ns_stack = [{"xml" => :xml}]
- @tag_stack = [[]]
- @text_stack = ['']
- @proc_stack = []
- @last_element = nil
- @version = @encoding = @standalone = nil
- @xml_stylesheets = []
- @xml_child_mode = false
- @xml_element = nil
- @last_xml_element = nil
- end
-
- # set instance vars for version, encoding, standalone
- def xmldecl(version, encoding, standalone)
- @version, @encoding, @standalone = version, encoding, standalone
- end
-
- def instruction(name, content)
- if name == "xml-stylesheet"
- params = parse_pi_content(content)
- if params.has_key?("href")
- @xml_stylesheets << XMLStyleSheet.new(params)
- end
- end
- end
-
- def tag_start(name, attributes)
- @text_stack.push('')
-
- ns = @ns_stack.last.dup
- attrs = {}
- attributes.each do |n, v|
- if /\Axmlns(?:\z|:)/ =~ n
- ns[$POSTMATCH] = v
- else
- attrs[n] = v
- end
- end
- @ns_stack.push(ns)
-
- prefix, local = split_name(name)
- @tag_stack.last.push([_ns(ns, prefix), local])
- @tag_stack.push([])
- if @xml_child_mode
- previous = @last_xml_element
- element_attrs = attributes.dup
- unless previous
- ns.each do |ns_prefix, value|
- next if ns_prefix == "xml"
- key = ns_prefix.empty? ? "xmlns" : "xmlns:#{ns_prefix}"
- element_attrs[key] ||= value
- end
- end
- next_element = XML::Element.new(local,
- prefix.empty? ? nil : prefix,
- _ns(ns, prefix),
- element_attrs)
- previous << next_element if previous
- @last_xml_element = next_element
- pr = Proc.new do |text, tags|
- if previous
- @last_xml_element = previous
- else
- @xml_element = @last_xml_element
- @last_xml_element = nil
- end
- end
- @proc_stack.push(pr)
- else
- if @rss.nil? and respond_to?("initial_start_#{local}", true)
- __send__("initial_start_#{local}", local, prefix, attrs, ns.dup)
- elsif respond_to?("start_#{local}", true)
- __send__("start_#{local}", local, prefix, attrs, ns.dup)
- else
- start_else_element(local, prefix, attrs, ns.dup)
- end
- end
- end
-
- def tag_end(name)
- if DEBUG
- p "end tag #{name}"
- p @tag_stack
- end
- text = @text_stack.pop
- tags = @tag_stack.pop
- pr = @proc_stack.pop
- pr.call(text, tags) unless pr.nil?
- @ns_stack.pop
- end
-
- def text(data)
- if @xml_child_mode
- @last_xml_element << data if @last_xml_element
- else
- @text_stack.last << data
- end
- end
-
- private
- def _ns(ns, prefix)
- ns.fetch(prefix, "")
- end
-
- CONTENT_PATTERN = /\s*([^=]+)=(["'])([^\2]+?)\2/
- # Extract the first name="value" pair from content.
- # Works with single quotes according to the constant
- # CONTENT_PATTERN. Return a Hash.
- def parse_pi_content(content)
- params = {}
- content.scan(CONTENT_PATTERN) do |name, quote, value|
- params[name] = value
- end
- params
- end
-
- def start_else_element(local, prefix, attrs, ns)
- class_name = self.class.class_name(_ns(ns, prefix), local)
- current_class = @last_element.class
- if known_class?(current_class, class_name)
- next_class = current_class.const_get(class_name)
- start_have_something_element(local, prefix, attrs, ns, next_class)
- else
- if !@do_validate or @ignore_unknown_element
- @proc_stack.push(nil)
- else
- parent = "ROOT ELEMENT???"
- if current_class.tag_name
- parent = current_class.tag_name
- end
- raise NotExpectedTagError.new(local, _ns(ns, prefix), parent)
- end
- end
- end
-
- if Module.method(:const_defined?).arity == -1
- def known_class?(target_class, class_name)
- class_name and
- (target_class.const_defined?(class_name, false) or
- target_class.constants.include?(class_name.to_sym))
- end
- else
- def known_class?(target_class, class_name)
- class_name and
- (target_class.const_defined?(class_name) or
- target_class.constants.include?(class_name))
- end
- end
-
- NAMESPLIT = /^(?:([\w:][-\w\d.]*):)?([\w:][-\w\d.]*)/
- def split_name(name)
- name =~ NAMESPLIT
- [$1 || '', $2]
- end
-
- def check_ns(tag_name, prefix, ns, require_uri)
- unless _ns(ns, prefix) == require_uri
- if @do_validate
- raise NSError.new(tag_name, prefix, require_uri)
- else
- # Force bind required URI with prefix
- @ns_stack.last[prefix] = require_uri
- end
- end
- end
-
- def start_get_text_element(tag_name, prefix, ns, required_uri)
- pr = Proc.new do |text, tags|
- setter = self.class.setter(required_uri, tag_name)
- if @last_element.respond_to?(setter)
- if @do_validate
- getter = self.class.getter(required_uri, tag_name)
- if @last_element.__send__(getter)
- raise TooMuchTagError.new(tag_name, @last_element.tag_name)
- end
- end
- @last_element.__send__(setter, text.to_s)
- else
- if @do_validate and !@ignore_unknown_element
- raise NotExpectedTagError.new(tag_name, _ns(ns, prefix),
- @last_element.tag_name)
- end
- end
- end
- @proc_stack.push(pr)
- end
-
- def start_have_something_element(tag_name, prefix, attrs, ns, klass)
- check_ns(tag_name, prefix, ns, klass.required_uri)
- attributes = collect_attributes(tag_name, prefix, attrs, ns, klass)
- @proc_stack.push(setup_next_element(tag_name, klass, attributes))
- end
-
- def collect_attributes(tag_name, prefix, attrs, ns, klass)
- attributes = {}
- klass.get_attributes.each do |a_name, a_uri, required, element_name|
- if a_uri.is_a?(String) or !a_uri.respond_to?(:include?)
- a_uri = [a_uri]
- end
- unless a_uri == [""]
- for prefix, uri in ns
- if a_uri.include?(uri)
- val = attrs["#{prefix}:#{a_name}"]
- break if val
- end
- end
- end
- if val.nil? and a_uri.include?("")
- val = attrs[a_name]
- end
-
- if @do_validate and required and val.nil?
- unless a_uri.include?("")
- for prefix, uri in ns
- if a_uri.include?(uri)
- a_name = "#{prefix}:#{a_name}"
- end
- end
- end
- raise MissingAttributeError.new(tag_name, a_name)
- end
-
- attributes[a_name] = val
- end
- attributes
- end
-
- def setup_next_element(tag_name, klass, attributes)
- previous = @last_element
- next_element = klass.new(@do_validate, attributes)
- previous.set_next_element(tag_name, next_element)
- @last_element = next_element
- @last_element.parent = previous if klass.need_parent?
- @xml_child_mode = @last_element.have_xml_content?
-
- Proc.new do |text, tags|
- p(@last_element.class) if DEBUG
- if @xml_child_mode
- @last_element.content = @xml_element.to_s
- xml_setter = @last_element.class.xml_setter
- @last_element.__send__(xml_setter, @xml_element)
- @xml_element = nil
- @xml_child_mode = false
- else
- if klass.have_content?
- if @last_element.need_base64_encode?
- text = text.lstrip.unpack("m").first
- end
- @last_element.content = text
- end
- end
- if @do_validate
- @last_element.validate_for_stream(tags, @ignore_unknown_element)
- end
- @last_element = previous
- end
- end
- end
-
- unless const_defined? :AVAILABLE_PARSER_LIBRARIES
- AVAILABLE_PARSER_LIBRARIES = [
- ["rss/xmlparser", :XMLParserParser],
- ["rss/xmlscanner", :XMLScanParser],
- ["rss/rexmlparser", :REXMLParser],
- ]
- end
-
- AVAILABLE_PARSERS = []
-
- AVAILABLE_PARSER_LIBRARIES.each do |lib, parser|
- begin
- require lib
- AVAILABLE_PARSERS.push(const_get(parser))
- rescue LoadError
- end
- end
-
- if AVAILABLE_PARSERS.empty?
- raise XMLParserNotFound
- end
-end
diff --git a/lib/rss/rexmlparser.rb b/lib/rss/rexmlparser.rb
deleted file mode 100644
index 4dabf59199..0000000000
--- a/lib/rss/rexmlparser.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-require "rexml/document"
-require "rexml/streamlistener"
-
-/\A(\d+)\.(\d+)(?:\.\d+)+\z/ =~ REXML::Version
-if ([$1.to_i, $2.to_i] <=> [2, 5]) < 0
- raise LoadError, "needs REXML 2.5 or later (#{REXML::Version})"
-end
-
-module RSS
-
- class REXMLParser < BaseParser
-
- class << self
- def listener
- REXMLListener
- end
- end
-
- private
- def _parse
- begin
- REXML::Document.parse_stream(@rss, @listener)
- rescue RuntimeError => e
- raise NotWellFormedError.new{e.message}
- rescue REXML::ParseException => e
- context = e.context
- line = context[0] if context
- raise NotWellFormedError.new(line){e.message}
- end
- end
-
- end
-
- class REXMLListener < BaseListener
-
- include REXML::StreamListener
- include ListenerMixin
-
- class << self
- def raise_for_undefined_entity?
- false
- end
- end
-
- def xmldecl(version, encoding, standalone)
- super(version, encoding, standalone == "yes")
- # Encoding is converted to UTF-8 when REXML parse XML.
- @encoding = 'UTF-8'
- end
-
- alias_method(:cdata, :text)
- end
-
-end
diff --git a/lib/rss/rss.rb b/lib/rss/rss.rb
deleted file mode 100644
index 4b943ec55b..0000000000
--- a/lib/rss/rss.rb
+++ /dev/null
@@ -1,1313 +0,0 @@
-require "time"
-
-class Time
- class << self
- unless respond_to?(:w3cdtf)
- def w3cdtf(date)
- if /\A\s*
- (-?\d+)-(\d\d)-(\d\d)
- (?:T
- (\d\d):(\d\d)(?::(\d\d))?
- (\.\d+)?
- (Z|[+-]\d\d:\d\d)?)?
- \s*\z/ix =~ date and (($5 and $8) or (!$5 and !$8))
- datetime = [$1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i]
- usec = 0
- usec = $7.to_f * 1000000 if $7
- zone = $8
- if zone
- off = zone_offset(zone, datetime[0])
- datetime = apply_offset(*(datetime + [off]))
- datetime << usec
- time = Time.utc(*datetime)
- time.localtime unless zone_utc?(zone)
- time
- else
- datetime << usec
- Time.local(*datetime)
- end
- else
- raise ArgumentError.new("invalid date: #{date.inspect}")
- end
- end
- end
- end
-
- unless method_defined?(:w3cdtf)
- def w3cdtf
- if usec.zero?
- fraction_digits = 0
- else
- fraction_digits = Math.log10(usec.to_s.sub(/0*$/, '').to_i).floor + 1
- end
- xmlschema(fraction_digits)
- end
- end
-end
-
-
-require "English"
-require "rss/utils"
-require "rss/converter"
-require "rss/xml-stylesheet"
-
-module RSS
-
- VERSION = "0.2.5"
-
- URI = "http://purl.org/rss/1.0/"
-
- DEBUG = false
-
- class Error < StandardError; end
-
- class OverlappedPrefixError < Error
- attr_reader :prefix
- def initialize(prefix)
- @prefix = prefix
- end
- end
-
- class InvalidRSSError < Error; end
-
- class MissingTagError < InvalidRSSError
- attr_reader :tag, :parent
- def initialize(tag, parent)
- @tag, @parent = tag, parent
- super("tag <#{tag}> is missing in tag <#{parent}>")
- end
- end
-
- class TooMuchTagError < InvalidRSSError
- attr_reader :tag, :parent
- def initialize(tag, parent)
- @tag, @parent = tag, parent
- super("tag <#{tag}> is too much in tag <#{parent}>")
- end
- end
-
- class MissingAttributeError < InvalidRSSError
- attr_reader :tag, :attribute
- def initialize(tag, attribute)
- @tag, @attribute = tag, attribute
- super("attribute <#{attribute}> is missing in tag <#{tag}>")
- end
- end
-
- class UnknownTagError < InvalidRSSError
- attr_reader :tag, :uri
- def initialize(tag, uri)
- @tag, @uri = tag, uri
- super("tag <#{tag}> is unknown in namespace specified by uri <#{uri}>")
- end
- end
-
- class NotExpectedTagError < InvalidRSSError
- attr_reader :tag, :uri, :parent
- def initialize(tag, uri, parent)
- @tag, @uri, @parent = tag, uri, parent
- super("tag <{#{uri}}#{tag}> is not expected in tag <#{parent}>")
- end
- end
- # For backward compatibility :X
- NotExceptedTagError = NotExpectedTagError
-
- class NotAvailableValueError < InvalidRSSError
- attr_reader :tag, :value, :attribute
- def initialize(tag, value, attribute=nil)
- @tag, @value, @attribute = tag, value, attribute
- message = "value <#{value}> of "
- message << "attribute <#{attribute}> of " if attribute
- message << "tag <#{tag}> is not available."
- super(message)
- end
- end
-
- class UnknownConversionMethodError < Error
- attr_reader :to, :from
- def initialize(to, from)
- @to = to
- @from = from
- super("can't convert to #{to} from #{from}.")
- end
- end
- # for backward compatibility
- UnknownConvertMethod = UnknownConversionMethodError
-
- class ConversionError < Error
- attr_reader :string, :to, :from
- def initialize(string, to, from)
- @string = string
- @to = to
- @from = from
- super("can't convert #{@string} to #{to} from #{from}.")
- end
- end
-
- class NotSetError < Error
- attr_reader :name, :variables
- def initialize(name, variables)
- @name = name
- @variables = variables
- super("required variables of #{@name} are not set: #{@variables.join(', ')}")
- end
- end
-
- class UnsupportedMakerVersionError < Error
- attr_reader :version
- def initialize(version)
- @version = version
- super("Maker doesn't support version: #{@version}")
- end
- end
-
- module BaseModel
- include Utils
-
- def install_have_child_element(tag_name, uri, occurs, name=nil, type=nil)
- name ||= tag_name
- add_need_initialize_variable(name)
- install_model(tag_name, uri, occurs, name)
-
- writer_type, reader_type = type
- def_corresponded_attr_writer name, writer_type
- def_corresponded_attr_reader name, reader_type
- install_element(name) do |n, elem_name|
- <<-EOC
- if @#{n}
- "\#{@#{n}.to_s(need_convert, indent)}"
- else
- ''
- end
-EOC
- end
- end
- alias_method(:install_have_attribute_element, :install_have_child_element)
-
- def install_have_children_element(tag_name, uri, occurs, name=nil, plural_name=nil)
- name ||= tag_name
- plural_name ||= "#{name}s"
- add_have_children_element(name, plural_name)
- add_plural_form(name, plural_name)
- install_model(tag_name, uri, occurs, plural_name, true)
-
- def_children_accessor(name, plural_name)
- install_element(name, "s") do |n, elem_name|
- <<-EOC
- rv = []
- @#{n}.each do |x|
- value = "\#{x.to_s(need_convert, indent)}"
- rv << value if /\\A\\s*\\z/ !~ value
- end
- rv.join("\n")
-EOC
- end
- end
-
- def install_text_element(tag_name, uri, occurs, name=nil, type=nil,
- disp_name=nil)
- name ||= tag_name
- disp_name ||= name
- self::ELEMENTS << name unless self::ELEMENTS.include?(name)
- add_need_initialize_variable(name)
- install_model(tag_name, uri, occurs, name)
-
- def_corresponded_attr_writer(name, type, disp_name)
- def_corresponded_attr_reader(name, type || :convert)
- install_element(name) do |n, elem_name|
- <<-EOC
- if respond_to?(:#{n}_content)
- content = #{n}_content
- else
- content = @#{n}
- end
- if content
- rv = "\#{indent}<#{elem_name}>"
- value = html_escape(content)
- if need_convert
- rv << convert(value)
- else
- rv << value
- end
- rv << "</#{elem_name}>"
- rv
- else
- ''
- end
-EOC
- end
- end
-
- def install_date_element(tag_name, uri, occurs, name=nil, type=nil, disp_name=nil)
- name ||= tag_name
- type ||= :w3cdtf
- disp_name ||= name
- self::ELEMENTS << name
- add_need_initialize_variable(name)
- install_model(tag_name, uri, occurs, name)
-
- # accessor
- convert_attr_reader name
- date_writer(name, type, disp_name)
-
- install_element(name) do |n, elem_name|
- <<-EOC
- if @#{n}
- rv = "\#{indent}<#{elem_name}>"
- value = html_escape(@#{n}.#{type})
- if need_convert
- rv << convert(value)
- else
- rv << value
- end
- rv << "</#{elem_name}>"
- rv
- else
- ''
- end
-EOC
- end
-
- end
-
- private
- def install_element(name, postfix="")
- elem_name = name.sub('_', ':')
- method_name = "#{name}_element#{postfix}"
- add_to_element_method(method_name)
- module_eval(<<-EOC, *get_file_and_line_from_caller(2))
- def #{method_name}(need_convert=true, indent='')
- #{yield(name, elem_name)}
- end
- private :#{method_name}
-EOC
- end
-
- def inherit_convert_attr_reader(*attrs)
- attrs.each do |attr|
- attr = attr.id2name if attr.kind_of?(Integer)
- module_eval(<<-EOC, *get_file_and_line_from_caller(2))
- def #{attr}_without_inherit
- convert(@#{attr})
- end
-
- def #{attr}
- if @#{attr}
- #{attr}_without_inherit
- elsif @parent
- @parent.#{attr}
- else
- nil
- end
- end
-EOC
- end
- end
-
- def uri_convert_attr_reader(*attrs)
- attrs.each do |attr|
- attr = attr.id2name if attr.kind_of?(Integer)
- module_eval(<<-EOC, *get_file_and_line_from_caller(2))
- def #{attr}_without_base
- convert(@#{attr})
- end
-
- def #{attr}
- value = #{attr}_without_base
- return nil if value.nil?
- if /\\A[a-z][a-z0-9+.\\-]*:/i =~ value
- value
- else
- "\#{base}\#{value}"
- end
- end
-EOC
- end
- end
-
- def convert_attr_reader(*attrs)
- attrs.each do |attr|
- attr = attr.id2name if attr.kind_of?(Integer)
- module_eval(<<-EOC, *get_file_and_line_from_caller(2))
- def #{attr}
- convert(@#{attr})
- end
-EOC
- end
- end
-
- def yes_clean_other_attr_reader(*attrs)
- attrs.each do |attr|
- attr = attr.id2name if attr.kind_of?(Integer)
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- attr_reader(:#{attr})
- def #{attr}?
- YesCleanOther.parse(@#{attr})
- end
- EOC
- end
- end
-
- def yes_other_attr_reader(*attrs)
- attrs.each do |attr|
- attr = attr.id2name if attr.kind_of?(Integer)
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- attr_reader(:#{attr})
- def #{attr}?
- Utils::YesOther.parse(@#{attr})
- end
- EOC
- end
- end
-
- def csv_attr_reader(*attrs)
- separator = nil
- if attrs.last.is_a?(Hash)
- options = attrs.pop
- separator = options[:separator]
- end
- separator ||= ", "
- attrs.each do |attr|
- attr = attr.id2name if attr.kind_of?(Integer)
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- attr_reader(:#{attr})
- def #{attr}_content
- if @#{attr}.nil?
- @#{attr}
- else
- @#{attr}.join(#{separator.dump})
- end
- end
- EOC
- end
- end
-
- def date_writer(name, type, disp_name=name)
- module_eval(<<-EOC, *get_file_and_line_from_caller(2))
- def #{name}=(new_value)
- if new_value.nil?
- @#{name} = new_value
- elsif new_value.kind_of?(Time)
- @#{name} = new_value.dup
- else
- if @do_validate
- begin
- @#{name} = Time.__send__('#{type}', new_value)
- rescue ArgumentError
- raise NotAvailableValueError.new('#{disp_name}', new_value)
- end
- else
- @#{name} = nil
- if /\\A\\s*\\z/ !~ new_value.to_s
- begin
- unless Date._parse(new_value, false).empty?
- @#{name} = Time.parse(new_value)
- end
- rescue ArgumentError
- end
- end
- end
- end
-
- # Is it need?
- if @#{name}
- class << @#{name}
- undef_method(:to_s)
- alias_method(:to_s, :#{type})
- end
- end
-
- end
-EOC
- end
-
- def integer_writer(name, disp_name=name)
- module_eval(<<-EOC, *get_file_and_line_from_caller(2))
- def #{name}=(new_value)
- if new_value.nil?
- @#{name} = new_value
- else
- if @do_validate
- begin
- @#{name} = Integer(new_value)
- rescue ArgumentError
- raise NotAvailableValueError.new('#{disp_name}', new_value)
- end
- else
- @#{name} = new_value.to_i
- end
- end
- end
-EOC
- end
-
- def positive_integer_writer(name, disp_name=name)
- module_eval(<<-EOC, *get_file_and_line_from_caller(2))
- def #{name}=(new_value)
- if new_value.nil?
- @#{name} = new_value
- else
- if @do_validate
- begin
- tmp = Integer(new_value)
- raise ArgumentError if tmp <= 0
- @#{name} = tmp
- rescue ArgumentError
- raise NotAvailableValueError.new('#{disp_name}', new_value)
- end
- else
- @#{name} = new_value.to_i
- end
- end
- end
-EOC
- end
-
- def boolean_writer(name, disp_name=name)
- module_eval(<<-EOC, *get_file_and_line_from_caller(2))
- def #{name}=(new_value)
- if new_value.nil?
- @#{name} = new_value
- else
- if @do_validate and
- ![true, false, "true", "false"].include?(new_value)
- raise NotAvailableValueError.new('#{disp_name}', new_value)
- end
- if [true, false].include?(new_value)
- @#{name} = new_value
- else
- @#{name} = new_value == "true"
- end
- end
- end
-EOC
- end
-
- def text_type_writer(name, disp_name=name)
- module_eval(<<-EOC, *get_file_and_line_from_caller(2))
- def #{name}=(new_value)
- if @do_validate and
- !["text", "html", "xhtml", nil].include?(new_value)
- raise NotAvailableValueError.new('#{disp_name}', new_value)
- end
- @#{name} = new_value
- end
-EOC
- end
-
- def content_writer(name, disp_name=name)
- klass_name = "self.class::#{Utils.to_class_name(name)}"
- module_eval(<<-EOC, *get_file_and_line_from_caller(2))
- def #{name}=(new_value)
- if new_value.is_a?(#{klass_name})
- @#{name} = new_value
- else
- @#{name} = #{klass_name}.new
- @#{name}.content = new_value
- end
- end
-EOC
- end
-
- def yes_clean_other_writer(name, disp_name=name)
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- def #{name}=(value)
- value = (value ? "yes" : "no") if [true, false].include?(value)
- @#{name} = value
- end
- EOC
- end
-
- def yes_other_writer(name, disp_name=name)
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- def #{name}=(new_value)
- if [true, false].include?(new_value)
- new_value = new_value ? "yes" : "no"
- end
- @#{name} = new_value
- end
- EOC
- end
-
- def csv_writer(name, disp_name=name)
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- def #{name}=(new_value)
- @#{name} = Utils::CSV.parse(new_value)
- end
- EOC
- end
-
- def csv_integer_writer(name, disp_name=name)
- module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- def #{name}=(new_value)
- @#{name} = Utils::CSV.parse(new_value) {|v| Integer(v)}
- end
- EOC
- end
-
- def def_children_accessor(accessor_name, plural_name)
- module_eval(<<-EOC, *get_file_and_line_from_caller(2))
- def #{plural_name}
- @#{accessor_name}
- end
-
- def #{accessor_name}(*args)
- if args.empty?
- @#{accessor_name}.first
- else
- @#{accessor_name}[*args]
- end
- end
-
- def #{accessor_name}=(*args)
- receiver = self.class.name
- warn("Warning:\#{caller.first.sub(/:in `.*'\z/, '')}: " \
- "Don't use `\#{receiver}\##{accessor_name} = XXX'/" \
- "`\#{receiver}\#set_#{accessor_name}(XXX)'. " \
- "Those APIs are not sense of Ruby. " \
- "Use `\#{receiver}\##{plural_name} << XXX' instead of them.")
- if args.size == 1
- @#{accessor_name}.push(args[0])
- else
- @#{accessor_name}.__send__("[]=", *args)
- end
- end
- alias_method(:set_#{accessor_name}, :#{accessor_name}=)
-EOC
- end
- end
-
- module SetupMaker
- def setup_maker(maker)
- target = maker_target(maker)
- unless target.nil?
- setup_maker_attributes(target)
- setup_maker_element(target)
- setup_maker_elements(target)
- end
- end
-
- private
- def maker_target(maker)
- nil
- end
-
- def setup_maker_attributes(target)
- end
-
- def setup_maker_element(target)
- self.class.need_initialize_variables.each do |var|
- value = __send__(var)
- next if value.nil?
- if value.respond_to?("setup_maker") and
- !not_need_to_call_setup_maker_variables.include?(var)
- value.setup_maker(target)
- else
- setter = "#{var}="
- if target.respond_to?(setter)
- target.__send__(setter, value)
- end
- end
- end
- end
-
- def not_need_to_call_setup_maker_variables
- []
- end
-
- def setup_maker_elements(parent)
- self.class.have_children_elements.each do |name, plural_name|
- if parent.respond_to?(plural_name)
- target = parent.__send__(plural_name)
- __send__(plural_name).each do |elem|
- elem.setup_maker(target)
- end
- end
- end
- end
- end
-
- class Element
- extend BaseModel
- include Utils
- extend Utils::InheritedReader
- include SetupMaker
-
- INDENT = " "
-
- MUST_CALL_VALIDATORS = {}
- MODELS = []
- GET_ATTRIBUTES = []
- HAVE_CHILDREN_ELEMENTS = []
- TO_ELEMENT_METHODS = []
- NEED_INITIALIZE_VARIABLES = []
- PLURAL_FORMS = {}
-
- class << self
- def must_call_validators
- inherited_hash_reader("MUST_CALL_VALIDATORS")
- end
- def models
- inherited_array_reader("MODELS")
- end
- def get_attributes
- inherited_array_reader("GET_ATTRIBUTES")
- end
- def have_children_elements
- inherited_array_reader("HAVE_CHILDREN_ELEMENTS")
- end
- def to_element_methods
- inherited_array_reader("TO_ELEMENT_METHODS")
- end
- def need_initialize_variables
- inherited_array_reader("NEED_INITIALIZE_VARIABLES")
- end
- def plural_forms
- inherited_hash_reader("PLURAL_FORMS")
- end
-
- def inherited_base
- ::RSS::Element
- end
-
- def inherited(klass)
- klass.const_set("MUST_CALL_VALIDATORS", {})
- klass.const_set("MODELS", [])
- klass.const_set("GET_ATTRIBUTES", [])
- klass.const_set("HAVE_CHILDREN_ELEMENTS", [])
- klass.const_set("TO_ELEMENT_METHODS", [])
- klass.const_set("NEED_INITIALIZE_VARIABLES", [])
- klass.const_set("PLURAL_FORMS", {})
-
- tag_name = klass.name.split(/::/).last
- tag_name[0, 1] = tag_name[0, 1].downcase
- klass.instance_variable_set("@tag_name", tag_name)
- klass.instance_variable_set("@have_content", false)
- end
-
- def install_must_call_validator(prefix, uri)
- self::MUST_CALL_VALIDATORS[uri] = prefix
- end
-
- def install_model(tag, uri, occurs=nil, getter=nil, plural=false)
- getter ||= tag
- if m = self::MODELS.find {|t, u, o, g, p| t == tag and u == uri}
- m[2] = occurs
- else
- self::MODELS << [tag, uri, occurs, getter, plural]
- end
- end
-
- def install_get_attribute(name, uri, required=true,
- type=nil, disp_name=nil,
- element_name=nil)
- disp_name ||= name
- element_name ||= name
- writer_type, reader_type = type
- def_corresponded_attr_writer name, writer_type, disp_name
- def_corresponded_attr_reader name, reader_type
- if type == :boolean and /^is/ =~ name
- alias_method "#{$POSTMATCH}?", name
- end
- self::GET_ATTRIBUTES << [name, uri, required, element_name]
- add_need_initialize_variable(disp_name)
- end
-
- def def_corresponded_attr_writer(name, type=nil, disp_name=nil)
- disp_name ||= name
- case type
- when :integer
- integer_writer name, disp_name
- when :positive_integer
- positive_integer_writer name, disp_name
- when :boolean
- boolean_writer name, disp_name
- when :w3cdtf, :rfc822, :rfc2822
- date_writer name, type, disp_name
- when :text_type
- text_type_writer name, disp_name
- when :content
- content_writer name, disp_name
- when :yes_clean_other
- yes_clean_other_writer name, disp_name
- when :yes_other
- yes_other_writer name, disp_name
- when :csv
- csv_writer name
- when :csv_integer
- csv_integer_writer name
- else
- attr_writer name
- end
- end
-
- def def_corresponded_attr_reader(name, type=nil)
- case type
- when :inherit
- inherit_convert_attr_reader name
- when :uri
- uri_convert_attr_reader name
- when :yes_clean_other
- yes_clean_other_attr_reader name
- when :yes_other
- yes_other_attr_reader name
- when :csv
- csv_attr_reader name
- when :csv_integer
- csv_attr_reader name, :separator => ","
- else
- convert_attr_reader name
- end
- end
-
- def content_setup(type=nil, disp_name=nil)
- writer_type, reader_type = type
- def_corresponded_attr_writer :content, writer_type, disp_name
- def_corresponded_attr_reader :content, reader_type
- @have_content = true
- end
-
- def have_content?
- @have_content
- end
-
- def add_have_children_element(variable_name, plural_name)
- self::HAVE_CHILDREN_ELEMENTS << [variable_name, plural_name]
- end
-
- def add_to_element_method(method_name)
- self::TO_ELEMENT_METHODS << method_name
- end
-
- def add_need_initialize_variable(variable_name)
- self::NEED_INITIALIZE_VARIABLES << variable_name
- end
-
- def add_plural_form(singular, plural)
- self::PLURAL_FORMS[singular] = plural
- end
-
- def required_prefix
- nil
- end
-
- def required_uri
- ""
- end
-
- def need_parent?
- false
- end
-
- def install_ns(prefix, uri)
- if self::NSPOOL.has_key?(prefix)
- raise OverlappedPrefixError.new(prefix)
- end
- self::NSPOOL[prefix] = uri
- end
-
- def tag_name
- @tag_name
- end
- end
-
- attr_accessor :parent, :do_validate
-
- def initialize(do_validate=true, attrs=nil)
- @parent = nil
- @converter = nil
- if attrs.nil? and (do_validate.is_a?(Hash) or do_validate.is_a?(Array))
- do_validate, attrs = true, do_validate
- end
- @do_validate = do_validate
- initialize_variables(attrs || {})
- end
-
- def tag_name
- self.class.tag_name
- end
-
- def full_name
- tag_name
- end
-
- def converter=(converter)
- @converter = converter
- targets = children.dup
- self.class.have_children_elements.each do |variable_name, plural_name|
- targets.concat(__send__(plural_name))
- end
- targets.each do |target|
- target.converter = converter unless target.nil?
- end
- end
-
- def convert(value)
- if @converter
- @converter.convert(value)
- else
- value
- end
- end
-
- def valid?(ignore_unknown_element=true)
- validate(ignore_unknown_element)
- true
- rescue RSS::Error
- false
- end
-
- def validate(ignore_unknown_element=true)
- do_validate = @do_validate
- @do_validate = true
- validate_attribute
- __validate(ignore_unknown_element)
- ensure
- @do_validate = do_validate
- end
-
- def validate_for_stream(tags, ignore_unknown_element=true)
- validate_attribute
- __validate(ignore_unknown_element, tags, false)
- end
-
- def to_s(need_convert=true, indent='')
- if self.class.have_content?
- return "" if !empty_content? and !content_is_set?
- rv = tag(indent) do |next_indent|
- if empty_content?
- ""
- else
- xmled_content
- end
- end
- else
- rv = tag(indent) do |next_indent|
- self.class.to_element_methods.collect do |method_name|
- __send__(method_name, false, next_indent)
- end
- end
- end
- rv = convert(rv) if need_convert
- rv
- end
-
- def have_xml_content?
- false
- end
-
- def need_base64_encode?
- false
- end
-
- def set_next_element(tag_name, next_element)
- klass = next_element.class
- prefix = ""
- prefix << "#{klass.required_prefix}_" if klass.required_prefix
- key = "#{prefix}#{tag_name.gsub(/-/, '_')}"
- if self.class.plural_forms.has_key?(key)
- ary = __send__("#{self.class.plural_forms[key]}")
- ary << next_element
- else
- __send__("#{key}=", next_element)
- end
- end
-
- protected
- def have_required_elements?
- self.class::MODELS.all? do |tag, uri, occurs, getter|
- if occurs.nil? or occurs == "+"
- child = __send__(getter)
- if child.is_a?(Array)
- children = child
- children.any? {|c| c.have_required_elements?}
- else
- !child.to_s.empty?
- end
- else
- true
- end
- end
- end
-
- private
- def initialize_variables(attrs)
- normalized_attrs = {}
- attrs.each do |key, value|
- normalized_attrs[key.to_s] = value
- end
- self.class.need_initialize_variables.each do |variable_name|
- value = normalized_attrs[variable_name.to_s]
- if value
- __send__("#{variable_name}=", value)
- else
- instance_variable_set("@#{variable_name}", nil)
- end
- end
- initialize_have_children_elements
- @content = normalized_attrs["content"] if self.class.have_content?
- end
-
- def initialize_have_children_elements
- self.class.have_children_elements.each do |variable_name, plural_name|
- instance_variable_set("@#{variable_name}", [])
- end
- end
-
- def tag(indent, additional_attrs={}, &block)
- next_indent = indent + INDENT
-
- attrs = collect_attrs
- return "" if attrs.nil?
-
- return "" unless have_required_elements?
-
- attrs.update(additional_attrs)
- start_tag = make_start_tag(indent, next_indent, attrs.dup)
-
- if block
- content = block.call(next_indent)
- else
- content = []
- end
-
- if content.is_a?(String)
- content = [content]
- start_tag << ">"
- end_tag = "</#{full_name}>"
- else
- content = content.reject{|x| x.empty?}
- if content.empty?
- return "" if attrs.empty?
- end_tag = "/>"
- else
- start_tag << ">\n"
- end_tag = "\n#{indent}</#{full_name}>"
- end
- end
-
- start_tag + content.join("\n") + end_tag
- end
-
- def make_start_tag(indent, next_indent, attrs)
- start_tag = ["#{indent}<#{full_name}"]
- unless attrs.empty?
- start_tag << attrs.collect do |key, value|
- %Q[#{h key}="#{h value}"]
- end.join("\n#{next_indent}")
- end
- start_tag.join(" ")
- end
-
- def collect_attrs
- attrs = {}
- _attrs.each do |name, required, alias_name|
- value = __send__(alias_name || name)
- return nil if required and value.nil?
- next if value.nil?
- return nil if attrs.has_key?(name)
- attrs[name] = value
- end
- attrs
- end
-
- def tag_name_with_prefix(prefix)
- "#{prefix}:#{tag_name}"
- end
-
- # For backward compatibility
- def calc_indent
- ''
- end
-
- def children
- rv = []
- self.class.models.each do |name, uri, occurs, getter|
- value = __send__(getter)
- next if value.nil?
- value = [value] unless value.is_a?(Array)
- value.each do |v|
- rv << v if v.is_a?(Element)
- end
- end
- rv
- end
-
- def _tags
- rv = []
- self.class.models.each do |name, uri, occurs, getter, plural|
- value = __send__(getter)
- next if value.nil?
- if plural and value.is_a?(Array)
- rv.concat([[uri, name]] * value.size)
- else
- rv << [uri, name]
- end
- end
- rv
- end
-
- def _attrs
- self.class.get_attributes.collect do |name, uri, required, element_name|
- [element_name, required, name]
- end
- end
-
- def __validate(ignore_unknown_element, tags=_tags, recursive=true)
- if recursive
- children.compact.each do |child|
- child.validate
- end
- end
- must_call_validators = self.class.must_call_validators
- tags = tag_filter(tags.dup)
- p tags if DEBUG
- must_call_validators.each do |uri, prefix|
- _validate(ignore_unknown_element, tags[uri], uri)
- meth = "#{prefix}_validate"
- if !prefix.empty? and respond_to?(meth, true)
- __send__(meth, ignore_unknown_element, tags[uri], uri)
- end
- end
- end
-
- def validate_attribute
- _attrs.each do |a_name, required, alias_name|
- value = instance_variable_get("@#{alias_name || a_name}")
- if required and value.nil?
- raise MissingAttributeError.new(tag_name, a_name)
- end
- __send__("#{alias_name || a_name}=", value)
- end
- end
-
- def _validate(ignore_unknown_element, tags, uri, models=self.class.models)
- count = 1
- do_redo = false
- not_shift = false
- tag = nil
- models = models.find_all {|model| model[1] == uri}
- element_names = models.collect {|model| model[0]}
- if tags
- tags_size = tags.size
- tags = tags.sort_by {|x| element_names.index(x) || tags_size}
- end
-
- _tags = tags.dup if tags
- models.each_with_index do |model, i|
- name, model_uri, occurs, getter = model
-
- if DEBUG
- p "before"
- p tags
- p model
- end
-
- if not_shift
- not_shift = false
- elsif tags
- tag = tags.shift
- end
-
- if DEBUG
- p "mid"
- p count
- end
-
- case occurs
- when '?'
- if count > 2
- raise TooMuchTagError.new(name, tag_name)
- else
- if name == tag
- do_redo = true
- else
- not_shift = true
- end
- end
- when '*'
- if name == tag
- do_redo = true
- else
- not_shift = true
- end
- when '+'
- if name == tag
- do_redo = true
- else
- if count > 1
- not_shift = true
- else
- raise MissingTagError.new(name, tag_name)
- end
- end
- else
- if name == tag
- if models[i+1] and models[i+1][0] != name and
- tags and tags.first == name
- raise TooMuchTagError.new(name, tag_name)
- end
- else
- raise MissingTagError.new(name, tag_name)
- end
- end
-
- if DEBUG
- p "after"
- p not_shift
- p do_redo
- p tag
- end
-
- if do_redo
- do_redo = false
- count += 1
- redo
- else
- count = 1
- end
-
- end
-
- if !ignore_unknown_element and !tags.nil? and !tags.empty?
- raise NotExpectedTagError.new(tags.first, uri, tag_name)
- end
-
- end
-
- def tag_filter(tags)
- rv = {}
- tags.each do |tag|
- rv[tag[0]] = [] unless rv.has_key?(tag[0])
- rv[tag[0]].push(tag[1])
- end
- rv
- end
-
- def empty_content?
- false
- end
-
- def content_is_set?
- if have_xml_content?
- __send__(self.class.xml_getter)
- else
- content
- end
- end
-
- def xmled_content
- if have_xml_content?
- __send__(self.class.xml_getter).to_s
- else
- _content = content
- _content = [_content].pack("m").delete("\n") if need_base64_encode?
- h(_content)
- end
- end
- end
-
- module RootElementMixin
-
- include XMLStyleSheetMixin
-
- attr_reader :output_encoding
- attr_reader :feed_type, :feed_subtype, :feed_version
- attr_accessor :version, :encoding, :standalone
- def initialize(feed_version, version=nil, encoding=nil, standalone=nil)
- super()
- @feed_type = nil
- @feed_subtype = nil
- @feed_version = feed_version
- @version = version || '1.0'
- @encoding = encoding
- @standalone = standalone
- @output_encoding = nil
- end
-
- def feed_info
- [@feed_type, @feed_version, @feed_subtype]
- end
-
- def output_encoding=(enc)
- @output_encoding = enc
- self.converter = Converter.new(@output_encoding, @encoding)
- end
-
- def setup_maker(maker)
- maker.version = version
- maker.encoding = encoding
- maker.standalone = standalone
-
- xml_stylesheets.each do |xss|
- xss.setup_maker(maker)
- end
-
- super
- end
-
- def to_feed(type, &block)
- Maker.make(type) do |maker|
- setup_maker(maker)
- block.call(maker) if block
- end
- end
-
- def to_rss(type, &block)
- to_feed("rss#{type}", &block)
- end
-
- def to_atom(type, &block)
- to_feed("atom:#{type}", &block)
- end
-
- def to_xml(type=nil, &block)
- if type.nil? or same_feed_type?(type)
- to_s
- else
- to_feed(type, &block).to_s
- end
- end
-
- private
- def same_feed_type?(type)
- if /^(atom|rss)?(\d+\.\d+)?(?::(.+))?$/i =~ type
- feed_type = ($1 || @feed_type).downcase
- feed_version = $2 || @feed_version
- feed_subtype = $3 || @feed_subtype
- [feed_type, feed_version, feed_subtype] == feed_info
- else
- false
- end
- end
-
- def tag(indent, attrs={}, &block)
- rv = super(indent, ns_declarations.merge(attrs), &block)
- return rv if rv.empty?
- "#{xmldecl}#{xml_stylesheet_pi}#{rv}"
- end
-
- def xmldecl
- rv = %Q[<?xml version="#{@version}"]
- if @output_encoding or @encoding
- rv << %Q[ encoding="#{@output_encoding or @encoding}"]
- end
- rv << %Q[ standalone="yes"] if @standalone
- rv << "?>\n"
- rv
- end
-
- def ns_declarations
- decls = {}
- self.class::NSPOOL.collect do |prefix, uri|
- prefix = ":#{prefix}" unless prefix.empty?
- decls["xmlns#{prefix}"] = uri
- end
- decls
- end
-
- def maker_target(target)
- target
- end
- end
-end
diff --git a/lib/rss/slash.rb b/lib/rss/slash.rb
deleted file mode 100644
index f102413b46..0000000000
--- a/lib/rss/slash.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require 'rss/1.0'
-
-module RSS
- SLASH_PREFIX = 'slash'
- SLASH_URI = "http://purl.org/rss/1.0/modules/slash/"
-
- RDF.install_ns(SLASH_PREFIX, SLASH_URI)
-
- module SlashModel
- extend BaseModel
-
- ELEMENT_INFOS = \
- [
- ["section"],
- ["department"],
- ["comments", :positive_integer],
- ["hit_parade", :csv_integer],
- ]
-
- class << self
- def append_features(klass)
- super
-
- return if klass.instance_of?(Module)
- klass.install_must_call_validator(SLASH_PREFIX, SLASH_URI)
- ELEMENT_INFOS.each do |name, type, *additional_infos|
- full_name = "#{SLASH_PREFIX}_#{name}"
- klass.install_text_element(full_name, SLASH_URI, "?",
- full_name, type, name)
- end
-
- klass.module_eval do
- alias_method(:slash_hit_parades, :slash_hit_parade)
- undef_method(:slash_hit_parade)
- alias_method(:slash_hit_parade, :slash_hit_parade_content)
- end
- end
- end
- end
-
- class RDF
- class Item; include SlashModel; end
- end
-
- SlashModel::ELEMENT_INFOS.each do |name, type|
- accessor_base = "#{SLASH_PREFIX}_#{name}"
- BaseListener.install_get_text_element(SLASH_URI, name, accessor_base)
- end
-end
diff --git a/lib/rss/syndication.rb b/lib/rss/syndication.rb
deleted file mode 100644
index 3eb15429f6..0000000000
--- a/lib/rss/syndication.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require "rss/1.0"
-
-module RSS
-
- SY_PREFIX = 'sy'
- SY_URI = "http://purl.org/rss/1.0/modules/syndication/"
-
- RDF.install_ns(SY_PREFIX, SY_URI)
-
- module SyndicationModel
-
- extend BaseModel
-
- ELEMENTS = []
-
- def self.append_features(klass)
- super
-
- klass.install_must_call_validator(SY_PREFIX, SY_URI)
- klass.module_eval do
- [
- ["updatePeriod"],
- ["updateFrequency", :positive_integer]
- ].each do |name, type|
- install_text_element(name, SY_URI, "?",
- "#{SY_PREFIX}_#{name}", type,
- "#{SY_PREFIX}:#{name}")
- end
-
- %w(updateBase).each do |name|
- install_date_element(name, SY_URI, "?",
- "#{SY_PREFIX}_#{name}", 'w3cdtf',
- "#{SY_PREFIX}:#{name}")
- end
- end
-
- klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1)
- alias_method(:_sy_updatePeriod=, :sy_updatePeriod=)
- def sy_updatePeriod=(new_value)
- new_value = new_value.strip
- validate_sy_updatePeriod(new_value) if @do_validate
- self._sy_updatePeriod = new_value
- end
- EOC
- end
-
- private
- SY_UPDATEPERIOD_AVAILABLE_VALUES = %w(hourly daily weekly monthly yearly)
- def validate_sy_updatePeriod(value)
- unless SY_UPDATEPERIOD_AVAILABLE_VALUES.include?(value)
- raise NotAvailableValueError.new("updatePeriod", value)
- end
- end
- end
-
- class RDF
- class Channel; include SyndicationModel; end
- end
-
- prefix_size = SY_PREFIX.size + 1
- SyndicationModel::ELEMENTS.uniq!
- SyndicationModel::ELEMENTS.each do |full_name|
- name = full_name[prefix_size..-1]
- BaseListener.install_get_text_element(SY_URI, name, full_name)
- end
-
-end
diff --git a/lib/rss/taxonomy.rb b/lib/rss/taxonomy.rb
deleted file mode 100644
index 276f63b05d..0000000000
--- a/lib/rss/taxonomy.rb
+++ /dev/null
@@ -1,145 +0,0 @@
-require "rss/1.0"
-require "rss/dublincore"
-
-module RSS
-
- TAXO_PREFIX = "taxo"
- TAXO_URI = "http://purl.org/rss/1.0/modules/taxonomy/"
-
- RDF.install_ns(TAXO_PREFIX, TAXO_URI)
-
- TAXO_ELEMENTS = []
-
- %w(link).each do |name|
- full_name = "#{TAXO_PREFIX}_#{name}"
- BaseListener.install_get_text_element(TAXO_URI, name, full_name)
- TAXO_ELEMENTS << "#{TAXO_PREFIX}_#{name}"
- end
-
- %w(topic topics).each do |name|
- class_name = Utils.to_class_name(name)
- BaseListener.install_class_name(TAXO_URI, name, "Taxonomy#{class_name}")
- TAXO_ELEMENTS << "#{TAXO_PREFIX}_#{name}"
- end
-
- module TaxonomyTopicsModel
- extend BaseModel
-
- def self.append_features(klass)
- super
-
- klass.install_must_call_validator(TAXO_PREFIX, TAXO_URI)
- %w(topics).each do |name|
- klass.install_have_child_element(name, TAXO_URI, "?",
- "#{TAXO_PREFIX}_#{name}")
- end
- end
-
- class TaxonomyTopics < Element
- include RSS10
-
- Bag = ::RSS::RDF::Bag
-
- class << self
- def required_prefix
- TAXO_PREFIX
- end
-
- def required_uri
- TAXO_URI
- end
- end
-
- @tag_name = "topics"
-
- install_have_child_element("Bag", RDF::URI, nil)
- install_must_call_validator('rdf', RDF::URI)
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.Bag = args[0]
- end
- self.Bag ||= Bag.new
- end
-
- def full_name
- tag_name_with_prefix(TAXO_PREFIX)
- end
-
- def maker_target(target)
- target.taxo_topics
- end
-
- def resources
- if @Bag
- @Bag.lis.collect do |li|
- li.resource
- end
- else
- []
- end
- end
- end
- end
-
- module TaxonomyTopicModel
- extend BaseModel
-
- def self.append_features(klass)
- super
- var_name = "#{TAXO_PREFIX}_topic"
- klass.install_have_children_element("topic", TAXO_URI, "*", var_name)
- end
-
- class TaxonomyTopic < Element
- include RSS10
-
- include DublinCoreModel
- include TaxonomyTopicsModel
-
- class << self
- def required_prefix
- TAXO_PREFIX
- end
-
- def required_uri
- TAXO_URI
- end
- end
-
- @tag_name = "topic"
-
- install_get_attribute("about", ::RSS::RDF::URI, true, nil, nil,
- "#{RDF::PREFIX}:about")
- install_text_element("link", TAXO_URI, "?", "#{TAXO_PREFIX}_link")
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.about = args[0]
- end
- end
-
- def full_name
- tag_name_with_prefix(TAXO_PREFIX)
- end
-
- def maker_target(target)
- target.new_taxo_topic
- end
- end
- end
-
- class RDF
- include TaxonomyTopicModel
- class Channel
- include TaxonomyTopicsModel
- end
- class Item; include TaxonomyTopicsModel; end
- end
-end
diff --git a/lib/rss/trackback.rb b/lib/rss/trackback.rb
deleted file mode 100644
index ee2491f332..0000000000
--- a/lib/rss/trackback.rb
+++ /dev/null
@@ -1,288 +0,0 @@
-require 'rss/1.0'
-require 'rss/2.0'
-
-module RSS
-
- TRACKBACK_PREFIX = 'trackback'
- TRACKBACK_URI = 'http://madskills.com/public/xml/rss/module/trackback/'
-
- RDF.install_ns(TRACKBACK_PREFIX, TRACKBACK_URI)
- Rss.install_ns(TRACKBACK_PREFIX, TRACKBACK_URI)
-
- module TrackBackUtils
- private
- def trackback_validate(ignore_unknown_element, tags, uri)
- return if tags.nil?
- if tags.find {|tag| tag == "about"} and
- !tags.find {|tag| tag == "ping"}
- raise MissingTagError.new("#{TRACKBACK_PREFIX}:ping", tag_name)
- end
- end
- end
-
- module BaseTrackBackModel
-
- ELEMENTS = %w(ping about)
-
- def append_features(klass)
- super
-
- unless klass.class == Module
- klass.module_eval {include TrackBackUtils}
-
- klass.install_must_call_validator(TRACKBACK_PREFIX, TRACKBACK_URI)
- %w(ping).each do |name|
- var_name = "#{TRACKBACK_PREFIX}_#{name}"
- klass_name = "TrackBack#{Utils.to_class_name(name)}"
- klass.install_have_child_element(name, TRACKBACK_URI, "?", var_name)
- klass.module_eval(<<-EOC, __FILE__, __LINE__)
- remove_method :#{var_name}
- def #{var_name}
- @#{var_name} and @#{var_name}.value
- end
-
- remove_method :#{var_name}=
- def #{var_name}=(value)
- @#{var_name} = Utils.new_with_value_if_need(#{klass_name}, value)
- end
- EOC
- end
-
- [%w(about s)].each do |name, postfix|
- var_name = "#{TRACKBACK_PREFIX}_#{name}"
- klass_name = "TrackBack#{Utils.to_class_name(name)}"
- klass.install_have_children_element(name, TRACKBACK_URI, "*",
- var_name)
- klass.module_eval(<<-EOC, __FILE__, __LINE__)
- remove_method :#{var_name}
- def #{var_name}(*args)
- if args.empty?
- @#{var_name}.first and @#{var_name}.first.value
- else
- ret = @#{var_name}.__send__("[]", *args)
- if ret.is_a?(Array)
- ret.collect {|x| x.value}
- else
- ret.value
- end
- end
- end
-
- remove_method :#{var_name}=
- remove_method :set_#{var_name}
- def #{var_name}=(*args)
- if args.size == 1
- item = Utils.new_with_value_if_need(#{klass_name}, args[0])
- @#{var_name}.push(item)
- else
- new_val = args.last
- if new_val.is_a?(Array)
- new_val = new_value.collect do |val|
- Utils.new_with_value_if_need(#{klass_name}, val)
- end
- else
- new_val = Utils.new_with_value_if_need(#{klass_name}, new_val)
- end
- @#{var_name}.__send__("[]=", *(args[0..-2] + [new_val]))
- end
- end
- alias set_#{var_name} #{var_name}=
- EOC
- end
- end
- end
- end
-
- module TrackBackModel10
- extend BaseModel
- extend BaseTrackBackModel
-
- class TrackBackPing < Element
- include RSS10
-
- class << self
-
- def required_prefix
- TRACKBACK_PREFIX
- end
-
- def required_uri
- TRACKBACK_URI
- end
-
- end
-
- @tag_name = "ping"
-
- [
- ["resource", ::RSS::RDF::URI, true]
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required, nil, nil,
- "#{::RSS::RDF::PREFIX}:#{name}")
- end
-
- alias_method(:value, :resource)
- alias_method(:value=, :resource=)
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.resource = args[0]
- end
- end
-
- def full_name
- tag_name_with_prefix(TRACKBACK_PREFIX)
- end
- end
-
- class TrackBackAbout < Element
- include RSS10
-
- class << self
-
- def required_prefix
- TRACKBACK_PREFIX
- end
-
- def required_uri
- TRACKBACK_URI
- end
-
- end
-
- @tag_name = "about"
-
- [
- ["resource", ::RSS::RDF::URI, true]
- ].each do |name, uri, required|
- install_get_attribute(name, uri, required, nil, nil,
- "#{::RSS::RDF::PREFIX}:#{name}")
- end
-
- alias_method(:value, :resource)
- alias_method(:value=, :resource=)
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.resource = args[0]
- end
- end
-
- def full_name
- tag_name_with_prefix(TRACKBACK_PREFIX)
- end
-
- private
- def maker_target(abouts)
- abouts.new_about
- end
-
- def setup_maker_attributes(about)
- about.resource = self.resource
- end
-
- end
- end
-
- module TrackBackModel20
- extend BaseModel
- extend BaseTrackBackModel
-
- class TrackBackPing < Element
- include RSS09
-
- @tag_name = "ping"
-
- content_setup
-
- class << self
-
- def required_prefix
- TRACKBACK_PREFIX
- end
-
- def required_uri
- TRACKBACK_URI
- end
-
- end
-
- alias_method(:value, :content)
- alias_method(:value=, :content=)
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.content = args[0]
- end
- end
-
- def full_name
- tag_name_with_prefix(TRACKBACK_PREFIX)
- end
-
- end
-
- class TrackBackAbout < Element
- include RSS09
-
- @tag_name = "about"
-
- content_setup
-
- class << self
-
- def required_prefix
- TRACKBACK_PREFIX
- end
-
- def required_uri
- TRACKBACK_URI
- end
-
- end
-
- alias_method(:value, :content)
- alias_method(:value=, :content=)
-
- def initialize(*args)
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.content = args[0]
- end
- end
-
- def full_name
- tag_name_with_prefix(TRACKBACK_PREFIX)
- end
-
- end
- end
-
- class RDF
- class Item; include TrackBackModel10; end
- end
-
- class Rss
- class Channel
- class Item; include TrackBackModel20; end
- end
- end
-
- BaseTrackBackModel::ELEMENTS.each do |name|
- class_name = Utils.to_class_name(name)
- BaseListener.install_class_name(TRACKBACK_URI, name,
- "TrackBack#{class_name}")
- end
-
- BaseTrackBackModel::ELEMENTS.collect! {|name| "#{TRACKBACK_PREFIX}_#{name}"}
-end
diff --git a/lib/rss/utils.rb b/lib/rss/utils.rb
deleted file mode 100644
index 0e4001e1f3..0000000000
--- a/lib/rss/utils.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-module RSS
- module Utils
- module_function
-
- # Convert a name_with_underscores to CamelCase.
- def to_class_name(name)
- name.split(/[_\-]/).collect do |part|
- "#{part[0, 1].upcase}#{part[1..-1]}"
- end.join("")
- end
-
- def get_file_and_line_from_caller(i=0)
- file, line, = caller[i].split(':')
- line = line.to_i
- line += 1 if i.zero?
- [file, line]
- end
-
- # escape '&', '"', '<' and '>' for use in HTML.
- def html_escape(s)
- s.to_s.gsub(/&/, "&amp;").gsub(/\"/, "&quot;").gsub(/>/, "&gt;").gsub(/</, "&lt;")
- end
- alias h html_escape
-
- # If +value+ is an instance of class +klass+, return it, else
- # create a new instance of +klass+ with value +value+.
- def new_with_value_if_need(klass, value)
- if value.is_a?(klass)
- value
- else
- klass.new(value)
- end
- end
-
- def element_initialize_arguments?(args)
- [true, false].include?(args[0]) and args[1].is_a?(Hash)
- end
-
- module YesCleanOther
- module_function
- def parse(value)
- if [true, false, nil].include?(value)
- value
- else
- case value.to_s
- when /\Ayes\z/i
- true
- when /\Aclean\z/i
- false
- else
- nil
- end
- end
- end
- end
-
- module YesOther
- module_function
- def parse(value)
- if [true, false].include?(value)
- value
- else
- /\Ayes\z/i.match(value.to_s) ? true : false
- end
- end
- end
-
- module CSV
- module_function
- def parse(value, &block)
- if value.is_a?(String)
- value = value.strip.split(/\s*,\s*/)
- value = value.collect(&block) if block_given?
- value
- else
- value
- end
- end
- end
-
- module InheritedReader
- def inherited_reader(constant_name)
- base_class = inherited_base
- result = base_class.const_get(constant_name)
- found_base_class = false
- ancestors.reverse_each do |klass|
- if found_base_class
- if klass.const_defined?(constant_name)
- result = yield(result, klass.const_get(constant_name))
- end
- else
- found_base_class = klass == base_class
- end
- end
- result
- end
-
- def inherited_array_reader(constant_name)
- inherited_reader(constant_name) do |result, current|
- current + result
- end
- end
-
- def inherited_hash_reader(constant_name)
- inherited_reader(constant_name) do |result, current|
- result.merge(current)
- end
- end
- end
- end
-end
diff --git a/lib/rss/xml-stylesheet.rb b/lib/rss/xml-stylesheet.rb
deleted file mode 100644
index 559d6bcd56..0000000000
--- a/lib/rss/xml-stylesheet.rb
+++ /dev/null
@@ -1,105 +0,0 @@
-require "rss/utils"
-
-module RSS
-
- module XMLStyleSheetMixin
- attr_accessor :xml_stylesheets
- def initialize(*args)
- super
- @xml_stylesheets = []
- end
-
- private
- def xml_stylesheet_pi
- xsss = @xml_stylesheets.collect do |xss|
- pi = xss.to_s
- pi = nil if /\A\s*\z/ =~ pi
- pi
- end.compact
- xsss.push("") unless xsss.empty?
- xsss.join("\n")
- end
- end
-
- class XMLStyleSheet
-
- include Utils
-
- ATTRIBUTES = %w(href type title media charset alternate)
-
- GUESS_TABLE = {
- "xsl" => "text/xsl",
- "css" => "text/css",
- }
-
- attr_accessor(*ATTRIBUTES)
- attr_accessor(:do_validate)
- def initialize(*attrs)
- if attrs.size == 1 and
- (attrs.first.is_a?(Hash) or attrs.first.is_a?(Array))
- attrs = attrs.first
- end
- @do_validate = true
- ATTRIBUTES.each do |attr|
- __send__("#{attr}=", nil)
- end
- vars = ATTRIBUTES.dup
- vars.unshift(:do_validate)
- attrs.each do |name, value|
- if vars.include?(name.to_s)
- __send__("#{name}=", value)
- end
- end
- end
-
- def to_s
- rv = ""
- if @href
- rv << %Q[<?xml-stylesheet]
- ATTRIBUTES.each do |name|
- if __send__(name)
- rv << %Q[ #{name}="#{h __send__(name)}"]
- end
- end
- rv << %Q[?>]
- end
- rv
- end
-
- remove_method(:href=)
- def href=(value)
- @href = value
- if @href and @type.nil?
- @type = guess_type(@href)
- end
- @href
- end
-
- remove_method(:alternate=)
- def alternate=(value)
- if value.nil? or /\A(?:yes|no)\z/ =~ value
- @alternate = value
- else
- if @do_validate
- args = ["?xml-stylesheet?", %Q[alternate="#{value}"]]
- raise NotAvailableValueError.new(*args)
- end
- end
- @alternate
- end
-
- def setup_maker(maker)
- xss = maker.xml_stylesheets.new_xml_stylesheet
- ATTRIBUTES.each do |attr|
- xss.__send__("#{attr}=", __send__(attr))
- end
- end
-
- private
- def guess_type(filename)
- /\.([^.]+)$/ =~ filename
- GUESS_TABLE[$1]
- end
-
- end
-end
diff --git a/lib/rss/xml.rb b/lib/rss/xml.rb
deleted file mode 100644
index 1ae878b772..0000000000
--- a/lib/rss/xml.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-require "rss/utils"
-
-module RSS
- module XML
- class Element
- include Enumerable
-
- attr_reader :name, :prefix, :uri, :attributes, :children
- def initialize(name, prefix=nil, uri=nil, attributes={}, children=[])
- @name = name
- @prefix = prefix
- @uri = uri
- @attributes = attributes
- if children.is_a?(String) or !children.respond_to?(:each)
- @children = [children]
- else
- @children = children
- end
- end
-
- def [](name)
- @attributes[name]
- end
-
- def []=(name, value)
- @attributes[name] = value
- end
-
- def <<(child)
- @children << child
- end
-
- def each(&block)
- @children.each(&block)
- end
-
- def ==(other)
- other.kind_of?(self.class) and
- @name == other.name and
- @uri == other.uri and
- @attributes == other.attributes and
- @children == other.children
- end
-
- def to_s
- rv = "<#{full_name}"
- attributes.each do |key, value|
- rv << " #{Utils.html_escape(key)}=\"#{Utils.html_escape(value)}\""
- end
- if children.empty?
- rv << "/>"
- else
- rv << ">"
- children.each do |child|
- rv << child.to_s
- end
- rv << "</#{full_name}>"
- end
- rv
- end
-
- def full_name
- if @prefix
- "#{@prefix}:#{@name}"
- else
- @name
- end
- end
- end
- end
-end
diff --git a/lib/rss/xmlparser.rb b/lib/rss/xmlparser.rb
deleted file mode 100644
index 3dfe7d461a..0000000000
--- a/lib/rss/xmlparser.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-begin
- require "xml/parser"
-rescue LoadError
- require "xmlparser"
-end
-
-begin
- require "xml/encoding-ja"
-rescue LoadError
- require "xmlencoding-ja"
- if defined?(Kconv)
- module XMLEncoding_ja
- class SJISHandler
- include Kconv
- end
- end
- end
-end
-
-module XML
- class Parser
- unless defined?(Error)
- Error = ::XMLParserError
- end
- end
-end
-
-module RSS
-
- class REXMLLikeXMLParser < ::XML::Parser
-
- include ::XML::Encoding_ja
-
- def listener=(listener)
- @listener = listener
- end
-
- def startElement(name, attrs)
- @listener.tag_start(name, attrs)
- end
-
- def endElement(name)
- @listener.tag_end(name)
- end
-
- def character(data)
- @listener.text(data)
- end
-
- def xmlDecl(version, encoding, standalone)
- @listener.xmldecl(version, encoding, standalone == 1)
- end
-
- def processingInstruction(target, content)
- @listener.instruction(target, content)
- end
-
- end
-
- class XMLParserParser < BaseParser
-
- class << self
- def listener
- XMLParserListener
- end
- end
-
- private
- def _parse
- begin
- parser = REXMLLikeXMLParser.new
- parser.listener = @listener
- parser.parse(@rss)
- rescue ::XML::Parser::Error => e
- raise NotWellFormedError.new(parser.line){e.message}
- end
- end
-
- end
-
- class XMLParserListener < BaseListener
-
- include ListenerMixin
-
- def xmldecl(version, encoding, standalone)
- super
- # Encoding is converted to UTF-8 when XMLParser parses XML.
- @encoding = 'UTF-8'
- end
-
- end
-
-end
diff --git a/lib/rss/xmlscanner.rb b/lib/rss/xmlscanner.rb
deleted file mode 100644
index 61b9fa6bf4..0000000000
--- a/lib/rss/xmlscanner.rb
+++ /dev/null
@@ -1,121 +0,0 @@
-require 'xmlscan/scanner'
-require 'stringio'
-
-module RSS
-
- class XMLScanParser < BaseParser
-
- class << self
- def listener
- XMLScanListener
- end
- end
-
- private
- def _parse
- begin
- if @rss.is_a?(String)
- input = StringIO.new(@rss)
- else
- input = @rss
- end
- scanner = XMLScan::XMLScanner.new(@listener)
- scanner.parse(input)
- rescue XMLScan::Error => e
- lineno = e.lineno || scanner.lineno || input.lineno
- raise NotWellFormedError.new(lineno){e.message}
- end
- end
-
- end
-
- class XMLScanListener < BaseListener
-
- include XMLScan::Visitor
- include ListenerMixin
-
- ENTITIES = {
- 'lt' => '<',
- 'gt' => '>',
- 'amp' => '&',
- 'quot' => '"',
- 'apos' => '\''
- }
-
- def on_xmldecl_version(str)
- @version = str
- end
-
- def on_xmldecl_encoding(str)
- @encoding = str
- end
-
- def on_xmldecl_standalone(str)
- @standalone = str
- end
-
- def on_xmldecl_end
- xmldecl(@version, @encoding, @standalone == "yes")
- end
-
- alias_method(:on_pi, :instruction)
- alias_method(:on_chardata, :text)
- alias_method(:on_cdata, :text)
-
- def on_etag(name)
- tag_end(name)
- end
-
- def on_entityref(ref)
- text(entity(ref))
- end
-
- def on_charref(code)
- text([code].pack('U'))
- end
-
- alias_method(:on_charref_hex, :on_charref)
-
- def on_stag(name)
- @attrs = {}
- end
-
- def on_attribute(name)
- @attrs[name] = @current_attr = ''
- end
-
- def on_attr_value(str)
- @current_attr << str
- end
-
- def on_attr_entityref(ref)
- @current_attr << entity(ref)
- end
-
- def on_attr_charref(code)
- @current_attr << [code].pack('U')
- end
-
- alias_method(:on_attr_charref_hex, :on_attr_charref)
-
- def on_stag_end(name)
- tag_start(name, @attrs)
- end
-
- def on_stag_end_empty(name)
- tag_start(name, @attrs)
- tag_end(name)
- end
-
- private
- def entity(ref)
- ent = ENTITIES[ref]
- if ent
- ent
- else
- wellformed_error("undefined entity: #{ref}")
- end
- end
- end
-
-end
diff --git a/lib/ruby2_keywords.gemspec b/lib/ruby2_keywords.gemspec
new file mode 100644
index 0000000000..e2cd397532
--- /dev/null
+++ b/lib/ruby2_keywords.gemspec
@@ -0,0 +1,23 @@
+_VERSION = "0.0.5"
+abort "Version must not reach 1" if _VERSION[/\d+/].to_i >= 1
+
+Gem::Specification.new do |s|
+ s.name = "ruby2_keywords"
+ s.version = _VERSION
+ s.summary = "Shim library for Module#ruby2_keywords"
+ s.homepage = "https://github.com/ruby/ruby2_keywords"
+ s.licenses = ["Ruby", "BSD-2-Clause"]
+ s.authors = ["Nobuyoshi Nakada"]
+ s.require_paths = ["lib"]
+ s.rdoc_options = ["--main", "README.md"]
+ s.extra_rdoc_files = [
+ "LICENSE",
+ "README.md",
+ "ChangeLog",
+ *Dir.glob("#{__dir__}/logs/ChangeLog-*[^~]").map {|path| path[(__dir__.size+1)..-1]},
+ ]
+ s.files = [
+ "lib/ruby2_keywords.rb",
+ ]
+ s.required_ruby_version = '>= 2.0.0'
+end
diff --git a/lib/rubygems.rb b/lib/rubygems.rb
index 9913b59ce1..d289cab0fd 100644
--- a/lib/rubygems.rb
+++ b/lib/rubygems.rb
@@ -1,270 +1,361 @@
-# -*- ruby -*-
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems/rubygems_version'
-require 'rubygems/defaults'
-require 'thread'
+require "rbconfig"
module Gem
- class LoadError < ::LoadError
- attr_accessor :name, :version_requirement
- end
+ VERSION = "4.1.0.dev"
end
-module Kernel
-
- ##
- # Use Kernel#gem to activate a specific version of +gem_name+.
- #
- # +version_requirements+ is a list of version requirements that the
- # specified gem must match, most commonly "= example.version.number". See
- # Gem::Requirement for how to specify a version requirement.
- #
- # If you will be activating the latest version of a gem, there is no need to
- # call Kernel#gem, Kernel#require will do the right thing for you.
- #
- # Kernel#gem returns true if the gem was activated, otherwise false. If the
- # gem could not be found, didn't match the version requirements, or a
- # different version was already activated, an exception will be raised.
- #
- # Kernel#gem should be called *before* any require statements (otherwise
- # RubyGems may load a conflicting library version).
- #
- # In older RubyGems versions, the environment variable GEM_SKIP could be
- # used to skip activation of specified gems, for example to test out changes
- # that haven't been installed yet. Now RubyGems defers to -I and the
- # RUBYLIB environment variable to skip activation of a gem.
- #
- # Example:
- #
- # GEM_SKIP=libA:libB ruby -I../libA -I../libB ./mycode.rb
-
- def gem(gem_name, *version_requirements) # :doc:
- skip_list = (ENV['GEM_SKIP'] || "").split(/:/)
- raise Gem::LoadError, "skipping #{gem_name}" if skip_list.include? gem_name
- Gem.activate(gem_name, *version_requirements)
- end
-
- private :gem
-
-end
+require_relative "rubygems/defaults"
+require_relative "rubygems/deprecate"
+require_relative "rubygems/errors"
+require_relative "rubygems/target_rbconfig"
+require_relative "rubygems/win_platform"
+require_relative "rubygems/util/atomic_file_writer"
##
-# Main module to hold all RubyGem classes/modules.
+# RubyGems is the Ruby standard for publishing and managing third party
+# libraries.
+#
+# For user documentation, see:
+#
+# * <tt>gem help</tt> and <tt>gem help [command]</tt>
+# * {RubyGems User Guide}[https://guides.rubygems.org/]
+# * {Frequently Asked Questions}[https://guides.rubygems.org/faqs]
+#
+# For gem developer documentation see:
+#
+# * {Creating Gems}[https://guides.rubygems.org/make-your-own-gem]
+# * Gem::Specification
+# * Gem::Version for version dependency notes
+#
+# Further RubyGems documentation can be found at:
+#
+# * {RubyGems Guides}[https://guides.rubygems.org]
+# * {RubyGems API}[https://guides.rubygems.org/rubygems-org-api/] (also available from
+# <tt>gem server</tt>)
+#
+# == RubyGems Plugins
+#
+# RubyGems will load plugins in the latest version of each installed gem or
+# $LOAD_PATH. Plugins must be named 'rubygems_plugin' (.rb, .so, etc) and
+# placed at the root of your gem's #require_path. Plugins are installed at a
+# special location and loaded on boot.
+#
+# For an example plugin, see the {Graph gem}[https://github.com/seattlerb/graph]
+# which adds a <tt>gem graph</tt> command.
+#
+# == RubyGems Defaults, Packaging
+#
+# RubyGems defaults are stored in lib/rubygems/defaults.rb. If you're packaging
+# RubyGems or implementing Ruby you can change RubyGems' defaults.
+#
+# For RubyGems packagers, provide lib/rubygems/defaults/operating_system.rb
+# and override any defaults from lib/rubygems/defaults.rb.
+#
+# For Ruby implementers, provide lib/rubygems/defaults/#{RUBY_ENGINE}.rb and
+# override any defaults from lib/rubygems/defaults.rb.
+#
+# If you need RubyGems to perform extra work on install or uninstall, your
+# defaults override file can set pre/post install and uninstall hooks.
+# See Gem::pre_install, Gem::pre_uninstall, Gem::post_install,
+# Gem::post_uninstall.
+#
+# == Bugs
+#
+# You can submit bugs to the
+# {RubyGems bug tracker}[https://github.com/ruby/rubygems/issues]
+# on GitHub
+#
+# == Credits
+#
+# RubyGems is currently maintained by Eric Hodel.
+#
+# RubyGems was originally developed at RubyConf 2003 by:
+#
+# * Rich Kilmer -- rich(at)infoether.com
+# * Chad Fowler -- chad(at)chadfowler.com
+# * David Black -- dblack(at)wobblini.net
+# * Paul Brannan -- paul(at)atdesk.com
+# * Jim Weirich -- jim(at)weirichhouse.org
+#
+# Contributors:
+#
+# * Gavin Sinclair -- gsinclair(at)soyabean.com.au
+# * George Marrows -- george.marrows(at)ntlworld.com
+# * Dick Davies -- rasputnik(at)hellooperator.net
+# * Mauricio Fernandez -- batsman.geo(at)yahoo.com
+# * Simon Strandgaard -- neoneye(at)adslhome.dk
+# * Dave Glasser -- glasser(at)mit.edu
+# * Paul Duncan -- pabs(at)pablotron.org
+# * Ville Aine -- vaine(at)cs.helsinki.fi
+# * Eric Hodel -- drbrain(at)segment7.net
+# * Daniel Berger -- djberg96(at)gmail.com
+# * Phil Hagelberg -- technomancy(at)gmail.com
+# * Ryan Davis -- ryand-ruby(at)zenspider.com
+# * Evan Phoenix -- evan(at)fallingsnow.net
+# * Steve Klabnik -- steve(at)steveklabnik.com
+#
+# (If your name is missing, PLEASE let us know!)
+#
+# == License
+#
+# See {LICENSE.txt}[https://github.com/ruby/rubygems/blob/master/LICENSE.txt] for permissions.
+#
+# Thanks!
+#
+# -The RubyGems Team
module Gem
+ RUBYGEMS_DIR = __dir__
- ConfigMap = {} unless defined?(ConfigMap)
- require 'rbconfig'
- RbConfig = Config unless defined? ::RbConfig
-
- ConfigMap.merge!(
- :BASERUBY => RbConfig::CONFIG["BASERUBY"],
- :EXEEXT => RbConfig::CONFIG["EXEEXT"],
- :RUBY_INSTALL_NAME => RbConfig::CONFIG["RUBY_INSTALL_NAME"],
- :RUBY_SO_NAME => RbConfig::CONFIG["RUBY_SO_NAME"],
- :arch => RbConfig::CONFIG["arch"],
- :bindir => RbConfig::CONFIG["bindir"],
- :datadir => RbConfig::CONFIG["datadir"],
- :libdir => RbConfig::CONFIG["libdir"],
- :ruby_install_name => RbConfig::CONFIG["ruby_install_name"],
- :ruby_version => RbConfig::CONFIG["ruby_version"],
- :sitedir => RbConfig::CONFIG["sitedir"],
- :sitelibdir => RbConfig::CONFIG["sitelibdir"],
- :vendordir => RbConfig::CONFIG["vendordir"] ,
- :vendorlibdir => RbConfig::CONFIG["vendorlibdir"]
- )
+ GEM_DEP_FILES = %w[
+ gem.deps.rb
+ gems.rb
+ Gemfile
+ Isolate
+ ].freeze
- DIRECTORIES = %w[cache doc gems specifications] unless defined?(DIRECTORIES)
+ ##
+ # Subdirectories in a gem repository
+
+ REPOSITORY_SUBDIRECTORIES = %w[
+ build_info
+ cache
+ doc
+ extensions
+ gems
+ plugins
+ specifications
+ ].freeze
- MUTEX = Mutex.new
+ ##
+ # Subdirectories in a gem repository for default gems
- RubyGemsPackageVersion = RubyGemsVersion
+ REPOSITORY_DEFAULT_GEM_SUBDIRECTORIES = %w[
+ gems
+ specifications/default
+ ].freeze
##
- # An Array of Regexps that match windows ruby platforms.
-
- WIN_PATTERNS = [
- /bccwin/i,
- /cygwin/i,
- /djgpp/i,
- /mingw/i,
- /mswin/i,
- /wince/i,
- ]
+ # The default value for SOURCE_DATE_EPOCH if not specified.
+ # We want a date after 1980-01-01, to prevent issues with Zip files.
+ # This particular timestamp is for 1980-01-02 00:00:00 GMT.
- @@source_index = nil
- @@win_platform = nil
+ DEFAULT_SOURCE_DATE_EPOCH = 315_619_200
@configuration = nil
+ @gemdeps = nil
@loaded_specs = {}
+ LOADED_SPECS_MUTEX = Thread::Mutex.new
+ @path_to_default_spec_map = {}
@platforms = []
@ruby = nil
- @sources = []
+ @ruby_api_version = nil
+ @sources = nil
+ @post_build_hooks ||= []
@post_install_hooks ||= []
@post_uninstall_hooks ||= []
@pre_uninstall_hooks ||= []
@pre_install_hooks ||= []
+ @pre_reset_hooks ||= []
+ @post_reset_hooks ||= []
- ##
- # Activates an installed gem matching +gem+. The gem must satisfy
- # +version_requirements+.
- #
- # Returns true if the gem is activated, false if it is already
- # loaded, or an exception otherwise.
- #
- # Gem#activate adds the library paths in +gem+ to $LOAD_PATH. Before a Gem
- # is activated its required Gems are activated. If the version information
- # is omitted, the highest version Gem of the supplied name is loaded. If a
- # Gem is not found that meets the version requirements or a required Gem is
- # not found, a Gem::LoadError is raised.
- #
- # More information on version requirements can be found in the
- # Gem::Requirement and Gem::Version documentation.
+ @default_source_date_epoch = nil
- def self.activate(gem, *version_requirements)
- if version_requirements.empty? then
- version_requirements = Gem::Requirement.default
- end
+ @discover_gems_on_require = true
- unless gem.respond_to?(:name) and
- gem.respond_to?(:version_requirements) then
- gem = Gem::Dependency.new(gem, version_requirements)
- end
+ @target_rbconfig = nil
- matches = Gem.source_index.find_name(gem.name, gem.version_requirements)
- report_activate_error(gem) if matches.empty?
+ ##
+ # Try to activate a gem containing +path+. Returns true if
+ # activation succeeded or wasn't needed because it was already
+ # activated. Returns false if it can't find the path in a gem.
- if @loaded_specs[gem.name] then
- # This gem is already loaded. If the currently loaded gem is not in the
- # list of candidate gems, then we have a version conflict.
- existing_spec = @loaded_specs[gem.name]
+ def self.try_activate(path)
+ # finds the _latest_ version... regardless of loaded specs and their deps
+ # if another gem had a requirement that would mean we shouldn't
+ # activate the latest version, then either it would already be activated
+ # or if it was ambiguous (and thus unresolved) the code in our custom
+ # require will try to activate the more specific version.
- unless matches.any? { |spec| spec.version == existing_spec.version } then
- raise Gem::Exception,
- "can't activate #{gem}, already activated #{existing_spec.full_name}"
- end
+ spec = Gem::Specification.find_by_path path
+ return false unless spec
+ return true if spec.activated?
- return false
+ begin
+ spec.activate
+ rescue Gem::LoadError => e # this could fail due to gem dep collisions, go lax
+ name = spec.name
+ spec = Gem::Specification.find_unloaded_by_path(path)
+ spec ||= Gem::Specification.find_by_name(name)
+ if spec.nil?
+ raise e
+ else
+ spec.activate
+ end
end
- # new load
- spec = matches.last
- return false if spec.loaded?
+ true
+ end
- spec.loaded = true
- @loaded_specs[spec.name] = spec
+ def self.needs
+ rs = Gem::RequestSet.new
- # Load dependent gems first
- spec.runtime_dependencies.each do |dep_gem|
- activate dep_gem
- end
+ yield rs
- # bin directory must come before library directories
- spec.require_paths.unshift spec.bindir if spec.bindir
+ finish_resolve rs
+ end
+
+ def self.finish_resolve(request_set = Gem::RequestSet.new)
+ request_set.import Gem::Specification.unresolved_deps.values
+ request_set.import Gem.loaded_specs.values.map {|s| Gem::Dependency.new(s.name, s.version) }
- require_paths = spec.require_paths.map do |path|
- File.join spec.full_gem_path, path
+ request_set.resolve_current.each do |s|
+ s.full_spec.activate
end
+ end
- sitelibdir = ConfigMap[:sitelibdir]
+ ##
+ # Find the full path to the executable for gem +name+. If the +exec_name+
+ # is not given, an exception will be raised, otherwise the
+ # specified executable's path is returned. +requirements+ allows
+ # you to specify specific gem versions.
- # gem directories must come after -I and ENV['RUBYLIB']
- insert_index = load_path_insert_index
+ def self.bin_path(name, exec_name = nil, *requirements)
+ requirements = Gem::Requirement.default if
+ requirements.empty?
- if insert_index then
- # gem directories must come after -I and ENV['RUBYLIB']
- $LOAD_PATH.insert(insert_index, *require_paths)
- else
- # we are probably testing in core, -I and RUBYLIB don't apply
- $LOAD_PATH.unshift(*require_paths)
- end
+ find_spec_for_exe(name, exec_name, requirements).bin_file exec_name
+ end
- return true
+ def self.find_and_activate_spec_for_exe(name, exec_name, requirements)
+ spec = find_spec_for_exe name, exec_name, requirements
+ Gem::LOADED_SPECS_MUTEX.synchronize do
+ spec.activate
+ finish_resolve
+ end
+ spec
end
+ private_class_method :find_and_activate_spec_for_exe
- ##
- # An Array of all possible load paths for all versions of all gems in the
- # Gem installation.
+ def self.find_spec_for_exe(name, exec_name, requirements)
+ raise ArgumentError, "you must supply exec_name" unless exec_name
- def self.all_load_paths
- result = []
+ dep = Gem::Dependency.new name, requirements
- Gem.path.each do |gemdir|
- each_load_path all_partials(gemdir) do |load_path|
- result << load_path
- end
+ loaded = Gem.loaded_specs[name]
+
+ return loaded if loaded && dep.matches_spec?(loaded)
+
+ specs = dep.matching_specs(true)
+
+ specs = specs.find_all do |spec|
+ spec.executables.include? exec_name
+ end if exec_name
+
+ unless spec = specs.first
+ msg = "can't find gem #{dep} with executable #{exec_name}"
+ raise Gem::GemNotFoundException, msg
end
- result
+ spec
end
+ private_class_method :find_spec_for_exe
##
- # Return all the partial paths in +gemdir+.
+ # Find and load the full path to the executable for gem +name+. If the
+ # +exec_name+ is not given, an exception will be raised, otherwise the
+ # specified executable's path is returned. +requirements+ allows
+ # you to specify specific gem versions.
+ #
+ # A side effect of this method is that it will activate the gem that
+ # contains the executable.
+ #
+ # This method should *only* be used in bin stub files.
+
+ def self.activate_and_load_bin_path(name, exec_name = nil, *requirements)
+ spec = find_and_activate_spec_for_exe name, exec_name, requirements
+
+ if spec.name == "bundler"
+ # Old versions of Bundler need a workaround to support nested `bundle
+ # exec` invocations by overriding `Gem.activate_bin_path`. However,
+ # RubyGems now uses this new `Gem.activate_and_load_bin_path` helper in
+ # binstubs, which is of course not overridden in Bundler since it didn't
+ # exist at the time. So, include the override here to workaround that.
+ load ENV["BUNDLE_BIN_PATH"] if ENV["BUNDLE_BIN_PATH"] && spec.version <= Gem::Version.create("2.5.22")
+
+ # Make sure there's no version of Bundler in `$LOAD_PATH` that's different
+ # from the version we just activated. If that was the case (it happens
+ # when testing Bundler from ruby/ruby), we would load Bundler extensions
+ # to RubyGems from the copy in `$LOAD_PATH` but then load the binstub from
+ # an installed copy, causing those copies to be mixed and yet more
+ # redefinition warnings.
+ #
+ require_path = $LOAD_PATH.resolve_feature_path("bundler").last.delete_suffix("/bundler.rb")
+ Gem.load_bundler_extensions(spec.version) if spec.full_require_paths.include?(require_path)
+ end
- def self.all_partials(gemdir)
- Dir[File.join(gemdir, 'gems/*')]
+ load spec.bin_file(exec_name)
end
- private_class_method :all_partials
-
##
- # See if a given gem is available.
-
- def self.available?(gem, *requirements)
- requirements = Gem::Requirement.default if requirements.empty?
-
- unless gem.respond_to?(:name) and
- gem.respond_to?(:version_requirements) then
- gem = Gem::Dependency.new gem, requirements
- end
+ # Find the full path to the executable for gem +name+. If the +exec_name+
+ # is not given, an exception will be raised, otherwise the
+ # specified executable's path is returned. +requirements+ allows
+ # you to specify specific gem versions.
+ #
+ # A side effect of this method is that it will activate the gem that
+ # contains the executable.
+ #
+ # This method should *only* be used in bin stub files.
- !Gem.source_index.search(gem).empty?
+ def self.activate_bin_path(name, exec_name = nil, *requirements) # :nodoc:
+ find_and_activate_spec_for_exe(name, exec_name, requirements).bin_file exec_name
end
##
# The mode needed to read a file as straight binary.
def self.binary_mode
- @binary_mode ||= RUBY_VERSION > '1.9' ? 'rb:ascii-8bit' : 'rb'
+ "rb"
end
##
# The path where gem executables are to be installed.
- def self.bindir(install_dir=Gem.dir)
- return File.join(install_dir, 'bin') unless
- install_dir.to_s == Gem.default_dir
+ def self.bindir(install_dir = Gem.dir)
+ return File.join install_dir, "bin" unless
+ install_dir.to_s == Gem.default_dir.to_s
Gem.default_bindir
end
##
- # Reset the +dir+ and +path+ values. The next time +dir+ or +path+
- # is requested, the values will be calculated from scratch. This is
- # mainly used by the unit tests to provide test isolation.
-
- def self.clear_paths
- @gem_home = nil
- @gem_path = nil
- @user_home = nil
-
- @@source_index = nil
+ # The path were rubygems plugins are to be installed.
- MUTEX.synchronize do
- @searcher = nil
- end
+ def self.plugindir(install_dir = Gem.dir)
+ File.join install_dir, "plugins"
end
##
- # The path to standard location of the user's .gemrc file.
+ # Reset the +dir+ and +path+ values. The next time +dir+ or +path+
+ # is requested, the values will be calculated from scratch. This is
+ # mainly used by the unit tests to provide test isolation.
- def self.config_file
- File.join Gem.user_home, '.gemrc'
+ def self.clear_paths
+ @paths = nil
+ @user_home = nil
+ @cache_home = nil
+ @data_home = nil
+ Gem::Specification.reset
+ Gem::Security.reset if defined?(Gem::Security)
end
##
@@ -283,211 +374,352 @@ module Gem
end
##
- # The path the the data directory specified by the gem name. If the
- # package is not available as a gem, return nil.
-
- def self.datadir(gem_name)
- spec = @loaded_specs[gem_name]
- return nil if spec.nil?
- File.join(spec.full_gem_path, 'data', gem_name)
- end
-
- ##
# A Zlib::Deflate.deflate wrapper
def self.deflate(data)
- require 'zlib'
+ require "zlib"
Zlib::Deflate.deflate data
end
- ##
- # The path where gems are to be installed.
+ # Retrieve the PathSupport object that RubyGems uses to
+ # lookup files.
- def self.dir
- @gem_home ||= nil
- set_home(ENV['GEM_HOME'] || Gem.configuration.home || default_dir) unless @gem_home
- @gem_home
+ def self.paths
+ @paths ||= Gem::PathSupport.new(ENV)
end
- ##
- # Expand each partial gem path with each of the required paths specified
- # in the Gem spec. Each expanded path is yielded.
+ # Initialize the filesystem paths to use from +env+.
+ # +env+ is a hash-like object (typically ENV) that
+ # is queried for 'GEM_HOME', 'GEM_PATH', and 'GEM_SPEC_CACHE'
+ # Keys for the +env+ hash should be Strings, and values of the hash should
+ # be Strings or +nil+.
- def self.each_load_path(partials)
- partials.each do |gp|
- base = File.basename(gp)
- specfn = File.join(dir, "specifications", base + ".gemspec")
- if File.exist?(specfn)
- spec = eval(File.read(specfn))
- spec.require_paths.each do |rp|
- yield(File.join(gp, rp))
+ def self.paths=(env)
+ clear_paths
+ target = {}
+ env.each_pair do |k,v|
+ case k
+ when "GEM_HOME", "GEM_PATH", "GEM_SPEC_CACHE"
+ case v
+ when nil, String
+ target[k] = v
+ when Array
+ unless Gem::Deprecate.skip
+ warn <<-EOWARN
+Array values in the parameter to `Gem.paths=` are deprecated.
+Please use a String or nil.
+An Array (#{env.inspect}) was passed in from #{caller[3]}
+ EOWARN
+ end
+ target[k] = v.join File::PATH_SEPARATOR
end
else
- filename = File.join(gp, 'lib')
- yield(filename) if File.exist?(filename)
+ target[k] = v
end
end
+ @paths = Gem::PathSupport.new ENV.to_hash.merge(target)
+ Gem::Specification.dirs = @paths.path
end
- private_class_method :each_load_path
+ ##
+ # The path where gems are to be installed.
+
+ def self.dir
+ paths.home
+ end
+
+ def self.path
+ paths.path
+ end
+
+ def self.spec_cache_dir
+ paths.spec_cache_dir
+ end
##
- # Quietly ensure the named Gem directory contains all the proper
+ # The RbConfig object for the deployment target platform.
+ #
+ # This is usually the same as the running platform, but may be
+ # different if you are cross-compiling.
+
+ def self.target_rbconfig
+ @target_rbconfig || Gem::TargetRbConfig.for_running_ruby
+ end
+
+ def self.set_target_rbconfig(rbconfig_path)
+ @target_rbconfig = Gem::TargetRbConfig.from_path(rbconfig_path)
+ Gem::Platform.local(refresh: true)
+ Gem.platforms << Gem::Platform.local unless Gem.platforms.include? Gem::Platform.local
+ @target_rbconfig
+ end
+
+ ##
+ # Quietly ensure the Gem directory +dir+ contains all the proper
# subdirectories. If we can't create a directory due to a permission
# problem, then we will silently continue.
+ #
+ # If +mode+ is given, missing directories are created with this mode.
+ #
+ # World-writable directories will never be created.
+
+ def self.ensure_gem_subdirectories(dir = Gem.dir, mode = nil)
+ ensure_subdirectories(dir, mode, REPOSITORY_SUBDIRECTORIES)
+ end
- def self.ensure_gem_subdirectories(gemdir)
- require 'fileutils'
+ ##
+ # Quietly ensure the Gem directory +dir+ contains all the proper
+ # subdirectories for handling default gems. If we can't create a
+ # directory due to a permission problem, then we will silently continue.
+ #
+ # If +mode+ is given, missing directories are created with this mode.
+ #
+ # World-writable directories will never be created.
- Gem::DIRECTORIES.each do |filename|
- fn = File.join gemdir, filename
- FileUtils.mkdir_p fn rescue nil unless File.exist? fn
+ def self.ensure_default_gem_subdirectories(dir = Gem.dir, mode = nil)
+ ensure_subdirectories(dir, mode, REPOSITORY_DEFAULT_GEM_SUBDIRECTORIES)
+ end
+
+ def self.ensure_subdirectories(dir, mode, subdirs) # :nodoc:
+ old_umask = File.umask
+ File.umask old_umask | 0o002
+
+ options = {}
+
+ options[:mode] = mode if mode
+
+ subdirs.each do |name|
+ subdir = File.join dir, name
+ next if File.exist? subdir
+
+ require "fileutils"
+
+ begin
+ FileUtils.mkdir_p subdir, **options
+ rescue SystemCallError
+ end
end
+ ensure
+ File.umask old_umask
end
##
- # Returns a list of paths matching +file+ that can be used by a gem to pick
+ # The extension API version of ruby. This includes the static vs non-static
+ # distinction as extensions cannot be shared between the two.
+
+ def self.extension_api_version # :nodoc:
+ if target_rbconfig["ENABLE_SHARED"] == "no"
+ "#{ruby_api_version}-static"
+ else
+ ruby_api_version
+ end
+ end
+
+ ##
+ # Returns a list of paths matching +glob+ that can be used by a gem to pick
# up features from other gems. For example:
#
# Gem.find_files('rdoc/discover').each do |path| load path end
#
- # find_files does not search $LOAD_PATH for files, only gems.
+ # if +check_load_path+ is true (the default), then find_files also searches
+ # $LOAD_PATH for files as well as gems.
+ #
+ # Note that find_files will return all files even if they are from different
+ # versions of the same gem. See also find_latest_files
+
+ def self.find_files(glob, check_load_path = true)
+ files = []
+
+ files = find_files_from_load_path glob if check_load_path
+
+ gem_specifications = @gemdeps ? Gem.loaded_specs.values : Gem::Specification.stubs
- def self.find_files(path)
- specs = searcher.find_all path
+ files.concat gem_specifications.flat_map {|spec|
+ spec.matches_for_glob("#{glob}#{Gem.suffix_pattern}")
+ }
- specs.map do |spec|
- searcher.matching_files spec, path
- end.flatten
+ # $LOAD_PATH might contain duplicate entries or reference
+ # the spec dirs directly, so we prune.
+ files.uniq! if check_load_path
+
+ files
+ end
+
+ def self.find_files_from_load_path(glob) # :nodoc:
+ glob_with_suffixes = "#{glob}#{Gem.suffix_pattern}"
+ $LOAD_PATH.flat_map do |load_path|
+ Gem::Util.glob_files_in_dir(glob_with_suffixes, load_path)
+ end.select {|file| File.file? file }
end
##
- # Finds the user's home directory.
- #--
- # Some comments from the ruby-talk list regarding finding the home
- # directory:
+ # Returns a list of paths matching +glob+ from the latest gems that can be
+ # used by a gem to pick up features from other gems. For example:
#
- # I have HOME, USERPROFILE and HOMEDRIVE + HOMEPATH. Ruby seems
- # to be depending on HOME in those code samples. I propose that
- # it should fallback to USERPROFILE and HOMEDRIVE + HOMEPATH (at
- # least on Win32).
-
- def self.find_home
- ['HOME', 'USERPROFILE'].each do |homekey|
- return ENV[homekey] if ENV[homekey]
- end
+ # Gem.find_latest_files('rdoc/discover').each do |path| load path end
+ #
+ # if +check_load_path+ is true (the default), then find_latest_files also
+ # searches $LOAD_PATH for files as well as gems.
+ #
+ # Unlike find_files, find_latest_files will return only files from the
+ # latest version of a gem.
- if ENV['HOMEDRIVE'] && ENV['HOMEPATH'] then
- return "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}"
- end
+ def self.find_latest_files(glob, check_load_path = true)
+ files = []
- begin
- File.expand_path("~")
- rescue
- if File::ALT_SEPARATOR then
- "C:/"
- else
- "/"
- end
- end
+ files = find_files_from_load_path glob if check_load_path
+
+ files.concat Gem::Specification.latest_specs(true).flat_map {|spec|
+ spec.matches_for_glob("#{glob}#{Gem.suffix_pattern}")
+ }
+
+ # $LOAD_PATH might contain duplicate entries or reference
+ # the spec dirs directly, so we prune.
+ files.uniq! if check_load_path
+
+ files
end
- private_class_method :find_home
+ ##
+ # Top level install helper method. Allows you to install gems interactively:
+ #
+ # % irb
+ # >> Gem.install "minitest"
+ # Fetching: minitest-5.14.0.gem (100%)
+ # => [#<Gem::Specification:0x1013b4528 @name="minitest", ...>]
+
+ def self.install(name, version = Gem::Requirement.default, *options)
+ require_relative "rubygems/dependency_installer"
+ inst = Gem::DependencyInstaller.new(*options)
+ inst.install name, version
+ inst.installed_gems
+ end
##
- # Zlib::GzipReader wrapper that unzips +data+.
+ # Get the default RubyGems API host. This is normally
+ # <tt>https://rubygems.org</tt>.
+
+ def self.host
+ @host ||= Gem::DEFAULT_HOST
+ end
- def self.gunzip(data)
- require 'stringio'
- require 'zlib'
- data = StringIO.new data
+ ## Set the default RubyGems API host.
- Zlib::GzipReader.new(data).read
+ def self.host=(host)
+ @host = host
end
##
- # Zlib::GzipWriter wrapper that zips +data+.
+ # The index to insert activated gem paths into the $LOAD_PATH. The activated
+ # gem's paths are inserted before site lib directory by default.
- def self.gzip(data)
- require 'stringio'
- require 'zlib'
- zipped = StringIO.new
+ def self.load_path_insert_index
+ $LOAD_PATH.each_with_index do |path, i|
+ return i if path.instance_variable_defined?(:@gem_prelude_index)
+ end
- Zlib::GzipWriter.wrap zipped do |io| io.write data end
+ index = $LOAD_PATH.index RbConfig::CONFIG["sitelibdir"]
- zipped.string
+ index || 0
end
##
- # A Zlib::Inflate#inflate wrapper
+ # The number of paths in the +$LOAD_PATH+ from activated gems. Used to
+ # prioritize +-I+ and <code>ENV['RUBYLIB']</code> entries during +require+.
- def self.inflate(data)
- require 'zlib'
- Zlib::Inflate.inflate data
+ def self.activated_gem_paths
+ @activated_gem_paths ||= 0
end
##
- # Return a list of all possible load paths for the latest version for all
- # gems in the Gem installation.
+ # Add a list of paths to the $LOAD_PATH at the proper place.
- def self.latest_load_paths
- result = []
+ def self.add_to_load_path(*paths)
+ @activated_gem_paths = activated_gem_paths + paths.size
- Gem.path.each do |gemdir|
- each_load_path(latest_partials(gemdir)) do |load_path|
- result << load_path
- end
- end
+ # gem directories must come after -I and ENV['RUBYLIB']
+ $LOAD_PATH.insert(Gem.load_path_insert_index, *paths)
+ end
+
+ @yaml_loaded = false
+ @use_psych = nil
+
+ ##
+ # Returns true if the Psych YAML parser is enabled via configuration.
- result
+ def self.use_psych?
+ @use_psych || false
end
##
- # Return only the latest partial paths in the given +gemdir+.
+ # Loads YAML, preferring Psych
- def self.latest_partials(gemdir)
- latest = {}
- all_partials(gemdir).each do |gp|
- base = File.basename(gp)
- if base =~ /(.*)-((\d+\.)*\d+)/ then
- name, version = $1, $2
- ver = Gem::Version.new(version)
- if latest[name].nil? || ver > latest[name][0]
- latest[name] = [ver, gp]
- end
- end
+ def self.load_yaml
+ return if @yaml_loaded
+
+ @use_psych = ENV["RUBYGEMS_USE_PSYCH"] == "true" ||
+ (defined?(@configuration) && @configuration && !@configuration[:use_psych].nil?)
+
+ if @use_psych
+ require "psych"
+ require_relative "rubygems/psych_tree"
end
- latest.collect { |k,v| v[1] }
+
+ require_relative "rubygems/yaml_serializer"
+ require_relative "rubygems/safe_yaml"
+
+ @yaml_loaded = true
end
- private_class_method :latest_partials
+ @safe_marshal_loaded = false
+
+ def self.load_safe_marshal
+ return if @safe_marshal_loaded
+
+ require_relative "rubygems/safe_marshal"
+
+ @safe_marshal_loaded = true
+ end
##
- # The index to insert activated gem paths into the $LOAD_PATH.
- #
- # Defaults to the site lib directory unless gem_prelude.rb has loaded paths,
- # then it inserts the activated gem's paths before the gem_prelude.rb paths
- # so you can override the gem_prelude.rb default $LOAD_PATH paths.
+ # Load Bundler extensions to RubyGems, making sure to avoid redefinition
+ # warnings in platform constants
- def self.load_path_insert_index
- index = $LOAD_PATH.index ConfigMap[:sitelibdir]
+ def self.load_bundler_extensions(version)
+ return unless version <= Gem::Version.create("2.6.9")
- $LOAD_PATH.each_with_index do |path, i|
- if path.instance_variables.include?(:@gem_prelude_index) or
- path.instance_variables.include?('@gem_prelude_index') then
- index = i
- break
- end
+ previous_platforms = {}
+
+ platform_const_list = ["JAVA", "MSWIN", "MSWIN64", "MINGW", "X64_MINGW_LEGACY", "X64_MINGW", "UNIVERSAL_MINGW", "WINDOWS", "X64_LINUX", "X64_LINUX_MUSL"]
+
+ platform_const_list.each do |platform|
+ previous_platforms[platform] = Gem::Platform.const_get(platform)
+ Gem::Platform.send(:remove_const, platform)
end
- index
+ require "bundler/rubygems_ext"
+
+ platform_const_list.each do |platform|
+ Gem::Platform.send(:remove_const, platform) if Gem::Platform.const_defined?(platform)
+ Gem::Platform.const_set(platform, previous_platforms[platform])
+ end
end
##
# The file name and line number of the caller of the caller of this method.
+ #
+ # +depth+ is how many layers up the call stack it should go.
+ #
+ # e.g.,
+ #
+ # def a; Gem.location_of_caller; end
+ # a #=> ["x.rb", 2] # (it'll vary depending on file name and line number)
+ #
+ # def b; c; end
+ # def c; Gem.location_of_caller(2); end
+ # b #=> ["x.rb", 6] # (it'll vary depending on file name and line number)
- def self.location_of_caller
- caller[1] =~ /(.*?):(\d+)$/i
+ def self.location_of_caller(depth = 1)
+ caller[depth] =~ /(.*?):(\d+).*?$/i
file = $1
lineno = $2.to_i
@@ -495,15 +727,6 @@ module Gem
end
##
- # manage_gems is useless and deprecated. Don't call it anymore.
-
- def self.manage_gems # :nodoc:
- file, lineno = location_of_caller
-
- warn "#{file}:#{lineno}:Warning: Gem::manage_gems is deprecated and will be removed on or after March 2009."
- end
-
- ##
# The version of the Marshal format for your Ruby.
def self.marshal_version
@@ -511,25 +734,6 @@ module Gem
end
##
- # Array of paths to search for Gems.
-
- def self.path
- @gem_path ||= nil
-
- unless @gem_path then
- paths = [ENV['GEM_PATH'] || Gem.configuration.path || default_path]
-
- if defined?(APPLE_GEM_HOME) and not ENV['GEM_PATH'] then
- paths << APPLE_GEM_HOME
- end
-
- set_paths paths.compact.join(File::PATH_SEPARATOR)
- end
-
- @gem_path
- end
-
- ##
# Set array of platforms this RubyGems supports (primarily for testing).
def self.platforms=(platforms)
@@ -548,6 +752,17 @@ module Gem
end
##
+ # Adds a post-build hook that will be passed an Gem::Installer instance
+ # when Gem::Installer#install is called. The hook is called after the gem
+ # has been extracted and extensions have been built but before the
+ # executables or gemspec has been written. If the hook returns +false+ then
+ # the gem's files will be removed and the install will be aborted.
+
+ def self.post_build(&hook)
+ @post_build_hooks << hook
+ end
+
+ ##
# Adds a post-install hook that will be passed an Gem::Installer instance
# when Gem::Installer#install is called
@@ -556,6 +771,23 @@ module Gem
end
##
+ # Adds a post-installs hook that will be passed a Gem::DependencyInstaller
+ # and a list of installed specifications when
+ # Gem::DependencyInstaller#install is complete
+
+ def self.done_installing(&hook)
+ @done_installing_hooks << hook
+ end
+
+ ##
+ # Adds a hook that will get run after Gem::Specification.reset is
+ # run.
+
+ def self.post_reset(&hook)
+ @post_reset_hooks << hook
+ end
+
+ ##
# Adds a post-uninstall hook that will be passed a Gem::Uninstaller instance
# and the spec that was uninstalled when Gem::Uninstaller#uninstall is
# called
@@ -566,13 +798,22 @@ module Gem
##
# Adds a pre-install hook that will be passed an Gem::Installer instance
- # when Gem::Installer#install is called
+ # when Gem::Installer#install is called. If the hook returns +false+ then
+ # the install will be aborted.
def self.pre_install(&hook)
@pre_install_hooks << hook
end
##
+ # Adds a hook that will get run before Gem::Specification.reset is
+ # run.
+
+ def self.pre_reset(&hook)
+ @pre_reset_hooks << hook
+ end
+
+ ##
# Adds a pre-uninstall hook that will be passed an Gem::Uninstaller instance
# and the spec that will be uninstalled when Gem::Uninstaller#uninstall is
# called
@@ -582,308 +823,652 @@ module Gem
end
##
- # The directory prefix this RubyGems was installed at.
+ # The directory prefix this RubyGems was installed at. If your
+ # prefix is in a standard location (ie, rubygems is installed where
+ # you'd expect it to be), then prefix returns nil.
def self.prefix
- prefix = File.dirname File.expand_path(__FILE__)
+ prefix = File.dirname RUBYGEMS_DIR
- if File.dirname(prefix) == File.expand_path(ConfigMap[:sitelibdir]) or
- File.dirname(prefix) == File.expand_path(ConfigMap[:libdir]) or
- 'lib' != File.basename(prefix) then
- nil
- else
- File.dirname prefix
+ if prefix != File.expand_path(RbConfig::CONFIG["sitelibdir"]) &&
+ prefix != File.expand_path(RbConfig::CONFIG["libdir"]) &&
+ File.basename(RUBYGEMS_DIR) == "lib"
+ prefix
end
end
##
- # Refresh source_index from disk and clear searcher.
+ # Refresh available gems from disk.
def self.refresh
- source_index.refresh!
-
- MUTEX.synchronize do
- @searcher = nil
- end
+ Gem::Specification.reset
end
##
# Safely read a file in binary mode on all platforms.
def self.read_binary(path)
- File.open path, binary_mode do |f| f.read end
+ File.binread(path)
end
##
- # Report a load error during activation. The message of load error
- # depends on whether it was a version mismatch or if there are not gems of
- # any version by the requested name.
-
- def self.report_activate_error(gem)
- matches = Gem.source_index.find_name(gem.name)
+ # Atomically write a file in binary mode on all platforms.
- if matches.empty? then
- error = Gem::LoadError.new(
- "Could not find RubyGem #{gem.name} (#{gem.version_requirements})\n")
- else
- error = Gem::LoadError.new(
- "RubyGem version error: " +
- "#{gem.name}(#{matches.first.version} not #{gem.version_requirements})\n")
+ def self.write_binary(path, data)
+ Gem::AtomicFileWriter.open(path) do |file|
+ file.write(data)
end
+ end
+
+ ##
+ # Open a file with given flags
- error.name = gem.name
- error.version_requirement = gem.version_requirements
- raise error
+ def self.open_file(path, flags, &block)
+ File.open(path, flags, &block)
end
- private_class_method :report_activate_error
+ ##
+ # Open a file with given flags, and protect access with a file lock
+
+ def self.open_file_with_lock(path, &block)
+ file_lock = "#{path}.lock"
+ open_file_with_flock(file_lock, &block)
+ ensure
+ require "fileutils"
+ FileUtils.rm_f file_lock
+ end
- def self.required_location(gemname, libfile, *version_constraints)
- version_constraints = Gem::Requirement.default if version_constraints.empty?
- matches = Gem.source_index.find_name(gemname, version_constraints)
- return nil if matches.empty?
- spec = matches.last
- spec.require_paths.each do |path|
- result = File.join(spec.full_gem_path, path, libfile)
- return result if File.exist?(result)
+ ##
+ # Open a file with given flags, and protect access with flock
+
+ def self.open_file_with_flock(path, &block)
+ # read-write mode is used rather than read-only in order to support NFS
+ mode = IO::RDWR | IO::APPEND | IO::CREAT | IO::BINARY
+ mode |= IO::SHARE_DELETE if IO.const_defined?(:SHARE_DELETE)
+
+ File.open(path, mode) do |io|
+ begin
+ # Try to get a lock without blocking.
+ # If we do, the file is locked.
+ # Otherwise, explain why we're waiting and get a lock, but block this time.
+ if io.flock(File::LOCK_EX | File::LOCK_NB) != 0
+ warn "Waiting for another process to let go of lock: #{path}"
+ io.flock(File::LOCK_EX)
+ end
+ io.puts(Process.pid)
+ rescue Errno::ENOSYS, Errno::ENOTSUP
+ end
+ yield io
end
- nil
end
##
# The path to the running Ruby interpreter.
def self.ruby
- if @ruby.nil? then
- @ruby = File.join(ConfigMap[:bindir],
- ConfigMap[:ruby_install_name])
- @ruby << ConfigMap[:EXEEXT]
+ if @ruby.nil?
+ @ruby = RbConfig.ruby
- # escape string in case path to ruby executable contain spaces.
- @ruby.sub!(/.*\s.*/m, '"\&"')
+ @ruby = "\"#{@ruby}\"" if /\s/.match?(@ruby)
end
@ruby
end
##
- # A Gem::Version for the currently running ruby.
+ # Returns a String containing the API compatibility version of Ruby
- def self.ruby_version
- return @ruby_version if defined? @ruby_version
- version = RUBY_VERSION.dup
- version << ".#{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL
- @ruby_version = Gem::Version.new version
+ def self.ruby_api_version
+ @ruby_api_version ||= target_rbconfig["ruby_version"].dup
end
- ##
- # The GemPathSearcher object used to search for matching installed gems.
-
- def self.searcher
- MUTEX.synchronize do
- @searcher ||= Gem::GemPathSearcher.new
+ def self.env_requirement(gem_name)
+ @env_requirements_by_name ||= {}
+ @env_requirements_by_name[gem_name] ||= begin
+ req = ENV["GEM_REQUIREMENT_#{gem_name.upcase}"] || ">= 0"
+ Gem::Requirement.create(req)
end
end
+ post_reset { @env_requirements_by_name = {} }
##
- # Set the Gem home directory (as reported by Gem.dir).
+ # Returns the latest release-version specification for the gem +name+.
- def self.set_home(home)
- home = home.gsub(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR
- @gem_home = home
- ensure_gem_subdirectories(@gem_home)
+ def self.latest_spec_for(name)
+ dependency = Gem::Dependency.new name
+ fetcher = Gem::SpecFetcher.fetcher
+ spec_tuples, = fetcher.spec_for_dependency dependency
+
+ spec, = spec_tuples.last
+
+ spec
end
- private_class_method :set_home
+ ##
+ # Returns the latest release version of RubyGems.
+
+ def self.latest_rubygems_version
+ latest_version_for("rubygems-update") ||
+ raise("Can't find 'rubygems-update' in any repo. Check `gem source list`.")
+ end
##
- # Set the Gem search path (as reported by Gem.path).
+ # Returns the version of the latest release-version of gem +name+
- def self.set_paths(gpaths)
- if gpaths
- @gem_path = gpaths.split(File::PATH_SEPARATOR)
+ def self.latest_version_for(name)
+ latest_spec_for(name)&.version
+ end
- if File::ALT_SEPARATOR then
- @gem_path.map! do |path|
- path.gsub File::ALT_SEPARATOR, File::SEPARATOR
- end
- end
+ ##
+ # A Gem::Version for the currently running Ruby.
- @gem_path << Gem.dir
- else
- # TODO: should this be Gem.default_path instead?
- @gem_path = [Gem.dir]
- end
+ def self.ruby_version
+ return @ruby_version if defined? @ruby_version
+ version = RUBY_VERSION.dup
- @gem_path.uniq!
- @gem_path.each do |path|
- if 0 == File.expand_path(path).index(Gem.user_home)
- next unless File.directory? Gem.user_home
- unless win_platform? then
- # only create by matching user
- next if Etc.getpwuid.uid != File::Stat.new(Gem.user_home).uid
- end
+ if RUBY_PATCHLEVEL == -1
+ if RUBY_ENGINE == "ruby"
+ desc = RUBY_DESCRIPTION[/\Aruby #{Regexp.quote(RUBY_VERSION)}([^ ]+) /, 1]
+ else
+ desc = RUBY_DESCRIPTION[/\A#{RUBY_ENGINE} #{Regexp.quote(RUBY_ENGINE_VERSION)} \(#{RUBY_VERSION}([^ ]+)\) /, 1]
end
- ensure_gem_subdirectories path
+ version << ".#{desc}" if desc
end
- end
- private_class_method :set_paths
+ @ruby_version = Gem::Version.new version
+ end
##
- # Returns the Gem::SourceIndex of specifications that are in the Gem.path
+ # A Gem::Version for the currently running RubyGems
- def self.source_index
- @@source_index ||= SourceIndex.from_installed_gems
+ def self.rubygems_version
+ return @rubygems_version if defined? @rubygems_version
+ @rubygems_version = Gem::Version.new Gem::VERSION
end
##
- # Returns an Array of sources to fetch remote gems from. If the sources
- # list is empty, attempts to load the "sources" gem, then uses
- # default_sources if it is not installed.
+ # Returns an Array of sources to fetch remote gems from. Uses
+ # default_sources if the sources list is empty.
def self.sources
- if @sources.empty? then
- begin
- gem 'sources', '> 0.0.1'
- require 'sources'
- rescue LoadError
- @sources = default_sources
- end
- end
-
- @sources
+ source_list = configuration.sources || default_sources
+ @sources ||= Gem::SourceList.from(source_list)
end
##
# Need to be able to set the sources without calling
# Gem.sources.replace since that would cause an infinite loop.
+ #
+ # DOC: This comment is not documentation about the method itself, it's
+ # more of a code comment about the implementation.
def self.sources=(new_sources)
- @sources = new_sources
+ if !new_sources
+ @sources = nil
+ else
+ @sources = Gem::SourceList.from(new_sources)
+ end
end
##
# Glob pattern for require-able path suffixes.
def self.suffix_pattern
- @suffix_pattern ||= "{#{suffixes.join(',')}}"
+ @suffix_pattern ||= "{#{suffixes.join(",")}}"
+ end
+
+ ##
+ # Regexp for require-able path suffixes.
+
+ def self.suffix_regexp
+ @suffix_regexp ||= /#{Regexp.union(suffixes)}\z/
+ end
+
+ ##
+ # Glob pattern for require-able plugin suffixes.
+
+ def self.plugin_suffix_pattern
+ @plugin_suffix_pattern ||= "_plugin#{suffix_pattern}"
+ end
+
+ ##
+ # Regexp for require-able plugin suffixes.
+
+ def self.plugin_suffix_regexp
+ @plugin_suffix_regexp ||= /_plugin#{suffix_regexp}\z/
end
##
# Suffixes for require-able paths.
def self.suffixes
- ['', '.rb', '.rbw', '.so', '.bundle', '.dll', '.sl', '.jar']
+ @suffixes ||= ["",
+ ".rb",
+ *%w[DLEXT DLEXT2].map do |key|
+ val = RbConfig::CONFIG[key]
+ next unless val && !val.empty?
+ ".#{val}"
+ end].compact.uniq
+ end
+
+ ##
+ # Suffixes for dynamic library require-able paths.
+
+ def self.dynamic_library_suffixes
+ @dynamic_library_suffixes ||= suffixes - [".rb"]
+ end
+
+ ##
+ # Prints the amount of time the supplied block takes to run using the debug
+ # UI output.
+
+ def self.time(msg, width = 0, display = Gem.configuration.verbose)
+ now = Time.now
+
+ value = yield
+
+ elapsed = Time.now - now
+
+ ui.say format("%2$*1$s: %3$3.3fs", -width, msg, elapsed) if display
+
+ value
+ end
+
+ ##
+ # Lazily loads DefaultUserInteraction and returns the default UI.
+
+ def self.ui
+ require_relative "rubygems/user_interaction"
+
+ Gem::DefaultUserInteraction.ui
end
##
# Use the +home+ and +paths+ values for Gem.dir and Gem.path. Used mainly
# by the unit tests to provide environment isolation.
- def self.use_paths(home, paths=[])
- clear_paths
- set_home(home) if home
- set_paths(paths.join(File::PATH_SEPARATOR)) if paths
+ def self.use_paths(home, *paths)
+ paths.flatten!
+ paths.compact!
+ hash = { "GEM_HOME" => home, "GEM_PATH" => paths.empty? ? home : paths.join(File::PATH_SEPARATOR) }
+ hash.delete_if {|_, v| v.nil? }
+ self.paths = hash
+ end
+
+ ##
+ # Is this a java platform?
+
+ def self.java_platform?
+ RUBY_PLATFORM == "java"
+ end
+
+ ##
+ # Is this platform Solaris?
+
+ def self.solaris_platform?
+ RUBY_PLATFORM.include?("solaris")
+ end
+
+ ##
+ # Is this platform FreeBSD
+
+ def self.freebsd_platform?
+ RbConfig::CONFIG["host_os"].to_s.include?("bsd")
+ end
+
+ ##
+ # Load +plugins+ as Ruby files
+
+ def self.load_plugin_files(plugins) # :nodoc:
+ plugins.each do |plugin|
+ # Skip older versions of the GemCutter plugin: Its commands are in
+ # RubyGems proper now.
+
+ next if /gemcutter-0\.[0-3]/.match?(plugin)
+
+ begin
+ load plugin
+ rescue ScriptError, StandardError => e
+ details = "#{plugin.inspect}: #{e.message} (#{e.class})"
+ warn "Error loading RubyGems plugin #{details}"
+ end
+ end
+ end
+
+ ##
+ # Find rubygems plugin files in the standard location and load them
+
+ def self.load_plugins
+ Gem.path.each do |gem_path|
+ load_plugin_files Gem::Util.glob_files_in_dir("*#{Gem.plugin_suffix_pattern}", plugindir(gem_path))
+ end
end
##
- # The home directory for the user.
+ # Find all 'rubygems_plugin' files in $LOAD_PATH and load them
- def self.user_home
- @user_home ||= find_home
+ def self.load_env_plugins
+ load_plugin_files find_files_from_load_path("rubygems_plugin")
end
##
- # Is this a windows platform?
+ # Looks for a gem dependency file at +path+ and activates the gems in the
+ # file if found. If the file is not found an ArgumentError is raised.
+ #
+ # If +path+ is not given the RUBYGEMS_GEMDEPS environment variable is used,
+ # but if no file is found no exception is raised.
+ #
+ # If '-' is given for +path+ RubyGems searches up from the current working
+ # directory for gem dependency files (gem.deps.rb, Gemfile, Isolate) and
+ # activates the gems in the first one found.
+ #
+ # You can run this automatically when rubygems starts. To enable, set
+ # the <code>RUBYGEMS_GEMDEPS</code> environment variable to either the path
+ # of your gem dependencies file or "-" to auto-discover in parent
+ # directories.
+ #
+ # NOTE: Enabling automatic discovery on multiuser systems can lead to
+ # execution of arbitrary code when used from directories outside your
+ # control.
+
+ def self.use_gemdeps(path = nil)
+ raise_exception = path
+
+ path ||= ENV["RUBYGEMS_GEMDEPS"]
+ return unless path
- def self.win_platform?
- if @@win_platform.nil? then
- @@win_platform = !!WIN_PATTERNS.find { |r| RUBY_PLATFORM =~ r }
+ path = path.dup
+
+ if path == "-"
+ Gem::Util.traverse_parents Dir.pwd do |directory|
+ dep_file = GEM_DEP_FILES.find {|f| File.file?(f) }
+
+ next unless dep_file
+
+ path = File.join directory, dep_file
+ break
+ end
+ end
+
+ unless File.file? path
+ return unless raise_exception
+
+ raise ArgumentError, "Unable to find gem dependencies file at #{path}"
+ end
+
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path(path)
+ require_relative "rubygems/user_interaction"
+ require "bundler"
+ begin
+ Gem::DefaultUserInteraction.use_ui(ui) do
+ Bundler.ui.silence do
+ @gemdeps = Bundler.setup
+ end
+ ensure
+ Gem::DefaultUserInteraction.ui.close
+ end
+ rescue Bundler::BundlerError => e
+ warn e.message
+ warn "You may need to `bundle install` to install missing gems"
+ warn ""
end
+ end
+
+ ##
+ # If the SOURCE_DATE_EPOCH environment variable is set, returns it's value.
+ # Otherwise, returns DEFAULT_SOURCE_DATE_EPOCH as a string.
+ #
+ # NOTE(@duckinator): The implementation is a tad weird because we want to:
+ # 1. Make builds reproducible by default, by having this function always
+ # return the same result during a given run.
+ # 2. Allow changing ENV['SOURCE_DATE_EPOCH'] at runtime, since multiple
+ # tests that set this variable will be run in a single process.
+ #
+ # If you simplify this function and a lot of tests fail, that is likely
+ # due to #2 above.
+ #
+ # Details on SOURCE_DATE_EPOCH:
+ # https://reproducible-builds.org/specs/source-date-epoch/
- @@win_platform
+ def self.source_date_epoch_string
+ specified_epoch = ENV["SOURCE_DATE_EPOCH"]
+
+ # If it's empty or just whitespace, treat it like it wasn't set at all.
+ specified_epoch = nil if !specified_epoch.nil? && specified_epoch.strip.empty?
+
+ epoch = specified_epoch || DEFAULT_SOURCE_DATE_EPOCH.to_s
+
+ epoch.strip
end
+ ##
+ # Returns the value of Gem.source_date_epoch_string, as a Time object.
+ #
+ # This is used throughout RubyGems for enabling reproducible builds.
+
+ def self.source_date_epoch
+ Time.at(source_date_epoch_string.to_i).utc.freeze
+ end
+
+ # FIX: Almost everywhere else we use the `def self.` way of defining class
+ # methods, and then we switch over to `class << self` here. Pick one or the
+ # other.
class << self
+ ##
+ # RubyGems distributors (like operating system package managers) can
+ # disable RubyGems update by setting this to error message printed to
+ # end-users on gem update --system instead of actual update.
+
+ attr_accessor :disable_system_update_message
+
+ ##
+ # Whether RubyGems should enhance builtin `require` to automatically
+ # check whether the path required is present in installed gems, and
+ # automatically activate them and add them to `$LOAD_PATH`.
+
+ attr_accessor :discover_gems_on_require
+
+ ##
+ # Hash of loaded Gem::Specification keyed by name
attr_reader :loaded_specs
##
- # The list of hooks to be run before Gem::Install#install does any work
+ # GemDependencyAPI object, which is set when .use_gemdeps is called.
+ # This contains all the information from the Gemfile.
+
+ attr_reader :gemdeps
+
+ ##
+ # Register a Gem::Specification for default gem.
+ #
+ # Two formats for the specification are supported:
+ #
+ # * MRI 2.0 style, where spec.files contains unprefixed require names.
+ # The spec's filenames will be registered as-is.
+ # * New style, where spec.files contains files prefixed with paths
+ # from spec.require_paths. The prefixes are stripped before
+ # registering the spec's filenames. Unprefixed files are omitted.
+ #
+
+ def register_default_spec(spec)
+ extended_require_paths = spec.require_paths.map {|f| f + "/" }
+ new_format = extended_require_paths.any? {|path| spec.files.any? {|f| f.start_with? path } }
+
+ if new_format
+ prefix_group = extended_require_paths.join("|")
+ prefix_pattern = /^(#{prefix_group})/
+ end
+
+ native_extension_suffixes = Gem.dynamic_library_suffixes.reject(&:empty?)
+
+ spec.files.each do |file|
+ if new_format
+ file = file.sub(prefix_pattern, "")
+ unless $~
+ # Also register native extension files (e.g. date_core.bundle)
+ # that are listed without require path prefix in the gemspec
+ next if file.include?("/")
+ next unless file.end_with?(*native_extension_suffixes)
+ end
+ end
+
+ spec.activate if already_loaded?(file)
+
+ @path_to_default_spec_map[file] = spec
+ @path_to_default_spec_map[file.sub(suffix_regexp, "")] = spec
+ end
+ end
+
+ ##
+ # Find a Gem::Specification of default gem from +path+
+
+ def find_default_spec(path)
+ @path_to_default_spec_map[path]
+ end
+
+ ##
+ # Find an unresolved Gem::Specification of default gem from +path+
+
+ def find_unresolved_default_spec(path)
+ default_spec = @path_to_default_spec_map[path]
+ default_spec if default_spec && loaded_specs[default_spec.name] != default_spec
+ end
+
+ ##
+ # Clear default gem related variables. It is for test
+
+ def clear_default_specs
+ @path_to_default_spec_map.clear
+ end
+
+ ##
+ # The list of hooks to be run after Gem::Installer#install extracts files
+ # and builds extensions
+
+ attr_reader :post_build_hooks
+
+ ##
+ # The list of hooks to be run after Gem::Installer#install completes
+ # installation
attr_reader :post_install_hooks
##
- # The list of hooks to be run before Gem::Uninstall#uninstall does any
- # work
+ # The list of hooks to be run after Gem::DependencyInstaller installs a
+ # set of gems
- attr_reader :post_uninstall_hooks
+ attr_reader :done_installing_hooks
##
- # The list of hooks to be run after Gem::Install#install is finished
+ # The list of hooks to be run after Gem::Specification.reset is run.
- attr_reader :pre_install_hooks
+ attr_reader :post_reset_hooks
##
- # The list of hooks to be run after Gem::Uninstall#uninstall is finished
+ # The list of hooks to be run after Gem::Uninstaller#uninstall completes
+ # installation
- attr_reader :pre_uninstall_hooks
+ attr_reader :post_uninstall_hooks
- # :stopdoc:
+ ##
+ # The list of hooks to be run before Gem::Installer#install does any work
- alias cache source_index # an alias for the old name
+ attr_reader :pre_install_hooks
- # :startdoc:
+ ##
+ # The list of hooks to be run before Gem::Specification.reset is run.
- end
+ attr_reader :pre_reset_hooks
- MARSHAL_SPEC_DIR = "quick/Marshal.#{Gem.marshal_version}/"
+ ##
+ # The list of hooks to be run before Gem::Uninstaller#uninstall does any
+ # work
- YAML_SPEC_DIR = 'quick/'
+ attr_reader :pre_uninstall_hooks
-end
+ private
-module Config
- # :stopdoc:
- class << self
- # Return the path to the data directory associated with the named
- # package. If the package is loaded as a gem, return the gem
- # specific data directory. Otherwise return a path to the share
- # area as define by "#{ConfigMap[:datadir]}/#{package_name}".
- def datadir(package_name)
- Gem.datadir(package_name) ||
- File.join(Gem::ConfigMap[:datadir], package_name)
+ def already_loaded?(file)
+ $LOADED_FEATURES.any? do |feature_path|
+ feature_path.end_with?(file) && default_gem_load_paths.any? {|load_path_entry| feature_path == "#{load_path_entry}/#{file}" }
+ end
+ end
+
+ def default_gem_load_paths
+ @default_gem_load_paths ||= $LOAD_PATH[load_path_insert_index..-1].map do |lp|
+ expanded = File.expand_path(lp)
+ next expanded unless File.exist?(expanded)
+
+ File.realpath(expanded)
+ end
end
end
- # :startdoc:
+
+ ##
+ # Location of Marshal quick gemspecs on remote repositories
+
+ MARSHAL_SPEC_DIR = "quick/Marshal.#{Gem.marshal_version}/".freeze
+
+ autoload :ConfigFile, File.expand_path("rubygems/config_file", __dir__)
+ autoload :CIDetector, File.expand_path("rubygems/ci_detector", __dir__)
+ autoload :Dependency, File.expand_path("rubygems/dependency", __dir__)
+ autoload :DependencyList, File.expand_path("rubygems/dependency_list", __dir__)
+ autoload :Installer, File.expand_path("rubygems/installer", __dir__)
+ autoload :Licenses, File.expand_path("rubygems/util/licenses", __dir__)
+ autoload :NameTuple, File.expand_path("rubygems/name_tuple", __dir__)
+ autoload :PathSupport, File.expand_path("rubygems/path_support", __dir__)
+ autoload :RequestSet, File.expand_path("rubygems/request_set", __dir__)
+ autoload :Requirement, File.expand_path("rubygems/requirement", __dir__)
+ autoload :Resolver, File.expand_path("rubygems/resolver", __dir__)
+ autoload :Source, File.expand_path("rubygems/source", __dir__)
+ autoload :SourceList, File.expand_path("rubygems/source_list", __dir__)
+ autoload :SpecFetcher, File.expand_path("rubygems/spec_fetcher", __dir__)
+ autoload :SpecificationPolicy, File.expand_path("rubygems/specification_policy", __dir__)
+ autoload :Util, File.expand_path("rubygems/util", __dir__)
+ autoload :Version, File.expand_path("rubygems/version", __dir__)
end
-require 'rubygems/exceptions'
-require 'rubygems/version'
-require 'rubygems/requirement'
-require 'rubygems/dependency'
-require 'rubygems/gem_path_searcher' # Needed for Kernel#gem
-require 'rubygems/source_index' # Needed for Kernel#gem
-require 'rubygems/platform'
-require 'rubygems/builder' # HACK: Needed for rake's package task.
+require_relative "rubygems/exceptions"
+require_relative "rubygems/specification"
+# REFACTOR: This should be pulled out into some kind of hacks file.
begin
- require 'rubygems/defaults/operating_system'
+ # Defaults the operating system (or packager) wants to provide for RubyGems.
+ require "rubygems/defaults/operating_system"
rescue LoadError
+ # Ignored
+rescue StandardError => e
+ path = e.backtrace_locations.reverse.find {|l| l.path.end_with?("rubygems/defaults/operating_system.rb") }.path
+ msg = "#{e.message}\n" \
+ "Loading the #{path} file caused an error. " \
+ "This file is owned by your OS, not by rubygems upstream. " \
+ "Please find out which OS package this file belongs to and follow the guidelines from your OS to report " \
+ "the problem and ask for help."
+ raise e.class, msg
end
-if defined?(RUBY_ENGINE) then
- begin
- require "rubygems/defaults/#{RUBY_ENGINE}"
- rescue LoadError
- end
+begin
+ # Defaults the Ruby implementation wants to provide for RubyGems
+ require "rubygems/defaults/#{RUBY_ENGINE}"
+rescue LoadError
end
-require 'rubygems/config_file'
-
-if RUBY_VERSION < '1.9' then
- require 'rubygems/custom_require'
+##
+# Loads the default specs.
+Gem::Specification.load_defaults
+
+require_relative "rubygems/core_ext/kernel_gem"
+
+path = File.join(__dir__, "rubygems/core_ext/kernel_require.rb")
+# When https://bugs.ruby-lang.org/issues/17259 is available, there is no need to override Kernel#warn
+if RUBY_ENGINE == "truffleruby" ||
+ RUBY_ENGINE == "ruby"
+ file = "<internal:#{path}>"
+else
+ require_relative "rubygems/core_ext/kernel_warn"
+ file = path
end
+eval File.read(path), nil, file
-Gem.clear_paths
+require ENV["BUNDLER_SETUP"] if ENV["BUNDLER_SETUP"] && !defined?(Bundler)
diff --git a/lib/rubygems/available_set.rb b/lib/rubygems/available_set.rb
new file mode 100644
index 0000000000..0af80cc3db
--- /dev/null
+++ b/lib/rubygems/available_set.rb
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+class Gem::AvailableSet
+ include Enumerable
+
+ Tuple = Struct.new(:spec, :source)
+
+ attr_accessor :remote # :nodoc:
+
+ def initialize
+ @set = []
+ @sorted = nil
+ @remote = true
+ end
+
+ attr_reader :set
+
+ def add(spec, source)
+ @set << Tuple.new(spec, source)
+ @sorted = nil
+ self
+ end
+
+ def <<(o)
+ case o
+ when Gem::AvailableSet
+ s = o.set
+ when Array
+ s = o.map do |sp,so|
+ if !sp.is_a?(Gem::Specification) || !so.is_a?(Gem::Source)
+ raise TypeError, "Array must be in [[spec, source], ...] form"
+ end
+
+ Tuple.new(sp,so)
+ end
+ else
+ raise TypeError, "must be a Gem::AvailableSet"
+ end
+
+ @set += s
+ @sorted = nil
+
+ self
+ end
+
+ ##
+ # Yields each Tuple in this AvailableSet
+
+ def each
+ return enum_for __method__ unless block_given?
+
+ @set.each do |tuple|
+ yield tuple
+ end
+ end
+
+ ##
+ # Yields the Gem::Specification for each Tuple in this AvailableSet
+
+ def each_spec
+ return enum_for __method__ unless block_given?
+
+ each do |tuple|
+ yield tuple.spec
+ end
+ end
+
+ def empty?
+ @set.empty?
+ end
+
+ def all_specs
+ @set.map(&:spec)
+ end
+
+ def match_platform!
+ @set.reject! {|t| !Gem::Platform.match_spec?(t.spec) }
+ @sorted = nil
+ self
+ end
+
+ def sorted
+ @sorted ||= @set.sort do |a,b|
+ i = b.spec <=> a.spec
+ i != 0 ? i : (a.source <=> b.source)
+ end
+ end
+
+ def size
+ @set.size
+ end
+
+ def source_for(spec)
+ f = @set.find {|t| t.spec == spec }
+ f.source
+ end
+
+ ##
+ # Converts this AvailableSet into a RequestSet that can be used to install
+ # gems.
+ #
+ # If +development+ is :none then no development dependencies are installed.
+ # Other options are :shallow for only direct development dependencies of the
+ # gems in this set or :all for all development dependencies.
+
+ def to_request_set(development = :none)
+ request_set = Gem::RequestSet.new
+ request_set.development = development == :all
+
+ each_spec do |spec|
+ request_set.always_install << spec
+
+ request_set.gem spec.name, spec.version
+ request_set.import spec.development_dependencies if
+ development == :shallow
+ end
+
+ request_set
+ end
+
+ ##
+ #
+ # Used by the Resolver, the protocol to use a AvailableSet as a
+ # search Set.
+
+ def find_all(req)
+ dep = req.dependency
+
+ match = @set.find_all do |t|
+ dep.match? t.spec
+ end
+
+ match.map do |t|
+ Gem::Resolver::LocalSpecification.new(self, t.spec, t.source)
+ end
+ end
+
+ def prefetch(reqs)
+ end
+
+ def pick_best!
+ return self if empty?
+
+ @set = [sorted.first]
+ @sorted = nil
+ self
+ end
+
+ def remove_installed!(dep)
+ @set.reject! do |_t|
+ # already locally installed
+ Gem::Specification.any? do |installed_spec|
+ dep.name == installed_spec.name &&
+ dep.requirement.satisfied_by?(installed_spec.version)
+ end
+ end
+
+ @sorted = nil
+ self
+ end
+
+ def inject_into_list(dep_list)
+ @set.each {|t| dep_list.add t.spec }
+ end
+end
diff --git a/lib/rubygems/basic_specification.rb b/lib/rubygems/basic_specification.rb
new file mode 100644
index 0000000000..0ed7fc60bb
--- /dev/null
+++ b/lib/rubygems/basic_specification.rb
@@ -0,0 +1,384 @@
+# frozen_string_literal: true
+
+##
+# BasicSpecification is an abstract class which implements some common code
+# used by both Specification and StubSpecification.
+
+class Gem::BasicSpecification
+ ##
+ # Allows installation of extensions for git: gems.
+
+ attr_writer :base_dir # :nodoc:
+
+ ##
+ # Sets the directory where extensions for this gem will be installed.
+
+ attr_writer :extension_dir # :nodoc:
+
+ ##
+ # Is this specification ignored for activation purposes?
+
+ attr_writer :ignored # :nodoc:
+
+ ##
+ # The path this gemspec was loaded from. This attribute is not persisted.
+
+ attr_accessor :loaded_from
+
+ ##
+ # Allows correct activation of git: and path: gems.
+
+ attr_writer :full_gem_path # :nodoc:
+
+ def initialize
+ internal_init
+ end
+
+ ##
+ # The path to the gem.build_complete file within the extension install
+ # directory.
+
+ def gem_build_complete_path # :nodoc:
+ File.join extension_dir, "gem.build_complete"
+ end
+
+ ##
+ # True when the gem has been activated
+
+ def activated?
+ raise NotImplementedError
+ end
+
+ ##
+ # Returns the full path to the base gem directory.
+ #
+ # eg: /usr/local/lib/ruby/gems/1.8
+
+ def base_dir
+ raise NotImplementedError
+ end
+
+ ##
+ # Return true if this spec can require +file+.
+
+ def contains_requirable_file?(file)
+ if ignored?
+ if platform == Gem::Platform::RUBY || Gem::Platform.local === platform
+ warn "Ignoring #{full_name} because its extensions are not built. " \
+ "Try: gem pristine #{name} --version #{version}"
+ end
+
+ return false
+ end
+
+ is_soext = file.end_with?(".so", ".o")
+
+ if is_soext
+ have_file? file.delete_suffix(File.extname(file)), Gem.dynamic_library_suffixes
+ else
+ have_file? file, Gem.suffixes
+ end
+ end
+
+ ##
+ # Return true if this spec should be ignored because it's missing extensions.
+
+ def ignored?
+ return @ignored unless @ignored.nil?
+
+ @ignored = missing_extensions?
+ end
+
+ def default_gem?
+ !loaded_from.nil? &&
+ File.dirname(loaded_from) == Gem.default_specifications_dir
+ end
+
+ ##
+ # Regular gems take precedence over default gems
+
+ def default_gem_priority
+ default_gem? ? 1 : -1
+ end
+
+ ##
+ # Gems higher up in +gem_path+ take precedence
+
+ def base_dir_priority(gem_path)
+ gem_path.index(base_dir) || gem_path.size
+ end
+
+ ##
+ # Returns full path to the directory where gem's extensions are installed.
+
+ def extension_dir
+ @extension_dir ||= File.expand_path(File.join(extensions_dir, full_name))
+ end
+
+ ##
+ # Returns path to the extensions directory.
+
+ def extensions_dir
+ Gem.default_ext_dir_for(base_dir) ||
+ File.join(base_dir, "extensions", Gem::Platform.local.to_s,
+ Gem.extension_api_version)
+ end
+
+ def find_full_gem_path # :nodoc:
+ File.expand_path File.join(gems_dir, full_name)
+ end
+
+ private :find_full_gem_path
+
+ ##
+ # The full path to the gem (install path + full name).
+ #
+ # TODO: This is duplicated with #gem_dir. Eventually either of them should be deprecated.
+
+ def full_gem_path
+ @full_gem_path ||= find_full_gem_path
+ end
+
+ ##
+ # Returns the full name (name-version) of this Gem. Platform information
+ # is included (name-version-platform) if it is specified and not the
+ # default Ruby platform.
+
+ def full_name
+ if platform == Gem::Platform::RUBY || platform.nil?
+ "#{name}-#{version}"
+ else
+ "#{name}-#{version}-#{platform}"
+ end
+ end
+
+ ##
+ # Returns the full name of this Gem (see `Gem::BasicSpecification#full_name`).
+ # Information about where the gem is installed is also included if not
+ # installed in the default GEM_HOME.
+
+ def full_name_with_location
+ if base_dir != Gem.dir
+ "#{full_name} in #{base_dir}"
+ else
+ full_name
+ end
+ end
+
+ ##
+ # Full paths in the gem to add to <code>$LOAD_PATH</code> when this gem is
+ # activated.
+
+ def full_require_paths
+ @full_require_paths ||=
+ begin
+ full_paths = raw_require_paths.map do |path|
+ File.join full_gem_path, path
+ end
+
+ full_paths << extension_dir if have_extensions?
+
+ full_paths
+ end
+ end
+
+ ##
+ # The path to the data directory for this gem.
+
+ def datadir
+ # TODO: drop the extra ", gem_name" which is uselessly redundant
+ File.expand_path(File.join(gems_dir, full_name, "data", name))
+ end
+
+ extend Gem::Deprecate
+ rubygems_deprecate :datadir, :none, "4.1"
+
+ ##
+ # Full path of the target library file.
+ # If the file is not in this gem, return nil.
+
+ def to_fullpath(path)
+ if activated?
+ @paths_map ||= {}
+ Gem.suffixes.each do |suf|
+ full_require_paths.each do |dir|
+ fullpath = "#{dir}/#{path}#{suf}"
+ next unless File.file?(fullpath)
+ @paths_map[path] ||= fullpath
+ end
+ end
+ @paths_map[path]
+ end
+ end
+
+ ##
+ # Returns the full path to this spec's gem directory.
+ # eg: /usr/local/lib/ruby/1.8/gems/mygem-1.0
+ #
+ # TODO: This is duplicated with #full_gem_path. Eventually either of them should be deprecated.
+
+ def gem_dir
+ @gem_dir ||= find_full_gem_path
+ end
+
+ ##
+ # Returns the full path to the gems directory containing this spec's
+ # gem directory. eg: /usr/local/lib/ruby/1.8/gems
+
+ def gems_dir
+ raise NotImplementedError
+ end
+
+ def internal_init # :nodoc:
+ @extension_dir = nil
+ @full_gem_path = nil
+ @gem_dir = nil
+ @ignored = nil
+ end
+
+ ##
+ # Name of the gem
+
+ def name
+ raise NotImplementedError
+ end
+
+ ##
+ # Platform of the gem
+
+ def platform
+ raise NotImplementedError
+ end
+
+ def installable_on_platform?(target_platform) # :nodoc:
+ return true if [Gem::Platform::RUBY, nil, target_platform].include?(platform)
+ return true if Gem::Platform.new(platform) === target_platform
+
+ false
+ end
+
+ def raw_require_paths # :nodoc:
+ raise NotImplementedError
+ end
+
+ ##
+ # Paths in the gem to add to <code>$LOAD_PATH</code> when this gem is
+ # activated.
+ #
+ # See also #require_paths=
+ #
+ # If you have an extension you do not need to add <code>"ext"</code> to the
+ # require path, the extension build process will copy the extension files
+ # into "lib" for you.
+ #
+ # The default value is <code>"lib"</code>
+ #
+ # Usage:
+ #
+ # # If all library files are in the root directory...
+ # spec.require_path = '.'
+
+ def require_paths
+ return raw_require_paths unless have_extensions?
+
+ [extension_dir].concat raw_require_paths
+ end
+
+ ##
+ # Returns the paths to the source files for use with analysis and
+ # documentation tools. These paths are relative to full_gem_path.
+
+ def source_paths
+ paths = raw_require_paths.dup
+
+ if have_extensions?
+ ext_dirs = extensions.map do |extension|
+ extension.split(File::SEPARATOR, 2).first
+ end.uniq
+
+ paths.concat ext_dirs
+ end
+
+ paths.uniq
+ end
+
+ ##
+ # Return all files in this gem that match for +glob+.
+
+ def matches_for_glob(glob) # TODO: rename?
+ glob = File.join(lib_dirs_glob, glob)
+
+ Dir[glob]
+ end
+
+ ##
+ # Returns the list of plugins in this spec.
+
+ def plugins
+ matches_for_glob("rubygems#{Gem.plugin_suffix_pattern}")
+ end
+
+ ##
+ # Returns a string usable in Dir.glob to match all requirable paths
+ # for this spec.
+
+ def lib_dirs_glob
+ dirs = if raw_require_paths
+ if raw_require_paths.size > 1
+ "{#{raw_require_paths.join(",")}}"
+ else
+ raw_require_paths.first
+ end
+ else
+ "lib" # default value for require_paths for bundler/inline
+ end
+
+ "#{full_gem_path}/#{dirs}"
+ end
+
+ ##
+ # Return a Gem::Specification from this gem
+
+ def to_spec
+ raise NotImplementedError
+ end
+
+ ##
+ # Version of the gem
+
+ def version
+ raise NotImplementedError
+ end
+
+ ##
+ # Whether this specification is stubbed - i.e. we have information
+ # about the gem from a stub line, without having to evaluate the
+ # entire gemspec file.
+ def stubbed?
+ raise NotImplementedError
+ end
+
+ def this
+ self
+ end
+
+ private
+
+ def have_extensions?
+ !extensions.empty?
+ end
+
+ def have_file?(file, suffixes)
+ return true if raw_require_paths.any? do |path|
+ base = File.join(gems_dir, full_name, path, file)
+ suffixes.any? {|suf| File.file? base + suf }
+ end
+
+ if have_extensions?
+ base = File.join extension_dir, file
+ suffixes.any? {|suf| File.file? base + suf }
+ else
+ false
+ end
+ end
+end
diff --git a/lib/rubygems/builder.rb b/lib/rubygems/builder.rb
deleted file mode 100644
index 6fd8528f56..0000000000
--- a/lib/rubygems/builder.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-module Gem
-
- ##
- # The Builder class processes RubyGem specification files
- # to produce a .gem file.
- #
- class Builder
-
- include UserInteraction
- ##
- # Constructs a builder instance for the provided specification
- #
- # spec:: [Gem::Specification] The specification instance
- #
- def initialize(spec)
- require "yaml"
- require "rubygems/package"
- require "rubygems/security"
-
- @spec = spec
- end
-
- ##
- # Builds the gem from the specification. Returns the name of the file
- # written.
- #
- def build
- @spec.mark_version
- @spec.validate
- @signer = sign
- write_package
- say success
- @spec.file_name
- end
-
- def success
- <<-EOM
- Successfully built RubyGem
- Name: #{@spec.name}
- Version: #{@spec.version}
- File: #{@spec.full_name+'.gem'}
-EOM
- end
-
- private
-
- def sign
- # if the signing key was specified, then load the file, and swap
- # to the public key (TODO: we should probably just omit the
- # signing key in favor of the signing certificate, but that's for
- # the future, also the signature algorithm should be configurable)
- signer = nil
- if @spec.respond_to?(:signing_key) && @spec.signing_key
- signer = Gem::Security::Signer.new(@spec.signing_key, @spec.cert_chain)
- @spec.signing_key = nil
- @spec.cert_chain = signer.cert_chain.map { |cert| cert.to_s }
- end
- signer
- end
-
- def write_package
- open @spec.file_name, 'wb' do |gem_io|
- Gem::Package.open gem_io, 'w', @signer do |pkg|
- pkg.metadata = @spec.to_yaml
-
- @spec.files.each do |file|
- next if File.directory? file
-
- stat = File.stat file
- mode = stat.mode & 0777
- size = stat.size
-
- pkg.add_file_simple file, mode, size do |tar_io|
- tar_io.write open(file, "rb") { |f| f.read }
- end
- end
- end
- end
- end
- end
-end
-
diff --git a/lib/rubygems/bundler_version_finder.rb b/lib/rubygems/bundler_version_finder.rb
new file mode 100644
index 0000000000..bbe7bf0ab5
--- /dev/null
+++ b/lib/rubygems/bundler_version_finder.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+module Gem::BundlerVersionFinder
+ def self.bundler_version
+ bcv = bundle_config_version
+ return if bcv == "system"
+
+ v = ENV["BUNDLER_VERSION"]
+ v = nil if v&.empty?
+
+ v ||= bundle_update_bundler_version
+ return if v == true
+
+ v ||= bcv unless bcv == "lockfile"
+
+ v ||= lockfile_version
+ return unless v
+
+ Gem::Version.new(v)
+ end
+
+ def self.prioritize!(specs)
+ exact_match_index = specs.find_index {|spec| spec.version == bundler_version }
+ return unless exact_match_index
+
+ specs.unshift(specs.delete_at(exact_match_index))
+ end
+
+ def self.bundle_update_bundler_version
+ return unless ["bundle", "bundler"].include? File.basename($0)
+ return unless "update".start_with?(ARGV.first || " ")
+ bundler_version = nil
+ update_index = nil
+ ARGV.each_with_index do |a, i|
+ if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
+ bundler_version = a
+ end
+ next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
+ bundler_version = $1 || true
+ update_index = i
+ end
+ bundler_version
+ end
+ private_class_method :bundle_update_bundler_version
+
+ def self.lockfile_version
+ return unless contents = lockfile_contents
+ regexp = /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
+ return unless contents =~ regexp
+ $1
+ end
+ private_class_method :lockfile_version
+
+ def self.lockfile_contents
+ gemfile = gemfile_path
+
+ return unless gemfile
+
+ lockfile = ENV["BUNDLE_LOCKFILE"]
+ lockfile = nil if lockfile&.empty?
+
+ lockfile ||= case gemfile
+ when "gems.rb" then "gems.locked"
+ else "#{gemfile}.lock"
+ end
+
+ return unless File.file?(lockfile)
+
+ File.read(lockfile)
+ end
+ private_class_method :lockfile_contents
+
+ def self.bundle_config_version
+ env_version = ENV["BUNDLE_VERSION"]
+ return env_version if env_version && !env_version.empty?
+
+ version = nil
+
+ [bundler_local_config_file, bundler_global_config_file].each do |config_file|
+ next unless config_file && File.file?(config_file)
+
+ contents = File.read(config_file)
+ contents =~ /^BUNDLE_VERSION:\s*["']?([^"'\s]+)["']?\s*$/
+
+ version = $1
+ break if version
+ end
+
+ version
+ end
+ private_class_method :bundle_config_version
+
+ def self.bundler_global_config_file
+ # see Bundler::Settings#global_config_file
+ if ENV["BUNDLE_CONFIG"] && !ENV["BUNDLE_CONFIG"].empty?
+ ENV["BUNDLE_CONFIG"]
+ elsif ENV["BUNDLE_USER_CONFIG"] && !ENV["BUNDLE_USER_CONFIG"].empty?
+ ENV["BUNDLE_USER_CONFIG"]
+ elsif ENV["BUNDLE_USER_HOME"] && !ENV["BUNDLE_USER_HOME"].empty?
+ ENV["BUNDLE_USER_HOME"] + "config"
+ elsif Gem.user_home && !Gem.user_home.empty?
+ Gem.user_home + ".bundle/config"
+ end
+ end
+ private_class_method :bundler_global_config_file
+
+ def self.bundler_local_config_file
+ gemfile = gemfile_path
+ return unless gemfile
+
+ File.join(File.dirname(gemfile), ".bundle", "config")
+ end
+ private_class_method :bundler_local_config_file
+
+ def self.gemfile_path
+ gemfile = ENV["BUNDLE_GEMFILE"]
+ gemfile = nil if gemfile&.empty?
+
+ unless gemfile
+ begin
+ Gem::Util.traverse_parents(Dir.pwd) do |directory|
+ next unless gemfile = Gem::GEM_DEP_FILES.find {|f| File.file?(f) }
+
+ gemfile = File.join directory, gemfile
+ break
+ end
+ rescue Errno::ENOENT
+ return
+ end
+ end
+
+ gemfile
+ end
+ private_class_method :gemfile_path
+end
diff --git a/lib/rubygems/ci_detector.rb b/lib/rubygems/ci_detector.rb
new file mode 100644
index 0000000000..7a2d4ee29a
--- /dev/null
+++ b/lib/rubygems/ci_detector.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Gem
+ module CIDetector
+ # NOTE: Any changes made here will need to be made to both lib/rubygems/ci_detector.rb and
+ # bundler/lib/bundler/ci_detector.rb (which are enforced duplicates).
+ # TODO: Drop that duplication once bundler drops support for RubyGems 3.4
+ #
+ # ## Recognized CI providers, their signifiers, and the relevant docs ##
+ #
+ # Travis CI - CI, TRAVIS https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
+ # Cirrus CI - CI, CIRRUS_CI https://cirrus-ci.org/guide/writing-tasks/#environment-variables
+ # Circle CI - CI, CIRCLECI https://circleci.com/docs/variables/#built-in-environment-variables
+ # Gitlab CI - CI, GITLAB_CI https://docs.gitlab.com/ee/ci/variables/
+ # AppVeyor - CI, APPVEYOR https://www.appveyor.com/docs/environment-variables/
+ # CodeShip - CI_NAME https://docs.cloudbees.com/docs/cloudbees-codeship/latest/pro-builds-and-configuration/environment-variables#_default_environment_variables
+ # dsari - CI, DSARI https://github.com/rfinnie/dsari#running
+ # Jenkins - BUILD_NUMBER https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
+ # TeamCity - TEAMCITY_VERSION https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html#Predefined+Server+Build+Parameters
+ # Appflow - CI_BUILD_ID https://ionic.io/docs/appflow/automation/environments#predefined-environments
+ # TaskCluster - TASKCLUSTER_ROOT_URL https://docs.taskcluster.net/docs/manual/design/env-vars
+ # Semaphore - CI, SEMAPHORE https://docs.semaphoreci.com/ci-cd-environment/environment-variables/
+ # BuildKite - CI, BUILDKITE https://buildkite.com/docs/pipelines/environment-variables
+ # GoCD - GO_SERVER_URL https://docs.gocd.org/current/faq/dev_use_current_revision_in_build.html
+ # GH Actions - CI, GITHUB_ACTIONS https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
+ #
+ # ### Some "standard" ENVs that multiple providers may set ###
+ #
+ # * CI - this is set by _most_ (but not all) CI providers now; it's approaching a standard.
+ # * CI_NAME - Not as frequently used, but some providers set this to specify their own name
+
+ # Any of these being set is a reasonably reliable indicator that we are
+ # executing in a CI environment.
+ ENV_INDICATORS = [
+ "CI",
+ "CI_NAME",
+ "CONTINUOUS_INTEGRATION",
+ "BUILD_NUMBER",
+ "CI_APP_ID",
+ "CI_BUILD_ID",
+ "CI_BUILD_NUMBER",
+ "RUN_ID",
+ "TASKCLUSTER_ROOT_URL",
+ ].freeze
+
+ # For each CI, this env suffices to indicate that we're on _that_ CI's
+ # containers. (A few of them only supply a CI_NAME variable, which is also
+ # nice). And if they set "CI" but we can't tell which one they are, we also
+ # want to know that - a bare "ci" without another token tells us as much.
+ ENV_DESCRIPTORS = {
+ "TRAVIS" => "travis",
+ "CIRCLECI" => "circle",
+ "CIRRUS_CI" => "cirrus",
+ "DSARI" => "dsari",
+ "SEMAPHORE" => "semaphore",
+ "JENKINS_URL" => "jenkins",
+ "BUILDKITE" => "buildkite",
+ "GO_SERVER_URL" => "go",
+ "GITLAB_CI" => "gitlab",
+ "GITHUB_ACTIONS" => "github",
+ "TASKCLUSTER_ROOT_URL" => "taskcluster",
+ "CI" => "ci",
+ }.freeze
+
+ def self.ci?
+ ENV_INDICATORS.any? {|var| ENV.include?(var) }
+ end
+
+ def self.ci_strings
+ matching_names = ENV_DESCRIPTORS.select {|env, _| ENV[env] }.values
+ matching_names << ENV["CI_NAME"].downcase if ENV["CI_NAME"]
+ matching_names.reject(&:empty?).sort.uniq
+ end
+ end
+end
diff --git a/lib/rubygems/command.rb b/lib/rubygems/command.rb
index 860764e6d5..d38363f293 100644
--- a/lib/rubygems/command.rb
+++ b/lib/rubygems/command.rb
@@ -1,406 +1,664 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'optparse'
+require_relative "vendored_optparse"
+require_relative "requirement"
+require_relative "user_interaction"
-require 'rubygems/user_interaction'
+##
+# Base class for all Gem commands. When creating a new gem command, define
+# #initialize, #execute, #arguments, #defaults_str, #description and #usage
+# (as appropriate). See the above mentioned methods for details.
+#
+# A very good example to look at is Gem::Commands::ContentsCommand
-module Gem
+class Gem::Command
+ include Gem::UserInteraction
- # Base class for all Gem commands. When creating a new gem command, define
- # #arguments, #defaults_str, #description and #usage (as appropriate).
- class Command
+ Gem::OptionParser.accept Symbol, &:to_sym
- include UserInteraction
+ ##
+ # The name of the command.
- # The name of the command.
- attr_reader :command
+ attr_reader :command
- # The options for the command.
- attr_reader :options
+ ##
+ # The options for the command.
- # The default options for the command.
- attr_accessor :defaults
+ attr_reader :options
- # The name of the command for command-line invocation.
- attr_accessor :program_name
+ ##
+ # The default options for the command.
- # A short description of the command.
- attr_accessor :summary
+ attr_accessor :defaults
- # Initializes a generic gem command named +command+. +summary+ is a short
- # description displayed in `gem help commands`. +defaults+ are the
- # default options. Defaults should be mirrored in #defaults_str, unless
- # there are none.
- #
- # Use add_option to add command-line switches.
- def initialize(command, summary=nil, defaults={})
- @command = command
- @summary = summary
- @program_name = "gem #{command}"
- @defaults = defaults
- @options = defaults.dup
- @option_groups = Hash.new { |h,k| h[k] = [] }
- @parser = nil
- @when_invoked = nil
- end
+ ##
+ # The name of the command for command-line invocation.
- # True if +long+ begins with the characters from +short+.
- def begins?(long, short)
- return false if short.nil?
- long[0, short.length] == short
- end
+ attr_accessor :program_name
- # Override to provide command handling.
- def execute
- fail "Generic command has no actions"
- end
+ ##
+ # A short description of the command.
- # Get all gem names from the command line.
- def get_all_gem_names
- args = options[:args]
+ attr_accessor :summary
- if args.nil? or args.empty? then
- raise Gem::CommandLineError,
- "Please specify at least one gem name (e.g. gem build GEMNAME)"
- end
+ ##
+ # Arguments used when building gems
- gem_names = args.select { |arg| arg !~ /^-/ }
- end
+ def self.build_args
+ @build_args ||= []
+ end
- # Get the single gem name from the command line. Fail if there is no gem
- # name or if there is more than one gem name given.
- def get_one_gem_name
- args = options[:args]
+ def self.build_args=(value)
+ @build_args = value
+ end
- if args.nil? or args.empty? then
- raise Gem::CommandLineError,
- "Please specify a gem name on the command line (e.g. gem build GEMNAME)"
- end
+ def self.common_options
+ @common_options ||= []
+ end
- if args.size > 1 then
- raise Gem::CommandLineError,
- "Too many gem names (#{args.join(', ')}); please specify only one"
- end
+ def self.add_common_option(*args, &handler)
+ Gem::Command.common_options << [args, handler]
+ end
- args.first
- end
+ def self.extra_args
+ @extra_args ||= []
+ end
- # Get a single optional argument from the command line. If more than one
- # argument is given, return only the first. Return nil if none are given.
- def get_one_optional_argument
- args = options[:args] || []
- args.first
+ def self.extra_args=(value)
+ case value
+ when Array
+ @extra_args = value
+ when String
+ @extra_args = value.split(" ")
end
+ end
- # Override to provide details of the arguments a command takes.
- # It should return a left-justified string, one argument per line.
- def arguments
- ""
- end
+ ##
+ # Return an array of extra arguments for the command. The extra arguments
+ # come from the gem configuration file read at program startup.
- # Override to display the default values of the command
- # options. (similar to +arguments+, but displays the default
- # values).
- def defaults_str
- ""
- end
+ def self.specific_extra_args(cmd)
+ specific_extra_args_hash[cmd]
+ end
- # Override to display a longer description of what this command does.
- def description
- nil
- end
+ ##
+ # Add a list of extra arguments for the given command. +args+ may be an
+ # array or a string to be split on white space.
- # Override to display the usage for an individual gem command.
- def usage
- program_name
- end
+ def self.add_specific_extra_args(cmd,args)
+ args = args.split(/\s+/) if args.is_a? String
+ specific_extra_args_hash[cmd] = args
+ end
- # Display the help message for the command.
- def show_help
- parser.program_name = usage
- say parser
- end
+ ##
+ # Accessor for the specific extra args hash (self initializing).
- # Invoke the command with the given list of arguments.
- def invoke(*args)
- handle_options(args)
- if options[:help]
- show_help
- elsif @when_invoked
- @when_invoked.call(options)
+ def self.specific_extra_args_hash
+ @specific_extra_args_hash ||= Hash.new do |h,k|
+ h[k] = Array.new
+ end
+ end
+
+ ##
+ # Initializes a generic gem command named +command+. +summary+ is a short
+ # description displayed in `gem help commands`. +defaults+ are the default
+ # options. Defaults should be mirrored in #defaults_str, unless there are
+ # none.
+ #
+ # When defining a new command subclass, use add_option to add command-line
+ # switches.
+ #
+ # Unhandled arguments (gem names, files, etc.) are left in
+ # <tt>options[:args]</tt>.
+
+ def initialize(command, summary = nil, defaults = {})
+ @command = command
+ @summary = summary
+ @program_name = "gem #{command}"
+ @defaults = defaults
+ @options = defaults.dup
+ @option_groups = Hash.new {|h,k| h[k] = [] }
+ @deprecated_options = { command => {} }
+ @parser = nil
+ @when_invoked = nil
+ end
+
+ ##
+ # True if +long+ begins with the characters from +short+.
+
+ def begins?(long, short)
+ return false if short.nil?
+ long[0, short.length] == short
+ end
+
+ ##
+ # Override to provide command handling.
+ #
+ # #options will be filled in with your parsed options, unparsed options will
+ # be left in <tt>options[:args]</tt>.
+ #
+ # See also: #get_all_gem_names, #get_one_gem_name,
+ # #get_one_optional_argument
+
+ def execute
+ raise Gem::Exception, "generic command has no actions"
+ end
+
+ ##
+ # Display to the user that a gem couldn't be found and reasons why
+ #--
+
+ def show_lookup_failure(gem_name, version, errors, suppress_suggestions = false, required_by = nil)
+ gem = "'#{gem_name}' (#{version})"
+ msg = String.new "Could not find a valid gem #{gem}"
+
+ if errors && !errors.empty?
+ msg << ", here is why:\n"
+ errors.each {|x| msg << " #{x.wordy}\n" }
+ else
+ if required_by && gem != required_by
+ msg << " (required by #{required_by}) in any repository"
else
- execute
+ msg << " in any repository"
end
end
- # Call the given block when invoked.
- #
- # Normal command invocations just executes the +execute+ method of
- # the command. Specifying an invocation block allows the test
- # methods to override the normal action of a command to determine
- # that it has been invoked correctly.
- def when_invoked(&block)
- @when_invoked = block
+ alert_error msg
+
+ unless suppress_suggestions
+ suggestions = Gem::SpecFetcher.fetcher.suggest_gems_from_name(gem_name, :latest, 10)
+ unless suggestions.empty?
+ alert_error "Possible alternatives: #{suggestions.join(", ")}"
+ end
end
+ end
- # Add a command-line option and handler to the command.
- #
- # See OptionParser#make_switch for an explanation of +opts+.
- #
- # +handler+ will be called with two values, the value of the argument and
- # the options hash.
- def add_option(*opts, &handler) # :yields: value, options
- group_name = Symbol === opts.first ? opts.shift : :options
+ ##
+ # Get all gem names from the command line.
- @option_groups[group_name] << [opts, handler]
- end
+ def get_all_gem_names
+ args = options[:args]
- # Remove previously defined command-line argument +name+.
- def remove_option(name)
- @option_groups.each do |_, option_list|
- option_list.reject! { |args, _| args.any? { |x| x =~ /^#{name}/ } }
- end
+ if args.nil? || args.empty?
+ raise Gem::CommandLineError,
+ "Please specify at least one gem name (e.g. gem build GEMNAME)"
end
- # Merge a set of command options with the set of default options
- # (without modifying the default option hash).
- def merge_options(new_options)
- @options = @defaults.clone
- new_options.each do |k,v| @options[k] = v end
+ args.reject {|arg| arg.start_with?("-") }
+ end
+
+ ##
+ # Get all [gem, version] from the command line.
+ #
+ # An argument in the form gem:ver is pull apart into the gen name and version,
+ # respectively.
+ def get_all_gem_names_and_versions
+ get_all_gem_names.map do |name|
+ extract_gem_name_and_version(name)
end
+ end
- # True if the command handles the given argument list.
- def handles?(args)
- begin
- parser.parse!(args.dup)
- return true
- rescue
- return false
- end
+ def extract_gem_name_and_version(name) # :nodoc:
+ if /\A(.*):(#{Gem::Requirement::PATTERN_RAW})\z/ =~ name
+ [$1, $2]
+ else
+ [name]
end
+ end
+
+ ##
+ # Get a single gem name from the command line. Fail if there is no gem name
+ # or if there is more than one gem name given.
+
+ def get_one_gem_name
+ args = options[:args]
- # Handle the given list of arguments by parsing them and recording
- # the results.
- def handle_options(args)
- args = add_extra_args(args)
- @options = @defaults.clone
- parser.parse!(args)
- @options[:args] = args
+ if args.nil? || args.empty?
+ raise Gem::CommandLineError,
+ "Please specify a gem name on the command line (e.g. gem build GEMNAME)"
end
- def add_extra_args(args)
- result = []
- s_extra = Command.specific_extra_args(@command)
- extra = Command.extra_args + s_extra
- while ! extra.empty?
- ex = []
- ex << extra.shift
- ex << extra.shift if extra.first.to_s =~ /^[^-]/
- result << ex if handles?(ex)
- end
- result.flatten!
- result.concat(args)
- result
+ if args.size > 1
+ raise Gem::CommandLineError,
+ "Too many gem names (#{args.join(", ")}); please specify only one"
end
- private
+ args.first
+ end
+
+ ##
+ # Get a single optional argument from the command line. If more than one
+ # argument is given, return only the first. Return nil if none are given.
+
+ def get_one_optional_argument
+ args = options[:args] || []
+ args.first
+ end
+
+ ##
+ # Override to provide details of the arguments a command takes. It should
+ # return a left-justified string, one argument per line.
+ #
+ # For example:
+ #
+ # def usage
+ # "#{program_name} FILE [FILE ...]"
+ # end
+ #
+ # def arguments
+ # "FILE name of file to find"
+ # end
+
+ def arguments
+ ""
+ end
+
+ ##
+ # Override to display the default values of the command options. (similar to
+ # +arguments+, but displays the default values).
+ #
+ # For example:
+ #
+ # def defaults_str
+ # --no-gems-first --no-all
+ # end
+
+ def defaults_str
+ ""
+ end
+
+ ##
+ # Override to display a longer description of what this command does.
+
+ def description
+ nil
+ end
+
+ ##
+ # Override to display the usage for an individual gem command.
+ #
+ # The text "[options]" is automatically appended to the usage text.
+
+ def usage
+ program_name
+ end
+
+ ##
+ # Display the help message for the command.
+
+ def show_help
+ parser.program_name = usage
+ say parser
+ end
+
+ ##
+ # Invoke the command with the given list of arguments.
+
+ def invoke(*args)
+ invoke_with_build_args args, nil
+ end
+
+ ##
+ # Invoke the command with the given list of normal arguments
+ # and additional build arguments.
+
+ def invoke_with_build_args(args, build_args)
+ handle_options args
+
+ options[:build_args] = build_args
+
+ if options[:silent]
+ old_ui = ui
+ self.ui = ui = Gem::SilentUI.new
+ end
- # Create on demand parser.
- def parser
- create_option_parser if @parser.nil?
- @parser
+ if options[:help]
+ show_help
+ elsif @when_invoked
+ @when_invoked.call options
+ else
+ execute
+ end
+ ensure
+ if ui
+ self.ui = old_ui
+ ui.close
+ end
+ end
+
+ ##
+ # Call the given block when invoked.
+ #
+ # Normal command invocations just executes the +execute+ method of the
+ # command. Specifying an invocation block allows the test methods to
+ # override the normal action of a command to determine that it has been
+ # invoked correctly.
+
+ def when_invoked(&block)
+ @when_invoked = block
+ end
+
+ ##
+ # Add a command-line option and handler to the command.
+ #
+ # See Gem::OptionParser#make_switch for an explanation of +opts+.
+ #
+ # +handler+ will be called with two values, the value of the argument and
+ # the options hash.
+ #
+ # If the first argument of add_option is a Symbol, it's used to group
+ # options in output. See `gem help list` for an example.
+
+ def add_option(*opts, &handler) # :yields: value, options
+ group_name = Symbol === opts.first ? opts.shift : :options
+
+ raise "Do not pass an empty string in opts" if opts.include?("")
+
+ @option_groups[group_name] << [opts, handler]
+ end
+
+ ##
+ # Remove previously defined command-line argument +name+.
+
+ def remove_option(name)
+ @option_groups.each do |_, option_list|
+ option_list.reject! {|args, _| args.any? {|x| x.is_a?(String) && x =~ /^#{name}/ } }
end
+ end
+
+ ##
+ # Mark a command-line option as deprecated, and optionally specify a
+ # deprecation horizon.
+ #
+ # Note that with the current implementation, every version of the option needs
+ # to be explicitly deprecated, so to deprecate an option defined as
+ #
+ # add_option('-t', '--[no-]test', 'Set test mode') do |value, options|
+ # # ... stuff ...
+ # end
+ #
+ # you would need to explicitly add a call to `deprecate_option` for every
+ # version of the option you want to deprecate, like
+ #
+ # deprecate_option('-t')
+ # deprecate_option('--test')
+ # deprecate_option('--no-test')
+
+ def deprecate_option(name, version: nil, extra_msg: nil)
+ @deprecated_options[command].merge!({ name => { "rg_version_to_expire" => version, "extra_msg" => extra_msg } })
+ end
+
+ def check_deprecated_options(options)
+ options.each do |option|
+ next unless option_is_deprecated?(option)
+ deprecation = @deprecated_options[command][option]
+ version_to_expire = deprecation["rg_version_to_expire"]
+
+ deprecate_option_msg = if version_to_expire
+ "The \"#{option}\" option has been deprecated and will be removed in Rubygems #{version_to_expire}."
+ else
+ "The \"#{option}\" option has been deprecated and will be removed in future versions of Rubygems."
+ end
- def create_option_parser
- @parser = OptionParser.new
+ extra_msg = deprecation["extra_msg"]
- @parser.separator("")
- regular_options = @option_groups.delete :options
+ deprecate_option_msg += " #{extra_msg}" if extra_msg
- configure_options "", regular_options
+ alert_warning(deprecate_option_msg)
+ end
+ end
+
+ ##
+ # Merge a set of command options with the set of default options (without
+ # modifying the default option hash).
+
+ def merge_options(new_options)
+ @options = @defaults.clone
+ new_options.each {|k,v| @options[k] = v }
+ end
+
+ ##
+ # True if the command handles the given argument list.
+
+ def handles?(args)
+ parser.parse!(args.dup)
+ true
+ rescue StandardError
+ false
+ end
+
+ ##
+ # Handle the given list of arguments by parsing them and recording the
+ # results.
+
+ def handle_options(args)
+ args = add_extra_args(args)
+ check_deprecated_options(args)
+ @options = Marshal.load Marshal.dump @defaults # deep copy
+ parser.parse!(args)
+ @options[:args] = args
+ end
+
+ ##
+ # Adds extra args from ~/.gemrc
+
+ def add_extra_args(args)
+ result = []
+
+ s_extra = Gem::Command.specific_extra_args(@command)
+ extra = Gem::Command.extra_args + s_extra
+
+ until extra.empty? do
+ ex = []
+ ex << extra.shift
+ ex << extra.shift if /^[^-]/.match?(extra.first.to_s)
+ result << ex if handles?(ex)
+ end
- @option_groups.sort_by { |n,_| n.to_s }.each do |group_name, option_list|
- configure_options group_name, option_list
- end
+ result.flatten!
+ result.concat(args)
+ result
+ end
- configure_options "Common", Command.common_options
+ def deprecated?
+ false
+ end
- @parser.separator("")
- unless arguments.empty?
- @parser.separator(" Arguments:")
- arguments.split(/\n/).each do |arg_desc|
- @parser.separator(" #{arg_desc}")
- end
- @parser.separator("")
- end
+ private
- @parser.separator(" Summary:")
- wrap(@summary, 80 - 4).split("\n").each do |line|
- @parser.separator(" #{line.strip}")
- end
+ def option_is_deprecated?(option)
+ @deprecated_options[command].key?(option)
+ end
- if description then
- formatted = description.split("\n\n").map do |chunk|
- wrap(chunk, 80 - 4)
- end.join("\n")
+ def add_parser_description # :nodoc:
+ return unless description
- @parser.separator ""
- @parser.separator " Description:"
- formatted.split("\n").each do |line|
- @parser.separator " #{line.rstrip}"
- end
- end
+ formatted = description.split("\n\n").map do |chunk|
+ wrap chunk, 80 - 4
+ end.join "\n"
- unless defaults_str.empty?
- @parser.separator("")
- @parser.separator(" Defaults:")
- defaults_str.split(/\n/).each do |line|
- @parser.separator(" #{line}")
- end
- end
+ @parser.separator nil
+ @parser.separator " Description:"
+ formatted.each_line do |line|
+ @parser.separator " #{line.rstrip}"
end
+ end
- def configure_options(header, option_list)
- return if option_list.nil? or option_list.empty?
+ def add_parser_options # :nodoc:
+ @parser.separator nil
- header = header.to_s.empty? ? '' : "#{header} "
- @parser.separator " #{header}Options:"
+ regular_options = @option_groups.delete :options
- option_list.each do |args, handler|
- dashes = args.select { |arg| arg =~ /^-/ }
- @parser.on(*args) do |value|
- handler.call(value, @options)
- end
- end
+ configure_options "", regular_options
- @parser.separator ''
+ @option_groups.sort_by {|n,_| n.to_s }.each do |group_name, option_list|
+ @parser.separator nil
+ configure_options group_name, option_list
end
+ end
- # Wraps +text+ to +width+
- def wrap(text, width)
- text.gsub(/(.{1,#{width}})( +|$\n?)|(.{1,#{width}})/, "\\1\\3\n")
- end
+ ##
+ # Adds a section with +title+ and +content+ to the parser help view. Used
+ # for adding command arguments and default arguments.
- ##################################################################
- # Class methods for Command.
- class << self
- def common_options
- @common_options ||= []
- end
-
- def add_common_option(*args, &handler)
- Gem::Command.common_options << [args, handler]
- end
+ def add_parser_run_info(title, content)
+ return if content.empty?
- def extra_args
- @extra_args ||= []
- end
+ @parser.separator nil
+ @parser.separator " #{title}:"
+ content.each_line do |line|
+ @parser.separator " #{line.rstrip}"
+ end
+ end
- def extra_args=(value)
- case value
- when Array
- @extra_args = value
- when String
- @extra_args = value.split
- end
- end
+ def add_parser_summary # :nodoc:
+ return unless @summary
- # Return an array of extra arguments for the command. The extra
- # arguments come from the gem configuration file read at program
- # startup.
- def specific_extra_args(cmd)
- specific_extra_args_hash[cmd]
- end
+ @parser.separator nil
+ @parser.separator " Summary:"
+ wrap(@summary, 80 - 4).each_line do |line|
+ @parser.separator " #{line.strip}"
+ end
+ end
- # Add a list of extra arguments for the given command. +args+
- # may be an array or a string to be split on white space.
- def add_specific_extra_args(cmd,args)
- args = args.split(/\s+/) if args.kind_of? String
- specific_extra_args_hash[cmd] = args
- end
+ ##
+ # Create on demand parser.
- # Accessor for the specific extra args hash (self initializing).
- def specific_extra_args_hash
- @specific_extra_args_hash ||= Hash.new do |h,k|
- h[k] = Array.new
- end
- end
- end
+ def parser
+ create_option_parser if @parser.nil?
+ @parser
+ end
- # ----------------------------------------------------------------
- # Add the options common to all commands.
+ ##
+ # Creates an option parser and fills it in with the help info for the
+ # command.
- add_common_option('-h', '--help',
- 'Get help on this command') do
- |value, options|
- options[:help] = true
- end
+ def create_option_parser
+ @parser = Gem::OptionParser.new
- add_common_option('-V', '--[no-]verbose',
- 'Set the verbose level of output') do |value, options|
- # Set us to "really verbose" so the progress meter works
- if Gem.configuration.verbose and value then
- Gem.configuration.verbose = 1
- else
- Gem.configuration.verbose = value
- end
- end
+ add_parser_options
- add_common_option('-q', '--quiet', 'Silence commands') do |value, options|
- Gem.configuration.verbose = false
- end
+ @parser.separator nil
+ configure_options "Common", Gem::Command.common_options
- # Backtrace and config-file are added so they show up in the help
- # commands. Both options are actually handled before the other
- # options get parsed.
+ add_parser_run_info "Arguments", arguments
+ add_parser_summary
+ add_parser_description
+ add_parser_run_info "Defaults", defaults_str
+ end
- add_common_option('--config-file FILE',
- "Use this config file instead of default") do
- end
+ def configure_options(header, option_list)
+ return if option_list.nil? || option_list.empty?
- add_common_option('--backtrace',
- 'Show stack backtrace on errors') do
- end
+ header = header.to_s.empty? ? "" : "#{header} "
+ @parser.separator " #{header}Options:"
- add_common_option('--debug',
- 'Turn on Ruby debugging') do
+ option_list.each do |args, handler|
+ @parser.on(*args) do |value|
+ handler.call(value, @options)
+ end
end
- # :stopdoc:
- HELP = %{
- RubyGems is a sophisticated package manager for Ruby. This is a
- basic help message containing pointers to more information.
+ @parser.separator ""
+ end
- Usage:
- gem -h/--help
- gem -v/--version
- gem command [arguments...] [options...]
+ ##
+ # Wraps +text+ to +width+
- Examples:
- gem install rake
- gem list --local
- gem build package.gemspec
- gem help install
+ def wrap(text, width) # :doc:
+ text.gsub(/(.{1,#{width}})( +|$\n?)|(.{1,#{width}})/, "\\1\\3\n")
+ end
- Further help:
- gem help commands list all 'gem' commands
- gem help examples show some examples of usage
- gem help platforms show information about platforms
- gem help <COMMAND> show help on COMMAND
- (e.g. 'gem help install')
- Further information:
- http://rubygems.rubyforge.org
- }.gsub(/^ /, "")
+ # ----------------------------------------------------------------
+ # Add the options common to all commands.
- # :startdoc:
+ add_common_option("-h", "--help",
+ "Get help on this command") do |_value, options|
+ options[:help] = true
+ end
- end # class
+ add_common_option("-V", "--[no-]verbose",
+ "Set the verbose level of output") do |value, _options|
+ # Set us to "really verbose" so the progress meter works
+ if Gem.configuration.verbose && value
+ Gem.configuration.verbose = 1
+ else
+ Gem.configuration.verbose = value
+ end
+ end
+
+ add_common_option("-q", "--quiet", "Silence command progress meter") do |_value, _options|
+ Gem.configuration.verbose = false
+ end
+
+ add_common_option("--silent",
+ "Silence RubyGems output") do |_value, options|
+ options[:silent] = true
+ end
+
+ # Backtrace and config-file are added so they show up in the help
+ # commands. Both options are actually handled before the other
+ # options get parsed.
+
+ add_common_option("--config-file FILE",
+ "Use this config file instead of default") do
+ end
+
+ add_common_option("--backtrace",
+ "Show stack backtrace on errors") do
+ end
+
+ add_common_option("--debug",
+ "Turn on Ruby debugging") do
+ end
+
+ add_common_option("--norc",
+ "Avoid loading any .gemrc file") do
+ end
+
+ # :stopdoc:
+
+ HELP = <<-HELP
+RubyGems is a package manager for Ruby.
+
+ Usage:
+ gem -h/--help
+ gem -v/--version
+ gem [global options...] command [arguments...] [options...]
+
+ Global options:
+ -C PATH run as if gem was started in <PATH>
+ instead of the current working directory
+
+ Examples:
+ gem install rake
+ gem list --local
+ gem build package.gemspec
+ gem push package-0.0.1.gem
+ gem help install
+
+ Further help:
+ gem help commands list all 'gem' commands
+ gem help examples show some examples of usage
+ gem help gem_dependencies gem dependencies file guide
+ gem help platforms gem platforms guide
+ gem help <COMMAND> show help on COMMAND
+ (e.g. 'gem help install')
+ Further information:
+ https://guides.rubygems.org
+ HELP
+
+ # :startdoc:
+end
- # This is where Commands will be placed in the namespace
- module Commands; end
+##
+# \Commands will be placed in this namespace
+module Gem::Commands
end
diff --git a/lib/rubygems/command_manager.rb b/lib/rubygems/command_manager.rb
index dd9a1aee15..76b2fba835 100644
--- a/lib/rubygems/command_manager.rb
+++ b/lib/rubygems/command_manager.rb
@@ -1,146 +1,254 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'timeout'
-require 'rubygems/command'
-require 'rubygems/user_interaction'
-
-module Gem
-
- ####################################################################
- # The command manager registers and installs all the individual
- # sub-commands supported by the gem command.
- class CommandManager
- include UserInteraction
-
- # Return the authoritative instance of the command manager.
- def self.instance
- @command_manager ||= CommandManager.new
- end
-
- # Register all the subcommands supported by the gem command.
- def initialize
- @commands = {}
- register_command :build
- register_command :cert
- register_command :check
- register_command :cleanup
- register_command :contents
- register_command :dependency
- register_command :environment
- register_command :fetch
- register_command :generate_index
- register_command :help
- register_command :install
- register_command :list
- register_command :lock
- register_command :mirror
- register_command :outdated
- register_command :pristine
- register_command :query
- register_command :rdoc
- register_command :search
- register_command :server
- register_command :sources
- register_command :specification
- register_command :stale
- register_command :uninstall
- register_command :unpack
- register_command :update
- register_command :which
- end
-
- # Register the command object.
- def register_command(command_obj)
- @commands[command_obj] = false
- end
-
- # Return the registered command from the command name.
- def [](command_name)
- command_name = command_name.intern
- return nil if @commands[command_name].nil?
- @commands[command_name] ||= load_and_instantiate(command_name)
+require_relative "command"
+require_relative "user_interaction"
+require_relative "text"
+
+##
+# The command manager registers and installs all the individual sub-commands
+# supported by the gem command.
+#
+# Extra commands can be provided by writing a rubygems_plugin.rb
+# file in an installed gem. You should register your command against the
+# Gem::CommandManager instance, like this:
+#
+# # file rubygems_plugin.rb
+# require 'rubygems/command_manager'
+#
+# Gem::CommandManager.instance.register_command :edit
+#
+# You should put the implementation of your command in rubygems/commands.
+#
+# # file rubygems/commands/edit_command.rb
+# class Gem::Commands::EditCommand < Gem::Command
+# # ...
+# end
+#
+# See Gem::Command for instructions on writing gem commands.
+
+class Gem::CommandManager
+ include Gem::Text
+ include Gem::UserInteraction
+
+ BUILTIN_COMMANDS = [ # :nodoc:
+ :build,
+ :cert,
+ :check,
+ :cleanup,
+ :contents,
+ :dependency,
+ :environment,
+ :exec,
+ :fetch,
+ :generate_index,
+ :help,
+ :info,
+ :install,
+ :list,
+ :lock,
+ :mirror,
+ :open,
+ :outdated,
+ :owner,
+ :pristine,
+ :push,
+ :rdoc,
+ :rebuild,
+ :search,
+ :server,
+ :signin,
+ :signout,
+ :sources,
+ :specification,
+ :stale,
+ :uninstall,
+ :unpack,
+ :update,
+ :which,
+ :yank,
+ ].freeze
+
+ ALIAS_COMMANDS = {
+ "i" => "install",
+ "login" => "signin",
+ "logout" => "signout",
+ }.freeze
+
+ ##
+ # Return the authoritative instance of the command manager.
+
+ def self.instance
+ @instance ||= new
+ end
+
+ ##
+ # Returns self. Allows a CommandManager instance to stand
+ # in for the class itself.
+
+ def instance
+ self
+ end
+
+ ##
+ # Reset the authoritative instance of the command manager.
+
+ def self.reset
+ @instance = nil
+ end
+
+ ##
+ # Register all the subcommands supported by the gem command.
+
+ def initialize
+ require_relative "vendored_timeout"
+ @commands = {}
+
+ BUILTIN_COMMANDS.each do |name|
+ register_command name
end
-
- # Return a list of all command names (as strings).
- def command_names
- @commands.keys.collect {|key| key.to_s}.sort
+ end
+
+ ##
+ # Register the Symbol +command+ as a gem command.
+
+ def register_command(command, obj = false)
+ @commands[command] = obj
+ end
+
+ ##
+ # Unregister the Symbol +command+ as a gem command.
+
+ def unregister_command(command)
+ @commands.delete command
+ end
+
+ ##
+ # Returns a Command instance for +command_name+
+
+ def [](command_name)
+ command_name = command_name.intern
+ return nil if @commands[command_name].nil?
+ @commands[command_name] ||= load_and_instantiate(command_name)
+ end
+
+ ##
+ # Return a sorted list of all command names as strings.
+
+ def command_names
+ @commands.keys.collect(&:to_s).sort
+ end
+
+ ##
+ # Run the command specified by +args+.
+
+ def run(args, build_args = nil)
+ process_args(args, build_args)
+ rescue StandardError, Gem::Timeout::Error => ex
+ if ex.respond_to?(:detailed_message)
+ msg = ex.detailed_message(highlight: false).sub(/\A(.*?)(?: \(.+?\))/) { $1 }
+ else
+ msg = ex.message
end
-
- # Run the config specified by +args+.
- def run(args)
- process_args(args)
- rescue StandardError, Timeout::Error => ex
- alert_error "While executing gem ... (#{ex.class})\n #{ex.to_s}"
- ui.errs.puts "\t#{ex.backtrace.join "\n\t"}" if
- Gem.configuration.backtrace
- terminate_interaction(1)
- rescue Interrupt
- alert_error "Interrupted"
- terminate_interaction(1)
+ alert_error clean_text("While executing gem ... (#{ex.class})\n #{msg}")
+ ui.backtrace ex
+
+ terminate_interaction(1)
+ rescue Interrupt
+ alert_error clean_text("Interrupted")
+ terminate_interaction(1)
+ end
+
+ def process_args(args, build_args = nil)
+ if args.empty?
+ say Gem::Command::HELP
+ terminate_interaction 1
end
- def process_args(args)
- args = args.to_str.split(/\s+/) if args.respond_to?(:to_str)
- if args.size == 0
- say Gem::Command::HELP
- terminate_interaction(1)
- end
- case args[0]
- when '-h', '--help'
- say Gem::Command::HELP
- terminate_interaction(0)
- when '-v', '--version'
- say Gem::RubyGemsVersion
- terminate_interaction(0)
- when /^-/
- alert_error "Invalid option: #{args[0]}. See 'gem --help'."
- terminate_interaction(1)
+ case args.first
+ when "-h", "--help" then
+ say Gem::Command::HELP
+ terminate_interaction 0
+ when "-v", "--version" then
+ say Gem::VERSION
+ terminate_interaction 0
+ when "-C" then
+ args.shift
+ start_point = args.shift
+ if Dir.exist?(start_point)
+ Dir.chdir(start_point) { invoke_command(args, build_args) }
else
- cmd_name = args.shift.downcase
- cmd = find_command(cmd_name)
- cmd.invoke(*args)
+ alert_error clean_text("#{start_point} isn't a directory.")
+ terminate_interaction 1
end
+ when /^-/ then
+ alert_error clean_text("Invalid option: #{args.first}. See 'gem --help'.")
+ terminate_interaction 1
+ else
+ invoke_command(args, build_args)
end
+ end
- def find_command(cmd_name)
- possibilities = find_command_possibilities(cmd_name)
- if possibilities.size > 1
- raise "Ambiguous command #{cmd_name} matches [#{possibilities.join(', ')}]"
- end
- if possibilities.size < 1
- raise "Unknown command #{cmd_name}"
- end
+ def find_command(cmd_name)
+ cmd_name = find_alias_command cmd_name
- self[possibilities.first]
- end
+ possibilities = find_command_possibilities cmd_name
- def find_command_possibilities(cmd_name)
- len = cmd_name.length
- self.command_names.select { |n| cmd_name == n[0,len] }
+ if possibilities.size > 1
+ raise Gem::CommandLineError,
+ "Ambiguous command #{cmd_name} matches [#{possibilities.join(", ")}]"
+ elsif possibilities.empty?
+ raise Gem::UnknownCommandError.new(cmd_name)
end
-
- private
- def load_and_instantiate(command_name)
- command_name = command_name.to_s
- retried = false
+ self[possibilities.first]
+ end
+
+ def find_alias_command(cmd_name)
+ alias_name = ALIAS_COMMANDS[cmd_name]
+ alias_name ? alias_name : cmd_name
+ end
+
+ def find_command_possibilities(cmd_name)
+ len = cmd_name.length
+ found = command_names.select {|name| cmd_name == name[0, len] }
+
+ exact = found.find {|name| name == cmd_name }
+
+ exact ? [exact] : found
+ end
+
+ private
+
+ def load_and_instantiate(command_name)
+ command_name = command_name.to_s
+ const_name = command_name.capitalize.gsub(/_(.)/) { $1.upcase } << "Command"
+
+ begin
begin
- const_name = command_name.capitalize.gsub(/_(.)/) { $1.upcase }
- Gem::Commands.const_get("#{const_name}Command").new
- rescue NameError
- if retried then
- raise
- else
- retried = true
- require "rubygems/commands/#{command_name}_command"
- retry
- end
+ require "rubygems/commands/#{command_name}_command"
+ rescue LoadError
+ # it may have been defined from a rubygems_plugin.rb file
end
+
+ Gem::Commands.const_get(const_name).new
+ rescue StandardError => e
+ alert_error clean_text("Loading command: #{command_name} (#{e.class})\n\t#{e}")
+ ui.backtrace e
end
end
-end
+
+ def invoke_command(args, build_args)
+ cmd_name = args.shift.downcase
+ cmd = find_command cmd_name
+ terminate_interaction 1 unless cmd
+ cmd.deprecation_warning if cmd.deprecated?
+ cmd.invoke_with_build_args args, build_args
+ end
+end
diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb
index e1f0122c6c..cfe1f8ec3c 100644
--- a/lib/rubygems/commands/build_command.rb
+++ b/lib/rubygems/commands/build_command.rb
@@ -1,53 +1,120 @@
-require 'rubygems/command'
-require 'rubygems/builder'
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../gemspec_helpers"
+require_relative "../package"
+require_relative "../version_option"
class Gem::Commands::BuildCommand < Gem::Command
+ include Gem::VersionOption
+ include Gem::GemspecHelpers
def initialize
- super('build', 'Build a gem from a gemspec')
+ super "build", "Build a gem from a gemspec"
+
+ add_platform_option
+
+ add_option "--force", "skip validation of the spec" do |_value, options|
+ options[:force] = true
+ end
+
+ add_option "--strict", "consider warnings as errors when validating the spec" do |_value, options|
+ options[:strict] = true
+ end
+
+ add_option "-o", "--output FILE", "output gem with the given filename" do |value, options|
+ options[:output] = value
+ end
end
def arguments # :nodoc:
"GEMSPEC_FILE gemspec file name to build a gem for"
end
+ def description # :nodoc:
+ <<-EOF
+The build command allows you to create a gem from a ruby gemspec.
+
+The best way to build a gem is to use a Rakefile and the Gem::PackageTask
+which ships with RubyGems.
+
+The gemspec can either be created by hand or extracted from an existing gem
+with gem spec:
+
+ $ gem unpack my_gem-1.0.gem
+ Unpacked gem: '.../my_gem-1.0'
+ $ gem spec my_gem-1.0.gem --ruby > my_gem-1.0/my_gem-1.0.gemspec
+ $ cd my_gem-1.0
+ [edit gem contents]
+ $ gem build my_gem-1.0.gemspec
+
+Gems can be saved to a specified filename with the output option:
+
+ $ gem build my_gem-1.0.gemspec --output=release.gem
+
+ EOF
+ end
+
def usage # :nodoc:
"#{program_name} GEMSPEC_FILE"
end
def execute
- gemspec = get_one_gem_name
- if File.exist?(gemspec)
- specs = load_gemspecs(gemspec)
- specs.each do |spec|
- Gem::Builder.new(spec).build
- end
+ if build_path = options[:build_path]
+ Dir.chdir(build_path) { build_gem }
+ return
+ end
+
+ build_gem
+ end
+
+ private
+
+ def build_gem
+ gemspec = resolve_gem_name
+
+ if gemspec
+ build_package(gemspec)
+ else
+ alert_error error_message
+ terminate_interaction(1)
+ end
+ end
+
+ def build_package(gemspec)
+ spec = Gem::Specification.load(gemspec)
+ if spec
+ Gem::Package.build(
+ spec,
+ options[:force],
+ options[:strict],
+ options[:output]
+ )
else
- alert_error "Gemspec file not found: #{gemspec}"
- end
- end
-
- def load_gemspecs(filename)
- if yaml?(filename)
- result = []
- open(filename) do |f|
- begin
- while not f.eof? and spec = Gem::Specification.from_yaml(f)
- result << spec
- end
- rescue Gem::EndOfYAMLException => e
- # OK
- end
- end
+ alert_error "Error loading gemspec. Aborting."
+ terminate_interaction 1
+ end
+ end
+
+ def resolve_gem_name
+ return find_gemspec unless gem_name
+
+ if File.exist?(gem_name)
+ gem_name
+ else
+ find_gemspec("#{gem_name}.gemspec") || find_gemspec(gem_name)
+ end
+ end
+
+ def error_message
+ if gem_name
+ "Couldn't find a gemspec file matching '#{gem_name}' in #{Dir.pwd}"
else
- result = [Gem::Specification.load(filename)]
+ "Couldn't find a gemspec file in #{Dir.pwd}"
end
- result
end
- def yaml?(filename)
- line = open(filename) { |f| line = f.gets }
- result = line =~ %r{!ruby/object:Gem::Specification}
- result
+ def gem_name
+ get_one_optional_argument
end
end
diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb
index f5b698855b..fe03841ddb 100644
--- a/lib/rubygems/commands/cert_command.rb
+++ b/lib/rubygems/commands/cert_command.rb
@@ -1,86 +1,325 @@
-require 'rubygems/command'
-require 'rubygems/security'
+# frozen_string_literal: true
-class Gem::Commands::CertCommand < Gem::Command
+require_relative "../command"
+require_relative "../security"
+class Gem::Commands::CertCommand < Gem::Command
def initialize
- super 'cert', 'Manage RubyGems certificates and signing settings'
-
- add_option('-a', '--add CERT',
- 'Add a trusted certificate.') do |value, options|
- cert = OpenSSL::X509::Certificate.new(File.read(value))
- Gem::Security.add_trusted_cert(cert)
- say "Added '#{cert.subject.to_s}'"
- end
-
- add_option('-l', '--list',
- 'List trusted certificates.') do |value, options|
- glob_str = File::join(Gem::Security::OPT[:trust_dir], '*.pem')
- Dir::glob(glob_str) do |path|
- begin
- cert = OpenSSL::X509::Certificate.new(File.read(path))
- # this could probably be formatted more gracefully
- say cert.subject.to_s
- rescue OpenSSL::X509::CertificateError
- next
- end
- end
- end
-
- add_option('-r', '--remove STRING',
- 'Remove trusted certificates containing',
- 'STRING.') do |value, options|
- trust_dir = Gem::Security::OPT[:trust_dir]
- glob_str = File::join(trust_dir, '*.pem')
-
- Dir::glob(glob_str) do |path|
- begin
- cert = OpenSSL::X509::Certificate.new(File.read(path))
- if cert.subject.to_s.downcase.index(value)
- say "Removed '#{cert.subject.to_s}'"
- File.unlink(path)
- end
- rescue OpenSSL::X509::CertificateError
- next
- end
- end
- end
-
- add_option('-b', '--build EMAIL_ADDR',
- 'Build private key and self-signed',
- 'certificate for EMAIL_ADDR.') do |value, options|
- vals = Gem::Security.build_self_signed_cert(value)
- File.chmod 0600, vals[:key_path]
- say "Public Cert: #{vals[:cert_path]}"
- say "Private Key: #{vals[:key_path]}"
- say "Don't forget to move the key file to somewhere private..."
- end
-
- add_option('-C', '--certificate CERT',
- 'Certificate for --sign command.') do |value, options|
- cert = OpenSSL::X509::Certificate.new(File.read(value))
- Gem::Security::OPT[:issuer_cert] = cert
- end
-
- add_option('-K', '--private-key KEY',
- 'Private key for --sign command.') do |value, options|
- key = OpenSSL::PKey::RSA.new(File.read(value))
- Gem::Security::OPT[:issuer_key] = key
- end
-
- add_option('-s', '--sign NEWCERT',
- 'Sign a certificate with my key and',
- 'certificate.') do |value, options|
- cert = OpenSSL::X509::Certificate.new(File.read(value))
- my_cert = Gem::Security::OPT[:issuer_cert]
- my_key = Gem::Security::OPT[:issuer_key]
- cert = Gem::Security.sign_cert(cert, my_key, my_cert)
- File.open(value, 'wb') { |file| file.write(cert.to_pem) }
+ super "cert", "Manage RubyGems certificates and signing settings",
+ add: [], remove: [], list: [], build: [], sign: []
+
+ add_option("-a", "--add CERT",
+ "Add a trusted certificate.") do |cert_file, options|
+ options[:add] << open_cert(cert_file)
+ end
+
+ add_option("-l", "--list [FILTER]",
+ "List trusted certificates where the",
+ "subject contains FILTER") do |filter, options|
+ filter ||= ""
+
+ options[:list] << filter
+ end
+
+ add_option("-r", "--remove FILTER",
+ "Remove trusted certificates where the",
+ "subject contains FILTER") do |filter, options|
+ options[:remove] << filter
end
+
+ add_option("-b", "--build EMAIL_ADDR",
+ "Build private key and self-signed",
+ "certificate for EMAIL_ADDR") do |email_address, options|
+ options[:build] << email_address
+ end
+
+ add_option("-C", "--certificate CERT",
+ "Signing certificate for --sign") do |cert_file, options|
+ options[:issuer_cert] = open_cert(cert_file)
+ options[:issuer_cert_file] = cert_file
+ end
+
+ add_option("-K", "--private-key KEY",
+ "Key for --sign or --build") do |key_file, options|
+ options[:key] = open_private_key(key_file)
+ end
+
+ add_option("-A", "--key-algorithm ALGORITHM",
+ "Select which key algorithm to use for --build") do |algorithm, options|
+ options[:key_algorithm] = algorithm
+ end
+
+ add_option("-s", "--sign CERT",
+ "Signs CERT with the key from -K",
+ "and the certificate from -C") do |cert_file, options|
+ raise Gem::OptionParser::InvalidArgument, "#{cert_file}: does not exist" unless
+ File.file? cert_file
+
+ options[:sign] << cert_file
+ end
+
+ add_option("-d", "--days NUMBER_OF_DAYS",
+ "Days before the certificate expires") do |days, options|
+ options[:expiration_length_days] = days.to_i
+ end
+
+ add_option("-R", "--re-sign",
+ "Re-signs the certificate from -C with the key from -K") do |resign, options|
+ options[:resign] = resign
+ end
+ end
+
+ def add_certificate(certificate) # :nodoc:
+ Gem::Security.trust_dir.trust_cert certificate
+
+ say "Added '#{certificate.subject}'"
+ end
+
+ def check_openssl
+ return if Gem::HAVE_OPENSSL
+
+ alert_error "OpenSSL library is required for the cert command"
+ terminate_interaction 1
+ end
+
+ def open_cert(certificate_file)
+ check_openssl
+ OpenSSL::X509::Certificate.new File.read certificate_file
+ rescue Errno::ENOENT
+ raise Gem::OptionParser::InvalidArgument, "#{certificate_file}: does not exist"
+ rescue OpenSSL::X509::CertificateError
+ raise Gem::OptionParser::InvalidArgument,
+ "#{certificate_file}: invalid X509 certificate"
+ end
+
+ def open_private_key(key_file)
+ check_openssl
+ passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"]
+ key = OpenSSL::PKey.read File.read(key_file), passphrase
+ raise Gem::OptionParser::InvalidArgument,
+ "#{key_file}: private key not found" unless key.private?
+ key
+ rescue Errno::ENOENT
+ raise Gem::OptionParser::InvalidArgument, "#{key_file}: does not exist"
+ rescue OpenSSL::PKey::PKeyError, ArgumentError
+ raise Gem::OptionParser::InvalidArgument, "#{key_file}: invalid RSA, DSA, or EC key"
end
def execute
+ check_openssl
+
+ options[:add].each do |certificate|
+ add_certificate certificate
+ end
+
+ options[:remove].each do |filter|
+ remove_certificates_matching filter
+ end
+
+ options[:list].each do |filter|
+ list_certificates_matching filter
+ end
+
+ options[:build].each do |email|
+ build email
+ end
+
+ if options[:resign]
+ re_sign_cert(
+ options[:issuer_cert],
+ options[:issuer_cert_file],
+ options[:key]
+ )
+ end
+
+ sign_certificates unless options[:sign].empty?
end
-end
+ def build(email)
+ unless valid_email?(email)
+ raise Gem::CommandLineError, "Invalid email address #{email}"
+ end
+
+ key, key_path = build_key
+ cert_path = build_cert email, key
+
+ say "Certificate: #{cert_path}"
+
+ if key_path
+ say "Private Key: #{key_path}"
+ say "Don't forget to move the key file to somewhere private!"
+ end
+ end
+
+ def build_cert(email, key) # :nodoc:
+ expiration_length_days = options[:expiration_length_days] ||
+ Gem.configuration.cert_expiration_length_days
+
+ cert = Gem::Security.create_cert_email(
+ email,
+ key,
+ Gem::Security::ONE_DAY * expiration_length_days
+ )
+
+ Gem::Security.write cert, "gem-public_cert.pem"
+ end
+
+ def build_key # :nodoc:
+ return options[:key] if options[:key]
+
+ passphrase = ask_for_password "Passphrase for your Private Key:"
+ say "\n"
+
+ passphrase_confirmation = ask_for_password "Please repeat the passphrase for your Private Key:"
+ say "\n"
+ raise Gem::CommandLineError,
+ "Passphrase and passphrase confirmation don't match" unless passphrase == passphrase_confirmation
+
+ algorithm = options[:key_algorithm] || Gem::Security::DEFAULT_KEY_ALGORITHM
+ key = Gem::Security.create_key(algorithm)
+ key_path = Gem::Security.write key, "gem-private_key.pem", 0o600, passphrase
+
+ [key, key_path]
+ end
+
+ def certificates_matching(filter)
+ return enum_for __method__, filter unless block_given?
+
+ Gem::Security.trusted_certificates.select do |certificate, _|
+ subject = certificate.subject.to_s
+ subject.downcase.index filter
+ end.sort_by do |certificate, _|
+ certificate.subject.to_a.map {|name, data,| [name, data] }
+ end.each do |certificate, path|
+ yield certificate, path
+ end
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The cert command manages signing keys and certificates for creating signed
+gems. Your signing certificate and private key are typically stored in
+~/.gem/gem-public_cert.pem and ~/.gem/gem-private_key.pem respectively.
+
+To build a certificate for signing gems:
+
+ gem cert --build you@example
+
+If you already have an RSA key, or are creating a new certificate for an
+existing key:
+
+ gem cert --build you@example --private-key /path/to/key.pem
+
+If you wish to trust a certificate you can add it to the trust list with:
+
+ gem cert --add /path/to/cert.pem
+
+You can list trusted certificates with:
+
+ gem cert --list
+
+or:
+
+ gem cert --list cert_subject_substring
+
+If you wish to remove a previously trusted certificate:
+
+ gem cert --remove cert_subject_substring
+
+To sign another gem author's certificate:
+
+ gem cert --sign /path/to/other_cert.pem
+
+For further reading on signing gems see `ri Gem::Security`.
+ EOF
+ end
+
+ def list_certificates_matching(filter) # :nodoc:
+ certificates_matching filter do |certificate, _|
+ # this could probably be formatted more gracefully
+ say certificate.subject.to_s
+ end
+ end
+
+ def load_default_cert
+ cert_file = File.join Gem.default_cert_path
+ cert = File.read cert_file
+ options[:issuer_cert] = OpenSSL::X509::Certificate.new cert
+ rescue Errno::ENOENT
+ alert_error \
+ "--certificate not specified and ~/.gem/gem-public_cert.pem does not exist"
+
+ terminate_interaction 1
+ rescue OpenSSL::X509::CertificateError
+ alert_error \
+ "--certificate not specified and ~/.gem/gem-public_cert.pem is not valid"
+
+ terminate_interaction 1
+ end
+
+ def load_default_key
+ key_file = File.join Gem.default_key_path
+ key = File.read key_file
+ passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"]
+ options[:key] = OpenSSL::PKey.read key, passphrase
+ rescue Errno::ENOENT
+ alert_error \
+ "--private-key not specified and ~/.gem/gem-private_key.pem does not exist"
+
+ terminate_interaction 1
+ rescue OpenSSL::PKey::PKeyError
+ alert_error \
+ "--private-key not specified and ~/.gem/gem-private_key.pem is not valid"
+
+ terminate_interaction 1
+ end
+
+ def load_defaults # :nodoc:
+ load_default_cert unless options[:issuer_cert]
+ load_default_key unless options[:key]
+ end
+
+ def remove_certificates_matching(filter) # :nodoc:
+ certificates_matching filter do |certificate, path|
+ FileUtils.rm path
+ say "Removed '#{certificate.subject}'"
+ end
+ end
+
+ def sign(cert_file)
+ cert = File.read cert_file
+ cert = OpenSSL::X509::Certificate.new cert
+
+ permissions = File.stat(cert_file).mode & 0o777
+
+ issuer_cert = options[:issuer_cert]
+ issuer_key = options[:key]
+
+ cert = Gem::Security.sign cert, issuer_key, issuer_cert
+
+ Gem::Security.write cert, cert_file, permissions
+ end
+
+ def sign_certificates # :nodoc:
+ load_defaults unless options[:sign].empty?
+
+ options[:sign].each do |cert_file|
+ sign cert_file
+ end
+ end
+
+ def re_sign_cert(cert, cert_path, private_key)
+ Gem::Security::Signer.re_sign_cert(cert, cert_path, private_key) do |expired_cert_path, new_expired_cert_path|
+ alert("Your certificate #{expired_cert_path} has been re-signed")
+ alert("Your expired certificate will be located at: #{new_expired_cert_path}")
+ end
+ end
+
+ private
+
+ def valid_email?(email)
+ # It's simple, but is all we need
+ email =~ /\A.+@.+\z/
+ end
+end
diff --git a/lib/rubygems/commands/check_command.rb b/lib/rubygems/commands/check_command.rb
index 17c2c8f9c7..fb23dd9cb4 100644
--- a/lib/rubygems/commands/check_command.rb
+++ b/lib/rubygems/commands/check_command.rb
@@ -1,75 +1,97 @@
-require 'rubygems/command'
-require 'rubygems/version_option'
-require 'rubygems/validator'
+# frozen_string_literal: true
-class Gem::Commands::CheckCommand < Gem::Command
+require_relative "../command"
+require_relative "../version_option"
+require_relative "../validator"
+require_relative "../doctor"
+class Gem::Commands::CheckCommand < Gem::Command
include Gem::VersionOption
def initialize
- super 'check', 'Check installed gems',
- :verify => false, :alien => false
+ super "check", "Check a gem repository for added or missing files",
+ alien: true, doctor: false, dry_run: false, gems: true
+
+ add_option("-a", "--[no-]alien",
+ 'Report "unmanaged" or rogue files in the',
+ "gem repository") do |value, options|
+ options[:alien] = value
+ end
- add_option( '--verify FILE',
- 'Verify gem file against its internal',
- 'checksum') do |value, options|
- options[:verify] = value
+ add_option("--[no-]doctor",
+ "Clean up uninstalled gems and broken",
+ "specifications") do |value, options|
+ options[:doctor] = value
end
- add_option('-a', '--alien', "Report 'unmanaged' or rogue files in the",
- "gem repository") do |value, options|
- options[:alien] = true
+ add_option("--[no-]dry-run",
+ "Do not remove files, only report what",
+ "would be removed") do |value, options|
+ options[:dry_run] = value
end
- add_option('-t', '--test', "Run unit tests for gem") do |value, options|
- options[:test] = true
+ add_option("--[no-]gems",
+ "Check installed gems for problems") do |value, options|
+ options[:gems] = value
end
- add_version_option 'run tests for'
+ add_version_option "check"
end
- def execute
- if options[:test]
- version = options[:version] || Gem::Requirement.default
- dep = Gem::Dependency.new get_one_gem_name, version
- gem_spec = Gem::SourceIndex.from_installed_gems.search(dep).first
- Gem::Validator.new.unit_test(gem_spec)
- end
+ def check_gems
+ say "Checking gems..."
+ say
+ gems = begin
+ get_all_gem_names
+ rescue StandardError
+ []
+ end
- if options[:alien]
- say "Performing the 'alien' operation"
- Gem::Validator.new.alien.each do |key, val|
- if(val.size > 0)
- say "#{key} has #{val.size} problems"
- val.each do |error_entry|
- say "\t#{error_entry.path}:"
- say "\t#{error_entry.problem}"
- say
- end
- else
- say "#{key} is error-free"
+ Gem::Validator.new.alien(gems).sort.each do |key, val|
+ if val.empty?
+ say "#{key} is error-free" if Gem.configuration.verbose
+ else
+ say "#{key} has #{val.size} problems"
+ val.each do |error_entry|
+ say " #{error_entry.path}:"
+ say " #{error_entry.problem}"
end
- say
end
+ say
end
+ end
- if options[:verify]
- gem_name = options[:verify]
- unless gem_name
- alert_error "Must specify a .gem file with --verify NAME"
- return
- end
- unless File.exist?(gem_name)
- alert_error "Unknown file: #{gem_name}."
- return
- end
- say "Verifying gem: '#{gem_name}'"
- begin
- Gem::Validator.new.verify_gem_file(gem_name)
- rescue Exception => e
- alert_error "#{gem_name} is invalid."
- end
+ def doctor
+ say "Checking for files from uninstalled gems..."
+ say
+
+ Gem.path.each do |gem_repo|
+ doctor = Gem::Doctor.new gem_repo, options[:dry_run]
+ doctor.doctor
end
end
+ def execute
+ check_gems if options[:gems]
+ doctor if options[:doctor]
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to check"
+ end
+
+ def defaults_str # :nodoc:
+ "--gems --alien"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The check command can list and repair problems with installed gems and
+specifications and will clean up gems that have been partially uninstalled.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [OPTIONS] [GEMNAME ...]"
+ end
end
diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb
index 40dcb9db34..c89a24eee9 100644
--- a/lib/rubygems/commands/cleanup_command.rb
+++ b/lib/rubygems/commands/cleanup_command.rb
@@ -1,17 +1,44 @@
-require 'rubygems/command'
-require 'rubygems/source_index'
-require 'rubygems/dependency_list'
+# frozen_string_literal: true
-class Gem::Commands::CleanupCommand < Gem::Command
+require_relative "../command"
+require_relative "../dependency_list"
+require_relative "../uninstaller"
+class Gem::Commands::CleanupCommand < Gem::Command
def initialize
- super 'cleanup',
- 'Clean up old versions of installed gems in the local repository',
- :force => false, :test => false, :install_dir => Gem.dir
+ super "cleanup",
+ "Clean up old versions of installed gems",
+ force: false, install_dir: Gem.dir,
+ check_dev: true
- add_option('-d', '--dryrun', "") do |value, options|
+ add_option("-n", "-d", "--dry-run",
+ "Do not uninstall gems") do |_value, options|
options[:dryrun] = true
end
+
+ add_option(:Deprecated, "--dryrun",
+ "Do not uninstall gems") do |_value, options|
+ options[:dryrun] = true
+ end
+ deprecate_option("--dryrun", extra_msg: "Use --dry-run instead")
+
+ add_option("-D", "--[no-]check-development",
+ "Check development dependencies while uninstalling",
+ "(default: true)") do |value, options|
+ options[:check_dev] = value
+ end
+
+ add_option("--[no-]user-install",
+ "Cleanup in user's home directory instead",
+ "of GEM_HOME.") do |value, options|
+ options[:user_install] = value
+ end
+
+ @candidate_gems = nil
+ @default_gems = []
+ @full = nil
+ @gems_to_cleanup = nil
+ @primary_gems = nil
end
def arguments # :nodoc:
@@ -19,7 +46,17 @@ class Gem::Commands::CleanupCommand < Gem::Command
end
def defaults_str # :nodoc:
- "--no-dryrun"
+ "--no-dry-run"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The cleanup command removes old versions of gems from GEM_HOME that are not
+required to meet a dependency. If a gem is installed elsewhere in GEM_PATH
+the cleanup command won't delete it.
+
+If no gems are named all gems in GEM_HOME are cleaned.
+ EOF
end
def usage # :nodoc:
@@ -28,64 +65,114 @@ class Gem::Commands::CleanupCommand < Gem::Command
def execute
say "Cleaning up installed gems..."
- primary_gems = {}
- Gem.source_index.each do |name, spec|
- if primary_gems[spec.name].nil? or
- primary_gems[spec.name].version < spec.version then
- primary_gems[spec.name] = spec
+ if options[:args].empty?
+ done = false
+ last_set = nil
+
+ until done do
+ clean_gems
+
+ this_set = @gems_to_cleanup.map(&:full_name).sort
+
+ done = this_set.empty? || last_set == this_set
+
+ last_set = this_set
end
+ else
+ clean_gems
end
- gems_to_cleanup = []
+ say "Clean up complete"
- unless options[:args].empty? then
- options[:args].each do |gem_name|
- specs = Gem.cache.search(/^#{gem_name}$/i)
- specs.each do |spec|
- gems_to_cleanup << spec
- end
- end
+ verbose do
+ skipped = @default_gems.map(&:full_name)
+
+ "Skipped default gems: #{skipped.join ", "}"
+ end
+ end
+
+ def clean_gems
+ get_primary_gems
+ get_candidate_gems
+ get_gems_to_cleanup
+
+ @full = Gem::DependencyList.from_specs
+
+ deplist = Gem::DependencyList.new
+ @gems_to_cleanup.each {|spec| deplist.add spec }
+
+ deps = deplist.strongly_connected_components.flatten
+
+ deps.reverse_each do |spec|
+ uninstall_dep spec
+ end
+ end
+
+ def get_candidate_gems
+ @candidate_gems = if options[:args].empty?
+ Gem::Specification.to_a
else
- Gem.source_index.each do |name, spec|
- gems_to_cleanup << spec
+ options[:args].flat_map do |gem_name|
+ Gem::Specification.find_all_by_name gem_name
end
end
+ end
- gems_to_cleanup = gems_to_cleanup.select { |spec|
- primary_gems[spec.name].version != spec.version
- }
+ def get_gems_to_cleanup
+ gems_to_cleanup = @candidate_gems.select do |spec|
+ @primary_gems[spec.name].version != spec.version
+ end
- uninstall_command = Gem::CommandManager.instance['uninstall']
- deplist = Gem::DependencyList.new
- gems_to_cleanup.uniq.each do |spec| deplist.add spec end
+ default_gems, gems_to_cleanup = gems_to_cleanup.partition(&:default_gem?)
- deps = deplist.strongly_connected_components.flatten.reverse
+ uninstall_from = options[:user_install] ? Gem.user_dir : Gem.dir
- deps.each do |spec|
- if options[:dryrun] then
- say "Dry Run Mode: Would uninstall #{spec.full_name}"
- else
- say "Attempting to uninstall #{spec.full_name}"
+ gems_to_cleanup = gems_to_cleanup.select do |spec|
+ spec.base_dir == uninstall_from
+ end
- options[:args] = [spec.name]
- options[:version] = "= #{spec.version}"
- options[:executables] = false
+ @default_gems += default_gems
+ @default_gems.uniq!
+ @gems_to_cleanup = gems_to_cleanup.uniq
+ end
- uninstaller = Gem::Uninstaller.new spec.name, options
+ def get_primary_gems
+ @primary_gems = {}
- begin
- uninstaller.uninstall
- rescue Gem::DependencyRemovalException,
- Gem::GemNotInHomeException => e
- say "Unable to uninstall #{spec.full_name}:"
- say "\t#{e.class}: #{e.message}"
- end
+ Gem::Specification.each do |spec|
+ if @primary_gems[spec.name].nil? ||
+ @primary_gems[spec.name].version < spec.version
+ @primary_gems[spec.name] = spec
end
end
-
- say "Clean Up Complete"
end
-end
+ def uninstall_dep(spec)
+ return unless @full.ok_to_remove?(spec.full_name, options[:check_dev])
+
+ if options[:dryrun]
+ say "Dry Run Mode: Would uninstall #{spec.full_name}"
+ return
+ end
+
+ say "Attempting to uninstall #{spec.full_name}"
+
+ uninstall_options = {
+ executables: false,
+ version: "= #{spec.version}",
+ }
+
+ uninstall_options[:user_install] = Gem.user_dir == spec.base_dir
+
+ uninstaller = Gem::Uninstaller.new spec.name, uninstall_options
+ begin
+ uninstaller.uninstall
+ rescue Gem::DependencyRemovalException, Gem::InstallError,
+ Gem::GemNotInHomeException, Gem::FilePermissionError => e
+ say "Unable to uninstall #{spec.full_name}:"
+ say "\t#{e.class}: #{e.message}"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/contents_command.rb b/lib/rubygems/commands/contents_command.rb
index bc75fb5c03..d4f9871868 100644
--- a/lib/rubygems/commands/contents_command.rb
+++ b/lib/rubygems/commands/contents_command.rb
@@ -1,25 +1,46 @@
-require 'rubygems/command'
-require 'rubygems/version_option'
+# frozen_string_literal: true
-class Gem::Commands::ContentsCommand < Gem::Command
+require_relative "../command"
+require_relative "../version_option"
+class Gem::Commands::ContentsCommand < Gem::Command
include Gem::VersionOption
def initialize
- super 'contents', 'Display the contents of the installed gems',
- :specdirs => [], :lib_only => false
+ super "contents", "Display the contents of the installed gems",
+ specdirs: [], lib_only: false, prefix: true,
+ show_install_dir: false
add_version_option
- add_option('-s', '--spec-dir a,b,c', Array,
+ add_option("--all",
+ "Contents for all gems") do |all, options|
+ options[:all] = all
+ end
+
+ add_option("-s", "--spec-dir a,b,c", Array,
"Search for gems under specific paths") do |spec_dirs, options|
options[:specdirs] = spec_dirs
end
- add_option('-l', '--[no-]lib-only',
+ add_option("-l", "--[no-]lib-only",
"Only return files in the Gem's lib_dirs") do |lib_only, options|
options[:lib_only] = lib_only
end
+
+ add_option("--[no-]prefix",
+ "Don't include installed path prefix") do |prefix, options|
+ options[:prefix] = prefix
+ end
+
+ add_option("--[no-]show-install-dir",
+ "Show only the gem install dir") do |show, options|
+ options[:show_install_dir] = show
+ end
+
+ @path_kind = nil
+ @spec_dirs = nil
+ @version = nil
end
def arguments # :nodoc:
@@ -27,48 +48,149 @@ class Gem::Commands::ContentsCommand < Gem::Command
end
def defaults_str # :nodoc:
- "--no-lib-only"
+ "--no-lib-only --prefix"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The contents command lists the files in an installed gem. The listing can
+be given as full file names, file names without the installed directory
+prefix or only the files that are requireable.
+ EOF
end
def usage # :nodoc:
- "#{program_name} GEMNAME"
+ "#{program_name} GEMNAME [GEMNAME ...]"
end
def execute
- version = options[:version] || Gem::Requirement.default
- gem = get_one_gem_name
+ @version = options[:version] || Gem::Requirement.default
+ @spec_dirs = specification_directories
+ @path_kind = path_description @spec_dirs
- s = options[:specdirs].map do |i|
- [i, File.join(i, "specifications")]
- end.flatten
+ names = gem_names
- path_kind = if s.empty? then
- s = Gem::SourceIndex.installed_spec_directories
- "default gem paths"
- else
- "specified path"
- end
+ names.each do |name|
+ found =
+ if options[:show_install_dir]
+ gem_install_dir name
+ else
+ gem_contents name
+ end
- si = Gem::SourceIndex.from_gems_in(*s)
+ terminate_interaction 1 unless found || names.length > 1
+ end
+ end
- gem_spec = si.find_name(gem, version).last
+ def files_in(spec)
+ if spec.default_gem?
+ files_in_default_gem spec
+ else
+ files_in_gem spec
+ end
+ end
+
+ def files_in_gem(spec)
+ gem_path = spec.full_gem_path
+ extra = "/{#{spec.require_paths.join ","}}" if options[:lib_only]
+ glob = "#{gem_path}#{extra}/**/*"
+ prefix_re = %r{#{Regexp.escape(gem_path)}/}
+
+ Dir[glob].map do |file|
+ [gem_path, file.sub(prefix_re, "")]
+ end
+ end
+
+ def files_in_default_gem(spec)
+ spec.files.filter_map do |file|
+ if file.start_with?("#{spec.bindir}/")
+ [RbConfig::CONFIG["bindir"], file.delete_prefix("#{spec.bindir}/")]
+ else
+ gem spec.name, spec.version
- unless gem_spec then
- say "Unable to find gem '#{gem}' in #{path_kind}"
+ require_path = spec.require_paths.find do |path|
+ file.start_with?("#{path}/")
+ end
- if Gem.configuration.verbose then
- say "\nDirectories searched:"
- s.each { |dir| say dir }
+ requirable_part = file.delete_prefix("#{require_path}/")
+
+ resolve = $LOAD_PATH.resolve_feature_path(requirable_part)&.last
+ next unless resolve
+
+ [resolve.delete_suffix(requirable_part), requirable_part]
end
+ end
+ end
+
+ def gem_contents(name)
+ spec = spec_for name
+
+ return false unless spec
- terminate_interaction
+ files = files_in spec
+
+ show_files files
+
+ true
+ end
+
+ def gem_install_dir(name)
+ spec = spec_for name
+
+ return false unless spec
+
+ say spec.gem_dir
+
+ true
+ end
+
+ def gem_names # :nodoc:
+ if options[:all]
+ Gem::Specification.map(&:name)
+ else
+ get_all_gem_names
end
+ end
- files = options[:lib_only] ? gem_spec.lib_files : gem_spec.files
- files.each do |f|
- say File.join(gem_spec.full_gem_path, f)
+ def path_description(spec_dirs) # :nodoc:
+ if spec_dirs.empty?
+ "default gem paths"
+ else
+ "specified path"
end
end
-end
+ def show_files(files)
+ files.sort.each do |prefix, basename|
+ absolute_path = File.join(prefix, basename)
+ next if File.directory? absolute_path
+
+ if options[:prefix]
+ say absolute_path
+ else
+ say basename
+ end
+ end
+ end
+
+ def spec_for(name)
+ spec = Gem::Specification.find_all_by_name(name, @version).first
+
+ return spec if spec
+
+ say "Unable to find gem '#{name}' in #{@path_kind}"
+
+ if Gem.configuration.verbose
+ say "\nDirectories searched:"
+ @spec_dirs.sort.each {|dir| say dir }
+ end
+ nil
+ end
+
+ def specification_directories # :nodoc:
+ options[:specdirs].flat_map do |i|
+ [i, File.join(i, "specifications")]
+ end
+ end
+end
diff --git a/lib/rubygems/commands/dependency_command.rb b/lib/rubygems/commands/dependency_command.rb
index 44b269bb11..9aaefae999 100644
--- a/lib/rubygems/commands/dependency_command.rb
+++ b/lib/rubygems/commands/dependency_command.rb
@@ -1,28 +1,28 @@
-require 'rubygems/command'
-require 'rubygems/local_remote_options'
-require 'rubygems/version_option'
-require 'rubygems/source_info_cache'
+# frozen_string_literal: true
-class Gem::Commands::DependencyCommand < Gem::Command
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+class Gem::Commands::DependencyCommand < Gem::Command
include Gem::LocalRemoteOptions
include Gem::VersionOption
def initialize
- super 'dependency',
- 'Show the dependencies of an installed gem',
- :version => Gem::Requirement.default, :domain => :local
+ super "dependency",
+ "Show the dependencies of an installed gem",
+ version: Gem::Requirement.default, domain: :local
add_version_option
add_platform_option
+ add_prerelease_option
- add_option('-R', '--[no-]reverse-dependencies',
- 'Include reverse dependencies in the output') do
- |value, options|
+ add_option("-R", "--[no-]reverse-dependencies",
+ "Include reverse dependencies in the output") do |value, options|
options[:reverse_dependencies] = value
end
- add_option('-p', '--pipe',
+ add_option("-p", "--pipe",
"Pipe Format (name --version ver)") do |value, options|
options[:pipe_format] = value
end
@@ -31,139 +31,158 @@ class Gem::Commands::DependencyCommand < Gem::Command
end
def arguments # :nodoc:
- "GEMNAME name of gem to show dependencies for"
+ "REGEXP show dependencies for gems whose names start with REGEXP"
end
def defaults_str # :nodoc:
"--local --version '#{Gem::Requirement.default}' --no-reverse-dependencies"
end
+ def description # :nodoc:
+ <<-EOF
+The dependency commands lists which other gems a given gem depends on. For
+local gems only the reverse dependencies can be shown (which gems depend on
+the named gem).
+
+The dependency list can be displayed in a format suitable for piping for
+use with other commands.
+ EOF
+ end
+
def usage # :nodoc:
- "#{program_name} GEMNAME"
+ "#{program_name} REGEXP"
end
- def execute
- options[:args] << '' if options[:args].empty?
- specs = {}
+ def fetch_remote_specs(name, requirement, prerelease) # :nodoc:
+ fetcher = Gem::SpecFetcher.fetcher
+
+ specs_type = prerelease ? :complete : :released
- source_indexes = Hash.new do |h, source_uri|
- h[source_uri] = Gem::SourceIndex.new
+ ss = if name.nil?
+ fetcher.detect(specs_type) { true }
+ else
+ fetcher.detect(specs_type) do |name_tuple|
+ name === name_tuple.name && requirement.satisfied_by?(name_tuple.version)
+ end
end
- pattern = if options[:args].length == 1 and
- options[:args].first =~ /\A\/(.*)\/(i)?\z/m then
- flags = $2 ? Regexp::IGNORECASE : nil
- Regexp.new $1, flags
- else
- /\A#{Regexp.union(*options[:args])}/
- end
+ ss.map {|tuple, source| source.fetch_spec(tuple) }
+ end
- dependency = Gem::Dependency.new pattern, options[:version]
+ def fetch_specs(name_pattern, requirement, prerelease) # :nodoc:
+ specs = []
- if options[:reverse_dependencies] and remote? and not local? then
- alert_error 'Only reverse dependencies for local gems are supported.'
- terminate_interaction 1
- end
+ if local?
+ specs.concat Gem::Specification.stubs.find_all {|spec|
+ name_matches = name_pattern ? name_pattern =~ spec.name : true
+ version_matches = requirement.satisfied_by?(spec.version)
- if local? then
- Gem.source_index.search(dependency).each do |spec|
- source_indexes[:local].add_spec spec
- end
+ name_matches && version_matches
+ }.map(&:to_spec)
end
- if remote? and not options[:reverse_dependencies] then
- fetcher = Gem::SpecFetcher.fetcher
+ specs.concat fetch_remote_specs name_pattern, requirement, prerelease if remote?
- begin
- fetcher.find_matching(dependency).each do |spec_tuple, source_uri|
- spec = fetcher.fetch_spec spec_tuple, URI.parse(source_uri)
+ ensure_specs specs
- source_indexes[source_uri].add_spec spec
- end
- rescue Gem::RemoteFetcher::FetchError => e
- raise unless fetcher.warn_legacy e do
- require 'rubygems/source_info_cache'
-
- specs = Gem::SourceInfoCache.search_with_source dependency, false
+ specs.uniq.sort
+ end
- specs.each do |spec, source_uri|
- source_indexes[source_uri].add_spec spec
- end
- end
+ def display_pipe(specs) # :nodoc:
+ specs.each do |spec|
+ next if spec.dependencies.empty?
+ spec.dependencies.sort_by(&:name).each do |dep|
+ say "#{dep.name} --version '#{dep.requirement}'"
end
end
+ end
- if source_indexes.empty? then
- patterns = options[:args].join ','
- say "No gems found matching #{patterns} (#{options[:version]})" if
- Gem.configuration.verbose
+ def display_readable(specs, reverse) # :nodoc:
+ response = String.new
- terminate_interaction 1
+ specs.each do |spec|
+ response << print_dependencies(spec)
+ unless reverse[spec.full_name].empty?
+ response << " Used by\n"
+ reverse[spec.full_name].each do |sp, dep|
+ response << " #{sp} (#{dep})\n"
+ end
+ end
+ response << "\n"
end
- specs = {}
+ say response
+ end
- source_indexes.values.each do |source_index|
- source_index.gems.each do |name, spec|
- specs[spec.full_name] = [source_index, spec]
- end
- end
+ def execute
+ ensure_local_only_reverse_dependencies
- reverse = Hash.new { |h, k| h[k] = [] }
+ pattern = name_pattern options[:args]
+ requirement = Gem::Requirement.new options[:version]
- if options[:reverse_dependencies] then
- specs.values.each do |_, spec|
- reverse[spec.full_name] = find_reverse_dependencies spec
- end
- end
+ specs = fetch_specs pattern, requirement, options[:prerelease]
- if options[:pipe_format] then
- specs.values.sort_by { |_, spec| spec }.each do |_, spec|
- unless spec.dependencies.empty?
- spec.dependencies.each do |dep|
- say "#{dep.name} --version '#{dep.version_requirements}'"
- end
- end
- end
+ reverse = reverse_dependencies specs
+
+ if options[:pipe_format]
+ display_pipe specs
else
- response = ''
-
- specs.values.sort_by { |_, spec| spec }.each do |_, spec|
- response << print_dependencies(spec)
- unless reverse[spec.full_name].empty? then
- response << " Used by\n"
- reverse[spec.full_name].each do |sp, dep|
- response << " #{sp} (#{dep})\n"
- end
- end
- response << "\n"
- end
+ display_readable specs, reverse
+ end
+ end
- say response
+ def ensure_local_only_reverse_dependencies # :nodoc:
+ if options[:reverse_dependencies] && remote? && !local?
+ alert_error "Only reverse dependencies for local gems are supported."
+ terminate_interaction 1
end
end
- def print_dependencies(spec, level = 0)
- response = ''
- response << ' ' * level + "Gem #{spec.full_name}\n"
- unless spec.dependencies.empty? then
- spec.dependencies.each do |dep|
- response << ' ' * level + " #{dep}\n"
+ def ensure_specs(specs) # :nodoc:
+ return unless specs.empty?
+
+ patterns = options[:args].join ","
+ say "No gems found matching #{patterns} (#{options[:version]})" if
+ Gem.configuration.verbose
+
+ terminate_interaction 1
+ end
+
+ def print_dependencies(spec, level = 0) # :nodoc:
+ response = String.new
+ response << " " * level + "Gem #{spec.full_name}\n"
+ unless spec.dependencies.empty?
+ spec.dependencies.sort_by(&:name).each do |dep|
+ response << " " * level + " #{dep}\n"
end
end
response
end
- # Retuns list of [specification, dep] that are satisfied by spec.
- def find_reverse_dependencies(spec)
+ def reverse_dependencies(specs) # :nodoc:
+ reverse = Hash.new {|h, k| h[k] = [] }
+
+ return reverse unless options[:reverse_dependencies]
+
+ specs.each do |spec|
+ reverse[spec.full_name] = find_reverse_dependencies spec
+ end
+
+ reverse
+ end
+
+ ##
+ # Returns an Array of [specification, dep] that are satisfied by +spec+.
+
+ def find_reverse_dependencies(spec) # :nodoc:
result = []
- Gem.source_index.each do |name, sp|
+ Gem::Specification.each do |sp|
sp.dependencies.each do |dep|
dep = Gem::Dependency.new(*dep) unless Gem::Dependency === dep
- if spec.name == dep.name and
- dep.version_requirements.satisfied_by?(spec.version) then
+ if spec.name == dep.name &&
+ dep.requirement.satisfied_by?(spec.version)
result << [sp.full_name, dep]
end
end
@@ -172,17 +191,16 @@ class Gem::Commands::DependencyCommand < Gem::Command
result
end
- def find_gems(name, source_index)
- specs = {}
+ private
- spec_list = source_index.search name, options[:version]
+ def name_pattern(args)
+ return if args.empty?
- spec_list.each do |spec|
- specs[spec.full_name] = [source_index, spec]
+ if args.length == 1 && args.first =~ /\A(.*)(i)?\z/m
+ flags = $2 ? Regexp::IGNORECASE : nil
+ Regexp.new $1, flags
+ else
+ /\A#{Regexp.union(*args)}/
end
-
- specs
end
-
end
-
diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb
index e672da54f0..a5eb521a53 100644
--- a/lib/rubygems/commands/environment_command.rb
+++ b/lib/rubygems/commands/environment_command.rb
@@ -1,57 +1,68 @@
-require 'rubygems/command'
+# frozen_string_literal: true
-class Gem::Commands::EnvironmentCommand < Gem::Command
+require_relative "../command"
+class Gem::Commands::EnvironmentCommand < Gem::Command
def initialize
- super 'environment', 'Display information about the RubyGems environment'
+ super "environment", "Display information about the RubyGems environment"
end
def arguments # :nodoc:
args = <<-EOF
- packageversion display the package version
- gemdir display the path where gems are installed
- gempath display path used to search for gems
+ home display the path where gems are installed. Aliases: gemhome, gemdir, GEM_HOME
+ path display path used to search for gems. Aliases: gempath, GEM_PATH
+ user_gemhome display the path where gems are installed when `--user-install` is given. Aliases: user_gemdir
version display the gem format version
remotesources display the remote gem servers
+ platform display the supported gem platforms
+ credentials display the path where credentials are stored
<omitted> display everything
EOF
- return args.gsub(/^\s+/, '')
+ args.gsub(/^\s+/, "")
end
def description # :nodoc:
<<-EOF
+The environment command lets you query rubygems for its configuration for
+use in shell scripts or as a debugging aid.
+
The RubyGems environment can be controlled through command line arguments,
gemrc files, environment variables and built-in defaults.
-Command line argument defaults and some RubyGems defaults can be set in
-~/.gemrc file for individual users and a /etc/gemrc for all users. A gemrc
-is a YAML file with the following YAML keys:
+Command line argument defaults and some RubyGems defaults can be set in a
+~/.gemrc file for individual users and a gemrc in the SYSTEM CONFIGURATION
+DIRECTORY for all users. These files are YAML files with the following YAML
+keys:
:sources: A YAML array of remote gem repositories to install gems from
- :verbose: Verbosity of the gem command. false, true, and :really are the
+ :verbose: Verbosity of the gem command. false, true, and :really are the
levels
:update_sources: Enable/disable automatic updating of repository metadata
+ :concurrent_downloads: The number of gem downloads to perform concurrently
:backtrace: Print backtrace when RubyGems encounters an error
- :bulk_threshold: Switch to a bulk update when this many sources are out of
- date (legacy setting)
:gempath: The paths in which to look for gems
- gem_command: A string containing arguments for the specified gem command
+ :disable_default_gem_server: Force specification of gem server host on push
+ <gem_command>: A string containing arguments for the specified gem command
Example:
:verbose: false
install: --no-wrappers
update: --no-wrappers
+ :disable_default_gem_server: true
-RubyGems' default local repository can be overriden with the GEM_PATH and
-GEM_HOME environment variables. GEM_HOME sets the default repository to
-install into. GEM_PATH allows multiple local repositories to be searched for
+RubyGems' default local repository can be overridden with the GEM_PATH and
+GEM_HOME environment variables. GEM_HOME sets the default repository to
+install into. GEM_PATH allows multiple local repositories to be searched for
gems.
If you are behind a proxy server, RubyGems uses the HTTP_PROXY,
HTTP_PROXY_USER and HTTP_PROXY_PASS environment variables to discover the
proxy server.
+If you would like to push gems to a private gem server the RUBYGEMS_HOST
+environment variable can be set to the URI for that server.
+
If you are packaging RubyGems all of RubyGems' defaults are in
lib/rubygems/defaults.rb. You may override these in
lib/rubygems/defaults/operating_system.rb
@@ -63,66 +74,109 @@ lib/rubygems/defaults/operating_system.rb
end
def execute
- out = ''
+ out = String.new
arg = options[:args][0]
- case arg
- when /^packageversion/ then
- out << Gem::RubyGemsPackageVersion
- when /^version/ then
- out << Gem::RubyGemsVersion
- when /^gemdir/, /^gemhome/, /^home/, /^GEM_HOME/ then
- out << Gem.dir
- when /^gempath/, /^path/, /^GEM_PATH/ then
- out << Gem.path.join(File::PATH_SEPARATOR)
- when /^remotesources/ then
- out << Gem.sources.join("\n")
- when nil then
- out = "RubyGems Environment:\n"
+ out <<
+ case arg
+ when /^version/ then
+ Gem::VERSION
+ when /^gemdir/, /^gemhome/, /^home/, /^GEM_HOME/ then
+ Gem.dir
+ when /^gempath/, /^path/, /^GEM_PATH/ then
+ Gem.path.join(File::PATH_SEPARATOR)
+ when /^user_gemdir/, /^user_gemhome/ then
+ Gem.user_dir
+ when /^remotesources/ then
+ Gem.sources.to_a.join("\n")
+ when /^platform/ then
+ Gem.platforms.join(File::PATH_SEPARATOR)
+ when /^credentials/, /^creds/ then
+ Gem.configuration.credentials_path
+ when nil then
+ show_environment
+ else
+ raise Gem::CommandLineError, "Unknown environment option [#{arg}]"
+ end
+ say out
+ true
+ end
- out << " - RUBYGEMS VERSION: #{Gem::RubyGemsVersion}\n"
+ def add_path(out, path)
+ path.each do |component|
+ out << " - #{component}\n"
+ end
+ end
- out << " - RUBY VERSION: #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}"
- out << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL
- out << ") [#{RUBY_PLATFORM}]\n"
+ def show_environment # :nodoc:
+ out = "RubyGems Environment:\n".dup
- out << " - INSTALLATION DIRECTORY: #{Gem.dir}\n"
+ out << " - RUBYGEMS VERSION: #{Gem::VERSION}\n"
- out << " - RUBYGEMS PREFIX: #{Gem.prefix}\n" unless Gem.prefix.nil?
+ out << " - RUBY VERSION: #{RUBY_VERSION} (#{RUBY_RELEASE_DATE} patchlevel #{RUBY_PATCHLEVEL}) [#{RUBY_PLATFORM}]\n"
- out << " - RUBY EXECUTABLE: #{Gem.ruby}\n"
+ out << " - INSTALLATION DIRECTORY: #{Gem.dir}\n"
- out << " - EXECUTABLE DIRECTORY: #{Gem.bindir}\n"
+ out << " - USER INSTALLATION DIRECTORY: #{Gem.user_dir}\n"
- out << " - RUBYGEMS PLATFORMS:\n"
- Gem.platforms.each do |platform|
- out << " - #{platform}\n"
- end
+ out << " - CREDENTIALS FILE: #{Gem.configuration.credentials_path}\n"
- out << " - GEM PATHS:\n"
- out << " - #{Gem.dir}\n"
+ out << " - RUBYGEMS PREFIX: #{Gem.prefix}\n" unless Gem.prefix.nil?
- path = Gem.path.dup
- path.delete Gem.dir
- path.each do |p|
- out << " - #{p}\n"
- end
+ out << " - RUBY EXECUTABLE: #{Gem.ruby}\n"
- out << " - GEM CONFIGURATION:\n"
- Gem.configuration.each do |name, value|
- out << " - #{name.inspect} => #{value.inspect}\n"
- end
+ out << " - GIT EXECUTABLE: #{git_path}\n"
- out << " - REMOTE SOURCES:\n"
- Gem.sources.each do |s|
- out << " - #{s}\n"
- end
+ out << " - EXECUTABLE DIRECTORY: #{Gem.bindir}\n"
+
+ out << " - SPEC CACHE DIRECTORY: #{Gem.spec_cache_dir}\n"
- else
- fail Gem::CommandLineError, "Unknown enviroment option [#{arg}]"
+ out << " - SYSTEM CONFIGURATION DIRECTORY: #{Gem::ConfigFile::SYSTEM_CONFIG_PATH}\n"
+
+ out << " - RUBYGEMS PLATFORMS:\n"
+ Gem.platforms.each do |platform|
+ out << " - #{platform}\n"
end
- say out
- true
+
+ out << " - GEM PATHS:\n"
+ out << " - #{Gem.dir}\n"
+
+ gem_path = Gem.path.dup
+ gem_path.delete Gem.dir
+ add_path out, gem_path
+
+ out << " - GEM CONFIGURATION:\n"
+ Gem.configuration.each do |name, value|
+ value = value.gsub(/./, "*") if name == "gemcutter_key"
+ out << " - #{name.inspect} => #{value.inspect}\n"
+ end
+
+ out << " - REMOTE SOURCES:\n"
+ Gem.sources.each do |s|
+ out << " - #{s}\n"
+ end
+
+ out << " - SHELL PATH:\n"
+
+ shell_path = ENV["PATH"].split(File::PATH_SEPARATOR)
+ add_path out, shell_path
+
+ out
end
-end
+ private
+
+ ##
+ # Git binary path
+
+ def git_path
+ exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
+ ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
+ exts.each do |ext|
+ exe = File.join(path, "git#{ext}")
+ return exe if File.executable?(exe) && !File.directory?(exe)
+ end
+ end
+ nil
+ end
+end
diff --git a/lib/rubygems/commands/exec_command.rb b/lib/rubygems/commands/exec_command.rb
new file mode 100644
index 0000000000..1feafbdd35
--- /dev/null
+++ b/lib/rubygems/commands/exec_command.rb
@@ -0,0 +1,259 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../dependency_installer"
+require_relative "../gem_runner"
+require_relative "../package"
+require_relative "../version_option"
+
+class Gem::Commands::ExecCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "exec", "Run a command from a gem", {
+ version: Gem::Requirement.default,
+ }
+
+ add_version_option
+ add_prerelease_option "to be installed"
+
+ add_option "-g", "--gem GEM", "run the executable from the given gem" do |value, options|
+ options[:gem_name] = value
+ end
+
+ add_option(:"Install/Update", "--conservative",
+ "Prefer the most recent installed version, ",
+ "rather than the latest version overall") do |_value, options|
+ options[:conservative] = true
+ end
+ end
+
+ def arguments # :nodoc:
+ "COMMAND the executable command to run"
+ end
+
+ def defaults_str # :nodoc:
+ "--version '#{Gem::Requirement.default}'"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The exec command handles installing (if necessary) and running an executable
+from a gem, regardless of whether that gem is currently installed.
+
+The exec command can be thought of as a shortcut to running `gem install` and
+then the executable from the installed gem.
+
+For example, `gem exec rails new .` will run `rails new .` in the current
+directory, without having to manually run `gem install rails`.
+Additionally, the exec command ensures the most recent version of the gem
+is used (unless run with `--conservative`), and that the gem is not installed
+to the same gem path as user-installed gems.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [options --] COMMAND [args]"
+ end
+
+ def execute
+ check_executable
+
+ print_command
+ if options[:gem_name] == "gem" && options[:executable] == "gem"
+ set_gem_exec_install_paths
+ Gem::GemRunner.new.run options[:args]
+ return
+ elsif options[:conservative]
+ install_if_needed
+ else
+ install
+ activate!
+ end
+
+ load!
+ end
+
+ private
+
+ def handle_options(args)
+ args = add_extra_args(args)
+ check_deprecated_options(args)
+ @options = Marshal.load Marshal.dump @defaults # deep copy
+ parser.order!(args) do |v|
+ # put the non-option back at the front of the list of arguments
+ args.unshift(v)
+
+ # stop parsing once we hit the first non-option,
+ # so you can call `gem exec rails --version` and it prints the rails
+ # version rather than rubygem's
+ break
+ end
+ @options[:args] = args
+
+ options[:executable], gem_version = extract_gem_name_and_version(options[:args].shift)
+ options[:gem_name] ||= options[:executable]
+
+ if gem_version
+ if options[:version].none?
+ options[:version] = Gem::Requirement.new(gem_version)
+ else
+ options[:version].concat [gem_version]
+ end
+ end
+
+ if options[:prerelease] && !options[:version].prerelease?
+ if options[:version].none?
+ options[:version] = Gem::Requirement.default_prerelease
+ else
+ options[:version].concat [Gem::Requirement.default_prerelease]
+ end
+ end
+ end
+
+ def check_executable
+ if options[:executable].nil?
+ raise Gem::CommandLineError,
+ "Please specify an executable to run (e.g. #{program_name} COMMAND)"
+ end
+ end
+
+ def print_command
+ verbose "running #{program_name} with:\n"
+ opts = options.reject {|_, v| v.nil? || Array(v).empty? }
+ max_length = opts.map {|k, _| k.size }.max
+ opts.each do |k, v|
+ next if v.nil?
+ verbose "\t#{k.to_s.rjust(max_length)}: #{v}"
+ end
+ verbose ""
+ end
+
+ def install_if_needed
+ activate!
+ rescue Gem::MissingSpecError
+ verbose "#{Gem::Dependency.new(options[:gem_name], options[:version])} not available locally, installing from remote"
+ install
+ activate!
+ end
+
+ def set_gem_exec_install_paths
+ home = Gem.dir
+
+ ENV["GEM_PATH"] = ([home] + Gem.path).join(File::PATH_SEPARATOR)
+ ENV["GEM_HOME"] = home
+ Gem.clear_paths
+ end
+
+ def install
+ set_gem_exec_install_paths
+
+ gem_name = options[:gem_name]
+ gem_version = options[:version]
+
+ install_options = options.merge(
+ minimal_deps: false,
+ wrappers: true
+ )
+
+ suppress_always_install do
+ dep_installer = Gem::DependencyInstaller.new install_options
+
+ request_set = dep_installer.resolve_dependencies gem_name, gem_version
+
+ verbose "Gems to install:"
+ request_set.sorted_requests.each do |activation_request|
+ verbose "\t#{activation_request.full_name}"
+ end
+
+ request_set.install install_options
+ end
+
+ Gem::Specification.reset
+ rescue Gem::InstallError => e
+ alert_error "Error installing #{gem_name}:\n\t#{e.message}"
+ terminate_interaction 1
+ rescue Gem::DependencyResolutionError => e
+ alert_error "Error installing #{gem_name}:\n\t#{e.message}"
+ terminate_interaction 2
+ rescue Gem::GemNotFoundException => e
+ show_lookup_failure e.name, e.version, e.errors, false
+
+ terminate_interaction 2
+ rescue Gem::UnsatisfiableDependencyError => e
+ show_lookup_failure e.name, e.version, e.errors, false,
+ "'#{gem_name}' (#{gem_version})"
+
+ terminate_interaction 2
+ end
+
+ def activate!
+ gem(options[:gem_name], options[:version])
+ Gem.finish_resolve
+
+ verbose "activated #{options[:gem_name]} (#{Gem.loaded_specs[options[:gem_name]].version})"
+ end
+
+ def load!
+ argv = ARGV.clone
+ ARGV.replace options[:args]
+
+ executable = options[:executable]
+
+ contains_executable = Gem.loaded_specs.values.select do |spec|
+ spec.executables.include?(executable)
+ end
+
+ if contains_executable.any? {|s| s.name == executable }
+ contains_executable.select! {|s| s.name == executable }
+ end
+
+ if contains_executable.empty?
+ spec = Gem.loaded_specs[executable]
+
+ if spec.nil? || spec.executables.empty?
+ alert_error "Failed to load executable `#{executable}`," \
+ " are you sure the gem `#{options[:gem_name]}` contains it?"
+ terminate_interaction 1
+ end
+
+ if spec.executables.size > 1
+ alert_error "Ambiguous which executable from gem `#{executable}` should be run: " \
+ "the options are #{spec.executables.sort}, specify one via COMMAND, and use `-g` and `-v` to specify gem and version"
+ terminate_interaction 1
+ end
+
+ contains_executable << spec
+ executable = spec.executable
+ end
+
+ if contains_executable.size > 1
+ alert_error "Ambiguous which gem `#{executable}` should come from: " \
+ "the options are #{contains_executable.map(&:name)}, " \
+ "specify one via `-g`"
+ terminate_interaction 1
+ end
+
+ old_exe = $0
+ $0 = executable
+ load Gem.activate_bin_path(contains_executable.first.name, executable, ">= 0.a")
+ ensure
+ $0 = old_exe if old_exe
+ ARGV.replace argv
+ end
+
+ def suppress_always_install
+ name = :always_install
+ cls = ::Gem::Resolver::InstallerSet
+ method = cls.instance_method(name)
+ cls.remove_method(name)
+ cls.define_method(name) { [] }
+
+ begin
+ yield
+ ensure
+ cls.remove_method(name)
+ cls.define_method(name, method)
+ end
+ end
+end
diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb
index 76c9924e6b..8e64a18cee 100644
--- a/lib/rubygems/commands/fetch_command.rb
+++ b/lib/rubygems/commands/fetch_command.rb
@@ -1,62 +1,109 @@
-require 'rubygems/command'
-require 'rubygems/local_remote_options'
-require 'rubygems/version_option'
-require 'rubygems/source_info_cache'
+# frozen_string_literal: true
-class Gem::Commands::FetchCommand < Gem::Command
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+class Gem::Commands::FetchCommand < Gem::Command
include Gem::LocalRemoteOptions
include Gem::VersionOption
def initialize
- super 'fetch', 'Download a gem and place it in the current directory'
+ defaults = {
+ suggest_alternate: true,
+ version: Gem::Requirement.default,
+ }
+
+ super "fetch", "Download a gem and place it in the current directory", defaults
add_bulk_threshold_option
add_proxy_option
add_source_option
+ add_clear_sources_option
add_version_option
add_platform_option
+ add_prerelease_option
+
+ add_option "--[no-]suggestions", "Suggest alternates when gems are not found" do |value, options|
+ options[:suggest_alternate] = value
+ end
end
def arguments # :nodoc:
- 'GEMNAME name of gem to download'
+ "GEMNAME name of gem to download"
end
def defaults_str # :nodoc:
"--version '#{Gem::Requirement.default}'"
end
+ def description # :nodoc:
+ <<-EOF
+The fetch command fetches gem files that can be stored for later use or
+unpacked to examine their contents.
+
+See the build command help for an example of unpacking a gem, modifying it,
+then repackaging it.
+ EOF
+ end
+
def usage # :nodoc:
"#{program_name} GEMNAME [GEMNAME ...]"
end
+ def check_version # :nodoc:
+ if options[:version] != Gem::Requirement.default &&
+ get_all_gem_names.size > 1
+ alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \
+ " version requirements using `gem fetch 'my_gem:1.0.0' 'my_other_gem:>=2'`"
+ terminate_interaction 1
+ end
+ end
+
def execute
- version = options[:version] || Gem::Requirement.default
- all = Gem::Requirement.default
+ check_version
- gem_names = get_all_gem_names
+ exit_code = fetch_gems
- gem_names.each do |gem_name|
- dep = Gem::Dependency.new gem_name, version
+ terminate_interaction exit_code
+ end
- specs_and_sources = Gem::SpecFetcher.fetcher.fetch dep, all
+ private
- specs_and_sources.sort_by { |spec,| spec.version }
+ def fetch_gems
+ exit_code = 0
- spec, source_uri = specs_and_sources.last
+ version = options[:version]
- if spec.nil? then
- alert_error "Could not find #{gem_name} in any repository"
- next
+ platform = Gem.platforms.last
+ gem_names = get_all_gem_names_and_versions
+
+ gem_names.each do |gem_name, gem_version|
+ gem_version ||= version
+ dep = Gem::Dependency.new gem_name, gem_version
+ dep.prerelease = options[:prerelease]
+ suppress_suggestions = !options[:suggest_alternate]
+
+ specs_and_sources, errors =
+ Gem::SpecFetcher.fetcher.spec_for_dependency dep
+
+ if platform
+ filtered = specs_and_sources.select {|s,| s.platform == platform }
+ specs_and_sources = filtered unless filtered.empty?
end
- path = Gem::RemoteFetcher.fetcher.download spec, source_uri
- FileUtils.mv path, "#{spec.full_name}.gem"
+ spec, source = specs_and_sources.max_by {|s,| s }
+ if spec.nil?
+ show_lookup_failure gem_name, gem_version, errors, suppress_suggestions, options[:domain]
+ exit_code |= 2
+ next
+ end
+ source.download spec
say "Downloaded #{spec.full_name}"
end
- end
+ exit_code
+ end
end
-
diff --git a/lib/rubygems/commands/generate_index_command.rb b/lib/rubygems/commands/generate_index_command.rb
index 1bd87569ed..13be92593b 100644
--- a/lib/rubygems/commands/generate_index_command.rb
+++ b/lib/rubygems/commands/generate_index_command.rb
@@ -1,57 +1,51 @@
-require 'rubygems/command'
-require 'rubygems/indexer'
-
-class Gem::Commands::GenerateIndexCommand < Gem::Command
-
- def initialize
- super 'generate_index',
- 'Generates the index files for a gem server directory',
- :directory => '.'
-
- add_option '-d', '--directory=DIRNAME',
- 'repository base dir containing gems subdir' do |dir, options|
- options[:directory] = File.expand_path dir
+# frozen_string_literal: true
+
+require_relative "../command"
+
+unless defined? Gem::Commands::GenerateIndexCommand
+ class Gem::Commands::GenerateIndexCommand < Gem::Command
+ module RubygemsTrampoline
+ def description # :nodoc:
+ <<~EOF
+ The generate_index command has been moved to the rubygems-generate_index gem.
+ EOF
+ end
+
+ def execute
+ alert_error "Install the rubygems-generate_index gem for the generate_index command"
+ end
+
+ def invoke_with_build_args(args, build_args)
+ name = "rubygems-generate_index"
+ spec = begin
+ Gem::Specification.find_by_name(name)
+ rescue Gem::LoadError
+ require "rubygems/dependency_installer"
+ Gem.install(name, Gem::Requirement.default, Gem::DependencyInstaller::DEFAULT_OPTIONS).find {|s| s.name == name }
+ end
+
+ # remove the methods defined in this file so that the methods defined in the gem are used instead,
+ # and without a method redefinition warning
+ %w[description execute invoke_with_build_args].each do |method|
+ RubygemsTrampoline.remove_method(method)
+ end
+ self.class.singleton_class.remove_method(:new)
+
+ spec.activate
+ Gem.load_plugin_files spec.matches_for_glob("rubygems_plugin#{Gem.suffix_pattern}")
+
+ self.class.new.invoke_with_build_args(args, build_args)
+ end
end
- end
-
- def defaults_str # :nodoc:
- "--directory ."
- end
+ private_constant :RubygemsTrampoline
- def description # :nodoc:
- <<-EOF
-The generate_index command creates a set of indexes for serving gems
-statically. The command expects a 'gems' directory under the path given to
-the --directory option. When done, it will generate a set of files like this:
-
- gems/ # .gem files you want to index
- quick/index
- quick/index.rz # quick index manifest
- quick/<gemname>.gemspec.rz # legacy YAML quick index file
- quick/Marshal.<version>/<gemname>.gemspec.rz # Marshal quick index file
- Marshal.<version>
- Marshal.<version>.Z # Marshal full index
- yaml
- yaml.Z # legacy YAML full index
-
-The .Z and .rz extension files are compressed with the inflate algorithm. The
-Marshal version number comes from ruby's Marshal::MAJOR_VERSION and
-Marshal::MINOR_VERSION constants. It is used to ensure compatibility. The
-yaml indexes exist for legacy RubyGems clients and fallback in case of Marshal
-version changes.
- EOF
- end
-
- def execute
- if not File.exist?(options[:directory]) or
- not File.directory?(options[:directory]) then
- alert_error "unknown directory name #{directory}."
- terminate_interaction 1
- else
- indexer = Gem::Indexer.new options[:directory]
- indexer.generate_index
+ # remove_method(:initialize) warns, but removing new does not warn
+ def self.new
+ command = allocate
+ command.send(:initialize, "generate_index", "Generates the index files for a gem server directory (requires rubygems-generate_index)")
+ command
end
- end
+ prepend(RubygemsTrampoline)
+ end
end
-
diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb
index 0c4a4ec16f..664f400561 100644
--- a/lib/rubygems/commands/help_command.rb
+++ b/lib/rubygems/commands/help_command.rb
@@ -1,7 +1,8 @@
-require 'rubygems/command'
+# frozen_string_literal: true
-class Gem::Commands::HelpCommand < Gem::Command
+require_relative "../command"
+class Gem::Commands::HelpCommand < Gem::Command
# :stopdoc:
EXAMPLES = <<-EOF
Some examples of 'gem' usage.
@@ -14,11 +15,6 @@ Some examples of 'gem' usage.
gem install rake --remote
-* Install 'rake' from remote server, and run unit tests,
- and generate RDocs:
-
- gem install --remote rake --test --rdoc --ri
-
* Install 'rake', but only version 0.3.1, even if dependencies
are not met, and into a user-specific directory:
@@ -42,7 +38,7 @@ Some examples of 'gem' usage.
* Create a gem:
- See http://rubygems.rubyforge.org/wiki/wiki.pl?CreateAGemInTenMinutes
+ See https://guides.rubygems.org/make-your-own-gem/
* See information about RubyGems:
@@ -51,6 +47,189 @@ Some examples of 'gem' usage.
* Update all gems on your system:
gem update
+
+* Update your local version of RubyGems
+
+ gem update --system
+ EOF
+
+ GEM_DEPENDENCIES = <<-EOF
+A gem dependencies file allows installation of a consistent set of gems across
+multiple environments. The RubyGems implementation is designed to be
+compatible with Bundler's Gemfile format. You can see additional
+documentation on the format at:
+
+ https://bundler.io
+
+RubyGems automatically looks for these gem dependencies files:
+
+* gem.deps.rb
+* Gemfile
+* Isolate
+
+These files are looked up automatically using `gem install -g`, or you can
+specify a custom file.
+
+When the RUBYGEMS_GEMDEPS environment variable is set to a gem dependencies
+file the gems from that file will be activated at startup time. Set it to a
+specific filename or to "-" to have RubyGems automatically discover the gem
+dependencies file by walking up from the current directory.
+
+You can also activate gem dependencies at program startup using
+Gem.use_gemdeps.
+
+NOTE: Enabling automatic discovery on multiuser systems can lead to execution
+of arbitrary code when used from directories outside your control.
+
+Gem Dependencies
+================
+
+Use #gem to declare which gems you directly depend upon:
+
+ gem 'rake'
+
+To depend on a specific set of versions:
+
+ gem 'rake', '>= 10.3.2'
+ # or for multiple version restrictions
+ gem 'rake', '>= 10.3.2', "< 13"
+
+RubyGems will require the gem name when activating the gem using
+the RUBYGEMS_GEMDEPS environment variable or Gem::use_gemdeps. Use the
+require: option to override this behavior if the gem does not have a file of
+that name or you don't want to require those files:
+
+ gem 'my_gem', require: 'other_file'
+
+To prevent RubyGems from requiring any files use:
+
+ gem 'my_gem', require: false
+
+To load dependencies from a .gemspec file:
+
+ gemspec
+
+RubyGems looks for the first .gemspec file in the current directory. To
+override this use the name: option:
+
+ gemspec name: 'specific_gem'
+
+To look in a different directory use the path: option:
+
+ gemspec name: 'specific_gem', path: 'gemspecs'
+
+To depend on a gem unpacked into a local directory:
+
+ gem 'modified_gem', path: 'vendor/modified_gem'
+
+To depend on a gem from git:
+
+ gem 'private_gem', git: 'git@my.company.example:private_gem.git'
+
+To depend on a gem from github:
+
+ gem 'private_gem', github: 'my_company/private_gem'
+
+To depend on a gem from a github gist:
+
+ gem 'bang', gist: '1232884'
+
+Git, github and gist support the ref:, branch: and tag: options to specify a
+commit reference or hash, branch or tag respectively to use for the gem.
+
+Setting the submodules: option to true for git, github and gist dependencies
+causes fetching of submodules when fetching the repository.
+
+You can depend on multiple gems from a single repository with the git method:
+
+ git 'https://github.com/rails/rails.git' do
+ gem 'activesupport'
+ gem 'activerecord'
+ end
+
+Gem Sources
+===========
+
+RubyGems uses the default sources for regular `gem install` for gem
+dependencies files. Unlike bundler, you do need to specify a source.
+
+You can override the sources used for downloading gems with:
+
+ source 'https://gem_server.example'
+
+You may specify multiple sources. Unlike bundler the prepend: option is not
+supported. Sources are used in-order, to prepend a source place it at the
+front of the list.
+
+Gem Platform
+============
+
+You can restrict gem dependencies to specific platforms with the #platform
+and #platforms methods:
+
+ platform :ruby_21 do
+ gem 'debugger'
+ end
+
+See the bundler Gemfile manual page for a list of platforms supported in a gem
+dependencies file.:
+
+ https://bundler.io/v2.5/man/gemfile.5.html
+
+Ruby Version and Engine Dependency
+==================================
+
+You can specify the version, engine and engine version of ruby to use with
+your gem dependencies file. If you are not running the specified version
+RubyGems will raise an exception.
+
+To depend on a specific version of ruby:
+
+ ruby '2.1.2'
+
+To depend on a specific ruby engine:
+
+ ruby '1.9.3', engine: 'jruby'
+
+To depend on a specific ruby engine version:
+
+ ruby '1.9.3', engine: 'jruby', engine_version: '1.7.11'
+
+Grouping Dependencies
+=====================
+
+Gem dependencies may be placed in groups that can be excluded from install.
+Dependencies required for development or testing of your code may be excluded
+when installed in a production environment.
+
+A #gem dependency may be placed in a group using the group: option:
+
+ gem 'minitest', group: :test
+
+To install dependencies from a gemfile without specific groups use the
+`--without` option for `gem install -g`:
+
+ $ gem install -g --without test
+
+The group: option also accepts multiple groups if the gem fits in multiple
+categories.
+
+Multiple groups may be excluded during install by comma-separating the groups for `--without` or by specifying `--without` multiple times.
+
+The #group method can also be used to place gems in groups:
+
+ group :test do
+ gem 'minitest'
+ gem 'minitest-emoji'
+ end
+
+The #group method allows multiple groups.
+
+The #gemspec development dependencies are placed in the :development group by
+default. This may be overridden with the :development_group option:
+
+ gemspec development_group: :other
+
EOF
PLATFORMS = <<-'EOF'
@@ -60,8 +239,9 @@ your current platform by running `gem environment`.
RubyGems matches platforms as follows:
- * The CPU must match exactly, unless one of the platforms has
- "universal" as the CPU.
+ * The CPU must match exactly unless one of the platforms has
+ "universal" as the CPU or the local CPU starts with "arm" and the gem's
+ CPU is exactly "arm" (for gems that support generic ARM architecture).
* The OS must match exactly.
* The versions must match exactly unless one of the versions is nil.
@@ -71,29 +251,41 @@ you pass must match "#{cpu}-#{os}" or "#{cpu}-#{os}-#{version}". On mswin
platforms, the version is the compiler version, not the OS version. (Ruby
compiled with VC6 uses "60" as the compiler version, VC8 uses "80".)
+For the ARM architecture, gems with a platform of "arm-linux" should run on a
+reasonable set of ARM CPUs and not depend on instructions present on a limited
+subset of the architecture. For example, the binary should run on platforms
+armv5, armv6hf, armv6l, armv7, etc. If you use the "arm-linux" platform
+please test your gem on a variety of ARM hardware before release to ensure it
+functions correctly.
+
Example platforms:
x86-freebsd # Any FreeBSD version on an x86 CPU
universal-darwin-8 # Darwin 8 only gems that run on any CPU
x86-mswin32-80 # Windows gems compiled with VC8
+ armv7-linux # Gem complied for an ARMv7 CPU running linux
+ arm-linux # Gem compiled for any ARM CPU running linux
When building platform gems, set the platform in the gem specification to
Gem::Platform::CURRENT. This will correctly mark the gem with your ruby's
platform.
EOF
+
+ # NOTE: when updating also update Gem::Command::HELP
+
+ SUBCOMMANDS = [
+ ["commands", :show_commands],
+ ["options", Gem::Command::HELP],
+ ["examples", EXAMPLES],
+ ["gem_dependencies", GEM_DEPENDENCIES],
+ ["platforms", PLATFORMS],
+ ].freeze
# :startdoc:
def initialize
- super 'help', "Provide help on the 'gem' command"
- end
+ super "help", "Provide help on the 'gem' command"
- def arguments # :nodoc:
- args = <<-EOF
- commands List all 'gem' commands
- examples Show examples of 'gem' usage
- <command> Show specific help for <command>
- EOF
- return args.gsub(/^\s+/, '')
+ @command_manager = Gem::CommandManager.instance
end
def usage # :nodoc:
@@ -101,72 +293,85 @@ platform.
end
def execute
- command_manager = Gem::CommandManager.instance
arg = options[:args][0]
- if begins? "commands", arg then
- out = []
- out << "GEM commands are:"
- out << nil
+ _, help = SUBCOMMANDS.find do |command,|
+ begins? command, arg
+ end
- margin_width = 4
+ if help
+ if Symbol === help
+ send help
+ else
+ say help
+ end
+ return
+ end
- desc_width = command_manager.command_names.map { |n| n.size }.max + 4
+ if options[:help]
+ show_help
- summary_width = 80 - margin_width - desc_width
- wrap_indent = ' ' * (margin_width + desc_width)
- format = "#{' ' * margin_width}%-#{desc_width}s%s"
+ elsif arg
+ show_command_help arg
- command_manager.command_names.each do |cmd_name|
- summary = command_manager[cmd_name].summary
- summary = wrap(summary, summary_width).split "\n"
- out << sprintf(format, cmd_name, summary.shift)
- until summary.empty? do
- out << "#{wrap_indent}#{summary.shift}"
- end
- end
+ else
+ say Gem::Command::HELP
+ end
+ end
- out << nil
- out << "For help on a particular command, use 'gem help COMMAND'."
- out << nil
- out << "Commands may be abbreviated, so long as they are unambiguous."
- out << "e.g. 'gem i rake' is short for 'gem install rake'."
+ def show_commands # :nodoc:
+ out = []
+ out << "GEM commands are:"
+ out << nil
- say out.join("\n")
+ margin_width = 4
- elsif begins? "options", arg then
- say Gem::Command::HELP
+ desc_width = @command_manager.command_names.map(&:size).max + 4
- elsif begins? "examples", arg then
- say EXAMPLES
+ summary_width = 80 - margin_width - desc_width
+ wrap_indent = " " * (margin_width + desc_width)
+ format = "#{" " * margin_width}%-#{desc_width}s%s"
- elsif begins? "platforms", arg then
- say PLATFORMS
+ @command_manager.command_names.each do |cmd_name|
+ command = @command_manager[cmd_name]
- elsif options[:help] then
- command = command_manager[options[:help]]
- if command
- # help with provided command
- command.invoke("--help")
- else
- alert_error "Unknown command #{options[:help]}. Try 'gem help commands'"
- end
+ next if command&.deprecated?
- elsif arg then
- possibilities = command_manager.find_command_possibilities(arg.downcase)
- if possibilities.size == 1
- command = command_manager[possibilities.first]
- command.invoke("--help")
- elsif possibilities.size > 1
- alert_warning "Ambiguous command #{arg} (#{possibilities.join(', ')})"
- else
- alert_warning "Unknown command #{arg}. Try gem help commands"
+ summary =
+ if command
+ command.summary
+ else
+ "[No command found for #{cmd_name}]"
+ end
+
+ summary = wrap(summary, summary_width).split "\n"
+ out << format(format, cmd_name, summary.shift)
+ until summary.empty? do
+ out << "#{wrap_indent}#{summary.shift}"
end
+ end
+
+ out << nil
+ out << "For help on a particular command, use 'gem help COMMAND'."
+ out << nil
+ out << "Commands may be abbreviated, so long as they are unambiguous."
+ out << "e.g. 'gem i rake' is short for 'gem install rake'."
+ say out.join("\n")
+ end
+
+ def show_command_help(command_name) # :nodoc:
+ command_name = command_name.downcase
+
+ possibilities = @command_manager.find_command_possibilities command_name
+
+ if possibilities.size == 1
+ command = @command_manager[possibilities.first]
+ command.invoke("--help")
+ elsif possibilities.size > 1
+ alert_warning "Ambiguous command #{command_name} (#{possibilities.join(", ")})"
else
- say Gem::Command::HELP
+ alert_warning "Unknown command #{command_name}. Try: gem help commands"
end
end
-
end
-
diff --git a/lib/rubygems/commands/info_command.rb b/lib/rubygems/commands/info_command.rb
new file mode 100644
index 0000000000..f65c639662
--- /dev/null
+++ b/lib/rubygems/commands/info_command.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../query_utils"
+
+class Gem::Commands::InfoCommand < Gem::Command
+ include Gem::QueryUtils
+
+ def initialize
+ super "info", "Show information for the given gem",
+ name: //, domain: :local, details: false, versions: true,
+ installed: nil, version: Gem::Requirement.default
+
+ add_query_options
+
+ remove_option("-d")
+
+ defaults[:details] = true
+ defaults[:exact] = true
+ end
+
+ def description # :nodoc:
+ "Info prints information about the gem such as name,"\
+ " description, website, license and installed paths"
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME"
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of the gem to print information about"
+ end
+
+ def defaults_str
+ "--local"
+ end
+end
diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb
index 1a6eb68a8b..6d3beec0b4 100644
--- a/lib/rubygems/commands/install_command.rb
+++ b/lib/rubygems/commands/install_command.rb
@@ -1,32 +1,46 @@
-require 'rubygems/command'
-require 'rubygems/doc_manager'
-require 'rubygems/install_update_options'
-require 'rubygems/dependency_installer'
-require 'rubygems/local_remote_options'
-require 'rubygems/validator'
-require 'rubygems/version_option'
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../install_update_options"
+require_relative "../dependency_installer"
+require_relative "../local_remote_options"
+require_relative "../validator"
+require_relative "../version_option"
+require_relative "../update_suggestion"
+
+##
+# Gem installer command line tool
+#
+# See `gem help install`
class Gem::Commands::InstallCommand < Gem::Command
+ attr_reader :installed_specs # :nodoc:
include Gem::VersionOption
include Gem::LocalRemoteOptions
include Gem::InstallUpdateOptions
+ include Gem::UpdateSuggestion
def initialize
defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({
- :generate_rdoc => true,
- :generate_ri => true,
- :format_executable => false,
- :test => false,
- :version => Gem::Requirement.default,
+ format_executable: false,
+ lock: true,
+ suggest_alternate: true,
+ version: Gem::Requirement.default,
+ without_groups: [],
})
- super 'install', 'Install a gem into the local repository', defaults
+ defaults.merge!(install_update_options)
+
+ super "install", "Install a gem into the local repository", defaults
add_install_update_options
add_local_remote_options
add_platform_option
add_version_option
+ add_prerelease_option "to be installed. (Only for listed gems)"
+
+ @installed_specs = []
end
def arguments # :nodoc:
@@ -34,8 +48,9 @@ class Gem::Commands::InstallCommand < Gem::Command
end
def defaults_str # :nodoc:
- "--both --version '#{Gem::Requirement.default}' --rdoc --ri --no-force\n" \
- "--no-test --install-dir #{Gem.dir}"
+ "--both --version '#{Gem::Requirement.default}' --no-force\n" \
+ "--install-dir #{Gem.dir} --lock\n" +
+ install_update_defaults_str
end
def description # :nodoc:
@@ -43,106 +58,211 @@ class Gem::Commands::InstallCommand < Gem::Command
The install command installs local or remote gem into a gem repository.
For gems with executables ruby installs a wrapper file into the executable
-directory by deault. This can be overridden with the --no-wrappers option.
+directory by default. This can be overridden with the --no-wrappers option.
The wrapper allows you to choose among alternate gem versions using _version_.
For example `rake _0.7.3_ --version` will run rake version 0.7.3 if a newer
version is also installed.
+
+Gem Dependency Files
+====================
+
+RubyGems can install a consistent set of gems across multiple environments
+using `gem install -g` when a gem dependencies file (gem.deps.rb, Gemfile or
+Isolate) is present. If no explicit file is given RubyGems attempts to find
+one in the current directory.
+
+When the RUBYGEMS_GEMDEPS environment variable is set to a gem dependencies
+file the gems from that file will be activated at startup time. Set it to a
+specific filename or to "-" to have RubyGems automatically discover the gem
+dependencies file by walking up from the current directory.
+
+NOTE: Enabling automatic discovery on multiuser systems can lead to
+execution of arbitrary code when used from directories outside your control.
+
+Extension Install Failures
+==========================
+
+If an extension fails to compile during gem installation the gem
+specification is not written out, but the gem remains unpacked in the
+repository. You may need to specify the path to the library's headers and
+libraries to continue. You can do this by adding a -- between RubyGems'
+options and the extension's build options:
+
+ $ gem install some_extension_gem
+ [build fails]
+ Gem files will remain installed in \\
+ /path/to/gems/some_extension_gem-1.0 for inspection.
+ Results logged to /path/to/gems/some_extension_gem-1.0/gem_make.out
+ $ gem install some_extension_gem -- --with-extension-lib=/path/to/lib
+ [build succeeds]
+ $ gem list some_extension_gem
+
+ *** LOCAL GEMS ***
+
+ some_extension_gem (1.0)
+ $
+
+If you correct the compilation errors by editing the gem files you will need
+to write the specification by hand. For example:
+
+ $ gem install some_extension_gem
+ [build fails]
+ Gem files will remain installed in \\
+ /path/to/gems/some_extension_gem-1.0 for inspection.
+ Results logged to /path/to/gems/some_extension_gem-1.0/gem_make.out
+ $ [cd /path/to/gems/some_extension_gem-1.0]
+ $ [edit files or what-have-you and run make]
+ $ gem spec ../../cache/some_extension_gem-1.0.gem --ruby > \\
+ ../../specifications/some_extension_gem-1.0.gemspec
+ $ gem list some_extension_gem
+
+ *** LOCAL GEMS ***
+
+ some_extension_gem (1.0)
+ $
+
+Command Alias
+==========================
+
+You can use `i` command instead of `install`.
+
+ $ gem i GEMNAME
+
EOF
end
def usage # :nodoc:
- "#{program_name} GEMNAME [GEMNAME ...] [options] -- --build-flags"
+ "#{program_name} [options] GEMNAME [GEMNAME ...] -- --build-flags"
+ end
+
+ def check_version # :nodoc:
+ if options[:version] != Gem::Requirement.default &&
+ get_all_gem_names.size > 1
+ alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \
+ " version requirements using `gem install 'my_gem:1.0.0' 'my_other_gem:>=2'`"
+ terminate_interaction 1
+ end
end
def execute
- if options[:include_dependencies] then
- alert "`gem install -y` is now default and will be removed"
- alert "use --ignore-dependencies to install only the gems you list"
+ if options.include? :gemdeps
+ install_from_gemdeps
+ return # not reached
end
- installed_gems = []
+ @installed_specs = []
- ENV.delete 'GEM_PATH' if options[:install_dir].nil? and RUBY_VERSION > '1.9'
+ ENV.delete "GEM_PATH" if options[:install_dir].nil?
- install_options = {
- :env_shebang => options[:env_shebang],
- :domain => options[:domain],
- :force => options[:force],
- :format_executable => options[:format_executable],
- :ignore_dependencies => options[:ignore_dependencies],
- :install_dir => options[:install_dir],
- :security_policy => options[:security_policy],
- :wrappers => options[:wrappers],
- :bin_dir => options[:bin_dir],
- :development => options[:development],
- }
+ check_version
- exit_code = 0
+ load_hooks
- get_all_gem_names.each do |gem_name|
- begin
- inst = Gem::DependencyInstaller.new install_options
- inst.install gem_name, options[:version]
+ exit_code = install_gems
- inst.installed_gems.each do |spec|
- say "Successfully installed #{spec.full_name}"
- end
+ show_installed
- installed_gems.push(*inst.installed_gems)
- rescue Gem::InstallError => e
- alert_error "Error installing #{gem_name}:\n\t#{e.message}"
- exit_code |= 1
- rescue Gem::GemNotFoundException => e
- alert_error e.message
- exit_code |= 2
-# rescue => e
-# # TODO: Fix this handle to allow the error to propagate to
-# # the top level handler. Examine the other errors as
-# # well. This implementation here looks suspicious to me --
-# # JimWeirich (4/Jan/05)
-# alert_error "Error installing gem #{gem_name}: #{e.message}"
-# return
+ say update_suggestion if eligible_for_update?
+
+ terminate_interaction exit_code
+ end
+
+ def install_from_gemdeps # :nodoc:
+ require_relative "../request_set"
+ rs = Gem::RequestSet.new
+
+ specs = rs.install_from_gemdeps options do |req, inst|
+ s = req.full_spec
+
+ if inst
+ say "Installing #{s.name} (#{s.version})"
+ else
+ say "Using #{s.name} (#{s.version})"
end
end
- unless installed_gems.empty? then
- gems = installed_gems.length == 1 ? 'gem' : 'gems'
- say "#{installed_gems.length} #{gems} installed"
- end
+ @installed_specs = specs
- # NOTE: *All* of the RI documents must be generated first.
- # For some reason, RI docs cannot be generated after any RDoc
- # documents are generated.
+ terminate_interaction
+ end
- if options[:generate_ri] then
- installed_gems.each do |gem|
- Gem::DocManager.new(gem, options[:rdoc_args]).generate_ri
- end
+ def install_gem(name, version) # :nodoc:
+ return if options[:conservative] &&
+ !Gem::Dependency.new(name, version).matching_specs.empty?
- Gem::DocManager.update_ri_cache
- end
+ req = Gem::Requirement.create(version)
- if options[:generate_rdoc] then
- installed_gems.each do |gem|
- Gem::DocManager.new(gem, options[:rdoc_args]).generate_rdoc
+ dinst = Gem::DependencyInstaller.new options
+
+ request_set = dinst.resolve_dependencies name, req
+
+ if options[:explain]
+ say "Gems to install:"
+
+ request_set.sorted_requests.each do |activation_request|
+ say " #{activation_request.full_name}"
end
+ else
+ @installed_specs.concat request_set.install options
end
- if options[:test] then
- installed_gems.each do |spec|
- gem_spec = Gem::SourceIndex.from_installed_gems.search(spec.name, spec.version.version).first
- result = Gem::Validator.new.unit_test(gem_spec)
- if result and not result.passed?
- unless ask_yes_no("...keep Gem?", true) then
- Gem::Uninstaller.new(spec.name, :version => spec.version.version).uninstall
- end
- end
+ show_install_errors dinst.errors
+ end
+
+ def install_gems # :nodoc:
+ exit_code = 0
+
+ get_all_gem_names_and_versions.each do |gem_name, gem_version|
+ gem_version ||= options[:version]
+ domain = options[:domain]
+ domain = :local unless options[:suggest_alternate]
+ suppress_suggestions = (domain == :local)
+
+ begin
+ install_gem gem_name, gem_version
+ rescue Gem::InstallError => e
+ alert_error "Error installing #{gem_name}:\n\t#{e.message}"
+ exit_code |= 1
+ rescue Gem::DependencyResolutionError => e
+ alert_error "Error installing #{gem_name}:\n\t#{e.message}"
+ exit_code |= 2
+ rescue Gem::UnsatisfiableDependencyError => e
+ show_lookup_failure e.name, e.version, e.errors, suppress_suggestions,
+ "'#{gem_name}' (#{gem_version})"
+
+ exit_code |= 2
end
end
- raise Gem::SystemExitException, exit_code
+ exit_code
end
-end
+ ##
+ # Loads post-install hooks
+
+ def load_hooks # :nodoc:
+ require_relative "../install_message"
+ require_relative "../rdoc"
+ end
+
+ def show_install_errors(errors) # :nodoc:
+ return unless errors
+ errors.each do |x|
+ next unless Gem::SourceFetchProblem === x
+
+ require_relative "../uri"
+ msg = "Unable to pull data from '#{Gem::Uri.redact(x.source.uri)}': #{x.error.message}"
+
+ alert_warning msg
+ end
+ end
+
+ def show_installed # :nodoc:
+ return if @installed_specs.empty?
+
+ gems = @installed_specs.length == 1 ? "gem" : "gems"
+ say "#{@installed_specs.length} #{gems} installed"
+ end
+end
diff --git a/lib/rubygems/commands/list_command.rb b/lib/rubygems/commands/list_command.rb
index f3e5da9551..fab4b73814 100644
--- a/lib/rubygems/commands/list_command.rb
+++ b/lib/rubygems/commands/list_command.rb
@@ -1,35 +1,42 @@
-require 'rubygems/command'
-require 'rubygems/commands/query_command'
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../query_utils"
##
-# An alternate to Gem::Commands::QueryCommand that searches for gems starting
-# with the the supplied argument.
+# Searches for gems starting with the supplied argument.
-class Gem::Commands::ListCommand < Gem::Commands::QueryCommand
+class Gem::Commands::ListCommand < Gem::Command
+ include Gem::QueryUtils
def initialize
- super 'list', 'Display gems whose name starts with STRING'
+ super "list", "Display local gems whose name matches REGEXP",
+ domain: :local, details: false, versions: true,
+ installed: nil, version: Gem::Requirement.default
- remove_option('--name-matches')
+ add_query_options
end
def arguments # :nodoc:
- "STRING start of gem name to look for"
+ "REGEXP regexp to look for in gem name"
end
def defaults_str # :nodoc:
"--local --no-details"
end
- def usage # :nodoc:
- "#{program_name} [STRING]"
- end
+ def description # :nodoc:
+ <<-EOF
+The list command is used to view the gems you have installed locally.
+
+The --details option displays additional details including the summary, the
+homepage, the author, the locations of different versions of the gem.
- def execute
- string = get_one_optional_argument || ''
- options[:name] = /^#{string}/i
- super
+To search for remote gems use the search command.
+ EOF
end
+ def usage # :nodoc:
+ "#{program_name} [REGEXP ...]"
+ end
end
-
diff --git a/lib/rubygems/commands/lock_command.rb b/lib/rubygems/commands/lock_command.rb
index 5a43978dd9..f7fd5ada16 100644
--- a/lib/rubygems/commands/lock_command.rb
+++ b/lib/rubygems/commands/lock_command.rb
@@ -1,13 +1,14 @@
-require 'rubygems/command'
+# frozen_string_literal: true
-class Gem::Commands::LockCommand < Gem::Command
+require_relative "../command"
+class Gem::Commands::LockCommand < Gem::Command
def initialize
- super 'lock', 'Generate a lockdown list of gems',
- :strict => false
+ super "lock", "Generate a lockdown list of gems",
+ strict: false
- add_option '-s', '--[no-]strict',
- 'fail if unable to satisfy a dependency' do |strict, options|
+ add_option "-s", "--[no-]strict",
+ "fail if unable to satisfy a dependency" do |strict, options|
options[:strict] = strict
end
end
@@ -30,7 +31,7 @@ generated.
Example:
- gemlock rails-1.0.0 > lockdown.rb
+ gem lock rails-1.0.0 > lockdown.rb
will produce in lockdown.rb:
@@ -58,7 +59,7 @@ lock it down to the exact version.
end
def complain(message)
- if options[:strict] then
+ if options[:strict]
raise Gem::Exception, message
else
say "# #{message}"
@@ -75,9 +76,9 @@ lock it down to the exact version.
until pending.empty? do
full_name = pending.shift
- spec = Gem::SourceIndex.load_specification spec_path(full_name)
+ spec = Gem::Specification.load spec_path(full_name)
- if spec.nil? then
+ if spec.nil?
complain "Could not find gem #{full_name}, try using the full name"
next
end
@@ -87,9 +88,9 @@ lock it down to the exact version.
spec.runtime_dependencies.each do |dep|
next if locked[dep.name]
- candidates = Gem.source_index.search dep
+ candidates = dep.matching_specs
- if candidates.empty? then
+ if candidates.empty?
complain "Unable to satisfy '#{dep}' from currently installed gems"
else
pending << candidates.last.full_name
@@ -103,8 +104,6 @@ lock it down to the exact version.
File.join path, "specifications", "#{gem_full_name}.gemspec"
end
- gemspecs.find { |gemspec| File.exist? gemspec }
+ gemspecs.find {|path| File.exist? path }
end
-
end
-
diff --git a/lib/rubygems/commands/mirror_command.rb b/lib/rubygems/commands/mirror_command.rb
index 959b8eaec3..b91a8db12d 100644
--- a/lib/rubygems/commands/mirror_command.rb
+++ b/lib/rubygems/commands/mirror_command.rb
@@ -1,111 +1,26 @@
-require 'yaml'
-require 'zlib'
-
-require 'rubygems/command'
-require 'open-uri'
-
-class Gem::Commands::MirrorCommand < Gem::Command
-
- def initialize
- super 'mirror', 'Mirror a gem repository'
- end
-
- def description # :nodoc:
- <<-EOF
-The mirror command uses the ~/.gemmirrorrc config file to mirror remote gem
-repositories to a local path. The config file is a YAML document that looks
-like this:
-
- ---
- - from: http://gems.example.com # source repository URI
- to: /path/to/mirror # destination directory
-
-Multiple sources and destinations may be specified.
- EOF
- end
-
- def execute
- config_file = File.join Gem.user_home, '.gemmirrorrc'
-
- raise "Config file #{config_file} not found" unless File.exist? config_file
-
- mirrors = YAML.load_file config_file
-
- raise "Invalid config file #{config_file}" unless mirrors.respond_to? :each
-
- mirrors.each do |mir|
- raise "mirror missing 'from' field" unless mir.has_key? 'from'
- raise "mirror missing 'to' field" unless mir.has_key? 'to'
-
- get_from = mir['from']
- save_to = File.expand_path mir['to']
-
- raise "Directory not found: #{save_to}" unless File.exist? save_to
- raise "Not a directory: #{save_to}" unless File.directory? save_to
-
- gems_dir = File.join save_to, "gems"
-
- if File.exist? gems_dir then
- raise "Not a directory: #{gems_dir}" unless File.directory? gems_dir
- else
- Dir.mkdir gems_dir
- end
-
- sourceindex_data = ''
-
- say "fetching: #{get_from}/Marshal.#{Gem.marshal_version}.Z"
-
- get_from = URI.parse get_from
-
- if get_from.scheme.nil? then
- get_from = get_from.to_s
- elsif get_from.scheme == 'file' then
- # check if specified URI contains a drive letter (file:/D:/Temp)
- get_from = get_from.to_s
- get_from = if get_from =~ /^file:.*[a-z]:/i then
- get_from[6..-1]
- else
- get_from[5..-1]
- end
- end
-
- open File.join(get_from.to_s, "Marshal.#{Gem.marshal_version}.Z"), "rb" do |y|
- sourceindex_data = Zlib::Inflate.inflate y.read
- open File.join(save_to, "Marshal.#{Gem.marshal_version}"), "wb" do |out|
- out.write sourceindex_data
- end
+# frozen_string_literal: true
+
+require_relative "../command"
+
+unless defined? Gem::Commands::MirrorCommand
+ class Gem::Commands::MirrorCommand < Gem::Command
+ def initialize
+ super("mirror", "Mirror all gem files (requires rubygems-mirror)")
+ begin
+ Gem::Specification.find_by_name("rubygems-mirror").activate
+ rescue Gem::LoadError
+ # no-op
end
+ end
- sourceindex = Marshal.load(sourceindex_data)
-
- progress = ui.progress_reporter sourceindex.size,
- "Fetching #{sourceindex.size} gems"
- sourceindex.each do |fullname, gem|
- gem_file = "#{fullname}.gem"
- gem_dest = File.join gems_dir, gem_file
-
- unless File.exist? gem_dest then
- begin
- open "#{get_from}/gems/#{gem_file}", "rb" do |g|
- contents = g.read
- open gem_dest, "wb" do |out|
- out.write contents
- end
- end
- rescue
- old_gf = gem_file
- gem_file = gem_file.downcase
- retry if old_gf != gem_file
- alert_error $!
- end
- end
-
- progress.updated gem_file
- end
+ def description # :nodoc:
+ <<-EOF
+The mirror command has been moved to the rubygems-mirror gem.
+ EOF
+ end
- progress.done
+ def execute
+ alert_error "Install the rubygems-mirror gem for the mirror command"
end
end
-
end
-
diff --git a/lib/rubygems/commands/open_command.rb b/lib/rubygems/commands/open_command.rb
new file mode 100644
index 0000000000..0fe90dc8b8
--- /dev/null
+++ b/lib/rubygems/commands/open_command.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+
+class Gem::Commands::OpenCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "open", "Open gem sources in editor"
+
+ add_option("-e", "--editor COMMAND", String,
+ "Prepends COMMAND to gem path. Could be used to specify editor.") do |command, options|
+ options[:editor] = command || get_env_editor
+ end
+ add_option("-v", "--version VERSION", String,
+ "Opens specific gem version") do |version|
+ options[:version] = version
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to open in editor"
+ end
+
+ def defaults_str # :nodoc:
+ "-e #{get_env_editor}"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+ The open command opens gem in editor and changes current path
+ to gem's source directory.
+ Editor command can be specified with -e option, otherwise rubygems
+ will look for editor in $EDITOR, $VISUAL and $GEM_EDITOR variables.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [-e COMMAND] GEMNAME"
+ end
+
+ def get_env_editor
+ ENV["GEM_EDITOR"] ||
+ ENV["VISUAL"] ||
+ ENV["EDITOR"] ||
+ "vi"
+ end
+
+ def execute
+ @version = options[:version] || Gem::Requirement.default
+ @editor = options[:editor] || get_env_editor
+
+ found = open_gem(get_one_gem_name)
+
+ terminate_interaction 1 unless found
+ end
+
+ def open_gem(name)
+ spec = spec_for name
+
+ return false unless spec
+
+ if spec.default_gem?
+ say "'#{name}' is a default gem and can't be opened."
+ return false
+ end
+
+ open_editor(spec.full_gem_path)
+ end
+
+ def open_editor(path)
+ system(*@editor.split(/\s+/) + [path], { chdir: path })
+ end
+
+ def spec_for(name)
+ spec = Gem::Specification.find_all_by_name(name, @version).first
+
+ return spec if spec
+
+ say "Unable to find gem '#{name}'"
+ end
+end
diff --git a/lib/rubygems/commands/outdated_command.rb b/lib/rubygems/commands/outdated_command.rb
index 9e054f988c..08a9221a26 100644
--- a/lib/rubygems/commands/outdated_command.rb
+++ b/lib/rubygems/commands/outdated_command.rb
@@ -1,33 +1,33 @@
-require 'rubygems/command'
-require 'rubygems/local_remote_options'
-require 'rubygems/spec_fetcher'
-require 'rubygems/version_option'
+# frozen_string_literal: true
-class Gem::Commands::OutdatedCommand < Gem::Command
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../spec_fetcher"
+require_relative "../version_option"
+class Gem::Commands::OutdatedCommand < Gem::Command
include Gem::LocalRemoteOptions
include Gem::VersionOption
def initialize
- super 'outdated', 'Display all gems that need updates'
+ super "outdated", "Display all gems that need updates"
add_local_remote_options
add_platform_option
end
- def execute
- locals = Gem::SourceIndex.from_installed_gems
-
- locals.outdated.sort.each do |name|
- local = locals.find_name(name).last
+ def description # :nodoc:
+ <<-EOF
+The outdated command lists gems you may wish to upgrade to a newer version.
- dep = Gem::Dependency.new local.name, ">= #{local.version}"
- remotes = Gem::SpecFetcher.fetcher.fetch dep
- remote = remotes.last.first
+You can check for dependency mismatches using the dependency command and
+update the gems with the update or install commands.
+ EOF
+ end
- say "#{local.name} (#{local.version} < #{remote.version})"
+ def execute
+ Gem::Specification.outdated_and_latest_version.each do |spec, remote_version|
+ say "#{spec.name} (#{spec.version} < #{remote_version})"
end
end
-
end
-
diff --git a/lib/rubygems/commands/owner_command.rb b/lib/rubygems/commands/owner_command.rb
new file mode 100644
index 0000000000..675e866734
--- /dev/null
+++ b/lib/rubygems/commands/owner_command.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../gemcutter_utilities"
+require_relative "../text"
+
+class Gem::Commands::OwnerCommand < Gem::Command
+ include Gem::Text
+ include Gem::LocalRemoteOptions
+ include Gem::GemcutterUtilities
+
+ def description # :nodoc:
+ <<-EOF
+The owner command lets you add and remove owners of a gem on a push
+server (the default is https://rubygems.org). Multiple owners can be
+added or removed at the same time, if the flag is given multiple times.
+
+The supported user identifiers are dependent on the push server.
+For rubygems.org, both e-mail and handle are supported, even though the
+user identifier field is called "email".
+
+The owner of a gem has the permission to push new versions, yank existing
+versions or edit the HTML page of the gem. Be careful of who you give push
+permission to.
+ EOF
+ end
+
+ def arguments # :nodoc:
+ "GEM gem to manage owners for"
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEM"
+ end
+
+ def initialize
+ super "owner", "Manage gem owners of a gem on the push server"
+ add_proxy_option
+ add_key_option
+ add_otp_option
+ defaults.merge! add: [], remove: []
+
+ add_option "-a", "--add NEW_OWNER", "Add an owner by user identifier" do |value, options|
+ options[:add] << value
+ end
+
+ add_option "-r", "--remove OLD_OWNER", "Remove an owner by user identifier" do |value, options|
+ options[:remove] << value
+ end
+
+ add_option "-h", "--host HOST",
+ "Use another gemcutter-compatible host",
+ " (e.g. https://rubygems.org)" do |value, options|
+ options[:host] = value
+ end
+ end
+
+ def execute
+ @host = options[:host]
+
+ sign_in(scope: get_owner_scope)
+ name = get_one_gem_name
+
+ add_owners name, options[:add]
+ remove_owners name, options[:remove]
+ show_owners name
+ end
+
+ def show_owners(name)
+ Gem.load_yaml
+
+ response = rubygems_api_request :get, "api/v1/gems/#{name}/owners.yaml" do |request|
+ request.add_field "Authorization", api_key
+ end
+
+ with_response response do |resp|
+ owners = Gem::SafeYAML.safe_load clean_text(resp.body)
+
+ say "Owners for gem: #{name}"
+ owners.each do |owner|
+ identifier = owner["email"] || owner["handle"] || owner["id"]
+ say "- #{identifier} (#{owner["role"]})"
+ end
+ end
+ end
+
+ def add_owners(name, owners)
+ manage_owners :post, name, owners
+ end
+
+ def remove_owners(name, owners)
+ manage_owners :delete, name, owners
+ end
+
+ def manage_owners(method, name, owners)
+ owners.each do |owner|
+ response = send_owner_request(method, name, owner)
+ action = method == :delete ? "Removing" : "Adding"
+
+ with_response response, "#{action} #{owner}"
+ rescue Gem::WebauthnVerificationError => e
+ raise e
+ rescue StandardError
+ # ignore early exits to allow for completing the iteration of all owners
+ end
+ end
+
+ private
+
+ def send_owner_request(method, name, owner)
+ rubygems_api_request method, "api/v1/gems/#{name}/owners", scope: get_owner_scope(method: method) do |request|
+ request.set_form_data "email" => owner
+ request.add_field "Authorization", api_key
+ end
+ end
+
+ def get_owner_scope(method: nil)
+ if method == :post || options.any? && options[:add].any?
+ :add_owner
+ elsif method == :delete || options.any? && options[:remove].any?
+ :remove_owner
+ end
+ end
+end
diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb
index d47fe54edd..10978c2af7 100644
--- a/lib/rubygems/commands/pristine_command.rb
+++ b/lib/rubygems/commands/pristine_command.rb
@@ -1,25 +1,73 @@
-require 'fileutils'
-require 'rubygems/command'
-require 'rubygems/format'
-require 'rubygems/installer'
-require 'rubygems/version_option'
+# frozen_string_literal: true
-class Gem::Commands::PristineCommand < Gem::Command
+require_relative "../command"
+require_relative "../package"
+require_relative "../installer"
+require_relative "../version_option"
+class Gem::Commands::PristineCommand < Gem::Command
include Gem::VersionOption
def initialize
- super 'pristine',
- 'Restores installed gems to pristine condition from files located in the gem cache',
- :version => Gem::Requirement.default
-
- add_option('--all',
- 'Restore all installed gems to pristine',
- 'condition') do |value, options|
+ super "pristine",
+ "Restores installed gems to pristine condition from files located in the gem cache",
+ version: Gem::Requirement.default,
+ extensions: true,
+ extensions_set: false,
+ all: false
+
+ add_option("--all",
+ "Restore all installed gems to pristine",
+ "condition") do |value, options|
options[:all] = value
end
- add_version_option('restore to', 'pristine condition')
+ add_option("--skip=gem_name",
+ "used on --all, skip if name == gem_name") do |value, options|
+ options[:skip] ||= []
+ options[:skip] << value
+ end
+
+ add_option("--[no-]extensions",
+ "Restore gems with extensions",
+ "in addition to regular gems") do |value, options|
+ options[:extensions_set] = true
+ options[:extensions] = value
+ end
+
+ add_option("--only-missing-extensions",
+ "Only restore gems with missing extensions") do |value, options|
+ options[:only_missing_extensions] = value
+ end
+
+ add_option("--only-executables",
+ "Only restore executables") do |value, options|
+ options[:only_executables] = value
+ end
+
+ add_option("--only-plugins",
+ "Only restore plugins") do |value, options|
+ options[:only_plugins] = value
+ end
+
+ add_option("-E", "--[no-]env-shebang",
+ "Rewrite executables with a shebang",
+ "of /usr/bin/env") do |value, options|
+ options[:env_shebang] = value
+ end
+
+ add_option("-i", "--install-dir DIR",
+ "Gem repository to get gems restored") do |value, options|
+ options[:install_dir] = File.expand_path(value)
+ end
+
+ add_option("-n", "--bindir DIR",
+ "Directory where executables are",
+ "located") do |value, options|
+ options[:bin_dir] = File.expand_path(value)
+ end
+
+ add_version_option("restore to", "pristine condition")
end
def arguments # :nodoc:
@@ -27,67 +75,149 @@ class Gem::Commands::PristineCommand < Gem::Command
end
def defaults_str # :nodoc:
- "--all"
+ "--extensions"
end
def description # :nodoc:
<<-EOF
-The pristine command compares the installed gems with the contents of the
-cached gem and restores any files that don't match the cached gem's copy.
+The pristine command compares an installed gem with the contents of its
+cached .gem file and restores any files that don't match the cached .gem's
+copy.
+
+If you have made modifications to an installed gem, the pristine command
+will revert them. All extensions are rebuilt and all bin stubs for the gem
+are regenerated after checking for modifications.
+
+Rebuilding extensions also refreshes C-extension gems against updated system
+libraries (for example after OS or package upgrades) to avoid mismatches like
+outdated library version warnings.
-If you have made modifications to your installed gems, the pristine command
-will revert them. After all the gem's files have been checked all bin stubs
-for the gem are regenerated.
+If the cached gem cannot be found it will be downloaded.
-If the cached gem cannot be found, you will need to use `gem install` to
-revert the gem.
+If --no-extensions is provided pristine will not attempt to restore a gem
+with an extension.
+
+If --extensions is given (but not --all or gem names) only gems with
+extensions will be restored.
EOF
end
def usage # :nodoc:
- "#{program_name} [args]"
+ "#{program_name} [GEMNAME ...]"
end
def execute
- gem_name = nil
-
- specs = if options[:all] then
- Gem::SourceIndex.from_installed_gems.map do |name, spec|
- spec
- end
- else
- gem_name = get_one_gem_name
- Gem::SourceIndex.from_installed_gems.find_name(gem_name,
- options[:version])
- end
-
- if specs.empty? then
+ install_dir = options[:install_dir]
+
+ specification_record = install_dir ? Gem::SpecificationRecord.from_path(install_dir) : Gem::Specification.specification_record
+
+ specs = if options[:all]
+ specification_record.map
+
+ # `--extensions` must be explicitly given to pristine only gems
+ # with extensions.
+ elsif options[:extensions_set] &&
+ options[:extensions] && options[:args].empty?
+ specification_record.select do |spec|
+ spec.extensions && !spec.extensions.empty?
+ end
+ elsif options[:only_missing_extensions]
+ specification_record.select(&:missing_extensions?)
+ else
+ get_all_gem_names.sort.flat_map do |gem_name|
+ specification_record.find_all_by_name(gem_name, options[:version]).reverse
+ end
+ end
+
+ specs = specs.select {|spec| spec.platform == RUBY_ENGINE || Gem::Platform.local === spec.platform || spec.platform == Gem::Platform::RUBY }
+
+ if specs.to_a.empty?
+ if options[:only_missing_extensions]
+ say "No gems with missing extensions to restore"
+ return
+ end
+
raise Gem::Exception,
- "Failed to find gem #{gem_name} #{options[:version]}"
+ "Failed to find gems #{options[:args]} #{options[:version]}"
end
- install_dir = Gem.dir # TODO use installer option
+ say "Restoring gems to pristine condition..."
- raise Gem::FilePermissionError.new(install_dir) unless
- File.writable?(install_dir)
+ specs.group_by(&:full_name_with_location).values.each do |grouped_specs|
+ spec = grouped_specs.find {|s| !s.default_gem? } || grouped_specs.first
- say "Restoring gem(s) to pristine condition..."
+ only_executables = options[:only_executables]
+ only_plugins = options[:only_plugins]
+
+ unless only_executables || only_plugins
+ # Default gemspecs include changes provided by ruby-core installer that
+ # can't currently be pristined (inclusion of compiled extension targets in
+ # the file list). So stick to resetting executables if it's a default gem.
+ only_executables = true if spec.default_gem?
+ end
- specs.each do |spec|
- gem = Dir[File.join(Gem.dir, 'cache', "#{spec.full_name}.gem")].first
+ if options.key? :skip
+ if options[:skip].include? spec.name
+ say "Skipped #{spec.full_name}, it was given through options"
+ next
+ end
+ end
- if gem.nil? then
- alert_error "Cached gem for #{spec.full_name} not found, use `gem install` to restore"
+ unless spec.extensions.empty? || options[:extensions] || only_executables || only_plugins
+ say "Skipped #{spec.full_name_with_location}, it needs to compile an extension"
next
end
- # TODO use installer options
- installer = Gem::Installer.new gem, :wrappers => true, :force => true
- installer.install
+ gem = spec.cache_file
+
+ unless File.exist?(gem) || only_executables || only_plugins
+ require_relative "../remote_fetcher"
+
+ say "Cached gem for #{spec.full_name_with_location} not found, attempting to fetch..."
- say "Restored #{spec.full_name}"
+ dep = Gem::Dependency.new spec.name, spec.version
+ found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dep
+
+ if found.empty?
+ say "Skipped #{spec.full_name}, it was not found from cache and remote sources"
+ next
+ end
+
+ spec_candidate, source = found.first
+ Gem::RemoteFetcher.fetcher.download spec_candidate, source.uri.to_s, spec.base_dir
+ end
+
+ env_shebang =
+ if options.include? :env_shebang
+ options[:env_shebang]
+ else
+ install_defaults = Gem::ConfigFile::PLATFORM_DEFAULTS["install"]
+ install_defaults.to_s["--env-shebang"]
+ end
+
+ bin_dir = options[:bin_dir] if options[:bin_dir]
+
+ installer_options = {
+ wrappers: true,
+ force: true,
+ install_dir: install_dir || spec.base_dir,
+ env_shebang: env_shebang,
+ build_args: spec.build_args,
+ bin_dir: bin_dir,
+ }
+
+ if only_executables
+ installer = Gem::Installer.for_spec(spec, installer_options)
+ installer.generate_bin
+ elsif only_plugins
+ installer = Gem::Installer.for_spec(spec, installer_options)
+ installer.generate_plugins
+ else
+ installer = Gem::Installer.at(gem, installer_options)
+ installer.install
+ end
+
+ say "Restored #{spec.full_name_with_location}"
end
end
-
end
-
diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb
new file mode 100644
index 0000000000..02931b3025
--- /dev/null
+++ b/lib/rubygems/commands/push_command.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../gemcutter_utilities"
+require_relative "../package"
+
+class Gem::Commands::PushCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::GemcutterUtilities
+
+ def description # :nodoc:
+ <<-EOF
+The push command uploads a gem to the push server (the default is
+https://rubygems.org) and adds it to the index.
+
+The gem can be removed from the index and deleted from the server using the yank
+command. For further discussion see the help for the yank command.
+
+The push command will use ~/.gem/credentials to authenticate to a server, but you can use the RubyGems environment variable GEM_HOST_API_KEY to set the api key to authenticate.
+ EOF
+ end
+
+ def arguments # :nodoc:
+ "GEM built gem to push up"
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEM"
+ end
+
+ def initialize
+ super "push", "Push a gem up to the gem server", host: host, attestations: []
+
+ @user_defined_host = false
+
+ add_proxy_option
+ add_key_option
+ add_otp_option
+
+ add_option("--host HOST",
+ "Push to another gemcutter-compatible host",
+ " (e.g. https://rubygems.org)") do |value, options|
+ options[:host] = value
+ @user_defined_host = true
+ end
+
+ add_option("--attestation FILE",
+ "Push with sigstore attestations") do |value, options|
+ options[:attestations] << value
+ end
+
+ @host = nil
+ end
+
+ def execute
+ gem_name = get_one_gem_name
+ default_gem_server, push_host = get_hosts_for(gem_name)
+
+ @host = if @user_defined_host
+ options[:host]
+ elsif default_gem_server
+ default_gem_server
+ elsif push_host
+ push_host
+ else
+ options[:host]
+ end
+
+ sign_in @host, scope: get_push_scope
+
+ send_gem(gem_name)
+ end
+
+ def send_gem(name)
+ args = [:post, "api/v1/gems"]
+
+ _, push_host = get_hosts_for(name)
+
+ @host ||= push_host
+
+ # Always include @host, even if it's nil
+ args += [@host, push_host]
+
+ say "Pushing gem to #{@host || Gem.host}..."
+
+ response = send_push_request(name, args)
+
+ with_response response
+ end
+
+ private
+
+ def send_push_request(name, args)
+ # Always honor explicit --attestation option
+ # Auto-attestation is only supported on rubygems.org with GitHub Actions (not JRuby)
+ if options[:attestations].any? || (RUBY_ENGINE != "jruby" && attestation_supported_host? && ENV["GITHUB_ACTIONS"])
+ send_push_request_with_attestation(name, args)
+ else
+ send_push_request_without_attestation(name, args)
+ end
+ end
+
+ def send_push_request_without_attestation(name, args)
+ scope = get_push_scope
+ rubygems_api_request(*args, scope: scope) do |request|
+ body = Gem.read_binary name
+ request.body = body
+ request.add_field "Content-Type", "application/octet-stream"
+ request.add_field "Content-Length", request.body.size
+ request.add_field "Authorization", api_key
+ end
+ end
+
+ def send_push_request_with_attestation(name, args)
+ attestations = if options[:attestations].any?
+ options[:attestations].map do |attestation|
+ Gem.read_binary(attestation)
+ end
+ else
+ bundle_path = attest!(name)
+ begin
+ [Gem.read_binary(bundle_path)]
+ ensure
+ File.unlink(bundle_path) if bundle_path && File.exist?(bundle_path)
+ end
+ end
+ bundles = "[" + attestations.join(",") + "]"
+
+ rubygems_api_request(*args, scope: get_push_scope) do |request|
+ request.set_form([
+ ["gem", Gem.read_binary(name), { filename: name, content_type: "application/octet-stream" }],
+ ["attestations", bundles, { content_type: "application/json" }],
+ ], "multipart/form-data")
+ request.add_field "Authorization", api_key
+ end
+ rescue StandardError => e
+ message = "Failed to push with attestation, retrying without attestation.\n"
+ message += if Gem.configuration.really_verbose
+ e.full_message
+ else
+ e.message
+ end
+ alert_warning message
+ send_push_request_without_attestation(name, args)
+ end
+
+ def attest!(name)
+ require "open3"
+ require "tempfile"
+
+ tempfile = Tempfile.new([File.basename(name, ".*"), ".sigstore.json"])
+ bundle = tempfile.path
+ tempfile.close(false)
+
+ env = defined?(Bundler.unbundled_env) ? Bundler.unbundled_env : ENV.to_h
+ out, st = Open3.capture2e(
+ env,
+ Gem.ruby, "-S", "gem", "exec", "--conservative",
+ "sigstore-cli", "sign", name, "--bundle", bundle,
+ unsetenv_others: true
+ )
+ raise Gem::Exception, "Failed to sign gem:\n\n#{out}" unless st.success?
+
+ bundle
+ end
+
+ def get_hosts_for(name)
+ gem_metadata = Gem::Package.new(name).spec.metadata
+
+ [
+ gem_metadata["default_gem_server"],
+ gem_metadata["allowed_push_host"],
+ ]
+ end
+
+ def get_push_scope
+ :push_rubygem
+ end
+
+ def attestation_supported_host?
+ host = (@host || Gem.host).to_s.chomp("/")
+ host == Gem::DEFAULT_HOST
+ end
+end
diff --git a/lib/rubygems/commands/query_command.rb b/lib/rubygems/commands/query_command.rb
deleted file mode 100644
index 29fe8acb79..0000000000
--- a/lib/rubygems/commands/query_command.rb
+++ /dev/null
@@ -1,233 +0,0 @@
-require 'rubygems/command'
-require 'rubygems/local_remote_options'
-require 'rubygems/spec_fetcher'
-require 'rubygems/version_option'
-
-class Gem::Commands::QueryCommand < Gem::Command
-
- include Gem::LocalRemoteOptions
- include Gem::VersionOption
-
- def initialize(name = 'query',
- summary = 'Query gem information in local or remote repositories')
- super name, summary,
- :name => //, :domain => :local, :details => false, :versions => true,
- :installed => false, :version => Gem::Requirement.default
-
- add_option('-i', '--[no-]installed',
- 'Check for installed gem') do |value, options|
- options[:installed] = value
- end
-
- add_version_option
-
- add_option('-n', '--name-matches REGEXP',
- 'Name of gem(s) to query on matches the',
- 'provided REGEXP') do |value, options|
- options[:name] = /#{value}/i
- end
-
- add_option('-d', '--[no-]details',
- 'Display detailed information of gem(s)') do |value, options|
- options[:details] = value
- end
-
- add_option( '--[no-]versions',
- 'Display only gem names') do |value, options|
- options[:versions] = value
- options[:details] = false unless value
- end
-
- add_option('-a', '--all',
- 'Display all gem versions') do |value, options|
- options[:all] = value
- end
-
- add_local_remote_options
- end
-
- def defaults_str # :nodoc:
- "--local --name-matches // --no-details --versions --no-installed"
- end
-
- def execute
- exit_code = 0
-
- name = options[:name]
-
- if options[:installed] then
- if name.source.empty? then
- alert_error "You must specify a gem name"
- exit_code |= 4
- elsif installed? name, options[:version] then
- say "true"
- else
- say "false"
- exit_code |= 1
- end
-
- raise Gem::SystemExitException, exit_code
- end
-
- dep = Gem::Dependency.new name, Gem::Requirement.default
-
- if local? then
- if ui.outs.tty? or both? then
- say
- say "*** LOCAL GEMS ***"
- say
- end
-
- specs = Gem.source_index.search dep
-
- spec_tuples = specs.map do |spec|
- [[spec.name, spec.version, spec.original_platform, spec], :local]
- end
-
- output_query_results spec_tuples
- end
-
- if remote? then
- if ui.outs.tty? or both? then
- say
- say "*** REMOTE GEMS ***"
- say
- end
-
- all = options[:all]
-
- begin
- fetcher = Gem::SpecFetcher.fetcher
- spec_tuples = fetcher.find_matching dep, all, false
- rescue Gem::RemoteFetcher::FetchError => e
- raise unless fetcher.warn_legacy e do
- require 'rubygems/source_info_cache'
-
- dep.name = '' if dep.name == //
-
- specs = Gem::SourceInfoCache.search_with_source dep, false, all
-
- spec_tuples = specs.map do |spec, source_uri|
- [[spec.name, spec.version, spec.original_platform, spec],
- source_uri]
- end
- end
- end
-
- output_query_results spec_tuples
- end
- end
-
- private
-
- ##
- # Check if gem +name+ version +version+ is installed.
-
- def installed?(name, version = Gem::Requirement.default)
- dep = Gem::Dependency.new name, version
- !Gem.source_index.search(dep).empty?
- end
-
- def output_query_results(spec_tuples)
- output = []
- versions = Hash.new { |h,name| h[name] = [] }
-
- spec_tuples.each do |spec_tuple, source_uri|
- versions[spec_tuple.first] << [spec_tuple, source_uri]
- end
-
- versions = versions.sort_by do |(name,_),_|
- name.downcase
- end
-
- versions.each do |gem_name, matching_tuples|
- matching_tuples = matching_tuples.sort_by do |(name, version,_),_|
- version
- end.reverse
-
- seen = {}
-
- matching_tuples.delete_if do |(name, version,_),_|
- if seen[version] then
- true
- else
- seen[version] = true
- false
- end
- end
-
- entry = gem_name.dup
-
- if options[:versions] then
- versions = matching_tuples.map { |(name, version,_),_| version }.uniq
- entry << " (#{versions.join ', '})"
- end
-
- if options[:details] then
- detail_tuple = matching_tuples.first
-
- spec = if detail_tuple.first.length == 4 then
- detail_tuple.first.last
- else
- uri = URI.parse detail_tuple.last
- Gem::SpecFetcher.fetcher.fetch_spec detail_tuple.first, uri
- end
-
- entry << "\n"
- authors = "Author#{spec.authors.length > 1 ? 's' : ''}: "
- authors << spec.authors.join(', ')
- entry << format_text(authors, 68, 4)
-
- if spec.rubyforge_project and not spec.rubyforge_project.empty? then
- rubyforge = "Rubyforge: http://rubyforge.org/projects/#{spec.rubyforge_project}"
- entry << "\n" << format_text(rubyforge, 68, 4)
- end
-
- if spec.homepage and not spec.homepage.empty? then
- entry << "\n" << format_text("Homepage: #{spec.homepage}", 68, 4)
- end
-
- if spec.loaded_from then
- if matching_tuples.length == 1 then
- loaded_from = File.dirname File.dirname(spec.loaded_from)
- entry << "\n" << " Installed at: #{loaded_from}"
- else
- label = 'Installed at'
- matching_tuples.each do |(_,version,_,s),|
- loaded_from = File.dirname File.dirname(s.loaded_from)
- entry << "\n" << " #{label} (#{version}): #{loaded_from}"
- label = ' ' * label.length
- end
- end
- end
-
- entry << "\n\n" << format_text(spec.summary, 68, 4)
- end
- output << entry
- end
-
- say output.join(options[:details] ? "\n\n" : "\n")
- end
-
- ##
- # Used for wrapping and indenting text
-
- def format_text(text, wrap, indent=0)
- result = []
- work = text.dup
-
- while work.length > wrap
- if work =~ /^(.{0,#{wrap}})[ \n]/o then
- result << $1
- work.slice!(0, $&.length)
- else
- result << work.slice!(0, wrap)
- end
- end
-
- result << work if work.length.nonzero?
- result.join("\n").gsub(/^/, " " * indent)
- end
-
-end
-
diff --git a/lib/rubygems/commands/rdoc_command.rb b/lib/rubygems/commands/rdoc_command.rb
index 82180d485c..62c4bf8ce9 100644
--- a/lib/rubygems/commands/rdoc_command.rb
+++ b/lib/rubygems/commands/rdoc_command.rb
@@ -1,82 +1,90 @@
-require 'rubygems/command'
-require 'rubygems/version_option'
-require 'rubygems/doc_manager'
-
-module Gem
- module Commands
- class RdocCommand < Command
- include VersionOption
-
- def initialize
- super('rdoc',
- 'Generates RDoc for pre-installed gems',
- {
- :version => Gem::Requirement.default,
- :include_rdoc => true,
- :include_ri => true,
- })
- add_option('--all',
- 'Generate RDoc/RI documentation for all',
- 'installed gems') do |value, options|
- options[:all] = value
- end
- add_option('--[no-]rdoc',
- 'Include RDoc generated documents') do
- |value, options|
- options[:include_rdoc] = value
- end
- add_option('--[no-]ri',
- 'Include RI generated documents'
- ) do |value, options|
- options[:include_ri] = value
- end
- add_version_option
- end
+# frozen_string_literal: true
- def arguments # :nodoc:
- "GEMNAME gem to generate documentation for (unless --all)"
- end
+require_relative "../command"
+require_relative "../version_option"
+require_relative "../rdoc"
+require "fileutils"
- def defaults_str # :nodoc:
- "--version '#{Gem::Requirement.default}' --rdoc --ri"
- end
+class Gem::Commands::RdocCommand < Gem::Command
+ include Gem::VersionOption
- def usage # :nodoc:
- "#{program_name} [args]"
- end
+ def initialize
+ super "rdoc", "Generates RDoc for pre-installed gems",
+ version: Gem::Requirement.default,
+ include_rdoc: false, include_ri: true, overwrite: false
- def execute
- if options[:all]
- specs = Gem::SourceIndex.from_installed_gems.collect { |name, spec|
- spec
- }
- else
- gem_name = get_one_gem_name
- specs = Gem::SourceIndex.from_installed_gems.search(
- gem_name, options[:version])
- end
-
- if specs.empty?
- fail "Failed to find gem #{gem_name} to generate RDoc for #{options[:version]}"
- end
-
- if options[:include_ri]
- specs.each do |spec|
- Gem::DocManager.new(spec).generate_ri
- end
-
- Gem::DocManager.update_ri_cache
- end
-
- if options[:include_rdoc]
- specs.each do |spec|
- Gem::DocManager.new(spec).generate_rdoc
- end
- end
-
- true
- end
+ add_option("--all",
+ "Generate RDoc/RI documentation for all",
+ "installed gems") do |value, options|
+ options[:all] = value
+ end
+
+ add_option("--[no-]rdoc",
+ "Generate RDoc HTML") do |value, options|
+ options[:include_rdoc] = value
+ end
+
+ add_option("--[no-]ri",
+ "Generate RI data") do |value, options|
+ options[:include_ri] = value
+ end
+
+ add_option("--[no-]overwrite",
+ "Overwrite installed documents") do |value, options|
+ options[:overwrite] = value
end
+ add_version_option
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME gem to generate documentation for (unless --all)"
+ end
+
+ def defaults_str # :nodoc:
+ "--version '#{Gem::Requirement.default}' --ri --no-overwrite"
+ end
+
+ def description # :nodoc:
+ <<-DESC
+The rdoc command builds documentation for installed gems. By default
+only documentation is built using rdoc, but additional types of
+documentation may be built through rubygems plugins and the
+Gem.post_installs hook.
+
+Use --overwrite to force rebuilding of documentation.
+ DESC
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [args]"
+ end
+
+ def execute
+ specs = if options[:all]
+ Gem::Specification.to_a
+ else
+ get_all_gem_names.flat_map do |name|
+ Gem::Specification.find_by_name name, options[:version]
+ end.uniq
+ end
+
+ if specs.empty?
+ alert_error "No matching gems found"
+ terminate_interaction 1
+ end
+
+ specs.each do |spec|
+ doc = Gem::RDoc.new spec, options[:include_rdoc], options[:include_ri]
+
+ doc.force = options[:overwrite]
+
+ if options[:overwrite]
+ FileUtils.rm_rf File.join(spec.doc_dir, "ri")
+ FileUtils.rm_rf File.join(spec.doc_dir, "rdoc")
+ end
+
+ doc.generate
+ end
end
end
diff --git a/lib/rubygems/commands/rebuild_command.rb b/lib/rubygems/commands/rebuild_command.rb
new file mode 100644
index 0000000000..23b9d7b3ba
--- /dev/null
+++ b/lib/rubygems/commands/rebuild_command.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: true
+
+require "digest"
+require "fileutils"
+require "tmpdir"
+require_relative "../gemspec_helpers"
+require_relative "../package"
+
+class Gem::Commands::RebuildCommand < Gem::Command
+ include Gem::GemspecHelpers
+
+ def initialize
+ super "rebuild", "Attempt to reproduce a build of a gem."
+
+ add_option "--diff", "If the files don't match, compare them using diffoscope." do |_value, options|
+ options[:diff] = true
+ end
+
+ add_option "--force", "Skip validation of the spec." do |_value, options|
+ options[:force] = true
+ end
+
+ add_option "--strict", "Consider warnings as errors when validating the spec." do |_value, options|
+ options[:strict] = true
+ end
+
+ add_option "--source GEM_SOURCE", "Specify the source to download the gem from." do |value, options|
+ options[:source] = value
+ end
+
+ add_option "--original GEM_FILE", "Specify a local file to compare against (instead of downloading it)." do |value, options|
+ options[:original_gem_file] = value
+ end
+
+ add_option "--gemspec GEMSPEC_FILE", "Specify the name of the gemspec file." do |value, options|
+ options[:gemspec_file] = value
+ end
+
+ add_option "-C PATH", "Run as if gem build was started in <PATH> instead of the current working directory." do |value, options|
+ options[:build_path] = value
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEM_NAME gem name on gem server\n" \
+ "GEM_VERSION gem version you are attempting to rebuild"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The rebuild command allows you to (attempt to) reproduce a build of a gem
+from a ruby gemspec.
+
+This command assumes the gemspec can be built with the `gem build` command.
+If you use any of `gem build`, `rake build`, or`rake release` in the
+build/release process for a gem, it is a potential candidate.
+
+You will need to match the RubyGems version used, since this is included in
+the Gem metadata.
+
+If the gem includes lockfiles (e.g. Gemfile.lock) and similar, it will
+require more effort to reproduce a build. For example, it might require
+more precisely matched versions of Ruby and/or Bundler to be used.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEM_NAME GEM_VERSION"
+ end
+
+ def execute
+ gem_name, gem_version = get_gem_name_and_version
+
+ old_dir, new_dir = prep_dirs
+
+ gem_filename = "#{gem_name}-#{gem_version}.gem"
+ old_file = File.join(old_dir, gem_filename)
+ new_file = File.join(new_dir, gem_filename)
+
+ if options[:original_gem_file]
+ FileUtils.copy_file(options[:original_gem_file], old_file)
+ else
+ download_gem(gem_name, gem_version, old_file)
+ end
+
+ rg_version = rubygems_version(old_file)
+ unless rg_version == Gem::VERSION
+ alert_error <<-EOF
+You need to use the same RubyGems version #{gem_name} v#{gem_version} was built with.
+
+#{gem_name} v#{gem_version} was built using RubyGems v#{rg_version}.
+Gem files include the version of RubyGems used to build them.
+This means in order to reproduce #{gem_filename}, you must also use RubyGems v#{rg_version}.
+
+You're using RubyGems v#{Gem::VERSION}.
+
+Please install RubyGems v#{rg_version} and try again.
+ EOF
+ terminate_interaction 1
+ end
+
+ source_date_epoch = get_timestamp(old_file).to_s
+
+ if build_path = options[:build_path]
+ Dir.chdir(build_path) { build_gem(gem_name, source_date_epoch, new_file) }
+ else
+ build_gem(gem_name, source_date_epoch, new_file)
+ end
+
+ compare(source_date_epoch, old_file, new_file)
+ end
+
+ private
+
+ def sha256(file)
+ Digest::SHA256.hexdigest(Gem.read_binary(file))
+ end
+
+ def get_timestamp(file)
+ mtime = nil
+ File.open(file, Gem.binary_mode) do |f|
+ Gem::Package::TarReader.new(f) do |tar|
+ mtime = tar.seek("metadata.gz") {|tf| tf.header.mtime }
+ end
+ end
+
+ mtime
+ end
+
+ def compare(source_date_epoch, old_file, new_file)
+ date = Time.at(source_date_epoch.to_i).strftime("%F %T %Z")
+
+ old_hash = sha256(old_file)
+ new_hash = sha256(new_file)
+
+ say
+ say "Built at: #{date} (#{source_date_epoch})"
+ say "Original build saved to: #{old_file}"
+ say "Reproduced build saved to: #{new_file}"
+ say "Working directory: #{options[:build_path] || Dir.pwd}"
+ say
+ say "Hash comparison:"
+ say " #{old_hash}\t#{old_file}"
+ say " #{new_hash}\t#{new_file}"
+ say
+
+ if old_hash == new_hash
+ say "SUCCESS - original and rebuild hashes matched"
+ else
+ say "FAILURE - original and rebuild hashes did not match"
+ say
+
+ if options[:diff]
+ if system("diffoscope", old_file, new_file).nil?
+ alert_error "error: could not find `diffoscope` executable"
+ end
+ else
+ say "Pass --diff for more details (requires diffoscope to be installed)."
+ end
+
+ terminate_interaction 1
+ end
+ end
+
+ def prep_dirs
+ rebuild_dir = Dir.mktmpdir("gem_rebuild")
+ old_dir = File.join(rebuild_dir, "old")
+ new_dir = File.join(rebuild_dir, "new")
+
+ FileUtils.mkdir_p(old_dir)
+ FileUtils.mkdir_p(new_dir)
+
+ [old_dir, new_dir]
+ end
+
+ def get_gem_name_and_version
+ args = options[:args] || []
+ if args.length == 2
+ gem_name, gem_version = args
+ elsif args.length > 2
+ raise Gem::CommandLineError, "Too many arguments"
+ else
+ raise Gem::CommandLineError, "Expected GEM_NAME and GEM_VERSION arguments (gem rebuild GEM_NAME GEM_VERSION)"
+ end
+
+ [gem_name, gem_version]
+ end
+
+ def build_gem(gem_name, source_date_epoch, output_file)
+ gemspec = options[:gemspec_file] || find_gemspec("#{gem_name}.gemspec")
+
+ if gemspec
+ build_package(gemspec, source_date_epoch, output_file)
+ else
+ alert_error error_message(gem_name)
+ terminate_interaction(1)
+ end
+ end
+
+ def build_package(gemspec, source_date_epoch, output_file)
+ with_source_date_epoch(source_date_epoch) do
+ spec = Gem::Specification.load(gemspec)
+ if spec
+ Gem::Package.build(
+ spec,
+ options[:force],
+ options[:strict],
+ output_file
+ )
+ else
+ alert_error "Error loading gemspec. Aborting."
+ terminate_interaction 1
+ end
+ end
+ end
+
+ def with_source_date_epoch(source_date_epoch)
+ old_sde = ENV["SOURCE_DATE_EPOCH"]
+ ENV["SOURCE_DATE_EPOCH"] = source_date_epoch.to_s
+
+ yield
+ ensure
+ ENV["SOURCE_DATE_EPOCH"] = old_sde
+ end
+
+ def error_message(gem_name)
+ if gem_name
+ "Couldn't find a gemspec file matching '#{gem_name}' in #{Dir.pwd}"
+ else
+ "Couldn't find a gemspec file in #{Dir.pwd}"
+ end
+ end
+
+ def download_gem(gem_name, gem_version, old_file)
+ # This code was based loosely off the `gem fetch` command.
+ version = "= #{gem_version}"
+ dep = Gem::Dependency.new gem_name, version
+
+ specs_and_sources, errors =
+ Gem::SpecFetcher.fetcher.spec_for_dependency dep
+
+ # There should never be more than one item in specs_and_sources,
+ # since we search for an exact version.
+ spec, source = specs_and_sources[0]
+
+ if spec.nil?
+ show_lookup_failure gem_name, version, errors, options[:domain]
+ terminate_interaction 1
+ end
+
+ download_path = source.download spec
+
+ FileUtils.move(download_path, old_file)
+
+ say "Downloaded #{gem_name} version #{gem_version} as #{old_file}."
+ end
+
+ def rubygems_version(gem_file)
+ Gem::Package.new(gem_file).spec.rubygems_version
+ end
+end
diff --git a/lib/rubygems/commands/search_command.rb b/lib/rubygems/commands/search_command.rb
index 96da19c0f7..50e161ac9b 100644
--- a/lib/rubygems/commands/search_command.rb
+++ b/lib/rubygems/commands/search_command.rb
@@ -1,37 +1,41 @@
-require 'rubygems/command'
-require 'rubygems/commands/query_command'
-
-module Gem
- module Commands
-
- class SearchCommand < QueryCommand
-
- def initialize
- super(
- 'search',
- 'Display all gems whose name contains STRING'
- )
- remove_option('--name-matches')
- end
-
- def arguments # :nodoc:
- "STRING fragment of gem name to search for"
- end
-
- def defaults_str # :nodoc:
- "--local --no-details"
- end
-
- def usage # :nodoc:
- "#{program_name} [STRING]"
- end
-
- def execute
- string = get_one_optional_argument
- options[:name] = /#{string}/i
- super
- end
- end
-
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../query_utils"
+
+class Gem::Commands::SearchCommand < Gem::Command
+ include Gem::QueryUtils
+
+ def initialize
+ super "search", "Display remote gems whose name matches REGEXP",
+ domain: :remote, details: false, versions: true,
+ installed: nil, version: Gem::Requirement.default
+
+ add_query_options
+ end
+
+ def arguments # :nodoc:
+ "REGEXP regexp to search for in gem name"
+ end
+
+ def defaults_str # :nodoc:
+ "--remote --no-details"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The search command displays remote gems whose name matches the given
+regexp.
+
+The --details option displays additional details from the gem but will
+take a little longer to complete as it must download the information
+individually from the index.
+
+To list local gems use the list command.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [REGEXP]"
end
end
diff --git a/lib/rubygems/commands/server_command.rb b/lib/rubygems/commands/server_command.rb
index 992ae1c8f8..f1dde4aa02 100644
--- a/lib/rubygems/commands/server_command.rb
+++ b/lib/rubygems/commands/server_command.rb
@@ -1,48 +1,26 @@
-require 'rubygems/command'
-require 'rubygems/server'
-
-class Gem::Commands::ServerCommand < Gem::Command
-
- def initialize
- super 'server', 'Documentation and gem repository HTTP server',
- :port => 8808, :gemdir => Gem.dir, :daemon => false
-
- add_option '-p', '--port=PORT', Integer,
- 'port to listen on' do |port, options|
- options[:port] = port
+# frozen_string_literal: true
+
+require_relative "../command"
+
+unless defined? Gem::Commands::ServerCommand
+ class Gem::Commands::ServerCommand < Gem::Command
+ def initialize
+ super("server", "Starts up a web server that hosts the RDoc (requires rubygems-server)")
+ begin
+ Gem::Specification.find_by_name("rubygems-server").activate
+ rescue Gem::LoadError
+ # no-op
+ end
end
- add_option '-d', '--dir=GEMDIR',
- 'directory from which to serve gems' do |gemdir, options|
- options[:gemdir] = File.expand_path gemdir
+ def description # :nodoc:
+ <<-EOF
+The server command has been moved to the rubygems-server gem.
+ EOF
end
- add_option '--[no-]daemon', 'run as a daemon' do |daemon, options|
- options[:daemon] = daemon
+ def execute
+ alert_error "Install the rubygems-server gem for the server command"
end
end
-
- def defaults_str # :nodoc:
- "--port 8808 --dir #{Gem.dir} --no-daemon"
- end
-
- def description # :nodoc:
- <<-EOF
-The server command starts up a web server that hosts the RDoc for your
-installed gems and can operate as a server for installation of gems on other
-machines.
-
-The cache files for installed gems must exist to use the server as a source
-for gem installation.
-
-To install gems from a running server, use `gem install GEMNAME --source
-http://gem_server_host:8808`
- EOF
- end
-
- def execute
- Gem::Server.run options
- end
-
end
-
diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb
new file mode 100644
index 0000000000..175599967c
--- /dev/null
+++ b/lib/rubygems/commands/setup_command.rb
@@ -0,0 +1,667 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+##
+# Installs RubyGems itself. This command is ordinarily only available from a
+# RubyGems checkout or tarball.
+
+class Gem::Commands::SetupCommand < Gem::Command
+ HISTORY_HEADER = %r{^##\s*[\d.a-zA-Z]+\s*/\s*\d{4}-\d{2}-\d{2}\s*$}
+ VERSION_MATCHER = %r{^##\s*([\d.a-zA-Z]+)\s*/\s*\d{4}-\d{2}-\d{2}\s*$}
+
+ ENV_PATHS = %w[/usr/bin/env /bin/env].freeze
+
+ def initialize
+ super "setup", "Install RubyGems",
+ format_executable: false, document: %w[ri],
+ force: true,
+ site_or_vendor: "sitelibdir",
+ destdir: "", prefix: "", previous_version: "",
+ regenerate_binstubs: true,
+ regenerate_plugins: true
+
+ add_option "--previous-version=VERSION",
+ "Previous version of RubyGems",
+ "Used for changelog processing" do |version, options|
+ options[:previous_version] = version
+ end
+
+ add_option "--prefix=PREFIX",
+ "Prefix path for installing RubyGems",
+ "Will not affect gem repository location" do |prefix, options|
+ options[:prefix] = File.expand_path prefix
+ end
+
+ add_option "--destdir=DESTDIR",
+ "Root directory to install RubyGems into",
+ "Mainly used for packaging RubyGems" do |destdir, options|
+ options[:destdir] = File.expand_path destdir
+ end
+
+ add_option "--[no-]vendor",
+ "Install into vendorlibdir not sitelibdir" do |vendor, options|
+ options[:site_or_vendor] = vendor ? "vendorlibdir" : "sitelibdir"
+ end
+
+ add_option "--[no-]format-executable",
+ "Makes `gem` match ruby",
+ "If Ruby is ruby18, gem will be gem18" do |value, options|
+ options[:format_executable] = value
+ end
+
+ add_option "--[no-]document [TYPES]", Array,
+ "Generate documentation for RubyGems",
+ "List the documentation types you wish to",
+ "generate. For example: rdoc,ri" do |value, options|
+ options[:document] = case value
+ when nil then %w[rdoc ri]
+ when false then []
+ else value
+ end
+ end
+
+ add_option "--[no-]rdoc",
+ "Generate RDoc documentation for RubyGems" do |value, options|
+ if value
+ options[:document] << "rdoc"
+ else
+ options[:document].delete "rdoc"
+ end
+
+ options[:document].uniq!
+ end
+
+ add_option "--[no-]ri",
+ "Generate RI documentation for RubyGems" do |value, options|
+ if value
+ options[:document] << "ri"
+ else
+ options[:document].delete "ri"
+ end
+
+ options[:document].uniq!
+ end
+
+ add_option "--[no-]regenerate-binstubs",
+ "Regenerate gem binstubs" do |value, options|
+ options[:regenerate_binstubs] = value
+ end
+
+ add_option "--[no-]regenerate-plugins",
+ "Regenerate gem plugins" do |value, options|
+ options[:regenerate_plugins] = value
+ end
+
+ add_option "-f", "--[no-]force",
+ "Forcefully overwrite binstubs" do |value, options|
+ options[:force] = value
+ end
+
+ add_option("-E", "--[no-]env-shebang",
+ "Rewrite executables with a shebang",
+ "of /usr/bin/env") do |value, options|
+ options[:env_shebang] = value
+ end
+
+ @verbose = nil
+ end
+
+ def defaults_str # :nodoc:
+ "--format-executable --document ri --regenerate-binstubs"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+Installs RubyGems itself.
+
+RubyGems installs RDoc for itself in GEM_HOME. By default this is:
+ #{Gem.dir}
+
+If you prefer a different directory, set the GEM_HOME environment variable.
+
+RubyGems will install the gem command with a name matching ruby's
+prefix and suffix. If ruby was installed as `ruby18`, gem will be
+installed as `gem18`.
+
+By default, this RubyGems will install gem as:
+ #{Gem.default_exec_format % "gem"}
+ EOF
+ end
+
+ module MakeDirs
+ def mkdir_p(path, **opts)
+ super
+ (@mkdirs ||= []) << path
+ end
+ end
+
+ def execute
+ @verbose = Gem.configuration.really_verbose
+
+ require "fileutils"
+ if Gem.configuration.really_verbose
+ extend FileUtils::Verbose
+ else
+ extend FileUtils
+ end
+ extend MakeDirs
+
+ lib_dir, bin_dir = make_destination_dirs
+ man_dir = generate_default_man_dir
+
+ install_lib lib_dir
+
+ install_executables bin_dir
+
+ remove_old_bin_files bin_dir
+
+ remove_old_lib_files lib_dir
+
+ # Can be removed one we drop support for bundler 2.2.3 (the last version installing man files to man_dir)
+ remove_old_man_files man_dir if man_dir && File.exist?(man_dir)
+
+ install_default_bundler_gem bin_dir
+
+ if mode = options[:dir_mode]
+ @mkdirs.uniq!
+ File.chmod(mode, @mkdirs)
+ end
+
+ say "RubyGems #{Gem::VERSION} installed"
+
+ regenerate_binstubs(bin_dir) if options[:regenerate_binstubs]
+ regenerate_plugins(bin_dir) if options[:regenerate_plugins]
+
+ uninstall_old_gemcutter
+
+ documentation_success = install_rdoc
+
+ say
+ if @verbose
+ say "-" * 78
+ say
+ end
+
+ if options[:previous_version].empty?
+ options[:previous_version] = Gem::VERSION.sub(/[0-9]+$/, "0")
+ end
+
+ options[:previous_version] = Gem::Version.new(options[:previous_version])
+
+ show_release_notes
+
+ say
+ say "-" * 78
+ say
+
+ say "RubyGems installed the following executables:"
+ say bin_file_names.map {|name| "\t#{name}\n" }
+ say
+
+ unless bin_file_names.grep(/#{File::SEPARATOR}gem$/)
+ say "If `gem` was installed by a previous RubyGems installation, you may need"
+ say "to remove it by hand."
+ say
+ end
+
+ if documentation_success
+ if options[:document].include? "rdoc"
+ say "Rdoc documentation was installed. You may now invoke:"
+ say " gem server"
+ say "and then peruse beautifully formatted documentation for your gems"
+ say "with your web browser."
+ say "If you do not wish to install this documentation in the future, use the"
+ say "--no-document flag, or set it as the default in your ~/.gemrc file. See"
+ say "'gem help env' for details."
+ say
+ end
+
+ if options[:document].include? "ri"
+ say "Ruby Interactive (ri) documentation was installed. ri is kind of like man "
+ say "pages for Ruby libraries. You may access it like this:"
+ say " ri Classname"
+ say " ri Classname.class_method"
+ say " ri Classname#instance_method"
+ say "If you do not wish to install this documentation in the future, use the"
+ say "--no-document flag, or set it as the default in your ~/.gemrc file. See"
+ say "'gem help env' for details."
+ say
+ end
+ end
+ end
+
+ def install_executables(bin_dir)
+ prog_mode = options[:prog_mode] || 0o755
+
+ executables = { "gem" => "exe" }
+ executables.each do |tool, path|
+ say "Installing #{tool} executable" if @verbose
+
+ Dir.chdir path do
+ bin_file = "gem"
+
+ require "tmpdir"
+
+ dest_file = target_bin_path(bin_dir, bin_file)
+ bin_tmp_file = File.join Dir.tmpdir, "#{bin_file}.#{$$}"
+
+ begin
+ bin = File.readlines bin_file
+ bin[0] = shebang
+
+ File.open bin_tmp_file, "w" do |fp|
+ fp.puts bin.join
+ end
+
+ install bin_tmp_file, dest_file, mode: prog_mode
+ bin_file_names << dest_file
+ ensure
+ rm bin_tmp_file
+ end
+
+ next unless Gem.win_platform?
+
+ begin
+ bin_cmd_file = File.join Dir.tmpdir, "#{bin_file}.bat"
+
+ File.open bin_cmd_file, "w" do |file|
+ file.puts <<-TEXT
+ @ECHO OFF
+ @"%~dp0#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %*
+ TEXT
+ end
+
+ install bin_cmd_file, "#{dest_file}.bat", mode: prog_mode
+ ensure
+ rm bin_cmd_file
+ end
+ end
+ end
+ end
+
+ def shebang
+ if options[:env_shebang]
+ ruby_name = RbConfig::CONFIG["ruby_install_name"]
+ @env_path ||= ENV_PATHS.find {|env_path| File.executable? env_path }
+ "#!#{@env_path} #{ruby_name}\n"
+ else
+ "#!#{Gem.ruby}\n"
+ end
+ end
+
+ def install_lib(lib_dir)
+ libs = { "RubyGems" => "lib" }
+ libs["Bundler"] = "bundler/lib"
+ libs.each do |tool, path|
+ say "Installing #{tool}" if @verbose
+
+ lib_files = files_in path
+
+ Dir.chdir path do
+ install_file_list(lib_files, lib_dir)
+ end
+ end
+ end
+
+ def install_rdoc
+ gem_doc_dir = File.join Gem.dir, "doc"
+ rubygems_name = "rubygems-#{Gem::VERSION}"
+ rubygems_doc_dir = File.join gem_doc_dir, rubygems_name
+
+ begin
+ Gem.ensure_gem_subdirectories Gem.dir
+ rescue SystemCallError
+ # ignore
+ end
+
+ if File.writable?(gem_doc_dir) &&
+ (!File.exist?(rubygems_doc_dir) ||
+ File.writable?(rubygems_doc_dir))
+ say "Removing old RubyGems RDoc and ri" if @verbose
+ Dir[File.join(Gem.dir, "doc", "rubygems-[0-9]*")].each do |dir|
+ rm_rf dir
+ end
+
+ require_relative "../rdoc"
+
+ return false unless defined?(Gem::RDoc)
+
+ fake_spec = Gem::Specification.new "rubygems", Gem::VERSION
+ def fake_spec.full_gem_path
+ File.expand_path "../../..", __dir__
+ end
+
+ generate_ri = options[:document].include? "ri"
+ generate_rdoc = options[:document].include? "rdoc"
+
+ rdoc = Gem::RDoc.new fake_spec, generate_rdoc, generate_ri
+ rdoc.generate
+
+ return true
+ elsif @verbose
+ say "Skipping RDoc generation, #{gem_doc_dir} not writable"
+ say "Set the GEM_HOME environment variable if you want RDoc generated"
+ end
+
+ false
+ end
+
+ def install_default_bundler_gem(bin_dir)
+ current_default_spec = Gem::Specification.default_stubs.find {|s| s.name == "bundler" }
+ specs_dir = if current_default_spec && default_dir == Gem.default_dir
+ all_specs_current_version = Gem::Specification.stubs.select {|s| s.full_name == current_default_spec.full_name }
+
+ Gem::Specification.remove_spec current_default_spec
+ loaded_from = current_default_spec.loaded_from
+ File.delete(loaded_from)
+
+ # Remove previous default gem executables if they were not shadowed by a regular gem
+ FileUtils.rm_rf current_default_spec.full_gem_path if all_specs_current_version.size == 1
+
+ File.dirname(loaded_from)
+ else
+ target_specs_dir = File.join(default_dir, "specifications", "default")
+ mkdir_p target_specs_dir, mode: 0o755
+ target_specs_dir
+ end
+
+ new_bundler_spec = Dir.chdir("bundler") { Gem::Specification.load("bundler.gemspec") }
+ full_name = new_bundler_spec.full_name
+ gemspec_path = "#{full_name}.gemspec"
+
+ default_spec_path = File.join(specs_dir, gemspec_path)
+ Gem.write_binary(default_spec_path, new_bundler_spec.to_ruby)
+
+ bundler_spec = Gem::Specification.load(default_spec_path)
+
+ # Remove gemspec that was same version of vendored bundler.
+ normal_gemspec = File.join(default_dir, "specifications", gemspec_path)
+ if File.file? normal_gemspec
+ File.delete normal_gemspec
+ end
+
+ # Remove gem files that were same version of vendored bundler.
+ if File.directory? bundler_spec.gems_dir
+ Dir.entries(bundler_spec.gems_dir).
+ select {|default_gem| File.basename(default_gem) == full_name }.
+ each {|default_gem| rm_r File.join(bundler_spec.gems_dir, default_gem) }
+ end
+
+ require_relative "../installer"
+
+ Dir.chdir("bundler") do
+ built_gem = Gem::Package.build(new_bundler_spec)
+ begin
+ installer = Gem::Installer.at(
+ built_gem,
+ env_shebang: options[:env_shebang],
+ format_executable: options[:format_executable],
+ force: options[:force],
+ bin_dir: bin_dir,
+ install_dir: default_dir,
+ wrappers: true
+ )
+ # We need to install only executable and default spec files.
+ # lib/bundler.rb and lib/bundler/* are available under the site_ruby directory.
+ installer.extract_bin
+ installer.generate_bin
+ installer.write_default_spec
+ ensure
+ FileUtils.rm_f built_gem
+ end
+ end
+
+ new_bundler_spec.executables.each {|executable| bin_file_names << target_bin_path(bin_dir, executable) }
+
+ say "Bundler #{new_bundler_spec.version} installed"
+ end
+
+ def make_destination_dirs
+ lib_dir, bin_dir = Gem.default_rubygems_dirs
+
+ unless lib_dir
+ lib_dir, bin_dir = generate_default_dirs
+ end
+
+ mkdir_p lib_dir, mode: 0o755
+ mkdir_p bin_dir, mode: 0o755
+
+ [lib_dir, bin_dir]
+ end
+
+ def generate_default_man_dir
+ prefix = options[:prefix]
+
+ if prefix.empty?
+ man_dir = RbConfig::CONFIG["mandir"]
+ return unless man_dir
+ else
+ man_dir = File.join prefix, "man"
+ end
+
+ prepend_destdir_if_present(man_dir)
+ end
+
+ def generate_default_dirs
+ prefix = options[:prefix]
+ site_or_vendor = options[:site_or_vendor]
+
+ if prefix.empty?
+ lib_dir = RbConfig::CONFIG[site_or_vendor]
+ bin_dir = RbConfig::CONFIG["bindir"]
+ else
+ lib_dir = File.join prefix, "lib"
+ bin_dir = File.join prefix, "bin"
+ end
+
+ [prepend_destdir_if_present(lib_dir), prepend_destdir_if_present(bin_dir)]
+ end
+
+ def files_in(dir)
+ Dir.chdir dir do
+ Dir.glob(File.join("**", "*"), File::FNM_DOTMATCH).
+ select {|f| !File.directory?(f) }
+ end
+ end
+
+ def remove_old_bin_files(bin_dir)
+ old_bin_files = {
+ "gem_mirror" => "gem mirror",
+ "gem_server" => "gem server",
+ "gemlock" => "gem lock",
+ "gemri" => "ri",
+ "gemwhich" => "gem which",
+ "index_gem_repository.rb" => "gem generate_index",
+ }
+
+ old_bin_files.each do |old_bin_file, new_name|
+ old_bin_path = File.join bin_dir, old_bin_file
+ next unless File.exist? old_bin_path
+
+ deprecation_message = "`#{old_bin_file}` has been deprecated. Use `#{new_name}` instead."
+
+ File.open old_bin_path, "w" do |fp|
+ fp.write <<-EOF
+#!#{Gem.ruby}
+
+abort "#{deprecation_message}"
+ EOF
+ end
+
+ next unless Gem.win_platform?
+
+ File.open "#{old_bin_path}.bat", "w" do |fp|
+ fp.puts %(@ECHO.#{deprecation_message})
+ end
+ end
+ end
+
+ def remove_old_lib_files(lib_dir)
+ lib_dirs = { File.join(lib_dir, "rubygems") => "lib/rubygems" }
+ lib_dirs[File.join(lib_dir, "bundler")] = "bundler/lib/bundler"
+ lib_dirs.each do |old_lib_dir, new_lib_dir|
+ lib_files = files_in(new_lib_dir)
+
+ old_lib_files = files_in(old_lib_dir)
+
+ to_remove = old_lib_files - lib_files
+
+ gauntlet_rubygems = File.join(lib_dir, "gauntlet_rubygems.rb")
+ to_remove << gauntlet_rubygems if File.exist? gauntlet_rubygems
+
+ to_remove.delete_if do |file|
+ file.start_with? "defaults"
+ end
+
+ remove_file_list(to_remove, old_lib_dir)
+ end
+ end
+
+ def remove_old_man_files(old_man_dir)
+ old_man1_dir = "#{old_man_dir}/man1"
+
+ if File.exist?(old_man1_dir)
+ man1_to_remove = Dir.chdir(old_man1_dir) { Dir["bundle*.1{,.txt,.ronn}"] }
+
+ remove_file_list(man1_to_remove, old_man1_dir)
+ end
+
+ old_man5_dir = "#{old_man_dir}/man5"
+
+ if File.exist?(old_man5_dir)
+ man5_to_remove = Dir.chdir(old_man5_dir) { Dir["gemfile.5{,.txt,.ronn}"] }
+
+ remove_file_list(man5_to_remove, old_man5_dir)
+ end
+ end
+
+ def show_release_notes
+ release_notes = File.join Dir.pwd, "CHANGELOG.md"
+
+ release_notes =
+ if File.exist? release_notes
+ history = File.read release_notes
+
+ history.force_encoding Encoding::UTF_8
+
+ text = history.split(HISTORY_HEADER)
+ text.shift # correct an off-by-one generated by split
+ version_lines = history.scan(HISTORY_HEADER)
+ versions = history.scan(VERSION_MATCHER).flatten.map do |x|
+ Gem::Version.new(x)
+ end
+
+ history_string = ""
+
+ until versions.length == 0 ||
+ versions.shift <= options[:previous_version] do
+ history_string += version_lines.shift + text.shift
+ end
+
+ history_string
+ else
+ "Oh-no! Unable to find release notes!"
+ end
+
+ say release_notes
+ end
+
+ def uninstall_old_gemcutter
+ require_relative "../uninstaller"
+
+ ui = Gem::Uninstaller.new("gemcutter", all: true, ignore: true,
+ version: "< 0.4")
+ ui.uninstall
+ rescue Gem::InstallError
+ end
+
+ def regenerate_binstubs(bindir)
+ require_relative "pristine_command"
+ say "Regenerating binstubs"
+
+ args = %w[--all --only-executables --silent]
+ args << "--bindir=#{bindir}"
+ args << "--install-dir=#{default_dir}"
+
+ if options[:env_shebang]
+ args << "--env-shebang"
+ end
+
+ command = Gem::Commands::PristineCommand.new
+ command.invoke(*args)
+ end
+
+ def regenerate_plugins(bindir)
+ require_relative "pristine_command"
+ say "Regenerating plugins"
+
+ args = %w[--all --only-plugins --silent]
+ args << "--bindir=#{bindir}"
+ args << "--install-dir=#{default_dir}"
+
+ command = Gem::Commands::PristineCommand.new
+ command.invoke(*args)
+ end
+
+ private
+
+ def default_dir
+ prefix = options[:prefix]
+
+ if prefix.empty?
+ dir = Gem.default_dir
+ else
+ dir = prefix
+ end
+
+ prepend_destdir_if_present(dir)
+ end
+
+ def prepend_destdir_if_present(path)
+ destdir = options[:destdir]
+ return path if destdir.empty?
+
+ File.join(options[:destdir], path.gsub(/^[a-zA-Z]:/, ""))
+ end
+
+ def install_file_list(files, dest_dir)
+ files.each do |file|
+ install_file file, dest_dir
+ end
+ end
+
+ def install_file(file, dest_dir)
+ dest_file = File.join dest_dir, file
+ dest_dir = File.dirname dest_file
+ unless File.directory? dest_dir
+ mkdir_p dest_dir, mode: 0o755
+ end
+
+ install file, dest_file, mode: options[:data_mode] || 0o644
+ end
+
+ def remove_file_list(files, dir)
+ Dir.chdir dir do
+ files.each do |file|
+ FileUtils.rm_f file
+
+ warn "unable to remove old file #{file} please remove it by hand" if
+ File.exist? file
+ end
+ end
+ end
+
+ def target_bin_path(bin_dir, bin_file)
+ bin_file_formatted = if options[:format_executable]
+ Gem.default_exec_format % bin_file
+ else
+ bin_file
+ end
+ File.join bin_dir, bin_file_formatted
+ end
+
+ def bin_file_names
+ @bin_file_names ||= []
+ end
+end
diff --git a/lib/rubygems/commands/signin_command.rb b/lib/rubygems/commands/signin_command.rb
new file mode 100644
index 0000000000..0f77908c5b
--- /dev/null
+++ b/lib/rubygems/commands/signin_command.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../gemcutter_utilities"
+
+class Gem::Commands::SigninCommand < Gem::Command
+ include Gem::GemcutterUtilities
+
+ def initialize
+ super "signin", "Sign in to any gemcutter-compatible host. "\
+ "It defaults to https://rubygems.org"
+
+ add_option("--host HOST", "Push to another gemcutter-compatible host") do |value, options|
+ options[:host] = value
+ end
+
+ add_otp_option
+ end
+
+ def description # :nodoc:
+ "The signin command executes host sign in for a push server (the default is"\
+ " https://rubygems.org). The host can be provided with the host flag or can"\
+ " be inferred from the provided gem. Host resolution matches the resolution"\
+ " strategy for the push command."
+ end
+
+ def usage # :nodoc:
+ program_name
+ end
+
+ def execute
+ sign_in options[:host]
+ end
+end
diff --git a/lib/rubygems/commands/signout_command.rb b/lib/rubygems/commands/signout_command.rb
new file mode 100644
index 0000000000..bdd01e4393
--- /dev/null
+++ b/lib/rubygems/commands/signout_command.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+class Gem::Commands::SignoutCommand < Gem::Command
+ def initialize
+ super "signout", "Sign out from all the current sessions."
+ end
+
+ def description # :nodoc:
+ "The `signout` command is used to sign out from all current sessions,"\
+ " allowing you to sign in using a different set of credentials."
+ end
+
+ def usage # :nodoc:
+ program_name
+ end
+
+ def execute
+ credentials_path = Gem.configuration.credentials_path
+
+ if !File.exist?(credentials_path)
+ alert_error "You are not currently signed in."
+ elsif !File.writable?(credentials_path)
+ alert_error "File '#{Gem.configuration.credentials_path}' is read-only."\
+ " Please make sure it is writable."
+ else
+ Gem.configuration.unset_api_key!
+ say "You have successfully signed out from all sessions."
+ end
+ end
+end
diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb
index 9aabb77cb1..b399af2bd3 100644
--- a/lib/rubygems/commands/sources_command.rb
+++ b/lib/rubygems/commands/sources_command.rb
@@ -1,152 +1,348 @@
-require 'fileutils'
-require 'rubygems/command'
-require 'rubygems/remote_fetcher'
-require 'rubygems/source_info_cache'
-require 'rubygems/spec_fetcher'
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../remote_fetcher"
+require_relative "../spec_fetcher"
+require_relative "../local_remote_options"
class Gem::Commands::SourcesCommand < Gem::Command
+ include Gem::LocalRemoteOptions
def initialize
- super 'sources',
- 'Manage the sources and cache file RubyGems uses to search for gems'
+ require "fileutils"
+
+ super "sources",
+ "Manage the sources and cache file RubyGems uses to search for gems"
- add_option '-a', '--add SOURCE_URI', 'Add source' do |value, options|
+ add_option "-a", "--add SOURCE_URI", "Add source" do |value, options|
options[:add] = value
end
- add_option '-l', '--list', 'List sources' do |value, options|
+ add_option "--append SOURCE_URI", "Append source (can be used multiple times)" do |value, options|
+ options[:append] = value
+ end
+
+ add_option "-p", "--prepend SOURCE_URI", "Prepend source (can be used multiple times)" do |value, options|
+ options[:prepend] = value
+ end
+
+ add_option "-l", "--list", "List sources" do |value, options|
options[:list] = value
end
- add_option '-r', '--remove SOURCE_URI', 'Remove source' do |value, options|
+ add_option "-r", "--remove SOURCE_URI", "Remove source" do |value, options|
options[:remove] = value
end
- add_option '-c', '--clear-all',
- 'Remove all sources (clear the cache)' do |value, options|
+ add_option "-c", "--clear-all", "Remove all sources (clear the cache)" do |value, options|
options[:clear_all] = value
end
- add_option '-u', '--update', 'Update source cache' do |value, options|
+ add_option "-u", "--update", "Update source cache" do |value, options|
options[:update] = value
end
- end
- def defaults_str
- '--list'
+ add_option "-f", "--[no-]force", "Do not show any confirmation prompts and behave as if 'yes' was always answered" do |value, options|
+ options[:force] = value
+ end
+
+ add_proxy_option
end
- def execute
- options[:list] = !(options[:add] ||
- options[:clear_all] ||
- options[:remove] ||
- options[:update])
-
- if options[:clear_all] then
- path = Gem::SpecFetcher.fetcher.dir
- FileUtils.rm_rf path
-
- if not File.exist?(path) then
- say "*** Removed specs cache ***"
- elsif not File.writable?(path) then
- say "*** Unable to remove source cache (write protected) ***"
+ def add_source(source_uri) # :nodoc:
+ source = build_new_source(source_uri)
+ source_uri = source.uri.to_s
+
+ begin
+ if Gem.sources.include? source
+ say "source #{source_uri} already present in the cache"
else
- say "*** Unable to remove source cache ***"
- end
+ source.load_specs :released
+ Gem.sources << source
+ Gem.configuration.write
- sic = Gem::SourceInfoCache
- remove_cache_file 'user', sic.user_cache_file
- remove_cache_file 'latest user', sic.latest_user_cache_file
- remove_cache_file 'system', sic.system_cache_file
- remove_cache_file 'latest system', sic.latest_system_cache_file
+ say "#{source_uri} added to sources"
+ end
+ rescue Gem::URI::Error, ArgumentError
+ say "#{source_uri} is not a URI"
+ terminate_interaction 1
+ rescue Gem::RemoteFetcher::FetchError => e
+ say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}"
+ terminate_interaction 1
end
+ end
- if options[:add] then
- source_uri = options[:add]
- uri = URI.parse source_uri
+ def append_source(source_uri) # :nodoc:
+ source = build_new_source(source_uri)
+ source_uri = source.uri.to_s
- begin
- Gem::SpecFetcher.fetcher.load_specs uri, 'specs'
- Gem.sources << source_uri
- Gem.configuration.write
+ begin
+ source.load_specs :released
+ was_present = Gem.sources.include?(source)
+ Gem.sources.append source
+ Gem.configuration.write
+ if was_present
+ say "#{source_uri} moved to end of sources"
+ else
say "#{source_uri} added to sources"
- rescue URI::Error, ArgumentError
- say "#{source_uri} is not a URI"
- rescue Gem::RemoteFetcher::FetchError => e
- yaml_uri = uri + 'yaml'
- gem_repo = Gem::RemoteFetcher.fetcher.fetch_size yaml_uri rescue false
-
- if e.uri =~ /specs\.#{Regexp.escape Gem.marshal_version}\.gz$/ and
- gem_repo then
-
- alert_warning <<-EOF
-RubyGems 1.2+ index not found for:
-\t#{source_uri}
-
-Will cause RubyGems to revert to legacy indexes, degrading performance.
- EOF
-
- say "#{source_uri} added to sources"
- else
- say "Error fetching #{source_uri}:\n\t#{e.message}"
- end
end
+ rescue Gem::URI::Error, ArgumentError
+ say "#{source_uri} is not a URI"
+ terminate_interaction 1
+ rescue Gem::RemoteFetcher::FetchError => e
+ say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}"
+ terminate_interaction 1
end
+ end
- if options[:remove] then
- source_uri = options[:remove]
+ def prepend_source(source_uri) # :nodoc:
+ source = build_new_source(source_uri)
+ source_uri = source.uri.to_s
- unless Gem.sources.include? source_uri then
- say "source #{source_uri} not present in cache"
- else
- Gem.sources.delete source_uri
- Gem.configuration.write
+ begin
+ source.load_specs :released
+ was_present = Gem.sources.include?(source)
+ Gem.sources.prepend source
+ Gem.configuration.write
- say "#{source_uri} removed from sources"
+ if was_present
+ say "#{source_uri} moved to top of sources"
+ else
+ say "#{source_uri} added to sources"
end
+ rescue Gem::URI::Error, ArgumentError
+ say "#{source_uri} is not a URI"
+ terminate_interaction 1
+ rescue Gem::RemoteFetcher::FetchError => e
+ say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}"
+ terminate_interaction 1
end
+ end
+
+ def check_typo_squatting(source)
+ if source.typo_squatting?("rubygems.org")
+ question = <<-QUESTION.chomp
+#{source.uri} is too similar to https://rubygems.org
+
+Do you want to add this source?
+ QUESTION
+
+ terminate_interaction 1 unless options[:force] || ask_yes_no(question)
+ end
+ end
+
+ def normalize_source_uri(source_uri) # :nodoc:
+ # Ensure the source URI has a trailing slash for proper RFC 2396 path merging
+ # Without a trailing slash, the last path segment is treated as a file and removed
+ # during relative path resolution (e.g., "/blish" + "gems/foo.gem" = "/gems/foo.gem")
+ # With a trailing slash, it's treated as a directory (e.g., "/blish/" + "gems/foo.gem" = "/blish/gems/foo.gem")
+ uri = Gem::URI.parse(source_uri)
+ uri.path = uri.path.gsub(%r{/+\z}, "") + "/" if uri.path && !uri.path.empty?
+ uri.to_s
+ rescue Gem::URI::Error
+ # If parsing fails, return the original URI and let later validation handle it
+ source_uri
+ end
+
+ def check_rubygems_https(source_uri) # :nodoc:
+ uri = Gem::URI source_uri
+
+ if uri.scheme && uri.scheme.casecmp("http").zero? &&
+ uri.host.casecmp("rubygems.org").zero?
+ question = <<-QUESTION.chomp
+https://rubygems.org is recommended for security over #{uri}
+
+Do you want to add this insecure source?
+ QUESTION
+
+ terminate_interaction 1 unless options[:force] || ask_yes_no(question)
+ end
+ end
- if options[:update] then
- fetcher = Gem::SpecFetcher.fetcher
+ def clear_all # :nodoc:
+ path = Gem.spec_cache_dir
+ FileUtils.rm_rf path
- if fetcher.legacy_repos.empty? then
- Gem.sources.each do |update_uri|
- update_uri = URI.parse update_uri
- fetcher.load_specs update_uri, 'specs'
- fetcher.load_specs update_uri, 'latest_specs'
- end
+ if File.exist? path
+ if File.writable? path
+ say "*** Unable to remove source cache ***"
else
- Gem::SourceInfoCache.cache true
- Gem::SourceInfoCache.cache.flush
+ say "*** Unable to remove source cache (write protected) ***"
end
- say "source cache successfully updated"
+ terminate_interaction 1
+ else
+ say "*** Removed specs cache ***"
+ end
+ end
+
+ def defaults_str # :nodoc:
+ "--list"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+RubyGems fetches gems from the sources you have configured (stored in your
+~/.gemrc).
+
+The default source is https://rubygems.org, but you may have other sources
+configured. This guide will help you update your sources or configure
+yourself to use your own gem server.
+
+Without any arguments the sources lists your currently configured sources:
+
+ $ gem sources
+ *** NO CONFIGURED SOURCES, DEFAULT SOURCES LISTED BELOW ***
+
+ https://rubygems.org
+
+This may list multiple sources or non-rubygems sources. You probably
+configured them before or have an old `~/.gemrc`. If you have sources you
+do not recognize you should remove them.
+
+RubyGems has been configured to serve gems via the following URLs through
+its history:
+
+* http://gems.rubyforge.org (RubyGems 1.3.5 and earlier)
+* http://rubygems.org (RubyGems 1.3.6 through 1.8.30, and 2.0.0)
+* https://rubygems.org (RubyGems 2.0.1 and newer)
+
+Since all of these sources point to the same set of gems you only need one
+of them in your list. https://rubygems.org is recommended as it brings the
+protections of an SSL connection to gem downloads.
+
+To add a private gem source use the --prepend argument to insert it before
+the default source. This is usually the best place for private gem sources:
+
+ $ gem sources --prepend https://my.private.source
+ https://my.private.source added to sources
+
+RubyGems will check to see if gems can be installed from the source given
+before it is added.
+
+To add or move a source after all other sources, use --append:
+
+ $ gem sources --append https://rubygems.org
+ https://rubygems.org moved to end of sources
+
+To remove a source use the --remove argument:
+
+ $ gem sources --remove https://my.private.source/
+ https://my.private.source/ removed from sources
+
+ EOF
+ end
+
+ def list # :nodoc:
+ if configured_sources
+ header = "*** CURRENT SOURCES ***"
+ list = configured_sources
+ else
+ header = "*** NO CONFIGURED SOURCES, DEFAULT SOURCES LISTED BELOW ***"
+ list = Gem.sources
end
- if options[:list] then
- say "*** CURRENT SOURCES ***"
- say
+ say header
+ say
+
+ list.each do |src|
+ say src
+ end
+ end
+
+ def list? # :nodoc:
+ !(options[:add] ||
+ options[:prepend] ||
+ options[:append] ||
+ options[:clear_all] ||
+ options[:remove] ||
+ options[:update])
+ end
+
+ def execute
+ clear_all if options[:clear_all]
+
+ add_source options[:add] if options[:add]
+
+ prepend_source options[:prepend] if options[:prepend]
+
+ append_source options[:append] if options[:append]
- Gem.sources.each do |source|
- say source
+ remove_source options[:remove] if options[:remove]
+
+ update if options[:update]
+
+ list if list?
+ end
+
+ def remove_source(source_uri) # :nodoc:
+ source = build_source(source_uri)
+ source_uri = source.uri.to_s
+
+ if configured_sources&.include? source
+ Gem.sources.delete source
+ Gem.configuration.write
+
+ if default_sources.include?(source) && configured_sources.one?
+ alert_warning "Removing a default source when it is the only source has no effect. Add a different source to #{config_file_name} if you want to stop using it as a source."
+ else
+ say "#{source_uri} removed from sources"
end
+ elsif configured_sources
+ say "source #{source_uri} cannot be removed because it's not present in #{config_file_name}"
+ else
+ say "source #{source_uri} cannot be removed because there are no configured sources in #{config_file_name}"
end
end
- private
+ def update # :nodoc:
+ Gem.sources.each_source do |src|
+ src.load_specs :released
+ src.load_specs :latest
+ end
+
+ say "source cache successfully updated"
+ end
- def remove_cache_file(desc, path)
+ def remove_cache_file(desc, path) # :nodoc:
FileUtils.rm_rf path
- if not File.exist?(path) then
+ if !File.exist?(path)
say "*** Removed #{desc} source cache ***"
- elsif not File.writable?(path) then
+ elsif !File.writable?(path)
say "*** Unable to remove #{desc} source cache (write protected) ***"
else
say "*** Unable to remove #{desc} source cache ***"
end
end
-end
+ private
+
+ def default_sources
+ Gem::SourceList.from(Gem.default_sources)
+ end
+
+ def configured_sources
+ return @configured_sources if defined?(@configured_sources)
+
+ configuration_sources = Gem.configuration.sources
+ @configured_sources = Gem::SourceList.from(configuration_sources) if configuration_sources
+ end
+ def config_file_name
+ Gem.configuration.config_file_name
+ end
+
+ def build_source(source_uri)
+ source_uri = normalize_source_uri(source_uri)
+ Gem::Source.new(source_uri)
+ end
+
+ def build_new_source(source_uri)
+ source = build_source(source_uri)
+ check_rubygems_https(source.uri.to_s)
+ check_typo_squatting(source)
+ source
+ end
+end
diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb
index 5aaf6d1797..15e543f1a6 100644
--- a/lib/rubygems/commands/specification_command.rb
+++ b/lib/rubygems/commands/specification_command.rb
@@ -1,77 +1,156 @@
-require 'yaml'
-require 'rubygems/command'
-require 'rubygems/local_remote_options'
-require 'rubygems/version_option'
-require 'rubygems/source_info_cache'
-require 'rubygems/format'
+# frozen_string_literal: true
-class Gem::Commands::SpecificationCommand < Gem::Command
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+require_relative "../package"
+class Gem::Commands::SpecificationCommand < Gem::Command
include Gem::LocalRemoteOptions
include Gem::VersionOption
def initialize
- super 'specification', 'Display gem specification (in yaml)',
- :domain => :local, :version => Gem::Requirement.default
+ Gem.load_yaml
+
+ super "specification", "Display gem specification (in yaml)",
+ domain: :local, version: Gem::Requirement.default,
+ format: :yaml
- add_version_option('examine')
+ add_version_option("examine")
add_platform_option
+ add_prerelease_option
- add_option('--all', 'Output specifications for all versions of',
- 'the gem') do |value, options|
+ add_option("--all", "Output specifications for all versions of",
+ "the gem") do |_value, options|
options[:all] = true
end
+ add_option("--ruby", "Output ruby format") do |_value, options|
+ options[:format] = :ruby
+ end
+
+ add_option("--yaml", "Output YAML format") do |_value, options|
+ options[:format] = :yaml
+ end
+
+ add_option("--marshal", "Output Marshal format") do |_value, options|
+ options[:format] = :marshal
+ end
+
add_local_remote_options
end
def arguments # :nodoc:
- "GEMFILE name of gem to show the gemspec for"
+ <<-ARGS
+GEM_OR_FILE gem name or a .gem file to show the gemspec for
+FIELD name of gemspec field to show
+ ARGS
end
def defaults_str # :nodoc:
- "--local --version '#{Gem::Requirement.default}'"
+ "--local --version '#{Gem::Requirement.default}' --yaml"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The specification command allows you to extract the specification from
+a gem for examination.
+
+The specification can be output in YAML, ruby or Marshal formats.
+
+Specific fields in the specification can be extracted in YAML format:
+
+ $ gem spec rake summary
+ --- Ruby based make-like utility.
+ ...
+
+ EOF
end
def usage # :nodoc:
- "#{program_name} [GEMFILE]"
+ "#{program_name} [GEM_OR_FILE] [FIELD]"
end
def execute
specs = []
- gem = get_one_gem_name
- dep = Gem::Dependency.new gem, options[:version]
+ gem = options[:args].shift
+
+ unless gem
+ raise Gem::CommandLineError,
+ "Please specify a gem name or a .gem file on the command line"
+ end
+
+ case v = options[:version]
+ when String
+ req = Gem::Requirement.create v
+ when Gem::Requirement
+ req = v
+ else
+ raise Gem::CommandLineError, "Unsupported version type: '#{v}'"
+ end
+
+ if !req.none? && options[:all]
+ alert_error "Specify --all or -v, not both"
+ terminate_interaction 1
+ end
+
+ if options[:all]
+ dep = Gem::Dependency.new gem
+ else
+ dep = Gem::Dependency.new gem, req
+ end
+
+ field = get_one_optional_argument
- if local? then
- if File.exist? gem then
- specs << Gem::Format.from_file_by_path(gem).spec rescue nil
+ raise Gem::CommandLineError, "--ruby and FIELD are mutually exclusive" if
+ field && options[:format] == :ruby
+
+ if local?
+ if File.exist? gem
+ begin
+ specs << Gem::Package.new(gem).spec
+ rescue StandardError
+ nil
+ end
end
- if specs.empty? then
- specs.push(*Gem.source_index.search(dep))
+ if specs.empty?
+ specs.push(*dep.matching_specs)
end
end
- if remote? then
- found = Gem::SpecFetcher.fetcher.fetch dep
+ if remote?
+ dep.prerelease = options[:prerelease]
+ found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dep
- specs.push(*found.map { |spec,| spec })
+ specs.push(*found.map {|spec,| spec })
end
- if specs.empty? then
- alert_error "Unknown gem '#{gem}'"
+ if specs.empty?
+ alert_error "No gem matching '#{dep}' found"
terminate_interaction 1
end
- output = lambda { |s| say s.to_yaml; say "\n" }
+ platform = get_platform_from_requirements(options)
- if options[:all] then
- specs.each(&output)
- else
- spec = specs.sort_by { |s| s.version }.last
- output[spec]
+ if platform
+ specs = specs.select {|s| s.platform.to_s == platform }
end
- end
-end
+ unless options[:all]
+ specs = [specs.max_by(&:version)]
+ end
+ specs.each do |s|
+ s = s.send field if field
+
+ say case options[:format]
+ when :ruby then s.to_ruby
+ when :marshal then Marshal.dump s
+ else Gem.use_psych? ? s.to_yaml : Gem::YAMLSerializer.dump(s)
+ end
+
+ say "\n"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/stale_command.rb b/lib/rubygems/commands/stale_command.rb
index 78cbdcc00a..0be2b85159 100644
--- a/lib/rubygems/commands/stale_command.rb
+++ b/lib/rubygems/commands/stale_command.rb
@@ -1,17 +1,30 @@
-require 'rubygems/command'
+# frozen_string_literal: true
+
+require_relative "../command"
class Gem::Commands::StaleCommand < Gem::Command
def initialize
- super('stale', 'List gems along with access times')
+ super("stale", "List gems along with access times")
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The stale command lists the latest access time for all the files in your
+installed gems.
+
+You can use this command to discover gems and gem versions you are no
+longer using.
+ EOF
end
def usage # :nodoc:
- "#{program_name}"
+ program_name.to_s
end
def execute
gem_to_atime = {}
- Gem.source_index.each do |name, spec|
+ Gem::Specification.each do |spec|
+ name = spec.full_name
Dir["#{spec.full_gem_path}/**/*.*"].each do |file|
next if File.directory?(file)
stat = File.stat(file)
@@ -20,8 +33,8 @@ class Gem::Commands::StaleCommand < Gem::Command
end
end
- gem_to_atime.sort_by { |_, atime| atime }.each do |name, atime|
- say "#{name} at #{atime.strftime '%c'}"
+ gem_to_atime.sort_by {|_, atime| atime }.each do |name, atime|
+ say "#{name} at #{atime.strftime "%c"}"
end
end
end
diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb
index 3d6e2383bc..3c26074f93 100644
--- a/lib/rubygems/commands/uninstall_command.rb
+++ b/lib/rubygems/commands/uninstall_command.rb
@@ -1,73 +1,204 @@
-require 'rubygems/command'
-require 'rubygems/version_option'
-require 'rubygems/uninstaller'
+# frozen_string_literal: true
-module Gem
- module Commands
- class UninstallCommand < Command
+require_relative "../command"
+require_relative "../version_option"
+require_relative "../uninstaller"
+require "fileutils"
- include VersionOption
+##
+# Gem uninstaller command line tool
+#
+# See `gem help uninstall`
- def initialize
- super 'uninstall', 'Uninstall gems from the local repository',
- :version => Gem::Requirement.default
+class Gem::Commands::UninstallCommand < Gem::Command
+ include Gem::VersionOption
- add_option('-a', '--[no-]all',
- 'Uninstall all matching versions'
- ) do |value, options|
- options[:all] = value
- end
+ def initialize
+ super "uninstall", "Uninstall gems from the local repository",
+ version: Gem::Requirement.default, user_install: true,
+ check_dev: false, vendor: false
- add_option('-I', '--[no-]ignore-dependencies',
- 'Ignore dependency requirements while',
- 'uninstalling') do |value, options|
- options[:ignore] = value
- end
+ add_option("-a", "--[no-]all",
+ "Uninstall all matching versions") do |value, options|
+ options[:all] = value
+ end
- add_option('-x', '--[no-]executables',
- 'Uninstall applicable executables without',
- 'confirmation') do |value, options|
- options[:executables] = value
- end
+ add_option("-I", "--[no-]ignore-dependencies",
+ "Ignore dependency requirements while",
+ "uninstalling") do |value, options|
+ options[:ignore] = value
+ end
- add_option('-i', '--install-dir DIR',
- 'Directory to uninstall gem from') do |value, options|
- options[:install_dir] = File.expand_path(value)
- end
+ add_option("-D", "--[no-]check-development",
+ "Check development dependencies while uninstalling",
+ "(default: false)") do |value, options|
+ options[:check_dev] = value
+ end
- add_option('-n', '--bindir DIR',
- 'Directory to remove binaries from') do |value, options|
- options[:bin_dir] = File.expand_path(value)
- end
+ add_option("-x", "--[no-]executables",
+ "Uninstall applicable executables without",
+ "confirmation") do |value, options|
+ options[:executables] = value
+ end
- add_version_option
- add_platform_option
- end
+ add_option("-i", "--install-dir DIR",
+ "Directory to uninstall gem from") do |value, options|
+ options[:install_dir] = File.expand_path(value)
+ end
- def arguments # :nodoc:
- "GEMNAME name of gem to uninstall"
- end
+ add_option("-n", "--bindir DIR",
+ "Directory to remove executables from") do |value, options|
+ options[:bin_dir] = File.expand_path(value)
+ end
- def defaults_str # :nodoc:
- "--version '#{Gem::Requirement.default}' --no-force " \
- "--install-dir #{Gem.dir}"
- end
+ add_option("--[no-]user-install",
+ "Uninstall from user's home directory",
+ "in addition to GEM_HOME.") do |value, options|
+ options[:user_install] = value
+ end
- def usage # :nodoc:
- "#{program_name} GEMNAME [GEMNAME ...]"
+ add_option("--[no-]format-executable",
+ "Assume executable names match Ruby's prefix and suffix.") do |value, options|
+ options[:format_executable] = value
+ end
+
+ add_option("--[no-]force",
+ "Uninstall all versions of the named gems",
+ "ignoring dependencies") do |value, options|
+ options[:force] = value
+ end
+
+ add_option("--[no-]abort-on-dependent",
+ "Prevent uninstalling gems that are",
+ "depended on by other gems.") do |value, options|
+ options[:abort_on_dependent] = value
+ end
+
+ add_version_option
+ add_platform_option
+
+ add_option("--vendor",
+ "Uninstall gem from the vendor directory.",
+ "Only for use by gem repackagers.") do |_value, options|
+ unless Gem.vendor_dir
+ raise Gem::OptionParser::InvalidOption.new "your platform is not supported"
end
- def execute
- get_all_gem_names.each do |gem_name|
- begin
- Gem::Uninstaller.new(gem_name, options).uninstall
- rescue Gem::GemNotInHomeException => e
- spec = e.spec
- alert("In order to remove #{spec.name}, please execute:\n" \
- "\tgem uninstall #{spec.name} --install-dir=#{spec.installation_path}")
- end
+ alert_warning "Use your OS package manager to uninstall vendor gems"
+ options[:vendor] = true
+ options[:install_dir] = Gem.vendor_dir
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to uninstall"
+ end
+
+ def defaults_str # :nodoc:
+ "--version '#{Gem::Requirement.default}' --no-force " \
+ "--user-install"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The uninstall command removes a previously installed gem.
+
+RubyGems will ask for confirmation if you are attempting to uninstall a gem
+that is a dependency of an existing gem. You can use the
+--ignore-dependencies option to skip this check.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME [GEMNAME ...]"
+ end
+
+ def check_version # :nodoc:
+ if options[:version] != Gem::Requirement.default &&
+ get_all_gem_names.size > 1
+ alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \
+ " version requirements using `gem uninstall 'my_gem:1.0.0' 'my_other_gem:>=2'`"
+ terminate_interaction 1
+ end
+ end
+
+ def execute
+ check_version
+
+ # Consider only gem specifications installed at `--install-dir`
+ Gem::Specification.dirs = options[:install_dir] if options[:install_dir]
+
+ if options[:all] && !options[:args].empty?
+ uninstall_specific
+ elsif options[:all]
+ uninstall_all
+ else
+ uninstall_specific
+ end
+ end
+
+ def uninstall_all
+ specs = Gem::Specification.reject(&:default_gem?)
+
+ specs.each do |spec|
+ options[:version] = spec.version
+ uninstall_gem spec.name
+ end
+
+ alert "Uninstalled all gems in #{options[:install_dir] || Gem.dir}"
+ end
+
+ def uninstall_specific
+ deplist = Gem::DependencyList.new
+ original_gem_version = {}
+
+ get_all_gem_names_and_versions.each do |name, version|
+ original_gem_version[name] = version || options[:version]
+
+ gem_specs = Gem::Specification.find_all_by_name(name, original_gem_version[name])
+
+ if gem_specs.empty?
+ say("Gem '#{name}' is not installed")
+ else
+ gem_specs.reject!(&:default_gem?) if gem_specs.size > 1
+
+ gem_specs.each do |spec|
+ deplist.add spec
end
end
end
+
+ deps = deplist.strongly_connected_components.flatten.reverse
+
+ gems_to_uninstall = {}
+
+ deps.each do |dep|
+ if original_gem_version[dep.name] == Gem::Requirement.default
+ next if gems_to_uninstall[dep.name]
+ gems_to_uninstall[dep.name] = true
+ else
+ options[:version] = dep.version
+ end
+
+ uninstall_gem(dep.name)
+ end
+ end
+
+ def uninstall_gem(gem_name)
+ uninstall(gem_name)
+ rescue Gem::GemNotInHomeException => e
+ spec = e.spec
+ alert("In order to remove #{spec.name}, please execute:\n" \
+ "\tgem uninstall #{spec.name} --install-dir=#{spec.base_dir}")
+ rescue Gem::UninstallError => e
+ spec = e.spec
+ alert_error("Error: unable to successfully uninstall '#{spec.name}' which is " \
+ "located at '#{spec.full_gem_path}'. This is most likely because" \
+ "the current user does not have the appropriate permissions")
+ terminate_interaction 1
+ end
+
+ def uninstall(gem_name)
+ Gem::Uninstaller.new(gem_name, options).uninstall
end
end
diff --git a/lib/rubygems/commands/unpack_command.rb b/lib/rubygems/commands/unpack_command.rb
index ab2494b0da..c2fc720297 100644
--- a/lib/rubygems/commands/unpack_command.rb
+++ b/lib/rubygems/commands/unpack_command.rb
@@ -1,21 +1,39 @@
-require 'fileutils'
-require 'rubygems/command'
-require 'rubygems/installer'
-require 'rubygems/version_option'
+# frozen_string_literal: true
-class Gem::Commands::UnpackCommand < Gem::Command
+require_relative "../command"
+require_relative "../version_option"
+require_relative "../security_option"
+require_relative "../remote_fetcher"
+require_relative "../package"
+
+# forward-declare
+
+module Gem::Security # :nodoc:
+ class Policy # :nodoc:
+ end
+end
+class Gem::Commands::UnpackCommand < Gem::Command
include Gem::VersionOption
+ include Gem::SecurityOption
def initialize
- super 'unpack', 'Unpack an installed gem to the current directory',
- :version => Gem::Requirement.default,
- :target => Dir.pwd
+ require "fileutils"
- add_option('--target', 'target directory for unpacking') do |value, options|
+ super "unpack", "Unpack an installed gem to the current directory",
+ version: Gem::Requirement.default,
+ target: Dir.pwd
+
+ add_option("--target=DIR",
+ "target directory for unpacking") do |value, options|
options[:target] = value
end
+ add_option("--spec", "unpack the gem specification") do |_value, options|
+ options[:spec] = true
+ end
+
+ add_security_option
add_version_option
end
@@ -27,6 +45,24 @@ class Gem::Commands::UnpackCommand < Gem::Command
"--version '#{Gem::Requirement.default}'"
end
+ def description
+ <<-EOF
+The unpack command allows you to examine the contents of a gem or modify
+them to help diagnose a bug.
+
+You can add the contents of the unpacked gem to the load path using the
+RUBYLIB environment variable or -I:
+
+ $ gem unpack my_gem
+ Unpacked gem: '.../my_gem-1.0'
+ [edit my_gem-1.0/lib/my_gem.rb]
+ $ ruby -Imy_gem-1.0/lib -S other_program
+
+You can repackage an unpacked gem using the build command. See the build
+command help for an example.
+ EOF
+ end
+
def usage # :nodoc:
"#{program_name} GEMNAME"
end
@@ -35,61 +71,98 @@ class Gem::Commands::UnpackCommand < Gem::Command
# TODO: allow, e.g., 'gem unpack rake-0.3.1'. Find a general solution for
# this, so that it works for uninstall as well. (And check other commands
# at the same time.)
+
def execute
- gemname = get_one_gem_name
- path = get_path(gemname, options[:version])
-
- if path then
- basename = File.basename(path).sub(/\.gem$/, '')
- target_dir = File.expand_path File.join(options[:target], basename)
- FileUtils.mkdir_p target_dir
- Gem::Installer.new(path, :unpack => true).unpack target_dir
- say "Unpacked gem: '#{target_dir}'"
- else
- alert_error "Gem '#{gemname}' not installed."
+ security_policy = options[:security_policy]
+
+ get_all_gem_names.each do |name|
+ dependency = Gem::Dependency.new name, options[:version]
+ path = get_path dependency
+
+ unless path
+ alert_error "Gem '#{name}' not installed nor fetchable."
+ next
+ end
+
+ if @options[:spec]
+ spec, metadata = Gem::Package.raw_spec(path, security_policy)
+
+ if metadata.nil?
+ alert_error "--spec is unsupported on '#{name}' (old format gem)"
+ next
+ end
+
+ spec_file = File.basename spec.spec_file
+
+ FileUtils.mkdir_p @options[:target] if @options[:target]
+
+ destination = if @options[:target]
+ File.join @options[:target], spec_file
+ else
+ spec_file
+ end
+
+ File.open destination, "w" do |io|
+ io.write metadata
+ end
+ else
+ basename = File.basename path, ".gem"
+ target_dir = File.expand_path basename, options[:target]
+
+ package = Gem::Package.new path, security_policy
+ package.extract_files target_dir
+
+ say "Unpacked gem: '#{target_dir}'"
+ end
end
end
+ ##
+ #
+ # Find cached filename in Gem.path. Returns nil if the file cannot be found.
+ #
+ #--
+ # TODO: see comments in get_path() about general service.
+
+ def find_in_cache(filename)
+ Gem.path.each do |path|
+ this_path = File.join(path, "cache", filename)
+ return this_path if File.exist? this_path
+ end
+
+ nil
+ end
+
+ ##
# Return the full path to the cached gem file matching the given
# name and version requirement. Returns 'nil' if no match.
#
# Example:
#
- # get_path('rake', '> 0.4') # -> '/usr/lib/ruby/gems/1.8/cache/rake-0.4.2.gem'
- # get_path('rake', '< 0.1') # -> nil
- # get_path('rak') # -> nil (exact name required)
+ # get_path 'rake', '> 0.4' # "/usr/lib/ruby/gems/1.8/cache/rake-0.4.2.gem"
+ # get_path 'rake', '< 0.1' # nil
+ # get_path 'rak' # nil (exact name required)
#--
- # TODO: This should be refactored so that it's a general service. I don't
- # think any of our existing classes are the right place though. Just maybe
- # 'Cache'?
- #
- # TODO: It just uses Gem.dir for now. What's an easy way to get the list of
- # source directories?
- def get_path(gemname, version_req)
- return gemname if gemname =~ /\.gem$/i
- specs = Gem::source_index.find_name gemname, version_req
+ def get_path(dependency)
+ return dependency.name if /\.gem$/i.match?(dependency.name)
- selected = specs.sort_by { |s| s.version }.last
+ specs = dependency.matching_specs
- return nil if selected.nil?
+ selected = specs.max_by(&:version)
- # We expect to find (basename).gem in the 'cache' directory.
- # Furthermore, the name match must be exact (ignoring case).
- if gemname =~ /^#{selected.name}$/i
- filename = selected.full_name + '.gem'
- path = nil
+ return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless
+ selected
- Gem.path.find do |gem_dir|
- path = File.join gem_dir, 'cache', filename
- File.exist? path
- end
+ return unless /^#{selected.name}$/i.match?(dependency.name)
- path
- else
- nil
- end
- end
+ # We expect to find (basename).gem in the 'cache' directory. Furthermore,
+ # the name match must be exact (ignoring case).
-end
+ path = find_in_cache File.basename selected.cache_file
+
+ return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless path
+ path
+ end
+end
diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb
index 28d3a5d382..d9740d814a 100644
--- a/lib/rubygems/commands/update_command.rb
+++ b/lib/rubygems/commands/update_command.rb
@@ -1,35 +1,54 @@
-require 'rubygems/command'
-require 'rubygems/command_manager'
-require 'rubygems/install_update_options'
-require 'rubygems/local_remote_options'
-require 'rubygems/spec_fetcher'
-require 'rubygems/version_option'
-require 'rubygems/commands/install_command'
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../command_manager"
+require_relative "../dependency_installer"
+require_relative "../install_update_options"
+require_relative "../local_remote_options"
+require_relative "../spec_fetcher"
+require_relative "../version_option"
+require_relative "../install_message" # must come before rdoc for messaging
+require_relative "../rdoc"
class Gem::Commands::UpdateCommand < Gem::Command
-
include Gem::InstallUpdateOptions
include Gem::LocalRemoteOptions
include Gem::VersionOption
+ attr_reader :installer # :nodoc:
+
+ attr_reader :updated # :nodoc:
+
def initialize
- super 'update',
- 'Update the named gems (or all installed gems) in the local repository',
- :generate_rdoc => true,
- :generate_ri => true,
- :force => false,
- :test => false
+ options = {
+ force: false,
+ }
+
+ options.merge!(install_update_options)
+
+ super "update", "Update installed gems to the latest version", options
add_install_update_options
- add_option('--system',
- 'Update the RubyGems system software') do |value, options|
- options[:system] = value
+ Gem::OptionParser.accept Gem::Version do |value|
+ Gem::Version.new value
+
+ value
end
- add_local_remote_options
+ add_option("--system [VERSION]", Gem::Version,
+ "Update the RubyGems system software") do |value, opts|
+ value ||= true
+
+ opts[:system] = value
+ end
+ add_local_remote_options
add_platform_option
+ add_prerelease_option "as update targets"
+
+ @updated = []
+ @installer = nil
end
def arguments # :nodoc:
@@ -37,145 +56,271 @@ class Gem::Commands::UpdateCommand < Gem::Command
end
def defaults_str # :nodoc:
- "--rdoc --ri --no-force --no-test --install-dir #{Gem.dir}"
+ "--no-force --install-dir #{Gem.dir}\n" +
+ install_update_defaults_str
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The update command will update your gems to the latest version.
+
+The update command does not remove the previous version. Use the cleanup
+command to remove old versions.
+ EOF
end
def usage # :nodoc:
"#{program_name} GEMNAME [GEMNAME ...]"
end
+ def check_latest_rubygems(version) # :nodoc:
+ if Gem.rubygems_version == version
+ say "Latest version already installed. Done."
+ terminate_interaction
+ end
+ end
+
+ def check_oldest_rubygems(version) # :nodoc:
+ if oldest_supported_version > version
+ alert_error "rubygems #{version} is not supported on #{RUBY_VERSION}. The oldest version supported by this ruby is #{oldest_supported_version}"
+ terminate_interaction 1
+ end
+ end
+
+ def check_update_arguments # :nodoc:
+ unless options[:args].empty?
+ alert_error "Gem names are not allowed with the --system option"
+ terminate_interaction 1
+ end
+ end
+
def execute
- hig = {}
+ if options[:system]
+ update_rubygems
+ return
+ end
+
+ gems_to_update = which_to_update(
+ highest_installed_gems,
+ options[:args].uniq
+ )
- if options[:system] then
- say "Updating RubyGems"
+ if options[:explain]
+ say "Gems to update:"
- unless options[:args].empty? then
- fail "No gem names are allowed with the --system option"
+ gems_to_update.each do |name_tuple|
+ say " #{name_tuple.full_name}"
end
- rubygems_update = Gem::Specification.new
- rubygems_update.name = 'rubygems-update'
- rubygems_update.version = Gem::Version.new Gem::RubyGemsVersion
- hig['rubygems-update'] = rubygems_update
+ return
+ end
- options[:user_install] = false
- else
- say "Updating installed gems"
+ say "Updating installed gems"
- hig = {} # highest installed gems
+ updated = update_gems gems_to_update
- Gem.source_index.each do |name, spec|
- if hig[spec.name].nil? or hig[spec.name].version < spec.version then
- hig[spec.name] = spec
- end
- end
+ installed_names = highest_installed_gems.keys
+ updated_names = updated.map(&:name)
+ not_updated_names = options[:args].uniq - updated_names
+ not_installed_names = not_updated_names - installed_names
+ up_to_date_names = not_updated_names - not_installed_names
+
+ if updated.empty?
+ say "Nothing to update"
+ else
+ say "Gems updated: #{updated_names.join(" ")}"
end
+ say "Gems already up-to-date: #{up_to_date_names.join(" ")}" unless up_to_date_names.empty?
+ say "Gems not currently installed: #{not_installed_names.join(" ")}" unless not_installed_names.empty?
+ end
+
+ def fetch_remote_gems(spec) # :nodoc:
+ dependency = Gem::Dependency.new spec.name, "> #{spec.version}"
+ dependency.prerelease = options[:prerelease]
- gems_to_update = which_to_update hig, options[:args]
+ fetcher = Gem::SpecFetcher.fetcher
- updated = []
+ spec_tuples, errors = fetcher.search_for_dependency dependency
- installer = Gem::DependencyInstaller.new options
+ error = errors.find {|e| e.respond_to? :exception }
- gems_to_update.uniq.sort.each do |name|
- next if updated.any? { |spec| spec.name == name }
+ raise error if error
- say "Updating #{name}"
- installer.install name
+ spec_tuples
+ end
+
+ def highest_installed_gems # :nodoc:
+ hig = {} # highest installed gems
+
+ # Get only gem specifications installed as --user-install
+ Gem::Specification.dirs = Gem.user_dir if options[:user_install]
- installer.installed_gems.each do |spec|
- updated << spec
- say "Successfully installed #{spec.full_name}"
+ Gem::Specification.each do |spec|
+ if hig[spec.name].nil? || hig[spec.name].version < spec.version
+ hig[spec.name] = spec
end
end
- if gems_to_update.include? "rubygems-update" then
- Gem.source_index.refresh!
+ hig
+ end
+
+ def highest_remote_name_tuple(spec) # :nodoc:
+ spec_tuples = fetch_remote_gems spec
+
+ highest_remote_gem = spec_tuples.max
+ return unless highest_remote_gem
- update_gems = Gem.source_index.search 'rubygems-update'
+ highest_remote_gem.first
+ end
+
+ def install_rubygems(spec) # :nodoc:
+ args = update_rubygems_arguments
+ version = spec.version
+
+ update_dir = File.join spec.base_dir, "gems", "rubygems-update-#{version}"
+
+ Dir.chdir update_dir do
+ say "Installing RubyGems #{version}" unless options[:silent]
- latest_update_gem = update_gems.sort_by { |s| s.version }.last
+ installed = preparing_gem_layout_for(version) do
+ system Gem.ruby, "--disable-gems", "setup.rb", *args
+ end
- say "Updating RubyGems to #{latest_update_gem.version}"
- installed = do_rubygems_update latest_update_gem.version
+ unless options[:silent]
+ say "RubyGems system software updated" if installed
+ end
+ end
+ end
- say "RubyGems system software updated" if installed
+ def preparing_gem_layout_for(version)
+ if Gem::Version.new(version) >= Gem::Version.new("3.2.a")
+ yield
else
- if updated.empty? then
- say "Nothing to update"
- else
- say "Gems updated: #{updated.map { |spec| spec.name }.join ', '}"
+ require "tmpdir"
+ Dir.mktmpdir("gem_update") do |tmpdir|
+ FileUtils.mv Gem.plugindir, tmpdir
+
+ status = yield
+
+ unless status
+ FileUtils.mv File.join(tmpdir, "plugins"), Gem.plugindir
+ end
+
+ status
end
end
end
- ##
- # Update the RubyGems software to +version+.
+ def rubygems_target_version
+ version = options[:system]
+ update_latest = version == true
- def do_rubygems_update(version)
- args = []
- args.push '--prefix', Gem.prefix unless Gem.prefix.nil?
- args << '--no-rdoc' unless options[:generate_rdoc]
- args << '--no-ri' unless options[:generate_ri]
- args << '--no-format-executable' if options[:no_format_executable]
+ unless update_latest
+ version = Gem::Version.new version
+ requirement = Gem::Requirement.new version
+
+ return version, requirement
+ end
- update_dir = File.join Gem.dir, 'gems', "rubygems-update-#{version}"
+ version = Gem::Version.new Gem::VERSION
+ requirement = Gem::Requirement.new ">= #{Gem::VERSION}"
- Dir.chdir update_dir do
- say "Installing RubyGems #{version}"
- setup_cmd = "#{Gem.ruby} setup.rb #{args.join ' '}"
+ rubygems_update = Gem::Specification.new
+ rubygems_update.name = "rubygems-update"
+ rubygems_update.version = version
+
+ highest_remote_tup = highest_remote_name_tuple(rubygems_update)
+ target = highest_remote_tup ? highest_remote_tup.version : version
+
+ [target, requirement]
+ end
+
+ def update_gem(name, version = Gem::Requirement.default)
+ return if @updated.any? {|spec| spec.name == name }
- # Make sure old rubygems isn't loaded
- old = ENV["RUBYOPT"]
- ENV.delete("RUBYOPT")
- system setup_cmd
- ENV["RUBYOPT"] = old if old
+ update_options = options.dup
+ update_options[:prerelease] = version.prerelease?
+
+ @installer = Gem::DependencyInstaller.new update_options
+
+ say "Updating #{name}" unless options[:system]
+ begin
+ @installer.install name, Gem::Requirement.new(version)
+ rescue Gem::InstallError, Gem::DependencyError => e
+ alert_error "Error installing #{name}:\n\t#{e.message}"
+ end
+
+ @installer.installed_gems.each do |spec|
+ @updated << spec
end
end
- def which_to_update(highest_installed_gems, gem_names)
- result = []
+ def update_gems(gems_to_update)
+ gems_to_update.uniq.sort.each do |name_tuple|
+ update_gem name_tuple.name, name_tuple.version
+ end
- highest_installed_gems.each do |l_name, l_spec|
- next if not gem_names.empty? and
- gem_names.all? { |name| /#{name}/ !~ l_spec.name }
+ @updated
+ end
- dependency = Gem::Dependency.new l_spec.name, "> #{l_spec.version}"
+ ##
+ # Update RubyGems software to the latest version.
- begin
- fetcher = Gem::SpecFetcher.fetcher
- spec_tuples = fetcher.find_matching dependency
- rescue Gem::RemoteFetcher::FetchError => e
- raise unless fetcher.warn_legacy e do
- require 'rubygems/source_info_cache'
+ def update_rubygems
+ if Gem.disable_system_update_message
+ alert_error Gem.disable_system_update_message
+ terminate_interaction 1
+ end
- dependency.name = '' if dependency.name == //
+ check_update_arguments
- specs = Gem::SourceInfoCache.search_with_source dependency
+ version, requirement = rubygems_target_version
- spec_tuples = specs.map do |spec, source_uri|
- [[spec.name, spec.version, spec.original_platform], source_uri]
- end
- end
- end
+ check_latest_rubygems version
- matching_gems = spec_tuples.select do |(name, version, platform),|
- name == l_name and Gem::Platform.match platform
- end
+ check_oldest_rubygems version
- highest_remote_gem = matching_gems.sort_by do |(name, version),|
- version
- end.last
+ installed_gems = Gem::Specification.find_all_by_name "rubygems-update", requirement
+ installed_gems = update_gem("rubygems-update", requirement) if installed_gems.empty? || installed_gems.first.version != version
+ return if installed_gems.empty?
- if highest_remote_gem and
- l_spec.version < highest_remote_gem.first[1] then
- result << l_name
- end
+ install_rubygems installed_gems.first
+ end
+
+ def update_rubygems_arguments # :nodoc:
+ args = []
+ args << "--silent" if options[:silent]
+ args << "--prefix" << Gem.prefix if Gem.prefix
+ args << "--no-document" unless options[:document].include?("rdoc") || options[:document].include?("ri")
+ args << "--no-format-executable" if options[:no_format_executable]
+ args << "--previous-version" << Gem::VERSION
+ args
+ end
+
+ def which_to_update(highest_installed_gems, gem_names)
+ result = []
+
+ highest_installed_gems.each do |_l_name, l_spec|
+ next if !gem_names.empty? &&
+ gem_names.none? {|name| name == l_spec.name }
+
+ highest_remote_tup = highest_remote_name_tuple l_spec
+ next unless highest_remote_tup
+
+ result << highest_remote_tup
end
result
end
-end
+ private
+ #
+ # Oldest version we support downgrading to. This is the version that
+ # originally ships with the oldest supported patch version of ruby.
+ #
+ def oldest_supported_version
+ @oldest_supported_version ||=
+ Gem::Version.new("3.3.3")
+ end
+end
diff --git a/lib/rubygems/commands/which_command.rb b/lib/rubygems/commands/which_command.rb
index 2267e44b11..5ed4d9d142 100644
--- a/lib/rubygems/commands/which_command.rb
+++ b/lib/rubygems/commands/which_command.rb
@@ -1,20 +1,18 @@
-require 'rubygems/command'
-require 'rubygems/gem_path_searcher'
+# frozen_string_literal: true
-class Gem::Commands::WhichCommand < Gem::Command
-
- EXT = %w[.rb .rbw .so .dll .bundle] # HACK
+require_relative "../command"
+class Gem::Commands::WhichCommand < Gem::Command
def initialize
- super 'which', 'Find the location of a library file you can require',
- :search_gems_first => false, :show_all => false
+ super "which", "Find the location of a library file you can require",
+ search_gems_first: false, show_all: false
- add_option '-a', '--[no-]all', 'show all matching files' do |show_all, options|
+ add_option "-a", "--[no-]all", "show all matching files" do |show_all, options|
options[:show_all] = show_all
end
- add_option '-g', '--[no-]gems-first',
- 'search gems before non-gems' do |gems_first, options|
+ add_option "-g", "--[no-]gems-first",
+ "search gems before non-gems" do |gems_first, options|
options[:search_gems_first] = gems_first
end
end
@@ -27,45 +25,54 @@ class Gem::Commands::WhichCommand < Gem::Command
"--no-gems-first --no-all"
end
- def usage # :nodoc:
- "#{program_name} FILE [FILE ...]"
+ def description # :nodoc:
+ <<-EOF
+The which command is like the shell which command and shows you where
+the file you wish to require lives.
+
+You can use the which command to help determine why you are requiring a
+version you did not expect or to look at the content of a file you are
+requiring to see why it does not behave as you expect.
+ EOF
end
def execute
- searcher = Gem::GemPathSearcher.new
+ found = true
options[:args].each do |arg|
+ arg = arg.sub(/#{Regexp.union(*Gem.suffixes)}$/, "")
dirs = $LOAD_PATH
- spec = searcher.find arg
- if spec then
- if options[:search_gems_first] then
- dirs = gem_paths(spec) + $LOAD_PATH
+ spec = Gem::Specification.find_by_path arg
+
+ if spec
+ if options[:search_gems_first]
+ dirs = spec.full_require_paths + $LOAD_PATH
else
- dirs = $LOAD_PATH + gem_paths(spec)
+ dirs = $LOAD_PATH + spec.full_require_paths
end
-
- say "(checking gem #{spec.full_name} for #{arg})" if
- Gem.configuration.verbose
end
paths = find_paths arg, dirs
- if paths.empty? then
- say "Can't find ruby library file or shared library #{arg}"
+ if paths.empty?
+ alert_error "Can't find Ruby library file or shared library #{arg}"
+ found = false
else
say paths
end
end
+
+ terminate_interaction 1 unless found
end
def find_paths(package_name, dirs)
result = []
dirs.each do |dir|
- EXT.each do |ext|
+ Gem.suffixes.each do |ext|
full_path = File.join dir, "#{package_name}#{ext}"
- if File.exist? full_path then
+ if File.exist?(full_path) && !File.directory?(full_path)
result << full_path
return result unless options[:show_all]
end
@@ -75,13 +82,7 @@ class Gem::Commands::WhichCommand < Gem::Command
result
end
- def gem_paths(spec)
- spec.require_paths.collect { |d| File.join spec.full_gem_path, d }
- end
-
def usage # :nodoc:
- "#{program_name} FILE [...]"
+ "#{program_name} FILE [FILE ...]"
end
-
end
-
diff --git a/lib/rubygems/commands/yank_command.rb b/lib/rubygems/commands/yank_command.rb
new file mode 100644
index 0000000000..fbdc262549
--- /dev/null
+++ b/lib/rubygems/commands/yank_command.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+require_relative "../gemcutter_utilities"
+
+class Gem::Commands::YankCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+ include Gem::GemcutterUtilities
+
+ def description # :nodoc:
+ <<-EOF
+The yank command permanently removes a gem you pushed to a server.
+
+Once you have pushed a gem several downloads will happen automatically
+via the webhooks. If you accidentally pushed passwords or other sensitive
+data you will need to change them immediately and yank your gem.
+ EOF
+ end
+
+ def arguments # :nodoc:
+ "GEM name of gem"
+ end
+
+ def usage # :nodoc:
+ "#{program_name} -v VERSION [-p PLATFORM] [--key KEY_NAME] [--host HOST] GEM"
+ end
+
+ def initialize
+ super "yank", "Remove a pushed gem from the index"
+
+ add_version_option("remove")
+ add_platform_option("remove")
+ add_otp_option
+
+ add_option("--host HOST",
+ "Yank from another gemcutter-compatible host",
+ " (e.g. https://rubygems.org)") do |value, options|
+ options[:host] = value
+ end
+
+ add_key_option
+ @host = nil
+ end
+
+ def execute
+ @host = options[:host]
+
+ sign_in @host, scope: get_yank_scope
+
+ version = get_version_from_requirements(options[:version])
+ platform = get_platform_from_requirements(options)
+
+ if version
+ yank_gem(version, platform)
+ else
+ say "A version argument is required: #{usage}"
+ terminate_interaction
+ end
+ end
+
+ def yank_gem(version, platform)
+ say "Yanking gem from #{host}..."
+ args = [:delete, version, platform, "api/v1/gems/yank"]
+ response = yank_api_request(*args)
+
+ say response.body
+ end
+
+ private
+
+ def yank_api_request(method, version, platform, api)
+ name = get_one_gem_name
+ response = rubygems_api_request(method, api, host, scope: get_yank_scope) do |request|
+ request.add_field("Authorization", api_key)
+
+ data = {
+ "gem_name" => name,
+ "version" => version,
+ }
+ data["platform"] = platform if platform
+
+ request.set_form_data data
+ end
+ response
+ end
+
+ def get_version_from_requirements(requirements)
+ requirements.requirements.first[1].version
+ rescue StandardError
+ nil
+ end
+
+ def get_yank_scope
+ :yank_rubygem
+ end
+end
diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb
index 276a5c151d..d5e9eb4e33 100644
--- a/lib/rubygems/config_file.rb
+++ b/lib/rubygems/config_file.rb
@@ -1,155 +1,438 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'yaml'
-require 'rubygems'
-
-# Store the gem command options specified in the configuration file. The
-# config file object acts much like a hash.
+require_relative "user_interaction"
+require "rbconfig"
+
+##
+# Gem::ConfigFile RubyGems options and gem command options from gemrc.
+#
+# gemrc is a YAML file that uses strings to match gem command arguments and
+# symbols to match RubyGems options.
+#
+# Gem command arguments use a String key that matches the command name and
+# allow you to specify default arguments:
+#
+# install: --no-rdoc --no-ri
+# update: --no-rdoc --no-ri
+#
+# You can use <tt>gem:</tt> to set default arguments for all commands.
+#
+# RubyGems options use symbol keys. Valid options are:
+#
+# +:backtrace+:: See #backtrace
+# +:bulk_threshold+:: See #bulk_threshold
+# +:verbose+:: See #verbose
+# +:update_sources+:: See #update_sources
+# +:concurrent_downloads+:: See #concurrent_downloads
+# +:cert_expiration_length_days+:: See #cert_expiration_length_days
+# +:install_extension_in_lib+:: See #install_extension_in_lib
+# +:ipv4_fallback_enabled+:: See #ipv4_fallback_enabled
+# +:global_gem_cache+:: See #global_gem_cache
+# +:use_psych+:: See #use_psych
+# +:gemhome+:: See #home
+# +:gempath+:: See #path
+# +:sources+:: Sets Gem::sources
+# +:disable_default_gem_server+:: See #disable_default_gem_server
+# +:ssl_verify_mode+:: See #ssl_verify_mode
+# +:ssl_ca_cert+:: See #ssl_ca_cert
+# +:ssl_client_cert+:: See #ssl_client_cert
+#
+# gemrc files may exist in various locations and are read and merged in
+# the following order:
+#
+# - system wide (/etc/gemrc)
+# - per user (~/.gemrc)
+# - per environment (gemrc files listed in the GEMRC environment variable)
class Gem::ConfigFile
+ include Gem::UserInteraction
- DEFAULT_BACKTRACE = false
- DEFAULT_BENCHMARK = false
+ DEFAULT_BACKTRACE = true
DEFAULT_BULK_THRESHOLD = 1000
DEFAULT_VERBOSITY = true
DEFAULT_UPDATE_SOURCES = true
+ DEFAULT_CONCURRENT_DOWNLOADS = 8
+ DEFAULT_CERT_EXPIRATION_LENGTH_DAYS = 365
+ DEFAULT_IPV4_FALLBACK_ENABLED = false
+ DEFAULT_INSTALL_EXTENSION_IN_LIB = true
+ DEFAULT_GLOBAL_GEM_CACHE = false
+ DEFAULT_USE_PSYCH = false
##
# For Ruby packagers to set configuration defaults. Set in
# rubygems/defaults/operating_system.rb
- OPERATING_SYSTEM_DEFAULTS = {}
+ OPERATING_SYSTEM_DEFAULTS = Gem.operating_system_defaults
##
# For Ruby implementers to set configuration defaults. Set in
# rubygems/defaults/#{RUBY_ENGINE}.rb
- PLATFORM_DEFAULTS = {}
+ PLATFORM_DEFAULTS = Gem.platform_defaults
+
+ # :stopdoc:
- system_config_path =
+ SYSTEM_CONFIG_PATH =
begin
- require 'Win32API'
+ require "etc"
+ Etc.sysconfdir
+ rescue LoadError, NoMethodError
+ RbConfig::CONFIG["sysconfdir"] || "/etc"
+ end
- CSIDL_COMMON_APPDATA = 0x0023
- path = 0.chr * 260
- SHGetFolderPath = Win32API.new 'shell32', 'SHGetFolderPath', 'LLLLP', 'L'
- SHGetFolderPath.call 0, CSIDL_COMMON_APPDATA, 0, 1, path
+ # :startdoc:
- path.strip
- rescue LoadError
- '/etc'
- end
+ SYSTEM_WIDE_CONFIG_FILE = File.join SYSTEM_CONFIG_PATH, "gemrc"
- SYSTEM_WIDE_CONFIG_FILE = File.join system_config_path, 'gemrc'
-
+ ##
# List of arguments supplied to the config file object.
+
attr_reader :args
- # Where to look for gems
+ ##
+ # Where to look for gems (deprecated)
+
attr_accessor :path
+ ##
+ # Where to install gems (deprecated)
+
attr_accessor :home
+ ##
# True if we print backtraces on errors.
+
attr_writer :backtrace
- # True if we are benchmarking this run.
- attr_accessor :benchmark
+ ##
+ # Bulk threshold value. If the number of missing gems are above this
+ # threshold value, then a bulk download technique is used. (deprecated)
- # Bulk threshold value. If the number of missing gems are above
- # this threshold value, then a bulk download technique is used.
attr_accessor :bulk_threshold
+ ##
# Verbose level of output:
# * false -- No output
# * true -- Normal output
# * :loud -- Extra output
+
attr_accessor :verbose
+ ##
+ # Number of gem downloads that should be performed concurrently.
+
+ attr_accessor :concurrent_downloads
+
+ ##
# True if we want to update the SourceInfoCache every time, false otherwise
+
attr_accessor :update_sources
+ ##
+ # True if we want to force specification of gem server when pushing a gem
+
+ attr_accessor :disable_default_gem_server
+
+ # openssl verify mode value, used for remote https connection
+
+ attr_reader :ssl_verify_mode
+
+ ##
+ # Path name of directory or file of openssl CA certificate, used for remote
+ # https connection
+
+ attr_accessor :ssl_ca_cert
+
+ ##
+ # sources to look for gems
+ attr_accessor :sources
+
+ ##
+ # Expiration length to sign a certificate
+
+ attr_accessor :cert_expiration_length_days
+
+ ##
+ # Install extensions into lib as well as into the extension directory.
+
+ attr_accessor :install_extension_in_lib
+
+ ##
+ # == Experimental ==
+ # Fallback to IPv4 when IPv6 is not reachable or slow (default: false)
+
+ attr_accessor :ipv4_fallback_enabled
+
+ ##
+ # Use a global cache for .gem files shared across all Ruby installations.
+ # When enabled, gems are cached to ~/.cache/gem/gems (or XDG_CACHE_HOME/gem/gems).
+
+ attr_accessor :global_gem_cache
+
+ ##
+ # Use Psych (C extension YAML parser) instead of the pure Ruby YAMLSerializer.
+
+ attr_accessor :use_psych
+
+ ##
+ # Path name of directory or file of openssl client certificate, used for remote https connection with client authentication
+
+ attr_reader :ssl_client_cert
+
+ ##
# Create the config file object. +args+ is the list of arguments
# from the command line.
#
# The following command line options are handled early here rather
# than later at the time most command options are processed.
#
- # * --config-file and --config-file==NAME -- Obviously these need
- # to be handled by the ConfigFile object to ensure we get the
- # right config file.
- #
- # * --backtrace -- Backtrace needs to be turned on early so that
- # errors before normal option parsing can be properly handled.
+ # <tt>--config-file</tt>, <tt>--config-file==NAME</tt>::
+ # Obviously these need to be handled by the ConfigFile object to ensure we
+ # get the right config file.
#
- # * --debug -- Enable Ruby level debug messages. Handled early
- # for the same reason as --backtrace.
+ # <tt>--backtrace</tt>::
+ # Backtrace needs to be turned on early so that errors before normal
+ # option parsing can be properly handled.
#
- def initialize(arg_list)
- @config_file_name = nil
- need_config_file_name = false
+ # <tt>--debug</tt>::
+ # Enable Ruby level debug messages. Handled early for the same reason as
+ # --backtrace.
+ #--
+ # TODO: parse options upstream, pass in options directly
- arg_list = arg_list.map do |arg|
- if need_config_file_name then
- @config_file_name = arg
- need_config_file_name = false
- nil
- elsif arg =~ /^--config-file=(.*)/ then
- @config_file_name = $1
- nil
- elsif arg =~ /^--config-file$/ then
- need_config_file_name = true
- nil
- else
- arg
- end
- end.compact
+ def initialize(args)
+ set_config_file_name(args)
@backtrace = DEFAULT_BACKTRACE
- @benchmark = DEFAULT_BENCHMARK
@bulk_threshold = DEFAULT_BULK_THRESHOLD
@verbose = DEFAULT_VERBOSITY
@update_sources = DEFAULT_UPDATE_SOURCES
+ @concurrent_downloads = DEFAULT_CONCURRENT_DOWNLOADS
+ @cert_expiration_length_days = DEFAULT_CERT_EXPIRATION_LENGTH_DAYS
+ @install_extension_in_lib = DEFAULT_INSTALL_EXTENSION_IN_LIB
+ @ipv4_fallback_enabled = ENV["IPV4_FALLBACK_ENABLED"] == "true" || DEFAULT_IPV4_FALLBACK_ENABLED
+ @global_gem_cache = ENV["RUBYGEMS_GLOBAL_GEM_CACHE"] == "true" || DEFAULT_GLOBAL_GEM_CACHE
+ @use_psych = ENV["RUBYGEMS_USE_PSYCH"] == "true" || DEFAULT_USE_PSYCH
operating_system_config = Marshal.load Marshal.dump(OPERATING_SYSTEM_DEFAULTS)
platform_config = Marshal.load Marshal.dump(PLATFORM_DEFAULTS)
system_config = load_file SYSTEM_WIDE_CONFIG_FILE
- user_config = load_file config_file_name.dup.untaint
+ user_config = load_file config_file_name
+
+ environment_config = (ENV["GEMRC"] || "").
+ split(File::PATH_SEPARATOR).inject({}) do |result, file|
+ result.merge load_file file
+ end
@hash = operating_system_config.merge platform_config
- @hash = @hash.merge system_config
- @hash = @hash.merge user_config
-
- # HACK these override command-line args, which is bad
- @backtrace = @hash[:backtrace] if @hash.key? :backtrace
- @benchmark = @hash[:benchmark] if @hash.key? :benchmark
- @bulk_threshold = @hash[:bulk_threshold] if @hash.key? :bulk_threshold
- Gem.sources = @hash[:sources] if @hash.key? :sources
- @verbose = @hash[:verbose] if @hash.key? :verbose
- @update_sources = @hash[:update_sources] if @hash.key? :update_sources
- @path = @hash[:gempath] if @hash.key? :gempath
- @home = @hash[:gemhome] if @hash.key? :gemhome
-
- handle_arguments arg_list
+ unless args.index "--norc"
+ @hash = @hash.merge system_config
+ @hash = @hash.merge user_config
+ @hash = @hash.merge environment_config
+ end
+
+ @hash.transform_keys! do |k|
+ # gemhome and gempath are not working with symbol keys
+ if %w[backtrace bulk_threshold verbose update_sources cert_expiration_length_days
+ concurrent_downloads install_extension_in_lib ipv4_fallback_enabled
+ global_gem_cache use_psych sources
+ disable_default_gem_server ssl_verify_mode ssl_ca_cert ssl_client_cert].include?(k)
+ k.to_sym
+ else
+ k
+ end
+ end
+
+ # HACK: these override command-line args, which is bad
+ @backtrace = @hash[:backtrace] if @hash.key? :backtrace
+ @bulk_threshold = @hash[:bulk_threshold] if @hash.key? :bulk_threshold
+ @verbose = @hash[:verbose] if @hash.key? :verbose
+ @update_sources = @hash[:update_sources] if @hash.key? :update_sources
+ @concurrent_downloads = @hash[:concurrent_downloads] if @hash.key? :concurrent_downloads
+ @cert_expiration_length_days = @hash[:cert_expiration_length_days] if @hash.key? :cert_expiration_length_days
+ @install_extension_in_lib = @hash[:install_extension_in_lib] if @hash.key? :install_extension_in_lib
+ @ipv4_fallback_enabled = @hash[:ipv4_fallback_enabled] if @hash.key? :ipv4_fallback_enabled
+ @global_gem_cache = @hash[:global_gem_cache] if @hash.key? :global_gem_cache
+ @use_psych = @hash[:use_psych] if @hash.key? :use_psych
+
+ @home = @hash[:gemhome] if @hash.key? :gemhome
+ @path = @hash[:gempath] if @hash.key? :gempath
+ @sources = @hash[:sources] if @hash.key? :sources
+ @disable_default_gem_server = @hash[:disable_default_gem_server] if @hash.key? :disable_default_gem_server
+ @ssl_verify_mode = @hash[:ssl_verify_mode] if @hash.key? :ssl_verify_mode
+ @ssl_ca_cert = @hash[:ssl_ca_cert] if @hash.key? :ssl_ca_cert
+ @ssl_client_cert = @hash[:ssl_client_cert] if @hash.key? :ssl_client_cert
+
+ @api_keys = nil
+ @rubygems_api_key = nil
+
+ handle_arguments args
+ end
+
+ ##
+ # Hash of RubyGems.org and alternate API keys
+
+ def api_keys
+ load_api_keys unless @api_keys
+
+ @api_keys
+ end
+
+ ##
+ # Checks the permissions of the credentials file. If they are not 0600 an
+ # error message is displayed and RubyGems aborts.
+
+ def check_credentials_permissions
+ return if Gem.win_platform? # windows doesn't write 0600 as 0600
+ return unless File.exist? credentials_path
+
+ existing_permissions = File.stat(credentials_path).mode & 0o777
+
+ return if existing_permissions == 0o600
+
+ alert_error <<-ERROR
+Your gem push credentials file located at:
+
+\t#{credentials_path}
+
+has file permissions of 0#{existing_permissions.to_s 8} but 0600 is required.
+
+To fix this error run:
+
+\tchmod 0600 #{credentials_path}
+
+You should reset your credentials at:
+
+\thttps://rubygems.org/profile/edit
+
+if you believe they were disclosed to a third party.
+ ERROR
+
+ terminate_interaction 1
+ end
+
+ ##
+ # Location of RubyGems.org credentials
+
+ def credentials_path
+ credentials = File.join Gem.user_home, ".gem", "credentials"
+ if File.exist? credentials
+ credentials
+ else
+ File.join Gem.data_home, "gem", "credentials"
+ end
+ end
+
+ def load_api_keys
+ check_credentials_permissions
+
+ @api_keys = if File.exist? credentials_path
+ load_file(credentials_path)
+ else
+ @hash
+ end
+
+ if @api_keys.key? :rubygems_api_key
+ @rubygems_api_key = @api_keys[:rubygems_api_key]
+ @api_keys[:rubygems] = @api_keys.delete :rubygems_api_key unless
+ @api_keys.key? :rubygems
+ end
+ end
+
+ ##
+ # Returns the RubyGems.org API key
+
+ def rubygems_api_key
+ load_api_keys unless @rubygems_api_key
+
+ @rubygems_api_key
+ end
+
+ ##
+ # Sets the RubyGems.org API key to +api_key+
+
+ def rubygems_api_key=(api_key)
+ set_api_key :rubygems_api_key, api_key
+
+ @rubygems_api_key = api_key
+ end
+
+ ##
+ # Set a specific host's API key to +api_key+
+
+ def set_api_key(host, api_key)
+ check_credentials_permissions
+
+ config = load_file(credentials_path).merge(host => api_key)
+
+ dirname = File.dirname credentials_path
+ require "fileutils"
+ FileUtils.mkdir_p(dirname)
+
+ permissions = 0o600 & ~File.umask
+ File.open(credentials_path, "w", permissions) do |f|
+ f.write self.class.dump_with_rubygems_yaml(config)
+ end
+
+ load_api_keys # reload
+ end
+
+ ##
+ # Remove the +~/.gem/credentials+ file to clear all the current sessions.
+
+ def unset_api_key!
+ return false unless File.exist?(credentials_path)
+
+ File.delete(credentials_path)
end
def load_file(filename)
+ yaml_errors = [ArgumentError]
+
+ return {} unless filename && !filename.empty? && File.exist?(filename)
+
begin
- YAML.load(File.read(filename)) if filename and File.exist?(filename)
- rescue ArgumentError
- warn "Failed to load #{config_file_name}"
+ config = self.class.load_with_rubygems_config_hash(File.read(filename))
+ has_invalid_keys = config.keys.any? {|k| k.to_s.gsub(%r{https?:\/\/}, "").include?(": ") }
+ has_invalid_values = config.values.any? {|v| v.is_a?(String) && v.gsub(%r{https?:\/\/}, "").match?(/\A\S+: /) }
+ if has_invalid_keys || has_invalid_values
+ warn "Failed to load #{filename} because it doesn't contain valid YAML hash"
+ return {}
+ else
+ return config
+ end
+ rescue *yaml_errors => e
+ warn "Failed to load #{filename}, #{e}"
rescue Errno::EACCES
- warn "Failed to load #{config_file_name} due to permissions problem."
- end or {}
+ warn "Failed to load #{filename} due to permissions problem."
+ end
+
+ {}
end
# True if the backtrace option has been specified, or debug is on.
def backtrace
- @backtrace or $DEBUG
+ @backtrace || $DEBUG
+ end
+
+ # Check state file is writable. Creates empty file if not present to ensure we can write to it.
+ def state_file_writable?
+ if File.exist?(state_file_name)
+ File.writable?(state_file_name)
+ else
+ require "fileutils"
+ FileUtils.mkdir_p File.dirname(state_file_name)
+ File.open(state_file_name, "w") {}
+ true
+ end
+ rescue Errno::EACCES
+ false
end
# The name of the configuration file.
@@ -157,22 +440,39 @@ class Gem::ConfigFile
@config_file_name || Gem.config_file
end
+ # The name of the state file.
+ def state_file_name
+ Gem.state_file
+ end
+
+ # Reads time of last update check from state file
+ def last_update_check
+ if File.readable?(state_file_name)
+ File.read(state_file_name).to_i
+ else
+ 0
+ end
+ end
+
+ # Writes time of last update check to state file
+ def last_update_check=(timestamp)
+ File.write(state_file_name, timestamp.to_s) if state_file_writable?
+ end
+
# Delegates to @hash
def each(&block)
hash = @hash.dup
hash.delete :update_sources
hash.delete :verbose
- hash.delete :benchmark
hash.delete :backtrace
hash.delete :bulk_threshold
yield :update_sources, @update_sources
yield :verbose, @verbose
- yield :benchmark, @benchmark
yield :backtrace, @backtrace
yield :bulk_threshold, @bulk_threshold
- yield 'config_file_name', @config_file_name if @config_file_name
+ yield "config_file_name", @config_file_name if @config_file_name
hash.each(&block)
end
@@ -185,10 +485,10 @@ class Gem::ConfigFile
case arg
when /^--(backtrace|traceback)$/ then
@backtrace = true
- when /^--bench(mark)?$/ then
- @benchmark = true
when /^--debug$/ then
$DEBUG = true
+
+ warn "NOTE: Debugging mode prints all exceptions even when rescued"
else
@args << arg
end
@@ -198,69 +498,155 @@ class Gem::ConfigFile
# Really verbose mode gives you extra output.
def really_verbose
case verbose
- when true, false, nil then false
- else true
+ when true, false, nil then
+ false
+ else
+ true
end
end
# to_yaml only overwrites things you can't override on the command line.
def to_yaml # :nodoc:
yaml_hash = {}
- yaml_hash[:backtrace] = @hash.key?(:backtrace) ? @hash[:backtrace] :
- DEFAULT_BACKTRACE
- yaml_hash[:benchmark] = @hash.key?(:benchmark) ? @hash[:benchmark] :
- DEFAULT_BENCHMARK
- yaml_hash[:bulk_threshold] = @hash.key?(:bulk_threshold) ?
- @hash[:bulk_threshold] : DEFAULT_BULK_THRESHOLD
- yaml_hash[:sources] = Gem.sources
- yaml_hash[:update_sources] = @hash.key?(:update_sources) ?
- @hash[:update_sources] : DEFAULT_UPDATE_SOURCES
- yaml_hash[:verbose] = @hash.key?(:verbose) ? @hash[:verbose] :
- DEFAULT_VERBOSITY
-
- keys = yaml_hash.keys.map { |key| key.to_s }
- keys << 'debug'
+ yaml_hash[:backtrace] = @hash.fetch(:backtrace, DEFAULT_BACKTRACE)
+ yaml_hash[:bulk_threshold] = @hash.fetch(:bulk_threshold, DEFAULT_BULK_THRESHOLD)
+ yaml_hash[:sources] = Gem.sources.to_a
+ yaml_hash[:update_sources] = @hash.fetch(:update_sources, DEFAULT_UPDATE_SOURCES)
+ yaml_hash[:verbose] = @hash.fetch(:verbose, DEFAULT_VERBOSITY)
+
+ yaml_hash[:concurrent_downloads] =
+ @hash.fetch(:concurrent_downloads, DEFAULT_CONCURRENT_DOWNLOADS)
+
+ yaml_hash[:install_extension_in_lib] =
+ @hash.fetch(:install_extension_in_lib, DEFAULT_INSTALL_EXTENSION_IN_LIB)
+
+ yaml_hash[:ssl_verify_mode] =
+ @hash[:ssl_verify_mode] if @hash.key? :ssl_verify_mode
+
+ yaml_hash[:ssl_ca_cert] =
+ @hash[:ssl_ca_cert] if @hash.key? :ssl_ca_cert
+
+ yaml_hash[:ssl_client_cert] =
+ @hash[:ssl_client_cert] if @hash.key? :ssl_client_cert
+
+ keys = yaml_hash.keys.map(&:to_s)
+ keys << "debug"
re = Regexp.union(*keys)
@hash.each do |key, value|
key = key.to_s
- next if key =~ re
+ next if key&.match?(re)
yaml_hash[key.to_s] = value
end
- yaml_hash.to_yaml
+ self.class.dump_with_rubygems_yaml(yaml_hash)
end
# Writes out this config file, replacing its source.
def write
- File.open config_file_name, 'w' do |fp|
- fp.write self.to_yaml
+ require "fileutils"
+ FileUtils.mkdir_p File.dirname(config_file_name)
+
+ File.open config_file_name, "w" do |io|
+ io.write to_yaml
end
end
# Return the configuration information for +key+.
def [](key)
- @hash[key.to_s]
+ @hash[key] || @hash[key.to_s]
end
# Set configuration option +key+ to +value+.
def []=(key, value)
- @hash[key.to_s] = value
+ @hash[key] = value
end
def ==(other) # :nodoc:
- self.class === other and
- @backtrace == other.backtrace and
- @benchmark == other.benchmark and
- @bulk_threshold == other.bulk_threshold and
- @verbose == other.verbose and
- @update_sources == other.update_sources and
- @hash == other.hash
+ self.class === other &&
+ @backtrace == other.backtrace &&
+ @bulk_threshold == other.bulk_threshold &&
+ @verbose == other.verbose &&
+ @update_sources == other.update_sources &&
+ @hash == other.hash
end
- protected
-
attr_reader :hash
+ protected :hash
-end
+ def self.dump_with_rubygems_yaml(content)
+ content.transform_keys! do |k|
+ k.is_a?(Symbol) ? ":#{k}" : k
+ end
+
+ require_relative "yaml_serializer"
+ Gem::YAMLSerializer.dump(content)
+ end
+
+ def self.load_with_rubygems_config_hash(yaml)
+ require_relative "yaml_serializer"
+
+ content = Gem::YAMLSerializer.load(yaml, permitted_classes: [])
+ return {} unless content.is_a?(Hash)
+
+ deep_transform_config_keys!(content)
+ end
+
+ private
+
+ def self.deep_transform_config_keys!(config)
+ config.transform_keys! do |k|
+ if k.match?(/\A:(.*)\Z/)
+ k[1..-1].to_sym
+ elsif k.include?("__") || k.match?(%r{/\Z})
+ if k.is_a?(Symbol)
+ k.to_s.gsub(/__/,".").gsub(%r{/\Z}, "").to_sym
+ else
+ k.dup.gsub(/__/,".").gsub(%r{/\Z}, "")
+ end
+ else
+ k
+ end
+ end
+
+ config.transform_values! do |v|
+ if v.is_a?(String)
+ if v.match?(/\A:(.*)\Z/)
+ v[1..-1].to_sym
+ elsif v.match?(/\A[+-]?\d+\Z/)
+ v.to_i
+ elsif v.match?(/\Atrue|false\Z/)
+ v == "true"
+ elsif v.empty?
+ nil
+ else
+ v
+ end
+ elsif v.respond_to?(:empty?) && v.empty?
+ nil
+ elsif v.is_a?(Hash)
+ deep_transform_config_keys!(v)
+ else
+ v
+ end
+ end
+
+ config
+ end
+
+ def set_config_file_name(args)
+ @config_file_name = ENV["GEMRC"]
+ need_config_file_name = false
+ args.each do |arg|
+ if need_config_file_name
+ @config_file_name = arg
+ need_config_file_name = false
+ elsif arg =~ /^--config-file=(.*)/
+ @config_file_name = $1
+ elsif /^--config-file$/.match?(arg)
+ need_config_file_name = true
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/core_ext/kernel_gem.rb b/lib/rubygems/core_ext/kernel_gem.rb
new file mode 100644
index 0000000000..4e09b95c44
--- /dev/null
+++ b/lib/rubygems/core_ext/kernel_gem.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module Kernel
+ ##
+ # Use Kernel#gem to activate a specific version of +gem_name+.
+ #
+ # +requirements+ is a list of version requirements that the
+ # specified gem must match, most commonly "= example.version.number". See
+ # Gem::Requirement for how to specify a version requirement.
+ #
+ # If you will be activating the latest version of a gem, there is no need to
+ # call Kernel#gem, Kernel#require will do the right thing for you.
+ #
+ # Kernel#gem returns true if the gem was activated, otherwise false. If the
+ # gem could not be found, didn't match the version requirements, or a
+ # different version was already activated, an exception will be raised.
+ #
+ # Kernel#gem should be called *before* any require statements (otherwise
+ # RubyGems may load a conflicting library version).
+ #
+ # Kernel#gem only loads prerelease versions when prerelease +requirements+
+ # are given:
+ #
+ # gem 'rake', '>= 1.1.a', '< 2'
+ #
+ # In older RubyGems versions, the environment variable GEM_SKIP could be
+ # used to skip activation of specified gems, for example to test out changes
+ # that haven't been installed yet. Now RubyGems defers to -I and the
+ # RUBYLIB environment variable to skip activation of a gem.
+ #
+ # Example:
+ #
+ # GEM_SKIP=libA:libB ruby -I../libA -I../libB ./mycode.rb
+
+ def gem(gem_name, *requirements) # :doc:
+ skip_list = (ENV["GEM_SKIP"] || "").split(/:/)
+ raise Gem::LoadError, "skipping #{gem_name}" if skip_list.include? gem_name
+
+ if gem_name.is_a? Gem::Dependency
+ unless Gem::Deprecate.skip
+ warn "#{Gem.location_of_caller.join ":"}:Warning: Kernel.gem no longer "\
+ "accepts a Gem::Dependency object, please pass the name "\
+ "and requirements directly"
+ end
+
+ requirements = gem_name.requirement
+ gem_name = gem_name.name
+ end
+
+ dep = Gem::Dependency.new(gem_name, *requirements)
+
+ loaded = Gem.loaded_specs[gem_name]
+
+ return false if loaded && dep.matches_spec?(loaded)
+
+ spec = dep.to_spec
+
+ if spec
+ if Gem::LOADED_SPECS_MUTEX.owned?
+ spec.activate
+ else
+ Gem::LOADED_SPECS_MUTEX.synchronize { spec.activate }
+ end
+ end
+ end
+
+ private :gem
+end
diff --git a/lib/rubygems/core_ext/kernel_require.rb b/lib/rubygems/core_ext/kernel_require.rb
new file mode 100644
index 0000000000..3a9bdbdc9d
--- /dev/null
+++ b/lib/rubygems/core_ext/kernel_require.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+#--
+# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
+# All rights reserved.
+# See LICENSE.txt for permissions.
+#++
+
+require "monitor"
+
+module Kernel
+ RUBYGEMS_ACTIVATION_MONITOR = Monitor.new # :nodoc:
+
+ # Make sure we have a reference to Ruby's original Kernel#require
+ unless defined?(gem_original_require)
+ # :stopdoc:
+ alias_method :gem_original_require, :require
+ private :gem_original_require
+ # :startdoc:
+ end
+
+ ##
+ # When RubyGems is required, Kernel#require is replaced with our own which
+ # is capable of loading gems on demand.
+ #
+ # When you call <tt>require 'x'</tt>, this is what happens:
+ # * If the file can be loaded from the existing Ruby loadpath, it
+ # is.
+ # * Otherwise, installed gems are searched for a file that matches.
+ # If it's found in gem 'y', that gem is activated (added to the
+ # loadpath).
+ #
+ # The normal <tt>require</tt> functionality of returning false if
+ # that file has already been loaded is preserved.
+
+ def require(path) # :doc:
+ return gem_original_require(path) unless Gem.discover_gems_on_require
+
+ RUBYGEMS_ACTIVATION_MONITOR.synchronize do
+ path = File.path(path)
+
+ # If +path+ belongs to a default gem, we activate it and then go straight
+ # to normal require
+
+ if spec = Gem.find_default_spec(path)
+ name = spec.name
+
+ next if Gem.loaded_specs[name]
+
+ # Ensure -I beats a default gem
+ resolved_path = begin
+ rp = nil
+ load_path_check_index = Gem.load_path_insert_index - Gem.activated_gem_paths
+ Gem.suffixes.find do |s|
+ $LOAD_PATH[0...load_path_check_index].find do |lp|
+ if File.symlink? lp # for backward compatibility
+ next
+ end
+
+ full_path = File.expand_path(File.join(lp, "#{path}#{s}"))
+ rp = full_path if File.file?(full_path)
+ end
+ end
+ rp
+ end
+
+ next if resolved_path
+
+ Kernel.send(:gem, name, Gem::Requirement.default_prerelease)
+
+ Gem.load_bundler_extensions(Gem.loaded_specs[name].version) if name == "bundler"
+
+ next
+ end
+
+ # If there are no unresolved deps, then we can use just try
+ # normal require handle loading a gem from the rescue below.
+
+ if Gem::Specification.unresolved_deps.empty?
+ next
+ end
+
+ # If +path+ is for a gem that has already been loaded, don't
+ # bother trying to find it in an unresolved gem, just go straight
+ # to normal require.
+ #--
+ # TODO request access to the C implementation of this to speed up RubyGems
+
+ if Gem::Specification.find_active_stub_by_path(path)
+ next
+ end
+
+ # Attempt to find +path+ in any unresolved gems...
+
+ found_specs = Gem::Specification.find_in_unresolved path
+
+ # If there are no directly unresolved gems, then try and find +path+
+ # in any gems that are available via the currently unresolved gems.
+ # For example, given:
+ #
+ # a => b => c => d
+ #
+ # If a and b are currently active with c being unresolved and d.rb is
+ # requested, then find_in_unresolved_tree will find d.rb in d because
+ # it's a dependency of c.
+ #
+ if found_specs.empty?
+ found_specs = Gem::Specification.find_in_unresolved_tree path
+
+ found_specs.each(&:activate)
+
+ # We found +path+ directly in an unresolved gem. Now we figure out, of
+ # the possible found specs, which one we should activate.
+ else
+
+ # Check that all the found specs are just different
+ # versions of the same gem
+ names = found_specs.map(&:name).uniq
+
+ if names.size > 1
+ raise Gem::LoadError, "#{path} found in multiple gems: #{names.join ", "}"
+ end
+
+ # Ok, now find a gem that has no conflicts, starting
+ # at the highest version.
+ valid = found_specs.find {|s| !s.has_conflicts? }
+
+ unless valid
+ le = Gem::LoadError.new "unable to find a version of '#{names.first}' to activate"
+ le.name = names.first
+ raise le
+ end
+
+ valid.activate
+ end
+ end
+
+ begin
+ gem_original_require(path)
+ rescue LoadError => load_error
+ if load_error.path == path &&
+ RUBYGEMS_ACTIVATION_MONITOR.synchronize { Gem.try_activate(path) }
+
+ return gem_original_require(path)
+ end
+
+ raise load_error
+ end
+ end
+
+ private :require
+end
diff --git a/lib/rubygems/core_ext/kernel_warn.rb b/lib/rubygems/core_ext/kernel_warn.rb
new file mode 100644
index 0000000000..f806b77fab
--- /dev/null
+++ b/lib/rubygems/core_ext/kernel_warn.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Kernel
+ rubygems_path = "#{__dir__}/" # Frames to be skipped start with this path.
+
+ original_warn = instance_method(:warn)
+
+ remove_method :warn
+
+ class << self
+ remove_method :warn
+ end
+
+ module_function define_method(:warn) {|*messages, **kw|
+ unless uplevel = kw[:uplevel]
+ return original_warn.bind_call(self, *messages, **kw)
+ end
+
+ # Ensure `uplevel` fits a `long`
+ uplevel, = [uplevel].pack("l!").unpack("l!")
+
+ if uplevel >= 0
+ start = 0
+ while uplevel >= 0
+ loc, = caller_locations(start, 1)
+ unless loc
+ # No more backtrace
+ start += uplevel
+ break
+ end
+
+ start += 1
+
+ next unless path = loc.path
+ unless path.start_with?(rubygems_path, "<internal:")
+ # Non-rubygems frames
+ uplevel -= 1
+ end
+ end
+ kw[:uplevel] = start
+ end
+
+ original_warn.bind_call(self, *messages, **kw)
+ }
+end
diff --git a/lib/rubygems/core_ext/tcpsocket_init.rb b/lib/rubygems/core_ext/tcpsocket_init.rb
new file mode 100644
index 0000000000..018c49dbeb
--- /dev/null
+++ b/lib/rubygems/core_ext/tcpsocket_init.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require "socket"
+
+module CoreExtensions
+ module TCPSocketExt
+ def self.prepended(base)
+ base.prepend Initializer
+ end
+
+ module Initializer
+ CONNECTION_TIMEOUT = 5
+ IPV4_DELAY_SECONDS = 0.1
+
+ def initialize(host, serv, *rest)
+ mutex = Thread::Mutex.new
+ addrs = []
+ threads = []
+ cond_var = Thread::ConditionVariable.new
+
+ Addrinfo.foreach(host, serv, nil, :STREAM) do |addr|
+ Thread.report_on_exception = false
+
+ threads << Thread.new(addr) do
+ # give head start to ipv6 addresses
+ sleep IPV4_DELAY_SECONDS if addr.ipv4?
+
+ # raises Errno::ECONNREFUSED when ip:port is unreachable
+ Socket.tcp(addr.ip_address, serv, connect_timeout: CONNECTION_TIMEOUT).close
+ mutex.synchronize do
+ addrs << addr.ip_address
+ cond_var.signal
+ end
+ end
+ end
+
+ mutex.synchronize do
+ timeout_time = CONNECTION_TIMEOUT + Time.now.to_f
+ while addrs.empty? && (remaining_time = timeout_time - Time.now.to_f) > 0
+ cond_var.wait(mutex, remaining_time)
+ end
+
+ host = addrs.shift unless addrs.empty?
+ end
+
+ threads.each {|t| t.kill.join if t.alive? }
+
+ super(host, serv, *rest)
+ end
+ end
+ end
+end
+
+TCPSocket.prepend CoreExtensions::TCPSocketExt
diff --git a/lib/rubygems/custom_require.rb b/lib/rubygems/custom_require.rb
deleted file mode 100755
index 78c7872b6f..0000000000
--- a/lib/rubygems/custom_require.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'rubygems'
-
-module Kernel
-
- ##
- # The Kernel#require from before RubyGems was loaded.
-
- alias gem_original_require require
-
- ##
- # When RubyGems is required, Kernel#require is replaced with our own which
- # is capable of loading gems on demand.
- #
- # When you call <tt>require 'x'</tt>, this is what happens:
- # * If the file can be loaded from the existing Ruby loadpath, it
- # is.
- # * Otherwise, installed gems are searched for a file that matches.
- # If it's found in gem 'y', that gem is activated (added to the
- # loadpath).
- #
- # The normal <tt>require</tt> functionality of returning false if
- # that file has already been loaded is preserved.
-
- def require(path) # :doc:
- gem_original_require path
- rescue LoadError => load_error
- if load_error.message =~ /#{Regexp.escape path}\z/ and
- spec = Gem.searcher.find(path) then
- Gem.activate(spec.name, "= #{spec.version}")
- gem_original_require path
- else
- raise load_error
- end
- end
-
- private :require
- private :gem_original_require
-
-end
-
diff --git a/lib/rubygems/defaults.rb b/lib/rubygems/defaults.rb
index 995b81e1b2..2247c49c81 100644
--- a/lib/rubygems/defaults.rb
+++ b/lib/rubygems/defaults.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
module Gem
+ DEFAULT_HOST = "https://rubygems.org"
- @post_install_hooks ||= []
+ @post_install_hooks ||= []
+ @done_installing_hooks ||= []
@post_uninstall_hooks ||= []
@pre_uninstall_hooks ||= []
@pre_install_hooks ||= []
@@ -9,7 +13,21 @@ module Gem
# An Array of the default sources that come with RubyGems
def self.default_sources
- %w[http://gems.rubyforge.org/]
+ @default_sources ||= %w[https://rubygems.org/]
+ end
+
+ ##
+ # Default spec directory path to be used if an alternate value is not
+ # specified in the environment
+
+ def self.default_spec_cache_dir
+ default_spec_cache_dir = File.join Gem.user_home, ".gem", "specs"
+
+ unless File.exist?(default_spec_cache_dir)
+ default_spec_cache_dir = File.join Gem.cache_home, "gem", "specs"
+ end
+
+ default_spec_cache_dir
end
##
@@ -17,72 +35,283 @@ module Gem
# specified in the environment
def self.default_dir
- if defined? RUBY_FRAMEWORK_VERSION then
- File.join File.dirname(ConfigMap[:sitedir]), 'Gems',
- ConfigMap[:ruby_version]
+ @default_dir ||= File.join(RbConfig::CONFIG["rubylibprefix"], "gems", RbConfig::CONFIG["ruby_version"])
+ end
+
+ ##
+ # Returns binary extensions dir for specified RubyGems base dir or nil
+ # if such directory cannot be determined.
+ #
+ # By default, the binary extensions are located side by side with their
+ # Ruby counterparts, therefore nil is returned
+
+ def self.default_ext_dir_for(base_dir)
+ nil
+ end
+
+ ##
+ # Paths where RubyGems' .rb files and bin files are installed
+
+ def self.default_rubygems_dirs
+ nil # default to standard layout
+ end
+
+ ##
+ # Path to specification files of default gems.
+
+ def self.default_specifications_dir
+ @default_specifications_dir ||= File.join(Gem.default_dir, "specifications", "default")
+ end
+
+ ##
+ # Finds the user's home directory.
+ #--
+ # Some comments from the ruby-talk list regarding finding the home
+ # directory:
+ #
+ # I have HOME, USERPROFILE and HOMEDRIVE + HOMEPATH. Ruby seems
+ # to be depending on HOME in those code samples. I propose that
+ # it should fallback to USERPROFILE and HOMEDRIVE + HOMEPATH (at
+ # least on Win32).
+ #++
+ #--
+ #
+ #++
+
+ def self.find_home
+ Dir.home.dup
+ rescue StandardError
+ if Gem.win_platform?
+ File.expand_path File.join(ENV["HOMEDRIVE"] || ENV["SystemDrive"], "/")
else
- ConfigMap[:sitelibdir].sub(%r'/site_ruby/(?=[^/]+)', '/gems/')
+ File.expand_path "/"
end
end
+ private_class_method :find_home
+
+ ##
+ # The home directory for the user.
+
+ def self.user_home
+ @user_home ||= find_home
+ end
+
##
# Path for gems in the user's home directory
def self.user_dir
- File.join(Gem.user_home, '.gem', ruby_engine,
- ConfigMap[:ruby_version])
+ gem_dir = File.join(Gem.user_home, ".gem")
+ gem_dir = File.join(Gem.data_home, "gem") unless File.exist?(gem_dir)
+ parts = [gem_dir, ruby_engine]
+ parts << RbConfig::CONFIG["ruby_version"] unless RbConfig::CONFIG["ruby_version"].empty?
+ File.join parts
+ end
+
+ ##
+ # The path to standard location of the user's configuration directory.
+
+ def self.config_home
+ @config_home ||= ENV["XDG_CONFIG_HOME"] || File.join(Gem.user_home, ".config")
+ end
+
+ ##
+ # Finds the user's config file
+
+ def self.find_config_file
+ gemrc = File.join Gem.user_home, ".gemrc"
+ if File.exist? gemrc
+ gemrc
+ else
+ File.join Gem.config_home, "gem", "gemrc"
+ end
+ end
+
+ ##
+ # The path to standard location of the user's .gemrc file.
+
+ def self.config_file
+ @config_file ||= find_config_file
+ end
+
+ ##
+ # The path to standard location of the user's state file.
+
+ def self.state_file
+ @state_file ||= File.join(Gem.state_home, "gem", "last_update_check")
+ end
+
+ ##
+ # The path to standard location of the user's cache directory.
+
+ def self.cache_home
+ @cache_home ||= ENV["XDG_CACHE_HOME"] || File.join(Gem.user_home, ".cache")
+ end
+
+ ##
+ # The path to the global gem cache directory.
+ # This is used when global_gem_cache is enabled to share .gem files
+ # across all Ruby installations.
+
+ def self.global_gem_cache_path
+ File.join(cache_home, "gem", "gems")
+ end
+
+ ##
+ # The path to standard location of the user's data directory.
+
+ def self.data_home
+ @data_home ||= ENV["XDG_DATA_HOME"] || File.join(Gem.user_home, ".local", "share")
+ end
+
+ ##
+ # The path to standard location of the user's state directory.
+
+ def self.state_home
+ @state_home ||= ENV["XDG_STATE_HOME"] || File.join(Gem.user_home, ".local", "state")
+ end
+
+ ##
+ # How String Gem paths should be split. Overridable for esoteric platforms.
+
+ def self.path_separator
+ File::PATH_SEPARATOR
end
##
# Default gem load path
def self.default_path
- [user_dir, default_dir]
+ path = []
+ path << user_dir if user_home && File.exist?(user_home)
+ path << default_dir
+ path << vendor_dir if vendor_dir && File.directory?(vendor_dir)
+ path
end
##
# Deduce Ruby's --program-prefix and --program-suffix from its install name
def self.default_exec_format
- baseruby = ConfigMap[:BASERUBY] || 'ruby'
- ConfigMap[:RUBY_INSTALL_NAME].sub(baseruby, '%s') rescue '%s'
+ exec_format = begin
+ RbConfig::CONFIG["ruby_install_name"].sub("ruby", "%s")
+ rescue StandardError
+ "%s"
+ end
+
+ unless exec_format.include?("%s")
+ raise Gem::Exception,
+ "[BUG] invalid exec_format #{exec_format.inspect}, no %s"
+ end
+
+ exec_format
end
##
# The default directory for binaries
def self.default_bindir
- if defined? RUBY_FRAMEWORK_VERSION then # mac framework support
- '/usr/bin'
- else # generic install
- ConfigMap[:bindir]
+ RbConfig::CONFIG["bindir"]
+ end
+
+ def self.ruby_engine
+ RUBY_ENGINE
+ end
+
+ ##
+ # The default signing key path
+
+ def self.default_key_path
+ default_key_path = File.join Gem.user_home, ".gem", "gem-private_key.pem"
+
+ unless File.exist?(default_key_path)
+ default_key_path = File.join Gem.data_home, "gem", "gem-private_key.pem"
end
+
+ default_key_path
end
##
- # The default system-wide source info cache directory
+ # The default signing certificate chain path
+
+ def self.default_cert_path
+ default_cert_path = File.join Gem.user_home, ".gem", "gem-public_cert.pem"
+
+ unless File.exist?(default_cert_path)
+ default_cert_path = File.join Gem.data_home, "gem", "gem-public_cert.pem"
+ end
- def self.default_system_source_cache_dir
- File.join Gem.dir, 'source_cache'
+ default_cert_path
end
##
- # The default user-specific source info cache directory
+ # Enables automatic installation into user directory
- def self.default_user_source_cache_dir
- File.join Gem.user_home, '.gem', 'source_cache'
+ def self.default_user_install # :nodoc:
+ if !ENV.key?("GEM_HOME") && File.exist?(Gem.dir) && !File.writable?(Gem.dir)
+ Gem.ui.say "Defaulting to user installation because default installation directory (#{Gem.dir}) is not writable."
+ return true
+ end
+
+ false
end
##
- # A wrapper around RUBY_ENGINE const that may not be defined
+ # Install extensions into lib as well as into the extension directory.
- def self.ruby_engine
- if defined? RUBY_ENGINE then
- RUBY_ENGINE
- else
- 'ruby'
+ def self.install_extension_in_lib # :nodoc:
+ Gem.configuration.install_extension_in_lib
+ end
+
+ ##
+ # Directory where vendor gems are installed.
+
+ def self.vendor_dir # :nodoc:
+ if vendor_dir = ENV["GEM_VENDOR"]
+ return vendor_dir.dup
end
+
+ return nil unless RbConfig::CONFIG.key? "vendordir"
+
+ File.join RbConfig::CONFIG["vendordir"], "gems",
+ RbConfig::CONFIG["ruby_version"]
end
-end
+ ##
+ # Default options for gem commands for Ruby packagers.
+ #
+ # The options here should be structured as an array of string "gem"
+ # command names as keys and a string of the default options as values.
+ #
+ # Example:
+ #
+ # def self.operating_system_defaults
+ # {
+ # 'install' => '--no-rdoc --no-ri --env-shebang',
+ # 'update' => '--no-rdoc --no-ri --env-shebang'
+ # }
+ # end
+ def self.operating_system_defaults
+ {}
+ end
+
+ ##
+ # Default options for gem commands for Ruby implementers.
+ #
+ # The options here should be structured as an array of string "gem"
+ # command names as keys and a string of the default options as values.
+ #
+ # Example:
+ #
+ # def self.platform_defaults
+ # {
+ # 'install' => '--no-rdoc --no-ri --env-shebang',
+ # 'update' => '--no-rdoc --no-ri --env-shebang'
+ # }
+ # end
+
+ def self.platform_defaults
+ {}
+ end
+end
diff --git a/lib/rubygems/dependency.rb b/lib/rubygems/dependency.rb
index 7b9904df55..1e91f493a6 100644
--- a/lib/rubygems/dependency.rb
+++ b/lib/rubygems/dependency.rb
@@ -1,26 +1,22 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'rubygems'
+# frozen_string_literal: true
##
-# The Dependency class holds a Gem name and a Gem::Requirement
+# The Dependency class holds a Gem name and a Gem::Requirement.
class Gem::Dependency
-
##
# Valid dependency types.
#--
# When this list is updated, be sure to change
# Gem::Specification::CURRENT_SPECIFICATION_VERSION as well.
+ #
+ # REFACTOR: This type of constant, TYPES, indicates we might want
+ # two classes, used via inheritance or duck typing.
TYPES = [
:development,
:runtime,
- ]
+ ].freeze
##
# Dependency name or regular expression.
@@ -28,92 +24,325 @@ class Gem::Dependency
attr_accessor :name
##
- # Dependency type.
+ # Allows you to force this dependency to be a prerelease.
- attr_reader :type
+ attr_writer :prerelease
##
- # Dependent versions.
+ # Constructs a dependency with +name+ and +requirements+. The last
+ # argument can optionally be the dependency type, which defaults to
+ # <tt>:runtime</tt>.
+
+ def initialize(name, *requirements)
+ case name
+ when String then # ok
+ when Regexp then
+ msg = ["NOTE: Dependency.new w/ a regexp is deprecated.",
+ "Dependency.new called from #{Gem.location_of_caller.join(":")}"]
+ warn msg.join("\n") unless Gem::Deprecate.skip
+ else
+ raise ArgumentError,
+ "dependency name must be a String, was #{name.inspect}"
+ end
- attr_writer :version_requirements
+ type = Symbol === requirements.last ? requirements.pop : :runtime
+ requirements = requirements.first if requirements.length == 1 # unpack
- ##
- # Orders dependencies by name only.
+ unless TYPES.include? type
+ raise ArgumentError, "Valid types are #{TYPES.inspect}, " \
+ "not #{type.inspect}"
+ end
- def <=>(other)
- [@name] <=> [other.name]
+ @name = name
+ @requirement = Gem::Requirement.create requirements
+ @type = type
+ @prerelease = false
+
+ # This is for Marshal backwards compatibility. See the comments in
+ # +requirement+ for the dirty details.
+
+ @version_requirements = @requirement
end
##
- # Constructs a dependency with +name+ and +requirements+.
+ # A dependency's hash is the XOR of the hashes of +name+, +type+,
+ # and +requirement+.
- def initialize(name, version_requirements, type=:runtime)
- @name = name
+ def hash # :nodoc:
+ name.hash ^ type.hash ^ requirement.hash
+ end
- unless TYPES.include? type
- raise ArgumentError, "Valid types are #{TYPES.inspect}, not #{@type.inspect}"
+ def inspect # :nodoc:
+ if prerelease?
+ format("<%s type=%p name=%p requirements=%p prerelease=ok>", self.class, type, name, requirement.to_s)
+ else
+ format("<%s type=%p name=%p requirements=%p>", self.class, type, name, requirement.to_s)
end
+ end
- @type = type
+ ##
+ # Does this dependency require a prerelease?
- @version_requirements = Gem::Requirement.create version_requirements
- @version_requirement = nil # Avoid warnings.
+ def prerelease?
+ @prerelease || requirement.prerelease?
end
- def version_requirements
- normalize if defined? @version_requirement and @version_requirement
- @version_requirements
+ ##
+ # Is this dependency simply asking for the latest version
+ # of a gem?
+
+ def latest_version?
+ @requirement.none?
end
- def requirement_list
- version_requirements.as_list
+ def pretty_print(q) # :nodoc:
+ q.group 1, "Gem::Dependency.new(", ")" do
+ q.pp name
+ q.text ","
+ q.breakable
+
+ q.pp requirement
+
+ q.text ","
+ q.breakable
+
+ q.pp type
+ end
end
- alias requirements_list requirement_list
+ ##
+ # What does this dependency require?
+
+ def requirement
+ return @requirement if defined?(@requirement) && @requirement
+
+ # @version_requirements and @version_requirement are legacy ivar
+ # names, and supported here because older gems need to keep
+ # working and Dependency doesn't implement marshal_dump and
+ # marshal_load. In a happier world, this would be an
+ # attr_accessor. The horrifying instance_variable_get you see
+ # below is also the legacy of some old restructurings.
+ #
+ # Note also that because of backwards compatibility (loading new
+ # gems in an old RubyGems installation), we can't add explicit
+ # marshaling to this class until we want to make a big
+ # break. Maybe 2.0.
+ #
+ # Children, define explicit marshal and unmarshal behavior for
+ # public classes. Marshal formats are part of your public API.
+
+ # REFACTOR: See above
+
+ if defined?(@version_requirement) && @version_requirement
+ version = @version_requirement.instance_variable_get :@version
+ @version_requirement = nil
+ @version_requirements = Gem::Requirement.new version
+ end
- def normalize
- ver = @version_requirement.instance_eval { @version }
- @version_requirements = Gem::Requirement.new([ver])
- @version_requirement = nil
+ @requirement = @version_requirements if defined?(@version_requirements)
+ end
+
+ def requirements_list
+ requirement.as_list
end
def to_s # :nodoc:
- "#{name} (#{version_requirements}, #{@type || :runtime})"
+ if type != :runtime
+ "#{name} (#{requirement}, #{type})"
+ else
+ "#{name} (#{requirement})"
+ end
+ end
+
+ ##
+ # Dependency type.
+
+ def type
+ @type ||= :runtime
+ end
+
+ def runtime?
+ @type == :runtime || !@type
end
def ==(other) # :nodoc:
- self.class === other &&
- self.name == other.name &&
- self.type == other.type &&
- self.version_requirements == other.version_requirements
+ Gem::Dependency === other &&
+ name == other.name &&
+ type == other.type &&
+ requirement == other.requirement
end
##
- # Uses this dependency as a pattern to compare to the dependency +other+.
- # This dependency will match if the name matches the other's name, and other
- # has only an equal version requirement that satisfies this dependency.
+ # Dependencies are ordered by name.
- def =~(other)
- return false unless self.class === other
+ def <=>(other)
+ name <=> other.name
+ end
+
+ ##
+ # Uses this dependency as a pattern to compare to +other+. This
+ # dependency will match if the name matches the other's name, and
+ # other has only an equal version requirement that satisfies this
+ # dependency.
- pattern = @name
- pattern = /\A#{@name}\Z/ unless Regexp === pattern
+ def =~(other)
+ unless Gem::Dependency === other
+ return unless other.respond_to?(:name) && other.respond_to?(:version)
+ other = Gem::Dependency.new other.name, other.version
+ end
- return false unless pattern =~ other.name
+ return false unless name === other.name
- reqs = other.version_requirements.requirements
+ reqs = other.requirement.requirements
return false unless reqs.length == 1
- return false unless reqs.first.first == '='
+ return false unless reqs.first.first == "="
version = reqs.first.last
- version_requirements.satisfied_by? version
+ requirement.satisfied_by? version
end
- def hash # :nodoc:
- name.hash + type.hash + version_requirements.hash
+ alias_method :===, :=~
+
+ ##
+ # :call-seq:
+ # dep.match? name => true or false
+ # dep.match? name, version => true or false
+ # dep.match? spec => true or false
+ #
+ # Does this dependency match the specification described by +name+ and
+ # +version+ or match +spec+?
+ #
+ # NOTE: Unlike #matches_spec? this method does not return true when the
+ # version is a prerelease version unless this is a prerelease dependency.
+
+ def match?(obj, version = nil, allow_prerelease = false)
+ if !version
+ name = obj.name
+ version = obj.version
+ else
+ name = obj
+ end
+
+ return false unless self.name === name
+
+ version = Gem::Version.new version
+
+ return true if requirement.none? && !version.prerelease?
+ return false if version.prerelease? &&
+ !allow_prerelease &&
+ !prerelease?
+
+ requirement.satisfied_by? version
end
-end
+ ##
+ # Does this dependency match +spec+?
+ #
+ # NOTE: This is not a convenience method. Unlike #match? this method
+ # returns true when +spec+ is a prerelease version even if this dependency
+ # is not a prerelease dependency.
+
+ def matches_spec?(spec)
+ return false unless name === spec.name
+ return true if requirement.none?
+
+ requirement.satisfied_by?(spec.version)
+ end
+
+ ##
+ # Merges the requirements of +other+ into this dependency
+
+ def merge(other)
+ unless name == other.name
+ raise ArgumentError,
+ "#{self} and #{other} have different names"
+ end
+
+ default = Gem::Requirement.default
+ self_req = requirement
+ other_req = other.requirement
+
+ return self.class.new name, self_req if other_req == default
+ return self.class.new name, other_req if self_req == default
+
+ self.class.new name, self_req.as_list.concat(other_req.as_list)
+ end
+
+ def matching_specs(platform_only = false)
+ matches = Gem::Specification.find_all_by_name(name, requirement)
+
+ if platform_only
+ matches.reject! do |spec|
+ spec.nil? || !Gem::Platform.match_spec?(spec)
+ end
+ end
+
+ matches.reject(&:ignored?)
+ end
+
+ ##
+ # True if the dependency will not always match the latest version.
+
+ def specific?
+ @requirement.specific?
+ end
+
+ def to_specs
+ matches = matching_specs true
+
+ # TODO: check Gem.activated_spec[self.name] in case matches falls outside
+
+ if matches.empty?
+ specs = Gem::Specification.stubs_for name
+
+ if specs.empty?
+ raise Gem::MissingSpecError.new name, requirement
+ else
+ raise Gem::MissingSpecVersionError.new name, requirement, specs
+ end
+ end
+
+ # TODO: any other resolver validations should go here
+
+ matches
+ end
+ def to_spec
+ matches = to_specs.compact
+
+ active = matches.find(&:activated?)
+ return active if active
+
+ unless prerelease?
+ # Consider prereleases only as a fallback
+ pre, matches = matches.partition {|spec| spec.version.prerelease? }
+ matches = pre if matches.empty?
+ end
+
+ matches.first
+ end
+
+ def identity
+ if prerelease?
+ if specific?
+ :complete
+ else
+ :abs_latest
+ end
+ elsif latest_version?
+ :latest
+ else
+ :released
+ end
+ end
+
+ def encode_with(coder) # :nodoc:
+ coder.add "name", @name
+ coder.add "requirement", @requirement
+ coder.add "type", @type
+ coder.add "prerelease", @prerelease
+ coder.add "version_requirements", @version_requirements
+ end
+end
diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb
index 9ae2659536..c842714d95 100644
--- a/lib/rubygems/dependency_installer.rb
+++ b/lib/rubygems/dependency_installer.rb
@@ -1,28 +1,47 @@
-require 'rubygems'
-require 'rubygems/dependency_list'
-require 'rubygems/installer'
-require 'rubygems/spec_fetcher'
-require 'rubygems/user_interaction'
+# frozen_string_literal: true
+
+require_relative "../rubygems"
+require_relative "dependency_list"
+require_relative "package"
+require_relative "installer"
+require_relative "spec_fetcher"
+require_relative "user_interaction"
+require_relative "available_set"
##
# Installs a gem along with all its dependencies from local and remote gems.
class Gem::DependencyInstaller
-
include Gem::UserInteraction
- attr_reader :gems_to_install
- attr_reader :installed_gems
+ DEFAULT_OPTIONS = { # :nodoc:
+ env_shebang: false,
+ document: %w[ri],
+ domain: :both, # HACK: dup
+ force: false,
+ format_executable: false, # HACK: dup
+ ignore_dependencies: false,
+ prerelease: false,
+ security_policy: nil, # HACK: NoSecurity requires OpenSSL. AlmostNo? Low?
+ wrappers: true,
+ build_args: nil,
+ build_docs_in_background: false,
+ }.freeze
+
+ ##
+ # Documentation types. For use by the Gem.done_installing hook
+
+ attr_reader :document
+
+ ##
+ # Errors from SpecFetcher while searching for remote specifications
- DEFAULT_OPTIONS = {
- :env_shebang => false,
- :domain => :both, # HACK dup
- :force => false,
- :format_executable => false, # HACK dup
- :ignore_dependencies => false,
- :security_policy => nil, # HACK NoSecurity requires OpenSSL. AlmostNo? Low?
- :wrappers => true
- }
+ attr_reader :errors
+
+ ##
+ # List of gems installed by #install in alphabetic order
+
+ attr_reader :installed_gems
##
# Creates a new installer instance.
@@ -37,222 +56,209 @@ class Gem::DependencyInstaller
# :format_executable:: See Gem::Installer#initialize.
# :ignore_dependencies:: Don't install any dependencies.
# :install_dir:: See Gem::Installer#install.
+ # :prerelease:: Allow prerelease versions. See #install.
# :security_policy:: See Gem::Installer::new and Gem::Security.
# :user_install:: See Gem::Installer.new
# :wrappers:: See Gem::Installer::new
+ # :build_args:: See Gem::Installer::new
def initialize(options = {})
- if options[:install_dir] then
- spec_dir = options[:install_dir], 'specifications'
- @source_index = Gem::SourceIndex.from_gems_in spec_dir
- else
- @source_index = Gem.source_index
- end
+ @only_install_dir = !options[:install_dir].nil?
+ @install_dir = options[:install_dir] || Gem.dir
+ @build_root = options[:build_root]
options = DEFAULT_OPTIONS.merge options
- @bin_dir = options[:bin_dir]
- @development = options[:development]
- @domain = options[:domain]
- @env_shebang = options[:env_shebang]
- @force = options[:force]
- @format_executable = options[:format_executable]
+ @bin_dir = options[:bin_dir]
+ @dev_shallow = options[:dev_shallow]
+ @development = options[:development]
+ @document = options[:document]
+ @domain = options[:domain]
+ @env_shebang = options[:env_shebang]
+ @force = options[:force]
+ @format_executable = options[:format_executable]
@ignore_dependencies = options[:ignore_dependencies]
- @security_policy = options[:security_policy]
- @user_install = options[:user_install]
- @wrappers = options[:wrappers]
-
+ @prerelease = options[:prerelease]
+ @security_policy = options[:security_policy]
+ @user_install = options[:user_install]
+ @wrappers = options[:wrappers]
+ @build_args = options[:build_args]
+ @build_jobs = options[:build_jobs]
+ @build_docs_in_background = options[:build_docs_in_background]
+ @dir_mode = options[:dir_mode]
+ @data_mode = options[:data_mode]
+ @prog_mode = options[:prog_mode]
+ @build_extension = options[:build_extension]
+ @install_plugin = options[:install_plugin]
+
+ # Indicates that we should not try to update any deps unless
+ # we absolutely must.
+ @minimal_deps = options[:minimal_deps]
+
+ @available = nil
@installed_gems = []
+ @toplevel_specs = nil
- @install_dir = options[:install_dir] || Gem.dir
@cache_dir = options[:cache_dir] || @install_dir
+
+ @errors = []
end
##
- # Returns a list of pairs of gemspecs and source_uris that match
- # Gem::Dependency +dep+ from both local (Dir.pwd) and remote (Gem.sources)
- # sources. Gems are sorted with newer gems prefered over older gems, and
- # local gems preferred over remote gems.
-
- def find_gems_with_sources(dep)
- gems_and_sources = []
-
- if @domain == :both or @domain == :local then
- Dir[File.join(Dir.pwd, "#{dep.name}-[0-9]*.gem")].each do |gem_file|
- spec = Gem::Format.from_file_by_path(gem_file).spec
- gems_and_sources << [spec, gem_file] if spec.name == dep.name
- end
- end
+ # Indicated, based on the requested domain, if local
+ # gems should be considered.
- if @domain == :both or @domain == :remote then
- begin
- requirements = dep.version_requirements.requirements.map do |req, ver|
- req
- end
+ def consider_local?
+ @domain == :both || @domain == :local
+ end
- all = requirements.length > 1 ||
- (requirements.first != ">=" and requirements.first != ">")
+ ##
+ # Indicated, based on the requested domain, if remote
+ # gems should be considered.
- found = Gem::SpecFetcher.fetcher.fetch dep, all
- gems_and_sources.push(*found)
+ def consider_remote?
+ @domain == :both || @domain == :remote
+ end
- rescue Gem::RemoteFetcher::FetchError => e
- if Gem.configuration.really_verbose then
- say "Error fetching remote data:\t\t#{e.message}"
- say "Falling back to local-only install"
+ def in_background(what) # :nodoc:
+ fork_happened = false
+ if @build_docs_in_background && Process.respond_to?(:fork)
+ begin
+ Process.fork do
+ yield
end
- @domain = :local
+ fork_happened = true
+ say "#{what} in a background process."
+ rescue NotImplementedError
end
end
-
- gems_and_sources.sort_by do |gem, source|
- [gem, source =~ /^http:\/\// ? 0 : 1] # local gems win
- end
+ yield unless fork_happened
end
##
- # Gathers all dependencies necessary for the installation from local and
- # remote sources unless the ignore_dependencies was given.
-
- def gather_dependencies
- specs = @specs_and_sources.map { |spec,_| spec }
-
- dependency_list = Gem::DependencyList.new
- dependency_list.add(*specs)
+ # Installs the gem +dep_or_name+ and all its dependencies. Returns an Array
+ # of installed gem specifications.
+ #
+ # If the +:prerelease+ option is set and there is a prerelease for
+ # +dep_or_name+ the prerelease version will be installed.
+ #
+ # Unless explicitly specified as a prerelease dependency, prerelease gems
+ # that +dep_or_name+ depend on will not be installed.
+ #
+ # If c-1.a depends on b-1 and a-1.a and there is a gem b-1.a available then
+ # c-1.a, b-1 and a-1.a will be installed. b-1.a will need to be installed
+ # separately.
- unless @ignore_dependencies then
- to_do = specs.dup
- seen = {}
+ def install(dep_or_name, version = Gem::Requirement.default)
+ request_set = resolve_dependencies dep_or_name, version
- until to_do.empty? do
- spec = to_do.shift
- next if spec.nil? or seen[spec.name]
- seen[spec.name] = true
+ @installed_gems = []
- deps = spec.runtime_dependencies
- deps |= spec.development_dependencies if @development
+ options = {
+ bin_dir: @bin_dir,
+ build_args: @build_args,
+ build_jobs: @build_jobs,
+ document: @document,
+ env_shebang: @env_shebang,
+ force: @force,
+ format_executable: @format_executable,
+ ignore_dependencies: @ignore_dependencies,
+ prerelease: @prerelease,
+ security_policy: @security_policy,
+ user_install: @user_install,
+ wrappers: @wrappers,
+ build_root: @build_root,
+ dir_mode: @dir_mode,
+ data_mode: @data_mode,
+ prog_mode: @prog_mode,
+ build_extension: @build_extension,
+ install_plugin: @install_plugin,
+ }
+ options[:install_dir] = @install_dir if @only_install_dir
+
+ request_set.install options do |_, installer|
+ @installed_gems << installer.spec if installer
+ end
- deps.each do |dep|
- results = find_gems_with_sources(dep).reverse
+ @installed_gems.sort!
- results.reject! do |dep_spec,|
- to_do.push dep_spec
+ # Since this is currently only called for docs, we can be lazy and just say
+ # it's documentation. Ideally the hook adder could decide whether to be in
+ # the background or not, and what to call it.
+ in_background "Installing documentation" do
+ Gem.done_installing_hooks.each do |hook|
+ hook.call self, @installed_gems
+ end
+ end unless Gem.done_installing_hooks.empty?
- @source_index.any? do |_, installed_spec|
- dep.name == installed_spec.name and
- dep.version_requirements.satisfied_by? installed_spec.version
- end
- end
+ @installed_gems
+ end
- results.each do |dep_spec, source_uri|
- next if seen[dep_spec.name]
- @specs_and_sources << [dep_spec, source_uri]
- dependency_list.add dep_spec
- end
- end
- end
+ def install_development_deps # :nodoc:
+ if @development && @dev_shallow
+ :shallow
+ elsif @development
+ :all
+ else
+ :none
end
-
- @gems_to_install = dependency_list.dependency_order.reverse
end
- ##
- # Finds a spec and the source_uri it came from for gem +gem_name+ and
- # +version+. Returns an Array of specs and sources required for
- # installation of the gem.
-
- def find_spec_by_name_and_version gem_name, version = Gem::Requirement.default
- spec_and_source = nil
-
- glob = if File::ALT_SEPARATOR then
- gem_name.gsub File::ALT_SEPARATOR, File::SEPARATOR
- else
- gem_name
- end
-
- local_gems = Dir["#{glob}*"].sort.reverse
-
- unless local_gems.empty? then
- local_gems.each do |gem_file|
- next unless gem_file =~ /gem$/
- begin
- spec = Gem::Format.from_file_by_path(gem_file).spec
- spec_and_source = [spec, gem_file]
- break
- rescue SystemCallError, Gem::Package::FormatError
+ def resolve_dependencies(dep_or_name, version) # :nodoc:
+ request_set = Gem::RequestSet.new
+ request_set.development = @development
+ request_set.development_shallow = @dev_shallow
+ request_set.soft_missing = @force
+ request_set.prerelease = @prerelease
+
+ installer_set = Gem::Resolver::InstallerSet.new @domain
+ installer_set.ignore_installed = (@minimal_deps == false) || @only_install_dir
+ installer_set.force = @force
+
+ if consider_local?
+ if dep_or_name =~ /\.gem$/ && File.file?(dep_or_name)
+ src = Gem::Source::SpecificFile.new dep_or_name
+ installer_set.add_local dep_or_name, src.spec, src
+ version = src.spec.version if version == Gem::Requirement.default
+ elsif dep_or_name =~ /\.gem$/ # rubocop:disable Performance/RegexpMatch
+ Dir[dep_or_name].each do |name|
+ src = Gem::Source::SpecificFile.new name
+ installer_set.add_local dep_or_name, src.spec, src
+ rescue Gem::Package::FormatError
end
+ # else This is a dependency. InstallerSet handles this case
end
end
- if spec_and_source.nil? then
- dep = Gem::Dependency.new gem_name, version
- spec_and_sources = find_gems_with_sources(dep).reverse
+ dependency =
+ if spec = installer_set.local?(dep_or_name)
+ installer_set.remote = nil if spec.dependencies.none?
+ Gem::Dependency.new spec.name, version
+ elsif String === dep_or_name
+ Gem::Dependency.new dep_or_name, version
+ else
+ dep_or_name
+ end
- spec_and_source = spec_and_sources.find { |spec, source|
- Gem::Platform.match spec.platform
- }
- end
+ dependency.prerelease = @prerelease
- if spec_and_source.nil? then
- raise Gem::GemNotFoundException,
- "could not find gem #{gem_name} locally or in a repository"
- end
+ request_set.import [dependency]
- @specs_and_sources = [spec_and_source]
- end
+ installer_set.add_always_install dependency
- ##
- # Installs the gem and all its dependencies. Returns an Array of installed
- # gems specifications.
+ request_set.always_install = installer_set.always_install
+ request_set.remote = installer_set.consider_remote?
- def install dep_or_name, version = Gem::Requirement.default
- if String === dep_or_name then
- find_spec_by_name_and_version dep_or_name, version
- else
- @specs_and_sources = [find_gems_with_sources(dep_or_name).last]
+ if @ignore_dependencies
+ installer_set.ignore_dependencies = true
+ request_set.ignore_dependencies = true
+ request_set.soft_missing = true
end
- @installed_gems = []
-
- gather_dependencies
+ request_set.resolve installer_set
- @gems_to_install.each do |spec|
- last = spec == @gems_to_install.last
- # HACK is this test for full_name acceptable?
- next if @source_index.any? { |n,_| n == spec.full_name } and not last
+ @errors.concat request_set.errors
- # TODO: make this sorta_verbose so other users can benefit from it
- say "Installing gem #{spec.full_name}" if Gem.configuration.really_verbose
-
- _, source_uri = @specs_and_sources.assoc spec
- begin
- local_gem_path = Gem::RemoteFetcher.fetcher.download spec, source_uri,
- @cache_dir
- rescue Gem::RemoteFetcher::FetchError
- next if @force
- raise
- end
-
- inst = Gem::Installer.new local_gem_path,
- :bin_dir => @bin_dir,
- :development => @development,
- :env_shebang => @env_shebang,
- :force => @force,
- :format_executable => @format_executable,
- :ignore_dependencies => @ignore_dependencies,
- :install_dir => @install_dir,
- :security_policy => @security_policy,
- :source_index => @source_index,
- :user_install => @user_install,
- :wrappers => @wrappers
-
- spec = inst.install
-
- @installed_gems << spec
- end
-
- @installed_gems
+ request_set
end
-
end
-
diff --git a/lib/rubygems/dependency_list.rb b/lib/rubygems/dependency_list.rb
index a129743914..d50cfe2d54 100644
--- a/lib/rubygems/dependency_list.rb
+++ b/lib/rubygems/dependency_list.rb
@@ -1,47 +1,77 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'tsort'
+require_relative "vendored_tsort"
+
+##
+# Gem::DependencyList is used for installing and uninstalling gems in the
+# correct order to avoid conflicts.
+#--
+# TODO: It appears that all but topo-sort functionality is being duplicated
+# (or is planned to be duplicated) elsewhere in rubygems. Is the majority of
+# this class necessary anymore? Especially #ok?, #why_not_ok?
class Gem::DependencyList
+ attr_reader :specs
- include TSort
+ include Enumerable
+ include Gem::TSort
- def self.from_source_index(src_index)
- deps = new
+ ##
+ # Allows enabling/disabling use of development dependencies
- src_index.each do |full_name, spec|
- deps.add spec
- end
+ attr_accessor :development
+
+ ##
+ # Creates a DependencyList from the current specs.
- deps
+ def self.from_specs
+ list = new
+ list.add(*Gem::Specification.to_a)
+ list
end
- def initialize
+ ##
+ # Creates a new DependencyList. If +development+ is true, development
+ # dependencies will be included.
+
+ def initialize(development = false)
@specs = []
+
+ @development = development
end
+ ##
# Adds +gemspecs+ to the dependency list.
+
def add(*gemspecs)
- @specs.push(*gemspecs)
+ @specs.concat gemspecs
end
- # Return a list of the specifications in the dependency list,
- # sorted in order so that no spec in the list depends on a gem
- # earlier in the list.
+ def clear
+ @specs.clear
+ end
+
+ ##
+ # Return a list of the gem specifications in the dependency list, sorted in
+ # order so that no gemspec in the list depends on a gemspec earlier in the
+ # list.
#
- # This is useful when removing gems from a set of installed gems.
- # By removing them in the returned order, you don't get into as
- # many dependency issues.
+ # This is useful when removing gems from a set of installed gems. By
+ # removing them in the returned order, you don't get into as many dependency
+ # issues.
#
- # If there are circular dependencies (yuck!), then gems will be
- # returned in order until only the circular dependents and anything
- # they reference are left. Then arbitrary gemspecs will be returned
- # until the circular dependency is broken, after which gems will be
- # returned in dependency order again.
+ # If there are circular dependencies (yuck!), then gems will be returned in
+ # order until only the circular dependents and anything they reference are
+ # left. Then arbitrary gemspecs will be returned until the circular
+ # dependency is broken, after which gems will be returned in dependency
+ # order again.
+
def dependency_order
sorted = strongly_connected_components.flatten
@@ -49,8 +79,8 @@ class Gem::DependencyList
seen = {}
sorted.each do |spec|
- if index = seen[spec.name] then
- if result[index].version < spec.version then
+ if index = seen[spec.name]
+ if result[index].version < spec.version
result[index] = spec
end
else
@@ -62,55 +92,106 @@ class Gem::DependencyList
result.reverse
end
+ ##
+ # Iterator over dependency_order
+
+ def each(&block)
+ dependency_order.each(&block)
+ end
+
def find_name(full_name)
- @specs.find { |spec| spec.full_name == full_name }
+ @specs.find {|spec| spec.full_name == full_name }
end
+ def inspect # :nodoc:
+ format("%s %p>", super[0..-2], map(&:full_name))
+ end
+
+ ##
# Are all the dependencies in the list satisfied?
+
def ok?
- @specs.all? do |spec|
- spec.runtime_dependencies.all? do |dep|
- @specs.find { |s| s.satisfies_requirement? dep }
+ why_not_ok?(:quick).empty?
+ end
+
+ def why_not_ok?(quick = false)
+ unsatisfied = Hash.new {|h,k| h[k] = [] }
+ each do |spec|
+ spec.runtime_dependencies.each do |dep|
+ inst = Gem::Specification.any? do |installed_spec|
+ dep.name == installed_spec.name &&
+ dep.requirement.satisfied_by?(installed_spec.version)
+ end
+
+ unless inst || @specs.find {|s| s.satisfies_requirement? dep }
+ unsatisfied[spec.name] << dep
+ return unsatisfied if quick
+ end
end
end
+
+ unsatisfied
end
- # Is is ok to remove a gem from the dependency list?
+ ##
+ # It is ok to remove a gemspec from the dependency list?
#
- # If removing the gemspec creates breaks a currently ok dependency,
- # then it is NOT ok to remove the gem.
- def ok_to_remove?(full_name)
+ # If removing the gemspec creates breaks a currently ok dependency, then it
+ # is NOT ok to remove the gemspec.
+
+ def ok_to_remove?(full_name, check_dev = true)
gem_to_remove = find_name full_name
- siblings = @specs.find_all { |s|
+ # If the state is inconsistent, at least don't crash
+ return true unless gem_to_remove
+
+ siblings = @specs.find_all do |s|
s.name == gem_to_remove.name &&
s.full_name != gem_to_remove.full_name
- }
+ end
deps = []
@specs.each do |spec|
- spec.dependencies.each do |dep|
+ check = check_dev ? spec.dependencies : spec.runtime_dependencies
+
+ check.each do |dep|
deps << dep if gem_to_remove.satisfies_requirement?(dep)
end
end
- deps.all? { |dep|
- siblings.any? { |s|
+ deps.all? do |dep|
+ siblings.any? do |s|
s.satisfies_requirement? dep
- }
- }
+ end
+ end
+ end
+
+ ##
+ # Remove everything in the DependencyList that matches but doesn't
+ # satisfy items in +dependencies+ (a hash of gem names to arrays of
+ # dependencies).
+
+ def remove_specs_unsatisfied_by(dependencies)
+ specs.reject! do |spec|
+ dep = dependencies[spec.name]
+ dep && !dep.requirement.satisfied_by?(spec.version)
+ end
end
+ ##
+ # Removes the gemspec matching +full_name+ from the dependency list
+
def remove_by_name(full_name)
- @specs.delete_if { |spec| spec.full_name == full_name }
+ @specs.delete_if {|spec| spec.full_name == full_name }
end
- # Return a hash of predecessors. <tt>result[spec]</tt> is an
- # Array of gemspecs that have a dependency satisfied by the named
- # spec.
+ ##
+ # Return a hash of predecessors. <tt>result[spec]</tt> is an Array of
+ # gemspecs that have a dependency satisfied by the named gemspec.
+
def spec_predecessors
- result = Hash.new { |h,k| h[k] = [] }
+ result = Hash.new {|h,k| h[k] = [] }
specs = @specs.sort.reverse
@@ -119,7 +200,7 @@ class Gem::DependencyList
next if spec == other
other.dependencies.each do |dep|
- if spec.satisfies_requirement? dep then
+ if spec.satisfies_requirement? dep
result[spec] << other
end
end
@@ -133,16 +214,16 @@ class Gem::DependencyList
@specs.each(&block)
end
- def tsort_each_child(node, &block)
+ def tsort_each_child(node)
specs = @specs.sort.reverse
- node.dependencies.each do |dep|
+ dependencies = node.runtime_dependencies
+ dependencies.push(*node.development_dependencies) if @development
+
+ dependencies.each do |dep|
specs.each do |spec|
- if spec.satisfies_requirement? dep then
- begin
- yield spec
- rescue TSort::Cyclic
- end
+ if spec.satisfies_requirement? dep
+ yield spec
break
end
end
@@ -151,15 +232,11 @@ class Gem::DependencyList
private
+ ##
# Count the number of gemspecs in the list +specs+ that are not in
# +ignored+.
+
def active_count(specs, ignored)
- result = 0
- specs.each do |spec|
- result += 1 unless ignored[spec.full_name]
- end
- result
+ specs.count {|spec| ignored[spec.full_name].nil? }
end
-
end
-
diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb
new file mode 100644
index 0000000000..eb503bb269
--- /dev/null
+++ b/lib/rubygems/deprecate.rb
@@ -0,0 +1,171 @@
+# frozen_string_literal: true
+
+module Gem
+ ##
+ # Provides 3 methods for declaring when something is going away.
+ #
+ # <tt>deprecate(name, repl, year, month)</tt>:
+ # Indicate something may be removed on/after a certain date.
+ #
+ # <tt>rubygems_deprecate(name, replacement=:none)</tt>:
+ # Indicate something will be removed in the next major RubyGems version,
+ # and (optionally) a replacement for it.
+ #
+ # +rubygems_deprecate_command+:
+ # Indicate a RubyGems command (in +lib/rubygems/commands/*.rb+) will be
+ # removed in the next RubyGems version.
+ #
+ # Also provides +skip_during+ for temporarily turning off deprecation warnings.
+ # This is intended to be used in the test suite, so deprecation warnings
+ # don't cause test failures if you need to make sure stderr is otherwise empty.
+ #
+ #
+ # Example usage of +deprecate+ and +rubygems_deprecate+:
+ #
+ # class Legacy
+ # def self.some_class_method
+ # # ...
+ # end
+ #
+ # def some_instance_method
+ # # ...
+ # end
+ #
+ # def some_old_method
+ # # ...
+ # end
+ #
+ # extend Gem::Deprecate
+ # deprecate :some_instance_method, "X.z", 2011, 4
+ # rubygems_deprecate :some_old_method, "Modern#some_new_method"
+ #
+ # class << self
+ # extend Gem::Deprecate
+ # deprecate :some_class_method, :none, 2011, 4
+ # end
+ # end
+ #
+ #
+ # Example usage of +rubygems_deprecate_command+:
+ #
+ # class Gem::Commands::QueryCommand < Gem::Command
+ # extend Gem::Deprecate
+ # rubygems_deprecate_command
+ #
+ # # ...
+ # end
+ #
+ #
+ # Example usage of +skip_during+:
+ #
+ # class TestSomething < Gem::Testcase
+ # def test_some_thing_with_deprecations
+ # Gem::Deprecate.skip_during do
+ # actual_stdout, actual_stderr = capture_output do
+ # Gem.something_deprecated
+ # end
+ # assert_empty actual_stdout
+ # assert_equal(expected, actual_stderr)
+ # end
+ # end
+ # end
+
+ module Deprecate
+ def self.skip # :nodoc:
+ @skip ||= false
+ end
+
+ def self.skip=(v) # :nodoc:
+ @skip = v
+ end
+
+ ##
+ # Temporarily turn off warnings. Intended for tests only.
+
+ def skip_during
+ original = Gem::Deprecate.skip
+ Gem::Deprecate.skip = true
+ yield
+ ensure
+ Gem::Deprecate.skip = original
+ end
+
+ def self.next_rubygems_major_version # :nodoc:
+ Gem::Version.new(Gem.rubygems_version.segments.first).bump
+ end
+
+ ##
+ # Simple deprecation method that deprecates +name+ by wrapping it up
+ # in a dummy method. It warns on each call to the dummy method
+ # telling the user of +repl+ (unless +repl+ is :none) and the
+ # year/month that it is planned to go away.
+
+ def deprecate(name, repl, year, month)
+ class_eval do
+ old = "_deprecated_#{name}"
+ alias_method old, name
+ define_method name do |*args, &block|
+ klass = is_a? Module
+ target = klass ? "#{self}." : "#{self.class}#"
+ msg = [
+ "NOTE: #{target}#{name} is deprecated",
+ repl == :none ? " with no replacement" : "; use #{repl} instead",
+ format(". It will be removed on or after %4d-%02d.", year, month),
+ "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}",
+ ]
+ warn "#{msg.join}." unless Gem::Deprecate.skip
+ send old, *args, &block
+ end
+ ruby2_keywords name if respond_to?(:ruby2_keywords, true)
+ end
+ end
+
+ ##
+ # Simple deprecation method that deprecates +name+ by wrapping it up
+ # in a dummy method. It warns on each call to the dummy method
+ # telling the user of +repl+ (unless +repl+ is :none) and the
+ # Rubygems version that it is planned to go away.
+
+ def rubygems_deprecate(name, replacement = :none, version = nil)
+ class_eval do
+ old = "_deprecated_#{name}"
+ alias_method old, name
+ define_method name do |*args, &block|
+ klass = is_a? Module
+ target = klass ? "#{self}." : "#{self.class}#"
+ version ||= Gem::Deprecate.next_rubygems_major_version
+ msg = [
+ "NOTE: #{target}#{name} is deprecated",
+ replacement == :none ? " with no replacement" : "; use #{replacement} instead",
+ ". It will be removed in Rubygems #{version}",
+ "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}",
+ ]
+ warn "#{msg.join}." unless Gem::Deprecate.skip
+ send old, *args, &block
+ end
+ ruby2_keywords name if respond_to?(:ruby2_keywords, true)
+ end
+ end
+
+ # Deprecation method to deprecate Rubygems commands
+ def rubygems_deprecate_command(version = nil)
+ class_eval do
+ define_method "deprecated?" do
+ true
+ end
+
+ define_method "deprecation_warning" do
+ version ||= Gem::Deprecate.next_rubygems_major_version
+ msg = [
+ "#{command} command is deprecated",
+ ". It will be removed in Rubygems #{version}.\n",
+ ]
+
+ alert_warning msg.join.to_s unless Gem::Deprecate.skip
+ end
+ end
+ end
+
+ module_function :rubygems_deprecate, :rubygems_deprecate_command, :skip_during
+ end
+end
diff --git a/lib/rubygems/digest/digest_adapter.rb b/lib/rubygems/digest/digest_adapter.rb
deleted file mode 100755
index d5a00b059d..0000000000
--- a/lib/rubygems/digest/digest_adapter.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/usr/bin/env ruby
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-module Gem
-
- # There is an incompatibility between the way Ruby 1.8.5 and 1.8.6
- # handles digests. This DigestAdapter will take a pre-1.8.6 digest
- # and adapt it to the 1.8.6 API.
- #
- # Note that only the digest and hexdigest methods are adapted,
- # since these are the only functions used by Gems.
- #
- class DigestAdapter
-
- # Initialize a digest adapter.
- def initialize(digest_class)
- @digest_class = digest_class
- end
-
- # Return a new digester. Since we are only implementing the stateless
- # methods, we will return ourself as the instance.
- def new
- self
- end
-
- # Return the digest of +string+ as a hex string.
- def hexdigest(string)
- @digest_class.new(string).hexdigest
- end
-
- # Return the digest of +string+ as a binary string.
- def digest(string)
- @digest_class.new(string).digest
- end
- end
-end \ No newline at end of file
diff --git a/lib/rubygems/digest/md5.rb b/lib/rubygems/digest/md5.rb
deleted file mode 100755
index f924579c08..0000000000
--- a/lib/rubygems/digest/md5.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env ruby
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'digest/md5'
-
-# :stopdoc:
-module Gem
- if RUBY_VERSION >= '1.8.6'
- MD5 = Digest::MD5
- else
- require 'rubygems/digest/digest_adapter'
- MD5 = DigestAdapter.new(Digest::MD5)
- def MD5.md5(string)
- self.hexdigest(string)
- end
- end
-end
-# :startdoc:
-
diff --git a/lib/rubygems/digest/sha1.rb b/lib/rubygems/digest/sha1.rb
deleted file mode 100755
index 2a6245dcd9..0000000000
--- a/lib/rubygems/digest/sha1.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env ruby
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'digest/sha1'
-
-module Gem
- if RUBY_VERSION >= '1.8.6'
- SHA1 = Digest::SHA1
- else
- require 'rubygems/digest/digest_adapter'
- SHA1 = DigestAdapter.new(Digest::SHA1)
- end
-end \ No newline at end of file
diff --git a/lib/rubygems/digest/sha2.rb b/lib/rubygems/digest/sha2.rb
deleted file mode 100755
index 7bef16aed2..0000000000
--- a/lib/rubygems/digest/sha2.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env ruby
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'digest/sha2'
-
-module Gem
- if RUBY_VERSION >= '1.8.6'
- SHA256 = Digest::SHA256
- else
- require 'rubygems/digest/digest_adapter'
- SHA256 = DigestAdapter.new(Digest::SHA256)
- end
-end
diff --git a/lib/rubygems/doc_manager.rb b/lib/rubygems/doc_manager.rb
deleted file mode 100644
index 00ef4c51e3..0000000000
--- a/lib/rubygems/doc_manager.rb
+++ /dev/null
@@ -1,214 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'fileutils'
-require 'rubygems'
-
-##
-# The documentation manager generates RDoc and RI for RubyGems.
-
-class Gem::DocManager
-
- include Gem::UserInteraction
-
- @configured_args = []
-
- def self.configured_args
- @configured_args ||= []
- end
-
- def self.configured_args=(args)
- case args
- when Array
- @configured_args = args
- when String
- @configured_args = args.split
- end
- end
-
- ##
- # Load RDoc from a gem if it is available, otherwise from Ruby's stdlib
-
- def self.load_rdoc
- begin
- gem 'rdoc'
- rescue Gem::LoadError
- # use built-in RDoc
- end
-
- begin
- require 'rdoc/rdoc'
- rescue LoadError => e
- raise Gem::DocumentError,
- "ERROR: RDoc documentation generator not installed!"
- end
- end
-
- ##
- # Updates the RI cache for RDoc 2 if it is installed
-
- def self.update_ri_cache
- load_rdoc rescue return
-
- return unless defined? RDoc::VERSION # RDoc 1 does not have VERSION
-
- require 'rdoc/ri/driver'
-
- options = {
- :use_cache => true,
- :use_system => true,
- :use_site => true,
- :use_home => true,
- :use_gems => true,
- :formatter => RDoc::RI::Formatter,
- }
-
- driver = RDoc::RI::Driver.new(options).class_cache
- end
-
- ##
- # Create a document manager for +spec+. +rdoc_args+ contains arguments for
- # RDoc (template etc.) as a String.
-
- def initialize(spec, rdoc_args="")
- @spec = spec
- @doc_dir = File.join(spec.installation_path, "doc", spec.full_name)
- @rdoc_args = rdoc_args.nil? ? [] : rdoc_args.split
- end
-
- ##
- # Is the RDoc documentation installed?
-
- def rdoc_installed?
- File.exist?(File.join(@doc_dir, "rdoc"))
- end
-
- ##
- # Generate the RI documents for this gem spec.
- #
- # Note that if both RI and RDoc documents are generated from the same
- # process, the RI docs should be done first (a likely bug in RDoc will cause
- # RI docs generation to fail if run after RDoc).
-
- def generate_ri
- if @spec.has_rdoc then
- setup_rdoc
- install_ri # RDoc bug, ri goes first
- end
-
- FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir)
- end
-
- ##
- # Generate the RDoc documents for this gem spec.
- #
- # Note that if both RI and RDoc documents are generated from the same
- # process, the RI docs should be done first (a likely bug in RDoc will cause
- # RI docs generation to fail if run after RDoc).
-
- def generate_rdoc
- if @spec.has_rdoc then
- setup_rdoc
- install_rdoc
- end
-
- FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir)
- end
-
- ##
- # Generate and install RDoc into the documentation directory
-
- def install_rdoc
- rdoc_dir = File.join @doc_dir, 'rdoc'
-
- FileUtils.rm_rf rdoc_dir
-
- say "Installing RDoc documentation for #{@spec.full_name}..."
- run_rdoc '--op', rdoc_dir
- end
-
- ##
- # Generate and install RI into the documentation directory
-
- def install_ri
- ri_dir = File.join @doc_dir, 'ri'
-
- FileUtils.rm_rf ri_dir
-
- say "Installing ri documentation for #{@spec.full_name}..."
- run_rdoc '--ri', '--op', ri_dir
- end
-
- ##
- # Run RDoc with +args+, which is an ARGV style argument list
-
- def run_rdoc(*args)
- args << @spec.rdoc_options
- args << self.class.configured_args
- args << '--quiet'
- args << @spec.require_paths.clone
- args << @spec.extra_rdoc_files
- args = args.flatten.map do |arg| arg.to_s end
-
- r = RDoc::RDoc.new
-
- old_pwd = Dir.pwd
- Dir.chdir(@spec.full_gem_path)
- begin
- r.document args
- rescue Errno::EACCES => e
- dirname = File.dirname e.message.split("-")[1].strip
- raise Gem::FilePermissionError.new(dirname)
- rescue RuntimeError => ex
- alert_error "While generating documentation for #{@spec.full_name}"
- ui.errs.puts "... MESSAGE: #{ex}"
- ui.errs.puts "... RDOC args: #{args.join(' ')}"
- ui.errs.puts "\t#{ex.backtrace.join "\n\t"}" if
- Gem.configuration.backtrace
- ui.errs.puts "(continuing with the rest of the installation)"
- ensure
- Dir.chdir(old_pwd)
- end
- end
-
- def setup_rdoc
- if File.exist?(@doc_dir) && !File.writable?(@doc_dir) then
- raise Gem::FilePermissionError.new(@doc_dir)
- end
-
- FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir)
-
- self.class.load_rdoc
- end
-
- ##
- # Remove RDoc and RI documentation
-
- def uninstall_doc
- raise Gem::FilePermissionError.new(@spec.installation_path) unless
- File.writable? @spec.installation_path
-
- original_name = [
- @spec.name, @spec.version, @spec.original_platform].join '-'
-
- doc_dir = File.join @spec.installation_path, 'doc', @spec.full_name
- unless File.directory? doc_dir then
- doc_dir = File.join @spec.installation_path, 'doc', original_name
- end
-
- FileUtils.rm_rf doc_dir
-
- ri_dir = File.join @spec.installation_path, 'ri', @spec.full_name
-
- unless File.directory? ri_dir then
- ri_dir = File.join @spec.installation_path, 'ri', original_name
- end
-
- FileUtils.rm_rf ri_dir
- end
-
-end
-
diff --git a/lib/rubygems/doctor.rb b/lib/rubygems/doctor.rb
new file mode 100644
index 0000000000..4f26260d83
--- /dev/null
+++ b/lib/rubygems/doctor.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require_relative "../rubygems"
+require_relative "user_interaction"
+
+##
+# Cleans up after a partially-failed uninstall or for an invalid
+# Gem::Specification.
+#
+# If a specification was removed by hand this will remove any remaining files.
+#
+# If a corrupt specification was installed this will clean up warnings by
+# removing the bogus specification.
+
+class Gem::Doctor
+ include Gem::UserInteraction
+
+ ##
+ # Maps a gem subdirectory to the files that are expected to exist in the
+ # subdirectory.
+
+ REPOSITORY_EXTENSION_MAP = [ # :nodoc:
+ ["specifications", ".gemspec"],
+ ["build_info", ".info"],
+ ["cache", ".gem"],
+ ["doc", ""],
+ ["extensions", ""],
+ ["gems", ""],
+ ["plugins", ""],
+ ].freeze
+
+ missing =
+ Gem::REPOSITORY_SUBDIRECTORIES.sort -
+ REPOSITORY_EXTENSION_MAP.map {|(k,_)| k }.sort
+
+ raise "Update REPOSITORY_EXTENSION_MAP, missing: #{missing.join ", "}" unless
+ missing.empty?
+
+ ##
+ # Creates a new Gem::Doctor that will clean up +gem_repository+. Only one
+ # gem repository may be cleaned at a time.
+ #
+ # If +dry_run+ is true no files or directories will be removed.
+
+ def initialize(gem_repository, dry_run = false)
+ @gem_repository = gem_repository
+ @dry_run = dry_run
+
+ @installed_specs = nil
+ end
+
+ ##
+ # Specs installed in this gem repository
+
+ def installed_specs # :nodoc:
+ @installed_specs ||= Gem::Specification.map(&:full_name)
+ end
+
+ ##
+ # Are we doctoring a gem repository?
+
+ def gem_repository?
+ !installed_specs.empty?
+ end
+
+ ##
+ # Cleans up uninstalled files and invalid gem specifications
+
+ def doctor
+ @orig_home = Gem.dir
+ @orig_path = Gem.path
+
+ say "Checking #{@gem_repository}"
+
+ Gem.use_paths @gem_repository.to_s
+
+ unless gem_repository?
+ say "This directory does not appear to be a RubyGems repository, " \
+ "skipping"
+ say
+ return
+ end
+
+ doctor_children
+
+ say
+ ensure
+ Gem.use_paths @orig_home, *@orig_path
+ end
+
+ ##
+ # Cleans up children of this gem repository
+
+ def doctor_children # :nodoc:
+ REPOSITORY_EXTENSION_MAP.each do |sub_directory, extension|
+ doctor_child sub_directory, extension
+ end
+ end
+
+ ##
+ # Removes files in +sub_directory+ with +extension+
+
+ def doctor_child(sub_directory, extension) # :nodoc:
+ directory = File.join(@gem_repository, sub_directory)
+
+ Dir.entries(directory).sort.each do |ent|
+ next if [".", ".."].include?(ent)
+
+ child = File.join(directory, ent)
+ next unless File.exist?(child)
+
+ basename = File.basename(child, extension)
+ next if installed_specs.include? basename
+ next if /^rubygems-\d/.match?(basename)
+ next if sub_directory == "specifications" && basename == "default"
+ next if sub_directory == "plugins" && Gem.plugin_suffix_regexp =~ basename
+
+ type = File.directory?(child) ? "directory" : "file"
+
+ action = if @dry_run
+ "Extra"
+ else
+ FileUtils.rm_r(child)
+ "Removed"
+ end
+
+ say "#{action} #{type} #{sub_directory}/#{File.basename(child)}"
+ end
+ rescue Errno::ENOENT
+ # ignore
+ end
+end
diff --git a/lib/rubygems/errors.rb b/lib/rubygems/errors.rb
new file mode 100644
index 0000000000..4bbc5217e0
--- /dev/null
+++ b/lib/rubygems/errors.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+#--
+# This file contains all the various exceptions and other errors that are used
+# inside of RubyGems.
+#
+# DOC: Confirm _all_
+#++
+
+module Gem
+ ##
+ # Raised when RubyGems is unable to load or activate a gem. Contains the
+ # name and version requirements of the gem that either conflicts with
+ # already activated gems or that RubyGems is otherwise unable to activate.
+
+ class LoadError < ::LoadError
+ # Name of gem
+ attr_accessor :name
+
+ # Version requirement of gem
+ attr_accessor :requirement
+ end
+
+ ##
+ # Raised when trying to activate a gem, and that gem does not exist on the
+ # system. Instead of rescuing from this class, make sure to rescue from the
+ # superclass Gem::LoadError to catch all types of load errors.
+ class MissingSpecError < Gem::LoadError
+ def initialize(name, requirement, extra_message = nil)
+ @name = name
+ @requirement = requirement
+ @extra_message = extra_message
+ super(message)
+ end
+
+ def message # :nodoc:
+ build_message +
+ "Checked in 'GEM_PATH=#{Gem.path.join(File::PATH_SEPARATOR)}' #{@extra_message}, execute `gem env` for more information"
+ end
+
+ private
+
+ def build_message
+ total = Gem::Specification.stubs.size
+ "Could not find '#{name}' (#{requirement}) among #{total} total gem(s)\n"
+ end
+ end
+
+ ##
+ # Raised when trying to activate a gem, and the gem exists on the system, but
+ # not the requested version. Instead of rescuing from this class, make sure to
+ # rescue from the superclass Gem::LoadError to catch all types of load errors.
+ class MissingSpecVersionError < MissingSpecError
+ attr_reader :specs
+
+ def initialize(name, requirement, specs)
+ @specs = specs
+ super(name, requirement)
+ end
+
+ private
+
+ def build_message
+ names = specs.map(&:full_name)
+ "Could not find '#{name}' (#{requirement}) - did find: [#{names.join ","}]\n"
+ end
+ end
+
+ # Raised when there are conflicting gem specs loaded
+
+ class ConflictError < LoadError
+ ##
+ # A Hash mapping conflicting specifications to the dependencies that
+ # caused the conflict
+
+ attr_reader :conflicts
+
+ ##
+ # The specification that had the conflict
+
+ attr_reader :target
+
+ def initialize(target, conflicts)
+ @target = target
+ @conflicts = conflicts
+ @name = target.name
+
+ reason = conflicts.map do |act, dependencies|
+ "#{act.full_name} conflicts with #{dependencies.join(", ")}"
+ end.join ", "
+
+ # TODO: improve message by saying who activated `con`
+
+ super("Unable to activate #{target.full_name}, because #{reason}")
+ end
+ end
+
+ class ErrorReason; end
+
+ # Generated when trying to lookup a gem to indicate that the gem
+ # was found, but that it isn't usable on the current platform.
+ #
+ # fetch and install read these and report them to the user to aid
+ # in figuring out why a gem couldn't be installed.
+ #
+ class PlatformMismatch < ErrorReason
+ ##
+ # the name of the gem
+ attr_reader :name
+
+ ##
+ # the version
+ attr_reader :version
+
+ ##
+ # The platforms that are mismatched
+ attr_reader :platforms
+
+ def initialize(name, version)
+ @name = name
+ @version = version
+ @platforms = []
+ end
+
+ ##
+ # append a platform to the list of mismatched platforms.
+ #
+ # Platforms are added via this instead of injected via the constructor
+ # so that we can loop over a list of mismatches and just add them rather
+ # than perform some kind of calculation mismatch summary before creation.
+ def add_platform(platform)
+ @platforms << platform
+ end
+
+ ##
+ # A wordy description of the error.
+ def wordy
+ format("Found %s (%s), but was for platform%s %s", @name, @version, @platforms.size == 1 ? "" : "s", @platforms.join(" ,"))
+ end
+ end
+
+ ##
+ # An error that indicates we weren't able to fetch some
+ # data from a source
+
+ class SourceFetchProblem < ErrorReason
+ ##
+ # Creates a new SourceFetchProblem for the given +source+ and +error+.
+
+ def initialize(source, error)
+ @source = source
+ @error = error
+ end
+
+ ##
+ # The source that had the fetch problem.
+
+ attr_reader :source
+
+ ##
+ # The fetch error which is an Exception subclass.
+
+ attr_reader :error
+
+ ##
+ # An English description of the error.
+
+ def wordy
+ "Unable to download data from #{Gem::Uri.redact(@source.uri)} - #{@error.message}"
+ end
+
+ ##
+ # The "exception" alias allows you to call raise on a SourceFetchProblem.
+
+ alias_method :exception, :error
+ end
+end
diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb
index c37507c62a..e00a70c662 100644
--- a/lib/rubygems/exceptions.rb
+++ b/lib/rubygems/exceptions.rb
@@ -1,4 +1,6 @@
-require 'rubygems'
+# frozen_string_literal: true
+
+require_relative "unknown_command_spell_checker"
##
# Base exception class for RubyGems. All exception raised by RubyGems are a
@@ -7,17 +9,65 @@ class Gem::Exception < RuntimeError; end
class Gem::CommandLineError < Gem::Exception; end
+class Gem::UnknownCommandError < Gem::Exception
+ attr_reader :unknown_command
+
+ def initialize(unknown_command)
+ self.class.attach_correctable
+
+ @unknown_command = unknown_command
+ super("Unknown command #{unknown_command}")
+ end
+
+ def self.attach_correctable
+ return if method_defined?(:corrections)
+
+ if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
+ DidYouMean.correct_error(Gem::UnknownCommandError, Gem::UnknownCommandSpellChecker)
+ end
+ end
+end
+
class Gem::DependencyError < Gem::Exception; end
class Gem::DependencyRemovalException < Gem::Exception; end
##
+# Raised by Gem::Resolver when dependency resolution fails.
+
+class Gem::DependencyResolutionError < Gem::DependencyError
+ def initialize(conflict)
+ @explanation = conflict.explanation
+ super @explanation
+ end
+
+ def explanation
+ @explanation
+ end
+
+ def conflict
+ nil
+ end
+
+ def conflicting_dependencies
+ []
+ end
+end
+
+##
# Raised when attempting to uninstall a gem that isn't in GEM_HOME.
class Gem::GemNotInHomeException < Gem::Exception
attr_accessor :spec
end
+###
+# Raised when removing a gem with the uninstall command fails
+
+class Gem::UninstallError < Gem::Exception
+ attr_accessor :spec
+end
+
class Gem::DocumentError < Gem::Exception; end
##
@@ -26,10 +76,15 @@ class Gem::EndOfYAMLException < Gem::Exception; end
##
# Signals that a file permission error is preventing the user from
-# installing in the requested directories.
+# operating on the given directory.
+
class Gem::FilePermissionError < Gem::Exception
- def initialize(path)
- super("You don't have write permissions into the #{path} directory.")
+ attr_reader :directory
+
+ def initialize(directory)
+ @directory = directory
+
+ super "You don't have write permissions for the #{directory} directory."
end
end
@@ -41,8 +96,47 @@ end
class Gem::GemNotFoundException < Gem::Exception; end
+class Gem::SpecificGemNotFoundException < Gem::GemNotFoundException
+ ##
+ # Creates a new SpecificGemNotFoundException for a gem with the given +name+
+ # and +version+. Any +errors+ encountered when attempting to find the gem
+ # are also stored.
+
+ def initialize(name, version, errors = nil)
+ super "Could not find a valid gem '#{name}' (#{version}) locally or in a repository"
+
+ @name = name
+ @version = version
+ @errors = errors
+ end
+
+ ##
+ # The name of the gem that could not be found.
+
+ attr_reader :name
+
+ ##
+ # The version of the gem that could not be found.
+
+ attr_reader :version
+
+ ##
+ # Errors encountered attempting to find the gem.
+
+ attr_reader :errors
+end
+
+Gem.deprecate_constant :SpecificGemNotFoundException
+
class Gem::InstallError < Gem::Exception; end
+class Gem::RuntimeRequirementNotMetError < Gem::InstallError
+ attr_accessor :suggestion
+ def message
+ [suggestion, super].compact.join("\n\t")
+ end
+end
+
##
# Potentially raised when a specification is validated.
class Gem::InvalidSpecificationException < Gem::Exception; end
@@ -66,19 +160,92 @@ class Gem::RemoteInstallationSkipped < Gem::Exception; end
# Represents an error communicating via HTTP.
class Gem::RemoteSourceException < Gem::Exception; end
+##
+# Raised when a gem dependencies file specifies a ruby version that does not
+# match the current version.
+
+class Gem::RubyVersionMismatch < Gem::Exception; end
+
+##
+# Raised by Gem::Validator when something is not right in a gem.
+
class Gem::VerificationError < Gem::Exception; end
##
+# Raised by Gem::WebauthnListener when an error occurs during security
+# device verification.
+
+class Gem::WebauthnVerificationError < Gem::Exception
+ def initialize(message)
+ super "Security device verification failed: #{message}"
+ end
+end
+
+##
# Raised to indicate that a system exit should occur with the specified
# exit_code
class Gem::SystemExitException < SystemExit
- attr_accessor :exit_code
+ ##
+ # The exit code for the process
+
+ alias_method :exit_code, :status
+
+ ##
+ # Creates a new SystemExitException with the given +exit_code+
def initialize(exit_code)
- @exit_code = exit_code
+ super exit_code, "Exiting RubyGems with exit_code #{exit_code}"
+ end
+end
- super "Exiting RubyGems with exit_code #{exit_code}"
+##
+# Raised by Resolver when a dependency requests a gem for which
+# there is no spec.
+
+class Gem::UnsatisfiableDependencyError < Gem::DependencyError
+ ##
+ # The unsatisfiable dependency. This is a
+ # Gem::Resolver::DependencyRequest, not a Gem::Dependency
+
+ attr_reader :dependency
+
+ ##
+ # Errors encountered which may have contributed to this exception
+
+ attr_accessor :errors
+
+ ##
+ # Creates a new UnsatisfiableDependencyError for the unsatisfiable
+ # Gem::Resolver::DependencyRequest +dep+
+
+ def initialize(dep, platform_mismatch = nil)
+ if platform_mismatch && !platform_mismatch.empty?
+ plats = platform_mismatch.map {|x| x.platform.to_s }.sort.uniq
+ super "Unable to resolve dependency: No match for '#{dep}' on this platform. Found: #{plats.join(", ")}"
+ else
+ if dep.explicit?
+ super "Unable to resolve dependency: user requested '#{dep}'"
+ else
+ super "Unable to resolve dependency: '#{dep.request_context}' requires '#{dep}'"
+ end
+ end
+
+ @dependency = dep
+ @errors = []
end
+ ##
+ # The name of the unresolved dependency
+
+ def name
+ @dependency.name
+ end
+
+ ##
+ # The Requirement of the unresolved dependency (not Version).
+
+ def version
+ @dependency.requirement
+ end
end
diff --git a/lib/rubygems/ext.rb b/lib/rubygems/ext.rb
index 97ee762a4a..b5ca126a08 100644
--- a/lib/rubygems/ext.rb
+++ b/lib/rubygems/ext.rb
@@ -1,18 +1,20 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems'
-
##
# Classes for building C extensions live here.
module Gem::Ext; end
-require 'rubygems/ext/builder'
-require 'rubygems/ext/configure_builder'
-require 'rubygems/ext/ext_conf_builder'
-require 'rubygems/ext/rake_builder'
-
+require_relative "ext/build_error"
+require_relative "ext/builder"
+require_relative "ext/configure_builder"
+require_relative "ext/ext_conf_builder"
+require_relative "ext/rake_builder"
+require_relative "ext/cmake_builder"
+require_relative "ext/cargo_builder"
diff --git a/lib/rubygems/ext/build_error.rb b/lib/rubygems/ext/build_error.rb
new file mode 100644
index 0000000000..0329c1eec3
--- /dev/null
+++ b/lib/rubygems/ext/build_error.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+##
+# Raised when there is an error while building extensions.
+
+require_relative "../exceptions"
+
+class Gem::Ext::BuildError < Gem::InstallError
+end
diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb
index 36e9ec18f6..e00cf159da 100644
--- a/lib/rubygems/ext/builder.rb
+++ b/lib/rubygems/ext/builder.rb
@@ -1,56 +1,271 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems/ext'
+require_relative "../user_interaction"
class Gem::Ext::Builder
+ include Gem::UserInteraction
+
+ class NoMakefileError < Gem::InstallError
+ end
+
+ attr_accessor :build_args # :nodoc:
def self.class_name
name =~ /Ext::(.*)Builder/
$1.downcase
end
- def self.make(dest_path, results)
- unless File.exist? 'Makefile' then
- raise Gem::InstallError, "Makefile not found:\n\n#{results.join "\n"}"
+ def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = ["clean", "", "install"],
+ target_rbconfig: Gem.target_rbconfig, n_jobs: nil)
+ unless File.exist? File.join(make_dir, "Makefile")
+ # No makefile exists, nothing to do.
+ raise NoMakefileError, "No Makefile found in #{make_dir}"
end
- mf = File.read('Makefile')
- mf = mf.gsub(/^RUBYARCHDIR\s*=\s*\$[^$]*/, "RUBYARCHDIR = #{dest_path}")
- mf = mf.gsub(/^RUBYLIBDIR\s*=\s*\$[^$]*/, "RUBYLIBDIR = #{dest_path}")
+ # try to find make program from Ruby configure arguments first
+ target_rbconfig["configure_args"] =~ /with-make-prog\=(\w+)/
+ make_program_name = ENV["MAKE"] || ENV["make"] || $1
+ make_program_name ||= RUBY_PLATFORM.include?("mswin") ? "nmake" : "make"
+ make_program = shellsplit(make_program_name)
- File.open('Makefile', 'wb') {|f| f.print mf}
+ is_nmake = /\bnmake/i.match?(make_program_name)
+ # The installation of the bundled gems is failed when DESTDIR is empty in mswin platform.
+ destdir = !is_nmake || ENV["DESTDIR"] && ENV["DESTDIR"] != "" ? format("DESTDIR=%s", ENV["DESTDIR"]) : ""
- make_program = ENV['make']
- unless make_program then
- make_program = (/mswin/ =~ RUBY_PLATFORM) ? 'nmake' : 'make'
+ # nmake doesn't support parallel build
+ unless is_nmake
+ have_make_arguments = make_program.size > 1
+
+ if !have_make_arguments && !ENV["MAKEFLAGS"] && n_jobs
+ make_program << "-j#{n_jobs}"
+ end
end
- ['', ' install'].each do |target|
- cmd = "#{make_program}#{target}"
- results << cmd
- results << `#{cmd} #{redirector}`
+ env = [destdir]
+
+ if sitedir
+ env << format("sitearchdir=%s", sitedir)
+ env << format("sitelibdir=%s", sitedir)
+ end
- raise Gem::InstallError, "make#{target} failed:\n\n#{results}" unless
- $?.success?
+ targets.each do |target|
+ # Pass DESTDIR via command line to override what's in MAKEFLAGS
+ cmd = [
+ *make_program,
+ *env,
+ target,
+ ].reject(&:empty?)
+ begin
+ run(cmd, results, "make #{target}".rstrip, make_dir)
+ rescue Gem::InstallError
+ raise unless target == "clean" # ignore clean failure
+ end
end
end
- def self.redirector
- '2>&1'
+ def self.ruby
+ # Gem.ruby is quoted if it contains whitespace
+ cmd = shellsplit(Gem.ruby)
+
+ # This load_path is only needed when running rubygems test without a proper installation.
+ # Prepending it in a normal installation will cause problem with order of $LOAD_PATH.
+ # Therefore only add load_path if it is not present in the default $LOAD_PATH.
+ load_path = File.expand_path("../..", __dir__)
+ case load_path
+ when RbConfig::CONFIG["sitelibdir"], RbConfig::CONFIG["vendorlibdir"], RbConfig::CONFIG["rubylibdir"]
+ cmd
+ else
+ cmd << "-I#{load_path}"
+ end
end
- def self.run(command, results)
- results << command
- results << `#{command} #{redirector}`
+ def self.run(command, results, command_name = nil, dir = Dir.pwd, env = {})
+ verbose = Gem.configuration.really_verbose
+
+ begin
+ rubygems_gemdeps = ENV["RUBYGEMS_GEMDEPS"]
+ ENV["RUBYGEMS_GEMDEPS"] = nil
+ if verbose
+ puts("current directory: #{dir}")
+ p(command)
+ end
+ results << "current directory: #{dir}"
+ results << shelljoin(command)
+
+ require "open3"
+ # Set $SOURCE_DATE_EPOCH for the subprocess.
+ build_env = { "SOURCE_DATE_EPOCH" => Gem.source_date_epoch_string }.merge(env)
+ output, status = begin
+ Open3.popen2e(build_env, *command, chdir: dir) do |stdin, stdouterr, wait_thread|
+ stdin.close
+ output = String.new
+ while line = stdouterr.gets
+ output << line
+ if verbose
+ print line
+ end
+ end
+ [output, wait_thread.value]
+ end
+ rescue StandardError => error
+ raise Gem::InstallError, "#{command_name || class_name} failed#{error.message}"
+ end
+ unless verbose
+ results << output
+ end
+ ensure
+ ENV["RUBYGEMS_GEMDEPS"] = rubygems_gemdeps
+ end
+
+ unless status.success?
+ results << "Building has failed. See above output for more information on the failure." if verbose
+ end
+
+ yield(status, results) if block_given?
+
+ unless status.success?
+ exit_reason =
+ if status.exited?
+ ", exit code #{status.exitstatus}"
+ elsif status.signaled?
+ ", uncaught signal #{status.termsig}"
+ end
- unless $?.success? then
- raise Gem::InstallError, "#{class_name} failed:\n\n#{results.join "\n"}"
+ raise Gem::InstallError, "#{command_name || class_name} failed#{exit_reason}"
end
end
-end
+ def self.shellsplit(command)
+ require "shellwords"
+
+ Shellwords.split(command)
+ end
+
+ def self.shelljoin(command)
+ require "shellwords"
+
+ Shellwords.join(command)
+ end
+
+ ##
+ # Creates a new extension builder for +spec+. If the +spec+ does not yet
+ # have build arguments, saved, set +build_args+ which is an ARGV-style
+ # array.
+
+ def initialize(spec, build_args = spec.build_args, target_rbconfig = Gem.target_rbconfig, build_jobs = nil)
+ @spec = spec
+ @build_args = build_args
+ @gem_dir = spec.full_gem_path
+ @target_rbconfig = target_rbconfig
+ @build_jobs = build_jobs
+ end
+
+ ##
+ # Chooses the extension builder class for +extension+
+
+ def builder_for(extension) # :nodoc:
+ case extension
+ when /extconf/ then
+ Gem::Ext::ExtConfBuilder
+ when /configure/ then
+ Gem::Ext::ConfigureBuilder
+ when /rakefile/i, /mkrf_conf/i then
+ Gem::Ext::RakeBuilder
+ when /CMakeLists.txt/ then
+ Gem::Ext::CmakeBuilder.new
+ when /Cargo.toml/ then
+ Gem::Ext::CargoBuilder.new
+ else
+ build_error("No builder for extension '#{extension}'")
+ end
+ end
+
+ ##
+ # Logs the build +output+, then raises Gem::Ext::BuildError.
+
+ def build_error(output, backtrace = nil) # :nodoc:
+ gem_make_out = write_gem_make_out output
+ message = <<-EOF
+ERROR: Failed to build gem native extension.
+
+ #{output}
+
+Gem files will remain installed in #{@gem_dir} for inspection.
+Results logged to #{gem_make_out}
+EOF
+
+ raise Gem::Ext::BuildError, message, backtrace
+ end
+
+ def build_extension(extension, dest_path) # :nodoc:
+ results = []
+
+ builder = builder_for(extension)
+
+ extension_dir =
+ File.expand_path File.join(@gem_dir, File.dirname(extension))
+ lib_dir = File.join @spec.full_gem_path, @spec.raw_require_paths.first
+
+ begin
+ FileUtils.mkdir_p dest_path
+
+ results = builder.build(extension, dest_path,
+ results, @build_args, lib_dir, extension_dir, @target_rbconfig, n_jobs: @build_jobs)
+
+ verbose { results.join("\n") }
+
+ write_gem_make_out results.join "\n"
+ rescue StandardError => e
+ results << e.message
+ build_error(results.join("\n"), $@)
+ end
+ end
+
+ ##
+ # Builds extensions. Valid types of extensions are extconf.rb files,
+ # configure scripts and rakefiles or mkrf_conf files.
+
+ def build_extensions
+ return if @spec.extensions.empty?
+
+ if @build_args.empty?
+ say "Building native extensions. This could take a while..."
+ else
+ say "Building native extensions with: '#{@build_args.join " "}'"
+ say "This could take a while..."
+ end
+
+ dest_path = @spec.extension_dir
+
+ require "fileutils"
+ FileUtils.rm_f @spec.gem_build_complete_path
+
+ @spec.extensions.each do |extension|
+ build_extension extension, dest_path
+ end
+
+ FileUtils.touch @spec.gem_build_complete_path
+ end
+
+ ##
+ # Writes +output+ to gem_make.out in the extension install directory.
+
+ def write_gem_make_out(output) # :nodoc:
+ destination = File.join @spec.extension_dir, "gem_make.out"
+
+ FileUtils.mkdir_p @spec.extension_dir
+
+ File.open destination, "wb" do |io|
+ io.puts output
+ end
+
+ destination
+ end
+end
diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb
new file mode 100644
index 0000000000..516459dd60
--- /dev/null
+++ b/lib/rubygems/ext/cargo_builder.rb
@@ -0,0 +1,349 @@
+# frozen_string_literal: true
+
+# This class is used by rubygems to build Rust extensions. It is a thin-wrapper
+# over the `cargo rustc` command which takes care of building Rust code in a way
+# that Ruby can use.
+class Gem::Ext::CargoBuilder < Gem::Ext::Builder
+ attr_accessor :spec, :runner, :profile
+
+ def initialize
+ require_relative "../command"
+ require_relative "cargo_builder/link_flag_converter"
+
+ @runner = self.class.method(:run)
+ @profile = :release
+ end
+
+ def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ require "tempfile"
+ require "fileutils"
+
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for Rust extensions. Ignoring"
+ end
+
+ # Where's the Cargo.toml of the crate we're building
+ cargo_toml = File.join(cargo_dir, "Cargo.toml")
+ # What's the crate's name
+ crate_name = cargo_crate_name(cargo_dir, cargo_toml, results)
+
+ begin
+ # Create a tmp dir to do the build in
+ tmp_dest = Dir.mktmpdir(".gem.", cargo_dir)
+
+ # Run the build
+ cmd = cargo_command(cargo_toml, tmp_dest, args, crate_name)
+ runner.call(cmd, results, "cargo", cargo_dir, build_env)
+
+ # Where do we expect Cargo to write the compiled library
+ dylib_path = cargo_dylib_path(tmp_dest, crate_name)
+
+ # Helpful error if we didn't find the compiled library
+ raise DylibNotFoundError, tmp_dest unless File.exist?(dylib_path)
+
+ # Cargo and Ruby differ on how the library should be named, rename from
+ # what Cargo outputs to what Ruby expects
+ dlext_name = "#{crate_name}.#{makefile_config("DLEXT")}"
+ dlext_path = File.join(File.dirname(dylib_path), dlext_name)
+ FileUtils.cp(dylib_path, dlext_path)
+
+ nesting = extension_nesting(extension)
+
+ if Gem.install_extension_in_lib && lib_dir
+ nested_lib_dir = File.join(lib_dir, nesting)
+ FileUtils.mkdir_p nested_lib_dir
+ FileUtils.cp_r dlext_path, nested_lib_dir, remove_destination: true
+ end
+
+ # move to final destination
+ nested_dest_path = File.join(dest_path, nesting)
+ FileUtils.mkdir_p nested_dest_path
+ FileUtils.cp_r dlext_path, nested_dest_path, remove_destination: true
+ ensure
+ # clean up intermediary build artifacts
+ FileUtils.rm_rf tmp_dest if tmp_dest
+ end
+
+ results
+ end
+
+ def build_env
+ build_env = rb_config_env
+ build_env["RUBY_STATIC"] = "true" if ruby_static? && ENV.key?("RUBY_STATIC")
+ cfg = "--cfg=rb_sys_gem --cfg=rubygems --cfg=rubygems_#{Gem::VERSION.tr(".", "_")}"
+ build_env["RUSTFLAGS"] = [ENV["RUSTFLAGS"], cfg].compact.join(" ")
+ build_env
+ end
+
+ def cargo_command(cargo_toml, dest_path, args = [], crate_name = nil)
+ cmd = []
+ cmd += [cargo, "rustc"]
+ cmd += ["--crate-type", "cdylib"]
+ cmd += ["--target", ENV["CARGO_BUILD_TARGET"]] if ENV["CARGO_BUILD_TARGET"]
+ cmd += ["--target-dir", dest_path]
+ cmd += ["--manifest-path", cargo_toml]
+ cmd += ["--lib"]
+ cmd += ["--profile", profile.to_s]
+ cmd += ["--locked"]
+ cmd += Gem::Command.build_args
+ cmd += args
+ cmd += ["--"]
+ cmd += [*cargo_rustc_args(dest_path, crate_name)]
+ cmd
+ end
+
+ private
+
+ def cargo
+ ENV.fetch("CARGO", "cargo")
+ end
+
+ # returns the directory nesting of the extension, ignoring the first part, so
+ # "ext/foo/bar/Cargo.toml" becomes "foo/bar"
+ def extension_nesting(extension)
+ parts = extension.to_s.split(Regexp.union([File::SEPARATOR, File::ALT_SEPARATOR].compact))
+
+ parts = parts.each_with_object([]) do |segment, final|
+ next if segment == "."
+ if segment == ".."
+ raise Gem::InstallError, "extension outside of gem root" if final.empty?
+ next final.pop
+ end
+ final << segment
+ end
+
+ File.join(parts[1...-1])
+ end
+
+ def rb_config_env
+ result = {}
+ RbConfig::CONFIG.each {|k, v| result["RBCONFIG_#{k}"] = v }
+ result
+ end
+
+ def cargo_rustc_args(dest_dir, crate_name)
+ [
+ *linker_args,
+ *mkmf_libpath,
+ *rustc_dynamic_linker_flags(dest_dir, crate_name),
+ *rustc_lib_flags(dest_dir),
+ *platform_specific_rustc_args(dest_dir),
+ ]
+ end
+
+ def platform_specific_rustc_args(dest_dir, flags = [])
+ if mingw_target?
+ # On mingw platforms, mkmf adds libruby to the linker flags
+ flags += libruby_args(dest_dir)
+
+ # Make sure ALSR is used on mingw
+ # see https://github.com/rust-lang/rust/pull/75406/files
+ flags += ["-C", "link-arg=-Wl,--dynamicbase"]
+ flags += ["-C", "link-arg=-Wl,--disable-auto-image-base"]
+
+ # If the gem is installed on a host with build tools installed, but is
+ # run on one that isn't the missing libraries will cause the extension
+ # to fail on start.
+ flags += ["-C", "link-arg=-static-libgcc"]
+ elsif darwin_target?
+ # Ventura does not always have this flag enabled
+ flags += ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]
+ end
+
+ flags
+ end
+
+ # We want to use the same linker that Ruby uses, so that the linker flags from
+ # mkmf work properly.
+ def linker_args
+ cc_flag = self.class.shellsplit(makefile_config("CC"))
+ # Avoid to ccache like tool from Rust build
+ # see https://github.com/ruby/rubygems/pull/8521#issuecomment-2689854359
+ # ex. CC="ccache gcc" or CC="sccache clang --any --args"
+ cc_flag.shift if cc_flag.size >= 2 && !cc_flag[1].start_with?("-")
+ linker = cc_flag.shift
+ link_args = cc_flag.flat_map {|a| ["-C", "link-arg=#{a}"] }
+
+ return mswin_link_args if linker == "cl"
+
+ ["-C", "linker=#{linker}", *link_args]
+ end
+
+ def mswin_link_args
+ args = []
+ args += ["-l", makefile_config("LIBRUBYARG_SHARED").chomp(".lib")]
+ args += split_flags("LIBS").flat_map {|lib| ["-l", lib.chomp(".lib")] }
+ args += split_flags("LOCAL_LIBS").flat_map {|lib| ["-l", lib.chomp(".lib")] }
+ args
+ end
+
+ def libruby_args(dest_dir)
+ libs = makefile_config(ruby_static? ? "LIBRUBYARG_STATIC" : "LIBRUBYARG_SHARED")
+ raw_libs = self.class.shellsplit(libs)
+ raw_libs.flat_map {|l| ldflag_to_link_modifier(l) }
+ end
+
+ def ruby_static?
+ return true if %w[1 true].include?(ENV["RUBY_STATIC"])
+
+ makefile_config("ENABLE_SHARED") == "no"
+ end
+
+ def cargo_dylib_path(dest_path, crate_name)
+ so_ext = RbConfig::CONFIG["SOEXT"]
+ prefix = so_ext == "dll" ? "" : "lib"
+ path_parts = [dest_path]
+ path_parts << ENV["CARGO_BUILD_TARGET"] if ENV["CARGO_BUILD_TARGET"]
+ path_parts += ["release", "#{prefix}#{crate_name}.#{so_ext}"]
+ File.join(*path_parts)
+ end
+
+ def cargo_crate_name(cargo_dir, manifest_path, results)
+ require "open3"
+ Gem.load_yaml
+
+ output, status =
+ begin
+ Open3.capture2e(cargo, "metadata", "--no-deps", "--format-version", "1", chdir: cargo_dir)
+ rescue StandardError => error
+ raise Gem::InstallError, "cargo metadata failed #{error.message}"
+ end
+
+ unless status.success?
+ if Gem.configuration.really_verbose
+ puts output
+ else
+ results << output
+ end
+
+ exit_reason =
+ if status.exited?
+ ", exit code #{status.exitstatus}"
+ elsif status.signaled?
+ ", uncaught signal #{status.termsig}"
+ end
+
+ raise Gem::InstallError, "cargo metadata failed#{exit_reason}"
+ end
+
+ # cargo metadata output is specified as json
+ require "json"
+ metadata = JSON.parse(output)
+ package = metadata["packages"].find {|pkg| normalize_path(pkg["manifest_path"]) == manifest_path }
+ unless package
+ found = metadata["packages"].map {|md| "#{md["name"]} at #{md["manifest_path"]}" }
+ raise Gem::InstallError, <<-EOF
+failed to determine cargo package name
+
+looking for: #{manifest_path}
+
+found:
+#{found.join("\n")}
+EOF
+ end
+ package["name"].tr("-", "_")
+ end
+
+ def normalize_path(path)
+ return path unless File::ALT_SEPARATOR
+
+ path.tr(File::ALT_SEPARATOR, File::SEPARATOR)
+ end
+
+ def rustc_dynamic_linker_flags(dest_dir, crate_name)
+ split_flags("DLDFLAGS").
+ filter_map {|arg| maybe_resolve_ldflag_variable(arg, dest_dir, crate_name) }.
+ flat_map {|arg| ldflag_to_link_modifier(arg) }
+ end
+
+ def rustc_lib_flags(dest_dir)
+ split_flags("LIBS").flat_map {|arg| ldflag_to_link_modifier(arg) }
+ end
+
+ def split_flags(var)
+ self.class.shellsplit(RbConfig::CONFIG.fetch(var, ""))
+ end
+
+ def ldflag_to_link_modifier(arg)
+ LinkFlagConverter.convert(arg)
+ end
+
+ def msvc_target?
+ makefile_config("target_os").include?("msvc")
+ end
+
+ def darwin_target?
+ makefile_config("target_os").include?("darwin")
+ end
+
+ def mingw_target?
+ makefile_config("target_os").include?("mingw")
+ end
+
+ def win_target?
+ target_platform = RbConfig::CONFIG["target_os"]
+ !!Gem::WIN_PATTERNS.find {|r| target_platform =~ r }
+ end
+
+ # Interpolate substitution vars in the arg (i.e. $(DEFFILE))
+ def maybe_resolve_ldflag_variable(input_arg, dest_dir, crate_name)
+ var_matches = input_arg.match(/\$\((\w+)\)/)
+
+ return input_arg unless var_matches
+
+ var_name = var_matches[1]
+
+ return input_arg if var_name.nil? || var_name.chomp.empty?
+
+ case var_name
+ # On windows, it is assumed that mkmf has setup an exports file for the
+ # extension, so we have to create one ourselves.
+ when "DEFFILE"
+ write_deffile(dest_dir, crate_name)
+ else
+ RbConfig::CONFIG[var_name]
+ end
+ end
+
+ def write_deffile(dest_dir, crate_name)
+ deffile_path = File.join(dest_dir, "#{crate_name}-#{RbConfig::CONFIG["arch"]}.def")
+ export_prefix = makefile_config("EXPORT_PREFIX") || ""
+
+ File.open(deffile_path, "w") do |f|
+ f.puts "EXPORTS"
+ f.puts "#{export_prefix.strip}Init_#{crate_name}"
+ end
+
+ deffile_path
+ end
+
+ # Corresponds to $(LIBPATH) in mkmf
+ def mkmf_libpath
+ ["-L", "native=#{makefile_config("libdir")}"]
+ end
+
+ def makefile_config(var_name)
+ val = RbConfig::MAKEFILE_CONFIG[var_name]
+
+ return unless val
+
+ RbConfig.expand(val.dup)
+ end
+
+ # Error raised when no cdylib artifact was created
+ class DylibNotFoundError < StandardError
+ def initialize(dir)
+ files = Dir.glob(File.join(dir, "**", "*")).map {|f| "- #{f}" }.join "\n"
+
+ super <<~MSG
+ Dynamic library not found for Rust extension (in #{dir})
+
+ Make sure you set "crate-type" in Cargo.toml to "cdylib"
+
+ Found files:
+ #{files}
+ MSG
+ end
+ end
+end
diff --git a/lib/rubygems/ext/cargo_builder/link_flag_converter.rb b/lib/rubygems/ext/cargo_builder/link_flag_converter.rb
new file mode 100644
index 0000000000..e4d196cb10
--- /dev/null
+++ b/lib/rubygems/ext/cargo_builder/link_flag_converter.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class Gem::Ext::CargoBuilder < Gem::Ext::Builder
+ # Converts Ruby link flags into something cargo understands
+ class LinkFlagConverter
+ FILTERED_PATTERNS = [
+ /compress-debug-sections/, # Not supported by all linkers, and not required for Rust
+ ].freeze
+
+ def self.convert(arg)
+ return [] if FILTERED_PATTERNS.any? {|p| p.match?(arg) }
+
+ case arg.chomp
+ when /^-L\s*(.+)$/
+ ["-L", "native=#{$1}"]
+ when /^--library=(\w+\S+)$/, /^-l\s*(\w+\S+)$/
+ ["-l", $1]
+ when /^-l\s*([^:\s])+/ # -lfoo, but not -l:libfoo.a
+ ["-l", $1]
+ when /^-F\s*(.*)$/
+ ["-l", "framework=#{$1}"]
+ else
+ ["-C", "link-args=#{arg}"]
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/ext/cmake_builder.rb b/lib/rubygems/ext/cmake_builder.rb
new file mode 100644
index 0000000000..e660ed558b
--- /dev/null
+++ b/lib/rubygems/ext/cmake_builder.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+# This builder creates extensions defined using CMake. Its is invoked if a Gem's spec file
+# sets the `extension` property to a string that contains `CMakeLists.txt`.
+#
+# In general, CMake projects are built in two steps:
+#
+# * configure
+# * build
+#
+# The builder follow this convention. First it runs a configuration step and then it runs a build step.
+#
+# CMake projects can be quite configurable - it is likely you will want to specify options when
+# installing a gem. To pass options to CMake specify them after `--` in the gem install command. For example:
+#
+# gem install <gem_name> -- --preset <preset_name>
+#
+# Note that options are ONLY sent to the configure step - it is not currently possible to specify
+# options for the build step. If this becomes and issue then the CMake builder can be updated to
+# support build options.
+#
+# Useful options to know are:
+#
+# -G to specify a generator (-G Ninja is recommended)
+# -D<CMAKE_VARIABLE> to set a CMake variable (for example -DCMAKE_BUILD_TYPE=Release)
+# --preset <preset_name> to use a preset
+#
+# If the Gem author provides presets, via CMakePresets.json file, you will likely want to use one of them.
+# If not, you may wish to specify a generator. Ninja is recommended because it can build projects in parallel
+# and thus much faster than building them serially like Make does.
+
+class Gem::Ext::CmakeBuilder < Gem::Ext::Builder
+ attr_accessor :runner, :profile
+ def initialize
+ @runner = self.class.method(:run)
+ @profile = :release
+ end
+
+ def build(extension, dest_path, results, args = [], lib_dir = nil, cmake_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for CMake extensions. Ignoring"
+ end
+
+ # Figure the build dir
+ build_dir = File.join(cmake_dir, "build")
+
+ # Check if the gem defined presets
+ check_presets(cmake_dir, args, results)
+
+ # Configure
+ configure(cmake_dir, build_dir, dest_path, args, results)
+
+ # Compile
+ compile(cmake_dir, build_dir, args, results)
+
+ results
+ end
+
+ def configure(cmake_dir, build_dir, install_dir, args, results)
+ cmd = ["cmake",
+ cmake_dir,
+ "-B",
+ build_dir,
+ "-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=#{install_dir}", # Windows
+ "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=#{install_dir}", # Not Windows
+ *Gem::Command.build_args,
+ *args]
+
+ runner.call(cmd, results, "cmake_configure", cmake_dir)
+ end
+
+ def compile(cmake_dir, build_dir, args, results)
+ cmd = ["cmake",
+ "--build",
+ build_dir.to_s,
+ "--config",
+ @profile.to_s]
+
+ runner.call(cmd, results, "cmake_compile", cmake_dir)
+ end
+
+ private
+
+ def check_presets(cmake_dir, args, results)
+ # Return if the user specified a preset
+ return unless args.grep(/--preset/i).empty?
+
+ cmd = ["cmake",
+ "--list-presets"]
+
+ presets = Array.new
+ begin
+ runner.call(cmd, presets, "cmake_presets", cmake_dir)
+
+ # Remove the first two lines of the array which is the current_directory and the command
+ # that was run
+ presets = presets[2..].join
+ results << <<~EOS
+ The gem author provided a list of presets that can be used to build the gem. To use a preset specify it on the command line:
+
+ gem install <gem_name> -- --preset <preset_name>
+
+ #{presets}
+ EOS
+ rescue Gem::InstallError
+ # Do nothing, CMakePresets.json was not included in the Gem
+ end
+ end
+end
diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb
index 1cde6915a7..230b214b3c 100644
--- a/lib/rubygems/ext/configure_builder.rb
+++ b/lib/rubygems/ext/configure_builder.rb
@@ -1,24 +1,26 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems/ext/builder'
-
class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder
+ def self.build(extension, dest_path, results, args = [], lib_dir = nil, configure_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for configure-based extensions. Ignoring"
+ end
- def self.build(extension, directory, dest_path, results)
- unless File.exist?('Makefile') then
- cmd = "sh ./configure --prefix=#{dest_path}"
+ unless File.exist?(File.join(configure_dir, "Makefile"))
+ cmd = ["sh", "./configure", "--prefix=#{dest_path}", *args]
- run cmd, results
+ run cmd, results, class_name, configure_dir
end
- make dest_path, results
+ make dest_path, results, configure_dir, target_rbconfig: target_rbconfig, n_jobs: n_jobs
results
end
-
end
-
diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb
index cbe0e80821..822454355d 100644
--- a/lib/rubygems/ext/ext_conf_builder.rb
+++ b/lib/rubygems/ext/ext_conf_builder.rb
@@ -1,23 +1,81 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems/ext/builder'
-
class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder
+ def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ require "fileutils"
+ require "tempfile"
+
+ tmp_dest = Dir.mktmpdir(".gem.", extension_dir)
+
+ # Some versions of `mktmpdir` return absolute paths, which will break make
+ # if the paths contain spaces.
+ #
+ # As such, we convert to a relative path.
+ tmp_dest_relative = get_relative_path(tmp_dest.clone, extension_dir)
+
+ destdir = ENV["DESTDIR"]
+
+ begin
+ cmd = ruby << File.basename(extension)
+ cmd << "--target-rbconfig=#{target_rbconfig.path}" if target_rbconfig.path
+ cmd.push(*args)
- def self.build(extension, directory, dest_path, results)
- cmd = "#{Gem.ruby} #{File.basename extension}"
- cmd << " #{ARGV.join ' '}" unless ARGV.empty?
+ run(cmd, results, class_name, extension_dir) do |s, r|
+ mkmf_log = File.join(extension_dir, "mkmf.log")
+ if File.exist? mkmf_log
+ unless s.success?
+ r << "To see why this extension failed to compile, please check" \
+ " the mkmf.log which can be found here:\n"
+ r << " " + File.join(dest_path, "mkmf.log") + "\n"
+ end
+ FileUtils.mv mkmf_log, dest_path
+ end
+ end
- run cmd, results
+ ENV["DESTDIR"] = nil
- make dest_path, results
+ make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig, n_jobs: n_jobs
+
+ full_tmp_dest = File.join(extension_dir, tmp_dest_relative)
+
+ is_cross_compiling = target_rbconfig["platform"] != RbConfig::CONFIG["platform"]
+ # Do not copy extension libraries by default when cross-compiling
+ # not to conflict with the one already built for the host platform.
+ if Gem.install_extension_in_lib && lib_dir && !is_cross_compiling
+ FileUtils.mkdir_p lib_dir
+ entries = Dir.entries(full_tmp_dest) - %w[. ..]
+ entries = entries.map {|entry| File.join full_tmp_dest, entry }
+ FileUtils.cp_r entries, lib_dir, remove_destination: true
+ end
+
+ FileUtils::Entry_.new(full_tmp_dest).traverse do |ent|
+ destent = ent.class.new(dest_path, ent.rel)
+ destent.exist? || FileUtils.mv(ent.path, destent.path)
+ end
+
+ make dest_path, results, extension_dir, tmp_dest_relative, ["clean"], target_rbconfig: target_rbconfig
+ ensure
+ ENV["DESTDIR"] = destdir
+ end
results
+ rescue Gem::Ext::Builder::NoMakefileError => error
+ results << error.message
+ results << "Skipping make for #{extension} as no Makefile was found."
+ # We are good, do not re-raise the error.
+ ensure
+ FileUtils.rm_rf tmp_dest if tmp_dest
end
+ def self.get_relative_path(path, base)
+ path[0..base.length - 1] = "." if path.start_with?(base)
+ path
+ end
end
-
diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb
index 0c64e611a0..d702d7f339 100644
--- a/lib/rubygems/ext/rake_builder.rb
+++ b/lib/rubygems/ext/rake_builder.rb
@@ -1,27 +1,37 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems/ext/builder'
-
class Gem::Ext::RakeBuilder < Gem::Ext::Builder
+ def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for Rake extensions. Ignoring"
+ end
- def self.build(extension, directory, dest_path, results)
- if File.basename(extension) =~ /mkrf_conf/i then
- cmd = "#{Gem.ruby} #{File.basename extension}"
- cmd << " #{ARGV.join " "}" unless ARGV.empty?
- run cmd, results
+ if /mkrf_conf/i.match?(File.basename(extension))
+ run([Gem.ruby, File.basename(extension), *args], results, class_name, extension_dir)
end
- cmd = ENV['rake'] || 'rake'
- cmd += " RUBYARCHDIR=#{dest_path} RUBYLIBDIR=#{dest_path}" # ENV is frozen
+ rake = ENV["rake"]
- run cmd, results
+ if rake
+ rake = shellsplit(rake)
+ else
+ begin
+ rake = ruby << "-rrubygems" << Gem.bin_path("rake", "rake")
+ rescue Gem::Exception
+ rake = [Gem.default_exec_format % "rake"]
+ end
+ end
+
+ rake_args = ["RUBYARCHDIR=#{dest_path}", "RUBYLIBDIR=#{dest_path}", *args]
+ run(rake + rake_args, results, class_name, extension_dir)
results
end
-
end
-
diff --git a/lib/rubygems/format.rb b/lib/rubygems/format.rb
deleted file mode 100644
index 7dc127d5f4..0000000000
--- a/lib/rubygems/format.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'fileutils'
-
-require 'rubygems/package'
-
-module Gem
-
- ##
- # The format class knows the guts of the RubyGem .gem file format
- # and provides the capability to read gem files
- #
- class Format
- attr_accessor :spec, :file_entries, :gem_path
- extend Gem::UserInteraction
-
- ##
- # Constructs an instance of a Format object, representing the gem's
- # data structure.
- #
- # gem:: [String] The file name of the gem
- #
- def initialize(gem_path)
- @gem_path = gem_path
- end
-
- ##
- # Reads the named gem file and returns a Format object, representing
- # the data from the gem file
- #
- # file_path:: [String] Path to the gem file
- #
- def self.from_file_by_path(file_path, security_policy = nil)
- format = nil
-
- unless File.exist?(file_path)
- raise Gem::Exception, "Cannot load gem at [#{file_path}] in #{Dir.pwd}"
- end
-
- # check for old version gem
- if File.read(file_path, 20).include?("MD5SUM =")
- require 'rubygems/old_format'
-
- format = OldFormat.from_file_by_path(file_path)
- else
- open file_path, Gem.binary_mode do |io|
- format = from_io io, file_path, security_policy
- end
- end
-
- return format
- end
-
- ##
- # Reads a gem from an io stream and returns a Format object, representing
- # the data from the gem file
- #
- # io:: [IO] Stream from which to read the gem
- #
- def self.from_io(io, gem_path="(io)", security_policy = nil)
- format = new gem_path
-
- Package.open io, 'r', security_policy do |pkg|
- format.spec = pkg.metadata
- format.file_entries = []
-
- pkg.each do |entry|
- size = entry.header.size
- mode = entry.header.mode
-
- format.file_entries << [{
- "size" => size, "mode" => mode, "path" => entry.full_name,
- },
- entry.read
- ]
- end
- end
-
- format
- end
-
- end
-end
diff --git a/lib/rubygems/gem_openssl.rb b/lib/rubygems/gem_openssl.rb
deleted file mode 100644
index 1456f2d7ce..0000000000
--- a/lib/rubygems/gem_openssl.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-# Some system might not have OpenSSL installed, therefore the core
-# library file openssl might not be available. We localize testing
-# for the presence of OpenSSL in this file.
-
-module Gem
- class << self
- # Is SSL (used by the signing commands) available on this
- # platform?
- def ssl_available?
- require 'rubygems/gem_openssl'
- @ssl_available
- end
-
- # Set the value of the ssl_available flag.
- attr_writer :ssl_available
-
- # Ensure that SSL is available. Throw an exception if it is not.
- def ensure_ssl_available
- unless ssl_available?
- fail Gem::Exception, "SSL is not installed on this system"
- end
- end
- end
-end
-
-begin
- require 'openssl'
-
- # Reference a constant defined in the .rb portion of ssl (just to
- # make sure that part is loaded too).
-
- dummy = OpenSSL::Digest::SHA1
-
- Gem.ssl_available = true
-
- class OpenSSL::X509::Certificate # :nodoc:
- # Check the validity of this certificate.
- def check_validity(issuer_cert = nil, time = Time.now)
- ret = if @not_before && @not_before > time
- [false, :expired, "not valid before '#@not_before'"]
- elsif @not_after && @not_after < time
- [false, :expired, "not valid after '#@not_after'"]
- elsif issuer_cert && !verify(issuer_cert.public_key)
- [false, :issuer, "#{issuer_cert.subject} is not issuer"]
- else
- [true, :ok, 'Valid certificate']
- end
-
- # return hash
- { :is_valid => ret[0], :error => ret[1], :desc => ret[2] }
- end
- end
-
-rescue LoadError, StandardError
- Gem.ssl_available = false
-end
-
-module Gem::SSL
-
- # We make our own versions of the constants here. This allows us
- # to reference the constants, even though some systems might not
- # have SSL installed in the Ruby core package.
- #
- # These constants are only used during load time. At runtime, any
- # method that makes a direct reference to SSL software must be
- # protected with a Gem.ensure_ssl_available call.
- #
- if Gem.ssl_available? then
- PKEY_RSA = OpenSSL::PKey::RSA
- DIGEST_SHA1 = OpenSSL::Digest::SHA1
- else
- PKEY_RSA = :rsa
- DIGEST_SHA1 = :sha1
- end
-
-end
-
diff --git a/lib/rubygems/gem_path_searcher.rb b/lib/rubygems/gem_path_searcher.rb
deleted file mode 100644
index e2b8543bb0..0000000000
--- a/lib/rubygems/gem_path_searcher.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'rubygems'
-
-##
-# GemPathSearcher has the capability to find loadable files inside
-# gems. It generates data up front to speed up searches later.
-
-class Gem::GemPathSearcher
-
- ##
- # Initialise the data we need to make searches later.
-
- def initialize
- # We want a record of all the installed gemspecs, in the order
- # we wish to examine them.
- @gemspecs = init_gemspecs
- # Map gem spec to glob of full require_path directories.
- # Preparing this information may speed up searches later.
- @lib_dirs = {}
- @gemspecs.each do |spec|
- @lib_dirs[spec.object_id] = lib_dirs_for(spec)
- end
- end
-
- ##
- # Look in all the installed gems until a matching _path_ is found.
- # Return the _gemspec_ of the gem where it was found. If no match
- # is found, return nil.
- #
- # The gems are searched in alphabetical order, and in reverse
- # version order.
- #
- # For example:
- #
- # find('log4r') # -> (log4r-1.1 spec)
- # find('log4r.rb') # -> (log4r-1.1 spec)
- # find('rake/rdoctask') # -> (rake-0.4.12 spec)
- # find('foobarbaz') # -> nil
- #
- # Matching paths can have various suffixes ('.rb', '.so', and
- # others), which may or may not already be attached to _file_.
- # This method doesn't care about the full filename that matches;
- # only that there is a match.
-
- def find(path)
- @gemspecs.find do |spec| matching_file? spec, path end
- end
-
- ##
- # Works like #find, but finds all gemspecs matching +path+.
-
- def find_all(path)
- @gemspecs.select do |spec|
- matching_file? spec, path
- end
- end
-
- ##
- # Attempts to find a matching path using the require_paths of the given
- # +spec+.
-
- def matching_file?(spec, path)
- !matching_files(spec, path).empty?
- end
-
- ##
- # Returns files matching +path+ in +spec+.
- #--
- # Some of the intermediate results are cached in @lib_dirs for speed.
-
- def matching_files(spec, path)
- glob = File.join @lib_dirs[spec.object_id], "#{path}#{Gem.suffix_pattern}"
- Dir[glob].select { |f| File.file? f.untaint }
- end
-
- ##
- # Return a list of all installed gemspecs, sorted by alphabetical order and
- # in reverse version order.
-
- def init_gemspecs
- Gem.source_index.map { |_, spec| spec }.sort { |a,b|
- (a.name <=> b.name).nonzero? || (b.version <=> a.version)
- }
- end
-
- ##
- # Returns library directories glob for a gemspec. For example,
- # '/usr/local/lib/ruby/gems/1.8/gems/foobar-1.0/{lib,ext}'
-
- def lib_dirs_for(spec)
- "#{spec.full_gem_path}/{#{spec.require_paths.join(',')}}"
- end
-
-end
-
diff --git a/lib/rubygems/gem_runner.rb b/lib/rubygems/gem_runner.rb
index 5f91398b5b..e60cebd0cb 100644
--- a/lib/rubygems/gem_runner.rb
+++ b/lib/rubygems/gem_runner.rb
@@ -1,58 +1,88 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems/command_manager'
-require 'rubygems/config_file'
-require 'rubygems/doc_manager'
+require_relative "../rubygems"
+require_relative "command_manager"
+
+##
+# Run an instance of the gem program.
+#
+# Gem::GemRunner is only intended for internal use by RubyGems itself. It
+# does not form any public API and may change at any time for any reason.
+#
+# If you would like to duplicate functionality of `gem` commands, use the
+# classes they call directly.
+
+class Gem::GemRunner
+ def initialize
+ @command_manager_class = Gem::CommandManager
+ @config_file_class = Gem::ConfigFile
+ end
+
+ ##
+ # Run the gem command with the following arguments.
-module Gem
+ def run(args)
+ validate_encoding args
+ build_args = extract_build_args args
- ####################################################################
- # Run an instance of the gem program.
- #
- class GemRunner
+ do_configuration args
- def initialize(options={})
- @command_manager_class = options[:command_manager] || Gem::CommandManager
- @config_file_class = options[:config_file] || Gem::ConfigFile
- @doc_manager_class = options[:doc_manager] || Gem::DocManager
+ begin
+ Gem.load_env_plugins
+ rescue StandardError
+ nil
end
+ Gem.load_plugins
- # Run the gem command with the following arguments.
- def run(args)
- start_time = Time.now
- do_configuration(args)
- cmd = @command_manager_class.instance
- cmd.command_names.each do |command_name|
- config_args = Gem.configuration[command_name]
- config_args = case config_args
- when String
- config_args.split ' '
- else
- Array(config_args)
- end
- Command.add_specific_extra_args command_name, config_args
- end
- cmd.run(Gem.configuration.args)
- end_time = Time.now
- if Gem.configuration.benchmark
- printf "\nExecution time: %0.2f seconds.\n", end_time-start_time
- puts "Press Enter to finish"
- STDIN.gets
+ cmd = @command_manager_class.instance
+
+ cmd.command_names.each do |command_name|
+ config_args = Gem.configuration[command_name]
+ config_args = case config_args
+ when String
+ config_args.split " "
+ else
+ Array(config_args)
end
+ Gem::Command.add_specific_extra_args command_name, config_args
end
- private
+ cmd.run Gem.configuration.args, build_args
+ end
+
+ ##
+ # Separates the build arguments (those following <code>--</code>) from the
+ # other arguments in the list.
+
+ def extract_build_args(args) # :nodoc:
+ return [] unless offset = args.index("--")
+
+ build_args = args.slice!(offset...args.length)
+
+ build_args.shift
+
+ build_args
+ end
+
+ private
+
+ def validate_encoding(args)
+ invalid_arg = args.find {|arg| !arg.valid_encoding? }
- def do_configuration(args)
- Gem.configuration = @config_file_class.new(args)
- Gem.use_paths(Gem.configuration[:gemhome], Gem.configuration[:gempath])
- Gem::Command.extra_args = Gem.configuration[:gem]
- @doc_manager_class.configured_args = Gem.configuration[:rdoc]
+ if invalid_arg
+ raise Gem::OptionParser::InvalidArgument.new("'#{invalid_arg.scrub}' has invalid encoding")
end
+ end
- end # class
-end # module
+ def do_configuration(args)
+ Gem.configuration = @config_file_class.new(args)
+ Gem.use_paths Gem.configuration[:gemhome], Gem.configuration[:gempath]
+ Gem::Command.extra_args = Gem.configuration[:gem]
+ end
+end
diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb
new file mode 100644
index 0000000000..9c22c14fad
--- /dev/null
+++ b/lib/rubygems/gemcutter_utilities.rb
@@ -0,0 +1,398 @@
+# frozen_string_literal: true
+
+require_relative "remote_fetcher"
+require_relative "text"
+require_relative "gemcutter_utilities/webauthn_listener"
+require_relative "gemcutter_utilities/webauthn_poller"
+
+##
+# Utility methods for using the RubyGems API.
+
+module Gem::GemcutterUtilities
+ ERROR_CODE = 1
+ API_SCOPES = [:index_rubygems, :push_rubygem, :yank_rubygem, :add_owner, :remove_owner, :access_webhooks].freeze
+ EXCLUSIVELY_API_SCOPES = [:show_dashboard].freeze
+
+ include Gem::Text
+
+ attr_writer :host
+ attr_writer :scope
+
+ ##
+ # Add the --key option
+
+ def add_key_option
+ add_option("-k", "--key KEYNAME", Symbol,
+ "Use the given API key",
+ "from #{Gem.configuration.credentials_path}") do |value,options|
+ options[:key] = value
+ end
+ end
+
+ ##
+ # Add the --otp option
+
+ def add_otp_option
+ add_option("--otp CODE",
+ "Digit code for multifactor authentication",
+ "You can also use the environment variable GEM_HOST_OTP_CODE") do |value, options|
+ options[:otp] = value
+ end
+ end
+
+ ##
+ # The API key from the command options or from the user's configuration.
+
+ def api_key
+ if ENV["GEM_HOST_API_KEY"]
+ ENV["GEM_HOST_API_KEY"]
+ elsif options[:key]
+ verify_api_key options[:key]
+ elsif Gem.configuration.api_keys.key?(host)
+ Gem.configuration.api_keys[host]
+ else
+ Gem.configuration.rubygems_api_key
+ end
+ end
+
+ ##
+ # The OTP code from the command options or from the user's configuration.
+
+ def otp
+ options[:otp] || ENV["GEM_HOST_OTP_CODE"]
+ end
+
+ def webauthn_enabled?
+ options[:webauthn]
+ end
+
+ ##
+ # The host to connect to either from the RUBYGEMS_HOST environment variable
+ # or from the user's configuration
+
+ def host
+ configured_host = Gem.host unless
+ Gem.configuration.disable_default_gem_server
+
+ @host ||=
+ begin
+ env_rubygems_host = ENV["RUBYGEMS_HOST"]
+ env_rubygems_host = nil if env_rubygems_host&.empty?
+
+ env_rubygems_host || configured_host
+ end
+ end
+
+ ##
+ # Creates an RubyGems API to +host+ and +path+ with the given HTTP +method+.
+ #
+ # If +allowed_push_host+ metadata is present, then it will only allow that host.
+
+ def rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, credentials: {}, &block)
+ require_relative "vendored_net_http"
+
+ self.host = host if host
+ unless self.host
+ alert_error "You must specify a gem server"
+ terminate_interaction(ERROR_CODE)
+ end
+
+ if allowed_push_host
+ allowed_host_uri = Gem::URI.parse(allowed_push_host)
+ host_uri = Gem::URI.parse(self.host)
+
+ unless (host_uri.scheme == allowed_host_uri.scheme) && (host_uri.host == allowed_host_uri.host)
+ alert_error "#{self.host.inspect} is not allowed by the gemspec, which only allows #{allowed_push_host.inspect}"
+ terminate_interaction(ERROR_CODE)
+ end
+ end
+
+ uri = Gem::URI.parse "#{self.host}/#{path}"
+ response = request_with_otp(method, uri, &block)
+
+ if mfa_unauthorized?(response)
+ fetch_otp(credentials)
+ response = request_with_otp(method, uri, &block)
+ end
+
+ if api_key_forbidden?(response)
+ update_scope(scope)
+ request_with_otp(method, uri, &block)
+ else
+ response
+ end
+ end
+
+ def mfa_unauthorized?(response)
+ response.is_a?(Gem::Net::HTTPUnauthorized) && response.body.start_with?("You have enabled multifactor authentication")
+ end
+
+ def update_scope(scope)
+ sign_in_host = host
+ pretty_host = pretty_host(sign_in_host)
+ update_scope_params = { scope => true }
+
+ say "The existing key doesn't have access of #{scope} on #{pretty_host}. Please sign in to update access."
+
+ identifier = ask "Username/email: "
+ password = ask_for_password " Password: "
+
+ response = rubygems_api_request(:put, "api/v1/api_key",
+ sign_in_host, scope: scope) do |request|
+ request.basic_auth identifier, password
+ request.body = Gem::URI.encode_www_form({ api_key: api_key }.merge(update_scope_params))
+ end
+
+ with_response response do |_resp|
+ say "Added #{scope} scope to the existing API key"
+ end
+ end
+
+ ##
+ # Signs in with the RubyGems API at +sign_in_host+ and sets the rubygems API
+ # key.
+
+ def sign_in(sign_in_host = nil, scope: nil)
+ sign_in_host ||= host
+ pretty_host = pretty_host(sign_in_host)
+ if api_key
+ say "You are already signed in on #{pretty_host}."
+ return
+ end
+ say "Enter your #{pretty_host} credentials."
+ say "Don't have an account yet? " \
+ "Create one at #{sign_in_host}/sign_up"
+
+ identifier = ask "Username/email: "
+ password = ask_for_password " Password: "
+ say "\n"
+
+ key_name = get_key_name(scope)
+ scope_params = get_scope_params(scope)
+ profile = get_user_profile(identifier, password)
+ mfa_params = get_mfa_params(profile)
+ all_params = scope_params.merge(mfa_params)
+ warning = profile["warning"]
+ credentials = { identifier: identifier, password: password }
+
+ say "#{warning}\n" if warning
+
+ response = rubygems_api_request(:post, "api/v1/api_key",
+ sign_in_host, credentials: credentials, scope: scope) do |request|
+ request.basic_auth identifier, password
+ request.body = Gem::URI.encode_www_form({ name: key_name }.merge(all_params))
+ end
+
+ with_response response do |resp|
+ say "Signed in with API key: #{key_name}."
+ set_api_key host, resp.body
+ end
+ end
+
+ ##
+ # Retrieves the pre-configured API key +key+ or terminates interaction with
+ # an error.
+
+ def verify_api_key(key)
+ if Gem.configuration.api_keys.key? key
+ Gem.configuration.api_keys[key]
+ else
+ alert_error "No such API key. Please add it to your configuration (done automatically on initial `gem push`)."
+ terminate_interaction(ERROR_CODE)
+ end
+ end
+
+ ##
+ # If +response+ is an HTTP Success (2XX) response, yields the response if a
+ # block was given or shows the response body to the user.
+ #
+ # If the response was not successful, shows an error to the user including
+ # the +error_prefix+ and the response body. If the response was a permanent redirect,
+ # shows an error to the user including the redirect location.
+
+ def with_response(response, error_prefix = nil)
+ case response
+ when Gem::Net::HTTPSuccess then
+ if block_given?
+ yield response
+ else
+ say clean_text(response.body)
+ end
+ when Gem::Net::HTTPPermanentRedirect, Gem::Net::HTTPRedirection then
+ message = "The request has redirected permanently to #{response["location"]}. Please check your defined push host URL."
+ message = "#{error_prefix}: #{message}" if error_prefix
+
+ say clean_text(message)
+ terminate_interaction(ERROR_CODE)
+ else
+ message = response.body
+ message = "#{error_prefix}: #{message}" if error_prefix
+
+ say clean_text(message)
+ terminate_interaction(ERROR_CODE)
+ end
+ end
+
+ ##
+ # Returns true when the user has enabled multifactor authentication from
+ # +response+ text and no otp provided by options.
+
+ def set_api_key(host, key)
+ if default_host?
+ Gem.configuration.rubygems_api_key = key
+ else
+ Gem.configuration.set_api_key host, key
+ end
+ end
+
+ private
+
+ def request_with_otp(method, uri, &block)
+ request_method = Gem::Net::HTTP.const_get method.to_s.capitalize
+
+ Gem::RemoteFetcher.fetcher.request(uri, request_method) do |req|
+ req["OTP"] = otp if otp
+ block.call(req)
+ end
+ ensure
+ options[:otp] = nil if webauthn_enabled?
+ end
+
+ def fetch_otp(credentials)
+ options[:otp] = if webauthn_url = webauthn_verification_url(credentials)
+ server = TCPServer.new 0
+ port = server.addr[1].to_s
+
+ url_with_port = "#{webauthn_url}?port=#{port}"
+ say "You have enabled multi-factor authentication. Please visit the following URL to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option."
+ say ""
+ say url_with_port
+ say ""
+
+ threads = [WebauthnListener.listener_thread(host, server), WebauthnPoller.poll_thread(options, host, webauthn_url, credentials)]
+ otp_thread = wait_for_otp_thread(*threads)
+
+ threads.each(&:join)
+
+ if error = otp_thread[:error]
+ alert_error error.message
+ terminate_interaction(1)
+ end
+
+ options[:webauthn] = true
+
+ say "You are verified with a security device. You may close the browser window."
+ otp_thread[:otp]
+ else
+ say "You have enabled multi-factor authentication. Please enter OTP code."
+ ask "Code: "
+ end
+ end
+
+ def wait_for_otp_thread(*threads)
+ loop do
+ threads.each do |otp_thread|
+ return otp_thread unless otp_thread.alive?
+ end
+ sleep 0.1
+ end
+ ensure
+ threads.each(&:exit)
+ end
+
+ def webauthn_verification_url(credentials)
+ response = rubygems_api_request(:post, "api/v1/webauthn_verification") do |request|
+ if credentials.empty?
+ request.add_field "Authorization", api_key
+ else
+ request.basic_auth credentials[:identifier], credentials[:password]
+ end
+ end
+ response.is_a?(Gem::Net::HTTPSuccess) ? response.body : nil
+ end
+
+ def pretty_host(host)
+ if default_host?
+ "RubyGems.org"
+ else
+ host
+ end
+ end
+
+ def get_scope_params(scope)
+ scope_params = { index_rubygems: true, push_rubygem: true }
+
+ if scope
+ scope_params = { scope => true }
+ else
+ say "The default access scope is:"
+ scope_params.each do |k, _v|
+ say " #{k}: y"
+ end
+ say "\n"
+ customise = ask_yes_no("Do you want to customise scopes?", false)
+ if customise
+ EXCLUSIVELY_API_SCOPES.each do |excl_scope|
+ selected = ask_yes_no("#{excl_scope} (exclusive scope, answering yes will not prompt for other scopes)", false)
+ next unless selected
+
+ return { excl_scope => true }
+ end
+
+ scope_params = {}
+
+ API_SCOPES.each do |s|
+ selected = ask_yes_no(s.to_s, false)
+ scope_params[s] = true if selected
+ end
+ end
+ say "\n"
+ end
+
+ scope_params
+ end
+
+ def default_host?
+ host == Gem::DEFAULT_HOST
+ end
+
+ def get_user_profile(identifier, password)
+ return {} unless default_host?
+
+ response = rubygems_api_request(:get, "api/v1/profile/me.yaml") do |request|
+ request.basic_auth identifier, password
+ end
+
+ with_response response do |resp|
+ Gem::ConfigFile.load_with_rubygems_config_hash(clean_text(resp.body))
+ end
+ end
+
+ def get_mfa_params(profile)
+ mfa_level = profile["mfa"]
+ params = {}
+ if ["ui_only", "ui_and_gem_signin"].include?(mfa_level)
+ selected = ask_yes_no("Would you like to enable MFA for this key? (strongly recommended)")
+ params["mfa"] = true if selected
+ end
+ params
+ end
+
+ def get_key_name(scope)
+ hostname = Socket.gethostname || "unknown-host"
+ user = ENV["USER"] || ENV["USERNAME"] || "unknown-user"
+ ts = Time.now.strftime("%Y%m%d%H%M%S")
+ default_key_name = "#{hostname}-#{user}-#{ts}"
+
+ key_name = ask "API Key name [#{default_key_name}]: " unless scope
+ if key_name.nil? || key_name.empty?
+ default_key_name
+ else
+ key_name
+ end
+ end
+
+ def api_key_forbidden?(response)
+ response.is_a?(Gem::Net::HTTPForbidden) && response.body.start_with?("The API key doesn't have access")
+ end
+end
diff --git a/lib/rubygems/gemcutter_utilities/webauthn_listener.rb b/lib/rubygems/gemcutter_utilities/webauthn_listener.rb
new file mode 100644
index 0000000000..3f56a077c9
--- /dev/null
+++ b/lib/rubygems/gemcutter_utilities/webauthn_listener.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require_relative "webauthn_listener/response"
+
+##
+# The WebauthnListener class retrieves an OTP after a user successfully WebAuthns with the Gem host.
+# An instance opens a socket using the TCPServer instance given and listens for a request from the Gem host.
+# The request should be a GET request to the root path and contains the OTP code in the form
+# of a query parameter `code`. The listener will return the code which will be used as the OTP for
+# API requests.
+#
+# Types of responses sent by the listener after receiving a request:
+# - 200 OK: OTP code was successfully retrieved
+# - 204 No Content: If the request was an OPTIONS request
+# - 400 Bad Request: If the request did not contain a query parameter `code`
+# - 404 Not Found: The request was not to the root path
+# - 405 Method Not Allowed: OTP code was not retrieved because the request was not a GET/OPTIONS request
+#
+# Example usage:
+#
+# thread = Gem::WebauthnListener.listener_thread("https://rubygems.example", server)
+# thread.join
+# otp = thread[:otp]
+# error = thread[:error]
+#
+
+module Gem::GemcutterUtilities
+ class WebauthnListener
+ attr_reader :host
+
+ def initialize(host)
+ @host = host
+ end
+
+ def self.listener_thread(host, server)
+ Thread.new do
+ thread = Thread.current
+ thread.abort_on_exception = true
+ thread.report_on_exception = false
+ thread[:otp] = new(host).wait_for_otp_code(server)
+ rescue Gem::WebauthnVerificationError => e
+ thread[:error] = e
+ ensure
+ server.close
+ end
+ end
+
+ def wait_for_otp_code(server)
+ loop do
+ socket = server.accept
+ request_line = socket.gets
+
+ method, req_uri, _protocol = request_line.split(" ")
+ req_uri = Gem::URI.parse(req_uri)
+
+ responder = SocketResponder.new(socket)
+
+ unless root_path?(req_uri)
+ responder.send(NotFoundResponse.for(host))
+ raise Gem::WebauthnVerificationError, "Page at #{req_uri.path} not found."
+ end
+
+ case method.upcase
+ when "OPTIONS"
+ responder.send(NoContentResponse.for(host))
+ next # will be GET
+ when "GET"
+ if otp = parse_otp_from_uri(req_uri)
+ responder.send(OkResponse.for(host))
+ return otp
+ end
+ responder.send(BadRequestResponse.for(host))
+ raise Gem::WebauthnVerificationError, "Did not receive OTP from #{host}."
+ else
+ responder.send(MethodNotAllowedResponse.for(host))
+ raise Gem::WebauthnVerificationError, "Invalid HTTP method #{method.upcase} received."
+ end
+ end
+ end
+
+ private
+
+ def root_path?(uri)
+ uri.path == "/"
+ end
+
+ def parse_otp_from_uri(uri)
+ query = uri.query
+ return unless query && !query.empty?
+
+ query.split("&") do |param|
+ key, value = param.split("=", 2)
+ if value && Gem::URI.decode_www_form_component(key) == "code"
+ return Gem::URI.decode_www_form_component(value)
+ end
+ end
+
+ nil
+ end
+
+ class SocketResponder
+ def initialize(socket)
+ @socket = socket
+ end
+
+ def send(response)
+ @socket.print response.to_s
+ @socket.close
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb b/lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb
new file mode 100644
index 0000000000..17baa64fff
--- /dev/null
+++ b/lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+##
+# The WebauthnListener Response class is used by the WebauthnListener to create
+# responses to be sent to the Gem host. It creates a Gem::Net::HTTPResponse instance
+# when initialized and can be converted to the appropriate format to be sent by a socket using `to_s`.
+# Gem::Net::HTTPResponse instances cannot be directly sent over a socket.
+#
+# Types of response classes:
+# - OkResponse
+# - NoContentResponse
+# - BadRequestResponse
+# - NotFoundResponse
+# - MethodNotAllowedResponse
+#
+# Example usage:
+#
+# server = TCPServer.new(0)
+# socket = server.accept
+#
+# response = OkResponse.for("https://rubygems.example")
+# socket.print response.to_s
+# socket.close
+#
+
+module Gem::GemcutterUtilities
+ class WebauthnListener
+ class Response
+ attr_reader :http_response
+
+ def self.for(host)
+ new(host)
+ end
+
+ def initialize(host)
+ @host = host
+
+ build_http_response
+ end
+
+ def to_s
+ status_line = "HTTP/#{@http_response.http_version} #{@http_response.code} #{@http_response.message}\r\n"
+ headers = @http_response.to_hash.map {|header, value| "#{header}: #{value.join(", ")}\r\n" }.join + "\r\n"
+ body = @http_response.body ? "#{@http_response.body}\n" : ""
+
+ status_line + headers + body
+ end
+
+ private
+
+ # Must be implemented in subclasses
+ def code
+ raise NotImplementedError
+ end
+
+ def reason_phrase
+ raise NotImplementedError
+ end
+
+ def body; end
+
+ def build_http_response
+ response_class = Gem::Net::HTTPResponse::CODE_TO_OBJ[code.to_s]
+ @http_response = response_class.new("1.1", code, reason_phrase)
+ @http_response.instance_variable_set(:@read, true)
+
+ add_connection_header
+ add_access_control_headers
+ add_body
+ end
+
+ def add_connection_header
+ @http_response["connection"] = "close"
+ end
+
+ def add_access_control_headers
+ @http_response["access-control-allow-origin"] = @host
+ @http_response["access-control-allow-methods"] = "POST"
+ @http_response["access-control-allow-headers"] = %w[Content-Type Authorization x-csrf-token]
+ end
+
+ def add_body
+ return unless body
+ @http_response["content-type"] = "text/plain; charset=utf-8"
+ @http_response["content-length"] = body.bytesize
+ @http_response.instance_variable_set(:@body, body)
+ end
+ end
+
+ class OkResponse < Response
+ private
+
+ def code
+ 200
+ end
+
+ def reason_phrase
+ "OK"
+ end
+
+ def body
+ "success"
+ end
+ end
+
+ class NoContentResponse < Response
+ private
+
+ def code
+ 204
+ end
+
+ def reason_phrase
+ "No Content"
+ end
+ end
+
+ class BadRequestResponse < Response
+ private
+
+ def code
+ 400
+ end
+
+ def reason_phrase
+ "Bad Request"
+ end
+
+ def body
+ "missing code parameter"
+ end
+ end
+
+ class NotFoundResponse < Response
+ private
+
+ def code
+ 404
+ end
+
+ def reason_phrase
+ "Not Found"
+ end
+ end
+
+ class MethodNotAllowedResponse < Response
+ private
+
+ def code
+ 405
+ end
+
+ def reason_phrase
+ "Method Not Allowed"
+ end
+
+ def add_access_control_headers
+ super
+ @http_response["allow"] = %w[GET OPTIONS]
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/gemcutter_utilities/webauthn_poller.rb b/lib/rubygems/gemcutter_utilities/webauthn_poller.rb
new file mode 100644
index 0000000000..fe3f163a88
--- /dev/null
+++ b/lib/rubygems/gemcutter_utilities/webauthn_poller.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+##
+# The WebauthnPoller class retrieves an OTP after a user successfully WebAuthns. An instance
+# polls the Gem host for the OTP code. The polling request (api/v1/webauthn_verification/<webauthn_token>/status.json)
+# is sent to the Gem host every 5 seconds and will timeout after 5 minutes. If the status field in the json response
+# is "success", the code field will contain the OTP code.
+#
+# Example usage:
+#
+# thread = Gem::WebauthnPoller.poll_thread(
+# {},
+# "RubyGems.org",
+# "https://rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY",
+# { email: "email@example.com", password: "password" }
+# )
+# thread.join
+# otp = thread[:otp]
+# error = thread[:error]
+#
+
+module Gem::GemcutterUtilities
+ class WebauthnPoller
+ include Gem::GemcutterUtilities
+ TIMEOUT_IN_SECONDS = 300
+
+ attr_reader :options, :host
+
+ def initialize(options, host)
+ @options = options
+ @host = host
+ end
+
+ def self.poll_thread(options, host, webauthn_url, credentials)
+ Thread.new do
+ thread = Thread.current
+ thread.abort_on_exception = true
+ thread.report_on_exception = false
+ thread[:otp] = new(options, host).poll_for_otp(webauthn_url, credentials)
+ rescue Gem::WebauthnVerificationError, Gem::Timeout::Error => e
+ thread[:error] = e
+ end
+ end
+
+ def poll_for_otp(webauthn_url, credentials)
+ Gem::Timeout.timeout(TIMEOUT_IN_SECONDS) do
+ loop do
+ response = webauthn_verification_poll_response(webauthn_url, credentials)
+ raise Gem::WebauthnVerificationError, response.message unless response.is_a?(Gem::Net::HTTPSuccess)
+
+ require "json"
+ parsed_response = JSON.parse(response.body)
+ case parsed_response["status"]
+ when "pending"
+ sleep 5
+ when "success"
+ return parsed_response["code"]
+ else
+ raise Gem::WebauthnVerificationError, parsed_response.fetch("message", "Invalid response from server")
+ end
+ end
+ end
+ end
+
+ private
+
+ def webauthn_verification_poll_response(webauthn_url, credentials)
+ webauthn_token = %r{(?<=\/)[^\/]+(?=$)}.match(webauthn_url)[0]
+ rubygems_api_request(:get, "api/v1/webauthn_verification/#{webauthn_token}/status.json") do |request|
+ if credentials.empty?
+ request.add_field "Authorization", api_key
+ elsif credentials[:identifier] && credentials[:password]
+ request.basic_auth credentials[:identifier], credentials[:password]
+ else
+ raise Gem::WebauthnVerificationError, "Provided missing credentials"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/gemspec_helpers.rb b/lib/rubygems/gemspec_helpers.rb
new file mode 100644
index 0000000000..2b20fcafa1
--- /dev/null
+++ b/lib/rubygems/gemspec_helpers.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require_relative "../rubygems"
+
+##
+# Mixin methods for commands that work with gemspecs.
+
+module Gem::GemspecHelpers
+ def find_gemspec(glob = "*.gemspec")
+ gemspecs = Dir.glob(glob).sort
+
+ if gemspecs.size > 1
+ alert_error "Multiple gemspecs found: #{gemspecs}, please specify one"
+ terminate_interaction(1)
+ end
+
+ gemspecs.first
+ end
+end
diff --git a/lib/rubygems/indexer.rb b/lib/rubygems/indexer.rb
deleted file mode 100644
index e2dd57d3fe..0000000000
--- a/lib/rubygems/indexer.rb
+++ /dev/null
@@ -1,370 +0,0 @@
-require 'fileutils'
-require 'tmpdir'
-require 'zlib'
-
-require 'rubygems'
-require 'rubygems/format'
-
-begin
- require 'builder/xchar'
-rescue LoadError
-end
-
-##
-# Top level class for building the gem repository index.
-
-class Gem::Indexer
-
- include Gem::UserInteraction
-
- ##
- # Index install location
-
- attr_reader :dest_directory
-
- ##
- # Index build directory
-
- attr_reader :directory
-
- ##
- # Create an indexer that will index the gems in +directory+.
-
- def initialize(directory)
- unless ''.respond_to? :to_xs then
- fail "Gem::Indexer requires that the XML Builder library be installed:" \
- "\n\tgem install builder"
- end
-
- @dest_directory = directory
- @directory = File.join Dir.tmpdir, "gem_generate_index_#{$$}"
-
- marshal_name = "Marshal.#{Gem.marshal_version}"
-
- @master_index = File.join @directory, 'yaml'
- @marshal_index = File.join @directory, marshal_name
-
- @quick_dir = File.join @directory, 'quick'
-
- @quick_marshal_dir = File.join @quick_dir, marshal_name
-
- @quick_index = File.join @quick_dir, 'index'
- @latest_index = File.join @quick_dir, 'latest_index'
-
- @specs_index = File.join @directory, "specs.#{Gem.marshal_version}"
- @latest_specs_index = File.join @directory,
- "latest_specs.#{Gem.marshal_version}"
-
- files = [
- @specs_index,
- "#{@specs_index}.gz",
- @latest_specs_index,
- "#{@latest_specs_index}.gz",
- @quick_dir,
- @master_index,
- "#{@master_index}.Z",
- @marshal_index,
- "#{@marshal_index}.Z",
- ]
-
- @files = files.map do |path|
- path.sub @directory, ''
- end
- end
-
- ##
- # Abbreviate the spec for downloading. Abbreviated specs are only used for
- # searching, downloading and related activities and do not need deployment
- # specific information (e.g. list of files). So we abbreviate the spec,
- # making it much smaller for quicker downloads.
-
- def abbreviate(spec)
- spec.files = []
- spec.test_files = []
- spec.rdoc_options = []
- spec.extra_rdoc_files = []
- spec.cert_chain = []
- spec
- end
-
- ##
- # Build various indicies
-
- def build_indicies(index)
- progress = ui.progress_reporter index.size,
- "Generating quick index gemspecs for #{index.size} gems",
- "Complete"
-
- index.each do |original_name, spec|
- spec_file_name = "#{original_name}.gemspec.rz"
- yaml_name = File.join @quick_dir, spec_file_name
- marshal_name = File.join @quick_marshal_dir, spec_file_name
-
- yaml_zipped = Gem.deflate spec.to_yaml
- open yaml_name, 'wb' do |io| io.write yaml_zipped end
-
- marshal_zipped = Gem.deflate Marshal.dump(spec)
- open marshal_name, 'wb' do |io| io.write marshal_zipped end
-
- progress.updated original_name
- end
-
- progress.done
-
- say "Generating specs index"
-
- open @specs_index, 'wb' do |io|
- specs = index.sort.map do |_, spec|
- platform = spec.original_platform
- platform = Gem::Platform::RUBY if platform.nil? or platform.empty?
- [spec.name, spec.version, platform]
- end
-
- specs = compact_specs specs
-
- Marshal.dump specs, io
- end
-
- say "Generating latest specs index"
-
- open @latest_specs_index, 'wb' do |io|
- specs = index.latest_specs.sort.map do |spec|
- platform = spec.original_platform
- platform = Gem::Platform::RUBY if platform.nil? or platform.empty?
- [spec.name, spec.version, platform]
- end
-
- specs = compact_specs specs
-
- Marshal.dump specs, io
- end
-
- say "Generating quick index"
-
- quick_index = File.join @quick_dir, 'index'
- open quick_index, 'wb' do |io|
- io.puts index.sort.map { |_, spec| spec.original_name }
- end
-
- say "Generating latest index"
-
- latest_index = File.join @quick_dir, 'latest_index'
- open latest_index, 'wb' do |io|
- io.puts index.latest_specs.sort.map { |spec| spec.original_name }
- end
-
- say "Generating Marshal master index"
-
- open @marshal_index, 'wb' do |io|
- io.write index.dump
- end
-
- progress = ui.progress_reporter index.size,
- "Generating YAML master index for #{index.size} gems (this may take a while)",
- "Complete"
-
- open @master_index, 'wb' do |io|
- io.puts "--- !ruby/object:#{index.class}"
- io.puts "gems:"
-
- gems = index.sort_by { |name, gemspec| gemspec.sort_obj }
- gems.each do |original_name, gemspec|
- yaml = gemspec.to_yaml.gsub(/^/, ' ')
- yaml = yaml.sub(/\A ---/, '') # there's a needed extra ' ' here
- io.print " #{original_name}:"
- io.puts yaml
-
- progress.updated original_name
- end
- end
-
- progress.done
-
- say "Compressing indicies"
- # use gzip for future files.
-
- compress quick_index, 'rz'
- paranoid quick_index, 'rz'
-
- compress latest_index, 'rz'
- paranoid latest_index, 'rz'
-
- compress @marshal_index, 'Z'
- paranoid @marshal_index, 'Z'
-
- compress @master_index, 'Z'
- paranoid @master_index, 'Z'
-
- gzip @specs_index
- gzip @latest_specs_index
- end
-
- ##
- # Collect specifications from .gem files from the gem directory.
-
- def collect_specs
- index = Gem::SourceIndex.new
-
- progress = ui.progress_reporter gem_file_list.size,
- "Loading #{gem_file_list.size} gems from #{@dest_directory}",
- "Loaded all gems"
-
- gem_file_list.each do |gemfile|
- if File.size(gemfile.to_s) == 0 then
- alert_warning "Skipping zero-length gem: #{gemfile}"
- next
- end
-
- begin
- spec = Gem::Format.from_file_by_path(gemfile).spec
-
- unless gemfile =~ /\/#{Regexp.escape spec.original_name}.*\.gem\z/i then
- alert_warning "Skipping misnamed gem: #{gemfile} => #{spec.full_name} (#{spec.original_name})"
- next
- end
-
- abbreviate spec
- sanitize spec
-
- index.gems[spec.original_name] = spec
-
- progress.updated spec.original_name
-
- rescue SignalException => e
- alert_error "Received signal, exiting"
- raise
- rescue Exception => e
- alert_error "Unable to process #{gemfile}\n#{e.message} (#{e.class})\n\t#{e.backtrace.join "\n\t"}"
- end
- end
-
- progress.done
-
- index
- end
-
- ##
- # Compacts Marshal output for the specs index data source by using identical
- # objects as much as possible.
-
- def compact_specs(specs)
- names = {}
- versions = {}
- platforms = {}
-
- specs.map do |(name, version, platform)|
- names[name] = name unless names.include? name
- versions[version] = version unless versions.include? version
- platforms[platform] = platform unless platforms.include? platform
-
- [names[name], versions[version], platforms[platform]]
- end
- end
-
- ##
- # Compress +filename+ with +extension+.
-
- def compress(filename, extension)
- data = Gem.read_binary filename
-
- zipped = Gem.deflate data
-
- open "#{filename}.#{extension}", 'wb' do |io|
- io.write zipped
- end
- end
-
- ##
- # List of gem file names to index.
-
- def gem_file_list
- Dir.glob(File.join(@dest_directory, "gems", "*.gem"))
- end
-
- ##
- # Builds and installs indexicies.
-
- def generate_index
- make_temp_directories
- index = collect_specs
- build_indicies index
- install_indicies
- rescue SignalException
- ensure
- FileUtils.rm_rf @directory
- end
-
- ##
- # Zlib::GzipWriter wrapper that gzips +filename+ on disk.
-
- def gzip(filename)
- Zlib::GzipWriter.open "#{filename}.gz" do |io|
- io.write Gem.read_binary(filename)
- end
- end
-
- ##
- # Install generated indicies into the destination directory.
-
- def install_indicies
- verbose = Gem.configuration.really_verbose
-
- say "Moving index into production dir #{@dest_directory}" if verbose
-
- @files.each do |file|
- src_name = File.join @directory, file
- dst_name = File.join @dest_directory, file
-
- FileUtils.rm_rf dst_name, :verbose => verbose
- FileUtils.mv src_name, @dest_directory, :verbose => verbose,
- :force => true
- end
- end
-
- ##
- # Make directories for index generation
-
- def make_temp_directories
- FileUtils.rm_rf @directory
- FileUtils.mkdir_p @directory, :mode => 0700
- FileUtils.mkdir_p @quick_marshal_dir
- end
-
- ##
- # Ensure +path+ and path with +extension+ are identical.
-
- def paranoid(path, extension)
- data = Gem.read_binary path
- compressed_data = Gem.read_binary "#{path}.#{extension}"
-
- unless data == Gem.inflate(compressed_data) then
- raise "Compressed file #{compressed_path} does not match uncompressed file #{path}"
- end
- end
-
- ##
- # Sanitize the descriptive fields in the spec. Sometimes non-ASCII
- # characters will garble the site index. Non-ASCII characters will
- # be replaced by their XML entity equivalent.
-
- def sanitize(spec)
- spec.summary = sanitize_string(spec.summary)
- spec.description = sanitize_string(spec.description)
- spec.post_install_message = sanitize_string(spec.post_install_message)
- spec.authors = spec.authors.collect { |a| sanitize_string(a) }
-
- spec
- end
-
- ##
- # Sanitize a single string.
-
- def sanitize_string(string)
- # HACK the #to_s is in here because RSpec has an Array of Arrays of
- # Strings for authors. Need a way to disallow bad values on gempsec
- # generation. (Probably won't happen.)
- string ? string.to_s.to_xs : string
- end
-
-end
-
diff --git a/lib/rubygems/indexer/abstract_index_builder.rb b/lib/rubygems/indexer/abstract_index_builder.rb
deleted file mode 100644
index 5815dcda87..0000000000
--- a/lib/rubygems/indexer/abstract_index_builder.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-require 'zlib'
-
-require 'rubygems/indexer'
-
-# Abstract base class for building gem indicies. Uses the template pattern
-# with subclass specialization in the +begin_index+, +end_index+ and +cleanup+
-# methods.
-class Gem::Indexer::AbstractIndexBuilder
-
- # Directory to put index files in
- attr_reader :directory
-
- # File name of the generated index
- attr_reader :filename
-
- # List of written files/directories to move into production
- attr_reader :files
-
- def initialize(filename, directory)
- @filename = filename
- @directory = directory
- @files = []
- end
-
- ##
- # Build a Gem index. Yields to block to handle the details of the
- # actual building. Calls +begin_index+, +end_index+ and +cleanup+ at
- # appropriate times to customize basic operations.
-
- def build
- FileUtils.mkdir_p @directory unless File.exist? @directory
- raise "not a directory: #{@directory}" unless File.directory? @directory
-
- file_path = File.join @directory, @filename
-
- @files << @filename
-
- File.open file_path, "wb" do |file|
- @file = file
- start_index
- yield
- end_index
- end
-
- cleanup
- ensure
- @file = nil
- end
-
- ##
- # Compress the given file.
-
- def compress(filename, ext="rz")
- data = open filename, 'rb' do |fp| fp.read end
-
- zipped = zip data
-
- File.open "#{filename}.#{ext}", "wb" do |file|
- file.write zipped
- end
- end
-
- # Called immediately before the yield in build. The index file is open and
- # available as @file.
- def start_index
- end
-
- # Called immediately after the yield in build. The index file is still open
- # and available as @file.
- def end_index
- end
-
- # Called from within builder after the index file has been closed.
- def cleanup
- end
-
- # Return an uncompressed version of a compressed string.
- def unzip(string)
- Zlib::Inflate.inflate(string)
- end
-
- # Return a compressed version of the given string.
- def zip(string)
- Zlib::Deflate.deflate(string)
- end
-
-end
-
diff --git a/lib/rubygems/indexer/latest_index_builder.rb b/lib/rubygems/indexer/latest_index_builder.rb
deleted file mode 100644
index a5798580a6..0000000000
--- a/lib/rubygems/indexer/latest_index_builder.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'rubygems/indexer'
-
-##
-# Construct the latest Gem index file.
-
-class Gem::Indexer::LatestIndexBuilder < Gem::Indexer::AbstractIndexBuilder
-
- def start_index
- super
-
- @index = Gem::SourceIndex.new
- end
-
- def end_index
- super
-
- latest = @index.latest_specs.sort.map { |spec| spec.original_name }
-
- @file.write latest.join("\n")
- end
-
- def cleanup
- super
-
- compress @file.path
-
- @files.delete 'latest_index' # HACK installed via QuickIndexBuilder :/
- end
-
- def add(spec)
- @index.add_spec(spec)
- end
-
-end
-
diff --git a/lib/rubygems/indexer/marshal_index_builder.rb b/lib/rubygems/indexer/marshal_index_builder.rb
deleted file mode 100644
index e1a4d9f9b8..0000000000
--- a/lib/rubygems/indexer/marshal_index_builder.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require 'rubygems/indexer'
-
-# Construct the master Gem index file.
-class Gem::Indexer::MarshalIndexBuilder < Gem::Indexer::MasterIndexBuilder
- def end_index
- gems = {}
- index = Gem::SourceIndex.new
-
- @index.each do |name, gemspec|
- gems[gemspec.original_name] = gemspec
- end
-
- index.instance_variable_get(:@gems).replace gems
-
- @file.write index.dump
- end
-end
diff --git a/lib/rubygems/indexer/master_index_builder.rb b/lib/rubygems/indexer/master_index_builder.rb
deleted file mode 100644
index 669ea5a1df..0000000000
--- a/lib/rubygems/indexer/master_index_builder.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-require 'rubygems/indexer'
-
-##
-# Construct the master Gem index file.
-
-class Gem::Indexer::MasterIndexBuilder < Gem::Indexer::AbstractIndexBuilder
-
- def start_index
- super
- @index = Gem::SourceIndex.new
- end
-
- def end_index
- super
-
- @file.puts "--- !ruby/object:#{@index.class}"
- @file.puts "gems:"
-
- gems = @index.sort_by { |name, gemspec| gemspec.sort_obj }
- gems.each do |name, gemspec|
- yaml = gemspec.to_yaml.gsub(/^/, ' ')
- yaml = yaml.sub(/\A ---/, '') # there's a needed extra ' ' here
- @file.print " #{gemspec.original_name}:"
- @file.puts yaml
- end
- end
-
- def cleanup
- super
-
- index_file_name = File.join @directory, @filename
-
- compress index_file_name, "Z"
- paranoid index_file_name, "#{index_file_name}.Z"
-
- @files << "#{@filename}.Z"
- end
-
- def add(spec)
- @index.add_spec(spec)
- end
-
- private
-
- def paranoid(path, compressed_path)
- data = Gem.read_binary path
- compressed_data = Gem.read_binary compressed_path
-
- if data != unzip(compressed_data) then
- raise "Compressed file #{compressed_path} does not match uncompressed file #{path}"
- end
- end
-
-end
diff --git a/lib/rubygems/indexer/quick_index_builder.rb b/lib/rubygems/indexer/quick_index_builder.rb
deleted file mode 100644
index dc36179dc5..0000000000
--- a/lib/rubygems/indexer/quick_index_builder.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'rubygems/indexer'
-
-##
-# Construct a quick index file and all of the individual specs to support
-# incremental loading.
-
-class Gem::Indexer::QuickIndexBuilder < Gem::Indexer::AbstractIndexBuilder
-
- def initialize(filename, directory)
- directory = File.join directory, 'quick'
-
- super filename, directory
- end
-
- def cleanup
- super
-
- quick_index_file = File.join @directory, @filename
- compress quick_index_file
-
- # the complete quick index is in a directory, so move it as a whole
- @files.delete 'index'
- @files << 'quick'
- end
-
- def add(spec)
- @file.puts spec.original_name
- add_yaml(spec)
- add_marshal(spec)
- end
-
- def add_yaml(spec)
- fn = File.join @directory, "#{spec.original_name}.gemspec.rz"
- zipped = zip spec.to_yaml
- File.open fn, "wb" do |gsfile| gsfile.write zipped end
- end
-
- def add_marshal(spec)
- # HACK why does this not work in #initialize?
- FileUtils.mkdir_p File.join(@directory, "Marshal.#{Gem.marshal_version}")
-
- fn = File.join @directory, "Marshal.#{Gem.marshal_version}",
- "#{spec.original_name}.gemspec.rz"
-
- zipped = zip Marshal.dump(spec)
- File.open fn, "wb" do |gsfile| gsfile.write zipped end
- end
-
-end
-
diff --git a/lib/rubygems/install_message.rb b/lib/rubygems/install_message.rb
new file mode 100644
index 0000000000..a24e26b918
--- /dev/null
+++ b/lib/rubygems/install_message.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "../rubygems"
+require_relative "user_interaction"
+
+##
+# A default post-install hook that displays "Successfully installed
+# some_gem-1.0"
+
+Gem.post_install do |installer|
+ ui = Gem::DefaultUserInteraction.ui
+ ui.say "Successfully installed #{installer.spec.full_name}"
+end
diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb
index dd35acb176..e8859cadaf 100644
--- a/lib/rubygems/install_update_options.rb
+++ b/lib/rubygems/install_update_options.rb
@@ -1,113 +1,224 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems'
-require 'rubygems/security'
+require_relative "../rubygems"
+require_relative "security_option"
##
# Mixin methods for install and update options for Gem::Commands
+
module Gem::InstallUpdateOptions
+ include Gem::SecurityOption
+ ##
# Add the install/update options to the option parser.
- def add_install_update_options
- OptionParser.accept Gem::Security::Policy do |value|
- value = Gem::Security::Policies[value]
- raise OptionParser::InvalidArgument, value if value.nil?
- value
- end
- add_option(:"Install/Update", '-i', '--install-dir DIR',
- 'Gem repository directory to get installed',
- 'gems') do |value, options|
+ def add_install_update_options
+ add_option(:"Install/Update", "-i", "--install-dir DIR",
+ "Gem repository directory to get installed",
+ "gems") do |value, options|
options[:install_dir] = File.expand_path(value)
end
- add_option(:"Install/Update", '-n', '--bindir DIR',
- 'Directory where binary files are',
- 'located') do |value, options|
+ add_option(:"Install/Update", "-n", "--bindir DIR",
+ "Directory where executables will be",
+ "placed when the gem is installed") do |value, options|
options[:bin_dir] = File.expand_path(value)
end
- add_option(:"Install/Update", '-d', '--[no-]rdoc',
- 'Generate RDoc documentation for the gem on',
- 'install') do |value, options|
- options[:generate_rdoc] = value
+ add_option(:"Install/Update", "-j", "--build-jobs VALUE", Integer,
+ "Specify the number of jobs to pass to `make` when installing",
+ "gems with native extensions.",
+ "Defaults to the number of processors.",
+ "This option is ignored on the mswin platform or",
+ "if the MAKEFLAGS environment variable is set.") do |value, options|
+ options[:build_jobs] = value
+ end
+
+ add_option(:"Install/Update", "--document [TYPES]", Array,
+ "Generate documentation for installed gems",
+ "List the documentation types you wish to",
+ "generate. For example: rdoc,ri") do |value, options|
+ options[:document] = case value
+ when nil then %w[ri]
+ when false then []
+ else value
+ end
end
- add_option(:"Install/Update", '--[no-]ri',
- 'Generate RI documentation for the gem on',
- 'install') do |value, options|
- options[:generate_ri] = value
+ add_option(:"Install/Update", "--build-root DIR",
+ "Temporary installation root. Useful for building",
+ "packages. Do not use this when installing remote gems.") do |value, options|
+ options[:build_root] = File.expand_path(value)
end
- add_option(:"Install/Update", '-E', '--[no-]env-shebang',
+ add_option(:"Install/Update", "--vendor",
+ "Install gem into the vendor directory.",
+ "Only for use by gem repackagers.") do |_value, options|
+ unless Gem.vendor_dir
+ raise Gem::OptionParser::InvalidOption.new "your platform is not supported"
+ end
+
+ options[:vendor] = true
+ options[:install_dir] = Gem.vendor_dir
+ end
+
+ add_option(:"Install/Update", "-N", "--no-document",
+ "Disable documentation generation") do |_value, options|
+ options[:document] = []
+ end
+
+ add_option(:"Install/Update", "-E", "--[no-]env-shebang",
"Rewrite the shebang line on installed",
"scripts to use /usr/bin/env") do |value, options|
options[:env_shebang] = value
end
- add_option(:"Install/Update", '-f', '--[no-]force',
- 'Force gem to install, bypassing dependency',
- 'checks') do |value, options|
+ add_option(:"Install/Update", "-f", "--[no-]force",
+ "Force gem to install, bypassing dependency",
+ "checks") do |value, options|
options[:force] = value
end
- add_option(:"Install/Update", '-t', '--[no-]test',
- 'Run unit tests prior to installation') do |value, options|
- options[:test] = value
- end
-
- add_option(:"Install/Update", '-w', '--[no-]wrappers',
- 'Use bin wrappers for executables',
- 'Not available on dosish platforms') do |value, options|
+ add_option(:"Install/Update", "-w", "--[no-]wrappers",
+ "Use bin wrappers for executables",
+ "Not available on dosish platforms") do |value, options|
options[:wrappers] = value
end
- add_option(:"Install/Update", '-P', '--trust-policy POLICY',
- Gem::Security::Policy,
- 'Specify gem trust policy') do |value, options|
- options[:security_policy] = value
- end
+ add_security_option
- add_option(:"Install/Update", '--ignore-dependencies',
- 'Do not install any required dependent gems') do |value, options|
+ add_option(:"Install/Update", "--ignore-dependencies",
+ "Do not install any required dependent gems") do |value, options|
options[:ignore_dependencies] = value
end
- add_option(:"Install/Update", '-y', '--include-dependencies',
- 'Unconditionally install the required',
- 'dependent gems') do |value, options|
- options[:include_dependencies] = value
- end
-
- add_option(:"Install/Update", '--[no-]format-executable',
- 'Make installed executable names match ruby.',
- 'If ruby is ruby18, foo_exec will be',
- 'foo_exec18') do |value, options|
+ add_option(:"Install/Update", "--[no-]format-executable",
+ "Make installed executable names match Ruby.",
+ "If Ruby is ruby18, foo_exec will be",
+ "foo_exec18") do |value, options|
options[:format_executable] = value
end
- add_option(:"Install/Update", '--[no-]user-install',
- 'Install in user\'s home directory instead',
- 'of GEM_HOME. Defaults to using home directory',
- 'only if GEM_HOME is not writable.') do |value, options|
+ add_option(:"Install/Update", "--[no-]user-install",
+ "Install in user's home directory instead",
+ "of GEM_HOME.") do |value, options|
options[:user_install] = value
end
add_option(:"Install/Update", "--development",
- "Install any additional development",
- "dependencies") do |value, options|
+ "Install additional development",
+ "dependencies") do |_value, options|
options[:development] = true
+ options[:dev_shallow] = true
+ end
+
+ add_option(:"Install/Update", "--development-all",
+ "Install development dependencies for all",
+ "gems (including dev deps themselves)") do |_value, options|
+ options[:development] = true
+ options[:dev_shallow] = false
+ end
+
+ add_option(:"Install/Update", "--conservative",
+ "Don't attempt to upgrade gems already",
+ "meeting version requirement") do |_value, options|
+ options[:conservative] = true
+ options[:minimal_deps] = true
+ end
+
+ add_option(:"Install/Update", "--[no-]minimal-deps",
+ "Don't upgrade any dependencies that already",
+ "meet version requirements") do |value, options|
+ options[:minimal_deps] = value
+ end
+
+ add_option(:"Install/Update", "--[no-]post-install-message",
+ "Print post install message") do |value, options|
+ options[:post_install_message] = value
+ end
+
+ add_option(:"Install/Update", "-g", "--file [FILE]",
+ "Read from a gem dependencies API file and",
+ "install the listed gems") do |v,_o|
+ v ||= Gem::GEM_DEP_FILES.find do |file|
+ File.exist? file
+ end
+
+ unless v
+ message = v ? v : "(tried #{Gem::GEM_DEP_FILES.join ", "})"
+
+ raise Gem::OptionParser::InvalidArgument,
+ "cannot find gem dependencies file #{message}"
+ end
+
+ options[:gemdeps] = v
+ end
+
+ add_option(:"Install/Update", "--without GROUPS", Array,
+ "Omit the named groups (comma separated)",
+ "when installing from a gem dependencies",
+ "file") do |v,_o|
+ options[:without_groups].concat v.map(&:intern)
+ end
+
+ add_option(:Deprecated, "--default",
+ "Add the gem's full specification to",
+ "specifications/default and extract only its bin") do |v,_o|
+ end
+
+ add_option(:"Install/Update", "--explain",
+ "Rather than install the gems, indicate which would",
+ "be installed") do |v,_o|
+ options[:explain] = v
+ end
+
+ add_option(:"Install/Update", "--[no-]lock",
+ "Create a lock file (when used with -g/--file)") do |v,_o|
+ options[:lock] = v
+ end
+
+ add_option(:"Install/Update", "--[no-]suggestions",
+ "Suggest alternates when gems are not found") do |v,_o|
+ options[:suggest_alternate] = v
+ end
+
+ add_option(:"Install/Update", "--target-rbconfig [FILE]",
+ "rbconfig.rb for the deployment target platform") do |v, _o|
+ Gem.set_target_rbconfig(v)
+ end
+
+ add_option(:"Install/Update", "--[no-]build-extension",
+ "Build native extensions during installation.",
+ "Defaults to true") do |v, _o|
+ options[:build_extension] = v
+ end
+
+ add_option(:"Install/Update", "--[no-]install-plugin",
+ "Install plugins during installation.",
+ "Defaults to true") do |v, _o|
+ options[:install_plugin] = v
end
end
- # Default options for the gem install command.
- def install_update_defaults_str
- '--rdoc --no-force --no-test --wrappers'
+ ##
+ # Default options for the gem install and update commands.
+
+ def install_update_options
+ {
+ document: %w[ri],
+ }
end
-end
+ ##
+ # Default description for the gem install and update commands.
+ def install_update_defaults_str
+ "--document=ri"
+ end
+end
diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb
index 259c3ed31e..a6e1dc4730 100644
--- a/lib/rubygems/installer.rb
+++ b/lib/rubygems/installer.rb
@@ -1,66 +1,75 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'fileutils'
-require 'pathname'
-require 'rbconfig'
-
-require 'rubygems/format'
-require 'rubygems/ext'
-require 'rubygems/require_paths_builder'
+require_relative "installer_uninstaller_utils"
+require_relative "exceptions"
+require_relative "package"
+require_relative "ext"
+require_relative "user_interaction"
##
-# The installer class processes RubyGem .gem files and installs the
-# files contained in the .gem into the Gem.path.
+# The installer installs the files contained in the .gem into the Gem.home.
#
# Gem::Installer does the work of putting files in all the right places on the
# filesystem including unpacking the gem into its gem dir, installing the
# gemspec in the specifications dir, storing the cached gem in the cache dir,
# and installing either wrappers or symlinks for executables.
+#
+# The installer invokes pre and post install hooks. Hooks can be added either
+# through a rubygems_plugin.rb file in an installed gem or via a
+# rubygems/defaults/#{RUBY_ENGINE}.rb or rubygems/defaults/operating_system.rb
+# file. See Gem.pre_install and Gem.post_install for details.
class Gem::Installer
+ ##
+ # Paths where env(1) might live. Some systems are broken and have it in
+ # /bin
+
+ ENV_PATHS = %w[/usr/bin/env /bin/env].freeze
##
- # Raised when there is an error while building extensions.
- #
- class ExtensionBuildError < Gem::InstallError; end
+ # Deprecated in favor of Gem::Ext::BuildError
+
+ ExtensionBuildError = Gem::Ext::BuildError # :nodoc:
include Gem::UserInteraction
- include Gem::RequirePathsBuilder
+ include Gem::InstallerUninstallerUtils
##
# The directory a gem's executables will be installed into
attr_reader :bin_dir
+ attr_reader :build_root # :nodoc:
+
##
# The gem repository the gem will be installed into
attr_reader :gem_home
##
- # The Gem::Specification for the gem being installed
+ # The options passed when the Gem::Installer was instantiated.
- attr_reader :spec
+ attr_reader :options
- @home_install_warning = false
- @path_warning = false
-
- class << self
-
- ##
- # True if we've warned about ~/.gems install
+ ##
+ # The gem package instance.
- attr_accessor :home_install_warning
+ attr_reader :package
+ class << self
##
- # True if we've warned about PATH not including Gem.bindir
-
- attr_accessor :path_warning
+ # Overrides the executable format.
+ #
+ # This is a sprintf format with a "%s" which will be replaced with the
+ # executable name. It is based off the ruby executable name's difference
+ # from "ruby".
attr_writer :exec_format
@@ -68,94 +77,181 @@ class Gem::Installer
def exec_format
@exec_format ||= Gem.default_exec_format
end
+ end
+
+ ##
+ # Construct an installer object for the gem file located at +path+
+
+ def self.at(path, options = {})
+ security_policy = options[:security_policy]
+ package = Gem::Package.new path, security_policy
+ new package, options
+ end
+
+ class FakePackage
+ attr_accessor :spec
+
+ attr_accessor :dir_mode
+ attr_accessor :prog_mode
+ attr_accessor :data_mode
+
+ def initialize(spec)
+ @spec = spec
+ end
+
+ def extract_files(destination_dir, pattern = "*")
+ FileUtils.mkdir_p destination_dir
+
+ spec.files.each do |file|
+ file = File.join destination_dir, file
+ next if File.exist? file
+ FileUtils.mkdir_p File.dirname(file)
+ File.open file, "w" do |fp|
+ fp.puts "# #{file}"
+ end
+ end
+ end
+ def copy_to(path)
+ end
+ end
+
+ ##
+ # Construct an installer object for an ephemeral gem (one where we don't
+ # actually have a .gem file, just a spec)
+
+ def self.for_spec(spec, options = {})
+ # FIXME: we should have a real Package class for this
+ new FakePackage.new(spec), options
end
##
- # Constructs an Installer instance that will install the gem located at
- # +gem+. +options+ is a Hash with the following keys:
+ # Constructs an Installer instance that will install the gem at +package+ which
+ # can either be a path or an instance of Gem::Package. +options+ is a Hash
+ # with the following keys:
#
+ # :bin_dir:: Where to put a bin wrapper if needed.
+ # :development:: Whether or not development dependencies should be installed.
# :env_shebang:: Use /usr/bin/env in bin wrappers.
# :force:: Overrides all version checks and security policy checks, except
# for a signed-gems-only policy.
+ # :format_executable:: Format the executable the same as the Ruby executable.
+ # If your Ruby is ruby18, foo_exec will be installed as
+ # foo_exec18.
# :ignore_dependencies:: Don't raise if a dependency is missing.
# :install_dir:: The directory to install the gem into.
- # :format_executable:: Format the executable the same as the ruby executable.
- # If your ruby is ruby18, foo_exec will be installed as
- # foo_exec18.
# :security_policy:: Use the specified security policy. See Gem::Security
+ # :user_install:: Indicate that the gem should be unpacked into the users
+ # personal gem directory.
+ # :only_install_dir:: Only validate dependencies against what is in the
+ # install_dir
# :wrappers:: Install wrappers if true, symlinks if false.
+ # :build_args:: An Array of arguments to pass to the extension builder
+ # process. If not set, then Gem::Command.build_args is used
+ # :post_install_message:: Print gem post install message if true
- def initialize(gem, options={})
- @gem = gem
+ def initialize(package, options = {})
+ require "fileutils"
- options = {
- :bin_dir => nil,
- :env_shebang => false,
- :exec_format => false,
- :force => false,
- :install_dir => Gem.dir,
- :source_index => Gem.source_index,
- }.merge options
+ @options = options
+ @package = package
- @env_shebang = options[:env_shebang]
- @force = options[:force]
- gem_home = options[:install_dir]
- @gem_home = Pathname.new(gem_home).expand_path
- @ignore_dependencies = options[:ignore_dependencies]
- @format_executable = options[:format_executable]
- @security_policy = options[:security_policy]
- @wrappers = options[:wrappers]
- @bin_dir = options[:bin_dir]
- @development = options[:development]
- @source_index = options[:source_index]
+ process_options
- begin
- @format = Gem::Format.from_file_by_path @gem, @security_policy
- rescue Gem::Package::FormatError
- raise Gem::InstallError, "invalid gem format for #{@gem}"
- end
+ @package.dir_mode = options[:dir_mode]
+ @package.prog_mode = options[:prog_mode]
+ @package.data_mode = options[:data_mode]
+ end
- begin
- FileUtils.mkdir_p @gem_home
- rescue Errno::EACCESS, Errno::ENOTDIR
- # We'll divert to ~/.gems below
- end
-
- if not File.writable? @gem_home or
- # TODO: Shouldn't have to test for existence of bindir; tests need it.
- (@gem_home.to_s == Gem.dir and File.exist? Gem.bindir and
- not File.writable? Gem.bindir) then
- if options[:user_install] == false then # You don't want to use ~
- raise Gem::FilePermissionError, @gem_home
- elsif options[:user_install].nil? then
- unless self.class.home_install_warning then
- alert_warning "Installing to ~/.gem since #{@gem_home} and\n\t #{Gem.bindir} aren't both writable."
- self.class.home_install_warning = true
+ ##
+ # Checks if +filename+ exists in +@bin_dir+.
+ #
+ # If +@force+ is set +filename+ is overwritten.
+ #
+ # If +filename+ exists and it is a RubyGems wrapper for a different gem, then
+ # the user is consulted.
+ #
+ # If +filename+ exists and +@bin_dir+ is Gem.default_bindir (/usr/local) the
+ # user is consulted.
+ #
+ # Otherwise +filename+ is overwritten.
+
+ def check_executable_overwrite(filename) # :nodoc:
+ return if @force
+
+ generated_bin = File.join @bin_dir, formatted_program_filename(filename)
+
+ return unless File.exist? generated_bin
+
+ ruby_executable = false
+ existing = nil
+
+ File.open generated_bin, "rb" do |io|
+ line = io.gets
+ shebang = /^#!.*ruby/o
+
+ # TruffleRuby uses a bash prelude in default launchers
+ if load_relative_enabled? || RUBY_ENGINE == "truffleruby"
+ until line.nil? || shebang.match?(line) do
+ line = io.gets
end
end
- options[:user_install] = true
+
+ next unless line&.match?(shebang)
+
+ io.gets # blankline
+
+ # TODO: detect a specially formatted comment instead of trying
+ # to find a string inside Ruby code.
+ next unless io.gets&.include?("This file was generated by RubyGems")
+
+ ruby_executable = true
+ existing = io.read.slice(/
+ ^\s*(
+ Gem\.activate_and_load_bin_path\( |
+ load \s Gem\.activate_bin_path\(
+ )
+ (['"])(.*?)(\2),
+ /x, 3)
end
- if options[:user_install] and not options[:unpack] then
- @gem_home = Gem.user_dir
+ return if spec.name == existing
- user_bin_dir = File.join(@gem_home, 'bin')
- unless ENV['PATH'].split(File::PATH_SEPARATOR).include? user_bin_dir then
- unless self.class.path_warning then
- alert_warning "You don't have #{user_bin_dir} in your PATH,\n\t gem executables will not run."
- self.class.path_warning = true
- end
- end
+ # somebody has written to RubyGems' directory, overwrite, too bad
+ return if Gem.default_bindir != @bin_dir && !ruby_executable
+
+ question = "#{spec.name}'s executable \"#{filename}\" conflicts with ".dup
+
+ if ruby_executable
+ question << (existing || "an unknown executable")
+
+ return if ask_yes_no "#{question}\nOverwrite the executable?", false
- FileUtils.mkdir_p @gem_home unless File.directory? @gem_home
- # If it's still not writable, you've got issues.
- raise Gem::FilePermissionError, @gem_home unless File.writable? @gem_home
+ conflict = "installed executable from #{existing}"
+ else
+ question << generated_bin
+
+ return if ask_yes_no "#{question}\nOverwrite the executable?", false
+
+ conflict = generated_bin
end
- @spec = @format.spec
+ raise Gem::InstallError,
+ "\"#{filename}\" from #{spec.name} conflicts with #{conflict}"
+ end
- @gem_dir = File.join(@gem_home, "gems", @spec.full_name).untaint
+ ##
+ # Lazy accessor for the spec's gem directory.
+
+ def gem_dir
+ @gem_dir ||= File.join(gem_home, "gems", spec.full_name)
+ end
+
+ ##
+ # Lazy accessor for the installer's spec.
+
+ def spec
+ @package.spec
end
##
@@ -170,72 +266,97 @@ class Gem::Installer
# specifications/<gem-version>.gemspec #=> the Gem::Specification
def install
- # If we're forcing the install then disable security unless the security
- # policy says that we only install singed gems.
- @security_policy = nil if @force and @security_policy and
- not @security_policy.only_signed
-
- unless @force then
- if rrv = @spec.required_ruby_version then
- unless rrv.satisfied_by? Gem.ruby_version then
- raise Gem::InstallError, "#{@spec.name} requires Ruby version #{rrv}"
- end
- end
-
- if rrgv = @spec.required_rubygems_version then
- unless rrgv.satisfied_by? Gem::Version.new(Gem::RubyGemsVersion) then
- raise Gem::InstallError,
- "#{@spec.name} requires RubyGems version #{rrgv}"
- end
- end
+ pre_install_checks
- unless @ignore_dependencies then
- deps = @spec.runtime_dependencies
- deps |= @spec.development_dependencies if @development
+ run_pre_install_hooks
- deps.each do |dep_gem|
- ensure_dependency @spec, dep_gem
- end
- end
- end
+ # Set loaded_from to ensure extension_dir is correct
+ spec.loaded_from = spec_file
- Gem.pre_install_hooks.each do |hook|
- hook.call self
- end
+ # Completely remove any previous gem files
+ FileUtils.rm_rf gem_dir
+ FileUtils.rm_rf spec.extension_dir
- FileUtils.mkdir_p @gem_home unless File.directory? @gem_home
+ dir_mode = options[:dir_mode]
+ FileUtils.mkdir_p gem_dir, mode: dir_mode && 0o755
- Gem.ensure_gem_subdirectories @gem_home
+ extract_files
- FileUtils.mkdir_p @gem_dir
+ build_extensions
+ write_build_info_file
+ run_post_build_hooks
- extract_files
generate_bin
- build_extensions
+ if options[:install_plugin] == false
+ remove_stale_plugins
+ warn_skipped_plugins
+ else
+ generate_plugins
+ end
+
write_spec
+ write_cache_file
+
+ File.chmod(dir_mode, gem_dir) if dir_mode
+
+ say clean_text(spec.post_install_message.to_s) if options[:post_install_message] && !spec.post_install_message.nil?
+
+ Gem::Specification.add_spec(spec) unless @install_dir
+
+ load_plugin unless options[:install_plugin] == false
+
+ run_post_install_hooks
- write_require_paths_file_if_needed
+ spec
+ rescue Errno::EACCES => e
+ # Permission denied - /path/to/foo
+ raise Gem::FilePermissionError, e.message.split(" - ").last
+ end
+
+ def run_pre_install_hooks # :nodoc:
+ Gem.pre_install_hooks.each do |hook|
+ next unless hook.call(self) == false
+ location = " at #{$1}" if hook.inspect =~ /[ @](.*:\d+)/
- # HACK remove? Isn't this done in multiple places?
- cached_gem = File.join @gem_home, "cache", @gem.split(/\//).pop
- unless File.exist? cached_gem then
- FileUtils.cp @gem, File.join(@gem_home, "cache")
+ message = "pre-install hook#{location} failed for #{spec.full_name}"
+ raise Gem::InstallError, message
end
+ end
- say @spec.post_install_message unless @spec.post_install_message.nil?
+ def run_post_build_hooks # :nodoc:
+ Gem.post_build_hooks.each do |hook|
+ next unless hook.call(self) == false
+ FileUtils.rm_rf gem_dir
- @spec.loaded_from = File.join(@gem_home, 'specifications',
- "#{@spec.full_name}.gemspec")
+ location = " at #{$1}" if hook.inspect =~ /[ @](.*:\d+)/
- @source_index.add_spec @spec
+ message = "post-build hook#{location} failed for #{spec.full_name}"
+ raise Gem::InstallError, message
+ end
+ end
+ def run_post_install_hooks # :nodoc:
Gem.post_install_hooks.each do |hook|
hook.call self
end
+ end
+
+ ##
+ #
+ # Return an Array of Specifications contained within the gem_home
+ # we'll be installing into.
- return @spec
- rescue Zlib::GzipFile::Error
- raise Gem::InstallError, "gzip error installing #{@gem}"
+ def installed_specs
+ @installed_specs ||= begin
+ specs = []
+
+ Gem::Util.glob_files_in_dir("*.gemspec", File.join(gem_home, "specifications")).each do |path|
+ spec = Gem::Specification.load path
+ specs << spec if spec
+ end
+
+ specs
+ end
end
##
@@ -246,27 +367,42 @@ class Gem::Installer
# dependency :: Gem::Dependency
def ensure_dependency(spec, dependency)
- unless installation_satisfies_dependency? dependency then
+ unless installation_satisfies_dependency? dependency
raise Gem::InstallError, "#{spec.name} requires #{dependency}"
end
-
true
end
##
- # True if the gems in the source_index satisfy +dependency+.
+ # True if the gems in the system satisfy +dependency+.
def installation_satisfies_dependency?(dependency)
- @source_index.find_name(dependency.name, dependency.version_requirements).size > 0
+ return true if @options[:development] && dependency.type == :development
+ return true if installed_specs.detect {|s| dependency.matches_spec? s }
+ return false if @only_install_dir
+ !dependency.matching_specs.empty?
end
##
- # Unpacks the gem into the given directory.
+ # The location of the spec file that is installed.
+ #
- def unpack(directory)
- @gem_dir = directory
- @format = Gem::Format.from_file_by_path @gem, @security_policy
- extract_files
+ def spec_file
+ File.join gem_home, "specifications", "#{spec.full_name}.gemspec"
+ end
+
+ def default_spec_dir
+ dir = File.join(gem_home, "specifications", "default")
+ FileUtils.mkdir_p dir
+ dir
+ end
+
+ ##
+ # The location of the default spec file for default gems.
+ #
+
+ def default_spec_file
+ File.join default_spec_dir, "#{spec.full_name}.gemspec"
end
##
@@ -274,86 +410,111 @@ class Gem::Installer
# specifications directory.
def write_spec
- rubycode = @spec.to_ruby
+ spec.installed_by_version = Gem.rubygems_version
- file_name = File.join @gem_home, 'specifications',
- "#{@spec.full_name}.gemspec"
+ Gem.write_binary(spec_file, spec.to_ruby_for_cache)
+ end
- file_name.untaint
+ ##
+ # Writes the full .gemspec specification (in Ruby) to the gem home's
+ # specifications/default directory.
+ #
+ # In contrast to #write_spec, this keeps file lists, so the `gem contents`
+ # command works.
- File.open(file_name, "w") do |file|
- file.puts rubycode
- end
+ def write_default_spec
+ Gem.write_binary(default_spec_file, spec.to_ruby)
end
##
# Creates windows .bat files for easy running of commands
- def generate_windows_script(bindir, filename)
- if Gem.win_platform? then
- script_name = filename + ".bat"
+ def generate_windows_script(filename, bindir)
+ if Gem.win_platform?
+ script_name = formatted_program_filename(filename) + ".bat"
script_path = File.join bindir, File.basename(script_name)
- File.open script_path, 'w' do |file|
+ File.open script_path, "w" do |file|
file.puts windows_stub_script(bindir, filename)
end
- say script_path if Gem.configuration.really_verbose
+ verbose script_path
end
end
- def generate_bin
- return if @spec.executables.nil? or @spec.executables.empty?
+ def generate_bin # :nodoc:
+ executables = spec.executables
+ return if executables.nil? || executables.empty?
- # If the user has asked for the gem to be installed in a directory that is
- # the system gem directory, then use the system bin directory, else create
- # (or use) a new bin dir under the gem_home.
- bindir = @bin_dir ? @bin_dir : Gem.bindir(@gem_home)
+ if @gem_home == Gem.user_dir
+ # If we get here, then one of the following likely happened:
+ # - `--user-install` was specified
+ # - `Gem::PathSupport#home` fell back to `Gem.user_dir`
+ # - GEM_HOME was manually set to `Gem.user_dir`
+
+ check_that_user_bin_dir_is_in_path(executables)
+ end
+
+ ensure_writable_dir @bin_dir
- Dir.mkdir bindir unless File.exist? bindir
- raise Gem::FilePermissionError.new(bindir) unless File.writable? bindir
+ executables.each do |filename|
+ bin_path = File.join gem_dir, spec.bindir, filename
+ next unless File.exist? bin_path
- @spec.executables.each do |filename|
- filename.untaint
- bin_path = File.expand_path File.join(@gem_dir, @spec.bindir, filename)
- mode = File.stat(bin_path).mode | 0111
- File.chmod mode, bin_path
+ mode = File.stat(bin_path).mode
+ dir_mode = options[:prog_mode] || (mode | 0o111)
- if @wrappers then
- generate_bin_script filename, bindir
+ unless dir_mode == mode
+ File.chmod dir_mode, bin_path
+ end
+
+ check_executable_overwrite filename
+
+ if @wrappers
+ generate_bin_script filename, @bin_dir
else
- generate_bin_symlink filename, bindir
+ generate_bin_symlink filename, @bin_dir
end
end
end
+ def generate_plugins # :nodoc:
+ latest = Gem::Specification.latest_spec_for(spec.name)
+ return if latest && latest.version > spec.version
+
+ ensure_writable_dir @plugins_dir
+
+ if spec.plugins.empty?
+ remove_plugins_for(spec, @plugins_dir)
+ else
+ regenerate_plugins_for(spec, @plugins_dir)
+ end
+ rescue ArgumentError => e
+ raise e, "#{latest.name} #{latest.version} #{spec.name} #{spec.version}: #{e.message}"
+ end
+
##
# Creates the scripts to run the applications in the gem.
#--
# The Windows script is generated in addition to the regular one due to a
# bug or misfeature in the Windows shell's pipe. See
- # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/193379
+ # https://blade.ruby-lang.org/ruby-talk/193379
def generate_bin_script(filename, bindir)
bin_script_path = File.join bindir, formatted_program_filename(filename)
- exec_path = File.join @gem_dir, @spec.bindir, filename
-
- # HACK some gems don't have #! in their executables, restore 2008/06
- #if File.read(exec_path, 2) == '#!' then
+ Gem.open_file_with_lock(bin_script_path) do
+ require "fileutils"
FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers
- File.open bin_script_path, 'w', 0755 do |file|
- file.print app_script_text(filename)
+ File.open(bin_script_path, "wb", 0o755) do |file|
+ file.write app_script_text(filename)
+ file.chmod(options[:prog_mode] || 0o755)
end
+ end
- say bin_script_path if Gem.configuration.really_verbose
+ verbose bin_script_path
- generate_windows_script bindir, filename
- #else
- # FileUtils.rm_f bin_script_path
- # FileUtils.cp exec_path, bin_script_path,
- # :verbose => Gem.configuration.really_verbose
- #end
+ generate_windows_script filename, bindir
end
##
@@ -361,215 +522,522 @@ class Gem::Installer
# the symlink if the gem being installed has a newer version.
def generate_bin_symlink(filename, bindir)
- if Gem.win_platform? then
- alert_warning "Unable to use symlinks on Windows, installing wrapper"
- generate_bin_script filename, bindir
- return
- end
-
- src = File.join @gem_dir, 'bin', filename
+ src = File.join gem_dir, spec.bindir, filename
dst = File.join bindir, formatted_program_filename(filename)
- if File.exist? dst then
- if File.symlink? dst then
+ if File.exist? dst
+ if File.symlink? dst
link = File.readlink(dst).split File::SEPARATOR
- cur_version = Gem::Version.create(link[-3].sub(/^.*-/, ''))
- return if @spec.version < cur_version
+ cur_version = Gem::Version.create(link[-3].sub(/^.*-/, ""))
+ return if spec.version < cur_version
end
File.unlink dst
end
- FileUtils.symlink src, dst, :verbose => Gem.configuration.really_verbose
+ FileUtils.symlink src, dst, verbose: Gem.configuration.really_verbose
+ rescue NotImplementedError, SystemCallError
+ alert_warning "Unable to use symlinks, installing wrapper"
+ generate_bin_script filename, bindir
end
##
# Generates a #! line for +bin_file_name+'s wrapper copying arguments if
# necessary.
+ #
+ # If the :custom_shebang config is set, then it is used as a template
+ # for how to create the shebang used for to run a gem's executables.
+ #
+ # The template supports 4 expansions:
+ #
+ # $env the path to the unix env utility
+ # $ruby the path to the currently running ruby interpreter
+ # $exec the path to the gem's executable
+ # $name the name of the gem the executable is for
+ #
def shebang(bin_file_name)
- if @env_shebang then
- "#!/usr/bin/env " + Gem::ConfigMap[:ruby_install_name]
- else
- path = File.join @gem_dir, @spec.bindir, bin_file_name
-
- File.open(path, "rb") do |file|
- first_line = file.gets
- if first_line =~ /^#!/ then
- # Preserve extra words on shebang line, like "-w". Thanks RPA.
- shebang = first_line.sub(/\A\#!.*?ruby\S*/, "#!#{Gem.ruby}")
- else
- # Create a plain shebang line.
- shebang = "#!#{Gem.ruby}"
+ path = File.join gem_dir, spec.bindir, bin_file_name
+ first_line = File.open(path, "rb", &:gets) || ""
+
+ if first_line.start_with?("#!")
+ # Preserve extra words on shebang line, like "-w". Thanks RPA.
+ shebang = first_line.sub(/\A\#!.*?ruby\S*((\s+\S+)+)/, "#!#{Gem.ruby}")
+ opts = $1
+ shebang.strip! # Avoid nasty ^M issues.
+ end
+
+ if which = Gem.configuration[:custom_shebang]
+ # replace bin_file_name with "ruby" to avoid endless loops
+ which = which.gsub(/ #{bin_file_name}$/," #{ruby_install_name}")
+
+ which = which.gsub(/\$(\w+)/) do
+ case $1
+ when "env"
+ @env_path ||= ENV_PATHS.find {|env_path| File.executable? env_path }
+ when "ruby"
+ "#{Gem.ruby}#{opts}"
+ when "exec"
+ bin_file_name
+ when "name"
+ spec.name
end
+ end
+
+ "#!#{which}"
+ elsif @env_shebang
+ # Create a plain shebang line.
+ @env_path ||= ENV_PATHS.find {|env_path| File.executable? env_path }
+ "#!#{@env_path} #{ruby_install_name}"
+ else
+ "#{bash_prolog_script}#!#{Gem.ruby}#{opts}"
+ end
+ end
+
+ ##
+ # Ensures the Gem::Specification written out for this gem is loadable upon
+ # installation.
+
+ def ensure_loadable_spec
+ ruby = spec.to_ruby_for_cache
+
+ begin
+ eval ruby
+ rescue StandardError, SyntaxError => e
+ raise Gem::InstallError,
+ "The specification for #{spec.full_name} is corrupt (#{e.class})"
+ end
+ end
+
+ def ensure_dependencies_met # :nodoc:
+ deps = spec.runtime_dependencies
+ deps |= spec.development_dependencies if @development
+
+ deps.each do |dep_gem|
+ ensure_dependency spec, dep_gem
+ end
+ end
+
+ def process_options # :nodoc:
+ @options = {
+ bin_dir: nil,
+ env_shebang: false,
+ force: false,
+ only_install_dir: false,
+ post_install_message: true,
+ }.merge options
+
+ @env_shebang = options[:env_shebang]
+ @force = options[:force]
+ @install_dir = options[:install_dir]
+ @user_install = options[:user_install]
+ @ignore_dependencies = options[:ignore_dependencies]
+ @format_executable = options[:format_executable]
+ @wrappers = options[:wrappers]
+ @only_install_dir = options[:only_install_dir]
+
+ @bin_dir = options[:bin_dir]
+ @development = options[:development]
+ @build_root = options[:build_root]
- shebang.strip # Avoid nasty ^M issues.
+ @build_args = options[:build_args]
+ @build_jobs = options[:build_jobs]
+
+ @gem_home = @install_dir || user_install_dir || Gem.dir
+
+ # If the user has asked for the gem to be installed in a directory that is
+ # the system gem directory, then use the system bin directory, else create
+ # (or use) a new bin dir under the gem_home.
+ @bin_dir ||= Gem.bindir(@gem_home)
+
+ @plugins_dir = Gem.plugindir(@gem_home)
+
+ unless @build_root.nil?
+ @bin_dir = File.join(@build_root, @bin_dir.gsub(/^[a-zA-Z]:/, ""))
+ @gem_home = File.join(@build_root, @gem_home.gsub(/^[a-zA-Z]:/, ""))
+ @plugins_dir = File.join(@build_root, @plugins_dir.gsub(/^[a-zA-Z]:/, ""))
+ alert_warning "You build with buildroot.\n Build root: #{@build_root}\n Bin dir: #{@bin_dir}\n Gem home: #{@gem_home}\n Plugins dir: #{@plugins_dir}"
+ end
+ end
+
+ def check_that_user_bin_dir_is_in_path(executables) # :nodoc:
+ user_bin_dir = @bin_dir || Gem.bindir(gem_home)
+ user_bin_dir = user_bin_dir.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR
+
+ path = ENV["PATH"]
+ path = path.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR
+
+ if Gem.win_platform?
+ path = path.downcase
+ user_bin_dir = user_bin_dir.downcase
+ end
+
+ path = path.split(File::PATH_SEPARATOR)
+
+ unless path.include? user_bin_dir
+ unless !Gem.win_platform? && (path.include? user_bin_dir.sub(ENV["HOME"], "~"))
+ alert_warning "You don't have #{user_bin_dir} in your PATH,\n\t gem executables (#{executables.join(", ")}) will not run."
end
end
end
+ def verify_gem_home # :nodoc:
+ FileUtils.mkdir_p gem_home, mode: options[:dir_mode] && 0o755
+ end
+
+ def verify_spec
+ unless Gem::Specification::VALID_NAME_PATTERN.match?(spec.name)
+ raise Gem::InstallError, "#{spec} has an invalid name"
+ end
+
+ if spec.raw_require_paths.any? {|path| path =~ /\R/ }
+ raise Gem::InstallError, "#{spec} has an invalid require_paths"
+ end
+
+ if spec.extensions.any? {|ext| ext =~ /\R/ }
+ raise Gem::InstallError, "#{spec} has an invalid extensions"
+ end
+
+ if /\R/.match?(spec.platform.to_s)
+ raise Gem::InstallError, "#{spec.platform} is an invalid platform"
+ end
+
+ unless /\A\d+\z/.match?(spec.specification_version.to_s)
+ raise Gem::InstallError, "#{spec} has an invalid specification_version"
+ end
+
+ if spec.dependencies.any? {|dep| dep.type != :runtime && dep.type != :development }
+ raise Gem::InstallError, "#{spec} has an invalid dependencies"
+ end
+
+ if spec.dependencies.any? {|dep| dep.name =~ /(?:\R|[<>])/ }
+ raise Gem::InstallError, "#{spec} has an invalid dependencies"
+ end
+
+ if spec.executables.any? {|name| !name.is_a?(String) || name != File.basename(name) || /\A\.\.?\z|\R/.match?(name) }
+ raise Gem::InstallError, "#{spec} has an invalid executable"
+ end
+
+ raise Gem::InstallError, "#{spec} has an invalid bindir" unless spec.bindir.is_a?(String)
+
+ expanded_gem_dir = File.expand_path(gem_dir)
+ expanded_bindir = File.expand_path(File.join(gem_dir, spec.bindir))
+ unless expanded_bindir == expanded_gem_dir || expanded_bindir.start_with?("#{expanded_gem_dir}/")
+ raise Gem::InstallError, "#{spec} has an invalid bindir"
+ end
+ end
+
##
# Return the text for an application file.
def app_script_text(bin_file_name)
- <<-TEXT
-#{shebang bin_file_name}
-#
-# This file was generated by RubyGems.
-#
-# The application '#{@spec.name}' is installed as part of a gem, and
-# this file is here to facilitate running it.
-#
+ # NOTE: that the `load` lines cannot be indented, as old RG versions match
+ # against the beginning of the line
+ escaped_bin_file_name = bin_file_name.gsub(/[\\']/) {|c| "\\#{c}" }
+ <<~TEXT
+ #{shebang bin_file_name}
+ #
+ # This file was generated by RubyGems.
+ #
+ # The application '#{spec.name}' is installed as part of a gem, and
+ # this file is here to facilitate running it.
+ #
+
+ require 'rubygems'
+ #{gemdeps_load(spec.name)}
+ version = "#{Gem::Requirement.default_prerelease}"
+
+ str = ARGV.first
+ if str
+ str = str.b[/\\A_(.*)_\\z/, 1]
+ if str and Gem::Version.correct?(str)
+ #{explicit_version_requirement(spec.name)}
+ ARGV.shift
+ end
+ end
-require 'rubygems'
+ if Gem.respond_to?(:activate_and_load_bin_path)
+ Gem.activate_and_load_bin_path('#{spec.name}', '#{escaped_bin_file_name}', version)
+ else
+ load Gem.activate_bin_path('#{spec.name}', '#{escaped_bin_file_name}', version)
+ end
+ TEXT
+ end
-version = "#{Gem::Requirement.default}"
+ def gemdeps_load(name)
+ return "" if name == "bundler"
-if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then
- version = $1
- ARGV.shift
-end
+ <<~TEXT
-gem '#{@spec.name}', version
-load '#{bin_file_name}'
-TEXT
+ Gem.use_gemdeps
+ TEXT
+ end
+
+ def explicit_version_requirement(name)
+ code = "version = str"
+ return code unless name == "bundler"
+
+ code += <<~TEXT
+
+ ENV['BUNDLER_VERSION'] = str
+ TEXT
end
##
- # return the stub script text used to launch the true ruby script
+ # return the stub script text used to launch the true Ruby script
def windows_stub_script(bindir, bin_file_name)
- <<-TEXT
-@ECHO OFF
-IF NOT "%~f0" == "~f0" GOTO :WinNT
-@"#{File.basename(Gem.ruby)}" "#{File.join(bindir, bin_file_name)}" %1 %2 %3 %4 %5 %6 %7 %8 %9
-GOTO :EOF
-:WinNT
-@"#{File.basename(Gem.ruby)}" "%~dpn0" %*
-TEXT
+ rb_topdir = RbConfig::TOPDIR || File.dirname(rb_config["bindir"])
+
+ # get ruby executable file name from RbConfig
+ ruby_exe = "#{rb_config["RUBY_INSTALL_NAME"]}#{rb_config["EXEEXT"]}"
+ ruby_exe = "ruby.exe" if ruby_exe.empty?
+
+ if File.exist?(File.join(bindir, ruby_exe))
+ # stub & ruby.exe within same folder. Portable
+ <<~TEXT
+ @ECHO OFF
+ @"%~dp0#{ruby_exe}" "%~dpn0" %*
+ TEXT
+ elsif bindir.downcase.start_with? rb_topdir.downcase
+ # stub within ruby folder, but not standard bin. Portable
+ require "pathname"
+ from = Pathname.new bindir
+ to = Pathname.new "#{rb_topdir}/bin"
+ rel = to.relative_path_from from
+ <<~TEXT
+ @ECHO OFF
+ @"%~dp0#{rel}/#{ruby_exe}" "%~dpn0" %*
+ TEXT
+ else
+ # outside ruby folder, maybe -user-install or bundler. Portable, but ruby
+ # is dependent on PATH
+ <<~TEXT
+ @ECHO OFF
+ @#{ruby_exe} "%~dpn0" %*
+ TEXT
+ end
end
-
##
# Builds extensions. Valid types of extensions are extconf.rb files,
# configure scripts and rakefiles or mkrf_conf files.
def build_extensions
- return if @spec.extensions.empty?
- say "Building native extensions. This could take a while..."
- start_dir = Dir.pwd
- dest_path = File.join @gem_dir, @spec.require_paths.first
- ran_rake = false # only run rake once
-
- @spec.extensions.each do |extension|
- break if ran_rake
- results = []
-
- builder = case extension
- when /extconf/ then
- Gem::Ext::ExtConfBuilder
- when /configure/ then
- Gem::Ext::ConfigureBuilder
- when /rakefile/i, /mkrf_conf/i then
- ran_rake = true
- Gem::Ext::RakeBuilder
- else
- results = ["No builder for extension '#{extension}'"]
- nil
- end
-
- begin
- Dir.chdir File.join(@gem_dir, File.dirname(extension))
- results = builder.build(extension, @gem_dir, dest_path, results)
-
- say results.join("\n") if Gem.configuration.really_verbose
-
- rescue => ex
- results = results.join "\n"
-
- File.open('gem_make.out', 'wb') { |f| f.puts results }
-
- message = <<-EOF
-ERROR: Failed to build gem native extension.
-
-#{results}
-
-Gem files will remain installed in #{@gem_dir} for inspection.
-Results logged to #{File.join(Dir.pwd, 'gem_make.out')}
- EOF
-
- raise ExtensionBuildError, message
- ensure
- Dir.chdir start_dir
- end
+ if options[:build_extension] == false
+ warn_skipped_extensions
+ return
end
- end
- ##
- # Reads the file index and extracts each file into the gem directory.
- #
- # Ensures that files can't be installed outside the gem directory.
+ builder = Gem::Ext::Builder.new spec, build_args, Gem.target_rbconfig, build_jobs
- def extract_files
- expand_and_validate_gem_dir
+ builder.build_extensions
+ end
- raise ArgumentError, "format required to extract from" if @format.nil?
+ def warn_skipped_extensions # :nodoc:
+ return if spec.extensions.empty?
- @format.file_entries.each do |entry, file_data|
- path = entry['path'].untaint
+ alert_warning "#{spec.full_name} contains native extensions that were not built.\n" \
+ "To build extensions, run: gem pristine #{spec.name} --extensions"
+ end
- if path =~ /\A\// then # for extra sanity
- raise Gem::InstallError,
- "attempt to install file into #{entry['path'].inspect}"
- end
+ def warn_skipped_plugins # :nodoc:
+ return if spec.plugins.empty?
- path = File.expand_path File.join(@gem_dir, path)
+ alert_warning "#{spec.full_name} contains plugins that were not installed.\n" \
+ "To install plugins, run: gem pristine #{spec.name} --only-plugins"
+ end
- if path !~ /\A#{Regexp.escape @gem_dir}/ then
- msg = "attempt to install file into %p under %p" %
- [entry['path'], @gem_dir]
- raise Gem::InstallError, msg
- end
+ def remove_stale_plugins # :nodoc:
+ return unless spec.plugins.empty?
- FileUtils.mkdir_p File.dirname(path)
+ ensure_writable_dir @plugins_dir
+ remove_plugins_for(spec, @plugins_dir)
+ end
- File.open(path, "wb") do |out|
- out.write file_data
- end
+ ##
+ # Reads the file index and extracts each file into the gem directory.
+ #
+ # Ensures that files can't be installed outside the gem directory.
- FileUtils.chmod entry['mode'], path
+ def extract_files
+ @package.extract_files gem_dir
+ end
- say path if Gem.configuration.really_verbose
- end
+ ##
+ # Extracts only the bin/ files from the gem into the gem directory.
+ # This is used by default gems to allow a gem-aware stub to function
+ # without the full gem installed.
+
+ def extract_bin
+ @package.extract_files gem_dir, "#{spec.bindir}/*"
end
##
# Prefix and suffix the program filename the same as ruby.
def formatted_program_filename(filename)
- if @format_executable then
+ if @format_executable
self.class.exec_format % File.basename(filename)
else
filename
end
end
- private
+ ##
+ #
+ # Return the target directory where the gem is to be installed. This
+ # directory is not guaranteed to be populated.
+ #
+
+ def dir
+ gem_dir.to_s
+ end
+
+ ##
+ # Filename of the gem being installed.
+
+ def gem
+ @package.gem.path
+ end
+
+ ##
+ # Performs various checks before installing the gem such as the install
+ # repository is writable and its directories exist, required Ruby and
+ # rubygems versions are met and that dependencies are installed.
+ #
+ # Version and dependency checks are skipped if this install is forced.
+ #
+ # The dependent check will be skipped if the install is ignoring dependencies.
+
+ def pre_install_checks
+ verify_gem_home
+
+ # The name and require_paths must be verified first, since it could contain
+ # ruby code that would be eval'ed in #ensure_loadable_spec
+ verify_spec
+
+ ensure_loadable_spec
+
+ Gem.ensure_gem_subdirectories gem_home
+
+ return true if @force
+
+ ensure_dependencies_met unless @ignore_dependencies
+
+ true
+ end
##
- # HACK Pathname is broken on windows.
+ # Writes the file containing the arguments for building this gem's
+ # extensions.
+
+ def write_build_info_file
+ return if build_args.empty?
+
+ build_info_dir = File.join gem_home, "build_info"
+
+ dir_mode = options[:dir_mode]
+ FileUtils.mkdir_p build_info_dir, mode: dir_mode && 0o755
+
+ build_info_file = File.join build_info_dir, "#{spec.full_name}.info"
+
+ File.open build_info_file, "w" do |io|
+ build_args.each do |arg|
+ io.puts arg
+ end
+ end
+
+ File.chmod(dir_mode, build_info_dir) if dir_mode
+ end
+
+ ##
+ # Writes the .gem file to the cache directory
+
+ def write_cache_file
+ cache_file = File.join gem_home, "cache", spec.file_name
+ @package.copy_to cache_file
+ end
+
+ def ensure_writable_dir(dir) # :nodoc:
+ require "fileutils"
+ FileUtils.mkdir_p dir, mode: options[:dir_mode] && 0o755
- def absolute_path? pathname
- pathname.absolute? or (Gem.win_platform? and pathname.to_s =~ /\A[a-z]:/i)
+ raise Gem::FilePermissionError.new(dir) unless File.writable? dir
end
- def expand_and_validate_gem_dir
- @gem_dir = Pathname.new(@gem_dir).expand_path
+ private
+
+ def user_install_dir
+ # never install to user home in --build-root mode
+ return unless @build_root.nil?
- unless absolute_path?(@gem_dir) then # HACK is this possible after #expand_path?
- raise ArgumentError, "install directory %p not absolute" % @gem_dir
+ # Please note that @user_install might have three states:
+ # * `true`: `--user-install`
+ # * `false`: `--no-user-install` and
+ # * `nil`: option was not specified
+ if @user_install || (@user_install.nil? && Gem.default_user_install)
+ Gem.user_dir
end
+ end
- @gem_dir = @gem_dir.to_s
+ def build_args
+ @build_args ||= begin
+ require_relative "command"
+ Gem::Command.build_args
+ end
end
-end
+ def build_jobs
+ @build_jobs ||= begin
+ require "etc"
+ Etc.nprocessors + 1
+ rescue LoadError
+ 1
+ end
+ end
+ def rb_config
+ Gem.target_rbconfig
+ end
+
+ def ruby_install_name
+ rb_config["ruby_install_name"]
+ end
+
+ def load_relative_enabled?
+ rb_config["LIBRUBY_RELATIVE"] == "yes"
+ end
+
+ def bash_prolog_script
+ if load_relative_enabled?
+ <<~EOS
+ #!/bin/sh
+ # -*- ruby -*-
+ _=_\\
+ =begin
+ bindir="${0%/*}"
+ ruby="$bindir/#{ruby_install_name}"
+ if [ ! -f "$ruby" ]; then
+ ruby="#{ruby_install_name}"
+ fi
+ exec "$ruby" "-x" "$0" "$@"
+ =end
+ EOS
+ else
+ ""
+ end
+ end
+
+ def load_plugin
+ specs = Gem::Specification.find_all_by_name(spec.name)
+ # If old version already exists, this plugin isn't loaded
+ # immediately. It's for avoiding a case that multiple versions
+ # are loaded at the same time.
+ return unless specs.size == 1
+
+ plugin_files = spec.plugins.filter_map do |plugin|
+ path = File.join(@plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}")
+ path if File.exist?(path)
+ end
+ Gem.load_plugin_files(plugin_files) unless plugin_files.empty?
+ end
+end
diff --git a/lib/rubygems/installer_uninstaller_utils.rb b/lib/rubygems/installer_uninstaller_utils.rb
new file mode 100644
index 0000000000..c5c2a52bab
--- /dev/null
+++ b/lib/rubygems/installer_uninstaller_utils.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+##
+# Helper methods for both Gem::Installer and Gem::Uninstaller
+
+module Gem::InstallerUninstallerUtils
+ def regenerate_plugins_for(spec, plugins_dir)
+ plugins = spec.plugins
+ return if plugins.empty?
+
+ require "pathname"
+
+ spec.plugins.each do |plugin|
+ plugin_script_path = File.join plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}"
+
+ File.open plugin_script_path, "wb" do |file|
+ file.puts "require_relative '#{Pathname.new(plugin).relative_path_from(Pathname.new(plugins_dir))}'"
+ end
+
+ verbose plugin_script_path
+ end
+ end
+
+ def remove_plugins_for(spec, plugins_dir)
+ FileUtils.rm_f Gem::Util.glob_files_in_dir("#{spec.name}#{Gem.plugin_suffix_pattern}", plugins_dir)
+ end
+end
diff --git a/lib/rubygems/local_remote_options.rb b/lib/rubygems/local_remote_options.rb
index 730cb69b83..3b88c43149 100644
--- a/lib/rubygems/local_remote_options.rb
+++ b/lib/rubygems/local_remote_options.rb
@@ -1,29 +1,34 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'uri'
-require 'rubygems'
+require_relative "vendor/uri/lib/uri"
+require_relative "../rubygems"
##
# Mixin methods for local and remote Gem::Command options.
module Gem::LocalRemoteOptions
-
##
- # Allows OptionParser to handle HTTP URIs.
+ # Allows Gem::OptionParser to handle HTTP URIs.
def accept_uri_http
- OptionParser.accept URI::HTTP do |value|
+ Gem::OptionParser.accept Gem::URI::HTTP do |value|
begin
- uri = URI.parse value
- rescue URI::InvalidURIError
- raise OptionParser::InvalidArgument, value
+ uri = Gem::URI.parse value
+ rescue Gem::URI::InvalidURIError
+ raise Gem::OptionParser::InvalidArgument, value
end
- raise OptionParser::InvalidArgument, value unless uri.scheme == 'http'
+ valid_uri_schemes = ["http", "https", "file", "s3"]
+ unless valid_uri_schemes.include?(uri.scheme)
+ msg = "Invalid uri scheme for #{value}\nPreface URLs with one of #{valid_uri_schemes.map {|s| "#{s}://" }}"
+ raise ArgumentError, msg
+ end
value
end
@@ -33,22 +38,23 @@ module Gem::LocalRemoteOptions
# Add local/remote options to the command line parser.
def add_local_remote_options
- add_option(:"Local/Remote", '-l', '--local',
- 'Restrict operations to the LOCAL domain') do |value, options|
+ add_option(:"Local/Remote", "-l", "--local",
+ "Restrict operations to the LOCAL domain") do |_value, options|
options[:domain] = :local
end
- add_option(:"Local/Remote", '-r', '--remote',
- 'Restrict operations to the REMOTE domain') do |value, options|
+ add_option(:"Local/Remote", "-r", "--remote",
+ "Restrict operations to the REMOTE domain") do |_value, options|
options[:domain] = :remote
end
- add_option(:"Local/Remote", '-b', '--both',
- 'Allow LOCAL and REMOTE operations') do |value, options|
+ add_option(:"Local/Remote", "-b", "--both",
+ "Allow LOCAL and REMOTE operations") do |_value, options|
options[:domain] = :both
end
add_bulk_threshold_option
+ add_clear_sources_option
add_source_option
add_proxy_option
add_update_sources_option
@@ -58,23 +64,33 @@ module Gem::LocalRemoteOptions
# Add the --bulk-threshold option
def add_bulk_threshold_option
- add_option(:"Local/Remote", '-B', '--bulk-threshold COUNT',
+ add_option(:"Local/Remote", "-B", "--bulk-threshold COUNT",
"Threshold for switching to bulk",
- "synchronization (default #{Gem.configuration.bulk_threshold})") do
- |value, options|
+ "synchronization (default #{Gem.configuration.bulk_threshold})") do |value, _options|
Gem.configuration.bulk_threshold = value.to_i
end
end
##
+ # Add the --clear-sources option
+
+ def add_clear_sources_option
+ add_option(:"Local/Remote", "--clear-sources",
+ "Clear the gem sources") do |_value, options|
+ Gem.sources = nil
+ options[:sources_cleared] = true
+ end
+ end
+
+ ##
# Add the --http-proxy option
def add_proxy_option
accept_uri_http
- add_option(:"Local/Remote", '-p', '--[no-]http-proxy [URL]', URI::HTTP,
- 'Use HTTP proxy for remote operations') do |value, options|
- options[:http_proxy] = (value == false) ? :no_proxy : value
+ add_option(:"Local/Remote", "-p", "--[no-]http-proxy [URL]", Gem::URI::HTTP,
+ "Use HTTP proxy for remote operations") do |value, options|
+ options[:http_proxy] = value == false ? :no_proxy : value
Gem.configuration[:http_proxy] = options[:http_proxy]
end
end
@@ -85,26 +101,24 @@ module Gem::LocalRemoteOptions
def add_source_option
accept_uri_http
- add_option(:"Local/Remote", '--source URL', URI::HTTP,
- 'Use URL as the remote source for gems') do |source, options|
- source << '/' if source !~ /\/\z/
+ add_option(:"Local/Remote", "-s", "--source URL", Gem::URI::HTTP,
+ "Append URL to list of remote gem sources") do |source, options|
+ source << "/" unless source.end_with?("/")
- if options[:added_source] then
- Gem.sources << source
+ if options.delete :sources_cleared
+ Gem.sources = [source]
else
- options[:added_source] = true
- Gem.sources.replace [source]
+ Gem.sources << source unless Gem.sources.include?(source)
end
end
end
##
- # Add the --update-source option
+ # Add the --update-sources option
def add_update_sources_option
-
- add_option(:"Local/Remote", '-u', '--[no-]update-sources',
- 'Update local source cache') do |value, options|
+ add_option(:Deprecated, "-u", "--[no-]update-sources",
+ "Update local source cache") do |value, _options|
Gem.configuration.update_sources = value
end
end
@@ -120,15 +134,13 @@ module Gem::LocalRemoteOptions
# Is local fetching enabled?
def local?
- options[:domain] == :local || options[:domain] == :both
+ [:local, :both].include?(options[:domain])
end
##
# Is remote fetching enabled?
def remote?
- options[:domain] == :remote || options[:domain] == :both
+ [:remote, :both].include?(options[:domain])
end
-
end
-
diff --git a/lib/rubygems/name_tuple.rb b/lib/rubygems/name_tuple.rb
new file mode 100644
index 0000000000..cbdf4d7ac5
--- /dev/null
+++ b/lib/rubygems/name_tuple.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+##
+#
+# Represents a gem of name +name+ at +version+ of +platform+. These
+# wrap the data returned from the indexes.
+
+class Gem::NameTuple
+ def initialize(name, version, platform = Gem::Platform::RUBY)
+ @name = name
+ @version = version
+
+ platform &&= platform.to_s
+ platform = Gem::Platform::RUBY if !platform || platform.empty?
+ @platform = platform
+ end
+
+ attr_reader :name, :version, :platform
+
+ ##
+ # Turn an array of [name, version, platform] into an array of
+ # NameTuple objects.
+
+ def self.from_list(list)
+ list.map {|t| new(*t) }
+ end
+
+ ##
+ # Turn an array of NameTuple objects back into an array of
+ # [name, version, platform] tuples.
+
+ def self.to_basic(list)
+ list.map(&:to_a)
+ end
+
+ ##
+ # A null NameTuple, ie name=nil, version=0
+
+ def self.null
+ new nil, Gem::Version.new(0), nil
+ end
+
+ ##
+ # Returns the full name (name-version) of this Gem. Platform information is
+ # included if it is not the default Ruby platform. This mimics the behavior
+ # of Gem::Specification#full_name.
+
+ def full_name
+ case @platform
+ when nil, "", Gem::Platform::RUBY
+ "#{@name}-#{@version}"
+ else
+ "#{@name}-#{@version}-#{@platform}"
+ end
+ end
+
+ ##
+ # Indicate if this NameTuple matches the current platform.
+
+ def match_platform?
+ Gem::Platform.match_gem? @platform, @name
+ end
+
+ ##
+ # Indicate if this NameTuple is for a prerelease version.
+ def prerelease?
+ @version.prerelease?
+ end
+
+ ##
+ # Return the name that the gemspec file would be
+
+ def spec_name
+ "#{full_name}.gemspec"
+ end
+
+ ##
+ # Convert back to the [name, version, platform] tuple
+
+ def to_a
+ [@name, @version, @platform]
+ end
+
+ alias_method :deconstruct, :to_a
+
+ def deconstruct_keys(keys)
+ { name: @name, version: @version, platform: @platform }
+ end
+
+ def inspect # :nodoc:
+ "#<Gem::NameTuple #{@name}, #{@version}, #{@platform}>"
+ end
+
+ alias_method :to_s, :inspect # :nodoc:
+
+ def <=>(other)
+ [@name, @version, Gem::Platform.sort_priority(@platform)] <=>
+ [other.name, other.version, Gem::Platform.sort_priority(other.platform)]
+ end
+
+ include Comparable
+
+ ##
+ # Compare with +other+. Supports another NameTuple or an Array
+ # in the [name, version, platform] format.
+
+ def ==(other)
+ case other
+ when self.class
+ @name == other.name &&
+ @version == other.version &&
+ @platform == other.platform
+ when Array
+ to_a == other
+ else
+ false
+ end
+ end
+
+ alias_method :eql?, :==
+
+ def hash
+ to_a.hash
+ end
+end
diff --git a/lib/rubygems/old_format.rb b/lib/rubygems/old_format.rb
deleted file mode 100644
index ef5d621f52..0000000000
--- a/lib/rubygems/old_format.rb
+++ /dev/null
@@ -1,148 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'fileutils'
-require 'yaml'
-require 'zlib'
-
-module Gem
-
- ##
- # The format class knows the guts of the RubyGem .gem file format
- # and provides the capability to read gem files
- #
- class OldFormat
- attr_accessor :spec, :file_entries, :gem_path
-
- ##
- # Constructs an instance of a Format object, representing the gem's
- # data structure.
- #
- # gem:: [String] The file name of the gem
- #
- def initialize(gem_path)
- @gem_path = gem_path
- end
-
- ##
- # Reads the named gem file and returns a Format object, representing
- # the data from the gem file
- #
- # file_path:: [String] Path to the gem file
- #
- def self.from_file_by_path(file_path)
- unless File.exist?(file_path)
- raise Gem::Exception, "Cannot load gem file [#{file_path}]"
- end
- File.open(file_path, 'rb') do |file|
- from_io(file, file_path)
- end
- end
-
- ##
- # Reads a gem from an io stream and returns a Format object, representing
- # the data from the gem file
- #
- # io:: [IO] Stream from which to read the gem
- #
- def self.from_io(io, gem_path="(io)")
- format = self.new(gem_path)
- skip_ruby(io)
- format.spec = read_spec(io)
- format.file_entries = []
- read_files_from_gem(io) do |entry, file_data|
- format.file_entries << [entry, file_data]
- end
- format
- end
-
- private
- ##
- # Skips the Ruby self-install header. After calling this method, the
- # IO index will be set after the Ruby code.
- #
- # file:: [IO] The IO to process (skip the Ruby code)
- #
- def self.skip_ruby(file)
- end_seen = false
- loop {
- line = file.gets
- if(line == nil || line.chomp == "__END__") then
- end_seen = true
- break
- end
- }
- if(end_seen == false) then
- raise Gem::Exception.new("Failed to find end of ruby script while reading gem")
- end
- end
-
- ##
- # Reads the specification YAML from the supplied IO and constructs
- # a Gem::Specification from it. After calling this method, the
- # IO index will be set after the specification header.
- #
- # file:: [IO] The IO to process
- #
- def self.read_spec(file)
- yaml = ''
- begin
- read_until_dashes(file) do |line|
- yaml << line
- end
- Specification.from_yaml(yaml)
- rescue YAML::Error => e
- raise Gem::Exception.new("Failed to parse gem specification out of gem file")
- rescue ArgumentError => e
- raise Gem::Exception.new("Failed to parse gem specification out of gem file")
- end
- end
-
- ##
- # Reads lines from the supplied IO until a end-of-yaml (---) is
- # reached
- #
- # file:: [IO] The IO to process
- # block:: [String] The read line
- #
- def self.read_until_dashes(file)
- while((line = file.gets) && line.chomp.strip != "---") do
- yield line
- end
- end
-
-
- ##
- # Reads the embedded file data from a gem file, yielding an entry
- # containing metadata about the file and the file contents themselves
- # for each file that's archived in the gem.
- # NOTE: Many of these methods should be extracted into some kind of
- # Gem file read/writer
- #
- # gem_file:: [IO] The IO to process
- #
- def self.read_files_from_gem(gem_file)
- errstr = "Error reading files from gem"
- header_yaml = ''
- begin
- self.read_until_dashes(gem_file) do |line|
- header_yaml << line
- end
- header = YAML.load(header_yaml)
- raise Gem::Exception.new(errstr) unless header
- header.each do |entry|
- file_data = ''
- self.read_until_dashes(gem_file) do |line|
- file_data << line
- end
- yield [entry, Zlib::Inflate.inflate(file_data.strip.unpack("m")[0])]
- end
- rescue Exception,Zlib::DataError => e
- raise Gem::Exception.new(errstr)
- end
- end
- end
-end
diff --git a/lib/rubygems/openssl.rb b/lib/rubygems/openssl.rb
new file mode 100644
index 0000000000..c44f619c4c
--- /dev/null
+++ b/lib/rubygems/openssl.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+autoload :OpenSSL, "openssl"
+
+module Gem
+ HAVE_OPENSSL = defined? OpenSSL::SSL # :nodoc:
+end
diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb
index 9cb393b0c7..7e41b18f66 100644
--- a/lib/rubygems/package.rb
+++ b/lib/rubygems/package.rb
@@ -1,95 +1,769 @@
-#++
-# Copyright (C) 2004 Mauricio Julio Fernández Pradier
+# frozen_string_literal: true
+
+# rubocop:disable Style/AsciiComments
+
+# Copyright (C) 2004 Mauricio Julio Fernández Pradier
# See LICENSE.txt for additional licensing information.
-#--
-
-require 'fileutils'
-require 'find'
-require 'stringio'
-require 'yaml'
-require 'zlib'
-
-require 'rubygems/digest/md5'
-require 'rubygems/security'
-require 'rubygems/specification'
-
-# Wrapper for FileUtils meant to provide logging and additional operations if
-# needed.
-class Gem::FileOperations
-
- def initialize(logger = nil)
- @logger = logger
- end
-
- def method_missing(meth, *args, &block)
- case
- when FileUtils.respond_to?(meth)
- @logger.log "#{meth}: #{args}" if @logger
- FileUtils.send meth, *args, &block
- when Gem::FileOperations.respond_to?(meth)
- @logger.log "#{meth}: #{args}" if @logger
- Gem::FileOperations.send meth, *args, &block
- else
- super
+
+# rubocop:enable Style/AsciiComments
+
+require_relative "win_platform"
+require_relative "security"
+require_relative "user_interaction"
+
+##
+# Example using a Gem::Package
+#
+# Builds a .gem file given a Gem::Specification. A .gem file is a tarball
+# which contains a data.tar.gz, metadata.gz, checksums.yaml.gz and possibly
+# signatures.
+#
+# require 'rubygems'
+# require 'rubygems/package'
+#
+# spec = Gem::Specification.new do |s|
+# s.summary = "Ruby based make-like utility."
+# s.name = 'rake'
+# s.version = PKG_VERSION
+# s.requirements << 'none'
+# s.files = PKG_FILES
+# s.description = <<-EOF
+# Rake is a Make-like program implemented in Ruby. Tasks
+# and dependencies are specified in standard Ruby syntax.
+# EOF
+# end
+#
+# Gem::Package.build spec
+#
+# Reads a .gem file.
+#
+# require 'rubygems'
+# require 'rubygems/package'
+#
+# the_gem = Gem::Package.new(path_to_dot_gem)
+# the_gem.contents # get the files in the gem
+# the_gem.extract_files destination_directory # extract the gem into a directory
+# the_gem.spec # get the spec out of the gem
+# the_gem.verify # check the gem is OK (contains valid gem specification, contains a not corrupt contents archive)
+#
+# #files are the files in the .gem tar file, not the Ruby files in the gem
+# #extract_files and #contents automatically call #verify
+
+class Gem::Package
+ include Gem::UserInteraction
+
+ class Error < Gem::Exception; end
+
+ class FormatError < Error
+ attr_reader :path
+
+ def initialize(message, source = nil)
+ if source
+ @path = source.is_a?(String) ? source : source.path
+
+ message += " in #{path}" if path
+ end
+
+ super message
end
end
-end
+ class PathError < Error
+ def initialize(destination, destination_dir)
+ super format("installing into parent path %s of %s is not allowed", destination, destination_dir)
+ end
+ end
-module Gem::Package
+ class SymlinkError < Error
+ def initialize(name, destination, destination_dir)
+ super format("installing symlink '%s' pointing to parent path %s of %s is not allowed", name, destination, destination_dir)
+ end
+ end
- class Error < StandardError; end
class NonSeekableIO < Error; end
- class ClosedIO < Error; end
- class BadCheckSum < Error; end
+
class TooLongFileName < Error; end
- class FormatError < Error; end
-
- def self.open(io, mode = "r", signer = nil, &block)
- tar_type = case mode
- when 'r' then TarInput
- when 'w' then TarOutput
- else
- raise "Unknown Package open mode"
- end
-
- tar_type.open(io, signer, &block)
- end
-
- def self.pack(src, destname, signer = nil)
- TarOutput.open(destname, signer) do |outp|
- dir_class.chdir(src) do
- outp.metadata = (file_class.read("RPA/metadata") rescue nil)
- find_class.find('.') do |entry|
- case
- when file_class.file?(entry)
- entry.sub!(%r{\./}, "")
- next if entry =~ /\ARPA\//
- stat = File.stat(entry)
- outp.add_file_simple(entry, stat.mode, stat.size) do |os|
- file_class.open(entry, "rb") do |f|
- os.write(f.read(4096)) until f.eof?
- end
- end
- when file_class.dir?(entry)
- entry.sub!(%r{\./}, "")
- next if entry == "RPA"
- outp.mkdir(entry, file_class.stat(entry).mode)
+
+ ##
+ # Raised when a tar file is corrupt
+
+ class TarInvalidError < Error; end
+
+ attr_accessor :build_time # :nodoc:
+
+ ##
+ # Checksums for the contents of the package
+
+ attr_reader :checksums
+
+ ##
+ # The files in this package. This is not the contents of the gem, just the
+ # files in the top-level container.
+
+ attr_reader :files
+
+ ##
+ # Reference to the gem being packaged.
+
+ attr_reader :gem
+
+ ##
+ # The security policy used for verifying the contents of this package.
+
+ attr_accessor :security_policy
+
+ ##
+ # Sets the Gem::Specification to use to build this package.
+
+ attr_writer :spec
+
+ ##
+ # Permission for directories
+ attr_accessor :dir_mode
+
+ ##
+ # Permission for program files
+ attr_accessor :prog_mode
+
+ ##
+ # Permission for other files
+ attr_accessor :data_mode
+
+ def self.build(spec, skip_validation = false, strict_validation = false, file_name = nil)
+ gem_file = file_name || spec.file_name
+
+ package = new gem_file
+ package.spec = spec
+ package.build skip_validation, strict_validation
+
+ gem_file
+ end
+
+ ##
+ # Creates a new Gem::Package for the file at +gem+. +gem+ can also be
+ # provided as an IO object.
+ #
+ # If +gem+ is an existing file in the old format a Gem::Package::Old will be
+ # returned.
+
+ def self.new(gem, security_policy = nil)
+ gem = if gem.is_a?(Gem::Package::Source)
+ gem
+ elsif gem.respond_to? :read
+ Gem::Package::IOSource.new gem
+ else
+ Gem::Package::FileSource.new gem
+ end
+
+ return super unless self == Gem::Package
+ return super unless gem.present?
+
+ return super unless gem.start
+ return super unless gem.start.include? "MD5SUM ="
+
+ Gem::Package::Old.new gem
+ end
+
+ ##
+ # Extracts the Gem::Specification and raw metadata from the .gem file at
+ # +path+.
+ #--
+
+ def self.raw_spec(path, security_policy = nil)
+ format = new(path, security_policy)
+ spec = format.spec
+
+ metadata = nil
+
+ File.open path, Gem.binary_mode do |io|
+ tar = Gem::Package::TarReader.new io
+ tar.each_entry do |entry|
+ case entry.full_name
+ when "metadata" then
+ metadata = entry.read
+ when "metadata.gz" then
+ metadata = Gem::Util.gunzip entry.read
+ end
+ end
+ end
+
+ [spec, metadata]
+ end
+
+ ##
+ # Creates a new package that will read or write to the file +gem+.
+
+ def initialize(gem, security_policy) # :notnew:
+ require "zlib"
+
+ @gem = gem
+
+ @build_time = Gem.source_date_epoch
+ @checksums = {}
+ @contents = nil
+ @digests = Hash.new {|h, algorithm| h[algorithm] = {} }
+ @files = nil
+ @security_policy = security_policy
+ @signatures = {}
+ @signer = nil
+ @spec = nil
+ end
+
+ ##
+ # Copies this package to +path+ (if possible)
+
+ def copy_to(path)
+ FileUtils.cp @gem.path, path unless File.exist? path
+ end
+
+ ##
+ # Adds a checksum for each entry in the gem to checksums.yaml.gz.
+
+ def add_checksums(tar)
+ Gem.load_yaml
+
+ checksums_by_algorithm = Hash.new {|h, algorithm| h[algorithm] = {} }
+
+ @checksums.each do |name, digests|
+ digests.each do |algorithm, digest|
+ checksums_by_algorithm[algorithm][name] = digest.hexdigest
+ end
+ end
+
+ tar.add_file_signed "checksums.yaml.gz", 0o444, @signer do |io|
+ gzip_to io do |gz_io|
+ if Gem.use_psych?
+ Psych.dump checksums_by_algorithm, gz_io
+ else
+ gz_io.write Gem::YAMLSerializer.dump(checksums_by_algorithm)
+ end
+ end
+ end
+ end
+
+ ##
+ # Adds the files listed in the packages's Gem::Specification to data.tar.gz
+ # and adds this file to the +tar+.
+
+ def add_contents(tar) # :nodoc:
+ digests = tar.add_file_signed "data.tar.gz", 0o444, @signer do |io|
+ gzip_to io do |gz_io|
+ Gem::Package::TarWriter.new gz_io do |data_tar|
+ add_files data_tar
+ end
+ end
+ end
+
+ @checksums["data.tar.gz"] = digests
+ end
+
+ ##
+ # Adds files included the package's Gem::Specification to the +tar+ file
+
+ def add_files(tar) # :nodoc:
+ @spec.files.each do |file|
+ stat = File.lstat file
+
+ if stat.symlink?
+ tar.add_symlink file, File.readlink(file), stat.mode
+ end
+
+ next unless stat.file?
+
+ tar.add_file_simple file, stat.mode, stat.size do |dst_io|
+ File.open file, "rb" do |src_io|
+ copy_stream(src_io, dst_io, stat.size)
+ end
+ end
+ end
+ end
+
+ ##
+ # Adds the package's Gem::Specification to the +tar+ file
+
+ def add_metadata(tar) # :nodoc:
+ digests = tar.add_file_signed "metadata.gz", 0o444, @signer do |io|
+ gzip_to io do |gz_io|
+ gz_io.write @spec.to_yaml
+ end
+ end
+
+ @checksums["metadata.gz"] = digests
+ end
+
+ ##
+ # Builds this package based on the specification set by #spec=
+
+ def build(skip_validation = false, strict_validation = false)
+ raise ArgumentError, "skip_validation = true and strict_validation = true are incompatible" if skip_validation && strict_validation
+
+ Gem.load_yaml
+
+ @spec.validate true, strict_validation unless skip_validation
+
+ setup_signer(
+ signer_options: {
+ expiration_length_days: Gem.configuration.cert_expiration_length_days,
+ }
+ )
+
+ @gem.with_write_io do |gem_io|
+ Gem::Package::TarWriter.new gem_io do |gem|
+ add_metadata gem
+ add_contents gem
+ add_checksums gem
+ end
+ end
+
+ say <<-EOM
+ Successfully built RubyGem
+ Name: #{@spec.name}
+ Version: #{@spec.version}
+ File: #{File.basename @gem.path}
+EOM
+ ensure
+ @signer = nil
+ end
+
+ ##
+ # A list of file names contained in this gem
+
+ def contents
+ return @contents if @contents
+
+ verify unless @spec
+
+ @contents = []
+
+ @gem.with_read_io do |io|
+ gem_tar = Gem::Package::TarReader.new io
+
+ gem_tar.each do |entry|
+ next unless entry.full_name == "data.tar.gz"
+
+ open_tar_gz entry do |pkg_tar|
+ pkg_tar.each do |contents_entry|
+ @contents << contents_entry.full_name
+ end
+ end
+
+ return @contents
+ end
+ end
+ rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e
+ raise Gem::Package::FormatError.new e.message, @gem
+ end
+
+ ##
+ # Creates a digest of the TarEntry +entry+ from the digest algorithm set by
+ # the security policy.
+
+ def digest(entry) # :nodoc:
+ algorithms = if @checksums
+ @checksums.to_h {|algorithm, _| [algorithm, Gem::Security.create_digest(algorithm)] }
+ elsif Gem::Security::DIGEST_NAME
+ { Gem::Security::DIGEST_NAME => Gem::Security.create_digest(Gem::Security::DIGEST_NAME) }
+ end
+
+ return @digests if algorithms.nil? || algorithms.empty?
+
+ buf = String.new(capacity: 16_384, encoding: Encoding::BINARY)
+ until entry.eof?
+ entry.readpartial(16_384, buf)
+ algorithms.each_value {|digester| digester << buf }
+ end
+ entry.rewind
+
+ algorithms.each do |algorithm, digester|
+ @digests[algorithm][entry.full_name] = digester
+ end
+
+ @digests
+ end
+
+ ##
+ # Extracts the files in this package into +destination_dir+
+ #
+ # If +pattern+ is specified, only entries matching that glob will be
+ # extracted.
+
+ def extract_files(destination_dir, pattern = "*")
+ verify unless @spec
+
+ FileUtils.mkdir_p destination_dir, mode: dir_mode && 0o755
+
+ @gem.with_read_io do |io|
+ reader = Gem::Package::TarReader.new io
+
+ reader.each do |entry|
+ next unless entry.full_name == "data.tar.gz"
+
+ extract_tar_gz entry, destination_dir, pattern
+
+ break # ignore further entries
+ end
+ end
+ rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e
+ raise Gem::Package::FormatError.new e.message, @gem
+ end
+
+ ##
+ # Extracts all the files in the gzipped tar archive +io+ into
+ # +destination_dir+.
+ #
+ # If an entry in the archive contains a relative path above
+ # +destination_dir+ or an absolute path is encountered an exception is
+ # raised.
+ #
+ # If +pattern+ is specified, only entries matching that glob will be
+ # extracted.
+
+ def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc:
+ destination_dir = File.realpath(destination_dir)
+
+ directories = []
+ symlinks = []
+
+ open_tar_gz io do |tar|
+ tar.each do |entry|
+ full_name = entry.full_name
+ next unless File.fnmatch pattern, full_name, File::FNM_DOTMATCH
+
+ destination = install_location full_name, destination_dir
+
+ if entry.symlink?
+ link_target = entry.header.linkname
+ real_destination = link_target.start_with?("/") ? link_target : File.expand_path(link_target, File.dirname(destination))
+
+ raise Gem::Package::SymlinkError.new(full_name, real_destination, destination_dir) unless
+ normalize_path(real_destination).start_with? normalize_path(destination_dir + "/")
+
+ symlinks << [full_name, link_target, destination, real_destination]
+ end
+
+ mkdir =
+ if entry.directory?
+ destination
else
- raise "Don't know how to pack this yet!"
+ File.dirname destination
+ end
+
+ unless directories.include?(mkdir)
+ FileUtils.mkdir_p mkdir, mode: dir_mode ? 0o755 : (entry.header.mode if entry.directory?)
+ directories << mkdir
+ end
+
+ real_mkdir = File.realpath(mkdir)
+ unless real_mkdir == destination_dir || normalize_path(real_mkdir).start_with?(normalize_path(destination_dir + "/"))
+ raise Gem::Package::PathError.new(real_mkdir, destination_dir)
+ end
+
+ if entry.file?
+ File.open(destination, "wb") do |out|
+ copy_stream(tar.io, out, entry.size)
+ # Flush needs to happen before chmod because there could be data
+ # in the IO buffer that needs to be written, and that could be
+ # written after the chmod (on close) which would mess up the perms
+ out.flush
+ out.chmod file_mode(entry.header.mode) & ~File.umask
end
end
+
+ verbose destination
end
end
+
+ symlinks.each do |name, target, destination, real_destination|
+ if File.exist?(real_destination)
+ create_symlink(target, destination)
+ else
+ alert_warning "#{@spec.full_name} ships with a dangling symlink named #{name} pointing to missing #{target} file. Ignoring"
+ end
+ end
+
+ if dir_mode
+ File.chmod(dir_mode, *directories)
+ end
end
-end
+ def file_mode(mode) # :nodoc:
+ ((mode & 0o111).zero? ? data_mode : prog_mode) ||
+ # If we're not using one of the default modes, then we're going to fall
+ # back to the mode from the tarball. In this case we need to mask it down
+ # to fit into 2^16 bits (the maximum value for a mode in CRuby since it
+ # gets put into an unsigned short).
+ (mode & ((1 << 16) - 1))
+ end
+
+ ##
+ # Gzips content written to +gz_io+ to +io+.
+ #--
+ # Also sets the gzip modification time to the package build time to ease
+ # testing.
+
+ def gzip_to(io) # :yields: gz_io
+ gz_io = Zlib::GzipWriter.new io, Zlib::BEST_COMPRESSION
+ gz_io.mtime = @build_time
+
+ yield gz_io
+ ensure
+ gz_io.close
+ end
+
+ ##
+ # Returns the full path for installing +filename+.
+ #
+ # If +filename+ is not inside +destination_dir+ an exception is raised.
+
+ def install_location(filename, destination_dir) # :nodoc:
+ raise Gem::Package::PathError.new(filename, destination_dir) if
+ filename.start_with? "/"
+
+ destination_dir = File.realpath(destination_dir)
+ destination = File.expand_path(filename, destination_dir)
+
+ raise Gem::Package::PathError.new(destination, destination_dir) unless
+ normalize_path(destination).start_with? normalize_path(destination_dir + "/")
+
+ destination
+ end
+
+ if Gem.win_platform?
+ def normalize_path(pathname) # :nodoc:
+ pathname.downcase
+ end
+ else
+ def normalize_path(pathname) # :nodoc:
+ pathname
+ end
+ end
+
+ ##
+ # Loads a Gem::Specification from the TarEntry +entry+
+
+ def load_spec_from_metadata(entry) # :nodoc:
+ limit = 10 * 1024 * 1024
+ case entry.full_name
+ when "metadata" then
+ @spec = Gem::Specification.from_yaml limit_read(entry, "metadata", limit)
+ when "metadata.gz" then
+ Zlib::GzipReader.wrap(entry, external_encoding: Encoding::UTF_8) do |gzio|
+ @spec = Gem::Specification.from_yaml limit_read(gzio, "metadata.gz", limit)
+ end
+ end
+ end
+
+ ##
+ # Opens +io+ as a gzipped tar archive
+
+ def open_tar_gz(io) # :nodoc:
+ Zlib::GzipReader.wrap io do |gzio|
+ tar = Gem::Package::TarReader.new gzio
+
+ yield tar
+ ensure
+ # Consume remaining gzip data to prevent the
+ # "attempt to close unfinished zstream; reset forced" warning
+ # when the GzipReader is closed with unconsumed compressed data.
+ begin
+ IO.copy_stream(gzio, IO::NULL)
+ rescue Zlib::GzipFile::Error, IOError
+ nil
+ end
+ end
+ end
+
+ ##
+ # Reads and loads checksums.yaml.gz from the tar file +gem+
+
+ def read_checksums(gem)
+ Gem.load_yaml
+
+ @checksums = gem.seek "checksums.yaml.gz" do |entry|
+ Zlib::GzipReader.wrap entry do |gz_io|
+ Gem::SafeYAML.safe_load limit_read(gz_io, "checksums.yaml.gz", 10 * 1024 * 1024)
+ end
+ end
+ end
+
+ ##
+ # Prepares the gem for signing and checksum generation. If a signing
+ # certificate and key are not present only checksum generation is set up.
+
+ def setup_signer(signer_options: {})
+ passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"]
+ if @spec.signing_key
+ @signer =
+ Gem::Security::Signer.new(
+ @spec.signing_key,
+ @spec.cert_chain,
+ passphrase,
+ signer_options
+ )
+
+ @spec.signing_key = nil
+ @spec.cert_chain = @signer.cert_chain.map(&:to_s)
+ else
+ @signer = Gem::Security::Signer.new nil, nil, passphrase
+ @spec.cert_chain = @signer.cert_chain.map(&:to_pem) if
+ @signer.cert_chain
+ end
+ end
+
+ ##
+ # The spec for this gem.
+ #
+ # If this is a package for a built gem the spec is loaded from the
+ # gem and returned. If this is a package for a gem being built the provided
+ # spec is returned.
+
+ def spec
+ verify unless @spec
+
+ @spec
+ end
+
+ ##
+ # Verifies that this gem:
+ #
+ # * Contains a valid gem specification
+ # * Contains a contents archive
+ # * The contents archive is not corrupt
+ #
+ # After verification the gem specification from the gem is available from
+ # #spec
+
+ def verify
+ @files = []
+ @spec = nil
+
+ @gem.with_read_io do |io|
+ Gem::Package::TarReader.new io do |reader|
+ read_checksums reader
+
+ verify_files reader
+ end
+ end
+
+ verify_checksums @digests, @checksums
-require 'rubygems/package/f_sync_dir'
-require 'rubygems/package/tar_header'
-require 'rubygems/package/tar_input'
-require 'rubygems/package/tar_output'
-require 'rubygems/package/tar_reader'
-require 'rubygems/package/tar_reader/entry'
-require 'rubygems/package/tar_writer'
+ @security_policy&.verify_signatures @spec, @digests, @signatures
+
+ true
+ rescue Gem::Security::Exception
+ @spec = nil
+ @files = []
+ raise
+ rescue Errno::ENOENT => e
+ raise Gem::Package::FormatError.new e.message
+ rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e
+ raise Gem::Package::FormatError.new e.message, @gem
+ end
+
+ private
+
+ ##
+ # Verifies the +checksums+ against the +digests+. This check is not
+ # cryptographically secure. Missing checksums are ignored.
+
+ def verify_checksums(digests, checksums) # :nodoc:
+ return unless checksums
+
+ checksums.sort.each do |algorithm, gem_digests|
+ gem_digests.sort.each do |file_name, gem_hexdigest|
+ computed_digest = digests[algorithm][file_name]
+
+ unless computed_digest.hexdigest == gem_hexdigest
+ raise Gem::Package::FormatError.new \
+ "#{algorithm} checksum mismatch for #{file_name}", @gem
+ end
+ end
+ end
+ end
+
+ ##
+ # Verifies +entry+ in a .gem file.
+
+ def verify_entry(entry)
+ file_name = entry.full_name
+ @files << file_name
+
+ case file_name
+ when /\.sig$/ then
+ @signatures[$`] = limit_read(entry, file_name, 1024 * 1024) if @security_policy
+ return
+ else
+ digest entry
+ end
+
+ load_spec_from_metadata entry
+ rescue StandardError
+ warn "Exception while verifying #{@gem.path}"
+ raise
+ end
+
+ ##
+ # Verifies the files of the +gem+
+
+ def verify_files(gem)
+ gem.each do |entry|
+ verify_entry entry
+ end
+
+ unless @spec
+ raise Gem::Package::FormatError.new "package metadata is missing", @gem
+ end
+
+ unless @files.include? "data.tar.gz"
+ raise Gem::Package::FormatError.new \
+ "package content (data.tar.gz) is missing", @gem
+ end
+
+ if (duplicates = @files.group_by {|f| f }.select {|_k,v| v.size > 1 }.map(&:first)) && duplicates.any?
+ raise Gem::Security::Exception, "duplicate files in the package: (#{duplicates.map(&:inspect).join(", ")})"
+ end
+ end
+
+ if RUBY_ENGINE == "truffleruby"
+ def copy_stream(src, dst, size) # :nodoc:
+ dst.write src.read(size)
+ end
+ else
+ def copy_stream(src, dst, size) # :nodoc:
+ IO.copy_stream(src, dst, size)
+ end
+ end
+
+ def limit_read(io, name, limit)
+ bytes = io.read(limit + 1)
+ raise Gem::Package::FormatError, "#{name} is too big (over #{limit} bytes)" if bytes.size > limit
+ bytes
+ end
+
+ if Gem.win_platform?
+ # Create a symlink and fallback to copy the file or directory on Windows,
+ # where symlink creation needs special privileges in form of the Developer Mode.
+ # JRuby on Windows raises TypeError from the wincode path-conversion helper
+ # when it cannot create the symlink, so fall back to copy in that case too.
+ def create_symlink(old_name, new_name)
+ File.symlink(old_name, new_name)
+ rescue Errno::EACCES, TypeError
+ from = File.expand_path(old_name, File.dirname(new_name))
+ FileUtils.cp_r(from, new_name)
+ end
+ else
+ def create_symlink(old_name, new_name)
+ File.symlink(old_name, new_name)
+ end
+ end
+end
+require_relative "package/digest_io"
+require_relative "package/source"
+require_relative "package/file_source"
+require_relative "package/io_source"
+require_relative "package/old"
+require_relative "package/tar_header"
+require_relative "package/tar_reader"
+require_relative "package/tar_reader/entry"
+require_relative "package/tar_writer"
diff --git a/lib/rubygems/package/digest_io.rb b/lib/rubygems/package/digest_io.rb
new file mode 100644
index 0000000000..f04ab97462
--- /dev/null
+++ b/lib/rubygems/package/digest_io.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+##
+# IO wrapper that creates digests of contents written to the IO it wraps.
+
+class Gem::Package::DigestIO
+ ##
+ # Collected digests for wrapped writes.
+ #
+ # {
+ # 'SHA1' => #<OpenSSL::Digest: [...]>,
+ # 'SHA512' => #<OpenSSL::Digest: [...]>,
+ # }
+
+ attr_reader :digests
+
+ ##
+ # Wraps +io+ and updates digest for each of the digest algorithms in
+ # the +digests+ Hash. Returns the digests hash. Example:
+ #
+ # io = StringIO.new
+ # digests = {
+ # 'SHA1' => OpenSSL::Digest.new('SHA1'),
+ # 'SHA512' => OpenSSL::Digest.new('SHA512'),
+ # }
+ #
+ # Gem::Package::DigestIO.wrap io, digests do |digest_io|
+ # digest_io.write "hello"
+ # end
+ #
+ # digests['SHA1'].hexdigest #=> "aaf4c61d[...]"
+ # digests['SHA512'].hexdigest #=> "9b71d224[...]"
+
+ def self.wrap(io, digests)
+ digest_io = new io, digests
+
+ yield digest_io
+
+ digests
+ end
+
+ ##
+ # Creates a new DigestIO instance. Using ::wrap is recommended, see the
+ # ::wrap documentation for documentation of +io+ and +digests+.
+
+ def initialize(io, digests)
+ @io = io
+ @digests = digests
+ end
+
+ ##
+ # Writes +data+ to the underlying IO and updates the digests
+
+ def write(data)
+ result = @io.write data
+
+ @digests.each do |_, digest|
+ digest << data
+ end
+
+ result
+ end
+end
diff --git a/lib/rubygems/package/f_sync_dir.rb b/lib/rubygems/package/f_sync_dir.rb
deleted file mode 100644
index 3e2e4a59a8..0000000000
--- a/lib/rubygems/package/f_sync_dir.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-#++
-# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-# See LICENSE.txt for additional licensing information.
-#--
-
-require 'rubygems/package'
-
-module Gem::Package::FSyncDir
-
- private
-
- ##
- # make sure this hits the disc
-
- def fsync_dir(dirname)
- dir = open dirname, 'r'
- dir.fsync
- rescue # ignore IOError if it's an unpatched (old) Ruby
- ensure
- dir.close if dir rescue nil
- end
-
-end
-
diff --git a/lib/rubygems/package/file_source.rb b/lib/rubygems/package/file_source.rb
new file mode 100644
index 0000000000..d9717e0f2a
--- /dev/null
+++ b/lib/rubygems/package/file_source.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+##
+# The primary source of gems is a file on disk, including all usages
+# internal to rubygems.
+#
+# This is a private class, do not depend on it directly. Instead, pass a path
+# object to `Gem::Package.new`.
+
+class Gem::Package::FileSource < Gem::Package::Source # :nodoc: all
+ attr_reader :path
+
+ def initialize(path)
+ @path = path
+ end
+
+ def start
+ @start ||= File.read path, 20
+ end
+
+ def present?
+ File.exist? path
+ end
+
+ def with_write_io(&block)
+ File.open path, "wb", &block
+ end
+
+ def with_read_io(&block)
+ File.open path, "rb", &block
+ end
+end
diff --git a/lib/rubygems/package/io_source.rb b/lib/rubygems/package/io_source.rb
new file mode 100644
index 0000000000..227835dfce
--- /dev/null
+++ b/lib/rubygems/package/io_source.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+##
+# Supports reading and writing gems from/to a generic IO object. This is
+# useful for other applications built on top of rubygems, such as
+# rubygems.org.
+#
+# This is a private class, do not depend on it directly. Instead, pass an IO
+# object to `Gem::Package.new`.
+
+class Gem::Package::IOSource < Gem::Package::Source # :nodoc: all
+ attr_reader :io
+
+ def initialize(io)
+ @io = io
+ end
+
+ def start
+ @start ||= begin
+ if io.pos > 0
+ raise Gem::Package::Error, "Cannot read start unless IO is at start"
+ end
+
+ value = io.read 20
+ io.rewind
+ value
+ end
+ end
+
+ def present?
+ true
+ end
+
+ def with_read_io
+ yield io
+ ensure
+ io.rewind
+ end
+
+ def with_write_io
+ yield io
+ ensure
+ io.rewind
+ end
+
+ def path
+ end
+end
diff --git a/lib/rubygems/package/old.rb b/lib/rubygems/package/old.rb
new file mode 100644
index 0000000000..1a13ac3e29
--- /dev/null
+++ b/lib/rubygems/package/old.rb
@@ -0,0 +1,169 @@
+# frozen_string_literal: true
+
+#--
+# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
+# All rights reserved.
+# See LICENSE.txt for permissions.
+#++
+
+##
+# The format class knows the guts of the ancient .gem file format and provides
+# the capability to read such ancient gems.
+#
+# Please pretend this doesn't exist.
+
+class Gem::Package::Old < Gem::Package
+ undef_method :spec=
+
+ ##
+ # Creates a new old-format package reader for +gem+. Old-format packages
+ # cannot be written.
+
+ def initialize(gem, security_policy)
+ require "fileutils"
+ require "zlib"
+ Gem.load_yaml
+
+ @contents = nil
+ @gem = gem
+ @security_policy = security_policy
+ @spec = nil
+ end
+
+ ##
+ # A list of file names contained in this gem
+
+ def contents
+ verify
+
+ return @contents if @contents
+
+ @gem.with_read_io do |io|
+ read_until_dashes io # spec
+ header = file_list io
+
+ @contents = header.map {|file| file["path"] }
+ end
+ end
+
+ ##
+ # Extracts the files in this package into +destination_dir+
+
+ def extract_files(destination_dir)
+ verify
+
+ errstr = "Error reading files from gem"
+
+ @gem.with_read_io do |io|
+ read_until_dashes io # spec
+ header = file_list io
+ raise Gem::Exception, errstr unless header
+
+ header.each do |entry|
+ full_name = entry["path"]
+
+ destination = install_location full_name, destination_dir
+
+ file_data = String.new
+
+ read_until_dashes io do |line|
+ file_data << line
+ end
+
+ file_data = file_data.strip.unpack1("m")
+ file_data = Zlib::Inflate.inflate file_data
+
+ raise Gem::Package::FormatError, "#{full_name} in #{@gem} is corrupt" if
+ file_data.length != entry["size"].to_i
+
+ FileUtils.rm_rf destination
+
+ FileUtils.mkdir_p File.dirname(destination), mode: dir_mode && 0o755
+
+ File.open destination, "wb", file_mode(entry["mode"]) do |out|
+ out.write file_data
+ end
+
+ verbose destination
+ end
+ end
+ rescue Zlib::DataError
+ raise Gem::Exception, errstr
+ end
+
+ ##
+ # Reads the file list section from the old-format gem +io+
+
+ def file_list(io) # :nodoc:
+ header = String.new
+
+ read_until_dashes io do |line|
+ header << line
+ end
+
+ Gem::SafeYAML.safe_load header
+ end
+
+ ##
+ # Reads lines until a "---" separator is found
+
+ def read_until_dashes(io) # :nodoc:
+ while (line = io.gets) && line.chomp.strip != "---" do
+ yield line if block_given?
+ end
+ end
+
+ ##
+ # Skips the Ruby self-install header in +io+.
+
+ def skip_ruby(io) # :nodoc:
+ loop do
+ line = io.gets
+
+ return if line.chomp == "__END__"
+ break unless line
+ end
+
+ raise Gem::Exception, "Failed to find end of Ruby script while reading gem"
+ end
+
+ ##
+ # The specification for this gem
+
+ def spec
+ verify
+
+ return @spec if @spec
+
+ yaml = String.new
+
+ @gem.with_read_io do |io|
+ skip_ruby io
+ read_until_dashes io do |line|
+ yaml << line
+ end
+ end
+
+ begin
+ @spec = Gem::Specification.from_yaml yaml
+ rescue Psych::SyntaxError
+ raise Gem::Exception, "Failed to parse gem specification out of gem file"
+ end
+ rescue ArgumentError
+ raise Gem::Exception, "Failed to parse gem specification out of gem file"
+ end
+
+ ##
+ # Raises an exception if a security policy that verifies data is active.
+ # Old format gems cannot be verified as signed.
+
+ def verify
+ return true unless @security_policy
+
+ raise Gem::Security::Exception,
+ "old format gems do not contain signatures and cannot be verified" if
+ @security_policy.verify_data
+
+ true
+ end
+end
diff --git a/lib/rubygems/package/source.rb b/lib/rubygems/package/source.rb
new file mode 100644
index 0000000000..8c44f8c305
--- /dev/null
+++ b/lib/rubygems/package/source.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class Gem::Package::Source # :nodoc:
+end
diff --git a/lib/rubygems/package/tar_header.rb b/lib/rubygems/package/tar_header.rb
index c194cc0530..dd20d65080 100644
--- a/lib/rubygems/package/tar_header.rb
+++ b/lib/rubygems/package/tar_header.rb
@@ -1,9 +1,11 @@
-#++
-# Copyright (C) 2004 Mauricio Julio Fernández Pradier
+# frozen_string_literal: true
+
+# rubocop:disable Style/AsciiComments
+
+# Copyright (C) 2004 Mauricio Julio Fernández Pradier
# See LICENSE.txt for additional licensing information.
-#--
-require 'rubygems/package'
+# rubocop:enable Style/AsciiComments
##
#--
@@ -26,8 +28,11 @@ require 'rubygems/package'
# char prefix[155]; # ASCII + (Z unless filled)
# };
#++
+# A header for a tar file
class Gem::Package::TarHeader
+ ##
+ # Fields in the tar header
FIELDS = [
:checksum,
@@ -46,169 +51,198 @@ class Gem::Package::TarHeader
:uid,
:uname,
:version,
- ]
-
- PACK_FORMAT = 'a100' + # name
- 'a8' + # mode
- 'a8' + # uid
- 'a8' + # gid
- 'a12' + # size
- 'a12' + # mtime
- 'a7a' + # chksum
- 'a' + # typeflag
- 'a100' + # linkname
- 'a6' + # magic
- 'a2' + # version
- 'a32' + # uname
- 'a32' + # gname
- 'a8' + # devmajor
- 'a8' + # devminor
- 'a155' # prefix
-
- UNPACK_FORMAT = 'A100' + # name
- 'A8' + # mode
- 'A8' + # uid
- 'A8' + # gid
- 'A12' + # size
- 'A12' + # mtime
- 'A8' + # checksum
- 'A' + # typeflag
- 'A100' + # linkname
- 'A6' + # magic
- 'A2' + # version
- 'A32' + # uname
- 'A32' + # gname
- 'A8' + # devmajor
- 'A8' + # devminor
- 'A155' # prefix
+ ].freeze
+
+ ##
+ # Pack format for a tar header
+
+ PACK_FORMAT = ("a100" + # name
+ "a8" + # mode
+ "a8" + # uid
+ "a8" + # gid
+ "a12" + # size
+ "a12" + # mtime
+ "a7a" + # chksum
+ "a" + # typeflag
+ "a100" + # linkname
+ "a6" + # magic
+ "a2" + # version
+ "a32" + # uname
+ "a32" + # gname
+ "a8" + # devmajor
+ "a8" + # devminor
+ "a155").freeze # prefix
+
+ ##
+ # Unpack format for a tar header
+
+ UNPACK_FORMAT = ("A100" + # name
+ "A8" + # mode
+ "A8" + # uid
+ "A8" + # gid
+ "A12" + # size
+ "A12" + # mtime
+ "A8" + # checksum
+ "A" + # typeflag
+ "A100" + # linkname
+ "A6" + # magic
+ "A2" + # version
+ "A32" + # uname
+ "A32" + # gname
+ "A8" + # devmajor
+ "A8" + # devminor
+ "A155").freeze # prefix
attr_reader(*FIELDS)
+ EMPTY_HEADER = ("\0" * 512).b.freeze # :nodoc:
+
+ ##
+ # Creates a tar header from IO +stream+
+
def self.from(stream)
header = stream.read 512
- empty = (header == "\0" * 512)
+ return EMPTY if header == EMPTY_HEADER
fields = header.unpack UNPACK_FORMAT
- name = fields.shift
- mode = fields.shift.oct
- uid = fields.shift.oct
- gid = fields.shift.oct
- size = fields.shift.oct
- mtime = fields.shift.oct
- checksum = fields.shift.oct
- typeflag = fields.shift
- linkname = fields.shift
- magic = fields.shift
- version = fields.shift.oct
- uname = fields.shift
- gname = fields.shift
- devmajor = fields.shift.oct
- devminor = fields.shift.oct
- prefix = fields.shift
-
- new :name => name,
- :mode => mode,
- :uid => uid,
- :gid => gid,
- :size => size,
- :mtime => mtime,
- :checksum => checksum,
- :typeflag => typeflag,
- :linkname => linkname,
- :magic => magic,
- :version => version,
- :uname => uname,
- :gname => gname,
- :devmajor => devmajor,
- :devminor => devminor,
- :prefix => prefix,
-
- :empty => empty
-
- # HACK unfactor for Rubinius
- #new :name => fields.shift,
- # :mode => fields.shift.oct,
- # :uid => fields.shift.oct,
- # :gid => fields.shift.oct,
- # :size => fields.shift.oct,
- # :mtime => fields.shift.oct,
- # :checksum => fields.shift.oct,
- # :typeflag => fields.shift,
- # :linkname => fields.shift,
- # :magic => fields.shift,
- # :version => fields.shift.oct,
- # :uname => fields.shift,
- # :gname => fields.shift,
- # :devmajor => fields.shift.oct,
- # :devminor => fields.shift.oct,
- # :prefix => fields.shift,
-
- # :empty => empty
+ new name: fields.shift,
+ mode: strict_oct(fields.shift),
+ uid: oct_or_256based(fields.shift),
+ gid: oct_or_256based(fields.shift),
+ size: strict_oct(fields.shift),
+ mtime: strict_oct(fields.shift),
+ checksum: strict_oct(fields.shift),
+ typeflag: fields.shift,
+ linkname: fields.shift,
+ magic: fields.shift,
+ version: strict_oct(fields.shift),
+ uname: fields.shift,
+ gname: fields.shift,
+ devmajor: strict_oct(fields.shift),
+ devminor: strict_oct(fields.shift),
+ prefix: fields.shift,
+
+ empty: false
+ end
+
+ def self.strict_oct(str)
+ str.strip!
+ return str.oct if /\A[0-7]*\z/.match?(str)
+
+ raise ArgumentError, "#{str.inspect} is not an octal string"
+ end
+
+ def self.oct_or_256based(str)
+ # \x80 flags a positive 256-based number
+ # \ff flags a negative 256-based number
+ # In case we have a match, parse it as a signed binary value
+ # in big-endian order, except that the high-order bit is ignored.
+
+ return str.unpack1("@4N") if /\A[\x80\xff]/n.match?(str)
+ strict_oct(str)
end
+ ##
+ # Creates a new TarHeader using +vals+
+
def initialize(vals)
- unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode] then
+ unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode]
raise ArgumentError, ":name, :size, :prefix and :mode required"
end
- vals[:uid] ||= 0
- vals[:gid] ||= 0
- vals[:mtime] ||= 0
- vals[:checksum] ||= ""
- vals[:typeflag] ||= "0"
- vals[:magic] ||= "ustar"
- vals[:version] ||= "00"
- vals[:uname] ||= "wheel"
- vals[:gname] ||= "wheel"
- vals[:devmajor] ||= 0
- vals[:devminor] ||= 0
-
- FIELDS.each do |name|
- instance_variable_set "@#{name}", vals[name]
- end
+ @checksum = vals[:checksum] || ""
+ @devmajor = vals[:devmajor] || 0
+ @devminor = vals[:devminor] || 0
+ @gid = vals[:gid] || 0
+ @gname = vals[:gname] || "wheel"
+ @linkname = vals[:linkname]
+ @magic = vals[:magic] || "ustar"
+ @mode = vals[:mode]
+ @mtime = vals[:mtime] || 0
+ @name = vals[:name]
+ @prefix = vals[:prefix]
+ @size = vals[:size]
+ @typeflag = vals[:typeflag]
+ @typeflag = "0" if @typeflag.nil? || @typeflag.empty?
+ @uid = vals[:uid] || 0
+ @uname = vals[:uname] || "wheel"
+ @version = vals[:version] || "00"
@empty = vals[:empty]
end
+ EMPTY = new({ # :nodoc:
+ checksum: 0,
+ gname: "",
+ linkname: "",
+ magic: "",
+ mode: 0,
+ name: "",
+ prefix: "",
+ size: 0,
+ uname: "",
+ version: 0,
+
+ empty: true,
+ }).freeze
+ private_constant :EMPTY
+
+ ##
+ # Is the tar entry empty?
+
def empty?
@empty
end
- def ==(other)
- self.class === other and
- @checksum == other.checksum and
- @devmajor == other.devmajor and
- @devminor == other.devminor and
- @gid == other.gid and
- @gname == other.gname and
- @linkname == other.linkname and
- @magic == other.magic and
- @mode == other.mode and
- @mtime == other.mtime and
- @name == other.name and
- @prefix == other.prefix and
- @size == other.size and
- @typeflag == other.typeflag and
- @uid == other.uid and
- @uname == other.uname and
- @version == other.version
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @checksum == other.checksum &&
+ @devmajor == other.devmajor &&
+ @devminor == other.devminor &&
+ @gid == other.gid &&
+ @gname == other.gname &&
+ @linkname == other.linkname &&
+ @magic == other.magic &&
+ @mode == other.mode &&
+ @mtime == other.mtime &&
+ @name == other.name &&
+ @prefix == other.prefix &&
+ @size == other.size &&
+ @typeflag == other.typeflag &&
+ @uid == other.uid &&
+ @uname == other.uname &&
+ @version == other.version
end
- def to_s
+ def to_s # :nodoc:
update_checksum
header
end
+ ##
+ # Updates the TarHeader's checksum
+
def update_checksum
header = header " " * 8
@checksum = oct calculate_checksum(header), 6
end
+ ##
+ # Header's full name, including prefix
+
+ def full_name
+ if prefix != ""
+ File.join prefix, name
+ else
+ name
+ end
+ end
+
private
def calculate_checksum(header)
- header.unpack("C*").inject { |a, b| a + b }
+ header.sum(0)
end
def header(checksum = @checksum)
@@ -229,17 +263,15 @@ class Gem::Package::TarHeader
gname,
oct(devmajor, 7),
oct(devminor, 7),
- prefix
+ prefix,
]
header = header.pack PACK_FORMAT
-
- header << ("\0" * ((512 - header.size) % 512))
+
+ header.ljust 512, "\0"
end
def oct(num, len)
- "%0#{len}o" % num
+ format("%0#{len}o", num)
end
-
end
-
diff --git a/lib/rubygems/package/tar_input.rb b/lib/rubygems/package/tar_input.rb
deleted file mode 100644
index 2ed3d6b772..0000000000
--- a/lib/rubygems/package/tar_input.rb
+++ /dev/null
@@ -1,219 +0,0 @@
-#++
-# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-# See LICENSE.txt for additional licensing information.
-#--
-
-require 'rubygems/package'
-
-class Gem::Package::TarInput
-
- include Gem::Package::FSyncDir
- include Enumerable
-
- attr_reader :metadata
-
- private_class_method :new
-
- def self.open(io, security_policy = nil, &block)
- is = new io, security_policy
-
- yield is
- ensure
- is.close if is
- end
-
- def initialize(io, security_policy = nil)
- @io = io
- @tarreader = Gem::Package::TarReader.new @io
- has_meta = false
-
- data_sig, meta_sig, data_dgst, meta_dgst = nil, nil, nil, nil
- dgst_algo = security_policy ? Gem::Security::OPT[:dgst_algo] : nil
-
- @tarreader.each do |entry|
- case entry.full_name
- when "metadata"
- @metadata = load_gemspec entry.read
- has_meta = true
- when "metadata.gz"
- begin
- # if we have a security_policy, then pre-read the metadata file
- # and calculate it's digest
- sio = nil
- if security_policy
- Gem.ensure_ssl_available
- sio = StringIO.new(entry.read)
- meta_dgst = dgst_algo.digest(sio.string)
- sio.rewind
- end
-
- gzis = Zlib::GzipReader.new(sio || entry)
- # YAML wants an instance of IO
- @metadata = load_gemspec(gzis)
- has_meta = true
- ensure
- gzis.close unless gzis.nil?
- end
- when 'metadata.gz.sig'
- meta_sig = entry.read
- when 'data.tar.gz.sig'
- data_sig = entry.read
- when 'data.tar.gz'
- if security_policy
- Gem.ensure_ssl_available
- data_dgst = dgst_algo.digest(entry.read)
- end
- end
- end
-
- if security_policy then
- Gem.ensure_ssl_available
-
- # map trust policy from string to actual class (or a serialized YAML
- # file, if that exists)
- if String === security_policy then
- if Gem::Security::Policy.key? security_policy then
- # load one of the pre-defined security policies
- security_policy = Gem::Security::Policy[security_policy]
- elsif File.exist? security_policy then
- # FIXME: this doesn't work yet
- security_policy = YAML.load File.read(security_policy)
- else
- raise Gem::Exception, "Unknown trust policy '#{security_policy}'"
- end
- end
-
- if data_sig && data_dgst && meta_sig && meta_dgst then
- # the user has a trust policy, and we have a signed gem
- # file, so use the trust policy to verify the gem signature
-
- begin
- security_policy.verify_gem(data_sig, data_dgst, @metadata.cert_chain)
- rescue Exception => e
- raise "Couldn't verify data signature: #{e}"
- end
-
- begin
- security_policy.verify_gem(meta_sig, meta_dgst, @metadata.cert_chain)
- rescue Exception => e
- raise "Couldn't verify metadata signature: #{e}"
- end
- elsif security_policy.only_signed
- raise Gem::Exception, "Unsigned gem"
- else
- # FIXME: should display warning here (trust policy, but
- # either unsigned or badly signed gem file)
- end
- end
-
- @tarreader.rewind
- @fileops = Gem::FileOperations.new
-
- raise Gem::Package::FormatError, "No metadata found!" unless has_meta
- end
-
- def close
- @io.close
- @tarreader.close
- end
-
- def each(&block)
- @tarreader.each do |entry|
- next unless entry.full_name == "data.tar.gz"
- is = zipped_stream entry
-
- begin
- Gem::Package::TarReader.new is do |inner|
- inner.each(&block)
- end
- ensure
- is.close if is
- end
- end
-
- @tarreader.rewind
- end
-
- def extract_entry(destdir, entry, expected_md5sum = nil)
- if entry.directory? then
- dest = File.join(destdir, entry.full_name)
-
- if File.dir? dest then
- @fileops.chmod entry.header.mode, dest, :verbose=>false
- else
- @fileops.mkdir_p dest, :mode => entry.header.mode, :verbose => false
- end
-
- fsync_dir dest
- fsync_dir File.join(dest, "..")
-
- return
- end
-
- # it's a file
- md5 = Digest::MD5.new if expected_md5sum
- destdir = File.join destdir, File.dirname(entry.full_name)
- @fileops.mkdir_p destdir, :mode => 0755, :verbose => false
- destfile = File.join destdir, File.basename(entry.full_name)
- @fileops.chmod 0600, destfile, :verbose => false rescue nil # Errno::ENOENT
-
- open destfile, "wb", entry.header.mode do |os|
- loop do
- data = entry.read 4096
- break unless data
- # HACK shouldn't we check the MD5 before writing to disk?
- md5 << data if expected_md5sum
- os.write(data)
- end
-
- os.fsync
- end
-
- @fileops.chmod entry.header.mode, destfile, :verbose => false
- fsync_dir File.dirname(destfile)
- fsync_dir File.join(File.dirname(destfile), "..")
-
- if expected_md5sum && expected_md5sum != md5.hexdigest then
- raise Gem::Package::BadCheckSum
- end
- end
-
- # Attempt to YAML-load a gemspec from the given _io_ parameter. Return
- # nil if it fails.
- def load_gemspec(io)
- Gem::Specification.from_yaml io
- rescue Gem::Exception
- nil
- end
-
- ##
- # Return an IO stream for the zipped entry.
- #
- # NOTE: Originally this method used two approaches, Return a GZipReader
- # directly, or read the GZipReader into a string and return a StringIO on
- # the string. The string IO approach was used for versions of ZLib before
- # 1.2.1 to avoid buffer errors on windows machines. Then we found that
- # errors happened with 1.2.1 as well, so we changed the condition. Then
- # we discovered errors occurred with versions as late as 1.2.3. At this
- # point (after some benchmarking to show we weren't seriously crippling
- # the unpacking speed) we threw our hands in the air and declared that
- # this method would use the String IO approach on all platforms at all
- # times. And that's the way it is.
-
- def zipped_stream(entry)
- if defined? Rubinius then
- zis = Zlib::GzipReader.new entry
- dis = zis.read
- is = StringIO.new(dis)
- else
- # This is Jamis Buck's Zlib workaround for some unknown issue
- entry.read(10) # skip the gzip header
- zis = Zlib::Inflate.new(-Zlib::MAX_WBITS)
- is = StringIO.new(zis.inflate(entry.read))
- end
- ensure
- zis.finish if zis
- end
-
-end
-
diff --git a/lib/rubygems/package/tar_output.rb b/lib/rubygems/package/tar_output.rb
deleted file mode 100644
index b22f7dd86b..0000000000
--- a/lib/rubygems/package/tar_output.rb
+++ /dev/null
@@ -1,143 +0,0 @@
-#++
-# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-# See LICENSE.txt for additional licensing information.
-#--
-
-require 'rubygems/package'
-
-##
-# TarOutput is a wrapper to TarWriter that builds gem-format tar file.
-#
-# Gem-format tar files contain the following files:
-# [data.tar.gz] A gzipped tar file containing the files that compose the gem
-# which will be extracted into the gem/ dir on installation.
-# [metadata.gz] A YAML format Gem::Specification.
-# [data.tar.gz.sig] A signature for the gem's data.tar.gz.
-# [metadata.gz.sig] A signature for the gem's metadata.gz.
-#
-# See TarOutput::open for usage details.
-
-class Gem::Package::TarOutput
-
- ##
- # Creates a new TarOutput which will yield a TarWriter object for the
- # data.tar.gz portion of a gem-format tar file.
- #
- # See #initialize for details on +io+ and +signer+.
- #
- # See #add_gem_contents for details on adding metadata to the tar file.
-
- def self.open(io, signer = nil, &block) # :yield: data_tar_writer
- tar_outputter = new io, signer
- tar_outputter.add_gem_contents(&block)
- tar_outputter.add_metadata
- tar_outputter.add_signatures
-
- ensure
- tar_outputter.close
- end
-
- ##
- # Creates a new TarOutput that will write a gem-format tar file to +io+. If
- # +signer+ is given, the data.tar.gz and metadata.gz will be signed and
- # the signatures will be added to the tar file.
-
- def initialize(io, signer)
- @io = io
- @signer = signer
-
- @tar_writer = Gem::Package::TarWriter.new @io
-
- @metadata = nil
-
- @data_signature = nil
- @meta_signature = nil
- end
-
- ##
- # Yields a TarWriter for the data.tar.gz inside a gem-format tar file.
- # The yielded TarWriter has been extended with a #metadata= method for
- # attaching a YAML format Gem::Specification which will be written by
- # add_metadata.
-
- def add_gem_contents
- @tar_writer.add_file "data.tar.gz", 0644 do |inner|
- sio = @signer ? StringIO.new : nil
- Zlib::GzipWriter.wrap(sio || inner) do |os|
-
- Gem::Package::TarWriter.new os do |data_tar_writer|
- def data_tar_writer.metadata() @metadata end
- def data_tar_writer.metadata=(metadata) @metadata = metadata end
-
- yield data_tar_writer
-
- @metadata = data_tar_writer.metadata
- end
- end
-
- # if we have a signing key, then sign the data
- # digest and return the signature
- if @signer then
- digest = Gem::Security::OPT[:dgst_algo].digest sio.string
- @data_signature = @signer.sign digest
- inner.write sio.string
- end
- end
-
- self
- end
-
- ##
- # Adds metadata.gz to the gem-format tar file which was saved from a
- # previous #add_gem_contents call.
-
- def add_metadata
- return if @metadata.nil?
-
- @tar_writer.add_file "metadata.gz", 0644 do |io|
- begin
- sio = @signer ? StringIO.new : nil
- gzos = Zlib::GzipWriter.new(sio || io)
- gzos.write @metadata
- ensure
- gzos.flush
- gzos.finish
-
- # if we have a signing key, then sign the metadata digest and return
- # the signature
- if @signer then
- digest = Gem::Security::OPT[:dgst_algo].digest sio.string
- @meta_signature = @signer.sign digest
- io.write sio.string
- end
- end
- end
- end
-
- ##
- # Adds data.tar.gz.sig and metadata.gz.sig to the gem-format tar files if
- # a Gem::Security::Signer was sent to initialize.
-
- def add_signatures
- if @data_signature then
- @tar_writer.add_file 'data.tar.gz.sig', 0644 do |io|
- io.write @data_signature
- end
- end
-
- if @meta_signature then
- @tar_writer.add_file 'metadata.gz.sig', 0644 do |io|
- io.write @meta_signature
- end
- end
- end
-
- ##
- # Closes the TarOutput.
-
- def close
- @tar_writer.close
- end
-
-end
-
diff --git a/lib/rubygems/package/tar_reader.rb b/lib/rubygems/package/tar_reader.rb
index 4aa9c26cc9..b66a8a62bc 100644
--- a/lib/rubygems/package/tar_reader.rb
+++ b/lib/rubygems/package/tar_reader.rb
@@ -1,15 +1,20 @@
-#++
-# Copyright (C) 2004 Mauricio Julio Fernández Pradier
+# frozen_string_literal: true
+
+# rubocop:disable Style/AsciiComments
+
+# Copyright (C) 2004 Mauricio Julio Fernández Pradier
# See LICENSE.txt for additional licensing information.
-#--
-require 'rubygems/package'
+# rubocop:enable Style/AsciiComments
-class Gem::Package::TarReader
+##
+# TarReader reads tar files and allows iteration over their items
- include Gem::Package
+class Gem::Package::TarReader
+ include Enumerable
- class UnexpectedEOF < StandardError; end
+ ##
+ # Creates a new TarReader on +io+ and yields it to the block, if given.
def self.new(io)
reader = super
@@ -25,62 +30,74 @@ class Gem::Package::TarReader
nil
end
+ attr_reader :io # :nodoc:
+
+ ##
+ # Creates a new tar file reader on +io+ which needs to respond to #pos,
+ # #eof?, #read, #getc and #pos=
+
def initialize(io)
@io = io
@init_pos = io.pos
end
+ ##
+ # Close the tar file
+
def close
end
- def each
- loop do
- return if @io.eof?
-
- header = Gem::Package::TarHeader.from @io
- return if header.empty?
-
- entry = Gem::Package::TarReader::Entry.new header, @io
- size = entry.header.size
-
- yield entry
+ ##
+ # Iterates over files in the tarball yielding each entry
- skip = (512 - (size % 512)) % 512
- pending = size - entry.bytes_read
+ def each
+ return enum_for __method__ unless block_given?
+ until @io.eof? do
begin
- # avoid reading...
- @io.seek pending, IO::SEEK_CUR
- pending = 0
- rescue Errno::EINVAL, NameError
- while pending > 0 do
- bytes_read = @io.read([pending, 4096].min).size
- raise UnexpectedEOF if @io.eof?
- pending -= bytes_read
- end
+ header = Gem::Package::TarHeader.from @io
+ rescue ArgumentError => e
+ # Specialize only exceptions from Gem::Package::TarHeader.strict_oct
+ raise e unless e.message.match?(/ is not an octal string$/)
+ raise Gem::Package::TarInvalidError, e.message
end
- @io.read skip # discard trailing zeros
-
- # make sure nobody can use #read, #getc or #rewind anymore
+ return if header.empty?
+ entry = Gem::Package::TarReader::Entry.new header, @io
+ yield entry
entry.close
end
end
- alias each_entry each
+ alias_method :each_entry, :each
##
# NOTE: Do not call #rewind during #each
def rewind
- if @init_pos == 0 then
- raise Gem::Package::NonSeekableIO unless @io.respond_to? :rewind
+ if @init_pos == 0
@io.rewind
else
- raise Gem::Package::NonSeekableIO unless @io.respond_to? :pos=
@io.pos = @init_pos
end
end
+ ##
+ # Seeks through the tar file until it finds the +entry+ with +name+ and
+ # yields it. Rewinds the tar file to the beginning when the block
+ # terminates.
+
+ def seek(name) # :yields: entry
+ found = find do |entry|
+ entry.full_name == name
+ end
+
+ return unless found
+
+ yield found
+ ensure
+ rewind
+ end
end
+require_relative "tar_reader/entry"
diff --git a/lib/rubygems/package/tar_reader/entry.rb b/lib/rubygems/package/tar_reader/entry.rb
index dcc66153d8..f837e86fd6 100644
--- a/lib/rubygems/package/tar_reader/entry.rb
+++ b/lib/rubygems/package/tar_reader/entry.rb
@@ -1,19 +1,44 @@
-#++
-# Copyright (C) 2004 Mauricio Julio Fernández Pradier
+# frozen_string_literal: true
+
+# rubocop:disable Style/AsciiComments
+
+# Copyright (C) 2004 Mauricio Julio Fernández Pradier
# See LICENSE.txt for additional licensing information.
-#--
-require 'rubygems/package'
+# rubocop:enable Style/AsciiComments
+
+##
+# Class for reading entries out of a tar file
class Gem::Package::TarReader::Entry
+ ##
+ # Creates a new tar entry for +header+ that will be read from +io+
+ # If a block is given, the entry is yielded and then closed.
+
+ def self.open(header, io, &block)
+ entry = new header, io
+ return entry unless block_given?
+ begin
+ yield entry
+ ensure
+ entry.close
+ end
+ end
+
+ ##
+ # Header for this tar entry
attr_reader :header
+ ##
+ # Creates a new tar entry for +header+ that will be read from +io+
+
def initialize(header, io)
@closed = false
@header = header
@io = io
@orig_pos = @io.pos
+ @end_pos = @orig_pos + @header.size
@read = 0
end
@@ -21,36 +46,59 @@ class Gem::Package::TarReader::Entry
raise IOError, "closed #{self.class}" if closed?
end
+ ##
+ # Number of bytes read out of the tar entry
+
def bytes_read
@read
end
+ ##
+ # Closes the tar entry
+
def close
+ return if closed?
+ # Seek to the end of the entry if it wasn't fully read
+ seek(0, IO::SEEK_END)
+ # discard trailing zeros
+ skip = (512 - (@header.size % 512)) % 512
+ @io.read(skip)
@closed = true
+ nil
end
+ ##
+ # Is the tar entry closed?
+
def closed?
@closed
end
+ ##
+ # Are we at the end of the tar entry?
+
def eof?
check_closed
@read >= @header.size
end
+ ##
+ # Full name of the tar entry
+
def full_name
- if @header.prefix != "" then
- File.join @header.prefix, @header.name
- else
- @header.name
- end
+ @header.full_name.force_encoding(Encoding::UTF_8)
+ rescue ArgumentError => e
+ raise unless e.message == "string contains null byte"
+ raise Gem::Package::TarInvalidError,
+ "tar is corrupt, name contains null byte"
end
- def getc
- check_closed
+ ##
+ # Read one byte from the tar entry
- return nil if @read >= @header.size
+ def getc
+ return nil if eof?
ret = @io.getc
@read += 1 if ret
@@ -58,42 +106,139 @@ class Gem::Package::TarReader::Entry
ret
end
+ ##
+ # Is this tar entry a directory?
+
def directory?
@header.typeflag == "5"
end
+ ##
+ # Is this tar entry a file?
+
def file?
@header.typeflag == "0"
end
+ ##
+ # Is this tar entry a symlink?
+
+ def symlink?
+ @header.typeflag == "2"
+ end
+
+ ##
+ # The position in the tar entry
+
def pos
check_closed
bytes_read
end
- def read(len = nil)
- check_closed
+ ##
+ # Seek to the position in the tar entry
- return nil if @read >= @header.size
+ def pos=(new_pos)
+ seek(new_pos, IO::SEEK_SET)
+ end
- len ||= @header.size - @read
- max_read = [len, @header.size - @read].min
+ def size
+ @header.size
+ end
+
+ alias_method :length, :size
+
+ ##
+ # Reads +maxlen+ bytes from the tar file entry, or the rest of the entry if nil
+
+ def read(maxlen = nil)
+ if eof?
+ return maxlen.to_i.zero? ? "" : nil
+ end
+
+ max_read = [maxlen, @header.size - @read].compact.min
ret = @io.read max_read
+ if ret.nil?
+ return maxlen ? nil : "" # IO.read returns nil on EOF with len argument
+ end
@read += ret.size
ret
end
- def rewind
+ def readpartial(maxlen, outbuf = "".b)
+ if eof? && maxlen > 0
+ raise EOFError, "end of file reached"
+ end
+
+ max_read = [maxlen, @header.size - @read].min
+
+ @io.readpartial(max_read, outbuf)
+ @read += outbuf.size
+
+ outbuf
+ end
+
+ ##
+ # Seeks to +offset+ bytes into the tar file entry
+ # +whence+ can be IO::SEEK_SET, IO::SEEK_CUR, or IO::SEEK_END
+
+ def seek(offset, whence = IO::SEEK_SET)
check_closed
- raise Gem::Package::NonSeekableIO unless @io.respond_to? :pos=
+ new_pos =
+ case whence
+ when IO::SEEK_SET then @orig_pos + offset
+ when IO::SEEK_CUR then @io.pos + offset
+ when IO::SEEK_END then @end_pos + offset
+ else
+ raise ArgumentError, "invalid whence"
+ end
+
+ if new_pos < @orig_pos
+ new_pos = @orig_pos
+ elsif new_pos > @end_pos
+ new_pos = @end_pos
+ end
- @io.pos = @orig_pos
- @read = 0
+ pending = new_pos - @io.pos
+
+ return 0 if pending == 0
+
+ if @io.respond_to?(:seek)
+ begin
+ # avoid reading if the @io supports seeking
+ @io.seek new_pos, IO::SEEK_SET
+ pending = 0
+ rescue Errno::EINVAL
+ end
+ end
+
+ # if seeking isn't supported or failed
+ # negative seek requires that we rewind and read
+ if pending < 0
+ @io.rewind
+ pending = new_pos
+ end
+
+ while pending > 0 do
+ size_read = @io.read([pending, 4096].min)&.size
+ raise(EOFError, "end of file reached") if size_read.nil?
+ pending -= size_read
+ end
+
+ @read = @io.pos - @orig_pos
+
+ 0
end
-end
+ ##
+ # Rewinds to the beginning of the tar file entry
+ def rewind
+ check_closed
+ seek(0, IO::SEEK_SET)
+ end
+end
diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb
index 6e11440e22..39fed9e2af 100644
--- a/lib/rubygems/package/tar_writer.rb
+++ b/lib/rubygems/package/tar_writer.rb
@@ -1,17 +1,34 @@
-#++
-# Copyright (C) 2004 Mauricio Julio Fernández Pradier
+# frozen_string_literal: true
+
+# rubocop:disable Style/AsciiComments
+
+# Copyright (C) 2004 Mauricio Julio Fernández Pradier
# See LICENSE.txt for additional licensing information.
-#--
-require 'rubygems/package'
+# rubocop:enable Style/AsciiComments
-class Gem::Package::TarWriter
+##
+# Allows writing of tar files
+class Gem::Package::TarWriter
class FileOverflow < StandardError; end
+ ##
+ # IO wrapper that allows writing a limited amount of data
+
class BoundedStream
+ ##
+ # Maximum number of bytes that can be written
+
+ attr_reader :limit
+
+ ##
+ # Number of bytes written
+
+ attr_reader :written
- attr_reader :limit, :written
+ ##
+ # Wraps +io+ and allows up to +limit+ bytes to be written
def initialize(io, limit)
@io = io
@@ -19,29 +36,42 @@ class Gem::Package::TarWriter
@written = 0
end
+ ##
+ # Writes +data+ onto the IO, raising a FileOverflow exception if the
+ # number of bytes will be more than #limit
+
def write(data)
- if data.size + @written > @limit
+ if data.bytesize + @written > @limit
raise FileOverflow, "You tried to feed more data than fits in the file."
end
@io.write data
- @written += data.size
- data.size
+ @written += data.bytesize
+ data.bytesize
end
-
end
+ ##
+ # IO wrapper that provides only #write
+
class RestrictedStream
+ ##
+ # Creates a new RestrictedStream wrapping +io+
def initialize(io)
@io = io
end
+ ##
+ # Writes +data+ onto the IO
+
def write(data)
@io.write data
end
-
end
+ ##
+ # Creates a new TarWriter, yielding it if a block is given
+
def self.new(io)
writer = super
@@ -56,20 +86,26 @@ class Gem::Package::TarWriter
nil
end
+ ##
+ # Creates a new TarWriter that will write to +io+
+
def initialize(io)
@io = io
@closed = false
end
- def add_file(name, mode)
- check_closed
+ ##
+ # Adds file +name+ with permissions +mode+ and mtime +mtime+ (sets
+ # Gem.source_date_epoch if not specified), and yields an IO for
+ # writing the file to
- raise Gem::Package::NonSeekableIO unless @io.respond_to? :pos=
+ def add_file(name, mode, mtime = nil) # :yields: io
+ check_closed
name, prefix = split_name name
init_pos = @io.pos
- @io.write "\0" * 512 # placeholder for the header
+ @io.write Gem::Package::TarHeader::EMPTY_HEADER # placeholder for the header
yield RestrictedStream.new(@io) if block_given?
@@ -81,8 +117,9 @@ class Gem::Package::TarWriter
final_pos = @io.pos
@io.pos = init_pos
- header = Gem::Package::TarHeader.new :name => name, :mode => mode,
- :size => size, :prefix => prefix
+ header = Gem::Package::TarHeader.new name: name, mode: mode,
+ size: size, prefix: prefix,
+ mtime: mtime || Gem.source_date_epoch
@io.write header
@io.pos = final_pos
@@ -90,13 +127,92 @@ class Gem::Package::TarWriter
self
end
- def add_file_simple(name, mode, size)
+ ##
+ # Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing
+ # the file. The +digest_algorithm+ is written to a read-only +name+.sum
+ # file following the given file contents containing the digest name and
+ # hexdigest separated by a tab.
+ #
+ # The created digest object is returned.
+
+ def add_file_digest(name, mode, digest_algorithms) # :yields: io
+ digests = digest_algorithms.map do |digest_algorithm|
+ digest = digest_algorithm.new
+ digest_name =
+ if digest.respond_to? :name
+ digest.name
+ else
+ digest_algorithm.class.name[/::([^:]+)\z/, 1]
+ end
+
+ [digest_name, digest]
+ end
+
+ digests = Hash[*digests.flatten]
+
+ add_file name, mode do |io|
+ Gem::Package::DigestIO.wrap io, digests do |digest_io|
+ yield digest_io
+ end
+ end
+
+ digests
+ end
+
+ ##
+ # Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing
+ # the file. The +signer+ is used to add a digest file using its
+ # digest_algorithm per add_file_digest and a cryptographic signature in
+ # +name+.sig. If the signer has no key only the checksum file is added.
+ #
+ # Returns the digest.
+
+ def add_file_signed(name, mode, signer)
+ digest_algorithms = [
+ signer.digest_algorithm,
+ Gem::Security.create_digest("SHA512"),
+ ].compact.uniq
+
+ digests = add_file_digest name, mode, digest_algorithms do |io|
+ yield io
+ end
+
+ signature_digest = digests.values.compact.find do |digest|
+ digest_name =
+ if digest.respond_to? :name
+ digest.name
+ else
+ digest.class.name[/::([^:]+)\z/, 1]
+ end
+
+ digest_name == signer.digest_name
+ end
+
+ raise "no #{signer.digest_name} in #{digests.values.compact}" unless signature_digest
+
+ if signer.key
+ signature = signer.sign signature_digest.digest
+
+ add_file_simple "#{name}.sig", 0o444, signature.length do |io|
+ io.write signature
+ end
+ end
+
+ digests
+ end
+
+ ##
+ # Add file +name+ with permissions +mode+ +size+ bytes long. Yields an IO
+ # to write the file to.
+
+ def add_file_simple(name, mode, size) # :yields: io
check_closed
name, prefix = split_name name
- header = Gem::Package::TarHeader.new(:name => name, :mode => mode,
- :size => size, :prefix => prefix).to_s
+ header = Gem::Package::TarHeader.new(name: name, mode: mode,
+ size: size, prefix: prefix,
+ mtime: Gem.source_date_epoch).to_s
@io.write header
os = BoundedStream.new @io, size
@@ -112,10 +228,35 @@ class Gem::Package::TarWriter
self
end
+ ##
+ # Adds symlink +name+ with permissions +mode+, linking to +target+.
+
+ def add_symlink(name, target, mode)
+ check_closed
+
+ name, prefix = split_name name
+
+ header = Gem::Package::TarHeader.new(name: name, mode: mode,
+ size: 0, typeflag: "2",
+ linkname: target,
+ prefix: prefix,
+ mtime: Gem.source_date_epoch).to_s
+
+ @io.write header
+
+ self
+ end
+
+ ##
+ # Raises IOError if the TarWriter is closed
+
def check_closed
raise IOError, "closed #{self.class}" if closed?
end
+ ##
+ # Closes the TarWriter
+
def close
check_closed
@@ -125,56 +266,67 @@ class Gem::Package::TarWriter
@closed = true
end
+ ##
+ # Is the TarWriter closed?
+
def closed?
@closed
end
+ ##
+ # Flushes the TarWriter's IO
+
def flush
check_closed
@io.flush if @io.respond_to? :flush
end
+ ##
+ # Creates a new directory in the tar file +name+ with +mode+
+
def mkdir(name, mode)
check_closed
name, prefix = split_name(name)
- header = Gem::Package::TarHeader.new :name => name, :mode => mode,
- :typeflag => "5", :size => 0,
- :prefix => prefix
+ header = Gem::Package::TarHeader.new name: name, mode: mode,
+ typeflag: "5", size: 0,
+ prefix: prefix,
+ mtime: Gem.source_date_epoch
@io.write header
self
end
+ ##
+ # Splits +name+ into a name and prefix that can fit in the TarHeader
+
def split_name(name) # :nodoc:
- raise Gem::Package::TooLongFileName if name.size > 256
-
- if name.size <= 100 then
- prefix = ""
- else
- parts = name.split(/\//)
- newname = parts.pop
- nxt = ""
-
- loop do
- nxt = parts.pop
- break if newname.size + 1 + nxt.size > 100
- newname = nxt + "/" + newname
+ if name.bytesize > 256
+ raise Gem::Package::TooLongFileName.new("File \"#{name}\" has a too long path (should be 256 or less)")
+ end
+
+ prefix = ""
+ if name.bytesize > 100
+ parts = name.split("/", -1) # parts are never empty here
+ name = parts.pop # initially empty for names with a trailing slash ("foo/.../bar/")
+ prefix = parts.join("/") # if empty, then it's impossible to split (parts is empty too)
+ while !parts.empty? && (prefix.bytesize > 155 || name.empty?)
+ name = parts.pop + "/" + name
+ prefix = parts.join("/")
end
- prefix = (parts + [nxt]).join "/"
- name = newname
+ if name.bytesize > 100 || prefix.empty?
+ raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long name (should be 100 or less)")
+ end
- if name.size > 100 or prefix.size > 155 then
- raise Gem::Package::TooLongFileName
+ if prefix.bytesize > 155
+ raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long base path (should be 155 or less)")
end
end
- return name, prefix
+ [name, prefix]
end
-
end
-
diff --git a/lib/rubygems/package_task.rb b/lib/rubygems/package_task.rb
new file mode 100644
index 0000000000..d26411684d
--- /dev/null
+++ b/lib/rubygems/package_task.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+# Copyright (c) 2003, 2004 Jim Weirich, 2009 Eric Hodel
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require_relative "../rubygems"
+require_relative "package"
+require "rake/packagetask"
+
+##
+# Create a package based upon a Gem::Specification. Gem packages, as well as
+# zip files and tar/gzipped packages can be produced by this task.
+#
+# In addition to the Rake targets generated by Rake::PackageTask, a
+# Gem::PackageTask will also generate the following tasks:
+#
+# [<b>"<em>package_dir</em>/<em>name</em>-<em>version</em>.gem"</b>]
+# Create a RubyGems package with the given name and version.
+#
+# Example using a Gem::Specification:
+#
+# require 'rubygems'
+# require 'rubygems/package_task'
+#
+# spec = Gem::Specification.new do |s|
+# s.summary = "Ruby based make-like utility."
+# s.name = 'rake'
+# s.version = PKG_VERSION
+# s.requirements << 'none'
+# s.files = PKG_FILES
+# s.description = <<-EOF
+# Rake is a Make-like program implemented in Ruby. Tasks
+# and dependencies are specified in standard Ruby syntax.
+# EOF
+# end
+#
+# Gem::PackageTask.new(spec) do |pkg|
+# pkg.need_zip = true
+# pkg.need_tar = true
+# end
+
+class Gem::PackageTask < Rake::PackageTask
+ ##
+ # Ruby Gem::Specification containing the metadata for this package. The
+ # name, version and package_files are automatically determined from the
+ # gemspec and don't need to be explicitly provided.
+
+ attr_accessor :gem_spec
+
+ ##
+ # Create a Gem Package task library. Automatically define the gem if a
+ # block is given. If no block is supplied, then #define needs to be called
+ # to define the task.
+
+ def initialize(gem_spec)
+ init gem_spec
+ yield self if block_given?
+ define if block_given?
+ end
+
+ ##
+ # Initialization tasks without the "yield self" or define operations.
+
+ def init(gem)
+ super gem.full_name, :noversion
+ @gem_spec = gem
+ @package_files += gem_spec.files if gem_spec.files
+ @fileutils_output = $stdout
+ end
+
+ ##
+ # Create the Rake tasks and actions specified by this Gem::PackageTask.
+ # (+define+ is automatically called if a block is given to +new+).
+
+ def define
+ super
+
+ gem_file = File.basename gem_spec.cache_file
+ gem_path = File.join package_dir, gem_file
+ gem_dir = File.join package_dir, gem_spec.full_name
+
+ task package: [:gem]
+
+ directory package_dir
+ directory gem_dir
+
+ desc "Build the gem file #{gem_file}"
+ task gem: [gem_path]
+
+ trace = Rake.application.options.trace
+ Gem.configuration.verbose = trace
+
+ file gem_path => [package_dir, gem_dir] + @gem_spec.files do
+ chdir(gem_dir) do
+ when_writing "Creating #{gem_spec.file_name}" do
+ Gem::Package.build gem_spec
+
+ verbose trace do
+ mv gem_file, ".."
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/path_support.rb b/lib/rubygems/path_support.rb
new file mode 100644
index 0000000000..13091e29ba
--- /dev/null
+++ b/lib/rubygems/path_support.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+##
+#
+# Gem::PathSupport facilitates the GEM_HOME and GEM_PATH environment settings
+# to the rest of RubyGems.
+#
+class Gem::PathSupport
+ ##
+ # The default system path for managing Gems.
+ attr_reader :home
+
+ ##
+ # Array of paths to search for Gems.
+ attr_reader :path
+
+ ##
+ # Directory with spec cache
+ attr_reader :spec_cache_dir # :nodoc:
+
+ ##
+ #
+ # Constructor. Takes a single argument which is to be treated like a
+ # hashtable, or defaults to ENV, the system environment.
+ #
+ def initialize(env)
+ @home = normalize_home_dir(env["GEM_HOME"] || Gem.default_dir)
+ @path = split_gem_path env["GEM_PATH"], @home
+
+ @spec_cache_dir = env["GEM_SPEC_CACHE"] || Gem.default_spec_cache_dir
+ end
+
+ private
+
+ def normalize_home_dir(home)
+ if File::ALT_SEPARATOR
+ home = home.gsub(File::ALT_SEPARATOR, File::SEPARATOR)
+ end
+
+ expand(home)
+ end
+
+ ##
+ # Split the Gem search path (as reported by Gem.path).
+
+ def split_gem_path(gpaths, home)
+ # FIX: it should be [home, *path], not [*path, home]
+
+ gem_path = []
+
+ if gpaths
+ gem_path = gpaths.split(Gem.path_separator)
+ # Handle the path_separator being set to a regexp, which will cause
+ # end_with? to error
+ if /#{Gem.path_separator}\z/.match?(gpaths)
+ gem_path += default_path
+ end
+
+ if File::ALT_SEPARATOR
+ gem_path.map! do |this_path|
+ this_path.gsub File::ALT_SEPARATOR, File::SEPARATOR
+ end
+ end
+
+ gem_path << home
+ else
+ gem_path = default_path
+ end
+
+ gem_path.map {|path| expand(path) }.uniq
+ end
+
+ # Return the default Gem path
+ def default_path
+ Gem.default_path + [@home]
+ end
+
+ def expand(path)
+ if File.directory?(path)
+ File.realpath(path)
+ else
+ path
+ end
+ end
+end
diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb
index 3e5b5cde66..367b00e7e1 100644
--- a/lib/rubygems/platform.rb
+++ b/lib/rubygems/platform.rb
@@ -1,28 +1,63 @@
-require 'rubygems'
+# frozen_string_literal: true
##
# Available list of platforms for targeting Gem installations.
+#
+# See `gem help platform` for information on platform matching.
class Gem::Platform
-
@local = nil
- attr_accessor :cpu
+ attr_accessor :cpu, :os, :version
- attr_accessor :os
+ def self.local(refresh: false)
+ return @local if @local && !refresh
+ @local = begin
+ arch = Gem.target_rbconfig["arch"]
+ arch = "#{arch}_60" if /mswin(?:32|64)$/.match?(arch)
+ new(arch)
+ end
+ end
- attr_accessor :version
+ def self.match_platforms?(platform, platforms)
+ platform = Gem::Platform.new(platform) unless platform.is_a?(Gem::Platform)
+ platforms.any? do |local_platform|
+ platform.nil? ||
+ local_platform == platform ||
+ (local_platform != Gem::Platform::RUBY && platform =~ local_platform)
+ end
+ end
+ private_class_method :match_platforms?
- def self.local
- arch = Gem::ConfigMap[:arch]
- arch = "#{arch}_60" if arch =~ /mswin32$/
- @local ||= new(arch)
+ def self.match_spec?(spec)
+ match_gem?(spec.platform, spec.name)
end
- def self.match(platform)
- Gem.platforms.any? do |local_platform|
- platform.nil? or local_platform == platform or
- (local_platform != Gem::Platform::RUBY and local_platform =~ platform)
+ if RUBY_ENGINE == "truffleruby"
+ def self.match_gem?(platform, gem_name)
+ raise "Not a string: #{gem_name.inspect}" unless String === gem_name
+
+ if REUSE_AS_BINARY_ON_TRUFFLERUBY.include?(gem_name)
+ match_platforms?(platform, [Gem::Platform::RUBY, Gem::Platform.local])
+ else
+ match_platforms?(platform, Gem.platforms)
+ end
+ end
+ else
+ def self.match_gem?(platform, gem_name)
+ match_platforms?(platform, Gem.platforms)
+ end
+ end
+
+ def self.sort_priority(platform)
+ platform == Gem::Platform::RUBY ? -1 : 1
+ end
+
+ def self.installable?(spec)
+ if spec.respond_to? :installable_platform?
+ spec.installable_platform?
+ else
+ match_spec? spec
end
end
@@ -30,7 +65,7 @@ class Gem::Platform
case arch
when Gem::Platform::CURRENT then
Gem::Platform.local
- when Gem::Platform::RUBY, nil, '' then
+ when Gem::Platform::RUBY, nil, "" then
Gem::Platform::RUBY
else
super
@@ -42,49 +77,47 @@ class Gem::Platform
when Array then
@cpu, @os, @version = arch
when String then
- arch = arch.split '-'
+ cpu, os = arch.sub(/-+$/, "").split("-", 2)
- if arch.length > 2 and arch.last !~ /\d/ then # reassemble x86-linux-gnu
- extra = arch.pop
- arch.last << "-#{extra}"
+ @cpu = if cpu&.match?(/i\d86/)
+ "x86"
+ else
+ cpu
end
- cpu = arch.shift
-
- @cpu = case cpu
- when /i\d86/ then 'x86'
- else cpu
- end
-
- if arch.length == 2 and arch.last =~ /^\d+(\.\d+)?$/ then # for command-line
- @os, @version = arch
- return
- end
-
- os, = arch
- @cpu, os = nil, cpu if os.nil? # legacy jruby
+ if os.nil?
+ @cpu = nil
+ os = cpu
+ end # legacy jruby
@os, @version = case os
- when /aix(\d+)/ then [ 'aix', $1 ]
- when /cygwin/ then [ 'cygwin', nil ]
- when /darwin(\d+)?/ then [ 'darwin', $1 ]
- when /freebsd(\d+)/ then [ 'freebsd', $1 ]
- when /hpux(\d+)/ then [ 'hpux', $1 ]
- when /^java$/, /^jruby$/ then [ 'java', nil ]
- when /^java([\d.]*)/ then [ 'java', $1 ]
- when /linux/ then [ 'linux', $1 ]
- when /mingw32/ then [ 'mingw32', nil ]
- when /(mswin\d+)(\_(\d+))?/ then
- os, version = $1, $3
- @cpu = 'x86' if @cpu.nil? and os =~ /32$/
+ when /aix-?(\d+)?/ then ["aix", $1]
+ when /cygwin/ then ["cygwin", nil]
+ when /darwin-?(\d+)?/ then ["darwin", $1]
+ when "macruby" then ["macruby", nil]
+ when /^macruby-?(\d+(?:\.\d+)*)?/ then ["macruby", $1]
+ when /freebsd-?(\d+)?/ then ["freebsd", $1]
+ when "java", "jruby" then ["java", nil]
+ when /^java-?(\d+(?:\.\d+)*)?/ then ["java", $1]
+ when /^dalvik-?(\d+)?$/ then ["dalvik", $1]
+ when /^dotnet$/ then ["dotnet", nil]
+ when /^dotnet-?(\d+(?:\.\d+)*)?/ then ["dotnet", $1]
+ when /linux-?(\w+)?/ then ["linux", $1]
+ when /mingw32/ then ["mingw32", nil]
+ when /mingw-?(\w+)?/ then ["mingw", $1]
+ when /(mswin\d+)(?:[_-](\d+))?/ then
+ os = $1
+ version = $2
+ @cpu = "x86" if @cpu.nil? && os.end_with?("32")
[os, version]
- when /netbsdelf/ then [ 'netbsdelf', nil ]
- when /openbsd(\d+\.\d+)/ then [ 'openbsd', $1 ]
- when /solaris(\d+\.\d+)/ then [ 'solaris', $1 ]
+ when /netbsdelf/ then ["netbsdelf", nil]
+ when /openbsd-?(\d+\.\d+)?/ then ["openbsd", $1]
+ when /solaris-?(\d+\.\d+)?/ then ["solaris", $1]
+ when /wasi/ then ["wasi", nil]
# test
- when /^(\w+_platform)(\d+)/ then [ $1, $2 ]
- else [ 'unknown', nil ]
- end
+ when /^(\w+_platform)-?(\d+)?/ then [$1, $2]
+ else ["unknown", nil]
+ end
when Gem::Platform then
@cpu = arch.cpu
@os = arch.os
@@ -94,16 +127,43 @@ class Gem::Platform
end
end
- def inspect
- "#<%s:0x%x @cpu=%p, @os=%p, @version=%p>" % [self.class, object_id, *to_a]
- end
-
def to_a
[@cpu, @os, @version]
end
def to_s
- to_a.compact.join '-'
+ to_a.compact.join(@cpu.nil? ? "" : "-")
+ end
+
+ ##
+ # Deconstructs the platform into an array for pattern matching.
+ # Returns [cpu, os, version].
+ #
+ # Gem::Platform.new("x86_64-linux").deconstruct #=> ["x86_64", "linux", nil]
+ #
+ # This enables array pattern matching:
+ #
+ # case Gem::Platform.new("arm64-darwin-21")
+ # in ["arm64", "darwin", version]
+ # # version => "21"
+ # end
+ alias_method :deconstruct, :to_a
+
+ ##
+ # Deconstructs the platform into a hash for pattern matching.
+ # Returns a hash with keys +:cpu+, +:os+, and +:version+.
+ #
+ # Gem::Platform.new("x86_64-darwin-20").deconstruct_keys(nil)
+ # #=> { cpu: "x86_64", os: "darwin", version: "20" }
+ #
+ # This enables hash pattern matching:
+ #
+ # case Gem::Platform.new("x86_64-linux")
+ # in cpu: "x86_64", os: "linux"
+ # # Matches Linux on x86_64
+ # end
+ def deconstruct_keys(keys)
+ { cpu: @cpu, os: @os, version: @version }
end
##
@@ -111,26 +171,65 @@ class Gem::Platform
# the same CPU, OS and version.
def ==(other)
- self.class === other and
- @cpu == other.cpu and @os == other.os and @version == other.version
+ self.class === other && to_a == other.to_a
+ end
+
+ alias_method :eql?, :==
+
+ def hash # :nodoc:
+ to_a.hash
end
##
# Does +other+ match this platform? Two platforms match if they have the
# same CPU, or either has a CPU of 'universal', they have the same OS, and
- # they have the same version, or either has no version.
+ # they have the same version, or either one has no version
+ #
+ # Additionally, the platform will match if the local CPU is 'arm' and the
+ # other CPU starts with "armv" (for generic 32-bit ARM family support).
+ #
+ # Of note, this method is not commutative. Indeed the OS 'linux' has a
+ # special case: the version is the libc name, yet while "no version" stands
+ # as a wildcard for a binary gem platform (as for other OSes), for the
+ # runtime platform "no version" stands for 'gnu'. To be able to distinguish
+ # these, the method receiver is the gem platform, while the argument is
+ # the runtime platform.
+ #
+ #--
+ # NOTE: Until it can be removed, changes to this method must also be reflected in `bundler/lib/bundler/rubygems_ext.rb`
def ===(other)
return nil unless Gem::Platform === other
+ # universal-mingw32 matches x64-mingw-ucrt
+ return true if (@cpu == "universal" || other.cpu == "universal") &&
+ @os.start_with?("mingw") && other.os.start_with?("mingw")
+
# cpu
- (@cpu == 'universal' or other.cpu == 'universal' or @cpu == other.cpu) and
+ ([nil,"universal"].include?(@cpu) || [nil, "universal"].include?(other.cpu) || @cpu == other.cpu ||
+ (@cpu == "arm" && other.cpu.start_with?("armv"))) &&
+
+ # os
+ @os == other.os &&
+
+ # version
+ (
+ (@os != "linux" && (@version.nil? || other.version.nil?)) ||
+ (@os == "linux" && (normalized_linux_version == other.normalized_linux_version || ["musl#{@version}", "musleabi#{@version}", "musleabihf#{@version}"].include?(other.version))) ||
+ @version == other.version
+ )
+ end
+
+ #--
+ # NOTE: Until it can be removed, changes to this method must also be reflected in `bundler/lib/bundler/rubygems_ext.rb`
+
+ def normalized_linux_version
+ return nil unless @version
- # os
- @os == other.os and
+ without_gnu_nor_abi_modifiers = @version.sub(/\Agnu/, "").sub(/eabi(hf)?\Z/, "")
+ return nil if without_gnu_nor_abi_modifiers.empty?
- # version
- (@version.nil? or other.version.nil? or @version == other.version)
+ without_gnu_nor_abi_modifiers
end
##
@@ -143,16 +242,19 @@ class Gem::Platform
when String then
# This data is from http://gems.rubyforge.org/gems/yaml on 19 Aug 2007
other = case other
- when /^i686-darwin(\d)/ then ['x86', 'darwin', $1]
- when /^i\d86-linux/ then ['x86', 'linux', nil]
- when 'java', 'jruby' then [nil, 'java', nil]
- when /mswin32(\_(\d+))?/ then ['x86', 'mswin32', $2]
- when 'powerpc-darwin' then ['powerpc', 'darwin', nil]
- when /powerpc-darwin(\d)/ then ['powerpc', 'darwin', $1]
- when /sparc-solaris2.8/ then ['sparc', 'solaris', '2.8']
- when /universal-darwin(\d)/ then ['universal', 'darwin', $1]
- else other
- end
+ when /^i686-darwin(\d)/ then ["x86", "darwin", $1]
+ when /^i\d86-linux/ then ["x86", "linux", nil]
+ when "java", "jruby" then [nil, "java", nil]
+ when /^dalvik(\d+)?$/ then [nil, "dalvik", $1]
+ when /dotnet(\-(\d+\.\d+))?/ then ["universal","dotnet", $2]
+ when /mswin32(\_(\d+))?/ then ["x86", "mswin32", $2]
+ when /mswin64(\_(\d+))?/ then ["x64", "mswin64", $2]
+ when "powerpc-darwin" then ["powerpc", "darwin", nil]
+ when /powerpc-darwin(\d)/ then ["powerpc", "darwin", $1]
+ when /sparc-solaris2.8/ then ["sparc", "solaris", "2.8"]
+ when /universal-darwin(\d)/ then ["universal", "darwin", $1]
+ else other
+ end
other = Gem::Platform.new other
else
@@ -163,16 +265,128 @@ class Gem::Platform
end
##
- # A pure-ruby gem that may use Gem::Specification#extensions to build
+ # A pure-Ruby gem that may use Gem::Specification#extensions to build
# binary files.
- RUBY = 'ruby'
+ RUBY = "ruby"
##
- # A platform-specific gem that is built for the packaging ruby's platform.
+ # A platform-specific gem that is built for the packaging Ruby's platform.
# This will be replaced with Gem::Platform::local.
- CURRENT = 'current'
+ CURRENT = "current"
+
+ JAVA = Gem::Platform.new("java") # :nodoc:
+ MSWIN = Gem::Platform.new("mswin32") # :nodoc:
+ MSWIN64 = Gem::Platform.new("mswin64") # :nodoc:
+ MINGW = Gem::Platform.new("x86-mingw32") # :nodoc:
+ X64_MINGW_LEGACY = Gem::Platform.new("x64-mingw32") # :nodoc:
+ X64_MINGW = Gem::Platform.new("x64-mingw-ucrt") # :nodoc:
+ UNIVERSAL_MINGW = Gem::Platform.new("universal-mingw") # :nodoc:
+ WINDOWS = [MSWIN, MSWIN64, UNIVERSAL_MINGW].freeze # :nodoc:
+ X64_LINUX = Gem::Platform.new("x86_64-linux") # :nodoc:
+ X64_LINUX_MUSL = Gem::Platform.new("x86_64-linux-musl") # :nodoc:
+
+ GENERICS = [JAVA, *WINDOWS].freeze # :nodoc:
+ private_constant :GENERICS
+
+ GENERIC_CACHE = GENERICS.each_with_object({}) {|g, h| h[g] = g } # :nodoc:
+ private_constant :GENERIC_CACHE
+
+ class << self
+ ##
+ # Returns the generic platform for the given platform.
+
+ def generic(platform)
+ return Gem::Platform::RUBY if platform.nil? || platform == Gem::Platform::RUBY
+
+ GENERIC_CACHE[platform] ||= begin
+ found = GENERICS.find do |match|
+ platform === match
+ end
+ found || Gem::Platform::RUBY
+ end
+ end
-end
+ ##
+ # Returns the platform specificity match for the given spec platform and user platform.
+
+ def platform_specificity_match(spec_platform, user_platform)
+ return -1 if spec_platform == user_platform
+ return 1_000_000 if spec_platform.nil? || spec_platform == Gem::Platform::RUBY || user_platform == Gem::Platform::RUBY
+
+ os_match(spec_platform, user_platform) +
+ cpu_match(spec_platform, user_platform) * 10 +
+ version_match(spec_platform, user_platform) * 100
+ end
+
+ ##
+ # Sorts and filters the best platform match for the given matching specs and platform.
+
+ def sort_and_filter_best_platform_match(matching, platform)
+ return matching if matching.one?
+
+ exact = matching.select {|spec| spec.platform == platform }
+ return exact if exact.any?
+
+ sorted_matching = sort_best_platform_match(matching, platform)
+ exemplary_spec = sorted_matching.first
+
+ sorted_matching.take_while {|spec| same_specificity?(platform, spec, exemplary_spec) && same_deps?(spec, exemplary_spec) }
+ end
+
+ ##
+ # Sorts the best platform match for the given matching specs and platform.
+
+ def sort_best_platform_match(matching, platform)
+ matching.sort_by.with_index do |spec, i|
+ [
+ platform_specificity_match(spec.platform, platform),
+ i, # for stable sort
+ ]
+ end
+ end
+
+ private
+
+ def same_specificity?(platform, spec, exemplary_spec)
+ platform_specificity_match(spec.platform, platform) == platform_specificity_match(exemplary_spec.platform, platform)
+ end
+
+ def same_deps?(spec, exemplary_spec)
+ spec.required_ruby_version == exemplary_spec.required_ruby_version &&
+ spec.required_rubygems_version == exemplary_spec.required_rubygems_version &&
+ spec.dependencies.sort == exemplary_spec.dependencies.sort
+ end
+
+ def os_match(spec_platform, user_platform)
+ if spec_platform.os == user_platform.os
+ 0
+ else
+ 1
+ end
+ end
+
+ def cpu_match(spec_platform, user_platform)
+ if spec_platform.cpu == user_platform.cpu
+ 0
+ elsif spec_platform.cpu == "arm" && user_platform.cpu.to_s.start_with?("arm")
+ 0
+ elsif spec_platform.cpu.nil? || spec_platform.cpu == "universal"
+ 1
+ else
+ 2
+ end
+ end
+ def version_match(spec_platform, user_platform)
+ if spec_platform.version == user_platform.version
+ 0
+ elsif spec_platform.version.nil?
+ 1
+ else
+ 2
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/psych_tree.rb b/lib/rubygems/psych_tree.rb
new file mode 100644
index 0000000000..8b4c425a33
--- /dev/null
+++ b/lib/rubygems/psych_tree.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gem
+ if defined? ::Psych::Visitors
+ class NoAliasYAMLTree < Psych::Visitors::YAMLTree
+ def self.create
+ new({})
+ end unless respond_to? :create
+
+ def visit_String(str)
+ return super unless str == "=" # or whatever you want
+
+ quote = Psych::Nodes::Scalar::SINGLE_QUOTED
+ @emitter.scalar str, nil, nil, false, true, quote
+ end
+
+ def visit_Hash(o)
+ super(o.compact)
+ end
+
+ # Noop this out so there are no anchors
+ def register(target, obj)
+ end
+
+ # This is ported over from the YAMLTree implementation in Ruby 1.9.3
+ def format_time(time)
+ if time.utc?
+ time.strftime("%Y-%m-%d %H:%M:%S.%9N Z")
+ else
+ time.strftime("%Y-%m-%d %H:%M:%S.%9N %:z")
+ end
+ end
+
+ private :format_time
+ end
+ end
+end
diff --git a/lib/rubygems/query_utils.rb b/lib/rubygems/query_utils.rb
new file mode 100644
index 0000000000..9849370b1a
--- /dev/null
+++ b/lib/rubygems/query_utils.rb
@@ -0,0 +1,349 @@
+# frozen_string_literal: true
+
+require_relative "local_remote_options"
+require_relative "spec_fetcher"
+require_relative "version_option"
+require_relative "text"
+
+module Gem::QueryUtils
+ include Gem::Text
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+
+ def add_query_options
+ add_option("-i", "--[no-]installed",
+ "Check for installed gem") do |value, options|
+ options[:installed] = value
+ end
+
+ add_option("-I", "Equivalent to --no-installed") do |_value, options|
+ options[:installed] = false
+ end
+
+ add_version_option command, "for use with --installed"
+
+ add_option("-d", "--[no-]details",
+ "Display detailed information of gem(s)") do |value, options|
+ options[:details] = value
+ end
+
+ add_option("--[no-]versions",
+ "Display only gem names") do |value, options|
+ options[:versions] = value
+ options[:details] = false unless value
+ end
+
+ add_option("-a", "--all",
+ "Display all gem versions") do |value, options|
+ options[:all] = value
+ end
+
+ add_option("-e", "--exact",
+ "Name of gem(s) to query on matches the",
+ "provided STRING") do |value, options|
+ options[:exact] = value
+ end
+
+ add_option("--[no-]prerelease",
+ "Display prerelease versions") do |value, options|
+ options[:prerelease] = value
+ end
+
+ add_local_remote_options
+ end
+
+ def defaults_str # :nodoc:
+ "--local --no-details --versions --no-installed"
+ end
+
+ def execute
+ gem_names = if args.empty?
+ [options[:name]]
+ else
+ options[:exact] ? args.map {|arg| /\A#{Regexp.escape(arg)}\Z/ } : args.map {|arg| /#{arg}/i }
+ end
+
+ terminate_interaction(check_installed_gems(gem_names)) if check_installed_gems?
+
+ gem_names.each {|n| show_gems(n) }
+ end
+
+ private
+
+ def check_installed_gems(gem_names)
+ exit_code = 0
+
+ if args.empty? && !gem_name?
+ alert_error "You must specify a gem name"
+ exit_code = 4
+ elsif gem_names.count > 1
+ alert_error "You must specify only ONE gem!"
+ exit_code = 4
+ else
+ installed = installed?(gem_names.first, options[:version])
+ installed = !installed unless options[:installed]
+
+ say(installed)
+ exit_code = 1 unless installed
+ end
+
+ exit_code
+ end
+
+ def check_installed_gems?
+ !options[:installed].nil?
+ end
+
+ def gem_name?
+ !options[:name].nil?
+ end
+
+ def prerelease
+ options[:prerelease]
+ end
+
+ def show_prereleases?
+ prerelease.nil? || prerelease
+ end
+
+ def args
+ options[:args].to_a
+ end
+
+ def display_header(type)
+ if (ui.outs.tty? && Gem.configuration.verbose) || both?
+ say
+ say "*** #{type} GEMS ***"
+ say
+ end
+ end
+
+ # Guts of original execute
+ def show_gems(name)
+ show_local_gems(name) if local?
+ show_remote_gems(name) if remote?
+ end
+
+ def show_local_gems(name, req = Gem::Requirement.default)
+ display_header("LOCAL")
+
+ specs = Gem::Specification.find_all do |s|
+ name_matches = name ? s.name =~ name : true
+ version_matches = show_prereleases? || !s.version.prerelease?
+
+ name_matches && version_matches
+ end.uniq(&:full_name)
+
+ spec_tuples = specs.map do |spec|
+ [spec.name_tuple, spec]
+ end
+
+ output_query_results(spec_tuples)
+ end
+
+ def show_remote_gems(name)
+ display_header("REMOTE")
+
+ fetcher = Gem::SpecFetcher.fetcher
+
+ spec_tuples = if name.nil?
+ fetcher.detect(specs_type) { true }
+ else
+ fetcher.detect(specs_type) do |name_tuple|
+ name === name_tuple.name && options[:version].satisfied_by?(name_tuple.version)
+ end
+ end
+
+ output_query_results(spec_tuples)
+ end
+
+ def specs_type
+ if options[:all] || options[:version].specific?
+ if options[:prerelease]
+ :complete
+ else
+ :released
+ end
+ elsif options[:prerelease]
+ :prerelease
+ else
+ :latest
+ end
+ end
+
+ ##
+ # Check if gem +name+ version +version+ is installed.
+
+ def installed?(name, req = Gem::Requirement.default)
+ Gem::Specification.any? {|s| s.name =~ name && req =~ s.version }
+ end
+
+ def output_query_results(spec_tuples)
+ output = []
+ versions = Hash.new {|h,name| h[name] = [] }
+
+ spec_tuples.each do |spec_tuple, source|
+ versions[spec_tuple.name] << [spec_tuple, source]
+ end
+
+ versions = versions.sort_by do |(n,_),_|
+ n.downcase
+ end
+
+ output_versions output, versions
+
+ say output.join(options[:details] ? "\n\n" : "\n")
+ end
+
+ def output_versions(output, versions)
+ versions.each do |_gem_name, matching_tuples|
+ matching_tuples = matching_tuples.sort_by {|n,_| n.version }.reverse
+
+ platforms = Hash.new {|h,version| h[version] = [] }
+
+ matching_tuples.each do |n, _|
+ platforms[n.version] << n.platform if n.platform
+ end
+
+ seen = {}
+
+ matching_tuples.delete_if do |n,_|
+ if seen[n.version]
+ true
+ else
+ seen[n.version] = true
+ false
+ end
+ end
+
+ output << clean_text(make_entry(matching_tuples, platforms))
+ end
+ end
+
+ def entry_details(entry, detail_tuple, specs, platforms)
+ return unless options[:details]
+
+ name_tuple, spec = detail_tuple
+
+ spec = spec.fetch_spec(name_tuple)if spec.respond_to?(:fetch_spec)
+
+ entry << "\n"
+
+ spec_platforms entry, platforms
+ spec_authors entry, spec
+ spec_homepage entry, spec
+ spec_license entry, spec
+ spec_loaded_from entry, spec, specs
+ spec_summary entry, spec
+ end
+
+ def entry_versions(entry, name_tuples, platforms, specs)
+ return unless options[:versions]
+
+ list =
+ if platforms.empty? || options[:details]
+ name_tuples.map(&:version).uniq
+ else
+ platforms.sort.reverse.map do |version, pls|
+ out = version.to_s
+
+ if options[:domain] == :local
+ default = specs.any? do |s|
+ !s.is_a?(Gem::Source) && s.version == version && s.default_gem?
+ end
+ out = "default: #{out}" if default
+ end
+
+ if pls != [Gem::Platform::RUBY]
+ platform_list = [pls.delete(Gem::Platform::RUBY), *pls.sort].compact
+ out = platform_list.unshift(out).join(" ")
+ end
+
+ out
+ end
+ end
+
+ entry << " (#{list.join ", "})"
+ end
+
+ def make_entry(entry_tuples, platforms)
+ detail_tuple = entry_tuples.first
+
+ name_tuples, specs = entry_tuples.flatten.partition do |item|
+ Gem::NameTuple === item
+ end
+
+ entry = [name_tuples.first.name]
+
+ entry_versions(entry, name_tuples, platforms, specs)
+ entry_details(entry, detail_tuple, specs, platforms)
+
+ entry.join
+ end
+
+ def spec_authors(entry, spec)
+ authors = "Author#{spec.authors.length > 1 ? "s" : ""}: ".dup
+ authors << spec.authors.join(", ")
+ entry << format_text(authors, 68, 4)
+ end
+
+ def spec_homepage(entry, spec)
+ return if spec.homepage.nil? || spec.homepage.empty?
+
+ entry << "\n" << format_text("Homepage: #{spec.homepage}", 68, 4)
+ end
+
+ def spec_license(entry, spec)
+ return if spec.license.nil? || spec.license.empty?
+
+ licenses = "License#{spec.licenses.length > 1 ? "s" : ""}: ".dup
+ licenses << spec.licenses.join(", ")
+ entry << "\n" << format_text(licenses, 68, 4)
+ end
+
+ def spec_loaded_from(entry, spec, specs)
+ return unless spec.loaded_from
+
+ if specs.length == 1
+ default = spec.default_gem? ? " (default)" : nil
+ entry << "\n" << " Installed at#{default}: #{spec.base_dir}"
+ else
+ label = "Installed at"
+ specs.each do |s|
+ version = s.version.to_s
+ default = s.default_gem? ? ", default" : ""
+ entry << "\n" << " #{label} (#{version}#{default}): #{s.base_dir}"
+ label = " " * label.length
+ end
+ end
+ end
+
+ def spec_platforms(entry, platforms)
+ non_ruby = platforms.any? do |_, pls|
+ pls.any? {|pl| pl != Gem::Platform::RUBY }
+ end
+
+ return unless non_ruby
+
+ if platforms.length == 1
+ title = platforms.values.length == 1 ? "Platform" : "Platforms"
+ entry << " #{title}: #{platforms.values.sort.join(", ")}\n"
+ else
+ entry << " Platforms:\n"
+
+ sorted_platforms = platforms.sort
+
+ sorted_platforms.each do |version, pls|
+ label = " #{version}: "
+ data = format_text pls.sort.join(", "), 68, label.length
+ data[0, label.length] = label
+ entry << data << "\n"
+ end
+ end
+ end
+
+ def spec_summary(entry, spec)
+ summary = truncate_text(spec.summary, "the summary for #{spec.full_name}")
+ entry << "\n\n" << format_text(summary, 68, 4)
+ end
+end
diff --git a/lib/rubygems/rdoc.rb b/lib/rubygems/rdoc.rb
new file mode 100644
index 0000000000..3524b161b2
--- /dev/null
+++ b/lib/rubygems/rdoc.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require_relative "../rubygems"
+
+begin
+ require "rdoc/rubygems_hook"
+ module Gem
+ ##
+ # Returns whether RDoc defines its own install hooks through a RubyGems
+ # plugin. This and whatever is guarded by it can be removed once no
+ # supported Ruby ships with RDoc older than 6.9.0.
+
+ def self.rdoc_hooks_defined_via_plugin?
+ Gem::Version.new(::RDoc::VERSION) >= Gem::Version.new("6.9.0")
+ end
+
+ if rdoc_hooks_defined_via_plugin?
+ RDoc = ::RDoc::RubyGemsHook
+ else
+ RDoc = ::RDoc::RubygemsHook
+
+ Gem.done_installing(&Gem::RDoc.method(:generation_hook))
+ end
+ end
+rescue LoadError
+end
diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb
index 1570740163..5b83dc6f6f 100644
--- a/lib/rubygems/remote_fetcher.rb
+++ b/lib/rubygems/remote_fetcher.rb
@@ -1,16 +1,18 @@
-require 'net/http'
-require 'stringio'
-require 'time'
-require 'uri'
+# frozen_string_literal: true
-require 'rubygems'
+require_relative "../rubygems"
+require_relative "request"
+require_relative "request/connection_pools"
+require_relative "s3_uri_signer"
+require_relative "uri_formatter"
+require_relative "uri"
+require_relative "user_interaction"
##
# RemoteFetcher handles the details of fetching gems and gem information from
# a remote source.
class Gem::RemoteFetcher
-
include Gem::UserInteraction
##
@@ -18,22 +20,32 @@ class Gem::RemoteFetcher
# that could happen while downloading from the internet.
class FetchError < Gem::Exception
-
##
# The URI which was being accessed when the exception happened.
- attr_accessor :uri
+ attr_accessor :uri, :original_uri
def initialize(message, uri)
- super message
- @uri = uri
+ uri = Gem::Uri.new(uri)
+
+ super uri.redact_credentials_from(message)
+
+ @original_uri = uri.to_s
+ @uri = uri.redacted.to_s
end
def to_s # :nodoc:
"#{super} (#{uri})"
end
+ end
+
+ ##
+ # A FetchError that indicates that the reason for not being
+ # able to fetch data was that the host could not be contacted
+ class UnknownHostError < FetchError
end
+ deprecate_constant(:UnknownHostError)
@fetcher = nil
@@ -41,9 +53,11 @@ class Gem::RemoteFetcher
# Cached RemoteFetcher instance.
def self.fetcher
- @fetcher ||= self.new Gem.configuration[:http_proxy]
+ @fetcher ||= new Gem.configuration[:http_proxy]
end
+ attr_accessor :headers
+
##
# Initialize a remote fetcher using the source URI and possible proxy
# information.
@@ -54,19 +68,41 @@ class Gem::RemoteFetcher
# * nil: respect environment variables (HTTP_PROXY, HTTP_PROXY_USER,
# HTTP_PROXY_PASS)
# * <tt>:no_proxy</tt>: ignore environment variables and _don't_ use a proxy
+ #
+ # +headers+: A set of additional HTTP headers to be sent to the server when
+ # fetching the gem.
+
+ def initialize(proxy = nil, dns = nil, headers = {})
+ require_relative "core_ext/tcpsocket_init" if Gem.configuration.ipv4_fallback_enabled
+ require_relative "vendored_net_http"
+ require_relative "vendor/uri/lib/uri"
- def initialize(proxy)
Socket.do_not_reverse_lookup = true
- @connections = {}
- @requests = Hash.new 0
- @proxy_uri =
- case proxy
- when :no_proxy then nil
- when nil then get_proxy_from_env
- when URI::HTTP then proxy
- else URI.parse(proxy)
- end
+ @proxy = proxy
+ @pools = {}
+ @pool_lock = Thread::Mutex.new
+ @pool_size = 1
+ @cert_files = Gem::Request.get_cert_files
+
+ @headers = headers
+ end
+
+ ##
+ # Given a name and requirement, downloads this gem into cache and returns the
+ # filename. Returns nil if the gem cannot be located.
+ #--
+ # Should probably be integrated with #download below, but that will be a
+ # larger, more encompassing effort. -erikh
+
+ def download_to_cache(dependency)
+ found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dependency
+
+ return if found.empty?
+
+ spec, source = found.max_by {|(s,_)| s.version }
+
+ download spec, source.uri
end
##
@@ -75,270 +111,240 @@ class Gem::RemoteFetcher
# always replaced.
def download(spec, source_uri, install_dir = Gem.dir)
- if File.writable?(install_dir)
- cache_dir = File.join install_dir, 'cache'
- else
- cache_dir = File.join(Gem.user_dir, 'cache')
- end
+ gem_file_name = File.basename spec.cache_file
+
+ install_cache_dir = File.join install_dir, "cache"
+ cache_dir =
+ if Gem.configuration.global_gem_cache
+ Gem.global_gem_cache_path
+ elsif Dir.pwd == install_dir # see fetch_command
+ install_dir
+ elsif File.writable?(install_cache_dir) || (File.writable?(install_dir) && !File.exist?(install_cache_dir))
+ install_cache_dir
+ else
+ File.join Gem.user_dir, "cache"
+ end
- gem_file_name = "#{spec.full_name}.gem"
local_gem_path = File.join cache_dir, gem_file_name
- FileUtils.mkdir_p cache_dir rescue nil unless File.exist? cache_dir
+ require "fileutils"
+ begin
+ FileUtils.mkdir_p cache_dir
+ rescue StandardError
+ nil
+ end unless File.exist? cache_dir
+
+ source_uri = Gem::Uri.new(source_uri)
- source_uri = URI.parse source_uri unless URI::Generic === source_uri
scheme = source_uri.scheme
- # URI.parse gets confused by MS Windows paths with forward slashes.
- scheme = nil if scheme =~ /^[a-z]$/i
+ # Gem::URI.parse gets confused by MS Windows paths with forward slashes.
+ scheme = nil if /^[a-z]$/i.match?(scheme)
+ # REFACTOR: split this up and dispatch on scheme (eg download_http)
+ # REFACTOR: be sure to clean up fake fetcher when you do this... cleaner
case scheme
- when 'http', 'https' then
- unless File.exist? local_gem_path then
+ when "http", "https", "s3" then
+ unless File.exist? local_gem_path
begin
- say "Downloading gem #{gem_file_name}" if
- Gem.configuration.really_verbose
+ verbose "Downloading gem #{gem_file_name}"
remote_gem_path = source_uri + "gems/#{gem_file_name}"
- gem = Gem::RemoteFetcher.fetcher.fetch_path remote_gem_path
- rescue Gem::RemoteFetcher::FetchError
+ cache_update_path remote_gem_path, local_gem_path
+ rescue FetchError
raise if spec.original_platform == spec.platform
alternate_name = "#{spec.original_name}.gem"
- say "Failed, downloading gem #{alternate_name}" if
- Gem.configuration.really_verbose
+ verbose "Failed, downloading gem #{alternate_name}"
remote_gem_path = source_uri + "gems/#{alternate_name}"
- gem = Gem::RemoteFetcher.fetcher.fetch_path remote_gem_path
+ cache_update_path remote_gem_path, local_gem_path
end
+ end
+ when "file" then
+ begin
+ path = source_uri.path
+ path = File.dirname(path) if File.extname(path) == ".gem"
- File.open local_gem_path, 'wb' do |fp|
- fp.write gem
- end
+ remote_gem_path = Gem::Util.correct_for_windows_path(File.join(path, "gems", gem_file_name))
+
+ FileUtils.cp(remote_gem_path, local_gem_path)
+ rescue Errno::EACCES
+ local_gem_path = source_uri.to_s
+ end
+
+ verbose "Using local gem #{local_gem_path}"
+ when nil then
+ source_path = if Gem.win_platform? && source_uri.scheme &&
+ !source_uri.path.include?(":")
+ "#{source_uri.scheme}:#{source_uri.path}"
+ else
+ source_uri.path
end
- when nil, 'file' then # TODO test for local overriding cache
+
+ source_path = Gem::UriFormatter.new(source_path).unescape
+
begin
- FileUtils.cp source_uri.to_s, local_gem_path
+ FileUtils.cp source_path, local_gem_path unless
+ File.identical?(source_path, local_gem_path)
rescue Errno::EACCES
local_gem_path = source_uri.to_s
end
- say "Using local gem #{local_gem_path}" if
- Gem.configuration.really_verbose
+ verbose "Using local gem #{local_gem_path}"
else
- raise Gem::InstallError, "unsupported URI scheme #{source_uri.scheme}"
+ raise ArgumentError, "unsupported URI scheme #{source_uri.scheme}"
end
local_gem_path
end
##
- # Downloads +uri+ and returns it as a String.
+ # File Fetcher. Dispatched by +fetch_path+. Use it instead.
- def fetch_path(uri, mtime = nil, head = false)
- data = open_uri_or_path uri, mtime, head
- data = Gem.gunzip data if data and not head and uri.to_s =~ /gz$/
- data
- rescue FetchError
- raise
- rescue Timeout::Error
- raise FetchError.new('timed out', uri)
- rescue IOError, SocketError, SystemCallError => e
- raise FetchError.new("#{e.class}: #{e}", uri)
+ def fetch_file(uri, *_)
+ Gem.read_binary Gem::Util.correct_for_windows_path uri.path
end
##
- # Returns the size of +uri+ in bytes.
+ # HTTP Fetcher. Dispatched by +fetch_path+. Use it instead.
- def fetch_size(uri) # TODO: phase this out
- response = fetch_path(uri, nil, true)
+ def fetch_http(uri, last_modified = nil, head = false, depth = 0)
+ fetch_type = head ? Gem::Net::HTTP::Head : Gem::Net::HTTP::Get
+ response = request uri, fetch_type, last_modified do |req|
+ headers.each {|k,v| req.add_field(k,v) }
+ end
- response['content-length'].to_i
- end
+ case response
+ when Gem::Net::HTTPOK, Gem::Net::HTTPNotModified then
+ response.uri = uri
+ head ? response : response.body
+ when Gem::Net::HTTPMovedPermanently, Gem::Net::HTTPFound, Gem::Net::HTTPSeeOther,
+ Gem::Net::HTTPTemporaryRedirect then
+ raise FetchError.new("too many redirects", uri) if depth > 10
- def escape(str)
- return unless str
- URI.escape(str)
- end
+ unless location = response["Location"]
+ raise FetchError.new("redirecting but no redirect location was given", uri)
+ end
+ location = Gem::Uri.new location
+
+ if https?(uri) && !https?(location)
+ raise FetchError.new("redirecting to non-https resource: #{location}", uri)
+ end
- def unescape(str)
- return unless str
- URI.unescape(str)
+ fetch_http(location, last_modified, head, depth + 1)
+ else
+ custom_error = response["X-Error-Message"]
+ error_detail = custom_error || response.message
+ raise FetchError.new("Bad response #{error_detail} #{response.code}", uri)
+ end
end
+ alias_method :fetch_https, :fetch_http
+
##
- # Returns an HTTP proxy URI if one is set in the environment variables.
+ # Downloads +uri+ and returns it as a String.
- def get_proxy_from_env
- env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
+ def fetch_path(uri, mtime = nil, head = false)
+ uri = Gem::Uri.new uri
- return nil if env_proxy.nil? or env_proxy.empty?
+ method = {
+ "http" => "fetch_http",
+ "https" => "fetch_http",
+ "s3" => "fetch_s3",
+ "file" => "fetch_file",
+ }.fetch(uri.scheme) { raise ArgumentError, "uri scheme is invalid: #{uri.scheme.inspect}" }
- uri = URI.parse env_proxy
+ data = send method, uri, mtime, head
- if uri and uri.user.nil? and uri.password.nil? then
- # Probably we have http_proxy_* variables?
- uri.user = escape(ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER'])
- uri.password = escape(ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS'])
+ if data && !head && uri.to_s.end_with?(".gz")
+ begin
+ data = Gem::Util.gunzip data
+ rescue Zlib::GzipFile::Error
+ raise FetchError.new("server did not return a valid file", uri)
+ end
end
- uri
- end
-
- ##
- # Normalize the URI by adding "http://" if it is missing.
-
- def normalize_uri(uri)
- (uri =~ /^(https?|ftp|file):/) ? uri : "http://#{uri}"
+ data
+ rescue Gem::Timeout::Error, IOError, SocketError, SystemCallError,
+ *(OpenSSL::SSL::SSLError if Gem::HAVE_OPENSSL) => e
+ raise FetchError.new("#{e.class}: #{e}", uri)
end
- ##
- # Creates or an HTTP connection based on +uri+, or retrieves an existing
- # connection, using a proxy if needed.
-
- def connection_for(uri)
- net_http_args = [uri.host, uri.port]
-
- if @proxy_uri then
- net_http_args += [
- @proxy_uri.host,
- @proxy_uri.port,
- @proxy_uri.user,
- @proxy_uri.password
- ]
- end
-
- connection_id = net_http_args.join ':'
- @connections[connection_id] ||= Net::HTTP.new(*net_http_args)
- connection = @connections[connection_id]
-
- if uri.scheme == 'https' and not connection.started? then
- require 'net/https'
- connection.use_ssl = true
- connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ def fetch_s3(uri, mtime = nil, head = false)
+ begin
+ public_uri = s3_uri_signer(uri, head ? "HEAD" : "GET").sign
+ rescue Gem::S3URISigner::ConfigurationError, Gem::S3URISigner::InstanceProfileError => e
+ raise FetchError.new(e.message, "s3://#{uri.host}")
end
+ fetch_https public_uri, mtime, head
+ end
- connection.start unless connection.started?
-
- connection
+ # we have our own signing code here to avoid a dependency on the aws-sdk gem
+ def s3_uri_signer(uri, method)
+ Gem::S3URISigner.new(uri, method)
end
##
- # Read the data from the (source based) URI, but if it is a file:// URI,
- # read from the filesystem instead.
-
- def open_uri_or_path(uri, last_modified = nil, head = false, depth = 0)
- raise "block is dead" if block_given?
+ # Downloads +uri+ to +path+ if necessary. If no path is given, it just
+ # passes the data.
- return open(get_file_uri_path(uri)) if file_uri? uri
+ def cache_update_path(uri, path = nil, update = true)
+ mtime = begin
+ path && File.stat(path).mtime
+ rescue StandardError
+ nil
+ end
- uri = URI.parse uri unless URI::Generic === uri
- raise ArgumentError, 'uri is not an HTTP URI' unless URI::HTTP === uri
+ data = fetch_path(uri, mtime)
- fetch_type = head ? Net::HTTP::Head : Net::HTTP::Get
- response = request uri, fetch_type, last_modified
-
- case response
- when Net::HTTPOK, Net::HTTPNotModified then
- head ? response : response.body
- when Net::HTTPMovedPermanently, Net::HTTPFound, Net::HTTPSeeOther,
- Net::HTTPTemporaryRedirect then
- raise FetchError.new('too many redirects', uri) if depth > 10
+ if data.nil? # indicates the server returned 304 Not Modified
+ return Gem.read_binary(path)
+ end
- open_uri_or_path(response['Location'], last_modified, head, depth + 1)
- else
- raise FetchError.new("bad response #{response.message} #{response.code}", uri)
+ if update && path
+ Gem.write_binary(path, data)
end
+
+ data
end
##
- # Performs a Net::HTTP request of type +request_class+ on +uri+ returning
- # a Net::HTTP response object. request maintains a table of persistent
+ # Performs a Gem::Net::HTTP request of type +request_class+ on +uri+ returning
+ # a Gem::Net::HTTP response object. request maintains a table of persistent
# connections to reduce connect overhead.
def request(uri, request_class, last_modified = nil)
- request = request_class.new uri.request_uri
-
- unless uri.nil? || uri.user.nil? || uri.user.empty? then
- request.basic_auth uri.user, uri.password
- end
-
- ua = "RubyGems/#{Gem::RubyGemsVersion} #{Gem::Platform.local}"
- ua << " Ruby/#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}"
- ua << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL
- ua << ")"
+ proxy = proxy_for @proxy, uri
+ pool = pools_for(proxy).pool_for uri
- request.add_field 'User-Agent', ua
- request.add_field 'Connection', 'keep-alive'
- request.add_field 'Keep-Alive', '30'
+ request = Gem::Request.new uri, request_class, last_modified, pool
- if last_modified then
- last_modified = last_modified.utc
- request.add_field 'If-Modified-Since', last_modified.rfc2822
+ request.fetch do |req|
+ yield req if block_given?
end
-
- connection = connection_for uri
-
- retried = false
- bad_response = false
-
- begin
- @requests[connection.object_id] += 1
- response = connection.request request
- say "#{request.method} #{response.code} #{response.message}: #{uri}" if
- Gem.configuration.really_verbose
- rescue Net::HTTPBadResponse
- reset connection
-
- raise FetchError.new('too many bad responses', uri) if bad_response
-
- bad_response = true
- retry
- # HACK work around EOFError bug in Net::HTTP
- # NOTE Errno::ECONNABORTED raised a lot on Windows, and make impossible
- # to install gems.
- rescue EOFError, Errno::ECONNABORTED, Errno::ECONNRESET
- requests = @requests[connection.object_id]
- say "connection reset after #{requests} requests, retrying" if
- Gem.configuration.really_verbose
-
- raise FetchError.new('too many connection resets', uri) if retried
-
- reset connection
-
- retried = true
- retry
- end
-
- response
end
- ##
- # Resets HTTP connection +connection+.
-
- def reset(connection)
- @requests.delete connection.object_id
-
- connection.finish
- connection.start
+ def https?(uri)
+ uri.scheme.casecmp("https").zero?
end
- ##
- # Checks if the provided string is a file:// URI.
-
- def file_uri?(uri)
- uri =~ %r{\Afile://}
+ def close_all
+ @pools.each_value(&:close_all)
end
- ##
- # Given a file:// URI, returns its local path.
+ private
- def get_file_uri_path(uri)
- uri.sub(%r{\Afile://}, '')
+ def proxy_for(proxy, uri)
+ Gem::Request.proxy_uri(proxy || Gem::Request.get_proxy_from_env(uri.scheme))
end
+ def pools_for(proxy)
+ @pool_lock.synchronize do
+ @pools[proxy] ||= Gem::Request::ConnectionPools.new proxy, @cert_files, @pool_size
+ end
+ end
end
-
diff --git a/lib/rubygems/request.rb b/lib/rubygems/request.rb
new file mode 100644
index 0000000000..e817ee5704
--- /dev/null
+++ b/lib/rubygems/request.rb
@@ -0,0 +1,299 @@
+# frozen_string_literal: true
+
+require_relative "vendored_net_http"
+require_relative "user_interaction"
+require_relative "uri_formatter"
+
+class Gem::Request
+ extend Gem::UserInteraction
+ include Gem::UserInteraction
+
+ ###
+ # Legacy. This is used in tests.
+ def self.create_with_proxy(uri, request_class, last_modified, proxy) # :nodoc:
+ cert_files = get_cert_files
+ proxy ||= get_proxy_from_env(uri.scheme)
+ pool = ConnectionPools.new proxy_uri(proxy), cert_files
+
+ new(uri, request_class, last_modified, pool.pool_for(uri))
+ end
+
+ def self.proxy_uri(proxy) # :nodoc:
+ require_relative "vendor/uri/lib/uri"
+ case proxy
+ when :no_proxy then nil
+ when Gem::URI::HTTP then proxy
+ else Gem::URI.parse(proxy)
+ end
+ end
+
+ def initialize(uri, request_class, last_modified, pool)
+ @uri = uri
+ @request_class = request_class
+ @last_modified = last_modified
+ @requests = Hash.new(0).compare_by_identity
+ @user_agent = user_agent
+
+ @connection_pool = pool
+ end
+
+ def proxy_uri
+ @connection_pool.proxy_uri
+ end
+
+ def cert_files
+ @connection_pool.cert_files
+ end
+
+ def self.get_cert_files
+ pattern = File.expand_path("./ssl_certs/*/*.pem", __dir__)
+ Dir.glob(pattern)
+ end
+
+ def self.configure_connection_for_https(connection, cert_files)
+ raise Gem::Exception.new("OpenSSL is not available. Install OpenSSL and rebuild Ruby (preferred) or use non-HTTPS sources") unless Gem::HAVE_OPENSSL
+
+ connection.use_ssl = true
+ connection.verify_mode =
+ Gem.configuration.ssl_verify_mode || OpenSSL::SSL::VERIFY_PEER
+ store = OpenSSL::X509::Store.new
+
+ if Gem.configuration.ssl_client_cert
+ pem = File.read Gem.configuration.ssl_client_cert
+ connection.cert = OpenSSL::X509::Certificate.new pem
+ connection.key = OpenSSL::PKey::RSA.new pem
+ end
+
+ store.set_default_paths
+ cert_files.each do |ssl_cert_file|
+ store.add_file ssl_cert_file
+ end
+ if Gem.configuration.ssl_ca_cert
+ if File.directory? Gem.configuration.ssl_ca_cert
+ store.add_path Gem.configuration.ssl_ca_cert
+ else
+ store.add_file Gem.configuration.ssl_ca_cert
+ end
+ end
+ connection.cert_store = store
+
+ connection.verify_callback = proc do |preverify_ok, store_context|
+ verify_certificate store_context unless preverify_ok
+
+ preverify_ok
+ end
+
+ connection
+ end
+
+ def self.verify_certificate(store_context)
+ depth = store_context.error_depth
+ error = store_context.error_string
+ number = store_context.error
+ cert = store_context.current_cert
+
+ ui.alert_error "SSL verification error at depth #{depth}: #{error} (#{number})"
+
+ extra_message = verify_certificate_message number, cert
+
+ ui.alert_error extra_message if extra_message
+ end
+
+ def self.verify_certificate_message(error_number, cert)
+ return unless cert
+ case error_number
+ when OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED then
+ require "time"
+ "Certificate #{cert.subject} expired at #{cert.not_after.iso8601}"
+ when OpenSSL::X509::V_ERR_CERT_NOT_YET_VALID then
+ require "time"
+ "Certificate #{cert.subject} not valid until #{cert.not_before.iso8601}"
+ when OpenSSL::X509::V_ERR_CERT_REJECTED then
+ "Certificate #{cert.subject} is rejected"
+ when OpenSSL::X509::V_ERR_CERT_UNTRUSTED then
+ "Certificate #{cert.subject} is not trusted"
+ when OpenSSL::X509::V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT then
+ "Certificate #{cert.issuer} is not trusted"
+ when OpenSSL::X509::V_ERR_INVALID_CA then
+ "Certificate #{cert.subject} is an invalid CA certificate"
+ when OpenSSL::X509::V_ERR_INVALID_PURPOSE then
+ "Certificate #{cert.subject} has an invalid purpose"
+ when OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN then
+ "Root certificate is not trusted (#{cert.subject})"
+ when OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY then
+ "You must add #{cert.issuer} to your local trusted store"
+ when
+ OpenSSL::X509::V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE then
+ "Cannot verify certificate issued by #{cert.issuer}"
+ end
+ end
+
+ ##
+ # Creates or an HTTP connection based on +uri+, or retrieves an existing
+ # connection, using a proxy if needed.
+
+ def connection_for(uri)
+ @connection_pool.checkout
+ rescue Gem::HAVE_OPENSSL ? OpenSSL::SSL::SSLError : Errno::EHOSTDOWN,
+ Errno::EHOSTDOWN => e
+ raise Gem::RemoteFetcher::FetchError.new(e.message, uri)
+ end
+
+ def fetch
+ request = @request_class.new @uri.request_uri
+
+ unless @uri.nil? || @uri.user.nil? || @uri.user.empty?
+ request.basic_auth Gem::UriFormatter.new(@uri.user).unescape,
+ Gem::UriFormatter.new(@uri.password).unescape
+ end
+
+ request.add_field "User-Agent", @user_agent
+ request.add_field "Connection", "keep-alive"
+ request.add_field "Keep-Alive", "30"
+
+ if @last_modified
+ require "time"
+ request.add_field "If-Modified-Since", @last_modified.httpdate
+ end
+
+ yield request if block_given?
+
+ perform_request request
+ end
+
+ ##
+ # Returns a proxy URI for the given +scheme+ if one is set in the
+ # environment variables.
+
+ def self.get_proxy_from_env(scheme = "http")
+ downcase_scheme = scheme.downcase
+ upcase_scheme = scheme.upcase
+ env_proxy = ENV["#{downcase_scheme}_proxy"] || ENV["#{upcase_scheme}_PROXY"]
+
+ no_env_proxy = env_proxy.nil? || env_proxy.empty?
+
+ if no_env_proxy
+ return ["https", "http"].include?(downcase_scheme) ? :no_proxy : get_proxy_from_env("http")
+ end
+
+ require "uri"
+ uri = Gem::URI(Gem::UriFormatter.new(env_proxy).normalize)
+
+ if uri && uri.user.nil? && uri.password.nil?
+ user = ENV["#{downcase_scheme}_proxy_user"] || ENV["#{upcase_scheme}_PROXY_USER"]
+ password = ENV["#{downcase_scheme}_proxy_pass"] || ENV["#{upcase_scheme}_PROXY_PASS"]
+
+ uri.user = Gem::UriFormatter.new(user).escape
+ uri.password = Gem::UriFormatter.new(password).escape
+ end
+
+ uri
+ end
+
+ def perform_request(request) # :nodoc:
+ connection = connection_for @uri
+
+ retried = false
+ bad_response = false
+
+ begin
+ @requests[connection] += 1
+
+ verbose "#{request.method} #{Gem::Uri.redact(@uri)}"
+
+ file_name = File.basename(@uri.path)
+ # perform download progress reporter only for gems
+ if request.response_body_permitted? && file_name =~ /\.gem$/
+ reporter = ui.download_reporter
+ response = connection.request(request) do |incomplete_response|
+ if Gem::Net::HTTPOK === incomplete_response
+ reporter.fetch(file_name, incomplete_response.content_length)
+ downloaded = 0
+ data = String.new
+
+ incomplete_response.read_body do |segment|
+ data << segment
+ downloaded += segment.length
+ reporter.update(downloaded)
+ end
+ reporter.done
+ if incomplete_response.respond_to? :body=
+ incomplete_response.body = data
+ else
+ incomplete_response.instance_variable_set(:@body, data)
+ end
+ end
+ end
+ else
+ response = connection.request request
+ end
+
+ verbose "#{response.code} #{response.message}"
+ rescue Gem::Net::HTTPBadResponse
+ verbose "bad response"
+
+ reset connection
+
+ raise Gem::RemoteFetcher::FetchError.new("too many bad responses", @uri) if bad_response
+
+ bad_response = true
+ retry
+ rescue Gem::Net::HTTPFatalError
+ verbose "fatal error"
+
+ raise Gem::RemoteFetcher::FetchError.new("fatal error", @uri)
+ # HACK: work around EOFError bug in Gem::Net::HTTP
+ # NOTE Errno::ECONNABORTED raised a lot on Windows, and make impossible
+ # to install gems.
+ rescue EOFError, Gem::Timeout::Error,
+ Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE
+
+ requests = @requests[connection]
+ verbose "connection reset after #{requests} requests, retrying"
+
+ raise Gem::RemoteFetcher::FetchError.new("too many connection resets", @uri) if retried
+
+ reset connection
+
+ retried = true
+ retry
+ end
+
+ response
+ ensure
+ @connection_pool.checkin connection
+ end
+
+ ##
+ # Resets HTTP connection +connection+.
+
+ def reset(connection)
+ @requests.delete connection
+
+ connection.finish
+ connection.start
+ end
+
+ def user_agent
+ ua = "RubyGems/#{Gem::VERSION} #{Gem::Platform.local}".dup
+
+ ruby_version = RUBY_VERSION
+ ruby_version += "dev" if RUBY_PATCHLEVEL == -1
+
+ ua << " Ruby/#{ruby_version} (#{RUBY_RELEASE_DATE}"
+ if RUBY_PATCHLEVEL >= 0
+ ua << " patchlevel #{RUBY_PATCHLEVEL}"
+ else
+ ua << " revision #{RUBY_REVISION}"
+ end
+ ua << ")"
+
+ ua << " #{RUBY_ENGINE}" if RUBY_ENGINE != "ruby"
+
+ ua
+ end
+end
+
+require_relative "request/http_pool"
+require_relative "request/https_pool"
+require_relative "request/connection_pools"
diff --git a/lib/rubygems/request/connection_pools.rb b/lib/rubygems/request/connection_pools.rb
new file mode 100644
index 0000000000..01e7e0629a
--- /dev/null
+++ b/lib/rubygems/request/connection_pools.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+class Gem::Request::ConnectionPools # :nodoc:
+ @client = Gem::Net::HTTP
+
+ class << self
+ attr_accessor :client
+ end
+
+ def initialize(proxy_uri, cert_files, pool_size = 1)
+ @proxy_uri = proxy_uri
+ @cert_files = cert_files
+ @pools = {}
+ @pool_mutex = Thread::Mutex.new
+ @pool_size = pool_size
+ end
+
+ def pool_for(uri)
+ http_args = net_http_args(uri, @proxy_uri)
+ key = http_args + [https?(uri)]
+ @pool_mutex.synchronize do
+ @pools[key] ||=
+ if https? uri
+ Gem::Request::HTTPSPool.new(http_args, @cert_files, @proxy_uri, @pool_size)
+ else
+ Gem::Request::HTTPPool.new(http_args, @cert_files, @proxy_uri, @pool_size)
+ end
+ end
+ end
+
+ def close_all
+ @pools.each_value(&:close_all)
+ end
+
+ private
+
+ ##
+ # Returns list of no_proxy entries (if any) from the environment
+
+ def get_no_proxy_from_env
+ env_no_proxy = ENV["no_proxy"] || ENV["NO_PROXY"]
+
+ return [] if env_no_proxy.nil? || env_no_proxy.empty?
+
+ env_no_proxy.split(/\s*,\s*/)
+ end
+
+ def https?(uri)
+ uri.scheme.casecmp("https").zero?
+ end
+
+ def no_proxy?(host, env_no_proxy)
+ host = host.downcase
+
+ env_no_proxy.any? do |pattern|
+ env_no_proxy_pattern = pattern.downcase.dup
+
+ # Remove dot in front of pattern for wildcard matching
+ env_no_proxy_pattern[0] = "" if env_no_proxy_pattern[0] == "."
+
+ host_tokens = host.split(".")
+ pattern_tokens = env_no_proxy_pattern.split(".")
+
+ intersection = (host_tokens - pattern_tokens) | (pattern_tokens - host_tokens)
+
+ # When we do the split into tokens we miss a dot character, so add it back if we need it
+ missing_dot = intersection.length > 0 ? 1 : 0
+ start = intersection.join(".").size + missing_dot
+
+ no_proxy_host = host[start..-1]
+
+ env_no_proxy_pattern == no_proxy_host
+ end
+ end
+
+ def net_http_args(uri, proxy_uri)
+ hostname = uri.hostname
+ net_http_args = [hostname, uri.port]
+
+ no_proxy = get_no_proxy_from_env
+
+ if proxy_uri && !no_proxy?(hostname, no_proxy)
+ proxy_hostname = proxy_uri.respond_to?(:hostname) ? proxy_uri.hostname : proxy_uri.host
+ net_http_args + [
+ proxy_hostname,
+ proxy_uri.port,
+ Gem::UriFormatter.new(proxy_uri.user).unescape,
+ Gem::UriFormatter.new(proxy_uri.password).unescape,
+ ]
+ elsif no_proxy? hostname, no_proxy
+ net_http_args + [nil, nil]
+ else
+ net_http_args
+ end
+ end
+end
diff --git a/lib/rubygems/request/http_pool.rb b/lib/rubygems/request/http_pool.rb
new file mode 100644
index 0000000000..468502ca6b
--- /dev/null
+++ b/lib/rubygems/request/http_pool.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+##
+# A connection "pool" that only manages one connection for now. Provides
+# thread safe `checkout` and `checkin` methods. The pool consists of one
+# connection that corresponds to `http_args`. This class is private, do not
+# use it.
+
+class Gem::Request::HTTPPool # :nodoc:
+ attr_reader :cert_files, :proxy_uri
+
+ def initialize(http_args, cert_files, proxy_uri, pool_size)
+ @http_args = http_args
+ @cert_files = cert_files
+ @proxy_uri = proxy_uri
+ @pool_size = pool_size
+
+ @queue = Thread::SizedQueue.new @pool_size
+ setup_queue
+ end
+
+ def checkout
+ @queue.pop || make_connection
+ end
+
+ def checkin(connection)
+ @queue.push connection
+ end
+
+ def close_all
+ until @queue.empty?
+ if (connection = @queue.pop(true)) && connection.started?
+ connection.finish
+ end
+ end
+
+ setup_queue
+ end
+
+ private
+
+ def make_connection
+ setup_connection Gem::Request::ConnectionPools.client.new(*@http_args)
+ end
+
+ def setup_connection(connection)
+ connection.start
+ connection
+ end
+
+ def setup_queue
+ @pool_size.times { @queue.push(nil) }
+ end
+end
diff --git a/lib/rubygems/request/https_pool.rb b/lib/rubygems/request/https_pool.rb
new file mode 100644
index 0000000000..cb1d4b59b6
--- /dev/null
+++ b/lib/rubygems/request/https_pool.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class Gem::Request::HTTPSPool < Gem::Request::HTTPPool # :nodoc:
+ private
+
+ def setup_connection(connection)
+ Gem::Request.configure_connection_for_https(connection, @cert_files)
+ super
+ end
+end
diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb
new file mode 100644
index 0000000000..eb8b4658f3
--- /dev/null
+++ b/lib/rubygems/request_set.rb
@@ -0,0 +1,514 @@
+# frozen_string_literal: true
+
+require_relative "vendored_tsort"
+
+##
+# A RequestSet groups a request to activate a set of dependencies.
+#
+# nokogiri = Gem::Dependency.new 'nokogiri', '~> 1.6'
+# pg = Gem::Dependency.new 'pg', '~> 0.14'
+#
+# set = Gem::RequestSet.new nokogiri, pg
+#
+# requests = set.resolve
+#
+# p requests.map { |r| r.full_name }
+# #=> ["nokogiri-1.6.0", "mini_portile-0.5.1", "pg-0.17.0"]
+
+class Gem::RequestSet
+ include Gem::TSort
+
+ ##
+ # Array of gems to install even if already installed
+
+ attr_accessor :always_install
+
+ attr_reader :dependencies
+
+ attr_accessor :development
+
+ ##
+ # Errors fetching gems during resolution.
+
+ attr_reader :errors
+
+ ##
+ # Set to true if you want to install only direct development dependencies.
+
+ attr_accessor :development_shallow
+
+ ##
+ # The set of git gems imported via load_gemdeps.
+
+ attr_reader :git_set # :nodoc:
+
+ ##
+ # When true, dependency resolution is not performed, only the requested gems
+ # are installed.
+
+ attr_accessor :ignore_dependencies
+
+ attr_reader :install_dir # :nodoc:
+
+ ##
+ # If true, allow dependencies to match prerelease gems.
+
+ attr_accessor :prerelease
+
+ ##
+ # When false no remote sets are used for resolving gems.
+
+ attr_accessor :remote
+
+ attr_reader :resolver # :nodoc:
+
+ ##
+ # Sets used for resolution
+
+ attr_reader :sets # :nodoc:
+
+ ##
+ # Treat missing dependencies as silent errors
+
+ attr_accessor :soft_missing
+
+ ##
+ # The set of vendor gems imported via load_gemdeps.
+
+ attr_reader :vendor_set # :nodoc:
+
+ ##
+ # The set of source gems imported via load_gemdeps.
+
+ attr_reader :source_set
+
+ ##
+ # Creates a RequestSet for a list of Gem::Dependency objects, +deps+. You
+ # can then #resolve and #install the resolved list of dependencies.
+ #
+ # nokogiri = Gem::Dependency.new 'nokogiri', '~> 1.6'
+ # pg = Gem::Dependency.new 'pg', '~> 0.14'
+ #
+ # set = Gem::RequestSet.new nokogiri, pg
+
+ def initialize(*deps)
+ @dependencies = deps
+
+ @always_install = []
+ @conservative = false
+ @dependency_names = {}
+ @development = false
+ @development_shallow = false
+ @errors = []
+ @git_set = nil
+ @ignore_dependencies = false
+ @install_dir = Gem.dir
+ @prerelease = false
+ @remote = true
+ @requests = []
+ @sets = []
+ @soft_missing = false
+ @sorted_requests = nil
+ @specs = nil
+ @vendor_set = nil
+ @source_set = nil
+
+ yield self if block_given?
+ end
+
+ ##
+ # Declare that a gem of name +name+ with +reqs+ requirements is needed.
+
+ def gem(name, *reqs)
+ if dep = @dependency_names[name]
+ dep.requirement.concat reqs
+ else
+ dep = Gem::Dependency.new name, *reqs
+ @dependency_names[name] = dep
+ @dependencies << dep
+ end
+ end
+
+ ##
+ # Add +deps+ Gem::Dependency objects to the set.
+
+ def import(deps)
+ @dependencies.concat deps
+ end
+
+ ##
+ # Installs gems for this RequestSet using the Gem::Installer +options+.
+ #
+ # If a +block+ is given an activation +request+ and +installer+ are yielded.
+ # The +installer+ will be +nil+ if a gem matching the request was already
+ # installed.
+
+ def install(options, &block) # :yields: request, installer
+ if dir = options[:install_dir]
+ requests = install_into dir, false, options, &block
+ return requests
+ end
+
+ @prerelease = options[:prerelease]
+
+ requests = []
+ download_queue = Thread::Queue.new
+
+ # Create a thread-safe list of gems to download
+ sorted_requests.each do |req|
+ download_queue << req
+ end
+
+ # Create N threads in a pool, have them download all the gems
+ threads = Array.new(Gem.configuration.concurrent_downloads) do
+ # When a thread pops this item, it knows to stop running. The symbol
+ # is queued here so that there will be one symbol per thread.
+ download_queue << :stop
+
+ Thread.new do
+ # The pop method will block waiting for items, so the only way
+ # to stop a thread from running is to provide a final item that
+ # means the thread should stop.
+ while req = download_queue.pop
+ break if req == :stop
+ req.spec.download options unless req.installed?
+ end
+ end
+ end
+
+ # Wait for all the downloads to finish before continuing
+ threads.each(&:value)
+
+ # Install requested gems after they have been downloaded
+ sorted_requests.each do |req|
+ if req.installed? && @always_install.none? {|spec| spec == req.spec.spec }
+ req.spec.spec.build_extensions unless options[:build_extension] == false
+ yield req, nil if block_given?
+ next
+ end
+
+ spec =
+ begin
+ req.spec.install options do |installer|
+ yield req, installer if block_given?
+ end
+ rescue Gem::RuntimeRequirementNotMetError => e
+ suggestion = "There are no versions of #{req.request} compatible with your Ruby & RubyGems"
+ suggestion += ". Maybe try installing an older version of the gem you're looking for?" unless @always_install.include?(req.spec.spec)
+ e.suggestion = suggestion
+ raise
+ end
+
+ requests << spec
+ end
+
+ return requests if options[:gemdeps]
+
+ install_hooks requests, options
+
+ requests
+ end
+
+ ##
+ # Installs from the gem dependencies files in the +:gemdeps+ option in
+ # +options+, yielding to the +block+ as in #install.
+ #
+ # If +:without_groups+ is given in the +options+, those groups in the gem
+ # dependencies file are not used. See Gem::Installer for other +options+.
+
+ def install_from_gemdeps(options, &block)
+ gemdeps = options[:gemdeps]
+
+ @install_dir = options[:install_dir] || Gem.dir
+ @prerelease = options[:prerelease]
+ @remote = options[:domain] != :local
+ @conservative = true if options[:conservative]
+
+ gem_deps_api = load_gemdeps gemdeps, options[:without_groups], true
+
+ resolve
+
+ if options[:explain]
+ puts "Gems to install:"
+
+ sorted_requests.each do |spec|
+ puts " #{spec.full_name}"
+ end
+ else
+ installed = install options, &block
+
+ if options.fetch :lock, true
+ lockfile =
+ Gem::RequestSet::Lockfile.build self, gemdeps, gem_deps_api.dependencies
+ lockfile.write
+ end
+
+ installed
+ end
+ end
+
+ def install_into(dir, force = true, options = {})
+ gem_home = ENV["GEM_HOME"]
+ ENV["GEM_HOME"] = dir
+
+ existing = force ? [] : specs_in(dir)
+ existing.delete_if {|s| @always_install.include? s }
+
+ dir = File.expand_path dir
+
+ installed = []
+
+ options[:development] = false
+ options[:install_dir] = dir
+ options[:only_install_dir] = true
+ @prerelease = options[:prerelease]
+
+ sorted_requests.each do |request|
+ spec = request.spec
+
+ if existing.find {|s| s.full_name == spec.full_name }
+ yield request, nil if block_given?
+ next
+ end
+
+ spec.install options do |installer|
+ yield request, installer if block_given?
+ end
+
+ installed << request
+ end
+
+ install_hooks installed, options
+
+ installed
+ ensure
+ ENV["GEM_HOME"] = gem_home
+ end
+
+ ##
+ # Call hooks on installed gems
+
+ def install_hooks(requests, options)
+ specs = requests.map do |request|
+ case request
+ when Gem::Resolver::ActivationRequest then
+ request.spec.spec
+ else
+ request
+ end
+ end
+
+ require_relative "dependency_installer"
+ inst = Gem::DependencyInstaller.new options
+ inst.installed_gems.replace specs
+
+ Gem.done_installing_hooks.each do |hook|
+ hook.call inst, specs
+ end unless Gem.done_installing_hooks.empty?
+ end
+
+ ##
+ # Load a dependency management file.
+
+ def load_gemdeps(path, without_groups = [], installing = false)
+ @git_set = Gem::Resolver::GitSet.new
+ @vendor_set = Gem::Resolver::VendorSet.new
+ @source_set = Gem::Resolver::SourceSet.new
+
+ @git_set.root_dir = @install_dir
+
+ lock_file = "#{File.expand_path(path)}.lock"
+ if File.exist?(lock_file)
+ load_lockfile lock_file
+ end
+
+ gf = Gem::RequestSet::GemDependencyAPI.new self, path
+ gf.installing = installing
+ gf.without_groups = without_groups if without_groups
+ gf.load
+ end
+
+ def load_lockfile(lock_file) # :nodoc:
+ require "bundler"
+ require "bundler/lockfile_parser"
+
+ # Bundler::Source::Path resolves relative `remote:` paths against
+ # Bundler.root, which raises when there is no Gemfile in the working
+ # directory. Anchor it to the lockfile's directory so PATH sections in a
+ # `gem install -g` lockfile can be parsed without a Bundler environment.
+ previous_root = Bundler.instance_variable_get(:@root)
+ Bundler.instance_variable_set(:@root, Pathname.new(File.expand_path(File.dirname(lock_file))))
+
+ parser = Bundler::LockfileParser.new(File.read(lock_file), lockfile_path: lock_file)
+
+ parser.specs.group_by(&:source).each do |source, specs|
+ case source
+ when Bundler::Source::Rubygems
+ remotes = source.remotes.map {|remote| Gem::Source.new(remote.to_s) }
+ remotes << Gem::Source.new(Gem::DEFAULT_HOST) if remotes.empty?
+ lock_set = Gem::Resolver::LockSet.new(remotes)
+ specs.each do |spec|
+ added = lock_set.add(spec.name, spec.version.to_s, spec.platform)
+ spec.dependencies.each do |dep|
+ added.each {|s| s.add_dependency dep }
+ end
+ end
+ @sets << lock_set
+ when Bundler::Source::Git
+ git_set = Gem::Resolver::GitSet.new
+ git_set.root_dir = @install_dir
+ specs.each do |spec|
+ git_spec = git_set.add_git_spec(
+ spec.name,
+ spec.version.to_s,
+ source.uri.to_s,
+ source.revision,
+ source.submodules || false
+ )
+ spec.dependencies.each {|dep| git_spec.add_dependency dep }
+ end
+ @sets << git_set
+ when Bundler::Source::Path
+ vendor_set = Gem::Resolver::VendorSet.new
+ specs.each do |spec|
+ loaded = vendor_set.add_vendor_gem(spec.name, source.path.to_s)
+ spec.dependencies.each {|dep| loaded.dependencies << dep }
+ end
+ @sets << vendor_set
+ end
+ end
+
+ parser.dependencies.each_value do |dep|
+ gem dep.name, *dep.requirement.as_list
+ end
+ ensure
+ Bundler.instance_variable_set(:@root, previous_root) if defined?(previous_root)
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[RequestSet:", "]" do
+ q.breakable
+
+ if @remote
+ q.text "remote"
+ q.breakable
+ end
+
+ if @prerelease
+ q.text "prerelease"
+ q.breakable
+ end
+
+ if @development_shallow
+ q.text "shallow development"
+ q.breakable
+ elsif @development
+ q.text "development"
+ q.breakable
+ end
+
+ if @soft_missing
+ q.text "soft missing"
+ end
+
+ q.group 2, "[dependencies:", "]" do
+ q.breakable
+ @dependencies.map do |dep|
+ q.text dep.to_s
+ q.breakable
+ end
+ end
+
+ q.breakable
+ q.text "sets:"
+
+ q.breakable
+ q.pp @sets.map(&:class)
+ end
+ end
+
+ ##
+ # Resolve the requested dependencies and return an Array of Specification
+ # objects to be activated.
+
+ def resolve(set = Gem::Resolver::BestSet.new)
+ @sets << set
+ @sets << @git_set
+ @sets << @vendor_set
+ @sets << @source_set
+
+ set = Gem::Resolver.compose_sets(*@sets)
+ set.remote = @remote
+ set.prerelease = @prerelease
+
+ resolver = Gem::Resolver.new @dependencies, set
+ resolver.development = @development
+ resolver.development_shallow = @development_shallow
+ resolver.ignore_dependencies = @ignore_dependencies
+ resolver.soft_missing = @soft_missing
+
+ if @conservative
+ installed_gems = {}
+ Gem::Specification.find_all do |spec|
+ (installed_gems[spec.name] ||= []) << spec
+ end
+ resolver.skip_gems = installed_gems
+ end
+
+ @resolver = resolver
+
+ @requests = resolver.resolve
+
+ @errors = set.errors
+
+ @requests
+ end
+
+ ##
+ # Resolve the requested dependencies against the gems available via Gem.path
+ # and return an Array of Specification objects to be activated.
+
+ def resolve_current
+ resolve Gem::Resolver::CurrentSet.new
+ end
+
+ def sorted_requests
+ @sorted_requests ||= strongly_connected_components.flatten
+ end
+
+ def specs
+ @specs ||= @requests.map(&:full_spec)
+ end
+
+ def specs_in(dir)
+ Gem::Util.glob_files_in_dir("*.gemspec", File.join(dir, "specifications")).map do |g|
+ Gem::Specification.load g
+ end
+ end
+
+ def tsort_each_node(&block) # :nodoc:
+ @requests.each(&block)
+ end
+
+ def tsort_each_child(node) # :nodoc:
+ node.spec.dependencies.each do |dep|
+ next if dep.type == :development && !@development
+
+ match = @requests.find do |r|
+ dep.match?(r.spec.name, r.spec.version, r.spec.is_a?(Gem::Resolver::InstalledSpecification) || @prerelease)
+ end
+
+ unless match
+ next if dep.type == :development && @development_shallow
+ next if @soft_missing
+ raise Gem::DependencyError,
+ "Unresolved dependency found during sorting - #{dep} (requested by #{node.spec.full_name})"
+ end
+
+ yield match
+ end
+ end
+end
+
+require_relative "request_set/gem_dependency_api"
+require_relative "request_set/lockfile"
diff --git a/lib/rubygems/request_set/gem_dependency_api.rb b/lib/rubygems/request_set/gem_dependency_api.rb
new file mode 100644
index 0000000000..99d96f928b
--- /dev/null
+++ b/lib/rubygems/request_set/gem_dependency_api.rb
@@ -0,0 +1,841 @@
+# frozen_string_literal: true
+
+##
+# A semi-compatible DSL for the Bundler Gemfile and Isolate gem dependencies
+# files.
+#
+# To work with both the Bundler Gemfile and Isolate formats this
+# implementation takes some liberties to allow compatibility with each, most
+# notably in #source.
+#
+# A basic gem dependencies file will look like the following:
+#
+# source 'https://rubygems.org'
+#
+# gem 'rails', '3.2.14a
+# gem 'devise', '~> 2.1', '>= 2.1.3'
+# gem 'cancan'
+# gem 'airbrake'
+# gem 'pg'
+#
+# RubyGems recommends saving this as gem.deps.rb over Gemfile or Isolate.
+#
+# To install the gems in this Gemfile use `gem install -g` to install it and
+# create a lockfile. The lockfile will ensure that when you make changes to
+# your gem dependencies file a minimum amount of change is made to the
+# dependencies of your gems.
+#
+# RubyGems can activate all the gems in your dependencies file at startup
+# using the RUBYGEMS_GEMDEPS environment variable or through Gem.use_gemdeps.
+# See Gem.use_gemdeps for details and warnings.
+#
+# See `gem help install` and `gem help gem_dependencies` for further details.
+
+class Gem::RequestSet::GemDependencyAPI
+ ENGINE_MAP = { # :nodoc:
+ jruby: %w[jruby],
+ jruby_18: %w[jruby],
+ jruby_19: %w[jruby],
+ maglev: %w[maglev],
+ mri: %w[ruby],
+ mri_18: %w[ruby],
+ mri_19: %w[ruby],
+ mri_20: %w[ruby],
+ mri_21: %w[ruby],
+ rbx: %w[rbx],
+ truffleruby: %w[truffleruby],
+ ruby: %w[ruby rbx maglev truffleruby],
+ ruby_18: %w[ruby rbx maglev truffleruby],
+ ruby_19: %w[ruby rbx maglev truffleruby],
+ ruby_20: %w[ruby rbx maglev truffleruby],
+ ruby_21: %w[ruby rbx maglev truffleruby],
+ }.freeze
+
+ mswin = Gem::Platform.new "x86-mswin32"
+ mswin64 = Gem::Platform.new "x64-mswin64"
+ x86_mingw = Gem::Platform.new "x86-mingw32"
+ x64_mingw = Gem::Platform.new "x64-mingw32"
+
+ PLATFORM_MAP = { # :nodoc:
+ jruby: Gem::Platform::RUBY,
+ jruby_18: Gem::Platform::RUBY,
+ jruby_19: Gem::Platform::RUBY,
+ maglev: Gem::Platform::RUBY,
+ mingw: x86_mingw,
+ mingw_18: x86_mingw,
+ mingw_19: x86_mingw,
+ mingw_20: x86_mingw,
+ mingw_21: x86_mingw,
+ mri: Gem::Platform::RUBY,
+ mri_18: Gem::Platform::RUBY,
+ mri_19: Gem::Platform::RUBY,
+ mri_20: Gem::Platform::RUBY,
+ mri_21: Gem::Platform::RUBY,
+ mswin: mswin,
+ mswin_18: mswin,
+ mswin_19: mswin,
+ mswin_20: mswin,
+ mswin_21: mswin,
+ mswin64: mswin64,
+ mswin64_19: mswin64,
+ mswin64_20: mswin64,
+ mswin64_21: mswin64,
+ rbx: Gem::Platform::RUBY,
+ ruby: Gem::Platform::RUBY,
+ ruby_18: Gem::Platform::RUBY,
+ ruby_19: Gem::Platform::RUBY,
+ ruby_20: Gem::Platform::RUBY,
+ ruby_21: Gem::Platform::RUBY,
+ truffleruby: Gem::Platform::RUBY,
+ x64_mingw: x64_mingw,
+ x64_mingw_20: x64_mingw,
+ x64_mingw_21: x64_mingw,
+ }.freeze
+
+ gt_eq_0 = Gem::Requirement.new ">= 0"
+ tilde_gt_1_8_0 = Gem::Requirement.new "~> 1.8.0"
+ tilde_gt_1_9_0 = Gem::Requirement.new "~> 1.9.0"
+ tilde_gt_2_0_0 = Gem::Requirement.new "~> 2.0.0"
+ tilde_gt_2_1_0 = Gem::Requirement.new "~> 2.1.0"
+
+ VERSION_MAP = { # :nodoc:
+ jruby: gt_eq_0,
+ jruby_18: tilde_gt_1_8_0,
+ jruby_19: tilde_gt_1_9_0,
+ maglev: gt_eq_0,
+ mingw: gt_eq_0,
+ mingw_18: tilde_gt_1_8_0,
+ mingw_19: tilde_gt_1_9_0,
+ mingw_20: tilde_gt_2_0_0,
+ mingw_21: tilde_gt_2_1_0,
+ mri: gt_eq_0,
+ mri_18: tilde_gt_1_8_0,
+ mri_19: tilde_gt_1_9_0,
+ mri_20: tilde_gt_2_0_0,
+ mri_21: tilde_gt_2_1_0,
+ mswin: gt_eq_0,
+ mswin_18: tilde_gt_1_8_0,
+ mswin_19: tilde_gt_1_9_0,
+ mswin_20: tilde_gt_2_0_0,
+ mswin_21: tilde_gt_2_1_0,
+ mswin64: gt_eq_0,
+ mswin64_19: tilde_gt_1_9_0,
+ mswin64_20: tilde_gt_2_0_0,
+ mswin64_21: tilde_gt_2_1_0,
+ rbx: gt_eq_0,
+ ruby: gt_eq_0,
+ ruby_18: tilde_gt_1_8_0,
+ ruby_19: tilde_gt_1_9_0,
+ ruby_20: tilde_gt_2_0_0,
+ ruby_21: tilde_gt_2_1_0,
+ truffleruby: gt_eq_0,
+ x64_mingw: gt_eq_0,
+ x64_mingw_20: tilde_gt_2_0_0,
+ x64_mingw_21: tilde_gt_2_1_0,
+ }.freeze
+
+ WINDOWS = { # :nodoc:
+ mingw: :only,
+ mingw_18: :only,
+ mingw_19: :only,
+ mingw_20: :only,
+ mingw_21: :only,
+ mri: :never,
+ mri_18: :never,
+ mri_19: :never,
+ mri_20: :never,
+ mri_21: :never,
+ mswin: :only,
+ mswin_18: :only,
+ mswin_19: :only,
+ mswin_20: :only,
+ mswin_21: :only,
+ mswin64: :only,
+ mswin64_19: :only,
+ mswin64_20: :only,
+ mswin64_21: :only,
+ rbx: :never,
+ ruby: :never,
+ ruby_18: :never,
+ ruby_19: :never,
+ ruby_20: :never,
+ ruby_21: :never,
+ x64_mingw: :only,
+ x64_mingw_20: :only,
+ x64_mingw_21: :only,
+ }.freeze
+
+ ##
+ # The gems required by #gem statements in the gem.deps.rb file
+
+ attr_reader :dependencies
+
+ ##
+ # A set of gems that are loaded via the +:git+ option to #gem
+
+ attr_reader :git_set # :nodoc:
+
+ ##
+ # A Hash containing gem names and files to require from those gems.
+
+ attr_reader :requires
+
+ ##
+ # A set of gems that are loaded via the +:path+ option to #gem
+
+ attr_reader :vendor_set # :nodoc:
+
+ ##
+ # The groups of gems to exclude from installation
+
+ attr_accessor :without_groups # :nodoc:
+
+ ##
+ # Creates a new GemDependencyAPI that will add dependencies to the
+ # Gem::RequestSet +set+ based on the dependency API description in +path+.
+
+ def initialize(set, path)
+ @set = set
+ @path = path
+
+ @current_groups = nil
+ @current_platforms = nil
+ @current_repository = nil
+ @dependencies = {}
+ @default_sources = true
+ @git_set = @set.git_set
+ @git_sources = {}
+ @installing = false
+ @requires = Hash.new {|h, name| h[name] = [] }
+ @vendor_set = @set.vendor_set
+ @source_set = @set.source_set
+ @gem_sources = {}
+ @without_groups = []
+
+ git_source :github do |repo_name|
+ repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include? "/"
+
+ "https://github.com/#{repo_name}.git"
+ end
+
+ git_source :bitbucket do |repo_name|
+ repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include? "/"
+
+ user, = repo_name.split "/", 2
+
+ "https://#{user}@bitbucket.org/#{repo_name}.git"
+ end
+ end
+
+ ##
+ # Adds +dependencies+ to the request set if any of the +groups+ are allowed.
+ # This is used for gemspec dependencies.
+
+ def add_dependencies(groups, dependencies) # :nodoc:
+ return unless (groups & @without_groups).empty?
+
+ dependencies.each do |dep|
+ @set.gem dep.name, *dep.requirement.as_list
+ end
+ end
+
+ private :add_dependencies
+
+ ##
+ # Finds a gemspec with the given +name+ that lives at +path+.
+
+ def find_gemspec(name, path) # :nodoc:
+ glob = File.join path, "#{name}.gemspec"
+
+ spec_files = Dir[glob]
+
+ case spec_files.length
+ when 1 then
+ spec_file = spec_files.first
+
+ spec = Gem::Specification.load spec_file
+
+ return spec if spec
+
+ raise ArgumentError, "invalid gemspec #{spec_file}"
+ when 0 then
+ raise ArgumentError, "no gemspecs found at #{Dir.pwd}"
+ else
+ raise ArgumentError,
+ "found multiple gemspecs at #{Dir.pwd}, " \
+ "use the name: option to specify the one you want"
+ end
+ end
+
+ ##
+ # Changes the behavior of gem dependency file loading to installing mode.
+ # In installing mode certain restrictions are ignored such as ruby version
+ # mismatch checks.
+
+ def installing=(installing) # :nodoc:
+ @installing = installing
+ end
+
+ ##
+ # Loads the gem dependency file and returns self.
+
+ def load
+ instance_eval File.read(@path), @path, 1
+
+ self
+ end
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # :call-seq:
+ # gem(name)
+ # gem(name, *requirements)
+ # gem(name, *requirements, options)
+ #
+ # Specifies a gem dependency with the given +name+ and +requirements+. You
+ # may also supply +options+ following the +requirements+
+ #
+ # +options+ include:
+ #
+ # require: ::
+ # RubyGems does not provide any autorequire features so requires in a gem
+ # dependencies file are recorded but ignored.
+ #
+ # In bundler the require: option overrides the file to require during
+ # Bundler.require. By default the name of the dependency is required in
+ # Bundler. A single file or an Array of files may be given.
+ #
+ # To disable requiring any file give +false+:
+ #
+ # gem 'rake', require: false
+ #
+ # group: ::
+ # Place the dependencies in the given dependency group. A single group or
+ # an Array of groups may be given.
+ #
+ # See also #group
+ #
+ # platform: ::
+ # Only install the dependency on the given platform. A single platform or
+ # an Array of platforms may be given.
+ #
+ # See #platform for a list of platforms available.
+ #
+ # path: ::
+ # Install this dependency from an unpacked gem in the given directory.
+ #
+ # gem 'modified_gem', path: 'vendor/modified_gem'
+ #
+ # git: ::
+ # Install this dependency from a git repository:
+ #
+ # gem 'private_gem', git: 'git@my.company.example:private_gem.git'
+ #
+ # gist: ::
+ # Install this dependency from the gist ID:
+ #
+ # gem 'bang', gist: '1232884'
+ #
+ # github: ::
+ # Install this dependency from a github git repository:
+ #
+ # gem 'private_gem', github: 'my_company/private_gem'
+ #
+ # submodules: ::
+ # Set to +true+ to include submodules when fetching the git repository for
+ # git:, gist: and github: dependencies.
+ #
+ # ref: ::
+ # Use the given commit name or SHA for git:, gist: and github:
+ # dependencies.
+ #
+ # branch: ::
+ # Use the given branch for git:, gist: and github: dependencies.
+ #
+ # tag: ::
+ # Use the given tag for git:, gist: and github: dependencies.
+
+ def gem(name, *requirements)
+ options = requirements.pop if requirements.last.is_a?(Hash)
+ options ||= {}
+
+ options[:git] = @current_repository if @current_repository
+
+ source_set = false
+
+ source_set ||= gem_path name, options
+ source_set ||= gem_git name, options
+ source_set ||= gem_git_source name, options
+ source_set ||= gem_source name, options
+
+ duplicate = @dependencies.include? name
+
+ @dependencies[name] =
+ if requirements.empty? && !source_set
+ Gem::Requirement.default
+ elsif source_set
+ Gem::Requirement.source_set
+ else
+ Gem::Requirement.create requirements
+ end
+
+ return unless gem_platforms name, options
+
+ groups = gem_group name, options
+
+ return unless (groups & @without_groups).empty?
+
+ pin_gem_source name, :default unless source_set
+
+ gem_requires name, options
+
+ if duplicate
+ warn <<-WARNING
+Gem dependencies file #{@path} requires #{name} more than once.
+ WARNING
+ end
+
+ @set.gem name, *requirements
+ end
+
+ ##
+ # Handles the git: option from +options+ for gem +name+.
+ #
+ # Returns +true+ if the gist or git option was handled.
+
+ def gem_git(name, options) # :nodoc:
+ if gist = options.delete(:gist)
+ options[:git] = "https://gist.github.com/#{gist}.git"
+ end
+
+ return unless repository = options.delete(:git)
+
+ pin_gem_source name, :git, repository
+
+ reference = gem_git_reference options
+
+ submodules = options.delete :submodules
+
+ @git_set.add_git_gem name, repository, reference, submodules
+
+ true
+ end
+
+ ##
+ # Handles the git options from +options+ for git gem.
+ #
+ # Returns reference for the git gem.
+
+ def gem_git_reference(options) # :nodoc:
+ ref = options.delete :ref
+ branch = options.delete :branch
+ tag = options.delete :tag
+
+ reference = nil
+ reference ||= ref
+ reference ||= branch
+ reference ||= tag
+
+ if ref && branch
+ warn <<-WARNING
+Gem dependencies file #{@path} includes git reference for both ref and branch but only ref is used.
+ WARNING
+ end
+ if (ref || branch) && tag
+ warn <<-WARNING
+Gem dependencies file #{@path} includes git reference for both ref/branch and tag but only ref/branch is used.
+ WARNING
+ end
+
+ reference
+ end
+
+ private :gem_git
+
+ ##
+ # Handles a git gem option from +options+ for gem +name+ for a git source
+ # registered through git_source.
+ #
+ # Returns +true+ if the custom source option was handled.
+
+ def gem_git_source(name, options) # :nodoc:
+ return unless git_source = (@git_sources.keys & options.keys).last
+
+ source_callback = @git_sources[git_source]
+ source_param = options.delete git_source
+
+ git_url = source_callback.call source_param
+
+ options[:git] = git_url
+
+ gem_git name, options
+
+ true
+ end
+
+ private :gem_git_source
+
+ ##
+ # Handles the :group and :groups +options+ for the gem with the given
+ # +name+.
+
+ def gem_group(name, options) # :nodoc:
+ g = options.delete :group
+ all_groups = g ? Array(g) : []
+
+ groups = options.delete :groups
+ all_groups |= groups if groups
+
+ all_groups |= @current_groups if @current_groups
+
+ all_groups
+ end
+
+ private :gem_group
+
+ ##
+ # Handles the path: option from +options+ for gem +name+.
+ #
+ # Returns +true+ if the path option was handled.
+
+ def gem_path(name, options) # :nodoc:
+ return unless directory = options.delete(:path)
+
+ pin_gem_source name, :path, directory
+
+ @vendor_set.add_vendor_gem name, directory
+
+ true
+ end
+
+ private :gem_path
+
+ ##
+ # Handles the source: option from +options+ for gem +name+.
+ #
+ # Returns +true+ if the source option was handled.
+
+ def gem_source(name, options) # :nodoc:
+ return unless source = options.delete(:source)
+
+ pin_gem_source name, :source, source
+
+ @source_set.add_source_gem name, source
+
+ true
+ end
+
+ private :gem_source
+
+ ##
+ # Handles the platforms: option from +options+. Returns true if the
+ # platform matches the current platform.
+
+ def gem_platforms(name, options) # :nodoc:
+ platform_names = Array(options.delete(:platform))
+ platform_names.concat Array(options.delete(:platforms))
+ platform_names.concat @current_platforms if @current_platforms
+
+ return true if platform_names.empty?
+
+ platform_names.any? do |platform_name|
+ raise ArgumentError, "unknown platform #{platform_name.inspect}" unless
+ platform = PLATFORM_MAP[platform_name]
+
+ next false unless Gem::Platform.match_gem? platform, name
+
+ if engines = ENGINE_MAP[platform_name]
+ next false unless engines.include? Gem.ruby_engine
+ end
+
+ case WINDOWS[platform_name]
+ when :only then
+ next false unless Gem.win_platform?
+ when :never then
+ next false if Gem.win_platform?
+ end
+
+ VERSION_MAP[platform_name].satisfied_by? Gem.ruby_version
+ end
+ end
+
+ private :gem_platforms
+
+ ##
+ # Records the require: option from +options+ and adds those files, or the
+ # default file to the require list for +name+.
+
+ def gem_requires(name, options) # :nodoc:
+ if options.include? :require
+ if requires = options.delete(:require)
+ @requires[name].concat Array requires
+ end
+ else
+ @requires[name] << name
+ end
+ raise ArgumentError, "Unhandled gem options #{options.inspect}" unless options.empty?
+ end
+
+ private :gem_requires
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Block form for specifying gems from a git +repository+.
+ #
+ # git 'https://github.com/rails/rails.git' do
+ # gem 'activesupport'
+ # gem 'activerecord'
+ # end
+
+ def git(repository)
+ @current_repository = repository
+
+ yield
+ ensure
+ @current_repository = nil
+ end
+
+ ##
+ # Defines a custom git source that uses +name+ to expand git repositories
+ # for use in gems built from git repositories. You must provide a block
+ # that accepts a git repository name for expansion.
+
+ def git_source(name, &callback)
+ @git_sources[name] = callback
+ end
+
+ ##
+ # Returns the basename of the file the dependencies were loaded from
+
+ def gem_deps_file # :nodoc:
+ File.basename @path
+ end
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Loads dependencies from a gemspec file.
+ #
+ # +options+ include:
+ #
+ # name: ::
+ # The name portion of the gemspec file. Defaults to searching for any
+ # gemspec file in the current directory.
+ #
+ # gemspec name: 'my_gem'
+ #
+ # path: ::
+ # The path the gemspec lives in. Defaults to the current directory:
+ #
+ # gemspec 'my_gem', path: 'gemspecs', name: 'my_gem'
+ #
+ # development_group: ::
+ # The group to add development dependencies to. By default this is
+ # :development. Only one group may be specified.
+
+ def gemspec(options = {})
+ name = options.delete(:name) || "{,*}"
+ path = options.delete(:path) || "."
+ development_group = options.delete(:development_group) || :development
+
+ spec = find_gemspec name, path
+
+ groups = gem_group spec.name, {}
+
+ self_dep = Gem::Dependency.new spec.name, spec.version
+
+ add_dependencies groups, [self_dep]
+ add_dependencies groups, spec.runtime_dependencies
+
+ @dependencies[spec.name] = Gem::Requirement.source_set
+
+ spec.dependencies.each do |dep|
+ @dependencies[dep.name] = dep.requirement
+ end
+
+ groups << development_group
+
+ add_dependencies groups, spec.development_dependencies
+
+ @vendor_set.add_vendor_gem spec.name, path
+ gem_requires spec.name, options
+ end
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Block form for placing a dependency in the given +groups+.
+ #
+ # group :development do
+ # gem 'debugger'
+ # end
+ #
+ # group :development, :test do
+ # gem 'minitest'
+ # end
+ #
+ # Groups can be excluded at install time using `gem install -g --without
+ # development`. See `gem help install` and `gem help gem_dependencies` for
+ # further details.
+
+ def group(*groups)
+ @current_groups = groups
+
+ yield
+ ensure
+ @current_groups = nil
+ end
+
+ ##
+ # Pins the gem +name+ to the given +source+. Adding a gem with the same
+ # name from a different +source+ will raise an exception.
+
+ def pin_gem_source(name, type = :default, source = nil)
+ source_description =
+ case type
+ when :default then "(default)"
+ when :path then "path: #{source}"
+ when :git then "git: #{source}"
+ when :source then "source: #{source}"
+ else "(unknown)"
+ end
+
+ raise ArgumentError,
+ "duplicate source #{source_description} for gem #{name}" if
+ @gem_sources.fetch(name, source) != source
+
+ @gem_sources[name] = source
+ end
+
+ private :pin_gem_source
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Block form for restricting gems to a set of platforms.
+ #
+ # The gem dependencies platform is different from Gem::Platform. A platform
+ # gem.deps.rb platform matches on the ruby engine, the ruby version and
+ # whether or not windows is allowed.
+ #
+ # :ruby, :ruby_XY ::
+ # Matches non-windows, non-jruby implementations where X and Y can be used
+ # to match releases in the 1.8, 1.9, 2.0 or 2.1 series.
+ #
+ # :mri, :mri_XY ::
+ # Matches non-windows C Ruby (Matz Ruby) or only the 1.8, 1.9, 2.0 or
+ # 2.1 series.
+ #
+ # :mingw, :mingw_XY ::
+ # Matches 32 bit C Ruby on MinGW or only the 1.8, 1.9, 2.0 or 2.1 series.
+ #
+ # :x64_mingw, :x64_mingw_XY ::
+ # Matches 64 bit C Ruby on MinGW or only the 1.8, 1.9, 2.0 or 2.1 series.
+ #
+ # :mswin, :mswin_XY ::
+ # Matches 32 bit C Ruby on Microsoft Windows or only the 1.8, 1.9, 2.0 or
+ # 2.1 series.
+ #
+ # :mswin64, :mswin64_XY ::
+ # Matches 64 bit C Ruby on Microsoft Windows or only the 1.8, 1.9, 2.0 or
+ # 2.1 series.
+ #
+ # :jruby, :jruby_XY ::
+ # Matches JRuby or JRuby in 1.8 or 1.9 mode.
+ #
+ # :maglev ::
+ # Matches Maglev
+ #
+ # :rbx ::
+ # Matches non-windows Rubinius
+ #
+ # NOTE: There is inconsistency in what environment a platform matches. You
+ # may need to read the source to know the exact details.
+
+ def platform(*platforms)
+ @current_platforms = platforms
+
+ yield
+ ensure
+ @current_platforms = nil
+ end
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Block form for restricting gems to a particular set of platforms. See
+ # #platform.
+
+ alias_method :platforms, :platform
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Restricts this gem dependencies file to the given ruby +version+.
+ #
+ # You may also provide +engine:+ and +engine_version:+ options to restrict
+ # this gem dependencies file to a particular ruby engine and its engine
+ # version. This matching is performed by using the RUBY_ENGINE and
+ # RUBY_ENGINE_VERSION constants.
+
+ def ruby(version, options = {})
+ engine = options[:engine]
+ engine_version = options[:engine_version]
+
+ raise ArgumentError,
+ "You must specify engine_version along with the Ruby engine" if
+ engine && !engine_version
+
+ return true if @installing
+
+ unless version == RUBY_VERSION
+ message = "Your Ruby version is #{RUBY_VERSION}, " \
+ "but your #{gem_deps_file} requires #{version}"
+
+ raise Gem::RubyVersionMismatch, message
+ end
+
+ if engine && engine != Gem.ruby_engine
+ message = "Your Ruby engine is #{Gem.ruby_engine}, " \
+ "but your #{gem_deps_file} requires #{engine}"
+
+ raise Gem::RubyVersionMismatch, message
+ end
+
+ if engine_version
+ if engine_version != RUBY_ENGINE_VERSION
+ message =
+ "Your Ruby engine version is #{Gem.ruby_engine} #{RUBY_ENGINE_VERSION}, " \
+ "but your #{gem_deps_file} requires #{engine} #{engine_version}"
+
+ raise Gem::RubyVersionMismatch, message
+ end
+ end
+
+ true
+ end
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Sets +url+ as a source for gems for this dependency API. RubyGems uses
+ # the default configured sources if no source was given. If a source is set
+ # only that source is used.
+ #
+ # This method differs in behavior from Bundler:
+ #
+ # * The +:gemcutter+, # +:rubygems+ and +:rubyforge+ sources are not
+ # supported as they are deprecated in bundler.
+ # * The +prepend:+ option is not supported. If you wish to order sources
+ # then list them in your preferred order.
+
+ def source(url)
+ Gem.sources.clear if @default_sources
+
+ @default_sources = false
+
+ Gem.sources << url
+ end
+end
diff --git a/lib/rubygems/request_set/lockfile.rb b/lib/rubygems/request_set/lockfile.rb
new file mode 100644
index 0000000000..8b9c9690d6
--- /dev/null
+++ b/lib/rubygems/request_set/lockfile.rb
@@ -0,0 +1,233 @@
+# frozen_string_literal: true
+
+##
+# Parses a gem.deps.rb.lock file and constructs a LockSet containing the
+# dependencies found inside. If the lock file is missing no LockSet is
+# constructed.
+
+class Gem::RequestSet::Lockfile
+ ##
+ # Raised when a lockfile cannot be parsed
+
+ class ParseError < Gem::Exception
+ ##
+ # The column where the error was encountered
+
+ attr_reader :column
+
+ ##
+ # The line where the error was encountered
+
+ attr_reader :line
+
+ ##
+ # The location of the lock file
+
+ attr_reader :path
+
+ ##
+ # Raises a ParseError with the given +message+ which was encountered at a
+ # +line+ and +column+ while parsing.
+
+ def initialize(message, column, line, path)
+ @line = line
+ @column = column
+ @path = path
+ super "#{message} (at line #{line} column #{column})"
+ end
+ end
+
+ ##
+ # Creates a new Lockfile for the given Gem::RequestSet and +gem_deps_file+
+ # location.
+
+ def self.build(request_set, gem_deps_file, dependencies = nil)
+ request_set.resolve
+ dependencies ||= requests_to_deps request_set.sorted_requests
+ new request_set, gem_deps_file, dependencies
+ end
+
+ def self.requests_to_deps(requests) # :nodoc:
+ deps = {}
+
+ requests.each do |request|
+ spec = request.spec
+ name = request.name
+ requirement = request.request.dependency.requirement
+
+ deps[name] = if [Gem::Resolver::VendorSpecification,
+ Gem::Resolver::GitSpecification].include? spec.class
+ Gem::Requirement.source_set
+ else
+ requirement
+ end
+ end
+
+ deps
+ end
+
+ ##
+ # The platforms for this Lockfile
+
+ attr_reader :platforms
+
+ def initialize(request_set, gem_deps_file, dependencies)
+ @set = request_set
+ @dependencies = dependencies
+ @gem_deps_file = File.expand_path(gem_deps_file)
+ @gem_deps_dir = File.dirname(@gem_deps_file)
+ @platforms = []
+ end
+
+ def add_DEPENDENCIES(out) # :nodoc:
+ out << "DEPENDENCIES"
+
+ out.concat @dependencies.sort.map {|name, requirement|
+ " #{name}#{requirement.for_lockfile}"
+ }
+
+ out << nil
+ end
+
+ def add_GEM(out, spec_groups) # :nodoc:
+ return if spec_groups.empty?
+
+ source_groups = spec_groups.values.flatten.group_by do |request|
+ request.spec.source.uri
+ end
+
+ source_groups.sort_by {|group,| group.to_s }.map do |group, requests|
+ out << "GEM"
+ out << " remote: #{group}"
+ out << " specs:"
+
+ requests.sort_by(&:name).each do |request|
+ next if request.spec.name == "bundler"
+ platform = "-#{request.spec.platform}" unless
+ request.spec.platform == Gem::Platform::RUBY
+
+ out << " #{request.name} (#{request.version}#{platform})"
+
+ request.full_spec.dependencies.sort.each do |dependency|
+ next if dependency.type == :development
+
+ requirement = dependency.requirement
+ out << " #{dependency.name}#{requirement.for_lockfile}"
+ end
+ end
+ out << nil
+ end
+ end
+
+ def add_GIT(out, git_requests)
+ return if git_requests.empty?
+
+ by_repository_revision = git_requests.group_by do |request|
+ source = request.spec.source
+ [source.repository, source.rev_parse]
+ end
+
+ by_repository_revision.each do |(repository, revision), requests|
+ out << "GIT"
+ out << " remote: #{repository}"
+ out << " revision: #{revision}"
+ out << " specs:"
+
+ requests.sort_by(&:name).each do |request|
+ out << " #{request.name} (#{request.version})"
+
+ dependencies = request.spec.dependencies.sort_by(&:name)
+ dependencies.each do |dep|
+ out << " #{dep.name}#{dep.requirement.for_lockfile}"
+ end
+ end
+ out << nil
+ end
+ end
+
+ def relative_path_from(dest, base) # :nodoc:
+ dest = File.expand_path(dest)
+ base = File.expand_path(base)
+
+ if dest.index(base) == 0
+ offset = dest[base.size + 1..-1]
+
+ return "." unless offset
+
+ offset
+ else
+ dest
+ end
+ end
+
+ def add_PATH(out, path_requests) # :nodoc:
+ return if path_requests.empty?
+
+ out << "PATH"
+ path_requests.each do |request|
+ directory = File.expand_path(request.spec.source.uri)
+
+ out << " remote: #{relative_path_from directory, @gem_deps_dir}"
+ out << " specs:"
+ out << " #{request.name} (#{request.version})"
+ end
+
+ out << nil
+ end
+
+ def add_PLATFORMS(out) # :nodoc:
+ out << "PLATFORMS"
+
+ platforms = requests.map {|request| request.spec.platform }.uniq
+
+ platforms = platforms.sort_by(&:to_s)
+
+ platforms.each do |platform|
+ out << " #{platform}"
+ end
+
+ out << nil
+ end
+
+ def spec_groups
+ requests.group_by {|request| request.spec.class }
+ end
+
+ ##
+ # The contents of the lock file.
+
+ def to_s
+ out = []
+
+ groups = spec_groups
+
+ add_PATH out, groups.delete(Gem::Resolver::VendorSpecification) { [] }
+
+ add_GIT out, groups.delete(Gem::Resolver::GitSpecification) { [] }
+
+ add_GEM out, groups
+
+ add_PLATFORMS out
+
+ add_DEPENDENCIES out
+
+ out.join "\n"
+ end
+
+ ##
+ # Writes the lock file alongside the gem dependencies file
+
+ def write
+ content = to_s
+
+ File.open "#{@gem_deps_file}.lock", "w" do |io|
+ io.write content
+ end
+ end
+
+ private
+
+ def requests
+ @set.sorted_requests
+ end
+end
diff --git a/lib/rubygems/require_paths_builder.rb b/lib/rubygems/require_paths_builder.rb
deleted file mode 100644
index fe4f593bf4..0000000000
--- a/lib/rubygems/require_paths_builder.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module Gem
- module RequirePathsBuilder
- def write_require_paths_file_if_needed(spec = @spec, gem_home = @gem_home)
- return if spec.require_paths == ["lib"] && (spec.bindir.nil? || spec.bindir == "bin")
- file_name = File.join(gem_home, 'gems', "#{@spec.full_name}", ".require_paths")
- file_name.untaint
- File.open(file_name, "w") do |file|
- spec.require_paths.each do |path|
- file.puts path
- end
- file.puts spec.bindir if spec.bindir
- end
- end
- end
-end \ No newline at end of file
diff --git a/lib/rubygems/requirement.rb b/lib/rubygems/requirement.rb
index c9128b5ebc..0d3f98eb0f 100644
--- a/lib/rubygems/requirement.rb
+++ b/lib/rubygems/requirement.rb
@@ -1,163 +1,298 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
+# frozen_string_literal: true
-require 'rubygems/version'
+require_relative "version"
##
-# Requirement version includes a prefaced comparator in addition
-# to a version number.
+# A Requirement is a set of one or more version restrictions. It supports a
+# few (<tt>=, !=, >, <, >=, <=, ~></tt>) different restriction operators.
#
-# A Requirement object can actually contain multiple, er,
-# requirements, as in (> 1.2, < 2.0).
+# See Gem::Version for a description on how versions and requirements work
+# together in RubyGems.
class Gem::Requirement
+ OPS = { # :nodoc:
+ "=" => lambda {|v, r| v == r },
+ "!=" => lambda {|v, r| v != r },
+ ">" => lambda {|v, r| v > r },
+ "<" => lambda {|v, r| v < r },
+ ">=" => lambda {|v, r| v >= r },
+ "<=" => lambda {|v, r| v <= r },
+ "~>" => lambda {|v, r| v >= r && v.release < r.bump },
+ }.freeze
- include Comparable
+ SOURCE_SET_REQUIREMENT = Struct.new(:for_lockfile).new "!" # :nodoc:
- attr_reader :requirements
+ quoted = Regexp.union(OPS.keys)
+ PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{Gem::Version::VERSION_PATTERN})\\s*".freeze # :nodoc:
- OPS = {
- "=" => lambda { |v, r| v == r },
- "!=" => lambda { |v, r| v != r },
- ">" => lambda { |v, r| v > r },
- "<" => lambda { |v, r| v < r },
- ">=" => lambda { |v, r| v >= r },
- "<=" => lambda { |v, r| v <= r },
- "~>" => lambda { |v, r| v >= r && v < r.bump }
- }
+ ##
+ # A regular expression that matches a requirement
+
+ PATTERN = /\A#{PATTERN_RAW}\z/
+
+ ##
+ # The default requirement matches any non-prerelease version
- OP_RE = /#{OPS.keys.map{ |k| Regexp.quote k }.join '|'}/o
+ DefaultRequirement = [">=", Gem::Version.new(0)].freeze
##
- # Factory method to create a Gem::Requirement object. Input may be a
- # Version, a String, or nil. Intended to simplify client code.
+ # The default requirement matches any version
+
+ DefaultPrereleaseRequirement = [">=", Gem::Version.new("0.a")].freeze
+
+ ##
+ # Raised when a bad requirement is encountered
+
+ class BadRequirementError < ArgumentError; end
+
+ ##
+ # Factory method to create a Gem::Requirement object. Input may be
+ # a Version, a String, or nil. Intended to simplify client code.
#
- # If the input is "weird", the default version requirement is returned.
+ # If the input is "weird", the default version requirement is
+ # returned.
+
+ def self.create(*inputs)
+ return new inputs if inputs.length > 1
+
+ input = inputs.shift
- def self.create(input)
case input
when Gem::Requirement then
input
when Gem::Version, Array then
new input
+ when "!" then
+ source_set
else
- if input.respond_to? :to_str then
- self.new [input.to_str]
+ if input.respond_to? :to_str
+ new [input.to_str]
else
- self.default
+ default
end
end
end
+ def self.default
+ new ">= 0"
+ end
+
+ def self.default_prerelease
+ new ">= 0.a"
+ end
+
+ ###
+ # A source set requirement, used for Gemfiles and lockfiles
+
+ def self.source_set # :nodoc:
+ SOURCE_SET_REQUIREMENT
+ end
+
##
- # A default "version requirement" can surely _only_ be '>= 0'.
- #--
- # This comment once said:
+ # Parse +obj+, returning an <tt>[op, version]</tt> pair. +obj+ can
+ # be a String or a Gem::Version.
#
- # "A default "version requirement" can surely _only_ be '> 0'."
+ # If +obj+ is a String, it can be either a full requirement
+ # specification, like <tt>">= 1.2"</tt>, or a simple version number,
+ # like <tt>"1.2"</tt>.
+ #
+ # parse("> 1.0") # => [">", Gem::Version.new("1.0")]
+ # parse("1.0") # => ["=", Gem::Version.new("1.0")]
+ # parse(Gem::Version.new("1.0")) # => ["=, Gem::Version.new("1.0")]
- def self.default
- self.new ['>= 0']
+ def self.parse(obj)
+ return ["=", obj] if Gem::Version === obj
+
+ unless PATTERN =~ obj.to_s
+ raise BadRequirementError, "Illformed requirement [#{obj.inspect}]"
+ end
+ op = -($1 || "=")
+ version = -$2
+
+ if op == ">=" && version == "0"
+ DefaultRequirement
+ elsif op == ">=" && version == "0.a"
+ DefaultPrereleaseRequirement
+ else
+ [op, Gem::Version.new(version)]
+ end
+ end
+
+ ##
+ # An array of requirement pairs. The first element of the pair is
+ # the op, and the second is the Gem::Version.
+
+ attr_reader :requirements # :nodoc:
+
+ ##
+ # Constructs a requirement from +requirements+. Requirements can be
+ # Strings, Gem::Versions, or Arrays of those. +nil+ and duplicate
+ # requirements are ignored. An empty set of +requirements+ is the
+ # same as <tt>">= 0"</tt>.
+
+ def initialize(*requirements)
+ requirements = requirements.flatten
+ requirements.compact!
+ requirements.uniq!
+
+ if requirements.empty?
+ @requirements = [DefaultRequirement]
+ else
+ @requirements = requirements.map! {|r| self.class.parse r }
+ end
end
##
- # Constructs a Requirement from +requirements+ which can be a String, a
- # Gem::Version, or an Array of those. See parse for details on the
- # formatting of requirement strings.
-
- def initialize(requirements)
- @requirements = case requirements
- when Array then
- requirements.map do |requirement|
- parse(requirement)
- end
- else
- [parse(requirements)]
- end
- @version = nil # Avoid warnings.
+ # Concatenates the +new+ requirements onto this requirement.
+
+ def concat(new)
+ new = new.flatten
+ new.compact!
+ new.uniq!
+ new = new.map {|r| self.class.parse r }
+
+ @requirements.concat new
end
##
- # Marshal raw requirements, rather than the full object
+ # Formats this requirement for use in a Gem::RequestSet::Lockfile.
- def marshal_dump # :nodoc:
- [@requirements]
+ def for_lockfile # :nodoc:
+ return if @requirements == [DefaultRequirement]
+
+ list = requirements.sort_by do |_, version|
+ version
+ end.map do |op, version|
+ "#{op} #{version}"
+ end.uniq
+
+ " (#{list.join ", "})"
end
##
- # Load custom marshal format
+ # true if this gem has no requirements.
+
+ def none?
+ if @requirements.size == 1
+ @requirements[0] == DefaultRequirement
+ else
+ false
+ end
+ end
+
+ ##
+ # true if the requirement is for only an exact version
+
+ def exact?
+ return false unless @requirements.size == 1
+ @requirements[0][0] == "="
+ end
+
+ def as_list # :nodoc:
+ requirements.map {|op, version| "#{op} #{version}" }
+ end
+
+ def hash # :nodoc:
+ requirements.map {|r| r.first == "~>" ? [r[0], r[1].to_s] : r }.sort.hash
+ end
+
+ def marshal_dump # :nodoc:
+ [@requirements]
+ end
def marshal_load(array) # :nodoc:
@requirements = array[0]
- @version = nil
+
+ raise TypeError, "wrong @requirements" unless Array === @requirements &&
+ @requirements.all? {|r| r.size == 2 && (r.first.is_a?(String) || r[0] = "=") && r.last.is_a?(Gem::Version) }
end
- def to_s # :nodoc:
- as_list.join(", ")
+ def yaml_initialize(tag, vals) # :nodoc:
+ vals.each do |ivar, val|
+ instance_variable_set "@#{ivar}", val
+ end
end
- def as_list
- normalize
- @requirements.collect { |req|
- "#{req[0]} #{req[1]}"
- }
+ def init_with(coder) # :nodoc:
+ yaml_initialize coder.tag, coder.map
end
- def normalize
- return if not defined? @version or @version.nil?
- @requirements = [parse(@version)]
- @nums = nil
- @version = nil
- @op = nil
+ def encode_with(coder) # :nodoc:
+ coder.add "requirements", @requirements
end
##
- # True if this requirement satisfied by the Gem::Version +version+.
+ # A requirement is a prerelease if any of the versions inside of it
+ # are prereleases
- def satisfied_by?(version)
- normalize
- @requirements.all? { |op, rv| satisfy?(op, version, rv) }
+ def prerelease?
+ requirements.any? {|r| r.last.prerelease? }
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 1, "Gem::Requirement.new(", ")" do
+ q.pp as_list
+ end
end
##
- # Is "+version+ +op+ +required_version+" satisfied?
+ # True if +version+ satisfies this Requirement.
- def satisfy?(op, version, required_version)
- OPS[op].call(version, required_version)
+ def satisfied_by?(version)
+ raise ArgumentError, "Need a Gem::Version: #{version.inspect}" unless
+ Gem::Version === version
+ requirements.all? {|op, rv| OPS.fetch(op).call version, rv }
end
+ alias_method :===, :satisfied_by?
+ alias_method :=~, :satisfied_by?
+
##
- # Parse the version requirement obj returning the operator and version.
- #
- # The requirement can be a String or a Gem::Version. A String can be an
- # operator (<, <=, =, =>, >, !=, ~>), a version number, or both, operator
- # first.
-
- def parse(obj)
- case obj
- when /^\s*(#{OP_RE})\s*([0-9.]+)\s*$/o then
- [$1, Gem::Version.new($2)]
- when /^\s*([0-9.]+)\s*$/ then
- ['=', Gem::Version.new($1)]
- when /^\s*(#{OP_RE})\s*$/o then
- [$1, Gem::Version.new('0')]
- when Gem::Version then
- ['=', obj]
- else
- fail ArgumentError, "Illformed requirement [#{obj.inspect}]"
- end
+ # True if the requirement will not always match the latest version.
+
+ def specific?
+ return true if @requirements.length > 1 # GIGO, > 1, > 2 is silly
+
+ !%w[> >=].include? @requirements.first.first # grab the operator
end
- def <=>(other) # :nodoc:
- to_s <=> other.to_s
+ def to_s # :nodoc:
+ as_list.join ", "
end
- def hash # :nodoc:
- to_s.hash
+ def ==(other) # :nodoc:
+ return unless Gem::Requirement === other
+
+ # An == check is always necessary
+ return false unless _sorted_requirements == other._sorted_requirements
+
+ # An == check is sufficient unless any requirements use ~>
+ return true unless _tilde_requirements.any?
+
+ # If any requirements use ~> we use the stricter `#eql?` that also checks
+ # that version precision is the same
+ _tilde_requirements.eql?(other._tilde_requirements)
+ end
+
+ protected
+
+ def _sorted_requirements
+ @_sorted_requirements ||= requirements.sort_by(&:to_s)
end
+ def _tilde_requirements
+ @_tilde_requirements ||= _sorted_requirements.select {|r| r.first == "~>" }
+ end
+
+ def initialize_copy(other) # :nodoc:
+ @requirements = other.requirements.dup
+ super
+ end
end
+class Gem::Version
+ # This is needed for compatibility with older yaml
+ # gemspecs.
+
+ Requirement = Gem::Requirement # :nodoc:
+end
diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb
new file mode 100644
index 0000000000..788206c056
--- /dev/null
+++ b/lib/rubygems/resolver.rb
@@ -0,0 +1,565 @@
+# frozen_string_literal: true
+
+require_relative "dependency"
+require_relative "exceptions"
+
+##
+# Given a set of Gem::Dependency objects as +needed+ and a way to query the
+# set of available specs via +set+, calculates a set of ActivationRequest
+# objects which indicate all the specs that should be activated to meet the
+# all the requirements.
+
+class Gem::Resolver
+ require_relative "vendored_pub_grub"
+
+ ##
+ # If the DEBUG_RESOLVER environment variable is set then debugging mode is
+ # enabled for the resolver. This will display information about the state
+ # of the resolver while a set of dependencies is being resolved.
+
+ DEBUG_RESOLVER = !ENV["DEBUG_RESOLVER"].nil?
+
+ ##
+ # Set to true if all development dependencies should be considered.
+
+ attr_accessor :development
+
+ ##
+ # Set to true if immediate development dependencies should be considered.
+
+ attr_accessor :development_shallow
+
+ ##
+ # When true, no dependencies are looked up for requested gems.
+
+ attr_accessor :ignore_dependencies
+
+ ##
+ # Hash of gems to skip resolution. Keyed by gem name, with arrays of
+ # gem specifications as values.
+
+ attr_accessor :skip_gems
+
+ ##
+ #
+
+ attr_accessor :soft_missing
+
+ ##
+ # Combines +sets+ into a ComposedSet that allows specification lookup in a
+ # uniform manner. If one of the +sets+ is itself a ComposedSet its sets are
+ # flattened into the result ComposedSet.
+
+ def self.compose_sets(*sets)
+ sets.compact!
+
+ sets = sets.flat_map do |set|
+ case set
+ when Gem::Resolver::BestSet then
+ set
+ when Gem::Resolver::ComposedSet then
+ set.sets
+ else
+ set
+ end
+ end
+
+ case sets.length
+ when 0 then
+ raise ArgumentError, "one set in the composition must be non-nil"
+ when 1 then
+ sets.first
+ else
+ Gem::Resolver::ComposedSet.new(*sets)
+ end
+ end
+
+ ##
+ # Creates a Resolver that queries only against the already installed gems
+ # for the +needed+ dependencies.
+
+ def self.for_current_gems(needed)
+ new needed, Gem::Resolver::CurrentSet.new
+ end
+
+ ##
+ # Create Resolver object which will resolve the tree starting
+ # with +needed+ Dependency objects.
+ #
+ # +set+ is an object that provides where to look for specifications to
+ # satisfy the Dependencies. This defaults to IndexSet, which will query
+ # rubygems.org.
+
+ def initialize(needed, set = nil)
+ @set = set || Gem::Resolver::IndexSet.new
+ @needed = needed
+
+ @development = false
+ @development_shallow = false
+ @ignore_dependencies = false
+ @skip_gems = {}
+ @soft_missing = false
+
+ @root_package = RootPackage.new
+ @root_version = Gem::PubGrub::Package.root_version
+
+ @packages = {}
+
+ @unfiltered_specs = Hash.new {|h, name| h[name] = find_unfiltered_specs_for(name) }
+ @all_specs = Hash.new {|h, name| h[name] = filter_specs(@unfiltered_specs[name]) }
+ @all_versions = Hash.new {|h, pkg| h[pkg] = @all_specs[pkg.to_s].map(&:version).uniq.sort }
+ @sorted_versions = Hash.new do |h, pkg|
+ h[pkg] = Gem::PubGrub::Package.root?(pkg) ? [@root_version] : @all_versions[pkg]
+ end
+ @cached_dependencies = Hash.new do |h, pkg|
+ h[pkg] = if Gem::PubGrub::Package.root?(pkg)
+ { @root_version => root_dependencies }
+ else
+ Hash.new {|v, ver| v[ver] = compute_dependencies(pkg, ver) }
+ end
+ end
+ @version_to_index = Hash.new {|h, pkg| h[pkg] = @sorted_versions[pkg].each_with_index.to_h }
+ @versions_for_cache = Hash.new {|h, pkg| h[pkg] = {} }
+ @spec_for_cache = Hash.new {|h, name| h[name] = build_spec_for_cache(name) }
+ end
+
+ ##
+ # Proceed with resolution! Returns an array of ActivationRequest objects.
+
+ def resolve
+ # Pre-check: raise UnsatisfiableDependencyError for root deps with no
+ # platform match. We filter by platform ONLY here (not required_ruby_version
+ # / required_rubygems_version): a foreign-platform gem is genuinely "not
+ # found", but a gem that exists yet is incompatible with the running Ruby
+ # should flow through the solver to a DependencyResolutionError that names
+ # the Ruby requirement. That matches Bundler (which models Ruby as a
+ # synthetic dependency, so this surfaces as a solve failure) and gives a
+ # clearer message than the platform-oriented UnsatisfiableDependencyError.
+ @needed.each do |dep|
+ next if @soft_missing
+ dep_request = DependencyRequest.new(dep, nil)
+ all = @set.find_all(dep_request)
+ matching = select_local_platforms(all)
+
+ next unless matching.empty?
+
+ exc = Gem::UnsatisfiableDependencyError.new(dep_request, all)
+ exc.errors = @set.errors
+ raise exc
+ end
+
+ solver = Gem::PubGrub::VersionSolver.new(
+ source: self,
+ root: @root_package,
+ strategy: Gem::Resolver::Strategy.new(self),
+ logger: make_logger
+ )
+ result = solver.solve
+
+ # Convert to Array<ActivationRequest>
+ needed_by_name = @needed.group_by(&:name)
+ result.filter_map do |package, version|
+ next if Gem::PubGrub::Package.root?(package)
+ spec = spec_for(package.to_s, version)
+ dep = needed_by_name[package.to_s]&.first || Gem::Dependency.new(package.to_s)
+ dep_request = DependencyRequest.new(dep, nil)
+ ActivationRequest.new(spec, dep_request)
+ end
+ rescue Gem::PubGrub::SolveFailure => e
+ extended = extract_extended_explanation(e.incompatibility)
+ if extended
+ message = "#{e.explanation}\n\n#{extended}"
+ raise Gem::DependencyResolutionError, Struct.new(:explanation).new(message)
+ else
+ raise Gem::DependencyResolutionError, e
+ end
+ end
+
+ # PubGrub source interface methods
+
+ def all_versions_for(package)
+ versions = @sorted_versions[package].reverse # highest first
+ name = package.to_s
+
+ if (skip_dep_gems = skip_gems[name]) && !skip_dep_gems.empty?
+ # Conservative mode: float the already-installed (skip) versions to the
+ # front so the solver prefers them. This sets *preference* only (it feeds
+ # the strategy's version-index map); it does not restrict availability, so
+ # every version stays selectable via versions_for. When an installed
+ # version is made impossible by a downstream conflict, the solver
+ # backtracks to a newer version instead of failing. Molinillo instead
+ # hard-restricted the candidate set to skip versions and raised.
+ #
+ # This reaches the same outcome as Bundler (upgrade-over-raise) for the
+ # common single-blocked-gem case, though the mechanism differs: Bundler
+ # hard-pins locked gems and selectively unlocks + re-solves on conflict,
+ # whereas we float as a preference and let PubGrub backtrack in one solve.
+ # The float can therefore over-upgrade when several installed gems are
+ # jointly involved in a conflict; that outcome-level divergence is
+ # accepted (see test_conservative_upgrades_when_installed_blocked).
+ skip_versions = skip_dep_gems.map(&:version)
+ preferred, rest = versions.partition {|v| skip_versions.include?(v) }
+ preferred + rest
+ else
+ # Prefer already-installed versions to avoid unnecessary upgrades
+ installed_versions = @all_specs[name].
+ select {|s| s.is_a?(Gem::Resolver::InstalledSpecification) }.
+ map(&:version)
+ if installed_versions.any?
+ preferred, rest = versions.partition {|v| installed_versions.include?(v) }
+ preferred + rest
+ else
+ versions
+ end
+ end
+ end
+
+ def versions_for(package, range = Gem::PubGrub::VersionRange.any)
+ @versions_for_cache[package][range] ||= begin
+ candidates = range.select_versions(@sorted_versions[package])
+
+ if Gem::PubGrub::Package.root?(package) ||
+ (@set.respond_to?(:prerelease) && @set.prerelease) ||
+ range_admits_prerelease?(range)
+ candidates
+ elsif @all_versions[package].any? {|v| !v.prerelease? }
+ candidates.reject(&:prerelease?)
+ else
+ # Only prereleases exist for this gem; fall back to them so
+ # dependencies like `>= 1.0` can still be satisfied.
+ candidates
+ end
+ end
+ end
+
+ def no_versions_incompatibility_for(_package, unsatisfied_term)
+ cause = Gem::PubGrub::Incompatibility::NoVersions.new(unsatisfied_term)
+
+ name = unsatisfied_term.package.to_s
+ constraint = unsatisfied_term.constraint
+ extended_explanation = build_extended_explanation(name, constraint)
+
+ custom_explanation = if extended_explanation
+ "#{constraint} could not be found in any repository"
+ end
+
+ Gem::Resolver::Incompatibility.new(
+ [unsatisfied_term],
+ cause: cause,
+ custom_explanation: custom_explanation,
+ extended_explanation: extended_explanation
+ )
+ end
+
+ def incompatibilities_for(package, version)
+ package_deps = @cached_dependencies[package]
+ sorted_versions = @sorted_versions[package]
+ package_deps[version].filter_map do |dep_package_name, dep_constraint|
+ dep_package = dep_constraint.package
+
+ low = high = @version_to_index[package][version]
+
+ # find version low such that all >= low share the same dep
+ while low > 0 &&
+ package_deps[sorted_versions[low - 1]][dep_package_name] == dep_constraint
+ low -= 1
+ end
+ low =
+ if low == 0
+ nil
+ else
+ sorted_versions[low]
+ end
+
+ # find version high such that all < high share the same dep
+ while high < sorted_versions.length &&
+ package_deps[sorted_versions[high]][dep_package_name] == dep_constraint
+ high += 1
+ end
+ high =
+ if high == sorted_versions.length
+ nil
+ else
+ sorted_versions[high]
+ end
+
+ range = Gem::PubGrub::VersionRange.new(min: low, max: high, include_min: !low.nil?)
+ self_constraint = Gem::PubGrub::VersionConstraint.new(package, range: range)
+
+ # No specs anywhere means an unknown package. Check @unfiltered_specs, not
+ # the filtered set, so a dep filtered out by platform/Ruby/prerelease falls
+ # through to NoVersions for proper hints instead. The band-scoped
+ # self_constraint lets clean sibling versions still resolve via backtracking.
+ if @unfiltered_specs[dep_package_name].empty?
+ cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint)
+ self_term = Gem::PubGrub::Term.new(self_constraint, true)
+ # PubGrub's default InvalidDependency rendering drops the version
+ # requirement ("depends on unknown package bar"). Supply a custom
+ # explanation so the missing dependency's constraint is preserved
+ # ("depends on bar = 0.5 which could not be found in any repository"),
+ # matching Molinillo's diagnostics.
+ return [Gem::PubGrub::Incompatibility.new(
+ [self_term],
+ cause: cause,
+ custom_explanation: "#{self_term.to_s(allow_every: true)} depends on #{dep_constraint} which could not be found in any repository"
+ )]
+ end
+
+ # An empty range means the requirement is self-contradictory (e.g. `> 2, < 1`).
+ if dep_constraint.range.empty?
+ return [Gem::Resolver::Incompatibility.new(
+ [Gem::PubGrub::Term.new(self_constraint, true)],
+ cause: Gem::PubGrub::Incompatibility::NoVersions.new(dep_constraint),
+ custom_explanation: "#{dep_package_name} cannot satisfy contradictory requirements #{dep_constraint.constraint_string}"
+ )]
+ end
+
+ Gem::PubGrub::Incompatibility.new(
+ [Gem::PubGrub::Term.new(self_constraint, true), Gem::PubGrub::Term.new(dep_constraint, false)],
+ cause: :dependency
+ )
+ end
+ end
+
+ ##
+ # Returns the gems in +specs+ that match the local platform.
+
+ def select_local_platforms(specs) # :nodoc:
+ specs.select do |spec|
+ Gem::Platform.installable? spec
+ end
+ end
+
+ private
+
+ def package_for(name)
+ @packages[name] ||= Gem::PubGrub::Package.new(name)
+ end
+
+ def root_dependencies
+ deps = {}
+ @needed.each do |dep|
+ constraint = Gem::PubGrub::RubyGems.requirement_to_constraint(package_for(dep.name), dep.requirement)
+ deps[dep.name] = deps.key?(dep.name) ? deps[dep.name].intersect(constraint) : constraint
+ end
+ deps
+ end
+
+ # Only the min bound is inspected: `~>` synthesises a max like `X.A`
+ # whose suffix looks prerelease to Gem::Version but is not the user's
+ # intent, so checking max would mis-admit prereleases for every `~>`.
+ def range_admits_prerelease?(range)
+ range.ranges.any? do |r|
+ next false if r.empty?
+ r.min&.prerelease?
+ end
+ end
+
+ def find_unfiltered_specs_for(name)
+ dep = Gem::Dependency.new(name, ">= 0.a")
+ dep_request = DependencyRequest.new(dep, nil)
+ @set.find_all(dep_request)
+ end
+
+ def filter_specs(specs)
+ filtered = select_local_platforms(specs)
+
+ unless @soft_missing
+ filtered = filtered.select do |s|
+ s.required_ruby_version.satisfied_by?(Gem.ruby_version) &&
+ s.required_rubygems_version.satisfied_by?(Gem.rubygems_version)
+ rescue StandardError
+ true
+ end
+ end
+
+ filtered
+ end
+
+ def spec_for(name, version)
+ @spec_for_cache[name][version]
+ end
+
+ def build_spec_for_cache(name)
+ # Rank sources by the order they were first supplied so that, when multiple
+ # sources offer the same version and platform, the earlier source wins.
+ source_rank = {}
+ @all_specs[name].each do |s|
+ source_rank[s.source] ||= source_rank.size
+ end
+
+ @all_specs[name].group_by(&:version).transform_values do |candidates|
+ next candidates.first if candidates.length == 1
+
+ # Prefer already-installed specs to avoid unnecessary downloads
+ installed = candidates.select {|s| s.is_a?(Gem::Resolver::InstalledSpecification) }
+ next installed.first if installed.length == 1
+ candidates = installed if installed.any?
+
+ # Among remaining candidates, prefer the most specific platform, then the
+ # earlier-supplied source.
+ candidates.min_by do |s|
+ [Gem::Platform.platform_specificity_match(s.platform, Gem::Platform.local),
+ source_rank[s.source]]
+ end
+ end
+ end
+
+ def compute_dependencies(package, version)
+ spec = spec_for(package.to_s, version)
+ return {} unless spec
+ return {} if @ignore_dependencies
+
+ spec.fetch_development_dependencies if @development && spec.respond_to?(:fetch_development_dependencies)
+
+ deps = {}
+ root_names = @needed.map(&:name)
+
+ spec.dependencies.each do |d|
+ next if d.name == package.to_s
+ next if d.type == :development && !@development
+ next if d.type == :development && @development_shallow && !root_names.include?(package.to_s)
+
+ dep_package = package_for(d.name)
+
+ # In force mode, skip deps that can't be satisfied - either no
+ # specs at all, or no specs matching the version requirement.
+ if @soft_missing
+ dep_specs = @all_specs[d.name]
+ matching = dep_specs.select {|s| d.requirement.satisfied_by?(s.version) }
+ next if matching.empty?
+ end
+
+ deps[d.name] = Gem::PubGrub::RubyGems.requirement_to_constraint(dep_package, d.requirement)
+ end
+
+ deps
+ end
+
+ def build_extended_explanation(name, constraint)
+ unfiltered = @unfiltered_specs[name]
+ return if unfiltered.empty?
+
+ filtered = @all_specs[name]
+ pkg = package_for(name)
+
+ # A prerelease hint applies when the source would strip prereleases for
+ # this constraint (global prerelease flag off and the constraint's range
+ # doesn't itself reach into prerelease territory) AND a prerelease of
+ # the gem exists somewhere.
+ prerelease_gated = !(@set.respond_to?(:prerelease) && @set.prerelease) &&
+ !range_admits_prerelease?(constraint.range)
+ has_prerelease_candidate = prerelease_gated &&
+ @all_versions[pkg].any?(&:prerelease?)
+
+ return if filtered.length == unfiltered.length && !has_prerelease_candidate
+
+ hints = []
+
+ # Check for specs that exist for other platforms
+ platform_specs = unfiltered.select do |s|
+ !Gem::Platform.installable?(s) && constraint.range.include?(s.version)
+ end
+ if platform_specs.any?
+ label = "#{name} (#{constraint.constraint_string})"
+ hints << "The source contains the following gems matching '#{label}':"
+ platform_specs.each do |s|
+ actual = s.respond_to?(:spec) ? s.spec : s
+ hints << " * #{actual.full_name}"
+ end
+ end
+
+ # Check for specs filtered by Ruby version
+ installable = select_local_platforms(unfiltered)
+ ruby_specs = installable.select do |s|
+ actual = s.respond_to?(:spec) ? s.spec : s
+ constraint.range.include?(s.version) &&
+ !actual.required_ruby_version.satisfied_by?(Gem.ruby_version)
+ rescue StandardError
+ false
+ end
+ if ruby_specs.any?
+ versions = ruby_specs.map(&:version).uniq.sort.reverse.first(3)
+ sample = ruby_specs.find {|s| s.version == versions.first }
+ actual = sample.respond_to?(:spec) ? sample.spec : sample
+ ruby_req = actual.required_ruby_version
+ hints << "#{name} #{versions.join(", ")} requires Ruby #{ruby_req} (you have #{Gem.ruby_version})"
+ end
+
+ # Check for specs filtered by prerelease status
+ if prerelease_gated
+ prerelease_versions = @all_versions[pkg].select(&:prerelease?)
+ if prerelease_versions.any?
+ versions = prerelease_versions.sort.reverse.first(3) # limit to avoid cluttering error output
+ hints << "#{name} #{versions.join(", ")} are pre-release versions. Use --prerelease to allow pre-release gems."
+ end
+ end
+
+ hints.empty? ? nil : hints.join("\n")
+ end
+
+ def extract_extended_explanation(incompatibility)
+ while incompatibility.cause.is_a?(Gem::PubGrub::Incompatibility::ConflictCause)
+ cause = incompatibility.cause
+
+ [cause.conflict, cause.other].each do |incompat|
+ if incompat.cause.is_a?(Gem::PubGrub::Incompatibility::NoVersions) &&
+ incompat.respond_to?(:extended_explanation) &&
+ incompat.extended_explanation
+ return incompat.extended_explanation
+ end
+ end
+
+ incompatibility = cause.conflict
+ end
+
+ nil
+ end
+
+ def make_logger
+ DEBUG_RESOLVER ? Gem::PubGrub::StderrLogger.new : Gem::PubGrub::NullLogger.new
+ end
+
+ # Custom root package so error messages say "your request depends on..."
+ # instead of PubGrub's default "root depends on...".
+ class RootPackage < Gem::PubGrub::Package
+ def initialize
+ super(:root)
+ end
+
+ def root?
+ true
+ end
+
+ def to_s
+ "your request"
+ end
+ end
+end
+
+require_relative "resolver/activation_request"
+require_relative "resolver/dependency_request"
+require_relative "resolver/incompatibility"
+require_relative "resolver/strategy"
+require_relative "resolver/requirement_list"
+require_relative "resolver/set"
+require_relative "resolver/api_set"
+require_relative "resolver/composed_set"
+require_relative "resolver/best_set"
+require_relative "resolver/current_set"
+require_relative "resolver/git_set"
+require_relative "resolver/index_set"
+require_relative "resolver/installer_set"
+require_relative "resolver/lock_set"
+require_relative "resolver/vendor_set"
+require_relative "resolver/source_set"
+
+require_relative "resolver/specification"
+require_relative "resolver/spec_specification"
+require_relative "resolver/api_specification"
+require_relative "resolver/git_specification"
+require_relative "resolver/index_specification"
+require_relative "resolver/installed_specification"
+require_relative "resolver/local_specification"
+require_relative "resolver/lock_specification"
+require_relative "resolver/vendor_specification"
diff --git a/lib/rubygems/resolver/activation_request.rb b/lib/rubygems/resolver/activation_request.rb
new file mode 100644
index 0000000000..5c722001b1
--- /dev/null
+++ b/lib/rubygems/resolver/activation_request.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+##
+# Specifies a Specification object that should be activated. Also contains a
+# dependency that was used to introduce this activation.
+
+class Gem::Resolver::ActivationRequest
+ ##
+ # The parent request for this activation request.
+
+ attr_reader :request
+
+ ##
+ # The specification to be activated.
+
+ attr_reader :spec
+
+ ##
+ # Creates a new ActivationRequest that will activate +spec+. The parent
+ # +request+ is used to provide diagnostics in case of conflicts.
+
+ def initialize(spec, request)
+ @spec = spec
+ @request = request
+ end
+
+ def ==(other) # :nodoc:
+ case other
+ when Gem::Specification
+ @spec == other
+ when Gem::Resolver::ActivationRequest
+ @spec == other.spec
+ else
+ false
+ end
+ end
+
+ def eql?(other)
+ self == other
+ end
+
+ def hash
+ @spec.hash
+ end
+
+ ##
+ # Is this activation request for a development dependency?
+
+ def development?
+ @request.development?
+ end
+
+ ##
+ # Downloads a gem at +path+ and returns the file path.
+
+ def download(path)
+ Gem.ensure_gem_subdirectories path
+
+ if @spec.respond_to? :sources
+ exception = nil
+ path = @spec.sources.find do |source|
+ source.download full_spec, path
+ rescue exception
+ end
+ return path if path
+ raise exception if exception
+
+ elsif @spec.respond_to? :source
+ source = @spec.source
+ source.download full_spec, path
+
+ else
+ source = Gem.sources.first
+ source.download full_spec, path
+ end
+ end
+
+ ##
+ # The full name of the specification to be activated.
+
+ def full_name
+ name_tuple.full_name
+ end
+
+ alias_method :to_s, :full_name
+
+ ##
+ # The Gem::Specification for this activation request.
+
+ def full_spec
+ Gem::Specification === @spec ? @spec : @spec.spec
+ end
+
+ def inspect # :nodoc:
+ format("#<%s for %p from %s>", self.class, @spec, @request)
+ end
+
+ ##
+ # True if the requested gem has already been installed.
+
+ def installed?
+ case @spec
+ when Gem::Resolver::VendorSpecification then
+ true
+ else
+ this_spec = full_spec
+
+ Gem::Specification.any? do |s|
+ s == this_spec && s.base_dir == this_spec.base_dir
+ end
+ end
+ end
+
+ ##
+ # The name of this activation request's specification
+
+ def name
+ @spec.name
+ end
+
+ ##
+ # Return the ActivationRequest that contained the dependency
+ # that we were activated for.
+
+ def parent
+ @request.requester
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[Activation request", "]" do
+ q.breakable
+ q.pp @spec
+
+ q.breakable
+ q.text " for "
+ q.pp @request
+ end
+ end
+
+ ##
+ # The version of this activation request's specification
+
+ def version
+ @spec.version
+ end
+
+ ##
+ # The platform of this activation request's specification
+
+ def platform
+ @spec.platform
+ end
+
+ private
+
+ def name_tuple
+ @name_tuple ||= Gem::NameTuple.new(name, version, platform)
+ end
+end
diff --git a/lib/rubygems/resolver/api_set.rb b/lib/rubygems/resolver/api_set.rb
new file mode 100644
index 0000000000..3f443519d8
--- /dev/null
+++ b/lib/rubygems/resolver/api_set.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+##
+# The global rubygems pool, available via the Compact Index API.
+# Returns instances of APISpecification.
+
+class Gem::Resolver::APISet < Gem::Resolver::Set
+ autoload :GemParser, File.expand_path("api_set/gem_parser", __dir__)
+
+ ##
+ # The URI for the Compact Index API this APISet uses.
+
+ attr_reader :dep_uri # :nodoc:
+
+ ##
+ # The Gem::Source that gems are fetched from
+
+ attr_reader :source
+
+ ##
+ # The corresponding place to fetch gems.
+
+ attr_reader :uri
+
+ ##
+ # Creates a new APISet that will retrieve gems from +uri+ using the Compact
+ # Index API URL +dep_uri+ which is described at
+ # https://guides.rubygems.org/rubygems-org-compact-index-api
+
+ def initialize(dep_uri = "https://index.rubygems.org/info/")
+ super()
+
+ dep_uri = Gem::URI dep_uri unless Gem::URI === dep_uri
+
+ @dep_uri = dep_uri
+ @uri = dep_uri + ".."
+
+ @data = Hash.new {|h,k| h[k] = [] }
+ @source = Gem::Source.new @uri
+
+ @to_fetch = []
+ end
+
+ ##
+ # Return an array of APISpecification objects matching
+ # DependencyRequest +req+.
+
+ def find_all(req)
+ res = []
+
+ return res unless @remote
+
+ if @to_fetch.include?(req.name)
+ prefetch_now
+ end
+
+ versions(req.name).each do |ver|
+ if req.dependency.match? req.name, ver[:number], @prerelease
+ res << Gem::Resolver::APISpecification.new(self, ver)
+ end
+ end
+
+ res
+ end
+
+ ##
+ # A hint run by the resolver to allow the Set to fetch
+ # data for DependencyRequests +reqs+.
+
+ def prefetch(reqs)
+ return unless @remote
+ names = reqs.map {|r| r.dependency.name }
+ needed = names - @data.keys - @to_fetch
+
+ @to_fetch += needed
+ end
+
+ def prefetch_now # :nodoc:
+ needed = @to_fetch
+ @to_fetch = []
+
+ needed.sort.each do |name|
+ versions(name)
+ end
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[APISet", "]" do
+ q.breakable
+ q.text "URI: #{@dep_uri}"
+
+ q.breakable
+ q.text "gem names:"
+ q.pp @data.keys
+ end
+ end
+
+ ##
+ # Return data for all versions of the gem +name+.
+
+ def versions(name) # :nodoc:
+ if @data.key?(name)
+ return @data[name]
+ end
+
+ uri = @dep_uri + name
+
+ begin
+ str = Gem::RemoteFetcher.fetcher.fetch_path uri
+ rescue Gem::RemoteFetcher::FetchError
+ @data[name] = []
+ else
+ lines(str).each do |ver|
+ number, platform, dependencies, requirements = parse_gem(ver)
+
+ platform ||= "ruby"
+ dependencies = dependencies.map {|dep_name, reqs| [dep_name, reqs.join(", ")] }
+ requirements = requirements.map {|req_name, reqs| [req_name.to_sym, reqs] }.to_h
+
+ @data[name] << { name: name, number: number, platform: platform, dependencies: dependencies, requirements: requirements }
+ end
+ end
+
+ @data[name]
+ end
+
+ private
+
+ def lines(str)
+ lines = str.split("\n")
+ header = lines.index("---")
+ header ? lines[header + 1..-1] : lines
+ end
+
+ def parse_gem(string)
+ @gem_parser ||= GemParser.new
+ @gem_parser.parse(string)
+ end
+end
diff --git a/lib/rubygems/resolver/api_set/gem_parser.rb b/lib/rubygems/resolver/api_set/gem_parser.rb
new file mode 100644
index 0000000000..4d827f4980
--- /dev/null
+++ b/lib/rubygems/resolver/api_set/gem_parser.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Gem::Resolver::APISet::GemParser
+ def parse(line)
+ version_and_platform, rest = line.split(" ", 2)
+ version, platform = version_and_platform.split("-", 2)
+ dependencies, requirements = rest.split("|", 2).map! {|s| s.split(",") } if rest
+ dependencies = dependencies ? dependencies.map! {|d| parse_dependency(d) } : []
+ requirements = requirements ? requirements.map! {|d| parse_dependency(d) } : []
+ [version, platform, dependencies, requirements]
+ end
+
+ private
+
+ def parse_dependency(string)
+ dependency = string.split(":", 2)
+ dependency[-1] = dependency[-1].split("&") if dependency.size > 1
+ dependency[0] = -dependency[0]
+ dependency
+ end
+end
diff --git a/lib/rubygems/resolver/api_specification.rb b/lib/rubygems/resolver/api_specification.rb
new file mode 100644
index 0000000000..ccfd6fe084
--- /dev/null
+++ b/lib/rubygems/resolver/api_specification.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+##
+# Represents a specification retrieved via the Compact Index API.
+#
+# This is used to avoid loading the full Specification object when all we need
+# is the name, version, and dependencies.
+
+class Gem::Resolver::APISpecification < Gem::Resolver::Specification
+ ##
+ # We assume that all instances of this class are immutable;
+ # so avoid duplicated generation for performance.
+ @@cache = {}
+ def self.new(set, api_data)
+ cache_key = [set, api_data]
+ cache = @@cache[cache_key]
+ return cache if cache
+ @@cache[cache_key] = super
+ end
+
+ ##
+ # Creates an APISpecification for the given +set+ from the Compact Index API
+ # +api_data+.
+ #
+ # See https://guides.rubygems.org/rubygems-org-compact-index-api for the
+ # format of the +api_data+.
+
+ def initialize(set, api_data)
+ super()
+
+ @set = set
+ @name = api_data[:name]
+ @version = Gem::Version.new(api_data[:number]).freeze
+ @platform = Gem::Platform.new(api_data[:platform]).freeze
+ @original_platform = api_data[:platform].freeze
+ @dependencies = api_data[:dependencies].map do |name, ver|
+ Gem::Dependency.new(name, ver.split(/\s*,\s*/)).freeze
+ end.freeze
+ @required_ruby_version = Gem::Requirement.new(api_data.dig(:requirements, :ruby)).freeze
+ @required_rubygems_version = Gem::Requirement.new(api_data.dig(:requirements, :rubygems)).freeze
+ end
+
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @set == other.set &&
+ @name == other.name &&
+ @version == other.version &&
+ @platform == other.platform
+ end
+
+ def hash
+ @set.hash ^ @name.hash ^ @version.hash ^ @platform.hash
+ end
+
+ def fetch_development_dependencies # :nodoc:
+ spec = source.fetch_spec Gem::NameTuple.new @name, @version, @platform
+
+ @dependencies = spec.dependencies
+ end
+
+ def installable_platform? # :nodoc:
+ Gem::Platform.match_gem? @platform, @name
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[APISpecification", "]" do
+ q.breakable
+ q.text "name: #{name}"
+
+ q.breakable
+ q.text "version: #{version}"
+
+ q.breakable
+ q.text "platform: #{platform}"
+
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp @dependencies
+
+ q.breakable
+ q.text "set uri: #{@set.dep_uri}"
+ end
+ end
+
+ ##
+ # Fetches a Gem::Specification for this APISpecification.
+
+ def spec # :nodoc:
+ @spec ||=
+ begin
+ tuple = Gem::NameTuple.new @name, @version, @platform
+ source.fetch_spec tuple
+ rescue Gem::RemoteFetcher::FetchError
+ raise if @original_platform == @platform
+
+ tuple = Gem::NameTuple.new @name, @version, @original_platform
+ source.fetch_spec tuple
+ end
+ end
+
+ def source # :nodoc:
+ @set.source
+ end
+end
diff --git a/lib/rubygems/resolver/best_set.rb b/lib/rubygems/resolver/best_set.rb
new file mode 100644
index 0000000000..e647a2c11b
--- /dev/null
+++ b/lib/rubygems/resolver/best_set.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+##
+# The BestSet chooses the best available method to query a remote index.
+#
+# It combines IndexSet and APISet
+
+class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet
+ ##
+ # Creates a BestSet for the given +sources+ or Gem::sources if none are
+ # specified. +sources+ must be a Gem::SourceList.
+
+ def initialize(sources = Gem.sources)
+ super()
+
+ @sources = sources
+ end
+
+ ##
+ # Picks which sets to use for the configured sources.
+
+ def pick_sets # :nodoc:
+ @sources.each_source do |source|
+ @sets << source.dependency_resolver_set(@prerelease)
+ end
+ end
+
+ def find_all(req) # :nodoc:
+ pick_sets if @remote && @sets.empty?
+
+ super
+ end
+
+ def prefetch(reqs) # :nodoc:
+ pick_sets if @remote && @sets.empty?
+
+ super
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[BestSet", "]" do
+ q.breakable
+ q.text "sets:"
+
+ q.breakable
+ q.pp @sets
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/composed_set.rb b/lib/rubygems/resolver/composed_set.rb
new file mode 100644
index 0000000000..e67dd41754
--- /dev/null
+++ b/lib/rubygems/resolver/composed_set.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+##
+# A ComposedSet allows multiple sets to be queried like a single set.
+#
+# To create a composed set with any number of sets use:
+#
+# Gem::Resolver.compose_sets set1, set2
+#
+# This method will eliminate nesting of composed sets.
+
+class Gem::Resolver::ComposedSet < Gem::Resolver::Set
+ attr_reader :sets # :nodoc:
+
+ ##
+ # Creates a new ComposedSet containing +sets+. Use
+ # Gem::Resolver::compose_sets instead.
+
+ def initialize(*sets)
+ super()
+
+ @sets = sets
+ end
+
+ ##
+ # When +allow_prerelease+ is set to +true+ prereleases gems are allowed to
+ # match dependencies.
+
+ def prerelease=(allow_prerelease)
+ super
+
+ sets.each do |set|
+ set.prerelease = allow_prerelease
+ end
+ end
+
+ ##
+ # Sets the remote network access for all composed sets.
+
+ def remote=(remote)
+ super
+
+ @sets.each {|set| set.remote = remote }
+ end
+
+ def errors
+ @errors + @sets.flat_map(&:errors)
+ end
+
+ ##
+ # Finds all specs matching +req+ in all sets.
+
+ def find_all(req)
+ @sets.flat_map do |s|
+ s.find_all req
+ end
+ end
+
+ ##
+ # Prefetches +reqs+ in all sets.
+
+ def prefetch(reqs)
+ @sets.each {|s| s.prefetch(reqs) }
+ end
+end
diff --git a/lib/rubygems/resolver/current_set.rb b/lib/rubygems/resolver/current_set.rb
new file mode 100644
index 0000000000..370e445089
--- /dev/null
+++ b/lib/rubygems/resolver/current_set.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+##
+# A set which represents the installed gems. Respects
+# all the normal settings that control where to look
+# for installed gems.
+
+class Gem::Resolver::CurrentSet < Gem::Resolver::Set
+ def find_all(req)
+ req.dependency.matching_specs
+ end
+end
diff --git a/lib/rubygems/resolver/dependency_request.rb b/lib/rubygems/resolver/dependency_request.rb
new file mode 100644
index 0000000000..60b338277f
--- /dev/null
+++ b/lib/rubygems/resolver/dependency_request.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+##
+# Used Internally. Wraps a Dependency object to also track which spec
+# contained the Dependency.
+
+class Gem::Resolver::DependencyRequest
+ ##
+ # The wrapped Gem::Dependency
+
+ attr_reader :dependency
+
+ ##
+ # The request for this dependency.
+
+ attr_reader :requester
+
+ ##
+ # Creates a new DependencyRequest for +dependency+ from +requester+.
+ # +requester may be nil if the request came from a user.
+
+ def initialize(dependency, requester)
+ @dependency = dependency
+ @requester = requester
+ end
+
+ def ==(other) # :nodoc:
+ case other
+ when Gem::Dependency
+ @dependency == other
+ when Gem::Resolver::DependencyRequest
+ @dependency == other.dependency
+ else
+ false
+ end
+ end
+
+ ##
+ # Is this dependency a development dependency?
+
+ def development?
+ @dependency.type == :development
+ end
+
+ ##
+ # Does this dependency request match +spec+?
+ #
+ # NOTE: #match? only matches prerelease versions when #dependency is a
+ # prerelease dependency.
+
+ def match?(spec, allow_prerelease = false)
+ @dependency.match? spec, nil, allow_prerelease
+ end
+
+ ##
+ # Does this dependency request match +spec+?
+ #
+ # NOTE: #matches_spec? matches prerelease versions. See also #match?
+
+ def matches_spec?(spec)
+ @dependency.matches_spec? spec
+ end
+
+ ##
+ # The name of the gem this dependency request is requesting.
+
+ def name
+ @dependency.name
+ end
+
+ def type
+ @dependency.type
+ end
+
+ ##
+ # Indicate that the request is for a gem explicitly requested by the user
+
+ def explicit?
+ @requester.nil?
+ end
+
+ ##
+ # Indicate that the request is for a gem requested as a dependency of
+ # another gem
+
+ def implicit?
+ !explicit?
+ end
+
+ ##
+ # Return a String indicating who caused this request to be added (only
+ # valid for implicit requests)
+
+ def request_context
+ @requester ? @requester.request : "(unknown)"
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[Dependency request ", "]" do
+ q.breakable
+ q.text @dependency.to_s
+
+ q.breakable
+ q.text " requested by "
+ q.pp @requester
+ end
+ end
+
+ ##
+ # The version requirement for this dependency request
+
+ def requirement
+ @dependency.requirement
+ end
+
+ def to_s # :nodoc:
+ @dependency.to_s
+ end
+end
diff --git a/lib/rubygems/resolver/git_set.rb b/lib/rubygems/resolver/git_set.rb
new file mode 100644
index 0000000000..2912378fe7
--- /dev/null
+++ b/lib/rubygems/resolver/git_set.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+##
+# A GitSet represents gems that are sourced from git repositories.
+#
+# This is used for gem dependency file support.
+#
+# Example:
+#
+# set = Gem::Resolver::GitSet.new
+# set.add_git_gem 'rake', 'git://example/rake.git', tag: 'rake-10.1.0'
+
+class Gem::Resolver::GitSet < Gem::Resolver::Set
+ ##
+ # The root directory for git gems in this set. This is usually Gem.dir, the
+ # installation directory for regular gems.
+
+ attr_accessor :root_dir
+
+ ##
+ # Contains repositories needing submodules
+
+ attr_reader :need_submodules # :nodoc:
+
+ ##
+ # A Hash containing git gem names for keys and a Hash of repository and
+ # git commit reference as values.
+
+ attr_reader :repositories # :nodoc:
+
+ ##
+ # A hash of gem names to Gem::Resolver::GitSpecifications
+
+ attr_reader :specs # :nodoc:
+
+ def initialize # :nodoc:
+ super()
+
+ @need_submodules = {}
+ @repositories = {}
+ @root_dir = Gem.dir
+ @specs = {}
+ end
+
+ def add_git_gem(name, repository, reference, submodules) # :nodoc:
+ @repositories[name] = [repository, reference]
+ @need_submodules[repository] = submodules
+ end
+
+ ##
+ # Adds and returns a GitSpecification with the given +name+ and +version+
+ # which came from a +repository+ at the given +reference+. If +submodules+
+ # is true they are checked out along with the repository.
+ #
+ # This fills in the prefetch information as enough information about the gem
+ # is present in the arguments.
+
+ def add_git_spec(name, version, repository, reference, submodules) # :nodoc:
+ add_git_gem name, repository, reference, submodules
+
+ source = Gem::Source::Git.new name, repository, reference
+ source.root_dir = @root_dir
+
+ spec = Gem::Specification.new do |s|
+ s.name = name
+ s.version = version
+ end
+
+ git_spec = Gem::Resolver::GitSpecification.new self, spec, source
+
+ @specs[spec.name] = git_spec
+
+ git_spec
+ end
+
+ ##
+ # Finds all git gems matching +req+
+
+ def find_all(req)
+ prefetch nil
+
+ specs.values.select do |spec|
+ req.match? spec
+ end
+ end
+
+ ##
+ # Prefetches specifications from the git repositories in this set.
+
+ def prefetch(reqs)
+ return unless @specs.empty?
+
+ @repositories.each do |name, (repository, reference)|
+ source = Gem::Source::Git.new name, repository, reference
+ source.root_dir = @root_dir
+ source.remote = @remote
+
+ source.specs.each do |spec|
+ git_spec = Gem::Resolver::GitSpecification.new self, spec, source
+
+ @specs[spec.name] = git_spec
+ end
+ end
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[GitSet", "]" do
+ next if @repositories.empty?
+ q.breakable
+
+ repos = @repositories.map do |name, (repository, reference)|
+ "#{name}: #{repository}@#{reference}"
+ end
+
+ q.seplist repos do |repo|
+ q.text repo
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/git_specification.rb b/lib/rubygems/resolver/git_specification.rb
new file mode 100644
index 0000000000..e587c17d2a
--- /dev/null
+++ b/lib/rubygems/resolver/git_specification.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+##
+# A GitSpecification represents a gem that is sourced from a git repository
+# and is being loaded through a gem dependencies file through the +git:+
+# option.
+
+class Gem::Resolver::GitSpecification < Gem::Resolver::SpecSpecification
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @set == other.set &&
+ @spec == other.spec &&
+ @source == other.source
+ end
+
+ def add_dependency(dependency) # :nodoc:
+ spec.dependencies << dependency
+ end
+
+ ##
+ # Installing a git gem only involves building the extensions and generating
+ # the executables.
+
+ def install(options = {})
+ require_relative "../installer"
+
+ installer = Gem::Installer.for_spec spec, options
+
+ yield installer if block_given?
+
+ installer.run_pre_install_hooks
+ installer.build_extensions
+ installer.run_post_build_hooks
+ installer.generate_bin
+ installer.run_post_install_hooks
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[GitSpecification", "]" do
+ q.breakable
+ q.text "name: #{name}"
+
+ q.breakable
+ q.text "version: #{version}"
+
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp dependencies
+
+ q.breakable
+ q.text "source:"
+ q.breakable
+ q.pp @source
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/incompatibility.rb b/lib/rubygems/resolver/incompatibility.rb
new file mode 100644
index 0000000000..57a60affb4
--- /dev/null
+++ b/lib/rubygems/resolver/incompatibility.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class Gem::Resolver::Incompatibility < Gem::PubGrub::Incompatibility
+ attr_reader :extended_explanation
+
+ def initialize(terms, cause:, custom_explanation: nil, extended_explanation: nil)
+ @extended_explanation = extended_explanation
+ super(terms, cause: cause, custom_explanation: custom_explanation)
+ end
+end
diff --git a/lib/rubygems/resolver/index_set.rb b/lib/rubygems/resolver/index_set.rb
new file mode 100644
index 0000000000..cddaf8773f
--- /dev/null
+++ b/lib/rubygems/resolver/index_set.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+##
+# The global rubygems pool represented via the traditional
+# source index.
+
+class Gem::Resolver::IndexSet < Gem::Resolver::Set
+ def initialize(source = nil) # :nodoc:
+ super()
+
+ @f =
+ if source
+ sources = Gem::SourceList.from [source]
+
+ Gem::SpecFetcher.new sources
+ else
+ Gem::SpecFetcher.fetcher
+ end
+
+ @all = Hash.new {|h,k| h[k] = [] }
+
+ list, errors = @f.available_specs :complete
+
+ @errors.concat errors
+
+ list.each do |uri, specs|
+ specs.each do |n|
+ @all[n.name] << [uri, n]
+ end
+ end
+
+ @specs = {}
+ end
+
+ ##
+ # Return an array of IndexSpecification objects matching
+ # DependencyRequest +req+.
+
+ def find_all(req)
+ res = []
+
+ return res unless @remote
+
+ name = req.dependency.name
+
+ @all[name].each do |uri, n|
+ next unless req.match? n, @prerelease
+ res << Gem::Resolver::IndexSpecification.new(
+ self, n.name, n.version, uri, n.platform
+ )
+ end
+
+ res
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[IndexSet", "]" do
+ q.breakable
+ q.text "sources:"
+ q.breakable
+ q.pp @f.sources
+
+ q.breakable
+ q.text "specs:"
+
+ q.breakable
+
+ names = @all.values.flat_map do |tuples|
+ tuples.map do |_, tuple|
+ tuple.full_name
+ end
+ end
+
+ q.seplist names do |name|
+ q.text name
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/index_specification.rb b/lib/rubygems/resolver/index_specification.rb
new file mode 100644
index 0000000000..7b95608071
--- /dev/null
+++ b/lib/rubygems/resolver/index_specification.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+##
+# Represents a possible Specification object returned from IndexSet. Used to
+# delay needed to download full Specification objects when only the +name+
+# and +version+ are needed.
+
+class Gem::Resolver::IndexSpecification < Gem::Resolver::Specification
+ ##
+ # An IndexSpecification is created from the index format described in `gem
+ # help generate_index`.
+ #
+ # The +set+ contains other specifications for this (URL) +source+.
+ #
+ # The +name+, +version+ and +platform+ are the name, version and platform of
+ # the gem.
+
+ def initialize(set, name, version, source, platform)
+ super()
+
+ @set = set
+ @name = name
+ @version = version
+ @source = source
+ @platform = Gem::Platform.new(platform.to_s)
+ @original_platform = platform.to_s
+
+ @spec = nil
+ end
+
+ ##
+ # The dependencies of the gem for this specification
+
+ def dependencies
+ spec.dependencies
+ end
+
+ ##
+ # The required_ruby_version constraint for this specification
+ #
+ # A fallback is included because when generated, some marshalled specs have it
+ # set to +nil+.
+
+ def required_ruby_version
+ spec.required_ruby_version || Gem::Requirement.default
+ end
+
+ ##
+ # The required_rubygems_version constraint for this specification
+ #
+ # A fallback is included because the original version of the specification
+ # API didn't include that field, so some marshalled specs in the index have it
+ # set to +nil+.
+
+ def required_rubygems_version
+ spec.required_rubygems_version || Gem::Requirement.default
+ end
+
+ def ==(other)
+ self.class === other &&
+ @name == other.name &&
+ @version == other.version &&
+ @platform == other.platform
+ end
+
+ def hash
+ @name.hash ^ @version.hash ^ @platform.hash
+ end
+
+ def inspect # :nodoc:
+ format("#<%s %s source %s>", self.class, full_name, @source)
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[Index specification", "]" do
+ q.breakable
+ q.text full_name
+
+ unless @platform == Gem::Platform::RUBY
+ q.breakable
+ q.text @platform.to_s
+ end
+
+ q.breakable
+ q.text "source "
+ q.pp @source
+ end
+ end
+
+ ##
+ # Fetches a Gem::Specification for this IndexSpecification from the #source.
+
+ def spec # :nodoc:
+ @spec ||=
+ begin
+ tuple = Gem::NameTuple.new @name, @version, @original_platform
+
+ @source.fetch_spec tuple
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/installed_specification.rb b/lib/rubygems/resolver/installed_specification.rb
new file mode 100644
index 0000000000..8280ae4672
--- /dev/null
+++ b/lib/rubygems/resolver/installed_specification.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+##
+# An InstalledSpecification represents a gem that is already installed
+# locally.
+
+class Gem::Resolver::InstalledSpecification < Gem::Resolver::SpecSpecification
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @set == other.set &&
+ @spec == other.spec
+ end
+
+ ##
+ # This is a null install as this specification is already installed.
+ # +options+ are ignored.
+
+ def install(options = {})
+ yield nil
+ end
+
+ ##
+ # Returns +true+ if this gem is installable for the current platform.
+
+ def installable_platform?
+ # BACKCOMPAT If the file is coming out of a specified file, then we
+ # ignore the platform. This code can be removed in RG 3.0.
+ return true if @source.is_a? Gem::Source::SpecificFile
+
+ super
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[InstalledSpecification", "]" do
+ q.breakable
+ q.text "name: #{name}"
+
+ q.breakable
+ q.text "version: #{version}"
+
+ q.breakable
+ q.text "platform: #{platform}"
+
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp spec.dependencies
+ end
+ end
+
+ ##
+ # The source for this specification
+
+ def source
+ @source ||= Gem::Source::Installed.new
+ end
+end
diff --git a/lib/rubygems/resolver/installer_set.rb b/lib/rubygems/resolver/installer_set.rb
new file mode 100644
index 0000000000..42ce0890e2
--- /dev/null
+++ b/lib/rubygems/resolver/installer_set.rb
@@ -0,0 +1,271 @@
+# frozen_string_literal: true
+
+##
+# A set of gems for installation sourced from remote sources and local .gem
+# files
+
+class Gem::Resolver::InstallerSet < Gem::Resolver::Set
+ ##
+ # List of Gem::Specification objects that must always be installed.
+
+ attr_reader :always_install # :nodoc:
+
+ ##
+ # Only install gems in the always_install list
+
+ attr_accessor :ignore_dependencies # :nodoc:
+
+ ##
+ # Do not look in the installed set when finding specifications. This is
+ # used by the --install-dir option to `gem install`
+
+ attr_accessor :ignore_installed # :nodoc:
+
+ ##
+ # The remote_set looks up remote gems for installation.
+
+ attr_reader :remote_set # :nodoc:
+
+ ##
+ # Ignore ruby & rubygems specification constraints.
+ #
+
+ attr_accessor :force # :nodoc:
+
+ ##
+ # Creates a new InstallerSet that will look for gems in +domain+.
+
+ def initialize(domain)
+ super()
+
+ @domain = domain
+
+ @f = Gem::SpecFetcher.fetcher
+
+ @always_install = []
+ @ignore_dependencies = false
+ @ignore_installed = false
+ @local = {}
+ @local_source = Gem::Source::Local.new
+ @remote_set = Gem::Resolver::BestSet.new
+ @force = false
+ @specs = {}
+ end
+
+ ##
+ # Looks up the latest specification for +dependency+ and adds it to the
+ # always_install list.
+
+ def add_always_install(dependency)
+ request = Gem::Resolver::DependencyRequest.new dependency, nil
+
+ found = find_all request
+
+ found.delete_if do |s|
+ s.version.prerelease? && !s.local?
+ end unless dependency.prerelease?
+
+ found = found.select do |s|
+ Gem::Source::SpecificFile === s.source ||
+ Gem::Platform.match_spec?(s)
+ end
+
+ found = found.sort_by do |s|
+ [s.version, Gem::Platform.sort_priority(s.platform)]
+ end
+
+ newest = found.last
+
+ unless newest
+ exc = Gem::UnsatisfiableDependencyError.new request
+ exc.errors = errors
+
+ raise exc
+ end
+
+ unless @force
+ found_matching_metadata = found.reverse.find do |spec|
+ metadata_satisfied?(spec)
+ end
+
+ if found_matching_metadata.nil?
+ ensure_required_ruby_version_met(newest.spec)
+ ensure_required_rubygems_version_met(newest.spec)
+ else
+ newest = found_matching_metadata
+ end
+ end
+
+ @always_install << newest.spec
+ end
+
+ ##
+ # Adds a local gem requested using +dep_name+ with the given +spec+ that can
+ # be loaded and installed using the +source+.
+
+ def add_local(dep_name, spec, source)
+ @local[dep_name] = [spec, source]
+ end
+
+ ##
+ # Should local gems should be considered?
+
+ def consider_local? # :nodoc:
+ @domain == :both || @domain == :local
+ end
+
+ ##
+ # Should remote gems should be considered?
+
+ def consider_remote? # :nodoc:
+ @domain == :both || @domain == :remote
+ end
+
+ ##
+ # Errors encountered while resolving gems
+
+ def errors
+ @errors + @remote_set.errors
+ end
+
+ ##
+ # Returns an array of IndexSpecification objects matching DependencyRequest
+ # +req+.
+
+ def find_all(req)
+ res = []
+
+ dep = req.dependency
+
+ return res if @ignore_dependencies &&
+ @always_install.none? {|spec| dep.match? spec }
+
+ name = dep.name
+
+ dep.matching_specs.each do |gemspec|
+ next if @always_install.any? {|spec| spec.name == gemspec.name }
+
+ res << Gem::Resolver::InstalledSpecification.new(self, gemspec)
+ end unless @ignore_installed
+
+ matching_local = []
+
+ if consider_local?
+ matching_local = @local.values.select do |spec, _|
+ req.match? spec
+ end.map do |spec, source|
+ Gem::Resolver::LocalSpecification.new self, spec, source
+ end
+
+ res.concat matching_local
+
+ begin
+ @local_source.find_all_gems(name, dep.requirement).each do |local_spec|
+ res << Gem::Resolver::IndexSpecification.new(
+ self, local_spec.name, local_spec.version,
+ @local_source, local_spec.platform
+ )
+ end
+ rescue Gem::Package::FormatError
+ # ignore
+ end
+ end
+
+ res.concat @remote_set.find_all req if consider_remote? && matching_local.empty?
+
+ res
+ end
+
+ def prefetch(reqs)
+ @remote_set.prefetch(reqs) if consider_remote?
+ end
+
+ def prerelease=(allow_prerelease)
+ super
+
+ @remote_set.prerelease = allow_prerelease
+ end
+
+ def inspect # :nodoc:
+ always_install = @always_install.map(&:full_name)
+
+ format("#<%s domain: %s specs: %p always install: %p>", self.class, @domain, @specs.keys, always_install)
+ end
+
+ ##
+ # Called from IndexSpecification to get a true Specification
+ # object.
+
+ def load_spec(name, ver, platform, source) # :nodoc:
+ key = "#{name}-#{ver}-#{platform}"
+
+ @specs.fetch key do
+ tuple = Gem::NameTuple.new name, ver, platform
+
+ @specs[key] = source.fetch_spec tuple
+ end
+ end
+
+ ##
+ # Has a local gem for +dep_name+ been added to this set?
+
+ def local?(dep_name) # :nodoc:
+ spec, _ = @local[dep_name]
+
+ spec
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[InstallerSet", "]" do
+ q.breakable
+ q.text "domain: #{@domain}"
+
+ q.breakable
+ q.text "specs: "
+ q.pp @specs.keys
+
+ q.breakable
+ q.text "always install: "
+ q.pp @always_install
+ end
+ end
+
+ def remote=(remote) # :nodoc:
+ case @domain
+ when :local then
+ @domain = :both if remote
+ when :remote then
+ @domain = nil unless remote
+ when :both then
+ @domain = :local unless remote
+ end
+ end
+
+ private
+
+ def metadata_satisfied?(spec)
+ spec.required_ruby_version.satisfied_by?(Gem.ruby_version) &&
+ spec.required_rubygems_version.satisfied_by?(Gem.rubygems_version)
+ end
+
+ def ensure_required_ruby_version_met(spec) # :nodoc:
+ if rrv = spec.required_ruby_version
+ ruby_version = Gem.ruby_version
+ unless rrv.satisfied_by? ruby_version
+ raise Gem::RuntimeRequirementNotMetError,
+ "#{spec.full_name} requires Ruby version #{rrv}. The current ruby version is #{ruby_version}."
+ end
+ end
+ end
+
+ def ensure_required_rubygems_version_met(spec) # :nodoc:
+ if rrgv = spec.required_rubygems_version
+ unless rrgv.satisfied_by? Gem.rubygems_version
+ rg_version = Gem::VERSION
+ raise Gem::RuntimeRequirementNotMetError,
+ "#{spec.full_name} requires RubyGems version #{rrgv}. The current RubyGems version is #{rg_version}. " \
+ "Try 'gem update --system' to update RubyGems itself."
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/local_specification.rb b/lib/rubygems/resolver/local_specification.rb
new file mode 100644
index 0000000000..b57d40e795
--- /dev/null
+++ b/lib/rubygems/resolver/local_specification.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+##
+# A LocalSpecification comes from a .gem file on the local filesystem.
+
+class Gem::Resolver::LocalSpecification < Gem::Resolver::SpecSpecification
+ ##
+ # Returns +true+ if this gem is installable for the current platform.
+
+ def installable_platform?
+ return true if @source.is_a? Gem::Source::SpecificFile
+
+ super
+ end
+
+ def local? # :nodoc:
+ true
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[LocalSpecification", "]" do
+ q.breakable
+ q.text "name: #{name}"
+
+ q.breakable
+ q.text "version: #{version}"
+
+ q.breakable
+ q.text "platform: #{platform}"
+
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp dependencies
+
+ q.breakable
+ q.text "source: #{@source.path}"
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/lock_set.rb b/lib/rubygems/resolver/lock_set.rb
new file mode 100644
index 0000000000..e5ee32a9a6
--- /dev/null
+++ b/lib/rubygems/resolver/lock_set.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+##
+# A set of gems from a gem dependencies lockfile.
+
+class Gem::Resolver::LockSet < Gem::Resolver::Set
+ attr_reader :specs # :nodoc:
+
+ ##
+ # Creates a new LockSet from the given +sources+
+
+ def initialize(sources)
+ super()
+
+ @sources = sources.map do |source|
+ Gem::Source::Lock.new source
+ end
+
+ @specs = []
+ end
+
+ ##
+ # Creates a new IndexSpecification in this set using the given +name+,
+ # +version+ and +platform+.
+ #
+ # The specification's set will be the current set, and the source will be
+ # the current set's source.
+
+ def add(name, version, platform) # :nodoc:
+ version = Gem::Version.new version
+ specs = [
+ Gem::Resolver::LockSpecification.new(self, name, version, @sources, platform),
+ ]
+
+ @specs.concat specs
+
+ specs
+ end
+
+ ##
+ # Returns an Array of IndexSpecification objects matching the
+ # DependencyRequest +req+.
+
+ def find_all(req)
+ @specs.select do |spec|
+ req.match? spec
+ end
+ end
+
+ ##
+ # Loads a Gem::Specification with the given +name+, +version+ and
+ # +platform+. +source+ is ignored.
+
+ def load_spec(name, version, platform, source) # :nodoc:
+ dep = Gem::Dependency.new name, version
+
+ found = @specs.find do |spec|
+ dep.matches_spec?(spec) && spec.platform == platform
+ end
+
+ tuple = Gem::NameTuple.new found.name, found.version, found.platform
+
+ found.source.fetch_spec tuple
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[LockSet", "]" do
+ q.breakable
+ q.text "source:"
+
+ q.breakable
+ q.pp @source
+
+ q.breakable
+ q.text "specs:"
+
+ q.breakable
+ q.pp @specs.map(&:full_name)
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/lock_specification.rb b/lib/rubygems/resolver/lock_specification.rb
new file mode 100644
index 0000000000..06f912dd85
--- /dev/null
+++ b/lib/rubygems/resolver/lock_specification.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+##
+# The LockSpecification comes from a lockfile (Gem::RequestSet::Lockfile).
+#
+# A LockSpecification's dependency information is pre-filled from the
+# lockfile.
+
+class Gem::Resolver::LockSpecification < Gem::Resolver::Specification
+ attr_reader :sources
+
+ def initialize(set, name, version, sources, platform)
+ super()
+
+ @name = name
+ @platform = platform
+ @set = set
+ @source = sources.first
+ @sources = sources
+ @version = version
+
+ @dependencies = []
+ @spec = nil
+ end
+
+ ##
+ # This is a null install as a locked specification is considered installed.
+ # +options+ are ignored.
+
+ def install(options = {})
+ destination = options[:install_dir] || Gem.dir
+
+ if File.exist? File.join(destination, "specifications", spec.spec_name)
+ yield nil
+ return
+ end
+
+ super
+ end
+
+ ##
+ # Adds +dependency+ from the lockfile to this specification
+
+ def add_dependency(dependency) # :nodoc:
+ @dependencies << dependency
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[LockSpecification", "]" do
+ q.breakable
+ q.text "name: #{@name}"
+
+ q.breakable
+ q.text "version: #{@version}"
+
+ unless @platform == Gem::Platform::RUBY
+ q.breakable
+ q.text "platform: #{@platform}"
+ end
+
+ unless @dependencies.empty?
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp @dependencies
+ end
+ end
+ end
+
+ ##
+ # A specification constructed from the lockfile is returned
+
+ def spec
+ @spec ||= Gem::Specification.find do |spec|
+ spec.name == @name && spec.version == @version
+ end
+
+ @spec ||= Gem::Specification.new do |s|
+ s.name = @name
+ s.version = @version
+ s.platform = @platform
+
+ s.dependencies.concat @dependencies
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/requirement_list.rb b/lib/rubygems/resolver/requirement_list.rb
new file mode 100644
index 0000000000..6f86f0f412
--- /dev/null
+++ b/lib/rubygems/resolver/requirement_list.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+##
+# The RequirementList is used to hold the requirements being considered
+# while resolving a set of gems.
+#
+# The RequirementList acts like a queue where the oldest items are removed
+# first.
+
+class Gem::Resolver::RequirementList
+ include Enumerable
+
+ ##
+ # Creates a new RequirementList.
+
+ def initialize
+ @exact = []
+ @list = []
+ end
+
+ def initialize_copy(other) # :nodoc:
+ @exact = @exact.dup
+ @list = @list.dup
+ end
+
+ ##
+ # Adds Resolver::DependencyRequest +req+ to this requirements list.
+
+ def add(req)
+ if req.requirement.exact?
+ @exact.push req
+ else
+ @list.push req
+ end
+ req
+ end
+
+ ##
+ # Enumerates requirements in the list
+
+ def each # :nodoc:
+ return enum_for __method__ unless block_given?
+
+ @exact.each do |requirement|
+ yield requirement
+ end
+
+ @list.each do |requirement|
+ yield requirement
+ end
+ end
+
+ ##
+ # How many elements are in the list
+
+ def size
+ @exact.size + @list.size
+ end
+
+ ##
+ # Is the list empty?
+
+ def empty?
+ @exact.empty? && @list.empty?
+ end
+
+ ##
+ # Remove the oldest DependencyRequest from the list.
+
+ def remove
+ return @exact.shift unless @exact.empty?
+ @list.shift
+ end
+
+ ##
+ # Returns the oldest five entries from the list.
+
+ def next5
+ x = @exact[0,5]
+ x + @list[0,5 - x.size]
+ end
+end
diff --git a/lib/rubygems/resolver/set.rb b/lib/rubygems/resolver/set.rb
new file mode 100644
index 0000000000..243fee5fd5
--- /dev/null
+++ b/lib/rubygems/resolver/set.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+##
+# Resolver sets are used to look up specifications (and their
+# dependencies) used in resolution. This set is abstract.
+
+class Gem::Resolver::Set
+ ##
+ # Set to true to disable network access for this set
+
+ attr_accessor :remote
+
+ ##
+ # Errors encountered when resolving gems
+
+ attr_accessor :errors
+
+ ##
+ # When true, allows matching of requests to prerelease gems.
+
+ attr_accessor :prerelease
+
+ def initialize # :nodoc:
+ @prerelease = false
+ @remote = true
+ @errors = []
+ end
+
+ ##
+ # The find_all method must be implemented. It returns all Resolver
+ # Specification objects matching the given DependencyRequest +req+.
+
+ def find_all(req)
+ raise NotImplementedError
+ end
+
+ ##
+ # The #prefetch method may be overridden, but this is not necessary. This
+ # default implementation does nothing, which is suitable for sets where
+ # looking up a specification is cheap (such as installed gems).
+ #
+ # When overridden, the #prefetch method should look up specifications
+ # matching +reqs+.
+
+ def prefetch(reqs)
+ end
+
+ ##
+ # When true, this set is allowed to access the network when looking up
+ # specifications or dependencies.
+
+ def remote? # :nodoc:
+ @remote
+ end
+end
diff --git a/lib/rubygems/resolver/source_set.rb b/lib/rubygems/resolver/source_set.rb
new file mode 100644
index 0000000000..074b473edc
--- /dev/null
+++ b/lib/rubygems/resolver/source_set.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+##
+# The SourceSet chooses the best available method to query a remote index.
+#
+# Kind off like BestSet but filters the sources for gems
+
+class Gem::Resolver::SourceSet < Gem::Resolver::Set
+ ##
+ # Creates a SourceSet for the given +sources+ or Gem::sources if none are
+ # specified. +sources+ must be a Gem::SourceList.
+
+ def initialize
+ super()
+
+ @links = {}
+ @sets = {}
+ end
+
+ def find_all(req) # :nodoc:
+ if set = get_set(req.dependency.name)
+ set.find_all req
+ else
+ []
+ end
+ end
+
+ # potentially no-op
+ def prefetch(reqs) # :nodoc:
+ reqs.each do |req|
+ if set = get_set(req.dependency.name)
+ set.prefetch reqs
+ end
+ end
+ end
+
+ def add_source_gem(name, source)
+ @links[name] = source
+ end
+
+ private
+
+ def get_set(name)
+ link = @links[name]
+ @sets[link] ||= Gem::Source.new(link).dependency_resolver_set(@prerelease) if link
+ end
+end
diff --git a/lib/rubygems/resolver/spec_specification.rb b/lib/rubygems/resolver/spec_specification.rb
new file mode 100644
index 0000000000..00ef9fdba0
--- /dev/null
+++ b/lib/rubygems/resolver/spec_specification.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+##
+# The Resolver::SpecSpecification contains common functionality for
+# Resolver specifications that are backed by a Gem::Specification.
+
+class Gem::Resolver::SpecSpecification < Gem::Resolver::Specification
+ ##
+ # A SpecSpecification is created for a +set+ for a Gem::Specification in
+ # +spec+. The +source+ is either where the +spec+ came from, or should be
+ # loaded from.
+
+ def initialize(set, spec, source = nil)
+ @set = set
+ @source = source
+ @spec = spec
+ end
+
+ ##
+ # The dependencies of the gem for this specification
+
+ def dependencies
+ spec.dependencies
+ end
+
+ ##
+ # The required_ruby_version constraint for this specification
+
+ def required_ruby_version
+ spec.required_ruby_version
+ end
+
+ ##
+ # The required_rubygems_version constraint for this specification
+
+ def required_rubygems_version
+ spec.required_rubygems_version
+ end
+
+ ##
+ # The name and version of the specification.
+ #
+ # Unlike Gem::Specification#full_name, the platform is not included.
+
+ def full_name
+ "#{spec.name}-#{spec.version}"
+ end
+
+ ##
+ # The name of the gem for this specification
+
+ def name
+ spec.name
+ end
+
+ ##
+ # The platform this gem works on.
+
+ def platform
+ spec.platform
+ end
+
+ ##
+ # The version of the gem for this specification.
+
+ def version
+ spec.version
+ end
+
+ ##
+ # The hash value for this specification.
+
+ def hash
+ spec.hash
+ end
+end
diff --git a/lib/rubygems/resolver/specification.rb b/lib/rubygems/resolver/specification.rb
new file mode 100644
index 0000000000..d2098ef0e2
--- /dev/null
+++ b/lib/rubygems/resolver/specification.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+##
+# A Resolver::Specification contains a subset of the information
+# contained in a Gem::Specification. Only the information necessary for
+# dependency resolution in the resolver is included.
+
+class Gem::Resolver::Specification
+ ##
+ # The dependencies of the gem for this specification
+
+ attr_reader :dependencies
+
+ ##
+ # The name of the gem for this specification
+
+ attr_reader :name
+
+ ##
+ # The platform this gem works on.
+
+ attr_reader :platform
+
+ ##
+ # The set this specification came from.
+
+ attr_reader :set
+
+ ##
+ # The source for this specification
+
+ attr_reader :source
+
+ ##
+ # The Gem::Specification for this Resolver::Specification.
+ #
+ # Implementers, note that #install updates @spec, so be sure to cache the
+ # Gem::Specification in @spec when overriding.
+
+ attr_reader :spec
+
+ ##
+ # The version of the gem for this specification.
+
+ attr_reader :version
+
+ ##
+ # The required_ruby_version constraint for this specification.
+
+ attr_reader :required_ruby_version
+
+ ##
+ # The required_ruby_version constraint for this specification.
+
+ attr_reader :required_rubygems_version
+
+ ##
+ # Sets default instance variables for the specification.
+
+ def initialize
+ @dependencies = nil
+ @name = nil
+ @platform = nil
+ @set = nil
+ @source = nil
+ @version = nil
+ @required_ruby_version = Gem::Requirement.default
+ @required_rubygems_version = Gem::Requirement.default
+ end
+
+ ##
+ # Fetches development dependencies if the source does not provide them by
+ # default (see APISpecification).
+
+ def fetch_development_dependencies # :nodoc:
+ end
+
+ ##
+ # The name and version of the specification.
+ #
+ # Unlike Gem::Specification#full_name, the platform is not included.
+
+ def full_name
+ "#{@name}-#{@version}"
+ end
+
+ ##
+ # Installs this specification using the Gem::Installer +options+. The
+ # install method yields a Gem::Installer instance, which indicates the
+ # gem will be installed, or +nil+, which indicates the gem is already
+ # installed.
+ #
+ # After installation #spec is updated to point to the just-installed
+ # specification.
+
+ def install(options = {})
+ require_relative "../installer"
+
+ gem = download options
+
+ installer = Gem::Installer.at gem, options
+
+ yield installer if block_given?
+
+ @spec = installer.install
+ end
+
+ def download(options)
+ dir = options[:install_dir] || Gem.dir
+
+ Gem.ensure_gem_subdirectories dir
+
+ source.download spec, dir
+ end
+
+ ##
+ # Returns true if this specification is installable on this platform.
+
+ def installable_platform?
+ Gem::Platform.match_spec? spec
+ end
+
+ def local? # :nodoc:
+ false
+ end
+end
diff --git a/lib/rubygems/resolver/strategy.rb b/lib/rubygems/resolver/strategy.rb
new file mode 100644
index 0000000000..bf0dbb6adc
--- /dev/null
+++ b/lib/rubygems/resolver/strategy.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# Custom PubGrub strategy with caching for version selection.
+# Modeled after Bundler's strategy to avoid redundant versions_for
+# calls during the solver's package selection loop.
+
+class Gem::Resolver::Strategy
+ def initialize(source)
+ @source = source
+ @package_priority_cache = Hash.new {|h, pkg| h[pkg] = {} }
+
+ @version_indexes = Hash.new do |h, k|
+ if Gem::PubGrub::Package.root?(k)
+ h[k] = { Gem::PubGrub::Package.root_version => 0 }
+ else
+ h[k] = @source.all_versions_for(k).each.with_index.to_h
+ end
+ end
+ end
+
+ def next_package_and_version(unsatisfied)
+ package, range = next_term_to_try_from(unsatisfied)
+ [package, most_preferred_version_of(package, range)]
+ end
+
+ private
+
+ def most_preferred_version_of(package, range)
+ versions = @source.versions_for(package, range)
+ indexes = @version_indexes[package]
+ versions.min_by {|version| indexes[version] || Float::INFINITY }
+ end
+
+ def next_term_to_try_from(unsatisfied)
+ unsatisfied.min_by do |package, range|
+ @package_priority_cache[package][range] ||= begin
+ matching_versions = @source.versions_for(package, range)
+ higher_versions = @source.versions_for(package, range.upper_invert)
+
+ [matching_versions.count <= 1 ? 0 : 1, higher_versions.count]
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/vendor_set.rb b/lib/rubygems/resolver/vendor_set.rb
new file mode 100644
index 0000000000..293a1e3331
--- /dev/null
+++ b/lib/rubygems/resolver/vendor_set.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+##
+# A VendorSet represents gems that have been unpacked into a specific
+# directory that contains a gemspec.
+#
+# This is used for gem dependency file support.
+#
+# Example:
+#
+# set = Gem::Resolver::VendorSet.new
+#
+# set.add_vendor_gem 'rake', 'vendor/rake'
+#
+# The directory vendor/rake must contain an unpacked rake gem along with a
+# rake.gemspec (watching the given name).
+
+class Gem::Resolver::VendorSet < Gem::Resolver::Set
+ ##
+ # The specifications for this set.
+
+ attr_reader :specs # :nodoc:
+
+ def initialize # :nodoc:
+ super()
+
+ @directories = {}
+ @specs = {}
+ end
+
+ ##
+ # Adds a specification to the set with the given +name+ which has been
+ # unpacked into the given +directory+.
+
+ def add_vendor_gem(name, directory) # :nodoc:
+ gemspec = File.join directory, "#{name}.gemspec"
+
+ spec = Gem::Specification.load gemspec
+
+ raise Gem::GemNotFoundException,
+ "unable to find #{gemspec} for gem #{name}" unless spec
+
+ spec.full_gem_path = File.expand_path directory
+
+ @specs[spec.name] = spec
+ @directories[spec] = directory
+
+ spec
+ end
+
+ ##
+ # Returns an Array of VendorSpecification objects matching the
+ # DependencyRequest +req+.
+
+ def find_all(req)
+ @specs.values.select do |spec|
+ req.match? spec
+ end.map do |spec|
+ source = Gem::Source::Vendor.new @directories[spec]
+ Gem::Resolver::VendorSpecification.new self, spec, source
+ end
+ end
+
+ ##
+ # Loads a spec with the given +name+. +version+, +platform+ and +source+ are
+ # ignored.
+
+ def load_spec(name, version, platform, source) # :nodoc:
+ @specs.fetch name
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[VendorSet", "]" do
+ next if @directories.empty?
+ q.breakable
+
+ dirs = @directories.map do |spec, directory|
+ "#{spec.full_name}: #{directory}"
+ end
+
+ q.seplist dirs do |dir|
+ q.text dir
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/vendor_specification.rb b/lib/rubygems/resolver/vendor_specification.rb
new file mode 100644
index 0000000000..ac78f54558
--- /dev/null
+++ b/lib/rubygems/resolver/vendor_specification.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+##
+# A VendorSpecification represents a gem that has been unpacked into a project
+# and is being loaded through a gem dependencies file through the +path:+
+# option.
+
+class Gem::Resolver::VendorSpecification < Gem::Resolver::SpecSpecification
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @set == other.set &&
+ @spec == other.spec &&
+ @source == other.source
+ end
+
+ ##
+ # This is a null install as this gem was unpacked into a directory.
+ # +options+ are ignored.
+
+ def install(options = {})
+ yield nil
+ end
+end
diff --git a/lib/rubygems/rubygems_version.rb b/lib/rubygems/rubygems_version.rb
deleted file mode 100644
index d7b5622d97..0000000000
--- a/lib/rubygems/rubygems_version.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# DO NOT EDIT
-# This file is auto-generated by build scripts.
-# See: rake update_version
-module Gem
- RubyGemsVersion = '1.3.1'
-end
diff --git a/lib/rubygems/s3_uri_signer.rb b/lib/rubygems/s3_uri_signer.rb
new file mode 100644
index 0000000000..148cba38c4
--- /dev/null
+++ b/lib/rubygems/s3_uri_signer.rb
@@ -0,0 +1,226 @@
+# frozen_string_literal: true
+
+require_relative "openssl"
+require_relative "user_interaction"
+
+##
+# S3URISigner implements AWS SigV4 for S3 Source to avoid a dependency on the aws-sdk-* gems
+# More on AWS SigV4: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
+class Gem::S3URISigner
+ include Gem::UserInteraction
+
+ class ConfigurationError < Gem::Exception
+ def initialize(message)
+ super message
+ end
+
+ def to_s # :nodoc:
+ super.to_s
+ end
+ end
+
+ class InstanceProfileError < Gem::Exception
+ def initialize(message)
+ super message
+ end
+
+ def to_s # :nodoc:
+ super.to_s
+ end
+ end
+
+ attr_accessor :uri
+ attr_accessor :method
+
+ def initialize(uri, method)
+ @uri = uri
+ @method = method
+ end
+
+ ##
+ # Signs S3 URI using query-params according to the reference: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
+ def sign(expiration = 86_400)
+ s3_config = fetch_s3_config
+
+ current_time = Time.now.utc
+ date_time = current_time.strftime("%Y%m%dT%H%M%SZ")
+ date = date_time[0,8]
+
+ credential_info = "#{date}/#{s3_config.region}/s3/aws4_request"
+ canonical_host = "#{uri.host}.s3.#{s3_config.region}.amazonaws.com"
+
+ query_params = generate_canonical_query_params(s3_config, date_time, credential_info, expiration)
+ canonical_request = generate_canonical_request(canonical_host, query_params)
+ string_to_sign = generate_string_to_sign(date_time, credential_info, canonical_request)
+ signature = generate_signature(s3_config, date, string_to_sign)
+
+ Gem::URI.parse("https://#{canonical_host}#{uri.path}?#{query_params}&X-Amz-Signature=#{signature}")
+ end
+
+ private
+
+ S3Config = Struct.new :access_key_id, :secret_access_key, :security_token, :region
+
+ def generate_canonical_query_params(s3_config, date_time, credential_info, expiration)
+ canonical_params = {}
+ canonical_params["X-Amz-Algorithm"] = "AWS4-HMAC-SHA256"
+ canonical_params["X-Amz-Credential"] = "#{s3_config.access_key_id}/#{credential_info}"
+ canonical_params["X-Amz-Date"] = date_time
+ canonical_params["X-Amz-Expires"] = expiration.to_s
+ canonical_params["X-Amz-SignedHeaders"] = "host"
+ canonical_params["X-Amz-Security-Token"] = s3_config.security_token if s3_config.security_token
+
+ # Sorting is required to generate proper signature
+ canonical_params.sort.to_h.map do |key, value|
+ "#{base64_uri_escape(key)}=#{base64_uri_escape(value)}"
+ end.join("&")
+ end
+
+ def generate_canonical_request(canonical_host, query_params)
+ [
+ method.upcase,
+ uri.path,
+ query_params,
+ "host:#{canonical_host}",
+ "", # empty params
+ "host",
+ "UNSIGNED-PAYLOAD",
+ ].join("\n")
+ end
+
+ def generate_string_to_sign(date_time, credential_info, canonical_request)
+ [
+ "AWS4-HMAC-SHA256",
+ date_time,
+ credential_info,
+ OpenSSL::Digest::SHA256.hexdigest(canonical_request),
+ ].join("\n")
+ end
+
+ def generate_signature(s3_config, date, string_to_sign)
+ date_key = OpenSSL::HMAC.digest("sha256", "AWS4" + s3_config.secret_access_key, date)
+ date_region_key = OpenSSL::HMAC.digest("sha256", date_key, s3_config.region)
+ date_region_service_key = OpenSSL::HMAC.digest("sha256", date_region_key, "s3")
+ signing_key = OpenSSL::HMAC.digest("sha256", date_region_service_key, "aws4_request")
+ OpenSSL::HMAC.hexdigest("sha256", signing_key, string_to_sign)
+ end
+
+ ##
+ # Extracts S3 configuration for S3 bucket
+ def fetch_s3_config
+ return S3Config.new(uri.user, uri.password, nil, "us-east-1") if uri.user && uri.password
+
+ s3_source = Gem.configuration[:s3_source] || Gem.configuration["s3_source"]
+ host = uri.host
+ raise ConfigurationError.new("no s3_source key exists in .gemrc") unless s3_source
+
+ auth = s3_source[host] || s3_source[host.to_sym]
+ raise ConfigurationError.new("no key for host #{host} in s3_source in .gemrc") unless auth
+
+ provider = auth[:provider] || auth["provider"]
+ case provider
+ when "env"
+ id = ENV["AWS_ACCESS_KEY_ID"]
+ secret = ENV["AWS_SECRET_ACCESS_KEY"]
+ security_token = ENV["AWS_SESSION_TOKEN"]
+ when "instance_profile"
+ credentials = ec2_metadata_credentials_json
+ id = credentials["AccessKeyId"]
+ secret = credentials["SecretAccessKey"]
+ security_token = credentials["Token"]
+ else
+ id = auth[:id] || auth["id"]
+ secret = auth[:secret] || auth["secret"]
+ security_token = auth[:security_token] || auth["security_token"]
+ end
+
+ raise ConfigurationError.new("s3_source for #{host} missing id or secret") unless id && secret
+
+ region = auth[:region] || auth["region"] || "us-east-1"
+ S3Config.new(id, secret, security_token, region)
+ end
+
+ def base64_uri_escape(str)
+ str.gsub(%r{[\+/=\n]}, BASE64_URI_TRANSLATE)
+ end
+
+ def ec2_metadata_credentials_json
+ require_relative "vendored_net_http"
+ require_relative "request"
+ require_relative "request/connection_pools"
+ require "json"
+
+ # First try V2 fallback to V1
+ res = nil
+ begin
+ res = ec2_metadata_credentials_imds_v2
+ rescue InstanceProfileError
+ alert_warning "Unable to access ec2 credentials via IMDSv2, falling back to IMDSv1"
+ res = ec2_metadata_credentials_imds_v1
+ end
+ res
+ end
+
+ def ec2_metadata_credentials_imds_v2
+ token = ec2_metadata_token
+ iam_info = ec2_metadata_request(EC2_IAM_INFO, token:)
+ # Expected format: arn:aws:iam::<id>:instance-profile/<role_name>
+ role_name = iam_info["InstanceProfileArn"].split("/").last
+ ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token:)
+ end
+
+ def ec2_metadata_credentials_imds_v1
+ iam_info = ec2_metadata_request(EC2_IAM_INFO, token: nil)
+ # Expected format: arn:aws:iam::<id>:instance-profile/<role_name>
+ role_name = iam_info["InstanceProfileArn"].split("/").last
+ ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token: nil)
+ end
+
+ def ec2_metadata_request(url, token:)
+ request = ec2_iam_request(Gem::URI(url), Gem::Net::HTTP::Get)
+
+ response = request.fetch do |req|
+ if token
+ req.add_field "X-aws-ec2-metadata-token", token
+ end
+ end
+
+ case response
+ when Gem::Net::HTTPOK then
+ JSON.parse(response.body)
+ else
+ raise InstanceProfileError.new("Unable to fetch AWS metadata from #{uri}: #{response.message} #{response.code}")
+ end
+ end
+
+ def ec2_metadata_token
+ request = ec2_iam_request(Gem::URI(EC2_IAM_TOKEN), Gem::Net::HTTP::Put)
+
+ response = request.fetch do |req|
+ req.add_field "X-aws-ec2-metadata-token-ttl-seconds", 60
+ end
+
+ case response
+ when Gem::Net::HTTPOK then
+ response.body
+ else
+ raise InstanceProfileError.new("Unable to fetch AWS metadata from #{uri}: #{response.message} #{response.code}")
+ end
+ end
+
+ def ec2_iam_request(uri, verb)
+ @request_pool ||= create_request_pool(uri)
+ Gem::Request.new(uri, verb, nil, @request_pool)
+ end
+
+ def create_request_pool(uri)
+ proxy_uri = Gem::Request.proxy_uri(Gem::Request.get_proxy_from_env(uri.scheme))
+ certs = Gem::Request.get_cert_files
+ Gem::Request::ConnectionPools.new(proxy_uri, certs).pool_for(uri)
+ end
+
+ BASE64_URI_TRANSLATE = { "+" => "%2B", "/" => "%2F", "=" => "%3D", "\n" => "" }.freeze
+ EC2_IAM_TOKEN = "http://169.254.169.254/latest/api/token"
+ EC2_IAM_INFO = "http://169.254.169.254/latest/meta-data/iam/info"
+ EC2_IAM_SECURITY_CREDENTIALS = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
+end
diff --git a/lib/rubygems/safe_marshal.rb b/lib/rubygems/safe_marshal.rb
new file mode 100644
index 0000000000..871f24727d
--- /dev/null
+++ b/lib/rubygems/safe_marshal.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require "stringio"
+
+require_relative "safe_marshal/reader"
+require_relative "safe_marshal/visitors/to_ruby"
+
+module Gem
+ ###
+ # This module is used for safely loading Marshal specs from a gem. The
+ # `safe_load` method defined on this module is specifically designed for
+ # loading Gem specifications.
+
+ module SafeMarshal
+ PERMITTED_CLASSES = %w[
+ Date
+ Time
+ Rational
+
+ Gem::Dependency
+ Gem::NameTuple
+ Gem::Platform
+ Gem::Requirement
+ Gem::Specification
+ Gem::Version
+ Gem::Version::Requirement
+
+ YAML::Syck::DefaultKey
+ YAML::PrivateType
+ ].freeze
+ private_constant :PERMITTED_CLASSES
+
+ PERMITTED_SYMBOLS = %w[
+ development
+ runtime
+
+ name
+ number
+ platform
+ dependencies
+ ].freeze
+ private_constant :PERMITTED_SYMBOLS
+
+ PERMITTED_IVARS = {
+ "String" => %w[E encoding @taguri @debug_created_info],
+ "Time" => %w[
+ offset zone nano_num nano_den submicro
+ @_zone @marshal_with_utc_coercion
+ ],
+ "Gem::Dependency" => %w[
+ @name @requirement @prerelease @version_requirement @version_requirements @type
+ @force_ruby_platform
+ ],
+ "Gem::NameTuple" => %w[@name @version @platform],
+ "Gem::Platform" => %w[@os @cpu @version],
+ "Psych::PrivateType" => %w[@value @type_id],
+ "YAML::PrivateType" => %w[@value @type_id],
+ }.freeze
+ private_constant :PERMITTED_IVARS
+
+ def self.safe_load(input)
+ load(input, permitted_classes: PERMITTED_CLASSES, permitted_symbols: PERMITTED_SYMBOLS, permitted_ivars: PERMITTED_IVARS)
+ end
+
+ def self.load(input, permitted_classes: [::Symbol], permitted_symbols: [], permitted_ivars: {})
+ root = Reader.new(StringIO.new(input, "r").binmode).read!
+
+ Visitors::ToRuby.new(
+ permitted_classes: permitted_classes,
+ permitted_symbols: permitted_symbols,
+ permitted_ivars: permitted_ivars,
+ ).visit(root)
+ end
+ end
+end
diff --git a/lib/rubygems/safe_marshal/elements.rb b/lib/rubygems/safe_marshal/elements.rb
new file mode 100644
index 0000000000..f8874b1b2f
--- /dev/null
+++ b/lib/rubygems/safe_marshal/elements.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+module Gem
+ module SafeMarshal
+ module Elements
+ class Element
+ end
+
+ class Symbol < Element
+ def initialize(name)
+ @name = name
+ end
+ attr_reader :name
+ end
+
+ class UserDefined < Element
+ def initialize(name, binary_string)
+ @name = name
+ @binary_string = binary_string
+ end
+
+ attr_reader :name, :binary_string
+ end
+
+ class UserMarshal < Element
+ def initialize(name, data)
+ @name = name
+ @data = data
+ end
+
+ attr_reader :name, :data
+ end
+
+ class String < Element
+ def initialize(str)
+ @str = str
+ end
+
+ attr_reader :str
+ end
+
+ class Hash < Element
+ def initialize(pairs)
+ @pairs = pairs
+ end
+
+ attr_reader :pairs
+ end
+
+ class HashWithDefaultValue < Hash
+ def initialize(pairs, default)
+ super(pairs)
+ @default = default
+ end
+
+ attr_reader :default
+ end
+
+ class Array < Element
+ def initialize(elements)
+ @elements = elements
+ end
+
+ attr_reader :elements
+ end
+
+ class Integer < Element
+ def initialize(int)
+ @int = int
+ end
+
+ attr_reader :int
+ end
+
+ class True < Element
+ def initialize
+ end
+ TRUE = new.freeze
+ end
+
+ class False < Element
+ def initialize
+ end
+
+ FALSE = new.freeze
+ end
+
+ class WithIvars < Element
+ def initialize(object, ivars)
+ @object = object
+ @ivars = ivars
+ end
+
+ attr_reader :object, :ivars
+ end
+
+ class Object < Element
+ def initialize(name)
+ @name = name
+ end
+ attr_reader :name
+ end
+
+ class Nil < Element
+ NIL = new.freeze
+ end
+
+ class ObjectLink < Element
+ def initialize(offset)
+ @offset = offset
+ end
+ attr_reader :offset
+ end
+
+ class SymbolLink < Element
+ def initialize(offset)
+ @offset = offset
+ end
+ attr_reader :offset
+ end
+
+ class Float < Element
+ def initialize(string)
+ @string = string
+ end
+ attr_reader :string
+ end
+
+ class Bignum < Element # rubocop:disable Lint/UnifiedInteger
+ def initialize(sign, data)
+ @sign = sign
+ @data = data
+ end
+ attr_reader :sign, :data
+ end
+
+ class UserClass < Element
+ def initialize(name, wrapped_object)
+ @name = name
+ @wrapped_object = wrapped_object
+ end
+ attr_reader :name, :wrapped_object
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/safe_marshal/reader.rb b/lib/rubygems/safe_marshal/reader.rb
new file mode 100644
index 0000000000..4362d65fd6
--- /dev/null
+++ b/lib/rubygems/safe_marshal/reader.rb
@@ -0,0 +1,325 @@
+# frozen_string_literal: true
+
+require_relative "elements"
+
+module Gem
+ module SafeMarshal
+ class Reader
+ class Error < StandardError
+ end
+
+ class UnsupportedVersionError < Error
+ end
+
+ class UnconsumedBytesError < Error
+ end
+
+ class NotImplementedError < Error
+ end
+
+ class EOFError < Error
+ end
+
+ class DataTooShortError < Error
+ end
+
+ class NegativeLengthError < Error
+ end
+
+ def initialize(io)
+ @io = io
+ end
+
+ def read!
+ read_header
+ root = read_element
+ raise UnconsumedBytesError, "expected EOF, got #{@io.read(10).inspect}... after top-level element #{root.class}" unless @io.eof?
+ root
+ end
+
+ private
+
+ MARSHAL_VERSION = [Marshal::MAJOR_VERSION, Marshal::MINOR_VERSION].map(&:chr).join.freeze
+ private_constant :MARSHAL_VERSION
+
+ def read_header
+ v = @io.read(2)
+ raise UnsupportedVersionError, "Unsupported marshal version #{v.bytes.map(&:ord).join(".")}, expected #{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}" unless v == MARSHAL_VERSION
+ end
+
+ def read_bytes(n)
+ raise NegativeLengthError if n < 0
+ str = @io.read(n)
+ raise EOFError, "expected #{n} bytes, got EOF" if str.nil?
+ raise DataTooShortError, "expected #{n} bytes, got #{str.inspect}" unless str.bytesize == n
+ str
+ end
+
+ def read_byte
+ @io.getbyte || raise(EOFError, "Unexpected EOF")
+ end
+
+ def read_integer
+ b = read_byte
+
+ case b
+ when 0x00
+ 0
+ when 0x01
+ read_byte
+ when 0x02
+ read_byte | (read_byte << 8)
+ when 0x03
+ read_byte | (read_byte << 8) | (read_byte << 16)
+ when 0x04
+ read_byte | (read_byte << 8) | (read_byte << 16) | (read_byte << 24)
+ when 0xFC
+ read_byte | (read_byte << 8) | (read_byte << 16) | (read_byte << 24) | -0x100000000
+ when 0xFD
+ read_byte | (read_byte << 8) | (read_byte << 16) | -0x1000000
+ when 0xFE
+ read_byte | (read_byte << 8) | -0x10000
+ when 0xFF
+ read_byte | -0x100
+ else
+ signed = (b ^ 128) - 128
+ if b >= 128
+ signed + 5
+ else
+ signed - 5
+ end
+ end
+ end
+
+ def read_element
+ type = read_byte
+ case type
+ when 34 then read_string # ?"
+ when 48 then read_nil # ?0
+ when 58 then read_symbol # ?:
+ when 59 then read_symbol_link # ?;
+ when 64 then read_object_link # ?@
+ when 70 then read_false # ?F
+ when 73 then read_object_with_ivars # ?I
+ when 84 then read_true # ?T
+ when 85 then read_user_marshal # ?U
+ when 91 then read_array # ?[
+ when 102 then read_float # ?f
+ when 105 then Elements::Integer.new(read_integer) # ?i
+ when 108 then read_bignum # ?l
+ when 111 then read_object # ?o
+ when 117 then read_user_defined # ?u
+ when 123 then read_hash # ?{
+ when 125 then read_hash_with_default_value # ?}
+ when 101 then read_extended_object # ?e
+ when 99 then read_class # ?c
+ when 109 then read_module # ?m
+ when 77 then read_class_or_module # ?M
+ when 100 then read_data # ?d
+ when 47 then read_regexp # ?/
+ when 83 then read_struct # ?S
+ when 67 then read_user_class # ?C
+ else
+ raise Error, "Unknown marshal type discriminator #{type.chr.inspect} (#{type})"
+ end
+ end
+
+ STRING_E_SYMBOL = Elements::Symbol.new("E").freeze
+ private_constant :STRING_E_SYMBOL
+
+ def read_symbol
+ len = read_integer
+ if len == 1
+ byte = read_byte
+ if byte == 69 # ?E
+ STRING_E_SYMBOL
+ else
+ Elements::Symbol.new(byte.chr)
+ end
+ else
+ name = read_bytes(len)
+ Elements::Symbol.new(name)
+ end
+ end
+
+ EMPTY_STRING = Elements::String.new("".b.freeze).freeze
+ private_constant :EMPTY_STRING
+
+ def read_string
+ length = read_integer
+ return EMPTY_STRING if length == 0
+ str = read_bytes(length)
+ Elements::String.new(str)
+ end
+
+ def read_true
+ Elements::True::TRUE
+ end
+
+ def read_false
+ Elements::False::FALSE
+ end
+
+ def read_user_defined
+ name = read_element
+ binary_string = read_bytes(read_integer)
+ Elements::UserDefined.new(name, binary_string)
+ end
+
+ EMPTY_ARRAY = Elements::Array.new([].freeze).freeze
+ private_constant :EMPTY_ARRAY
+
+ def read_array
+ length = read_integer
+ return EMPTY_ARRAY if length == 0
+ raise NegativeLengthError if length < 0
+ elements = Array.new(length) do
+ read_element
+ end
+ Elements::Array.new(elements)
+ end
+
+ def read_object_with_ivars
+ object = read_element
+ length = read_integer
+ raise NegativeLengthError if length < 0
+ ivars = Array.new(length) do
+ [read_element, read_element]
+ end
+ Elements::WithIvars.new(object, ivars)
+ end
+
+ def read_symbol_link
+ offset = read_integer
+ Elements::SymbolLink.new(offset)
+ end
+
+ def read_user_marshal
+ name = read_element
+ data = read_element
+ Elements::UserMarshal.new(name, data)
+ end
+
+ # profiling bundle install --full-index shows that
+ # offset 6 is by far the most common object link,
+ # so we special case it to avoid allocating a new
+ # object a third of the time.
+ # the following are all the object links that
+ # appear more than 10000 times in my profiling
+
+ OBJECT_LINKS = {
+ 6 => Elements::ObjectLink.new(6).freeze,
+ 30 => Elements::ObjectLink.new(30).freeze,
+ 81 => Elements::ObjectLink.new(81).freeze,
+ 34 => Elements::ObjectLink.new(34).freeze,
+ 38 => Elements::ObjectLink.new(38).freeze,
+ 50 => Elements::ObjectLink.new(50).freeze,
+ 91 => Elements::ObjectLink.new(91).freeze,
+ 42 => Elements::ObjectLink.new(42).freeze,
+ 46 => Elements::ObjectLink.new(46).freeze,
+ 150 => Elements::ObjectLink.new(150).freeze,
+ 100 => Elements::ObjectLink.new(100).freeze,
+ 104 => Elements::ObjectLink.new(104).freeze,
+ 108 => Elements::ObjectLink.new(108).freeze,
+ 242 => Elements::ObjectLink.new(242).freeze,
+ 246 => Elements::ObjectLink.new(246).freeze,
+ 139 => Elements::ObjectLink.new(139).freeze,
+ 143 => Elements::ObjectLink.new(143).freeze,
+ 114 => Elements::ObjectLink.new(114).freeze,
+ 308 => Elements::ObjectLink.new(308).freeze,
+ 200 => Elements::ObjectLink.new(200).freeze,
+ 54 => Elements::ObjectLink.new(54).freeze,
+ 62 => Elements::ObjectLink.new(62).freeze,
+ 1_286_245 => Elements::ObjectLink.new(1_286_245).freeze,
+ }.freeze
+ private_constant :OBJECT_LINKS
+
+ def read_object_link
+ offset = read_integer
+ OBJECT_LINKS[offset] || Elements::ObjectLink.new(offset)
+ end
+
+ EMPTY_HASH = Elements::Hash.new([].freeze).freeze
+ private_constant :EMPTY_HASH
+
+ def read_hash
+ length = read_integer
+ return EMPTY_HASH if length == 0
+ pairs = Array.new(length) do
+ [read_element, read_element]
+ end
+ Elements::Hash.new(pairs)
+ end
+
+ def read_hash_with_default_value
+ length = read_integer
+ raise NegativeLengthError if length < 0
+ pairs = Array.new(length) do
+ [read_element, read_element]
+ end
+ default = read_element
+ Elements::HashWithDefaultValue.new(pairs, default)
+ end
+
+ def read_object
+ name = read_element
+ object = Elements::Object.new(name)
+ length = read_integer
+ raise NegativeLengthError if length < 0
+ ivars = Array.new(length) do
+ [read_element, read_element]
+ end
+ Elements::WithIvars.new(object, ivars)
+ end
+
+ def read_nil
+ Elements::Nil::NIL
+ end
+
+ def read_float
+ string = read_bytes(read_integer)
+ Elements::Float.new(string)
+ end
+
+ def read_bignum
+ sign = read_byte
+ data = read_bytes(read_integer * 2)
+ Elements::Bignum.new(sign, data)
+ end
+
+ def read_extended_object
+ raise NotImplementedError, "Reading Marshal objects of type extended_object is not implemented"
+ end
+
+ def read_class
+ raise NotImplementedError, "Reading Marshal objects of type class is not implemented"
+ end
+
+ def read_module
+ raise NotImplementedError, "Reading Marshal objects of type module is not implemented"
+ end
+
+ def read_class_or_module
+ raise NotImplementedError, "Reading Marshal objects of type class_or_module is not implemented"
+ end
+
+ def read_data
+ raise NotImplementedError, "Reading Marshal objects of type data is not implemented"
+ end
+
+ def read_regexp
+ raise NotImplementedError, "Reading Marshal objects of type regexp is not implemented"
+ end
+
+ def read_struct
+ raise NotImplementedError, "Reading Marshal objects of type struct is not implemented"
+ end
+
+ def read_user_class
+ name = read_element
+ wrapped_object = read_element
+ Elements::UserClass.new(name, wrapped_object)
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/safe_marshal/visitors/stream_printer.rb b/lib/rubygems/safe_marshal/visitors/stream_printer.rb
new file mode 100644
index 0000000000..162b36ad05
--- /dev/null
+++ b/lib/rubygems/safe_marshal/visitors/stream_printer.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require_relative "visitor"
+
+module Gem::SafeMarshal
+ module Visitors
+ class StreamPrinter < Visitor
+ def initialize(io, indent: "")
+ @io = io
+ @indent = indent
+ @level = 0
+ end
+
+ def visit(target)
+ @io.write("#{@indent * @level}#{target.class}")
+ target.instance_variables.each do |ivar|
+ value = target.instance_variable_get(ivar)
+ next if Elements::Element === value || Array === value
+ @io.write(" #{ivar}=#{value.inspect}")
+ end
+ @io.write("\n")
+ begin
+ @level += 1
+ super
+ ensure
+ @level -= 1
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/safe_marshal/visitors/to_ruby.rb b/lib/rubygems/safe_marshal/visitors/to_ruby.rb
new file mode 100644
index 0000000000..a1f9481776
--- /dev/null
+++ b/lib/rubygems/safe_marshal/visitors/to_ruby.rb
@@ -0,0 +1,428 @@
+# frozen_string_literal: true
+
+require_relative "visitor"
+
+module Gem::SafeMarshal
+ module Visitors
+ class ToRuby < Visitor
+ def initialize(permitted_classes:, permitted_symbols:, permitted_ivars:)
+ @permitted_classes = permitted_classes
+ @permitted_symbols = ["E"].concat(permitted_symbols).concat(permitted_classes)
+ @permitted_ivars = permitted_ivars
+
+ @objects = []
+ @symbols = []
+ @class_cache = {}
+
+ @stack = ["root"]
+ @stack_idx = 1
+ end
+
+ def inspect # :nodoc:
+ format("#<%s permitted_classes: %p permitted_symbols: %p permitted_ivars: %p>",
+ self.class, @permitted_classes, @permitted_symbols, @permitted_ivars)
+ end
+
+ def visit(target)
+ stack_idx = @stack_idx
+ super
+ ensure
+ @stack_idx = stack_idx - 1
+ end
+
+ private
+
+ def push_stack(element)
+ @stack[@stack_idx] = element
+ @stack_idx += 1
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Array(a)
+ array = register_object([])
+
+ elements = a.elements
+ size = elements.size
+ idx = 0
+ # not idiomatic, but there's a huge number of IMEMOs allocated here, so we avoid the block
+ # because this is such a hot path when doing a bundle install with the full index
+ while idx < size
+ push_stack idx
+ array << visit(elements[idx])
+ idx += 1
+ end
+
+ array
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Symbol(s)
+ name = s.name
+ raise UnpermittedSymbolError.new(symbol: name, stack: formatted_stack) unless @permitted_symbols.include?(name)
+ visit_symbol_type(s)
+ end
+
+ def map_ivars(klass, ivars)
+ stack_idx = @stack_idx
+ ivars.map.with_index do |(k, v), i|
+ @stack_idx = stack_idx
+
+ push_stack "ivar_"
+ push_stack i
+ k = resolve_ivar(klass, k)
+
+ @stack_idx = stack_idx
+ push_stack k
+
+ next k, visit(v)
+ end
+ end
+
+ def visit_Gem_SafeMarshal_Elements_WithIvars(e)
+ object_offset = @objects.size
+ push_stack "object"
+ object = visit(e.object)
+ ivars = map_ivars(object.class, e.ivars)
+
+ case e.object
+ when Elements::UserDefined
+ if object.class == ::Time
+ internal = []
+
+ ivars.reject! do |k, v|
+ case k
+ when :offset, :zone, :nano_num, :nano_den, :submicro
+ internal << [k, v]
+ true
+ else
+ false
+ end
+ end
+
+ s = e.object.binary_string
+ # 122 is the largest integer that can be represented in marshal in a single byte
+ raise TimeTooLargeError.new("binary string too large", stack: formatted_stack) if s.bytesize > 122
+
+ marshal_string = "\x04\bIu:\tTime".b
+ marshal_string.concat(s.bytesize + 5)
+ marshal_string << s
+ # internal is limited to 5, so no overflow is possible
+ marshal_string.concat(internal.size + 5)
+
+ internal.each do |k, v|
+ k = k.name
+ # ivar name can't be too large because only known ivars are in the internal ivars list
+ marshal_string.concat(":")
+ marshal_string.concat(k.bytesize + 5)
+ marshal_string.concat(k)
+ dumped = Marshal.dump(v)
+ dumped[0, 2] = ""
+ marshal_string.concat(dumped)
+ end
+
+ object = @objects[object_offset] = Marshal.load(marshal_string)
+ end
+ when Elements::String
+ enc = nil
+
+ ivars.reject! do |k, v|
+ case k
+ when :E
+ case v
+ when TrueClass
+ enc = "UTF-8"
+ when FalseClass
+ enc = "US-ASCII"
+ else
+ raise FormatError, "Unexpected value for String :E #{v.inspect}"
+ end
+ when :encoding
+ enc = v
+ else
+ next false
+ end
+ true
+ end
+
+ object.force_encoding(enc) if enc
+ end
+
+ ivars.each do |k, v|
+ object.instance_variable_set k, v
+ end
+ object
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Hash(o)
+ hash = register_object({})
+
+ o.pairs.each_with_index do |(k, v), i|
+ push_stack i
+ k = visit(k)
+ push_stack k
+ hash[k] = visit(v)
+ end
+
+ hash
+ end
+
+ def visit_Gem_SafeMarshal_Elements_HashWithDefaultValue(o)
+ hash = visit_Gem_SafeMarshal_Elements_Hash(o)
+ push_stack :default
+ hash.default = visit(o.default)
+ hash
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Object(o)
+ register_object(resolve_class(o.name).allocate)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_ObjectLink(o)
+ @objects.fetch(o.offset)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_SymbolLink(o)
+ @symbols.fetch(o.offset)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_UserDefined(o)
+ register_object(call_method(resolve_class(o.name), :_load, o.binary_string))
+ end
+
+ def visit_Gem_SafeMarshal_Elements_UserMarshal(o)
+ klass = resolve_class(o.name)
+ compat = COMPAT_CLASSES.fetch(klass, nil)
+ idx = @objects.size
+ object = register_object(call_method(compat || klass, :allocate))
+
+ push_stack :data
+ ret = call_method(object, :marshal_load, visit(o.data))
+
+ if compat
+ object = @objects[idx] = ret
+ end
+
+ object
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Integer(i)
+ i.int
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Nil(_)
+ nil
+ end
+
+ def visit_Gem_SafeMarshal_Elements_True(_)
+ true
+ end
+
+ def visit_Gem_SafeMarshal_Elements_False(_)
+ false
+ end
+
+ def visit_Gem_SafeMarshal_Elements_String(s)
+ register_object(+s.str)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Float(f)
+ register_object(
+ case f.string
+ when "inf"
+ ::Float::INFINITY
+ when "-inf"
+ -::Float::INFINITY
+ when "nan"
+ ::Float::NAN
+ else
+ f.string.to_f
+ end
+ )
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Bignum(b)
+ result = 0
+ b.data.each_byte.with_index do |byte, exp|
+ result += (byte * 2**(exp * 8))
+ end
+
+ case b.sign
+ when 43 # ?+
+ result
+ when 45 # ?-
+ -result
+ else
+ raise FormatError, "Unexpected sign for Bignum #{b.sign.chr.inspect} (#{b.sign})"
+ end
+ end
+
+ def visit_Gem_SafeMarshal_Elements_UserClass(r)
+ if resolve_class(r.name) == ::Hash && r.wrapped_object.is_a?(Elements::Hash)
+
+ hash = register_object({}.compare_by_identity)
+
+ o = r.wrapped_object
+ o.pairs.each_with_index do |(k, v), i|
+ push_stack i
+ k = visit(k)
+ push_stack k
+ hash[k] = visit(v)
+ end
+
+ if o.is_a?(Elements::HashWithDefaultValue)
+ push_stack :default
+ hash.default = visit(o.default)
+ end
+
+ hash
+ else
+ raise UnsupportedError.new("Unsupported user class #{resolve_class(r.name)} in marshal stream", stack: formatted_stack)
+ end
+ end
+
+ def resolve_class(n)
+ @class_cache[n] ||= begin
+ to_s = resolve_symbol_name(n)
+ raise UnpermittedClassError.new(name: to_s, stack: formatted_stack) unless @permitted_classes.include?(to_s)
+ visit_symbol_type(n)
+ begin
+ ::Object.const_get(to_s)
+ rescue NameError
+ raise ArgumentError, "Undefined class #{to_s.inspect}"
+ end
+ end
+ end
+
+ class RationalCompat
+ def marshal_load(s)
+ num, den = s
+ raise ArgumentError, "Expected 2 ints" unless s.size == 2 && num.is_a?(Integer) && den.is_a?(Integer)
+ Rational(num, den)
+ end
+ end
+ private_constant :RationalCompat
+
+ COMPAT_CLASSES = {}.tap do |h|
+ h[Rational] = RationalCompat
+ end.compare_by_identity.freeze
+ private_constant :COMPAT_CLASSES
+
+ def resolve_ivar(klass, name)
+ to_s = resolve_symbol_name(name)
+
+ raise UnpermittedIvarError.new(symbol: to_s, klass: klass, stack: formatted_stack) unless @permitted_ivars.fetch(klass.name, [].freeze).include?(to_s)
+
+ visit_symbol_type(name)
+ end
+
+ def visit_symbol_type(element)
+ case element
+ when Elements::Symbol
+ sym = element.name.to_sym
+ @symbols << sym
+ sym
+ when Elements::SymbolLink
+ visit_Gem_SafeMarshal_Elements_SymbolLink(element)
+ end
+ end
+
+ # This is a hot method, so avoid respond_to? checks on every invocation
+ if :read.respond_to?(:name)
+ def resolve_symbol_name(element)
+ case element
+ when Elements::Symbol
+ element.name
+ when Elements::SymbolLink
+ visit_Gem_SafeMarshal_Elements_SymbolLink(element).name
+ else
+ raise FormatError, "Expected symbol or symbol link, got #{element.inspect} @ #{formatted_stack.join(".")}"
+ end
+ end
+ else
+ def resolve_symbol_name(element)
+ case element
+ when Elements::Symbol
+ element.name
+ when Elements::SymbolLink
+ visit_Gem_SafeMarshal_Elements_SymbolLink(element).to_s
+ else
+ raise FormatError, "Expected symbol or symbol link, got #{element.inspect} @ #{formatted_stack.join(".")}"
+ end
+ end
+ end
+
+ def register_object(o)
+ @objects << o
+ o
+ end
+
+ def call_method(receiver, method, *args)
+ receiver.__send__(method, *args)
+ rescue NoMethodError => e
+ raise unless e.receiver == receiver
+
+ raise MethodCallError, "Unable to call #{method.inspect} on #{receiver.inspect}, perhaps it is a class using marshal compat, which is not visible in ruby? #{e}"
+ end
+
+ def formatted_stack
+ formatted = []
+ @stack[0, @stack_idx].each do |e|
+ if e.is_a?(Integer)
+ if formatted.last == "ivar_"
+ formatted[-1] = "ivar_#{e}"
+ else
+ formatted << "[#{e}]"
+ end
+ else
+ formatted << e
+ end
+ end
+ formatted
+ end
+
+ class Error < StandardError
+ end
+
+ class TimeTooLargeError < Error
+ def initialize(message, stack:)
+ super "#{message} @ #{stack.join "."}"
+ end
+ end
+
+ class UnpermittedSymbolError < Error
+ def initialize(symbol:, stack:)
+ @symbol = symbol
+ @stack = stack
+ super "Attempting to load unpermitted symbol #{symbol.inspect} @ #{stack.join "."}"
+ end
+ end
+
+ class UnpermittedIvarError < Error
+ def initialize(symbol:, klass:, stack:)
+ @symbol = symbol
+ @klass = klass
+ @stack = stack
+ super "Attempting to set unpermitted ivar #{symbol.inspect} on object of class #{klass} @ #{stack.join "."}"
+ end
+ end
+
+ class UnpermittedClassError < Error
+ def initialize(name:, stack:)
+ @name = name
+ @stack = stack
+ super "Attempting to load unpermitted class #{name.inspect} @ #{stack.join "."}"
+ end
+ end
+
+ class UnsupportedError < Error
+ def initialize(message, stack:)
+ super "#{message} @ #{stack.join "."}"
+ end
+ end
+
+ class FormatError < Error
+ end
+
+ class MethodCallError < Error
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/safe_marshal/visitors/visitor.rb b/lib/rubygems/safe_marshal/visitors/visitor.rb
new file mode 100644
index 0000000000..c9a079dc0e
--- /dev/null
+++ b/lib/rubygems/safe_marshal/visitors/visitor.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Gem::SafeMarshal::Visitors
+ class Visitor
+ def visit(target)
+ send DISPATCH.fetch(target.class), target
+ end
+
+ private
+
+ DISPATCH = Gem::SafeMarshal::Elements.constants.each_with_object({}) do |c, h|
+ next if c == :Element
+
+ klass = Gem::SafeMarshal::Elements.const_get(c)
+ h[klass] = :"visit_#{klass.name.gsub("::", "_")}"
+ h.default = :visit_unknown_element
+ end.compare_by_identity.freeze
+ private_constant :DISPATCH
+
+ def visit_unknown_element(e)
+ raise ArgumentError, "Attempting to visit unknown element #{e.inspect}"
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Array(target)
+ target.elements.each {|e| visit(e) }
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Bignum(target); end
+ def visit_Gem_SafeMarshal_Elements_False(target); end
+ def visit_Gem_SafeMarshal_Elements_Float(target); end
+
+ def visit_Gem_SafeMarshal_Elements_Hash(target)
+ target.pairs.each do |k, v|
+ visit(k)
+ visit(v)
+ end
+ end
+
+ def visit_Gem_SafeMarshal_Elements_HashWithDefaultValue(target)
+ visit_Gem_SafeMarshal_Elements_Hash(target)
+ visit(target.default)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Integer(target); end
+ def visit_Gem_SafeMarshal_Elements_Nil(target); end
+
+ def visit_Gem_SafeMarshal_Elements_Object(target)
+ visit(target.name)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_ObjectLink(target); end
+ def visit_Gem_SafeMarshal_Elements_String(target); end
+ def visit_Gem_SafeMarshal_Elements_Symbol(target); end
+ def visit_Gem_SafeMarshal_Elements_SymbolLink(target); end
+ def visit_Gem_SafeMarshal_Elements_True(target); end
+
+ def visit_Gem_SafeMarshal_Elements_UserDefined(target)
+ visit(target.name)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_UserMarshal(target)
+ visit(target.name)
+ visit(target.data)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_WithIvars(target)
+ visit(target.object)
+ target.ivars.each do |k, v|
+ visit(k)
+ visit(v)
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/safe_yaml.rb b/lib/rubygems/safe_yaml.rb
new file mode 100644
index 0000000000..f4bba00136
--- /dev/null
+++ b/lib/rubygems/safe_yaml.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gem
+ ###
+ # This module is used for safely loading YAML specs from a gem. The
+ # `safe_load` method defined on this module is specifically designed for
+ # loading Gem specifications. For loading other YAML safely, please see
+ # Psych.safe_load
+
+ module SafeYAML
+ PERMITTED_CLASSES = %w[
+ Symbol
+ Time
+ Date
+ Gem::Dependency
+ Gem::Platform
+ Gem::Requirement
+ Gem::Specification
+ Gem::Version
+ Gem::Version::Requirement
+ ].freeze
+
+ PERMITTED_SYMBOLS = %w[
+ development
+ runtime
+ ].freeze
+
+ @aliases_enabled = true
+ def self.aliases_enabled=(value) # :nodoc:
+ @aliases_enabled = !!value
+ end
+
+ def self.aliases_enabled? # :nodoc:
+ @aliases_enabled
+ end
+
+ def self.safe_load(input)
+ if Gem.use_psych?
+ ::Psych.safe_load(input, permitted_classes: PERMITTED_CLASSES,
+ permitted_symbols: PERMITTED_SYMBOLS, aliases: @aliases_enabled)
+ else
+ Gem::YAMLSerializer.load(
+ input,
+ permitted_classes: PERMITTED_CLASSES,
+ permitted_symbols: PERMITTED_SYMBOLS,
+ aliases: aliases_enabled?
+ )
+ end
+ end
+
+ class << self
+ alias_method :load, :safe_load
+ end
+ end
+end
diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb
index abf3cf4a6a..69ba87b07f 100644
--- a/lib/rubygems/security.rb
+++ b/lib/rubygems/security.rb
@@ -1,82 +1,94 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems'
-require 'rubygems/gem_openssl'
+require_relative "exceptions"
+require_relative "openssl"
-# = Signed Gems README
-#
-# == Table of Contents
-# * Overview
-# * Walkthrough
-# * Command-Line Options
-# * OpenSSL Reference
-# * Bugs/TODO
-# * About the Author
+##
+# = Signing gems
#
-# == Overview
-#
-# Gem::Security implements cryptographic signatures in RubyGems. The section
+# The Gem::Security implements cryptographic signatures for gems. The section
# below is a step-by-step guide to using signed gems and generating your own.
#
# == Walkthrough
#
+# === Building your certificate
+#
# In order to start signing your gems, you'll need to build a private key and
# a self-signed certificate. Here's how:
#
-# # build a private key and certificate for gemmaster@example.com
-# $ gem cert --build gemmaster@example.com
+# # build a private key and certificate for yourself:
+# $ gem cert --build you@example.com
#
-# This could take anywhere from 5 seconds to 10 minutes, depending on the
-# speed of your computer (public key algorithms aren't exactly the speediest
-# crypto algorithms in the world). When it's finished, you'll see the files
-# "gem-private_key.pem" and "gem-public_cert.pem" in the current directory.
+# This could take anywhere from a few seconds to a minute or two, depending on
+# the speed of your computer (public key algorithms aren't exactly the
+# speediest crypto algorithms in the world). When it's finished, you'll see
+# the files "gem-private_key.pem" and "gem-public_cert.pem" in the current
+# directory.
#
-# First things first: take the "gem-private_key.pem" file and move it
-# somewhere private, preferably a directory only you have access to, a floppy
-# (yuck!), a CD-ROM, or something comparably secure. Keep your private key
-# hidden; if it's compromised, someone can sign packages as you (note: PKI has
-# ways of mitigating the risk of stolen keys; more on that later).
+# First things first: Move both files to ~/.gem if you don't already have a
+# key and certificate in that directory. Ensure the file permissions make the
+# key unreadable by others (by default the file is saved securely).
#
-# Now, let's sign an existing gem. I'll be using my Imlib2-Ruby bindings, but
-# you can use whatever gem you'd like. Open up your existing gemspec file and
-# add the following lines:
+# Keep your private key hidden; if it's compromised, someone can sign packages
+# as you (note: PKI has ways of mitigating the risk of stolen keys; more on
+# that later).
#
-# # signing key and certificate chain
-# s.signing_key = '/mnt/floppy/gem-private_key.pem'
-# s.cert_chain = ['gem-public_cert.pem']
+# === Signing Gems
#
-# (Be sure to replace "/mnt/floppy" with the ultra-secret path to your private
-# key).
+# In RubyGems 2 and newer there is no extra work to sign a gem. RubyGems will
+# automatically find your key and certificate in your home directory and use
+# them to sign newly packaged gems.
#
-# After that, go ahead and build your gem as usual. Congratulations, you've
-# just built your first signed gem! If you peek inside your gem file, you'll
-# see a couple of new files have been added:
+# If your certificate is not self-signed (signed by a third party) RubyGems
+# will attempt to load the certificate chain from the trusted certificates.
+# Use <code>gem cert --add signing_cert.pem</code> to add your signers as
+# trusted certificates. See below for further information on certificate
+# chains.
#
-# $ tar tf tar tf Imlib2-Ruby-0.5.0.gem
-# data.tar.gz
-# data.tar.gz.sig
+# If you build your gem it will automatically be signed. If you peek inside
+# your gem file, you'll see a couple of new files have been added:
+#
+# $ tar tf your-gem-1.0.gem
# metadata.gz
-# metadata.gz.sig
+# metadata.gz.sig # metadata signature
+# data.tar.gz
+# data.tar.gz.sig # data signature
+# checksums.yaml.gz
+# checksums.yaml.gz.sig # checksums signature
+#
+# === Manually signing gems
+#
+# If you wish to store your key in a separate secure location you'll need to
+# set your gems up for signing by hand. To do this, set the
+# <code>signing_key</code> and <code>cert_chain</code> in the gemspec before
+# packaging your gem:
+#
+# s.signing_key = '/secure/path/to/gem-private_key.pem'
+# s.cert_chain = %w[/secure/path/to/gem-public_cert.pem]
+#
+# When you package your gem with these options set RubyGems will automatically
+# load your key and certificate from the secure paths.
+#
+# === Signed gems and security policies
#
# Now let's verify the signature. Go ahead and install the gem, but add the
-# following options: "-P HighSecurity", like this:
+# following options: <code>-P HighSecurity</code>, like this:
#
# # install the gem with using the security policy "HighSecurity"
-# $ sudo gem install Imlib2-Ruby-0.5.0.gem -P HighSecurity
+# $ sudo gem install your.gem -P HighSecurity
#
-# The -P option sets your security policy -- we'll talk about that in just a
-# minute. Eh, what's this?
+# The <code>-P</code> option sets your security policy -- we'll talk about
+# that in just a minute. Eh, what's this?
#
-# Attempting local installation of 'Imlib2-Ruby-0.5.0.gem'
-# ERROR: Error installing gem Imlib2-Ruby-0.5.0.gem[.gem]: Couldn't
-# verify data signature: Untrusted Signing Chain Root: cert =
-# '/CN=gemmaster/DC=example/DC=com', error = 'path
-# "/root/.rubygems/trust/cert-15dbb43a6edf6a70a85d4e784e2e45312cff7030.pem"
-# does not exist'
+# $ gem install -P HighSecurity your-gem-1.0.gem
+# ERROR: While executing gem ... (Gem::Security::Exception)
+# root cert /CN=you/DC=example is not trusted
#
# The culprit here is the security policy. RubyGems has several different
# security policies. Let's take a short break and go over the security
@@ -103,52 +115,57 @@ require 'rubygems/gem_openssl'
# * HighSecurity - Here's the bugger that got us into this mess.
# The HighSecurity policy is identical to the MediumSecurity policy,
# except that it does not allow unsigned gems. A malicious user
-# doesn't have a whole lot of options here; he can't modify the
-# package contents without invalidating the signature, and he can't
+# doesn't have a whole lot of options here; they can't modify the
+# package contents without invalidating the signature, and they can't
# modify or remove signature or the signing certificate chain, or
# RubyGems will simply refuse to install the package. Oh well, maybe
-# he'll have better luck causing problems for CPAN users instead :).
+# they'll have better luck causing problems for CPAN users instead :).
#
-# So, the reason RubyGems refused to install our shiny new signed gem was
-# because it was from an untrusted source. Well, my code is infallible
-# (hah!), so I'm going to add myself as a trusted source.
+# The reason RubyGems refused to install your shiny new signed gem was because
+# it was from an untrusted source. Well, your code is infallible (naturally),
+# so you need to add yourself as a trusted source:
#
-# Here's how:
+# # add trusted certificate
+# gem cert --add ~/.gem/gem-public_cert.pem
#
-# # add trusted certificate
-# gem cert --add gem-public_cert.pem
-#
-# I've added my public certificate as a trusted source. Now I can install
-# packages signed my private key without any hassle. Let's try the install
-# command above again:
+# You've now added your public certificate as a trusted source. Now you can
+# install packages signed by your private key without any hassle. Let's try
+# the install command above again:
#
# # install the gem with using the HighSecurity policy (and this time
# # without any shenanigans)
-# $ sudo gem install Imlib2-Ruby-0.5.0.gem -P HighSecurity
+# $ gem install -P HighSecurity your-gem-1.0.gem
+# Successfully installed your-gem-1.0
+# 1 gem installed
#
-# This time RubyGems should accept your signed package and begin installing.
-# While you're waiting for RubyGems to work it's magic, have a look at some of
-# the other security commands:
+# This time RubyGems will accept your signed package and begin installing.
#
-# Usage: gem cert [options]
+# While you're waiting for RubyGems to work it's magic, have a look at some of
+# the other security commands by running <code>gem help cert</code>:
#
# Options:
-# -a, --add CERT Add a trusted certificate.
-# -l, --list List trusted certificates.
-# -r, --remove STRING Remove trusted certificates containing STRING.
-# -b, --build EMAIL_ADDR Build private key and self-signed certificate
-# for EMAIL_ADDR.
-# -C, --certificate CERT Certificate for --sign command.
-# -K, --private-key KEY Private key for --sign command.
-# -s, --sign NEWCERT Sign a certificate with my key and certificate.
-#
-# (By the way, you can pull up this list any time you'd like by typing "gem
-# cert --help")
-#
-# Hmm. We've already covered the "--build" option, and the "--add", "--list",
-# and "--remove" commands seem fairly straightforward; they allow you to add,
-# list, and remove the certificates in your trusted certificate list. But
-# what's with this "--sign" option?
+# -a, --add CERT Add a trusted certificate.
+# -l, --list [FILTER] List trusted certificates where the
+# subject contains FILTER
+# -r, --remove FILTER Remove trusted certificates where the
+# subject contains FILTER
+# -b, --build EMAIL_ADDR Build private key and self-signed
+# certificate for EMAIL_ADDR
+# -C, --certificate CERT Signing certificate for --sign
+# -K, --private-key KEY Key for --sign or --build
+# -A, --key-algorithm ALGORITHM Select key algorithm for --build from RSA, DSA, or EC. Defaults to RSA.
+# -s, --sign CERT Signs CERT with the key from -K
+# and the certificate from -C
+# -d, --days NUMBER_OF_DAYS Days before the certificate expires
+# -R, --re-sign Re-signs the certificate from -C with the key from -K
+#
+# We've already covered the <code>--build</code> option, and the
+# <code>--add</code>, <code>--list</code>, and <code>--remove</code> commands
+# seem fairly straightforward; they allow you to add, list, and remove the
+# certificates in your trusted certificate list. But what's with this
+# <code>--sign</code> option?
+#
+# === Certificate chains
#
# To answer that question, let's take a look at "certificate chains", a
# concept I mentioned earlier. There are a couple of problems with
@@ -170,111 +187,107 @@ require 'rubygems/gem_openssl'
# trust. Here's a hypothetical example of a trust hierarchy based (roughly)
# on geography:
#
-#
# --------------------------
-# | rubygems@rubyforge.org |
+# | rubygems@rubygems.org |
# --------------------------
# |
# -----------------------------------
# | |
# ---------------------------- -----------------------------
-# | seattle.rb@zenspider.com | | dcrubyists@richkilmer.com |
+# | seattlerb@seattlerb.org | | dcrubyists@richkilmer.com |
# ---------------------------- -----------------------------
# | | | |
# --------------- ---------------- ----------- --------------
-# | alf@seattle | | bob@portland | | pabs@dc | | tomcope@dc |
+# | drbrain | | zenspider | | pabs@dc | | tomcope@dc |
# --------------- ---------------- ----------- --------------
#
#
-# Now, rather than having 4 trusted certificates (one for alf@seattle,
-# bob@portland, pabs@dc, and tomecope@dc), a user could actually get by with 1
-# certificate: the "rubygems@rubyforge.org" certificate. Here's how it works:
+# Now, rather than having 4 trusted certificates (one for drbrain, zenspider,
+# pabs@dc, and tomecope@dc), a user could actually get by with one
+# certificate, the "rubygems@rubygems.org" certificate.
+#
+# Here's how it works:
#
-# I install "Alf2000-Ruby-0.1.0.gem", a package signed by "alf@seattle". I've
-# never heard of "alf@seattle", but his certificate has a valid signature from
-# the "seattle.rb@zenspider.com" certificate, which in turn has a valid
-# signature from the "rubygems@rubyforge.org" certificate. Voila! At this
-# point, it's much more reasonable for me to trust a package signed by
-# "alf@seattle", because I can establish a chain to "rubygems@rubyforge.org",
-# which I do trust.
+# I install "rdoc-3.12.gem", a package signed by "drbrain". I've never heard
+# of "drbrain", but his certificate has a valid signature from the
+# "seattle.rb@seattlerb.org" certificate, which in turn has a valid signature
+# from the "rubygems@rubygems.org" certificate. Voila! At this point, it's
+# much more reasonable for me to trust a package signed by "drbrain", because
+# I can establish a chain to "rubygems@rubygems.org", which I do trust.
#
-# And the "--sign" option allows all this to happen. A developer creates
-# their build certificate with the "--build" option, then has their
-# certificate signed by taking it with them to their next regional Ruby meetup
-# (in our hypothetical example), and it's signed there by the person holding
-# the regional RubyGems signing certificate, which is signed at the next
-# RubyConf by the holder of the top-level RubyGems certificate. At each point
-# the issuer runs the same command:
+# === Signing certificates
+#
+# The <code>--sign</code> option allows all this to happen. A developer
+# creates their build certificate with the <code>--build</code> option, then
+# has their certificate signed by taking it with them to their next regional
+# Ruby meetup (in our hypothetical example), and it's signed there by the
+# person holding the regional RubyGems signing certificate, which is signed at
+# the next RubyConf by the holder of the top-level RubyGems certificate. At
+# each point the issuer runs the same command:
#
# # sign a certificate with the specified key and certificate
# # (note that this modifies client_cert.pem!)
# $ gem cert -K /mnt/floppy/issuer-priv_key.pem -C issuer-pub_cert.pem
# --sign client_cert.pem
#
-# Then the holder of issued certificate (in this case, our buddy
-# "alf@seattle"), can start using this signed certificate to sign RubyGems.
-# By the way, in order to let everyone else know about his new fancy signed
-# certificate, "alf@seattle" would change his gemspec file to look like this:
-#
-# # signing key (still kept in an undisclosed location!)
-# s.signing_key = '/mnt/floppy/alf-private_key.pem'
-#
-# # certificate chain (includes the issuer certificate now too)
-# s.cert_chain = ['/home/alf/doc/seattlerb-public_cert.pem',
-# '/home/alf/doc/alf_at_seattle-public_cert.pem']
+# Then the holder of issued certificate (in this case, your buddy "drbrain"),
+# can start using this signed certificate to sign RubyGems. By the way, in
+# order to let everyone else know about his new fancy signed certificate,
+# "drbrain" would save his newly signed certificate as
+# <code>~/.gem/gem-public_cert.pem</code>
#
-# Obviously, this RubyGems trust infrastructure doesn't exist yet. Also, in
-# the "real world" issuers actually generate the child certificate from a
+# Obviously this RubyGems trust infrastructure doesn't exist yet. Also, in
+# the "real world", issuers actually generate the child certificate from a
# certificate request, rather than sign an existing certificate. And our
# hypothetical infrastructure is missing a certificate revocation system.
# These are that can be fixed in the future...
#
-# I'm sure your new signed gem has finished installing by now (unless you're
-# installing rails and all it's dependencies, that is ;D). At this point you
-# should know how to do all of these new and interesting things:
+# At this point you should know how to do all of these new and interesting
+# things:
#
# * build a gem signing key and certificate
-# * modify your existing gems to support signing
# * adjust your security policy
# * modify your trusted certificate list
# * sign a certificate
#
-# If you've got any questions, feel free to contact me at the email address
-# below. The next couple of sections
+# == Manually verifying signatures
#
+# In case you don't trust RubyGems you can verify gem signatures manually:
#
-# == Command-Line Options
+# 1. Fetch and unpack the gem
#
-# Here's a brief summary of the certificate-related command line options:
+# gem fetch some_signed_gem
+# tar -xf some_signed_gem-1.0.gem
#
-# gem install
-# -P, --trust-policy POLICY Specify gem trust policy.
+# 2. Grab the public key from the gemspec
#
-# gem cert
-# -a, --add CERT Add a trusted certificate.
-# -l, --list List trusted certificates.
-# -r, --remove STRING Remove trusted certificates containing
-# STRING.
-# -b, --build EMAIL_ADDR Build private key and self-signed
-# certificate for EMAIL_ADDR.
-# -C, --certificate CERT Certificate for --sign command.
-# -K, --private-key KEY Private key for --sign command.
-# -s, --sign NEWCERT Sign a certificate with my key and
-# certificate.
+# gem spec some_signed_gem-1.0.gem cert_chain | \
+# ruby -rpsych -e 'puts Psych.load($stdin)' > public_key.crt
#
-# A more detailed description of each options is available in the walkthrough
-# above.
+# 3. Generate a SHA1 hash of the data.tar.gz
#
+# openssl dgst -sha1 < data.tar.gz > my.hash
+#
+# 4. Verify the signature
+#
+# openssl rsautl -verify -inkey public_key.crt -certin \
+# -in data.tar.gz.sig > verified.hash
+#
+# 5. Compare your hash to the verified hash
+#
+# diff -s verified.hash my.hash
+#
+# 6. Repeat 5 and 6 with metadata.gz
#
# == OpenSSL Reference
#
-# The .pem files generated by --build and --sign are just basic OpenSSL PEM
-# files. Here's a couple of useful commands for manipulating them:
+# The .pem files generated by --build and --sign are PEM files. Here's a
+# couple of useful OpenSSL commands for manipulating them:
#
# # convert a PEM format X509 certificate into DER format:
# # (note: Windows .cer files are X509 certificates in DER format)
# $ openssl x509 -in input.pem -outform der -out output.der
-#
+#
# # print out the certificate in a human-readable format:
# $ openssl x509 -in input.pem -noout -text
#
@@ -282,7 +295,7 @@ require 'rubygems/gem_openssl'
#
# # convert a PEM format RSA key into DER format:
# $ openssl rsa -in input_key.pem -outform der -out output_key.der
-#
+#
# # print out the key in a human readable format:
# $ openssl rsa -in input_key.pem -noout -text
#
@@ -291,8 +304,8 @@ require 'rubygems/gem_openssl'
# * There's no way to define a system-wide trust list.
# * custom security policies (from a YAML file, etc)
# * Simple method to generate a signed certificate request
-# * Support for OCSP, SCVP, CRLs, or some other form of cert
-# status check (list is in order of preference)
+# * Support for OCSP, SCVP, CRLs, or some other form of cert status check
+# (list is in order of preference)
# * Support for encrypted private keys
# * Some sort of semi-formal trust hierarchy (see long-winded explanation
# above)
@@ -302,485 +315,301 @@ require 'rubygems/gem_openssl'
# MediumSecurity and HighSecurity policies)
# * Better explanation of X509 naming (ie, we don't have to use email
# addresses)
-# * Possible alternate signing mechanisms (eg, via PGP). this could be done
-# pretty easily by adding a :signing_type attribute to the gemspec, then add
-# the necessary support in other places
# * Honor AIA field (see note about OCSP above)
-# * Maybe honor restriction extensions?
+# * Honor extension restrictions
# * Might be better to store the certificate chain as a PKCS#7 or PKCS#12
-# file, instead of an array embedded in the metadata. ideas?
-# * Possibly embed signature and key algorithms into metadata (right now
-# they're assumed to be the same as what's set in Gem::Security::OPT)
+# file, instead of an array embedded in the metadata.
#
-# == About the Author
+# == Original author
#
# Paul Duncan <pabs@pablotron.org>
-# http://pablotron.org/
+# https://pablotron.org/
module Gem::Security
+ ##
+ # Gem::Security default exception type
class Exception < Gem::Exception; end
- #
- # default options for most of the methods below
- #
- OPT = {
- # private key options
- :key_algo => Gem::SSL::PKEY_RSA,
- :key_size => 2048,
-
- # public cert options
- :cert_age => 365 * 24 * 3600, # 1 year
- :dgst_algo => Gem::SSL::DIGEST_SHA1,
-
- # x509 certificate extensions
- :cert_exts => {
- 'basicConstraints' => 'CA:FALSE',
- 'subjectKeyIdentifier' => 'hash',
- 'keyUsage' => 'keyEncipherment,dataEncipherment,digitalSignature',
- },
-
- # save the key and cert to a file in build_self_signed_cert()?
- :save_key => true,
- :save_cert => true,
-
- # if you define either of these, then they'll be used instead of
- # the output_fmt macro below
- :save_key_path => nil,
- :save_cert_path => nil,
-
- # output name format for self-signed certs
- :output_fmt => 'gem-%s.pem',
- :munge_re => Regexp.new(/[^a-z0-9_.-]+/),
-
- # output directory for trusted certificate checksums
- :trust_dir => File::join(Gem.user_home, '.gem', 'trust'),
-
- # default permissions for trust directory and certs
- :perms => {
- :trust_dir => 0700,
- :trusted_cert => 0600,
- :signing_cert => 0600,
- :signing_key => 0600,
- },
- }
+ ##
+ # Used internally to select the signing digest from all computed digests
+ DIGEST_NAME = "SHA256" # :nodoc:
+
+ ##
+ # Length of keys created by RSA and DSA keys
+
+ RSA_DSA_KEY_LENGTH = 3072
+
+ ##
+ # Default algorithm to use when building a key pair
+
+ DEFAULT_KEY_ALGORITHM = "RSA"
+
+ ##
+ # Named curve used for Elliptic Curve
+
+ EC_NAME = "secp384r1"
+
+ ##
+ # Cipher used to encrypt the key pair used to sign gems.
+ # Must be in the list returned by OpenSSL::Cipher.ciphers
+
+ KEY_CIPHER = OpenSSL::Cipher.new("AES-256-CBC") if defined?(OpenSSL::Cipher)
+
+ ##
+ # One day in seconds
+
+ ONE_DAY = 86_400
+
+ ##
+ # One year in seconds
+
+ ONE_YEAR = ONE_DAY * 365
+
+ ##
+ # The default set of extensions are:
#
- # A Gem::Security::Policy object encapsulates the settings for verifying
- # signed gem files. This is the base class. You can either declare an
- # instance of this or use one of the preset security policies below.
- #
- class Policy
- attr_accessor :verify_data, :verify_signer, :verify_chain,
- :verify_root, :only_trusted, :only_signed
-
- #
- # Create a new Gem::Security::Policy object with the given mode and
- # options.
- #
- def initialize(policy = {}, opt = {})
- # set options
- @opt = Gem::Security::OPT.merge(opt)
-
- # build policy
- policy.each_pair do |key, val|
- case key
- when :verify_data then @verify_data = val
- when :verify_signer then @verify_signer = val
- when :verify_chain then @verify_chain = val
- when :verify_root then @verify_root = val
- when :only_trusted then @only_trusted = val
- when :only_signed then @only_signed = val
- end
- end
+ # * The certificate is not a certificate authority
+ # * The key for the certificate may be used for key and data encipherment
+ # and digital signatures
+ # * The certificate contains a subject key identifier
+
+ EXTENSIONS = {
+ "basicConstraints" => "CA:FALSE",
+ "keyUsage" =>
+ "keyEncipherment,dataEncipherment,digitalSignature",
+ "subjectKeyIdentifier" => "hash",
+ }.freeze
+
+ def self.alt_name_or_x509_entry(certificate, x509_entry)
+ alt_name = certificate.extensions.find do |extension|
+ extension.oid == "#{x509_entry}AltName"
end
- #
- # Get the path to the file for this cert.
- #
- def self.trusted_cert_path(cert, opt = {})
- opt = Gem::Security::OPT.merge(opt)
+ return alt_name.value if alt_name
- # get digest algorithm, calculate checksum of root.subject
- algo = opt[:dgst_algo]
- dgst = algo.hexdigest(cert.subject.to_s)
+ certificate.send x509_entry
+ end
- # build path to trusted cert file
- name = "cert-#{dgst}.pem"
+ ##
+ # Creates an unsigned certificate for +subject+ and +key+. The lifetime of
+ # the key is from the current time to +age+ which defaults to one year.
+ #
+ # The +extensions+ restrict the key to the indicated uses.
- # join and return path components
- File::join(opt[:trust_dir], name)
- end
+ def self.create_cert(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1)
+ cert = OpenSSL::X509::Certificate.new
- #
- # Verify that the gem data with the given signature and signing chain
- # matched this security policy at the specified time.
- #
- def verify_gem(signature, data, chain, time = Time.now)
- Gem.ensure_ssl_available
- cert_class = OpenSSL::X509::Certificate
- exc = Gem::Security::Exception
- chain ||= []
-
- chain = chain.map{ |str| cert_class.new(str) }
- signer, ch_len = chain[-1], chain.size
-
- # make sure signature is valid
- if @verify_data
- # get digest algorithm (TODO: this should be configurable)
- dgst = @opt[:dgst_algo]
-
- # verify the data signature (this is the most important part, so don't
- # screw it up :D)
- v = signer.public_key.verify(dgst.new, signature, data)
- raise exc, "Invalid Gem Signature" unless v
-
- # make sure the signer is valid
- if @verify_signer
- # make sure the signing cert is valid right now
- v = signer.check_validity(nil, time)
- raise exc, "Invalid Signature: #{v[:desc]}" unless v[:is_valid]
- end
- end
+ cert.public_key = get_public_key(key)
+ cert.version = 2
+ cert.serial = serial
- # make sure the certificate chain is valid
- if @verify_chain
- # iterate down over the chain and verify each certificate against it's
- # issuer
- (ch_len - 1).downto(1) do |i|
- issuer, cert = chain[i - 1, 2]
- v = cert.check_validity(issuer, time)
- raise exc, "%s: cert = '%s', error = '%s'" % [
- 'Invalid Signing Chain', cert.subject, v[:desc]
- ] unless v[:is_valid]
- end
-
- # verify root of chain
- if @verify_root
- # make sure root is self-signed
- root = chain[0]
- raise exc, "%s: %s (subject = '%s', issuer = '%s')" % [
- 'Invalid Signing Chain Root',
- 'Subject does not match Issuer for Gem Signing Chain',
- root.subject.to_s,
- root.issuer.to_s,
- ] unless root.issuer.to_s == root.subject.to_s
-
- # make sure root is valid
- v = root.check_validity(root, time)
- raise exc, "%s: cert = '%s', error = '%s'" % [
- 'Invalid Signing Chain Root', root.subject, v[:desc]
- ] unless v[:is_valid]
-
- # verify that the chain root is trusted
- if @only_trusted
- # get digest algorithm, calculate checksum of root.subject
- algo = @opt[:dgst_algo]
- path = Gem::Security::Policy.trusted_cert_path(root, @opt)
-
- # check to make sure trusted path exists
- raise exc, "%s: cert = '%s', error = '%s'" % [
- 'Untrusted Signing Chain Root',
- root.subject.to_s,
- "path \"#{path}\" does not exist",
- ] unless File.exist?(path)
-
- # load calculate digest from saved cert file
- save_cert = OpenSSL::X509::Certificate.new(File.read(path))
- save_dgst = algo.digest(save_cert.public_key.to_s)
-
- # create digest of public key
- pkey_str = root.public_key.to_s
- cert_dgst = algo.digest(pkey_str)
-
- # now compare the two digests, raise exception
- # if they don't match
- raise exc, "%s: %s (saved = '%s', root = '%s')" % [
- 'Invalid Signing Chain Root',
- "Saved checksum doesn't match root checksum",
- save_dgst, cert_dgst,
- ] unless save_dgst == cert_dgst
- end
- end
-
- # return the signing chain
- chain.map { |cert| cert.subject }
- end
+ cert.not_before = Time.now
+ cert.not_after = Time.now + age
+
+ cert.subject = subject
+
+ ef = OpenSSL::X509::ExtensionFactory.new nil, cert
+
+ cert.extensions = extensions.map do |ext_name, value|
+ ef.create_extension ext_name, value
end
+
+ cert
end
- #
- # No security policy: all package signature checks are disabled.
- #
- NoSecurity = Policy.new(
- :verify_data => false,
- :verify_signer => false,
- :verify_chain => false,
- :verify_root => false,
- :only_trusted => false,
- :only_signed => false
- )
+ ##
+ # Gets the right public key from a PKey instance
- #
- # AlmostNo security policy: only verify that the signing certificate is the
- # one that actually signed the data. Make no attempt to verify the signing
- # certificate chain.
- #
- # This policy is basically useless. better than nothing, but can still be
- # easily spoofed, and is not recommended.
- #
- AlmostNoSecurity = Policy.new(
- :verify_data => true,
- :verify_signer => false,
- :verify_chain => false,
- :verify_root => false,
- :only_trusted => false,
- :only_signed => false
- )
+ def self.get_public_key(key)
+ # Ruby 3.0 (Ruby/OpenSSL 2.2) or later
+ return OpenSSL::PKey.read(key.public_to_der) if key.respond_to?(:public_to_der)
+ return key.public_key unless key.is_a?(OpenSSL::PKey::EC)
- #
- # Low security policy: only verify that the signing certificate is actually
- # the gem signer, and that the signing certificate is valid.
- #
- # This policy is better than nothing, but can still be easily spoofed, and
- # is not recommended.
- #
- LowSecurity = Policy.new(
- :verify_data => true,
- :verify_signer => true,
- :verify_chain => false,
- :verify_root => false,
- :only_trusted => false,
- :only_signed => false
- )
+ ec_key = OpenSSL::PKey::EC.new(key.group.curve_name)
+ ec_key.public_key = key.public_key
+ ec_key
+ end
- #
- # Medium security policy: verify the signing certificate, verify the signing
- # certificate chain all the way to the root certificate, and only trust root
- # certificates that we have explicitly allowed trust for.
- #
- # This security policy is reasonable, but it allows unsigned packages, so a
- # malicious person could simply delete the package signature and pass the
- # gem off as unsigned.
- #
- MediumSecurity = Policy.new(
- :verify_data => true,
- :verify_signer => true,
- :verify_chain => true,
- :verify_root => true,
- :only_trusted => true,
- :only_signed => false
- )
+ ##
+ # Creates a self-signed certificate with an issuer and subject from +email+,
+ # a subject alternative name of +email+ and the given +extensions+ for the
+ # +key+.
- #
- # High security policy: only allow signed gems to be installed, verify the
- # signing certificate, verify the signing certificate chain all the way to
- # the root certificate, and only trust root certificates that we have
- # explicitly allowed trust for.
- #
- # This security policy is significantly more difficult to bypass, and offers
- # a reasonable guarantee that the contents of the gem have not been altered.
- #
- HighSecurity = Policy.new(
- :verify_data => true,
- :verify_signer => true,
- :verify_chain => true,
- :verify_root => true,
- :only_trusted => true,
- :only_signed => true
- )
+ def self.create_cert_email(email, key, age = ONE_YEAR, extensions = EXTENSIONS)
+ subject = email_to_name email
- #
- # Hash of configured security policies
- #
- Policies = {
- 'NoSecurity' => NoSecurity,
- 'AlmostNoSecurity' => AlmostNoSecurity,
- 'LowSecurity' => LowSecurity,
- 'MediumSecurity' => MediumSecurity,
- 'HighSecurity' => HighSecurity,
- }
+ extensions = extensions.merge "subjectAltName" => "email:#{email}"
- #
- # Sign the cert cert with @signing_key and @signing_cert, using the digest
- # algorithm opt[:dgst_algo]. Returns the newly signed certificate.
- #
- def self.sign_cert(cert, signing_key, signing_cert, opt = {})
- opt = OPT.merge(opt)
+ create_cert_self_signed subject, key, age, extensions
+ end
- # set up issuer information
- cert.issuer = signing_cert.subject
- cert.sign(signing_key, opt[:dgst_algo].new)
+ ##
+ # Creates a self-signed certificate with an issuer and subject of +subject+
+ # and the given +extensions+ for the +key+.
- cert
+ def self.create_cert_self_signed(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1)
+ certificate = create_cert subject, key, age, extensions
+
+ sign certificate, key, certificate, age, extensions, serial
end
- #
- # Make sure the trust directory exists. If it does exist, make sure it's
- # actually a directory. If not, then create it with the appropriate
- # permissions.
- #
- def self.verify_trust_dir(path, perms)
- # if the directory exists, then make sure it is in fact a directory. if
- # it doesn't exist, then create it with the appropriate permissions
- if File.exist?(path)
- # verify that the trust directory is actually a directory
- unless File.directory?(path)
- err = "trust directory #{path} isn't a directory"
- raise Gem::Security::Exception, err
+ ##
+ # Creates a new digest instance using the specified +algorithm+. The default
+ # is SHA256.
+
+ def self.create_digest(algorithm = DIGEST_NAME)
+ OpenSSL::Digest.new(algorithm)
+ end
+
+ ##
+ # Creates a new key pair of the specified +algorithm+. RSA, DSA, and EC
+ # are supported.
+
+ def self.create_key(algorithm)
+ if defined?(OpenSSL::PKey)
+ case algorithm.downcase
+ when "dsa"
+ OpenSSL::PKey::DSA.new(RSA_DSA_KEY_LENGTH)
+ when "rsa"
+ OpenSSL::PKey::RSA.new(RSA_DSA_KEY_LENGTH)
+ when "ec"
+ OpenSSL::PKey::EC.generate(EC_NAME)
+ else
+ raise Gem::Security::Exception,
+ "#{algorithm} algorithm not found. RSA, DSA, and EC algorithms are supported."
end
- else
- # trust directory doesn't exist, so create it with permissions
- FileUtils.mkdir_p(path)
- FileUtils.chmod(perms, path)
end
end
- #
- # Build a certificate from the given DN and private key.
- #
- def self.build_cert(name, key, opt = {})
- Gem.ensure_ssl_available
- opt = OPT.merge(opt)
-
- # create new cert
- ret = OpenSSL::X509::Certificate.new
-
- # populate cert attributes
- ret.version = 2
- ret.serial = 0
- ret.public_key = key.public_key
- ret.not_before = Time.now
- ret.not_after = Time.now + opt[:cert_age]
- ret.subject = name
-
- # add certificate extensions
- ef = OpenSSL::X509::ExtensionFactory.new(nil, ret)
- ret.extensions = opt[:cert_exts].map { |k, v| ef.create_extension(k, v) }
-
- # sign cert
- i_key, i_cert = opt[:issuer_key] || key, opt[:issuer_cert] || ret
- ret = sign_cert(ret, i_key, i_cert, opt)
-
- # return cert
- ret
+ ##
+ # Turns +email_address+ into an OpenSSL::X509::Name
+
+ def self.email_to_name(email_address)
+ email_address = email_address.gsub(/[^\w@.-]+/i, "_")
+
+ cn, dcs = email_address.split "@"
+
+ dcs = dcs.split "."
+
+ OpenSSL::X509::Name.new([
+ ["CN", cn],
+ *dcs.map {|dc| ["DC", dc] },
+ ])
end
- #
- # Build a self-signed certificate for the given email address.
- #
- def self.build_self_signed_cert(email_addr, opt = {})
- Gem.ensure_ssl_available
- opt = OPT.merge(opt)
- path = { :key => nil, :cert => nil }
-
- # split email address up
- cn, dcs = email_addr.split('@')
- dcs = dcs.split('.')
-
- # munge email CN and DCs
- cn = cn.gsub(opt[:munge_re], '_')
- dcs = dcs.map { |dc| dc.gsub(opt[:munge_re], '_') }
-
- # create DN
- name = "CN=#{cn}/" << dcs.map { |dc| "DC=#{dc}" }.join('/')
- name = OpenSSL::X509::Name::parse(name)
-
- # build private key
- key = opt[:key_algo].new(opt[:key_size])
-
- # method name pretty much says it all :)
- verify_trust_dir(opt[:trust_dir], opt[:perms][:trust_dir])
-
- # if we're saving the key, then write it out
- if opt[:save_key]
- path[:key] = opt[:save_key_path] || (opt[:output_fmt] % 'private_key')
- File.open(path[:key], 'wb') do |file|
- file.chmod(opt[:perms][:signing_key])
- file.write(key.to_pem)
- end
+ ##
+ # Signs +expired_certificate+ with +private_key+ if the keys match and the
+ # expired certificate was self-signed.
+ #--
+ # TODO increment serial
+
+ def self.re_sign(expired_certificate, private_key, age = ONE_YEAR, extensions = EXTENSIONS)
+ raise Gem::Security::Exception,
+ "incorrect signing key for re-signing " +
+ expired_certificate.subject.to_s unless
+ expired_certificate.check_private_key(private_key)
+
+ unless expired_certificate.subject.to_s ==
+ expired_certificate.issuer.to_s
+ subject = alt_name_or_x509_entry expired_certificate, :subject
+ issuer = alt_name_or_x509_entry expired_certificate, :issuer
+
+ raise Gem::Security::Exception,
+ "#{subject} is not self-signed, contact #{issuer} " \
+ "to obtain a valid certificate"
end
- # build self-signed public cert from key
- cert = build_cert(name, key, opt)
+ serial = expired_certificate.serial + 1
- # if we're saving the cert, then write it out
- if opt[:save_cert]
- path[:cert] = opt[:save_cert_path] || (opt[:output_fmt] % 'public_cert')
- File.open(path[:cert], 'wb') do |file|
- file.chmod(opt[:perms][:signing_cert])
- file.write(cert.to_pem)
- end
- end
+ create_cert_self_signed(expired_certificate.subject, private_key, age,
+ extensions, serial)
+ end
- # return key, cert, and paths (if applicable)
- { :key => key, :cert => cert,
- :key_path => path[:key], :cert_path => path[:cert] }
+ ##
+ # Resets the trust directory for verifying gems.
+
+ def self.reset
+ @trust_dir = nil
end
+ ##
+ # Sign the public key from +certificate+ with the +signing_key+ and
+ # +signing_cert+, using the Gem::Security::DIGEST_NAME. Uses the
+ # default certificate validity range and extensions.
#
- # Add certificate to trusted cert list.
- #
- # Note: At the moment these are stored in OPT[:trust_dir], although that
- # directory may change in the future.
- #
- def self.add_trusted_cert(cert, opt = {})
- opt = OPT.merge(opt)
+ # Returns the newly signed certificate.
- # get destination path
- path = Gem::Security::Policy.trusted_cert_path(cert, opt)
+ def self.sign(certificate, signing_key, signing_cert, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1)
+ signee_subject = certificate.subject
+ signee_key = certificate.public_key
- # verify trust directory (can't write to nowhere, you know)
- verify_trust_dir(opt[:trust_dir], opt[:perms][:trust_dir])
+ alt_name = certificate.extensions.find do |extension|
+ extension.oid == "subjectAltName"
+ end
- # write cert to output file
- File.open(path, 'wb') do |file|
- file.chmod(opt[:perms][:trusted_cert])
- file.write(cert.to_pem)
+ extensions = extensions.merge "subjectAltName" => alt_name.value if
+ alt_name
+
+ issuer_alt_name = signing_cert.extensions.find do |extension|
+ extension.oid == "subjectAltName"
end
- # return nil
- nil
+ extensions = extensions.merge "issuerAltName" => issuer_alt_name.value if
+ issuer_alt_name
+
+ signed = create_cert signee_subject, signee_key, age, extensions, serial
+ signed.issuer = signing_cert.subject
+
+ signed.sign signing_key, Gem::Security::DIGEST_NAME
end
- #
- # Basic OpenSSL-based package signing class.
- #
- class Signer
- attr_accessor :key, :cert_chain
+ ##
+ # Returns a Gem::Security::TrustDir which wraps the directory where trusted
+ # certificates live.
- def initialize(key, cert_chain)
- Gem.ensure_ssl_available
- @algo = Gem::Security::OPT[:dgst_algo]
- @key, @cert_chain = key, cert_chain
+ def self.trust_dir
+ return @trust_dir if @trust_dir
- # check key, if it's a file, and if it's key, leave it alone
- if @key && !@key.kind_of?(OpenSSL::PKey::PKey)
- @key = OpenSSL::PKey::RSA.new(File.read(@key))
- end
+ dir = File.join Gem.user_home, ".gem", "trust"
- # check cert chain, if it's a file, load it, if it's cert data, convert
- # it into a cert object, and if it's a cert object, leave it alone
- if @cert_chain
- @cert_chain = @cert_chain.map do |cert|
- # check cert, if it's a file, load it, if it's cert data, convert it
- # into a cert object, and if it's a cert object, leave it alone
- if cert && !cert.kind_of?(OpenSSL::X509::Certificate)
- cert = File.read(cert) if File::exist?(cert)
- cert = OpenSSL::X509::Certificate.new(cert)
- end
- cert
- end
- end
- end
+ @trust_dir ||= Gem::Security::TrustDir.new dir
+ end
- #
- # Sign data with given digest algorithm
- #
- def sign(data)
- @key.sign(@algo.new, data)
+ ##
+ # Enumerates the trusted certificates via Gem::Security::TrustDir.
+
+ def self.trusted_certificates(&block)
+ trust_dir.each_certificate(&block)
+ end
+
+ ##
+ # Writes +pemmable+, which must respond to +to_pem+ to +path+ with the given
+ # +permissions+. If passed +cipher+ and +passphrase+ those arguments will be
+ # passed to +to_pem+.
+
+ def self.write(pemmable, path, permissions = 0o600, passphrase = nil, cipher = KEY_CIPHER)
+ path = File.expand_path path
+
+ File.open path, "wb", permissions do |io|
+ if passphrase && cipher
+ io.write pemmable.to_pem cipher, passphrase
+ else
+ io.write pemmable.to_pem
+ end
end
+ path
end
+
+ reset
+end
+
+if Gem::HAVE_OPENSSL
+ require_relative "security/policy"
+ require_relative "security/policies"
+ require_relative "security/trust_dir"
end
+require_relative "security/signer"
diff --git a/lib/rubygems/security/policies.rb b/lib/rubygems/security/policies.rb
new file mode 100644
index 0000000000..41f66043ad
--- /dev/null
+++ b/lib/rubygems/security/policies.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+module Gem::Security
+ ##
+ # No security policy: all package signature checks are disabled.
+
+ NoSecurity = Policy.new(
+ "No Security",
+ verify_data: false,
+ verify_signer: false,
+ verify_chain: false,
+ verify_root: false,
+ only_trusted: false,
+ only_signed: false
+ )
+
+ ##
+ # AlmostNo security policy: only verify that the signing certificate is the
+ # one that actually signed the data. Make no attempt to verify the signing
+ # certificate chain.
+ #
+ # This policy is basically useless. better than nothing, but can still be
+ # easily spoofed, and is not recommended.
+
+ AlmostNoSecurity = Policy.new(
+ "Almost No Security",
+ verify_data: true,
+ verify_signer: false,
+ verify_chain: false,
+ verify_root: false,
+ only_trusted: false,
+ only_signed: false
+ )
+
+ ##
+ # Low security policy: only verify that the signing certificate is actually
+ # the gem signer, and that the signing certificate is valid.
+ #
+ # This policy is better than nothing, but can still be easily spoofed, and
+ # is not recommended.
+
+ LowSecurity = Policy.new(
+ "Low Security",
+ verify_data: true,
+ verify_signer: true,
+ verify_chain: false,
+ verify_root: false,
+ only_trusted: false,
+ only_signed: false
+ )
+
+ ##
+ # Medium security policy: verify the signing certificate, verify the signing
+ # certificate chain all the way to the root certificate, and only trust root
+ # certificates that we have explicitly allowed trust for.
+ #
+ # This security policy is reasonable, but it allows unsigned packages, so a
+ # malicious person could simply delete the package signature and pass the
+ # gem off as unsigned.
+
+ MediumSecurity = Policy.new(
+ "Medium Security",
+ verify_data: true,
+ verify_signer: true,
+ verify_chain: true,
+ verify_root: true,
+ only_trusted: true,
+ only_signed: false
+ )
+
+ ##
+ # High security policy: only allow signed gems to be installed, verify the
+ # signing certificate, verify the signing certificate chain all the way to
+ # the root certificate, and only trust root certificates that we have
+ # explicitly allowed trust for.
+ #
+ # This security policy is significantly more difficult to bypass, and offers
+ # a reasonable guarantee that the contents of the gem have not been altered.
+
+ HighSecurity = Policy.new(
+ "High Security",
+ verify_data: true,
+ verify_signer: true,
+ verify_chain: true,
+ verify_root: true,
+ only_trusted: true,
+ only_signed: true
+ )
+
+ ##
+ # Policy used to verify a certificate and key when signing a gem
+
+ SigningPolicy = Policy.new(
+ "Signing Policy",
+ verify_data: false,
+ verify_signer: true,
+ verify_chain: true,
+ verify_root: true,
+ only_trusted: false,
+ only_signed: false
+ )
+
+ ##
+ # Hash of configured security policies
+
+ Policies = {
+ "NoSecurity" => NoSecurity,
+ "AlmostNoSecurity" => AlmostNoSecurity,
+ "LowSecurity" => LowSecurity,
+ "MediumSecurity" => MediumSecurity,
+ "HighSecurity" => HighSecurity,
+ # SigningPolicy is not intended for use by `gem -P` so do not list it
+ }.freeze
+end
diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb
new file mode 100644
index 0000000000..128958ab80
--- /dev/null
+++ b/lib/rubygems/security/policy.rb
@@ -0,0 +1,288 @@
+# frozen_string_literal: true
+
+require_relative "../user_interaction"
+
+##
+# A Gem::Security::Policy object encapsulates the settings for verifying
+# signed gem files. This is the base class. You can either declare an
+# instance of this or use one of the preset security policies in
+# Gem::Security::Policies.
+
+class Gem::Security::Policy
+ include Gem::UserInteraction
+
+ attr_reader :name
+
+ attr_accessor :only_signed
+ attr_accessor :only_trusted
+ attr_accessor :verify_chain
+ attr_accessor :verify_data
+ attr_accessor :verify_root
+ attr_accessor :verify_signer
+
+ ##
+ # Create a new Gem::Security::Policy object with the given mode and
+ # options.
+
+ def initialize(name, policy = {}, opt = {})
+ @name = name
+
+ @opt = opt
+
+ # Default to security
+ @only_signed = true
+ @only_trusted = true
+ @verify_chain = true
+ @verify_data = true
+ @verify_root = true
+ @verify_signer = true
+
+ policy.each_pair do |key, val|
+ case key
+ when :verify_data then @verify_data = val
+ when :verify_signer then @verify_signer = val
+ when :verify_chain then @verify_chain = val
+ when :verify_root then @verify_root = val
+ when :only_trusted then @only_trusted = val
+ when :only_signed then @only_signed = val
+ end
+ end
+ end
+
+ ##
+ # Verifies each certificate in +chain+ has signed the following certificate
+ # and is valid for the given +time+.
+
+ def check_chain(chain, time)
+ raise Gem::Security::Exception, "missing signing chain" unless chain
+ raise Gem::Security::Exception, "empty signing chain" if chain.empty?
+
+ begin
+ chain.each_cons 2 do |issuer, cert|
+ check_cert cert, issuer, time
+ end
+
+ true
+ rescue Gem::Security::Exception => e
+ raise Gem::Security::Exception, "invalid signing chain: #{e.message}"
+ end
+ end
+
+ ##
+ # Verifies that +data+ matches the +signature+ created by +public_key+ and
+ # the +digest+ algorithm.
+
+ def check_data(public_key, digest, signature, data)
+ raise Gem::Security::Exception, "invalid signature" unless
+ public_key.verify digest, signature, data.digest
+
+ true
+ end
+
+ ##
+ # Ensures that +signer+ is valid for +time+ and was signed by the +issuer+.
+ # If the +issuer+ is +nil+ no verification is performed.
+
+ def check_cert(signer, issuer, time)
+ raise Gem::Security::Exception, "missing signing certificate" unless
+ signer
+
+ message = "certificate #{signer.subject}"
+
+ if (not_before = signer.not_before) && not_before > time
+ raise Gem::Security::Exception,
+ "#{message} not valid before #{not_before}"
+ end
+
+ if (not_after = signer.not_after) && not_after < time
+ raise Gem::Security::Exception, "#{message} not valid after #{not_after}"
+ end
+
+ if issuer && !signer.verify(issuer.public_key)
+ raise Gem::Security::Exception,
+ "#{message} was not issued by #{issuer.subject}"
+ end
+
+ true
+ end
+
+ ##
+ # Ensures the public key of +key+ matches the public key in +signer+
+
+ def check_key(signer, key)
+ unless signer && key
+ return true unless @only_signed
+
+ raise Gem::Security::Exception, "missing key or signature"
+ end
+
+ raise Gem::Security::Exception,
+ "certificate #{signer.subject} does not match the signing key" unless
+ signer.check_private_key(key)
+
+ true
+ end
+
+ ##
+ # Ensures the root certificate in +chain+ is self-signed and valid for
+ # +time+.
+
+ def check_root(chain, time)
+ raise Gem::Security::Exception, "missing signing chain" unless chain
+
+ root = chain.first
+
+ raise Gem::Security::Exception, "missing root certificate" unless root
+
+ raise Gem::Security::Exception,
+ "root certificate #{root.subject} is not self-signed " \
+ "(issuer #{root.issuer})" if
+ root.issuer != root.subject
+
+ check_cert root, root, time
+ end
+
+ ##
+ # Ensures the root of +chain+ has a trusted certificate in Gem::Security.trust_dir and
+ # the digests of the two certificates match according to +digester+
+
+ def check_trust(chain, digester, trust_dir)
+ raise Gem::Security::Exception, "missing signing chain" unless chain
+
+ root = chain.first
+
+ raise Gem::Security::Exception, "missing root certificate" unless root
+
+ path = Gem::Security.trust_dir.cert_path root
+
+ unless File.exist? path
+ message = "root cert #{root.subject} is not trusted".dup
+
+ message << " (root of signing cert #{chain.last.subject})" if
+ chain.length > 1
+
+ raise Gem::Security::Exception, message
+ end
+
+ save_cert = OpenSSL::X509::Certificate.new File.read path
+ save_dgst = digester.digest save_cert.public_key.to_pem
+
+ pkey_str = root.public_key.to_pem
+ cert_dgst = digester.digest pkey_str
+
+ raise Gem::Security::Exception,
+ "trusted root certificate #{root.subject} checksum " \
+ "does not match signing root certificate checksum" unless
+ save_dgst == cert_dgst
+
+ true
+ end
+
+ ##
+ # Extracts the email or subject from +certificate+
+
+ def subject(certificate) # :nodoc:
+ certificate.extensions.each do |extension|
+ next unless extension.oid == "subjectAltName"
+
+ return extension.value
+ end
+
+ certificate.subject.to_s
+ end
+
+ def inspect # :nodoc:
+ format("[Policy: %s - data: %p signer: %p chain: %p root: %p " \
+ "signed-only: %p trusted-only: %p]", @name, @verify_chain, @verify_data, @verify_root, @verify_signer, @only_signed, @only_trusted)
+ end
+
+ ##
+ # For +full_name+, verifies the certificate +chain+ is valid, the +digests+
+ # match the signatures +signatures+ created by the signer depending on the
+ # +policy+ settings.
+ #
+ # If +key+ is given it is used to validate the signing certificate.
+
+ def verify(chain, key = nil, digests = {}, signatures = {}, full_name = "(unknown)")
+ if signatures.empty?
+ if @only_signed
+ raise Gem::Security::Exception,
+ "unsigned gems are not allowed by the #{name} policy"
+ elsif digests.empty?
+ # lack of signatures is irrelevant if there is nothing to check
+ # against
+ else
+ alert_warning "#{full_name} is not signed"
+ return
+ end
+ end
+
+ opt = @opt
+ digester = Gem::Security.create_digest
+ trust_dir = opt[:trust_dir]
+ time = Time.now
+
+ _, signer_digests = digests.find do |_algorithm, file_digests|
+ file_digests.values.first.name == Gem::Security::DIGEST_NAME
+ end
+
+ if @verify_data
+ raise Gem::Security::Exception, "no digests provided (probable bug)" if
+ signer_digests.nil? || signer_digests.empty?
+ else
+ signer_digests = {}
+ end
+
+ signer = chain.last
+
+ check_key signer, key if key
+
+ check_cert signer, nil, time if @verify_signer
+
+ check_chain chain, time if @verify_chain
+
+ check_root chain, time if @verify_root
+
+ if @only_trusted
+ check_trust chain, digester, trust_dir
+ elsif signatures.empty? && digests.empty?
+ # trust is irrelevant if there's no signatures to verify
+ else
+ alert_warning "#{subject signer} is not trusted for #{full_name}"
+ end
+
+ signatures.each do |file, _|
+ digest = signer_digests[file]
+
+ raise Gem::Security::Exception, "missing digest for #{file}" unless
+ digest
+ end
+
+ signer_digests.each do |file, digest|
+ signature = signatures[file]
+
+ raise Gem::Security::Exception, "missing signature for #{file}" unless
+ signature
+
+ check_data signer.public_key, digester, signature, digest if @verify_data
+ end
+
+ true
+ end
+
+ ##
+ # Extracts the certificate chain from the +spec+ and calls #verify to ensure
+ # the signatures and certificate chain is valid according to the policy..
+
+ def verify_signatures(spec, digests, signatures)
+ chain = spec.cert_chain.map do |cert_pem|
+ OpenSSL::X509::Certificate.new cert_pem
+ end
+
+ verify chain, nil, digests, signatures, spec.full_name
+
+ true
+ end
+
+ alias_method :to_s, :name # :nodoc:
+end
diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb
new file mode 100644
index 0000000000..eeeeb52906
--- /dev/null
+++ b/lib/rubygems/security/signer.rb
@@ -0,0 +1,212 @@
+# frozen_string_literal: true
+
+##
+# Basic OpenSSL-based package signing class.
+
+require_relative "../user_interaction"
+
+class Gem::Security::Signer
+ include Gem::UserInteraction
+
+ ##
+ # The chain of certificates for signing including the signing certificate
+
+ attr_accessor :cert_chain
+
+ ##
+ # The private key for the signing certificate
+
+ attr_accessor :key
+
+ ##
+ # The digest algorithm used to create the signature
+
+ attr_reader :digest_algorithm
+
+ ##
+ # The name of the digest algorithm, used to pull digests out of the hash by
+ # name.
+
+ attr_reader :digest_name # :nodoc:
+
+ ##
+ # Gem::Security::Signer options
+
+ attr_reader :options
+
+ DEFAULT_OPTIONS = {
+ expiration_length_days: 365,
+ }.freeze
+
+ ##
+ # Attempts to re-sign an expired cert with a given private key
+ def self.re_sign_cert(expired_cert, expired_cert_path, private_key)
+ return unless expired_cert.not_after < Time.now
+
+ expiry = expired_cert.not_after.strftime("%Y%m%d%H%M%S")
+ expired_cert_file = "#{File.basename(expired_cert_path)}.expired.#{expiry}"
+ new_expired_cert_path = File.join(Gem.user_home, ".gem", expired_cert_file)
+
+ Gem::Security.write(expired_cert, new_expired_cert_path)
+
+ re_signed_cert = Gem::Security.re_sign(
+ expired_cert,
+ private_key,
+ Gem::Security::ONE_DAY * Gem.configuration.cert_expiration_length_days
+ )
+
+ Gem::Security.write(re_signed_cert, expired_cert_path)
+
+ yield(expired_cert_path, new_expired_cert_path) if block_given?
+ end
+
+ ##
+ # Creates a new signer with an RSA +key+ or path to a key, and a certificate
+ # +chain+ containing X509 certificates, encoding certificates or paths to
+ # certificates.
+
+ def initialize(key, cert_chain, passphrase = nil, options = {})
+ @cert_chain = cert_chain
+ @key = key
+ @passphrase = passphrase
+ @options = DEFAULT_OPTIONS.merge(options)
+
+ unless @key
+ default_key = File.join Gem.default_key_path
+ @key = default_key if File.exist? default_key
+ end
+
+ unless @cert_chain
+ default_cert = File.join Gem.default_cert_path
+ @cert_chain = [default_cert] if File.exist? default_cert
+ end
+
+ @digest_name = Gem::Security::DIGEST_NAME
+ @digest_algorithm = Gem::Security.create_digest(@digest_name)
+
+ if @key && !@key.is_a?(OpenSSL::PKey::PKey)
+ @key = OpenSSL::PKey.read(File.read(@key), @passphrase)
+ end
+
+ if @cert_chain
+ @cert_chain = @cert_chain.compact.map do |cert|
+ next cert if OpenSSL::X509::Certificate === cert
+
+ cert = File.read cert if File.exist? cert
+
+ OpenSSL::X509::Certificate.new cert
+ end
+
+ load_cert_chain
+ end
+ end
+
+ ##
+ # Extracts the full name of +cert+. If the certificate has a subjectAltName
+ # this value is preferred, otherwise the subject is used.
+
+ def extract_name(cert) # :nodoc:
+ subject_alt_name = cert.extensions.find {|e| e.oid == "subjectAltName" }
+
+ if subject_alt_name
+ /\Aemail:/ =~ subject_alt_name.value # rubocop:disable Performance/StartWith
+
+ $' || subject_alt_name.value
+ else
+ cert.subject
+ end
+ end
+
+ ##
+ # Loads any missing issuers in the cert chain from the trusted certificates.
+ #
+ # If the issuer does not exist it is ignored as it will be checked later.
+
+ def load_cert_chain # :nodoc:
+ return if @cert_chain.empty?
+
+ while @cert_chain.first.issuer.to_s != @cert_chain.first.subject.to_s do
+ issuer = Gem::Security.trust_dir.issuer_of @cert_chain.first
+
+ break unless issuer # cert chain is verified later
+
+ @cert_chain.unshift issuer
+ end
+ end
+
+ ##
+ # Sign data with given digest algorithm
+
+ def sign(data)
+ return unless @key
+
+ raise Gem::Security::Exception, "no certs provided" if @cert_chain.empty?
+
+ if @cert_chain.length == 1 && @cert_chain.last.not_after < Time.now
+ alert("Your certificate has expired, trying to re-sign it...")
+
+ re_sign_key(
+ expiration_length: (Gem::Security::ONE_DAY * options[:expiration_length_days])
+ )
+ end
+
+ full_name = extract_name @cert_chain.last
+
+ Gem::Security::SigningPolicy.verify @cert_chain, @key, {}, {}, full_name
+
+ @key.sign @digest_algorithm.new, data
+ end
+
+ ##
+ # Attempts to re-sign the private key if the signing certificate is expired.
+ #
+ # The key will be re-signed if:
+ # * The expired certificate is self-signed
+ # * The expired certificate is saved at ~/.gem/gem-public_cert.pem
+ # and the private key is saved at ~/.gem/gem-private_key.pem
+ # * There is no file matching the expiry date at
+ # ~/.gem/gem-public_cert.pem.expired.%Y%m%d%H%M%S
+ #
+ # If the signing certificate can be re-signed the expired certificate will
+ # be saved as ~/.gem/gem-public_cert.pem.expired.%Y%m%d%H%M%S where the
+ # expiry time (not after) is used for the timestamp.
+
+ def re_sign_key(expiration_length: Gem::Security::ONE_YEAR) # :nodoc:
+ old_cert = @cert_chain.last
+
+ disk_cert_path = File.join(Gem.default_cert_path)
+ disk_cert = begin
+ File.read(disk_cert_path)
+ rescue StandardError
+ nil
+ end
+
+ disk_key_path = File.join(Gem.default_key_path)
+ disk_key = begin
+ OpenSSL::PKey.read(File.read(disk_key_path), @passphrase)
+ rescue StandardError
+ nil
+ end
+
+ return unless disk_key
+
+ if disk_key.to_pem == @key.to_pem && disk_cert == old_cert.to_pem
+ expiry = old_cert.not_after.strftime("%Y%m%d%H%M%S")
+ old_cert_file = "gem-public_cert.pem.expired.#{expiry}"
+ old_cert_path = File.join(Gem.user_home, ".gem", old_cert_file)
+
+ unless File.exist?(old_cert_path)
+ Gem::Security.write(old_cert, old_cert_path)
+
+ cert = Gem::Security.re_sign(old_cert, @key, expiration_length)
+
+ Gem::Security.write(cert, disk_cert_path)
+
+ alert("Your cert: #{disk_cert_path} has been auto re-signed with the key: #{disk_key_path}")
+ alert("Your expired cert will be located at: #{old_cert_path}")
+
+ @cert_chain = [cert]
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/security/trust_dir.rb b/lib/rubygems/security/trust_dir.rb
new file mode 100644
index 0000000000..d23d161cfe
--- /dev/null
+++ b/lib/rubygems/security/trust_dir.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+##
+# The TrustDir manages the trusted certificates for gem signature
+# verification.
+
+class Gem::Security::TrustDir
+ ##
+ # Default permissions for the trust directory and its contents
+
+ DEFAULT_PERMISSIONS = {
+ trust_dir: 0o700,
+ trusted_cert: 0o600,
+ }.freeze
+
+ ##
+ # The directory where trusted certificates will be stored.
+
+ attr_reader :dir
+
+ ##
+ # Creates a new TrustDir using +dir+ where the directory and file
+ # permissions will be checked according to +permissions+
+
+ def initialize(dir, permissions = DEFAULT_PERMISSIONS)
+ @dir = dir
+ @permissions = permissions
+
+ @digester = Gem::Security.create_digest
+ end
+
+ ##
+ # Returns the path to the trusted +certificate+
+
+ def cert_path(certificate)
+ name_path certificate.subject
+ end
+
+ ##
+ # Enumerates trusted certificates.
+
+ def each_certificate
+ return enum_for __method__ unless block_given?
+
+ glob = File.join @dir, "*.pem"
+
+ Dir[glob].each do |certificate_file|
+ certificate = load_certificate certificate_file
+
+ yield certificate, certificate_file
+ rescue OpenSSL::X509::CertificateError
+ next # HACK: warn
+ end
+ end
+
+ ##
+ # Returns the issuer certificate of the given +certificate+ if it exists in
+ # the trust directory.
+
+ def issuer_of(certificate)
+ path = name_path certificate.issuer
+
+ return unless File.exist? path
+
+ load_certificate path
+ end
+
+ ##
+ # Returns the path to the trusted certificate with the given ASN.1 +name+
+
+ def name_path(name)
+ digest = @digester.hexdigest name.to_s
+
+ File.join @dir, "cert-#{digest}.pem"
+ end
+
+ ##
+ # Loads the given +certificate_file+
+
+ def load_certificate(certificate_file)
+ pem = File.read certificate_file
+
+ OpenSSL::X509::Certificate.new pem
+ end
+
+ ##
+ # Add a certificate to trusted certificate list.
+
+ def trust_cert(certificate)
+ verify
+
+ destination = cert_path certificate
+
+ File.open destination, "wb", 0o600 do |io|
+ io.write certificate.to_pem
+ io.chmod(@permissions[:trusted_cert])
+ end
+ end
+
+ ##
+ # Make sure the trust directory exists. If it does exist, make sure it's
+ # actually a directory. If not, then create it with the appropriate
+ # permissions.
+
+ def verify
+ require "fileutils"
+ if File.exist? @dir
+ raise Gem::Security::Exception,
+ "trust directory #{@dir} is not a directory" unless
+ File.directory? @dir
+
+ FileUtils.chmod 0o700, @dir
+ else
+ FileUtils.mkdir_p @dir, mode: @permissions[:trust_dir]
+ end
+ end
+end
diff --git a/lib/rubygems/security_option.rb b/lib/rubygems/security_option.rb
new file mode 100644
index 0000000000..3a101fe9db
--- /dev/null
+++ b/lib/rubygems/security_option.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+#--
+# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
+# All rights reserved.
+# See LICENSE.txt for permissions.
+#++
+
+require_relative "../rubygems"
+
+# forward-declare
+
+module Gem::Security # :nodoc:
+ class Policy # :nodoc:
+ end
+end
+
+##
+# Mixin methods for security option for Gem::Commands
+
+module Gem::SecurityOption
+ def add_security_option
+ Gem::OptionParser.accept Gem::Security::Policy do |value|
+ require_relative "security"
+
+ raise Gem::OptionParser::InvalidArgument, "OpenSSL not installed" unless
+ defined?(Gem::Security::HighSecurity)
+
+ policy = Gem::Security::Policies[value]
+ unless policy
+ valid = Gem::Security::Policies.keys.sort
+ raise Gem::OptionParser::InvalidArgument, "#{value} (#{valid.join ", "} are valid)"
+ end
+ policy
+ end
+
+ add_option(:"Install/Update", "-P", "--trust-policy POLICY",
+ Gem::Security::Policy,
+ "Specify gem trust policy") do |value, options|
+ options[:security_policy] = value
+ end
+ end
+end
diff --git a/lib/rubygems/server.rb b/lib/rubygems/server.rb
deleted file mode 100644
index 2c617ff144..0000000000
--- a/lib/rubygems/server.rb
+++ /dev/null
@@ -1,629 +0,0 @@
-require 'webrick'
-require 'yaml'
-require 'zlib'
-require 'erb'
-
-require 'rubygems'
-require 'rubygems/doc_manager'
-
-##
-# Gem::Server and allows users to serve gems for consumption by
-# `gem --remote-install`.
-#
-# gem_server starts an HTTP server on the given port and serves the following:
-# * "/" - Browsing of gem spec files for installed gems
-# * "/specs.#{Gem.marshal_version}.gz" - specs name/version/platform index
-# * "/latest_specs.#{Gem.marshal_version}.gz" - latest specs
-# name/version/platform index
-# * "/quick/" - Individual gemspecs
-# * "/gems" - Direct access to download the installable gems
-# * legacy indexes:
-# * "/Marshal.#{Gem.marshal_version}" - Full SourceIndex dump of metadata
-# for installed gems
-# * "/yaml" - YAML dump of metadata for installed gems - deprecated
-#
-# == Usage
-#
-# gem_server = Gem::Server.new Gem.dir, 8089, false
-# gem_server.run
-#
-#--
-# TODO Refactor into a real WEBrick servlet to remove code duplication.
-
-class Gem::Server
-
- include Gem::UserInteraction
-
- DOC_TEMPLATE = <<-'WEBPAGE'
- <?xml version="1.0" encoding="iso-8859-1"?>
- <!DOCTYPE html
- PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-
- <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
- <head>
- <title>RubyGems Documentation Index</title>
- <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
- </head>
- <body>
- <div id="fileHeader">
- <h1>RubyGems Documentation Index</h1>
- </div>
- <!-- banner header -->
-
- <div id="bodyContent">
- <div id="contextContent">
- <div id="description">
- <h1>Summary</h1>
- <p>There are <%=values["gem_count"]%> gems installed:</p>
- <p>
- <%= values["specs"].map { |v| "<a href=\"##{v["name"]}\">#{v["name"]}</a>" }.join ', ' %>.
- <h1>Gems</h1>
-
- <dl>
- <% values["specs"].each do |spec| %>
- <dt>
- <% if spec["first_name_entry"] then %>
- <a name="<%=spec["name"]%>"></a>
- <% end %>
-
- <b><%=spec["name"]%> <%=spec["version"]%></b>
-
- <% if spec["rdoc_installed"] then %>
- <a href="<%=spec["doc_path"]%>">[rdoc]</a>
- <% else %>
- <span title="rdoc not installed">[rdoc]</span>
- <% end %>
-
- <% if spec["homepage"] then %>
- <a href="<%=spec["homepage"]%>" title="<%=spec["homepage"]%>">[www]</a>
- <% else %>
- <span title="no homepage available">[www]</span>
- <% end %>
-
- <% if spec["has_deps"] then %>
- - depends on
- <%= spec["dependencies"].map { |v| "<a href=\"##{v["name"]}\">#{v["name"]}</a>" }.join ', ' %>.
- <% end %>
- </dt>
- <dd>
- <%=spec["summary"]%>
- <% if spec["executables"] then %>
- <br/>
-
- <% if spec["only_one_executable"] then %>
- Executable is
- <% else %>
- Executables are
- <%end%>
-
- <%= spec["executables"].map { |v| "<span class=\"context-item-name\">#{v["executable"]}</span>"}.join ', ' %>.
-
- <%end%>
- <br/>
- <br/>
- </dd>
- <% end %>
- </dl>
-
- </div>
- </div>
- </div>
- <div id="validator-badges">
- <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
- </div>
- </body>
- </html>
- WEBPAGE
-
- # CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108
- RDOC_CSS = <<-RDOCCSS
-body {
- font-family: Verdana,Arial,Helvetica,sans-serif;
- font-size: 90%;
- margin: 0;
- margin-left: 40px;
- padding: 0;
- background: white;
-}
-
-h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; }
-h1 { font-size: 150%; }
-h2,h3,h4 { margin-top: 1em; }
-
-a { background: #eef; color: #039; text-decoration: none; }
-a:hover { background: #039; color: #eef; }
-
-/* Override the base stylesheets Anchor inside a table cell */
-td > a {
- background: transparent;
- color: #039;
- text-decoration: none;
-}
-
-/* and inside a section title */
-.section-title > a {
- background: transparent;
- color: #eee;
- text-decoration: none;
-}
-
-/* === Structural elements =================================== */
-
-div#index {
- margin: 0;
- margin-left: -40px;
- padding: 0;
- font-size: 90%;
-}
-
-
-div#index a {
- margin-left: 0.7em;
-}
-
-div#index .section-bar {
- margin-left: 0px;
- padding-left: 0.7em;
- background: #ccc;
- font-size: small;
-}
-
-
-div#classHeader, div#fileHeader {
- width: auto;
- color: white;
- padding: 0.5em 1.5em 0.5em 1.5em;
- margin: 0;
- margin-left: -40px;
- border-bottom: 3px solid #006;
-}
-
-div#classHeader a, div#fileHeader a {
- background: inherit;
- color: white;
-}
-
-div#classHeader td, div#fileHeader td {
- background: inherit;
- color: white;
-}
-
-
-div#fileHeader {
- background: #057;
-}
-
-div#classHeader {
- background: #048;
-}
-
-
-.class-name-in-header {
- font-size: 180%;
- font-weight: bold;
-}
-
-
-div#bodyContent {
- padding: 0 1.5em 0 1.5em;
-}
-
-div#description {
- padding: 0.5em 1.5em;
- background: #efefef;
- border: 1px dotted #999;
-}
-
-div#description h1,h2,h3,h4,h5,h6 {
- color: #125;;
- background: transparent;
-}
-
-div#validator-badges {
- text-align: center;
-}
-div#validator-badges img { border: 0; }
-
-div#copyright {
- color: #333;
- background: #efefef;
- font: 0.75em sans-serif;
- margin-top: 5em;
- margin-bottom: 0;
- padding: 0.5em 2em;
-}
-
-
-/* === Classes =================================== */
-
-table.header-table {
- color: white;
- font-size: small;
-}
-
-.type-note {
- font-size: small;
- color: #DEDEDE;
-}
-
-.xxsection-bar {
- background: #eee;
- color: #333;
- padding: 3px;
-}
-
-.section-bar {
- color: #333;
- border-bottom: 1px solid #999;
- margin-left: -20px;
-}
-
-
-.section-title {
- background: #79a;
- color: #eee;
- padding: 3px;
- margin-top: 2em;
- margin-left: -30px;
- border: 1px solid #999;
-}
-
-.top-aligned-row { vertical-align: top }
-.bottom-aligned-row { vertical-align: bottom }
-
-/* --- Context section classes ----------------------- */
-
-.context-row { }
-.context-item-name { font-family: monospace; font-weight: bold; color: black; }
-.context-item-value { font-size: small; color: #448; }
-.context-item-desc { color: #333; padding-left: 2em; }
-
-/* --- Method classes -------------------------- */
-.method-detail {
- background: #efefef;
- padding: 0;
- margin-top: 0.5em;
- margin-bottom: 1em;
- border: 1px dotted #ccc;
-}
-.method-heading {
- color: black;
- background: #ccc;
- border-bottom: 1px solid #666;
- padding: 0.2em 0.5em 0 0.5em;
-}
-.method-signature { color: black; background: inherit; }
-.method-name { font-weight: bold; }
-.method-args { font-style: italic; }
-.method-description { padding: 0 0.5em 0 0.5em; }
-
-/* --- Source code sections -------------------- */
-
-a.source-toggle { font-size: 90%; }
-div.method-source-code {
- background: #262626;
- color: #ffdead;
- margin: 1em;
- padding: 0.5em;
- border: 1px dashed #999;
- overflow: hidden;
-}
-
-div.method-source-code pre { color: #ffdead; overflow: hidden; }
-
-/* --- Ruby keyword styles --------------------- */
-
-.standalone-code { background: #221111; color: #ffdead; overflow: hidden; }
-
-.ruby-constant { color: #7fffd4; background: transparent; }
-.ruby-keyword { color: #00ffff; background: transparent; }
-.ruby-ivar { color: #eedd82; background: transparent; }
-.ruby-operator { color: #00ffee; background: transparent; }
-.ruby-identifier { color: #ffdead; background: transparent; }
-.ruby-node { color: #ffa07a; background: transparent; }
-.ruby-comment { color: #b22222; font-weight: bold; background: transparent; }
-.ruby-regexp { color: #ffa07a; background: transparent; }
-.ruby-value { color: #7fffd4; background: transparent; }
- RDOCCSS
-
- def self.run(options)
- new(options[:gemdir], options[:port], options[:daemon]).run
- end
-
- def initialize(gem_dir, port, daemon)
- Socket.do_not_reverse_lookup = true
-
- @gem_dir = gem_dir
- @port = port
- @daemon = daemon
- logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL
- @server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger
-
- @spec_dir = File.join @gem_dir, 'specifications'
-
- unless File.directory? @spec_dir then
- raise ArgumentError, "#{@gem_dir} does not appear to be a gem repository"
- end
-
- @source_index = Gem::SourceIndex.from_gems_in @spec_dir
- end
-
- def Marshal(req, res)
- @source_index.refresh!
-
- res['date'] = File.stat(@spec_dir).mtime
-
- index = Marshal.dump @source_index
-
- if req.request_method == 'HEAD' then
- res['content-length'] = index.length
- return
- end
-
- if req.path =~ /Z$/ then
- res['content-type'] = 'application/x-deflate'
- index = Gem.deflate index
- else
- res['content-type'] = 'application/octet-stream'
- end
-
- res.body << index
- end
-
- def latest_specs(req, res)
- @source_index.refresh!
-
- res['content-type'] = 'application/x-gzip'
-
- res['date'] = File.stat(@spec_dir).mtime
-
- specs = @source_index.latest_specs.sort.map do |spec|
- platform = spec.original_platform
- platform = Gem::Platform::RUBY if platform.nil?
- [spec.name, spec.version, platform]
- end
-
- specs = Marshal.dump specs
-
- if req.path =~ /\.gz$/ then
- specs = Gem.gzip specs
- res['content-type'] = 'application/x-gzip'
- else
- res['content-type'] = 'application/octet-stream'
- end
-
- if req.request_method == 'HEAD' then
- res['content-length'] = specs.length
- else
- res.body << specs
- end
- end
-
- def quick(req, res)
- @source_index.refresh!
-
- res['content-type'] = 'text/plain'
- res['date'] = File.stat(@spec_dir).mtime
-
- case req.request_uri.path
- when '/quick/index' then
- res.body << @source_index.map { |name,| name }.sort.join("\n")
- when '/quick/index.rz' then
- index = @source_index.map { |name,| name }.sort.join("\n")
- res['content-type'] = 'application/x-deflate'
- res.body << Gem.deflate(index)
- when '/quick/latest_index' then
- index = @source_index.latest_specs.map { |spec| spec.full_name }
- res.body << index.sort.join("\n")
- when '/quick/latest_index.rz' then
- index = @source_index.latest_specs.map { |spec| spec.full_name }
- res['content-type'] = 'application/x-deflate'
- res.body << Gem.deflate(index.sort.join("\n"))
- when %r|^/quick/(Marshal.#{Regexp.escape Gem.marshal_version}/)?(.*?)-([0-9.]+)(-.*?)?\.gemspec\.rz$| then
- dep = Gem::Dependency.new $2, $3
- specs = @source_index.search dep
- marshal_format = $1
-
- selector = [$2, $3, $4].map { |s| s.inspect }.join ' '
-
- platform = if $4 then
- Gem::Platform.new $4.sub(/^-/, '')
- else
- Gem::Platform::RUBY
- end
-
- specs = specs.select { |s| s.platform == platform }
-
- if specs.empty? then
- res.status = 404
- res.body = "No gems found matching #{selector}"
- elsif specs.length > 1 then
- res.status = 500
- res.body = "Multiple gems found matching #{selector}"
- elsif marshal_format then
- res['content-type'] = 'application/x-deflate'
- res.body << Gem.deflate(Marshal.dump(specs.first))
- else # deprecated YAML format
- res['content-type'] = 'application/x-deflate'
- res.body << Gem.deflate(specs.first.to_yaml)
- end
- else
- raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found."
- end
- end
-
- def root(req, res)
- @source_index.refresh!
- res['date'] = File.stat(@spec_dir).mtime
-
- raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless
- req.path == '/'
-
- specs = []
- total_file_count = 0
-
- @source_index.each do |path, spec|
- total_file_count += spec.files.size
- deps = spec.dependencies.map do |dep|
- { "name" => dep.name,
- "type" => dep.type,
- "version" => dep.version_requirements.to_s, }
- end
-
- deps = deps.sort_by { |dep| [dep["name"].downcase, dep["version"]] }
- deps.last["is_last"] = true unless deps.empty?
-
- # executables
- executables = spec.executables.sort.collect { |exec| {"executable" => exec} }
- executables = nil if executables.empty?
- executables.last["is_last"] = true if executables
-
- specs << {
- "authors" => spec.authors.sort.join(", "),
- "date" => spec.date.to_s,
- "dependencies" => deps,
- "doc_path" => "/doc_root/#{spec.full_name}/rdoc/index.html",
- "executables" => executables,
- "only_one_executable" => (executables && executables.size == 1),
- "full_name" => spec.full_name,
- "has_deps" => !deps.empty?,
- "homepage" => spec.homepage,
- "name" => spec.name,
- "rdoc_installed" => Gem::DocManager.new(spec).rdoc_installed?,
- "summary" => spec.summary,
- "version" => spec.version.to_s,
- }
- end
-
- specs << {
- "authors" => "Chad Fowler, Rich Kilmer, Jim Weirich, Eric Hodel and others",
- "dependencies" => [],
- "doc_path" => "/doc_root/rubygems-#{Gem::RubyGemsVersion}/rdoc/index.html",
- "executables" => [{"executable" => 'gem', "is_last" => true}],
- "only_one_executable" => true,
- "full_name" => "rubygems-#{Gem::RubyGemsVersion}",
- "has_deps" => false,
- "homepage" => "http://rubygems.org/",
- "name" => 'rubygems',
- "rdoc_installed" => true,
- "summary" => "RubyGems itself",
- "version" => Gem::RubyGemsVersion,
- }
-
- specs = specs.sort_by { |spec| [spec["name"].downcase, spec["version"]] }
- specs.last["is_last"] = true
-
- # tag all specs with first_name_entry
- last_spec = nil
- specs.each do |spec|
- is_first = last_spec.nil? || (last_spec["name"].downcase != spec["name"].downcase)
- spec["first_name_entry"] = is_first
- last_spec = spec
- end
-
- # create page from template
- template = ERB.new(DOC_TEMPLATE)
- res['content-type'] = 'text/html'
-
- values = { "gem_count" => specs.size.to_s, "specs" => specs,
- "total_file_count" => total_file_count.to_s }
-
- result = template.result binding
- res.body = result
- end
-
- def run
- @server.listen nil, @port
-
- say "Starting gem server on http://localhost:#{@port}/"
-
- WEBrick::Daemon.start if @daemon
-
- @server.mount_proc "/yaml", method(:yaml)
- @server.mount_proc "/yaml.Z", method(:yaml)
-
- @server.mount_proc "/Marshal.#{Gem.marshal_version}", method(:Marshal)
- @server.mount_proc "/Marshal.#{Gem.marshal_version}.Z", method(:Marshal)
-
- @server.mount_proc "/specs.#{Gem.marshal_version}", method(:specs)
- @server.mount_proc "/specs.#{Gem.marshal_version}.gz", method(:specs)
-
- @server.mount_proc "/latest_specs.#{Gem.marshal_version}",
- method(:latest_specs)
- @server.mount_proc "/latest_specs.#{Gem.marshal_version}.gz",
- method(:latest_specs)
-
- @server.mount_proc "/quick/", method(:quick)
-
- @server.mount_proc("/gem-server-rdoc-style.css") do |req, res|
- res['content-type'] = 'text/css'
- res['date'] = File.stat(@spec_dir).mtime
- res.body << RDOC_CSS
- end
-
- @server.mount_proc "/", method(:root)
-
- paths = { "/gems" => "/cache/", "/doc_root" => "/doc/" }
- paths.each do |mount_point, mount_dir|
- @server.mount(mount_point, WEBrick::HTTPServlet::FileHandler,
- File.join(@gem_dir, mount_dir), true)
- end
-
- trap("INT") { @server.shutdown; exit! }
- trap("TERM") { @server.shutdown; exit! }
-
- @server.start
- end
-
- def specs(req, res)
- @source_index.refresh!
-
- res['date'] = File.stat(@spec_dir).mtime
-
- specs = @source_index.sort.map do |_, spec|
- platform = spec.original_platform
- platform = Gem::Platform::RUBY if platform.nil?
- [spec.name, spec.version, platform]
- end
-
- specs = Marshal.dump specs
-
- if req.path =~ /\.gz$/ then
- specs = Gem.gzip specs
- res['content-type'] = 'application/x-gzip'
- else
- res['content-type'] = 'application/octet-stream'
- end
-
- if req.request_method == 'HEAD' then
- res['content-length'] = specs.length
- else
- res.body << specs
- end
- end
-
- def yaml(req, res)
- @source_index.refresh!
-
- res['date'] = File.stat(@spec_dir).mtime
-
- index = @source_index.to_yaml
-
- if req.path =~ /Z$/ then
- res['content-type'] = 'application/x-deflate'
- index = Gem.deflate index
- else
- res['content-type'] = 'text/plain'
- end
-
- if req.request_method == 'HEAD' then
- res['content-length'] = index.length
- return
- end
-
- res.body << index
- end
-
-end
-
diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb
new file mode 100644
index 0000000000..86717e3e71
--- /dev/null
+++ b/lib/rubygems/source.rb
@@ -0,0 +1,253 @@
+# frozen_string_literal: true
+
+require_relative "text"
+##
+# A Source knows how to list and fetch gems from a RubyGems marshal index.
+#
+# There are other Source subclasses for installed gems, local gems, the
+# Compact Index API and so-forth.
+
+class Gem::Source
+ include Comparable
+ include Gem::Text
+
+ FILES = { # :nodoc:
+ released: "specs",
+ latest: "latest_specs",
+ prerelease: "prerelease_specs",
+ }.freeze
+
+ ##
+ # The URI this source will fetch gems from.
+
+ attr_reader :uri
+
+ ##
+ # Creates a new Source which will use the index located at +uri+.
+
+ def initialize(uri)
+ require_relative "uri"
+ @uri = Gem::Uri.parse!(uri)
+ @update_cache = nil
+ end
+
+ ##
+ # Sources are ordered by installation preference.
+
+ def <=>(other)
+ case other
+ when Gem::Source::Installed,
+ Gem::Source::Local,
+ Gem::Source::Lock,
+ Gem::Source::SpecificFile,
+ Gem::Source::Git,
+ Gem::Source::Vendor then
+ -1
+ when Gem::Source then
+ unless @uri
+ return 0 unless other.uri
+ return 1
+ end
+
+ return -1 unless other.uri
+
+ # Returning 1 here ensures that when sorting a list of sources, the
+ # original ordering of sources supplied by the user is preserved.
+ return 1 unless @uri.to_s == other.uri.to_s
+
+ 0
+ end
+ end
+
+ def ==(other) # :nodoc:
+ self.class === other && @uri == other.uri
+ end
+
+ alias_method :eql?, :== # :nodoc:
+
+ ##
+ # Returns a Set that can fetch specifications from this source.
+ #
+ # The set will optionally fetch prereleases if requested.
+ #
+ def dependency_resolver_set(prerelease = false)
+ new_dependency_resolver_set.tap {|set| set.prerelease = prerelease }
+ end
+
+ def hash # :nodoc:
+ @uri.hash
+ end
+
+ ##
+ # Returns the local directory to write +uri+ to.
+
+ def cache_dir(uri)
+ # Correct for windows paths
+ escaped_path = uri.path.sub(%r{^/([a-z]):/}i, '/\\1-/')
+
+ File.join Gem.spec_cache_dir, "#{uri.host}%#{uri.port}", File.dirname(escaped_path)
+ end
+
+ ##
+ # Returns true when it is possible and safe to update the cache directory.
+
+ def update_cache?
+ return @update_cache unless @update_cache.nil?
+ @update_cache =
+ begin
+ File.stat(Gem.user_home).uid == Process.uid
+ rescue Errno::ENOENT
+ false
+ end
+ end
+
+ ##
+ # Fetches a specification for the given Gem::NameTuple.
+
+ def fetch_spec(name_tuple)
+ fetcher = Gem::RemoteFetcher.fetcher
+
+ spec_file_name = name_tuple.spec_name
+
+ source_uri = enforce_trailing_slash(uri) + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}"
+
+ cache_dir = cache_dir source_uri
+
+ local_spec = File.join cache_dir, spec_file_name
+
+ if File.exist? local_spec
+ spec = Gem.read_binary local_spec
+ Gem.load_safe_marshal
+ spec = begin
+ Gem::SafeMarshal.safe_load(spec)
+ rescue StandardError
+ nil
+ end
+ return spec if spec
+ end
+
+ source_uri.path << ".rz"
+
+ spec = fetcher.fetch_path source_uri
+ spec = Gem::Util.inflate spec
+
+ if update_cache?
+ require "fileutils"
+ FileUtils.mkdir_p cache_dir
+
+ File.open local_spec, "wb" do |io|
+ io.write spec
+ end
+ end
+
+ Gem.load_safe_marshal
+ # TODO: Investigate setting Gem::Specification#loaded_from to a URI
+ Gem::SafeMarshal.safe_load spec
+ end
+
+ ##
+ # Loads +type+ kind of specs fetching from +@uri+ if the on-disk cache is
+ # out of date.
+ #
+ # +type+ is one of the following:
+ #
+ # :released => Return the list of all released specs
+ # :latest => Return the list of only the highest version of each gem
+ # :prerelease => Return the list of all prerelease only specs
+ #
+
+ def load_specs(type)
+ file = FILES[type]
+ fetcher = Gem::RemoteFetcher.fetcher
+ file_name = "#{file}.#{Gem.marshal_version}"
+ spec_path = enforce_trailing_slash(uri) + "#{file_name}.gz"
+ cache_dir = cache_dir spec_path
+ local_file = File.join(cache_dir, file_name)
+ retried = false
+
+ if update_cache?
+ require "fileutils"
+ FileUtils.mkdir_p cache_dir
+ end
+
+ spec_dump = fetcher.cache_update_path spec_path, local_file, update_cache?
+
+ Gem.load_safe_marshal
+ begin
+ Gem::NameTuple.from_list Gem::SafeMarshal.safe_load(spec_dump)
+ rescue ArgumentError
+ if update_cache? && !retried
+ FileUtils.rm local_file
+ retried = true
+ retry
+ else
+ raise Gem::Exception.new("Invalid spec cache file in #{local_file}")
+ end
+ end
+ end
+
+ ##
+ # Downloads +spec+ and writes it to +dir+. See also
+ # Gem::RemoteFetcher#download.
+
+ def download(spec, dir = Dir.pwd)
+ fetcher = Gem::RemoteFetcher.fetcher
+ fetcher.download spec, uri.to_s, dir
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) do
+ q.group 2, "[Remote:", "]" do
+ q.breakable
+ q.text @uri.to_s
+
+ if api = uri
+ q.breakable
+ q.text "API URI: "
+ q.text api.to_s
+ end
+ end
+ end
+ end
+
+ def typo_squatting?(host, distance_threshold = 4)
+ return if @uri.host.nil?
+ levenshtein_distance(@uri.host, host).between? 1, distance_threshold
+ end
+
+ private
+
+ def new_dependency_resolver_set
+ return Gem::Resolver::IndexSet.new self if uri.scheme == "file"
+
+ fetch_uri = if uri.host == "rubygems.org"
+ index_uri = uri.dup
+ index_uri.host = "index.rubygems.org"
+ index_uri
+ else
+ uri
+ end
+
+ bundler_api_uri = enforce_trailing_slash(fetch_uri) + "versions"
+
+ begin
+ fetcher = Gem::RemoteFetcher.fetcher
+ response = fetcher.fetch_path bundler_api_uri, nil, true
+ rescue Gem::RemoteFetcher::FetchError
+ Gem::Resolver::IndexSet.new self
+ else
+ Gem::Resolver::APISet.new response.uri + "./info/"
+ end
+ end
+
+ def enforce_trailing_slash(uri)
+ uri.merge(uri.path.gsub(%r{/+$}, "") + "/")
+ end
+end
+
+require_relative "source/git"
+require_relative "source/installed"
+require_relative "source/specific_file"
+require_relative "source/local"
+require_relative "source/lock"
+require_relative "source/vendor"
diff --git a/lib/rubygems/source/git.rb b/lib/rubygems/source/git.rb
new file mode 100644
index 0000000000..baf2f9dd4c
--- /dev/null
+++ b/lib/rubygems/source/git.rb
@@ -0,0 +1,244 @@
+# frozen_string_literal: true
+
+##
+# A git gem for use in a gem dependencies file.
+#
+# Example:
+#
+# source =
+# Gem::Source::Git.new 'rake', 'git@example:rake.git', 'rake-10.1.0', false
+#
+# source.specs
+
+class Gem::Source::Git < Gem::Source
+ ##
+ # The name of the gem created by this git gem.
+
+ attr_reader :name
+
+ ##
+ # The commit reference used for checking out this git gem.
+
+ attr_reader :reference
+
+ ##
+ # When false the cache for this repository will not be updated.
+
+ attr_accessor :remote
+
+ ##
+ # The git repository this gem is sourced from.
+
+ attr_reader :repository
+
+ ##
+ # The directory for cache and git gem installation
+
+ attr_accessor :root_dir
+
+ ##
+ # Does this repository need submodules checked out too?
+
+ attr_reader :need_submodules
+
+ ##
+ # Creates a new git gem source for a gems from loaded from +repository+ at
+ # the given +reference+. The +name+ is only used to track the repository
+ # back to a gem dependencies file, it has no real significance as a git
+ # repository may contain multiple gems. If +submodules+ is true, submodules
+ # will be checked out when the gem is installed.
+
+ def initialize(name, repository, reference, submodules = false)
+ require_relative "../uri"
+ @uri = Gem::Uri.parse(repository)
+ @name = name
+ @repository = repository
+ @reference = reference || "HEAD"
+ @need_submodules = submodules
+
+ @remote = true
+ @root_dir = Gem.dir
+ end
+
+ def <=>(other)
+ case other
+ when Gem::Source::Git then
+ 0
+ when Gem::Source::Vendor,
+ Gem::Source::Lock then
+ -1
+ when Gem::Source then
+ 1
+ end
+ end
+
+ def ==(other) # :nodoc:
+ super &&
+ @name == other.name &&
+ @repository == other.repository &&
+ @reference == other.reference &&
+ @need_submodules == other.need_submodules
+ end
+
+ def git_command
+ ENV.fetch("git", "git")
+ end
+
+ ##
+ # Checks out the files for the repository into the install_dir.
+
+ def checkout # :nodoc:
+ cache
+
+ return false unless File.exist? repo_cache_dir
+
+ unless File.exist? install_dir
+ system git_command, "clone", "--quiet", "--no-checkout",
+ repo_cache_dir, install_dir
+ end
+
+ Dir.chdir install_dir do
+ system git_command, "fetch", "--quiet", "--force", "--tags", install_dir
+
+ success = system git_command, "reset", "--quiet", "--hard", rev_parse
+
+ if @need_submodules
+ require "open3"
+ _, status = Open3.capture2e(git_command, "submodule", "update", "--quiet", "--init", "--recursive")
+
+ success &&= status.success?
+ end
+
+ success
+ end
+ end
+
+ ##
+ # Creates a local cache repository for the git gem.
+
+ def cache # :nodoc:
+ return unless @remote
+
+ if File.exist? repo_cache_dir
+ Dir.chdir repo_cache_dir do
+ system git_command, "fetch", "--quiet", "--force", "--tags",
+ @repository, "refs/heads/*:refs/heads/*"
+ end
+ else
+ system git_command, "clone", "--quiet", "--bare", "--no-hardlinks",
+ @repository, repo_cache_dir
+ end
+ end
+
+ ##
+ # Directory where git gems get unpacked and so-forth.
+
+ def base_dir # :nodoc:
+ File.join @root_dir, "bundler"
+ end
+
+ ##
+ # A short reference for use in git gem directories
+
+ def dir_shortref # :nodoc:
+ rev_parse[0..11]
+ end
+
+ ##
+ # Nothing to download for git gems
+
+ def download(full_spec, path) # :nodoc:
+ end
+
+ ##
+ # The directory where the git gem will be installed.
+
+ def install_dir # :nodoc:
+ return unless File.exist? repo_cache_dir
+
+ File.join base_dir, "gems", "#{@name}-#{dir_shortref}"
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) do
+ q.group 2, "[Git: ", "]" do
+ q.breakable
+ q.text @repository
+
+ q.breakable
+ q.text @reference
+ end
+ end
+ end
+
+ ##
+ # The directory where the git gem's repository will be cached.
+
+ def repo_cache_dir # :nodoc:
+ File.join @root_dir, "cache", "bundler", "git", "#{@name}-#{uri_hash}"
+ end
+
+ ##
+ # Converts the git reference for the repository into a commit hash.
+
+ def rev_parse # :nodoc:
+ hash = nil
+
+ Dir.chdir repo_cache_dir do
+ hash = Gem::Util.popen(git_command, "rev-parse", @reference).strip
+ end
+
+ raise Gem::Exception,
+ "unable to find reference #{@reference} in #{@repository}" unless
+ $?.success?
+
+ hash
+ end
+
+ ##
+ # Loads all gemspecs in the repository
+
+ def specs
+ checkout
+
+ return [] unless install_dir
+
+ Dir.chdir install_dir do
+ Dir["{,*,*/*}.gemspec"].filter_map do |spec_file|
+ directory = File.dirname spec_file
+ file = File.basename spec_file
+
+ Dir.chdir directory do
+ spec = Gem::Specification.load file
+ if spec
+ spec.base_dir = base_dir
+
+ spec.extension_dir =
+ File.join base_dir, "extensions", Gem::Platform.local.to_s,
+ Gem.extension_api_version, "#{name}-#{dir_shortref}"
+
+ spec.full_gem_path = File.dirname spec.loaded_from if spec
+ end
+ spec
+ end
+ end
+ end
+ end
+
+ ##
+ # A hash for the git gem based on the git repository Gem::URI.
+
+ def uri_hash # :nodoc:
+ require_relative "../openssl"
+
+ normalized =
+ if @repository.match?(%r{^\w+://(\w+@)?})
+ uri = Gem::URI(@repository).normalize.to_s.sub %r{/$},""
+ uri.sub(/\A(\w+)/) { $1.downcase }
+ else
+ @repository
+ end
+
+ OpenSSL::Digest::SHA1.hexdigest normalized
+ end
+end
diff --git a/lib/rubygems/source/installed.rb b/lib/rubygems/source/installed.rb
new file mode 100644
index 0000000000..f5c96fee51
--- /dev/null
+++ b/lib/rubygems/source/installed.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+##
+# Represents an installed gem. This is used for dependency resolution.
+
+class Gem::Source::Installed < Gem::Source
+ def initialize # :nodoc:
+ @uri = nil
+ end
+
+ ##
+ # Installed sources sort before all other sources
+
+ def <=>(other)
+ case other
+ when Gem::Source::Git,
+ Gem::Source::Lock,
+ Gem::Source::Vendor then
+ -1
+ when Gem::Source::Installed then
+ 0
+ when Gem::Source then
+ 1
+ end
+ end
+
+ ##
+ # We don't need to download an installed gem
+
+ def download(spec, path)
+ nil
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) do
+ q.text "[Installed]"
+ end
+ end
+end
diff --git a/lib/rubygems/source/local.rb b/lib/rubygems/source/local.rb
new file mode 100644
index 0000000000..4bef31a265
--- /dev/null
+++ b/lib/rubygems/source/local.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+##
+# The local source finds gems in the current directory for fulfilling
+# dependencies.
+
+class Gem::Source::Local < Gem::Source
+ def initialize # :nodoc:
+ @specs = nil
+ @api_uri = nil
+ @uri = nil
+ @load_specs_names = {}
+ end
+
+ ##
+ # Local sorts before Gem::Source and after Gem::Source::Installed
+
+ def <=>(other)
+ case other
+ when Gem::Source::Installed,
+ Gem::Source::Lock then
+ -1
+ when Gem::Source::Local then
+ 0
+ when Gem::Source then
+ 1
+ end
+ end
+
+ def inspect # :nodoc:
+ keys = @specs ? @specs.keys.sort : "NOT LOADED"
+ format("#<%s specs: %p>", self.class, keys)
+ end
+
+ def load_specs(type) # :nodoc:
+ @load_specs_names[type] ||= begin
+ names = []
+
+ @specs = {}
+
+ Dir["*.gem"].each do |file|
+ pkg = Gem::Package.new(file)
+ spec = pkg.spec
+ rescue SystemCallError, Gem::Package::FormatError
+ # ignore
+ else
+ tup = spec.name_tuple
+ @specs[tup] = [File.expand_path(file), pkg]
+
+ case type
+ when :released
+ unless pkg.spec.version.prerelease?
+ names << pkg.spec.name_tuple
+ end
+ when :prerelease
+ if pkg.spec.version.prerelease?
+ names << pkg.spec.name_tuple
+ end
+ when :latest
+ tup = pkg.spec.name_tuple
+
+ cur = names.find {|x| x.name == tup.name }
+ if !cur
+ names << tup
+ elsif cur.version < tup.version
+ names.delete cur
+ names << tup
+ end
+ else
+ names << pkg.spec.name_tuple
+ end
+ end
+
+ names
+ end
+ end
+
+ def find_gem(gem_name, version = Gem::Requirement.default, prerelease = false) # :nodoc:
+ find_all_gems(gem_name, version, prerelease).max_by(&:version)
+ end
+
+ def find_all_gems(gem_name, version = Gem::Requirement.default, prerelease = false) # :nodoc:
+ load_specs :complete
+
+ found = []
+
+ @specs.each do |n, data|
+ next unless n.name == gem_name
+ s = data[1].spec
+
+ if version.satisfied_by?(s.version)
+ if prerelease
+ found << s
+ elsif !s.version.prerelease? || version.prerelease?
+ found << s
+ end
+ end
+ end
+
+ found
+ end
+
+ def fetch_spec(name) # :nodoc:
+ load_specs :complete
+
+ if data = @specs[name]
+ data.last.spec
+ else
+ raise Gem::Exception, "Unable to find spec for #{name.inspect}"
+ end
+ end
+
+ def download(spec, cache_dir = nil) # :nodoc:
+ load_specs :complete
+
+ @specs.each do |_name, data|
+ return data[0] if data[1].spec == spec
+ end
+
+ raise Gem::Exception, "Unable to find file for '#{spec.full_name}'"
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) do
+ q.group 2, "[Local gems:", "]" do
+ q.breakable
+ if @specs
+ q.seplist @specs.keys do |v|
+ q.text v.full_name
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/source/lock.rb b/lib/rubygems/source/lock.rb
new file mode 100644
index 0000000000..70849210bd
--- /dev/null
+++ b/lib/rubygems/source/lock.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+##
+# A Lock source wraps an installed gem's source and sorts before other sources
+# during dependency resolution. This allows RubyGems to prefer gems from
+# dependency lock files.
+
+class Gem::Source::Lock < Gem::Source
+ ##
+ # The wrapped Gem::Source
+
+ attr_reader :wrapped
+
+ ##
+ # Creates a new Lock source that wraps +source+ and moves it earlier in the
+ # sort list.
+
+ def initialize(source)
+ @wrapped = source
+ end
+
+ def <=>(other) # :nodoc:
+ case other
+ when Gem::Source::Lock then
+ @wrapped <=> other.wrapped
+ when Gem::Source then
+ 1
+ end
+ end
+
+ def ==(other) # :nodoc:
+ (self <=> other) == 0
+ end
+
+ def hash # :nodoc:
+ @wrapped.hash ^ 3
+ end
+
+ ##
+ # Delegates to the wrapped source's fetch_spec method.
+
+ def fetch_spec(name_tuple)
+ @wrapped.fetch_spec name_tuple
+ end
+
+ def uri # :nodoc:
+ @wrapped.uri
+ end
+end
diff --git a/lib/rubygems/source/specific_file.rb b/lib/rubygems/source/specific_file.rb
new file mode 100644
index 0000000000..dde1d48a21
--- /dev/null
+++ b/lib/rubygems/source/specific_file.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+##
+# A source representing a single .gem file. This is used for installation of
+# local gems.
+
+class Gem::Source::SpecificFile < Gem::Source
+ ##
+ # The path to the gem for this specific file.
+
+ attr_reader :path
+
+ ##
+ # Creates a new SpecificFile for the gem in +file+
+
+ def initialize(file)
+ @uri = nil
+ @path = ::File.expand_path(file)
+
+ @package = Gem::Package.new @path
+ @spec = @package.spec
+ @name = @spec.name_tuple
+ end
+
+ ##
+ # The Gem::Specification extracted from this .gem.
+
+ attr_reader :spec
+
+ def load_specs(*a) # :nodoc:
+ [@name]
+ end
+
+ def fetch_spec(name) # :nodoc:
+ return @spec if name == @name
+ raise Gem::Exception, "Unable to find '#{name}'"
+ end
+
+ def download(spec, dir = nil) # :nodoc:
+ return @path if spec == @spec
+ raise Gem::Exception, "Unable to download '#{spec.full_name}'"
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) do
+ q.group 2, "[SpecificFile:", "]" do
+ q.breakable
+ q.text @path
+ end
+ end
+ end
+
+ ##
+ # Orders this source against +other+.
+ #
+ # If +other+ is a SpecificFile from a different gem name +nil+ is returned.
+ #
+ # If +other+ is a SpecificFile from the same gem name the versions are
+ # compared using Gem::Version#<=>
+ #
+ # Otherwise Gem::Source#<=> is used.
+
+ def <=>(other)
+ case other
+ when Gem::Source::SpecificFile then
+ return nil if @spec.name != other.spec.name
+
+ @spec.version <=> other.spec.version
+ else
+ super
+ end
+ end
+end
diff --git a/lib/rubygems/source/vendor.rb b/lib/rubygems/source/vendor.rb
new file mode 100644
index 0000000000..44ef614441
--- /dev/null
+++ b/lib/rubygems/source/vendor.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+##
+# This represents a vendored source that is similar to an installed gem.
+
+class Gem::Source::Vendor < Gem::Source::Installed
+ ##
+ # Creates a new Vendor source for a gem that was unpacked at +path+.
+
+ def initialize(path)
+ @uri = path
+ end
+
+ def <=>(other)
+ case other
+ when Gem::Source::Lock then
+ -1
+ when Gem::Source::Vendor then
+ 0
+ when Gem::Source then
+ 1
+ end
+ end
+end
diff --git a/lib/rubygems/source_index.rb b/lib/rubygems/source_index.rb
deleted file mode 100644
index 8a8db2ef0d..0000000000
--- a/lib/rubygems/source_index.rb
+++ /dev/null
@@ -1,559 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'rubygems'
-require 'rubygems/user_interaction'
-require 'rubygems/specification'
-module Gem
- autoload(:SpecFetcher, 'rubygems/spec_fetcher')
-end
-
-##
-# The SourceIndex object indexes all the gems available from a
-# particular source (e.g. a list of gem directories, or a remote
-# source). A SourceIndex maps a gem full name to a gem
-# specification.
-#
-# NOTE:: The class used to be named Cache, but that became
-# confusing when cached source fetchers where introduced. The
-# constant Gem::Cache is an alias for this class to allow old
-# YAMLized source index objects to load properly.
-
-class Gem::SourceIndex
-
- include Enumerable
-
- include Gem::UserInteraction
-
- attr_reader :gems # :nodoc:
-
- ##
- # Directories to use to refresh this SourceIndex when calling refresh!
-
- attr_accessor :spec_dirs
-
- class << self
- include Gem::UserInteraction
-
- ##
- # Factory method to construct a source index instance for a given
- # path.
- #
- # deprecated::
- # If supplied, from_installed_gems will act just like
- # +from_gems_in+. This argument is deprecated and is provided
- # just for backwards compatibility, and should not generally
- # be used.
- #
- # return::
- # SourceIndex instance
-
- def from_installed_gems(*deprecated)
- if deprecated.empty?
- from_gems_in(*installed_spec_directories)
- else
- from_gems_in(*deprecated) # HACK warn
- end
- end
-
- ##
- # Returns a list of directories from Gem.path that contain specifications.
-
- def installed_spec_directories
- Gem.path.collect { |dir| File.join(dir, "specifications") }
- end
-
- ##
- # Creates a new SourceIndex from the ruby format gem specifications in
- # +spec_dirs+.
-
- def from_gems_in(*spec_dirs)
- source_index = new
- source_index.spec_dirs = spec_dirs
- source_index.refresh!
- end
-
- ##
- # Loads a ruby-format specification from +file_name+ and returns the
- # loaded spec.
-
- def load_specification(file_name)
- begin
- spec_code = if RUBY_VERSION < '1.9' then
- File.read file_name
- else
- File.read file_name, :encoding => 'UTF-8'
- end.untaint
-
- gemspec = eval spec_code, binding, file_name
-
- if gemspec.is_a?(Gem::Specification)
- gemspec.loaded_from = file_name
- return gemspec
- end
- alert_warning "File '#{file_name}' does not evaluate to a gem specification"
- rescue SignalException, SystemExit
- raise
- rescue SyntaxError => e
- alert_warning e
- alert_warning spec_code
- rescue Exception => e
- alert_warning "#{e.inspect}\n#{spec_code}"
- alert_warning "Invalid .gemspec format in '#{file_name}'"
- end
- return nil
- end
-
- end
-
- ##
- # Constructs a source index instance from the provided
- # specifications
- #
- # specifications::
- # [Hash] hash of [Gem name, Gem::Specification] pairs
-
- def initialize(specifications={})
- @gems = specifications
- @spec_dirs = nil
- end
-
- ##
- # Reconstruct the source index from the specifications in +spec_dirs+.
-
- def load_gems_in(*spec_dirs)
- @gems.clear
-
- spec_dirs.reverse_each do |spec_dir|
- spec_files = Dir.glob File.join(spec_dir, '*.gemspec')
-
- spec_files.each do |spec_file|
- gemspec = self.class.load_specification spec_file.untaint
- add_spec gemspec if gemspec
- end
- end
-
- self
- end
-
- ##
- # Returns an Array specifications for the latest versions of each gem in
- # this index.
-
- def latest_specs
- result = Hash.new { |h,k| h[k] = [] }
- latest = {}
-
- sort.each do |_, spec|
- name = spec.name
- curr_ver = spec.version
- prev_ver = latest.key?(name) ? latest[name].version : nil
-
- next unless prev_ver.nil? or curr_ver >= prev_ver or
- latest[name].platform != Gem::Platform::RUBY
-
- if prev_ver.nil? or
- (curr_ver > prev_ver and spec.platform == Gem::Platform::RUBY) then
- result[name].clear
- latest[name] = spec
- end
-
- if spec.platform != Gem::Platform::RUBY then
- result[name].delete_if do |result_spec|
- result_spec.platform == spec.platform
- end
- end
-
- result[name] << spec
- end
-
- result.values.flatten
- end
-
- ##
- # Add a gem specification to the source index.
-
- def add_spec(gem_spec)
- @gems[gem_spec.full_name] = gem_spec
- end
-
- ##
- # Add gem specifications to the source index.
-
- def add_specs(*gem_specs)
- gem_specs.each do |spec|
- add_spec spec
- end
- end
-
- ##
- # Remove a gem specification named +full_name+.
-
- def remove_spec(full_name)
- @gems.delete(full_name)
- end
-
- ##
- # Iterate over the specifications in the source index.
-
- def each(&block) # :yields: gem.full_name, gem
- @gems.each(&block)
- end
-
- ##
- # The gem specification given a full gem spec name.
-
- def specification(full_name)
- @gems[full_name]
- end
-
- ##
- # The signature for the source index. Changes in the signature indicate a
- # change in the index.
-
- def index_signature
- require 'rubygems/digest/sha2'
-
- Gem::SHA256.new.hexdigest(@gems.keys.sort.join(',')).to_s
- end
-
- ##
- # The signature for the given gem specification.
-
- def gem_signature(gem_full_name)
- require 'rubygems/digest/sha2'
-
- Gem::SHA256.new.hexdigest(@gems[gem_full_name].to_yaml).to_s
- end
-
- def size
- @gems.size
- end
- alias length size
-
- ##
- # Find a gem by an exact match on the short name.
-
- def find_name(gem_name, version_requirement = Gem::Requirement.default)
- dep = Gem::Dependency.new(/^#{gem_name}$/, version_requirement)
- search dep
- end
-
- ##
- # Search for a gem by Gem::Dependency +gem_pattern+. If +only_platform+
- # is true, only gems matching Gem::Platform.local will be returned. An
- # Array of matching Gem::Specification objects is returned.
- #
- # For backwards compatibility, a String or Regexp pattern may be passed as
- # +gem_pattern+, and a Gem::Requirement for +platform_only+. This
- # behavior is deprecated and will be removed.
-
- def search(gem_pattern, platform_only = false)
- version_requirement = nil
- only_platform = false
-
- # TODO - Remove support and warning for legacy arguments after 2008/11
- unless Gem::Dependency === gem_pattern
- warn "#{Gem.location_of_caller.join ':'}:Warning: Gem::SourceIndex#search support for #{gem_pattern.class} patterns is deprecated"
- end
-
- case gem_pattern
- when Regexp then
- version_requirement = platform_only || Gem::Requirement.default
- when Gem::Dependency then
- only_platform = platform_only
- version_requirement = gem_pattern.version_requirements
- gem_pattern = if Regexp === gem_pattern.name then
- gem_pattern.name
- elsif gem_pattern.name.empty? then
- //
- else
- /^#{Regexp.escape gem_pattern.name}$/
- end
- else
- version_requirement = platform_only || Gem::Requirement.default
- gem_pattern = /#{gem_pattern}/i
- end
-
- unless Gem::Requirement === version_requirement then
- version_requirement = Gem::Requirement.create version_requirement
- end
-
- specs = @gems.values.select do |spec|
- spec.name =~ gem_pattern and
- version_requirement.satisfied_by? spec.version
- end
-
- if only_platform then
- specs = specs.select do |spec|
- Gem::Platform.match spec.platform
- end
- end
-
- specs.sort_by { |s| s.sort_obj }
- end
-
- ##
- # Replaces the gems in the source index from specifications in the
- # directories this source index was created from. Raises an exception if
- # this source index wasn't created from a directory (via from_gems_in or
- # from_installed_gems, or having spec_dirs set).
-
- def refresh!
- raise 'source index not created from disk' if @spec_dirs.nil?
- load_gems_in(*@spec_dirs)
- end
-
- ##
- # Returns an Array of Gem::Specifications that are not up to date.
-
- def outdated
- outdateds = []
-
- latest_specs.each do |local|
- dependency = Gem::Dependency.new local.name, ">= #{local.version}"
-
- begin
- fetcher = Gem::SpecFetcher.fetcher
- remotes = fetcher.find_matching dependency
- remotes = remotes.map { |(name, version,_),_| version }
- rescue Gem::RemoteFetcher::FetchError => e
- raise unless fetcher.warn_legacy e do
- require 'rubygems/source_info_cache'
-
- specs = Gem::SourceInfoCache.search_with_source dependency, true
-
- remotes = specs.map { |spec,| spec.version }
- end
- end
-
- latest = remotes.sort.last
-
- outdateds << local.name if latest and local.version < latest
- end
-
- outdateds
- end
-
- ##
- # Updates this SourceIndex from +source_uri+. If +all+ is false, only the
- # latest gems are fetched.
-
- def update(source_uri, all)
- source_uri = URI.parse source_uri unless URI::Generic === source_uri
- source_uri.path += '/' unless source_uri.path =~ /\/$/
-
- use_incremental = false
-
- begin
- gem_names = fetch_quick_index source_uri, all
- remove_extra gem_names
- missing_gems = find_missing gem_names
-
- return false if missing_gems.size.zero?
-
- say "Missing metadata for #{missing_gems.size} gems" if
- missing_gems.size > 0 and Gem.configuration.really_verbose
-
- use_incremental = missing_gems.size <= Gem.configuration.bulk_threshold
- rescue Gem::OperationNotSupportedError => ex
- alert_error "Falling back to bulk fetch: #{ex.message}" if
- Gem.configuration.really_verbose
- use_incremental = false
- end
-
- if use_incremental then
- update_with_missing(source_uri, missing_gems)
- else
- new_index = fetch_bulk_index(source_uri)
- @gems.replace(new_index.gems)
- end
-
- true
- end
-
- def ==(other) # :nodoc:
- self.class === other and @gems == other.gems
- end
-
- def dump
- Marshal.dump(self)
- end
-
- private
-
- def fetcher
- require 'rubygems/remote_fetcher'
-
- Gem::RemoteFetcher.fetcher
- end
-
- def fetch_index_from(source_uri)
- @fetch_error = nil
-
- indexes = %W[
- Marshal.#{Gem.marshal_version}.Z
- Marshal.#{Gem.marshal_version}
- yaml.Z
- yaml
- ]
-
- indexes.each do |name|
- spec_data = nil
- index = source_uri + name
- begin
- spec_data = fetcher.fetch_path index
- spec_data = unzip(spec_data) if name =~ /\.Z$/
-
- if name =~ /Marshal/ then
- return Marshal.load(spec_data)
- else
- return YAML.load(spec_data)
- end
- rescue => e
- if Gem.configuration.really_verbose then
- alert_error "Unable to fetch #{name}: #{e.message}"
- end
-
- @fetch_error = e
- end
- end
-
- nil
- end
-
- def fetch_bulk_index(source_uri)
- say "Bulk updating Gem source index for: #{source_uri}" if
- Gem.configuration.verbose
-
- index = fetch_index_from(source_uri)
- if index.nil? then
- raise Gem::RemoteSourceException,
- "Error fetching remote gem cache: #{@fetch_error}"
- end
- @fetch_error = nil
- index
- end
-
- ##
- # Get the quick index needed for incremental updates.
-
- def fetch_quick_index(source_uri, all)
- index = all ? 'index' : 'latest_index'
-
- zipped_index = fetcher.fetch_path source_uri + "quick/#{index}.rz"
-
- unzip(zipped_index).split("\n")
- rescue ::Exception => e
- unless all then
- say "Latest index not found, using quick index" if
- Gem.configuration.really_verbose
-
- fetch_quick_index source_uri, true
- else
- raise Gem::OperationNotSupportedError,
- "No quick index found: #{e.message}"
- end
- end
-
- ##
- # Make a list of full names for all the missing gemspecs.
-
- def find_missing(spec_names)
- unless defined? @originals then
- @originals = {}
- each do |full_name, spec|
- @originals[spec.original_name] = spec
- end
- end
-
- spec_names.find_all { |full_name|
- @originals[full_name].nil?
- }
- end
-
- def remove_extra(spec_names)
- dictionary = spec_names.inject({}) { |h, k| h[k] = true; h }
- each do |name, spec|
- remove_spec name unless dictionary.include? spec.original_name
- end
- end
-
- ##
- # Unzip the given string.
-
- def unzip(string)
- require 'zlib'
- Gem.inflate string
- end
-
- ##
- # Tries to fetch Marshal representation first, then YAML
-
- def fetch_single_spec(source_uri, spec_name)
- @fetch_error = nil
-
- begin
- marshal_uri = source_uri + "quick/Marshal.#{Gem.marshal_version}/#{spec_name}.gemspec.rz"
- zipped = fetcher.fetch_path marshal_uri
- return Marshal.load(unzip(zipped))
- rescue => ex
- @fetch_error = ex
-
- if Gem.configuration.really_verbose then
- say "unable to fetch marshal gemspec #{marshal_uri}: #{ex.class} - #{ex}"
- end
- end
-
- begin
- yaml_uri = source_uri + "quick/#{spec_name}.gemspec.rz"
- zipped = fetcher.fetch_path yaml_uri
- return YAML.load(unzip(zipped))
- rescue => ex
- @fetch_error = ex
- if Gem.configuration.really_verbose then
- say "unable to fetch YAML gemspec #{yaml_uri}: #{ex.class} - #{ex}"
- end
- end
-
- nil
- end
-
- ##
- # Update the cached source index with the missing names.
-
- def update_with_missing(source_uri, missing_names)
- progress = ui.progress_reporter(missing_names.size,
- "Updating metadata for #{missing_names.size} gems from #{source_uri}")
- missing_names.each do |spec_name|
- gemspec = fetch_single_spec(source_uri, spec_name)
- if gemspec.nil? then
- ui.say "Failed to download spec #{spec_name} from #{source_uri}:\n" \
- "\t#{@fetch_error.message}"
- else
- add_spec gemspec
- progress.updated spec_name
- end
- @fetch_error = nil
- end
- progress.done
- progress.count
- end
-
-end
-
-module Gem
-
- # :stopdoc:
-
- # Cache is an alias for SourceIndex to allow older YAMLized source index
- # objects to load properly.
- Cache = SourceIndex
-
- # :startdoc:
-
-end
-
diff --git a/lib/rubygems/source_info_cache.rb b/lib/rubygems/source_info_cache.rb
deleted file mode 100644
index fdb30ad8d3..0000000000
--- a/lib/rubygems/source_info_cache.rb
+++ /dev/null
@@ -1,393 +0,0 @@
-require 'fileutils'
-
-require 'rubygems'
-require 'rubygems/source_info_cache_entry'
-require 'rubygems/user_interaction'
-
-##
-# SourceInfoCache stores a copy of the gem index for each gem source.
-#
-# There are two possible cache locations, the system cache and the user cache:
-# * The system cache is preferred if it is writable or can be created.
-# * The user cache is used otherwise
-#
-# Once a cache is selected, it will be used for all operations.
-# SourceInfoCache will not switch between cache files dynamically.
-#
-# Cache data is a Hash mapping a source URI to a SourceInfoCacheEntry.
-#
-#--
-# To keep things straight, this is how the cache objects all fit together:
-#
-# Gem::SourceInfoCache
-# @cache_data = {
-# source_uri => Gem::SourceInfoCacheEntry
-# @size = source index size
-# @source_index = Gem::SourceIndex
-# ...
-# }
-
-class Gem::SourceInfoCache
-
- include Gem::UserInteraction
-
- ##
- # The singleton Gem::SourceInfoCache. If +all+ is true, a full refresh will
- # be performed if the singleton instance is being initialized.
-
- def self.cache(all = false)
- return @cache if @cache
- @cache = new
- @cache.refresh all if Gem.configuration.update_sources
- @cache
- end
-
- def self.cache_data
- cache.cache_data
- end
-
- ##
- # The name of the system cache file.
-
- def self.latest_system_cache_file
- File.join File.dirname(system_cache_file),
- "latest_#{File.basename system_cache_file}"
- end
-
- ##
- # The name of the latest user cache file.
-
- def self.latest_user_cache_file
- File.join File.dirname(user_cache_file),
- "latest_#{File.basename user_cache_file}"
- end
-
- ##
- # Reset all singletons, discarding any changes.
-
- def self.reset
- @cache = nil
- @system_cache_file = nil
- @user_cache_file = nil
- end
-
- ##
- # Search all source indexes. See Gem::SourceInfoCache#search.
-
- def self.search(*args)
- cache.search(*args)
- end
-
- ##
- # Search all source indexes returning the source_uri. See
- # Gem::SourceInfoCache#search_with_source.
-
- def self.search_with_source(*args)
- cache.search_with_source(*args)
- end
-
- ##
- # The name of the system cache file. (class method)
-
- def self.system_cache_file
- @system_cache_file ||= Gem.default_system_source_cache_dir
- end
-
- ##
- # The name of the user cache file.
-
- def self.user_cache_file
- @user_cache_file ||=
- ENV['GEMCACHE'] || Gem.default_user_source_cache_dir
- end
-
- def initialize # :nodoc:
- @cache_data = nil
- @cache_file = nil
- @dirty = false
- @only_latest = true
- end
-
- ##
- # The most recent cache data.
-
- def cache_data
- return @cache_data if @cache_data
- cache_file # HACK writable check
-
- @only_latest = true
-
- @cache_data = read_cache_data latest_cache_file
-
- @cache_data
- end
-
- ##
- # The name of the cache file.
-
- def cache_file
- return @cache_file if @cache_file
- @cache_file = (try_file(system_cache_file) or
- try_file(user_cache_file) or
- raise "unable to locate a writable cache file")
- end
-
- ##
- # Write the cache to a local file (if it is dirty).
-
- def flush
- write_cache if @dirty
- @dirty = false
- end
-
- def latest_cache_data
- latest_cache_data = {}
-
- cache_data.each do |repo, sice|
- latest = sice.source_index.latest_specs
-
- new_si = Gem::SourceIndex.new
- new_si.add_specs(*latest)
-
- latest_sice = Gem::SourceInfoCacheEntry.new new_si, sice.size
- latest_cache_data[repo] = latest_sice
- end
-
- latest_cache_data
- end
-
- ##
- # The name of the latest cache file.
-
- def latest_cache_file
- File.join File.dirname(cache_file), "latest_#{File.basename cache_file}"
- end
-
- ##
- # The name of the latest system cache file.
-
- def latest_system_cache_file
- self.class.latest_system_cache_file
- end
-
- ##
- # The name of the latest user cache file.
-
- def latest_user_cache_file
- self.class.latest_user_cache_file
- end
-
- ##
- # Merges the complete cache file into this Gem::SourceInfoCache.
-
- def read_all_cache_data
- if @only_latest then
- @only_latest = false
- all_data = read_cache_data cache_file
-
- cache_data.update all_data do |source_uri, latest_sice, all_sice|
- all_sice.source_index.gems.update latest_sice.source_index.gems
-
- Gem::SourceInfoCacheEntry.new all_sice.source_index, latest_sice.size
- end
-
- begin
- refresh true
- rescue Gem::RemoteFetcher::FetchError
- end
- end
- end
-
- ##
- # Reads cached data from +file+.
-
- def read_cache_data(file)
- # Marshal loads 30-40% faster from a String, and 2MB on 20061116 is small
- data = open file, 'rb' do |fp| fp.read end
- cache_data = Marshal.load data
-
- cache_data.each do |url, sice|
- next unless sice.is_a?(Hash)
- update
-
- cache = sice['cache']
- size = sice['size']
-
- if cache.is_a?(Gem::SourceIndex) and size.is_a?(Numeric) then
- new_sice = Gem::SourceInfoCacheEntry.new cache, size
- cache_data[url] = new_sice
- else # irreperable, force refetch.
- reset_cache_for url, cache_data
- end
- end
-
- cache_data
- rescue Errno::ENOENT
- {}
- rescue => e
- if Gem.configuration.really_verbose then
- say "Exception during cache_data handling: #{e.class} - #{e}"
- say "Cache file was: #{file}"
- say "\t#{e.backtrace.join "\n\t"}"
- end
-
- {}
- end
-
- ##
- # Refreshes each source in the cache from its repository. If +all+ is
- # false, only latest gems are updated.
-
- def refresh(all)
- Gem.sources.each do |source_uri|
- cache_entry = cache_data[source_uri]
- if cache_entry.nil? then
- cache_entry = Gem::SourceInfoCacheEntry.new nil, 0
- cache_data[source_uri] = cache_entry
- end
-
- update if cache_entry.refresh source_uri, all
- end
-
- flush
- end
-
- def reset_cache_for(url, cache_data)
- say "Reseting cache for #{url}" if Gem.configuration.really_verbose
-
- sice = Gem::SourceInfoCacheEntry.new Gem::SourceIndex.new, 0
- sice.refresh url, false # HACK may be unnecessary, see ::cache and #refresh
-
- cache_data[url] = sice
- cache_data
- end
-
- def reset_cache_data
- @cache_data = nil
- @only_latest = true
- end
-
- ##
- # Force cache file to be reset, useful for integration testing of rubygems
-
- def reset_cache_file
- @cache_file = nil
- end
-
- ##
- # Searches all source indexes. See Gem::SourceIndex#search for details on
- # +pattern+ and +platform_only+. If +all+ is set to true, the full index
- # will be loaded before searching.
-
- def search(pattern, platform_only = false, all = false)
- read_all_cache_data if all
-
- cache_data.map do |source_uri, sic_entry|
- next unless Gem.sources.include? source_uri
- # TODO - Remove this gunk after 2008/11
- unless pattern.kind_of?(Gem::Dependency)
- pattern = Gem::Dependency.new(pattern, Gem::Requirement.default)
- end
- sic_entry.source_index.search pattern, platform_only
- end.flatten.compact
- end
-
- # Searches all source indexes for +pattern+. If +only_platform+ is true,
- # only gems matching Gem.platforms will be selected. Returns an Array of
- # pairs containing the Gem::Specification found and the source_uri it was
- # found at.
- def search_with_source(pattern, only_platform = false, all = false)
- read_all_cache_data if all
-
- results = []
-
- cache_data.map do |source_uri, sic_entry|
- next unless Gem.sources.include? source_uri
-
- # TODO - Remove this gunk after 2008/11
- unless pattern.kind_of?(Gem::Dependency)
- pattern = Gem::Dependency.new(pattern, Gem::Requirement.default)
- end
-
- sic_entry.source_index.search(pattern, only_platform).each do |spec|
- results << [spec, source_uri]
- end
- end
-
- results
- end
-
- ##
- # Set the source info cache data directly. This is mainly used for unit
- # testing when we don't want to read a file system to grab the cached source
- # index information. The +hash+ should map a source URL into a
- # SourceInfoCacheEntry.
-
- def set_cache_data(hash)
- @cache_data = hash
- update
- end
-
- ##
- # The name of the system cache file.
-
- def system_cache_file
- self.class.system_cache_file
- end
-
- ##
- # Determine if +path+ is a candidate for a cache file. Returns +path+ if
- # it is, nil if not.
-
- def try_file(path)
- return path if File.writable? path
- return nil if File.exist? path
-
- dir = File.dirname path
-
- unless File.exist? dir then
- begin
- FileUtils.mkdir_p dir
- rescue RuntimeError, SystemCallError
- return nil
- end
- end
-
- return path if File.writable? dir
-
- nil
- end
-
- ##
- # Mark the cache as updated (i.e. dirty).
-
- def update
- @dirty = true
- end
-
- ##
- # The name of the user cache file.
-
- def user_cache_file
- self.class.user_cache_file
- end
-
- ##
- # Write data to the proper cache files.
-
- def write_cache
- if not File.exist?(cache_file) or not @only_latest then
- open cache_file, 'wb' do |io|
- io.write Marshal.dump(cache_data)
- end
- end
-
- open latest_cache_file, 'wb' do |io|
- io.write Marshal.dump(latest_cache_data)
- end
- end
-
- reset
-
-end
-
diff --git a/lib/rubygems/source_info_cache_entry.rb b/lib/rubygems/source_info_cache_entry.rb
deleted file mode 100644
index c3f75e5b99..0000000000
--- a/lib/rubygems/source_info_cache_entry.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-require 'rubygems'
-require 'rubygems/source_index'
-require 'rubygems/remote_fetcher'
-
-##
-# Entries held by a SourceInfoCache.
-
-class Gem::SourceInfoCacheEntry
-
- ##
- # The source index for this cache entry.
-
- attr_reader :source_index
-
- ##
- # The size of the of the source entry. Used to determine if the
- # source index has changed.
-
- attr_reader :size
-
- ##
- # Create a cache entry.
-
- def initialize(si, size)
- @source_index = si || Gem::SourceIndex.new({})
- @size = size
- @all = false
- end
-
- def refresh(source_uri, all)
- begin
- marshal_uri = URI.join source_uri.to_s, "Marshal.#{Gem.marshal_version}"
- remote_size = Gem::RemoteFetcher.fetcher.fetch_size marshal_uri
- rescue Gem::RemoteSourceException
- yaml_uri = URI.join source_uri.to_s, 'yaml'
- remote_size = Gem::RemoteFetcher.fetcher.fetch_size yaml_uri
- end
-
- # TODO Use index_signature instead of size?
- return false if @size == remote_size and @all
-
- updated = @source_index.update source_uri, all
- @size = remote_size
- @all = all
-
- updated
- end
-
- def ==(other) # :nodoc:
- self.class === other and
- @size == other.size and
- @source_index == other.source_index
- end
-
-end
-
diff --git a/lib/rubygems/source_list.rb b/lib/rubygems/source_list.rb
new file mode 100644
index 0000000000..19bf4595c4
--- /dev/null
+++ b/lib/rubygems/source_list.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+##
+# The SourceList represents the sources rubygems has been configured to use.
+# A source may be created from an array of sources:
+#
+# Gem::SourceList.from %w[https://rubygems.example https://internal.example]
+#
+# Or by adding them:
+#
+# sources = Gem::SourceList.new
+# sources << 'https://rubygems.example'
+#
+# The most common way to get a SourceList is Gem.sources.
+
+class Gem::SourceList
+ include Enumerable
+
+ ##
+ # Creates a new SourceList
+
+ def initialize
+ @sources = []
+ end
+
+ ##
+ # The sources in this list
+
+ attr_reader :sources
+
+ ##
+ # Creates a new SourceList from an array of sources.
+
+ def self.from(ary)
+ list = new
+
+ list.replace ary
+
+ list
+ end
+
+ def initialize_copy(other) # :nodoc:
+ @sources = @sources.dup
+ end
+
+ ##
+ # Appends +obj+ to the source list which may be a Gem::Source, Gem::URI or URI
+ # String.
+
+ def <<(obj)
+ src = case obj
+ when Gem::Source
+ obj
+ else
+ Gem::Source.new(obj)
+ end
+
+ @sources << src unless @sources.include?(src)
+ src
+ end
+
+ ##
+ # Prepends +obj+ to the beginning of the source list which may be a Gem::Source, Gem::URI or URI
+ # Moves +obj+ to the beginning of the list if already present.
+ # String.
+
+ def prepend(obj)
+ src = case obj
+ when Gem::Source
+ obj
+ else
+ Gem::Source.new(obj)
+ end
+
+ @sources.delete(src) if @sources.include?(src)
+ @sources.unshift(src)
+ src
+ end
+
+ ##
+ # Appends +obj+ to the end of the source list, moving it if already present.
+ # +obj+ may be a Gem::Source, Gem::URI or URI String.
+ # Moves +obj+ to the end of the list if already present.
+
+ def append(obj)
+ src = case obj
+ when Gem::Source
+ obj
+ else
+ Gem::Source.new(obj)
+ end
+
+ @sources.delete(src) if @sources.include?(src)
+ @sources << src
+ src
+ end
+
+ ##
+ # Replaces this SourceList with the sources in +other+ See #<< for
+ # acceptable items in +other+.
+
+ def replace(other)
+ clear
+
+ other.each do |x|
+ self << x
+ end
+
+ self
+ end
+
+ ##
+ # Removes all sources from the SourceList.
+
+ def clear
+ @sources.clear
+ end
+
+ ##
+ # Yields each source URI in the list.
+
+ def each
+ @sources.each {|s| yield s.uri.to_s }
+ end
+
+ ##
+ # Yields each source in the list.
+
+ def each_source(&b)
+ @sources.each(&b)
+ end
+
+ ##
+ # Returns true if there are no sources in this SourceList.
+
+ def empty?
+ @sources.empty?
+ end
+
+ def ==(other) # :nodoc:
+ to_a == other
+ end
+
+ ##
+ # Returns an Array of source URI Strings.
+
+ def to_a
+ @sources.map {|x| x.uri.to_s }
+ end
+
+ alias_method :to_ary, :to_a
+
+ ##
+ # Returns the first source in the list.
+
+ def first
+ @sources.first
+ end
+
+ ##
+ # Returns true if this source list includes +other+ which may be a
+ # Gem::Source or a source URI.
+
+ def include?(other)
+ if other.is_a? Gem::Source
+ @sources.include? other
+ else
+ @sources.find {|x| x.uri.to_s == other.to_s }
+ end
+ end
+
+ ##
+ # Deletes +source+ from the source list which may be a Gem::Source or a URI.
+
+ def delete(source)
+ if source.is_a? Gem::Source
+ @sources.delete source
+ else
+ @sources.delete_if {|x| x.uri.to_s == source.to_s }
+ end
+ end
+end
diff --git a/lib/rubygems/spec_fetcher.rb b/lib/rubygems/spec_fetcher.rb
index a1fc82ed4f..835dedf948 100644
--- a/lib/rubygems/spec_fetcher.rb
+++ b/lib/rubygems/spec_fetcher.rb
@@ -1,33 +1,44 @@
-require 'zlib'
+# frozen_string_literal: true
-require 'rubygems'
-require 'rubygems/remote_fetcher'
-require 'rubygems/user_interaction'
+require_relative "remote_fetcher"
+require_relative "user_interaction"
+require_relative "errors"
+require_relative "text"
+require_relative "name_tuple"
##
# SpecFetcher handles metadata updates from remote gem repositories.
class Gem::SpecFetcher
-
include Gem::UserInteraction
+ include Gem::Text
##
- # The SpecFetcher cache dir.
+ # Cache of latest specs
- attr_reader :dir # :nodoc:
+ attr_reader :latest_specs # :nodoc:
##
- # Cache of latest specs
+ # Sources for this SpecFetcher
- attr_reader :latest_specs # :nodoc:
+ attr_reader :sources # :nodoc:
##
- # Cache of all spces
+ # Cache of all released specs
attr_reader :specs # :nodoc:
+ ##
+ # Cache of prerelease specs
+
+ attr_reader :prerelease_specs # :nodoc:
+
@fetcher = nil
+ ##
+ # Default fetcher instance. Use this instead of ::new to reduce object
+ # allocation.
+
def self.fetcher
@fetcher ||= new
end
@@ -36,214 +47,244 @@ class Gem::SpecFetcher
@fetcher = fetcher
end
- def initialize
- @dir = File.join Gem.user_home, '.gem', 'specs'
- @update_cache = File.stat(Gem.user_home).uid == Process.uid
+ ##
+ # Creates a new SpecFetcher. Ordinarily you want to use the default fetcher
+ # from Gem::SpecFetcher::fetcher which uses the Gem.sources.
+ #
+ # If you need to retrieve specifications from a different +source+, you can
+ # send it as an argument.
+
+ def initialize(sources = nil)
+ @sources = sources || Gem.sources
+
+ @update_cache =
+ begin
+ File.stat(Gem.user_home).uid == Process.uid
+ rescue Errno::EACCES, Errno::ENOENT
+ false
+ end
@specs = {}
@latest_specs = {}
+ @prerelease_specs = {}
+
+ @caches = {
+ latest: @latest_specs,
+ prerelease: @prerelease_specs,
+ released: @specs,
+ }
@fetcher = Gem::RemoteFetcher.fetcher
end
##
- # Retuns the local directory to write +uri+ to.
+ #
+ # Find and fetch gem name tuples that match +dependency+.
+ #
+ # If +matching_platform+ is false, gems for all platforms are returned.
- def cache_dir(uri)
- File.join @dir, "#{uri.host}%#{uri.port}", File.dirname(uri.path)
- end
+ def search_for_dependency(dependency, matching_platform = true)
+ found = {}
- ##
- # Fetch specs matching +dependency+. If +all+ is true, all matching
- # versions are returned. If +matching_platform+ is false, all platforms are
- # returned.
+ rejected_specs = {}
- def fetch(dependency, all = false, matching_platform = true)
- specs_and_sources = find_matching dependency, all, matching_platform
+ list, errors = available_specs(dependency.identity)
- specs_and_sources.map do |spec_tuple, source_uri|
- [fetch_spec(spec_tuple, URI.parse(source_uri)), source_uri]
- end
-
- rescue Gem::RemoteFetcher::FetchError => e
- raise unless warn_legacy e do
- require 'rubygems/source_info_cache'
+ list.each do |source, specs|
+ if dependency.name.is_a?(String) && specs.respond_to?(:bsearch)
+ start_index = (0...specs.length).bsearch {|i| specs[i].name >= dependency.name }
+ end_index = (0...specs.length).bsearch {|i| specs[i].name > dependency.name }
+ specs = specs[start_index...end_index] if start_index && end_index
+ end
- return Gem::SourceInfoCache.search_with_source(dependency,
- matching_platform, all)
+ found[source] = specs.select do |tup|
+ if dependency.match?(tup)
+ if matching_platform && !Gem::Platform.match_gem?(tup.platform, tup.name)
+ pm = (
+ rejected_specs[dependency] ||= \
+ Gem::PlatformMismatch.new(tup.name, tup.version))
+ pm.add_platform tup.platform
+ false
+ else
+ true
+ end
+ end
+ end
end
- end
-
- def fetch_spec(spec, source_uri)
- spec = spec - [nil, 'ruby', '']
- spec_file_name = "#{spec.join '-'}.gemspec"
- uri = source_uri + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}"
+ errors += rejected_specs.values
- cache_dir = cache_dir uri
+ tuples = []
- local_spec = File.join cache_dir, spec_file_name
-
- if File.exist? local_spec then
- spec = Gem.read_binary local_spec
- else
- uri.path << '.rz'
-
- spec = @fetcher.fetch_path uri
- spec = Gem.inflate spec
-
- if @update_cache then
- FileUtils.mkdir_p cache_dir
-
- open local_spec, 'wb' do |io|
- io.write spec
- end
+ found.each do |source, specs|
+ specs.each do |s|
+ tuples << [s, source]
end
end
- # TODO: Investigate setting Gem::Specification#loaded_from to a URI
- Marshal.load spec
+ tuples = tuples.sort_by {|x| x[0].version }
+
+ [tuples, errors]
end
##
- # Find spec names that match +dependency+. If +all+ is true, all matching
- # versions are returned. If +matching_platform+ is false, gems for all
- # platforms are returned.
+ # Return all gem name tuples who's names match +obj+
- def find_matching(dependency, all = false, matching_platform = true)
- found = {}
+ def detect(type = :complete)
+ tuples = []
- list(all).each do |source_uri, specs|
- found[source_uri] = specs.select do |spec_name, version, spec_platform|
- dependency =~ Gem::Dependency.new(spec_name, version) and
- (not matching_platform or Gem::Platform.match(spec_platform))
+ list, _ = available_specs(type)
+ list.each do |source, specs|
+ specs.each do |tup|
+ if yield(tup)
+ tuples << [tup, source]
+ end
end
end
- specs_and_sources = []
+ tuples
+ end
- found.each do |source_uri, specs|
- uri_str = source_uri.to_s
- specs_and_sources.push(*specs.map { |spec| [spec, uri_str] })
+ ##
+ # Find and fetch specs that match +dependency+.
+ #
+ # If +matching_platform+ is false, gems for all platforms are returned.
+
+ def spec_for_dependency(dependency, matching_platform = true)
+ tuples, errors = search_for_dependency(dependency, matching_platform)
+
+ specs = []
+ tuples.each do |tup, source|
+ spec = source.fetch_spec(tup)
+ rescue Gem::RemoteFetcher::FetchError => e
+ errors << Gem::SourceFetchProblem.new(source, e)
+ else
+ specs << [spec, source]
end
- specs_and_sources
+ [specs, errors]
end
##
- # Returns Array of gem repositories that were generated with RubyGems less
- # than 1.2.
+ # Suggests gems based on the supplied +gem_name+. Returns an array of
+ # alternative gem names.
- def legacy_repos
- Gem.sources.reject do |source_uri|
- source_uri = URI.parse source_uri
- spec_path = source_uri + "specs.#{Gem.marshal_version}.gz"
+ def suggest_gems_from_name(gem_name, type = :latest, num_results = 5)
+ gem_name = gem_name.downcase.tr("_-", "")
- begin
- @fetcher.fetch_size spec_path
- rescue Gem::RemoteFetcher::FetchError
- begin
- @fetcher.fetch_size(source_uri + 'yaml') # re-raise if non-repo
- rescue Gem::RemoteFetcher::FetchError
- alert_error "#{source_uri} does not appear to be a repository"
- raise
- end
- false
- end
- end
- end
+ # All results for 3-character-or-shorter (minus hyphens/underscores) gem
+ # names get rejected, so we just return an empty array immediately instead.
+ return [] if gem_name.length <= 3
- ##
- # Returns a list of gems available for each source in Gem::sources. If
- # +all+ is true, all versions are returned instead of only latest versions.
+ max = gem_name.size / 2
+ names = available_specs(type).first.values.flatten(1)
- def list(all = false)
- list = {}
+ min_length = gem_name.length - max
+ max_length = gem_name.length + max
- file = all ? 'specs' : 'latest_specs'
+ gem_name_with_postfix = "#{gem_name}ruby"
+ gem_name_with_prefix = "ruby#{gem_name}"
- Gem.sources.each do |source_uri|
- source_uri = URI.parse source_uri
+ matches = names.filter_map do |n|
+ len = n.name.length
+ # If the gem doesn't support the current platform, bail early.
+ next unless n.match_platform?
- if all and @specs.include? source_uri then
- list[source_uri] = @specs[source_uri]
- elsif not all and @latest_specs.include? source_uri then
- list[source_uri] = @latest_specs[source_uri]
- else
- specs = load_specs source_uri, file
+ # If the length is min_length or shorter, we've done `max` deletions.
+ # This would be rejected later, so we skip it for performance.
+ next if len <= min_length
- cache = all ? @specs : @latest_specs
+ # The candidate name, normalized the same as gem_name.
+ normalized_name = n.name.downcase
+ normalized_name.tr!("_-", "")
- cache[source_uri] = specs
- list[source_uri] = specs
- end
- end
+ # If the gem is "{NAME}-ruby" and "ruby-{NAME}", we want to return it.
+ # But we already removed hyphens, so we check "{NAME}ruby" and "ruby{NAME}".
+ next [n.name, 0] if normalized_name == gem_name_with_postfix
+ next [n.name, 0] if normalized_name == gem_name_with_prefix
- list
- end
+ # If the length is max_length or longer, we've done `max` insertions.
+ # This would be rejected later, so we skip it for performance.
+ next if len >= max_length
- ##
- # Loads specs in +file+, fetching from +source_uri+ if the on-disk cache is
- # out of date.
-
- def load_specs(source_uri, file)
- file_name = "#{file}.#{Gem.marshal_version}"
- spec_path = source_uri + "#{file_name}.gz"
- cache_dir = cache_dir spec_path
- local_file = File.join(cache_dir, file_name)
- loaded = false
-
- if File.exist? local_file then
- spec_dump = @fetcher.fetch_path spec_path, File.mtime(local_file)
-
- if spec_dump.nil? then
- spec_dump = Gem.read_binary local_file
- else
- loaded = true
- end
- else
- spec_dump = @fetcher.fetch_path spec_path
- loaded = true
- end
+ # If we found an exact match (after stripping underscores and hyphens),
+ # that's our most likely candidate.
+ # Return it immediately, and skip the rest of the loop.
+ return [n.name] if normalized_name == gem_name
- specs = Marshal.load spec_dump
+ distance = levenshtein_distance gem_name, normalized_name
- if loaded and @update_cache then
- begin
- FileUtils.mkdir_p cache_dir
+ # Skip current candidate, if the edit distance is greater than allowed.
+ next if distance >= max
- open local_file, 'wb' do |io|
- Marshal.dump specs, io
- end
- rescue
- end
+ # If all else fails, return the name and the calculated distance.
+ [n.name, distance]
end
- specs
+ matches = if matches.empty? && type != :prerelease
+ suggest_gems_from_name gem_name, :prerelease
+ else
+ matches.uniq.sort_by {|_name, dist| dist }
+ end
+
+ matches.map {|name, _dist| name }.uniq.first(num_results)
end
##
- # Warn about legacy repositories if +exception+ indicates only legacy
- # repositories are available, and yield to the block. Returns false if the
- # exception indicates some other FetchError.
-
- def warn_legacy(exception)
- uri = exception.uri.to_s
- if uri =~ /specs\.#{Regexp.escape Gem.marshal_version}\.gz$/ then
- alert_warning <<-EOF
-RubyGems 1.2+ index not found for:
-\t#{legacy_repos.join "\n\t"}
-
-RubyGems will revert to legacy indexes degrading performance.
- EOF
-
- yield
+ # Returns a list of gems available for each source in Gem::sources.
+ #
+ # +type+ can be one of 3 values:
+ # :released => Return the list of all released specs
+ # :complete => Return the list of all specs
+ # :latest => Return the list of only the highest version of each gem
+ # :prerelease => Return the list of all prerelease only specs
+ #
+
+ def available_specs(type)
+ errors = []
+ list = {}
- return true
+ @sources.each_source do |source|
+ names = case type
+ when :latest
+ tuples_for source, :latest
+ when :released
+ tuples_for source, :released
+ when :complete
+ names =
+ tuples_for(source, :prerelease, true) +
+ tuples_for(source, :released)
+
+ names.sort
+ when :abs_latest
+ names =
+ tuples_for(source, :prerelease, true) +
+ tuples_for(source, :latest)
+
+ names.sort
+ when :prerelease
+ tuples_for(source, :prerelease)
+ else
+ raise Gem::Exception, "Unknown type - :#{type}"
+ end
+ rescue Gem::RemoteFetcher::FetchError => e
+ errors << Gem::SourceFetchProblem.new(source, e)
+ else
+ list[source] = names
end
- false
+ [list, errors]
end
+ ##
+ # Retrieves NameTuples from +source+ of the given +type+ (:prerelease,
+ # etc.). If +gracefully_ignore+ is true, errors are ignored.
+
+ def tuples_for(source, type, gracefully_ignore = false) # :nodoc:
+ @caches[type][source.uri] ||=
+ source.load_specs(type).sort_by(&:name)
+ rescue Gem::RemoteFetcher::FetchError
+ raise unless gracefully_ignore
+ []
+ end
end
-
diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb
index 2e6cdc1b04..51729d755b 100644
--- a/lib/rubygems/specification.rb
+++ b/lib/rubygems/specification.rb
@@ -1,1262 +1,2604 @@
+# frozen_string_literal: true
+
+#
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems'
-require 'rubygems/version'
-require 'rubygems/requirement'
-require 'rubygems/platform'
-
-# :stopdoc:
-# Time::today has been deprecated in 0.9.5 and will be removed.
-if RUBY_VERSION < '1.9' then
- def Time.today
- t = Time.now
- t - ((t.to_f + t.gmt_offset) % 86400)
- end unless defined? Time.today
-end
+require_relative "basic_specification"
+require_relative "stub_specification"
+require_relative "platform"
+require_relative "specification_record"
+
+require "rbconfig"
+
+##
+# The Specification class contains the information for a gem. Typically
+# defined in a .gemspec file or a Rakefile, and looks like this:
+#
+# Gem::Specification.new do |s|
+# s.name = 'example'
+# s.version = '0.1.0'
+# s.licenses = ['MIT']
+# s.summary = "This is an example!"
+# s.description = "Much longer explanation of the example!"
+# s.authors = ["Ruby Coder"]
+# s.email = 'rubycoder@example.com'
+# s.files = ["lib/example.rb"]
+# s.homepage = 'https://rubygems.org/gems/example'
+# s.metadata = { "source_code_uri" => "https://github.com/example/example" }
+# end
+#
+# Starting in RubyGems 2.0, a Specification can hold arbitrary
+# metadata. See #metadata for restrictions on the format and size of metadata
+# items you may add to a specification.
+#
+# Specifications must be deterministic, as in the example above. For instance,
+# you cannot define attributes conditionally:
+#
+# # INVALID: do not do this.
+# unless RUBY_ENGINE == "jruby"
+# s.extensions << "ext/example/extconf.rb"
+# end
+#
+
+class Gem::Specification < Gem::BasicSpecification
+ # REFACTOR: Consider breaking out this version stuff into a separate
+ # module. There's enough special stuff around it that it may justify
+ # a separate class.
+
+ ##
+ # The version number of a specification that does not specify one
+ # (i.e. RubyGems 0.7 or earlier).
+
+ NONEXISTENT_SPECIFICATION_VERSION = -1
+
+ ##
+ # The specification version applied to any new Specification instances
+ # created. This should be bumped whenever something in the spec format
+ # changes.
+ #
+ # Specification Version History:
+ #
+ # spec ruby
+ # ver ver yyyy-mm-dd description
+ # -1 <0.8.0 pre-spec-version-history
+ # 1 0.8.0 2004-08-01 Deprecated "test_suite_file" for "test_files"
+ # "test_file=x" is a shortcut for "test_files=[x]"
+ # 2 0.9.5 2007-10-01 Added "required_rubygems_version"
+ # Now forward-compatible with future versions
+ # 3 1.3.2 2009-01-03 Added Fixnum validation to specification_version
+ # 4 1.9.0 2011-06-07 Added metadata
+ #--
+ # When updating this number, be sure to also update #to_ruby.
+ #
+ # NOTE RubyGems < 1.2 cannot load specification versions > 2.
+
+ CURRENT_SPECIFICATION_VERSION = 4 # :nodoc:
+
+ ##
+ # An informal list of changes to the specification. The highest-valued
+ # key should be equal to the CURRENT_SPECIFICATION_VERSION.
+
+ SPECIFICATION_VERSION_HISTORY = { # :nodoc:
+ -1 => ["(RubyGems versions up to and including 0.7 did not have versioned specifications)"],
+ 1 => [
+ 'Deprecated "test_suite_file" in favor of the new, but equivalent, "test_files"',
+ '"test_file=x" is a shortcut for "test_files=[x]"',
+ ],
+ 2 => [
+ 'Added "required_rubygems_version"',
+ "Now forward-compatible with future versions",
+ ],
+ 3 => [
+ "Added Fixnum validation to the specification_version",
+ ],
+ 4 => [
+ "Added sandboxed freeform metadata to the specification version.",
+ ],
+ }.freeze
+
+ MARSHAL_FIELDS = { # :nodoc:
+ -1 => 16,
+ 1 => 16,
+ 2 => 16,
+ 3 => 17,
+ 4 => 18,
+ }.freeze
+
+ today = Time.now.utc
+ TODAY = Time.utc(today.year, today.month, today.day) # :nodoc:
+
+ @load_cache = {} # :nodoc:
+ @load_cache_mutex = Thread::Mutex.new
+
+ VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc:
+
+ # :startdoc:
+
+ ##
+ # List of attribute names: [:name, :version, ...]
+
+ @@required_attributes = [:rubygems_version,
+ :specification_version,
+ :name,
+ :version,
+ :date,
+ :summary,
+ :require_paths]
+
+ ##
+ # Map of attribute names to default values.
+
+ @@default_value = {
+ authors: [],
+ autorequire: nil,
+ bindir: "bin",
+ cert_chain: [],
+ date: nil,
+ dependencies: [],
+ description: nil,
+ email: nil,
+ executables: [],
+ extensions: [],
+ extra_rdoc_files: [],
+ files: [],
+ homepage: nil,
+ licenses: [],
+ metadata: {},
+ name: nil,
+ platform: Gem::Platform::RUBY,
+ post_install_message: nil,
+ rdoc_options: [],
+ require_paths: ["lib"],
+ required_ruby_version: Gem::Requirement.default,
+ required_rubygems_version: Gem::Requirement.default,
+ requirements: [],
+ rubygems_version: Gem::VERSION,
+ signing_key: nil,
+ specification_version: CURRENT_SPECIFICATION_VERSION,
+ summary: nil,
+ test_files: [],
+ version: nil,
+ }.freeze
+
+ # rubocop:disable Style/MutableConstant
+ INITIALIZE_CODE_FOR_DEFAULTS = {} # :nodoc:
+ # rubocop:enable Style/MutableConstant
+
+ @@default_value.each do |k,v|
+ INITIALIZE_CODE_FOR_DEFAULTS[k] = case v
+ when [], {}, true, false, nil, Numeric, Symbol
+ v.inspect
+ when String
+ v.dump
+ else
+ "default_value(:#{k}).dup"
+ end
+ end
+
+ @@attributes = @@default_value.keys.sort_by(&:to_s)
+ @@array_attributes = @@default_value.select {|_k,v| v.is_a?(Array) }.keys
+ @@nil_attributes, @@non_nil_attributes = @@default_value.keys.partition do |k|
+ @@default_value[k].nil?
+ end
-class Date; end # for ruby_code if date.rb wasn't required
+ # Sentinel object to represent "not found" stubs
+ NOT_FOUND = Struct.new(:to_spec, :this).new # :nodoc:
+ deprecate_constant :NOT_FOUND
-# :startdoc:
+ # Tracking removed method calls to warn users during build time.
+ REMOVED_METHODS = [:rubyforge_project=, :mark_version].freeze # :nodoc:
+ def removed_method_calls
+ @removed_method_calls ||= []
+ end
-module Gem
+ ######################################################################
+ # :section: Required gemspec attributes
##
- # == Gem::Specification
+ # This gem's name.
#
- # The Specification class contains the metadata for a Gem. Typically
- # defined in a .gemspec file or a Rakefile, and looks like this:
+ # Usage:
#
- # spec = Gem::Specification.new do |s|
- # s.name = 'rfoo'
- # s.version = '1.0'
- # s.summary = 'Example gem specification'
- # ...
- # end
+ # spec.name = 'rake'
+
+ attr_accessor :name
+
+ ##
+ # This gem's version.
+ #
+ # The version string can contain numbers and periods, such as +1.0.0+.
+ # A gem is a 'prerelease' gem if the version has a letter in it, such as
+ # +1.0.0.pre+.
#
- # There are many <em>gemspec attributes</em>, and the best place to learn
- # about them in the "Gemspec Reference" linked from the RubyGems wiki.
+ # Usage:
+ #
+ # spec.version = '0.4.1'
- class Specification
+ attr_reader :version
- ##
- # Allows deinstallation of gems with legacy platforms.
+ ##
+ # A short summary of this gem's description. Displayed in <tt>gem list -d</tt>.
+ #
+ # The #description should be more detailed than the summary.
+ #
+ # Usage:
+ #
+ # spec.summary = "This is a small summary of my gem"
- attr_accessor :original_platform # :nodoc:
+ attr_reader :summary
- ##
- # The the version number of a specification that does not specify one
- # (i.e. RubyGems 0.7 or earlier).
+ ##
+ # Files included in this gem. You cannot append to this accessor, you must
+ # assign to it.
+ #
+ # Only add files you can require to this list, not directories, etc.
+ #
+ # Directories are automatically stripped from this list when building a gem,
+ # other non-files cause an error.
+ #
+ # Usage:
+ #
+ # require 'rake'
+ # spec.files = FileList['lib/**/*.rb',
+ # 'bin/*',
+ # '[A-Z]*'].to_a
+ #
+ # # or without Rake...
+ # spec.files = Dir['lib/**/*.rb'] + Dir['bin/*']
+ # spec.files += Dir['[A-Z]*']
+ # spec.files.reject! { |fn| fn.include? "CVS" }
+
+ def files
+ # DO NOT CHANGE TO ||= ! This is not a normal accessor. (yes, it sucks)
+ # DOC: Why isn't it normal? Why does it suck? How can we fix this?
+ @files = [@files,
+ @test_files,
+ add_bindir(@executables),
+ @extra_rdoc_files,
+ @extensions].flatten.compact.uniq.sort
+ end
- NONEXISTENT_SPECIFICATION_VERSION = -1
+ ##
+ # A list of authors for this gem.
+ #
+ # Alternatively, a single author can be specified by assigning a string to
+ # +spec.author+
+ #
+ # Usage:
+ #
+ # spec.authors = ['John Jones', 'Mary Smith']
- ##
- # The specification version applied to any new Specification instances
- # created. This should be bumped whenever something in the spec format
- # changes.
- #--
- # When updating this number, be sure to also update #to_ruby.
- #
- # NOTE RubyGems < 1.2 cannot load specification versions > 2.
+ def authors=(value)
+ @authors = Array(value).flatten.grep(String)
+ end
- CURRENT_SPECIFICATION_VERSION = 2
+ ######################################################################
+ # :section: Recommended gemspec attributes
- ##
- # An informal list of changes to the specification. The highest-valued
- # key should be equal to the CURRENT_SPECIFICATION_VERSION.
+ ##
+ # The version of Ruby required by this gem
+ #
+ # Usage:
+ #
+ # spec.required_ruby_version = '>= 2.7.0'
- SPECIFICATION_VERSION_HISTORY = {
- -1 => ['(RubyGems versions up to and including 0.7 did not have versioned specifications)'],
- 1 => [
- 'Deprecated "test_suite_file" in favor of the new, but equivalent, "test_files"',
- '"test_file=x" is a shortcut for "test_files=[x]"'
- ],
- 2 => [
- 'Added "required_rubygems_version"',
- 'Now forward-compatible with future versions',
- ],
- }
+ attr_reader :required_ruby_version
- # :stopdoc:
- MARSHAL_FIELDS = { -1 => 16, 1 => 16, 2 => 16 }
+ ##
+ # A long description of this gem
+ #
+ # The description should be more detailed than the summary but not
+ # excessively long. A few paragraphs is a recommended length with no
+ # examples or formatting.
+ #
+ # Usage:
+ #
+ # spec.description = <<~EOF
+ # Rake is a Make-like program implemented in Ruby. Tasks and
+ # dependencies are specified in standard Ruby syntax.
+ # EOF
- now = Time.at(Time.now.to_i)
- TODAY = now - ((now.to_i + now.gmt_offset) % 86400)
- # :startdoc:
+ attr_reader :description
- ##
- # List of Specification instances.
+ ##
+ # A contact email address (or addresses) for this gem
+ #
+ # Usage:
+ #
+ # spec.email = 'john.jones@example.com'
+ # spec.email = ['jack@example.com', 'jill@example.com']
- @@list = []
+ attr_accessor :email
- ##
- # Optional block used to gather newly defined instances.
+ ##
+ # The URL of this gem's home page
+ #
+ # Usage:
+ #
+ # spec.homepage = 'https://github.com/ruby/rake'
- @@gather = nil
+ attr_accessor :homepage
- ##
- # List of attribute names: [:name, :version, ...]
- @@required_attributes = []
+ ##
+ # The license for this gem.
+ #
+ # The license must be no more than 64 characters.
+ #
+ # This should just be the name of your license. The full text of the license
+ # should be inside of the gem (at the top level) when you build it.
+ #
+ # The simplest way is to specify the standard SPDX ID
+ # https://spdx.org/licenses/ for the license.
+ # Ideally, you should pick one that is OSI (Open Source Initiative)
+ # https://opensource.org/licenses/ approved.
+ #
+ # The most commonly used OSI-approved licenses are MIT and Apache-2.0.
+ # GitHub also provides a license picker at https://choosealicense.com/.
+ #
+ # You can also use a custom license file along with your gemspec and specify
+ # a LicenseRef-<idstring>, where idstring is the name of the file containing
+ # the license text.
+ #
+ # You should specify a license for your gem so that people know how they are
+ # permitted to use it and any restrictions you're placing on it. Not
+ # specifying a license means all rights are reserved; others have no right
+ # to use the code for any purpose.
+ #
+ # You can set multiple licenses with #licenses=
+ #
+ # Usage:
+ # spec.license = 'MIT'
- ##
- # List of _all_ attributes and default values:
- #
- # [[:name, nil],
- # [:bindir, 'bin'],
- # ...]
+ def license=(o)
+ self.licenses = [o]
+ end
- @@attributes = []
+ ##
+ # The license(s) for the library.
+ #
+ # Each license must be a short name, no more than 64 characters.
+ #
+ # This should just be the name of your license. The full
+ # text of the license should be inside of the gem when you build it.
+ #
+ # See #license= for more discussion
+ #
+ # Usage:
+ # spec.licenses = ['MIT', 'GPL-2.0']
- @@nil_attributes = []
- @@non_nil_attributes = [:@original_platform]
+ def licenses=(licenses)
+ @licenses = Array licenses
+ end
- ##
- # List of array attributes
+ ##
+ # The metadata holds extra data for this gem that may be useful to other
+ # consumers and is settable by gem authors.
+ #
+ # Metadata items have the following restrictions:
+ #
+ # * The metadata must be a Hash object
+ # * All keys and values must be Strings
+ # * Keys can be a maximum of 128 bytes and values can be a maximum of 1024
+ # bytes
+ # * All strings must be UTF-8, no binary data is allowed
+ #
+ # You can use metadata to specify links to your gem's homepage, codebase,
+ # documentation, wiki, mailing list, issue tracker and changelog.
+ #
+ # s.metadata = {
+ # "bug_tracker_uri" => "https://example.com/user/bestgemever/issues",
+ # "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md",
+ # "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1",
+ # "homepage_uri" => "https://bestgemever.example.io",
+ # "mailing_list_uri" => "https://groups.example.com/bestgemever",
+ # "source_code_uri" => "https://example.com/user/bestgemever",
+ # "wiki_uri" => "https://example.com/user/bestgemever/wiki",
+ # "funding_uri" => "https://example.com/donate"
+ # }
+ #
+ # These links will be used on your gem's page on rubygems.org and must pass
+ # validation against following regex.
+ #
+ # %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z}
- @@array_attributes = []
+ attr_accessor :metadata
- ##
- # Map of attribute names to default values.
+ ######################################################################
+ # :section: Optional gemspec attributes
- @@default_value = {}
+ ##
+ # Singular (alternative) writer for #authors
+ #
+ # Usage:
+ #
+ # spec.author = 'John Jones'
- ##
- # Names of all specification attributes
+ def author=(o)
+ self.authors = [o]
+ end
- def self.attribute_names
- @@attributes.map { |name, default| name }
- end
+ ##
+ # The path in the gem for executable scripts. Usually 'exe'
+ #
+ # Usage:
+ #
+ # spec.bindir = 'exe'
- ##
- # Default values for specification attributes
+ attr_accessor :bindir
- def self.attribute_defaults
- @@attributes.dup
- end
+ ##
+ # The certificate chain used to sign this gem. See Gem::Security for
+ # details.
- ##
- # The default value for specification attribute +name+
+ attr_accessor :cert_chain
- def self.default_value(name)
- @@default_value[name]
- end
+ ##
+ # A message that gets displayed after the gem is installed.
+ #
+ # Usage:
+ #
+ # spec.post_install_message = "Thanks for installing!"
- ##
- # Required specification attributes
+ attr_accessor :post_install_message
- def self.required_attributes
- @@required_attributes.dup
- end
+ ##
+ # The platform this gem runs on.
+ #
+ # This is usually Gem::Platform::RUBY or Gem::Platform::CURRENT.
+ #
+ # Most gems contain pure Ruby code; they should simply leave the default
+ # value in place. Some gems contain C (or other) code to be compiled into a
+ # Ruby "extension". The gem should leave the default value in place unless
+ # the code will only compile on a certain type of system. Some gems consist
+ # of pre-compiled code ("binary gems"). It's especially important that they
+ # set the platform attribute appropriately. A shortcut is to set the
+ # platform to Gem::Platform::CURRENT, which will cause the gem builder to set
+ # the platform to the appropriate value for the system on which the build is
+ # being performed.
+ #
+ # If this attribute is set to a non-default value, it will be included in
+ # the filename of the gem when it is built such as:
+ # nokogiri-1.6.0-x86-mingw32.gem
+ #
+ # Usage:
+ #
+ # spec.platform = Gem::Platform.local
- ##
- # Is +name+ a required attribute?
+ def platform=(platform)
+ @original_platform = platform
- def self.required_attribute?(name)
- @@required_attributes.include? name.to_sym
- end
+ case platform
+ when Gem::Platform::CURRENT then
+ @new_platform = Gem::Platform.local
+ @original_platform = @new_platform.to_s
- ##
- # Specification attributes that are arrays (appendable and so-forth)
+ when Gem::Platform then
+ @new_platform = platform
- def self.array_attributes
- @@array_attributes.dup
+ # legacy constants
+ when nil, Gem::Platform::RUBY then
+ @new_platform = Gem::Platform::RUBY
+ when "mswin32" then # was Gem::Platform::WIN32
+ @new_platform = Gem::Platform.new "x86-mswin32"
+ when "i586-linux" then # was Gem::Platform::LINUX_586
+ @new_platform = Gem::Platform.new "x86-linux"
+ when "powerpc-darwin" then # was Gem::Platform::DARWIN
+ @new_platform = Gem::Platform.new "ppc-darwin"
+ else
+ @new_platform = Gem::Platform.new platform
end
- ##
- # A list of Specification instances that have been defined in this Ruby
- # instance.
+ @platform = @new_platform.to_s
+ end
+
+ ##
+ # Paths in the gem to add to <code>$LOAD_PATH</code> when this gem is
+ # activated.
+ #--
+ # See also #require_paths
+ #++
+ # If you have an extension you do not need to add <code>"ext"</code> to the
+ # require path, the extension build process will copy the extension files
+ # into "lib" for you.
+ #
+ # The default value is <code>"lib"</code>
+ #
+ # Usage:
+ #
+ # # If all library files are in the root directory...
+ # spec.require_paths = ['.']
+
+ def require_paths=(val)
+ @require_paths = Array(val)
+ end
+
+ ##
+ # The RubyGems version required by this gem
+
+ attr_reader :required_rubygems_version
+
+ ##
+ # The key used to sign this gem. See Gem::Security for details.
+
+ attr_accessor :signing_key
- def self.list
- @@list
+ ##
+ # Adds a development dependency named +gem+ with +requirements+ to this
+ # gem.
+ #
+ # Usage:
+ #
+ # spec.add_development_dependency 'example', '~> 1.1', '>= 1.1.4'
+ #
+ # Development dependencies aren't installed by default and aren't
+ # activated when a gem is required.
+
+ def add_development_dependency(gem, *requirements)
+ add_dependency_with_type(gem, :development, requirements)
+ end
+
+ ##
+ # Adds a runtime dependency named +gem+ with +requirements+ to this gem.
+ #
+ # Usage:
+ #
+ # spec.add_dependency 'example', '~> 1.1', '>= 1.1.4'
+
+ def add_dependency(gem, *requirements)
+ if requirements.uniq.size != requirements.size
+ warn "WARNING: duplicated #{gem} dependency #{requirements}"
end
- ##
- # Specifies the +name+ and +default+ for a specification attribute, and
- # creates a reader and writer method like Module#attr_accessor.
- #
- # The reader method returns the default if the value hasn't been set.
+ add_dependency_with_type(gem, :runtime, requirements)
+ end
+
+ ##
+ # Executables included in the gem.
+ #
+ # For example, the rake gem has rake as an executable. You don't specify the
+ # full path (as in bin/rake); all application-style files are expected to be
+ # found in bindir. These files must be executable Ruby files. Files that
+ # use bash or other interpreters will not work.
+ #
+ # Executables included may only be ruby scripts, not scripts for other
+ # languages or compiled binaries.
+ #
+ # Usage:
+ #
+ # spec.executables << 'rake'
+
+ def executables
+ @executables ||= []
+ end
+
+ ##
+ # Extensions to build when installing the gem, specifically the paths to
+ # extconf.rb-style files used to compile extensions.
+ #
+ # These files will be run when the gem is installed, causing the C (or
+ # whatever) code to be compiled on the user's machine.
+ #
+ # Usage:
+ #
+ # spec.extensions << 'ext/rmagic/extconf.rb'
+ #
+ # See Gem::Ext::Builder for information about writing extensions for gems.
+
+ def extensions
+ @extensions ||= []
+ end
+
+ ##
+ # Extra files to add to RDoc such as README or doc/examples.txt
+ #
+ # When the user elects to generate the RDoc documentation for a gem (typically
+ # at install time), all the library files are sent to RDoc for processing.
+ # This option allows you to have some non-code files included for a more
+ # complete set of documentation.
+ #
+ # Usage:
+ #
+ # spec.extra_rdoc_files = ['README', 'doc/user-guide.txt']
+
+ def extra_rdoc_files
+ @extra_rdoc_files ||= []
+ end
+
+ ##
+ # The version of RubyGems that installed this gem. Returns
+ # <code>Gem::Version.new(0)</code> for gems installed by versions earlier
+ # than RubyGems 2.2.0.
+
+ def installed_by_version # :nodoc:
+ @installed_by_version ||= Gem::Version.new(0)
+ end
+
+ ##
+ # Sets the version of RubyGems that installed this gem. See also
+ # #installed_by_version.
+
+ def installed_by_version=(version) # :nodoc:
+ @installed_by_version = Gem::Version.new version
+ end
+
+ ##
+ # Specifies the rdoc options to be used when generating API documentation.
+ #
+ # Usage:
+ #
+ # spec.rdoc_options << '--title' << 'Rake -- Ruby Make' <<
+ # '--main' << 'README' <<
+ # '--line-numbers'
+
+ def rdoc_options
+ @rdoc_options ||= []
+ end
+
+ LATEST_RUBY_WITHOUT_PATCH_VERSIONS = Gem::Version.new("2.1")
- def self.attribute(name, default=nil)
- ivar_name = "@#{name}".intern
- if default.nil? then
- @@nil_attributes << ivar_name
+ ##
+ # The version of Ruby required by this gem. The ruby version can be
+ # specified to the patch-level:
+ #
+ # $ ruby -v -e 'p Gem.ruby_version'
+ # ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-darwin12.4.0]
+ # #<Gem::Version "2.0.0.247">
+ #
+ # Prereleases can also be specified.
+ #
+ # Usage:
+ #
+ # # This gem will work with 1.8.6 or greater...
+ # spec.required_ruby_version = '>= 1.8.6'
+ #
+ # # Only with final releases of major version 2 where minor version is at least 3
+ # spec.required_ruby_version = '~> 2.3'
+ #
+ # # Only prereleases or final releases after 2.6.0.preview2
+ # spec.required_ruby_version = '> 2.6.0.preview2'
+ #
+ # # This gem will work with 2.3.0 or greater, including major version 3, but lesser than 4.0.0
+ # spec.required_ruby_version = '>= 2.3', '< 4'
+
+ def required_ruby_version=(req)
+ @required_ruby_version = Gem::Requirement.create req
+
+ @required_ruby_version.requirements.map! do |op, v|
+ if v >= LATEST_RUBY_WITHOUT_PATCH_VERSIONS && v.release.segments.size == 4
+ [op == "~>" ? "=" : op, Gem::Version.new(v.segments.tap {|s| s.delete_at(3) }.join("."))]
else
- @@non_nil_attributes << [ivar_name, default]
+ [op, v]
end
-
- @@attributes << [name, default]
- @@default_value[name] = default
- attr_accessor(name)
end
+ end
- ##
- # Same as :attribute, but ensures that values assigned to the attribute
- # are array values by applying :to_a to the value.
+ ##
+ # The RubyGems version required by this gem
- def self.array_attribute(name)
- @@non_nil_attributes << ["@#{name}".intern, []]
+ def required_rubygems_version=(req)
+ @required_rubygems_version = Gem::Requirement.create req
+ end
- @@array_attributes << name
- @@attributes << [name, []]
- @@default_value[name] = []
- code = %{
- def #{name}
- @#{name} ||= []
- end
- def #{name}=(value)
- @#{name} = Array(value)
- end
- }
+ ##
+ # Lists the external (to RubyGems) requirements that must be met for this gem
+ # to work. It's simply information for the user.
+ #
+ # Usage:
+ #
+ # spec.requirements << 'libmagick, v6.0'
+ # spec.requirements << 'A good graphics card'
- module_eval code, __FILE__, __LINE__ - 9
- end
+ def requirements
+ @requirements ||= []
+ end
+
+ ##
+ # A collection of unit test files. They will be loaded as unit tests when
+ # the user requests a gem to be unit tested.
+ #
+ # Usage:
+ # spec.test_files = Dir.glob('test/tc_*.rb')
+ # spec.test_files = ['tests/test-suite.rb']
+
+ def test_files=(files) # :nodoc:
+ @test_files = Array files
+ end
+
+ ######################################################################
+ # :section: Read-only attributes
+
+ ##
+ # The version of RubyGems used to create this gem.
+
+ attr_accessor :rubygems_version
+
+ ##
+ # The path where this gem installs its extensions.
+
+ def extensions_dir
+ @extensions_dir ||= super
+ end
+
+ ######################################################################
+ # :section: Specification internals
+
+ ##
+ # True when this gemspec has been activated. This attribute is not persisted.
- ##
- # Same as attribute above, but also records this attribute as mandatory.
+ attr_accessor :activated
- def self.required_attribute(*args)
- @@required_attributes << args.first
- attribute(*args)
+ alias_method :activated?, :activated
+
+ ##
+ # Autorequire was used by old RubyGems to automatically require a file.
+ #
+ # Deprecated: It is neither supported nor functional.
+
+ attr_accessor :autorequire # :nodoc:
+
+ ##
+ # Allows deinstallation of gems with legacy platforms.
+
+ attr_writer :original_platform # :nodoc:
+
+ ##
+ # The Gem::Specification version of this gemspec.
+ #
+ # Do not set this, it is set automatically when the gem is packaged.
+
+ attr_accessor :specification_version
+
+ def self._all # :nodoc:
+ specification_record.all
+ end
+
+ def self.clear_load_cache # :nodoc:
+ @load_cache_mutex.synchronize do
+ @load_cache.clear
end
+ end
+ private_class_method :clear_load_cache
- ##
- # Sometimes we don't want the world to use a setter method for a
- # particular attribute.
- #
- # +read_only+ makes it private so we can still use it internally.
+ def self.gem_path # :nodoc:
+ Gem.path
+ end
+ private_class_method :gem_path
- def self.read_only(*names)
- names.each do |name|
- private "#{name}="
+ def self.each_gemspec(dirs) # :nodoc:
+ dirs.each do |dir|
+ Gem::Util.glob_files_in_dir("*.gemspec", dir).each do |path|
+ yield path
end
end
+ end
- # Shortcut for creating several attributes at once (each with a default
- # value of +nil+).
+ def self.gemspec_stubs_in(dir, pattern) # :nodoc:
+ Gem::Util.glob_files_in_dir(pattern, dir).map {|path| yield path }.select(&:valid?)
+ end
- def self.attributes(*args)
- args.each do |arg|
- attribute(arg, nil)
- end
+ def self.each_spec(dirs) # :nodoc:
+ each_gemspec(dirs) do |path|
+ spec = self.load path
+ yield spec if spec
end
+ end
+
+ ##
+ # Returns a Gem::StubSpecification for every installed gem
- ##
- # Some attributes require special behaviour when they are accessed. This
- # allows for that.
+ def self.stubs
+ specification_record.stubs
+ end
- def self.overwrite_accessor(name, &block)
- remove_method name
- define_method(name, &block)
+ ##
+ # Returns a Gem::StubSpecification for default gems
+
+ def self.default_stubs(pattern = "*.gemspec")
+ base_dir = Gem.default_dir
+ gems_dir = File.join base_dir, "gems"
+ gemspec_stubs_in(Gem.default_specifications_dir, pattern) do |path|
+ Gem::StubSpecification.default_gemspec_stub(path, base_dir, gems_dir)
end
+ end
- ##
- # Defines a _singular_ version of an existing _plural_ attribute (i.e. one
- # whose value is expected to be an array). This means just creating a
- # helper method that takes a single value and appends it to the array.
- # These are created for convenience, so that in a spec, one can write
- #
- # s.require_path = 'mylib'
- #
- # instead of:
- #
- # s.require_paths = ['mylib']
- #
- # That above convenience is available courtesy of:
- #
- # attribute_alias_singular :require_path, :require_paths
-
- def self.attribute_alias_singular(singular, plural)
- define_method("#{singular}=") { |val|
- send("#{plural}=", [val])
- }
- define_method("#{singular}") {
- val = send("#{plural}")
- val.nil? ? nil : val.first
- }
+ ##
+ # Returns a Gem::StubSpecification for installed gem named +name+
+ # only returns stubs that match Gem.platforms
+
+ def self.stubs_for(name)
+ specification_record.stubs_for(name)
+ end
+
+ ##
+ # Finds stub specifications matching a pattern from the standard locations,
+ # optionally filtering out specs not matching the current platform
+ #
+ def self.stubs_for_pattern(pattern, match_platform = true) # :nodoc:
+ specification_record.stubs_for_pattern(pattern, match_platform)
+ end
+
+ def self._resort!(specs) # :nodoc:
+ specs.sort! do |a, b|
+ names = a.name <=> b.name
+ next names if names.nonzero?
+ versions = b.version <=> a.version
+ next versions if versions.nonzero?
+ platforms = Gem::Platform.sort_priority(b.platform) <=> Gem::Platform.sort_priority(a.platform)
+ next platforms if platforms.nonzero?
+ default_gem = a.default_gem_priority <=> b.default_gem_priority
+ next default_gem if default_gem.nonzero?
+ a.base_dir_priority(gem_path) <=> b.base_dir_priority(gem_path)
end
+ end
- ##
- # Dump only crucial instance variables.
- #--
- # MAINTAIN ORDER!
-
- def _dump(limit)
- Marshal.dump [
- @rubygems_version,
- @specification_version,
- @name,
- @version,
- (Time === @date ? @date : (require 'time'; Time.parse(@date.to_s))),
- @summary,
- @required_ruby_version,
- @required_rubygems_version,
- @original_platform,
- @dependencies,
- @rubyforge_project,
- @email,
- @authors,
- @description,
- @homepage,
- @has_rdoc,
- @new_platform,
- ]
+ ##
+ # Loads the default specifications. It should be called only once.
+
+ def self.load_defaults
+ each_spec([Gem.default_specifications_dir]) do |spec|
+ # #load returns nil if the spec is bad, so we just ignore
+ # it at this stage
+ Gem.register_default_spec(spec)
end
+ end
- ##
- # Load custom marshal format, re-initializing defaults as needed
+ ##
+ # Adds +spec+ to the known specifications, keeping the collection
+ # properly sorted.
- def self._load(str)
- array = Marshal.load str
+ def self.add_spec(spec)
+ specification_record.add_spec(spec)
+ end
- spec = Gem::Specification.new
- spec.instance_variable_set :@specification_version, array[1]
+ ##
+ # Removes +spec+ from the known specs.
- current_version = CURRENT_SPECIFICATION_VERSION
+ def self.remove_spec(spec)
+ specification_record.remove_spec(spec)
+ end
- field_count = if spec.specification_version > current_version then
- spec.instance_variable_set :@specification_version,
- current_version
- MARSHAL_FIELDS[current_version]
- else
- MARSHAL_FIELDS[spec.specification_version]
- end
+ ##
+ # Returns all specifications. This method is discouraged from use.
+ # You probably want to use one of the Enumerable methods instead.
- if array.size < field_count then
- raise TypeError, "invalid Gem::Specification format #{array.inspect}"
- end
+ def self.all
+ warn "NOTE: Specification.all called from #{caller(1, 1).first}" unless
+ Gem::Deprecate.skip
+ _all
+ end
- spec.instance_variable_set :@rubygems_version, array[0]
- # spec version
- spec.instance_variable_set :@name, array[2]
- spec.instance_variable_set :@version, array[3]
- spec.instance_variable_set :@date, array[4]
- spec.instance_variable_set :@summary, array[5]
- spec.instance_variable_set :@required_ruby_version, array[6]
- spec.instance_variable_set :@required_rubygems_version, array[7]
- spec.instance_variable_set :@original_platform, array[8]
- spec.instance_variable_set :@dependencies, array[9]
- spec.instance_variable_set :@rubyforge_project, array[10]
- spec.instance_variable_set :@email, array[11]
- spec.instance_variable_set :@authors, array[12]
- spec.instance_variable_set :@description, array[13]
- spec.instance_variable_set :@homepage, array[14]
- spec.instance_variable_set :@has_rdoc, array[15]
- spec.instance_variable_set :@new_platform, array[16]
- spec.instance_variable_set :@platform, array[16].to_s
- spec.instance_variable_set :@loaded, false
-
- spec
- end
+ ##
+ # Sets the known specs to +specs+.
- ##
- # List of depedencies that will automatically be activated at runtime.
+ def self.all=(specs)
+ specification_record.all = specs
+ end
- def runtime_dependencies
- dependencies.select { |d| d.type == :runtime || d.type == nil }
- end
+ ##
+ # Return full names of all specs in sorted order.
+
+ def self.all_names
+ specification_record.all_names
+ end
- ##
- # List of dependencies that are used for development
+ ##
+ # Return the list of all array-oriented instance variables.
+ #--
+ # Not sure why we need to use so much stupid reflection in here...
+
+ def self.array_attributes
+ @@array_attributes.dup
+ end
+
+ ##
+ # Return the list of all instance variables.
+ #--
+ # Not sure why we need to use so much stupid reflection in here...
+
+ def self.attribute_names
+ @@attributes.dup
+ end
+
+ ##
+ # Return the directories that Specification uses to find specs.
+
+ def self.dirs
+ @@dirs ||= Gem::SpecificationRecord.dirs_from(gem_path)
+ end
+
+ ##
+ # Set the directories that Specification uses to find specs. Setting
+ # this resets the list of known specs.
+
+ def self.dirs=(dirs)
+ reset
+
+ @@dirs = Gem::SpecificationRecord.dirs_from(Array(dirs))
+ end
+
+ extend Enumerable
+
+ ##
+ # Enumerate every known spec. See ::dirs= and ::add_spec to set the list of
+ # specs.
+
+ def self.each(&block)
+ specification_record.each(&block)
+ end
+
+ ##
+ # Returns every spec that matches +name+ and optional +requirements+.
- def development_dependencies
- dependencies.select { |d| d.type == :development }
+ def self.find_all_by_name(name, *requirements)
+ specification_record.find_all_by_name(name, *requirements)
+ end
+
+ ##
+ # Returns every spec that has the given +full_name+
+
+ def self.find_all_by_full_name(full_name)
+ stubs.select {|s| s.full_name == full_name }.map(&:to_spec)
+ end
+
+ ##
+ # Find the best specification matching a +name+ and +requirements+. Raises
+ # if the dependency doesn't resolve to a valid specification.
+
+ def self.find_by_name(name, *requirements)
+ requirements = Gem::Requirement.default if requirements.empty?
+
+ Gem::Dependency.new(name, *requirements).to_spec
+ end
+
+ ##
+ # Find the best specification matching a +full_name+.
+ def self.find_by_full_name(full_name)
+ stubs.find {|s| s.full_name == full_name }&.to_spec
+ end
+
+ ##
+ # Return the best specification that contains the file matching +path+.
+
+ def self.find_by_path(path)
+ specification_record.find_by_path(path)
+ end
+
+ ##
+ # Return the best specification that contains the file matching +path+
+ # amongst the specs that are not loaded. This method is different than
+ # +find_inactive_by_path+ as it will filter out loaded specs by their name.
+
+ def self.find_unloaded_by_path(path)
+ specification_record.find_unloaded_by_path(path)
+ end
+
+ ##
+ # Return the best specification that contains the file matching +path+
+ # amongst the specs that are not activated.
+
+ def self.find_inactive_by_path(path)
+ specification_record.find_inactive_by_path(path)
+ end
+
+ ##
+ # Return the best specification that contains the file matching +path+, among
+ # those already activated.
+
+ def self.find_active_stub_by_path(path)
+ specification_record.find_active_stub_by_path(path)
+ end
+
+ ##
+ # Return currently unresolved specs that contain the file matching +path+.
+
+ def self.find_in_unresolved(path)
+ unresolved_specs.find_all {|spec| spec.contains_requirable_file? path }
+ end
+
+ ##
+ # Search through all unresolved deps and sub-dependencies and return
+ # specs that contain the file matching +path+.
+
+ def self.find_in_unresolved_tree(path)
+ unresolved_specs.each do |spec|
+ spec.traverse do |_from_spec, _dep, to_spec, trail|
+ if to_spec.has_conflicts? || to_spec.conflicts_when_loaded_with?(trail)
+ :next
+ else
+ return trail.reverse if to_spec.contains_requirable_file? path
+ end
+ end
end
- def test_suite_file # :nodoc:
- warn 'test_suite_file deprecated, use test_files'
- test_files.first
+ []
+ end
+
+ def self.unresolved_specs
+ unresolved_deps.values.flat_map(&:to_specs)
+ end
+ private_class_method :unresolved_specs
+
+ ##
+ # Special loader for YAML files. When a Specification object is loaded
+ # from a YAML file, it bypasses the normal Ruby object initialization
+ # routine (#initialize). This method makes up for that and deals with
+ # gems of different ages.
+ #
+ # +input+ can be anything that YAML.load() accepts: String or IO.
+
+ def self.from_yaml(input)
+ Gem.load_yaml
+
+ input = normalize_yaml_input input
+ spec = Gem::SafeYAML.safe_load input
+
+ if spec && spec.class == FalseClass
+ raise Gem::EndOfYAMLException
end
- def test_suite_file=(val) # :nodoc:
- warn 'test_suite_file= deprecated, use test_files='
- @test_files = [] unless defined? @test_files
- @test_files << val
+ unless Gem::Specification === spec
+ raise Gem::Exception, "YAML data doesn't evaluate to gem specification"
end
- ##
- # true when this gemspec has been loaded from a specifications directory.
- # This attribute is not persisted.
+ spec.specification_version ||= NONEXISTENT_SPECIFICATION_VERSION
+ spec.reset_nil_attributes_to_default
+ spec.flatten_require_paths
+
+ spec
+ end
- attr_accessor :loaded
+ ##
+ # Return the latest specs, optionally including prerelease specs if
+ # +prerelease+ is true.
- ##
- # Path this gemspec was loaded from. This attribute is not persisted.
+ def self.latest_specs(prerelease = false)
+ specification_record.latest_specs(prerelease)
+ end
- attr_accessor :loaded_from
+ ##
+ # Return the latest installed spec for gem +name+.
- ##
- # Returns an array with bindir attached to each executable in the
- # executables list
+ def self.latest_spec_for(name)
+ specification_record.latest_spec_for(name)
+ end
- def add_bindir(executables)
- return nil if executables.nil?
+ def self._latest_specs(specs, prerelease = false) # :nodoc:
+ result = {}
- if @bindir then
- Array(executables).map { |e| File.join(@bindir, e) }
- else
- executables
+ specs.reverse_each do |spec|
+ unless prerelease
+ next if spec.version.prerelease?
end
- rescue
- return nil
+
+ result[spec.name] = spec
end
- ##
- # Files in the Gem under one of the require_paths
+ result.flat_map(&:last).sort_by(&:name)
+ end
+
+ ##
+ # Loads Ruby format gemspec from +file+.
- def lib_files
- @files.select do |file|
- require_paths.any? do |path|
- file.index(path) == 0
+ def self.load(file)
+ return unless file
+
+ spec = @load_cache_mutex.synchronize { @load_cache[file] }
+ return spec if spec
+
+ return unless File.file?(file)
+
+ code = Gem.open_file(file, "r:UTF-8:-", &:read)
+
+ begin
+ spec = eval code, binding, file
+
+ if Gem::Specification === spec
+ spec.loaded_from = File.expand_path file.to_s
+ @load_cache_mutex.synchronize do
+ prev = @load_cache[file]
+ if prev
+ spec = prev
+ else
+ @load_cache[file] = spec
+ end
end
+ return spec
end
+
+ warn "[#{file}] isn't a Gem::Specification (#{spec.class} instead)."
+ rescue SignalException, SystemExit
+ raise
+ rescue SyntaxError, StandardError => e
+ warn "Invalid gemspec in [#{file}]: #{e}"
end
- ##
- # True if this gem was loaded from disk
+ nil
+ end
- alias :loaded? :loaded
+ ##
+ # Specification attributes that must be non-nil
- ##
- # True if this gem has files in test_files
+ def self.non_nil_attributes
+ @@non_nil_attributes.dup
+ end
- def has_unit_tests?
- not test_files.empty?
- end
+ ##
+ # Make sure the YAML specification is properly formatted with dashes
+
+ def self.normalize_yaml_input(input)
+ result = input.respond_to?(:read) ? input.read : input
+ result = "--- " + result unless result.start_with?("--- ")
+ result = result.dup
+ result.gsub!(/ !!null \n/, " \n")
+ # date: 2011-04-26 00:00:00.000000000Z
+ # date: 2011-04-26 00:00:00.000000000 Z
+ result.gsub!(/^(date: \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+?)Z/, '\1 Z')
+ result
+ end
+
+ ##
+ # Return a list of all outdated local gem names. This method is HEAVY
+ # as it must go fetch specifications from the server.
+ #
+ # Use outdated_and_latest_version if you wish to retrieve the latest remote
+ # version as well.
+
+ def self.outdated
+ outdated_and_latest_version.map {|local, _| local.name }
+ end
+
+ ##
+ # Enumerates the outdated local gems yielding the local specification and
+ # the latest remote version.
+ #
+ # This method may take some time to return as it must check each local gem
+ # against the server's index.
+
+ def self.outdated_and_latest_version
+ return enum_for __method__ unless block_given?
+
+ # TODO: maybe we should switch to rubygems' version service?
+ fetcher = Gem::SpecFetcher.fetcher
- alias has_test_suite? has_unit_tests? # :nodoc: deprecated
-
- ##
- # Specification constructor. Assigns the default values to the
- # attributes, adds this spec to the list of loaded specs (see
- # Specification.list), and yields itself for further initialization.
+ latest_specs(true).each do |local_spec|
+ dependency =
+ Gem::Dependency.new local_spec.name, ">= #{local_spec.version}"
- def initialize
- @new_platform = nil
- assign_defaults
- @loaded = false
- @loaded_from = nil
- @@list << self
+ remotes, = fetcher.search_for_dependency dependency
+ remotes = remotes.map {|n, _| n.version }
- yield self if block_given?
+ latest_remote = remotes.sort.last
- @@gather.call(self) if @@gather
+ yield [local_spec, latest_remote] if
+ latest_remote && local_spec.version < latest_remote
end
- ##
- # Each attribute has a default value (possibly nil). Here, we initialize
- # all attributes to their default value. This is done through the
- # accessor methods, so special behaviours will be honored. Furthermore,
- # we take a _copy_ of the default so each specification instance has its
- # own empty arrays, etc.
+ nil
+ end
- def assign_defaults
- @@nil_attributes.each do |name|
- instance_variable_set name, nil
- end
+ ##
+ # Is +name+ a required attribute?
- @@non_nil_attributes.each do |name, default|
- value = case default
- when Time, Numeric, Symbol, true, false, nil then default
- else default.dup
- end
+ def self.required_attribute?(name)
+ @@required_attributes.include? name.to_sym
+ end
+
+ ##
+ # Required specification attributes
- instance_variable_set name, value
+ def self.required_attributes
+ @@required_attributes.dup
+ end
+
+ ##
+ # Reset the list of known specs, running pre and post reset hooks
+ # registered in Gem.
+
+ def self.reset
+ @@dirs = nil
+ Gem.pre_reset_hooks.each(&:call)
+ @specification_record = nil
+ clear_load_cache
+
+ unless unresolved_deps.empty?
+ unresolved = unresolved_deps.filter_map do |name, dep|
+ matching_versions = find_all_by_name(name)
+ next if dep.latest_version? && matching_versions.any?(&:default_gem?)
+
+ [dep, matching_versions.uniq(&:full_name)]
+ end.to_h
+
+ unless unresolved.empty?
+ warn "WARN: Unresolved or ambiguous specs during Gem::Specification.reset:"
+ unresolved.each do |dep, versions|
+ warn " #{dep}"
+
+ unless versions.empty?
+ warn " Available/installed versions of this gem:"
+ versions.each {|s| warn " - #{s.version}" }
+ end
+ end
+ warn "WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>'"
+ warn "Please report a bug if this causes problems."
end
- # HACK
- instance_variable_set :@new_platform, Gem::Platform::RUBY
+ unresolved_deps.clear
end
+ Gem.post_reset_hooks.each(&:call)
+ end
- ##
- # Special loader for YAML files. When a Specification object is loaded
- # from a YAML file, it bypasses the normal Ruby object initialization
- # routine (#initialize). This method makes up for that and deals with
- # gems of different ages.
- #
- # 'input' can be anything that YAML.load() accepts: String or IO.
+ ##
+ # Keeps track of all currently known specifications
- def self.from_yaml(input)
- input = normalize_yaml_input input
- spec = YAML.load input
+ def self.specification_record
+ @specification_record ||= Gem::SpecificationRecord.new(dirs)
+ end
- if spec && spec.class == FalseClass then
- raise Gem::EndOfYAMLException
- end
+ # DOC: This method needs documented or nodoc'd
+ def self.unresolved_deps
+ @unresolved_deps ||= Hash.new {|h, n| h[n] = Gem::Dependency.new n }
+ end
- unless Gem::Specification === spec then
- raise Gem::Exception, "YAML data doesn't evaluate to gem specification"
+ ##
+ # Load custom marshal format, re-initializing defaults as needed
+
+ def self._load(str)
+ Gem.load_yaml
+ Gem.load_safe_marshal
+
+ yaml_set = false
+ retry_count = 0
+
+ array = begin
+ Gem::SafeMarshal.safe_load str
+ rescue ArgumentError => e
+ # Avoid an infinite retry loop when the argument error has nothing to do
+ # with the classes not being defined.
+ # 1 retry each allowed in case all 3 of
+ # - YAML
+ # - YAML::Syck::DefaultKey
+ # - YAML::PrivateType
+ # need to be defined
+ raise if retry_count >= 3
+
+ #
+ # Some very old marshaled specs included references to `YAML::PrivateType`
+ # and `YAML::Syck::DefaultKey` constants due to bugs in the old emitter
+ # that generated them. Workaround the issue by defining the necessary
+ # constants and retrying.
+ #
+ message = e.message
+ raise unless message.include?("YAML::")
+
+ unless Object.const_defined?(:YAML)
+ Object.const_set "YAML", Module.new
+ yaml_set = true
end
- unless (spec.instance_variables.include? '@specification_version' or
- spec.instance_variables.include? :@specification_version) and
- spec.instance_variable_get :@specification_version
- spec.instance_variable_set :@specification_version,
- NONEXISTENT_SPECIFICATION_VERSION
+ if message.include?("YAML::Syck::")
+ YAML.const_set "Syck", YAML unless YAML.const_defined?(:Syck)
+
+ YAML::Syck.const_set "DefaultKey", Class.new if message.include?("YAML::Syck::DefaultKey") && !YAML::Syck.const_defined?(:DefaultKey)
+ elsif message.include?("YAML::PrivateType") && !YAML.const_defined?(:PrivateType)
+ YAML.const_set "PrivateType", Class.new { attr_accessor :type_id, :value }
end
- spec
- end
+ retry_count += 1
+ retry
+ ensure
+ Object.__send__(:remove_const, "YAML") if yaml_set
+ end
+
+ spec = Gem::Specification.new
+ spec.instance_variable_set :@specification_version, array[1]
+
+ current_version = CURRENT_SPECIFICATION_VERSION
+
+ field_count = if spec.specification_version > current_version
+ spec.instance_variable_set :@specification_version,
+ current_version
+ MARSHAL_FIELDS[current_version]
+ else
+ MARSHAL_FIELDS[spec.specification_version]
+ end
+
+ if array.size < field_count
+ raise TypeError, "invalid Gem::Specification format #{array.inspect}"
+ end
+
+ spec.instance_variable_set :@rubygems_version, array[0]
+ # spec version
+ spec.instance_variable_set :@name, array[2]
+ spec.instance_variable_set :@version, array[3]
+ spec.date = array[4]
+ spec.instance_variable_set :@summary, array[5]
+ spec.instance_variable_set :@required_ruby_version, array[6]
+ spec.instance_variable_set :@required_rubygems_version, array[7]
+ spec.platform = array[8]
+ spec.instance_variable_set :@dependencies, array[9]
+ # offset due to rubyforge_project removal
+ spec.instance_variable_set :@email, array[11]
+ spec.instance_variable_set :@authors, array[12]
+ spec.instance_variable_set :@description, array[13]
+ spec.instance_variable_set :@homepage, array[14]
+ # offset due to has_rdoc removal
+ spec.instance_variable_set :@licenses, array[17]
+ spec.instance_variable_set :@metadata, array[18]
+ spec.instance_variable_set :@loaded, false
+ spec.instance_variable_set :@activated, false
+
+ spec
+ end
- ##
- # Loads ruby format gemspec from +filename+
+ def <=>(other) # :nodoc:
+ sort_obj <=> other.sort_obj
+ end
- def self.load(filename)
- gemspec = nil
- fail "NESTED Specification.load calls not allowed!" if @@gather
- @@gather = proc { |gs| gemspec = gs }
- data = File.read(filename)
- eval(data)
- gemspec
- ensure
- @@gather = nil
- end
+ def ==(other) # :nodoc:
+ self.class === other &&
+ name == other.name &&
+ version == other.version &&
+ platform == other.platform
+ end
- ##
- # Make sure the YAML specification is properly formatted with dashes
+ ##
+ # Dump only crucial instance variables.
+ #--
+ # MAINTAIN ORDER!
+ # (down with the man)
+
+ def _dump(limit)
+ Marshal.dump [
+ @rubygems_version,
+ @specification_version,
+ @name,
+ @version,
+ date,
+ @summary,
+ @required_ruby_version,
+ @required_rubygems_version,
+ @original_platform,
+ @dependencies,
+ "", # rubyforge_project
+ @email,
+ @authors,
+ @description,
+ @homepage,
+ true, # has_rdoc
+ @new_platform,
+ @licenses,
+ @metadata,
+ ]
+ end
- def self.normalize_yaml_input(input)
- result = input.respond_to?(:read) ? input.read : input
- result = "--- " + result unless result =~ /^--- /
- result
- end
-
- ##
- # Sets the rubygems_version to the current RubyGems version
+ ##
+ # Activate this spec, registering it as a loaded spec and adding
+ # it's lib paths to $LOAD_PATH. Returns true if the spec was
+ # activated, false if it was previously activated. Freaks out if
+ # there are conflicts upon activation.
- def mark_version
- @rubygems_version = RubyGemsVersion
+ def activate
+ other = Gem.loaded_specs[name]
+ if other
+ check_version_conflict other
+ return false
end
- ##
- # Ignore unknown attributes while loading
+ raise_if_conflicts
- def method_missing(sym, *a, &b) # :nodoc:
- if @specification_version > CURRENT_SPECIFICATION_VERSION and
- sym.to_s =~ /=$/ then
- warn "ignoring #{sym} loading #{full_name}" if $DEBUG
- else
- super
- end
- end
+ activate_dependencies
+ add_self_to_load_path
- ##
- # Adds a development dependency named +gem+ with +requirements+ to this
- # Gem. For example:
- #
- # spec.add_development_dependency 'jabber4r', '> 0.1', '<= 0.5'
- #
- # Development dependencies aren't installed by default and aren't
- # activated when a gem is required.
-
- def add_development_dependency(gem, *requirements)
- add_dependency_with_type(gem, :development, *requirements)
- end
+ Gem.loaded_specs[name] = self
+ @activated = true
+ @loaded = true
- ##
- # Adds a runtime dependency named +gem+ with +requirements+ to this Gem.
- # For example:
- #
- # spec.add_runtime_dependency 'jabber4r', '> 0.1', '<= 0.5'
+ true
+ end
- def add_runtime_dependency(gem, *requirements)
- add_dependency_with_type(gem, :runtime, *requirements)
- end
+ ##
+ # Activate all unambiguously resolved runtime dependencies of this
+ # spec. Add any ambiguous dependencies to the unresolved list to be
+ # resolved later, as needed.
- ##
- # Adds a runtime dependency
+ def activate_dependencies
+ unresolved = Gem::Specification.unresolved_deps
- alias add_dependency add_runtime_dependency
+ runtime_dependencies.each do |spec_dep|
+ if loaded = Gem.loaded_specs[spec_dep.name]
+ next if spec_dep.matches_spec? loaded
- ##
- # Returns the full name (name-version) of this Gem. Platform information
- # is included (name-version-platform) if it is specified and not the
- # default Ruby platform.
+ msg = "can't satisfy '#{spec_dep}', already activated '#{loaded.full_name}'"
+ e = Gem::LoadError.new msg
+ e.name = spec_dep.name
- def full_name
- if platform == Gem::Platform::RUBY or platform.nil? then
- "#{@name}-#{@version}"
- else
- "#{@name}-#{@version}-#{platform}"
+ raise e
end
- end
- ##
- # Returns the full name (name-version) of this gemspec using the original
- # platform. For use with legacy gems.
+ specs = spec_dep.matching_specs(true).uniq(&:full_name)
- def original_name # :nodoc:
- if platform == Gem::Platform::RUBY or platform.nil? then
- "#{@name}-#{@version}"
+ if specs.size == 0
+ raise Gem::MissingSpecError.new(spec_dep.name, spec_dep.requirement, "at: #{spec_file}")
+ elsif specs.size == 1
+ specs.first.activate
else
- "#{@name}-#{@version}-#{@original_platform}"
+ name = spec_dep.name
+ unresolved[name] = unresolved[name].merge spec_dep
end
end
- ##
- # The full path to the gem (install path + full name).
+ unresolved.delete self.name
+ end
- def full_gem_path
- path = File.join installation_path, 'gems', full_name
- return path if File.directory? path
- File.join installation_path, 'gems', original_name
- end
+ ##
+ # Abbreviate the spec for downloading. Abbreviated specs are only used for
+ # searching, downloading and related activities and do not need deployment
+ # specific information (e.g. list of files). So we abbreviate the spec,
+ # making it much smaller for quicker downloads.
+
+ def abbreviate
+ self.files = []
+ self.test_files = []
+ self.rdoc_options = []
+ self.extra_rdoc_files = []
+ self.cert_chain = []
+ end
- ##
- # The default (generated) file name of the gem.
+ ##
+ # Sanitize the descriptive fields in the spec. Sometimes non-ASCII
+ # characters will garble the site index. Non-ASCII characters will
+ # be replaced by their XML entity equivalent.
+
+ def sanitize
+ self.summary = sanitize_string(summary)
+ self.description = sanitize_string(description)
+ self.post_install_message = sanitize_string(post_install_message)
+ self.authors = authors.collect {|a| sanitize_string(a) }
+ end
- def file_name
- full_name + ".gem"
- end
+ ##
+ # Sanitize a single string.
- ##
- # The directory that this gem was installed into.
+ def sanitize_string(string)
+ return string unless string
- def installation_path
- path = File.dirname(@loaded_from).split(File::SEPARATOR)[0..-2]
- path = path.join File::SEPARATOR
- File.expand_path path
- end
+ # HACK: the #to_s is in here because RSpec has an Array of Arrays of
+ # Strings for authors. Need a way to disallow bad values on gemspec
+ # generation. (Probably won't happen.)
+ string.to_s
+ end
- ##
- # Checks if this specification meets the requirement of +dependency+.
+ ##
+ # Returns an array with bindir attached to each executable in the
+ # +executables+ list
+
+ def add_bindir(executables)
+ return nil if executables.nil?
- def satisfies_requirement?(dependency)
- return @name == dependency.name &&
- dependency.version_requirements.satisfied_by?(@version)
+ if @bindir
+ Array(executables).map {|e| File.join(@bindir, e) }
+ else
+ executables
end
+ rescue StandardError
+ nil
+ end
- ##
- # Returns an object you can use to sort specifications in #sort_by.
+ ##
+ # Adds a dependency on gem +dependency+ with type +type+ that requires
+ # +requirements+. Valid types are currently <tt>:runtime</tt> and
+ # <tt>:development</tt>.
- def sort_obj
- [@name, @version.to_ints, @new_platform == Gem::Platform::RUBY ? -1 : 1]
+ def add_dependency_with_type(dependency, type, requirements)
+ requirements = if requirements.empty?
+ Gem::Requirement.default
+ else
+ requirements.flatten
end
- def <=>(other) # :nodoc:
- sort_obj <=> other.sort_obj
+ unless dependency.respond_to?(:name) &&
+ dependency.respond_to?(:requirement)
+ dependency = Gem::Dependency.new(dependency.to_s, requirements, type)
end
- ##
- # Tests specs for equality (across all attributes).
+ dependencies << dependency
+ end
+
+ private :add_dependency_with_type
+
+ alias_method :add_runtime_dependency, :add_dependency
+
+ ##
+ # Adds this spec's require paths to LOAD_PATH, in the proper location.
+
+ def add_self_to_load_path
+ return if default_gem?
+
+ paths = full_require_paths
- def ==(other) # :nodoc:
- self.class === other && same_attributes?(other)
+ Gem.add_to_load_path(*paths)
+ end
+
+ ##
+ # Singular reader for #authors. Returns the first author in the list
+
+ def author
+ (val = authors) && val.first
+ end
+
+ ##
+ # The list of author names who wrote this gem.
+ #
+ # spec.authors = ['Chad Fowler', 'Jim Weirich', 'Rich Kilmer']
+
+ def authors
+ @authors ||= []
+ end
+
+ ##
+ # Returns the full path to installed gem's bin directory.
+ #
+ # NOTE: do not confuse this with +bindir+, which is just 'bin', not
+ # a full path.
+
+ def bin_dir
+ @bin_dir ||= File.join gem_dir, bindir
+ end
+
+ ##
+ # Returns the full path to an executable named +name+ in this gem.
+
+ def bin_file(name)
+ File.join bin_dir, name
+ end
+
+ ##
+ # Returns the build_args used to install the gem
+
+ def build_args
+ if File.exist? build_info_file
+ build_info = File.readlines build_info_file
+ build_info = build_info.map(&:strip)
+ build_info.delete ""
+ build_info
+ else
+ []
+ end
+ end
+
+ ##
+ # Builds extensions for this platform if the gem has extensions listed and
+ # the gem.build_complete file is missing.
+
+ def build_extensions # :nodoc:
+ return if extensions.empty?
+ return if default_gem?
+ # we need to fresh build when same name and version of default gems
+ return if self.class.find_by_full_name(full_name)&.default_gem?
+ return if File.exist? gem_build_complete_path
+ return unless File.writable?(base_dir)
+ return unless File.exist?(File.join(base_dir, "extensions"))
+
+ begin
+ # We need to require things in $LOAD_PATH without looking for the
+ # extension we are about to build.
+ unresolved_deps = Gem::Specification.unresolved_deps.dup
+ Gem::Specification.unresolved_deps.clear
+
+ require_relative "config_file"
+ require_relative "ext"
+ require_relative "user_interaction"
+
+ ui = Gem::SilentUI.new
+ Gem::DefaultUserInteraction.use_ui ui do
+ builder = Gem::Ext::Builder.new self
+ builder.build_extensions
+ end
+ ensure
+ ui&.close
+ Gem::Specification.unresolved_deps.replace unresolved_deps
end
+ end
- alias eql? == # :nodoc:
+ ##
+ # Returns the full path to the build info directory
+
+ def build_info_dir
+ File.join base_dir, "build_info"
+ end
- ##
- # True if this gem has the same attributes as +other+.
+ ##
+ # Returns the full path to the file containing the build
+ # information generated when the gem was installed
+
+ def build_info_file
+ File.join build_info_dir, "#{full_name}.info"
+ end
- def same_attributes?(other)
- @@attributes.each do |name, default|
- return false unless self.send(name) == other.send(name)
+ ##
+ # Returns the full path to the cache directory containing this
+ # spec's cached gem.
+
+ def cache_dir
+ File.join base_dir, "cache"
+ end
+
+ ##
+ # Returns the full path to the cached gem for this spec.
+
+ def cache_file
+ File.join cache_dir, "#{full_name}.gem"
+ end
+
+ ##
+ # Return any possible conflicts against the currently loaded specs.
+
+ def conflicts
+ conflicts = {}
+ runtime_dependencies.each do |dep|
+ spec = Gem.loaded_specs[dep.name]
+ if spec && !spec.satisfies_requirement?(dep)
+ (conflicts[spec] ||= []) << dep
end
- true
end
+ env_req = Gem.env_requirement(name)
+ (conflicts[self] ||= []) << env_req unless env_req.satisfied_by? version
+ conflicts
+ end
- private :same_attributes?
+ ##
+ # return true if there will be conflict when spec if loaded together with the list of specs.
- def hash # :nodoc:
- @@attributes.inject(0) { |hash_code, (name, default_value)|
- n = self.send(name).hash
- hash_code + n
- }
+ def conflicts_when_loaded_with?(list_of_specs) # :nodoc:
+ result = list_of_specs.any? do |spec|
+ spec.runtime_dependencies.any? {|dep| (dep.name == name) && !satisfies_requirement?(dep) }
end
+ result
+ end
- def to_yaml(opts = {}) # :nodoc:
- mark_version
-
- attributes = @@attributes.map { |name,| name.to_s }.sort
- attributes = attributes - %w[name version platform]
-
- yaml = YAML.quick_emit object_id, opts do |out|
- out.map taguri, to_yaml_style do |map|
- map.add 'name', @name
- map.add 'version', @version
- platform = case @original_platform
- when nil, '' then
- 'ruby'
- when String then
- @original_platform
- else
- @original_platform.to_s
- end
- map.add 'platform', platform
-
- attributes.each do |name|
- map.add name, instance_variable_get("@#{name}")
- end
- end
- end
+ ##
+ # Return true if there are possible conflicts against the currently loaded specs.
+
+ def has_conflicts?
+ return true unless Gem.env_requirement(name).satisfied_by?(version)
+ runtime_dependencies.any? do |dep|
+ spec = Gem.loaded_specs[dep.name]
+ spec && !spec.satisfies_requirement?(dep)
end
+ rescue ArgumentError => e
+ raise e, "#{name} #{version}: #{e.message}"
+ end
- def yaml_initialize(tag, vals) # :nodoc:
- vals.each do |ivar, val|
- instance_variable_set "@#{ivar}", val
- end
+ # The date this gem was created.
+ #
+ # If SOURCE_DATE_EPOCH is set as an environment variable, use that to support
+ # reproducible builds; otherwise, default to the current UTC date.
+ #
+ # Details on SOURCE_DATE_EPOCH:
+ # https://reproducible-builds.org/specs/source-date-epoch/
- @original_platform = @platform # for backwards compatibility
- self.platform = Gem::Platform.new @platform
+ def date
+ @date ||= Time.utc(*Gem.source_date_epoch.utc.to_a[3..5].reverse)
+ end
+
+ DateLike = Object.new # :nodoc:
+ def DateLike.===(obj) # :nodoc:
+ defined?(::Date) && Date === obj
+ end
+
+ DateTimeFormat = # :nodoc:
+ /\A
+ (\d{4})-(\d{2})-(\d{2})
+ (\s+ \d{2}:\d{2}:\d{2}\.\d+ \s* (Z | [-+]\d\d:\d\d) )?
+ \Z/x
+
+ ##
+ # The date this gem was created
+ #
+ # DO NOT set this, it is set automatically when the gem is packaged.
+
+ def date=(date)
+ # We want to end up with a Time object with one-day resolution.
+ # This is the cleanest, most-readable, faster-than-using-Date
+ # way to do it.
+ @date = case date
+ when String then
+ if DateTimeFormat =~ date
+ Time.utc($1.to_i, $2.to_i, $3.to_i)
+ else
+ raise(Gem::InvalidSpecificationException,
+ "invalid date format in specification: #{date.inspect}")
+ end
+ when Time, DateLike then
+ Time.utc(date.year, date.month, date.day)
+ else
+ TODAY
end
+ end
- ##
- # Returns a Ruby code representation of this specification, such that it
- # can be eval'ed and reconstruct the same specification later. Attributes
- # that still have their default values are omitted.
+ ##
+ # The default value for specification attribute +name+
- def to_ruby
- mark_version
- result = []
- result << "# -*- encoding: utf-8 -*-"
- result << nil
- result << "Gem::Specification.new do |s|"
+ def default_value(name)
+ @@default_value[name]
+ end
- result << " s.name = #{ruby_code name}"
- result << " s.version = #{ruby_code version}"
- unless platform.nil? or platform == Gem::Platform::RUBY then
- result << " s.platform = #{ruby_code original_platform}"
- end
- result << ""
- result << " s.required_rubygems_version = #{ruby_code required_rubygems_version} if s.respond_to? :required_rubygems_version="
-
- handled = [
- :dependencies,
- :name,
- :platform,
- :required_rubygems_version,
- :specification_version,
- :version,
- ]
-
- attributes = @@attributes.sort_by { |attr_name,| attr_name.to_s }
-
- attributes.each do |attr_name, default|
- next if handled.include? attr_name
- current_value = self.send(attr_name)
- if current_value != default or
- self.class.required_attribute? attr_name then
- result << " s.#{attr_name} = #{ruby_code current_value}"
+ ##
+ # A list of Gem::Dependency objects this gem depends on.
+ #
+ # Use #add_dependency or #add_development_dependency to add dependencies to
+ # a gem.
+
+ def dependencies
+ @dependencies ||= []
+ end
+
+ ##
+ # Return a list of all gems that have a dependency on this gemspec. The
+ # list is structured with entries that conform to:
+ #
+ # [depending_gem, dependency, [list_of_gems_that_satisfy_dependency]]
+
+ def dependent_gems(check_dev = true)
+ out = []
+ Gem::Specification.each do |spec|
+ deps = check_dev ? spec.dependencies : spec.runtime_dependencies
+ deps.each do |dep|
+ next unless satisfies_requirement?(dep)
+ sats = []
+ find_all_satisfiers(dep) do |sat|
+ sats << sat
end
+ out << [spec, dep, sats]
end
+ end
+ out
+ end
- result << nil
- result << " if s.respond_to? :specification_version then"
- result << " current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION"
- result << " s.specification_version = #{specification_version}"
- result << nil
+ ##
+ # Returns all specs that matches this spec's runtime dependencies.
- result << " if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then"
+ def dependent_specs
+ runtime_dependencies.flat_map(&:to_specs)
+ end
- unless dependencies.empty? then
- dependencies.each do |dep|
- version_reqs_param = dep.requirements_list.inspect
- dep.instance_variable_set :@type, :runtime if dep.type.nil? # HACK
- result << " s.add_#{dep.type}_dependency(%q<#{dep.name}>, #{version_reqs_param})"
- end
- end
+ ##
+ # A detailed description of this gem. See also #summary
- result << " else"
+ def description=(str)
+ @description = str.to_s
+ end
- unless dependencies.empty? then
- dependencies.each do |dep|
- version_reqs_param = dep.requirements_list.inspect
- result << " s.add_dependency(%q<#{dep.name}>, #{version_reqs_param})"
- end
- end
+ ##
+ # List of dependencies that are used for development
- result << ' end'
+ def development_dependencies
+ dependencies.select {|d| d.type == :development }
+ end
- result << " else"
- dependencies.each do |dep|
- version_reqs_param = dep.requirements_list.inspect
- result << " s.add_dependency(%q<#{dep.name}>, #{version_reqs_param})"
- end
- result << " end"
+ ##
+ # Returns the full path to this spec's documentation directory. If +type+
+ # is given it will be appended to the end. For example:
+ #
+ # spec.doc_dir # => "/path/to/gem_repo/doc/a-1"
+ #
+ # spec.doc_dir 'ri' # => "/path/to/gem_repo/doc/a-1/ri"
- result << "end"
- result << nil
+ def doc_dir(type = nil)
+ @doc_dir ||= File.join base_dir, "doc", full_name
- result.join "\n"
+ if type
+ File.join @doc_dir, type
+ else
+ @doc_dir
end
+ end
- ##
- # Checks that the specification contains all required fields, and does a
- # very basic sanity check.
- #
- # Raises InvalidSpecificationException if the spec does not pass the
- # checks..
+ def encode_with(coder) # :nodoc:
+ coder.add "name", @name
+ coder.add "version", @version
+ coder.add "platform", platform.to_s
+ coder.add "original_platform", original_platform.to_s if platform.to_s != original_platform.to_s
- def validate
- extend Gem::UserInteraction
- normalize
+ attributes = @@attributes.map(&:to_s) - %w[name version platform]
+ attributes.each do |name|
+ value = instance_variable_get("@#{name}")
+ coder.add name, value unless value.nil?
+ end
+ end
- if rubygems_version != RubyGemsVersion then
- raise Gem::InvalidSpecificationException,
- "expected RubyGems version #{RubyGemsVersion}, was #{rubygems_version}"
- end
+ def eql?(other) # :nodoc:
+ self.class === other && same_attributes?(other)
+ end
- @@required_attributes.each do |symbol|
- unless self.send symbol then
- raise Gem::InvalidSpecificationException,
- "missing value for attribute #{symbol}"
- end
- end
+ ##
+ # Singular accessor for #executables
- if require_paths.empty? then
- raise Gem::InvalidSpecificationException,
- "specification must have at least one require_path"
- end
+ def executable
+ (val = executables) && val.first
+ end
- case platform
- when Gem::Platform, Platform::RUBY then # ok
- else
- raise Gem::InvalidSpecificationException,
- "invalid platform #{platform.inspect}, see Gem::Platform"
- end
+ ##
+ # Singular accessor for #executables
- unless Array === authors and
- authors.all? { |author| String === author } then
- raise Gem::InvalidSpecificationException,
- 'authors must be Array of Strings'
- end
+ def executable=(o)
+ self.executables = [o]
+ end
- # Warnings
+ ##
+ # Sets executables to +value+, ensuring it is an array.
- %w[author email homepage rubyforge_project summary].each do |attribute|
- value = self.send attribute
- alert_warning "no #{attribute} specified" if value.nil? or value.empty?
- end
+ def executables=(value)
+ @executables = Array(value)
+ end
+
+ ##
+ # Sets extensions to +extensions+, ensuring it is an array.
+
+ def extensions=(extensions)
+ @extensions = Array extensions
+ end
- alert_warning "RDoc will not be generated (has_rdoc == false)" unless
- has_rdoc
+ ##
+ # Sets extra_rdoc_files to +files+, ensuring it is an array.
- alert_warning "deprecated autorequire specified" if autorequire
+ def extra_rdoc_files=(files)
+ @extra_rdoc_files = Array files
+ end
- executables.each do |executable|
- executable_path = File.join bindir, executable
- shebang = File.read(executable_path, 2) == '#!'
+ ##
+ # The default (generated) file name of the gem. See also #spec_name.
+ #
+ # spec.file_name # => "example-1.0.gem"
- alert_warning "#{executable_path} is missing #! line" unless shebang
- end
+ def file_name
+ "#{full_name}.gem"
+ end
- true
- end
+ ##
+ # Sets files to +files+, ensuring it is an array.
- ##
- # Normalize the list of files so that:
- # * All file lists have redundancies removed.
- # * Files referenced in the extra_rdoc_files are included in the package
- # file list.
- #
- # Also, the summary and description are converted to a normal format.
-
- def normalize
- if defined?(@extra_rdoc_files) and @extra_rdoc_files then
- @extra_rdoc_files.uniq!
- @files ||= []
- @files.concat(@extra_rdoc_files)
- end
- @files.uniq! if @files
+ def files=(files)
+ @files = Array files
+ end
+
+ ##
+ # Finds all gems that satisfy +dep+
+
+ def find_all_satisfiers(dep)
+ Gem::Specification.each do |spec|
+ yield spec if spec.satisfies_requirement? dep
end
+ end
- ##
- # Return a list of all gems that have a dependency on this gemspec. The
- # list is structured with entries that conform to:
- #
- # [depending_gem, dependency, [list_of_gems_that_satisfy_dependency]]
-
- def dependent_gems
- out = []
- Gem.source_index.each do |name,gem|
- gem.dependencies.each do |dep|
- if self.satisfies_requirement?(dep) then
- sats = []
- find_all_satisfiers(dep) do |sat|
- sats << sat
- end
- out << [gem, dep, sats]
- end
- end
- end
- out
+ private :find_all_satisfiers
+
+ ##
+ # Creates a duplicate spec without large blobs that aren't used at runtime.
+
+ def for_cache
+ spec = dup
+
+ spec.files = nil
+ spec.test_files = nil
+
+ spec
+ end
+
+ ##
+ # Work around old bundler versions removing my methods
+ # Can be removed once RubyGems can no longer install Bundler 2.5
+
+ def gem_dir # :nodoc:
+ super
+ end
+
+ def gems_dir
+ @gems_dir ||= File.join(base_dir, "gems")
+ end
+
+ ##
+ # True if this gem has files in test_files
+
+ def has_unit_tests? # :nodoc:
+ !test_files.empty?
+ end
+
+ # :stopdoc:
+ alias_method :has_test_suite?, :has_unit_tests?
+ # :startdoc:
+
+ def hash # :nodoc:
+ name.hash ^ version.hash
+ end
+
+ def init_with(coder) # :nodoc:
+ @installed_by_version ||= nil
+ yaml_initialize coder.tag, coder.map
+ end
+
+ eval <<-RUBY, binding, __FILE__, __LINE__ + 1
+ # frozen_string_literal: true
+
+ def set_nil_attributes_to_nil
+ #{@@nil_attributes.map {|key| "@#{key} = nil" }.join "; "}
end
+ private :set_nil_attributes_to_nil
- def to_s
- "#<Gem::Specification name=#{@name} version=#{@version}>"
+ def set_not_nil_attributes_to_default_values
+ #{@@non_nil_attributes.map {|key| "@#{key} = #{INITIALIZE_CODE_FOR_DEFAULTS[key]}" }.join ";"}
end
+ private :set_not_nil_attributes_to_default_values
+ RUBY
- def add_dependency_with_type(dependency, type, *requirements)
- requirements = if requirements.empty? then
- Gem::Requirement.default
- else
- requirements.flatten
- end
+ ##
+ # Specification constructor. Assigns the default values to the attributes
+ # and yields itself for further initialization. Optionally takes +name+ and
+ # +version+.
- unless dependency.respond_to?(:name) &&
- dependency.respond_to?(:version_requirements)
+ def initialize(name = nil, version = nil)
+ super()
+ @gems_dir = nil
+ @base_dir = nil
+ @loaded = false
+ @activated = false
+ @loaded_from = nil
+ @original_platform = nil
+ @installed_by_version = nil
- dependency = Dependency.new(dependency, requirements, type)
- end
+ set_nil_attributes_to_nil
+ set_not_nil_attributes_to_default_values
+
+ @new_platform = Gem::Platform::RUBY
- dependencies << dependency
+ self.name = name if name
+ self.version = version if version
+
+ if (platform = Gem.platforms.last) && platform != Gem::Platform::RUBY && platform != Gem::Platform.local
+ self.platform = platform
end
- private :add_dependency_with_type
+ yield self if block_given?
+ end
- def find_all_satisfiers(dep)
- Gem.source_index.each do |name,gem|
- if(gem.satisfies_requirement?(dep)) then
- yield gem
+ ##
+ # Duplicates Array and Gem::Requirement attributes from +other_spec+ so state isn't shared.
+ #
+
+ def initialize_copy(other_spec)
+ self.class.array_attributes.each do |name|
+ name = :"@#{name}"
+ next unless other_spec.instance_variable_defined? name
+
+ begin
+ val = other_spec.instance_variable_get(name)
+ if val
+ instance_variable_set name, val.dup
+ elsif Gem.configuration.really_verbose
+ warn "WARNING: #{full_name} has an invalid nil value for #{name}"
end
+ rescue TypeError
+ e = Gem::FormatException.new \
+ "#{full_name} has an invalid value for #{name}"
+
+ e.file_path = loaded_from
+ raise e
end
end
- private :find_all_satisfiers
-
- ##
- # Return a string containing a Ruby code representation of the given
- # object.
-
- def ruby_code(obj)
- case obj
- when String then '%q{' + obj + '}'
- when Array then obj.inspect
- when Gem::Version then obj.to_s.inspect
- when Date then '%q{' + obj.strftime('%Y-%m-%d') + '}'
- when Time then '%q{' + obj.strftime('%Y-%m-%d') + '}'
- when Numeric then obj.inspect
- when true, false, nil then obj.inspect
- when Gem::Platform then "Gem::Platform.new(#{obj.to_a.inspect})"
- when Gem::Requirement then "Gem::Requirement.new(#{obj.to_s.inspect})"
- else raise Exception, "ruby_code case not handled: #{obj.class}"
- end
+ @required_ruby_version = other_spec.required_ruby_version.dup
+ @required_rubygems_version = other_spec.required_rubygems_version.dup
+ end
+
+ def base_dir
+ return Gem.dir unless loaded_from
+ @base_dir ||= if default_gem?
+ File.dirname File.dirname File.dirname loaded_from
+ else
+ File.dirname File.dirname loaded_from
end
-
- private :ruby_code
+ end
- # :section: Required gemspec attributes
-
- ##
- # The version of RubyGems used to create this gem
+ def inspect # :nodoc:
+ if $DEBUG
+ super
+ else
+ "#{super[0..-2]} #{full_name}>"
+ end
+ end
- required_attribute :rubygems_version, Gem::RubyGemsVersion
+ ##
+ # Files in the Gem under one of the require_paths
- ##
- # The Gem::Specification version of this gemspec
+ def lib_files
+ @files.select do |file|
+ require_paths.any? do |path|
+ file.start_with? path
+ end
+ end
+ end
- required_attribute :specification_version, CURRENT_SPECIFICATION_VERSION
+ ##
+ # Singular accessor for #licenses
- ##
- # This gem's name
+ def license
+ licenses.first
+ end
- required_attribute :name
+ ##
+ # Plural accessor for setting licenses
+ #
+ # See #license= for details
- ##
- # This gem's version
+ def licenses
+ @licenses ||= []
+ end
- required_attribute :version
+ def internal_init # :nodoc:
+ super
+ @bin_dir = nil
+ @doc_dir = nil
+ @ri_dir = nil
+ @spec_dir = nil
+ @spec_file = nil
+ end
- ##
- # The date this gem was created
+ ##
+ # Track removed method calls to warn about during build time.
+ # Warn about unknown attributes while loading a spec.
- required_attribute :date, TODAY
+ def method_missing(sym, *a, &b) # :nodoc:
+ if REMOVED_METHODS.include?(sym)
+ removed_method_calls << sym
+ return
+ end
- ##
- # A short summary of this gem's description. Displayed in `gem list -d`.
+ if @specification_version > CURRENT_SPECIFICATION_VERSION &&
+ sym.to_s.end_with?("=")
+ warn "ignoring #{sym} loading #{full_name}" if $DEBUG
+ else
+ super
+ end
+ end
- required_attribute :summary
+ ##
+ # Is this specification missing its extensions? When this returns true you
+ # probably want to build_extensions
- ##
- # Paths in the gem to add to $LOAD_PATH when this gem is activated
+ def missing_extensions?
+ return false if RUBY_ENGINE == "jruby"
+ return false if extensions.empty?
+ return false if default_gem?
+ return false if File.exist? gem_build_complete_path
- required_attribute :require_paths, ['lib']
+ true
+ end
- # :section: Optional gemspec attributes
+ ##
+ # Normalize the list of files so that:
+ # * All file lists have redundancies removed.
+ # * Files referenced in the extra_rdoc_files are included in the package
+ # file list.
+
+ def normalize
+ if defined?(@extra_rdoc_files) && @extra_rdoc_files
+ @extra_rdoc_files.uniq!
+ @files ||= []
+ @files.concat(@extra_rdoc_files)
+ end
+
+ @files = @files.uniq.sort if @files
+ @extensions = @extensions.uniq.sort if @extensions
+ @test_files = @test_files.uniq.sort if @test_files
+ @executables = @executables.uniq.sort if @executables
+ @extra_rdoc_files = @extra_rdoc_files.uniq.sort if @extra_rdoc_files
+ end
- ##
- # A contact email for this gem
-
- attribute :email
+ ##
+ # Return a NameTuple that represents this Specification
- ##
- # The URL of this gem's home page
+ def name_tuple
+ Gem::NameTuple.new name, version, original_platform
+ end
- attribute :homepage
+ ##
+ # Returns the full name (name-version) of this gemspec using the original
+ # platform. For use with legacy gems.
- ##
- # The rubyforge project this gem lives under. i.e. RubyGems'
- # rubyforge_project is "rubygems".
-
- attribute :rubyforge_project
+ def original_name # :nodoc:
+ if platform == Gem::Platform::RUBY || platform.nil?
+ "#{@name}-#{@version}"
+ else
+ "#{@name}-#{@version}-#{@original_platform}"
+ end
+ end
- ##
- # A long description of this gem
+ ##
+ # Cruft. Use +platform+.
- attribute :description
+ def original_platform # :nodoc:
+ @original_platform ||= platform
+ end
- ##
- # Autorequire was used by old RubyGems to automatically require a file.
- # It no longer is supported.
+ ##
+ # The platform this gem runs on. See Gem::Platform for details.
- attribute :autorequire
+ def platform
+ @new_platform ||= Gem::Platform::RUBY # rubocop:disable Naming/MemoizedInstanceVariableName
+ end
- ##
- # The default executable for this gem.
+ def pretty_print(q) # :nodoc:
+ q.group 2, "Gem::Specification.new do |s|", "end" do
+ q.breakable
- attribute :default_executable
+ attributes = @@attributes - [:name, :version]
+ attributes.unshift :installed_by_version
+ attributes.unshift :version
+ attributes.unshift :name
- ##
- # The path in the gem for executable scripts
+ attributes.each do |attr_name|
+ current_value = send attr_name
+ current_value = current_value.sort if [:files, :test_files].include? attr_name
+ next unless current_value != default_value(attr_name) ||
+ self.class.required_attribute?(attr_name)
- attribute :bindir, 'bin'
+ q.text "s.#{attr_name} = "
- ##
- # True if this gem is RDoc-compliant
+ if attr_name == :date
+ current_value = current_value.utc
- attribute :has_rdoc, false
+ q.text "Time.utc(#{current_value.year}, #{current_value.month}, #{current_value.day})"
+ else
+ q.pp current_value
+ end
- ##
- # True if this gem supports RDoc
+ q.breakable
+ end
+ end
+ end
- alias :has_rdoc? :has_rdoc
+ ##
+ # Raise an exception if the version of this spec conflicts with the one
+ # that is already loaded (+other+)
- ##
- # The ruby of version required by this gem
+ def check_version_conflict(other) # :nodoc:
+ return if version == other.version
- attribute :required_ruby_version, Gem::Requirement.default
+ # This gem is already loaded. If the currently loaded gem is not in the
+ # list of candidate gems, then we have a version conflict.
- ##
- # The RubyGems version required by this gem
+ msg = "can't activate #{full_name}, already activated #{other.full_name}"
- attribute :required_rubygems_version, Gem::Requirement.default
+ e = Gem::LoadError.new msg
+ e.name = name
- ##
- # The platform this gem runs on. See Gem::Platform for details.
+ raise e
+ end
- attribute :platform, Gem::Platform::RUBY
+ private :check_version_conflict
- ##
- # The key used to sign this gem. See Gem::Security for details.
+ ##
+ # Check the spec for possible conflicts and freak out if there are any.
- attribute :signing_key, nil
+ def raise_if_conflicts # :nodoc:
+ if has_conflicts?
+ raise Gem::ConflictError.new self, conflicts
+ end
+ end
- ##
- # The certificate chain used to sign this gem. See Gem::Security for
- # details.
+ ##
+ # Sets rdoc_options to +value+, ensuring it is a flat array of strings.
+ # Handles malformed gemspecs where rdoc_options may be a Hash or contain Hashes.
- attribute :cert_chain, []
+ def rdoc_options=(options)
+ @rdoc_options = Array(options).flat_map do |opt|
+ opt.is_a?(Hash) ? opt.to_a.flatten.map(&:to_s) : opt
+ end
+ end
- ##
- # A message that gets displayed after the gem is installed
+ ##
+ # Singular accessor for #require_paths
- attribute :post_install_message, nil
+ def require_path
+ (val = require_paths) && val.first
+ end
- ##
- # The list of authors who wrote this gem
+ ##
+ # Singular accessor for #require_paths
- array_attribute :authors
+ def require_path=(path)
+ self.require_paths = Array(path)
+ end
- ##
- # Files included in this gem
+ ##
+ # Set requirements to +req+, ensuring it is an array.
- array_attribute :files
+ def requirements=(req)
+ @requirements = Array req
+ end
- ##
- # Test files included in this gem
+ def respond_to_missing?(m, include_private = false) # :nodoc:
+ false
+ end
- array_attribute :test_files
+ ##
+ # Returns the full path to this spec's ri directory.
- ##
- # An ARGV-style array of options to RDoc
+ def ri_dir
+ @ri_dir ||= File.join base_dir, "ri", full_name
+ end
- array_attribute :rdoc_options
+ ##
+ # Return a string containing a Ruby code representation of the given
+ # object.
+
+ def ruby_code(obj)
+ case obj
+ when String then obj.dump + ".freeze"
+ when Array then "[" + obj.map {|x| ruby_code x }.join(", ") + "]"
+ when Hash then
+ seg = obj.keys.sort.map {|k| "#{k.to_s.dump} => #{obj[k].to_s.dump}" }
+ "{ #{seg.join(", ")} }"
+ when Gem::Version then ruby_code(obj.to_s)
+ when DateLike then obj.strftime("%Y-%m-%d").dump
+ when Time then obj.strftime("%Y-%m-%d").dump
+ when Numeric then obj.inspect
+ when true, false, nil then obj.inspect
+ when Gem::Platform then "Gem::Platform.new(#{ruby_code obj.to_a})"
+ when Gem::Requirement then
+ list = obj.as_list
+ "Gem::Requirement.new(#{ruby_code(list.size == 1 ? obj.to_s : list)})"
+ else raise Gem::Exception, "ruby_code case not handled: #{obj.class}"
+ end
+ end
- ##
- # Extra files to add to RDoc
+ private :ruby_code
- array_attribute :extra_rdoc_files
+ ##
+ # List of dependencies that will automatically be activated at runtime.
- ##
- # Executables included in the gem
+ def runtime_dependencies
+ dependencies.select(&:runtime?)
+ end
- array_attribute :executables
+ ##
+ # True if this gem has the same attributes as +other+.
- ##
- # Extensions to build when installing the gem. See
- # Gem::Installer#build_extensions for valid values.
+ def same_attributes?(spec)
+ @@attributes.all? {|name, _default| send(name) == spec.send(name) }
+ end
- array_attribute :extensions
+ private :same_attributes?
- ##
- # An array or things required by this gem. Not used by anything
- # presently.
+ ##
+ # Checks if this specification meets the requirement of +dependency+.
- array_attribute :requirements
+ def satisfies_requirement?(dependency)
+ @name == dependency.name &&
+ dependency.requirement.satisfied_by?(@version)
+ end
- ##
- # A list of Gem::Dependency objects this gem depends on. Only appendable.
+ ##
+ # Returns an object you can use to sort specifications in #sort_by.
- array_attribute :dependencies
+ def sort_obj
+ [@name, @version, Gem::Platform.sort_priority(@new_platform)]
+ end
- read_only :dependencies
+ ##
+ # Used by Gem::Resolver to order Gem::Specification objects
- # :section: Aliased gemspec attributes
+ def source # :nodoc:
+ Gem::Source::Installed.new
+ end
- ##
- # Singular accessor for executables
-
- attribute_alias_singular :executable, :executables
+ ##
+ # Returns the full path to the directory containing this spec's
+ # gemspec file. eg: /usr/local/lib/ruby/gems/1.8/specifications
- ##
- # Singular accessor for authors
+ def spec_dir
+ @spec_dir ||= File.join base_dir, "specifications"
+ end
- attribute_alias_singular :author, :authors
+ ##
+ # Returns the full path to this spec's gemspec file.
+ # eg: /usr/local/lib/ruby/gems/1.8/specifications/mygem-1.0.gemspec
- ##
- # Singular accessor for require_paths
+ def spec_file
+ @spec_file ||= File.join spec_dir, "#{full_name}.gemspec"
+ end
- attribute_alias_singular :require_path, :require_paths
+ ##
+ # The default name of the gemspec. See also #file_name
+ #
+ # spec.spec_name # => "example-1.0.gemspec"
- ##
- # Singular accessor for test_files
+ def spec_name
+ "#{full_name}.gemspec"
+ end
- attribute_alias_singular :test_file, :test_files
+ ##
+ # A short summary of this gem's description.
- overwrite_accessor :version= do |version|
- @version = Version.create(version)
- end
+ def summary=(str)
+ @summary = str.to_s.strip.
+ gsub(/(\w-)\n[ \t]*(\w)/, '\1\2').gsub(/\n[ \t]*/, " ") # so. weird.
+ end
- overwrite_accessor :platform do
- @new_platform
- end
+ ##
+ # Singular accessor for #test_files
- overwrite_accessor :platform= do |platform|
- if @original_platform.nil? or
- @original_platform == Gem::Platform::RUBY then
- @original_platform = platform
- end
+ def test_file # :nodoc:
+ (val = test_files) && val.first
+ end
- case platform
- when Gem::Platform::CURRENT then
- @new_platform = Gem::Platform.local
- @original_platform = @new_platform.to_s
-
- when Gem::Platform then
- @new_platform = platform
-
- # legacy constants
- when nil, Gem::Platform::RUBY then
- @new_platform = Gem::Platform::RUBY
- when 'mswin32' then # was Gem::Platform::WIN32
- @new_platform = Gem::Platform.new 'x86-mswin32'
- when 'i586-linux' then # was Gem::Platform::LINUX_586
- @new_platform = Gem::Platform.new 'x86-linux'
- when 'powerpc-darwin' then # was Gem::Platform::DARWIN
- @new_platform = Gem::Platform.new 'ppc-darwin'
- else
- @new_platform = Gem::Platform.new platform
- end
+ ##
+ # Singular mutator for #test_files
+
+ def test_file=(file) # :nodoc:
+ self.test_files = [file]
+ end
- @platform = @new_platform.to_s
+ ##
+ # Test files included in this gem. You cannot append to this accessor, you
+ # must assign to it.
+
+ def test_files # :nodoc:
+ # Handle the possibility that we have @test_suite_file but not
+ # @test_files. This will happen when an old gem is loaded via
+ # YAML.
+ if defined? @test_suite_file
+ @test_files = [@test_suite_file].flatten
+ @test_suite_file = nil
+ end
+ if defined?(@test_files) && @test_files
+ @test_files
+ else
+ @test_files = []
+ end
+ end
- @new_platform
+ ##
+ # Returns a Ruby code representation of this specification, such that it can
+ # be eval'ed and reconstruct the same specification later. Attributes that
+ # still have their default values are omitted.
+
+ def to_ruby
+ result = []
+ result << "# -*- encoding: utf-8 -*-"
+ result << "#{Gem::StubSpecification::PREFIX}#{name} #{version} #{platform} #{raw_require_paths.join("\0")}"
+ result << "#{Gem::StubSpecification::PREFIX}#{extensions.join "\0"}" unless
+ extensions.empty?
+ result << nil
+ result << "Gem::Specification.new do |s|"
+
+ result << " s.name = #{ruby_code name}"
+ result << " s.version = #{ruby_code version}"
+ unless platform.nil? || platform == Gem::Platform::RUBY
+ result << " s.platform = #{ruby_code original_platform}"
+ end
+ result << ""
+ result << " s.required_rubygems_version = #{ruby_code required_rubygems_version} if s.respond_to? :required_rubygems_version="
+
+ if metadata && !metadata.empty?
+ result << " s.metadata = #{ruby_code metadata} if s.respond_to? :metadata="
+ end
+ result << " s.require_paths = #{ruby_code raw_require_paths}"
+
+ handled = [
+ :dependencies,
+ :name,
+ :platform,
+ :require_paths,
+ :required_rubygems_version,
+ :specification_version,
+ :version,
+ :metadata,
+ :signing_key,
+ ]
+
+ @@attributes.each do |attr_name|
+ next if handled.include? attr_name
+ current_value = send(attr_name)
+ if current_value != default_value(attr_name) || self.class.required_attribute?(attr_name)
+ result << " s.#{attr_name} = #{ruby_code current_value}"
+ end
end
- overwrite_accessor :required_ruby_version= do |value|
- @required_ruby_version = Gem::Requirement.create(value)
+ if String === signing_key
+ result << " s.signing_key = #{ruby_code signing_key}"
end
- overwrite_accessor :required_rubygems_version= do |value|
- @required_rubygems_version = Gem::Requirement.create(value)
+ if @installed_by_version
+ result << nil
+ result << " s.installed_by_version = #{ruby_code Gem::VERSION}"
end
- overwrite_accessor :date= do |date|
- # We want to end up with a Time object with one-day resolution.
- # This is the cleanest, most-readable, faster-than-using-Date
- # way to do it.
- case date
- when String then
- @date = if /\A(\d{4})-(\d{2})-(\d{2})\Z/ =~ date then
- Time.local($1.to_i, $2.to_i, $3.to_i)
- else
- require 'time'
- Time.parse date
- end
- when Time then
- @date = Time.local(date.year, date.month, date.day)
- when Date then
- @date = Time.local(date.year, date.month, date.day)
- else
- @date = TODAY
+ unless dependencies.empty?
+ result << nil
+ result << " s.specification_version = #{specification_version}"
+ result << nil
+
+ dependencies.each do |dep|
+ dep.instance_variable_set :@type, :runtime if dep.type.nil? # HACK
+ result << " s.add_#{dep.type}_dependency(%q<#{dep.name}>.freeze, #{ruby_code dep.requirements_list})"
end
end
- overwrite_accessor :date do
- self.date = nil if @date.nil? # HACK Sets the default value for date
- @date
- end
+ result << "end"
+ result << nil
- overwrite_accessor :summary= do |str|
- @summary = if str then
- str.strip.
- gsub(/(\w-)\n[ \t]*(\w)/, '\1\2').
- gsub(/\n[ \t]*/, " ")
- end
- end
+ result.join "\n"
+ end
+
+ ##
+ # Returns a Ruby lighter-weight code representation of this specification,
+ # used for indexing only.
+ #
+ # See #to_ruby.
+
+ def to_ruby_for_cache
+ for_cache.to_ruby
+ end
+
+ def to_s # :nodoc:
+ "#<Gem::Specification name=#{@name} version=#{@version}>"
+ end
+
+ ##
+ # Returns self
+
+ def to_spec
+ self
+ end
+
+ def to_yaml(opts = {}) # :nodoc:
+ Gem.load_yaml
+
+ if Gem.use_psych?
+ # Because the user can switch the YAML engine behind our
+ # back, we have to check again here to make sure that our
+ # psych code was properly loaded, and load it if not.
+ unless Gem.const_defined?(:NoAliasYAMLTree)
+ require_relative "psych_tree"
+ end
+
+ builder = Gem::NoAliasYAMLTree.create
+ builder << self
+ ast = builder.tree
+
+ require "stringio"
+ io = StringIO.new
+ io.set_encoding Encoding::UTF_8
- overwrite_accessor :description= do |str|
- @description = if str then
- str.strip.
- gsub(/(\w-)\n[ \t]*(\w)/, '\1\2').
- gsub(/\n[ \t]*/, " ")
- end
+ Psych::Visitors::Emitter.new(io).accept(ast)
+
+ io.string.gsub(/ !!null \n/, " \n")
+ else
+ Gem::YAMLSerializer.dump(self)
end
+ end
- overwrite_accessor :default_executable do
- begin
- if defined?(@default_executable) and @default_executable
- result = @default_executable
- elsif @executables and @executables.size == 1
- result = Array(@executables).first
- else
- result = nil
+ ##
+ # Recursively walk dependencies of this spec, executing the +block+ for each
+ # hop.
+
+ def traverse(trail = [], visited = {}, &block)
+ trail.push(self)
+ begin
+ runtime_dependencies.each do |dep|
+ dep.matching_specs(true).each do |dep_spec|
+ next if visited.key?(dep_spec)
+ visited[dep_spec] = true
+ trail.push(dep_spec)
+ begin
+ result = block[self, dep, dep_spec, trail]
+ ensure
+ trail.pop
+ end
+ next if result == :next
+ spec_name = dep_spec.name
+ dep_spec.traverse(trail, visited, &block) unless
+ trail.any? {|s| s.name == spec_name }
end
- result
- rescue
- nil
end
+ ensure
+ trail.pop
end
+ end
- overwrite_accessor :test_files do
- # Handle the possibility that we have @test_suite_file but not
- # @test_files. This will happen when an old gem is loaded via
- # YAML.
- if defined? @test_suite_file then
- @test_files = [@test_suite_file].flatten
- @test_suite_file = nil
- end
- if defined?(@test_files) and @test_files then
- @test_files
+ ##
+ # Checks that the specification contains all required fields, and does a
+ # very basic sanity check.
+ #
+ # Raises InvalidSpecificationException if the spec does not pass the
+ # checks..
+
+ def validate(packaging = true, strict = false)
+ normalize
+
+ validation_policy = Gem::SpecificationPolicy.new(self)
+ validation_policy.packaging = packaging
+ validation_policy.validate(strict)
+ end
+
+ def keep_only_files_and_directories
+ @executables.delete_if {|x| File.directory?(File.join(@bindir, x)) }
+ @extensions.delete_if {|x| File.directory?(x) && !File.symlink?(x) }
+ @extra_rdoc_files.delete_if {|x| File.directory?(x) && !File.symlink?(x) }
+ @files.delete_if {|x| File.directory?(x) && !File.symlink?(x) }
+ @test_files.delete_if {|x| File.directory?(x) && !File.symlink?(x) }
+ end
+
+ def validate_for_resolution
+ Gem::SpecificationPolicy.new(self).validate_for_resolution
+ end
+
+ ##
+ # Set the version to +version+.
+
+ def version=(version)
+ @version = version.nil? ? version : Gem::Version.create(version)
+ end
+
+ def stubbed?
+ false
+ end
+
+ def yaml_initialize(tag, vals) # :nodoc:
+ vals.each do |ivar, val|
+ case ivar
+ when "date"
+ # Force Date to go through the extra coerce logic in date=
+ self.date = val
+ when "platform"
+ self.platform = val
+ when "rdoc_options"
+ self.rdoc_options = val
+ when "requirements"
+ self.requirements = val
else
- @test_files = []
+ instance_variable_set "@#{ivar}", val
end
end
+ end
+
+ ##
+ # Reset nil attributes to their default values to make the spec valid
- overwrite_accessor :files do
- result = []
- result.push(*@files) if defined?(@files)
- result.push(*@test_files) if defined?(@test_files)
- result.push(*(add_bindir(@executables)))
- result.push(*@extra_rdoc_files) if defined?(@extra_rdoc_files)
- result.push(*@extensions) if defined?(@extensions)
- result.uniq.compact
+ def reset_nil_attributes_to_default
+ nil_attributes = self.class.non_nil_attributes.find_all do |name|
+ !instance_variable_defined?("@#{name}") || instance_variable_get("@#{name}").nil?
end
+ nil_attributes.each do |attribute|
+ default = default_value attribute
+
+ value = case default
+ when Time, Numeric, Symbol, true, false, nil then default
+ else default.dup
+ end
+
+ instance_variable_set "@#{attribute}", value
+ end
+
+ @installed_by_version ||= nil
+
+ nil
end
-end
+ def flatten_require_paths # :nodoc:
+ return unless raw_require_paths.first.is_a?(Array)
+
+ warn "#{name} #{version} includes a gemspec with `require_paths` set to an array of arrays. Newer versions of this gem might've already fixed this"
+ raw_require_paths.flatten!
+ end
+ def raw_require_paths # :nodoc:
+ @require_paths
+ end
+end
diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb
new file mode 100644
index 0000000000..478e294e09
--- /dev/null
+++ b/lib/rubygems/specification_policy.rb
@@ -0,0 +1,557 @@
+# frozen_string_literal: true
+
+require_relative "user_interaction"
+
+class Gem::SpecificationPolicy
+ include Gem::UserInteraction
+
+ VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc:
+
+ SPECIAL_CHARACTERS = /\A[#{Regexp.escape(".-_")}]+/ # :nodoc:
+
+ VALID_URI_PATTERN = %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z} # :nodoc:
+
+ METADATA_LINK_KEYS = %w[
+ homepage_uri
+ changelog_uri
+ source_code_uri
+ documentation_uri
+ wiki_uri
+ mailing_list_uri
+ bug_tracker_uri
+ download_uri
+ funding_uri
+ ].freeze # :nodoc:
+
+ def initialize(specification)
+ @warnings = 0
+
+ @specification = specification
+ end
+
+ ##
+ # If set to true, run packaging-specific checks, as well.
+
+ attr_accessor :packaging
+
+ ##
+ # Does a sanity check on the specification.
+ #
+ # Raises InvalidSpecificationException if the spec does not pass the
+ # checks.
+ #
+ # It also performs some validations that do not raise but print warning
+ # messages instead.
+
+ def validate(strict = false)
+ validate_required!
+ validate_required_metadata!
+
+ validate_optional(strict) if packaging || strict
+
+ true
+ end
+
+ ##
+ # Does a sanity check on the specification.
+ #
+ # Raises InvalidSpecificationException if the spec does not pass the
+ # checks.
+ #
+ # Only runs checks that are considered necessary for the specification to be
+ # functional.
+
+ def validate_required!
+ validate_nil_attributes
+
+ validate_rubygems_version
+
+ validate_required_attributes
+
+ validate_name
+
+ validate_require_paths
+
+ @specification.keep_only_files_and_directories
+
+ validate_non_files
+
+ validate_self_inclusion_in_files_list
+
+ validate_specification_version
+
+ validate_platform
+
+ validate_array_attributes
+
+ validate_authors_field
+
+ validate_licenses_length
+
+ validate_duplicate_dependencies
+ end
+
+ def validate_required_metadata!
+ validate_metadata
+
+ validate_lazy_metadata
+ end
+
+ def validate_optional(strict)
+ validate_licenses
+
+ validate_permissions
+
+ validate_values
+
+ validate_dependencies
+
+ validate_required_ruby_version
+
+ validate_extensions
+
+ validate_removed_attributes
+
+ validate_unique_links
+
+ if @warnings > 0
+ if strict
+ error "specification has warnings"
+ else
+ alert_warning help_text
+ end
+ end
+ end
+
+ ##
+ # Implementation for Specification#validate_for_resolution
+
+ def validate_for_resolution
+ validate_required!
+ end
+
+ ##
+ # Implementation for Specification#validate_metadata
+
+ def validate_metadata
+ metadata = @specification.metadata
+
+ unless Hash === metadata
+ error "metadata must be a hash"
+ end
+
+ metadata.each do |key, value|
+ entry = "metadata['#{key}']"
+ unless key.is_a?(String)
+ error "metadata keys must be a String"
+ end
+
+ if key.size > 128
+ error "metadata key is too large (#{key.size} > 128)"
+ end
+
+ unless value.is_a?(String)
+ error "#{entry} value must be a String"
+ end
+
+ if value.size > 1024
+ error "#{entry} value is too large (#{value.size} > 1024)"
+ end
+
+ next unless METADATA_LINK_KEYS.include? key
+ unless VALID_URI_PATTERN.match?(value)
+ error "#{entry} has invalid link: #{value.inspect}"
+ end
+ end
+ end
+
+ ##
+ # Checks that no duplicate dependencies are specified.
+
+ def validate_duplicate_dependencies # :nodoc:
+ # NOTE: see REFACTOR note in Gem::Dependency about types - this might be brittle
+ seen = Gem::Dependency::TYPES.inject({}) {|types, type| types.merge({ type => {} }) }
+
+ error_messages = []
+ @specification.dependencies.each do |dep|
+ if prev = seen[dep.type][dep.name]
+ error_messages << <<-MESSAGE
+duplicate dependency on #{dep}, (#{prev.requirement}) use:
+ add_#{dep.type}_dependency \"#{dep.name}\", \"#{dep.requirement}\", \"#{prev.requirement}\"
+ MESSAGE
+ end
+
+ seen[dep.type][dep.name] = dep
+ end
+ if error_messages.any?
+ error error_messages.join
+ end
+ end
+
+ ##
+ # Checks that the gem does not depend on itself.
+
+ def validate_dependencies # :nodoc:
+ error_messages = []
+ @specification.dependencies.each do |dep|
+ if dep.name == @specification.name # error on self reference
+ error_messages << "Dependencies of this gem include a self-reference."
+ end
+ end
+
+ error error_messages.join if error_messages.any?
+ end
+
+ def validate_required_ruby_version
+ if @specification.required_ruby_version.requirements == [Gem::Requirement::DefaultRequirement]
+ warning "make sure you specify the oldest ruby version constraint (like \">= 3.0\") that you want your gem to support by setting the `required_ruby_version` gemspec attribute"
+ end
+ end
+
+ ##
+ # Issues a warning for each file to be packaged which is world-readable.
+ #
+ # Implementation for Specification#validate_permissions
+
+ def validate_permissions
+ return if Gem.win_platform?
+
+ @specification.files.each do |file|
+ next unless File.file?(file)
+ next if File.stat(file).mode & 0o444 == 0o444
+ warning "#{file} is not world-readable"
+ end
+
+ @specification.executables.each do |name|
+ exec = File.join @specification.bindir, name
+ next unless File.file?(exec)
+ next if File.stat(exec).executable?
+ warning "#{exec} is not executable"
+ end
+ end
+
+ private
+
+ def validate_nil_attributes
+ nil_attributes = Gem::Specification.non_nil_attributes.select do |attrname|
+ @specification.instance_variable_get("@#{attrname}").nil?
+ end
+ return if nil_attributes.empty?
+ error "#{nil_attributes.join ", "} must not be nil"
+ end
+
+ def validate_rubygems_version
+ return unless packaging
+
+ rubygems_version = @specification.rubygems_version
+
+ return if rubygems_version == Gem::VERSION
+
+ warning "expected RubyGems version #{Gem::VERSION}, was #{rubygems_version}"
+
+ @specification.rubygems_version = Gem::VERSION
+ end
+
+ def validate_required_attributes
+ Gem::Specification.required_attributes.each do |symbol|
+ unless @specification.send symbol
+ error "missing value for attribute #{symbol}"
+ end
+ end
+ end
+
+ def validate_name
+ name = @specification.name
+
+ if !name.is_a?(String)
+ error "invalid value for attribute name: \"#{name.inspect}\" must be a string"
+ elsif !/[a-zA-Z]/.match?(name)
+ error "invalid value for attribute name: #{name.dump} must include at least one letter"
+ elsif !VALID_NAME_PATTERN.match?(name)
+ error "invalid value for attribute name: #{name.dump} can only include letters, numbers, dashes, and underscores"
+ elsif SPECIAL_CHARACTERS.match?(name)
+ error "invalid value for attribute name: #{name.dump} cannot begin with a period, dash, or underscore"
+ end
+ end
+
+ def validate_require_paths
+ return unless @specification.raw_require_paths.empty?
+
+ error "specification must have at least one require_path"
+ end
+
+ def validate_non_files
+ return unless packaging
+
+ non_files = @specification.files.reject {|x| File.file?(x) || File.symlink?(x) }
+
+ unless non_files.empty?
+ error "[\"#{non_files.join "\", \""}\"] are not files"
+ end
+ end
+
+ def validate_self_inclusion_in_files_list
+ file_name = @specification.file_name
+
+ return unless @specification.files.include?(file_name)
+
+ error "#{@specification.full_name} contains itself (#{file_name}), check your files list"
+ end
+
+ def validate_specification_version
+ return if @specification.specification_version.is_a?(Integer)
+
+ error "specification_version must be an Integer (did you mean version?)"
+ end
+
+ def validate_platform
+ platform = @specification.platform
+
+ case platform
+ when Gem::Platform, Gem::Platform::RUBY # ok
+ else
+ error "invalid platform #{platform.inspect}, see Gem::Platform"
+ end
+ end
+
+ def validate_array_attributes
+ Gem::Specification.array_attributes.each do |field|
+ validate_array_attribute(field)
+ end
+ end
+
+ def validate_array_attribute(field)
+ val = @specification.send(field)
+ klass = case field
+ when :dependencies then
+ Gem::Dependency
+ else
+ String
+ end
+
+ unless Array === val && val.all? {|x| x.is_a?(klass) || (field == :licenses && x.nil?) }
+ error "#{field} must be an Array of #{klass}"
+ end
+ end
+
+ def validate_authors_field
+ return unless @specification.authors.empty?
+
+ error "authors may not be empty"
+ end
+
+ def validate_licenses_length
+ licenses = @specification.licenses
+
+ licenses.each do |license|
+ next if license.nil?
+
+ if license.length > 64
+ error "each license must be 64 characters or less"
+ end
+ end
+ end
+
+ def validate_licenses
+ licenses = @specification.licenses
+
+ licenses.each do |license|
+ next if Gem::Licenses.match?(license) || license.nil?
+ license_id_deprecated = Gem::Licenses.deprecated_license_id?(license)
+ exception_id_deprecated = Gem::Licenses.deprecated_exception_id?(license)
+ suggestions = Gem::Licenses.suggestions(license)
+
+ if license_id_deprecated
+ main_message = "License identifier '#{license}' is deprecated"
+ elsif exception_id_deprecated
+ main_message = "Exception identifier at '#{license}' is deprecated"
+ else
+ main_message = "License identifier '#{license}' is invalid"
+ end
+
+ message = <<-WARNING
+#{main_message}. Use an identifier from
+https://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license,
+or set it to nil if you don't want to specify a license.
+ WARNING
+ message += "Did you mean #{suggestions.map {|s| "'#{s}'" }.join(", ")}?\n" unless suggestions.nil?
+ warning(message)
+ end
+
+ warning <<-WARNING if licenses.empty?
+licenses is empty, but is recommended. Use an license identifier from
+https://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license,
+or set it to nil if you don't want to specify a license.
+ WARNING
+ end
+
+ LAZY = '"FIxxxXME" or "TOxxxDO"'.gsub(/xxx/, "")
+ LAZY_PATTERN = /\AFI XME|\ATO DO/x
+ HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i
+
+ def validate_lazy_metadata
+ unless @specification.authors.grep(LAZY_PATTERN).empty?
+ error "#{LAZY} is not an author"
+ end
+
+ unless Array(@specification.email).grep(LAZY_PATTERN).empty?
+ error "#{LAZY} is not an email"
+ end
+
+ if LAZY_PATTERN.match?(@specification.description)
+ error "#{LAZY} is not a description"
+ end
+
+ if LAZY_PATTERN.match?(@specification.summary)
+ error "#{LAZY} is not a summary"
+ end
+
+ homepage = @specification.homepage
+
+ # Make sure a homepage is valid HTTP/HTTPS URI
+ if homepage && !homepage.empty?
+ require_relative "vendor/uri/lib/uri"
+ begin
+ homepage_uri = Gem::URI.parse(homepage)
+ unless [Gem::URI::HTTP, Gem::URI::HTTPS].member? homepage_uri.class
+ error "\"#{homepage}\" is not a valid HTTP URI"
+ end
+ rescue Gem::URI::InvalidURIError
+ error "\"#{homepage}\" is not a valid HTTP URI"
+ end
+ end
+ end
+
+ def validate_values
+ %w[author homepage summary files].each do |attribute|
+ validate_attribute_present(attribute)
+ end
+
+ if @specification.description == @specification.summary
+ warning "description and summary are identical"
+ end
+
+ # TODO: raise at some given date
+ warning "deprecated autorequire specified" if @specification.autorequire
+
+ @specification.executables.each do |executable|
+ validate_executable(executable)
+ validate_shebang_line_in(executable)
+ end
+
+ @specification.files.select {|f| File.symlink?(f) }.each do |file|
+ warning "#{file} is a symlink, which is not supported on all platforms"
+ end
+ end
+
+ def validate_attribute_present(attribute)
+ value = @specification.send attribute
+ warning("no #{attribute} specified") if value.nil? || value.empty?
+ end
+
+ def validate_executable(executable)
+ separators = [File::SEPARATOR, File::ALT_SEPARATOR, File::PATH_SEPARATOR].compact.map {|sep| Regexp.escape(sep) }.join
+ return unless executable.match?(/[\s#{separators}]/)
+
+ error "executable \"#{executable}\" contains invalid characters"
+ end
+
+ def validate_shebang_line_in(executable)
+ executable_path = File.join(@specification.bindir, executable)
+ return if File.read(executable_path, 2) == "#!"
+
+ warning "#{executable_path} is missing #! line"
+ end
+
+ def validate_removed_attributes # :nodoc:
+ @specification.removed_method_calls.each do |attr|
+ warning("#{attr} is deprecated and ignored. Please remove this from your gemspec to ensure that your gem continues to build in the future.")
+ end
+ end
+
+ def validate_extensions # :nodoc:
+ require_relative "ext"
+ builder = Gem::Ext::Builder.new(@specification)
+
+ validate_rake_extensions(builder)
+ validate_rust_extensions(builder)
+ validate_extension_require_relative
+ end
+
+ def validate_rust_extensions(builder) # :nodoc:
+ rust_extension = @specification.extensions.any? {|s| builder.builder_for(s).is_a? Gem::Ext::CargoBuilder }
+ missing_cargo_lock = !@specification.files.any? {|f| f.end_with?("Cargo.lock") }
+
+ error <<-ERROR if rust_extension && missing_cargo_lock
+You have specified rust based extension, but Cargo.lock is not part of the gem files. Please run `cargo generate-lockfile` or any other command to generate Cargo.lock and ensure it is added to your gem files section in gemspec.
+ ERROR
+ end
+
+ def validate_rake_extensions(builder) # :nodoc:
+ rake_extension = @specification.extensions.any? {|s| builder.builder_for(s) == Gem::Ext::RakeBuilder }
+ rake_dependency = @specification.dependencies.any? {|d| d.name == "rake" && d.type == :runtime }
+
+ warning <<-WARNING if rake_extension && !rake_dependency
+You have specified rake based extension, but rake is not added as runtime dependency. It is recommended to add rake as a runtime dependency in gemspec since there's no guarantee rake will be already installed.
+ WARNING
+ end
+
+ def validate_extension_require_relative # :nodoc:
+ return unless @specification.extensions.any?
+
+ require_paths = @specification.require_paths
+
+ @specification.files.each do |rb_file|
+ next unless rb_file.end_with?(".rb")
+ next unless require_paths.any? {|rp| rb_file.start_with?("#{rp}/") }
+ next unless File.file?(rb_file)
+
+ File.foreach(rb_file).with_index(1) do |line, lineno|
+ next unless line =~ /^\s*require_relative\s+["']([^"']+)["']/
+
+ required_path = Regexp.last_match(1)
+ resolved = File.join(File.dirname(rb_file), required_path)
+
+ next if @specification.files.any? {|f| f == "#{resolved}.rb" || f == resolved }
+
+ warning <<~WARNING
+ #{rb_file}:#{lineno} uses `require_relative "#{required_path}"` to load a compiled extension.
+ This will break in RubyGems 4.2, which will stop copying compiled extensions into the gem's lib directory.
+ Use `require` instead of `require_relative` to load compiled extensions.
+ WARNING
+ end
+ end
+ end
+
+ def validate_unique_links
+ links = @specification.metadata.slice(*METADATA_LINK_KEYS)
+ grouped = links.group_by {|_key, uri| uri }
+ grouped.each do |uri, copies|
+ next unless copies.length > 1
+ keys = copies.map(&:first).join("\n ")
+ warning <<~WARNING
+ You have specified the uri:
+ #{uri}
+ for all of the following keys:
+ #{keys}
+ Only the first one will be shown on rubygems.org
+ WARNING
+ end
+ end
+
+ def warning(statement) # :nodoc:
+ @warnings += 1
+
+ alert_warning statement
+ end
+
+ def error(statement) # :nodoc:
+ raise Gem::InvalidSpecificationException, statement
+ ensure
+ alert_warning help_text
+ end
+
+ def help_text # :nodoc:
+ "See https://guides.rubygems.org/specification-reference/ for help"
+ end
+end
diff --git a/lib/rubygems/specification_record.rb b/lib/rubygems/specification_record.rb
new file mode 100644
index 0000000000..c7e5cbedb5
--- /dev/null
+++ b/lib/rubygems/specification_record.rb
@@ -0,0 +1,225 @@
+# frozen_string_literal: true
+
+module Gem
+ class SpecificationRecord
+ def self.dirs_from(paths)
+ paths.map do |path|
+ File.join(path, "specifications")
+ end
+ end
+
+ def self.from_path(path)
+ new(dirs_from([path]))
+ end
+
+ def initialize(dirs)
+ @all = nil
+ @stubs = nil
+ @stubs_by_name = {}
+ @spec_with_requirable_file = {}
+ @active_stub_with_requirable_file = {}
+
+ @dirs = dirs
+ end
+
+ # Sentinel object to represent "not found" stubs
+ NOT_FOUND = Struct.new(:to_spec, :this).new
+ private_constant :NOT_FOUND
+
+ ##
+ # Returns the list of all specifications in the record
+
+ def all
+ @all ||= stubs.map(&:to_spec)
+ end
+
+ ##
+ # Returns a Gem::StubSpecification for every specification in the record
+
+ def stubs
+ @stubs ||= begin
+ pattern = "*.gemspec"
+ stubs = stubs_for_pattern(pattern, false)
+
+ @stubs_by_name = stubs.select {|s| Gem::Platform.match_spec? s }.group_by(&:name)
+ stubs
+ end
+ end
+
+ ##
+ # Returns a Gem::StubSpecification for every specification in the record
+ # named +name+ only returns stubs that match Gem.platforms
+
+ def stubs_for(name)
+ if @stubs
+ @stubs_by_name[name] || []
+ else
+ @stubs_by_name[name] ||= stubs_for_pattern("#{name}-*.gemspec").select do |s|
+ s.name == name
+ end
+ end
+ end
+
+ ##
+ # Finds stub specifications matching a pattern in the record, optionally
+ # filtering out specs not matching the current platform
+
+ def stubs_for_pattern(pattern, match_platform = true)
+ installed_stubs = installed_stubs(pattern)
+ installed_stubs.select! {|s| Gem::Platform.match_spec? s } if match_platform
+ stubs = installed_stubs + Gem::Specification.default_stubs(pattern)
+ Gem::Specification._resort!(stubs)
+ stubs
+ end
+
+ ##
+ # Adds +spec+ to the record, keeping the collection properly sorted.
+
+ def add_spec(spec)
+ return if all.include? spec
+
+ all << spec
+ stubs << spec
+ (@stubs_by_name[spec.name] ||= []) << spec
+
+ Gem::Specification._resort!(@stubs_by_name[spec.name])
+ Gem::Specification._resort!(stubs)
+ end
+
+ ##
+ # Removes +spec+ from the record.
+
+ def remove_spec(spec)
+ all.delete spec.to_spec
+ stubs.delete spec
+ (@stubs_by_name[spec.name] || []).delete spec
+ end
+
+ ##
+ # Sets the specs known by the record to +specs+.
+
+ def all=(specs)
+ @stubs_by_name = specs.group_by(&:name)
+ @all = @stubs = specs
+ end
+
+ ##
+ # Return full names of all specs in the record in sorted order.
+
+ def all_names
+ all.map(&:full_name)
+ end
+
+ include Enumerable
+
+ ##
+ # Enumerate every known spec.
+
+ def each
+ return enum_for(:each) unless block_given?
+
+ all.each do |x|
+ yield x
+ end
+ end
+
+ ##
+ # Returns every spec in the record that matches +name+ and optional +requirements+.
+
+ def find_all_by_name(name, *requirements)
+ req = Gem::Requirement.create(*requirements)
+ env_req = Gem.env_requirement(name)
+
+ matches = stubs_for(name).find_all do |spec|
+ req.satisfied_by?(spec.version) && env_req.satisfied_by?(spec.version)
+ end.map(&:to_spec)
+
+ if name == "bundler" && !req.specific?
+ require_relative "bundler_version_finder"
+ Gem::BundlerVersionFinder.prioritize!(matches)
+ end
+
+ matches
+ end
+
+ ##
+ # Return the best specification in the record that contains the file matching +path+.
+
+ def find_by_path(path)
+ path = path.dup.freeze
+ spec = @spec_with_requirable_file[path] ||= stubs.find do |s|
+ s.contains_requirable_file? path
+ end || NOT_FOUND
+
+ spec.to_spec
+ end
+
+ ##
+ # Return the best specification that contains the file matching +path+
+ # amongst the specs that are not loaded. This method is different than
+ # +find_inactive_by_path+ as it will filter out loaded specs by their name.
+
+ def find_unloaded_by_path(path)
+ stub = stubs.find do |s|
+ next if Gem.loaded_specs[s.name]
+ s.contains_requirable_file? path
+ end
+ stub&.to_spec
+ end
+
+ ##
+ # Return the best specification in the record that contains the file
+ # matching +path+ amongst the specs that are not activated.
+
+ def find_inactive_by_path(path)
+ stub = stubs.find do |s|
+ next if s.activated?
+ s.contains_requirable_file? path
+ end
+ stub&.to_spec
+ end
+
+ ##
+ # Return the best specification in the record that contains the file
+ # matching +path+, among those already activated.
+
+ def find_active_stub_by_path(path)
+ stub = @active_stub_with_requirable_file[path] ||= stubs.find do |s|
+ s.activated? && s.contains_requirable_file?(path)
+ end || NOT_FOUND
+
+ stub.this
+ end
+
+ ##
+ # Return the latest specs in the record, optionally including prerelease
+ # specs if +prerelease+ is true.
+
+ def latest_specs(prerelease)
+ Gem::Specification._latest_specs stubs, prerelease
+ end
+
+ ##
+ # Return the latest installed spec in the record for gem +name+.
+
+ def latest_spec_for(name)
+ latest_specs(true).find {|installed_spec| installed_spec.name == name }
+ end
+
+ private
+
+ def installed_stubs(pattern)
+ map_stubs(pattern) do |path, base_dir, gems_dir|
+ Gem::StubSpecification.gemspec_stub(path, base_dir, gems_dir)
+ end
+ end
+
+ def map_stubs(pattern)
+ @dirs.flat_map do |dir|
+ base_dir = File.dirname dir
+ gems_dir = File.join base_dir, "gems"
+ Gem::Specification.gemspec_stubs_in(dir, pattern) {|path| yield path, base_dir, gems_dir }
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/ssl_certs/.document b/lib/rubygems/ssl_certs/.document
new file mode 100644
index 0000000000..fb66f13c33
--- /dev/null
+++ b/lib/rubygems/ssl_certs/.document
@@ -0,0 +1 @@
+# Ignore all files in this directory
diff --git a/lib/rubygems/ssl_certs/rubygems.org/GlobalSign.pem b/lib/rubygems/ssl_certs/rubygems.org/GlobalSign.pem
new file mode 100644
index 0000000000..8afb219058
--- /dev/null
+++ b/lib/rubygems/ssl_certs/rubygems.org/GlobalSign.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G
+A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp
+Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4
+MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG
+A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8
+RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT
+gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm
+KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd
+QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ
+XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw
+DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o
+LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU
+RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp
+jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK
+6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX
+mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs
+Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH
+WD9f
+-----END CERTIFICATE-----
diff --git a/lib/rubygems/stub_specification.rb b/lib/rubygems/stub_specification.rb
new file mode 100644
index 0000000000..53b337ed85
--- /dev/null
+++ b/lib/rubygems/stub_specification.rb
@@ -0,0 +1,236 @@
+# frozen_string_literal: true
+
+##
+# Gem::StubSpecification reads the stub: line from the gemspec. This prevents
+# us having to eval the entire gemspec in order to find out certain
+# information.
+
+class Gem::StubSpecification < Gem::BasicSpecification
+ # :nodoc:
+ PREFIX = "# stub: "
+
+ # :nodoc:
+ OPEN_MODE = "r:UTF-8:-"
+
+ class StubLine # :nodoc: all
+ attr_reader :name, :version, :platform, :require_paths, :extensions,
+ :full_name
+
+ NO_EXTENSIONS = [].freeze
+
+ # These are common require paths.
+ REQUIRE_PATHS = { # :nodoc:
+ "lib" => "lib",
+ "test" => "test",
+ "ext" => "ext",
+ }.freeze
+
+ # These are common require path lists. This hash is used to optimize
+ # and consolidate require_path objects. Most specs just specify "lib"
+ # in their require paths, so lets take advantage of that by pre-allocating
+ # a require path list for that case.
+ REQUIRE_PATH_LIST = { # :nodoc:
+ "lib" => ["lib"].freeze,
+ }.freeze
+
+ def initialize(data, extensions)
+ parts = data[PREFIX.length..-1].split(" ", 4)
+ @name = -parts[0]
+ @version = if Gem::Version.correct?(parts[1])
+ Gem::Version.new(parts[1])
+ else
+ Gem::Version.new(0)
+ end
+
+ @platform = Gem::Platform.new parts[2]
+ @extensions = extensions
+ @full_name = if platform == Gem::Platform::RUBY
+ "#{name}-#{version}"
+ else
+ "#{name}-#{version}-#{platform}"
+ end
+
+ path_list = parts.last
+ @require_paths = REQUIRE_PATH_LIST[path_list] || path_list.split("\0").map! do |x|
+ REQUIRE_PATHS[x] || x
+ end
+ end
+ end
+
+ def self.default_gemspec_stub(filename, base_dir, gems_dir)
+ new filename, base_dir, gems_dir, true
+ end
+
+ def self.gemspec_stub(filename, base_dir, gems_dir)
+ new filename, base_dir, gems_dir, false
+ end
+
+ attr_reader :base_dir, :gems_dir
+
+ def initialize(filename, base_dir, gems_dir, default_gem)
+ super()
+
+ self.loaded_from = filename
+ @data = nil
+ @name = nil
+ @spec = nil
+ @base_dir = base_dir
+ @gems_dir = gems_dir
+ @default_gem = default_gem
+ end
+
+ ##
+ # True when this gem has been activated
+
+ def activated?
+ @activated ||= !loaded_spec.nil?
+ end
+
+ def default_gem?
+ @default_gem
+ end
+
+ def build_extensions # :nodoc:
+ return if default_gem?
+ return if extensions.empty?
+
+ to_spec.build_extensions
+ end
+
+ ##
+ # If the gemspec contains a stubline, returns a StubLine instance. Otherwise
+ # returns the full Gem::Specification.
+
+ def data
+ unless @data
+ begin
+ saved_lineno = $.
+
+ Gem.open_file loaded_from, OPEN_MODE do |file|
+ file.readline # discard encoding line
+ stubline = file.readline
+ if stubline.start_with?(PREFIX)
+ extline = file.readline
+
+ extensions =
+ if extline.delete_prefix!(PREFIX)
+ extline.chomp!
+ extline.split "\0"
+ else
+ StubLine::NO_EXTENSIONS
+ end
+
+ stubline.chomp! # readline(chomp: true) allocates 3x as much as .readline.chomp!
+ @data = StubLine.new stubline, extensions
+ end
+ rescue EOFError
+ end
+ ensure
+ $. = saved_lineno
+ end
+ end
+
+ @data ||= to_spec
+ end
+
+ private :data
+
+ def raw_require_paths # :nodoc:
+ data.require_paths
+ end
+
+ def missing_extensions?
+ return false if RUBY_ENGINE == "jruby"
+ return false if default_gem?
+ return false if extensions.empty?
+ return false if File.exist? gem_build_complete_path
+
+ to_spec.missing_extensions?
+ end
+
+ ##
+ # Name of the gem
+
+ def name
+ data.name
+ end
+
+ ##
+ # Platform of the gem
+
+ def platform
+ data.platform
+ end
+
+ ##
+ # Extensions for this gem
+
+ def extensions
+ data.extensions
+ end
+
+ ##
+ # Version of the gem
+
+ def version
+ data.version
+ end
+
+ def full_name
+ data.full_name
+ end
+
+ ##
+ # The full Gem::Specification for this gem, loaded from evalling its gemspec
+
+ def spec
+ @spec ||= loaded_spec if @data
+ @spec ||= Gem::Specification.load(loaded_from)
+ end
+ alias_method :to_spec, :spec
+
+ ##
+ # Is this StubSpecification valid? i.e. have we found a stub line, OR does
+ # the filename contain a valid gemspec?
+
+ def valid?
+ data
+ end
+
+ ##
+ # Is there a stub line present for this StubSpecification?
+
+ def stubbed?
+ data.is_a? StubLine
+ end
+
+ def ==(other) # :nodoc:
+ self.class === other &&
+ name == other.name &&
+ version == other.version &&
+ platform == other.platform
+ end
+
+ alias_method :eql?, :== # :nodoc:
+
+ def hash # :nodoc:
+ name.hash ^ version.hash ^ platform.hash
+ end
+
+ def <=>(other) # :nodoc:
+ sort_obj <=> other.sort_obj
+ end
+
+ def sort_obj # :nodoc:
+ [name, version, Gem::Platform.sort_priority(platform)]
+ end
+
+ private
+
+ def loaded_spec
+ spec = Gem.loaded_specs[name]
+ return unless spec && spec.version == version && spec.default_gem? == default_gem?
+
+ spec
+ end
+end
diff --git a/lib/rubygems/target_rbconfig.rb b/lib/rubygems/target_rbconfig.rb
new file mode 100644
index 0000000000..21d90ee9db
--- /dev/null
+++ b/lib/rubygems/target_rbconfig.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require "rbconfig"
+
+##
+# A TargetConfig is a wrapper around an RbConfig object that provides a
+# consistent interface for querying configuration for *deployment target
+# platform*, where the gem being installed is intended to run on.
+#
+# The TargetConfig is typically created from the RbConfig of the running Ruby
+# process, but can also be created from an RbConfig file on disk for cross-
+# compiling gems.
+
+class Gem::TargetRbConfig
+ attr_reader :path
+
+ def initialize(rbconfig, path)
+ @rbconfig = rbconfig
+ @path = path
+ end
+
+ ##
+ # Creates a TargetRbConfig for the platform that RubyGems is running on.
+
+ def self.for_running_ruby
+ new(::RbConfig, nil)
+ end
+
+ ##
+ # Creates a TargetRbConfig from the RbConfig file at the given path.
+ # Typically used for cross-compiling gems.
+
+ def self.from_path(rbconfig_path)
+ namespace = Module.new do |m|
+ # Load the rbconfig.rb file within a new anonymous module to avoid
+ # conflicts with the rbconfig for the running platform.
+ Kernel.load rbconfig_path, m
+ end
+ rbconfig = namespace.const_get(:RbConfig)
+
+ new(rbconfig, rbconfig_path)
+ end
+
+ ##
+ # Queries the configuration for the given key.
+
+ def [](key)
+ @rbconfig::CONFIG[key]
+ end
+end
diff --git a/lib/rubygems/test_utilities.rb b/lib/rubygems/test_utilities.rb
deleted file mode 100644
index 8b23d3236e..0000000000
--- a/lib/rubygems/test_utilities.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-require 'tempfile'
-require 'rubygems'
-require 'rubygems/remote_fetcher'
-
-##
-# A fake Gem::RemoteFetcher for use in tests or to avoid real live HTTP
-# requests when testing code that uses RubyGems.
-#
-# Example:
-#
-# @fetcher = Gem::FakeFetcher.new
-# @fetcher.data['http://gems.example.com/yaml'] = source_index.to_yaml
-# Gem::RemoteFetcher.fetcher = @fetcher
-#
-# # invoke RubyGems code
-#
-# paths = @fetcher.paths
-# assert_equal 'http://gems.example.com/yaml', paths.shift
-# assert paths.empty?, paths.join(', ')
-#
-# See RubyGems' tests for more examples of FakeFetcher.
-
-class Gem::FakeFetcher
-
- attr_reader :data
- attr_accessor :paths
-
- def initialize
- @data = {}
- @paths = []
- end
-
- def fetch_path path, mtime = nil
- path = path.to_s
- @paths << path
- raise ArgumentError, 'need full URI' unless path =~ %r'^http://'
-
- unless @data.key? path then
- raise Gem::RemoteFetcher::FetchError.new("no data for #{path}", path)
- end
-
- data = @data[path]
-
- if data.respond_to?(:call) then
- data.call
- else
- if path.to_s =~ /gz$/ and not data.nil? and not data.empty? then
- data = Gem.gunzip data
- end
-
- data
- end
- end
-
- def fetch_size(path)
- path = path.to_s
- @paths << path
-
- raise ArgumentError, 'need full URI' unless path =~ %r'^http://'
-
- unless @data.key? path then
- raise Gem::RemoteFetcher::FetchError.new("no data for #{path}", path)
- end
-
- data = @data[path]
-
- data.respond_to?(:call) ? data.call : data.length
- end
-
- def download spec, source_uri, install_dir = Gem.dir
- name = "#{spec.full_name}.gem"
- path = File.join(install_dir, 'cache', name)
-
- Gem.ensure_gem_subdirectories install_dir
-
- if source_uri =~ /^http/ then
- File.open(path, "wb") do |f|
- f.write fetch_path(File.join(source_uri, "gems", name))
- end
- else
- FileUtils.cp source_uri, path
- end
-
- path
- end
-
-end
-
-# :stopdoc:
-class Gem::RemoteFetcher
-
- def self.fetcher=(fetcher)
- @fetcher = fetcher
- end
-
-end
-# :startdoc:
-
-##
-# A StringIO duck-typed class that uses Tempfile instead of String as the
-# backing store.
-#--
-# This class was added to flush out problems in Rubinius' IO implementation.
-
-class TempIO
-
- @@count = 0
-
- def initialize(string = '')
- @tempfile = Tempfile.new "TempIO-#{@@count += 1}"
- @tempfile.binmode
- @tempfile.write string
- @tempfile.rewind
- end
-
- def method_missing(meth, *args, &block)
- @tempfile.send(meth, *args, &block)
- end
-
- def respond_to?(meth)
- @tempfile.respond_to? meth
- end
-
- def string
- @tempfile.flush
-
- Gem.read_binary @tempfile.path
- end
-
-end
-
diff --git a/lib/rubygems/text.rb b/lib/rubygems/text.rb
new file mode 100644
index 0000000000..0550dc473d
--- /dev/null
+++ b/lib/rubygems/text.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+##
+# A collection of text-wrangling methods
+
+module Gem::Text
+ ##
+ # Remove any non-printable characters and make the text suitable for
+ # printing.
+ def clean_text(text)
+ text = text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".")
+
+ # Match C1 control characters (U+0080-U+009F) as codepoints. This requires
+ # a valid UTF-8 string so the regexp does not split a multibyte sequence;
+ # strings in other encodings are left unchanged.
+ if text.encoding == Encoding::UTF_8 && text.valid_encoding?
+ text = text.gsub(/[\u0080-\u009f]/, ".")
+ end
+
+ text
+ end
+
+ def truncate_text(text, description, max_length = 100_000)
+ raise ArgumentError, "max_length must be positive" unless max_length > 0
+ return text if text.size <= max_length
+ "Truncating #{description} to #{max_length.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse} characters:\n" + text[0, max_length]
+ end
+
+ ##
+ # Wraps +text+ to +wrap+ characters and optionally indents by +indent+
+ # characters
+
+ def format_text(text, wrap, indent = 0)
+ result = []
+ work = clean_text(text)
+
+ while work.length > wrap do
+ if work =~ /^(.{0,#{wrap}})[ \n]/
+ result << $1.rstrip
+ work.slice!(0, $&.length)
+ else
+ result << work.slice!(0, wrap)
+ end
+ end
+
+ result << work if work.length.nonzero?
+ result.join("\n").gsub(/^/, " " * indent)
+ end
+
+ def min3(a, b, c) # :nodoc:
+ if a < b && a < c
+ a
+ elsif b < c
+ b
+ else
+ c
+ end
+ end
+
+ # Returns a value representing the "cost" of transforming str1 into str2
+ # Vendored version of DidYouMean::Levenshtein.distance from the ruby/did_you_mean gem @ 1.4.0
+ # https://github.com/ruby/did_you_mean/blob/2ddf39b874808685965dbc47d344cf6c7651807c/lib/did_you_mean/levenshtein.rb#L7-L37
+ def levenshtein_distance(str1, str2)
+ n = str1.length
+ m = str2.length
+ return m if n.zero?
+ return n if m.zero?
+
+ d = (0..m).to_a
+ x = nil
+
+ # to avoid duplicating an enumerable object, create it outside of the loop
+ str2_codepoints = str2.codepoints
+
+ str1.each_codepoint.with_index(1) do |char1, i|
+ j = 0
+ while j < m
+ cost = char1 == str2_codepoints[j] ? 0 : 1
+ x = min3(
+ d[j + 1] + 1, # insertion
+ i + 1, # deletion
+ d[j] + cost # substitution
+ )
+ d[j] = i
+ i = x
+
+ j += 1
+ end
+ d[m] = x
+ end
+
+ x
+ end
+end
diff --git a/lib/rubygems/timer.rb b/lib/rubygems/timer.rb
deleted file mode 100644
index 06250f26b5..0000000000
--- a/lib/rubygems/timer.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-#
-# This file defines a $log variable for logging, and a time() method for recording timing
-# information.
-#
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-
-$log = Object.new
-def $log.debug(str)
- STDERR.puts str
-end
-
-def time(msg, width=25)
- t = Time.now
- return_value = yield
- elapsed = Time.now.to_f - t.to_f
- elapsed = sprintf("%3.3f", elapsed)
- $log.debug "#{msg.ljust(width)}: #{elapsed}s"
- return_value
-end
-
diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb
index 5f19da5e82..fe4c3a80cf 100644
--- a/lib/rubygems/uninstaller.rb
+++ b/lib/rubygems/uninstaller.rb
@@ -1,29 +1,37 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'fileutils'
-require 'rubygems'
-require 'rubygems/dependency_list'
-require 'rubygems/doc_manager'
-require 'rubygems/user_interaction'
+require "fileutils"
+require_relative "../rubygems"
+require_relative "installer_uninstaller_utils"
+require_relative "dependency_list"
+require_relative "user_interaction"
##
# An Uninstaller.
+#
+# The uninstaller fires pre and post uninstall hooks. Hooks can be added
+# either through a rubygems_plugin.rb file in an installed gem or via a
+# rubygems/defaults/#{RUBY_ENGINE}.rb or rubygems/defaults/operating_system.rb
+# file. See Gem.pre_uninstall and Gem.post_uninstall for details.
class Gem::Uninstaller
-
include Gem::UserInteraction
+ include Gem::InstallerUninstallerUtils
+
##
# The directory a gem's executables will be installed into
attr_reader :bin_dir
##
- # The gem repository the gem will be installed into
+ # The gem repository the gem will be uninstalled from
attr_reader :gem_home
@@ -34,20 +42,49 @@ class Gem::Uninstaller
attr_reader :spec
##
- # Constructs an uninstaller that will uninstall +gem+
+ # Constructs an uninstaller that will uninstall gem named +gem+.
+ # +options+ is a Hash with the following keys:
+ #
+ # :version:: Version requirement for the gem to uninstall. If not specified,
+ # uses Gem::Requirement.default.
+ # :install_dir:: The directory where the gem is installed. If not specified,
+ # uses Gem.dir.
+ # :executables:: Whether executables should be removed without confirmation or not. If nil, asks the user explicitly.
+ # :all:: If more than one version matches the requirement, whether to forcefully remove all matching versions or ask the user to select specific matching versions that should be removed.
+ # :ignore:: Ignore broken dependency checks when uninstalling.
+ # :bin_dir:: Directory containing executables to remove. If not specified,
+ # uses Gem.bindir.
+ # :format_executable:: In order to find executables to be removed, format executable names using Gem::Installer.exec_format.
+ # :abort_on_dependent:: Directly abort uninstallation if dependencies would be broken, rather than asking the user for confirmation.
+ # :check_dev:: When checking if uninstalling gem would leave broken dependencies around, also consider development dependencies.
+ # :force:: Set both :all and :ignore to true for forced uninstallation.
+ # :user_install:: Uninstall from user gem directory instead of system directory.
def initialize(gem, options = {})
- @gem = gem
- @version = options[:version] || Gem::Requirement.default
- gem_home = options[:install_dir] || Gem.dir
- @gem_home = File.expand_path gem_home
- @force_executables = options[:executables]
- @force_all = options[:all]
- @force_ignore = options[:ignore]
- @bin_dir = options[:bin_dir]
+ @gem = gem
+ @version = options[:version] || Gem::Requirement.default
+ @install_dir = options[:install_dir]
+ @gem_home = File.realpath(@install_dir || Gem.dir)
+ @user_dir = File.exist?(Gem.user_dir) ? File.realpath(Gem.user_dir) : Gem.user_dir
+ @force_executables = options[:executables]
+ @force_all = options[:all]
+ @force_ignore = options[:ignore]
+ @bin_dir = options[:bin_dir]
+ @format_executable = options[:format_executable]
+ @abort_on_dependent = options[:abort_on_dependent]
+ @check_dev = options[:check_dev]
+
+ if options[:force]
+ @force_all = true
+ @force_ignore = true
+ end
+
+ # only add user directory if install_dir is not set
+ @user_install = false
+ @user_install = options[:user_install] unless @install_dir
- spec_dir = File.join @gem_home, 'specifications'
- @source_index = Gem::SourceIndex.from_gems_in spec_dir
+ # Optimization: populated during #uninstall
+ @default_specs_matching_uninstall_params = []
end
##
@@ -55,44 +92,90 @@ class Gem::Uninstaller
# directory, and the cached .gem file.
def uninstall
- list = @source_index.find_name @gem, @version
+ dependency = Gem::Dependency.new @gem, @version
+
+ list = []
+
+ specification_record.stubs.each do |spec|
+ next unless dependency.matches_spec? spec
+
+ list << spec
+ end
+
+ if list.empty?
+ raise Gem::InstallError, "gem #{@gem.inspect} is not installed"
+ end
- if list.empty? then
- raise Gem::InstallError, "Unknown gem #{@gem} #{@version}"
+ default_specs, list = list.partition(&:default_gem?)
+ warn_cannot_uninstall_default_gems(default_specs - list)
+ @default_specs_matching_uninstall_params = default_specs.map(&:to_spec)
- elsif list.size > 1 and @force_all then
- remove_all list.dup
+ list, other_repo_specs = list.partition do |spec|
+ @gem_home == spec.base_dir ||
+ (@user_install && spec.base_dir == @user_dir)
+ end
+
+ list.sort!
+
+ if list.empty?
+ return unless other_repo_specs.any?
+
+ other_repos = other_repo_specs.map(&:base_dir).uniq
+
+ message = ["#{@gem} is not installed in GEM_HOME, try:"]
+ message.concat other_repos.map {|repo|
+ "\tgem uninstall -i #{repo} #{@gem}"
+ }
- elsif list.size > 1 then
- gem_names = list.collect {|gem| gem.full_name} + ["All versions"]
+ raise Gem::InstallError, message.join("\n")
+ elsif @force_all
+ remove_all list
+
+ elsif list.size > 1
+ gem_names = list.map(&:full_name_with_location)
+ gem_names << "All versions"
say
- gem_name, index = choose_from_list "Select gem to uninstall:", gem_names
+ _, index = choose_from_list "Select gem to uninstall:", gem_names
- if index == list.size then
- remove_all list.dup
- elsif index >= 0 && index < list.size then
- uninstall_gem list[index], list.dup
+ if index == list.size
+ remove_all list
+ elsif index && index >= 0 && index < list.size
+ uninstall_gem list[index]
else
- say "Error: must enter a number [1-#{list.size+1}]"
+ say "Error: must enter a number [1-#{list.size + 1}]"
end
else
- uninstall_gem list.first, list.dup
+ uninstall_gem list.first
end
end
##
# Uninstalls gem +spec+
- def uninstall_gem(spec, specs)
+ def uninstall_gem(stub)
+ spec = stub.to_spec
+
@spec = spec
+ unless dependencies_ok? spec
+ if abort_on_dependent? || !ask_if_ok(spec)
+ raise Gem::DependencyRemovalException,
+ "Uninstallation aborted due to dependent gem(s)"
+ end
+ end
+
Gem.pre_uninstall_hooks.each do |hook|
hook.call self
end
- specs.each { |s| remove_executables s }
- remove spec, specs
+ remove_executables @spec
+ remove_plugins @spec
+ remove @spec
+
+ specification_record.remove_spec(stub)
+
+ regenerate_plugins
Gem.post_uninstall_hooks.each do |hook|
hook.call self
@@ -102,48 +185,54 @@ class Gem::Uninstaller
end
##
- # Removes installed executables and batch files (windows only) for
- # +gemspec+.
+ # Removes installed executables and batch files (windows only) for +spec+.
- def remove_executables(gemspec)
- return if gemspec.nil?
+ def remove_executables(spec)
+ return if spec.executables.empty? || default_spec_matches?(spec)
- if gemspec.executables.size > 0 then
- bindir = @bin_dir ? @bin_dir : (Gem.bindir @gem_home)
+ executables = spec.executables.clone
- list = @source_index.find_name(gemspec.name).delete_if { |spec|
- spec.version == gemspec.version
- }
+ # Leave any executables created by other installed versions
+ # of this gem installed.
- executables = gemspec.executables.clone
+ list = Gem::Specification.find_all do |s|
+ s.name == spec.name && s.version != spec.version
+ end
- list.each do |spec|
- spec.executables.each do |exe_name|
- executables.delete(exe_name)
- end
+ list.each do |s|
+ s.executables.each do |exe_name|
+ executables.delete exe_name
end
+ end
- return if executables.size == 0
+ return if executables.empty?
- answer = if @force_executables.nil? then
- ask_yes_no("Remove executables:\n" \
- "\t#{gemspec.executables.join(", ")}\n\nin addition to the gem?",
- true) # " # appease ruby-mode - don't ask
- else
- @force_executables
- end
+ executables = executables.map {|exec| formatted_program_filename exec }
- unless answer then
- say "Executables and scripts will remain installed."
- else
- raise Gem::FilePermissionError, bindir unless File.writable? bindir
+ remove = if @force_executables.nil?
+ ask_yes_no("Remove executables:\n" \
+ "\t#{executables.join ", "}\n\n" \
+ "in addition to the gem?",
+ true)
+ else
+ @force_executables
+ end
+
+ if remove
+ bin_dir = @bin_dir || Gem.bindir(spec.base_dir)
+
+ raise Gem::FilePermissionError, bin_dir unless File.writable? bin_dir
- gemspec.executables.each do |exe_name|
- say "Removing #{exe_name}"
- FileUtils.rm_f File.join(bindir, exe_name)
- FileUtils.rm_f File.join(bindir, "#{exe_name}.bat")
- end
+ executables.each do |exe_name|
+ say "Removing #{exe_name}"
+
+ exe_file = File.join bin_dir, exe_name
+
+ safe_delete { FileUtils.rm exe_file }
+ safe_delete { FileUtils.rm "#{exe_file}.bat" }
end
+ else
+ say "Executables and scripts will remain installed."
end
end
@@ -153,90 +242,199 @@ class Gem::Uninstaller
# NOTE: removes uninstalled gems from +list+.
def remove_all(list)
- list.dup.each { |spec| uninstall_gem spec, list }
+ list.each {|spec| uninstall_gem spec }
end
##
# spec:: the spec of the gem to be uninstalled
- # list:: the list of all such gems
- #
- # Warning: this method modifies the +list+ parameter. Once it has
- # uninstalled a gem, it is removed from that list.
-
- def remove(spec, list)
- unless dependencies_ok? spec then
- raise Gem::DependencyRemovalException,
- "Uninstallation aborted due to dependent gem(s)"
- end
- unless path_ok? spec then
+ def remove(spec)
+ unless path_ok?(@gem_home, spec) ||
+ (@user_install && path_ok?(@user_dir, spec))
e = Gem::GemNotInHomeException.new \
- "Gem is not installed in directory #{@gem_home}"
+ "Gem '#{spec.full_name}' is not installed in directory #{@gem_home}"
e.spec = spec
raise e
end
- raise Gem::FilePermissionError, spec.installation_path unless
- File.writable?(spec.installation_path)
+ raise Gem::FilePermissionError, spec.base_dir unless
+ File.writable?(spec.base_dir)
- FileUtils.rm_rf spec.full_gem_path
+ full_gem_path = spec.full_gem_path
+ exclusions = []
- original_platform_name = [
- spec.name, spec.version, spec.original_platform].join '-'
+ if default_spec_matches?(spec) && spec.executables.any?
+ exclusions = spec.executables.map {|exe| File.join(spec.bin_dir, exe) }
+ exclusions << File.dirname(exclusions.last) until exclusions.last == full_gem_path
+ end
- spec_dir = File.join spec.installation_path, 'specifications'
- gemspec = File.join spec_dir, "#{spec.full_name}.gemspec"
+ safe_delete { rm_r full_gem_path, exclusions: exclusions }
+ safe_delete { FileUtils.rm_r spec.extension_dir }
- unless File.exist? gemspec then
- gemspec = File.join spec_dir, "#{original_platform_name}.gemspec"
- end
+ old_platform_name = spec.original_name
- FileUtils.rm_rf gemspec
+ gem = spec.cache_file
+ gem = File.join(spec.cache_dir, "#{old_platform_name}.gem") unless
+ File.exist? gem
- cache_dir = File.join spec.installation_path, 'cache'
- gem = File.join cache_dir, "#{spec.full_name}.gem"
+ safe_delete { FileUtils.rm_r gem }
- unless File.exist? gem then
- gem = File.join cache_dir, "#{original_platform_name}.gem"
+ begin
+ Gem::RDoc.new(spec).remove
+ rescue NameError
end
- FileUtils.rm_rf gem
+ gemspec = spec.spec_file
- Gem::DocManager.new(spec).uninstall_doc
+ unless File.exist? gemspec
+ gemspec = File.join(File.dirname(gemspec), "#{old_platform_name}.gemspec")
+ end
- say "Successfully uninstalled #{spec.full_name}"
+ safe_delete { FileUtils.rm_r gemspec }
+ announce_deletion_of(spec)
+ end
+
+ ##
+ # Remove any plugin wrappers for +spec+.
- list.delete spec
+ def remove_plugins(spec) # :nodoc:
+ return if spec.plugins.empty?
+
+ remove_plugins_for(spec, plugin_dir_for(spec))
end
- def path_ok?(spec)
- full_path = File.join @gem_home, 'gems', spec.full_name
- original_path = File.join @gem_home, 'gems', spec.original_name
+ ##
+ # Regenerates plugin wrappers after removal.
+
+ def regenerate_plugins
+ latest = specification_record.latest_spec_for(@spec.name)
+ return if latest.nil?
+
+ regenerate_plugins_for(latest, plugin_dir_for(@spec))
+ end
+
+ ##
+ # Is +spec+ in +gem_dir+?
+
+ def path_ok?(gem_dir, spec)
+ full_path = File.join gem_dir, "gems", spec.full_name
+ original_path = File.join gem_dir, "gems", spec.original_name
full_path == spec.full_gem_path || original_path == spec.full_gem_path
end
- def dependencies_ok?(spec)
+ ##
+ # Returns true if it is OK to remove +spec+ or this is a forced
+ # uninstallation.
+
+ def dependencies_ok?(spec) # :nodoc:
return true if @force_ignore
- deplist = Gem::DependencyList.from_source_index @source_index
- deplist.ok_to_remove?(spec.full_name) || ask_if_ok(spec)
+ deplist = Gem::DependencyList.from_specs
+ deplist.ok_to_remove?(spec.full_name, @check_dev)
end
- def ask_if_ok(spec)
- msg = ['']
- msg << 'You have requested to uninstall the gem:'
+ ##
+ # Should the uninstallation abort if a dependency will go unsatisfied?
+ #
+ # See ::new.
+
+ def abort_on_dependent? # :nodoc:
+ @abort_on_dependent
+ end
+
+ ##
+ # Asks if it is OK to remove +spec+. Returns true if it is OK.
+
+ def ask_if_ok(spec) # :nodoc:
+ msg = [""]
+ msg << "You have requested to uninstall the gem:"
msg << "\t#{spec.full_name}"
- spec.dependent_gems.each do |gem,dep,satlist|
- msg <<
- ("#{gem.name}-#{gem.version} depends on " +
- "[#{dep.name} (#{dep.version_requirements})]")
+ msg << ""
+
+ siblings = Gem::Specification.select do |s|
+ s.name == spec.name && s.full_name != spec.full_name
end
- msg << 'If you remove this gems, one or more dependencies will not be met.'
- msg << 'Continue with Uninstall?'
- return ask_yes_no(msg.join("\n"), true)
+
+ spec.dependent_gems(@check_dev).each do |dep_spec, dep, _satlist|
+ unless siblings.any? {|s| s.satisfies_requirement? dep }
+ msg << "#{dep_spec.name}-#{dep_spec.version} depends on #{dep}"
+ end
+ end
+
+ msg << "If you remove this gem, these dependencies will not be met."
+ msg << "Continue with Uninstall?"
+ ask_yes_no(msg.join("\n"), false)
end
-end
+ ##
+ # Returns the formatted version of the executable +filename+
+
+ def formatted_program_filename(filename) # :nodoc:
+ # TODO perhaps the installer should leave a small manifest
+ # of what it did for us to find rather than trying to recreate
+ # it again.
+ if @format_executable
+ require_relative "installer"
+ Gem::Installer.exec_format % File.basename(filename)
+ else
+ filename
+ end
+ end
+
+ def safe_delete(&block)
+ block.call
+ rescue Errno::ENOENT
+ nil
+ rescue Errno::EPERM
+ e = Gem::UninstallError.new
+ e.spec = @spec
+
+ raise e
+ end
+
+ private
+ def rm_r(path, exclusions:)
+ FileUtils::Entry_.new(path).postorder_traverse do |ent|
+ ent.remove unless exclusions.include?(ent.path)
+ end
+ end
+
+ def specification_record
+ @specification_record ||= @install_dir ? Gem::SpecificationRecord.from_path(@install_dir) : Gem::Specification.specification_record
+ end
+
+ def announce_deletion_of(spec)
+ name = spec.full_name
+ say "Successfully uninstalled #{name}"
+ if default_spec_matches?(spec)
+ say(
+ "There was both a regular copy and a default copy of #{name}. The " \
+ "regular copy was successfully uninstalled, but the default copy " \
+ "was left around because default gems can't be removed."
+ )
+ end
+ end
+
+ # @return true if the specs of any default gems are `==` to the given `spec`.
+ def default_spec_matches?(spec)
+ !default_specs_that_match(spec).empty?
+ end
+
+ # @return [Array] specs of default gems that are `==` to the given `spec`.
+ def default_specs_that_match(spec)
+ @default_specs_matching_uninstall_params.select {|default_spec| spec == default_spec }
+ end
+
+ def warn_cannot_uninstall_default_gems(specs)
+ specs.each do |spec|
+ say "Gem #{spec.full_name} cannot be uninstalled because it is a default gem"
+ end
+ end
+
+ def plugin_dir_for(spec)
+ Gem.plugindir(spec.base_dir)
+ end
+end
diff --git a/lib/rubygems/unknown_command_spell_checker.rb b/lib/rubygems/unknown_command_spell_checker.rb
new file mode 100644
index 0000000000..ee5c2fbe04
--- /dev/null
+++ b/lib/rubygems/unknown_command_spell_checker.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Gem::UnknownCommandSpellChecker
+ attr_reader :error
+
+ def initialize(error)
+ @error = error
+ end
+
+ def corrections
+ @corrections ||=
+ spell_checker.correct(error.unknown_command).map(&:inspect)
+ end
+
+ private
+
+ def spell_checker
+ dictionary = Gem::CommandManager.instance.command_names
+ DidYouMean::SpellChecker.new(dictionary: dictionary)
+ end
+end
diff --git a/lib/rubygems/update_suggestion.rb b/lib/rubygems/update_suggestion.rb
new file mode 100644
index 0000000000..6f3ec5f493
--- /dev/null
+++ b/lib/rubygems/update_suggestion.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+##
+# Mixin methods for Gem::Command to promote available RubyGems update
+
+module Gem::UpdateSuggestion
+ ONE_WEEK = 7 * 24 * 60 * 60
+
+ ##
+ # Message to promote available RubyGems update with related gem update command.
+
+ def update_suggestion
+ <<-MESSAGE
+
+A new release of RubyGems is available: #{Gem.rubygems_version} → #{Gem.latest_rubygems_version}!
+Run `gem update --system #{Gem.latest_rubygems_version}` to update your installation.
+
+ MESSAGE
+ end
+
+ ##
+ # Determines if current environment is eligible for update suggestion.
+
+ def eligible_for_update?
+ # explicit opt-out
+ return false if Gem.configuration[:prevent_update_suggestion]
+ return false if ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"]
+
+ # focus only on human usage of final RubyGems releases
+ return false unless Gem.ui.tty?
+ return false if Gem.rubygems_version.prerelease?
+ return false if Gem.disable_system_update_message
+ return false if Gem::CIDetector.ci?
+
+ # check makes sense only when we can store timestamp of last try
+ # otherwise we will not be able to prevent "annoying" update message
+ # on each command call
+ return unless Gem.configuration.state_file_writable?
+
+ # load time of last check, ensure the difference is enough to repeat the suggestion
+ check_time = Time.now.to_i
+ last_update_check = Gem.configuration.last_update_check
+ return false if (check_time - last_update_check) < ONE_WEEK
+
+ # compare current and latest version, this is the part where
+ # latest rubygems spec is fetched from remote
+ (Gem.rubygems_version < Gem.latest_rubygems_version).tap do |eligible|
+ # store the time of last successful check into state file
+ Gem.configuration.last_update_check = check_time
+
+ return eligible
+ end
+ rescue StandardError # don't block install command on any problem
+ false
+ end
+end
diff --git a/lib/rubygems/uri.rb b/lib/rubygems/uri.rb
new file mode 100644
index 0000000000..d729c67d26
--- /dev/null
+++ b/lib/rubygems/uri.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+##
+# The Uri handles rubygems source URIs.
+#
+
+class Gem::Uri
+ ##
+ # Parses and redacts uri
+
+ def self.redact(uri)
+ new(uri).redacted
+ end
+
+ ##
+ # Parses uri, raising if it's invalid
+
+ def self.parse!(uri)
+ require_relative "vendor/uri/lib/uri"
+
+ raise Gem::URI::InvalidURIError unless uri
+
+ return uri unless uri.is_a?(String)
+
+ # Always escape URI's to deal with potential spaces and such
+ # It should also be considered that source_uri may already be
+ # a valid URI with escaped characters. e.g. "{DESede}" is encoded
+ # as "%7BDESede%7D". If this is escaped again the percentage
+ # symbols will be escaped.
+ begin
+ Gem::URI.parse(uri)
+ rescue Gem::URI::InvalidURIError
+ Gem::URI.parse(Gem::URI::RFC2396_PARSER.escape(uri))
+ end
+ end
+
+ ##
+ # Parses uri, returning the original uri if it's invalid
+
+ def self.parse(uri)
+ parse!(uri)
+ rescue Gem::URI::InvalidURIError
+ uri
+ end
+
+ def initialize(source_uri)
+ @parsed_uri = parse(source_uri)
+ end
+
+ def redacted
+ return self unless valid_uri?
+
+ if token? || oauth_basic?
+ with_redacted_user
+ elsif password?
+ with_redacted_password
+ else
+ self
+ end
+ end
+
+ def to_s
+ @parsed_uri.to_s
+ end
+
+ def redact_credentials_from(text)
+ return text unless valid_uri? && password? && text.include?(to_s)
+
+ text.sub(password, "REDACTED")
+ end
+
+ def method_missing(method_name, *args, &blk)
+ if @parsed_uri.respond_to?(method_name)
+ @parsed_uri.send(method_name, *args, &blk)
+ else
+ super
+ end
+ end
+
+ def respond_to_missing?(method_name, include_private = false)
+ @parsed_uri.respond_to?(method_name, include_private) || super
+ end
+
+ protected
+
+ # Add a protected reader for the cloned instance to access the original object's parsed uri
+ attr_reader :parsed_uri
+
+ private
+
+ def parse!(uri)
+ self.class.parse!(uri)
+ end
+
+ def parse(uri)
+ self.class.parse(uri)
+ end
+
+ def with_redacted_user
+ clone.tap {|uri| uri.user = "REDACTED" }
+ end
+
+ def with_redacted_password
+ clone.tap {|uri| uri.password = "REDACTED" }
+ end
+
+ def valid_uri?
+ !@parsed_uri.is_a?(String)
+ end
+
+ def password?
+ !!password
+ end
+
+ def oauth_basic?
+ password == "x-oauth-basic"
+ end
+
+ def token?
+ !user.nil? && password.nil?
+ end
+
+ def initialize_copy(original)
+ @parsed_uri = original.parsed_uri.clone
+ end
+end
diff --git a/lib/rubygems/uri_formatter.rb b/lib/rubygems/uri_formatter.rb
new file mode 100644
index 0000000000..8856fdadd2
--- /dev/null
+++ b/lib/rubygems/uri_formatter.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+##
+# The UriFormatter handles URIs from user-input and escaping.
+#
+# uf = Gem::UriFormatter.new 'example.com'
+#
+# p uf.normalize #=> 'http://example.com'
+
+class Gem::UriFormatter
+ ##
+ # The URI to be formatted.
+
+ attr_reader :uri
+
+ ##
+ # Creates a new URI formatter for +uri+.
+
+ def initialize(uri)
+ require "cgi/escape"
+ require "cgi/util" unless defined?(CGI::EscapeExt)
+
+ @uri = uri
+ end
+
+ ##
+ # Escapes the #uri for use as a CGI parameter
+
+ def escape
+ return unless @uri
+ CGI.escape @uri
+ end
+
+ ##
+ # Normalize the URI by adding "http://" if it is missing.
+
+ def normalize
+ /^(https?|ftp|file):/i.match?(@uri) ? @uri : "http://#{@uri}"
+ end
+
+ ##
+ # Unescapes the #uri which came from a CGI parameter
+
+ def unescape
+ return unless @uri
+ CGI.unescape @uri
+ end
+end
diff --git a/lib/rubygems/user_interaction.rb b/lib/rubygems/user_interaction.rb
index 30a728c597..9fe3e755c4 100644
--- a/lib/rubygems/user_interaction.rb
+++ b/lib/rubygems/user_interaction.rb
@@ -1,360 +1,647 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-module Gem
+require_relative "text"
+
+##
+# Module that defines the default UserInteraction. Any class including this
+# module will have access to the +ui+ method that returns the default UI.
+
+module Gem::DefaultUserInteraction
+ include Gem::Text
##
- # Module that defines the default UserInteraction. Any class including this
- # module will have access to the +ui+ method that returns the default UI.
+ # The default UI is a class variable of the singleton class for this
+ # module.
- module DefaultUserInteraction
+ @ui = nil
- ##
- # The default UI is a class variable of the singleton class for this
- # module.
+ ##
+ # Return the default UI.
- @ui = nil
+ def self.ui
+ @ui ||= Gem::ConsoleUI.new
+ end
- ##
- # Return the default UI.
+ ##
+ # Set the default UI. If the default UI is never explicitly set, a simple
+ # console based UserInteraction will be used automatically.
+
+ def self.ui=(new_ui)
+ @ui = new_ui
+ end
+
+ ##
+ # Use +new_ui+ for the duration of +block+.
+
+ def self.use_ui(new_ui)
+ old_ui = @ui
+ @ui = new_ui
+ yield
+ ensure
+ @ui = old_ui
+ end
+
+ ##
+ # See DefaultUserInteraction::ui
+
+ def ui
+ Gem::DefaultUserInteraction.ui
+ end
+
+ ##
+ # See DefaultUserInteraction::ui=
+
+ def ui=(new_ui)
+ Gem::DefaultUserInteraction.ui = new_ui
+ end
+
+ ##
+ # See DefaultUserInteraction::use_ui
+
+ def use_ui(new_ui, &block)
+ Gem::DefaultUserInteraction.use_ui(new_ui, &block)
+ end
+end
+
+##
+# UserInteraction allows RubyGems to interact with the user through standard
+# methods that can be replaced with more-specific UI methods for different
+# displays.
+#
+# Since UserInteraction dispatches to a concrete UI class you may need to
+# reference other classes for specific behavior such as Gem::ConsoleUI or
+# Gem::SilentUI.
+#
+# Example:
+#
+# class X
+# include Gem::UserInteraction
+#
+# def get_answer
+# n = ask("What is the meaning of life?")
+# end
+# end
+
+module Gem::UserInteraction
+ include Gem::DefaultUserInteraction
+
+ ##
+ # Displays an alert +statement+. Asks a +question+ if given.
+
+ def alert(statement, question = nil)
+ ui.alert statement, question
+ end
+
+ ##
+ # Displays an error +statement+ to the error output location. Asks a
+ # +question+ if given.
+
+ def alert_error(statement, question = nil)
+ ui.alert_error statement, question
+ end
+
+ ##
+ # Displays a warning +statement+ to the warning output location. Asks a
+ # +question+ if given.
+
+ def alert_warning(statement, question = nil)
+ ui.alert_warning statement, question
+ end
+
+ ##
+ # Asks a +question+ and returns the answer.
+
+ def ask(question)
+ ui.ask question
+ end
+
+ ##
+ # Asks for a password with a +prompt+
+
+ def ask_for_password(prompt)
+ ui.ask_for_password prompt
+ end
+
+ ##
+ # Asks a yes or no +question+. Returns true for yes, false for no.
+
+ def ask_yes_no(question, default = nil)
+ ui.ask_yes_no question, default
+ end
+
+ ##
+ # Asks the user to answer +question+ with an answer from the given +list+.
+
+ def choose_from_list(question, list)
+ ui.choose_from_list question, list
+ end
- def self.ui
- @ui ||= Gem::ConsoleUI.new
+ ##
+ # Displays the given +statement+ on the standard output (or equivalent).
+
+ def say(statement = "")
+ ui.say statement
+ end
+
+ ##
+ # Terminates the RubyGems process with the given +exit_code+
+
+ def terminate_interaction(exit_code = 0)
+ ui.terminate_interaction exit_code
+ end
+
+ ##
+ # Calls +say+ with +msg+ or the results of the block if really_verbose
+ # is true.
+
+ def verbose(msg = nil)
+ say(clean_text(msg || yield)) if Gem.configuration.really_verbose
+ end
+end
+
+##
+# Gem::StreamUI implements a simple stream based user interface.
+
+class Gem::StreamUI
+ ##
+ # The input stream
+
+ attr_reader :ins
+
+ ##
+ # The output stream
+
+ attr_reader :outs
+
+ ##
+ # The error stream
+
+ attr_reader :errs
+
+ ##
+ # Creates a new StreamUI wrapping +in_stream+ for user input, +out_stream+
+ # for standard output, +err_stream+ for error output. If +usetty+ is true
+ # then special operations (like asking for passwords) will use the TTY
+ # commands to disable character echo.
+
+ def initialize(in_stream, out_stream, err_stream = $stderr, usetty = true)
+ @ins = in_stream
+ @outs = out_stream
+ @errs = err_stream
+ @usetty = usetty
+ end
+
+ ##
+ # Returns true if TTY methods should be used on this StreamUI.
+
+ def tty?
+ @usetty && @ins.tty?
+ end
+
+ ##
+ # Prints a formatted backtrace to the errors stream if backtraces are
+ # enabled.
+
+ def backtrace(exception)
+ return unless Gem.configuration.backtrace
+
+ @errs.puts "\t#{exception.backtrace.join "\n\t"}"
+ end
+
+ ##
+ # Choose from a list of options. +question+ is a prompt displayed above
+ # the list. +list+ is a list of option strings. Returns the pair
+ # [option_name, option_index].
+
+ def choose_from_list(question, list)
+ @outs.puts question
+
+ list.each_with_index do |item, index|
+ @outs.puts " #{index + 1}. #{item}"
end
- ##
- # Set the default UI. If the default UI is never explicitly set, a simple
- # console based UserInteraction will be used automatically.
+ @outs.print "> "
+ @outs.flush
- def self.ui=(new_ui)
- @ui = new_ui
+ result = @ins.gets
+
+ return nil, nil unless result
+
+ result = result.strip.to_i - 1
+ return nil, nil unless (0...list.size) === result
+ [list[result], result]
+ end
+
+ ##
+ # Ask a question. Returns a true for yes, false for no. If not connected
+ # to a tty, raises an exception if default is nil, otherwise returns
+ # default.
+
+ def ask_yes_no(question, default = nil)
+ unless tty?
+ if default.nil?
+ raise Gem::OperationNotSupportedError,
+ "Not connected to a tty and no default specified"
+ else
+ return default
+ end
end
- ##
- # Use +new_ui+ for the duration of +block+.
-
- def self.use_ui(new_ui)
- old_ui = @ui
- @ui = new_ui
- yield
- ensure
- @ui = old_ui
+ default_answer = case default
+ when nil
+ "yn"
+ when true
+ "Yn"
+ else
+ "yN"
end
- ##
- # See DefaultUserInteraction::ui
+ result = nil
- def ui
- DefaultUserInteraction.ui
+ while result.nil? do
+ result = case ask "#{question} [#{default_answer}]"
+ when /^y/i then true
+ when /^n/i then false
+ when /^$/ then default
+ end
end
- ##
- # See DefaultUserInteraction::ui=
+ result
+ end
- def ui=(new_ui)
- DefaultUserInteraction.ui = new_ui
- end
+ ##
+ # Ask a question. Returns an answer if connected to a tty, nil otherwise.
- ##
- # See DefaultUserInteraction::use_ui
+ def ask(question)
+ return nil unless tty?
- def use_ui(new_ui, &block)
- DefaultUserInteraction.use_ui(new_ui, &block)
- end
+ @outs.print(question + " ")
+ @outs.flush
+ result = @ins.gets
+ result&.chomp!
+ result
end
##
- # Make the default UI accessable without the "ui." prefix. Classes
- # including this module may use the interaction methods on the default UI
- # directly. Classes may also reference the ui and ui= methods.
- #
- # Example:
- #
- # class X
- # include Gem::UserInteraction
- #
- # def get_answer
- # n = ask("What is the meaning of life?")
- # end
- # end
-
- module UserInteraction
-
- include DefaultUserInteraction
-
- [:alert,
- :alert_error,
- :alert_warning,
- :ask,
- :ask_yes_no,
- :choose_from_list,
- :say,
- :terminate_interaction ].each do |methname|
- class_eval %{
- def #{methname}(*args)
- ui.#{methname}(*args)
- end
- }, __FILE__, __LINE__
+ # Ask for a password. Does not echo response to terminal.
+
+ def ask_for_password(question)
+ return nil unless tty?
+
+ @outs.print(question, " ")
+ @outs.flush
+
+ password = _gets_noecho
+ @outs.puts
+ password&.chomp!
+ password
+ end
+
+ def require_io_console
+ @require_io_console ||= begin
+ begin
+ require "io/console"
+ rescue LoadError
+ end
+ true
end
end
+ def _gets_noecho
+ require_io_console
+ @ins.noecho { @ins.gets }
+ end
+
##
- # StreamUI implements a simple stream based user interface.
+ # Display a statement.
- class StreamUI
+ def say(statement = "")
+ @outs.puts statement
+ end
- attr_reader :ins, :outs, :errs
+ ##
+ # Display an informational alert. Will ask +question+ if it is not nil.
- def initialize(in_stream, out_stream, err_stream=STDERR)
- @ins = in_stream
- @outs = out_stream
- @errs = err_stream
- end
+ def alert(statement, question = nil)
+ @outs.puts "INFO: #{statement}"
+ ask(question) if question
+ end
- ##
- # Choose from a list of options. +question+ is a prompt displayed above
- # the list. +list+ is a list of option strings. Returns the pair
- # [option_name, option_index].
+ ##
+ # Display a warning on stderr. Will ask +question+ if it is not nil.
- def choose_from_list(question, list)
- @outs.puts question
+ def alert_warning(statement, question = nil)
+ @errs.puts "WARNING: #{statement}"
+ ask(question) if question
+ end
- list.each_with_index do |item, index|
- @outs.puts " #{index+1}. #{item}"
- end
+ ##
+ # Display an error message in a location expected to get error messages.
+ # Will ask +question+ if it is not nil.
- @outs.print "> "
- @outs.flush
+ def alert_error(statement, question = nil)
+ @errs.puts "ERROR: #{statement}"
+ ask(question) if question
+ end
- result = @ins.gets
+ ##
+ # Terminate the application with exit code +status+, running any exit
+ # handlers that might have been defined.
+
+ def terminate_interaction(status = 0)
+ close
+ raise Gem::SystemExitException, status
+ end
- return nil, nil unless result
+ def close
+ end
- result = result.strip.to_i - 1
- return list[result], result
+ ##
+ # Return a progress reporter object chosen from the current verbosity.
+
+ def progress_reporter(*args)
+ case Gem.configuration.verbose
+ when nil, false
+ SilentProgressReporter.new(@outs, *args)
+ when true
+ SimpleProgressReporter.new(@outs, *args)
+ else
+ VerboseProgressReporter.new(@outs, *args)
end
+ end
+ ##
+ # An absolutely silent progress reporter.
+
+ class SilentProgressReporter
##
- # Ask a question. Returns a true for yes, false for no. If not connected
- # to a tty, raises an exception if default is nil, otherwise returns
- # default.
-
- def ask_yes_no(question, default=nil)
- unless @ins.tty? then
- if default.nil? then
- raise Gem::OperationNotSupportedError,
- "Not connected to a tty and no default specified"
- else
- return default
- end
- end
+ # The count of items is never updated for the silent progress reporter.
- qstr = case default
- when nil
- 'yn'
- when true
- 'Yn'
- else
- 'yN'
- end
-
- result = nil
-
- while result.nil?
- result = ask("#{question} [#{qstr}]")
- result = case result
- when /^[Yy].*/
- true
- when /^[Nn].*/
- false
- when /^$/
- default
- else
- nil
- end
- end
+ attr_reader :count
- return result
+ ##
+ # Creates a silent progress reporter that ignores all input arguments.
+
+ def initialize(out_stream, size, initial_message, terminal_message = nil)
end
##
- # Ask a question. Returns an answer if connected to a tty, nil otherwise.
+ # Does not print +message+ when updated as this object has taken a vow of
+ # silence.
- def ask(question)
- return nil if not @ins.tty?
+ def updated(message)
+ end
- @outs.print(question + " ")
- @outs.flush
+ ##
+ # Does not print anything when complete as this object has taken a vow of
+ # silence.
- result = @ins.gets
- result.chomp! if result
- result
+ def done
end
+ end
+
+ ##
+ # A basic dotted progress reporter.
+
+ class SimpleProgressReporter
+ include Gem::DefaultUserInteraction
##
- # Display a statement.
+ # The number of progress items counted so far.
- def say(statement="")
- @outs.puts statement
- end
+ attr_reader :count
##
- # Display an informational alert. Will ask +question+ if it is not nil.
+ # Creates a new progress reporter that will write to +out_stream+ for
+ # +size+ items. Shows the given +initial_message+ when progress starts
+ # and the +terminal_message+ when it is complete.
- def alert(statement, question=nil)
- @outs.puts "INFO: #{statement}"
- ask(question) if question
+ def initialize(out_stream, size, initial_message, terminal_message = "complete")
+ @out = out_stream
+ @total = size
+ @count = 0
+ @terminal_message = terminal_message
+
+ @out.puts initial_message
end
##
- # Display a warning in a location expected to get error messages. Will
- # ask +question+ if it is not nil.
+ # Prints out a dot and ignores +message+.
- def alert_warning(statement, question=nil)
- @errs.puts "WARNING: #{statement}"
- ask(question) if question
+ def updated(message)
+ @count += 1
+ @out.print "."
+ @out.flush
end
##
- # Display an error message in a location expected to get error messages.
- # Will ask +question+ if it is not nil.
+ # Prints out the terminal message.
- def alert_error(statement, question=nil)
- @errs.puts "ERROR: #{statement}"
- ask(question) if question
+ def done
+ @out.puts "\n#{@terminal_message}"
end
+ end
+
+ ##
+ # A progress reporter that prints out messages about the current progress.
+
+ class VerboseProgressReporter
+ include Gem::DefaultUserInteraction
##
- # Terminate the application with exit code +status+, running any exit
- # handlers that might have been defined.
+ # The number of progress items counted so far.
- def terminate_interaction(status = 0)
- raise Gem::SystemExitException, status
- end
+ attr_reader :count
##
- # Return a progress reporter object chosen from the current verbosity.
-
- def progress_reporter(*args)
- case Gem.configuration.verbose
- when nil, false
- SilentProgressReporter.new(@outs, *args)
- when true
- SimpleProgressReporter.new(@outs, *args)
- else
- VerboseProgressReporter.new(@outs, *args)
- end
+ # Creates a new progress reporter that will write to +out_stream+ for
+ # +size+ items. Shows the given +initial_message+ when progress starts
+ # and the +terminal_message+ when it is complete.
+
+ def initialize(out_stream, size, initial_message, terminal_message = "complete")
+ @out = out_stream
+ @total = size
+ @count = 0
+ @terminal_message = terminal_message
+
+ @out.puts initial_message
end
##
- # An absolutely silent progress reporter.
+ # Prints out the position relative to the total and the +message+.
- class SilentProgressReporter
- attr_reader :count
+ def updated(message)
+ @count += 1
+ @out.puts "#{@count}/#{@total}: #{message}"
+ end
- def initialize(out_stream, size, initial_message, terminal_message = nil)
- end
+ ##
+ # Prints out the terminal message.
- def updated(message)
- end
+ def done
+ @out.puts @terminal_message
+ end
+ end
- def done
- end
+ ##
+ # Return a download reporter object chosen from the current verbosity
+
+ def download_reporter(*args)
+ if [nil, false].include?(Gem.configuration.verbose) || !@outs.tty?
+ SilentDownloadReporter.new(@outs, *args)
+ else
+ ThreadedDownloadReporter.new(@outs, *args)
end
+ end
+
+ ##
+ # An absolutely silent download reporter.
+ class SilentDownloadReporter
##
- # A basic dotted progress reporter.
+ # The silent download reporter ignores all arguments
- class SimpleProgressReporter
- include DefaultUserInteraction
+ def initialize(out_stream, *args)
+ end
- attr_reader :count
+ ##
+ # The silent download reporter does not display +filename+ or care about
+ # +filesize+ because it is silent.
- def initialize(out_stream, size, initial_message,
- terminal_message = "complete")
- @out = out_stream
- @total = size
- @count = 0
- @terminal_message = terminal_message
+ def fetch(filename, filesize)
+ end
- @out.puts initial_message
- end
+ ##
+ # Nothing can update the silent download reporter.
- ##
- # Prints out a dot and ignores +message+.
+ def update(current)
+ end
- def updated(message)
- @count += 1
- @out.print "."
- @out.flush
- end
+ ##
+ # The silent download reporter won't tell you when the download is done.
+ # Because it is silent.
- ##
- # Prints out the terminal message.
+ def done
+ end
+ end
- def done
- @out.puts "\n#{@terminal_message}"
- end
+ ##
+ # A progress reporter that behaves nicely with threaded downloading.
- end
+ class ThreadedDownloadReporter
+ MUTEX = Thread::Mutex.new
##
- # A progress reporter that prints out messages about the current progress.
+ # The current file name being displayed
- class VerboseProgressReporter
- include DefaultUserInteraction
+ attr_reader :file_name
- attr_reader :count
+ ##
+ # Creates a new threaded download reporter that will display on
+ # +out_stream+. The other arguments are ignored.
- def initialize(out_stream, size, initial_message,
- terminal_message = 'complete')
- @out = out_stream
- @total = size
- @count = 0
- @terminal_message = terminal_message
+ def initialize(out_stream, *args)
+ @file_name = nil
+ @out = out_stream
+ end
+
+ ##
+ # Tells the download reporter that the +file_name+ is being fetched.
+ # The other arguments are ignored.
- @out.puts initial_message
+ def fetch(file_name, *args)
+ if @file_name.nil?
+ @file_name = file_name
+ locked_puts "Fetching #{@file_name}"
end
+ end
- ##
- # Prints out the position relative to the total and the +message+.
+ ##
+ # Updates the threaded download reporter for the given number of +bytes+.
- def updated(message)
- @count += 1
- @out.puts "#{@count}/#{@total}: #{message}"
- end
+ def update(bytes)
+ # Do nothing.
+ end
+
+ ##
+ # Indicates the download is complete.
- ##
- # Prints out the terminal message.
+ def done
+ # Do nothing.
+ end
+
+ private
- def done
- @out.puts @terminal_message
+ def locked_puts(message)
+ MUTEX.synchronize do
+ @out.puts message
end
end
end
+end
+
+##
+# Subclass of StreamUI that instantiates the user interaction using $stdin,
+# $stdout, and $stderr.
+class Gem::ConsoleUI < Gem::StreamUI
##
- # Subclass of StreamUI that instantiates the user interaction using STDIN,
- # STDOUT, and STDERR.
+ # The Console UI has no arguments as it defaults to reading input from
+ # stdin, output to stdout and warnings or errors to stderr.
- class ConsoleUI < StreamUI
- def initialize
- super(STDIN, STDOUT, STDERR)
- end
+ def initialize
+ super $stdin, $stdout, $stderr, true
end
+end
+
+##
+# SilentUI is a UI choice that is absolutely silent.
+class Gem::SilentUI < Gem::StreamUI
##
- # SilentUI is a UI choice that is absolutely silent.
+ # The SilentUI has no arguments as it does not use any stream.
- class SilentUI
- def method_missing(sym, *args, &block)
- self
- end
+ def initialize
+ io = NullIO.new
+ super io, io, io, false
end
-end
+ def close
+ end
+
+ def download_reporter(*args) # :nodoc:
+ SilentDownloadReporter.new(@outs, *args)
+ end
+
+ def progress_reporter(*args) # :nodoc:
+ SilentProgressReporter.new(@outs, *args)
+ end
+ ##
+ # An absolutely silent IO.
+
+ class NullIO
+ def puts(*args)
+ end
+
+ def print(*args)
+ end
+
+ def flush
+ end
+
+ def gets(*args)
+ end
+
+ def tty?
+ false
+ end
+ end
+end
diff --git a/lib/rubygems/util.rb b/lib/rubygems/util.rb
new file mode 100644
index 0000000000..ee4106c6ce
--- /dev/null
+++ b/lib/rubygems/util.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+##
+# This module contains various utility methods as module methods.
+
+module Gem::Util
+ ##
+ # Zlib::GzipReader wrapper that unzips +data+.
+
+ def self.gunzip(data)
+ require "zlib"
+ require "stringio"
+ data = StringIO.new(data, "r")
+
+ gzip_reader = begin
+ Zlib::GzipReader.new(data)
+ rescue Zlib::GzipFile::Error => e
+ raise e.class, e.inspect, e.backtrace
+ end
+
+ unzipped = gzip_reader.read
+ unzipped.force_encoding Encoding::BINARY
+ unzipped
+ end
+
+ ##
+ # Zlib::GzipWriter wrapper that zips +data+.
+
+ def self.gzip(data)
+ require "zlib"
+ require "stringio"
+ zipped = StringIO.new(String.new, "w")
+ zipped.set_encoding Encoding::BINARY
+
+ Zlib::GzipWriter.wrap zipped do |io|
+ io.write data
+ end
+
+ zipped.string
+ end
+
+ ##
+ # A Zlib::Inflate#inflate wrapper
+
+ def self.inflate(data)
+ require "zlib"
+ Zlib::Inflate.inflate data
+ end
+
+ ##
+ # This calls IO.popen and reads the result
+
+ def self.popen(*command)
+ IO.popen command, &:read
+ end
+
+ ##
+ # Enumerates the parents of +directory+.
+
+ def self.traverse_parents(directory, &block)
+ return enum_for __method__, directory unless block_given?
+
+ here = File.expand_path directory
+ loop do
+ begin
+ Dir.chdir here, &block
+ rescue StandardError
+ Errno::EACCES
+ end
+
+ new_here = File.expand_path("..", here)
+ return if new_here == here # toplevel
+ here = new_here
+ end
+ end
+
+ ##
+ # Globs for files matching +pattern+ inside of +directory+,
+ # returning absolute paths to the matching files.
+
+ def self.glob_files_in_dir(glob, base_path)
+ Dir.glob(glob, base: base_path).map! {|f| File.expand_path(f, base_path) }
+ end
+
+ ##
+ # Corrects +path+ (usually returned by `Gem::URI.parse().path` on Windows), that
+ # comes with a leading slash.
+
+ def self.correct_for_windows_path(path)
+ if path[0].chr == "/" && path[1].chr.match?(/[a-z]/i) && path[2].chr == ":"
+ path[1..-1]
+ else
+ path
+ end
+ end
+end
diff --git a/lib/rubygems/util/atomic_file_writer.rb b/lib/rubygems/util/atomic_file_writer.rb
new file mode 100644
index 0000000000..32767c6a79
--- /dev/null
+++ b/lib/rubygems/util/atomic_file_writer.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+# Based on ActiveSupport's AtomicFile implementation
+# Copyright (c) David Heinemeier Hansson
+# https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/file/atomic.rb
+# Licensed under the MIT License
+
+module Gem
+ class AtomicFileWriter
+ ##
+ # Write to a file atomically. Useful for situations where you don't
+ # want other processes or threads to see half-written files.
+
+ def self.open(file_name)
+ require "securerandom" unless defined?(SecureRandom)
+
+ old_stat = begin
+ File.stat(file_name)
+ rescue SystemCallError
+ nil
+ end
+
+ # Names can't be longer than 255B
+ tmp_suffix = ".tmp.#{SecureRandom.hex}"
+ dirname = File.dirname(file_name)
+ basename = File.basename(file_name)
+ tmp_path = File.join(dirname, ".#{basename.byteslice(0, 254 - tmp_suffix.bytesize)}#{tmp_suffix}")
+
+ flags = File::RDWR | File::CREAT | File::EXCL | File::BINARY
+ flags |= File::SHARE_DELETE if defined?(File::SHARE_DELETE)
+
+ File.open(tmp_path, flags) do |temp_file|
+ temp_file.binmode
+ if old_stat
+ # Set correct permissions on new file
+ begin
+ File.chown(old_stat.uid, old_stat.gid, temp_file.path)
+ # This operation will affect filesystem ACL's
+ File.chmod(old_stat.mode, temp_file.path)
+ rescue Errno::EPERM, Errno::EACCES
+ # Changing file ownership failed, moving on.
+ end
+ end
+
+ return_val = yield temp_file
+ rescue StandardError => error
+ begin
+ temp_file.close
+ rescue StandardError
+ nil
+ end
+
+ begin
+ File.unlink(temp_file.path)
+ rescue StandardError
+ nil
+ end
+
+ raise error
+ else
+ begin
+ File.rename(temp_file.path, file_name)
+ rescue StandardError
+ begin
+ File.unlink(temp_file.path)
+ rescue StandardError
+ end
+
+ raise
+ end
+
+ return_val
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/util/licenses.rb b/lib/rubygems/util/licenses.rb
new file mode 100644
index 0000000000..caf53d0b7e
--- /dev/null
+++ b/lib/rubygems/util/licenses.rb
@@ -0,0 +1,888 @@
+# frozen_string_literal: true
+
+# This is generated by generate_spdx_license_list.rb, any edits to this
+# file will be discarded.
+
+require_relative "../text"
+
+class Gem::Licenses
+ extend Gem::Text
+
+ NONSTANDARD = "Nonstandard"
+ LICENSE_REF = "LicenseRef-.+"
+
+ # Software Package Data Exchange (SPDX) standard open-source software
+ # license identifiers
+ LICENSE_IDENTIFIERS = %w[
+ 0BSD
+ 3D-Slicer-1.0
+ AAL
+ ADSL
+ AFL-1.1
+ AFL-1.2
+ AFL-2.0
+ AFL-2.1
+ AFL-3.0
+ AGPL-1.0-only
+ AGPL-1.0-or-later
+ AGPL-3.0-only
+ AGPL-3.0-or-later
+ ALGLIB-Documentation
+ AMD-newlib
+ AMDPLPA
+ AML
+ AML-glslang
+ AMPAS
+ ANTLR-PD
+ ANTLR-PD-fallback
+ APAFML
+ APL-1.0
+ APSL-1.0
+ APSL-1.1
+ APSL-1.2
+ APSL-2.0
+ ASWF-Digital-Assets-1.0
+ ASWF-Digital-Assets-1.1
+ Abstyles
+ AdaCore-doc
+ Adobe-2006
+ Adobe-Display-PostScript
+ Adobe-Glyph
+ Adobe-Utopia
+ Advanced-Cryptics-Dictionary
+ Afmparse
+ Aladdin
+ Apache-1.0
+ Apache-1.1
+ Apache-2.0
+ App-s2p
+ Arphic-1999
+ Artistic-1.0
+ Artistic-1.0-Perl
+ Artistic-1.0-cl8
+ Artistic-2.0
+ Artistic-dist
+ Aspell-RU
+ BOLA-1.1
+ BSD-1-Clause
+ BSD-2-Clause
+ BSD-2-Clause-Darwin
+ BSD-2-Clause-Patent
+ BSD-2-Clause-Views
+ BSD-2-Clause-first-lines
+ BSD-2-Clause-pkgconf-disclaimer
+ BSD-3-Clause
+ BSD-3-Clause-Attribution
+ BSD-3-Clause-Clear
+ BSD-3-Clause-HP
+ BSD-3-Clause-LBNL
+ BSD-3-Clause-Modification
+ BSD-3-Clause-No-Military-License
+ BSD-3-Clause-No-Nuclear-License
+ BSD-3-Clause-No-Nuclear-License-2014
+ BSD-3-Clause-No-Nuclear-Warranty
+ BSD-3-Clause-Open-MPI
+ BSD-3-Clause-Sun
+ BSD-3-Clause-Tso
+ BSD-3-Clause-acpica
+ BSD-3-Clause-flex
+ BSD-4-Clause
+ BSD-4-Clause-Shortened
+ BSD-4-Clause-UC
+ BSD-4.3RENO
+ BSD-4.3TAHOE
+ BSD-Advertising-Acknowledgement
+ BSD-Attribution-HPND-disclaimer
+ BSD-Inferno-Nettverk
+ BSD-Mark-Modifications
+ BSD-Protection
+ BSD-Source-Code
+ BSD-Source-beginning-file
+ BSD-Systemics
+ BSD-Systemics-W3Works
+ BSL-1.0
+ BUSL-1.1
+ Baekmuk
+ Bahyph
+ Barr
+ Beerware
+ BitTorrent-1.0
+ BitTorrent-1.1
+ Bitstream-Charter
+ Bitstream-Vera
+ BlueOak-1.0.0
+ Boehm-GC
+ Boehm-GC-without-fee
+ Borceux
+ Brian-Gladman-2-Clause
+ Brian-Gladman-3-Clause
+ Buddy
+ C-UDA-1.0
+ CAL-1.0
+ CAL-1.0-Combined-Work-Exception
+ CAPEC-tou
+ CATOSL-1.1
+ CC-BY-1.0
+ CC-BY-2.0
+ CC-BY-2.5
+ CC-BY-2.5-AU
+ CC-BY-3.0
+ CC-BY-3.0-AT
+ CC-BY-3.0-AU
+ CC-BY-3.0-DE
+ CC-BY-3.0-IGO
+ CC-BY-3.0-NL
+ CC-BY-3.0-US
+ CC-BY-4.0
+ CC-BY-NC-1.0
+ CC-BY-NC-2.0
+ CC-BY-NC-2.5
+ CC-BY-NC-3.0
+ CC-BY-NC-3.0-DE
+ CC-BY-NC-4.0
+ CC-BY-NC-ND-1.0
+ CC-BY-NC-ND-2.0
+ CC-BY-NC-ND-2.5
+ CC-BY-NC-ND-3.0
+ CC-BY-NC-ND-3.0-DE
+ CC-BY-NC-ND-3.0-IGO
+ CC-BY-NC-ND-4.0
+ CC-BY-NC-SA-1.0
+ CC-BY-NC-SA-2.0
+ CC-BY-NC-SA-2.0-DE
+ CC-BY-NC-SA-2.0-FR
+ CC-BY-NC-SA-2.0-UK
+ CC-BY-NC-SA-2.5
+ CC-BY-NC-SA-3.0
+ CC-BY-NC-SA-3.0-DE
+ CC-BY-NC-SA-3.0-IGO
+ CC-BY-NC-SA-4.0
+ CC-BY-ND-1.0
+ CC-BY-ND-2.0
+ CC-BY-ND-2.5
+ CC-BY-ND-3.0
+ CC-BY-ND-3.0-DE
+ CC-BY-ND-4.0
+ CC-BY-SA-1.0
+ CC-BY-SA-2.0
+ CC-BY-SA-2.0-UK
+ CC-BY-SA-2.1-JP
+ CC-BY-SA-2.5
+ CC-BY-SA-3.0
+ CC-BY-SA-3.0-AT
+ CC-BY-SA-3.0-DE
+ CC-BY-SA-3.0-IGO
+ CC-BY-SA-4.0
+ CC-PDDC
+ CC-PDM-1.0
+ CC-SA-1.0
+ CC0-1.0
+ CDDL-1.0
+ CDDL-1.1
+ CDL-1.0
+ CDLA-Permissive-1.0
+ CDLA-Permissive-2.0
+ CDLA-Sharing-1.0
+ CECILL-1.0
+ CECILL-1.1
+ CECILL-2.0
+ CECILL-2.1
+ CECILL-B
+ CECILL-C
+ CERN-OHL-1.1
+ CERN-OHL-1.2
+ CERN-OHL-P-2.0
+ CERN-OHL-S-2.0
+ CERN-OHL-W-2.0
+ CFITSIO
+ CMU-Mach
+ CMU-Mach-nodoc
+ CNRI-Jython
+ CNRI-Python
+ CNRI-Python-GPL-Compatible
+ COIL-1.0
+ CPAL-1.0
+ CPL-1.0
+ CPOL-1.02
+ CUA-OPL-1.0
+ Caldera
+ Caldera-no-preamble
+ Catharon
+ ClArtistic
+ Clips
+ Community-Spec-1.0
+ Condor-1.1
+ Cornell-Lossless-JPEG
+ Cronyx
+ Crossword
+ CryptoSwift
+ CrystalStacker
+ Cube
+ D-FSL-1.0
+ DEC-3-Clause
+ DL-DE-BY-2.0
+ DL-DE-ZERO-2.0
+ DOC
+ DRL-1.0
+ DRL-1.1
+ DSDP
+ DocBook-DTD
+ DocBook-Schema
+ DocBook-Stylesheet
+ DocBook-XML
+ Dotseqn
+ ECL-1.0
+ ECL-2.0
+ EFL-1.0
+ EFL-2.0
+ EPICS
+ EPL-1.0
+ EPL-2.0
+ ESA-PL-permissive-2.4
+ ESA-PL-strong-copyleft-2.4
+ ESA-PL-weak-copyleft-2.4
+ EUDatagrid
+ EUPL-1.0
+ EUPL-1.1
+ EUPL-1.2
+ Elastic-2.0
+ Entessa
+ ErlPL-1.1
+ Eurosym
+ FBM
+ FDK-AAC
+ FSFAP
+ FSFAP-no-warranty-disclaimer
+ FSFUL
+ FSFULLR
+ FSFULLRSD
+ FSFULLRWD
+ FSL-1.1-ALv2
+ FSL-1.1-MIT
+ FTL
+ Fair
+ Ferguson-Twofish
+ Frameworx-1.0
+ FreeBSD-DOC
+ FreeImage
+ Furuseth
+ GCR-docs
+ GD
+ GFDL-1.1-invariants-only
+ GFDL-1.1-invariants-or-later
+ GFDL-1.1-no-invariants-only
+ GFDL-1.1-no-invariants-or-later
+ GFDL-1.1-only
+ GFDL-1.1-or-later
+ GFDL-1.2-invariants-only
+ GFDL-1.2-invariants-or-later
+ GFDL-1.2-no-invariants-only
+ GFDL-1.2-no-invariants-or-later
+ GFDL-1.2-only
+ GFDL-1.2-or-later
+ GFDL-1.3-invariants-only
+ GFDL-1.3-invariants-or-later
+ GFDL-1.3-no-invariants-only
+ GFDL-1.3-no-invariants-or-later
+ GFDL-1.3-only
+ GFDL-1.3-or-later
+ GL2PS
+ GLWTPL
+ GPL-1.0-only
+ GPL-1.0-or-later
+ GPL-2.0-only
+ GPL-2.0-or-later
+ GPL-3.0-only
+ GPL-3.0-or-later
+ Game-Programming-Gems
+ Giftware
+ Glide
+ Glulxe
+ Graphics-Gems
+ Gutmann
+ HDF5
+ HIDAPI
+ HP-1986
+ HP-1989
+ HPND
+ HPND-DEC
+ HPND-Fenneberg-Livingston
+ HPND-INRIA-IMAG
+ HPND-Intel
+ HPND-Kevlin-Henney
+ HPND-MIT-disclaimer
+ HPND-Markus-Kuhn
+ HPND-Netrek
+ HPND-Pbmplus
+ HPND-SMC
+ HPND-UC
+ HPND-UC-export-US
+ HPND-doc
+ HPND-doc-sell
+ HPND-export-US
+ HPND-export-US-acknowledgement
+ HPND-export-US-modify
+ HPND-export2-US
+ HPND-merchantability-variant
+ HPND-sell-MIT-disclaimer-xserver
+ HPND-sell-regexpr
+ HPND-sell-variant
+ HPND-sell-variant-MIT-disclaimer
+ HPND-sell-variant-MIT-disclaimer-rev
+ HPND-sell-variant-critical-systems
+ HTMLTIDY
+ HaskellReport
+ Hippocratic-2.1
+ IBM-pibs
+ ICU
+ IEC-Code-Components-EULA
+ IJG
+ IJG-short
+ IPA
+ IPL-1.0
+ ISC
+ ISC-Veillard
+ ISO-permission
+ ImageMagick
+ Imlib2
+ Info-ZIP
+ Inner-Net-2.0
+ InnoSetup
+ Intel
+ Intel-ACPI
+ Interbase-1.0
+ JPL-image
+ JPNIC
+ JSON
+ Jam
+ JasPer-2.0
+ Kastrup
+ Kazlib
+ Knuth-CTAN
+ LAL-1.2
+ LAL-1.3
+ LGPL-2.0-only
+ LGPL-2.0-or-later
+ LGPL-2.1-only
+ LGPL-2.1-or-later
+ LGPL-3.0-only
+ LGPL-3.0-or-later
+ LGPLLR
+ LOOP
+ LPD-document
+ LPL-1.0
+ LPL-1.02
+ LPPL-1.0
+ LPPL-1.1
+ LPPL-1.2
+ LPPL-1.3a
+ LPPL-1.3c
+ LZMA-SDK-9.11-to-9.20
+ LZMA-SDK-9.22
+ Latex2e
+ Latex2e-translated-notice
+ Leptonica
+ LiLiQ-P-1.1
+ LiLiQ-R-1.1
+ LiLiQ-Rplus-1.1
+ Libpng
+ Linux-OpenIB
+ Linux-man-pages-1-para
+ Linux-man-pages-copyleft
+ Linux-man-pages-copyleft-2-para
+ Linux-man-pages-copyleft-var
+ Lucida-Bitmap-Fonts
+ MIPS
+ MIT
+ MIT-0
+ MIT-CMU
+ MIT-Click
+ MIT-Festival
+ MIT-Khronos-old
+ MIT-Modern-Variant
+ MIT-STK
+ MIT-Wu
+ MIT-advertising
+ MIT-enna
+ MIT-feh
+ MIT-open-group
+ MIT-testregex
+ MITNFA
+ MMIXware
+ MMPL-1.0.1
+ MPEG-SSG
+ MPL-1.0
+ MPL-1.1
+ MPL-2.0
+ MPL-2.0-no-copyleft-exception
+ MS-LPL
+ MS-PL
+ MS-RL
+ MTLL
+ Mackerras-3-Clause
+ Mackerras-3-Clause-acknowledgment
+ MakeIndex
+ Martin-Birgmeier
+ McPhee-slideshow
+ Minpack
+ MirOS
+ Motosoto
+ MulanPSL-1.0
+ MulanPSL-2.0
+ Multics
+ Mup
+ NAIST-2003
+ NASA-1.3
+ NBPL-1.0
+ NCBI-PD
+ NCGL-UK-2.0
+ NCL
+ NCSA
+ NGPL
+ NICTA-1.0
+ NIST-PD
+ NIST-PD-TNT
+ NIST-PD-fallback
+ NIST-Software
+ NLOD-1.0
+ NLOD-2.0
+ NLPL
+ NOSL
+ NPL-1.0
+ NPL-1.1
+ NPOSL-3.0
+ NRL
+ NTIA-PD
+ NTP
+ NTP-0
+ Naumen
+ NetCDF
+ Newsletr
+ Nokia
+ Noweb
+ O-UDA-1.0
+ OAR
+ OCCT-PL
+ OCLC-2.0
+ ODC-By-1.0
+ ODbL-1.0
+ OFFIS
+ OFL-1.0
+ OFL-1.0-RFN
+ OFL-1.0-no-RFN
+ OFL-1.1
+ OFL-1.1-RFN
+ OFL-1.1-no-RFN
+ OGC-1.0
+ OGDL-Taiwan-1.0
+ OGL-Canada-2.0
+ OGL-UK-1.0
+ OGL-UK-2.0
+ OGL-UK-3.0
+ OGTSL
+ OLDAP-1.1
+ OLDAP-1.2
+ OLDAP-1.3
+ OLDAP-1.4
+ OLDAP-2.0
+ OLDAP-2.0.1
+ OLDAP-2.1
+ OLDAP-2.2
+ OLDAP-2.2.1
+ OLDAP-2.2.2
+ OLDAP-2.3
+ OLDAP-2.4
+ OLDAP-2.5
+ OLDAP-2.6
+ OLDAP-2.7
+ OLDAP-2.8
+ OLFL-1.3
+ OML
+ OPL-1.0
+ OPL-UK-3.0
+ OPUBL-1.0
+ OSC-1.0
+ OSET-PL-2.1
+ OSL-1.0
+ OSL-1.1
+ OSL-2.0
+ OSL-2.1
+ OSL-3.0
+ OSSP
+ OpenMDW-1.0
+ OpenPBS-2.3
+ OpenSSL
+ OpenSSL-standalone
+ OpenVision
+ PADL
+ PDDL-1.0
+ PHP-3.0
+ PHP-3.01
+ PPL
+ PSF-2.0
+ ParaType-Free-Font-1.3
+ Parity-6.0.0
+ Parity-7.0.0
+ Pixar
+ Plexus
+ PolyForm-Noncommercial-1.0.0
+ PolyForm-Small-Business-1.0.0
+ PostgreSQL
+ Python-2.0
+ Python-2.0.1
+ QPL-1.0
+ QPL-1.0-INRIA-2004
+ Qhull
+ RHeCos-1.1
+ RPL-1.1
+ RPL-1.5
+ RPSL-1.0
+ RSA-MD
+ RSCPL
+ Rdisc
+ Ruby
+ Ruby-pty
+ SAX-PD
+ SAX-PD-2.0
+ SCEA
+ SGI-B-1.0
+ SGI-B-1.1
+ SGI-B-2.0
+ SGI-OpenGL
+ SGMLUG-PM
+ SGP4
+ SHL-0.5
+ SHL-0.51
+ SISSL
+ SISSL-1.2
+ SL
+ SMAIL-GPL
+ SMLNJ
+ SMPPL
+ SNIA
+ SOFA
+ SPL-1.0
+ SSH-OpenSSH
+ SSH-short
+ SSLeay-standalone
+ SSPL-1.0
+ SUL-1.0
+ SWL
+ Saxpath
+ SchemeReport
+ Sendmail
+ Sendmail-8.23
+ Sendmail-Open-Source-1.1
+ SimPL-2.0
+ Sleepycat
+ Soundex
+ Spencer-86
+ Spencer-94
+ Spencer-99
+ SugarCRM-1.1.3
+ Sun-PPP
+ Sun-PPP-2000
+ SunPro
+ Symlinks
+ TAPR-OHL-1.0
+ TCL
+ TCP-wrappers
+ TGPPL-1.0
+ TMate
+ TORQUE-1.1
+ TOSL
+ TPDL
+ TPL-1.0
+ TTWL
+ TTYP0
+ TU-Berlin-1.0
+ TU-Berlin-2.0
+ TekHVC
+ TermReadKey
+ ThirdEye
+ TrustedQSL
+ UCAR
+ UCL-1.0
+ UMich-Merit
+ UPL-1.0
+ URT-RLE
+ Ubuntu-font-1.0
+ UnRAR
+ Unicode-3.0
+ Unicode-DFS-2015
+ Unicode-DFS-2016
+ Unicode-TOU
+ UnixCrypt
+ Unlicense
+ Unlicense-libtelnet
+ Unlicense-libwhirlpool
+ VOSTROM
+ VSL-1.0
+ Vim
+ Vixie-Cron
+ W3C
+ W3C-19980720
+ W3C-20150513
+ WTFNMFPL
+ WTFPL
+ Watcom-1.0
+ Widget-Workshop
+ WordNet
+ Wsuipa
+ X11
+ X11-distribute-modifications-variant
+ X11-no-permit-persons
+ X11-swapped
+ XFree86-1.1
+ XSkat
+ Xdebug-1.03
+ Xerox
+ Xfig
+ Xnet
+ YPL-1.0
+ YPL-1.1
+ ZPL-1.1
+ ZPL-2.0
+ ZPL-2.1
+ Zed
+ Zeeff
+ Zend-2.0
+ Zimbra-1.3
+ Zimbra-1.4
+ Zlib
+ any-OSI
+ any-OSI-perl-modules
+ bcrypt-Solar-Designer
+ blessing
+ bzip2-1.0.6
+ check-cvs
+ checkmk
+ copyleft-next-0.3.0
+ copyleft-next-0.3.1
+ curl
+ cve-tou
+ diffmark
+ dtoa
+ dvipdfm
+ eGenix
+ etalab-2.0
+ fwlw
+ gSOAP-1.3b
+ generic-xts
+ gnuplot
+ gtkbook
+ hdparm
+ hyphen-bulgarian
+ iMatix
+ jove
+ libpng-1.6.35
+ libpng-2.0
+ libselinux-1.0
+ libtiff
+ libutil-David-Nugent
+ lsof
+ magaz
+ mailprio
+ man2html
+ metamail
+ mpi-permissive
+ mpich2
+ mplus
+ ngrep
+ pkgconf
+ pnmstitch
+ psfrag
+ psutils
+ python-ldap
+ radvd
+ snprintf
+ softSurfer
+ ssh-keyscan
+ swrule
+ threeparttable
+ ulem
+ w3m
+ wwl
+ xinetd
+ xkeyboard-config-Zinoviev
+ xlock
+ xpp
+ xzoom
+ zlib-acknowledgement
+ ].freeze
+
+ DEPRECATED_LICENSE_IDENTIFIERS = %w[
+ AGPL-1.0
+ AGPL-3.0
+ BSD-2-Clause-FreeBSD
+ BSD-2-Clause-NetBSD
+ GFDL-1.1
+ GFDL-1.2
+ GFDL-1.3
+ GPL-1.0
+ GPL-1.0+
+ GPL-2.0
+ GPL-2.0+
+ GPL-2.0-with-GCC-exception
+ GPL-2.0-with-autoconf-exception
+ GPL-2.0-with-bison-exception
+ GPL-2.0-with-classpath-exception
+ GPL-2.0-with-font-exception
+ GPL-3.0
+ GPL-3.0+
+ GPL-3.0-with-GCC-exception
+ GPL-3.0-with-autoconf-exception
+ LGPL-2.0
+ LGPL-2.0+
+ LGPL-2.1
+ LGPL-2.1+
+ LGPL-3.0
+ LGPL-3.0+
+ Net-SNMP
+ Nunit
+ StandardML-NJ
+ bzip2-1.0.5
+ eCos-2.0
+ wxWindows
+ ].freeze
+
+ # exception identifiers
+ EXCEPTION_IDENTIFIERS = %w[
+ 389-exception
+ Asterisk-exception
+ Asterisk-linking-protocols-exception
+ Autoconf-exception-2.0
+ Autoconf-exception-3.0
+ Autoconf-exception-generic
+ Autoconf-exception-generic-3.0
+ Autoconf-exception-macro
+ Bison-exception-1.24
+ Bison-exception-2.2
+ Bootloader-exception
+ CGAL-linking-exception
+ CLISP-exception-2.0
+ Classpath-exception-2.0
+ Classpath-exception-2.0-short
+ DigiRule-FOSS-exception
+ Digia-Qt-LGPL-exception-1.1
+ FLTK-exception
+ Fawkes-Runtime-exception
+ Font-exception-2.0
+ GCC-exception-2.0
+ GCC-exception-2.0-note
+ GCC-exception-3.1
+ GNAT-exception
+ GNOME-examples-exception
+ GNU-compiler-exception
+ GPL-3.0-389-ds-base-exception
+ GPL-3.0-interface-exception
+ GPL-3.0-linking-exception
+ GPL-3.0-linking-source-exception
+ GPL-CC-1.0
+ GStreamer-exception-2005
+ GStreamer-exception-2008
+ Gmsh-exception
+ Independent-modules-exception
+ KiCad-libraries-exception
+ LGPL-3.0-linking-exception
+ LLGPL
+ LLVM-exception
+ LZMA-exception
+ Libtool-exception
+ Linux-syscall-note
+ OCCT-exception-1.0
+ OCaml-LGPL-linking-exception
+ OpenJDK-assembly-exception-1.0
+ PCRE2-exception
+ PS-or-PDF-font-exception-20170817
+ QPL-1.0-INRIA-2004-exception
+ Qt-GPL-exception-1.0
+ Qt-LGPL-exception-1.1
+ Qwt-exception-1.0
+ RRDtool-FLOSS-exception-2.0
+ SANE-exception
+ SHL-2.0
+ SHL-2.1
+ SWI-exception
+ Simple-Library-Usage-exception
+ Swift-exception
+ Texinfo-exception
+ UBDL-exception
+ Universal-FOSS-exception-1.0
+ WxWindows-exception-3.1
+ cryptsetup-OpenSSL-exception
+ eCos-exception-2.0
+ erlang-otp-linking-exception
+ fmt-exception
+ freertos-exception-2.0
+ gnu-javamail-exception
+ harbour-exception
+ i2p-gpl-java-exception
+ kvirc-openssl-exception
+ libpri-OpenH323-exception
+ mif-exception
+ mxml-exception
+ openvpn-openssl-exception
+ polyparse-exception
+ romic-exception
+ rsync-linking-exception
+ sqlitestudio-OpenSSL-exception
+ stunnel-exception
+ u-boot-exception-2.0
+ vsftpd-openssl-exception
+ x11vnc-openssl-exception
+ ].freeze
+
+ DEPRECATED_EXCEPTION_IDENTIFIERS = %w[
+ Nokia-Qt-exception-1.1
+ ].freeze
+
+ VALID_REGEXP = /
+ \A
+ (?:
+ #{Regexp.union(LICENSE_IDENTIFIERS)}
+ \+?
+ (?:\s WITH \s #{Regexp.union(EXCEPTION_IDENTIFIERS)})?
+ | #{NONSTANDARD}
+ | #{LICENSE_REF}
+ )
+ \Z
+ /ox
+
+ DEPRECATED_LICENSE_REGEXP = /
+ \A
+ #{Regexp.union(DEPRECATED_LICENSE_IDENTIFIERS)}
+ \+?
+ (?:\s WITH \s .+?)?
+ \Z
+ /ox
+
+ DEPRECATED_EXCEPTION_REGEXP = /
+ \A
+ .+?
+ \+?
+ (?:\s WITH \s #{Regexp.union(DEPRECATED_EXCEPTION_IDENTIFIERS)})
+ \Z
+ /ox
+
+ def self.match?(license)
+ VALID_REGEXP.match?(license)
+ end
+
+ def self.deprecated_license_id?(license)
+ DEPRECATED_LICENSE_REGEXP.match?(license)
+ end
+
+ def self.deprecated_exception_id?(license)
+ DEPRECATED_EXCEPTION_REGEXP.match?(license)
+ end
+
+ def self.suggestions(license)
+ by_distance = LICENSE_IDENTIFIERS.group_by do |identifier|
+ levenshtein_distance(identifier, license)
+ end
+ lowest = by_distance.keys.min
+ return unless lowest < license.size
+ by_distance[lowest]
+ end
+end
diff --git a/lib/rubygems/validator.rb b/lib/rubygems/validator.rb
index 4dd12ad4df..eb5b513570 100644
--- a/lib/rubygems/validator.rb
+++ b/lib/rubygems/validator.rb
@@ -1,74 +1,50 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'find'
-
-require 'rubygems/digest/md5'
-require 'rubygems/format'
-require 'rubygems/installer'
+require_relative "package"
+require_relative "installer"
##
# Validator performs various gem file and gem database validation
class Gem::Validator
-
include Gem::UserInteraction
- ##
- # Given a gem file's contents, validates against its own MD5 checksum
- # gem_data:: [String] Contents of the gem file
-
- def verify_gem(gem_data)
- raise Gem::VerificationError, 'empty gem file' if gem_data.size == 0
-
- unless gem_data =~ /MD5SUM/ then
- return # Don't worry about it...this sucks. Need to fix MD5 stuff for
- # new format
- # FIXME
- end
-
- sum_data = gem_data.gsub(/MD5SUM = "([a-z0-9]+)"/,
- "MD5SUM = \"#{"F" * 32}\"")
-
- unless Gem::MD5.hexdigest(sum_data) == $1.to_s then
- raise Gem::VerificationError, 'invalid checksum for gem file'
- end
- end
-
- ##
- # Given the path to a gem file, validates against its own MD5 checksum
- #
- # gem_path:: [String] Path to gem file
-
- def verify_gem_file(gem_path)
- open gem_path, Gem.binary_mode do |file|
- gem_data = file.read
- verify_gem gem_data
- end
- rescue Errno::ENOENT
- raise Gem::VerificationError, "missing gem file #{gem_path}"
+ def initialize # :nodoc:
+ require "find"
end
private
def find_files_for_gem(gem_directory)
installed_files = []
- Find.find(gem_directory) {|file_name|
- fn = file_name.slice((gem_directory.size)..(file_name.size-1)).sub(/^\//, "")
- if(!(fn =~ /CVS/ || File.directory?(fn) || fn == "")) then
- installed_files << fn
- end
- }
+ Find.find gem_directory do |file_name|
+ fn = file_name[gem_directory.size..file_name.size - 1].sub(%r{^/}, "")
+ installed_files << fn unless
+ fn.empty? || fn.include?("CVS") || File.directory?(file_name)
+ end
+
installed_files
end
public
- ErrorData = Struct.new :path, :problem
+ ##
+ # Describes a problem with a file in a gem.
+
+ ErrorData = Struct.new :path, :problem do
+ def <=>(other) # :nodoc:
+ return nil unless self.class === other
+
+ [path, problem] <=> [other.path, other.problem]
+ end
+ end
##
# Checks the gem directory for the following potential
@@ -80,130 +56,89 @@ class Gem::Validator
# * 1 cache - 1 spec - 1 directory.
#
# returns a hash of ErrorData objects, keyed on the problem gem's name.
+ #--
+ # TODO needs further cleanup
- def alien
- errors = {}
+ def alien(gems = [])
+ errors = Hash.new {|h,k| h[k] = {} }
- Gem::SourceIndex.from_installed_gems.each do |gem_name, gem_spec|
- errors[gem_name] ||= []
+ Gem::Specification.each do |spec|
+ unless gems.empty?
+ next unless gems.include? spec.name
+ end
+ next if spec.default_gem?
- gem_path = File.join(Gem.dir, "cache", gem_spec.full_name) + ".gem"
- spec_path = File.join(Gem.dir, "specifications", gem_spec.full_name) + ".gemspec"
- gem_directory = File.join(Gem.dir, "gems", gem_spec.full_name)
+ gem_name = spec.file_name
+ gem_path = spec.cache_file
+ spec_path = spec.spec_file
+ gem_directory = spec.full_gem_path
- installed_files = find_files_for_gem(gem_directory)
+ unless File.directory? gem_directory
+ errors[gem_name][spec.full_name] =
+ "Gem registered but doesn't exist at #{gem_directory}"
+ next
+ end
- unless File.exist? spec_path then
- errors[gem_name] << ErrorData.new(spec_path, "Spec file doesn't exist for installed gem")
+ unless File.exist? spec_path
+ errors[gem_name][spec_path] = "Spec file missing for installed gem"
end
begin
- verify_gem_file(gem_path)
+ unless File.readable?(gem_path)
+ raise Gem::VerificationError, "missing gem file #{gem_path}"
+ end
- open gem_path, Gem.binary_mode do |file|
- format = Gem::Format.from_file_by_path(gem_path)
- format.file_entries.each do |entry, data|
- # Found this file. Delete it from list
- installed_files.delete remove_leading_dot_dir(entry['path'])
+ good, gone, unreadable = nil, nil, nil, nil
- next unless data # HACK `gem check -a mkrf`
+ File.open gem_path, Gem.binary_mode do |_file|
+ package = Gem::Package.new gem_path
- open File.join(gem_directory, entry['path']), Gem.binary_mode do |f|
- unless Gem::MD5.hexdigest(f.read).to_s ==
- Gem::MD5.hexdigest(data).to_s then
- errors[gem_name] << ErrorData.new(entry['path'], "installed file doesn't match original from gem")
- end
- end
+ good, gone = package.contents.partition do |file_name|
+ File.exist? File.join(gem_directory, file_name)
end
- end
- rescue Gem::VerificationError => e
- errors[gem_name] << ErrorData.new(gem_path, e.message)
- end
- # Clean out directories that weren't explicitly included in the gemspec
- # FIXME: This still allows arbitrary incorrect directories.
- installed_files.delete_if {|potential_directory|
- File.directory?(File.join(gem_directory, potential_directory))
- }
- if(installed_files.size > 0) then
- errors[gem_name] << ErrorData.new(gem_path, "Unmanaged files in gem: #{installed_files.inspect}")
- end
- end
+ gone.sort.each do |path|
+ errors[gem_name][path] = "Missing file"
+ end
- errors
- end
+ good, unreadable = good.partition do |file_name|
+ File.readable? File.join(gem_directory, file_name)
+ end
- if RUBY_VERSION < '1.9' then
- class TestRunner
- def initialize(suite, ui)
- @suite = suite
- @ui = ui
- end
+ unreadable.sort.each do |path|
+ errors[gem_name][path] = "Unreadable file"
+ end
- def self.run(suite, ui)
- require 'test/unit/ui/testrunnermediator'
- return new(suite, ui).start
- end
+ good.each do |entry, data|
+ next unless data # HACK: `gem check -a mkrf`
- def start
- @mediator = Test::Unit::UI::TestRunnerMediator.new(@suite)
- @mediator.add_listener(Test::Unit::TestResult::FAULT, &method(:add_fault))
- return @mediator.run_suite
- end
+ source = File.join gem_directory, entry["path"]
- def add_fault(fault)
- if Gem.configuration.verbose then
- @ui.say fault.long_display
+ File.open source, Gem.binary_mode do |f|
+ unless f.read == data
+ errors[gem_name][entry["path"]] = "Modified from original"
+ end
+ end
+ end
end
- end
- end
-
- autoload :TestRunner, 'test/unit/ui/testrunnerutilities'
- end
-
- ##
- # Runs unit tests for a given gem specification
-
- def unit_test(gem_spec)
- start_dir = Dir.pwd
- Dir.chdir(gem_spec.full_gem_path)
- $: << File.join(Gem.dir, "gems", gem_spec.full_name)
- # XXX: why do we need this gem_spec when we've already got 'spec'?
- test_files = gem_spec.test_files
-
- if test_files.empty? then
- say "There are no unit tests to run for #{gem_spec.full_name}"
- return nil
- end
- gem gem_spec.name, "= #{gem_spec.version.version}"
+ installed_files = find_files_for_gem(gem_directory)
+ extras = installed_files - good - unreadable
- test_files.each do |f| require f end
-
- if RUBY_VERSION < '1.9' then
- suite = Test::Unit::TestSuite.new("#{gem_spec.name}-#{gem_spec.version}")
-
- ObjectSpace.each_object(Class) do |klass|
- suite << klass.suite if (klass < Test::Unit::TestCase)
+ extras.each do |extra|
+ errors[gem_name][extra] = "Extra file"
+ end
+ rescue Gem::VerificationError => e
+ errors[gem_name][gem_path] = e.message
end
-
- result = TestRunner.run suite, ui
-
- alert_error result.to_s unless result.passed?
- else
- result = MiniTest::Unit.new
- result.run
end
- result
- ensure
- Dir.chdir(start_dir)
- end
+ errors.each do |name, subhash|
+ errors[name] = subhash.map do |path, msg|
+ ErrorData.new path, msg
+ end.sort
+ end
- private
- def remove_leading_dot_dir(path)
- path.sub(/^\.\//, "")
+ errors
end
-
end
-
diff --git a/lib/rubygems/vendor/.document b/lib/rubygems/vendor/.document
new file mode 100644
index 0000000000..0c43bbd6b3
--- /dev/null
+++ b/lib/rubygems/vendor/.document
@@ -0,0 +1 @@
+# Vendored files do not need to be documented
diff --git a/lib/rubygems/vendor/net-http/lib/net/http.rb b/lib/rubygems/vendor/net-http/lib/net/http.rb
new file mode 100644
index 0000000000..4800cd25f1
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http.rb
@@ -0,0 +1,2608 @@
+# frozen_string_literal: true
+#
+# = net/http.rb
+#
+# Copyright (c) 1999-2007 Yukihiro Matsumoto
+# Copyright (c) 1999-2007 Minero Aoki
+# Copyright (c) 2001 GOTOU Yuuzou
+#
+# Written and maintained by Minero Aoki <aamine@loveruby.net>.
+# HTTPS support added by GOTOU Yuuzou <gotoyuzo@notwork.org>.
+#
+# This file is derived from "http-access.rb".
+#
+# Documented by Minero Aoki; converted to RDoc by William Webber.
+#
+# This program is free software. You can re-distribute and/or
+# modify this program under the same terms of ruby itself ---
+# Ruby Distribution License or GNU General Public License.
+#
+# See Gem::Net::HTTP for an overview and examples.
+#
+
+require_relative '../../../net-protocol/lib/net/protocol'
+require_relative '../../../uri/lib/uri'
+require_relative '../../../resolv/lib/resolv'
+autoload :OpenSSL, 'openssl'
+
+module Gem::Net #:nodoc:
+
+ # :stopdoc:
+ class HTTPBadResponse < StandardError; end
+ class HTTPHeaderSyntaxError < StandardError; end
+ # :startdoc:
+
+ # \Class \Gem::Net::HTTP provides a rich library that implements the client
+ # in a client-server model that uses the \HTTP request-response protocol.
+ # For information about \HTTP, see:
+ #
+ # - {Hypertext Transfer Protocol}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol].
+ # - {Technical overview}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Technical_overview].
+ #
+ # == About the Examples
+ #
+ # :include: doc/net-http/examples.rdoc
+ #
+ # == Strategies
+ #
+ # - If you will make only a few GET requests,
+ # consider using {OpenURI}[https://docs.ruby-lang.org/en/master/OpenURI.html].
+ # - If you will make only a few requests of all kinds,
+ # consider using the various singleton convenience methods in this class.
+ # Each of the following methods automatically starts and finishes
+ # a {session}[rdoc-ref:Gem::Net::HTTP@Sessions] that sends a single request:
+ #
+ # # Return string response body.
+ # Gem::Net::HTTP.get(hostname, path)
+ # Gem::Net::HTTP.get(uri)
+ #
+ # # Write string response body to $stdout.
+ # Gem::Net::HTTP.get_print(hostname, path)
+ # Gem::Net::HTTP.get_print(uri)
+ #
+ # # Return response as Gem::Net::HTTPResponse object.
+ # Gem::Net::HTTP.get_response(hostname, path)
+ # Gem::Net::HTTP.get_response(uri)
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # Gem::Net::HTTP.post(uri, data)
+ # params = {title: 'foo', body: 'bar', userId: 1}
+ # Gem::Net::HTTP.post_form(uri, params)
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # Gem::Net::HTTP.put(uri, data)
+ #
+ # - If performance is important, consider using sessions, which lower request overhead.
+ # This {session}[rdoc-ref:Gem::Net::HTTP@Sessions] has multiple requests for
+ # {HTTP methods}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods]
+ # and {WebDAV methods}[https://en.wikipedia.org/wiki/WebDAV#Implementation]:
+ #
+ # Gem::Net::HTTP.start(hostname) do |http|
+ # # Session started automatically before block execution.
+ # http.get(path)
+ # http.head(path)
+ # body = 'Some text'
+ # http.post(path, body) # Can also have a block.
+ # http.put(path, body)
+ # http.delete(path)
+ # http.options(path)
+ # http.trace(path)
+ # http.patch(path, body) # Can also have a block.
+ # http.copy(path)
+ # http.lock(path, body)
+ # http.mkcol(path, body)
+ # http.move(path)
+ # http.propfind(path, body)
+ # http.proppatch(path, body)
+ # http.unlock(path, body)
+ # # Session finished automatically at block exit.
+ # end
+ #
+ # The methods cited above are convenience methods that, via their few arguments,
+ # allow minimal control over the requests.
+ # For greater control, consider using {request objects}[rdoc-ref:Gem::Net::HTTPRequest].
+ #
+ # == URIs
+ #
+ # On the internet, a Gem::URI
+ # ({Universal Resource Identifier}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier])
+ # is a string that identifies a particular resource.
+ # It consists of some or all of: scheme, hostname, path, query, and fragment;
+ # see {Gem::URI syntax}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax].
+ #
+ # A Ruby {Gem::URI::Generic}[https://docs.ruby-lang.org/en/master/Gem::URI/Generic.html] object
+ # represents an internet Gem::URI.
+ # It provides, among others, methods
+ # +scheme+, +hostname+, +path+, +query+, and +fragment+.
+ #
+ # === Schemes
+ #
+ # An internet \Gem::URI has
+ # a {scheme}[https://en.wikipedia.org/wiki/List_of_URI_schemes].
+ #
+ # The two schemes supported in \Gem::Net::HTTP are <tt>'https'</tt> and <tt>'http'</tt>:
+ #
+ # uri.scheme # => "https"
+ # Gem::URI('http://example.com').scheme # => "http"
+ #
+ # === Hostnames
+ #
+ # A hostname identifies a server (host) to which requests may be sent:
+ #
+ # hostname = uri.hostname # => "jsonplaceholder.typicode.com"
+ # Gem::Net::HTTP.start(hostname) do |http|
+ # # Some HTTP stuff.
+ # end
+ #
+ # === Paths
+ #
+ # A host-specific path identifies a resource on the host:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/todos/1'
+ # hostname = _uri.hostname
+ # path = _uri.path
+ # Gem::Net::HTTP.get(hostname, path)
+ #
+ # === Queries
+ #
+ # A host-specific query adds name/value pairs to the Gem::URI:
+ #
+ # _uri = uri.dup
+ # params = {userId: 1, completed: false}
+ # _uri.query = Gem::URI.encode_www_form(params)
+ # _uri # => #<Gem::URI::HTTPS https://jsonplaceholder.typicode.com?userId=1&completed=false>
+ # Gem::Net::HTTP.get(_uri)
+ #
+ # === Fragments
+ #
+ # A {Gem::URI fragment}[https://en.wikipedia.org/wiki/URI_fragment] has no effect
+ # in \Gem::Net::HTTP;
+ # the same data is returned, regardless of whether a fragment is included.
+ #
+ # == Request Headers
+ #
+ # Request headers may be used to pass additional information to the host,
+ # similar to arguments passed in a method call;
+ # each header is a name/value pair.
+ #
+ # Each of the \Gem::Net::HTTP methods that sends a request to the host
+ # has optional argument +headers+,
+ # where the headers are expressed as a hash of field-name/value pairs:
+ #
+ # headers = {Accept: 'application/json', Connection: 'Keep-Alive'}
+ # Gem::Net::HTTP.get(uri, headers)
+ #
+ # See lists of both standard request fields and common request fields at
+ # {Request Fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields].
+ # A host may also accept other custom fields.
+ #
+ # == \HTTP Sessions
+ #
+ # A _session_ is a connection between a server (host) and a client that:
+ #
+ # - Is begun by instance method Gem::Net::HTTP#start.
+ # - May contain any number of requests.
+ # - Is ended by instance method Gem::Net::HTTP#finish.
+ #
+ # See example sessions at {Strategies}[rdoc-ref:Gem::Net::HTTP@Strategies].
+ #
+ # === Session Using \Gem::Net::HTTP.start
+ #
+ # If you have many requests to make to a single host (and port),
+ # consider using singleton method Gem::Net::HTTP.start with a block;
+ # the method handles the session automatically by:
+ #
+ # - Calling #start before block execution.
+ # - Executing the block.
+ # - Calling #finish after block execution.
+ #
+ # In the block, you can use these instance methods,
+ # each of which that sends a single request:
+ #
+ # - {HTTP methods}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods]:
+ #
+ # - #get, #request_get: GET.
+ # - #head, #request_head: HEAD.
+ # - #post, #request_post: POST.
+ # - #delete: DELETE.
+ # - #options: OPTIONS.
+ # - #trace: TRACE.
+ # - #patch: PATCH.
+ #
+ # - {WebDAV methods}[https://en.wikipedia.org/wiki/WebDAV#Implementation]:
+ #
+ # - #copy: COPY.
+ # - #lock: LOCK.
+ # - #mkcol: MKCOL.
+ # - #move: MOVE.
+ # - #propfind: PROPFIND.
+ # - #proppatch: PROPPATCH.
+ # - #unlock: UNLOCK.
+ #
+ # === Session Using \Gem::Net::HTTP.start and \Gem::Net::HTTP.finish
+ #
+ # You can manage a session manually using methods #start and #finish:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.start
+ # http.get('/todos/1')
+ # http.get('/todos/2')
+ # http.delete('/posts/1')
+ # http.finish # Needed to free resources.
+ #
+ # === Single-Request Session
+ #
+ # Certain convenience methods automatically handle a session by:
+ #
+ # - Creating an \HTTP object
+ # - Starting a session.
+ # - Sending a single request.
+ # - Finishing the session.
+ # - Destroying the object.
+ #
+ # Such methods that send GET requests:
+ #
+ # - ::get: Returns the string response body.
+ # - ::get_print: Writes the string response body to $stdout.
+ # - ::get_response: Returns a Gem::Net::HTTPResponse object.
+ #
+ # Such methods that send POST requests:
+ #
+ # - ::post: Posts data to the host.
+ # - ::post_form: Posts form data to the host.
+ #
+ # == \HTTP Requests and Responses
+ #
+ # Many of the methods above are convenience methods,
+ # each of which sends a request and returns a string
+ # without directly using \Gem::Net::HTTPRequest and \Gem::Net::HTTPResponse objects.
+ #
+ # You can, however, directly create a request object, send the request,
+ # and retrieve the response object; see:
+ #
+ # - Gem::Net::HTTPRequest.
+ # - Gem::Net::HTTPResponse.
+ #
+ # == Following Redirection
+ #
+ # Each returned response is an instance of a subclass of Gem::Net::HTTPResponse.
+ # See the {response class hierarchy}[rdoc-ref:Gem::Net::HTTPResponse@Response+Subclasses].
+ #
+ # In particular, class Gem::Net::HTTPRedirection is the parent
+ # of all redirection classes.
+ # This allows you to craft a case statement to handle redirections properly:
+ #
+ # def fetch(uri, limit = 10)
+ # # You should choose a better exception.
+ # raise ArgumentError, 'Too many HTTP redirects' if limit == 0
+ #
+ # res = Gem::Net::HTTP.get_response(Gem::URI(uri))
+ # case res
+ # when Gem::Net::HTTPSuccess # Any success class.
+ # res
+ # when Gem::Net::HTTPRedirection # Any redirection class.
+ # location = res['Location']
+ # warn "Redirected to #{location}"
+ # fetch(location, limit - 1)
+ # else # Any other class.
+ # res.value
+ # end
+ # end
+ #
+ # fetch(uri)
+ #
+ # == Basic Authentication
+ #
+ # Basic authentication is performed according to
+ # {RFC2617}[http://www.ietf.org/rfc/rfc2617.txt]:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.basic_auth('user', 'pass')
+ # res = Gem::Net::HTTP.start(hostname) do |http|
+ # http.request(req)
+ # end
+ #
+ # == Streaming Response Bodies
+ #
+ # By default \Gem::Net::HTTP reads an entire response into memory. If you are
+ # handling large files or wish to implement a progress bar you can instead
+ # stream the body directly to an IO.
+ #
+ # Gem::Net::HTTP.start(hostname) do |http|
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # http.request(req) do |res|
+ # open('t.tmp', 'w') do |f|
+ # res.read_body do |chunk|
+ # f.write chunk
+ # end
+ # end
+ # end
+ # end
+ #
+ # == HTTPS
+ #
+ # HTTPS is enabled for an \HTTP connection by Gem::Net::HTTP#use_ssl=:
+ #
+ # Gem::Net::HTTP.start(hostname, :use_ssl => true) do |http|
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # res = http.request(req)
+ # end
+ #
+ # Or if you simply want to make a GET request, you may pass in a Gem::URI
+ # object that has an \HTTPS URL. \Gem::Net::HTTP automatically turns on TLS
+ # verification if the Gem::URI object has a 'https' Gem::URI scheme:
+ #
+ # uri # => #<Gem::URI::HTTPS https://jsonplaceholder.typicode.com/>
+ # Gem::Net::HTTP.get(uri)
+ #
+ # == Proxy Server
+ #
+ # An \HTTP object can have
+ # a {proxy server}[https://en.wikipedia.org/wiki/Proxy_server].
+ #
+ # You can create an \HTTP object with a proxy server
+ # using method Gem::Net::HTTP.new or method Gem::Net::HTTP.start.
+ #
+ # The proxy may be defined either by argument +p_addr+
+ # or by environment variable <tt>'http_proxy'</tt>.
+ #
+ # === Proxy Using Argument +p_addr+ as a \String
+ #
+ # When argument +p_addr+ is a string hostname,
+ # the returned +http+ has the given host as its proxy:
+ #
+ # http = Gem::Net::HTTP.new(hostname, nil, 'proxy.example')
+ # http.proxy? # => true
+ # http.proxy_from_env? # => false
+ # http.proxy_address # => "proxy.example"
+ # # These use default values.
+ # http.proxy_port # => 80
+ # http.proxy_user # => nil
+ # http.proxy_pass # => nil
+ #
+ # The port, username, and password for the proxy may also be given:
+ #
+ # http = Gem::Net::HTTP.new(hostname, nil, 'proxy.example', 8000, 'pname', 'ppass')
+ # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false>
+ # http.proxy? # => true
+ # http.proxy_from_env? # => false
+ # http.proxy_address # => "proxy.example"
+ # http.proxy_port # => 8000
+ # http.proxy_user # => "pname"
+ # http.proxy_pass # => "ppass"
+ #
+ # === Proxy Using '<tt>ENV['http_proxy']</tt>'
+ #
+ # When environment variable <tt>'http_proxy'</tt>
+ # is set to a \Gem::URI string,
+ # the returned +http+ will have the server at that Gem::URI as its proxy;
+ # note that the \Gem::URI string must have a protocol
+ # such as <tt>'http'</tt> or <tt>'https'</tt>:
+ #
+ # ENV['http_proxy'] = 'http://example.com'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.proxy? # => true
+ # http.proxy_from_env? # => true
+ # http.proxy_address # => "example.com"
+ # # These use default values.
+ # http.proxy_port # => 80
+ # http.proxy_user # => nil
+ # http.proxy_pass # => nil
+ #
+ # The \Gem::URI string may include proxy username, password, and port number:
+ #
+ # ENV['http_proxy'] = 'http://pname:ppass@example.com:8000'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.proxy? # => true
+ # http.proxy_from_env? # => true
+ # http.proxy_address # => "example.com"
+ # http.proxy_port # => 8000
+ # http.proxy_user # => "pname"
+ # http.proxy_pass # => "ppass"
+ #
+ # === Filtering Proxies
+ #
+ # With method Gem::Net::HTTP.new (but not Gem::Net::HTTP.start),
+ # you can use argument +p_no_proxy+ to filter proxies:
+ #
+ # - Reject a certain address:
+ #
+ # http = Gem::Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example')
+ # http.proxy_address # => nil
+ #
+ # - Reject certain domains or subdomains:
+ #
+ # http = Gem::Net::HTTP.new('example.com', nil, 'my.proxy.example', 8000, 'pname', 'ppass', 'proxy.example')
+ # http.proxy_address # => nil
+ #
+ # - Reject certain addresses and port combinations:
+ #
+ # http = Gem::Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example:1234')
+ # http.proxy_address # => "proxy.example"
+ #
+ # http = Gem::Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example:8000')
+ # http.proxy_address # => nil
+ #
+ # - Reject a list of the types above delimited using a comma:
+ #
+ # http = Gem::Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'my.proxy,proxy.example:8000')
+ # http.proxy_address # => nil
+ #
+ # http = Gem::Net::HTTP.new('example.com', nil, 'my.proxy', 8000, 'pname', 'ppass', 'my.proxy,proxy.example:8000')
+ # http.proxy_address # => nil
+ #
+ # == Compression and Decompression
+ #
+ # \Gem::Net::HTTP does not compress the body of a request before sending.
+ #
+ # By default, \Gem::Net::HTTP adds header <tt>'Accept-Encoding'</tt>
+ # to a new {request object}[rdoc-ref:Gem::Net::HTTPRequest]:
+ #
+ # Gem::Net::HTTP::Get.new(uri)['Accept-Encoding']
+ # # => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
+ #
+ # This requests the server to zip-encode the response body if there is one;
+ # the server is not required to do so.
+ #
+ # \Gem::Net::HTTP does not automatically decompress a response body
+ # if the response has header <tt>'Content-Range'</tt>.
+ #
+ # Otherwise decompression (or not) depends on the value of header
+ # {Content-Encoding}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-encoding-response-header]:
+ #
+ # - <tt>'deflate'</tt>, <tt>'gzip'</tt>, or <tt>'x-gzip'</tt>:
+ # decompresses the body and deletes the header.
+ # - <tt>'none'</tt> or <tt>'identity'</tt>:
+ # does not decompress the body, but deletes the header.
+ # - Any other value:
+ # leaves the body and header unchanged.
+ #
+ # == What's Here
+ #
+ # First, what's elsewhere. Class Gem::Net::HTTP:
+ #
+ # - Inherits from {class Object}[https://docs.ruby-lang.org/en/master/Object.html#class-Object-label-What-27s+Here].
+ #
+ # This is a categorized summary of methods and attributes.
+ #
+ # === \Gem::Net::HTTP Objects
+ #
+ # - {::new}[rdoc-ref:Gem::Net::HTTP.new]:
+ # Creates a new instance.
+ # - {#inspect}[rdoc-ref:Gem::Net::HTTP#inspect]:
+ # Returns a string representation of +self+.
+ #
+ # === Sessions
+ #
+ # - {::start}[rdoc-ref:Gem::Net::HTTP.start]:
+ # Begins a new session in a new \Gem::Net::HTTP object.
+ # - {#started?}[rdoc-ref:Gem::Net::HTTP#started?]:
+ # Returns whether in a session.
+ # - {#finish}[rdoc-ref:Gem::Net::HTTP#finish]:
+ # Ends an active session.
+ # - {#start}[rdoc-ref:Gem::Net::HTTP#start]:
+ # Begins a new session in an existing \Gem::Net::HTTP object (+self+).
+ #
+ # === Connections
+ #
+ # - {:continue_timeout}[rdoc-ref:Gem::Net::HTTP#continue_timeout]:
+ # Returns the continue timeout.
+ # - {#continue_timeout=}[rdoc-ref:Gem::Net::HTTP#continue_timeout=]:
+ # Sets the continue timeout seconds.
+ # - {:keep_alive_timeout}[rdoc-ref:Gem::Net::HTTP#keep_alive_timeout]:
+ # Returns the keep-alive timeout.
+ # - {:keep_alive_timeout=}[rdoc-ref:Gem::Net::HTTP#keep_alive_timeout=]:
+ # Sets the keep-alive timeout.
+ # - {:max_retries}[rdoc-ref:Gem::Net::HTTP#max_retries]:
+ # Returns the maximum retries.
+ # - {#max_retries=}[rdoc-ref:Gem::Net::HTTP#max_retries=]:
+ # Sets the maximum retries.
+ # - {:open_timeout}[rdoc-ref:Gem::Net::HTTP#open_timeout]:
+ # Returns the open timeout.
+ # - {:open_timeout=}[rdoc-ref:Gem::Net::HTTP#open_timeout=]:
+ # Sets the open timeout.
+ # - {:read_timeout}[rdoc-ref:Gem::Net::HTTP#read_timeout]:
+ # Returns the open timeout.
+ # - {:read_timeout=}[rdoc-ref:Gem::Net::HTTP#read_timeout=]:
+ # Sets the read timeout.
+ # - {:ssl_timeout}[rdoc-ref:Gem::Net::HTTP#ssl_timeout]:
+ # Returns the ssl timeout.
+ # - {:ssl_timeout=}[rdoc-ref:Gem::Net::HTTP#ssl_timeout=]:
+ # Sets the ssl timeout.
+ # - {:write_timeout}[rdoc-ref:Gem::Net::HTTP#write_timeout]:
+ # Returns the write timeout.
+ # - {write_timeout=}[rdoc-ref:Gem::Net::HTTP#write_timeout=]:
+ # Sets the write timeout.
+ #
+ # === Requests
+ #
+ # - {::get}[rdoc-ref:Gem::Net::HTTP.get]:
+ # Sends a GET request and returns the string response body.
+ # - {::get_print}[rdoc-ref:Gem::Net::HTTP.get_print]:
+ # Sends a GET request and write the string response body to $stdout.
+ # - {::get_response}[rdoc-ref:Gem::Net::HTTP.get_response]:
+ # Sends a GET request and returns a response object.
+ # - {::post_form}[rdoc-ref:Gem::Net::HTTP.post_form]:
+ # Sends a POST request with form data and returns a response object.
+ # - {::post}[rdoc-ref:Gem::Net::HTTP.post]:
+ # Sends a POST request with data and returns a response object.
+ # - {::put}[rdoc-ref:Gem::Net::HTTP.put]:
+ # Sends a PUT request with data and returns a response object.
+ # - {#copy}[rdoc-ref:Gem::Net::HTTP#copy]:
+ # Sends a COPY request and returns a response object.
+ # - {#delete}[rdoc-ref:Gem::Net::HTTP#delete]:
+ # Sends a DELETE request and returns a response object.
+ # - {#get}[rdoc-ref:Gem::Net::HTTP#get]:
+ # Sends a GET request and returns a response object.
+ # - {#head}[rdoc-ref:Gem::Net::HTTP#head]:
+ # Sends a HEAD request and returns a response object.
+ # - {#lock}[rdoc-ref:Gem::Net::HTTP#lock]:
+ # Sends a LOCK request and returns a response object.
+ # - {#mkcol}[rdoc-ref:Gem::Net::HTTP#mkcol]:
+ # Sends a MKCOL request and returns a response object.
+ # - {#move}[rdoc-ref:Gem::Net::HTTP#move]:
+ # Sends a MOVE request and returns a response object.
+ # - {#options}[rdoc-ref:Gem::Net::HTTP#options]:
+ # Sends a OPTIONS request and returns a response object.
+ # - {#patch}[rdoc-ref:Gem::Net::HTTP#patch]:
+ # Sends a PATCH request and returns a response object.
+ # - {#post}[rdoc-ref:Gem::Net::HTTP#post]:
+ # Sends a POST request and returns a response object.
+ # - {#propfind}[rdoc-ref:Gem::Net::HTTP#propfind]:
+ # Sends a PROPFIND request and returns a response object.
+ # - {#proppatch}[rdoc-ref:Gem::Net::HTTP#proppatch]:
+ # Sends a PROPPATCH request and returns a response object.
+ # - {#put}[rdoc-ref:Gem::Net::HTTP#put]:
+ # Sends a PUT request and returns a response object.
+ # - {#request}[rdoc-ref:Gem::Net::HTTP#request]:
+ # Sends a request and returns a response object.
+ # - {#request_get}[rdoc-ref:Gem::Net::HTTP#request_get]:
+ # Sends a GET request and forms a response object;
+ # if a block given, calls the block with the object,
+ # otherwise returns the object.
+ # - {#request_head}[rdoc-ref:Gem::Net::HTTP#request_head]:
+ # Sends a HEAD request and forms a response object;
+ # if a block given, calls the block with the object,
+ # otherwise returns the object.
+ # - {#request_post}[rdoc-ref:Gem::Net::HTTP#request_post]:
+ # Sends a POST request and forms a response object;
+ # if a block given, calls the block with the object,
+ # otherwise returns the object.
+ # - {#send_request}[rdoc-ref:Gem::Net::HTTP#send_request]:
+ # Sends a request and returns a response object.
+ # - {#trace}[rdoc-ref:Gem::Net::HTTP#trace]:
+ # Sends a TRACE request and returns a response object.
+ # - {#unlock}[rdoc-ref:Gem::Net::HTTP#unlock]:
+ # Sends an UNLOCK request and returns a response object.
+ #
+ # === Responses
+ #
+ # - {:close_on_empty_response}[rdoc-ref:Gem::Net::HTTP#close_on_empty_response]:
+ # Returns whether to close connection on empty response.
+ # - {:close_on_empty_response=}[rdoc-ref:Gem::Net::HTTP#close_on_empty_response=]:
+ # Sets whether to close connection on empty response.
+ # - {:ignore_eof}[rdoc-ref:Gem::Net::HTTP#ignore_eof]:
+ # Returns whether to ignore end-of-file when reading a response body
+ # with <tt>Content-Length</tt> headers.
+ # - {:ignore_eof=}[rdoc-ref:Gem::Net::HTTP#ignore_eof=]:
+ # Sets whether to ignore end-of-file when reading a response body
+ # with <tt>Content-Length</tt> headers.
+ # - {:response_body_encoding}[rdoc-ref:Gem::Net::HTTP#response_body_encoding]:
+ # Returns the encoding to use for the response body.
+ # - {#response_body_encoding=}[rdoc-ref:Gem::Net::HTTP#response_body_encoding=]:
+ # Sets the response body encoding.
+ #
+ # === Proxies
+ #
+ # - {:proxy_address}[rdoc-ref:Gem::Net::HTTP#proxy_address]:
+ # Returns the proxy address.
+ # - {:proxy_address=}[rdoc-ref:Gem::Net::HTTP#proxy_address=]:
+ # Sets the proxy address.
+ # - {::proxy_class?}[rdoc-ref:Gem::Net::HTTP.proxy_class?]:
+ # Returns whether +self+ is a proxy class.
+ # - {#proxy?}[rdoc-ref:Gem::Net::HTTP#proxy?]:
+ # Returns whether +self+ has a proxy.
+ # - {#proxy_address}[rdoc-ref:Gem::Net::HTTP#proxy_address]:
+ # Returns the proxy address.
+ # - {#proxy_from_env?}[rdoc-ref:Gem::Net::HTTP#proxy_from_env?]:
+ # Returns whether the proxy is taken from an environment variable.
+ # - {:proxy_from_env=}[rdoc-ref:Gem::Net::HTTP#proxy_from_env=]:
+ # Sets whether the proxy is to be taken from an environment variable.
+ # - {:proxy_pass}[rdoc-ref:Gem::Net::HTTP#proxy_pass]:
+ # Returns the proxy password.
+ # - {:proxy_pass=}[rdoc-ref:Gem::Net::HTTP#proxy_pass=]:
+ # Sets the proxy password.
+ # - {:proxy_port}[rdoc-ref:Gem::Net::HTTP#proxy_port]:
+ # Returns the proxy port.
+ # - {:proxy_port=}[rdoc-ref:Gem::Net::HTTP#proxy_port=]:
+ # Sets the proxy port.
+ # - {#proxy_user}[rdoc-ref:Gem::Net::HTTP#proxy_user]:
+ # Returns the proxy user name.
+ # - {:proxy_user=}[rdoc-ref:Gem::Net::HTTP#proxy_user=]:
+ # Sets the proxy user.
+ #
+ # === Security
+ #
+ # - {:ca_file}[rdoc-ref:Gem::Net::HTTP#ca_file]:
+ # Returns the path to a CA certification file.
+ # - {:ca_file=}[rdoc-ref:Gem::Net::HTTP#ca_file=]:
+ # Sets the path to a CA certification file.
+ # - {:ca_path}[rdoc-ref:Gem::Net::HTTP#ca_path]:
+ # Returns the path of to CA directory containing certification files.
+ # - {:ca_path=}[rdoc-ref:Gem::Net::HTTP#ca_path=]:
+ # Sets the path of to CA directory containing certification files.
+ # - {:cert}[rdoc-ref:Gem::Net::HTTP#cert]:
+ # Returns the OpenSSL::X509::Certificate object to be used for client certification.
+ # - {:cert=}[rdoc-ref:Gem::Net::HTTP#cert=]:
+ # Sets the OpenSSL::X509::Certificate object to be used for client certification.
+ # - {:cert_store}[rdoc-ref:Gem::Net::HTTP#cert_store]:
+ # Returns the X509::Store to be used for verifying peer certificate.
+ # - {:cert_store=}[rdoc-ref:Gem::Net::HTTP#cert_store=]:
+ # Sets the X509::Store to be used for verifying peer certificate.
+ # - {:ciphers}[rdoc-ref:Gem::Net::HTTP#ciphers]:
+ # Returns the available SSL ciphers.
+ # - {:ciphers=}[rdoc-ref:Gem::Net::HTTP#ciphers=]:
+ # Sets the available SSL ciphers.
+ # - {:extra_chain_cert}[rdoc-ref:Gem::Net::HTTP#extra_chain_cert]:
+ # Returns the extra X509 certificates to be added to the certificate chain.
+ # - {:extra_chain_cert=}[rdoc-ref:Gem::Net::HTTP#extra_chain_cert=]:
+ # Sets the extra X509 certificates to be added to the certificate chain.
+ # - {:key}[rdoc-ref:Gem::Net::HTTP#key]:
+ # Returns the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
+ # - {:key=}[rdoc-ref:Gem::Net::HTTP#key=]:
+ # Sets the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
+ # - {:max_version}[rdoc-ref:Gem::Net::HTTP#max_version]:
+ # Returns the maximum SSL version.
+ # - {:max_version=}[rdoc-ref:Gem::Net::HTTP#max_version=]:
+ # Sets the maximum SSL version.
+ # - {:min_version}[rdoc-ref:Gem::Net::HTTP#min_version]:
+ # Returns the minimum SSL version.
+ # - {:min_version=}[rdoc-ref:Gem::Net::HTTP#min_version=]:
+ # Sets the minimum SSL version.
+ # - {#peer_cert}[rdoc-ref:Gem::Net::HTTP#peer_cert]:
+ # Returns the X509 certificate chain for the session's socket peer.
+ # - {:ssl_version}[rdoc-ref:Gem::Net::HTTP#ssl_version]:
+ # Returns the SSL version.
+ # - {:ssl_version=}[rdoc-ref:Gem::Net::HTTP#ssl_version=]:
+ # Sets the SSL version.
+ # - {#use_ssl=}[rdoc-ref:Gem::Net::HTTP#use_ssl=]:
+ # Sets whether a new session is to use Transport Layer Security.
+ # - {#use_ssl?}[rdoc-ref:Gem::Net::HTTP#use_ssl?]:
+ # Returns whether +self+ uses SSL.
+ # - {:verify_callback}[rdoc-ref:Gem::Net::HTTP#verify_callback]:
+ # Returns the callback for the server certification verification.
+ # - {:verify_callback=}[rdoc-ref:Gem::Net::HTTP#verify_callback=]:
+ # Sets the callback for the server certification verification.
+ # - {:verify_depth}[rdoc-ref:Gem::Net::HTTP#verify_depth]:
+ # Returns the maximum depth for the certificate chain verification.
+ # - {:verify_depth=}[rdoc-ref:Gem::Net::HTTP#verify_depth=]:
+ # Sets the maximum depth for the certificate chain verification.
+ # - {:verify_hostname}[rdoc-ref:Gem::Net::HTTP#verify_hostname]:
+ # Returns the flags for server the certification verification at the beginning of the SSL/TLS session.
+ # - {:verify_hostname=}[rdoc-ref:Gem::Net::HTTP#verify_hostname=]:
+ # Sets he flags for server the certification verification at the beginning of the SSL/TLS session.
+ # - {:verify_mode}[rdoc-ref:Gem::Net::HTTP#verify_mode]:
+ # Returns the flags for server the certification verification at the beginning of the SSL/TLS session.
+ # - {:verify_mode=}[rdoc-ref:Gem::Net::HTTP#verify_mode=]:
+ # Sets the flags for server the certification verification at the beginning of the SSL/TLS session.
+ #
+ # === Addresses and Ports
+ #
+ # - {:address}[rdoc-ref:Gem::Net::HTTP#address]:
+ # Returns the string host name or host IP.
+ # - {::default_port}[rdoc-ref:Gem::Net::HTTP.default_port]:
+ # Returns integer 80, the default port to use for HTTP requests.
+ # - {::http_default_port}[rdoc-ref:Gem::Net::HTTP.http_default_port]:
+ # Returns integer 80, the default port to use for HTTP requests.
+ # - {::https_default_port}[rdoc-ref:Gem::Net::HTTP.https_default_port]:
+ # Returns integer 443, the default port to use for HTTPS requests.
+ # - {#ipaddr}[rdoc-ref:Gem::Net::HTTP#ipaddr]:
+ # Returns the IP address for the connection.
+ # - {#ipaddr=}[rdoc-ref:Gem::Net::HTTP#ipaddr=]:
+ # Sets the IP address for the connection.
+ # - {:local_host}[rdoc-ref:Gem::Net::HTTP#local_host]:
+ # Returns the string local host used to establish the connection.
+ # - {:local_host=}[rdoc-ref:Gem::Net::HTTP#local_host=]:
+ # Sets the string local host used to establish the connection.
+ # - {:local_port}[rdoc-ref:Gem::Net::HTTP#local_port]:
+ # Returns the integer local port used to establish the connection.
+ # - {:local_port=}[rdoc-ref:Gem::Net::HTTP#local_port=]:
+ # Sets the integer local port used to establish the connection.
+ # - {:port}[rdoc-ref:Gem::Net::HTTP#port]:
+ # Returns the integer port number.
+ #
+ # === \HTTP Version
+ #
+ # - {::version_1_2?}[rdoc-ref:Gem::Net::HTTP.version_1_2?]
+ # (aliased as {::version_1_2}[rdoc-ref:Gem::Net::HTTP.version_1_2]):
+ # Returns true; retained for compatibility.
+ #
+ # === Debugging
+ #
+ # - {#set_debug_output}[rdoc-ref:Gem::Net::HTTP#set_debug_output]:
+ # Sets the output stream for debugging.
+ #
+ class HTTP < Protocol
+
+ # :stopdoc:
+ VERSION = "0.9.1"
+ HTTPVersion = '1.1'
+ begin
+ require 'zlib'
+ HAVE_ZLIB=true
+ rescue LoadError
+ HAVE_ZLIB=false
+ end
+ # :startdoc:
+
+ # Returns +true+; retained for compatibility.
+ def HTTP.version_1_2
+ true
+ end
+
+ # Returns +true+; retained for compatibility.
+ def HTTP.version_1_2?
+ true
+ end
+
+ # Returns +false+; retained for compatibility.
+ def HTTP.version_1_1? #:nodoc:
+ false
+ end
+
+ class << HTTP
+ alias is_version_1_1? version_1_1? #:nodoc:
+ alias is_version_1_2? version_1_2? #:nodoc:
+ end
+
+ # :call-seq:
+ # Gem::Net::HTTP.get_print(hostname, path, port = 80) -> nil
+ # Gem::Net::HTTP:get_print(uri, headers = {}, port = uri.port) -> nil
+ #
+ # Like Gem::Net::HTTP.get, but writes the returned body to $stdout;
+ # returns +nil+.
+ def HTTP.get_print(uri_or_host, path_or_headers = nil, port = nil)
+ get_response(uri_or_host, path_or_headers, port) {|res|
+ res.read_body do |chunk|
+ $stdout.print chunk
+ end
+ }
+ nil
+ end
+
+ # :call-seq:
+ # Gem::Net::HTTP.get(hostname, path, port = 80) -> body
+ # Gem::Net::HTTP:get(uri, headers = {}, port = uri.port) -> body
+ #
+ # Sends a GET request and returns the \HTTP response body as a string.
+ #
+ # With string arguments +hostname+ and +path+:
+ #
+ # hostname = 'jsonplaceholder.typicode.com'
+ # path = '/todos/1'
+ # puts Gem::Net::HTTP.get(hostname, path)
+ #
+ # Output:
+ #
+ # {
+ # "userId": 1,
+ # "id": 1,
+ # "title": "delectus aut autem",
+ # "completed": false
+ # }
+ #
+ # With Gem::URI object +uri+ and optional hash argument +headers+:
+ #
+ # uri = Gem::URI('https://jsonplaceholder.typicode.com/todos/1')
+ # headers = {'Content-type' => 'application/json; charset=UTF-8'}
+ # Gem::Net::HTTP.get(uri, headers)
+ #
+ # Related:
+ #
+ # - Gem::Net::HTTP::Get: request class for \HTTP method +GET+.
+ # - Gem::Net::HTTP#get: convenience method for \HTTP method +GET+.
+ #
+ def HTTP.get(uri_or_host, path_or_headers = nil, port = nil)
+ get_response(uri_or_host, path_or_headers, port).body
+ end
+
+ # :call-seq:
+ # Gem::Net::HTTP.get_response(hostname, path, port = 80) -> http_response
+ # Gem::Net::HTTP:get_response(uri, headers = {}, port = uri.port) -> http_response
+ #
+ # Like Gem::Net::HTTP.get, but returns a Gem::Net::HTTPResponse object
+ # instead of the body string.
+ def HTTP.get_response(uri_or_host, path_or_headers = nil, port = nil, &block)
+ if path_or_headers && !path_or_headers.is_a?(Hash)
+ host = uri_or_host
+ path = path_or_headers
+ new(host, port || HTTP.default_port).start {|http|
+ return http.request_get(path, &block)
+ }
+ else
+ uri = uri_or_host
+ headers = path_or_headers
+ start(uri.hostname, uri.port,
+ :use_ssl => uri.scheme == 'https') {|http|
+ return http.request_get(uri, headers, &block)
+ }
+ end
+ end
+
+ # Posts data to a host; returns a Gem::Net::HTTPResponse object.
+ #
+ # Argument +url+ must be a URL;
+ # argument +data+ must be a string:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # headers = {'content-type': 'application/json'}
+ # res = Gem::Net::HTTP.post(_uri, data, headers) # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ # puts res.body
+ #
+ # Output:
+ #
+ # {
+ # "title": "foo",
+ # "body": "bar",
+ # "userId": 1,
+ # "id": 101
+ # }
+ #
+ # Related:
+ #
+ # - Gem::Net::HTTP::Post: request class for \HTTP method +POST+.
+ # - Gem::Net::HTTP#post: convenience method for \HTTP method +POST+.
+ #
+ def HTTP.post(url, data, header = nil)
+ start(url.hostname, url.port,
+ :use_ssl => url.scheme == 'https' ) {|http|
+ http.post(url, data, header)
+ }
+ end
+
+ # Posts data to a host; returns a Gem::Net::HTTPResponse object.
+ #
+ # Argument +url+ must be a Gem::URI;
+ # argument +data+ must be a hash:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # data = {title: 'foo', body: 'bar', userId: 1}
+ # res = Gem::Net::HTTP.post_form(_uri, data) # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ # puts res.body
+ #
+ # Output:
+ #
+ # {
+ # "title": "foo",
+ # "body": "bar",
+ # "userId": "1",
+ # "id": 101
+ # }
+ #
+ def HTTP.post_form(url, params)
+ req = Post.new(url)
+ req.form_data = params
+ req.basic_auth url.user, url.password if url.user
+ start(url.hostname, url.port,
+ :use_ssl => url.scheme == 'https' ) {|http|
+ http.request(req)
+ }
+ end
+
+ # Sends a PUT request to the server; returns a Gem::Net::HTTPResponse object.
+ #
+ # Argument +url+ must be a URL;
+ # argument +data+ must be a string:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # headers = {'content-type': 'application/json'}
+ # res = Gem::Net::HTTP.put(_uri, data, headers) # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ # puts res.body
+ #
+ # Output:
+ #
+ # {
+ # "title": "foo",
+ # "body": "bar",
+ # "userId": 1,
+ # "id": 101
+ # }
+ #
+ # Related:
+ #
+ # - Gem::Net::HTTP::Put: request class for \HTTP method +PUT+.
+ # - Gem::Net::HTTP#put: convenience method for \HTTP method +PUT+.
+ #
+ def HTTP.put(url, data, header = nil)
+ start(url.hostname, url.port,
+ :use_ssl => url.scheme == 'https' ) {|http|
+ http.put(url, data, header)
+ }
+ end
+
+ #
+ # \HTTP session management
+ #
+
+ # Returns integer +80+, the default port to use for \HTTP requests:
+ #
+ # Gem::Net::HTTP.default_port # => 80
+ #
+ def HTTP.default_port
+ http_default_port()
+ end
+
+ # Returns integer +80+, the default port to use for \HTTP requests:
+ #
+ # Gem::Net::HTTP.http_default_port # => 80
+ #
+ def HTTP.http_default_port
+ 80
+ end
+
+ # Returns integer +443+, the default port to use for HTTPS requests:
+ #
+ # Gem::Net::HTTP.https_default_port # => 443
+ #
+ def HTTP.https_default_port
+ 443
+ end
+
+ def HTTP.socket_type #:nodoc: obsolete
+ BufferedIO
+ end
+
+ # :call-seq:
+ # HTTP.start(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, opts) -> http
+ # HTTP.start(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, opts) {|http| ... } -> object
+ #
+ # Creates a new \Gem::Net::HTTP object, +http+, via \Gem::Net::HTTP.new:
+ #
+ # - For arguments +address+ and +port+, see Gem::Net::HTTP.new.
+ # - For proxy-defining arguments +p_addr+ through +p_pass+,
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ # - For argument +opts+, see below.
+ #
+ # With no block given:
+ #
+ # - Calls <tt>http.start</tt> with no block (see #start),
+ # which opens a TCP connection and \HTTP session.
+ # - Returns +http+.
+ # - The caller should call #finish to close the session:
+ #
+ # http = Gem::Net::HTTP.start(hostname)
+ # http.started? # => true
+ # http.finish
+ # http.started? # => false
+ #
+ # With a block given:
+ #
+ # - Calls <tt>http.start</tt> with the block (see #start), which:
+ #
+ # - Opens a TCP connection and \HTTP session.
+ # - Calls the block,
+ # which may make any number of requests to the host.
+ # - Closes the \HTTP session and TCP connection on block exit.
+ # - Returns the block's value +object+.
+ #
+ # - Returns +object+.
+ #
+ # Example:
+ #
+ # hostname = 'jsonplaceholder.typicode.com'
+ # Gem::Net::HTTP.start(hostname) do |http|
+ # puts http.get('/todos/1').body
+ # puts http.get('/todos/2').body
+ # end
+ #
+ # Output:
+ #
+ # {
+ # "userId": 1,
+ # "id": 1,
+ # "title": "delectus aut autem",
+ # "completed": false
+ # }
+ # {
+ # "userId": 1,
+ # "id": 2,
+ # "title": "quis ut nam facilis et officia qui",
+ # "completed": false
+ # }
+ #
+ # If the last argument given is a hash, it is the +opts+ hash,
+ # where each key is a method or accessor to be called,
+ # and its value is the value to be set.
+ #
+ # The keys may include:
+ #
+ # - #ca_file
+ # - #ca_path
+ # - #cert
+ # - #cert_store
+ # - #ciphers
+ # - #close_on_empty_response
+ # - +ipaddr+ (calls #ipaddr=)
+ # - #keep_alive_timeout
+ # - #key
+ # - #open_timeout
+ # - #read_timeout
+ # - #ssl_timeout
+ # - #ssl_version
+ # - +use_ssl+ (calls #use_ssl=)
+ # - #verify_callback
+ # - #verify_depth
+ # - #verify_mode
+ # - #write_timeout
+ #
+ # Note: If +port+ is +nil+ and <tt>opts[:use_ssl]</tt> is a truthy value,
+ # the value passed to +new+ is Gem::Net::HTTP.https_default_port, not +port+.
+ #
+ def HTTP.start(address, *arg, &block) # :yield: +http+
+ arg.pop if opt = Hash.try_convert(arg[-1])
+ port, p_addr, p_port, p_user, p_pass = *arg
+ p_addr = :ENV if arg.size < 2
+ port = https_default_port if !port && opt && opt[:use_ssl]
+ http = new(address, port, p_addr, p_port, p_user, p_pass)
+ http.ipaddr = opt[:ipaddr] if opt && opt[:ipaddr]
+
+ if opt
+ if opt[:use_ssl]
+ opt = {verify_mode: OpenSSL::SSL::VERIFY_PEER}.update(opt)
+ end
+ http.methods.grep(/\A(\w+)=\z/) do |meth|
+ key = $1.to_sym
+ opt.key?(key) or next
+ http.__send__(meth, opt[key])
+ end
+ end
+
+ http.start(&block)
+ end
+
+ class << HTTP
+ alias newobj new # :nodoc:
+ end
+
+ # Returns a new \Gem::Net::HTTP object +http+
+ # (but does not open a TCP connection or \HTTP session).
+ #
+ # With only string argument +address+ given
+ # (and <tt>ENV['http_proxy']</tt> undefined or +nil+),
+ # the returned +http+:
+ #
+ # - Has the given address.
+ # - Has the default port number, Gem::Net::HTTP.default_port (80).
+ # - Has no proxy.
+ #
+ # Example:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false>
+ # http.address # => "jsonplaceholder.typicode.com"
+ # http.port # => 80
+ # http.proxy? # => false
+ #
+ # With integer argument +port+ also given,
+ # the returned +http+ has the given port:
+ #
+ # http = Gem::Net::HTTP.new(hostname, 8000)
+ # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:8000 open=false>
+ # http.port # => 8000
+ #
+ # For proxy-defining arguments +p_addr+ through +p_no_proxy+,
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ #
+ def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil, p_use_ssl = nil)
+ http = super address, port
+
+ if proxy_class? then # from Gem::Net::HTTP::Proxy()
+ http.proxy_from_env = @proxy_from_env
+ http.proxy_address = @proxy_address
+ http.proxy_port = @proxy_port
+ http.proxy_user = @proxy_user
+ http.proxy_pass = @proxy_pass
+ http.proxy_use_ssl = @proxy_use_ssl
+ elsif p_addr == :ENV then
+ http.proxy_from_env = true
+ else
+ if p_addr && p_no_proxy && !Gem::URI::Generic.use_proxy?(address, address, port, p_no_proxy)
+ p_addr = nil
+ p_port = nil
+ end
+ http.proxy_address = p_addr
+ http.proxy_port = p_port || default_port
+ http.proxy_user = p_user
+ http.proxy_pass = p_pass
+ http.proxy_use_ssl = p_use_ssl
+ end
+
+ http
+ end
+
+ class << HTTP
+ # Allows to set the default configuration that will be used
+ # when creating a new connection.
+ #
+ # Example:
+ #
+ # Gem::Net::HTTP.default_configuration = {
+ # read_timeout: 1,
+ # write_timeout: 1
+ # }
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.open_timeout # => 60
+ # http.read_timeout # => 1
+ # http.write_timeout # => 1
+ #
+ attr_accessor :default_configuration
+ end
+
+ # Creates a new \Gem::Net::HTTP object for the specified server address,
+ # without opening the TCP connection or initializing the \HTTP session.
+ # The +address+ should be a DNS hostname or IP address.
+ def initialize(address, port = nil) # :nodoc:
+ defaults = {
+ keep_alive_timeout: 2,
+ close_on_empty_response: false,
+ open_timeout: 60,
+ read_timeout: 60,
+ write_timeout: 60,
+ continue_timeout: nil,
+ max_retries: 1,
+ debug_output: nil,
+ response_body_encoding: false,
+ ignore_eof: true
+ }
+ options = defaults.merge(self.class.default_configuration || {})
+
+ @address = address
+ @port = (port || HTTP.default_port)
+ @ipaddr = nil
+ @local_host = nil
+ @local_port = nil
+ @curr_http_version = HTTPVersion
+ @keep_alive_timeout = options[:keep_alive_timeout]
+ @last_communicated = nil
+ @close_on_empty_response = options[:close_on_empty_response]
+ @socket = nil
+ @started = false
+ @open_timeout = options[:open_timeout]
+ @read_timeout = options[:read_timeout]
+ @write_timeout = options[:write_timeout]
+ @continue_timeout = options[:continue_timeout]
+ @max_retries = options[:max_retries]
+ @debug_output = options[:debug_output]
+ @response_body_encoding = options[:response_body_encoding]
+ @ignore_eof = options[:ignore_eof]
+ @tcpsocket_supports_open_timeout = nil
+
+ @proxy_from_env = false
+ @proxy_uri = nil
+ @proxy_address = nil
+ @proxy_port = nil
+ @proxy_user = nil
+ @proxy_pass = nil
+ @proxy_use_ssl = nil
+
+ @use_ssl = false
+ @ssl_context = nil
+ @ssl_session = nil
+ @sspi_enabled = false
+ SSL_IVNAMES.each do |ivname|
+ instance_variable_set ivname, nil
+ end
+ end
+
+ # Returns a string representation of +self+:
+ #
+ # Gem::Net::HTTP.new(hostname).inspect
+ # # => "#<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false>"
+ #
+ def inspect
+ "#<#{self.class} #{@address}:#{@port} open=#{started?}>"
+ end
+
+ # *WARNING* This method opens a serious security hole.
+ # Never use this method in production code.
+ #
+ # Sets the output stream for debugging:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # File.open('t.tmp', 'w') do |file|
+ # http.set_debug_output(file)
+ # http.start
+ # http.get('/nosuch/1')
+ # http.finish
+ # end
+ # puts File.read('t.tmp')
+ #
+ # Output:
+ #
+ # opening connection to jsonplaceholder.typicode.com:80...
+ # opened
+ # <- "GET /nosuch/1 HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: jsonplaceholder.typicode.com\r\n\r\n"
+ # -> "HTTP/1.1 404 Not Found\r\n"
+ # -> "Date: Mon, 12 Dec 2022 21:14:11 GMT\r\n"
+ # -> "Content-Type: application/json; charset=utf-8\r\n"
+ # -> "Content-Length: 2\r\n"
+ # -> "Connection: keep-alive\r\n"
+ # -> "X-Powered-By: Express\r\n"
+ # -> "X-Ratelimit-Limit: 1000\r\n"
+ # -> "X-Ratelimit-Remaining: 999\r\n"
+ # -> "X-Ratelimit-Reset: 1670879660\r\n"
+ # -> "Vary: Origin, Accept-Encoding\r\n"
+ # -> "Access-Control-Allow-Credentials: true\r\n"
+ # -> "Cache-Control: max-age=43200\r\n"
+ # -> "Pragma: no-cache\r\n"
+ # -> "Expires: -1\r\n"
+ # -> "X-Content-Type-Options: nosniff\r\n"
+ # -> "Etag: W/\"2-vyGp6PvFo4RvsFtPoIWeCReyIC8\"\r\n"
+ # -> "Via: 1.1 vegur\r\n"
+ # -> "CF-Cache-Status: MISS\r\n"
+ # -> "Server-Timing: cf-q-config;dur=1.3000000762986e-05\r\n"
+ # -> "Report-To: {\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=yOr40jo%2BwS1KHzhTlVpl54beJ5Wx2FcG4gGV0XVrh3X9OlR5q4drUn2dkt5DGO4GDcE%2BVXT7CNgJvGs%2BZleIyMu8CLieFiDIvOviOY3EhHg94m0ZNZgrEdpKD0S85S507l1vsEwEHkoTm%2Ff19SiO\"}],\"group\":\"cf-nel\",\"max_age\":604800}\r\n"
+ # -> "NEL: {\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}\r\n"
+ # -> "Server: cloudflare\r\n"
+ # -> "CF-RAY: 778977dc484ce591-DFW\r\n"
+ # -> "alt-svc: h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400\r\n"
+ # -> "\r\n"
+ # reading 2 bytes...
+ # -> "{}"
+ # read 2 bytes
+ # Conn keep-alive
+ #
+ def set_debug_output(output)
+ warn 'Gem::Net::HTTP#set_debug_output called after HTTP started', uplevel: 1 if started?
+ @debug_output = output
+ end
+
+ # Returns the string host name or host IP given as argument +address+ in ::new.
+ attr_reader :address
+
+ # Returns the integer port number given as argument +port+ in ::new.
+ attr_reader :port
+
+ # Sets or returns the string local host used to establish the connection;
+ # initially +nil+.
+ attr_accessor :local_host
+
+ # Sets or returns the integer local port used to establish the connection;
+ # initially +nil+.
+ attr_accessor :local_port
+
+ # Returns the encoding to use for the response body;
+ # see #response_body_encoding=.
+ attr_reader :response_body_encoding
+
+ # Sets the encoding to be used for the response body;
+ # returns the encoding.
+ #
+ # The given +value+ may be:
+ #
+ # - An Encoding object.
+ # - The name of an encoding.
+ # - An alias for an encoding name.
+ #
+ # See {Encoding}[https://docs.ruby-lang.org/en/master/Encoding.html].
+ #
+ # Examples:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.response_body_encoding = Encoding::US_ASCII # => #<Encoding:US-ASCII>
+ # http.response_body_encoding = 'US-ASCII' # => "US-ASCII"
+ # http.response_body_encoding = 'ASCII' # => "ASCII"
+ #
+ def response_body_encoding=(value)
+ value = Encoding.find(value) if value.is_a?(String)
+ @response_body_encoding = value
+ end
+
+ # Sets whether to determine the proxy from environment variable
+ # '<tt>ENV['http_proxy']</tt>';
+ # see {Proxy Using ENV['http_proxy']}[rdoc-ref:Gem::Net::HTTP@Proxy+Using+-27ENV-5B-27http_proxy-27-5D-27].
+ attr_writer :proxy_from_env
+
+ # Sets the proxy address;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ attr_writer :proxy_address
+
+ # Sets the proxy port;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ attr_writer :proxy_port
+
+ # Sets the proxy user;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ attr_writer :proxy_user
+
+ # Sets the proxy password;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ attr_writer :proxy_pass
+
+ # Sets whether the proxy uses SSL;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ attr_writer :proxy_use_ssl
+
+ # Returns the IP address for the connection.
+ #
+ # If the session has not been started,
+ # returns the value set by #ipaddr=,
+ # or +nil+ if it has not been set:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.ipaddr # => nil
+ # http.ipaddr = '172.67.155.76'
+ # http.ipaddr # => "172.67.155.76"
+ #
+ # If the session has been started,
+ # returns the IP address from the socket:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.start
+ # http.ipaddr # => "172.67.155.76"
+ # http.finish
+ #
+ def ipaddr
+ started? ? @socket.io.peeraddr[3] : @ipaddr
+ end
+
+ # Sets the IP address for the connection:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.ipaddr # => nil
+ # http.ipaddr = '172.67.155.76'
+ # http.ipaddr # => "172.67.155.76"
+ #
+ # The IP address may not be set if the session has been started.
+ def ipaddr=(addr)
+ raise IOError, "ipaddr value changed, but session already started" if started?
+ @ipaddr = addr
+ end
+
+ # Sets or returns the numeric (\Integer or \Float) number of seconds
+ # to wait for a connection to open;
+ # initially 60.
+ # If the connection is not made in the given interval,
+ # an exception is raised.
+ attr_accessor :open_timeout
+
+ # Returns the numeric (\Integer or \Float) number of seconds
+ # to wait for one block to be read (via one read(2) call);
+ # see #read_timeout=.
+ attr_reader :read_timeout
+
+ # Returns the numeric (\Integer or \Float) number of seconds
+ # to wait for one block to be written (via one write(2) call);
+ # see #write_timeout=.
+ attr_reader :write_timeout
+
+ # Sets the maximum number of times to retry an idempotent request in case of
+ # \Gem::Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET,
+ # Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError,
+ # Gem::Timeout::Error.
+ # The initial value is 1.
+ #
+ # Argument +retries+ must be a non-negative numeric value:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.max_retries = 2 # => 2
+ # http.max_retries # => 2
+ #
+ def max_retries=(retries)
+ retries = retries.to_int
+ if retries < 0
+ raise ArgumentError, 'max_retries should be non-negative integer number'
+ end
+ @max_retries = retries
+ end
+
+ # Returns the maximum number of times to retry an idempotent request;
+ # see #max_retries=.
+ attr_reader :max_retries
+
+ # Sets the read timeout, in seconds, for +self+ to integer +sec+;
+ # the initial value is 60.
+ #
+ # Argument +sec+ must be a non-negative numeric value:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.read_timeout # => 60
+ # http.get('/todos/1') # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ # http.read_timeout = 0
+ # http.get('/todos/1') # Raises Gem::Net::ReadTimeout.
+ #
+ def read_timeout=(sec)
+ @socket.read_timeout = sec if @socket
+ @read_timeout = sec
+ end
+
+ # Sets the write timeout, in seconds, for +self+ to integer +sec+;
+ # the initial value is 60.
+ #
+ # Argument +sec+ must be a non-negative numeric value:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # body = 'bar' * 200000
+ # data = <<EOF
+ # {"title": "foo", "body": "#{body}", "userId": "1"}
+ # EOF
+ # headers = {'content-type': 'application/json'}
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.write_timeout # => 60
+ # http.post(_uri.path, data, headers)
+ # # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ # http.write_timeout = 0
+ # http.post(_uri.path, data, headers) # Raises Gem::Net::WriteTimeout.
+ #
+ def write_timeout=(sec)
+ @socket.write_timeout = sec if @socket
+ @write_timeout = sec
+ end
+
+ # Returns the continue timeout value;
+ # see continue_timeout=.
+ attr_reader :continue_timeout
+
+ # Sets the continue timeout value,
+ # which is the number of seconds to wait for an expected 100 Continue response.
+ # If the \HTTP object does not receive a response in this many seconds
+ # it sends the request body.
+ def continue_timeout=(sec)
+ @socket.continue_timeout = sec if @socket
+ @continue_timeout = sec
+ end
+
+ # Sets or returns the numeric (\Integer or \Float) number of seconds
+ # to keep the connection open after a request is sent;
+ # initially 2.
+ # If a new request is made during the given interval,
+ # the still-open connection is used;
+ # otherwise the connection will have been closed
+ # and a new connection is opened.
+ attr_accessor :keep_alive_timeout
+
+ # Sets or returns whether to ignore end-of-file when reading a response body
+ # with <tt>Content-Length</tt> headers;
+ # initially +true+.
+ attr_accessor :ignore_eof
+
+ # Returns +true+ if the \HTTP session has been started:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.started? # => false
+ # http.start
+ # http.started? # => true
+ # http.finish # => nil
+ # http.started? # => false
+ #
+ # Gem::Net::HTTP.start(hostname) do |http|
+ # http.started?
+ # end # => true
+ # http.started? # => false
+ #
+ def started?
+ @started
+ end
+
+ alias active? started? #:nodoc: obsolete
+
+ # Sets or returns whether to close the connection when the response is empty;
+ # initially +false+.
+ attr_accessor :close_on_empty_response
+
+ # Returns +true+ if +self+ uses SSL, +false+ otherwise.
+ # See Gem::Net::HTTP#use_ssl=.
+ def use_ssl?
+ @use_ssl
+ end
+
+ # Sets whether a new session is to use
+ # {Transport Layer Security}[https://en.wikipedia.org/wiki/Transport_Layer_Security]:
+ #
+ # Raises IOError if attempting to change during a session.
+ #
+ # Raises OpenSSL::SSL::SSLError if the port is not an HTTPS port.
+ def use_ssl=(flag)
+ flag = flag ? true : false
+ if started? and @use_ssl != flag
+ raise IOError, "use_ssl value changed, but session already started"
+ end
+ @use_ssl = flag
+ end
+
+ SSL_ATTRIBUTES = [
+ :ca_file,
+ :ca_path,
+ :cert,
+ :cert_store,
+ :ciphers,
+ :extra_chain_cert,
+ :key,
+ :ssl_timeout,
+ :ssl_version,
+ :min_version,
+ :max_version,
+ :verify_callback,
+ :verify_depth,
+ :verify_mode,
+ :verify_hostname,
+ ].freeze # :nodoc:
+
+ SSL_IVNAMES = SSL_ATTRIBUTES.map { |a| "@#{a}".to_sym }.freeze # :nodoc:
+
+ # Sets or returns the path to a CA certification file in PEM format.
+ attr_accessor :ca_file
+
+ # Sets or returns the path of to CA directory
+ # containing certification files in PEM format.
+ attr_accessor :ca_path
+
+ # Sets or returns the OpenSSL::X509::Certificate object
+ # to be used for client certification.
+ attr_accessor :cert
+
+ # Sets or returns the X509::Store to be used for verifying peer certificate.
+ attr_accessor :cert_store
+
+ # Sets or returns the available SSL ciphers.
+ # See {OpenSSL::SSL::SSLContext#ciphers=}[OpenSSL::SSL::SSL::Context#ciphers=].
+ attr_accessor :ciphers
+
+ # Sets or returns the extra X509 certificates to be added to the certificate chain.
+ # See {OpenSSL::SSL::SSLContext#add_certificate}[OpenSSL::SSL::SSL::Context#add_certificate].
+ attr_accessor :extra_chain_cert
+
+ # Sets or returns the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
+ attr_accessor :key
+
+ # Sets or returns the SSL timeout seconds.
+ attr_accessor :ssl_timeout
+
+ # Sets or returns the SSL version.
+ # See {OpenSSL::SSL::SSLContext#ssl_version=}[OpenSSL::SSL::SSL::Context#ssl_version=].
+ attr_accessor :ssl_version
+
+ # Sets or returns the minimum SSL version.
+ # See {OpenSSL::SSL::SSLContext#min_version=}[OpenSSL::SSL::SSL::Context#min_version=].
+ attr_accessor :min_version
+
+ # Sets or returns the maximum SSL version.
+ # See {OpenSSL::SSL::SSLContext#max_version=}[OpenSSL::SSL::SSL::Context#max_version=].
+ attr_accessor :max_version
+
+ # Sets or returns the callback for the server certification verification.
+ attr_accessor :verify_callback
+
+ # Sets or returns the maximum depth for the certificate chain verification.
+ attr_accessor :verify_depth
+
+ # Sets or returns the flags for server the certification verification
+ # at the beginning of the SSL/TLS session.
+ # OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable.
+ attr_accessor :verify_mode
+
+ # Sets or returns whether to verify that the server certificate is valid
+ # for the hostname.
+ # See {OpenSSL::SSL::SSLContext#verify_hostname=}[OpenSSL::SSL::SSL::Context#verify_hostname=].
+ attr_accessor :verify_hostname
+
+ # Returns the X509 certificate chain (an array of strings)
+ # for the session's socket peer,
+ # or +nil+ if none.
+ def peer_cert
+ if not use_ssl? or not @socket
+ return nil
+ end
+ @socket.io.peer_cert
+ end
+
+ # Starts an \HTTP session.
+ #
+ # Without a block, returns +self+:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false>
+ # http.start
+ # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=true>
+ # http.started? # => true
+ # http.finish
+ #
+ # With a block, calls the block with +self+,
+ # finishes the session when the block exits,
+ # and returns the block's value:
+ #
+ # http.start do |http|
+ # http
+ # end
+ # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false>
+ # http.started? # => false
+ #
+ def start # :yield: http
+ raise IOError, 'HTTP session already opened' if @started
+ if block_given?
+ begin
+ do_start
+ return yield(self)
+ ensure
+ do_finish
+ end
+ end
+ do_start
+ self
+ end
+
+ # Finishes the \HTTP session:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.start
+ # http.started? # => true
+ # http.finish # => nil
+ # http.started? # => false
+ #
+ # Raises IOError if not in a session.
+ def finish
+ raise IOError, 'HTTP session not yet started' unless started?
+ do_finish
+ end
+
+ # :stopdoc:
+ def do_start
+ connect
+ @started = true
+ end
+ private :do_start
+
+ def connect
+ if use_ssl?
+ # reference early to load OpenSSL before connecting,
+ # as OpenSSL may take time to load.
+ @ssl_context = OpenSSL::SSL::SSLContext.new
+ end
+
+ if proxy? then
+ conn_addr = proxy_address
+ conn_port = proxy_port
+ else
+ conn_addr = conn_address
+ conn_port = port
+ end
+
+ debug "opening connection to #{conn_addr}:#{conn_port}..."
+ begin
+ s = timeouted_connect(conn_addr, conn_port)
+ rescue => e
+ if (defined?(IO::TimeoutError) && e.is_a?(IO::TimeoutError)) || e.is_a?(Errno::ETIMEDOUT) # for compatibility with previous versions
+ e = Gem::Net::OpenTimeout.new(e)
+ end
+ raise e, "Failed to open TCP connection to " +
+ "#{conn_addr}:#{conn_port} (#{e.message})"
+ end
+ s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
+ debug "opened"
+ if use_ssl?
+ if proxy?
+ if @proxy_use_ssl
+ proxy_sock = OpenSSL::SSL::SSLSocket.new(s)
+ ssl_socket_connect(proxy_sock, @open_timeout)
+ else
+ proxy_sock = s
+ end
+ proxy_sock = BufferedIO.new(proxy_sock, read_timeout: @read_timeout,
+ write_timeout: @write_timeout,
+ continue_timeout: @continue_timeout,
+ debug_output: @debug_output)
+ buf = +"CONNECT #{conn_address}:#{@port} HTTP/#{HTTPVersion}\r\n" \
+ "Host: #{@address}:#{@port}\r\n"
+ if proxy_user
+ credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0')
+ buf << "Proxy-Authorization: Basic #{credential}\r\n"
+ end
+ buf << "\r\n"
+ proxy_sock.write(buf)
+ HTTPResponse.read_new(proxy_sock).value
+ # assuming nothing left in buffers after successful CONNECT response
+ end
+
+ ssl_parameters = Hash.new
+ iv_list = instance_variables
+ SSL_IVNAMES.each_with_index do |ivname, i|
+ if iv_list.include?(ivname)
+ value = instance_variable_get(ivname)
+ unless value.nil?
+ ssl_parameters[SSL_ATTRIBUTES[i]] = value
+ end
+ end
+ end
+ @ssl_context.set_params(ssl_parameters)
+ unless @ssl_context.session_cache_mode.nil? # a dummy method on JRuby
+ @ssl_context.session_cache_mode =
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
+ end
+ if @ssl_context.respond_to?(:session_new_cb) # not implemented under JRuby
+ @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
+ end
+
+ # Still do the post_connection_check below even if connecting
+ # to IP address
+ verify_hostname = @ssl_context.verify_hostname
+
+ # Server Name Indication (SNI) RFC 3546/6066
+ case @address
+ when Gem::Resolv::IPv4::Regex, Gem::Resolv::IPv6::Regex
+ # don't set SNI, as IP addresses in SNI is not valid
+ # per RFC 6066, section 3.
+
+ # Avoid openssl warning
+ @ssl_context.verify_hostname = false
+ else
+ ssl_host_address = @address
+ end
+
+ debug "starting SSL for #{conn_addr}:#{conn_port}..."
+ s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
+ s.sync_close = true
+ s.hostname = ssl_host_address if s.respond_to?(:hostname=) && ssl_host_address
+
+ if @ssl_session and
+ Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
+ s.session = @ssl_session
+ end
+ ssl_socket_connect(s, @open_timeout)
+ if (@ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE) && verify_hostname
+ s.post_connection_check(@address)
+ end
+ debug "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}"
+ end
+ @socket = BufferedIO.new(s, read_timeout: @read_timeout,
+ write_timeout: @write_timeout,
+ continue_timeout: @continue_timeout,
+ debug_output: @debug_output)
+ @last_communicated = nil
+ on_connect
+ rescue => exception
+ if s
+ debug "Conn close because of connect error #{exception}"
+ s.close
+ end
+ raise
+ end
+ private :connect
+
+ tcp_socket_parameters = TCPSocket.instance_method(:initialize).parameters
+ TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT = if tcp_socket_parameters != [[:rest]]
+ tcp_socket_parameters.include?([:key, :open_timeout])
+ else
+ # Use Socket.tcp to find out since there is no parameters information for TCPSocket#initialize
+ # See discussion in https://github.com/ruby/net-http/pull/224
+ Socket.method(:tcp).parameters.include?([:key, :open_timeout])
+ end
+ private_constant :TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT
+
+ def timeouted_connect(conn_addr, conn_port)
+ if TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT
+ TCPSocket.open(conn_addr, conn_port, @local_host, @local_port, open_timeout: @open_timeout)
+ else
+ Gem::Timeout.timeout(@open_timeout, Gem::Net::OpenTimeout) {
+ TCPSocket.open(conn_addr, conn_port, @local_host, @local_port)
+ }
+ end
+ end
+ private :timeouted_connect
+
+ def on_connect
+ end
+ private :on_connect
+
+ def do_finish
+ @started = false
+ @socket.close if @socket
+ @socket = nil
+ end
+ private :do_finish
+
+ #
+ # proxy
+ #
+
+ public
+
+ # no proxy
+ @is_proxy_class = false
+ @proxy_from_env = false
+ @proxy_addr = nil
+ @proxy_port = nil
+ @proxy_user = nil
+ @proxy_pass = nil
+ @proxy_use_ssl = nil
+
+ # Creates an \HTTP proxy class which behaves like \Gem::Net::HTTP, but
+ # performs all access via the specified proxy.
+ #
+ # This class is obsolete. You may pass these same parameters directly to
+ # \Gem::Net::HTTP.new. See Gem::Net::HTTP.new for details of the arguments.
+ def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_use_ssl = nil) #:nodoc:
+ return self unless p_addr
+
+ Class.new(self) {
+ @is_proxy_class = true
+
+ if p_addr == :ENV then
+ @proxy_from_env = true
+ @proxy_address = nil
+ @proxy_port = nil
+ else
+ @proxy_from_env = false
+ @proxy_address = p_addr
+ @proxy_port = p_port || default_port
+ end
+
+ @proxy_user = p_user
+ @proxy_pass = p_pass
+ @proxy_use_ssl = p_use_ssl
+ }
+ end
+
+ # :startdoc:
+
+ class << HTTP
+ # Returns true if self is a class which was created by HTTP::Proxy.
+ def proxy_class?
+ defined?(@is_proxy_class) ? @is_proxy_class : false
+ end
+
+ # Returns the address of the proxy host, or +nil+ if none;
+ # see Gem::Net::HTTP@Proxy+Server.
+ attr_reader :proxy_address
+
+ # Returns the port number of the proxy host, or +nil+ if none;
+ # see Gem::Net::HTTP@Proxy+Server.
+ attr_reader :proxy_port
+
+ # Returns the user name for accessing the proxy, or +nil+ if none;
+ # see Gem::Net::HTTP@Proxy+Server.
+ attr_reader :proxy_user
+
+ # Returns the password for accessing the proxy, or +nil+ if none;
+ # see Gem::Net::HTTP@Proxy+Server.
+ attr_reader :proxy_pass
+
+ # Use SSL when talking to the proxy. If Gem::Net::HTTP does not use a proxy, nil.
+ attr_reader :proxy_use_ssl
+ end
+
+ # Returns +true+ if a proxy server is defined, +false+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ def proxy?
+ !!(@proxy_from_env ? proxy_uri : @proxy_address)
+ end
+
+ # Returns +true+ if the proxy server is defined in the environment,
+ # +false+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ def proxy_from_env?
+ @proxy_from_env
+ end
+
+ # The proxy Gem::URI determined from the environment for this connection.
+ def proxy_uri # :nodoc:
+ return if @proxy_uri == false
+ @proxy_uri ||= Gem::URI::HTTP.new(
+ "http", nil, address, port, nil, nil, nil, nil, nil
+ ).find_proxy || false
+ @proxy_uri || nil
+ end
+
+ # Returns the address of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ def proxy_address
+ if @proxy_from_env then
+ proxy_uri&.hostname
+ else
+ @proxy_address
+ end
+ end
+
+ # Returns the port number of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ def proxy_port
+ if @proxy_from_env then
+ proxy_uri&.port
+ else
+ @proxy_port
+ end
+ end
+
+ # Returns the user name of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ def proxy_user
+ if @proxy_from_env
+ user = proxy_uri&.user
+ unescape(user) if user
+ else
+ @proxy_user
+ end
+ end
+
+ # Returns the password of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ def proxy_pass
+ if @proxy_from_env
+ pass = proxy_uri&.password
+ unescape(pass) if pass
+ else
+ @proxy_pass
+ end
+ end
+
+ alias proxyaddr proxy_address #:nodoc: obsolete
+ alias proxyport proxy_port #:nodoc: obsolete
+
+ private
+ # :stopdoc:
+
+ def unescape(value)
+ require 'cgi/escape'
+ require 'cgi/util' unless defined?(CGI::EscapeExt)
+ CGI.unescape(value)
+ end
+
+ # without proxy, obsolete
+
+ def conn_address # :nodoc:
+ @ipaddr || address()
+ end
+
+ def conn_port # :nodoc:
+ port()
+ end
+
+ def edit_path(path)
+ if proxy?
+ if path.start_with?("ftp://") || use_ssl?
+ path
+ else
+ "http://#{addr_port}#{path}"
+ end
+ else
+ path
+ end
+ end
+ # :startdoc:
+
+ #
+ # HTTP operations
+ #
+
+ public
+
+ # :call-seq:
+ # get(path, initheader = nil) {|res| ... }
+ #
+ # Sends a GET request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Get object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # With a block given, calls the block with the response body:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.get('/todos/1') do |res|
+ # p res
+ # end # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # Output:
+ #
+ # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}"
+ #
+ # With no block given, simply returns the response object:
+ #
+ # http.get('/') # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # Related:
+ #
+ # - Gem::Net::HTTP::Get: request class for \HTTP method GET.
+ # - Gem::Net::HTTP.get: sends GET request, returns response body.
+ #
+ def get(path, initheader = nil, dest = nil, &block) # :yield: +body_segment+
+ res = nil
+
+ request(Get.new(path, initheader)) {|r|
+ r.read_body dest, &block
+ res = r
+ }
+ res
+ end
+
+ # Sends a HEAD request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Head object
+ # created from string +path+ and initial headers hash +initheader+:
+ #
+ # res = http.head('/todos/1') # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ # res.body # => nil
+ # res.to_hash.take(3)
+ # # =>
+ # [["date", ["Wed, 15 Feb 2023 15:25:42 GMT"]],
+ # ["content-type", ["application/json; charset=utf-8"]],
+ # ["connection", ["close"]]]
+ #
+ def head(path, initheader = nil)
+ request(Head.new(path, initheader))
+ end
+
+ # :call-seq:
+ # post(path, data, initheader = nil) {|res| ... }
+ #
+ # Sends a POST request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Post object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # With a block given, calls the block with the response body:
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.post('/todos', data) do |res|
+ # p res
+ # end # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ # Output:
+ #
+ # "{\n \"{\\\"userId\\\": 1, \\\"id\\\": 1, \\\"title\\\": \\\"delectus aut autem\\\", \\\"completed\\\": false}\": \"\",\n \"id\": 201\n}"
+ #
+ # With no block given, simply returns the response object:
+ #
+ # http.post('/todos', data) # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ # Related:
+ #
+ # - Gem::Net::HTTP::Post: request class for \HTTP method POST.
+ # - Gem::Net::HTTP.post: sends POST request, returns response body.
+ #
+ def post(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
+ send_entity(path, data, initheader, dest, Post, &block)
+ end
+
+ # :call-seq:
+ # patch(path, data, initheader = nil) {|res| ... }
+ #
+ # Sends a PATCH request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Patch object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # With a block given, calls the block with the response body:
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.patch('/todos/1', data) do |res|
+ # p res
+ # end # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # Output:
+ #
+ # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false,\n \"{\\\"userId\\\": 1, \\\"id\\\": 1, \\\"title\\\": \\\"delectus aut autem\\\", \\\"completed\\\": false}\": \"\"\n}"
+ #
+ # With no block given, simply returns the response object:
+ #
+ # http.patch('/todos/1', data) # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
+ send_entity(path, data, initheader, dest, Patch, &block)
+ end
+
+ # Sends a PUT request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Put object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.put('/todos/1', data) # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # Related:
+ #
+ # - Gem::Net::HTTP::Put: request class for \HTTP method PUT.
+ # - Gem::Net::HTTP.put: sends PUT request, returns response body.
+ #
+ def put(path, data, initheader = nil)
+ request(Put.new(path, initheader), data)
+ end
+
+ # Sends a PROPPATCH request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Proppatch object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.proppatch('/todos/1', data)
+ #
+ def proppatch(path, body, initheader = nil)
+ request(Proppatch.new(path, initheader), body)
+ end
+
+ # Sends a LOCK request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Lock object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.lock('/todos/1', data)
+ #
+ def lock(path, body, initheader = nil)
+ request(Lock.new(path, initheader), body)
+ end
+
+ # Sends an UNLOCK request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Unlock object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.unlock('/todos/1', data)
+ #
+ def unlock(path, body, initheader = nil)
+ request(Unlock.new(path, initheader), body)
+ end
+
+ # Sends an Options request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Options object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.options('/')
+ #
+ def options(path, initheader = nil)
+ request(Options.new(path, initheader))
+ end
+
+ # Sends a PROPFIND request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Propfind object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.propfind('/todos/1', data)
+ #
+ def propfind(path, body = nil, initheader = {'Depth' => '0'})
+ request(Propfind.new(path, initheader), body)
+ end
+
+ # Sends a DELETE request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Delete object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.delete('/todos/1')
+ #
+ def delete(path, initheader = {'Depth' => 'Infinity'})
+ request(Delete.new(path, initheader))
+ end
+
+ # Sends a MOVE request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Move object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.move('/todos/1')
+ #
+ def move(path, initheader = nil)
+ request(Move.new(path, initheader))
+ end
+
+ # Sends a COPY request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Copy object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.copy('/todos/1')
+ #
+ def copy(path, initheader = nil)
+ request(Copy.new(path, initheader))
+ end
+
+ # Sends a MKCOL request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Mkcol object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http.mkcol('/todos/1', data)
+ # http = Gem::Net::HTTP.new(hostname)
+ #
+ def mkcol(path, body = nil, initheader = nil)
+ request(Mkcol.new(path, initheader), body)
+ end
+
+ # Sends a TRACE request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Trace object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.trace('/todos/1')
+ #
+ def trace(path, initheader = nil)
+ request(Trace.new(path, initheader))
+ end
+
+ # Sends a GET request to the server;
+ # forms the response into a Gem::Net::HTTPResponse object.
+ #
+ # The request is based on the Gem::Net::HTTP::Get object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # With no block given, returns the response object:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.request_get('/todos') # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # With a block given, calls the block with the response object
+ # and returns the response object:
+ #
+ # http.request_get('/todos') do |res|
+ # p res
+ # end # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # Output:
+ #
+ # #<Gem::Net::HTTPOK 200 OK readbody=false>
+ #
+ def request_get(path, initheader = nil, &block) # :yield: +response+
+ request(Get.new(path, initheader), &block)
+ end
+
+ # Sends a HEAD request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Head object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.head('/todos/1') # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ def request_head(path, initheader = nil, &block)
+ request(Head.new(path, initheader), &block)
+ end
+
+ # Sends a POST request to the server;
+ # forms the response into a Gem::Net::HTTPResponse object.
+ #
+ # The request is based on the Gem::Net::HTTP::Post object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # With no block given, returns the response object:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.post('/todos', 'xyzzy')
+ # # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ # With a block given, calls the block with the response body
+ # and returns the response object:
+ #
+ # http.post('/todos', 'xyzzy') do |res|
+ # p res
+ # end # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ # Output:
+ #
+ # "{\n \"xyzzy\": \"\",\n \"id\": 201\n}"
+ #
+ def request_post(path, data, initheader = nil, &block) # :yield: +response+
+ request Post.new(path, initheader), data, &block
+ end
+
+ # Sends a PUT request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Put object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.put('/todos/1', 'xyzzy')
+ # # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ def request_put(path, data, initheader = nil, &block) #:nodoc:
+ request Put.new(path, initheader), data, &block
+ end
+
+ alias get2 request_get #:nodoc: obsolete
+ alias head2 request_head #:nodoc: obsolete
+ alias post2 request_post #:nodoc: obsolete
+ alias put2 request_put #:nodoc: obsolete
+
+ # Sends an \HTTP request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTPRequest object
+ # created from string +path+, string +data+, and initial headers hash +header+.
+ # That object is an instance of the
+ # {subclass of Gem::Net::HTTPRequest}[rdoc-ref:Gem::Net::HTTPRequest@Request+Subclasses],
+ # that corresponds to the given uppercase string +name+,
+ # which must be
+ # an {HTTP request method}[https://en.wikipedia.org/wiki/HTTP#Request_methods]
+ # or a {WebDAV request method}[https://en.wikipedia.org/wiki/WebDAV#Implementation].
+ #
+ # Examples:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.send_request('GET', '/todos/1')
+ # # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ # http.send_request('POST', '/todos', 'xyzzy')
+ # # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ def send_request(name, path, data = nil, header = nil)
+ has_response_body = name != 'HEAD'
+ r = HTTPGenericRequest.new(name,(data ? true : false),has_response_body,path,header)
+ request r, data
+ end
+
+ # Sends the given request +req+ to the server;
+ # forms the response into a Gem::Net::HTTPResponse object.
+ #
+ # The given +req+ must be an instance of a
+ # {subclass of Gem::Net::HTTPRequest}[rdoc-ref:Gem::Net::HTTPRequest@Request+Subclasses].
+ # Argument +body+ should be given only if needed for the request.
+ #
+ # With no block given, returns the response object:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ #
+ # req = Gem::Net::HTTP::Get.new('/todos/1')
+ # http.request(req)
+ # # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # req = Gem::Net::HTTP::Post.new('/todos')
+ # http.request(req, 'xyzzy')
+ # # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ # With a block given, calls the block with the response and returns the response:
+ #
+ # req = Gem::Net::HTTP::Get.new('/todos/1')
+ # http.request(req) do |res|
+ # p res
+ # end # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # Output:
+ #
+ # #<Gem::Net::HTTPOK 200 OK readbody=false>
+ #
+ def request(req, body = nil, &block) # :yield: +response+
+ unless started?
+ start {
+ req['connection'] ||= 'close'
+ return request(req, body, &block)
+ }
+ end
+ if proxy_user()
+ req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl?
+ end
+ req.set_body_internal body
+ res = transport_request(req, &block)
+ if sspi_auth?(res)
+ sspi_auth(req)
+ res = transport_request(req, &block)
+ end
+ res
+ end
+
+ private
+
+ # Executes a request which uses a representation
+ # and returns its body.
+ def send_entity(path, data, initheader, dest, type, &block)
+ res = nil
+ request(type.new(path, initheader), data) {|r|
+ r.read_body dest, &block
+ res = r
+ }
+ res
+ end
+
+ # :stopdoc:
+
+ IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/.freeze # :nodoc:
+
+ def transport_request(req)
+ count = 0
+ begin
+ begin_transport req
+ res = catch(:response) {
+ begin
+ req.exec @socket, @curr_http_version, edit_path(req.path)
+ rescue Errno::EPIPE
+ # Failure when writing full request, but we can probably
+ # still read the received response.
+ end
+
+ begin
+ res = HTTPResponse.read_new(@socket)
+ res.decode_content = req.decode_content
+ res.body_encoding = @response_body_encoding
+ res.ignore_eof = @ignore_eof
+ end while res.kind_of?(HTTPInformation)
+
+ res.uri = req.uri
+
+ res
+ }
+ res.reading_body(@socket, req.response_body_permitted?) {
+ if block_given?
+ count = max_retries # Don't restart in the middle of a download
+ yield res
+ end
+ }
+ rescue Gem::Net::OpenTimeout
+ raise
+ rescue Gem::Net::ReadTimeout, IOError, EOFError,
+ Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, Errno::ETIMEDOUT,
+ # avoid a dependency on OpenSSL
+ defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError,
+ Gem::Timeout::Error => exception
+ if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method)
+ count += 1
+ @socket.close if @socket
+ debug "Conn close because of error #{exception}, and retry"
+ retry
+ end
+ debug "Conn close because of error #{exception}"
+ @socket.close if @socket
+ raise
+ end
+
+ end_transport req, res
+ res
+ rescue => exception
+ debug "Conn close because of error #{exception}"
+ @socket.close if @socket
+ raise exception
+ end
+
+ def begin_transport(req)
+ if @socket.closed?
+ connect
+ elsif @last_communicated
+ if @last_communicated + @keep_alive_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ debug 'Conn close because of keep_alive_timeout'
+ @socket.close
+ connect
+ elsif @socket.io.to_io.wait_readable(0) && @socket.eof?
+ debug "Conn close because of EOF"
+ @socket.close
+ connect
+ end
+ end
+
+ if not req.response_body_permitted? and @close_on_empty_response
+ req['connection'] ||= 'close'
+ end
+
+ req.update_uri address, port, use_ssl?
+ req['host'] ||= addr_port()
+ end
+
+ def end_transport(req, res)
+ @curr_http_version = res.http_version
+ @last_communicated = nil
+ if @socket.closed?
+ debug 'Conn socket closed'
+ elsif not res.body and @close_on_empty_response
+ debug 'Conn close'
+ @socket.close
+ elsif keep_alive?(req, res)
+ debug 'Conn keep-alive'
+ @last_communicated = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ else
+ debug 'Conn close'
+ @socket.close
+ end
+ end
+
+ def keep_alive?(req, res)
+ return false if req.connection_close?
+ if @curr_http_version <= '1.0'
+ res.connection_keep_alive?
+ else # HTTP/1.1 or later
+ not res.connection_close?
+ end
+ end
+
+ def sspi_auth?(res)
+ return false unless @sspi_enabled
+ if res.kind_of?(HTTPProxyAuthenticationRequired) and
+ proxy? and res["Proxy-Authenticate"].include?("Negotiate")
+ begin
+ require 'win32/sspi'
+ true
+ rescue LoadError
+ false
+ end
+ else
+ false
+ end
+ end
+
+ def sspi_auth(req)
+ n = Win32::SSPI::NegotiateAuth.new
+ req["Proxy-Authorization"] = "Negotiate #{n.get_initial_token}"
+ # Some versions of ISA will close the connection if this isn't present.
+ req["Connection"] = "Keep-Alive"
+ req["Proxy-Connection"] = "Keep-Alive"
+ res = transport_request(req)
+ authphrase = res["Proxy-Authenticate"] or return res
+ req["Proxy-Authorization"] = "Negotiate #{n.complete_authentication(authphrase)}"
+ rescue => err
+ raise HTTPAuthenticationError.new('HTTP authentication failed', err)
+ end
+
+ #
+ # utils
+ #
+
+ private
+
+ def addr_port
+ addr = address
+ addr = "[#{addr}]" if addr.include?(":")
+ default_port = use_ssl? ? HTTP.https_default_port : HTTP.http_default_port
+ default_port == port ? addr : "#{addr}:#{port}"
+ end
+
+ # Adds a message to debugging output
+ def debug(msg)
+ return unless @debug_output
+ @debug_output << msg
+ @debug_output << "\n"
+ end
+
+ alias_method :D, :debug
+ end
+
+ # for backward compatibility until Ruby 4.0
+ # https://bugs.ruby-lang.org/issues/20900
+ # https://github.com/bblimke/webmock/pull/1081
+ HTTPSession = HTTP
+ deprecate_constant :HTTPSession
+end
+
+require_relative 'http/exceptions'
+
+require_relative 'http/header'
+
+require_relative 'http/generic_request'
+require_relative 'http/request'
+require_relative 'http/requests'
+
+require_relative 'http/response'
+require_relative 'http/responses'
+
+require_relative 'http/proxy_delta'
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb b/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb
new file mode 100644
index 0000000000..218df9a8bd
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+module Gem::Net
+ # Gem::Net::HTTP exception class.
+ # You cannot use Gem::Net::HTTPExceptions directly; instead, you must use
+ # its subclasses.
+ module HTTPExceptions # :nodoc:
+ def initialize(msg, res) #:nodoc:
+ super msg
+ @response = res
+ end
+ attr_reader :response
+ alias data response #:nodoc: obsolete
+ end
+
+ # :stopdoc:
+ class HTTPError < ProtocolError
+ include HTTPExceptions
+ end
+
+ class HTTPRetriableError < ProtoRetriableError
+ include HTTPExceptions
+ end
+
+ class HTTPClientException < ProtoServerError
+ include HTTPExceptions
+ end
+
+ class HTTPFatalError < ProtoFatalError
+ include HTTPExceptions
+ end
+
+ # We cannot use the name "HTTPServerError", it is the name of the response.
+ HTTPServerException = HTTPClientException # :nodoc:
+ deprecate_constant(:HTTPServerException)
+end
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb b/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb
new file mode 100644
index 0000000000..d6496d4ac1
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb
@@ -0,0 +1,429 @@
+# frozen_string_literal: true
+#
+# \HTTPGenericRequest is the parent of the Gem::Net::HTTPRequest class.
+#
+# Do not use this directly; instead, use a subclass of Gem::Net::HTTPRequest.
+#
+# == About the Examples
+#
+# :include: doc/net-http/examples.rdoc
+#
+class Gem::Net::HTTPGenericRequest
+
+ include Gem::Net::HTTPHeader
+
+ def initialize(m, reqbody, resbody, uri_or_path, initheader = nil) # :nodoc:
+ @method = m
+ @request_has_body = reqbody
+ @response_has_body = resbody
+
+ if Gem::URI === uri_or_path then
+ raise ArgumentError, "not an HTTP Gem::URI" unless Gem::URI::HTTP === uri_or_path
+ hostname = uri_or_path.host
+ raise ArgumentError, "no host component for Gem::URI" unless (hostname && hostname.length > 0)
+ @uri = uri_or_path.dup
+ @path = uri_or_path.request_uri
+ raise ArgumentError, "no HTTP request path given" unless @path
+ else
+ @uri = nil
+ raise ArgumentError, "no HTTP request path given" unless uri_or_path
+ raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty?
+ @path = uri_or_path.dup
+ end
+
+ @decode_content = false
+
+ if Gem::Net::HTTP::HAVE_ZLIB then
+ if !initheader ||
+ !initheader.keys.any? { |k|
+ %w[accept-encoding range].include? k.downcase
+ } then
+ @decode_content = true if @response_has_body
+ initheader = initheader ? initheader.dup : {}
+ initheader["accept-encoding"] =
+ "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
+ end
+ end
+
+ initialize_http_header initheader
+ self['Accept'] ||= '*/*'
+ self['User-Agent'] ||= 'Ruby'
+ self['Host'] ||= @uri.authority if @uri
+ @body = nil
+ @body_stream = nil
+ @body_data = nil
+ end
+
+ # Returns the string method name for the request:
+ #
+ # Gem::Net::HTTP::Get.new(uri).method # => "GET"
+ # Gem::Net::HTTP::Post.new(uri).method # => "POST"
+ #
+ attr_reader :method
+
+ # Returns the string path for the request:
+ #
+ # Gem::Net::HTTP::Get.new(uri).path # => "/"
+ # Gem::Net::HTTP::Post.new('example.com').path # => "example.com"
+ #
+ attr_reader :path
+
+ # Returns the Gem::URI object for the request, or +nil+ if none:
+ #
+ # Gem::Net::HTTP::Get.new(uri).uri
+ # # => #<Gem::URI::HTTPS https://jsonplaceholder.typicode.com/>
+ # Gem::Net::HTTP::Get.new('example.com').uri # => nil
+ #
+ attr_reader :uri
+
+ # Returns +false+ if the request's header <tt>'Accept-Encoding'</tt>
+ # has been set manually or deleted
+ # (indicating that the user intends to handle encoding in the response),
+ # +true+ otherwise:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri) # => #<Gem::Net::HTTP::Get GET>
+ # req['Accept-Encoding'] # => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
+ # req.decode_content # => true
+ # req['Accept-Encoding'] = 'foo'
+ # req.decode_content # => false
+ # req.delete('Accept-Encoding')
+ # req.decode_content # => false
+ #
+ attr_reader :decode_content
+
+ # Returns a string representation of the request:
+ #
+ # Gem::Net::HTTP::Post.new(uri).inspect # => "#<Gem::Net::HTTP::Post POST>"
+ #
+ def inspect
+ "\#<#{self.class} #{@method}>"
+ end
+
+ # Returns a string representation of the request with the details for pp:
+ #
+ # require 'pp'
+ # post = Gem::Net::HTTP::Post.new(uri)
+ # post.inspect # => "#<Gem::Net::HTTP::Post POST>"
+ # post.pretty_inspect
+ # # => #<Gem::Net::HTTP::Post
+ # POST
+ # path="/"
+ # headers={"accept-encoding" => ["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
+ # "accept" => ["*/*"],
+ # "user-agent" => ["Ruby"],
+ # "host" => ["www.ruby-lang.org"]}>
+ #
+ def pretty_print(q)
+ q.object_group(self) {
+ q.breakable
+ q.text @method
+ q.breakable
+ q.text "path="; q.pp @path
+ q.breakable
+ q.text "headers="; q.pp to_hash
+ }
+ end
+
+ ##
+ # Don't automatically decode response content-encoding if the user indicates
+ # they want to handle it.
+
+ def []=(key, val) # :nodoc:
+ @decode_content = false if key.downcase == 'accept-encoding'
+
+ super key, val
+ end
+
+ # Returns whether the request may have a body:
+ #
+ # Gem::Net::HTTP::Post.new(uri).request_body_permitted? # => true
+ # Gem::Net::HTTP::Get.new(uri).request_body_permitted? # => false
+ #
+ def request_body_permitted?
+ @request_has_body
+ end
+
+ # Returns whether the response may have a body:
+ #
+ # Gem::Net::HTTP::Post.new(uri).response_body_permitted? # => true
+ # Gem::Net::HTTP::Head.new(uri).response_body_permitted? # => false
+ #
+ def response_body_permitted?
+ @response_has_body
+ end
+
+ def body_exist? # :nodoc:
+ warn "Gem::Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?", uplevel: 1 if $VERBOSE
+ response_body_permitted?
+ end
+
+ # Returns the string body for the request, or +nil+ if there is none:
+ #
+ # req = Gem::Net::HTTP::Post.new(uri)
+ # req.body # => nil
+ # req.body = '{"title": "foo","body": "bar","userId": 1}'
+ # req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}"
+ #
+ attr_reader :body
+
+ # Sets the body for the request:
+ #
+ # req = Gem::Net::HTTP::Post.new(uri)
+ # req.body # => nil
+ # req.body = '{"title": "foo","body": "bar","userId": 1}'
+ # req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}"
+ #
+ def body=(str)
+ @body = str
+ @body_stream = nil
+ @body_data = nil
+ str
+ end
+
+ # Returns the body stream object for the request, or +nil+ if there is none:
+ #
+ # req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST>
+ # req.body_stream # => nil
+ # require 'stringio'
+ # req.body_stream = StringIO.new('xyzzy') # => #<StringIO:0x0000027d1e5affa8>
+ # req.body_stream # => #<StringIO:0x0000027d1e5affa8>
+ #
+ attr_reader :body_stream
+
+ # Sets the body stream for the request:
+ #
+ # req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST>
+ # req.body_stream # => nil
+ # require 'stringio'
+ # req.body_stream = StringIO.new('xyzzy') # => #<StringIO:0x0000027d1e5affa8>
+ # req.body_stream # => #<StringIO:0x0000027d1e5affa8>
+ #
+ def body_stream=(input)
+ @body = nil
+ @body_stream = input
+ @body_data = nil
+ input
+ end
+
+ def set_body_internal(str) #:nodoc: internal use only
+ raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream)
+ self.body = str if str
+ if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted?
+ self.body = ''
+ end
+ end
+
+ #
+ # write
+ #
+
+ def exec(sock, ver, path) #:nodoc: internal use only
+ if @body
+ send_request_with_body sock, ver, path, @body
+ elsif @body_stream
+ send_request_with_body_stream sock, ver, path, @body_stream
+ elsif @body_data
+ send_request_with_body_data sock, ver, path, @body_data
+ else
+ write_header sock, ver, path
+ end
+ end
+
+ def update_uri(addr, port, ssl) # :nodoc: internal use only
+ # reflect the connection and @path to @uri
+ return unless @uri
+
+ if ssl
+ scheme = 'https'
+ klass = Gem::URI::HTTPS
+ else
+ scheme = 'http'
+ klass = Gem::URI::HTTP
+ end
+
+ if host = self['host']
+ host = Gem::URI.parse("//#{host}").host # Remove a port component from the existing Host header
+ elsif host = @uri.host
+ else
+ host = addr
+ end
+ # convert the class of the Gem::URI
+ if @uri.is_a?(klass)
+ @uri.host = host
+ @uri.port = port
+ else
+ @uri = klass.new(
+ scheme, @uri.userinfo,
+ host, port, nil,
+ @uri.path, nil, @uri.query, nil)
+ end
+ end
+
+ private
+
+ # :stopdoc:
+
+ class Chunker #:nodoc:
+ def initialize(sock)
+ @sock = sock
+ @prev = nil
+ end
+
+ def write(buf)
+ # avoid memcpy() of buf, buf can huge and eat memory bandwidth
+ rv = buf.bytesize
+ @sock.write("#{rv.to_s(16)}\r\n", buf, "\r\n")
+ rv
+ end
+
+ def finish
+ @sock.write("0\r\n\r\n")
+ end
+ end
+
+ def send_request_with_body(sock, ver, path, body)
+ self.content_length = body.bytesize
+ delete 'Transfer-Encoding'
+ write_header sock, ver, path
+ wait_for_continue sock, ver if sock.continue_timeout
+ sock.write body
+ end
+
+ def send_request_with_body_stream(sock, ver, path, f)
+ unless content_length() or chunked?
+ raise ArgumentError,
+ "Content-Length not given and Transfer-Encoding is not `chunked'"
+ end
+ write_header sock, ver, path
+ wait_for_continue sock, ver if sock.continue_timeout
+ if chunked?
+ chunker = Chunker.new(sock)
+ IO.copy_stream(f, chunker)
+ chunker.finish
+ else
+ IO.copy_stream(f, sock)
+ end
+ end
+
+ def send_request_with_body_data(sock, ver, path, params)
+ if /\Amultipart\/form-data\z/i !~ self.content_type
+ self.content_type = 'application/x-www-form-urlencoded'
+ return send_request_with_body(sock, ver, path, Gem::URI.encode_www_form(params))
+ end
+
+ opt = @form_option.dup
+ require 'securerandom' unless defined?(SecureRandom)
+ opt[:boundary] ||= SecureRandom.urlsafe_base64(40)
+ self.set_content_type(self.content_type, boundary: opt[:boundary])
+ if chunked?
+ write_header sock, ver, path
+ encode_multipart_form_data(sock, params, opt)
+ else
+ require 'tempfile'
+ file = Tempfile.new('multipart')
+ file.binmode
+ encode_multipart_form_data(file, params, opt)
+ file.rewind
+ self.content_length = file.size
+ write_header sock, ver, path
+ IO.copy_stream(file, sock)
+ file.close(true)
+ end
+ end
+
+ def encode_multipart_form_data(out, params, opt)
+ charset = opt[:charset]
+ boundary = opt[:boundary]
+ require 'securerandom' unless defined?(SecureRandom)
+ boundary ||= SecureRandom.urlsafe_base64(40)
+ chunked_p = chunked?
+
+ buf = +''
+ params.each do |key, value, h={}|
+ key = quote_string(key, charset)
+ filename =
+ h.key?(:filename) ? h[:filename] :
+ value.respond_to?(:to_path) ? File.basename(value.to_path) :
+ nil
+
+ buf << "--#{boundary}\r\n"
+ if filename
+ filename = quote_string(filename, charset)
+ type = h[:content_type] || 'application/octet-stream'
+ buf << "Content-Disposition: form-data; " \
+ "name=\"#{key}\"; filename=\"#{filename}\"\r\n" \
+ "Content-Type: #{type}\r\n\r\n"
+ if !out.respond_to?(:write) || !value.respond_to?(:read)
+ # if +out+ is not an IO or +value+ is not an IO
+ buf << (value.respond_to?(:read) ? value.read : value)
+ elsif value.respond_to?(:size) && chunked_p
+ # if +out+ is an IO and +value+ is a File, use IO.copy_stream
+ flush_buffer(out, buf, chunked_p)
+ out << "%x\r\n" % value.size if chunked_p
+ IO.copy_stream(value, out)
+ out << "\r\n" if chunked_p
+ else
+ # +out+ is an IO, and +value+ is not a File but an IO
+ flush_buffer(out, buf, chunked_p)
+ 1 while flush_buffer(out, value.read(4096), chunked_p)
+ end
+ else
+ # non-file field:
+ # HTML5 says, "The parts of the generated multipart/form-data
+ # resource that correspond to non-file fields must not have a
+ # Content-Type header specified."
+ buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
+ buf << (value.respond_to?(:read) ? value.read : value)
+ end
+ buf << "\r\n"
+ end
+ buf << "--#{boundary}--\r\n"
+ flush_buffer(out, buf, chunked_p)
+ out << "0\r\n\r\n" if chunked_p
+ end
+
+ def quote_string(str, charset)
+ str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset
+ str.gsub(/[\\"]/, '\\\\\&')
+ end
+
+ def flush_buffer(out, buf, chunked_p)
+ return unless buf
+ out << "%x\r\n"%buf.bytesize if chunked_p
+ out << buf
+ out << "\r\n" if chunked_p
+ buf.clear
+ end
+
+ ##
+ # Waits up to the continue timeout for a response from the server provided
+ # we're speaking HTTP 1.1 and are expecting a 100-continue response.
+
+ def wait_for_continue(sock, ver)
+ if ver >= '1.1' and @header['expect'] and
+ @header['expect'].include?('100-continue')
+ if sock.io.to_io.wait_readable(sock.continue_timeout)
+ res = Gem::Net::HTTPResponse.read_new(sock)
+ unless res.kind_of?(Gem::Net::HTTPContinue)
+ res.decode_content = @decode_content
+ throw :response, res
+ end
+ end
+ end
+ end
+
+ def write_header(sock, ver, path)
+ reqline = "#{@method} #{path} HTTP/#{ver}"
+ if /[\r\n]/ =~ reqline
+ raise ArgumentError, "A Request-Line must not contain CR or LF"
+ end
+ buf = +''
+ buf << reqline << "\r\n"
+ each_capitalized do |k,v|
+ buf << "#{k}: #{v}\r\n"
+ end
+ buf << "\r\n"
+ sock.write buf
+ end
+
+end
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/header.rb b/lib/rubygems/vendor/net-http/lib/net/http/header.rb
new file mode 100644
index 0000000000..bc68cd2eef
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/header.rb
@@ -0,0 +1,985 @@
+# frozen_string_literal: true
+#
+# The \HTTPHeader module provides access to \HTTP headers.
+#
+# The module is included in:
+#
+# - Gem::Net::HTTPGenericRequest (and therefore Gem::Net::HTTPRequest).
+# - Gem::Net::HTTPResponse.
+#
+# The headers are a hash-like collection of key/value pairs called _fields_.
+#
+# == Request and Response Fields
+#
+# Headers may be included in:
+#
+# - A Gem::Net::HTTPRequest object:
+# the object's headers will be sent with the request.
+# Any fields may be defined in the request;
+# see {Setters}[rdoc-ref:Gem::Net::HTTPHeader@Setters].
+# - A Gem::Net::HTTPResponse object:
+# the objects headers are usually those returned from the host.
+# Fields may be retrieved from the object;
+# see {Getters}[rdoc-ref:Gem::Net::HTTPHeader@Getters]
+# and {Iterators}[rdoc-ref:Gem::Net::HTTPHeader@Iterators].
+#
+# Exactly which fields should be sent or expected depends on the host;
+# see:
+#
+# - {Request fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields].
+# - {Response fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields].
+#
+# == About the Examples
+#
+# :include: doc/net-http/examples.rdoc
+#
+# == Fields
+#
+# A header field is a key/value pair.
+#
+# === Field Keys
+#
+# A field key may be:
+#
+# - A string: Key <tt>'Accept'</tt> is treated as if it were
+# <tt>'Accept'.downcase</tt>; i.e., <tt>'accept'</tt>.
+# - A symbol: Key <tt>:Accept</tt> is treated as if it were
+# <tt>:Accept.to_s.downcase</tt>; i.e., <tt>'accept'</tt>.
+#
+# Examples:
+#
+# req = Gem::Net::HTTP::Get.new(uri)
+# req[:accept] # => "*/*"
+# req['Accept'] # => "*/*"
+# req['ACCEPT'] # => "*/*"
+#
+# req['accept'] = 'text/html'
+# req[:accept] = 'text/html'
+# req['ACCEPT'] = 'text/html'
+#
+# === Field Values
+#
+# A field value may be returned as an array of strings or as a string:
+#
+# - These methods return field values as arrays:
+#
+# - #get_fields: Returns the array value for the given key,
+# or +nil+ if it does not exist.
+# - #to_hash: Returns a hash of all header fields:
+# each key is a field name; its value is the array value for the field.
+#
+# - These methods return field values as string;
+# the string value for a field is equivalent to
+# <tt>self[key.downcase.to_s].join(', '))</tt>:
+#
+# - #[]: Returns the string value for the given key,
+# or +nil+ if it does not exist.
+# - #fetch: Like #[], but accepts a default value
+# to be returned if the key does not exist.
+#
+# The field value may be set:
+#
+# - #[]=: Sets the value for the given key;
+# the given value may be a string, a symbol, an array, or a hash.
+# - #add_field: Adds a given value to a value for the given key
+# (not overwriting the existing value).
+# - #delete: Deletes the field for the given key.
+#
+# Example field values:
+#
+# - \String:
+#
+# req['Accept'] = 'text/html' # => "text/html"
+# req['Accept'] # => "text/html"
+# req.get_fields('Accept') # => ["text/html"]
+#
+# - \Symbol:
+#
+# req['Accept'] = :text # => :text
+# req['Accept'] # => "text"
+# req.get_fields('Accept') # => ["text"]
+#
+# - Simple array:
+#
+# req[:foo] = %w[bar baz bat]
+# req[:foo] # => "bar, baz, bat"
+# req.get_fields(:foo) # => ["bar", "baz", "bat"]
+#
+# - Simple hash:
+#
+# req[:foo] = {bar: 0, baz: 1, bat: 2}
+# req[:foo] # => "bar, 0, baz, 1, bat, 2"
+# req.get_fields(:foo) # => ["bar", "0", "baz", "1", "bat", "2"]
+#
+# - Nested:
+#
+# req[:foo] = [%w[bar baz], {bat: 0, bam: 1}]
+# req[:foo] # => "bar, baz, bat, 0, bam, 1"
+# req.get_fields(:foo) # => ["bar", "baz", "bat", "0", "bam", "1"]
+#
+# req[:foo] = {bar: %w[baz bat], bam: {bah: 0, bad: 1}}
+# req[:foo] # => "bar, baz, bat, bam, bah, 0, bad, 1"
+# req.get_fields(:foo) # => ["bar", "baz", "bat", "bam", "bah", "0", "bad", "1"]
+#
+# == Convenience Methods
+#
+# Various convenience methods retrieve values, set values, query values,
+# set form values, or iterate over fields.
+#
+# === Setters
+#
+# \Method #[]= can set any field, but does little to validate the new value;
+# some of the other setter methods provide some validation:
+#
+# - #[]=: Sets the string or array value for the given key.
+# - #add_field: Creates or adds to the array value for the given key.
+# - #basic_auth: Sets the string authorization header for <tt>'Authorization'</tt>.
+# - #content_length=: Sets the integer length for field <tt>'Content-Length</tt>.
+# - #content_type=: Sets the string value for field <tt>'Content-Type'</tt>.
+# - #proxy_basic_auth: Sets the string authorization header for <tt>'Proxy-Authorization'</tt>.
+# - #set_range: Sets the value for field <tt>'Range'</tt>.
+#
+# === Form Setters
+#
+# - #set_form: Sets an HTML form data set.
+# - #set_form_data: Sets header fields and a body from HTML form data.
+#
+# === Getters
+#
+# \Method #[] can retrieve the value of any field that exists,
+# but always as a string;
+# some of the other getter methods return something different
+# from the simple string value:
+#
+# - #[]: Returns the string field value for the given key.
+# - #content_length: Returns the integer value of field <tt>'Content-Length'</tt>.
+# - #content_range: Returns the Range value of field <tt>'Content-Range'</tt>.
+# - #content_type: Returns the string value of field <tt>'Content-Type'</tt>.
+# - #fetch: Returns the string field value for the given key.
+# - #get_fields: Returns the array field value for the given +key+.
+# - #main_type: Returns first part of the string value of field <tt>'Content-Type'</tt>.
+# - #sub_type: Returns second part of the string value of field <tt>'Content-Type'</tt>.
+# - #range: Returns an array of Range objects of field <tt>'Range'</tt>, or +nil+.
+# - #range_length: Returns the integer length of the range given in field <tt>'Content-Range'</tt>.
+# - #type_params: Returns the string parameters for <tt>'Content-Type'</tt>.
+#
+# === Queries
+#
+# - #chunked?: Returns whether field <tt>'Transfer-Encoding'</tt> is set to <tt>'chunked'</tt>.
+# - #connection_close?: Returns whether field <tt>'Connection'</tt> is set to <tt>'close'</tt>.
+# - #connection_keep_alive?: Returns whether field <tt>'Connection'</tt> is set to <tt>'keep-alive'</tt>.
+# - #key?: Returns whether a given key exists.
+#
+# === Iterators
+#
+# - #each_capitalized: Passes each field capitalized-name/value pair to the block.
+# - #each_capitalized_name: Passes each capitalized field name to the block.
+# - #each_header: Passes each field name/value pair to the block.
+# - #each_name: Passes each field name to the block.
+# - #each_value: Passes each string field value to the block.
+#
+module Gem::Net::HTTPHeader
+ # The maximum length of HTTP header keys.
+ MAX_KEY_LENGTH = 1024
+ # The maximum length of HTTP header values.
+ MAX_FIELD_LENGTH = 65536
+
+ def initialize_http_header(initheader) #:nodoc:
+ @header = {}
+ return unless initheader
+ initheader.each do |key, value|
+ warn "net/http: duplicated HTTP header: #{key}", uplevel: 3 if key?(key) and $VERBOSE
+ if value.nil?
+ warn "net/http: nil HTTP header: #{key}", uplevel: 3 if $VERBOSE
+ else
+ value = value.strip # raise error for invalid byte sequences
+ if key.to_s.bytesize > MAX_KEY_LENGTH
+ raise ArgumentError, "too long (#{key.bytesize} bytes) header: #{key[0, 30].inspect}..."
+ end
+ if value.to_s.bytesize > MAX_FIELD_LENGTH
+ raise ArgumentError, "header #{key} has too long field value: #{value.bytesize}"
+ end
+ if value.count("\r\n") > 0
+ raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF"
+ end
+ @header[key.downcase.to_s] = [value]
+ end
+ end
+ end
+
+ def size #:nodoc: obsolete
+ @header.size
+ end
+
+ alias length size #:nodoc: obsolete
+
+ # Returns the string field value for the case-insensitive field +key+,
+ # or +nil+ if there is no such key;
+ # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Connection'] # => "keep-alive"
+ # res['Nosuch'] # => nil
+ #
+ # Note that some field values may be retrieved via convenience methods;
+ # see {Getters}[rdoc-ref:Gem::Net::HTTPHeader@Getters].
+ def [](key)
+ a = @header[key.downcase.to_s] or return nil
+ a.join(', ')
+ end
+
+ # Sets the value for the case-insensitive +key+ to +val+,
+ # overwriting the previous value if the field exists;
+ # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req['Accept'] # => "*/*"
+ # req['Accept'] = 'text/html'
+ # req['Accept'] # => "text/html"
+ #
+ # Note that some field values may be set via convenience methods;
+ # see {Setters}[rdoc-ref:Gem::Net::HTTPHeader@Setters].
+ def []=(key, val)
+ unless val
+ @header.delete key.downcase.to_s
+ return val
+ end
+ set_field(key, val)
+ end
+
+ # Adds value +val+ to the value array for field +key+ if the field exists;
+ # creates the field with the given +key+ and +val+ if it does not exist.
+ # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.add_field('Foo', 'bar')
+ # req['Foo'] # => "bar"
+ # req.add_field('Foo', 'baz')
+ # req['Foo'] # => "bar, baz"
+ # req.add_field('Foo', %w[baz bam])
+ # req['Foo'] # => "bar, baz, baz, bam"
+ # req.get_fields('Foo') # => ["bar", "baz", "baz", "bam"]
+ #
+ def add_field(key, val)
+ stringified_downcased_key = key.downcase.to_s
+ if @header.key?(stringified_downcased_key)
+ append_field_value(@header[stringified_downcased_key], val)
+ else
+ set_field(key, val)
+ end
+ end
+
+ # :stopdoc:
+ private def set_field(key, val)
+ case val
+ when Enumerable
+ ary = []
+ append_field_value(ary, val)
+ @header[key.downcase.to_s] = ary
+ else
+ val = val.to_s # for compatibility use to_s instead of to_str
+ if val.b.count("\r\n") > 0
+ raise ArgumentError, 'header field value cannot include CR/LF'
+ end
+ @header[key.downcase.to_s] = [val]
+ end
+ end
+
+ private def append_field_value(ary, val)
+ case val
+ when Enumerable
+ val.each{|x| append_field_value(ary, x)}
+ else
+ val = val.to_s
+ if /[\r\n]/n.match?(val.b)
+ raise ArgumentError, 'header field value cannot include CR/LF'
+ end
+ ary.push val
+ end
+ end
+ # :startdoc:
+
+ # Returns the array field value for the given +key+,
+ # or +nil+ if there is no such field;
+ # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res.get_fields('Connection') # => ["keep-alive"]
+ # res.get_fields('Nosuch') # => nil
+ #
+ def get_fields(key)
+ stringified_downcased_key = key.downcase.to_s
+ return nil unless @header[stringified_downcased_key]
+ @header[stringified_downcased_key].dup
+ end
+
+ # call-seq:
+ # fetch(key, default_val = nil) {|key| ... } -> object
+ # fetch(key, default_val = nil) -> value or default_val
+ #
+ # With a block, returns the string value for +key+ if it exists;
+ # otherwise returns the value of the block;
+ # ignores the +default_val+;
+ # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ #
+ # # Field exists; block not called.
+ # res.fetch('Connection') do |value|
+ # fail 'Cannot happen'
+ # end # => "keep-alive"
+ #
+ # # Field does not exist; block called.
+ # res.fetch('Nosuch') do |value|
+ # value.downcase
+ # end # => "nosuch"
+ #
+ # With no block, returns the string value for +key+ if it exists;
+ # otherwise, returns +default_val+ if it was given;
+ # otherwise raises an exception:
+ #
+ # res.fetch('Connection', 'Foo') # => "keep-alive"
+ # res.fetch('Nosuch', 'Foo') # => "Foo"
+ # res.fetch('Nosuch') # Raises KeyError.
+ #
+ def fetch(key, *args, &block) #:yield: +key+
+ a = @header.fetch(key.downcase.to_s, *args, &block)
+ a.kind_of?(Array) ? a.join(', ') : a
+ end
+
+ # Calls the block with each key/value pair:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res.each_header do |key, value|
+ # p [key, value] if key.start_with?('c')
+ # end
+ #
+ # Output:
+ #
+ # ["content-type", "application/json; charset=utf-8"]
+ # ["connection", "keep-alive"]
+ # ["cache-control", "max-age=43200"]
+ # ["cf-cache-status", "HIT"]
+ # ["cf-ray", "771d17e9bc542cf5-ORD"]
+ #
+ # Returns an enumerator if no block is given.
+ #
+ # Gem::Net::HTTPHeader#each is an alias for Gem::Net::HTTPHeader#each_header.
+ def each_header #:yield: +key+, +value+
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each do |k,va|
+ yield k, va.join(', ')
+ end
+ end
+
+ alias each each_header
+
+ # Calls the block with each field key:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res.each_key do |key|
+ # p key if key.start_with?('c')
+ # end
+ #
+ # Output:
+ #
+ # "content-type"
+ # "connection"
+ # "cache-control"
+ # "cf-cache-status"
+ # "cf-ray"
+ #
+ # Returns an enumerator if no block is given.
+ #
+ # Gem::Net::HTTPHeader#each_name is an alias for Gem::Net::HTTPHeader#each_key.
+ def each_name(&block) #:yield: +key+
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each_key(&block)
+ end
+
+ alias each_key each_name
+
+ # Calls the block with each capitalized field name:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res.each_capitalized_name do |key|
+ # p key if key.start_with?('C')
+ # end
+ #
+ # Output:
+ #
+ # "Content-Type"
+ # "Connection"
+ # "Cache-Control"
+ # "Cf-Cache-Status"
+ # "Cf-Ray"
+ #
+ # The capitalization is system-dependent;
+ # see {Case Mapping}[https://docs.ruby-lang.org/en/master/case_mapping_rdoc.html].
+ #
+ # Returns an enumerator if no block is given.
+ def each_capitalized_name #:yield: +key+
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each_key do |k|
+ yield capitalize(k)
+ end
+ end
+
+ # Calls the block with each string field value:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res.each_value do |value|
+ # p value if value.start_with?('c')
+ # end
+ #
+ # Output:
+ #
+ # "chunked"
+ # "cf-q-config;dur=6.0000002122251e-06"
+ # "cloudflare"
+ #
+ # Returns an enumerator if no block is given.
+ def each_value #:yield: +value+
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each_value do |va|
+ yield va.join(', ')
+ end
+ end
+
+ # Removes the header for the given case-insensitive +key+
+ # (see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]);
+ # returns the deleted value, or +nil+ if no such field exists:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.delete('Accept') # => ["*/*"]
+ # req.delete('Nosuch') # => nil
+ #
+ def delete(key)
+ @header.delete(key.downcase.to_s)
+ end
+
+ # Returns +true+ if the field for the case-insensitive +key+ exists, +false+ otherwise:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.key?('Accept') # => true
+ # req.key?('Nosuch') # => false
+ #
+ def key?(key)
+ @header.key?(key.downcase.to_s)
+ end
+
+ # Returns a hash of the key/value pairs:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.to_hash
+ # # =>
+ # {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
+ # "accept"=>["*/*"],
+ # "user-agent"=>["Ruby"],
+ # "host"=>["jsonplaceholder.typicode.com"]}
+ #
+ def to_hash
+ @header.dup
+ end
+
+ # Like #each_header, but the keys are returned in capitalized form.
+ #
+ # Gem::Net::HTTPHeader#canonical_each is an alias for Gem::Net::HTTPHeader#each_capitalized.
+ def each_capitalized
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each do |k,v|
+ yield capitalize(k), v.join(', ')
+ end
+ end
+
+ alias canonical_each each_capitalized
+
+ def capitalize(name) # :nodoc:
+ name.to_s.split('-'.freeze).map {|s| s.capitalize }.join('-'.freeze)
+ end
+ private :capitalize
+
+ # Returns an array of Range objects that represent
+ # the value of field <tt>'Range'</tt>,
+ # or +nil+ if there is no such field;
+ # see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req['Range'] = 'bytes=0-99,200-299,400-499'
+ # req.range # => [0..99, 200..299, 400..499]
+ # req.delete('Range')
+ # req.range # # => nil
+ #
+ def range
+ return nil unless @header['range']
+
+ value = self['Range']
+ # byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec )
+ # *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] )
+ # corrected collected ABNF
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5
+ unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value
+ raise Gem::Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'"
+ end
+
+ byte_range_set = $1
+ result = byte_range_set.split(/,/).map {|spec|
+ m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or
+ raise Gem::Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'"
+ d1 = m[1].to_i
+ d2 = m[2].to_i
+ if m[1] and m[2]
+ if d1 > d2
+ raise Gem::Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'"
+ end
+ d1..d2
+ elsif m[1]
+ d1..-1
+ elsif m[2]
+ -d2..-1
+ else
+ raise Gem::Net::HTTPHeaderSyntaxError, 'range is not specified'
+ end
+ }
+ # if result.empty?
+ # byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec
+ # but above regexp already denies it.
+ if result.size == 1 && result[0].begin == 0 && result[0].end == -1
+ raise Gem::Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length'
+ end
+ result
+ end
+
+ # call-seq:
+ # set_range(length) -> length
+ # set_range(offset, length) -> range
+ # set_range(begin..length) -> range
+ #
+ # Sets the value for field <tt>'Range'</tt>;
+ # see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]:
+ #
+ # With argument +length+:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.set_range(100) # => 100
+ # req['Range'] # => "bytes=0-99"
+ #
+ # With arguments +offset+ and +length+:
+ #
+ # req.set_range(100, 100) # => 100...200
+ # req['Range'] # => "bytes=100-199"
+ #
+ # With argument +range+:
+ #
+ # req.set_range(100..199) # => 100..199
+ # req['Range'] # => "bytes=100-199"
+ #
+ # Gem::Net::HTTPHeader#range= is an alias for Gem::Net::HTTPHeader#set_range.
+ def set_range(r, e = nil)
+ unless r
+ @header.delete 'range'
+ return r
+ end
+ r = (r...r+e) if e
+ case r
+ when Numeric
+ n = r.to_i
+ rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}")
+ when Range
+ first = r.first
+ last = r.end
+ last -= 1 if r.exclude_end?
+ if last == -1
+ rangestr = (first > 0 ? "#{first}-" : "-#{-first}")
+ else
+ raise Gem::Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0
+ raise Gem::Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0
+ raise Gem::Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last
+ rangestr = "#{first}-#{last}"
+ end
+ else
+ raise TypeError, 'Range/Integer is required'
+ end
+ @header['range'] = ["bytes=#{rangestr}"]
+ r
+ end
+
+ alias range= set_range
+
+ # Returns the value of field <tt>'Content-Length'</tt> as an integer,
+ # or +nil+ if there is no such field;
+ # see {Content-Length request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-request-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/nosuch/1')
+ # res.content_length # => 2
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res.content_length # => nil
+ #
+ def content_length
+ return nil unless key?('Content-Length')
+ len = self['Content-Length'].slice(/\d+/) or
+ raise Gem::Net::HTTPHeaderSyntaxError, 'wrong Content-Length format'
+ len.to_i
+ end
+
+ # Sets the value of field <tt>'Content-Length'</tt> to the given numeric;
+ # see {Content-Length response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-response-header]:
+ #
+ # _uri = uri.dup
+ # hostname = _uri.hostname # => "jsonplaceholder.typicode.com"
+ # _uri.path = '/posts' # => "/posts"
+ # req = Gem::Net::HTTP::Post.new(_uri) # => #<Gem::Net::HTTP::Post POST>
+ # req.body = '{"title": "foo","body": "bar","userId": 1}'
+ # req.content_length = req.body.size # => 42
+ # req.content_type = 'application/json'
+ # res = Gem::Net::HTTP.start(hostname) do |http|
+ # http.request(req)
+ # end # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ def content_length=(len)
+ unless len
+ @header.delete 'content-length'
+ return nil
+ end
+ @header['content-length'] = [len.to_i.to_s]
+ end
+
+ # Returns +true+ if field <tt>'Transfer-Encoding'</tt>
+ # exists and has value <tt>'chunked'</tt>,
+ # +false+ otherwise;
+ # see {Transfer-Encoding response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#transfer-encoding-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Transfer-Encoding'] # => "chunked"
+ # res.chunked? # => true
+ #
+ def chunked?
+ return false unless @header['transfer-encoding']
+ field = self['Transfer-Encoding']
+ (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false
+ end
+
+ # Returns a Range object representing the value of field
+ # <tt>'Content-Range'</tt>, or +nil+ if no such field exists;
+ # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Content-Range'] # => nil
+ # res['Content-Range'] = 'bytes 0-499/1000'
+ # res['Content-Range'] # => "bytes 0-499/1000"
+ # res.content_range # => 0..499
+ #
+ def content_range
+ return nil unless @header['content-range']
+ m = %r<\A\s*(\w+)\s+(\d+)-(\d+)/(\d+|\*)>.match(self['Content-Range']) or
+ raise Gem::Net::HTTPHeaderSyntaxError, 'wrong Content-Range format'
+ return unless m[1] == 'bytes'
+ m[2].to_i .. m[3].to_i
+ end
+
+ # Returns the integer representing length of the value of field
+ # <tt>'Content-Range'</tt>, or +nil+ if no such field exists;
+ # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Content-Range'] # => nil
+ # res['Content-Range'] = 'bytes 0-499/1000'
+ # res.range_length # => 500
+ #
+ def range_length
+ r = content_range() or return nil
+ r.end - r.begin + 1
+ end
+
+ # Returns the {media type}[https://en.wikipedia.org/wiki/Media_type]
+ # from the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.content_type # => "application/json"
+ #
+ def content_type
+ main = main_type()
+ return nil unless main
+
+ sub = sub_type()
+ if sub
+ "#{main}/#{sub}"
+ else
+ main
+ end
+ end
+
+ # Returns the leading ('type') part of the
+ # {media type}[https://en.wikipedia.org/wiki/Media_type]
+ # from the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.main_type # => "application"
+ #
+ def main_type
+ return nil unless @header['content-type']
+ self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip
+ end
+
+ # Returns the trailing ('subtype') part of the
+ # {media type}[https://en.wikipedia.org/wiki/Media_type]
+ # from the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.sub_type # => "json"
+ #
+ def sub_type
+ return nil unless @header['content-type']
+ _, sub = *self['Content-Type'].split(';').first.to_s.split('/')
+ return nil unless sub
+ sub.strip
+ end
+
+ # Returns the trailing ('parameters') part of the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.type_params # => {"charset"=>"utf-8"}
+ #
+ def type_params
+ result = {}
+ list = self['Content-Type'].to_s.split(';')
+ list.shift
+ list.each do |param|
+ k, v = *param.split('=', 2)
+ result[k.strip] = v.strip
+ end
+ result
+ end
+
+ # Sets the value of field <tt>'Content-Type'</tt>;
+ # returns the new value;
+ # see {Content-Type request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-request-header]:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.set_content_type('application/json') # => ["application/json"]
+ #
+ # Gem::Net::HTTPHeader#content_type= is an alias for Gem::Net::HTTPHeader#set_content_type.
+ def set_content_type(type, params = {})
+ @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')]
+ end
+
+ alias content_type= set_content_type
+
+ # Sets the request body to a URL-encoded string derived from argument +params+,
+ # and sets request header field <tt>'Content-Type'</tt>
+ # to <tt>'application/x-www-form-urlencoded'</tt>.
+ #
+ # The resulting request is suitable for HTTP request +POST+ or +PUT+.
+ #
+ # Argument +params+ must be suitable for use as argument +enum+ to
+ # {Gem::URI.encode_www_form}[https://docs.ruby-lang.org/en/master/Gem::URI.html#method-c-encode_www_form].
+ #
+ # With only argument +params+ given,
+ # sets the body to a URL-encoded string with the default separator <tt>'&'</tt>:
+ #
+ # req = Gem::Net::HTTP::Post.new('example.com')
+ #
+ # req.set_form_data(q: 'ruby', lang: 'en')
+ # req.body # => "q=ruby&lang=en"
+ # req['Content-Type'] # => "application/x-www-form-urlencoded"
+ #
+ # req.set_form_data([['q', 'ruby'], ['lang', 'en']])
+ # req.body # => "q=ruby&lang=en"
+ #
+ # req.set_form_data(q: ['ruby', 'perl'], lang: 'en')
+ # req.body # => "q=ruby&q=perl&lang=en"
+ #
+ # req.set_form_data([['q', 'ruby'], ['q', 'perl'], ['lang', 'en']])
+ # req.body # => "q=ruby&q=perl&lang=en"
+ #
+ # With string argument +sep+ also given,
+ # uses that string as the separator:
+ #
+ # req.set_form_data({q: 'ruby', lang: 'en'}, '|')
+ # req.body # => "q=ruby|lang=en"
+ #
+ # Gem::Net::HTTPHeader#form_data= is an alias for Gem::Net::HTTPHeader#set_form_data.
+ def set_form_data(params, sep = '&')
+ query = Gem::URI.encode_www_form(params)
+ query.gsub!(/&/, sep) if sep != '&'
+ self.body = query
+ self.content_type = 'application/x-www-form-urlencoded'
+ end
+
+ alias form_data= set_form_data
+
+ # Stores form data to be used in a +POST+ or +PUT+ request.
+ #
+ # The form data given in +params+ consists of zero or more fields;
+ # each field is:
+ #
+ # - A scalar value.
+ # - A name/value pair.
+ # - An IO stream opened for reading.
+ #
+ # Argument +params+ should be an
+ # {Enumerable}[https://docs.ruby-lang.org/en/master/Enumerable.html#module-Enumerable-label-Enumerable+in+Ruby+Classes]
+ # (method <tt>params.map</tt> will be called),
+ # and is often an array or hash.
+ #
+ # First, we set up a request:
+ #
+ # _uri = uri.dup
+ # _uri.path ='/posts'
+ # req = Gem::Net::HTTP::Post.new(_uri)
+ #
+ # <b>Argument +params+ As an Array</b>
+ #
+ # When +params+ is an array,
+ # each of its elements is a subarray that defines a field;
+ # the subarray may contain:
+ #
+ # - One string:
+ #
+ # req.set_form([['foo'], ['bar'], ['baz']])
+ #
+ # - Two strings:
+ #
+ # req.set_form([%w[foo 0], %w[bar 1], %w[baz 2]])
+ #
+ # - When argument +enctype+ (see below) is given as
+ # <tt>'multipart/form-data'</tt>:
+ #
+ # - A string name and an IO stream opened for reading:
+ #
+ # require 'stringio'
+ # req.set_form([['file', StringIO.new('Ruby is cool.')]])
+ #
+ # - A string name, an IO stream opened for reading,
+ # and an options hash, which may contain these entries:
+ #
+ # - +:filename+: The name of the file to use.
+ # - +:content_type+: The content type of the uploaded file.
+ #
+ # Example:
+ #
+ # req.set_form([['file', file, {filename: "other-filename.foo"}]]
+ #
+ # The various forms may be mixed:
+ #
+ # req.set_form(['foo', %w[bar 1], ['file', file]])
+ #
+ # <b>Argument +params+ As a Hash</b>
+ #
+ # When +params+ is a hash,
+ # each of its entries is a name/value pair that defines a field:
+ #
+ # - The name is a string.
+ # - The value may be:
+ #
+ # - +nil+.
+ # - Another string.
+ # - An IO stream opened for reading
+ # (only when argument +enctype+ -- see below -- is given as
+ # <tt>'multipart/form-data'</tt>).
+ #
+ # Examples:
+ #
+ # # Nil-valued fields.
+ # req.set_form({'foo' => nil, 'bar' => nil, 'baz' => nil})
+ #
+ # # String-valued fields.
+ # req.set_form({'foo' => 0, 'bar' => 1, 'baz' => 2})
+ #
+ # # IO-valued field.
+ # require 'stringio'
+ # req.set_form({'file' => StringIO.new('Ruby is cool.')})
+ #
+ # # Mixture of fields.
+ # req.set_form({'foo' => nil, 'bar' => 1, 'file' => file})
+ #
+ # Optional argument +enctype+ specifies the value to be given
+ # to field <tt>'Content-Type'</tt>, and must be one of:
+ #
+ # - <tt>'application/x-www-form-urlencoded'</tt> (the default).
+ # - <tt>'multipart/form-data'</tt>;
+ # see {RFC 7578}[https://www.rfc-editor.org/rfc/rfc7578].
+ #
+ # Optional argument +formopt+ is a hash of options
+ # (applicable only when argument +enctype+
+ # is <tt>'multipart/form-data'</tt>)
+ # that may include the following entries:
+ #
+ # - +:boundary+: The value is the boundary string for the multipart message.
+ # If not given, the boundary is a random string.
+ # See {Boundary}[https://www.rfc-editor.org/rfc/rfc7578#section-4.1].
+ # - +:charset+: Value is the character set for the form submission.
+ # Field names and values of non-file fields should be encoded with this charset.
+ #
+ def set_form(params, enctype='application/x-www-form-urlencoded', formopt={})
+ @body_data = params
+ @body = nil
+ @body_stream = nil
+ @form_option = formopt
+ case enctype
+ when /\Aapplication\/x-www-form-urlencoded\z/i,
+ /\Amultipart\/form-data\z/i
+ self.content_type = enctype
+ else
+ raise ArgumentError, "invalid enctype: #{enctype}"
+ end
+ end
+
+ # Sets header <tt>'Authorization'</tt> using the given
+ # +account+ and +password+ strings:
+ #
+ # req.basic_auth('my_account', 'my_password')
+ # req['Authorization']
+ # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA=="
+ #
+ def basic_auth(account, password)
+ @header['authorization'] = [basic_encode(account, password)]
+ end
+
+ # Sets header <tt>'Proxy-Authorization'</tt> using the given
+ # +account+ and +password+ strings:
+ #
+ # req.proxy_basic_auth('my_account', 'my_password')
+ # req['Proxy-Authorization']
+ # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA=="
+ #
+ def proxy_basic_auth(account, password)
+ @header['proxy-authorization'] = [basic_encode(account, password)]
+ end
+
+ def basic_encode(account, password) # :nodoc:
+ 'Basic ' + ["#{account}:#{password}"].pack('m0')
+ end
+ private :basic_encode
+
+ # Returns whether the HTTP session is to be closed.
+ def connection_close?
+ token = /(?:\A|,)\s*close\s*(?:\z|,)/i
+ @header['connection']&.grep(token) {return true}
+ @header['proxy-connection']&.grep(token) {return true}
+ false
+ end
+
+ # Returns whether the HTTP session is to be kept alive.
+ def connection_keep_alive?
+ token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i
+ @header['connection']&.grep(token) {return true}
+ @header['proxy-connection']&.grep(token) {return true}
+ false
+ end
+
+end
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/proxy_delta.rb b/lib/rubygems/vendor/net-http/lib/net/http/proxy_delta.rb
new file mode 100644
index 0000000000..137295a883
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/proxy_delta.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+module Gem::Net::HTTP::ProxyDelta #:nodoc: internal use only
+ private
+
+ def conn_address
+ proxy_address()
+ end
+
+ def conn_port
+ proxy_port()
+ end
+
+ def edit_path(path)
+ use_ssl? ? path : "http://#{addr_port()}#{path}"
+ end
+end
+
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/request.rb b/lib/rubygems/vendor/net-http/lib/net/http/request.rb
new file mode 100644
index 0000000000..495ec9be54
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/request.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+# This class is the base class for \Gem::Net::HTTP request classes.
+# The class should not be used directly;
+# instead you should use its subclasses, listed below.
+#
+# == Creating a Request
+#
+# An request object may be created with either a Gem::URI or a string hostname:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('https://jsonplaceholder.typicode.com/')
+# req = Gem::Net::HTTP::Get.new(uri) # => #<Gem::Net::HTTP::Get GET>
+# req = Gem::Net::HTTP::Get.new(uri.hostname) # => #<Gem::Net::HTTP::Get GET>
+#
+# And with any of the subclasses:
+#
+# req = Gem::Net::HTTP::Head.new(uri) # => #<Gem::Net::HTTP::Head HEAD>
+# req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST>
+# req = Gem::Net::HTTP::Put.new(uri) # => #<Gem::Net::HTTP::Put PUT>
+# # ...
+#
+# The new instance is suitable for use as the argument to Gem::Net::HTTP#request.
+#
+# == Request Headers
+#
+# A new request object has these header fields by default:
+#
+# req.to_hash
+# # =>
+# {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
+# "accept"=>["*/*"],
+# "user-agent"=>["Ruby"],
+# "host"=>["jsonplaceholder.typicode.com"]}
+#
+# See:
+#
+# - {Request header Accept-Encoding}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Accept-Encoding]
+# and {Compression and Decompression}[rdoc-ref:Gem::Net::HTTP@Compression+and+Decompression].
+# - {Request header Accept}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#accept-request-header].
+# - {Request header User-Agent}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#user-agent-request-header].
+# - {Request header Host}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#host-request-header].
+#
+# You can add headers or override default headers:
+#
+# # res = Gem::Net::HTTP::Get.new(uri, {'foo' => '0', 'bar' => '1'})
+#
+# This class (and therefore its subclasses) also includes (indirectly)
+# module Gem::Net::HTTPHeader, which gives access to its
+# {methods for setting headers}[rdoc-ref:Gem::Net::HTTPHeader@Setters].
+#
+# == Request Subclasses
+#
+# Subclasses for HTTP requests:
+#
+# - Gem::Net::HTTP::Get
+# - Gem::Net::HTTP::Head
+# - Gem::Net::HTTP::Post
+# - Gem::Net::HTTP::Put
+# - Gem::Net::HTTP::Delete
+# - Gem::Net::HTTP::Options
+# - Gem::Net::HTTP::Trace
+# - Gem::Net::HTTP::Patch
+#
+# Subclasses for WebDAV requests:
+#
+# - Gem::Net::HTTP::Propfind
+# - Gem::Net::HTTP::Proppatch
+# - Gem::Net::HTTP::Mkcol
+# - Gem::Net::HTTP::Copy
+# - Gem::Net::HTTP::Move
+# - Gem::Net::HTTP::Lock
+# - Gem::Net::HTTP::Unlock
+#
+class Gem::Net::HTTPRequest < Gem::Net::HTTPGenericRequest
+ # Creates an HTTP request object for +path+.
+ #
+ # +initheader+ are the default headers to use. Gem::Net::HTTP adds
+ # Accept-Encoding to enable compression of the response body unless
+ # Accept-Encoding or Range are supplied in +initheader+.
+
+ def initialize(path, initheader = nil)
+ super self.class::METHOD,
+ self.class::REQUEST_HAS_BODY,
+ self.class::RESPONSE_HAS_BODY,
+ path, initheader
+ end
+end
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/requests.rb b/lib/rubygems/vendor/net-http/lib/net/http/requests.rb
new file mode 100644
index 0000000000..f990761042
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/requests.rb
@@ -0,0 +1,444 @@
+# frozen_string_literal: true
+
+# HTTP/1.1 methods --- RFC2616
+
+# \Class for representing
+# {HTTP method GET}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#GET_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Get.new(uri) # => #<Gem::Net::HTTP::Get GET>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: optional.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes.
+#
+# Related:
+#
+# - Gem::Net::HTTP.get: sends +GET+ request, returns response body.
+# - Gem::Net::HTTP#get: sends +GET+ request, returns response object.
+#
+class Gem::Net::HTTP::Get < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'GET'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method HEAD}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#HEAD_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Head.new(uri) # => #<Gem::Net::HTTP::Head HEAD>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: optional.
+# - Response body: no.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes.
+#
+# Related:
+#
+# - Gem::Net::HTTP#head: sends +HEAD+ request, returns response object.
+#
+class Gem::Net::HTTP::Head < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'HEAD'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = false
+end
+
+# \Class for representing
+# {HTTP method POST}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#POST_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# uri.path = '/posts'
+# req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST>
+# req.body = '{"title": "foo","body": "bar","userId": 1}'
+# req.content_type = 'application/json'
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: yes.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: no.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes.
+#
+# Related:
+#
+# - Gem::Net::HTTP.post: sends +POST+ request, returns response object.
+# - Gem::Net::HTTP#post: sends +POST+ request, returns response object.
+#
+class Gem::Net::HTTP::Post < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'POST'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method PUT}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PUT_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# uri.path = '/posts'
+# req = Gem::Net::HTTP::Put.new(uri) # => #<Gem::Net::HTTP::Put PUT>
+# req.body = '{"title": "foo","body": "bar","userId": 1}'
+# req.content_type = 'application/json'
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: yes.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
+#
+# Related:
+#
+# - Gem::Net::HTTP.put: sends +PUT+ request, returns response object.
+# - Gem::Net::HTTP#put: sends +PUT+ request, returns response object.
+#
+class Gem::Net::HTTP::Put < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'PUT'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method DELETE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#DELETE_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# uri.path = '/posts/1'
+# req = Gem::Net::HTTP::Delete.new(uri) # => #<Gem::Net::HTTP::Delete DELETE>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: optional.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
+#
+# Related:
+#
+# - Gem::Net::HTTP#delete: sends +DELETE+ request, returns response object.
+#
+class Gem::Net::HTTP::Delete < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'DELETE'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method OPTIONS}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#OPTIONS_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Options.new(uri) # => #<Gem::Net::HTTP::Options OPTIONS>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: optional.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
+#
+# Related:
+#
+# - Gem::Net::HTTP#options: sends +OPTIONS+ request, returns response object.
+#
+class Gem::Net::HTTP::Options < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'OPTIONS'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method TRACE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#TRACE_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Trace.new(uri) # => #<Gem::Net::HTTP::Trace TRACE>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: no.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
+#
+# Related:
+#
+# - Gem::Net::HTTP#trace: sends +TRACE+ request, returns response object.
+#
+class Gem::Net::HTTP::Trace < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'TRACE'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method PATCH}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PATCH_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# uri.path = '/posts'
+# req = Gem::Net::HTTP::Patch.new(uri) # => #<Gem::Net::HTTP::Patch PATCH>
+# req.body = '{"title": "foo","body": "bar","userId": 1}'
+# req.content_type = 'application/json'
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: yes.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: no.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
+#
+# Related:
+#
+# - Gem::Net::HTTP#patch: sends +PATCH+ request, returns response object.
+#
+class Gem::Net::HTTP::Patch < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'PATCH'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+#
+# WebDAV methods --- RFC2518
+#
+
+# \Class for representing
+# {WebDAV method PROPFIND}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Propfind.new(uri) # => #<Gem::Net::HTTP::Propfind PROPFIND>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#propfind: sends +PROPFIND+ request, returns response object.
+#
+class Gem::Net::HTTP::Propfind < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'PROPFIND'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method PROPPATCH}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Proppatch.new(uri) # => #<Gem::Net::HTTP::Proppatch PROPPATCH>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#proppatch: sends +PROPPATCH+ request, returns response object.
+#
+class Gem::Net::HTTP::Proppatch < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'PROPPATCH'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method MKCOL}[http://www.webdav.org/specs/rfc4918.html#METHOD_MKCOL]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Mkcol.new(uri) # => #<Gem::Net::HTTP::Mkcol MKCOL>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#mkcol: sends +MKCOL+ request, returns response object.
+#
+class Gem::Net::HTTP::Mkcol < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'MKCOL'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method COPY}[http://www.webdav.org/specs/rfc4918.html#METHOD_COPY]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Copy.new(uri) # => #<Gem::Net::HTTP::Copy COPY>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#copy: sends +COPY+ request, returns response object.
+#
+class Gem::Net::HTTP::Copy < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'COPY'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method MOVE}[http://www.webdav.org/specs/rfc4918.html#METHOD_MOVE]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Move.new(uri) # => #<Gem::Net::HTTP::Move MOVE>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#move: sends +MOVE+ request, returns response object.
+#
+class Gem::Net::HTTP::Move < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'MOVE'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method LOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_LOCK]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Lock.new(uri) # => #<Gem::Net::HTTP::Lock LOCK>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#lock: sends +LOCK+ request, returns response object.
+#
+class Gem::Net::HTTP::Lock < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'LOCK'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method UNLOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_UNLOCK]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Unlock.new(uri) # => #<Gem::Net::HTTP::Unlock UNLOCK>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#unlock: sends +UNLOCK+ request, returns response object.
+#
+class Gem::Net::HTTP::Unlock < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'UNLOCK'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/response.rb b/lib/rubygems/vendor/net-http/lib/net/http/response.rb
new file mode 100644
index 0000000000..dc164f1504
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/response.rb
@@ -0,0 +1,739 @@
+# frozen_string_literal: true
+
+# This class is the base class for \Gem::Net::HTTP response classes.
+#
+# == About the Examples
+#
+# :include: doc/net-http/examples.rdoc
+#
+# == Returned Responses
+#
+# \Method Gem::Net::HTTP.get_response returns
+# an instance of one of the subclasses of \Gem::Net::HTTPResponse:
+#
+# Gem::Net::HTTP.get_response(uri)
+# # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+# Gem::Net::HTTP.get_response(hostname, '/nosuch')
+# # => #<Gem::Net::HTTPNotFound 404 Not Found readbody=true>
+#
+# As does method Gem::Net::HTTP#request:
+#
+# req = Gem::Net::HTTP::Get.new(uri)
+# Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+#
+# \Class \Gem::Net::HTTPResponse includes module Gem::Net::HTTPHeader,
+# which provides access to response header values via (among others):
+#
+# - \Hash-like method <tt>[]</tt>.
+# - Specific reader methods, such as +content_type+.
+#
+# Examples:
+#
+# res = Gem::Net::HTTP.get_response(uri) # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+# res['Content-Type'] # => "text/html; charset=UTF-8"
+# res.content_type # => "text/html"
+#
+# == Response Subclasses
+#
+# \Class \Gem::Net::HTTPResponse has a subclass for each
+# {HTTP status code}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes].
+# You can look up the response class for a given code:
+#
+# Gem::Net::HTTPResponse::CODE_TO_OBJ['200'] # => Gem::Net::HTTPOK
+# Gem::Net::HTTPResponse::CODE_TO_OBJ['400'] # => Gem::Net::HTTPBadRequest
+# Gem::Net::HTTPResponse::CODE_TO_OBJ['404'] # => Gem::Net::HTTPNotFound
+#
+# And you can retrieve the status code for a response object:
+#
+# Gem::Net::HTTP.get_response(uri).code # => "200"
+# Gem::Net::HTTP.get_response(hostname, '/nosuch').code # => "404"
+#
+# The response subclasses (indentation shows class hierarchy):
+#
+# - Gem::Net::HTTPUnknownResponse (for unhandled \HTTP extensions).
+#
+# - Gem::Net::HTTPInformation:
+#
+# - Gem::Net::HTTPContinue (100)
+# - Gem::Net::HTTPSwitchProtocol (101)
+# - Gem::Net::HTTPProcessing (102)
+# - Gem::Net::HTTPEarlyHints (103)
+#
+# - Gem::Net::HTTPSuccess:
+#
+# - Gem::Net::HTTPOK (200)
+# - Gem::Net::HTTPCreated (201)
+# - Gem::Net::HTTPAccepted (202)
+# - Gem::Net::HTTPNonAuthoritativeInformation (203)
+# - Gem::Net::HTTPNoContent (204)
+# - Gem::Net::HTTPResetContent (205)
+# - Gem::Net::HTTPPartialContent (206)
+# - Gem::Net::HTTPMultiStatus (207)
+# - Gem::Net::HTTPAlreadyReported (208)
+# - Gem::Net::HTTPIMUsed (226)
+#
+# - Gem::Net::HTTPRedirection:
+#
+# - Gem::Net::HTTPMultipleChoices (300)
+# - Gem::Net::HTTPMovedPermanently (301)
+# - Gem::Net::HTTPFound (302)
+# - Gem::Net::HTTPSeeOther (303)
+# - Gem::Net::HTTPNotModified (304)
+# - Gem::Net::HTTPUseProxy (305)
+# - Gem::Net::HTTPTemporaryRedirect (307)
+# - Gem::Net::HTTPPermanentRedirect (308)
+#
+# - Gem::Net::HTTPClientError:
+#
+# - Gem::Net::HTTPBadRequest (400)
+# - Gem::Net::HTTPUnauthorized (401)
+# - Gem::Net::HTTPPaymentRequired (402)
+# - Gem::Net::HTTPForbidden (403)
+# - Gem::Net::HTTPNotFound (404)
+# - Gem::Net::HTTPMethodNotAllowed (405)
+# - Gem::Net::HTTPNotAcceptable (406)
+# - Gem::Net::HTTPProxyAuthenticationRequired (407)
+# - Gem::Net::HTTPRequestTimeOut (408)
+# - Gem::Net::HTTPConflict (409)
+# - Gem::Net::HTTPGone (410)
+# - Gem::Net::HTTPLengthRequired (411)
+# - Gem::Net::HTTPPreconditionFailed (412)
+# - Gem::Net::HTTPRequestEntityTooLarge (413)
+# - Gem::Net::HTTPRequestURITooLong (414)
+# - Gem::Net::HTTPUnsupportedMediaType (415)
+# - Gem::Net::HTTPRequestedRangeNotSatisfiable (416)
+# - Gem::Net::HTTPExpectationFailed (417)
+# - Gem::Net::HTTPMisdirectedRequest (421)
+# - Gem::Net::HTTPUnprocessableEntity (422)
+# - Gem::Net::HTTPLocked (423)
+# - Gem::Net::HTTPFailedDependency (424)
+# - Gem::Net::HTTPUpgradeRequired (426)
+# - Gem::Net::HTTPPreconditionRequired (428)
+# - Gem::Net::HTTPTooManyRequests (429)
+# - Gem::Net::HTTPRequestHeaderFieldsTooLarge (431)
+# - Gem::Net::HTTPUnavailableForLegalReasons (451)
+#
+# - Gem::Net::HTTPServerError:
+#
+# - Gem::Net::HTTPInternalServerError (500)
+# - Gem::Net::HTTPNotImplemented (501)
+# - Gem::Net::HTTPBadGateway (502)
+# - Gem::Net::HTTPServiceUnavailable (503)
+# - Gem::Net::HTTPGatewayTimeOut (504)
+# - Gem::Net::HTTPVersionNotSupported (505)
+# - Gem::Net::HTTPVariantAlsoNegotiates (506)
+# - Gem::Net::HTTPInsufficientStorage (507)
+# - Gem::Net::HTTPLoopDetected (508)
+# - Gem::Net::HTTPNotExtended (510)
+# - Gem::Net::HTTPNetworkAuthenticationRequired (511)
+#
+# There is also the Gem::Net::HTTPBadResponse exception which is raised when
+# there is a protocol error.
+#
+class Gem::Net::HTTPResponse
+ class << self
+ # true if the response has a body.
+ def body_permitted?
+ self::HAS_BODY
+ end
+
+ def exception_type # :nodoc: internal use only
+ self::EXCEPTION_TYPE
+ end
+
+ def read_new(sock) #:nodoc: internal use only
+ httpv, code, msg = read_status_line(sock)
+ res = response_class(code).new(httpv, code, msg)
+ each_response_header(sock) do |k,v|
+ res.add_field k, v
+ end
+ res
+ end
+
+ private
+ # :stopdoc:
+
+ def read_status_line(sock)
+ str = sock.readline
+ m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or
+ raise Gem::Net::HTTPBadResponse, "wrong status line: #{str.dump}"
+ m.captures
+ end
+
+ def response_class(code)
+ CODE_TO_OBJ[code] or
+ CODE_CLASS_TO_OBJ[code[0,1]] or
+ Gem::Net::HTTPUnknownResponse
+ end
+
+ def each_response_header(sock)
+ key = value = nil
+ while true
+ line = sock.readuntil("\n", true).sub(/\s+\z/, '')
+ break if line.empty?
+ if line[0] == ?\s or line[0] == ?\t and value
+ value << ' ' unless value.empty?
+ value << line.strip
+ else
+ yield key, value if key
+ key, value = line.strip.split(/\s*:\s*/, 2)
+ raise Gem::Net::HTTPBadResponse, 'wrong header line format' if value.nil?
+ end
+ end
+ yield key, value if key
+ end
+ end
+
+ # next is to fix bug in RDoc, where the private inside class << self
+ # spills out.
+ public
+
+ include Gem::Net::HTTPHeader
+
+ def initialize(httpv, code, msg) #:nodoc: internal use only
+ @http_version = httpv
+ @code = code
+ @message = msg
+ initialize_http_header nil
+ @body = nil
+ @read = false
+ @uri = nil
+ @decode_content = false
+ @body_encoding = false
+ @ignore_eof = true
+ end
+
+ # The HTTP version supported by the server.
+ attr_reader :http_version
+
+ # The HTTP result code string. For example, '302'. You can also
+ # determine the response type by examining which response subclass
+ # the response object is an instance of.
+ attr_reader :code
+
+ # The HTTP result message sent by the server. For example, 'Not Found'.
+ attr_reader :message
+ alias msg message # :nodoc: obsolete
+
+ # The Gem::URI used to fetch this response. The response Gem::URI is only available
+ # if a Gem::URI was used to create the request.
+ attr_reader :uri
+
+ # Set to true automatically when the request did not contain an
+ # Accept-Encoding header from the user.
+ attr_accessor :decode_content
+
+ # Returns the value set by body_encoding=, or +false+ if none;
+ # see #body_encoding=.
+ attr_reader :body_encoding
+
+ # Sets the encoding that should be used when reading the body:
+ #
+ # - If the given value is an Encoding object, that encoding will be used.
+ # - Otherwise if the value is a string, the value of
+ # {Encoding#find(value)}[https://docs.ruby-lang.org/en/master/Encoding.html#method-c-find]
+ # will be used.
+ # - Otherwise an encoding will be deduced from the body itself.
+ #
+ # Examples:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # req = Gem::Net::HTTP::Get.new('/')
+ #
+ # http.request(req) do |res|
+ # p res.body.encoding # => #<Encoding:ASCII-8BIT>
+ # end
+ #
+ # http.request(req) do |res|
+ # res.body_encoding = "UTF-8"
+ # p res.body.encoding # => #<Encoding:UTF-8>
+ # end
+ #
+ def body_encoding=(value)
+ value = Encoding.find(value) if value.is_a?(String)
+ @body_encoding = value
+ end
+
+ # Whether to ignore EOF when reading bodies with a specified Content-Length
+ # header.
+ attr_accessor :ignore_eof
+
+ def inspect # :nodoc:
+ "#<#{self.class} #{@code} #{@message} readbody=#{@read}>"
+ end
+
+ #
+ # response <-> exception relationship
+ #
+
+ def code_type #:nodoc:
+ self.class
+ end
+
+ def error! #:nodoc:
+ message = @code
+ message = "#{message} #{@message.dump}" if @message
+ raise error_type().new(message, self)
+ end
+
+ def error_type #:nodoc:
+ self.class::EXCEPTION_TYPE
+ end
+
+ # Raises an HTTP error if the response is not 2xx (success).
+ def value
+ error! unless self.kind_of?(Gem::Net::HTTPSuccess)
+ end
+
+ def uri= uri # :nodoc:
+ @uri = uri.dup if uri
+ end
+
+ #
+ # header (for backward compatibility only; DO NOT USE)
+ #
+
+ def response #:nodoc:
+ warn "Gem::Net::HTTPResponse#response is obsolete", uplevel: 1 if $VERBOSE
+ self
+ end
+
+ def header #:nodoc:
+ warn "Gem::Net::HTTPResponse#header is obsolete", uplevel: 1 if $VERBOSE
+ self
+ end
+
+ def read_header #:nodoc:
+ warn "Gem::Net::HTTPResponse#read_header is obsolete", uplevel: 1 if $VERBOSE
+ self
+ end
+
+ #
+ # body
+ #
+
+ def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only
+ @socket = sock
+ @body_exist = reqmethodallowbody && self.class.body_permitted?
+ begin
+ yield
+ self.body # ensure to read body
+ ensure
+ @socket = nil
+ end
+ end
+
+ # Gets the entity body returned by the remote HTTP server.
+ #
+ # If a block is given, the body is passed to the block, and
+ # the body is provided in fragments, as it is read in from the socket.
+ #
+ # If +dest+ argument is given, response is read into that variable,
+ # with <code>dest#<<</code> method (it could be String or IO, or any
+ # other object responding to <code><<</code>).
+ #
+ # Calling this method a second or subsequent time for the same
+ # HTTPResponse object will return the value already read.
+ #
+ # http.request_get('/index.html') {|res|
+ # puts res.read_body
+ # }
+ #
+ # http.request_get('/index.html') {|res|
+ # p res.read_body.object_id # 538149362
+ # p res.read_body.object_id # 538149362
+ # }
+ #
+ # # using iterator
+ # http.request_get('/index.html') {|res|
+ # res.read_body do |segment|
+ # print segment
+ # end
+ # }
+ #
+ def read_body(dest = nil, &block)
+ if @read
+ raise IOError, "#{self.class}\#read_body called twice" if dest or block
+ return @body
+ end
+ to = procdest(dest, block)
+ stream_check
+ if @body_exist
+ read_body_0 to
+ @body = to
+ else
+ @body = nil
+ end
+ @read = true
+ return if @body.nil?
+
+ case enc = @body_encoding
+ when Encoding, false, nil
+ # Encoding: force given encoding
+ # false/nil: do not force encoding
+ else
+ # other value: detect encoding from body
+ enc = detect_encoding(@body)
+ end
+
+ @body.force_encoding(enc) if enc
+
+ @body
+ end
+
+ # Returns the string response body;
+ # note that repeated calls for the unmodified body return a cached string:
+ #
+ # path = '/todos/1'
+ # Gem::Net::HTTP.start(hostname) do |http|
+ # res = http.get(path)
+ # p res.body
+ # p http.head(path).body # No body.
+ # end
+ #
+ # Output:
+ #
+ # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}"
+ # nil
+ #
+ def body
+ read_body()
+ end
+
+ # Sets the body of the response to the given value.
+ def body=(value)
+ @body = value
+ end
+
+ alias entity body #:nodoc: obsolete
+
+ private
+
+ # :nodoc:
+ def detect_encoding(str, encoding=nil)
+ if encoding
+ elsif encoding = type_params['charset']
+ elsif encoding = check_bom(str)
+ else
+ encoding = case content_type&.downcase
+ when %r{text/x(?:ht)?ml|application/(?:[^+]+\+)?xml}
+ /\A<xml[ \t\r\n]+
+ version[ \t\r\n]*=[ \t\r\n]*(?:"[0-9.]+"|'[0-9.]*')[ \t\r\n]+
+ encoding[ \t\r\n]*=[ \t\r\n]*
+ (?:"([A-Za-z][\-A-Za-z0-9._]*)"|'([A-Za-z][\-A-Za-z0-9._]*)')/x =~ str
+ encoding = $1 || $2 || Encoding::UTF_8
+ when %r{text/html.*}
+ sniff_encoding(str)
+ end
+ end
+ return encoding
+ end
+
+ # :nodoc:
+ def sniff_encoding(str, encoding=nil)
+ # the encoding sniffing algorithm
+ # http://www.w3.org/TR/html5/parsing.html#determining-the-character-encoding
+ if enc = scanning_meta(str)
+ enc
+ # 6. last visited page or something
+ # 7. frequency
+ elsif str.ascii_only?
+ Encoding::US_ASCII
+ elsif str.dup.force_encoding(Encoding::UTF_8).valid_encoding?
+ Encoding::UTF_8
+ end
+ # 8. implementation-defined or user-specified
+ end
+
+ # :nodoc:
+ def check_bom(str)
+ case str.byteslice(0, 2)
+ when "\xFE\xFF"
+ return Encoding::UTF_16BE
+ when "\xFF\xFE"
+ return Encoding::UTF_16LE
+ end
+ if "\xEF\xBB\xBF" == str.byteslice(0, 3)
+ return Encoding::UTF_8
+ end
+ nil
+ end
+
+ # :nodoc:
+ def scanning_meta(str)
+ require 'strscan'
+ ss = StringScanner.new(str)
+ if ss.scan_until(/<meta[\t\n\f\r ]*/)
+ attrs = {} # attribute_list
+ got_pragma = false
+ need_pragma = nil
+ charset = nil
+
+ # step: Attributes
+ while attr = get_attribute(ss)
+ name, value = *attr
+ next if attrs[name]
+ attrs[name] = true
+ case name
+ when 'http-equiv'
+ got_pragma = true if value == 'content-type'
+ when 'content'
+ encoding = extracting_encodings_from_meta_elements(value)
+ unless charset
+ charset = encoding
+ end
+ need_pragma = true
+ when 'charset'
+ need_pragma = false
+ charset = value
+ end
+ end
+
+ # step: Processing
+ return if need_pragma.nil?
+ return if need_pragma && !got_pragma
+
+ charset = Encoding.find(charset) rescue nil
+ return unless charset
+ charset = Encoding::UTF_8 if charset == Encoding::UTF_16
+ return charset # tentative
+ end
+ nil
+ end
+
+ def get_attribute(ss)
+ ss.scan(/[\t\n\f\r \/]*/)
+ if ss.peek(1) == '>'
+ ss.getch
+ return nil
+ end
+ name = ss.scan(/[^=\t\n\f\r \/>]*/)
+ name.downcase!
+ raise if name.empty?
+ ss.skip(/[\t\n\f\r ]*/)
+ if ss.getch != '='
+ value = ''
+ return [name, value]
+ end
+ ss.skip(/[\t\n\f\r ]*/)
+ case ss.peek(1)
+ when '"'
+ ss.getch
+ value = ss.scan(/[^"]+/)
+ value.downcase!
+ ss.getch
+ when "'"
+ ss.getch
+ value = ss.scan(/[^']+/)
+ value.downcase!
+ ss.getch
+ when '>'
+ value = ''
+ else
+ value = ss.scan(/[^\t\n\f\r >]+/)
+ value.downcase!
+ end
+ [name, value]
+ end
+
+ def extracting_encodings_from_meta_elements(value)
+ # http://dev.w3.org/html5/spec/fetching-resources.html#algorithm-for-extracting-an-encoding-from-a-meta-element
+ if /charset[\t\n\f\r ]*=(?:"([^"]*)"|'([^']*)'|["']|\z|([^\t\n\f\r ;]+))/i =~ value
+ return $1 || $2 || $3
+ end
+ return nil
+ end
+
+ ##
+ # Checks for a supported Content-Encoding header and yields an Inflate
+ # wrapper for this response's socket when zlib is present. If the
+ # Content-Encoding is not supported or zlib is missing, the plain socket is
+ # yielded.
+ #
+ # If a Content-Range header is present, a plain socket is yielded as the
+ # bytes in the range may not be a complete deflate block.
+
+ def inflater # :nodoc:
+ return yield @socket unless Gem::Net::HTTP::HAVE_ZLIB
+ return yield @socket unless @decode_content
+ return yield @socket if self['content-range']
+
+ v = self['content-encoding']
+ case v&.downcase
+ when 'deflate', 'gzip', 'x-gzip' then
+ self.delete 'content-encoding'
+
+ inflate_body_io = Inflater.new(@socket)
+
+ begin
+ yield inflate_body_io
+ success = true
+ ensure
+ begin
+ inflate_body_io.finish
+ if self['content-length']
+ self['content-length'] = inflate_body_io.bytes_inflated.to_s
+ end
+ rescue => err
+ # Ignore #finish's error if there is an exception from yield
+ raise err if success
+ end
+ end
+ when 'none', 'identity' then
+ self.delete 'content-encoding'
+
+ yield @socket
+ else
+ yield @socket
+ end
+ end
+
+ def read_body_0(dest)
+ inflater do |inflate_body_io|
+ if chunked?
+ read_chunked dest, inflate_body_io
+ return
+ end
+
+ @socket = inflate_body_io
+
+ clen = content_length()
+ if clen
+ @socket.read clen, dest, @ignore_eof
+ return
+ end
+ clen = range_length()
+ if clen
+ @socket.read clen, dest
+ return
+ end
+ @socket.read_all dest
+ end
+ end
+
+ ##
+ # read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF,
+ # etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip
+ # encoded.
+ #
+ # See RFC 2616 section 3.6.1 for definitions
+
+ def read_chunked(dest, chunk_data_io) # :nodoc:
+ total = 0
+ while true
+ line = @socket.readline
+ hexlen = line.slice(/[0-9a-fA-F]+/) or
+ raise Gem::Net::HTTPBadResponse, "wrong chunk size line: #{line}"
+ len = hexlen.hex
+ break if len == 0
+ begin
+ chunk_data_io.read len, dest
+ ensure
+ total += len
+ @socket.read 2 # \r\n
+ end
+ end
+ until @socket.readline.empty?
+ # none
+ end
+ end
+
+ def stream_check
+ raise IOError, 'attempt to read body out of block' if @socket.nil? || @socket.closed?
+ end
+
+ def procdest(dest, block)
+ raise ArgumentError, 'both arg and block given for HTTP method' if
+ dest and block
+ if block
+ Gem::Net::ReadAdapter.new(block)
+ else
+ dest || +''
+ end
+ end
+
+ ##
+ # Inflater is a wrapper around Gem::Net::BufferedIO that transparently inflates
+ # zlib and gzip streams.
+
+ class Inflater # :nodoc:
+
+ ##
+ # Creates a new Inflater wrapping +socket+
+
+ def initialize socket
+ @socket = socket
+ # zlib with automatic gzip detection
+ @inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
+ end
+
+ ##
+ # Finishes the inflate stream.
+
+ def finish
+ return if @inflate.total_in == 0
+ @inflate.finish
+ end
+
+ ##
+ # The number of bytes inflated, used to update the Content-Length of
+ # the response.
+
+ def bytes_inflated
+ @inflate.total_out
+ end
+
+ ##
+ # Returns a Gem::Net::ReadAdapter that inflates each read chunk into +dest+.
+ #
+ # This allows a large response body to be inflated without storing the
+ # entire body in memory.
+
+ def inflate_adapter(dest)
+ if dest.respond_to?(:set_encoding)
+ dest.set_encoding(Encoding::ASCII_8BIT)
+ elsif dest.respond_to?(:force_encoding)
+ dest.force_encoding(Encoding::ASCII_8BIT)
+ end
+ block = proc do |compressed_chunk|
+ @inflate.inflate(compressed_chunk) do |chunk|
+ compressed_chunk.clear
+ dest << chunk
+ end
+ end
+
+ Gem::Net::ReadAdapter.new(block)
+ end
+
+ ##
+ # Reads +clen+ bytes from the socket, inflates them, then writes them to
+ # +dest+. +ignore_eof+ is passed down to Gem::Net::BufferedIO#read
+ #
+ # Unlike Gem::Net::BufferedIO#read, this method returns more than +clen+ bytes.
+ # At this time there is no way for a user of Gem::Net::HTTPResponse to read a
+ # specific number of bytes from the HTTP response body, so this internal
+ # API does not return the same number of bytes as were requested.
+ #
+ # See https://bugs.ruby-lang.org/issues/6492 for further discussion.
+
+ def read clen, dest, ignore_eof = false
+ temp_dest = inflate_adapter(dest)
+
+ @socket.read clen, temp_dest, ignore_eof
+ end
+
+ ##
+ # Reads the rest of the socket, inflates it, then writes it to +dest+.
+
+ def read_all dest
+ temp_dest = inflate_adapter(dest)
+
+ @socket.read_all temp_dest
+ end
+
+ end
+
+end
+
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/responses.rb b/lib/rubygems/vendor/net-http/lib/net/http/responses.rb
new file mode 100644
index 0000000000..62ce1cba1b
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/responses.rb
@@ -0,0 +1,1242 @@
+# frozen_string_literal: true
+#--
+# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+
+module Gem::Net
+
+ # Unknown HTTP response
+ class HTTPUnknownResponse < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPError #
+ end
+
+ # Parent class for informational (1xx) HTTP response classes.
+ #
+ # An informational response indicates that the request was received and understood.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.1xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#1xx_informational_response].
+ #
+ class HTTPInformation < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = false
+ EXCEPTION_TYPE = HTTPError #
+ end
+
+ # Parent class for success (2xx) HTTP response classes.
+ #
+ # A success response indicates the action requested by the client
+ # was received, understood, and accepted.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.2xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_success].
+ #
+ class HTTPSuccess < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPError #
+ end
+
+ # Parent class for redirection (3xx) HTTP response classes.
+ #
+ # A redirection response indicates the client must take additional action
+ # to complete the request.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.3xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_redirection].
+ #
+ class HTTPRedirection < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPRetriableError #
+ end
+
+ # Parent class for client error (4xx) HTTP response classes.
+ #
+ # A client error response indicates that the client may have caused an error.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.4xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors].
+ #
+ class HTTPClientError < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPClientException #
+ end
+
+ # Parent class for server error (5xx) HTTP response classes.
+ #
+ # A server error response indicates that the server failed to fulfill a request.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.5xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#5xx_server_errors].
+ #
+ class HTTPServerError < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPFatalError #
+ end
+
+ # Response class for +Continue+ responses (status code 100).
+ #
+ # A +Continue+ response indicates that the server has received the request headers.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/100].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-100-continue].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#100].
+ #
+ class HTTPContinue < HTTPInformation
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Switching Protocol</tt> responses (status code 101).
+ #
+ # The <tt>Switching Protocol<tt> response indicates that the server has received
+ # a request to switch protocols, and has agreed to do so.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#101].
+ #
+ class HTTPSwitchProtocol < HTTPInformation
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for +Processing+ responses (status code 102).
+ #
+ # The +Processing+ response indicates that the server has received
+ # and is processing the request, but no response is available yet.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 2518}[https://www.rfc-editor.org/rfc/rfc2518#section-10.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#102].
+ #
+ class HTTPProcessing < HTTPInformation
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Early Hints</tt> responses (status code 103).
+ #
+ # The <tt>Early Hints</tt> indicates that the server has received
+ # and is processing the request, and contains certain headers;
+ # the final response is not available yet.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103].
+ # - {RFC 8297}[https://www.rfc-editor.org/rfc/rfc8297.html#section-2].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#103].
+ #
+ class HTTPEarlyHints < HTTPInformation
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for +OK+ responses (status code 200).
+ #
+ # The +OK+ response indicates that the server has received
+ # a request and has responded successfully.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-200-ok].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#200].
+ #
+ class HTTPOK < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for +Created+ responses (status code 201).
+ #
+ # The +Created+ response indicates that the server has received
+ # and has fulfilled a request to create a new resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-201-created].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#201].
+ #
+ class HTTPCreated < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for +Accepted+ responses (status code 202).
+ #
+ # The +Accepted+ response indicates that the server has received
+ # and is processing a request, but the processing has not yet been completed.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-202-accepted].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#202].
+ #
+ class HTTPAccepted < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Non-Authoritative Information</tt> responses (status code 203).
+ #
+ # The <tt>Non-Authoritative Information</tt> response indicates that the server
+ # is a transforming proxy (such as a Web accelerator)
+ # that received a 200 OK response from its origin,
+ # and is returning a modified version of the origin's response.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/203].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-203-non-authoritative-infor].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#203].
+ #
+ class HTTPNonAuthoritativeInformation < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>No Content</tt> responses (status code 204).
+ #
+ # The <tt>No Content</tt> response indicates that the server
+ # successfully processed the request, and is not returning any content.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-204-no-content].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#204].
+ #
+ class HTTPNoContent < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Reset Content</tt> responses (status code 205).
+ #
+ # The <tt>Reset Content</tt> response indicates that the server
+ # successfully processed the request,
+ # asks that the client reset its document view, and is not returning any content.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/205].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-205-reset-content].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#205].
+ #
+ class HTTPResetContent < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Partial Content</tt> responses (status code 206).
+ #
+ # The <tt>Partial Content</tt> response indicates that the server is delivering
+ # only part of the resource (byte serving)
+ # due to a Range header in the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-206-partial-content].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#206].
+ #
+ class HTTPPartialContent < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Multi-Status (WebDAV)</tt> responses (status code 207).
+ #
+ # The <tt>Multi-Status (WebDAV)</tt> response indicates that the server
+ # has received the request,
+ # and that the message body can contain a number of separate response codes.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 4818}[https://www.rfc-editor.org/rfc/rfc4918#section-11.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#207].
+ #
+ class HTTPMultiStatus < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Already Reported (WebDAV)</tt> responses (status code 208).
+ #
+ # The <tt>Already Reported (WebDAV)</tt> response indicates that the server
+ # has received the request,
+ # and that the members of a DAV binding have already been enumerated
+ # in a preceding part of the (multi-status) response,
+ # and are not being included again.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 5842}[https://www.rfc-editor.org/rfc/rfc5842.html#section-7.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#208].
+ #
+ class HTTPAlreadyReported < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>IM Used</tt> responses (status code 226).
+ #
+ # The <tt>IM Used</tt> response indicates that the server has fulfilled a request
+ # for the resource, and the response is a representation of the result
+ # of one or more instance-manipulations applied to the current instance.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 3229}[https://www.rfc-editor.org/rfc/rfc3229.html#section-10.4.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#226].
+ #
+ class HTTPIMUsed < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Multiple Choices</tt> responses (status code 300).
+ #
+ # The <tt>Multiple Choices</tt> response indicates that the server
+ # offers multiple options for the resource from which the client may choose.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/300].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-300-multiple-choices].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#300].
+ #
+ class HTTPMultipleChoices < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPMultipleChoice = HTTPMultipleChoices
+
+ # Response class for <tt>Moved Permanently</tt> responses (status code 301).
+ #
+ # The <tt>Moved Permanently</tt> response indicates that links or records
+ # returning this response should be updated to use the given URL.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-301-moved-permanently].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#301].
+ #
+ class HTTPMovedPermanently < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Found</tt> responses (status code 302).
+ #
+ # The <tt>Found</tt> response indicates that the client
+ # should look at (browse to) another URL.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-302-found].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#302].
+ #
+ class HTTPFound < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPMovedTemporarily = HTTPFound
+
+ # Response class for <tt>See Other</tt> responses (status code 303).
+ #
+ # The response to the request can be found under another Gem::URI using the GET method.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-303-see-other].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#303].
+ #
+ class HTTPSeeOther < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Not Modified</tt> responses (status code 304).
+ #
+ # Indicates that the resource has not been modified since the version
+ # specified by the request headers.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-304-not-modified].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#304].
+ #
+ class HTTPNotModified < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Use Proxy</tt> responses (status code 305).
+ #
+ # The requested resource is available only through a proxy,
+ # whose address is provided in the response.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-305-use-proxy].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#305].
+ #
+ class HTTPUseProxy < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Temporary Redirect</tt> responses (status code 307).
+ #
+ # The request should be repeated with another Gem::URI;
+ # however, future requests should still use the original Gem::URI.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-307-temporary-redirect].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#307].
+ #
+ class HTTPTemporaryRedirect < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Permanent Redirect</tt> responses (status code 308).
+ #
+ # This and all future requests should be directed to the given Gem::URI.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-308-permanent-redirect].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#308].
+ #
+ class HTTPPermanentRedirect < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Bad Request</tt> responses (status code 400).
+ #
+ # The server cannot or will not process the request due to an apparent client error.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-400-bad-request].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#400].
+ #
+ class HTTPBadRequest < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Unauthorized</tt> responses (status code 401).
+ #
+ # Authentication is required, but either was not provided or failed.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-401-unauthorized].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#401].
+ #
+ class HTTPUnauthorized < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Payment Required</tt> responses (status code 402).
+ #
+ # Reserved for future use.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-402-payment-required].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#402].
+ #
+ class HTTPPaymentRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Forbidden</tt> responses (status code 403).
+ #
+ # The request contained valid data and was understood by the server,
+ # but the server is refusing action.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-403-forbidden].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#403].
+ #
+ class HTTPForbidden < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Not Found</tt> responses (status code 404).
+ #
+ # The requested resource could not be found but may be available in the future.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-404-not-found].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#404].
+ #
+ class HTTPNotFound < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Method Not Allowed</tt> responses (status code 405).
+ #
+ # The request method is not supported for the requested resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-405-method-not-allowed].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#405].
+ #
+ class HTTPMethodNotAllowed < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Not Acceptable</tt> responses (status code 406).
+ #
+ # The requested resource is capable of generating only content
+ # that not acceptable according to the Accept headers sent in the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-406-not-acceptable].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#406].
+ #
+ class HTTPNotAcceptable < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Proxy Authentication Required</tt> responses (status code 407).
+ #
+ # The client must first authenticate itself with the proxy.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-407-proxy-authentication-re].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#407].
+ #
+ class HTTPProxyAuthenticationRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Request Gem::Timeout</tt> responses (status code 408).
+ #
+ # The server timed out waiting for the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-408-request-timeout].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#408].
+ #
+ class HTTPRequestTimeout < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPRequestTimeOut = HTTPRequestTimeout
+
+ # Response class for <tt>Conflict</tt> responses (status code 409).
+ #
+ # The request could not be processed because of conflict in the current state of the resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-409-conflict].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#409].
+ #
+ class HTTPConflict < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Gone</tt> responses (status code 410).
+ #
+ # The resource requested was previously in use but is no longer available
+ # and will not be available again.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-410-gone].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#410].
+ #
+ class HTTPGone < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Length Required</tt> responses (status code 411).
+ #
+ # The request did not specify the length of its content,
+ # which is required by the requested resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-411-length-required].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#411].
+ #
+ class HTTPLengthRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Precondition Failed</tt> responses (status code 412).
+ #
+ # The server does not meet one of the preconditions
+ # specified in the request headers.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-412-precondition-failed].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#412].
+ #
+ class HTTPPreconditionFailed < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Payload Too Large</tt> responses (status code 413).
+ #
+ # The request is larger than the server is willing or able to process.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-413-content-too-large].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#413].
+ #
+ class HTTPPayloadTooLarge < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPRequestEntityTooLarge = HTTPPayloadTooLarge
+
+ # Response class for <tt>Gem::URI Too Long</tt> responses (status code 414).
+ #
+ # The Gem::URI provided was too long for the server to process.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-414-uri-too-long].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#414].
+ #
+ class HTTPURITooLong < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPRequestURITooLong = HTTPURITooLong
+ HTTPRequestURITooLarge = HTTPRequestURITooLong
+
+ # Response class for <tt>Unsupported Media Type</tt> responses (status code 415).
+ #
+ # The request entity has a media type which the server or resource does not support.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-415-unsupported-media-type].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#415].
+ #
+ class HTTPUnsupportedMediaType < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Range Not Satisfiable</tt> responses (status code 416).
+ #
+ # The request entity has a media type which the server or resource does not support.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-416-range-not-satisfiable].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#416].
+ #
+ class HTTPRangeNotSatisfiable < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPRequestedRangeNotSatisfiable = HTTPRangeNotSatisfiable
+
+ # Response class for <tt>Expectation Failed</tt> responses (status code 417).
+ #
+ # The server cannot meet the requirements of the Expect request-header field.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/417].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-417-expectation-failed].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#417].
+ #
+ class HTTPExpectationFailed < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # 418 I'm a teapot - RFC 2324; a joke RFC
+ # See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#418.
+
+ # 420 Enhance Your Calm - Twitter
+
+ # Response class for <tt>Misdirected Request</tt> responses (status code 421).
+ #
+ # The request was directed at a server that is not able to produce a response.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-421-misdirected-request].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#421].
+ #
+ class HTTPMisdirectedRequest < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Unprocessable Entity</tt> responses (status code 422).
+ #
+ # The request was well-formed but had semantic errors.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-422-unprocessable-content].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#422].
+ #
+ class HTTPUnprocessableEntity < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Locked (WebDAV)</tt> responses (status code 423).
+ #
+ # The requested resource is locked.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.3].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#423].
+ #
+ class HTTPLocked < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Failed Dependency (WebDAV)</tt> responses (status code 424).
+ #
+ # The request failed because it depended on another request and that request failed.
+ # See {424 Failed Dependency (WebDAV)}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#424].
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.4].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#424].
+ #
+ class HTTPFailedDependency < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # 425 Too Early
+ # https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#425.
+
+ # Response class for <tt>Upgrade Required</tt> responses (status code 426).
+ #
+ # The client should switch to the protocol given in the Upgrade header field.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/426].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-426-upgrade-required].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#426].
+ #
+ class HTTPUpgradeRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Precondition Required</tt> responses (status code 428).
+ #
+ # The origin server requires the request to be conditional.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/428].
+ # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-3].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#428].
+ #
+ class HTTPPreconditionRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Too Many Requests</tt> responses (status code 429).
+ #
+ # The user has sent too many requests in a given amount of time.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429].
+ # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-4].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#429].
+ #
+ class HTTPTooManyRequests < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Request Header Fields Too Large</tt> responses (status code 431).
+ #
+ # An individual header field is too large,
+ # or all the header fields collectively, are too large.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431].
+ # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-5].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#431].
+ #
+ class HTTPRequestHeaderFieldsTooLarge < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Unavailable For Legal Reasons</tt> responses (status code 451).
+ #
+ # A server operator has received a legal demand to deny access to a resource or to a set of resources
+ # that includes the requested resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/451].
+ # - {RFC 7725}[https://www.rfc-editor.org/rfc/rfc7725.html#section-3].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#451].
+ #
+ class HTTPUnavailableForLegalReasons < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ # 444 No Response - Nginx
+ # 449 Retry With - Microsoft
+ # 450 Blocked by Windows Parental Controls - Microsoft
+ # 499 Client Closed Request - Nginx
+
+ # Response class for <tt>Internal Server Error</tt> responses (status code 500).
+ #
+ # An unexpected condition was encountered and no more specific message is suitable.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-500-internal-server-error].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#500].
+ #
+ class HTTPInternalServerError < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Not Implemented</tt> responses (status code 501).
+ #
+ # The server either does not recognize the request method,
+ # or it lacks the ability to fulfil the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/501].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-501-not-implemented].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#501].
+ #
+ class HTTPNotImplemented < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Bad Gateway</tt> responses (status code 502).
+ #
+ # The server was acting as a gateway or proxy
+ # and received an invalid response from the upstream server.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-502-bad-gateway].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#502].
+ #
+ class HTTPBadGateway < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Service Unavailable</tt> responses (status code 503).
+ #
+ # The server cannot handle the request
+ # (because it is overloaded or down for maintenance).
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-503-service-unavailable].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#503].
+ #
+ class HTTPServiceUnavailable < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Gateway Gem::Timeout</tt> responses (status code 504).
+ #
+ # The server was acting as a gateway or proxy
+ # and did not receive a timely response from the upstream server.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-504-gateway-timeout].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#504].
+ #
+ class HTTPGatewayTimeout < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPGatewayTimeOut = HTTPGatewayTimeout
+
+ # Response class for <tt>HTTP Version Not Supported</tt> responses (status code 505).
+ #
+ # The server does not support the HTTP version used in the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/505].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-505-http-version-not-suppor].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#505].
+ #
+ class HTTPVersionNotSupported < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Variant Also Negotiates</tt> responses (status code 506).
+ #
+ # Transparent content negotiation for the request results in a circular reference.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/506].
+ # - {RFC 2295}[https://www.rfc-editor.org/rfc/rfc2295#section-8.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#506].
+ #
+ class HTTPVariantAlsoNegotiates < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Insufficient Storage (WebDAV)</tt> responses (status code 507).
+ #
+ # The server is unable to store the representation needed to complete the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/507].
+ # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.5].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#507].
+ #
+ class HTTPInsufficientStorage < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Loop Detected (WebDAV)</tt> responses (status code 508).
+ #
+ # The server detected an infinite loop while processing the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/508].
+ # - {RFC 5942}[https://www.rfc-editor.org/rfc/rfc5842.html#section-7.2].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#508].
+ #
+ class HTTPLoopDetected < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ # 509 Bandwidth Limit Exceeded - Apache bw/limited extension
+
+ # Response class for <tt>Not Extended</tt> responses (status code 510).
+ #
+ # Further extensions to the request are required for the server to fulfill it.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/510].
+ # - {RFC 2774}[https://www.rfc-editor.org/rfc/rfc2774.html#section-7].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#510].
+ #
+ class HTTPNotExtended < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Network Authentication Required</tt> responses (status code 511).
+ #
+ # The client needs to authenticate to gain network access.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/511].
+ # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-6].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#511].
+ #
+ class HTTPNetworkAuthenticationRequired < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+end
+
+class Gem::Net::HTTPResponse
+ # :stopdoc:
+ CODE_CLASS_TO_OBJ = {
+ '1' => Gem::Net::HTTPInformation,
+ '2' => Gem::Net::HTTPSuccess,
+ '3' => Gem::Net::HTTPRedirection,
+ '4' => Gem::Net::HTTPClientError,
+ '5' => Gem::Net::HTTPServerError
+ }.freeze
+ CODE_TO_OBJ = {
+ '100' => Gem::Net::HTTPContinue,
+ '101' => Gem::Net::HTTPSwitchProtocol,
+ '102' => Gem::Net::HTTPProcessing,
+ '103' => Gem::Net::HTTPEarlyHints,
+
+ '200' => Gem::Net::HTTPOK,
+ '201' => Gem::Net::HTTPCreated,
+ '202' => Gem::Net::HTTPAccepted,
+ '203' => Gem::Net::HTTPNonAuthoritativeInformation,
+ '204' => Gem::Net::HTTPNoContent,
+ '205' => Gem::Net::HTTPResetContent,
+ '206' => Gem::Net::HTTPPartialContent,
+ '207' => Gem::Net::HTTPMultiStatus,
+ '208' => Gem::Net::HTTPAlreadyReported,
+ '226' => Gem::Net::HTTPIMUsed,
+
+ '300' => Gem::Net::HTTPMultipleChoices,
+ '301' => Gem::Net::HTTPMovedPermanently,
+ '302' => Gem::Net::HTTPFound,
+ '303' => Gem::Net::HTTPSeeOther,
+ '304' => Gem::Net::HTTPNotModified,
+ '305' => Gem::Net::HTTPUseProxy,
+ '307' => Gem::Net::HTTPTemporaryRedirect,
+ '308' => Gem::Net::HTTPPermanentRedirect,
+
+ '400' => Gem::Net::HTTPBadRequest,
+ '401' => Gem::Net::HTTPUnauthorized,
+ '402' => Gem::Net::HTTPPaymentRequired,
+ '403' => Gem::Net::HTTPForbidden,
+ '404' => Gem::Net::HTTPNotFound,
+ '405' => Gem::Net::HTTPMethodNotAllowed,
+ '406' => Gem::Net::HTTPNotAcceptable,
+ '407' => Gem::Net::HTTPProxyAuthenticationRequired,
+ '408' => Gem::Net::HTTPRequestTimeout,
+ '409' => Gem::Net::HTTPConflict,
+ '410' => Gem::Net::HTTPGone,
+ '411' => Gem::Net::HTTPLengthRequired,
+ '412' => Gem::Net::HTTPPreconditionFailed,
+ '413' => Gem::Net::HTTPPayloadTooLarge,
+ '414' => Gem::Net::HTTPURITooLong,
+ '415' => Gem::Net::HTTPUnsupportedMediaType,
+ '416' => Gem::Net::HTTPRangeNotSatisfiable,
+ '417' => Gem::Net::HTTPExpectationFailed,
+ '421' => Gem::Net::HTTPMisdirectedRequest,
+ '422' => Gem::Net::HTTPUnprocessableEntity,
+ '423' => Gem::Net::HTTPLocked,
+ '424' => Gem::Net::HTTPFailedDependency,
+ '426' => Gem::Net::HTTPUpgradeRequired,
+ '428' => Gem::Net::HTTPPreconditionRequired,
+ '429' => Gem::Net::HTTPTooManyRequests,
+ '431' => Gem::Net::HTTPRequestHeaderFieldsTooLarge,
+ '451' => Gem::Net::HTTPUnavailableForLegalReasons,
+
+ '500' => Gem::Net::HTTPInternalServerError,
+ '501' => Gem::Net::HTTPNotImplemented,
+ '502' => Gem::Net::HTTPBadGateway,
+ '503' => Gem::Net::HTTPServiceUnavailable,
+ '504' => Gem::Net::HTTPGatewayTimeout,
+ '505' => Gem::Net::HTTPVersionNotSupported,
+ '506' => Gem::Net::HTTPVariantAlsoNegotiates,
+ '507' => Gem::Net::HTTPInsufficientStorage,
+ '508' => Gem::Net::HTTPLoopDetected,
+ '510' => Gem::Net::HTTPNotExtended,
+ '511' => Gem::Net::HTTPNetworkAuthenticationRequired,
+ }.freeze
+end
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/status.rb b/lib/rubygems/vendor/net-http/lib/net/http/status.rb
new file mode 100644
index 0000000000..9110b108b8
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/status.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require_relative '../http'
+
+if $0 == __FILE__
+ require 'open-uri'
+ File.foreach(__FILE__) do |line|
+ puts line
+ break if line.start_with?('end')
+ end
+ puts
+ puts "Gem::Net::HTTP::STATUS_CODES = {"
+ url = "https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv"
+ Gem::URI(url).read.each_line do |line|
+ code, mes, = line.split(',')
+ next if ['(Unused)', 'Unassigned', 'Description'].include?(mes)
+ puts " #{code} => '#{mes}',"
+ end
+ puts "} # :nodoc:"
+end
+
+Gem::Net::HTTP::STATUS_CODES = {
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 102 => 'Processing',
+ 103 => 'Early Hints',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status',
+ 208 => 'Already Reported',
+ 226 => 'IM Used',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 307 => 'Temporary Redirect',
+ 308 => 'Permanent 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 => 'Content Too Large',
+ 414 => 'URI Too Long',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 421 => 'Misdirected Request',
+ 422 => 'Unprocessable Content',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 425 => 'Too Early',
+ 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',
+ 506 => 'Variant Also Negotiates',
+ 507 => 'Insufficient Storage',
+ 508 => 'Loop Detected',
+ 510 => 'Not Extended (OBSOLETED)',
+ 511 => 'Network Authentication Required',
+} # :nodoc:
diff --git a/lib/rubygems/vendor/net-http/lib/net/https.rb b/lib/rubygems/vendor/net-http/lib/net/https.rb
new file mode 100644
index 0000000000..f104c85c81
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/https.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+=begin
+
+= net/https -- SSL/TLS enhancement for Gem::Net::HTTP.
+
+ This file has been merged with net/http. There is no longer any need to
+ require_relative 'https' to use HTTPS.
+
+ See Gem::Net::HTTP for details on how to make HTTPS connections.
+
+== Info
+ 'OpenSSL for Ruby 2' project
+ Copyright (C) 2001 GOTOU Yuuzou <gotoyuzo@notwork.org>
+ All rights reserved.
+
+== Licence
+ This program is licensed under the same licence as Ruby.
+ (See the file 'LICENCE'.)
+
+=end
+
+require_relative 'http'
+require 'openssl'
diff --git a/lib/rubygems/vendor/net-protocol/lib/net/protocol.rb b/lib/rubygems/vendor/net-protocol/lib/net/protocol.rb
new file mode 100644
index 0000000000..53d34d8d98
--- /dev/null
+++ b/lib/rubygems/vendor/net-protocol/lib/net/protocol.rb
@@ -0,0 +1,544 @@
+# frozen_string_literal: true
+#
+# = net/protocol.rb
+#
+#--
+# Copyright (c) 1999-2004 Yukihiro Matsumoto
+# Copyright (c) 1999-2004 Minero Aoki
+#
+# written and maintained by Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software. You can re-distribute and/or
+# modify this program under the same terms as Ruby itself,
+# Ruby Distribute License or GNU General Public License.
+#
+# $Id$
+#++
+#
+# WARNING: This file is going to remove.
+# Do not rely on the implementation written in this file.
+#
+
+require 'socket'
+require_relative '../../../timeout/lib/timeout'
+require 'io/wait'
+
+module Gem::Net # :nodoc:
+
+ class Protocol #:nodoc: internal use only
+ VERSION = "0.2.2"
+
+ private
+ def Protocol.protocol_param(name, val)
+ module_eval(<<-End, __FILE__, __LINE__ + 1)
+ def #{name}
+ #{val}
+ end
+ End
+ end
+
+ def ssl_socket_connect(s, timeout)
+ if timeout
+ while true
+ raise Gem::Net::OpenTimeout if timeout <= 0
+ start = Process.clock_gettime Process::CLOCK_MONOTONIC
+ # to_io is required because SSLSocket doesn't have wait_readable yet
+ case s.connect_nonblock(exception: false)
+ when :wait_readable; s.to_io.wait_readable(timeout)
+ when :wait_writable; s.to_io.wait_writable(timeout)
+ else; break
+ end
+ timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
+ end
+ else
+ s.connect
+ end
+ end
+ end
+
+
+ class ProtocolError < StandardError; end
+ class ProtoSyntaxError < ProtocolError; end
+ class ProtoFatalError < ProtocolError; end
+ class ProtoUnknownError < ProtocolError; end
+ class ProtoServerError < ProtocolError; end
+ class ProtoAuthError < ProtocolError; end
+ class ProtoCommandError < ProtocolError; end
+ class ProtoRetriableError < ProtocolError; end
+ ProtocRetryError = ProtoRetriableError
+
+ ##
+ # OpenTimeout, a subclass of Gem::Timeout::Error, is raised if a connection cannot
+ # be created within the open_timeout.
+
+ class OpenTimeout < Gem::Timeout::Error; end
+
+ ##
+ # ReadTimeout, a subclass of Gem::Timeout::Error, is raised if a chunk of the
+ # response cannot be read within the read_timeout.
+
+ class ReadTimeout < Gem::Timeout::Error
+ def initialize(io = nil)
+ @io = io
+ end
+ attr_reader :io
+
+ def message
+ msg = super
+ if @io
+ msg = "#{msg} with #{@io.inspect}"
+ end
+ msg
+ end
+ end
+
+ ##
+ # WriteTimeout, a subclass of Gem::Timeout::Error, is raised if a chunk of the
+ # response cannot be written within the write_timeout. Not raised on Windows.
+
+ class WriteTimeout < Gem::Timeout::Error
+ def initialize(io = nil)
+ @io = io
+ end
+ attr_reader :io
+
+ def message
+ msg = super
+ if @io
+ msg = "#{msg} with #{@io.inspect}"
+ end
+ msg
+ end
+ end
+
+
+ class BufferedIO #:nodoc: internal use only
+ def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil)
+ @io = io
+ @read_timeout = read_timeout
+ @write_timeout = write_timeout
+ @continue_timeout = continue_timeout
+ @debug_output = debug_output
+ @rbuf = ''.b
+ @rbuf_empty = true
+ @rbuf_offset = 0
+ end
+
+ attr_reader :io
+ attr_accessor :read_timeout
+ attr_accessor :write_timeout
+ attr_accessor :continue_timeout
+ attr_accessor :debug_output
+
+ def inspect
+ "#<#{self.class} io=#{@io}>"
+ end
+
+ def eof?
+ @io.eof?
+ end
+
+ def closed?
+ @io.closed?
+ end
+
+ def close
+ @io.close
+ end
+
+ #
+ # Read
+ #
+
+ public
+
+ def read(len, dest = ''.b, ignore_eof = false)
+ LOG "reading #{len} bytes..."
+ read_bytes = 0
+ begin
+ while read_bytes + rbuf_size < len
+ if s = rbuf_consume_all
+ read_bytes += s.bytesize
+ dest << s
+ end
+ rbuf_fill
+ end
+ s = rbuf_consume(len - read_bytes)
+ read_bytes += s.bytesize
+ dest << s
+ rescue EOFError
+ raise unless ignore_eof
+ end
+ LOG "read #{read_bytes} bytes"
+ dest
+ end
+
+ def read_all(dest = ''.b)
+ LOG 'reading all...'
+ read_bytes = 0
+ begin
+ while true
+ if s = rbuf_consume_all
+ read_bytes += s.bytesize
+ dest << s
+ end
+ rbuf_fill
+ end
+ rescue EOFError
+ ;
+ end
+ LOG "read #{read_bytes} bytes"
+ dest
+ end
+
+ def readuntil(terminator, ignore_eof = false)
+ offset = @rbuf_offset
+ begin
+ until idx = @rbuf.index(terminator, offset)
+ offset = @rbuf.bytesize
+ rbuf_fill
+ end
+ return rbuf_consume(idx + terminator.bytesize - @rbuf_offset)
+ rescue EOFError
+ raise unless ignore_eof
+ return rbuf_consume
+ end
+ end
+
+ def readline
+ readuntil("\n").chop
+ end
+
+ private
+
+ BUFSIZE = 1024 * 16
+
+ def rbuf_fill
+ tmp = @rbuf_empty ? @rbuf : nil
+ case rv = @io.read_nonblock(BUFSIZE, tmp, exception: false)
+ when String
+ @rbuf_empty = false
+ if rv.equal?(tmp)
+ @rbuf_offset = 0
+ else
+ @rbuf << rv
+ rv.clear
+ end
+ return
+ when :wait_readable
+ (io = @io.to_io).wait_readable(@read_timeout) or raise Gem::Net::ReadTimeout.new(io)
+ # continue looping
+ when :wait_writable
+ # OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable.
+ # http://www.openssl.org/support/faq.html#PROG10
+ (io = @io.to_io).wait_writable(@read_timeout) or raise Gem::Net::ReadTimeout.new(io)
+ # continue looping
+ when nil
+ raise EOFError, 'end of file reached'
+ end while true
+ end
+
+ def rbuf_flush
+ if @rbuf_empty
+ @rbuf.clear
+ @rbuf_offset = 0
+ end
+ nil
+ end
+
+ def rbuf_size
+ @rbuf.bytesize - @rbuf_offset
+ end
+
+ def rbuf_consume_all
+ rbuf_consume if rbuf_size > 0
+ end
+
+ def rbuf_consume(len = nil)
+ if @rbuf_offset == 0 && (len.nil? || len == @rbuf.bytesize)
+ s = @rbuf
+ @rbuf = ''.b
+ @rbuf_offset = 0
+ @rbuf_empty = true
+ elsif len.nil?
+ s = @rbuf.byteslice(@rbuf_offset..-1)
+ @rbuf = ''.b
+ @rbuf_offset = 0
+ @rbuf_empty = true
+ else
+ s = @rbuf.byteslice(@rbuf_offset, len)
+ @rbuf_offset += len
+ @rbuf_empty = @rbuf_offset == @rbuf.bytesize
+ rbuf_flush
+ end
+
+ @debug_output << %Q[-> #{s.dump}\n] if @debug_output
+ s
+ end
+
+ #
+ # Write
+ #
+
+ public
+
+ def write(*strs)
+ writing {
+ write0(*strs)
+ }
+ end
+
+ alias << write
+
+ def writeline(str)
+ writing {
+ write0 str + "\r\n"
+ }
+ end
+
+ private
+
+ def writing
+ @written_bytes = 0
+ @debug_output << '<- ' if @debug_output
+ yield
+ @debug_output << "\n" if @debug_output
+ bytes = @written_bytes
+ @written_bytes = nil
+ bytes
+ end
+
+ def write0(*strs)
+ @debug_output << strs.map(&:dump).join if @debug_output
+ orig_written_bytes = @written_bytes
+ strs.each_with_index do |str, i|
+ need_retry = true
+ case len = @io.write_nonblock(str, exception: false)
+ when Integer
+ @written_bytes += len
+ len -= str.bytesize
+ if len == 0
+ if strs.size == i+1
+ return @written_bytes - orig_written_bytes
+ else
+ need_retry = false
+ # next string
+ end
+ elsif len < 0
+ str = str.byteslice(len, -len)
+ else # len > 0
+ need_retry = false
+ # next string
+ end
+ # continue looping
+ when :wait_writable
+ (io = @io.to_io).wait_writable(@write_timeout) or raise Gem::Net::WriteTimeout.new(io)
+ # continue looping
+ end while need_retry
+ end
+ end
+
+ #
+ # Logging
+ #
+
+ private
+
+ def LOG_off
+ @save_debug_out = @debug_output
+ @debug_output = nil
+ end
+
+ def LOG_on
+ @debug_output = @save_debug_out
+ end
+
+ def LOG(msg)
+ return unless @debug_output
+ @debug_output << msg + "\n"
+ end
+ end
+
+
+ class InternetMessageIO < BufferedIO #:nodoc: internal use only
+ def initialize(*, **)
+ super
+ @wbuf = nil
+ end
+
+ #
+ # Read
+ #
+
+ def each_message_chunk
+ LOG 'reading message...'
+ LOG_off()
+ read_bytes = 0
+ while (line = readuntil("\r\n")) != ".\r\n"
+ read_bytes += line.size
+ yield line.delete_prefix('.')
+ end
+ LOG_on()
+ LOG "read message (#{read_bytes} bytes)"
+ end
+
+ # *library private* (cannot handle 'break')
+ def each_list_item
+ while (str = readuntil("\r\n")) != ".\r\n"
+ yield str.chop
+ end
+ end
+
+ def write_message_0(src)
+ prev = @written_bytes
+ each_crlf_line(src) do |line|
+ write0 dot_stuff(line)
+ end
+ @written_bytes - prev
+ end
+
+ #
+ # Write
+ #
+
+ def write_message(src)
+ LOG "writing message from #{src.class}"
+ LOG_off()
+ len = writing {
+ using_each_crlf_line {
+ write_message_0 src
+ }
+ }
+ LOG_on()
+ LOG "wrote #{len} bytes"
+ len
+ end
+
+ def write_message_by_block(&block)
+ LOG 'writing message from block'
+ LOG_off()
+ len = writing {
+ using_each_crlf_line {
+ begin
+ block.call(WriteAdapter.new(self.method(:write_message_0)))
+ rescue LocalJumpError
+ # allow `break' from writer block
+ end
+ }
+ }
+ LOG_on()
+ LOG "wrote #{len} bytes"
+ len
+ end
+
+ private
+
+ def dot_stuff(s)
+ s.sub(/\A\./, '..')
+ end
+
+ def using_each_crlf_line
+ @wbuf = ''.b
+ yield
+ if not @wbuf.empty? # unterminated last line
+ write0 dot_stuff(@wbuf.chomp) + "\r\n"
+ elsif @written_bytes == 0 # empty src
+ write0 "\r\n"
+ end
+ write0 ".\r\n"
+ @wbuf = nil
+ end
+
+ def each_crlf_line(src)
+ buffer_filling(@wbuf, src) do
+ while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/)
+ yield line.chomp("\n") + "\r\n"
+ end
+ end
+ end
+
+ def buffer_filling(buf, src)
+ case src
+ when String # for speeding up.
+ 0.step(src.size - 1, 1024) do |i|
+ buf << src[i, 1024]
+ yield
+ end
+ when File # for speeding up.
+ while s = src.read(1024)
+ buf << s
+ yield
+ end
+ else # generic reader
+ src.each do |str|
+ buf << str
+ yield if buf.size > 1024
+ end
+ yield unless buf.empty?
+ end
+ end
+ end
+
+
+ #
+ # The writer adapter class
+ #
+ class WriteAdapter
+ def initialize(writer)
+ @writer = writer
+ end
+
+ def inspect
+ "#<#{self.class} writer=#{@writer.inspect}>"
+ end
+
+ def write(str)
+ @writer.call(str)
+ end
+
+ alias print write
+
+ def <<(str)
+ write str
+ self
+ end
+
+ def puts(str = '')
+ write str.chomp("\n") + "\n"
+ end
+
+ def printf(*args)
+ write sprintf(*args)
+ end
+ end
+
+
+ class ReadAdapter #:nodoc: internal use only
+ def initialize(block)
+ @block = block
+ end
+
+ def inspect
+ "#<#{self.class}>"
+ end
+
+ def <<(str)
+ call_block(str, &@block) if @block
+ end
+
+ private
+
+ # This method is needed because @block must be called by yield,
+ # not Proc#call. You can see difference when using `break' in
+ # the block.
+ def call_block(str)
+ yield str
+ end
+ end
+
+
+ module NetPrivate #:nodoc: obsolete
+ Socket = ::Gem::Net::InternetMessageIO
+ end
+
+end # module Gem::Net
diff --git a/lib/rubygems/vendor/optparse/lib/optionparser.rb b/lib/rubygems/vendor/optparse/lib/optionparser.rb
new file mode 100644
index 0000000000..4b9b40d82a
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optionparser.rb
@@ -0,0 +1,2 @@
+# frozen_string_literal: false
+require_relative 'optparse'
diff --git a/lib/rubygems/vendor/optparse/lib/optparse.rb b/lib/rubygems/vendor/optparse/lib/optparse.rb
new file mode 100644
index 0000000000..d39d9dd4e0
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse.rb
@@ -0,0 +1,2467 @@
+# frozen_string_literal: true
+#
+# optparse.rb - command-line option analysis with the Gem::OptionParser class.
+#
+# Author:: Nobu Nakada
+# Documentation:: Nobu Nakada and Gavin Sinclair.
+#
+# See Gem::OptionParser for documentation.
+#
+require 'set' unless defined?(Set)
+
+#--
+# == Developer Documentation (not for RDoc output)
+#
+# === Class tree
+#
+# - Gem::OptionParser:: front end
+# - Gem::OptionParser::Switch:: each switches
+# - Gem::OptionParser::List:: options list
+# - Gem::OptionParser::ParseError:: errors on parsing
+# - Gem::OptionParser::AmbiguousOption
+# - Gem::OptionParser::NeedlessArgument
+# - Gem::OptionParser::MissingArgument
+# - Gem::OptionParser::InvalidOption
+# - Gem::OptionParser::InvalidArgument
+# - Gem::OptionParser::AmbiguousArgument
+#
+# === Object relationship diagram
+#
+# +--------------+
+# | Gem::OptionParser |<>-----+
+# +--------------+ | +--------+
+# | ,-| Switch |
+# on_head -------->+---------------+ / +--------+
+# accept/reject -->| List |<|>-
+# | |<|>- +----------+
+# on ------------->+---------------+ `-| argument |
+# : : | class |
+# +---------------+ |==========|
+# on_tail -------->| | |pattern |
+# +---------------+ |----------|
+# Gem::OptionParser.accept ->| DefaultList | |converter |
+# reject |(shared between| +----------+
+# | all instances)|
+# +---------------+
+#
+#++
+#
+# == Gem::OptionParser
+#
+# === New to +Gem::OptionParser+?
+#
+# See the {Tutorial}[optparse/tutorial.rdoc].
+#
+# === Introduction
+#
+# Gem::OptionParser is a class for command-line option analysis. It is much more
+# advanced, yet also easier to use, than GetoptLong, and is a more Ruby-oriented
+# solution.
+#
+# === Features
+#
+# 1. The argument specification and the code to handle it are written in the
+# same place.
+# 2. It can output an option summary; you don't need to maintain this string
+# separately.
+# 3. Optional and mandatory arguments are specified very gracefully.
+# 4. Arguments can be automatically converted to a specified class.
+# 5. Arguments can be restricted to a certain set.
+#
+# All of these features are demonstrated in the examples below. See
+# #make_switch for full documentation.
+#
+# === Minimal example
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+#
+# options = {}
+# Gem::OptionParser.new do |parser|
+# parser.banner = "Usage: example.rb [options]"
+#
+# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
+# options[:verbose] = v
+# end
+# end.parse!
+#
+# p options
+# p ARGV
+#
+# === Generating Help
+#
+# Gem::OptionParser can be used to automatically generate help for the commands you
+# write:
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+#
+# Options = Struct.new(:name)
+#
+# class Parser
+# def self.parse(options)
+# args = Options.new("world")
+#
+# opt_parser = Gem::OptionParser.new do |parser|
+# parser.banner = "Usage: example.rb [options]"
+#
+# parser.on("-nNAME", "--name=NAME", "Name to say hello to") do |n|
+# args.name = n
+# end
+#
+# parser.on("-h", "--help", "Prints this help") do
+# puts parser
+# exit
+# end
+# end
+#
+# opt_parser.parse!(options)
+# return args
+# end
+# end
+# options = Parser.parse %w[--help]
+#
+# #=>
+# # Usage: example.rb [options]
+# # -n, --name=NAME Name to say hello to
+# # -h, --help Prints this help
+#
+# === Required Arguments
+#
+# For options that require an argument, option specification strings may include an
+# option name in all caps. If an option is used without the required argument,
+# an exception will be raised.
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+#
+# options = {}
+# Gem::OptionParser.new do |parser|
+# parser.on("-r", "--require LIBRARY",
+# "Require the LIBRARY before executing your script") do |lib|
+# puts "You required #{lib}!"
+# end
+# end.parse!
+#
+# Used:
+#
+# $ ruby optparse-test.rb -r
+# optparse-test.rb:9:in '<main>': missing argument: -r (Gem::OptionParser::MissingArgument)
+# $ ruby optparse-test.rb -r my-library
+# You required my-library!
+#
+# === Type Coercion
+#
+# Gem::OptionParser supports the ability to coerce command line arguments
+# into objects for us.
+#
+# Gem::OptionParser comes with a few ready-to-use kinds of type
+# coercion. They are:
+#
+# - Date -- Anything accepted by +Date.parse+ (need to require +optparse/date+)
+# - DateTime -- Anything accepted by +DateTime.parse+ (need to require +optparse/date+)
+# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+ (need to require +optparse/time+)
+# - URI -- Anything accepted by +Gem::URI.parse+ (need to require +optparse/uri+)
+# - Shellwords -- Anything accepted by +Shellwords.shellwords+ (need to require +optparse/shellwords+)
+# - String -- Any non-empty string
+# - Integer -- Any integer. Will convert octal. (e.g. 124, -3, 040)
+# - Float -- Any float. (e.g. 10, 3.14, -100E+13)
+# - Numeric -- Any integer, float, or rational (1, 3.4, 1/3)
+# - DecimalInteger -- Like +Integer+, but no octal format.
+# - OctalInteger -- Like +Integer+, but no decimal format.
+# - DecimalNumeric -- Decimal integer or float.
+# - TrueClass -- Accepts '+, yes, true, -, no, false' and
+# defaults as +true+
+# - FalseClass -- Same as +TrueClass+, but defaults to +false+
+# - Array -- Strings separated by ',' (e.g. 1,2,3)
+# - Regexp -- Regular expressions. Also includes options.
+#
+# We can also add our own coercions, which we will cover below.
+#
+# ==== Using Built-in Conversions
+#
+# As an example, the built-in +Time+ conversion is used. The other built-in
+# conversions behave in the same way.
+# Gem::OptionParser will attempt to parse the argument
+# as a +Time+. If it succeeds, that time will be passed to the
+# handler block. Otherwise, an exception will be raised.
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+# require 'rubygems/vendor/optparse/lib/optparse/time'
+# Gem::OptionParser.new do |parser|
+# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
+# p time
+# end
+# end.parse!
+#
+# Used:
+#
+# $ ruby optparse-test.rb -t nonsense
+# ... invalid argument: -t nonsense (Gem::OptionParser::InvalidArgument)
+# $ ruby optparse-test.rb -t 10-11-12
+# 2010-11-12 00:00:00 -0500
+# $ ruby optparse-test.rb -t 9:30
+# 2014-08-13 09:30:00 -0400
+#
+# ==== Creating Custom Conversions
+#
+# The +accept+ method on Gem::OptionParser may be used to create converters.
+# It specifies which conversion block to call whenever a class is specified.
+# The example below uses it to fetch a +User+ object before the +on+ handler receives it.
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+#
+# User = Struct.new(:id, :name)
+#
+# def find_user id
+# not_found = ->{ raise "No User Found for id #{id}" }
+# [ User.new(1, "Sam"),
+# User.new(2, "Gandalf") ].find(not_found) do |u|
+# u.id == id
+# end
+# end
+#
+# op = Gem::OptionParser.new
+# op.accept(User) do |user_id|
+# find_user user_id.to_i
+# end
+#
+# op.on("--user ID", User) do |user|
+# puts user
+# end
+#
+# op.parse!
+#
+# Used:
+#
+# $ ruby optparse-test.rb --user 1
+# #<struct User id=1, name="Sam">
+# $ ruby optparse-test.rb --user 2
+# #<struct User id=2, name="Gandalf">
+# $ ruby optparse-test.rb --user 3
+# optparse-test.rb:15:in 'block in find_user': No User Found for id 3 (RuntimeError)
+#
+# === Store options to a Hash
+#
+# The +into+ option of +order+, +parse+ and so on methods stores command line options into a Hash.
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+#
+# options = {}
+# Gem::OptionParser.new do |parser|
+# parser.on('-a')
+# parser.on('-b NUM', Integer)
+# parser.on('-v', '--verbose')
+# end.parse!(into: options)
+#
+# p options
+#
+# Used:
+#
+# $ ruby optparse-test.rb -a
+# {:a=>true}
+# $ ruby optparse-test.rb -a -v
+# {:a=>true, :verbose=>true}
+# $ ruby optparse-test.rb -a -b 100
+# {:a=>true, :b=>100}
+#
+# === Complete example
+#
+# The following example is a complete Ruby program. You can run it and see the
+# effect of specifying various options. This is probably the best way to learn
+# the features of +optparse+.
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+# require 'rubygems/vendor/optparse/lib/optparse/time'
+# require 'ostruct'
+# require 'pp'
+#
+# class OptparseExample
+# Version = '1.0.0'
+#
+# CODES = %w[iso-2022-jp shift_jis euc-jp utf8 binary]
+# CODE_ALIASES = { "jis" => "iso-2022-jp", "sjis" => "shift_jis" }
+#
+# class ScriptOptions
+# attr_accessor :library, :inplace, :encoding, :transfer_type,
+# :verbose, :extension, :delay, :time, :record_separator,
+# :list
+#
+# def initialize
+# self.library = []
+# self.inplace = false
+# self.encoding = "utf8"
+# self.transfer_type = :auto
+# self.verbose = false
+# end
+#
+# def define_options(parser)
+# parser.banner = "Usage: example.rb [options]"
+# parser.separator ""
+# parser.separator "Specific options:"
+#
+# # add additional options
+# perform_inplace_option(parser)
+# delay_execution_option(parser)
+# execute_at_time_option(parser)
+# specify_record_separator_option(parser)
+# list_example_option(parser)
+# specify_encoding_option(parser)
+# optional_option_argument_with_keyword_completion_option(parser)
+# boolean_verbose_option(parser)
+#
+# parser.separator ""
+# parser.separator "Common options:"
+# # No argument, shows at tail. This will print an options summary.
+# # Try it and see!
+# parser.on_tail("-h", "--help", "Show this message") do
+# puts parser
+# exit
+# end
+# # Another typical switch to print the version.
+# parser.on_tail("--version", "Show version") do
+# puts Version
+# exit
+# end
+# end
+#
+# def perform_inplace_option(parser)
+# # Specifies an optional option argument
+# parser.on("-i", "--inplace [EXTENSION]",
+# "Edit ARGV files in place",
+# "(make backup if EXTENSION supplied)") do |ext|
+# self.inplace = true
+# self.extension = ext || ''
+# self.extension.sub!(/\A\.?(?=.)/, ".") # Ensure extension begins with dot.
+# end
+# end
+#
+# def delay_execution_option(parser)
+# # Cast 'delay' argument to a Float.
+# parser.on("--delay N", Float, "Delay N seconds before executing") do |n|
+# self.delay = n
+# end
+# end
+#
+# def execute_at_time_option(parser)
+# # Cast 'time' argument to a Time object.
+# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
+# self.time = time
+# end
+# end
+#
+# def specify_record_separator_option(parser)
+# # Cast to octal integer.
+# parser.on("-F", "--irs [OCTAL]", Gem::OptionParser::OctalInteger,
+# "Specify record separator (default \\0)") do |rs|
+# self.record_separator = rs
+# end
+# end
+#
+# def list_example_option(parser)
+# # List of arguments.
+# parser.on("--list x,y,z", Array, "Example 'list' of arguments") do |list|
+# self.list = list
+# end
+# end
+#
+# def specify_encoding_option(parser)
+# # Keyword completion. We are specifying a specific set of arguments (CODES
+# # and CODE_ALIASES - notice the latter is a Hash), and the user may provide
+# # the shortest unambiguous text.
+# code_list = (CODE_ALIASES.keys + CODES).join(', ')
+# parser.on("--code CODE", CODES, CODE_ALIASES, "Select encoding",
+# "(#{code_list})") do |encoding|
+# self.encoding = encoding
+# end
+# end
+#
+# def optional_option_argument_with_keyword_completion_option(parser)
+# # Optional '--type' option argument with keyword completion.
+# parser.on("--type [TYPE]", [:text, :binary, :auto],
+# "Select transfer type (text, binary, auto)") do |t|
+# self.transfer_type = t
+# end
+# end
+#
+# def boolean_verbose_option(parser)
+# # Boolean switch.
+# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
+# self.verbose = v
+# end
+# end
+# end
+#
+# #
+# # Return a structure describing the options.
+# #
+# def parse(args)
+# # The options specified on the command line will be collected in
+# # *options*.
+#
+# @options = ScriptOptions.new
+# @args = Gem::OptionParser.new do |parser|
+# @options.define_options(parser)
+# parser.parse!(args)
+# end
+# @options
+# end
+#
+# attr_reader :parser, :options
+# end # class OptparseExample
+#
+# example = OptparseExample.new
+# options = example.parse(ARGV)
+# pp options # example.options
+# pp ARGV
+#
+# === Shell Completion
+#
+# For modern shells (e.g. bash, zsh, etc.), you can use shell
+# completion for command line options.
+#
+# === Further documentation
+#
+# The above examples, along with the accompanying
+# {Tutorial}[optparse/tutorial.rdoc],
+# should be enough to learn how to use this class.
+# If you have any questions, file a ticket at http://bugs.ruby-lang.org.
+#
+class Gem::OptionParser
+ # The version string
+ VERSION = "0.8.0"
+ Version = VERSION # for compatibility
+
+ # :stopdoc:
+ NoArgument = [NO_ARGUMENT = :NONE, nil].freeze
+ RequiredArgument = [REQUIRED_ARGUMENT = :REQUIRED, true].freeze
+ OptionalArgument = [OPTIONAL_ARGUMENT = :OPTIONAL, false].freeze
+ # :startdoc:
+
+ #
+ # Keyword completion module. This allows partial arguments to be specified
+ # and resolved against a list of acceptable values.
+ #
+ module Completion
+ # :nodoc:
+
+ def self.regexp(key, icase)
+ Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'), icase)
+ end
+
+ def self.candidate(key, icase = false, pat = nil, &block)
+ pat ||= Completion.regexp(key, icase)
+ candidates = []
+ block.call do |k, *v|
+ (if Regexp === k
+ kn = ""
+ k === key
+ else
+ kn = defined?(k.id2name) ? k.id2name : k
+ pat === kn
+ end) or next
+ v << k if v.empty?
+ candidates << [k, v, kn]
+ end
+ candidates
+ end
+
+ def self.completable?(key)
+ String.try_convert(key) or defined?(key.id2name)
+ end
+
+ def candidate(key, icase = false, pat = nil, &_)
+ Completion.candidate(key, icase, pat, &method(:each))
+ end
+
+ public
+ def complete(key, icase = false, pat = nil)
+ candidates = candidate(key, icase, pat, &method(:each)).sort_by {|k, v, kn| kn.size}
+ if candidates.size == 1
+ canon, sw, * = candidates[0]
+ elsif candidates.size > 1
+ canon, sw, cn = candidates.shift
+ candidates.each do |k, v, kn|
+ next if sw == v
+ if String === cn and String === kn
+ if cn.rindex(kn, 0)
+ canon, sw, cn = k, v, kn
+ next
+ elsif kn.rindex(cn, 0)
+ next
+ end
+ end
+ throw :ambiguous, key
+ end
+ end
+ if canon
+ block_given? or return key, *sw
+ yield(key, *sw)
+ end
+ end
+
+ def convert(opt = nil, val = nil, *)
+ val
+ end
+ end
+
+ #
+ # Map from option/keyword string to object with completion.
+ #
+ class OptionMap < Hash
+ include Completion
+ end
+
+ #
+ # Individual switch class. Not important to the user.
+ #
+ # Defined within Switch are several Switch-derived classes: NoArgument,
+ # RequiredArgument, etc.
+ #
+ class Switch
+ # :nodoc:
+
+ attr_reader :pattern, :conv, :short, :long, :arg, :desc, :block
+
+ #
+ # Guesses argument style from +arg+. Returns corresponding
+ # Gem::OptionParser::Switch class (OptionalArgument, etc.).
+ #
+ def self.guess(arg)
+ case arg
+ when ""
+ t = self
+ when /\A=?\[/
+ t = Switch::OptionalArgument
+ when /\A\s+\[/
+ t = Switch::PlacedArgument
+ else
+ t = Switch::RequiredArgument
+ end
+ self >= t or incompatible_argument_styles(arg, t)
+ t
+ end
+
+ def self.incompatible_argument_styles(arg, t)
+ raise(ArgumentError, "#{arg}: incompatible argument styles\n #{self}, #{t}",
+ ParseError.filter_backtrace(caller(2)))
+ end
+
+ def self.pattern
+ NilClass
+ end
+
+ def initialize(pattern = nil, conv = nil,
+ short = nil, long = nil, arg = nil,
+ desc = ([] if short or long), block = nil, values = nil, &_block)
+ raise if Array === pattern
+ block ||= _block
+ @pattern, @conv, @short, @long, @arg, @desc, @block, @values =
+ pattern, conv, short, long, arg, desc, block, values
+ end
+
+ #
+ # Parses +arg+ and returns rest of +arg+ and matched portion to the
+ # argument pattern. Yields when the pattern doesn't match substring.
+ #
+ def parse_arg(arg) # :nodoc:
+ pattern or return nil, [arg]
+ unless m = pattern.match(arg)
+ yield(InvalidArgument, arg)
+ return arg, []
+ end
+ if String === m
+ m = [s = m]
+ else
+ m = m.to_a
+ s = m[0]
+ return nil, m unless String === s
+ end
+ raise InvalidArgument, arg unless arg.rindex(s, 0)
+ return nil, m if s.length == arg.length
+ yield(InvalidArgument, arg) # didn't match whole arg
+ return arg[s.length..-1], m
+ end
+ private :parse_arg
+
+ #
+ # Parses argument, converts and returns +arg+, +block+ and result of
+ # conversion. Yields at semi-error condition instead of raising an
+ # exception.
+ #
+ def conv_arg(arg, val = []) # :nodoc:
+ v, = *val
+ if conv
+ val = conv.call(*val)
+ else
+ val = proc {|v| v}.call(*val)
+ end
+ if @values
+ @values.include?(val) or raise InvalidArgument, v
+ end
+ return arg, block, val
+ end
+ private :conv_arg
+
+ #
+ # Produces the summary text. Each line of the summary is yielded to the
+ # block (without newline).
+ #
+ # +sdone+:: Already summarized short style options keyed hash.
+ # +ldone+:: Already summarized long style options keyed hash.
+ # +width+:: Width of left side (option part). In other words, the right
+ # side (description part) starts after +width+ columns.
+ # +max+:: Maximum width of left side -> the options are filled within
+ # +max+ columns.
+ # +indent+:: Prefix string indents all summarized lines.
+ #
+ def summarize(sdone = {}, ldone = {}, width = 1, max = width - 1, indent = "")
+ sopts, lopts = [], [], nil
+ @short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short
+ @long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long
+ return if sopts.empty? and lopts.empty? # completely hidden
+
+ left = [sopts.join(', ')]
+ right = desc.dup
+
+ while s = lopts.shift
+ l = left[-1].length + s.length
+ l += arg.length if left.size == 1 && arg
+ l < max or sopts.empty? or left << +''
+ left[-1] << (left[-1].empty? ? ' ' * 4 : ', ') << s
+ end
+
+ if arg
+ left[0] << (left[1] ? arg.sub(/\A(\[?)=/, '\1') + ',' : arg)
+ end
+ mlen = left.collect {|ss| ss.length}.max.to_i
+ while mlen > width and l = left.shift
+ mlen = left.collect {|ss| ss.length}.max.to_i if l.length == mlen
+ if l.length < width and (r = right[0]) and !r.empty?
+ l = l.to_s.ljust(width) + ' ' + r
+ right.shift
+ end
+ yield(indent + l)
+ end
+
+ while begin l = left.shift; r = right.shift; l or r end
+ l = l.to_s.ljust(width) + ' ' + r if r and !r.empty?
+ yield(indent + l)
+ end
+
+ self
+ end
+
+ def add_banner(to) # :nodoc:
+ unless @short or @long
+ s = desc.join
+ to << " [" + s + "]..." unless s.empty?
+ end
+ to
+ end
+
+ def match_nonswitch?(str) # :nodoc:
+ @pattern =~ str unless @short or @long
+ end
+
+ #
+ # Main name of the switch.
+ #
+ def switch_name
+ (long.first || short.first).sub(/\A-+(?:\[no-\])?/, '')
+ end
+
+ def compsys(sdone, ldone) # :nodoc:
+ sopts, lopts = [], []
+ @short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short
+ @long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long
+ return if sopts.empty? and lopts.empty? # completely hidden
+
+ (sopts+lopts).each do |opt|
+ # "(-x -c -r)-l[left justify]"
+ if /\A--\[no-\](.+)$/ =~ opt
+ o = $1
+ yield("--#{o}", desc.join(""))
+ yield("--no-#{o}", desc.join(""))
+ else
+ yield("#{opt}", desc.join(""))
+ end
+ end
+ end
+
+ def pretty_print_contents(q) # :nodoc:
+ if @block
+ q.text ":" + @block.source_location.join(":") + ":"
+ first = false
+ else
+ first = true
+ end
+ [@short, @long].each do |list|
+ list.each do |opt|
+ if first
+ q.text ":"
+ first = false
+ end
+ q.breakable
+ q.text opt
+ end
+ end
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) {pretty_print_contents(q)}
+ end
+
+ def omitted_argument(val) # :nodoc:
+ val.pop if val.size == 3 and val.last.nil?
+ val
+ end
+
+ #
+ # Switch that takes no arguments.
+ #
+ class NoArgument < self
+
+ #
+ # Raises an exception if any arguments given.
+ #
+ def parse(arg, argv)
+ yield(NeedlessArgument, arg) if arg
+ conv_arg(arg)
+ end
+
+ def self.incompatible_argument_styles(*) # :nodoc:
+ end
+
+ def self.pattern # :nodoc:
+ Object
+ end
+
+ def pretty_head # :nodoc:
+ "NoArgument"
+ end
+ end
+
+ #
+ # Switch that takes an argument.
+ #
+ class RequiredArgument < self
+
+ #
+ # Raises an exception if argument is not present.
+ #
+ def parse(arg, argv, &_)
+ unless arg
+ raise MissingArgument if argv.empty?
+ arg = argv.shift
+ end
+ conv_arg(*parse_arg(arg, &method(:raise)))
+ end
+
+ def pretty_head # :nodoc:
+ "Required"
+ end
+ end
+
+ #
+ # Switch that can omit argument.
+ #
+ class OptionalArgument < self
+
+ #
+ # Parses argument if given, or uses default value.
+ #
+ def parse(arg, argv, &error)
+ if arg
+ conv_arg(*parse_arg(arg, &error))
+ else
+ omitted_argument conv_arg(arg)
+ end
+ end
+
+ def pretty_head # :nodoc:
+ "Optional"
+ end
+ end
+
+ #
+ # Switch that takes an argument, which does not begin with '-' or is '-'.
+ #
+ class PlacedArgument < self
+
+ #
+ # Returns nil if argument is not present or begins with '-' and is not '-'.
+ #
+ def parse(arg, argv, &error)
+ if !(val = arg) and (argv.empty? or /\A-./ =~ (val = argv[0]))
+ return nil, block
+ end
+ opt = (val = parse_arg(val, &error))[1]
+ val = conv_arg(*val)
+ if opt and !arg
+ argv.shift
+ else
+ omitted_argument val
+ val[0] = nil
+ end
+ val
+ end
+
+ def pretty_head # :nodoc:
+ "Placed"
+ end
+ end
+ end
+
+ #
+ # Simple option list providing mapping from short and/or long option
+ # string to Gem::OptionParser::Switch and mapping from acceptable argument to
+ # matching pattern and converter pair. Also provides summary feature.
+ #
+ class List
+ # :nodoc:
+
+ # Map from acceptable argument types to pattern and converter pairs.
+ attr_reader :atype
+
+ # Map from short style option switches to actual switch objects.
+ attr_reader :short
+
+ # Map from long style option switches to actual switch objects.
+ attr_reader :long
+
+ # List of all switches and summary string.
+ attr_reader :list
+
+ #
+ # Just initializes all instance variables.
+ #
+ def initialize
+ @atype = {}
+ @short = OptionMap.new
+ @long = OptionMap.new
+ @list = []
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group(1, "(", ")") do
+ @list.each do |sw|
+ next unless Switch === sw
+ q.group(1, "(" + sw.pretty_head, ")") do
+ sw.pretty_print_contents(q)
+ end
+ end
+ end
+ end
+
+ #
+ # See Gem::OptionParser.accept.
+ #
+ def accept(t, pat = /.*/m, &block)
+ if pat
+ pat.respond_to?(:match) or
+ raise TypeError, "has no 'match'", ParseError.filter_backtrace(caller(2))
+ else
+ pat = t if t.respond_to?(:match)
+ end
+ unless block
+ block = pat.method(:convert).to_proc if pat.respond_to?(:convert)
+ end
+ @atype[t] = [pat, block]
+ end
+
+ #
+ # See Gem::OptionParser.reject.
+ #
+ def reject(t)
+ @atype.delete(t)
+ end
+
+ #
+ # Adds +sw+ according to +sopts+, +lopts+ and +nlopts+.
+ #
+ # +sw+:: Gem::OptionParser::Switch instance to be added.
+ # +sopts+:: Short style option list.
+ # +lopts+:: Long style option list.
+ # +nlopts+:: Negated long style options list.
+ #
+ def update(sw, sopts, lopts, nsw = nil, nlopts = nil) # :nodoc:
+ sopts.each {|o| @short[o] = sw} if sopts
+ lopts.each {|o| @long[o] = sw} if lopts
+ nlopts.each {|o| @long[o] = nsw} if nsw and nlopts
+ used = @short.invert.update(@long.invert)
+ @list.delete_if {|o| Switch === o and !used[o]}
+ end
+ private :update
+
+ #
+ # Inserts +switch+ at the head of the list, and associates short, long
+ # and negated long options. Arguments are:
+ #
+ # +switch+:: Gem::OptionParser::Switch instance to be inserted.
+ # +short_opts+:: List of short style options.
+ # +long_opts+:: List of long style options.
+ # +nolong_opts+:: List of long style options with "no-" prefix.
+ #
+ # prepend(switch, short_opts, long_opts, nolong_opts)
+ #
+ def prepend(*args)
+ update(*args)
+ @list.unshift(args[0])
+ end
+
+ #
+ # Appends +switch+ at the tail of the list, and associates short, long
+ # and negated long options. Arguments are:
+ #
+ # +switch+:: Gem::OptionParser::Switch instance to be inserted.
+ # +short_opts+:: List of short style options.
+ # +long_opts+:: List of long style options.
+ # +nolong_opts+:: List of long style options with "no-" prefix.
+ #
+ # append(switch, short_opts, long_opts, nolong_opts)
+ #
+ def append(*args)
+ update(*args)
+ @list.push(args[0])
+ end
+
+ #
+ # Searches +key+ in +id+ list. The result is returned or yielded if a
+ # block is given. If it isn't found, nil is returned.
+ #
+ def search(id, key)
+ if list = __send__(id)
+ val = list.fetch(key) {return nil}
+ block_given? ? yield(val) : val
+ end
+ end
+
+ #
+ # Searches list +id+ for +opt+ and the optional patterns for completion
+ # +pat+. If +icase+ is true, the search is case insensitive. The result
+ # is returned or yielded if a block is given. If it isn't found, nil is
+ # returned.
+ #
+ def complete(id, opt, icase = false, *pat, &block)
+ __send__(id).complete(opt, icase, *pat, &block)
+ end
+
+ def get_candidates(id)
+ yield __send__(id).keys
+ end
+
+ #
+ # Iterates over each option, passing the option to the +block+.
+ #
+ def each_option(&block)
+ list.each(&block)
+ end
+
+ #
+ # Creates the summary table, passing each line to the +block+ (without
+ # newline). The arguments +args+ are passed along to the summarize
+ # method which is called on every option.
+ #
+ def summarize(*args, &block)
+ sum = []
+ list.reverse_each do |opt|
+ if opt.respond_to?(:summarize) # perhaps Gem::OptionParser::Switch
+ s = []
+ opt.summarize(*args) {|l| s << l}
+ sum.concat(s.reverse)
+ elsif !opt or opt.empty?
+ sum << ""
+ elsif opt.respond_to?(:each_line)
+ sum.concat([*opt.each_line].reverse)
+ else
+ sum.concat([*opt.each].reverse)
+ end
+ end
+ sum.reverse_each(&block)
+ end
+
+ def add_banner(to) # :nodoc:
+ list.each do |opt|
+ if opt.respond_to?(:add_banner)
+ opt.add_banner(to)
+ end
+ end
+ to
+ end
+
+ def compsys(*args, &block) # :nodoc:
+ list.each do |opt|
+ if opt.respond_to?(:compsys)
+ opt.compsys(*args, &block)
+ end
+ end
+ end
+ end
+
+ #
+ # Hash with completion search feature. See Gem::OptionParser::Completion.
+ #
+ class CompletingHash < Hash
+ include Completion
+
+ #
+ # Completion for hash key.
+ #
+ def match(key)
+ *values = fetch(key) {
+ raise AmbiguousArgument, catch(:ambiguous) {return complete(key)}
+ }
+ return key, *values
+ end
+ end
+
+ # :stopdoc:
+
+ #
+ # Enumeration of acceptable argument styles. Possible values are:
+ #
+ # NO_ARGUMENT:: The switch takes no arguments. (:NONE)
+ # REQUIRED_ARGUMENT:: The switch requires an argument. (:REQUIRED)
+ # OPTIONAL_ARGUMENT:: The switch requires an optional argument. (:OPTIONAL)
+ #
+ # Use like --switch=argument (long style) or -Xargument (short style). For
+ # short style, only portion matched to argument pattern is treated as
+ # argument.
+ #
+ ArgumentStyle = {}
+ NoArgument.each {|el| ArgumentStyle[el] = Switch::NoArgument}
+ RequiredArgument.each {|el| ArgumentStyle[el] = Switch::RequiredArgument}
+ OptionalArgument.each {|el| ArgumentStyle[el] = Switch::OptionalArgument}
+ ArgumentStyle.freeze
+
+ #
+ # Switches common used such as '--', and also provides default
+ # argument classes
+ #
+ DefaultList = List.new
+ DefaultList.short['-'] = Switch::NoArgument.new {}
+ DefaultList.long[''] = Switch::NoArgument.new {throw :terminate}
+
+ COMPSYS_HEADER = <<'XXX' # :nodoc:
+
+typeset -A opt_args
+local context state line
+
+_arguments -s -S \
+XXX
+
+ def compsys(to, name = File.basename($0)) # :nodoc:
+ to << "#compdef #{name}\n"
+ to << COMPSYS_HEADER
+ visit(:compsys, {}, {}) {|o, d|
+ to << %Q[ "#{o}[#{d.gsub(/[\\\"\[\]]/, '\\\\\&')}]" \\\n]
+ }
+ to << " '*:file:_files' && return 0\n"
+ end
+
+ def help_exit
+ if $stdout.tty? && (pager = ENV.values_at(*%w[RUBY_PAGER PAGER]).find {|e| e && !e.empty?})
+ less = ENV["LESS"]
+ args = [{"LESS" => "#{less} -Fe"}, pager, "w"]
+ print = proc do |f|
+ f.puts help
+ rescue Errno::EPIPE
+ # pager terminated
+ end
+ if Process.respond_to?(:fork) and false
+ IO.popen("-") {|f| f ? Process.exec(*args, in: f) : print.call($stdout)}
+ # unreachable
+ end
+ IO.popen(*args, &print)
+ else
+ puts help
+ end
+ exit
+ end
+
+ #
+ # Default options for ARGV, which never appear in option summary.
+ #
+ Officious = {}
+
+ #
+ # --help
+ # Shows option summary.
+ #
+ Officious['help'] = proc do |parser|
+ Switch::NoArgument.new do |arg|
+ parser.help_exit
+ end
+ end
+
+ #
+ # --*-completion-bash=WORD
+ # Shows candidates for command line completion.
+ #
+ Officious['*-completion-bash'] = proc do |parser|
+ Switch::RequiredArgument.new do |arg|
+ puts parser.candidate(arg)
+ exit
+ end
+ end
+
+ #
+ # --*-completion-zsh[=NAME:FILE]
+ # Creates zsh completion file.
+ #
+ Officious['*-completion-zsh'] = proc do |parser|
+ Switch::OptionalArgument.new do |arg|
+ parser.compsys($stdout, arg)
+ exit
+ end
+ end
+
+ #
+ # --version
+ # Shows version string if Version is defined.
+ #
+ Officious['version'] = proc do |parser|
+ Switch::OptionalArgument.new do |pkg|
+ if pkg
+ begin
+ require_relative 'optparse/version'
+ rescue LoadError
+ else
+ show_version(*pkg.split(/,/)) or
+ abort("#{parser.program_name}: no version found in package #{pkg}")
+ exit
+ end
+ end
+ v = parser.ver or abort("#{parser.program_name}: version unknown")
+ puts v
+ exit
+ end
+ end
+
+ # :startdoc:
+
+ #
+ # Class methods
+ #
+
+ #
+ # Initializes a new instance and evaluates the optional block in context
+ # of the instance. Arguments +args+ are passed to #new, see there for
+ # description of parameters.
+ #
+ # This method is *deprecated*, its behavior corresponds to the older #new
+ # method.
+ #
+ def self.with(*args, &block)
+ opts = new(*args)
+ opts.instance_eval(&block)
+ opts
+ end
+
+ #
+ # Returns an incremented value of +default+ according to +arg+.
+ #
+ def self.inc(arg, default = nil)
+ case arg
+ when Integer
+ arg.nonzero?
+ when nil
+ default.to_i + 1
+ end
+ end
+
+ #
+ # See self.inc
+ #
+ def inc(*args)
+ self.class.inc(*args)
+ end
+
+ #
+ # Initializes the instance and yields itself if called with a block.
+ #
+ # +banner+:: Banner message.
+ # +width+:: Summary width.
+ # +indent+:: Summary indent.
+ #
+ def initialize(banner = nil, width = 32, indent = ' ' * 4)
+ @stack = [DefaultList, List.new, List.new]
+ @program_name = nil
+ @banner = banner
+ @summary_width = width
+ @summary_indent = indent
+ @default_argv = ARGV
+ @require_exact = false
+ @raise_unknown = true
+ add_officious
+ yield self if block_given?
+ end
+
+ def add_officious # :nodoc:
+ list = base()
+ Officious.each do |opt, block|
+ list.long[opt] ||= block.call(self)
+ end
+ end
+
+ #
+ # Terminates option parsing. Optional parameter +arg+ is a string pushed
+ # back to be the first non-option argument.
+ #
+ def terminate(arg = nil)
+ self.class.terminate(arg)
+ end
+ #
+ # See #terminate.
+ #
+ def self.terminate(arg = nil)
+ throw :terminate, arg
+ end
+
+ @stack = [DefaultList]
+ #
+ # Returns the global top option list.
+ #
+ # Do not use directly.
+ #
+ def self.top() DefaultList end
+
+ #
+ # Directs to accept specified class +t+. The argument string is passed to
+ # the block in which it should be converted to the desired class.
+ #
+ # +t+:: Argument class specifier, any object including Class.
+ # +pat+:: Pattern for argument, defaults to +t+ if it responds to match.
+ #
+ # accept(t, pat, &block)
+ #
+ def accept(*args, &blk) top.accept(*args, &blk) end
+ #
+ # See #accept.
+ #
+ def self.accept(*args, &blk) top.accept(*args, &blk) end
+
+ #
+ # Directs to reject specified class argument.
+ #
+ # +type+:: Argument class specifier, any object including Class.
+ #
+ # reject(type)
+ #
+ def reject(*args, &blk) top.reject(*args, &blk) end
+ #
+ # See #reject.
+ #
+ def self.reject(*args, &blk) top.reject(*args, &blk) end
+
+ #
+ # Instance methods
+ #
+
+ # Heading banner preceding summary.
+ attr_writer :banner
+
+ # Program name to be emitted in error message and default banner,
+ # defaults to $0.
+ attr_writer :program_name
+
+ # Width for option list portion of summary. Must be Numeric.
+ attr_accessor :summary_width
+
+ # Indentation for summary. Must be String (or have + String method).
+ attr_accessor :summary_indent
+
+ # Strings to be parsed in default.
+ attr_accessor :default_argv
+
+ # Whether to require that options match exactly (disallows providing
+ # abbreviated long option as short option).
+ attr_accessor :require_exact
+
+ # Whether to raise at unknown option.
+ attr_accessor :raise_unknown
+
+ #
+ # Heading banner preceding summary.
+ #
+ def banner
+ unless @banner
+ @banner = +"Usage: #{program_name} [options]"
+ visit(:add_banner, @banner)
+ end
+ @banner
+ end
+
+ #
+ # Program name to be emitted in error message and default banner, defaults
+ # to $0.
+ #
+ def program_name
+ @program_name || strip_ext(File.basename($0))
+ end
+
+ private def strip_ext(name) # :nodoc:
+ exts = /#{
+ require "rbconfig"
+ Regexp.union(*RbConfig::CONFIG["EXECUTABLE_EXTS"]&.split(" "))
+ }\z/o
+ name.sub(exts, "")
+ end
+
+ # for experimental cascading :-)
+ alias set_banner banner=
+ alias set_program_name program_name=
+ alias set_summary_width summary_width=
+ alias set_summary_indent summary_indent=
+
+ # Version
+ attr_writer :version
+ # Release code
+ attr_writer :release
+
+ #
+ # Version
+ #
+ def version
+ (defined?(@version) && @version) || (defined?(::Version) && ::Version)
+ end
+
+ #
+ # Release code
+ #
+ def release
+ (defined?(@release) && @release) || (defined?(::Release) && ::Release) || (defined?(::RELEASE) && ::RELEASE)
+ end
+
+ #
+ # Returns version string from program_name, version and release.
+ #
+ def ver
+ if v = version
+ str = +"#{program_name} #{[v].join('.')}"
+ str << " (#{v})" if v = release
+ str
+ end
+ end
+
+ #
+ # Shows warning message with the program name
+ #
+ # +mesg+:: Message, defaulted to +$!+.
+ #
+ # See Kernel#warn.
+ #
+ def warn(mesg = $!)
+ super("#{program_name}: #{mesg}")
+ end
+
+ #
+ # Shows message with the program name then aborts.
+ #
+ # +mesg+:: Message, defaulted to +$!+.
+ #
+ # See Kernel#abort.
+ #
+ def abort(mesg = $!)
+ super("#{program_name}: #{mesg}")
+ end
+
+ #
+ # Subject of #on / #on_head, #accept / #reject
+ #
+ def top
+ @stack[-1]
+ end
+
+ #
+ # Subject of #on_tail.
+ #
+ def base
+ @stack[1]
+ end
+
+ #
+ # Pushes a new List.
+ #
+ # If a block is given, yields +self+ and returns the result of the
+ # block, otherwise returns +self+.
+ #
+ def new
+ @stack.push(List.new)
+ if block_given?
+ yield self
+ else
+ self
+ end
+ end
+
+ #
+ # Removes the last List.
+ #
+ def remove
+ @stack.pop
+ end
+
+ #
+ # Puts option summary into +to+ and returns +to+. Yields each line if
+ # a block is given.
+ #
+ # +to+:: Output destination, which must have method <<. Defaults to [].
+ # +width+:: Width of left side, defaults to @summary_width.
+ # +max+:: Maximum length allowed for left side, defaults to +width+ - 1.
+ # +indent+:: Indentation, defaults to @summary_indent.
+ #
+ def summarize(to = [], width = @summary_width, max = width - 1, indent = @summary_indent, &blk)
+ nl = "\n"
+ blk ||= proc {|l| to << (l.index(nl, -1) ? l : l + nl)}
+ visit(:summarize, {}, {}, width, max, indent, &blk)
+ to
+ end
+
+ #
+ # Returns option summary string.
+ #
+ def help; summarize("#{banner}".sub(/\n?\z/, "\n")) end
+ alias to_s help
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) do
+ first = true
+ if @stack.size > 2
+ @stack.each_with_index do |s, i|
+ next if i < 2
+ next if s.list.empty?
+ if first
+ first = false
+ q.text ":"
+ end
+ q.breakable
+ s.pretty_print(q)
+ end
+ end
+ end
+ end
+
+ def inspect # :nodoc:
+ require 'pp'
+ pretty_print_inspect
+ end
+
+ #
+ # Returns option summary list.
+ #
+ def to_a; summarize("#{banner}".split(/^/)) end
+
+ #
+ # Checks if an argument is given twice, in which case an ArgumentError is
+ # raised. Called from Gem::OptionParser#switch only.
+ #
+ # +obj+:: New argument.
+ # +prv+:: Previously specified argument.
+ # +msg+:: Exception message.
+ #
+ def notwice(obj, prv, msg) # :nodoc:
+ unless !prv or prv == obj
+ raise(ArgumentError, "argument #{msg} given twice: #{obj}",
+ ParseError.filter_backtrace(caller(2)))
+ end
+ obj
+ end
+ private :notwice
+
+ SPLAT_PROC = proc {|*a| a.length <= 1 ? a.first : a} # :nodoc:
+
+ # :call-seq:
+ # make_switch(params, block = nil)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ def make_switch(opts, block = nil)
+ short, long, nolong, style, pattern, conv, not_pattern, not_conv, not_style = [], [], []
+ ldesc, sdesc, desc, arg = [], [], []
+ default_style = Switch::NoArgument
+ default_pattern = nil
+ klass = nil
+ q, a = nil
+ has_arg = false
+ values = nil
+
+ opts.each do |o|
+ # argument class
+ next if search(:atype, o) do |pat, c|
+ klass = notwice(o, klass, 'type')
+ if not_style and not_style != Switch::NoArgument
+ not_pattern, not_conv = pat, c
+ else
+ default_pattern, conv = pat, c
+ end
+ end
+
+ # directly specified pattern(any object possible to match)
+ if !Completion.completable?(o) and o.respond_to?(:match)
+ pattern = notwice(o, pattern, 'pattern')
+ if pattern.respond_to?(:convert)
+ conv = pattern.method(:convert).to_proc
+ else
+ conv = SPLAT_PROC
+ end
+ next
+ end
+
+ # anything others
+ case o
+ when Proc, Method
+ block = notwice(o, block, 'block')
+ when Array, Hash, Set
+ if Array === o
+ o, v = o.partition {|v,| Completion.completable?(v)}
+ values = notwice(v, values, 'values') unless v.empty?
+ next if o.empty?
+ end
+ case pattern
+ when CompletingHash
+ when nil
+ pattern = CompletingHash.new
+ conv = pattern.method(:convert).to_proc if pattern.respond_to?(:convert)
+ else
+ raise ArgumentError, "argument pattern given twice"
+ end
+ o.each {|pat, *v| pattern[pat] = v.fetch(0) {pat}}
+ when Range
+ values = notwice(o, values, 'values')
+ when Module
+ raise ArgumentError, "unsupported argument type: #{o}", ParseError.filter_backtrace(caller(4))
+ when *ArgumentStyle.keys
+ style = notwice(ArgumentStyle[o], style, 'style')
+ when /\A--no-([^\[\]=\s]*)(.+)?/
+ q, a = $1, $2
+ o = notwice(a ? Object : TrueClass, klass, 'type')
+ not_pattern, not_conv = search(:atype, o) unless not_style
+ not_style = (not_style || default_style).guess(arg = a) if a
+ default_style = Switch::NoArgument
+ default_pattern, conv = search(:atype, FalseClass) unless default_pattern
+ ldesc << "--no-#{q}"
+ (q = q.downcase).tr!('_', '-')
+ long << "no-#{q}"
+ nolong << q
+ when /\A--\[no-\]([^\[\]=\s]*)(.+)?/
+ q, a = $1, $2
+ o = notwice(a ? Object : TrueClass, klass, 'type')
+ if a
+ default_style = default_style.guess(arg = a)
+ default_pattern, conv = search(:atype, o) unless default_pattern
+ end
+ ldesc << "--[no-]#{q}"
+ (o = q.downcase).tr!('_', '-')
+ long << o
+ not_pattern, not_conv = search(:atype, FalseClass) unless not_style
+ not_style = Switch::NoArgument
+ nolong << "no-#{o}"
+ when /\A--([^\[\]=\s]*)(.+)?/
+ q, a = $1, $2
+ if a
+ o = notwice(NilClass, klass, 'type')
+ default_style = default_style.guess(arg = a)
+ default_pattern, conv = search(:atype, o) unless default_pattern
+ end
+ ldesc << "--#{q}"
+ (o = q.downcase).tr!('_', '-')
+ long << o
+ when /\A-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/
+ q, a = $1, $2
+ o = notwice(Object, klass, 'type')
+ if a
+ default_style = default_style.guess(arg = a)
+ default_pattern, conv = search(:atype, o) unless default_pattern
+ else
+ has_arg = true
+ end
+ sdesc << "-#{q}"
+ short << Regexp.new(q)
+ when /\A-(.)(.+)?/
+ q, a = $1, $2
+ if a
+ o = notwice(NilClass, klass, 'type')
+ default_style = default_style.guess(arg = a)
+ default_pattern, conv = search(:atype, o) unless default_pattern
+ end
+ sdesc << "-#{q}"
+ short << q
+ when /\A=/
+ style = notwice(default_style.guess(arg = o), style, 'style')
+ default_pattern, conv = search(:atype, Object) unless default_pattern
+ else
+ desc.push(o) if o && !o.empty?
+ end
+ end
+
+ default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern
+ if Range === values and klass
+ unless (!values.begin or klass === values.begin) and
+ (!values.end or klass === values.end)
+ raise ArgumentError, "range does not match class"
+ end
+ end
+ if !(short.empty? and long.empty?)
+ if has_arg and default_style == Switch::NoArgument
+ default_style = Switch::RequiredArgument
+ end
+ s = (style || default_style).new(pattern || default_pattern,
+ conv, sdesc, ldesc, arg, desc, block, values)
+ elsif !block
+ if style or pattern
+ raise ArgumentError, "no switch given", ParseError.filter_backtrace(caller)
+ end
+ s = desc
+ else
+ short << pattern
+ s = (style || default_style).new(pattern,
+ conv, nil, nil, arg, desc, block, values)
+ end
+ return s, short, long,
+ (not_style.new(not_pattern, not_conv, sdesc, ldesc, nil, desc, block) if not_style),
+ nolong
+ end
+
+ # ----
+ # Option definition phase methods
+ #
+ # These methods are used to define options, or to construct an
+ # Gem::OptionParser instance in other words.
+
+ # :call-seq:
+ # define(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ def define(*opts, &block)
+ top.append(*(sw = make_switch(opts, block)))
+ sw[0]
+ end
+
+ # :call-seq:
+ # on(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ def on(*opts, &block)
+ define(*opts, &block)
+ self
+ end
+ alias def_option define
+
+ # :call-seq:
+ # define_head(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ def define_head(*opts, &block)
+ top.prepend(*(sw = make_switch(opts, block)))
+ sw[0]
+ end
+
+ # :call-seq:
+ # on_head(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ # The new option is added at the head of the summary.
+ #
+ def on_head(*opts, &block)
+ define_head(*opts, &block)
+ self
+ end
+ alias def_head_option define_head
+
+ # :call-seq:
+ # define_tail(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ def define_tail(*opts, &block)
+ base.append(*(sw = make_switch(opts, block)))
+ sw[0]
+ end
+
+ #
+ # :call-seq:
+ # on_tail(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ # The new option is added at the tail of the summary.
+ #
+ def on_tail(*opts, &block)
+ define_tail(*opts, &block)
+ self
+ end
+ alias def_tail_option define_tail
+
+ #
+ # Add separator in summary.
+ #
+ def separator(string)
+ top.append(string, nil, nil)
+ end
+
+ # ----
+ # Arguments parse phase methods
+ #
+ # These methods parse +argv+, convert, and store the results by
+ # calling handlers. As these methods do not modify +self+, +self+
+ # can be frozen.
+
+ #
+ # Parses command line arguments +argv+ in order. When a block is given,
+ # each non-option argument is yielded. When optional +into+ keyword
+ # argument is provided, the parsed option values are stored there via
+ # <code>[]=</code> method (so it can be Hash, or OpenStruct, or other
+ # similar object).
+ #
+ # Returns the rest of +argv+ left unparsed.
+ #
+ def order(*argv, **keywords, &nonopt)
+ argv = argv[0].dup if argv.size == 1 and Array === argv[0]
+ order!(argv, **keywords, &nonopt)
+ end
+
+ #
+ # Same as #order, but removes switches destructively.
+ # Non-option arguments remain in +argv+.
+ #
+ def order!(argv = default_argv, into: nil, **keywords, &nonopt)
+ setter = ->(name, val) {into[name.to_sym] = val} if into
+ parse_in_order(argv, setter, **keywords, &nonopt)
+ end
+
+ def parse_in_order(argv = default_argv, setter = nil, exact: require_exact, **, &nonopt) # :nodoc:
+ opt, arg, val, rest = nil
+ nonopt ||= proc {|a| throw :terminate, a}
+ argv.unshift(arg) if arg = catch(:terminate) {
+ while arg = argv.shift
+ case arg
+ # long option
+ when /\A--([^=]*)(?:=(.*))?/m
+ opt, rest = $1, $2
+ opt.tr!('_', '-')
+ begin
+ if exact
+ sw, = search(:long, opt)
+ else
+ sw, = complete(:long, opt, true)
+ end
+ rescue ParseError
+ throw :terminate, arg unless raise_unknown
+ raise $!.set_option(arg, true)
+ else
+ unless sw
+ throw :terminate, arg unless raise_unknown
+ raise InvalidOption, arg
+ end
+ end
+ begin
+ opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)}
+ val = callback!(cb, 1, val) if cb
+ callback!(setter, 2, sw.switch_name, val) if setter
+ rescue ParseError
+ raise $!.set_option(arg, rest)
+ end
+
+ # short option
+ when /\A-(.)((=).*|.+)?/m
+ eq, rest, opt = $3, $2, $1
+ has_arg, val = eq, rest
+ begin
+ sw, = search(:short, opt)
+ unless sw
+ begin
+ sw, = complete(:short, opt)
+ # short option matched.
+ val = arg.delete_prefix('-')
+ has_arg = true
+ rescue InvalidOption
+ raise if exact
+ # if no short options match, try completion with long
+ # options.
+ sw, = complete(:long, opt)
+ eq ||= !rest
+ end
+ end
+ rescue ParseError
+ throw :terminate, arg unless raise_unknown
+ raise $!.set_option(arg, true)
+ end
+ begin
+ opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq}
+ rescue ParseError
+ raise $!.set_option(arg, arg.length > 2)
+ else
+ raise InvalidOption, arg if has_arg and !eq and arg == "-#{opt}"
+ end
+ begin
+ argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-')
+ val = callback!(cb, 1, val) if cb
+ callback!(setter, 2, sw.switch_name, val) if setter
+ rescue ParseError
+ raise $!.set_option(arg, arg.length > 2)
+ end
+
+ # non-option argument
+ else
+ catch(:prune) do
+ visit(:each_option) do |sw0|
+ sw = sw0
+ sw.block.call(arg) if Switch === sw and sw.match_nonswitch?(arg)
+ end
+ nonopt.call(arg)
+ end
+ end
+ end
+
+ nil
+ }
+
+ visit(:search, :short, nil) {|sw| sw.block.call(*argv) if !sw.pattern}
+
+ argv
+ end
+ private :parse_in_order
+
+ # Calls callback with _val_.
+ def callback!(cb, max_arity, *args) # :nodoc:
+ args.compact!
+
+ if (size = args.size) < max_arity and cb.to_proc.lambda?
+ (arity = cb.arity) < 0 and arity = (1-arity)
+ arity = max_arity if arity > max_arity
+ args[arity - 1] = nil if arity > size
+ end
+ cb.call(*args)
+ end
+ private :callback!
+
+ #
+ # Parses command line arguments +argv+ in permutation mode and returns
+ # list of non-option arguments. When optional +into+ keyword
+ # argument is provided, the parsed option values are stored there via
+ # <code>[]=</code> method (so it can be Hash, or OpenStruct, or other
+ # similar object).
+ #
+ def permute(*argv, **keywords)
+ argv = argv[0].dup if argv.size == 1 and Array === argv[0]
+ permute!(argv, **keywords)
+ end
+
+ #
+ # Same as #permute, but removes switches destructively.
+ # Non-option arguments remain in +argv+.
+ #
+ def permute!(argv = default_argv, **keywords)
+ nonopts = []
+ order!(argv, **keywords) {|nonopt| nonopts << nonopt}
+ argv[0, 0] = nonopts
+ argv
+ end
+
+ #
+ # Parses command line arguments +argv+ in order when environment variable
+ # POSIXLY_CORRECT is set, and in permutation mode otherwise.
+ # When optional +into+ keyword argument is provided, the parsed option
+ # values are stored there via <code>[]=</code> method (so it can be Hash,
+ # or OpenStruct, or other similar object).
+ #
+ def parse(*argv, **keywords)
+ argv = argv[0].dup if argv.size == 1 and Array === argv[0]
+ parse!(argv, **keywords)
+ end
+
+ #
+ # Same as #parse, but removes switches destructively.
+ # Non-option arguments remain in +argv+.
+ #
+ def parse!(argv = default_argv, **keywords)
+ if ENV.include?('POSIXLY_CORRECT')
+ order!(argv, **keywords)
+ else
+ permute!(argv, **keywords)
+ end
+ end
+
+ #
+ # Wrapper method for getopts.rb.
+ #
+ # params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option")
+ # # params["a"] = true # -a
+ # # params["b"] = "1" # -b1
+ # # params["foo"] = "1" # --foo
+ # # params["bar"] = "x" # --bar x
+ # # params["zot"] = "z" # --zot Z
+ #
+ # Option +symbolize_names+ (boolean) specifies whether returned Hash keys should be Symbols; defaults to +false+ (use Strings).
+ #
+ # params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option", symbolize_names: true)
+ # # params[:a] = true # -a
+ # # params[:b] = "1" # -b1
+ # # params[:foo] = "1" # --foo
+ # # params[:bar] = "x" # --bar x
+ # # params[:zot] = "z" # --zot Z
+ #
+ def getopts(*args, symbolize_names: false, **keywords)
+ argv = Array === args.first ? args.shift : default_argv
+ single_options, *long_options = *args
+
+ result = {}
+ setter = (symbolize_names ?
+ ->(name, val) {result[name.to_sym] = val}
+ : ->(name, val) {result[name] = val})
+
+ single_options.scan(/(.)(:)?/) do |opt, val|
+ if val
+ setter[opt, nil]
+ define("-#{opt} VAL")
+ else
+ setter[opt, false]
+ define("-#{opt}")
+ end
+ end if single_options
+
+ long_options.each do |arg|
+ arg, desc = arg.split(';', 2)
+ opt, val = arg.split(':', 2)
+ if val
+ setter[opt, (val unless val.empty?)]
+ define("--#{opt}=#{result[opt] || "VAL"}", *[desc].compact)
+ else
+ setter[opt, false]
+ define("--#{opt}", *[desc].compact)
+ end
+ end
+
+ parse_in_order(argv, setter, **keywords)
+ result
+ end
+
+ #
+ # See #getopts.
+ #
+ def self.getopts(*args, symbolize_names: false)
+ new.getopts(*args, symbolize_names: symbolize_names)
+ end
+
+ #
+ # Traverses @stack, sending each element method +id+ with +args+ and
+ # +block+.
+ #
+ def visit(id, *args, &block) # :nodoc:
+ @stack.reverse_each do |el|
+ el.__send__(id, *args, &block)
+ end
+ nil
+ end
+ private :visit
+
+ #
+ # Searches +key+ in @stack for +id+ hash and returns or yields the result.
+ #
+ def search(id, key) # :nodoc:
+ block_given = block_given?
+ visit(:search, id, key) do |k|
+ return block_given ? yield(k) : k
+ end
+ end
+ private :search
+
+ #
+ # Completes shortened long style option switch and returns pair of
+ # canonical switch and switch descriptor Gem::OptionParser::Switch.
+ #
+ # +typ+:: Searching table.
+ # +opt+:: Searching key.
+ # +icase+:: Search case insensitive if true.
+ # +pat+:: Optional pattern for completion.
+ #
+ def complete(typ, opt, icase = false, *pat) # :nodoc:
+ if pat.empty?
+ search(typ, opt) {|sw| return [sw, opt]} # exact match or...
+ end
+ ambiguous = catch(:ambiguous) {
+ visit(:complete, typ, opt, icase, *pat) {|o, *sw| return sw}
+ }
+ exc = ambiguous ? AmbiguousOption : InvalidOption
+ raise exc.new(opt, additional: proc {|o| additional_message(typ, o)})
+ end
+ private :complete
+
+ #
+ # Returns additional info.
+ #
+ def additional_message(typ, opt)
+ return unless typ and opt and defined?(DidYouMean::SpellChecker)
+ all_candidates = []
+ visit(:get_candidates, typ) do |candidates|
+ all_candidates.concat(candidates)
+ end
+ all_candidates.select! {|cand| cand.is_a?(String) }
+ checker = DidYouMean::SpellChecker.new(dictionary: all_candidates)
+ DidYouMean.formatter.message_for(all_candidates & checker.correct(opt))
+ end
+
+ #
+ # Return candidates for +word+.
+ #
+ def candidate(word)
+ list = []
+ case word
+ when '-'
+ long = short = true
+ when /\A--/
+ word, arg = word.split(/=/, 2)
+ argpat = Completion.regexp(arg, false) if arg and !arg.empty?
+ long = true
+ when /\A-/
+ short = true
+ end
+ pat = Completion.regexp(word, long)
+ visit(:each_option) do |opt|
+ next unless Switch === opt
+ opts = (long ? opt.long : []) + (short ? opt.short : [])
+ opts = Completion.candidate(word, true, pat, &opts.method(:each)).map(&:first) if pat
+ if /\A=/ =~ opt.arg
+ opts.map! {|sw| sw + "="}
+ if arg and CompletingHash === opt.pattern
+ if opts = opt.pattern.candidate(arg, false, argpat)
+ opts.map!(&:last)
+ end
+ end
+ end
+ list.concat(opts)
+ end
+ list
+ end
+
+ #
+ # Loads options from file names as +filename+. Does nothing when the file
+ # is not present. Returns whether successfully loaded.
+ #
+ # +filename+ defaults to basename of the program without suffix in a
+ # directory ~/.options, then the basename with '.options' suffix
+ # under XDG and Haiku standard places.
+ #
+ # The optional +into+ keyword argument works exactly like that accepted in
+ # method #parse.
+ #
+ def load(filename = nil, **keywords)
+ unless filename
+ basename = File.basename($0, '.*')
+ return true if load(File.expand_path("~/.options/#{basename}"), **keywords) rescue nil
+ basename << ".options"
+ if !(xdg = ENV['XDG_CONFIG_HOME']) or xdg.empty?
+ # https://specifications.freedesktop.org/basedir-spec/latest/#variables
+ #
+ # If $XDG_CONFIG_HOME is either not set or empty, a default
+ # equal to $HOME/.config should be used.
+ xdg = ['~/.config', true]
+ end
+ return [
+ xdg,
+
+ *ENV['XDG_CONFIG_DIRS']&.split(File::PATH_SEPARATOR),
+
+ # Haiku
+ ['~/config/settings', true],
+ ].any? {|dir, expand|
+ next if !dir or dir.empty?
+ filename = File.join(dir, basename)
+ filename = File.expand_path(filename) if expand
+ load(filename, **keywords) rescue nil
+ }
+ end
+ begin
+ parse(*File.readlines(filename, chomp: true), **keywords)
+ true
+ rescue Errno::ENOENT, Errno::ENOTDIR
+ false
+ end
+ end
+
+ #
+ # Parses environment variable +env+ or its uppercase with splitting like a
+ # shell.
+ #
+ # +env+ defaults to the basename of the program.
+ #
+ def environment(env = File.basename($0, '.*'), **keywords)
+ env = ENV[env] || ENV[env.upcase] or return
+ require 'shellwords'
+ parse(*Shellwords.shellwords(env), **keywords)
+ end
+
+ #
+ # Acceptable argument classes
+ #
+
+ #
+ # Any string and no conversion. This is fall-back.
+ #
+ accept(Object) {|s,|s or s.nil?}
+
+ accept(NilClass) {|s,|s}
+
+ #
+ # Any non-empty string, and no conversion.
+ #
+ accept(String, /.+/m) {|s,*|s}
+
+ #
+ # Ruby/C-like integer, octal for 0-7 sequence, binary for 0b, hexadecimal
+ # for 0x, and decimal for others; with optional sign prefix. Converts to
+ # Integer.
+ #
+ decimal = '\d+(?:_\d+)*'
+ binary = 'b[01]+(?:_[01]+)*'
+ hex = 'x[\da-f]+(?:_[\da-f]+)*'
+ octal = "0(?:[0-7]+(?:_[0-7]+)*|#{binary}|#{hex})?"
+ integer = "#{octal}|#{decimal}"
+
+ accept(Integer, %r"\A[-+]?(?:#{integer})\z"io) {|s,|
+ begin
+ Integer(s)
+ rescue ArgumentError
+ raise Gem::OptionParser::InvalidArgument, s
+ end if s
+ }
+
+ #
+ # Float number format, and converts to Float.
+ #
+ float = "(?:#{decimal}(?=(.)?)(?:\\.(?:#{decimal})?)?|\\.#{decimal})(?:E[-+]?#{decimal})?"
+ floatpat = %r"\A[-+]?#{float}\z"io
+ accept(Float, floatpat) {|s,| s.to_f if s}
+
+ #
+ # Generic numeric format, converts to Integer for integer format, Float
+ # for float format, and Rational for rational format.
+ #
+ real = "[-+]?(?:#{octal}|#{float})"
+ accept(Numeric, /\A(#{real})(?:\/(#{real}))?\z/io) {|s, d, f, n,|
+ if n
+ Rational(d, n)
+ elsif f
+ Float(s)
+ else
+ Integer(s)
+ end
+ }
+
+ #
+ # Decimal integer format, to be converted to Integer.
+ #
+ DecimalInteger = /\A[-+]?#{decimal}\z/io
+ accept(DecimalInteger, DecimalInteger) {|s,|
+ begin
+ Integer(s, 10)
+ rescue ArgumentError
+ raise Gem::OptionParser::InvalidArgument, s
+ end if s
+ }
+
+ #
+ # Ruby/C like octal/hexadecimal/binary integer format, to be converted to
+ # Integer.
+ #
+ OctalInteger = /\A[-+]?(?:[0-7]+(?:_[0-7]+)*|0(?:#{binary}|#{hex}))\z/io
+ accept(OctalInteger, OctalInteger) {|s,|
+ begin
+ Integer(s, 8)
+ rescue ArgumentError
+ raise Gem::OptionParser::InvalidArgument, s
+ end if s
+ }
+
+ #
+ # Decimal integer/float number format, to be converted to Integer for
+ # integer format, Float for float format.
+ #
+ DecimalNumeric = floatpat # decimal integer is allowed as float also.
+ accept(DecimalNumeric, floatpat) {|s, f|
+ begin
+ if f
+ Float(s)
+ else
+ Integer(s)
+ end
+ rescue ArgumentError
+ raise Gem::OptionParser::InvalidArgument, s
+ end if s
+ }
+
+ #
+ # Boolean switch, which means whether it is present or not, whether it is
+ # absent or not with prefix no-, or it takes an argument
+ # yes/no/true/false/+/-.
+ #
+ yesno = CompletingHash.new
+ %w[- no false].each {|el| yesno[el] = false}
+ %w[+ yes true].each {|el| yesno[el] = true}
+ yesno['nil'] = false # should be nil?
+ accept(TrueClass, yesno) {|arg, val| val == nil or val}
+ #
+ # Similar to TrueClass, but defaults to false.
+ #
+ accept(FalseClass, yesno) {|arg, val| val != nil and val}
+
+ #
+ # List of strings separated by ",".
+ #
+ accept(Array) do |s, |
+ if s
+ s = s.split(',').collect {|ss| ss unless ss.empty?}
+ end
+ s
+ end
+
+ #
+ # Regular expression with options.
+ #
+ accept(Regexp, %r"\A/((?:\\.|[^\\])*)/([[:alpha:]]+)?\z|.*") do |all, s, o|
+ f = 0
+ if o
+ f |= Regexp::IGNORECASE if /i/ =~ o
+ f |= Regexp::MULTILINE if /m/ =~ o
+ f |= Regexp::EXTENDED if /x/ =~ o
+ case o = o.delete("imx")
+ when ""
+ when "u"
+ s = s.encode(Encoding::UTF_8)
+ when "e"
+ s = s.encode(Encoding::EUC_JP)
+ when "s"
+ s = s.encode(Encoding::SJIS)
+ when "n"
+ f |= Regexp::NOENCODING
+ else
+ raise Gem::OptionParser::InvalidArgument, "unknown regexp option - #{o}"
+ end
+ else
+ s ||= all
+ end
+ Regexp.new(s, f)
+ end
+
+ #
+ # Exceptions
+ #
+
+ #
+ # Base class of exceptions from Gem::OptionParser.
+ #
+ class ParseError < RuntimeError
+ # Reason which caused the error.
+ Reason = 'parse error'
+
+ # :nodoc:
+ def initialize(*args, additional: nil)
+ @additional = additional
+ @arg0, = args
+ @args = args
+ @reason = nil
+ end
+
+ attr_reader :args
+ attr_writer :reason
+ attr_accessor :additional
+
+ #
+ # Pushes back erred argument(s) to +argv+.
+ #
+ def recover(argv)
+ argv[0, 0] = @args
+ argv
+ end
+
+ DIR = File.join(__dir__, '')
+ def self.filter_backtrace(array)
+ unless $DEBUG
+ array.delete_if {|bt| bt.start_with?(DIR)}
+ end
+ array
+ end
+
+ def set_backtrace(array)
+ super(self.class.filter_backtrace(array))
+ end
+
+ def set_option(opt, eq)
+ if eq
+ @args[0] = opt
+ else
+ @args.unshift(opt)
+ end
+ self
+ end
+
+ #
+ # Returns error reason. Override this for I18N.
+ #
+ def reason
+ @reason || self.class::Reason
+ end
+
+ def inspect
+ "#<#{self.class}: #{args.join(' ')}>"
+ end
+
+ #
+ # Default stringizing method to emit standard error message.
+ #
+ def message
+ "#{reason}: #{args.join(' ')}#{additional[@arg0] if additional}"
+ end
+
+ alias to_s message
+ end
+
+ #
+ # Raises when ambiguously completable string is encountered.
+ #
+ class AmbiguousOption < ParseError
+ const_set(:Reason, 'ambiguous option')
+ end
+
+ #
+ # Raises when there is an argument for a switch which takes no argument.
+ #
+ class NeedlessArgument < ParseError
+ const_set(:Reason, 'needless argument')
+ end
+
+ #
+ # Raises when a switch with mandatory argument has no argument.
+ #
+ class MissingArgument < ParseError
+ const_set(:Reason, 'missing argument')
+ end
+
+ #
+ # Raises when switch is undefined.
+ #
+ class InvalidOption < ParseError
+ const_set(:Reason, 'invalid option')
+ end
+
+ #
+ # Raises when the given argument does not match required format.
+ #
+ class InvalidArgument < ParseError
+ const_set(:Reason, 'invalid argument')
+ end
+
+ #
+ # Raises when the given argument word can't be completed uniquely.
+ #
+ class AmbiguousArgument < InvalidArgument
+ const_set(:Reason, 'ambiguous argument')
+ end
+
+ #
+ # Miscellaneous
+ #
+
+ #
+ # Extends command line arguments array (ARGV) to parse itself.
+ #
+ module Arguable
+
+ #
+ # Sets Gem::OptionParser object, when +opt+ is +false+ or +nil+, methods
+ # Gem::OptionParser::Arguable#options and Gem::OptionParser::Arguable#options= are
+ # undefined. Thus, there is no ways to access the Gem::OptionParser object
+ # via the receiver object.
+ #
+ def options=(opt)
+ unless @optparse = opt
+ class << self
+ undef_method(:options)
+ undef_method(:options=)
+ end
+ end
+ end
+
+ #
+ # Actual Gem::OptionParser object, automatically created if nonexistent.
+ #
+ # If called with a block, yields the Gem::OptionParser object and returns the
+ # result of the block. If an Gem::OptionParser::ParseError exception occurs
+ # in the block, it is rescued, a error message printed to STDERR and
+ # +nil+ returned.
+ #
+ def options
+ @optparse ||= Gem::OptionParser.new
+ @optparse.default_argv = self
+ block_given? or return @optparse
+ begin
+ yield @optparse
+ rescue ParseError
+ @optparse.warn $!
+ nil
+ end
+ end
+
+ #
+ # Parses +self+ destructively in order and returns +self+ containing the
+ # rest arguments left unparsed.
+ #
+ def order!(**keywords, &blk) options.order!(self, **keywords, &blk) end
+
+ #
+ # Parses +self+ destructively in permutation mode and returns +self+
+ # containing the rest arguments left unparsed.
+ #
+ def permute!(**keywords) options.permute!(self, **keywords) end
+
+ #
+ # Parses +self+ destructively and returns +self+ containing the
+ # rest arguments left unparsed.
+ #
+ def parse!(**keywords) options.parse!(self, **keywords) end
+
+ #
+ # Substitution of getopts is possible as follows. Also see
+ # Gem::OptionParser#getopts.
+ #
+ # def getopts(*args)
+ # ($OPT = ARGV.getopts(*args)).each do |opt, val|
+ # eval "$OPT_#{opt.gsub(/[^A-Za-z0-9_]/, '_')} = val"
+ # end
+ # rescue Gem::OptionParser::ParseError
+ # end
+ #
+ def getopts(*args, symbolize_names: false, **keywords)
+ options.getopts(self, *args, symbolize_names: symbolize_names, **keywords)
+ end
+
+ #
+ # Initializes instance variable.
+ #
+ def self.extend_object(obj)
+ super
+ obj.instance_eval {@optparse = nil}
+ end
+
+ def initialize(*args) # :nodoc:
+ super
+ @optparse = nil
+ end
+ end
+
+ #
+ # Acceptable argument classes. Now contains DecimalInteger, OctalInteger
+ # and DecimalNumeric. See Acceptable argument classes (in source code).
+ #
+ module Acceptables
+ const_set(:DecimalInteger, Gem::OptionParser::DecimalInteger)
+ const_set(:OctalInteger, Gem::OptionParser::OctalInteger)
+ const_set(:DecimalNumeric, Gem::OptionParser::DecimalNumeric)
+ end
+end
+
+# ARGV is arguable by Gem::OptionParser
+ARGV.extend(Gem::OptionParser::Arguable)
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/ac.rb b/lib/rubygems/vendor/optparse/lib/optparse/ac.rb
new file mode 100644
index 0000000000..28a5b1b33e
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/ac.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: false
+require_relative '../optparse'
+
+#
+# autoconf-like options.
+#
+class Gem::OptionParser::AC < Gem::OptionParser
+ # :stopdoc:
+ private
+
+ def _check_ac_args(name, block)
+ unless /\A\w[-\w]*\z/ =~ name
+ raise ArgumentError, name
+ end
+ unless block
+ raise ArgumentError, "no block given", ParseError.filter_backtrace(caller)
+ end
+ end
+
+ ARG_CONV = proc {|val| val.nil? ? true : val}
+ private_constant :ARG_CONV
+
+ def _ac_arg_enable(prefix, name, help_string, block)
+ _check_ac_args(name, block)
+
+ sdesc = []
+ ldesc = ["--#{prefix}-#{name}"]
+ desc = [help_string]
+ q = name.downcase
+ ac_block = proc {|val| block.call(ARG_CONV.call(val))}
+ enable = Switch::PlacedArgument.new(nil, ARG_CONV, sdesc, ldesc, nil, desc, ac_block)
+ disable = Switch::NoArgument.new(nil, proc {false}, sdesc, ldesc, nil, desc, ac_block)
+ top.append(enable, [], ["enable-" + q], disable, ['disable-' + q])
+ enable
+ end
+
+ # :startdoc:
+
+ public
+
+ # Define <tt>--enable</tt> / <tt>--disable</tt> style option
+ #
+ # Appears as <tt>--enable-<i>name</i></tt> in help message.
+ def ac_arg_enable(name, help_string, &block)
+ _ac_arg_enable("enable", name, help_string, block)
+ end
+
+ # Define <tt>--enable</tt> / <tt>--disable</tt> style option
+ #
+ # Appears as <tt>--disable-<i>name</i></tt> in help message.
+ def ac_arg_disable(name, help_string, &block)
+ _ac_arg_enable("disable", name, help_string, block)
+ end
+
+ # Define <tt>--with</tt> / <tt>--without</tt> style option
+ #
+ # Appears as <tt>--with-<i>name</i></tt> in help message.
+ def ac_arg_with(name, help_string, &block)
+ _check_ac_args(name, block)
+
+ sdesc = []
+ ldesc = ["--with-#{name}"]
+ desc = [help_string]
+ q = name.downcase
+ with = Switch::PlacedArgument.new(*search(:atype, String), sdesc, ldesc, nil, desc, block)
+ without = Switch::NoArgument.new(nil, proc {}, sdesc, ldesc, nil, desc, block)
+ top.append(with, [], ["with-" + q], without, ['without-' + q])
+ with
+ end
+end
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/date.rb b/lib/rubygems/vendor/optparse/lib/optparse/date.rb
new file mode 100644
index 0000000000..d9a9f4f48a
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/date.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: false
+require_relative '../optparse'
+require 'date'
+
+Gem::OptionParser.accept(DateTime) do |s,|
+ begin
+ DateTime.parse(s) if s
+ rescue ArgumentError
+ raise Gem::OptionParser::InvalidArgument, s
+ end
+end
+Gem::OptionParser.accept(Date) do |s,|
+ begin
+ Date.parse(s) if s
+ rescue ArgumentError
+ raise Gem::OptionParser::InvalidArgument, s
+ end
+end
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb b/lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb
new file mode 100644
index 0000000000..70762f033b
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+require_relative '../optparse'
+
+class Gem::OptionParser
+ # :call-seq:
+ # define_by_keywords(options, method, **params)
+ #
+ # :include: ../../doc/optparse/creates_option.rdoc
+ #
+ # Defines options which set in to _options_ for keyword parameters
+ # of _method_.
+ #
+ # Parameters for each keywords are given as elements of _params_.
+ #
+ def define_by_keywords(options, method, **params)
+ method.parameters.each do |type, name|
+ case type
+ when :key, :keyreq
+ op, cl = *(type == :key ? %w"[ ]" : ["", ""])
+ define("--#{name}=#{op}#{name.upcase}#{cl}", *params[name]) do |o|
+ options[name] = o
+ end
+ end
+ end
+ options
+ end
+end
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb b/lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb
new file mode 100644
index 0000000000..d47ad60255
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: false
+# -*- ruby -*-
+
+require 'shellwords'
+require_relative '../optparse'
+
+Gem::OptionParser.accept(Shellwords) {|s,| Shellwords.shellwords(s)}
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/time.rb b/lib/rubygems/vendor/optparse/lib/optparse/time.rb
new file mode 100644
index 0000000000..c59e1e4ced
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/time.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: false
+require_relative '../optparse'
+require 'time'
+
+Gem::OptionParser.accept(Time) do |s,|
+ begin
+ (Time.httpdate(s) rescue Time.parse(s)) if s
+ rescue
+ raise Gem::OptionParser::InvalidArgument, s
+ end
+end
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/uri.rb b/lib/rubygems/vendor/optparse/lib/optparse/uri.rb
new file mode 100644
index 0000000000..398127479a
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/uri.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: false
+# -*- ruby -*-
+
+require_relative '../optparse'
+require_relative '../../../uri/lib/uri'
+
+Gem::OptionParser.accept(Gem::URI) {|s,| Gem::URI.parse(s) if s}
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/version.rb b/lib/rubygems/vendor/optparse/lib/optparse/version.rb
new file mode 100644
index 0000000000..e39889ae87
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/version.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: false
+# Gem::OptionParser internal utility
+
+class << Gem::OptionParser
+ #
+ # Shows version string in packages if Version is defined.
+ #
+ # +pkgs+:: package list
+ #
+ def show_version(*pkgs)
+ progname = ARGV.options.program_name
+ result = false
+ show = proc do |klass, cname, version|
+ str = "#{progname}"
+ unless klass == ::Object and cname == :VERSION
+ version = version.join(".") if Array === version
+ str << ": #{klass}" unless klass == Object
+ str << " version #{version}"
+ end
+ [:Release, :RELEASE].find do |rel|
+ if klass.const_defined?(rel)
+ str << " (#{klass.const_get(rel)})"
+ end
+ end
+ puts str
+ result = true
+ end
+ if pkgs.size == 1 and pkgs[0] == "all"
+ self.search_const(::Object, /\AV(?:ERSION|ersion)\z/) do |klass, cname, version|
+ unless cname[1] == ?e and klass.const_defined?(:Version)
+ show.call(klass, cname.intern, version)
+ end
+ end
+ else
+ pkgs.each do |pkg|
+ begin
+ pkg = pkg.split(/::|\//).inject(::Object) {|m, c| m.const_get(c)}
+ v = case
+ when pkg.const_defined?(:Version)
+ pkg.const_get(n = :Version)
+ when pkg.const_defined?(:VERSION)
+ pkg.const_get(n = :VERSION)
+ else
+ n = nil
+ "unknown"
+ end
+ show.call(pkg, n, v)
+ rescue NameError
+ end
+ end
+ end
+ result
+ end
+
+ # :stopdoc:
+
+ def each_const(path, base = ::Object)
+ path.split(/::|\//).inject(base) do |klass, name|
+ raise NameError, path unless Module === klass
+ klass.constants.grep(/#{name}/i) do |c|
+ klass.const_defined?(c) or next
+ klass.const_get(c)
+ end
+ end
+ end
+
+ def search_const(klass, name)
+ klasses = [klass]
+ while klass = klasses.shift
+ klass.constants.each do |cname|
+ klass.const_defined?(cname) or next
+ const = klass.const_get(cname)
+ yield klass, cname, const if name === cname
+ klasses << const if Module === const and const != ::Object
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb
new file mode 100644
index 0000000000..818e947477
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb
@@ -0,0 +1,53 @@
+require_relative "pub_grub/package"
+require_relative "pub_grub/static_package_source"
+require_relative "pub_grub/term"
+require_relative "pub_grub/version_range"
+require_relative "pub_grub/version_constraint"
+require_relative "pub_grub/version_union"
+require_relative "pub_grub/version_solver"
+require_relative "pub_grub/incompatibility"
+require_relative 'pub_grub/solve_failure'
+require_relative 'pub_grub/failure_writer'
+require_relative 'pub_grub/version'
+
+module Gem::PubGrub
+ # Minimal logger that doesn't require the 'logger' gem
+ class NullLogger
+ def info(&block); end
+ def debug(&block); end
+ def warn(&block); end
+ def error(&block); end
+ end
+
+ class StderrLogger
+ def info(&block)
+ $stderr.puts "INFO: #{block.call}" if block
+ end
+
+ def debug(&block)
+ $stderr.puts "DEBUG: #{block.call}" if block
+ end
+
+ def warn(&block)
+ $stderr.puts "WARN: #{block.call}" if block
+ end
+
+ def error(&block)
+ $stderr.puts "ERROR: #{block.call}" if block
+ end
+ end
+
+ class << self
+ attr_writer :logger
+
+ def logger
+ @logger || default_logger
+ end
+
+ private
+
+ def default_logger
+ @logger = $DEBUG ? StderrLogger.new : NullLogger.new
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb
new file mode 100644
index 0000000000..7a11cf0933
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb
@@ -0,0 +1,20 @@
+module Gem::PubGrub
+ class Assignment
+ attr_reader :term, :cause, :decision_level, :index
+ def initialize(term, cause, decision_level, index)
+ @term = term
+ @cause = cause
+ @decision_level = decision_level
+ @index = index
+ end
+
+ def self.decision(package, version, decision_level, index)
+ term = Term.new(VersionConstraint.exact(package, version), true)
+ new(term, :decision, decision_level, index)
+ end
+
+ def decision?
+ cause == :decision
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb
new file mode 100644
index 0000000000..c8dbf2a5ab
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb
@@ -0,0 +1,169 @@
+require_relative 'version_constraint'
+require_relative 'incompatibility'
+
+module Gem::PubGrub
+ # Types:
+ #
+ # Where possible, Gem::PubGrub will accept user-defined types, so long as they quack.
+ #
+ # ## "Package":
+ #
+ # This class will be used to represent the various packages being solved for.
+ # .to_s will be called when displaying errors and debugging info, it should
+ # probably return the package's name.
+ # It must also have a reasonable definition of #== and #hash
+ #
+ # Example classes: String ("rails")
+ #
+ #
+ # ## "Version":
+ #
+ # This class will be used to represent a single version number.
+ #
+ # Versions don't need to store their associated package, however they will
+ # only be compared against other versions of the same package.
+ #
+ # It must be Comparible (and implement <=> reasonably)
+ #
+ # Example classes: Gem::Version, Integer
+ #
+ #
+ # ## "Dependency"
+ #
+ # This class represents the requirement one package has on another. It is
+ # returned by dependencies_for(package, version) and will be passed to
+ # parse_dependency to convert it to a format Gem::PubGrub understands.
+ #
+ # It must also have a reasonable definition of #==
+ #
+ # Example classes: String ("~> 1.0"), Gem::Requirement
+ #
+ class BasicPackageSource
+ # Override me!
+ #
+ # This is called per package to find all possible versions of a package.
+ #
+ # It is called at most once per-package
+ #
+ # Returns: Array of versions for a package, in preferred order of selection
+ def all_versions_for(package)
+ raise NotImplementedError
+ end
+
+ # Override me!
+ #
+ # Returns: Hash in the form of { package => requirement, ... }
+ def dependencies_for(package, version)
+ raise NotImplementedError
+ end
+
+ # Override me!
+ #
+ # Convert a (user-defined) dependency into a format Gem::PubGrub understands.
+ #
+ # Package is passed to this method but for many implementations is not
+ # needed.
+ #
+ # Returns: either a Gem::PubGrub::VersionRange, Gem::PubGrub::VersionUnion, or a
+ # Gem::PubGrub::VersionConstraint
+ def parse_dependency(package, dependency)
+ raise NotImplementedError
+ end
+
+ # Override me!
+ #
+ # If not overridden, this will call dependencies_for with the root package.
+ #
+ # Returns: Hash in the form of { package => requirement, ... } (see dependencies_for)
+ def root_dependencies
+ dependencies_for(@root_package, @root_version)
+ end
+
+ def initialize
+ @root_package = Package.root
+ @root_version = Package.root_version
+
+ @sorted_versions = Hash.new do |h,k|
+ if k == @root_package
+ h[k] = [@root_version]
+ else
+ h[k] = all_versions_for(k).sort
+ end
+ end
+
+ @cached_dependencies = Hash.new do |packages, package|
+ if package == @root_package
+ packages[package] = {
+ @root_version => root_dependencies
+ }
+ else
+ packages[package] = Hash.new do |versions, version|
+ versions[version] = dependencies_for(package, version)
+ end
+ end
+ end
+ end
+
+ def versions_for(package, range=VersionRange.any)
+ range.select_versions(@sorted_versions[package])
+ end
+
+ def no_versions_incompatibility_for(_package, unsatisfied_term)
+ cause = Incompatibility::NoVersions.new(unsatisfied_term)
+
+ Incompatibility.new([unsatisfied_term], cause: cause)
+ end
+
+ def incompatibilities_for(package, version)
+ package_deps = @cached_dependencies[package]
+ sorted_versions = @sorted_versions[package]
+ package_deps[version].map do |dep_package, dep_constraint_name|
+ low = high = sorted_versions.index(version)
+
+ # find version low such that all >= low share the same dep
+ while low > 0 &&
+ package_deps[sorted_versions[low - 1]][dep_package] == dep_constraint_name
+ low -= 1
+ end
+ low =
+ if low == 0
+ nil
+ else
+ sorted_versions[low]
+ end
+
+ # find version high such that all < high share the same dep
+ while high < sorted_versions.length &&
+ package_deps[sorted_versions[high]][dep_package] == dep_constraint_name
+ high += 1
+ end
+ high =
+ if high == sorted_versions.length
+ nil
+ else
+ sorted_versions[high]
+ end
+
+ range = VersionRange.new(min: low, max: high, include_min: !low.nil?)
+
+ self_constraint = VersionConstraint.new(package, range: range)
+
+ if !@packages.include?(dep_package)
+ # no such package -> this version is invalid
+ end
+
+ dep_constraint = parse_dependency(dep_package, dep_constraint_name)
+ if !dep_constraint
+ # falsey indicates this dependency was invalid
+ cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint_name)
+ return [Incompatibility.new([Term.new(self_constraint, true)], cause: cause)]
+ elsif !dep_constraint.is_a?(VersionConstraint)
+ # Upgrade range/union to VersionConstraint
+ dep_constraint = VersionConstraint.new(dep_package, range: dep_constraint)
+ end
+
+ Incompatibility.new([Term.new(self_constraint, true), Term.new(dep_constraint, false)], cause: :dependency)
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb
new file mode 100644
index 0000000000..d8bfde0286
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb
@@ -0,0 +1,182 @@
+module Gem::PubGrub
+ class FailureWriter
+ def initialize(root)
+ @root = root
+
+ # { Incompatibility => Integer }
+ @derivations = {}
+
+ # [ [ String, Integer or nil ] ]
+ @lines = []
+
+ # { Incompatibility => Integer }
+ @line_numbers = {}
+
+ count_derivations(root)
+ end
+
+ def write
+ return @root.to_s unless @root.conflict?
+
+ visit(@root)
+
+ padding = @line_numbers.empty? ? 0 : "(#{@line_numbers.values.last}) ".length
+
+ @lines.map do |message, number|
+ next "" if message.empty?
+
+ lead = number ? "(#{number}) " : ""
+ lead = lead.ljust(padding)
+ message = message.gsub("\n", "\n" + " " * (padding + 2))
+ "#{lead}#{message}"
+ end.join("\n")
+ end
+
+ private
+
+ def write_line(incompatibility, message, numbered:)
+ if numbered
+ number = @line_numbers.length + 1
+ @line_numbers[incompatibility] = number
+ end
+
+ @lines << [message, number]
+ end
+
+ def visit(incompatibility, conclusion: false)
+ raise unless incompatibility.conflict?
+
+ numbered = conclusion || @derivations[incompatibility] > 1;
+ conjunction = conclusion || incompatibility == @root ? "So," : "And"
+
+ cause = incompatibility.cause
+
+ if cause.conflict.conflict? && cause.other.conflict?
+ conflict_line = @line_numbers[cause.conflict]
+ other_line = @line_numbers[cause.other]
+
+ if conflict_line && other_line
+ write_line(
+ incompatibility,
+ "Because #{cause.conflict} (#{conflict_line})\nand #{cause.other} (#{other_line}),\n#{incompatibility}.",
+ numbered: numbered
+ )
+ elsif conflict_line || other_line
+ with_line = conflict_line ? cause.conflict : cause.other
+ without_line = conflict_line ? cause.other : cause.conflict
+ line = @line_numbers[with_line]
+
+ visit(without_line);
+ write_line(
+ incompatibility,
+ "#{conjunction} because #{with_line} (#{line}),\n#{incompatibility}.",
+ numbered: numbered
+ )
+ else
+ single_line_conflict = single_line?(cause.conflict.cause)
+ single_line_other = single_line?(cause.other.cause)
+
+ if single_line_conflict || single_line_other
+ first = single_line_other ? cause.conflict : cause.other
+ second = single_line_other ? cause.other : cause.conflict
+ visit(first)
+ visit(second)
+ write_line(
+ incompatibility,
+ "Thus, #{incompatibility}.",
+ numbered: numbered
+ )
+ else
+ visit(cause.conflict, conclusion: true)
+ @lines << ["", nil]
+ visit(cause.other)
+
+ write_line(
+ incompatibility,
+ "#{conjunction} because #{cause.conflict} (#{@line_numbers[cause.conflict]}),\n#{incompatibility}.",
+ numbered: numbered
+ )
+ end
+ end
+ elsif cause.conflict.conflict? || cause.other.conflict?
+ derived = cause.conflict.conflict? ? cause.conflict : cause.other
+ ext = cause.conflict.conflict? ? cause.other : cause.conflict
+
+ derived_line = @line_numbers[derived]
+ if derived_line
+ write_line(
+ incompatibility,
+ "Because #{ext}\nand #{derived} (#{derived_line}),\n#{incompatibility}.",
+ numbered: numbered
+ )
+ elsif collapsible?(derived)
+ derived_cause = derived.cause
+ if derived_cause.conflict.conflict?
+ collapsed_derived = derived_cause.conflict
+ collapsed_ext = derived_cause.other
+ else
+ collapsed_derived = derived_cause.other
+ collapsed_ext = derived_cause.conflict
+ end
+
+ visit(collapsed_derived)
+
+ write_line(
+ incompatibility,
+ "#{conjunction} because #{collapsed_ext}\nand #{ext},\n#{incompatibility}.",
+ numbered: numbered
+ )
+ else
+ visit(derived)
+ write_line(
+ incompatibility,
+ "#{conjunction} because #{ext},\n#{incompatibility}.",
+ numbered: numbered
+ )
+ end
+ else
+ write_line(
+ incompatibility,
+ "Because #{cause.conflict}\nand #{cause.other},\n#{incompatibility}.",
+ numbered: numbered
+ )
+ end
+ end
+
+ def single_line?(cause)
+ !cause.conflict.conflict? && !cause.other.conflict?
+ end
+
+ def collapsible?(incompatibility)
+ return false if @derivations[incompatibility] > 1
+
+ cause = incompatibility.cause
+ # If incompatibility is derived from two derived incompatibilities,
+ # there are too many transitive causes to display concisely.
+ return false if cause.conflict.conflict? && cause.other.conflict?
+
+ # If incompatibility is derived from two external incompatibilities, it
+ # tends to be confusing to collapse it.
+ return false unless cause.conflict.conflict? || cause.other.conflict?
+
+ # If incompatibility's internal cause is numbered, collapsing it would
+ # get too noisy.
+ complex = cause.conflict.conflict? ? cause.conflict : cause.other
+
+ !@line_numbers.has_key?(complex)
+ end
+
+ def count_derivations(incompatibility)
+ if @derivations.has_key?(incompatibility)
+ @derivations[incompatibility] += 1
+ else
+ @derivations[incompatibility] = 1
+ if incompatibility.conflict?
+ cause = incompatibility.cause
+ count_derivations(cause.conflict)
+ count_derivations(cause.other)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb
new file mode 100644
index 0000000000..b5652b5e01
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb
@@ -0,0 +1,150 @@
+module Gem::PubGrub
+ class Incompatibility
+ ConflictCause = Struct.new(:incompatibility, :satisfier) do
+ alias_method :conflict, :incompatibility
+ alias_method :other, :satisfier
+ end
+
+ InvalidDependency = Struct.new(:package, :constraint) do
+ end
+
+ NoVersions = Struct.new(:constraint) do
+ end
+
+ attr_reader :terms, :cause
+
+ def initialize(terms, cause:, custom_explanation: nil)
+ @cause = cause
+ @terms = cleanup_terms(terms)
+ @custom_explanation = custom_explanation
+
+ if cause == :dependency && @terms.length != 2
+ raise ArgumentError, "a dependency Incompatibility must have exactly two terms. Got #{@terms.inspect}"
+ end
+ end
+
+ def hash
+ cause.hash ^ terms.hash
+ end
+
+ def eql?(other)
+ cause.eql?(other.cause) &&
+ terms.eql?(other.terms)
+ end
+
+ def failure?
+ terms.empty? || (terms.length == 1 && Package.root?(terms[0].package) && terms[0].positive?)
+ end
+
+ def conflict?
+ ConflictCause === cause
+ end
+
+ # Returns all external incompatibilities in this incompatibility's
+ # derivation graph
+ def external_incompatibilities
+ if conflict?
+ [
+ cause.conflict,
+ cause.other
+ ].flat_map(&:external_incompatibilities)
+ else
+ [this]
+ end
+ end
+
+ def to_s
+ return @custom_explanation if @custom_explanation
+
+ case cause
+ when :root
+ "(root dependency)"
+ when :dependency
+ "#{terms[0].to_s(allow_every: true)} depends on #{terms[1].invert}"
+ when Gem::PubGrub::Incompatibility::InvalidDependency
+ "#{terms[0].to_s(allow_every: true)} depends on unknown package #{cause.package}"
+ when Gem::PubGrub::Incompatibility::NoVersions
+ "no versions satisfy #{cause.constraint}"
+ when Gem::PubGrub::Incompatibility::ConflictCause
+ if failure?
+ "version solving has failed"
+ elsif terms.length == 1
+ term = terms[0]
+ if term.positive?
+ if term.constraint.any?
+ "#{term.package} cannot be used"
+ else
+ "#{term.to_s(allow_every: true)} cannot be used"
+ end
+ else
+ "#{term.invert} is required"
+ end
+ else
+ if terms.all?(&:positive?)
+ if terms.length == 2
+ "#{terms[0].to_s(allow_every: true)} is incompatible with #{terms[1]}"
+ else
+ "one of #{terms.map(&:to_s).join(" or ")} must be false"
+ end
+ elsif terms.all?(&:negative?)
+ if terms.length == 2
+ "either #{terms[0].invert} or #{terms[1].invert}"
+ else
+ "one of #{terms.map(&:invert).join(" or ")} must be true";
+ end
+ else
+ positive = terms.select(&:positive?)
+ negative = terms.select(&:negative?).map(&:invert)
+
+ if positive.length == 1
+ "#{positive[0].to_s(allow_every: true)} requires #{negative.join(" or ")}"
+ else
+ "if #{positive.join(" and ")} then #{negative.join(" or ")}"
+ end
+ end
+ end
+ else
+ raise "unhandled cause: #{cause.inspect}"
+ end
+ end
+
+ def inspect
+ "#<#{self.class} #{to_s}>"
+ end
+
+ def pretty_print(q)
+ q.group 2, "#<#{self.class}", ">" do
+ q.breakable
+ q.text to_s
+
+ q.breakable
+ q.text " caused by "
+ q.pp @cause
+ end
+ end
+
+ private
+
+ def cleanup_terms(terms)
+ terms.each do |term|
+ raise "#{term.inspect} must be a term" unless term.is_a?(Term)
+ end
+
+ if terms.length != 1 && ConflictCause === cause
+ terms = terms.reject do |term|
+ term.positive? && Package.root?(term.package)
+ end
+ end
+
+ # Optimized simple cases
+ return terms if terms.length <= 1
+ return terms if terms.length == 2 && terms[0].package != terms[1].package
+
+ terms.group_by(&:package).map do |package, common_terms|
+ common_terms.inject do |acc, term|
+ acc.intersect(term)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb
new file mode 100644
index 0000000000..6baa908f60
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gem::PubGrub
+ class Package
+
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def inspect
+ "#<#{self.class} #{name.inspect}>"
+ end
+
+ def <=>(other)
+ name <=> other.name
+ end
+
+ ROOT = Package.new(:root)
+ ROOT_VERSION = 0
+
+ def self.root
+ ROOT
+ end
+
+ def self.root_version
+ ROOT_VERSION
+ end
+
+ def self.root?(package)
+ if package.respond_to?(:root?)
+ package.root?
+ else
+ package == root
+ end
+ end
+
+ def to_s
+ name.to_s
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb
new file mode 100644
index 0000000000..f6a6ae6964
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb
@@ -0,0 +1,121 @@
+require_relative 'assignment'
+
+module Gem::PubGrub
+ class PartialSolution
+ attr_reader :assignments, :decisions
+ attr_reader :attempted_solutions
+
+ def initialize
+ reset!
+
+ @attempted_solutions = 1
+ @backtracking = false
+ end
+
+ def decision_level
+ @decisions.length
+ end
+
+ def relation(term)
+ package = term.package
+ return :overlap if !@terms.key?(package)
+
+ @relation_cache[package][term] ||=
+ @terms[package].relation(term)
+ end
+
+ def satisfies?(term)
+ relation(term) == :subset
+ end
+
+ def derive(term, cause)
+ add_assignment(Assignment.new(term, cause, decision_level, assignments.length))
+ end
+
+ def satisfier(term)
+ assignment =
+ @assignments_by[term.package].bsearch do |assignment_by|
+ @cumulative_assignments[assignment_by].satisfies?(term)
+ end
+
+ assignment || raise("#{term} unsatisfied")
+ end
+
+ # A list of unsatisfied terms
+ def unsatisfied
+ @required.keys.reject do |package|
+ @decisions.key?(package)
+ end.map do |package|
+ @terms[package]
+ end
+ end
+
+ def decide(package, version)
+ @attempted_solutions += 1 if @backtracking
+ @backtracking = false;
+
+ decisions[package] = version
+ assignment = Assignment.decision(package, version, decision_level, assignments.length)
+ add_assignment(assignment)
+ end
+
+ def backtrack(previous_level)
+ @backtracking = true
+
+ new_assignments = assignments.select do |assignment|
+ assignment.decision_level <= previous_level
+ end
+
+ new_decisions = Hash[decisions.first(previous_level)]
+
+ reset!
+
+ @decisions = new_decisions
+
+ new_assignments.each do |assignment|
+ add_assignment(assignment)
+ end
+ end
+
+ private
+
+ def reset!
+ # { Array<Assignment> }
+ @assignments = []
+
+ # { Package => Array<Assignment> }
+ @assignments_by = Hash.new { |h,k| h[k] = [] }
+ @cumulative_assignments = {}.compare_by_identity
+
+ # { Package => Package::Version }
+ @decisions = {}
+
+ # { Package => Term }
+ @terms = {}
+ @relation_cache = Hash.new { |h,k| h[k] = {} }
+
+ # { Package => Boolean }
+ @required = {}
+ end
+
+ def add_assignment(assignment)
+ term = assignment.term
+ package = term.package
+
+ @assignments << assignment
+ @assignments_by[package] << assignment
+
+ @required[package] = true if term.positive?
+
+ if @terms.key?(package)
+ old_term = @terms[package]
+ @terms[package] = old_term.intersect(term)
+ else
+ @terms[package] = term
+ end
+ @relation_cache[package].clear
+
+ @cumulative_assignments[assignment] = @terms[package]
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb
new file mode 100644
index 0000000000..60ca3ca2ea
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb
@@ -0,0 +1,45 @@
+module Gem::PubGrub
+ module RubyGems
+ extend self
+
+ def requirement_to_range(requirement)
+ ranges = requirement.requirements.map do |(op, ver)|
+ case op
+ when "~>"
+ name = "~> #{ver}"
+ bump = ver.class.new(ver.bump.to_s + ".A")
+ VersionRange.new(name: name, min: ver, max: bump, include_min: true)
+ when ">"
+ VersionRange.new(min: ver)
+ when ">="
+ VersionRange.new(min: ver, include_min: true)
+ when "<"
+ VersionRange.new(max: ver)
+ when "<="
+ VersionRange.new(max: ver, include_max: true)
+ when "="
+ VersionRange.new(min: ver, max: ver, include_min: true, include_max: true)
+ when "!="
+ VersionRange.new(min: ver, max: ver, include_min: true, include_max: true).invert
+ else
+ raise "bad version specifier: #{op}"
+ end
+ end
+
+ ranges.inject(&:intersect)
+ end
+
+ def requirement_to_constraint(package, requirement)
+ Gem::PubGrub::VersionConstraint.new(package, range: requirement_to_range(requirement))
+ end
+
+ def parse_range(dep)
+ requirement_to_range(Gem::Requirement.new(dep))
+ end
+
+ def parse_constraint(package, dep)
+ range = parse_range(dep)
+ Gem::PubGrub::VersionConstraint.new(package, range: range)
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb
new file mode 100644
index 0000000000..c4181d2b25
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb
@@ -0,0 +1,19 @@
+require_relative 'failure_writer'
+
+module Gem::PubGrub
+ class SolveFailure < StandardError
+ attr_reader :incompatibility
+
+ def initialize(incompatibility)
+ @incompatibility = incompatibility
+ end
+
+ def to_s
+ "Could not find compatible versions\n\n#{explanation}"
+ end
+
+ def explanation
+ @explanation ||= FailureWriter.new(@incompatibility).write
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb
new file mode 100644
index 0000000000..9e1de7d7a1
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb
@@ -0,0 +1,61 @@
+require_relative 'package'
+require_relative 'rubygems'
+require_relative 'version_constraint'
+require_relative 'incompatibility'
+require_relative 'basic_package_source'
+
+module Gem::PubGrub
+ class StaticPackageSource < BasicPackageSource
+ class DSL
+ def initialize(packages, root_deps)
+ @packages = packages
+ @root_deps = root_deps
+ end
+
+ def root(deps:)
+ @root_deps.update(deps)
+ end
+
+ def add(name, version, deps: {})
+ version = Gem::Version.new(version)
+ @packages[name] ||= {}
+ raise ArgumentError, "#{name} #{version} declared twice" if @packages[name].key?(version)
+ @packages[name][version] = clean_deps(name, version, deps)
+ end
+
+ private
+
+ # Exclude redundant self-referencing dependencies
+ def clean_deps(name, version, deps)
+ deps.reject {|dep_name, req| name == dep_name && Gem::PubGrub::RubyGems.parse_range(req).include?(version) }
+ end
+ end
+
+ def initialize
+ @root_deps = {}
+ @packages = {}
+
+ yield DSL.new(@packages, @root_deps)
+
+ super()
+ end
+
+ def all_versions_for(package)
+ @packages[package].keys
+ end
+
+ def root_dependencies
+ @root_deps
+ end
+
+ def dependencies_for(package, version)
+ @packages[package][version]
+ end
+
+ def parse_dependency(package, dependency)
+ return false unless @packages.key?(package)
+
+ Gem::PubGrub::RubyGems.parse_constraint(package, dependency)
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb
new file mode 100644
index 0000000000..b9874cdece
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb
@@ -0,0 +1,42 @@
+module Gem::PubGrub
+ class Strategy
+ def initialize(source)
+ @source = source
+
+ @root_package = Package.root
+ @root_version = Package.root_version
+
+ @version_indexes = Hash.new do |h,k|
+ if k == @root_package
+ h[k] = { @root_version => 0 }
+ else
+ h[k] = @source.all_versions_for(k).each.with_index.to_h
+ end
+ end
+ end
+
+ def next_package_and_version(unsatisfied)
+ package, range = next_term_to_try_from(unsatisfied)
+
+ [package, most_preferred_version_of(package, range)]
+ end
+
+ private
+
+ def most_preferred_version_of(package, range)
+ versions = @source.versions_for(package, range)
+
+ indexes = @version_indexes[package]
+ versions.min_by { |version| indexes[version] || Float::INFINITY }
+ end
+
+ def next_term_to_try_from(unsatisfied)
+ unsatisfied.min_by do |package, range|
+ matching_versions = @source.versions_for(package, range)
+ higher_versions = @source.versions_for(package, range.upper_invert)
+
+ [matching_versions.count <= 1 ? 0 : 1, higher_versions.count]
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb
new file mode 100644
index 0000000000..bb26bdc911
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb
@@ -0,0 +1,105 @@
+module Gem::PubGrub
+ class Term
+ attr_reader :package, :constraint, :positive
+
+ def initialize(constraint, positive)
+ @constraint = constraint
+ @package = @constraint.package
+ @positive = positive
+ end
+
+ def to_s(allow_every: false)
+ if positive
+ @constraint.to_s(allow_every: allow_every)
+ else
+ "not #{@constraint}"
+ end
+ end
+
+ def hash
+ constraint.hash ^ positive.hash
+ end
+
+ def eql?(other)
+ positive == other.positive &&
+ constraint.eql?(other.constraint)
+ end
+
+ def invert
+ self.class.new(@constraint, !@positive)
+ end
+ alias_method :inverse, :invert
+
+ def intersect(other)
+ raise ArgumentError, "packages must match" if package != other.package
+
+ if positive? && other.positive?
+ self.class.new(constraint.intersect(other.constraint), true)
+ elsif negative? && other.negative?
+ self.class.new(constraint.union(other.constraint), false)
+ else
+ positive = positive? ? self : other
+ negative = negative? ? self : other
+ self.class.new(positive.constraint.intersect(negative.constraint.invert), true)
+ end
+ end
+
+ def difference(other)
+ intersect(other.invert)
+ end
+
+ def relation(other)
+ if positive? && other.positive?
+ constraint.relation(other.constraint)
+ elsif negative? && other.positive?
+ if constraint.allows_all?(other.constraint)
+ :disjoint
+ else
+ :overlap
+ end
+ elsif positive? && other.negative?
+ if !other.constraint.allows_any?(constraint)
+ :subset
+ elsif other.constraint.allows_all?(constraint)
+ :disjoint
+ else
+ :overlap
+ end
+ elsif negative? && other.negative?
+ if constraint.allows_all?(other.constraint)
+ :subset
+ else
+ :overlap
+ end
+ else
+ raise
+ end
+ end
+
+ def normalized_constraint
+ @normalized_constraint ||= positive ? constraint : constraint.invert
+ end
+
+ def satisfies?(other)
+ raise ArgumentError, "packages must match" unless package == other.package
+
+ relation(other) == :subset
+ end
+
+ def positive?
+ @positive
+ end
+
+ def negative?
+ !positive?
+ end
+
+ def empty?
+ @empty ||= normalized_constraint.empty?
+ end
+
+ def inspect
+ "#<#{self.class} #{self}>"
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb
new file mode 100644
index 0000000000..5701bf0656
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb
@@ -0,0 +1,3 @@
+module Gem::PubGrub
+ VERSION = "0.5.0"
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb
new file mode 100644
index 0000000000..ee998b3271
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb
@@ -0,0 +1,129 @@
+require_relative 'version_range'
+
+module Gem::PubGrub
+ class VersionConstraint
+ attr_reader :package, :range
+
+ # @param package [Gem::PubGrub::Package]
+ # @param range [Gem::PubGrub::VersionRange]
+ def initialize(package, range: nil)
+ @package = package
+ @range = range
+ end
+
+ def hash
+ package.hash ^ range.hash
+ end
+
+ def ==(other)
+ package == other.package &&
+ range == other.range
+ end
+
+ def eql?(other)
+ package.eql?(other.package) &&
+ range.eql?(other.range)
+ end
+
+ class << self
+ def exact(package, version)
+ range = VersionRange.new(min: version, max: version, include_min: true, include_max: true)
+ new(package, range: range)
+ end
+
+ def any(package)
+ new(package, range: VersionRange.any)
+ end
+
+ def empty(package)
+ new(package, range: VersionRange.empty)
+ end
+ end
+
+ def intersect(other)
+ unless package == other.package
+ raise ArgumentError, "Can only intersect between VersionConstraint of the same package"
+ end
+
+ self.class.new(package, range: range.intersect(other.range))
+ end
+
+ def union(other)
+ unless package == other.package
+ raise ArgumentError, "Can only intersect between VersionConstraint of the same package"
+ end
+
+ self.class.new(package, range: range.union(other.range))
+ end
+
+ def invert
+ new_range = range.invert
+ self.class.new(package, range: new_range)
+ end
+
+ def difference(other)
+ intersect(other.invert)
+ end
+
+ def allows_all?(other)
+ range.allows_all?(other.range)
+ end
+
+ def allows_any?(other)
+ range.intersects?(other.range)
+ end
+
+ def subset?(other)
+ other.allows_all?(self)
+ end
+
+ def overlap?(other)
+ other.allows_any?(self)
+ end
+
+ def disjoint?(other)
+ !overlap?(other)
+ end
+
+ def relation(other)
+ if subset?(other)
+ :subset
+ elsif overlap?(other)
+ :overlap
+ else
+ :disjoint
+ end
+ end
+
+ def to_s(allow_every: false)
+ if Package.root?(package)
+ package.to_s
+ elsif allow_every && any?
+ "every version of #{package}"
+ else
+ "#{package} #{constraint_string}"
+ end
+ end
+
+ def constraint_string
+ if any?
+ ">= 0"
+ else
+ range.to_s
+ end
+ end
+
+ def empty?
+ range.empty?
+ end
+
+ # Does this match every version of the package
+ def any?
+ range.any?
+ end
+
+ def inspect
+ "#<#{self.class} #{self}>"
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb
new file mode 100644
index 0000000000..fa0e2d5742
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb
@@ -0,0 +1,423 @@
+# frozen_string_literal: true
+
+module Gem::PubGrub
+ class VersionRange
+ attr_reader :min, :max, :include_min, :include_max
+
+ alias_method :include_min?, :include_min
+ alias_method :include_max?, :include_max
+
+ class Empty < VersionRange
+ undef_method :min, :max
+ undef_method :include_min, :include_min?
+ undef_method :include_max, :include_max?
+
+ def initialize
+ end
+
+ def empty?
+ true
+ end
+
+ def eql?(other)
+ other.empty?
+ end
+
+ def hash
+ [].hash
+ end
+
+ def intersects?(_)
+ false
+ end
+
+ def intersect(other)
+ self
+ end
+
+ def allows_all?(other)
+ other.empty?
+ end
+
+ def include?(_)
+ false
+ end
+
+ def any?
+ false
+ end
+
+ def to_s
+ "(no versions)"
+ end
+
+ def ==(other)
+ other.class == self.class
+ end
+
+ def invert
+ VersionRange.any
+ end
+
+ def select_versions(_)
+ []
+ end
+ end
+
+ EMPTY = Empty.new
+ Empty.singleton_class.undef_method(:new)
+
+ def self.empty
+ EMPTY
+ end
+
+ def self.any
+ new
+ end
+
+ def initialize(min: nil, max: nil, include_min: false, include_max: false, name: nil)
+ raise ArgumentError, "Ranges without a lower bound cannot have include_min == true" if !min && include_min == true
+ raise ArgumentError, "Ranges without an upper bound cannot have include_max == true" if !max && include_max == true
+
+ @min = min
+ @max = max
+ @include_min = include_min
+ @include_max = include_max
+ @name = name
+ end
+
+ def hash
+ @hash ||= min.hash ^ max.hash ^ include_min.hash ^ include_max.hash
+ end
+
+ def eql?(other)
+ if other.is_a?(VersionRange)
+ !other.empty? &&
+ min.eql?(other.min) &&
+ max.eql?(other.max) &&
+ include_min.eql?(other.include_min) &&
+ include_max.eql?(other.include_max)
+ else
+ ranges.eql?(other.ranges)
+ end
+ end
+
+ def ranges
+ [self]
+ end
+
+ def include?(version)
+ compare_version(version) == 0
+ end
+
+ # Partitions passed versions into [lower, within, higher]
+ #
+ # versions must be sorted
+ def partition_versions(versions)
+ min_index =
+ if !min || versions.empty?
+ 0
+ elsif include_min?
+ (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= min }
+ else
+ (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > min }
+ end
+
+ lower = versions.slice(0, min_index)
+ versions = versions.slice(min_index, versions.size)
+
+ max_index =
+ if !max || versions.empty?
+ versions.size
+ elsif include_max?
+ (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > max }
+ else
+ (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= max }
+ end
+
+ [
+ lower,
+ versions.slice(0, max_index),
+ versions.slice(max_index, versions.size)
+ ]
+ end
+
+ # Returns versions which are included by this range.
+ #
+ # versions must be sorted
+ def select_versions(versions)
+ return versions if any?
+
+ partition_versions(versions)[1]
+ end
+
+ def compare_version(version)
+ if min
+ case version <=> min
+ when -1
+ return -1
+ when 0
+ return -1 if !include_min
+ when 1
+ end
+ end
+
+ if max
+ case version <=> max
+ when -1
+ when 0
+ return 1 if !include_max
+ when 1
+ return 1
+ end
+ end
+
+ 0
+ end
+
+ def strictly_lower?(other)
+ return false if !max || !other.min
+
+ case max <=> other.min
+ when 0
+ !include_max || !other.include_min
+ when -1
+ true
+ when 1
+ false
+ end
+ end
+
+ def strictly_higher?(other)
+ other.strictly_lower?(self)
+ end
+
+ def intersects?(other)
+ return false if other.empty?
+ return other.intersects?(self) if other.is_a?(VersionUnion)
+ !strictly_lower?(other) && !strictly_higher?(other)
+ end
+ alias_method :allows_any?, :intersects?
+
+ def intersect(other)
+ return other if other.empty?
+ return other.intersect(self) if other.is_a?(VersionUnion)
+
+ min_range =
+ if !min
+ other
+ elsif !other.min
+ self
+ else
+ case min <=> other.min
+ when 0
+ include_min ? other : self
+ when -1
+ other
+ when 1
+ self
+ end
+ end
+
+ max_range =
+ if !max
+ other
+ elsif !other.max
+ self
+ else
+ case max <=> other.max
+ when 0
+ include_max ? other : self
+ when -1
+ self
+ when 1
+ other
+ end
+ end
+
+ if !min_range.equal?(max_range) && min_range.min && max_range.max
+ case min_range.min <=> max_range.max
+ when -1
+ when 0
+ if !min_range.include_min || !max_range.include_max
+ return EMPTY
+ end
+ when 1
+ return EMPTY
+ end
+ end
+
+ VersionRange.new(
+ min: min_range.min,
+ include_min: min_range.include_min,
+ max: max_range.max,
+ include_max: max_range.include_max
+ )
+ end
+
+ # The span covered by two ranges
+ #
+ # If self and other are contiguous, this builds a union of the two ranges.
+ # (if they aren't you are probably calling the wrong method)
+ def span(other)
+ return self if other.empty?
+
+ min_range =
+ if !min
+ self
+ elsif !other.min
+ other
+ else
+ case min <=> other.min
+ when 0
+ include_min ? self : other
+ when -1
+ self
+ when 1
+ other
+ end
+ end
+
+ max_range =
+ if !max
+ self
+ elsif !other.max
+ other
+ else
+ case max <=> other.max
+ when 0
+ include_max ? self : other
+ when -1
+ other
+ when 1
+ self
+ end
+ end
+
+ VersionRange.new(
+ min: min_range.min,
+ include_min: min_range.include_min,
+ max: max_range.max,
+ include_max: max_range.include_max
+ )
+ end
+
+ def union(other)
+ return other.union(self) if other.is_a?(VersionUnion)
+
+ if contiguous_to?(other)
+ span(other)
+ else
+ VersionUnion.union([self, other])
+ end
+ end
+
+ def contiguous_to?(other)
+ return false if other.empty?
+ return true if any?
+
+ intersects?(other) || contiguous_below?(other) || contiguous_above?(other)
+ end
+
+ def contiguous_below?(other)
+ return false if !max || !other.min
+
+ max == other.min && (include_max || other.include_min)
+ end
+
+ def contiguous_above?(other)
+ other.contiguous_below?(self)
+ end
+
+ def allows_all?(other)
+ return true if other.empty?
+
+ if other.is_a?(VersionUnion)
+ return VersionUnion.new([self]).allows_all?(other)
+ end
+
+ return false if max && !other.max
+ return false if min && !other.min
+
+ if min
+ case min <=> other.min
+ when -1
+ when 0
+ return false if !include_min && other.include_min
+ when 1
+ return false
+ end
+ end
+
+ if max
+ case max <=> other.max
+ when -1
+ return false
+ when 0
+ return false if !include_max && other.include_max
+ when 1
+ end
+ end
+
+ true
+ end
+
+ def any?
+ !min && !max
+ end
+
+ def empty?
+ false
+ end
+
+ def to_s
+ @name ||= constraints.join(", ")
+ end
+
+ def inspect
+ "#<#{self.class} #{to_s}>"
+ end
+
+ def upper_invert
+ return self.class.empty unless max
+
+ VersionRange.new(min: max, include_min: !include_max)
+ end
+
+ def invert
+ return self.class.empty if any?
+
+ low = -> { VersionRange.new(max: min, include_max: !include_min) }
+ high = -> { VersionRange.new(min: max, include_min: !include_max) }
+
+ if !min
+ high.call
+ elsif !max
+ low.call
+ else
+ low.call.union(high.call)
+ end
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ min == other.min &&
+ max == other.max &&
+ include_min == other.include_min &&
+ include_max == other.include_max
+ end
+
+ private
+
+ def constraints
+ return ["any"] if any?
+ return ["= #{min}"] if min.to_s == max.to_s
+
+ c = []
+ c << "#{include_min ? ">=" : ">"} #{min}" if min
+ c << "#{include_max ? "<=" : "<"} #{max}" if max
+ c
+ end
+
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb
new file mode 100644
index 0000000000..3341d8fe3b
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb
@@ -0,0 +1,236 @@
+require_relative 'partial_solution'
+require_relative 'term'
+require_relative 'incompatibility'
+require_relative 'solve_failure'
+require_relative 'strategy'
+
+module Gem::PubGrub
+ class VersionSolver
+ attr_reader :logger
+ attr_reader :source
+ attr_reader :solution
+ attr_reader :strategy
+
+ def initialize(source:, root: Package.root, strategy: Strategy.new(source), logger: Gem::PubGrub.logger)
+ @logger = logger
+
+ @source = source
+ @strategy = strategy
+
+ # { package => [incompatibility, ...]}
+ @incompatibilities = Hash.new do |h, k|
+ h[k] = []
+ end
+
+ @seen_incompatibilities = {}
+
+ @solution = PartialSolution.new
+
+ add_incompatibility Incompatibility.new([
+ Term.new(VersionConstraint.any(root), false)
+ ], cause: :root)
+
+ propagate(root)
+ end
+
+ def solved?
+ solution.unsatisfied.empty?
+ end
+
+ # Returns true if there is more work to be done, false otherwise
+ def work
+ unsatisfied_terms = solution.unsatisfied
+ if unsatisfied_terms.empty?
+ logger.info { "Solution found after #{solution.attempted_solutions} attempts:" }
+ solution.decisions.each do |package, version|
+ next if Package.root?(package)
+ logger.info { "* #{package} #{version}" }
+ end
+
+ return false
+ end
+
+ next_package = choose_package_version_from(unsatisfied_terms)
+ propagate(next_package)
+
+ true
+ end
+
+ def solve
+ while work; end
+
+ solution.decisions
+ end
+
+ alias_method :result, :solve
+
+ private
+
+ def propagate(initial_package)
+ changed = [initial_package]
+ while package = changed.shift
+ @incompatibilities[package].reverse_each do |incompatibility|
+ result = propagate_incompatibility(incompatibility)
+ if result == :conflict
+ root_cause = resolve_conflict(incompatibility)
+ changed.clear
+ changed << propagate_incompatibility(root_cause)
+ elsif result # should be a Package
+ changed << result
+ end
+ end
+ changed.uniq!
+ end
+ end
+
+ def propagate_incompatibility(incompatibility)
+ unsatisfied = nil
+ incompatibility.terms.each do |term|
+ relation = solution.relation(term)
+ if relation == :disjoint
+ return nil
+ elsif relation == :overlap
+ # If more than one term is inconclusive, we can't deduce anything
+ return nil if unsatisfied
+ unsatisfied = term
+ end
+ end
+
+ if !unsatisfied
+ return :conflict
+ end
+
+ logger.debug { "derived: #{unsatisfied.invert}" }
+
+ solution.derive(unsatisfied.invert, incompatibility)
+
+ unsatisfied.package
+ end
+
+ def choose_package_version_from(unsatisfied_terms)
+ remaining = unsatisfied_terms.map { |t| [t.package, t.constraint.range] }.to_h
+
+ package, version = strategy.next_package_and_version(remaining)
+
+ logger.debug { "attempting #{package} #{version}" }
+
+ if version.nil?
+ unsatisfied_term = unsatisfied_terms.find { |t| t.package == package }
+ add_incompatibility source.no_versions_incompatibility_for(package, unsatisfied_term)
+ return package
+ end
+
+ conflict = false
+
+ source.incompatibilities_for(package, version).each do |incompatibility|
+ if @seen_incompatibilities.include?(incompatibility)
+ logger.debug { "knew: #{incompatibility}" }
+ next
+ end
+ @seen_incompatibilities[incompatibility] = true
+
+ add_incompatibility incompatibility
+
+ conflict ||= incompatibility.terms.all? do |term|
+ term.package == package || solution.satisfies?(term)
+ end
+ end
+
+ unless conflict
+ logger.info { "selected #{package} #{version}" }
+
+ solution.decide(package, version)
+ else
+ logger.info { "conflict: #{conflict.inspect}" }
+ end
+
+ package
+ end
+
+ def resolve_conflict(incompatibility)
+ logger.info { "conflict: #{incompatibility}" }
+
+ new_incompatibility = nil
+
+ while !incompatibility.failure?
+ most_recent_term = nil
+ most_recent_satisfier = nil
+ difference = nil
+
+ previous_level = 1
+
+ incompatibility.terms.each do |term|
+ satisfier = solution.satisfier(term)
+
+ if most_recent_satisfier.nil?
+ most_recent_term = term
+ most_recent_satisfier = satisfier
+ elsif most_recent_satisfier.index < satisfier.index
+ previous_level = [previous_level, most_recent_satisfier.decision_level].max
+ most_recent_term = term
+ most_recent_satisfier = satisfier
+ difference = nil
+ else
+ previous_level = [previous_level, satisfier.decision_level].max
+ end
+
+ if most_recent_term == term
+ difference = most_recent_satisfier.term.difference(most_recent_term)
+ if difference.empty?
+ difference = nil
+ else
+ difference_satisfier = solution.satisfier(difference.inverse)
+ previous_level = [previous_level, difference_satisfier.decision_level].max
+ end
+ end
+ end
+
+ if previous_level < most_recent_satisfier.decision_level ||
+ most_recent_satisfier.decision?
+
+ logger.info { "backtracking to #{previous_level}" }
+ solution.backtrack(previous_level)
+
+ if new_incompatibility
+ add_incompatibility(new_incompatibility)
+ end
+
+ return incompatibility
+ end
+
+ new_terms = []
+ new_terms += incompatibility.terms - [most_recent_term]
+ new_terms += most_recent_satisfier.cause.terms.reject { |term|
+ term.package == most_recent_satisfier.term.package
+ }
+ if difference
+ new_terms << difference.invert
+ end
+
+ new_incompatibility = Incompatibility.new(new_terms, cause: Incompatibility::ConflictCause.new(incompatibility, most_recent_satisfier.cause))
+
+ if incompatibility.to_s == new_incompatibility.to_s
+ logger.info { "!! failed to resolve conflicts, this shouldn't have happened" }
+ break
+ end
+
+ incompatibility = new_incompatibility
+
+ partially = difference ? " partially" : ""
+ logger.info { "! #{most_recent_term} is#{partially} satisfied by #{most_recent_satisfier.term}" }
+ logger.info { "! which is caused by #{most_recent_satisfier.cause}" }
+ logger.info { "! thus #{incompatibility}" }
+ end
+
+ raise SolveFailure.new(incompatibility)
+ end
+
+ def add_incompatibility(incompatibility)
+ logger.debug { "fact: #{incompatibility}" }
+ incompatibility.terms.each do |term|
+ package = term.package
+ @incompatibilities[package] << incompatibility
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb
new file mode 100644
index 0000000000..4166318a98
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+module Gem::PubGrub
+ class VersionUnion
+ attr_reader :ranges
+
+ def self.normalize_ranges(ranges)
+ ranges = ranges.flat_map do |range|
+ range.ranges
+ end
+
+ ranges.reject!(&:empty?)
+
+ return [] if ranges.empty?
+
+ mins, ranges = ranges.partition { |r| !r.min }
+ original_ranges = mins + ranges.sort_by { |r| [r.min, r.include_min ? 0 : 1] }
+ ranges = [original_ranges.shift]
+ original_ranges.each do |range|
+ if ranges.last.contiguous_to?(range)
+ ranges << ranges.pop.span(range)
+ else
+ ranges << range
+ end
+ end
+
+ ranges
+ end
+
+ def self.union(ranges, normalize: true)
+ ranges = normalize_ranges(ranges) if normalize
+
+ if ranges.size == 0
+ VersionRange.empty
+ elsif ranges.size == 1
+ ranges[0]
+ else
+ new(ranges)
+ end
+ end
+
+ def initialize(ranges)
+ raise ArgumentError unless ranges.all? { |r| r.instance_of?(VersionRange) }
+ @ranges = ranges
+ end
+
+ def hash
+ ranges.hash
+ end
+
+ def eql?(other)
+ ranges.eql?(other.ranges)
+ end
+
+ def include?(version)
+ !!ranges.bsearch {|r| r.compare_version(version) }
+ end
+
+ def select_versions(all_versions)
+ versions = []
+ ranges.inject(all_versions) do |acc, range|
+ _, matching, higher = range.partition_versions(acc)
+ versions.concat matching
+ higher
+ end
+ versions
+ end
+
+ def intersects?(other)
+ my_ranges = ranges.dup
+ other_ranges = other.ranges.dup
+
+ my_range = my_ranges.shift
+ other_range = other_ranges.shift
+ while my_range && other_range
+ if my_range.intersects?(other_range)
+ return true
+ end
+
+ if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max)
+ other_range = other_ranges.shift
+ else
+ my_range = my_ranges.shift
+ end
+ end
+ end
+ alias_method :allows_any?, :intersects?
+
+ def allows_all?(other)
+ my_ranges = ranges.dup
+
+ my_range = my_ranges.shift
+
+ other.ranges.all? do |other_range|
+ while my_range
+ break if my_range.allows_all?(other_range)
+ my_range = my_ranges.shift
+ end
+
+ !!my_range
+ end
+ end
+
+ def empty?
+ false
+ end
+
+ def any?
+ false
+ end
+
+ def intersect(other)
+ my_ranges = ranges.dup
+ other_ranges = other.ranges.dup
+ new_ranges = []
+
+ my_range = my_ranges.shift
+ other_range = other_ranges.shift
+ while my_range && other_range
+ new_ranges << my_range.intersect(other_range)
+
+ if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max)
+ other_range = other_ranges.shift
+ else
+ my_range = my_ranges.shift
+ end
+ end
+ new_ranges.reject!(&:empty?)
+ VersionUnion.union(new_ranges, normalize: false)
+ end
+
+ def upper_invert
+ ranges.last.upper_invert
+ end
+
+ def invert
+ ranges.map(&:invert).inject(:intersect)
+ end
+
+ def union(other)
+ VersionUnion.union([self, other])
+ end
+
+ def to_s
+ output = []
+
+ ranges = self.ranges.dup
+ while !ranges.empty?
+ ne = []
+ range = ranges.shift
+ while !ranges.empty? && ranges[0].min.to_s == range.max.to_s
+ ne << range.max
+ range = range.span(ranges.shift)
+ end
+
+ ne.map! {|x| "!= #{x}" }
+ if ne.empty?
+ output << range.to_s
+ elsif range.any?
+ output << ne.join(', ')
+ else
+ output << "#{range}, #{ne.join(', ')}"
+ end
+ end
+
+ output.join(" OR ")
+ end
+
+ def inspect
+ "#<#{self.class} #{to_s}>"
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ self.ranges == other.ranges
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/resolv/lib/resolv.rb b/lib/rubygems/vendor/resolv/lib/resolv.rb
new file mode 100644
index 0000000000..4f48e0642b
--- /dev/null
+++ b/lib/rubygems/vendor/resolv/lib/resolv.rb
@@ -0,0 +1,3499 @@
+# frozen_string_literal: true
+
+require 'socket'
+require_relative '../../../vendored_timeout'
+require 'io/wait'
+require_relative '../../../vendored_securerandom'
+require 'rbconfig'
+
+# Gem::Resolv is a thread-aware DNS resolver library written in Ruby. Gem::Resolv can
+# handle multiple DNS requests concurrently without blocking the entire Ruby
+# interpreter.
+#
+# See also resolv-replace.rb to replace the libc resolver with Gem::Resolv.
+#
+# Gem::Resolv can look up various DNS resources using the DNS module directly.
+#
+# Examples:
+#
+# p Gem::Resolv.getaddress "www.ruby-lang.org"
+# p Gem::Resolv.getname "210.251.121.214"
+#
+# Gem::Resolv::DNS.open do |dns|
+# ress = dns.getresources "www.ruby-lang.org", Gem::Resolv::DNS::Resource::IN::A
+# p ress.map(&:address)
+# ress = dns.getresources "ruby-lang.org", Gem::Resolv::DNS::Resource::IN::MX
+# p ress.map { |r| [r.exchange.to_s, r.preference] }
+# end
+#
+#
+# == Bugs
+#
+# * NIS is not supported.
+# * /etc/nsswitch.conf is not supported.
+
+class Gem::Resolv
+
+ # The version string
+ VERSION = "0.7.0"
+
+ ##
+ # Looks up the first IP address for +name+.
+
+ def self.getaddress(name)
+ DefaultResolver.getaddress(name)
+ end
+
+ ##
+ # Looks up all IP address for +name+.
+
+ def self.getaddresses(name)
+ DefaultResolver.getaddresses(name)
+ end
+
+ ##
+ # Iterates over all IP addresses for +name+.
+
+ def self.each_address(name, &block)
+ DefaultResolver.each_address(name, &block)
+ end
+
+ ##
+ # Looks up the hostname of +address+.
+
+ def self.getname(address)
+ DefaultResolver.getname(address)
+ end
+
+ ##
+ # Looks up all hostnames for +address+.
+
+ def self.getnames(address)
+ DefaultResolver.getnames(address)
+ end
+
+ ##
+ # Iterates over all hostnames for +address+.
+
+ def self.each_name(address, &proc)
+ DefaultResolver.each_name(address, &proc)
+ end
+
+ ##
+ # Creates a new Gem::Resolv using +resolvers+.
+ #
+ # If +resolvers+ is not given, a hash, or +nil+, uses a Hosts resolver and
+ # and a DNS resolver. If +resolvers+ is a hash, uses the hash as
+ # configuration for the DNS resolver.
+
+ def initialize(resolvers=(arg_not_set = true; nil), use_ipv6: (keyword_not_set = true; nil))
+ if !keyword_not_set && !arg_not_set
+ warn "Support for separate use_ipv6 keyword is deprecated, as it is ignored if an argument is provided. Do not provide a positional argument if using the use_ipv6 keyword argument.", uplevel: 1
+ end
+
+ @resolvers = case resolvers
+ when Hash, nil
+ [Hosts.new, DNS.new(DNS::Config.default_config_hash.merge(resolvers || {}))]
+ else
+ resolvers
+ end
+ end
+
+ ##
+ # Looks up the first IP address for +name+.
+
+ def getaddress(name)
+ each_address(name) {|address| return address}
+ raise ResolvError.new("no address for #{name}")
+ end
+
+ ##
+ # Looks up all IP address for +name+.
+
+ def getaddresses(name)
+ ret = []
+ each_address(name) {|address| ret << address}
+ return ret
+ end
+
+ ##
+ # Iterates over all IP addresses for +name+.
+
+ def each_address(name)
+ if AddressRegex =~ name
+ yield name
+ return
+ end
+ yielded = false
+ @resolvers.each {|r|
+ r.each_address(name) {|address|
+ yield address.to_s
+ yielded = true
+ }
+ return if yielded
+ }
+ end
+
+ ##
+ # Looks up the hostname of +address+.
+
+ def getname(address)
+ each_name(address) {|name| return name}
+ raise ResolvError.new("no name for #{address}")
+ end
+
+ ##
+ # Looks up all hostnames for +address+.
+
+ def getnames(address)
+ ret = []
+ each_name(address) {|name| ret << name}
+ return ret
+ end
+
+ ##
+ # Iterates over all hostnames for +address+.
+
+ def each_name(address)
+ yielded = false
+ @resolvers.each {|r|
+ r.each_name(address) {|name|
+ yield name.to_s
+ yielded = true
+ }
+ return if yielded
+ }
+ end
+
+ ##
+ # Indicates a failure to resolve a name or address.
+
+ class ResolvError < StandardError; end
+
+ ##
+ # Indicates a timeout resolving a name or address.
+
+ class ResolvTimeout < Gem::Timeout::Error; end
+
+ ##
+ # Gem::Resolv::Hosts is a hostname resolver that uses the system hosts file.
+
+ class Hosts
+ if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM || ::RbConfig::CONFIG['host_os'] =~ /mswin/
+ begin
+ require 'win32/resolv' unless defined?(Win32::Resolv)
+ hosts = Win32::Resolv.get_hosts_path || IO::NULL
+ rescue LoadError
+ end
+ end
+ # The default file name for host names
+ DefaultFileName = hosts || '/etc/hosts'
+
+ ##
+ # Creates a new Gem::Resolv::Hosts, using +filename+ for its data source.
+
+ def initialize(filename = DefaultFileName)
+ @filename = filename
+ @mutex = Thread::Mutex.new
+ @initialized = nil
+ end
+
+ def lazy_initialize # :nodoc:
+ @mutex.synchronize {
+ unless @initialized
+ @name2addr = {}
+ @addr2name = {}
+ File.open(@filename, 'rb') {|f|
+ f.each {|line|
+ line.sub!(/#.*/, '')
+ addr, *hostnames = line.split(/\s+/)
+ next unless addr
+ (@addr2name[addr] ||= []).concat(hostnames)
+ hostnames.each {|hostname| (@name2addr[hostname] ||= []) << addr}
+ }
+ }
+ @name2addr.each {|name, arr| arr.reverse!}
+ @initialized = true
+ end
+ }
+ self
+ end
+
+ ##
+ # Gets the IP address of +name+ from the hosts file.
+
+ def getaddress(name)
+ each_address(name) {|address| return address}
+ raise ResolvError.new("#{@filename} has no name: #{name}")
+ end
+
+ ##
+ # Gets all IP addresses for +name+ from the hosts file.
+
+ def getaddresses(name)
+ ret = []
+ each_address(name) {|address| ret << address}
+ return ret
+ end
+
+ ##
+ # Iterates over all IP addresses for +name+ retrieved from the hosts file.
+
+ def each_address(name, &proc)
+ lazy_initialize
+ @name2addr[name]&.each(&proc)
+ end
+
+ ##
+ # Gets the hostname of +address+ from the hosts file.
+
+ def getname(address)
+ each_name(address) {|name| return name}
+ raise ResolvError.new("#{@filename} has no address: #{address}")
+ end
+
+ ##
+ # Gets all hostnames for +address+ from the hosts file.
+
+ def getnames(address)
+ ret = []
+ each_name(address) {|name| ret << name}
+ return ret
+ end
+
+ ##
+ # Iterates over all hostnames for +address+ retrieved from the hosts file.
+
+ def each_name(address, &proc)
+ lazy_initialize
+ @addr2name[address]&.each(&proc)
+ end
+ end
+
+ ##
+ # Gem::Resolv::DNS is a DNS stub resolver.
+ #
+ # Information taken from the following places:
+ #
+ # * STD0013
+ # * RFC 1035
+ # * ftp://ftp.isi.edu/in-notes/iana/assignments/dns-parameters
+ # * etc.
+
+ class DNS
+
+ ##
+ # Default DNS Port
+
+ Port = 53
+
+ ##
+ # Default DNS UDP packet size
+
+ UDPSize = 512
+
+ ##
+ # Creates a new DNS resolver. See Gem::Resolv::DNS.new for argument details.
+ #
+ # Yields the created DNS resolver to the block, if given, otherwise
+ # returns it.
+
+ def self.open(*args)
+ dns = new(*args)
+ return dns unless block_given?
+ begin
+ yield dns
+ ensure
+ dns.close
+ end
+ end
+
+ ##
+ # Creates a new DNS resolver.
+ #
+ # +config_info+ can be:
+ #
+ # nil:: Uses /etc/resolv.conf.
+ # String:: Path to a file using /etc/resolv.conf's format.
+ # Hash:: Must contain :nameserver, :search and :ndots keys.
+ # :nameserver_port can be used to specify port number of nameserver address.
+ # :raise_timeout_errors can be used to raise timeout errors
+ # as exceptions instead of treating the same as an NXDOMAIN response.
+ #
+ # The value of :nameserver should be an address string or
+ # an array of address strings.
+ # - :nameserver => '8.8.8.8'
+ # - :nameserver => ['8.8.8.8', '8.8.4.4']
+ #
+ # The value of :nameserver_port should be an array of
+ # pair of nameserver address and port number.
+ # - :nameserver_port => [['8.8.8.8', 53], ['8.8.4.4', 53]]
+ #
+ # Example:
+ #
+ # Gem::Resolv::DNS.new(:nameserver => ['210.251.121.21'],
+ # :search => ['ruby-lang.org'],
+ # :ndots => 1)
+
+ def initialize(config_info=nil)
+ @mutex = Thread::Mutex.new
+ @config = Config.new(config_info)
+ @initialized = nil
+ end
+
+ # Sets the resolver timeouts. This may be a single positive number
+ # or an array of positive numbers representing timeouts in seconds.
+ # If an array is specified, a DNS request will retry and wait for
+ # each successive interval in the array until a successful response
+ # is received. Specifying +nil+ reverts to the default timeouts:
+ # [ 5, second = 5 * 2 / nameserver_count, 2 * second, 4 * second ]
+ #
+ # Example:
+ #
+ # dns.timeouts = 3
+ #
+ def timeouts=(values)
+ @config.timeouts = values
+ end
+
+ def lazy_initialize # :nodoc:
+ @mutex.synchronize {
+ unless @initialized
+ @config.lazy_initialize
+ @initialized = true
+ end
+ }
+ self
+ end
+
+ ##
+ # Closes the DNS resolver.
+
+ def close
+ @mutex.synchronize {
+ if @initialized
+ @initialized = false
+ end
+ }
+ end
+
+ ##
+ # Gets the IP address of +name+ from the DNS resolver.
+ #
+ # +name+ can be a Gem::Resolv::DNS::Name or a String. Retrieved address will
+ # be a Gem::Resolv::IPv4 or Gem::Resolv::IPv6
+
+ def getaddress(name)
+ each_address(name) {|address| return address}
+ raise ResolvError.new("DNS result has no information for #{name}")
+ end
+
+ ##
+ # Gets all IP addresses for +name+ from the DNS resolver.
+ #
+ # +name+ can be a Gem::Resolv::DNS::Name or a String. Retrieved addresses will
+ # be a Gem::Resolv::IPv4 or Gem::Resolv::IPv6
+
+ def getaddresses(name)
+ ret = []
+ each_address(name) {|address| ret << address}
+ return ret
+ end
+
+ ##
+ # Iterates over all IP addresses for +name+ retrieved from the DNS
+ # resolver.
+ #
+ # +name+ can be a Gem::Resolv::DNS::Name or a String. Retrieved addresses will
+ # be a Gem::Resolv::IPv4 or Gem::Resolv::IPv6
+
+ def each_address(name)
+ if use_ipv6?
+ each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address}
+ end
+ each_resource(name, Resource::IN::A) {|resource| yield resource.address}
+ end
+
+ def use_ipv6? # :nodoc:
+ @config.lazy_initialize unless @config.instance_variable_get(:@initialized)
+
+ use_ipv6 = @config.use_ipv6?
+ unless use_ipv6.nil?
+ return use_ipv6
+ end
+
+ begin
+ list = Socket.ip_address_list
+ rescue NotImplementedError
+ return true
+ end
+ list.any? {|a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
+ end
+ private :use_ipv6?
+
+ ##
+ # Gets the hostname for +address+ from the DNS resolver.
+ #
+ # +address+ must be a Gem::Resolv::IPv4, Gem::Resolv::IPv6 or a String. Retrieved
+ # name will be a Gem::Resolv::DNS::Name.
+
+ def getname(address)
+ each_name(address) {|name| return name}
+ raise ResolvError.new("DNS result has no information for #{address}")
+ end
+
+ ##
+ # Gets all hostnames for +address+ from the DNS resolver.
+ #
+ # +address+ must be a Gem::Resolv::IPv4, Gem::Resolv::IPv6 or a String. Retrieved
+ # names will be Gem::Resolv::DNS::Name instances.
+
+ def getnames(address)
+ ret = []
+ each_name(address) {|name| ret << name}
+ return ret
+ end
+
+ ##
+ # Iterates over all hostnames for +address+ retrieved from the DNS
+ # resolver.
+ #
+ # +address+ must be a Gem::Resolv::IPv4, Gem::Resolv::IPv6 or a String. Retrieved
+ # names will be Gem::Resolv::DNS::Name instances.
+
+ def each_name(address)
+ case address
+ when Name
+ ptr = address
+ when IPv4, IPv6
+ ptr = address.to_name
+ when IPv4::Regex
+ ptr = IPv4.create(address).to_name
+ when IPv6::Regex
+ ptr = IPv6.create(address).to_name
+ else
+ raise ResolvError.new("cannot interpret as address: #{address}")
+ end
+ each_resource(ptr, Resource::IN::PTR) {|resource| yield resource.name}
+ end
+
+ ##
+ # Look up the +typeclass+ DNS resource of +name+.
+ #
+ # +name+ must be a Gem::Resolv::DNS::Name or a String.
+ #
+ # +typeclass+ should be one of the following:
+ #
+ # * Gem::Resolv::DNS::Resource::IN::A
+ # * Gem::Resolv::DNS::Resource::IN::AAAA
+ # * Gem::Resolv::DNS::Resource::IN::ANY
+ # * Gem::Resolv::DNS::Resource::IN::CNAME
+ # * Gem::Resolv::DNS::Resource::IN::HINFO
+ # * Gem::Resolv::DNS::Resource::IN::MINFO
+ # * Gem::Resolv::DNS::Resource::IN::MX
+ # * Gem::Resolv::DNS::Resource::IN::NS
+ # * Gem::Resolv::DNS::Resource::IN::PTR
+ # * Gem::Resolv::DNS::Resource::IN::SOA
+ # * Gem::Resolv::DNS::Resource::IN::TXT
+ # * Gem::Resolv::DNS::Resource::IN::WKS
+ #
+ # Returned resource is represented as a Gem::Resolv::DNS::Resource instance,
+ # i.e. Gem::Resolv::DNS::Resource::IN::A.
+
+ def getresource(name, typeclass)
+ each_resource(name, typeclass) {|resource| return resource}
+ raise ResolvError.new("DNS result has no information for #{name}")
+ end
+
+ ##
+ # Looks up all +typeclass+ DNS resources for +name+. See #getresource for
+ # argument details.
+
+ def getresources(name, typeclass)
+ ret = []
+ each_resource(name, typeclass) {|resource| ret << resource}
+ return ret
+ end
+
+ ##
+ # Iterates over all +typeclass+ DNS resources for +name+. See
+ # #getresource for argument details.
+
+ def each_resource(name, typeclass, &proc)
+ fetch_resource(name, typeclass) {|reply, reply_name|
+ extract_resources(reply, reply_name, typeclass, &proc)
+ }
+ end
+
+ # :stopdoc:
+
+ def fetch_resource(name, typeclass)
+ lazy_initialize
+ truncated = {}
+ requesters = {}
+ udp_requester = begin
+ make_udp_requester
+ rescue Errno::EACCES
+ # fall back to TCP
+ end
+ senders = {}
+
+ begin
+ @config.resolv(name) do |candidate, tout, nameserver, port|
+ msg = Message.new
+ msg.rd = 1
+ msg.add_question(candidate, typeclass)
+
+ requester = requesters.fetch([nameserver, port]) do
+ if !truncated[candidate] && udp_requester
+ udp_requester
+ else
+ requesters[[nameserver, port]] = make_tcp_requester(nameserver, port)
+ end
+ end
+
+ unless sender = senders[[candidate, requester, nameserver, port]]
+ sender = requester.sender(msg, candidate, nameserver, port)
+ next if !sender
+ senders[[candidate, requester, nameserver, port]] = sender
+ end
+ reply, reply_name = requester.request(sender, tout)
+ case reply.rcode
+ when RCode::NoError
+ if reply.tc == 1 and not Requester::TCP === requester
+ # Retry via TCP:
+ truncated[candidate] = true
+ redo
+ else
+ yield(reply, reply_name)
+ end
+ return
+ when RCode::NXDomain
+ raise Config::NXDomain.new(reply_name.to_s)
+ else
+ raise Config::OtherResolvError.new(reply_name.to_s)
+ end
+ end
+ ensure
+ udp_requester&.close
+ requesters.each_value { |requester| requester&.close }
+ end
+ end
+
+ def make_udp_requester # :nodoc:
+ nameserver_port = @config.nameserver_port
+ if nameserver_port.length == 1
+ Requester::ConnectedUDP.new(*nameserver_port[0])
+ else
+ Requester::UnconnectedUDP.new(*nameserver_port)
+ end
+ end
+
+ def make_tcp_requester(host, port) # :nodoc:
+ return Requester::TCP.new(host, port)
+ rescue Errno::ECONNREFUSED
+ # Treat a refused TCP connection attempt to a nameserver like a timeout,
+ # as Gem::Resolv::DNS::Config#resolv considers ResolvTimeout exceptions as a
+ # hint to try the next nameserver:
+ raise ResolvTimeout
+ end
+
+ def extract_resources(msg, name, typeclass) # :nodoc:
+ if typeclass < Resource::ANY
+ n0 = Name.create(name)
+ msg.each_resource {|n, ttl, data|
+ yield data if n0 == n
+ }
+ end
+ yielded = false
+ n0 = Name.create(name)
+ msg.each_resource {|n, ttl, data|
+ if n0 == n
+ case data
+ when typeclass
+ yield data
+ yielded = true
+ when Resource::CNAME
+ n0 = data.name
+ end
+ end
+ }
+ return if yielded
+ msg.each_resource {|n, ttl, data|
+ if n0 == n
+ case data
+ when typeclass
+ yield data
+ end
+ end
+ }
+ end
+
+ def self.random(arg) # :nodoc:
+ begin
+ Gem::SecureRandom.random_number(arg)
+ rescue NotImplementedError
+ rand(arg)
+ end
+ end
+
+ RequestID = {} # :nodoc:
+ RequestIDMutex = Thread::Mutex.new # :nodoc:
+
+ def self.allocate_request_id(host, port) # :nodoc:
+ id = nil
+ RequestIDMutex.synchronize {
+ h = (RequestID[[host, port]] ||= {})
+ begin
+ id = random(0x0000..0xffff)
+ end while h[id]
+ h[id] = true
+ }
+ id
+ end
+
+ def self.free_request_id(host, port, id) # :nodoc:
+ RequestIDMutex.synchronize {
+ key = [host, port]
+ if h = RequestID[key]
+ h.delete id
+ if h.empty?
+ RequestID.delete key
+ end
+ end
+ }
+ end
+
+ case RUBY_PLATFORM
+ when *[
+ # https://www.rfc-editor.org/rfc/rfc6056.txt
+ # Appendix A. Survey of the Algorithms in Use by Some Popular Implementations
+ /freebsd/, /linux/, /netbsd/, /openbsd/, /solaris/,
+ /darwin/, # the same as FreeBSD
+ ] then
+ def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc:
+ udpsock.bind(bind_host, 0)
+ end
+ else
+ # Sequential port assignment
+ def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc:
+ # Ephemeral port number range recommended by RFC 6056
+ port = random(1024..65535)
+ udpsock.bind(bind_host, port)
+ rescue Errno::EADDRINUSE, # POSIX
+ Errno::EACCES, # SunOS: See PRIV_SYS_NFS in privileges(5)
+ Errno::EPERM # FreeBSD: security.mac.portacl.port_high is configurable. See mac_portacl(4).
+ retry
+ end
+ end
+
+ class Requester # :nodoc:
+ def initialize
+ @senders = {}
+ @socks = nil
+ end
+
+ def request(sender, tout)
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ timelimit = start + tout
+ begin
+ sender.send
+ rescue Errno::EHOSTUNREACH, # multi-homed IPv6 may generate this
+ Errno::ENETUNREACH
+ raise ResolvTimeout
+ end
+ while true
+ before_select = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ timeout = timelimit - before_select
+ if timeout <= 0
+ raise ResolvTimeout
+ end
+ if @socks.size == 1
+ select_result = @socks[0].wait_readable(timeout) ? [ @socks ] : nil
+ else
+ select_result = IO.select(@socks, nil, nil, timeout)
+ end
+ if !select_result
+ after_select = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ next if after_select < timelimit
+ raise ResolvTimeout
+ end
+ begin
+ reply, from = recv_reply(select_result[0])
+ rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD
+ Errno::ECONNRESET # Windows
+ # No name server running on the server?
+ # Don't wait anymore.
+ raise ResolvTimeout
+ end
+ begin
+ msg = Message.decode(reply)
+ rescue DecodeError
+ next # broken DNS message ignored
+ end
+ if sender == sender_for(from, msg)
+ break
+ else
+ # unexpected DNS message ignored
+ end
+ end
+ return msg, sender.data
+ end
+
+ def sender_for(addr, msg)
+ @senders[[addr,msg.id]]
+ end
+
+ def close
+ socks = @socks
+ @socks = nil
+ socks&.each(&:close)
+ end
+
+ class Sender # :nodoc:
+ def initialize(msg, data, sock)
+ @msg = msg
+ @data = data
+ @sock = sock
+ end
+ end
+
+ class UnconnectedUDP < Requester # :nodoc:
+ def initialize(*nameserver_port)
+ super()
+ @nameserver_port = nameserver_port
+ @initialized = false
+ @mutex = Thread::Mutex.new
+ end
+
+ def lazy_initialize
+ @mutex.synchronize {
+ next if @initialized
+ @initialized = true
+ @socks_hash = {}
+ @socks = []
+ @nameserver_port.each {|host, port|
+ if host.index(':')
+ bind_host = "::"
+ af = Socket::AF_INET6
+ else
+ bind_host = "0.0.0.0"
+ af = Socket::AF_INET
+ end
+ next if @socks_hash[bind_host]
+ begin
+ sock = UDPSocket.new(af)
+ rescue Errno::EAFNOSUPPORT, Errno::EPROTONOSUPPORT
+ next # The kernel doesn't support the address family.
+ end
+ @socks << sock
+ @socks_hash[bind_host] = sock
+ sock.do_not_reverse_lookup = true
+ DNS.bind_random_port(sock, bind_host)
+ }
+ }
+ self
+ end
+
+ def recv_reply(readable_socks)
+ lazy_initialize
+ reply, from = readable_socks[0].recvfrom(UDPSize)
+ return reply, [from[3],from[1]]
+ end
+
+ def sender(msg, data, host, port=Port)
+ host = Addrinfo.ip(host).ip_address
+ lazy_initialize
+ sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"]
+ return nil if !sock
+ service = [host, port]
+ id = DNS.allocate_request_id(host, port)
+ request = msg.encode
+ request[0,2] = [id].pack('n')
+ return @senders[[service, id]] =
+ Sender.new(request, data, sock, host, port)
+ end
+
+ def close
+ @mutex.synchronize {
+ if @initialized
+ super
+ @senders.each_key {|service, id|
+ DNS.free_request_id(service[0], service[1], id)
+ }
+ @initialized = false
+ end
+ }
+ end
+
+ class Sender < Requester::Sender # :nodoc:
+ def initialize(msg, data, sock, host, port)
+ super(msg, data, sock)
+ @host = host
+ @port = port
+ end
+ attr_reader :data
+
+ def send
+ raise "@sock is nil." if @sock.nil?
+ @sock.send(@msg, 0, @host, @port)
+ end
+ end
+ end
+
+ class ConnectedUDP < Requester # :nodoc:
+ def initialize(host, port=Port)
+ super()
+ @host = host
+ @port = port
+ @mutex = Thread::Mutex.new
+ @initialized = false
+ end
+
+ def lazy_initialize
+ @mutex.synchronize {
+ next if @initialized
+ @initialized = true
+ is_ipv6 = @host.index(':')
+ sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET)
+ @socks = [sock]
+ sock.do_not_reverse_lookup = true
+ DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0")
+ sock.connect(@host, @port)
+ }
+ self
+ end
+
+ def recv_reply(readable_socks)
+ lazy_initialize
+ reply = readable_socks[0].recv(UDPSize)
+ return reply, nil
+ end
+
+ def sender(msg, data, host=@host, port=@port)
+ lazy_initialize
+ unless host == @host && port == @port
+ raise RequestError.new("host/port don't match: #{host}:#{port}")
+ end
+ id = DNS.allocate_request_id(@host, @port)
+ request = msg.encode
+ request[0,2] = [id].pack('n')
+ return @senders[[nil,id]] = Sender.new(request, data, @socks[0])
+ end
+
+ def close
+ @mutex.synchronize do
+ if @initialized
+ super
+ @senders.each_key {|from, id|
+ DNS.free_request_id(@host, @port, id)
+ }
+ @initialized = false
+ end
+ end
+ end
+
+ class Sender < Requester::Sender # :nodoc:
+ def send
+ raise "@sock is nil." if @sock.nil?
+ @sock.send(@msg, 0)
+ end
+ attr_reader :data
+ end
+ end
+
+ class MDNSOneShot < UnconnectedUDP # :nodoc:
+ def sender(msg, data, host, port=Port)
+ lazy_initialize
+ id = DNS.allocate_request_id(host, port)
+ request = msg.encode
+ request[0,2] = [id].pack('n')
+ sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"]
+ return @senders[id] =
+ UnconnectedUDP::Sender.new(request, data, sock, host, port)
+ end
+
+ def sender_for(addr, msg)
+ lazy_initialize
+ @senders[msg.id]
+ end
+ end
+
+ class TCP < Requester # :nodoc:
+ def initialize(host, port=Port)
+ super()
+ @host = host
+ @port = port
+ sock = TCPSocket.new(@host, @port)
+ @socks = [sock]
+ @senders = {}
+ end
+
+ def recv_reply(readable_socks)
+ len = readable_socks[0].read(2).unpack('n')[0]
+ reply = @socks[0].read(len)
+ return reply, nil
+ end
+
+ def sender(msg, data, host=@host, port=@port)
+ unless host == @host && port == @port
+ raise RequestError.new("host/port don't match: #{host}:#{port}")
+ end
+ id = DNS.allocate_request_id(@host, @port)
+ request = msg.encode
+ request[0,2] = [request.length, id].pack('nn')
+ return @senders[[nil,id]] = Sender.new(request, data, @socks[0])
+ end
+
+ class Sender < Requester::Sender # :nodoc:
+ def send
+ @sock.print(@msg)
+ @sock.flush
+ end
+ attr_reader :data
+ end
+
+ def close
+ super
+ @senders.each_key {|from,id|
+ DNS.free_request_id(@host, @port, id)
+ }
+ end
+ end
+
+ ##
+ # Indicates a problem with the DNS request.
+
+ class RequestError < StandardError
+ end
+ end
+
+ class Config # :nodoc:
+ def initialize(config_info=nil)
+ @mutex = Thread::Mutex.new
+ @config_info = config_info
+ @initialized = nil
+ @timeouts = nil
+ end
+
+ def timeouts=(values)
+ if values
+ values = Array(values)
+ values.each do |t|
+ Numeric === t or raise ArgumentError, "#{t.inspect} is not numeric"
+ t > 0.0 or raise ArgumentError, "timeout=#{t} must be positive"
+ end
+ @timeouts = values
+ else
+ @timeouts = nil
+ end
+ end
+
+ def Config.parse_resolv_conf(filename)
+ nameserver = []
+ search = nil
+ ndots = 1
+ File.open(filename, 'rb') {|f|
+ f.each {|line|
+ line.sub!(/[#;].*/, '')
+ keyword, *args = line.split(/\s+/)
+ next unless keyword
+ case keyword
+ when 'nameserver'
+ nameserver.concat(args.each(&:freeze))
+ when 'domain'
+ next if args.empty?
+ search = [args[0].freeze]
+ when 'search'
+ next if args.empty?
+ search = args.each(&:freeze)
+ when 'options'
+ args.each {|arg|
+ case arg
+ when /\Andots:(\d+)\z/
+ ndots = $1.to_i
+ end
+ }
+ end
+ }
+ }
+ return { :nameserver => nameserver.freeze, :search => search.freeze, :ndots => ndots.freeze }.freeze
+ end
+
+ def Config.default_config_hash(filename="/etc/resolv.conf")
+ if File.exist? filename
+ Config.parse_resolv_conf(filename)
+ elsif defined?(Win32::Resolv)
+ search, nameserver = Win32::Resolv.get_resolv_info
+ config_hash = {}
+ config_hash[:nameserver] = nameserver if nameserver
+ config_hash[:search] = [search].flatten if search
+ config_hash
+ else
+ {}
+ end
+ end
+
+ def lazy_initialize
+ @mutex.synchronize {
+ unless @initialized
+ @nameserver_port = []
+ @use_ipv6 = nil
+ @search = nil
+ @ndots = 1
+ case @config_info
+ when nil
+ config_hash = Config.default_config_hash
+ when String
+ config_hash = Config.parse_resolv_conf(@config_info)
+ when Hash
+ config_hash = @config_info.dup
+ if String === config_hash[:nameserver]
+ config_hash[:nameserver] = [config_hash[:nameserver]]
+ end
+ if String === config_hash[:search]
+ config_hash[:search] = [config_hash[:search]]
+ end
+ else
+ raise ArgumentError.new("invalid resolv configuration: #{@config_info.inspect}")
+ end
+ if config_hash.include? :nameserver
+ @nameserver_port = config_hash[:nameserver].map {|ns| [ns, Port] }
+ end
+ if config_hash.include? :nameserver_port
+ @nameserver_port = config_hash[:nameserver_port].map {|ns, port| [ns, (port || Port)] }
+ end
+ if config_hash.include? :use_ipv6
+ @use_ipv6 = config_hash[:use_ipv6]
+ end
+ @search = config_hash[:search] if config_hash.include? :search
+ @ndots = config_hash[:ndots] if config_hash.include? :ndots
+ @raise_timeout_errors = config_hash[:raise_timeout_errors]
+
+ if @nameserver_port.empty?
+ @nameserver_port << ['0.0.0.0', Port]
+ end
+ if @search
+ @search = @search.map {|arg| Label.split(arg) }
+ else
+ hostname = Socket.gethostname
+ if /\./ =~ hostname
+ @search = [Label.split($')]
+ else
+ @search = [[]]
+ end
+ end
+
+ if !@nameserver_port.kind_of?(Array) ||
+ @nameserver_port.any? {|ns_port|
+ !(Array === ns_port) ||
+ ns_port.length != 2
+ !(String === ns_port[0]) ||
+ !(Integer === ns_port[1])
+ }
+ raise ArgumentError.new("invalid nameserver config: #{@nameserver_port.inspect}")
+ end
+
+ if !@search.kind_of?(Array) ||
+ !@search.all? {|ls| ls.all? {|l| Label::Str === l } }
+ raise ArgumentError.new("invalid search config: #{@search.inspect}")
+ end
+
+ if !@ndots.kind_of?(Integer)
+ raise ArgumentError.new("invalid ndots config: #{@ndots.inspect}")
+ end
+
+ @initialized = true
+ end
+ }
+ self
+ end
+
+ def single?
+ lazy_initialize
+ if @nameserver_port.length == 1
+ return @nameserver_port[0]
+ else
+ return nil
+ end
+ end
+
+ def nameserver_port
+ @nameserver_port
+ end
+
+ def use_ipv6?
+ @use_ipv6
+ end
+
+ def generate_candidates(name)
+ candidates = nil
+ name = Name.create(name)
+ if name.absolute?
+ candidates = [name]
+ else
+ if @ndots <= name.length - 1
+ candidates = [Name.new(name.to_a)]
+ else
+ candidates = []
+ end
+ candidates.concat(@search.map {|domain| Name.new(name.to_a + domain)})
+ fname = Name.create("#{name}.")
+ if !candidates.include?(fname)
+ candidates << fname
+ end
+ end
+ return candidates
+ end
+
+ InitialTimeout = 5
+
+ def generate_timeouts
+ ts = [InitialTimeout]
+ ts << ts[-1] * 2 / @nameserver_port.length
+ ts << ts[-1] * 2
+ ts << ts[-1] * 2
+ return ts
+ end
+
+ def resolv(name)
+ candidates = generate_candidates(name)
+ timeouts = @timeouts || generate_timeouts
+ timeout_error = false
+ begin
+ candidates.each {|candidate|
+ begin
+ timeouts.each {|tout|
+ @nameserver_port.each {|nameserver, port|
+ begin
+ yield candidate, tout, nameserver, port
+ rescue ResolvTimeout
+ end
+ }
+ }
+ timeout_error = true
+ raise ResolvError.new("DNS resolv timeout: #{name}")
+ rescue NXDomain
+ end
+ }
+ rescue ResolvError
+ raise if @raise_timeout_errors && timeout_error
+ end
+ end
+
+ ##
+ # Indicates no such domain was found.
+
+ class NXDomain < ResolvError
+ end
+
+ ##
+ # Indicates some other unhandled resolver error was encountered.
+
+ class OtherResolvError < ResolvError
+ end
+ end
+
+ module OpCode # :nodoc:
+ Query = 0
+ IQuery = 1
+ Status = 2
+ Notify = 4
+ Update = 5
+ end
+
+ module RCode # :nodoc:
+ NoError = 0
+ FormErr = 1
+ ServFail = 2
+ NXDomain = 3
+ NotImp = 4
+ Refused = 5
+ YXDomain = 6
+ YXRRSet = 7
+ NXRRSet = 8
+ NotAuth = 9
+ NotZone = 10
+ BADVERS = 16
+ BADSIG = 16
+ BADKEY = 17
+ BADTIME = 18
+ BADMODE = 19
+ BADNAME = 20
+ BADALG = 21
+ end
+
+ ##
+ # Indicates that the DNS response was unable to be decoded.
+
+ class DecodeError < StandardError
+ end
+
+ ##
+ # Indicates that the DNS request was unable to be encoded.
+
+ class EncodeError < StandardError
+ end
+
+ module Label # :nodoc:
+ def self.split(arg)
+ labels = []
+ arg.scan(/[^\.]+/) {labels << Str.new($&)}
+ return labels
+ end
+
+ class Str # :nodoc:
+ def initialize(string)
+ @string = string
+ # case insensivity of DNS labels doesn't apply non-ASCII characters. [RFC 4343]
+ # This assumes @string is given in ASCII compatible encoding.
+ @downcase = string.b.downcase
+ end
+ attr_reader :string, :downcase
+
+ def to_s
+ return @string
+ end
+
+ def inspect
+ return "#<#{self.class} #{self}>"
+ end
+
+ def ==(other)
+ return self.class == other.class && @downcase == other.downcase
+ end
+
+ def eql?(other)
+ return self == other
+ end
+
+ def hash
+ return @downcase.hash
+ end
+ end
+ end
+
+ ##
+ # A representation of a DNS name.
+
+ class Name
+
+ ##
+ # Creates a new DNS name from +arg+. +arg+ can be:
+ #
+ # Name:: returns +arg+.
+ # String:: Creates a new Name.
+
+ def self.create(arg)
+ case arg
+ when Name
+ return arg
+ when String
+ return Name.new(Label.split(arg), /\.\z/ =~ arg ? true : false)
+ else
+ raise ArgumentError.new("cannot interpret as DNS name: #{arg.inspect}")
+ end
+ end
+
+ def initialize(labels, absolute=true) # :nodoc:
+ labels = labels.map {|label|
+ case label
+ when String then Label::Str.new(label)
+ when Label::Str then label
+ else
+ raise ArgumentError, "unexpected label: #{label.inspect}"
+ end
+ }
+ @labels = labels
+ @absolute = absolute
+ end
+
+ def inspect # :nodoc:
+ "#<#{self.class}: #{self}#{@absolute ? '.' : ''}>"
+ end
+
+ ##
+ # True if this name is absolute.
+
+ def absolute?
+ return @absolute
+ end
+
+ def ==(other) # :nodoc:
+ return false unless Name === other
+ return false unless @absolute == other.absolute?
+ return @labels == other.to_a
+ end
+
+ alias eql? == # :nodoc:
+
+ ##
+ # Returns true if +other+ is a subdomain.
+ #
+ # Example:
+ #
+ # domain = Gem::Resolv::DNS::Name.create("y.z")
+ # p Gem::Resolv::DNS::Name.create("w.x.y.z").subdomain_of?(domain) #=> true
+ # p Gem::Resolv::DNS::Name.create("x.y.z").subdomain_of?(domain) #=> true
+ # p Gem::Resolv::DNS::Name.create("y.z").subdomain_of?(domain) #=> false
+ # p Gem::Resolv::DNS::Name.create("z").subdomain_of?(domain) #=> false
+ # p Gem::Resolv::DNS::Name.create("x.y.z.").subdomain_of?(domain) #=> false
+ # p Gem::Resolv::DNS::Name.create("w.z").subdomain_of?(domain) #=> false
+ #
+
+ def subdomain_of?(other)
+ raise ArgumentError, "not a domain name: #{other.inspect}" unless Name === other
+ return false if @absolute != other.absolute?
+ other_len = other.length
+ return false if @labels.length <= other_len
+ return @labels[-other_len, other_len] == other.to_a
+ end
+
+ def hash # :nodoc:
+ return @labels.hash ^ @absolute.hash
+ end
+
+ def to_a # :nodoc:
+ return @labels
+ end
+
+ def length # :nodoc:
+ return @labels.length
+ end
+
+ def [](i) # :nodoc:
+ return @labels[i]
+ end
+
+ ##
+ # returns the domain name as a string.
+ #
+ # The domain name doesn't have a trailing dot even if the name object is
+ # absolute.
+ #
+ # Example:
+ #
+ # p Gem::Resolv::DNS::Name.create("x.y.z.").to_s #=> "x.y.z"
+ # p Gem::Resolv::DNS::Name.create("x.y.z").to_s #=> "x.y.z"
+
+ def to_s
+ return @labels.join('.')
+ end
+ end
+
+ class Message # :nodoc:
+ @@identifier = -1
+
+ def initialize(id = (@@identifier += 1) & 0xffff)
+ @id = id
+ @qr = 0
+ @opcode = 0
+ @aa = 0
+ @tc = 0
+ @rd = 0 # recursion desired
+ @ra = 0 # recursion available
+ @rcode = 0
+ @question = []
+ @answer = []
+ @authority = []
+ @additional = []
+ end
+
+ attr_accessor :id, :qr, :opcode, :aa, :tc, :rd, :ra, :rcode
+ attr_reader :question, :answer, :authority, :additional
+
+ def ==(other)
+ return @id == other.id &&
+ @qr == other.qr &&
+ @opcode == other.opcode &&
+ @aa == other.aa &&
+ @tc == other.tc &&
+ @rd == other.rd &&
+ @ra == other.ra &&
+ @rcode == other.rcode &&
+ @question == other.question &&
+ @answer == other.answer &&
+ @authority == other.authority &&
+ @additional == other.additional
+ end
+
+ def add_question(name, typeclass)
+ @question << [Name.create(name), typeclass]
+ end
+
+ def each_question
+ @question.each {|name, typeclass|
+ yield name, typeclass
+ }
+ end
+
+ def add_answer(name, ttl, data)
+ @answer << [Name.create(name), ttl, data]
+ end
+
+ def each_answer
+ @answer.each {|name, ttl, data|
+ yield name, ttl, data
+ }
+ end
+
+ def add_authority(name, ttl, data)
+ @authority << [Name.create(name), ttl, data]
+ end
+
+ def each_authority
+ @authority.each {|name, ttl, data|
+ yield name, ttl, data
+ }
+ end
+
+ def add_additional(name, ttl, data)
+ @additional << [Name.create(name), ttl, data]
+ end
+
+ def each_additional
+ @additional.each {|name, ttl, data|
+ yield name, ttl, data
+ }
+ end
+
+ def each_resource
+ each_answer {|name, ttl, data| yield name, ttl, data}
+ each_authority {|name, ttl, data| yield name, ttl, data}
+ each_additional {|name, ttl, data| yield name, ttl, data}
+ end
+
+ def encode
+ return MessageEncoder.new {|msg|
+ msg.put_pack('nnnnnn',
+ @id,
+ (@qr & 1) << 15 |
+ (@opcode & 15) << 11 |
+ (@aa & 1) << 10 |
+ (@tc & 1) << 9 |
+ (@rd & 1) << 8 |
+ (@ra & 1) << 7 |
+ (@rcode & 15),
+ @question.length,
+ @answer.length,
+ @authority.length,
+ @additional.length)
+ @question.each {|q|
+ name, typeclass = q
+ msg.put_name(name)
+ msg.put_pack('nn', typeclass::TypeValue, typeclass::ClassValue)
+ }
+ [@answer, @authority, @additional].each {|rr|
+ rr.each {|r|
+ name, ttl, data = r
+ msg.put_name(name)
+ msg.put_pack('nnN', data.class::TypeValue, data.class::ClassValue, ttl)
+ msg.put_length16 {data.encode_rdata(msg)}
+ }
+ }
+ }.to_s
+ end
+
+ class MessageEncoder # :nodoc:
+ def initialize
+ @data = ''.dup
+ @names = {}
+ yield self
+ end
+
+ def to_s
+ return @data
+ end
+
+ def put_bytes(d)
+ @data << d
+ end
+
+ def put_pack(template, *d)
+ @data << d.pack(template)
+ end
+
+ def put_length16
+ length_index = @data.length
+ @data << "\0\0"
+ data_start = @data.length
+ yield
+ data_end = @data.length
+ @data[length_index, 2] = [data_end - data_start].pack("n")
+ end
+
+ def put_string(d)
+ self.put_pack("C", d.length)
+ @data << d
+ end
+
+ def put_string_list(ds)
+ ds.each {|d|
+ self.put_string(d)
+ }
+ end
+
+ def put_name(d, compress: true)
+ put_labels(d.to_a, compress: compress)
+ end
+
+ def put_labels(d, compress: true)
+ d.each_index {|i|
+ domain = d[i..-1]
+ if compress && idx = @names[domain]
+ self.put_pack("n", 0xc000 | idx)
+ return
+ else
+ if @data.length < 0x4000
+ @names[domain] = @data.length
+ end
+ self.put_label(d[i])
+ end
+ }
+ @data << "\0"
+ end
+
+ def put_label(d)
+ self.put_string(d.to_s)
+ end
+ end
+
+ def Message.decode(m)
+ o = Message.new(0)
+ MessageDecoder.new(m) {|msg|
+ id, flag, qdcount, ancount, nscount, arcount =
+ msg.get_unpack('nnnnnn')
+ o.id = id
+ o.tc = (flag >> 9) & 1
+ o.rcode = flag & 15
+ return o unless o.tc.zero?
+
+ o.qr = (flag >> 15) & 1
+ o.opcode = (flag >> 11) & 15
+ o.aa = (flag >> 10) & 1
+ o.rd = (flag >> 8) & 1
+ o.ra = (flag >> 7) & 1
+ (1..qdcount).each {
+ name, typeclass = msg.get_question
+ o.add_question(name, typeclass)
+ }
+ (1..ancount).each {
+ name, ttl, data = msg.get_rr
+ o.add_answer(name, ttl, data)
+ }
+ (1..nscount).each {
+ name, ttl, data = msg.get_rr
+ o.add_authority(name, ttl, data)
+ }
+ (1..arcount).each {
+ name, ttl, data = msg.get_rr
+ o.add_additional(name, ttl, data)
+ }
+ }
+ return o
+ end
+
+ class MessageDecoder # :nodoc:
+ def initialize(data)
+ @data = data
+ @index = 0
+ @limit = data.bytesize
+ yield self
+ end
+
+ def inspect
+ "\#<#{self.class}: #{@data.byteslice(0, @index).inspect} #{@data.byteslice(@index..-1).inspect}>"
+ end
+
+ def get_length16
+ len, = self.get_unpack('n')
+ save_limit = @limit
+ @limit = @index + len
+ d = yield(len)
+ if @index < @limit
+ raise DecodeError.new("junk exists")
+ elsif @limit < @index
+ raise DecodeError.new("limit exceeded")
+ end
+ @limit = save_limit
+ return d
+ end
+
+ def get_bytes(len = @limit - @index)
+ raise DecodeError.new("limit exceeded") if @limit < @index + len
+ d = @data.byteslice(@index, len)
+ @index += len
+ return d
+ end
+
+ def get_unpack(template)
+ len = 0
+ template.each_byte {|byte|
+ byte = "%c" % byte
+ case byte
+ when ?c, ?C
+ len += 1
+ when ?n
+ len += 2
+ when ?N
+ len += 4
+ else
+ raise StandardError.new("unsupported template: '#{byte.chr}' in '#{template}'")
+ end
+ }
+ raise DecodeError.new("limit exceeded") if @limit < @index + len
+ arr = @data.unpack("@#{@index}#{template}")
+ @index += len
+ return arr
+ end
+
+ def get_string
+ raise DecodeError.new("limit exceeded") if @limit <= @index
+ len = @data.getbyte(@index)
+ raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len
+ d = @data.byteslice(@index + 1, len)
+ @index += 1 + len
+ return d
+ end
+
+ def get_string_list
+ strings = []
+ while @index < @limit
+ strings << self.get_string
+ end
+ strings
+ end
+
+ def get_list
+ [].tap do |values|
+ while @index < @limit
+ values << yield
+ end
+ end
+ end
+
+ def get_name
+ return Name.new(self.get_labels)
+ end
+
+ def get_labels
+ prev_index = @index
+ save_index = nil
+ d = []
+ size = -1
+ while true
+ raise DecodeError.new("limit exceeded") if @limit <= @index
+ case @data.getbyte(@index)
+ when 0
+ @index += 1
+ if save_index
+ @index = save_index
+ end
+ return d
+ when 192..255
+ idx = self.get_unpack('n')[0] & 0x3fff
+ if prev_index <= idx
+ raise DecodeError.new("non-backward name pointer")
+ end
+ prev_index = idx
+ if !save_index
+ save_index = @index
+ end
+ @index = idx
+ else
+ l = self.get_label
+ d << l
+ size += 1 + l.string.bytesize
+ raise DecodeError.new("name label data exceed 255 octets") if size > 255
+ end
+ end
+ end
+
+ def get_label
+ return Label::Str.new(self.get_string)
+ end
+
+ def get_question
+ name = self.get_name
+ type, klass = self.get_unpack("nn")
+ return name, Resource.get_class(type, klass)
+ end
+
+ def get_rr
+ name = self.get_name
+ type, klass, ttl = self.get_unpack('nnN')
+ typeclass = Resource.get_class(type, klass)
+ res = self.get_length16 do
+ begin
+ typeclass.decode_rdata self
+ rescue => e
+ raise DecodeError, e.message, e.backtrace
+ end
+ end
+ res.instance_variable_set :@ttl, ttl
+ return name, ttl, res
+ end
+ end
+ end
+
+ ##
+ # SvcParams for service binding RRs. [RFC9460]
+
+ class SvcParams
+ include Enumerable
+
+ ##
+ # Create a list of SvcParams with the given initial content.
+ #
+ # +params+ has to be an enumerable of +SvcParam+s.
+ # If its content has +SvcParam+s with the duplicate key,
+ # the one appears last takes precedence.
+
+ def initialize(params = [])
+ @params = {}
+
+ params.each do |param|
+ add param
+ end
+ end
+
+ ##
+ # Get SvcParam for the given +key+ in this list.
+
+ def [](key)
+ @params[canonical_key(key)]
+ end
+
+ ##
+ # Get the number of SvcParams in this list.
+
+ def count
+ @params.count
+ end
+
+ ##
+ # Get whether this list is empty.
+
+ def empty?
+ @params.empty?
+ end
+
+ ##
+ # Add the SvcParam +param+ to this list, overwriting the existing one with the same key.
+
+ def add(param)
+ @params[param.class.key_number] = param
+ end
+
+ ##
+ # Remove the +SvcParam+ with the given +key+ and return it.
+
+ def delete(key)
+ @params.delete(canonical_key(key))
+ end
+
+ ##
+ # Enumerate the +SvcParam+s in this list.
+
+ def each(&block)
+ return enum_for(:each) unless block
+ @params.each_value(&block)
+ end
+
+ def encode(msg) # :nodoc:
+ @params.keys.sort.each do |key|
+ msg.put_pack('n', key)
+ msg.put_length16 do
+ @params.fetch(key).encode(msg)
+ end
+ end
+ end
+
+ def self.decode(msg) # :nodoc:
+ params = msg.get_list do
+ key, = msg.get_unpack('n')
+ msg.get_length16 do
+ SvcParam::ClassHash[key].decode(msg)
+ end
+ end
+
+ return self.new(params)
+ end
+
+ private
+
+ def canonical_key(key) # :nodoc:
+ case key
+ when Integer
+ key
+ when /\Akey(\d+)\z/
+ Integer($1)
+ when Symbol
+ SvcParam::ClassHash[key].key_number
+ else
+ raise TypeError, 'key must be either String or Symbol'
+ end
+ end
+ end
+
+ ##
+ # Base class for SvcParam. [RFC9460]
+
+ class SvcParam
+
+ ##
+ # Get the presentation name of the SvcParamKey.
+
+ def self.key_name
+ const_get(:KeyName)
+ end
+
+ ##
+ # Get the registered number of the SvcParamKey.
+
+ def self.key_number
+ const_get(:KeyNumber)
+ end
+
+ ClassHash = Hash.new do |h, key| # :nodoc:
+ case key
+ when Integer
+ Generic.create(key)
+ when /\Akey(?<key>\d+)\z/
+ Generic.create(key.to_int)
+ when Symbol
+ raise KeyError, "unknown key #{key}"
+ else
+ raise TypeError, 'key must be either String or Symbol'
+ end
+ end
+
+ ##
+ # Generic SvcParam abstract class.
+
+ class Generic < SvcParam
+
+ ##
+ # SvcParamValue in wire-format byte string.
+
+ attr_reader :value
+
+ ##
+ # Create generic SvcParam
+
+ def initialize(value)
+ @value = value
+ end
+
+ def encode(msg) # :nodoc:
+ msg.put_bytes(@value)
+ end
+
+ def self.decode(msg) # :nodoc:
+ return self.new(msg.get_bytes)
+ end
+
+ def self.create(key_number)
+ c = Class.new(Generic)
+ key_name = :"key#{key_number}"
+ c.const_set(:KeyName, key_name)
+ c.const_set(:KeyNumber, key_number)
+ self.const_set(:"Key#{key_number}", c)
+ ClassHash[key_name] = ClassHash[key_number] = c
+ return c
+ end
+ end
+
+ ##
+ # "mandatory" SvcParam -- Mandatory keys in service binding RR
+
+ class Mandatory < SvcParam
+ KeyName = :mandatory
+ KeyNumber = 0
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Mandatory keys.
+
+ attr_reader :keys
+
+ ##
+ # Initialize "mandatory" ScvParam.
+
+ def initialize(keys)
+ @keys = keys.map(&:to_int)
+ end
+
+ def encode(msg) # :nodoc:
+ @keys.sort.each do |key|
+ msg.put_pack('n', key)
+ end
+ end
+
+ def self.decode(msg) # :nodoc:
+ keys = msg.get_list { msg.get_unpack('n')[0] }
+ return self.new(keys)
+ end
+ end
+
+ ##
+ # "alpn" SvcParam -- Additional supported protocols
+
+ class ALPN < SvcParam
+ KeyName = :alpn
+ KeyNumber = 1
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Supported protocol IDs.
+
+ attr_reader :protocol_ids
+
+ ##
+ # Initialize "alpn" ScvParam.
+
+ def initialize(protocol_ids)
+ @protocol_ids = protocol_ids.map(&:to_str)
+ end
+
+ def encode(msg) # :nodoc:
+ msg.put_string_list(@protocol_ids)
+ end
+
+ def self.decode(msg) # :nodoc:
+ return self.new(msg.get_string_list)
+ end
+ end
+
+ ##
+ # "no-default-alpn" SvcParam -- No support for default protocol
+
+ class NoDefaultALPN < SvcParam
+ KeyName = :'no-default-alpn'
+ KeyNumber = 2
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ def encode(msg) # :nodoc:
+ # no payload
+ end
+
+ def self.decode(msg) # :nodoc:
+ return self.new
+ end
+ end
+
+ ##
+ # "port" SvcParam -- Port for alternative endpoint
+
+ class Port < SvcParam
+ KeyName = :port
+ KeyNumber = 3
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Port number.
+
+ attr_reader :port
+
+ ##
+ # Initialize "port" ScvParam.
+
+ def initialize(port)
+ @port = port.to_int
+ end
+
+ def encode(msg) # :nodoc:
+ msg.put_pack('n', @port)
+ end
+
+ def self.decode(msg) # :nodoc:
+ port, = msg.get_unpack('n')
+ return self.new(port)
+ end
+ end
+
+ ##
+ # "ipv4hint" SvcParam -- IPv4 address hints
+
+ class IPv4Hint < SvcParam
+ KeyName = :ipv4hint
+ KeyNumber = 4
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Set of IPv4 addresses.
+
+ attr_reader :addresses
+
+ ##
+ # Initialize "ipv4hint" ScvParam.
+
+ def initialize(addresses)
+ @addresses = addresses.map {|address| IPv4.create(address) }
+ end
+
+ def encode(msg) # :nodoc:
+ @addresses.each do |address|
+ msg.put_bytes(address.address)
+ end
+ end
+
+ def self.decode(msg) # :nodoc:
+ addresses = msg.get_list { IPv4.new(msg.get_bytes(4)) }
+ return self.new(addresses)
+ end
+ end
+
+ ##
+ # "ipv6hint" SvcParam -- IPv6 address hints
+
+ class IPv6Hint < SvcParam
+ KeyName = :ipv6hint
+ KeyNumber = 6
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Set of IPv6 addresses.
+
+ attr_reader :addresses
+
+ ##
+ # Initialize "ipv6hint" ScvParam.
+
+ def initialize(addresses)
+ @addresses = addresses.map {|address| IPv6.create(address) }
+ end
+
+ def encode(msg) # :nodoc:
+ @addresses.each do |address|
+ msg.put_bytes(address.address)
+ end
+ end
+
+ def self.decode(msg) # :nodoc:
+ addresses = msg.get_list { IPv6.new(msg.get_bytes(16)) }
+ return self.new(addresses)
+ end
+ end
+
+ ##
+ # "dohpath" SvcParam -- DNS over HTTPS path template [RFC9461]
+
+ class DoHPath < SvcParam
+ KeyName = :dohpath
+ KeyNumber = 7
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # URI template for DoH queries.
+
+ attr_reader :template
+
+ ##
+ # Initialize "dohpath" ScvParam.
+
+ def initialize(template)
+ @template = template.encode('utf-8')
+ end
+
+ def encode(msg) # :nodoc:
+ msg.put_bytes(@template)
+ end
+
+ def self.decode(msg) # :nodoc:
+ template = msg.get_bytes.force_encoding('utf-8')
+ return self.new(template)
+ end
+ end
+ end
+
+ ##
+ # A DNS query abstract class.
+
+ class Query
+ def encode_rdata(msg) # :nodoc:
+ raise EncodeError.new("#{self.class} is query.")
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ raise DecodeError.new("#{self.class} is query.")
+ end
+ end
+
+ ##
+ # A DNS resource abstract class.
+
+ class Resource < Query
+
+ ##
+ # Remaining Time To Live for this Resource.
+
+ attr_reader :ttl
+
+ ClassHash = Module.new do
+ module_function
+
+ def []=(type_class_value, klass)
+ type_value, class_value = type_class_value
+ Resource.const_set(:"Type#{type_value}_Class#{class_value}", klass)
+ end
+ end
+
+ def encode_rdata(msg) # :nodoc:
+ raise NotImplementedError.new
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ raise NotImplementedError.new
+ end
+
+ def ==(other) # :nodoc:
+ return false unless self.class == other.class
+ s_ivars = self.instance_variables
+ s_ivars.sort!
+ s_ivars.delete :@ttl
+ o_ivars = other.instance_variables
+ o_ivars.sort!
+ o_ivars.delete :@ttl
+ return s_ivars == o_ivars &&
+ s_ivars.collect {|name| self.instance_variable_get name} ==
+ o_ivars.collect {|name| other.instance_variable_get name}
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ h = 0
+ vars = self.instance_variables
+ vars.delete :@ttl
+ vars.each {|name|
+ h ^= self.instance_variable_get(name).hash
+ }
+ return h
+ end
+
+ def self.get_class(type_value, class_value) # :nodoc:
+ cache = :"Type#{type_value}_Class#{class_value}"
+
+ return (const_defined?(cache) && const_get(cache)) ||
+ Generic.create(type_value, class_value)
+ end
+
+ ##
+ # A generic resource abstract class.
+
+ class Generic < Resource
+
+ ##
+ # Creates a new generic resource.
+
+ def initialize(data)
+ @data = data
+ end
+
+ ##
+ # Data for this generic resource.
+
+ attr_reader :data
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_bytes(data)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ return self.new(msg.get_bytes)
+ end
+
+ def self.create(type_value, class_value) # :nodoc:
+ c = Class.new(Generic)
+ c.const_set(:TypeValue, type_value)
+ c.const_set(:ClassValue, class_value)
+ Generic.const_set("Type#{type_value}_Class#{class_value}", c)
+ ClassHash[[type_value, class_value]] = c
+ return c
+ end
+ end
+
+ ##
+ # Domain Name resource abstract class.
+
+ class DomainName < Resource
+
+ ##
+ # Creates a new DomainName from +name+.
+
+ def initialize(name)
+ @name = name
+ end
+
+ ##
+ # The name of this DomainName.
+
+ attr_reader :name
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_name(@name)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ return self.new(msg.get_name)
+ end
+ end
+
+ # Standard (class generic) RRs
+
+ ClassValue = nil # :nodoc:
+
+ ##
+ # An authoritative name server.
+
+ class NS < DomainName
+ TypeValue = 2 # :nodoc:
+ end
+
+ ##
+ # The canonical name for an alias.
+
+ class CNAME < DomainName
+ TypeValue = 5 # :nodoc:
+ end
+
+ ##
+ # Start Of Authority resource.
+
+ class SOA < Resource
+
+ TypeValue = 6 # :nodoc:
+
+ ##
+ # Creates a new SOA record. See the attr documentation for the
+ # details of each argument.
+
+ def initialize(mname, rname, serial, refresh, retry_, expire, minimum)
+ @mname = mname
+ @rname = rname
+ @serial = serial
+ @refresh = refresh
+ @retry = retry_
+ @expire = expire
+ @minimum = minimum
+ end
+
+ ##
+ # Name of the host where the master zone file for this zone resides.
+
+ attr_reader :mname
+
+ ##
+ # The person responsible for this domain name.
+
+ attr_reader :rname
+
+ ##
+ # The version number of the zone file.
+
+ attr_reader :serial
+
+ ##
+ # How often, in seconds, a secondary name server is to check for
+ # updates from the primary name server.
+
+ attr_reader :refresh
+
+ ##
+ # How often, in seconds, a secondary name server is to retry after a
+ # failure to check for a refresh.
+
+ attr_reader :retry
+
+ ##
+ # Time in seconds that a secondary name server is to use the data
+ # before refreshing from the primary name server.
+
+ attr_reader :expire
+
+ ##
+ # The minimum number of seconds to be used for TTL values in RRs.
+
+ attr_reader :minimum
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_name(@mname)
+ msg.put_name(@rname)
+ msg.put_pack('NNNNN', @serial, @refresh, @retry, @expire, @minimum)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ mname = msg.get_name
+ rname = msg.get_name
+ serial, refresh, retry_, expire, minimum = msg.get_unpack('NNNNN')
+ return self.new(
+ mname, rname, serial, refresh, retry_, expire, minimum)
+ end
+ end
+
+ ##
+ # A Pointer to another DNS name.
+
+ class PTR < DomainName
+ TypeValue = 12 # :nodoc:
+ end
+
+ ##
+ # Host Information resource.
+
+ class HINFO < Resource
+
+ TypeValue = 13 # :nodoc:
+
+ ##
+ # Creates a new HINFO running +os+ on +cpu+.
+
+ def initialize(cpu, os)
+ @cpu = cpu
+ @os = os
+ end
+
+ ##
+ # CPU architecture for this resource.
+
+ attr_reader :cpu
+
+ ##
+ # Operating system for this resource.
+
+ attr_reader :os
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_string(@cpu)
+ msg.put_string(@os)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ cpu = msg.get_string
+ os = msg.get_string
+ return self.new(cpu, os)
+ end
+ end
+
+ ##
+ # Mailing list or mailbox information.
+
+ class MINFO < Resource
+
+ TypeValue = 14 # :nodoc:
+
+ def initialize(rmailbx, emailbx)
+ @rmailbx = rmailbx
+ @emailbx = emailbx
+ end
+
+ ##
+ # Domain name responsible for this mail list or mailbox.
+
+ attr_reader :rmailbx
+
+ ##
+ # Mailbox to use for error messages related to the mail list or mailbox.
+
+ attr_reader :emailbx
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_name(@rmailbx)
+ msg.put_name(@emailbx)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ rmailbx = msg.get_string
+ emailbx = msg.get_string
+ return self.new(rmailbx, emailbx)
+ end
+ end
+
+ ##
+ # Mail Exchanger resource.
+
+ class MX < Resource
+
+ TypeValue= 15 # :nodoc:
+
+ ##
+ # Creates a new MX record with +preference+, accepting mail at
+ # +exchange+.
+
+ def initialize(preference, exchange)
+ @preference = preference
+ @exchange = exchange
+ end
+
+ ##
+ # The preference for this MX.
+
+ attr_reader :preference
+
+ ##
+ # The host of this MX.
+
+ attr_reader :exchange
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_pack('n', @preference)
+ msg.put_name(@exchange)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ preference, = msg.get_unpack('n')
+ exchange = msg.get_name
+ return self.new(preference, exchange)
+ end
+ end
+
+ ##
+ # Unstructured text resource.
+
+ class TXT < Resource
+
+ TypeValue = 16 # :nodoc:
+
+ def initialize(first_string, *rest_strings)
+ @strings = [first_string, *rest_strings]
+ end
+
+ ##
+ # Returns an Array of Strings for this TXT record.
+
+ attr_reader :strings
+
+ ##
+ # Returns the concatenated string from +strings+.
+
+ def data
+ @strings.join("")
+ end
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_string_list(@strings)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ strings = msg.get_string_list
+ return self.new(*strings)
+ end
+ end
+
+ ##
+ # Location resource
+
+ class LOC < Resource
+
+ TypeValue = 29 # :nodoc:
+
+ def initialize(version, ssize, hprecision, vprecision, latitude, longitude, altitude)
+ @version = version
+ @ssize = Gem::Resolv::LOC::Size.create(ssize)
+ @hprecision = Gem::Resolv::LOC::Size.create(hprecision)
+ @vprecision = Gem::Resolv::LOC::Size.create(vprecision)
+ @latitude = Gem::Resolv::LOC::Coord.create(latitude)
+ @longitude = Gem::Resolv::LOC::Coord.create(longitude)
+ @altitude = Gem::Resolv::LOC::Alt.create(altitude)
+ end
+
+ ##
+ # Returns the version value for this LOC record which should always be 00
+
+ attr_reader :version
+
+ ##
+ # The spherical size of this LOC
+ # in meters using scientific notation as 2 integers of XeY
+
+ attr_reader :ssize
+
+ ##
+ # The horizontal precision using ssize type values
+ # in meters using scientific notation as 2 integers of XeY
+ # for precision use value/2 e.g. 2m = +/-1m
+
+ attr_reader :hprecision
+
+ ##
+ # The vertical precision using ssize type values
+ # in meters using scientific notation as 2 integers of XeY
+ # for precision use value/2 e.g. 2m = +/-1m
+
+ attr_reader :vprecision
+
+ ##
+ # The latitude for this LOC where 2**31 is the equator
+ # in thousandths of an arc second as an unsigned 32bit integer
+
+ attr_reader :latitude
+
+ ##
+ # The longitude for this LOC where 2**31 is the prime meridian
+ # in thousandths of an arc second as an unsigned 32bit integer
+
+ attr_reader :longitude
+
+ ##
+ # The altitude of the LOC above a reference sphere whose surface sits 100km below the WGS84 spheroid
+ # in centimeters as an unsigned 32bit integer
+
+ attr_reader :altitude
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_bytes(@version)
+ msg.put_bytes(@ssize.scalar)
+ msg.put_bytes(@hprecision.scalar)
+ msg.put_bytes(@vprecision.scalar)
+ msg.put_bytes(@latitude.coordinates)
+ msg.put_bytes(@longitude.coordinates)
+ msg.put_bytes(@altitude.altitude)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ version = msg.get_bytes(1)
+ ssize = msg.get_bytes(1)
+ hprecision = msg.get_bytes(1)
+ vprecision = msg.get_bytes(1)
+ latitude = msg.get_bytes(4)
+ longitude = msg.get_bytes(4)
+ altitude = msg.get_bytes(4)
+ return self.new(
+ version,
+ Gem::Resolv::LOC::Size.new(ssize),
+ Gem::Resolv::LOC::Size.new(hprecision),
+ Gem::Resolv::LOC::Size.new(vprecision),
+ Gem::Resolv::LOC::Coord.new(latitude,"lat"),
+ Gem::Resolv::LOC::Coord.new(longitude,"lon"),
+ Gem::Resolv::LOC::Alt.new(altitude)
+ )
+ end
+ end
+
+ ##
+ # A Query type requesting any RR.
+
+ class ANY < Query
+ TypeValue = 255 # :nodoc:
+ end
+
+ ##
+ # CAA resource record defined in RFC 8659
+ #
+ # These records identify certificate authority allowed to issue
+ # certificates for the given domain.
+
+ class CAA < Resource
+ TypeValue = 257
+
+ ##
+ # Creates a new CAA for +flags+, +tag+ and +value+.
+
+ def initialize(flags, tag, value)
+ unless (0..255) === flags
+ raise ArgumentError.new('flags must be an Integer between 0 and 255')
+ end
+ unless (1..15) === tag.bytesize
+ raise ArgumentError.new('length of tag must be between 1 and 15')
+ end
+
+ @flags = flags
+ @tag = tag
+ @value = value
+ end
+
+ ##
+ # Flags for this property:
+ # - Bit 0 : 0 = not critical, 1 = critical
+
+ attr_reader :flags
+
+ ##
+ # Property tag ("issue", "issuewild", "iodef"...).
+
+ attr_reader :tag
+
+ ##
+ # Property value.
+
+ attr_reader :value
+
+ ##
+ # Whether the critical flag is set on this property.
+
+ def critical?
+ flags & 0x80 != 0
+ end
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_pack('C', @flags)
+ msg.put_string(@tag)
+ msg.put_bytes(@value)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ flags, = msg.get_unpack('C')
+ tag = msg.get_string
+ value = msg.get_bytes
+ self.new flags, tag, value
+ end
+ end
+
+ ClassInsensitiveTypes = [ # :nodoc:
+ NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA
+ ]
+
+ ##
+ # module IN contains ARPA Internet specific RRs.
+
+ module IN
+
+ ClassValue = 1 # :nodoc:
+
+ ClassInsensitiveTypes.each {|s|
+ c = Class.new(s)
+ c.const_set(:TypeValue, s::TypeValue)
+ c.const_set(:ClassValue, ClassValue)
+ ClassHash[[s::TypeValue, ClassValue]] = c
+ self.const_set(s.name.sub(/.*::/, ''), c)
+ }
+
+ ##
+ # IPv4 Address resource
+
+ class A < Resource
+ TypeValue = 1
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+
+ ##
+ # Creates a new A for +address+.
+
+ def initialize(address)
+ @address = IPv4.create(address)
+ end
+
+ ##
+ # The Gem::Resolv::IPv4 address for this A.
+
+ attr_reader :address
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_bytes(@address.address)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ return self.new(IPv4.new(msg.get_bytes(4)))
+ end
+ end
+
+ ##
+ # Well Known Service resource.
+
+ class WKS < Resource
+ TypeValue = 11
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+
+ def initialize(address, protocol, bitmap)
+ @address = IPv4.create(address)
+ @protocol = protocol
+ @bitmap = bitmap
+ end
+
+ ##
+ # The host these services run on.
+
+ attr_reader :address
+
+ ##
+ # IP protocol number for these services.
+
+ attr_reader :protocol
+
+ ##
+ # A bit map of enabled services on this host.
+ #
+ # If protocol is 6 (TCP) then the 26th bit corresponds to the SMTP
+ # service (port 25). If this bit is set, then an SMTP server should
+ # be listening on TCP port 25; if zero, SMTP service is not
+ # supported.
+
+ attr_reader :bitmap
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_bytes(@address.address)
+ msg.put_pack("n", @protocol)
+ msg.put_bytes(@bitmap)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ address = IPv4.new(msg.get_bytes(4))
+ protocol, = msg.get_unpack("n")
+ bitmap = msg.get_bytes
+ return self.new(address, protocol, bitmap)
+ end
+ end
+
+ ##
+ # An IPv6 address record.
+
+ class AAAA < Resource
+ TypeValue = 28
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+
+ ##
+ # Creates a new AAAA for +address+.
+
+ def initialize(address)
+ @address = IPv6.create(address)
+ end
+
+ ##
+ # The Gem::Resolv::IPv6 address for this AAAA.
+
+ attr_reader :address
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_bytes(@address.address)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ return self.new(IPv6.new(msg.get_bytes(16)))
+ end
+ end
+
+ ##
+ # SRV resource record defined in RFC 2782
+ #
+ # These records identify the hostname and port that a service is
+ # available at.
+
+ class SRV < Resource
+ TypeValue = 33
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+
+ # Create a SRV resource record.
+ #
+ # See the documentation for #priority, #weight, #port and #target
+ # for +priority+, +weight+, +port and +target+ respectively.
+
+ def initialize(priority, weight, port, target)
+ @priority = priority.to_int
+ @weight = weight.to_int
+ @port = port.to_int
+ @target = Name.create(target)
+ end
+
+ # The priority of this target host.
+ #
+ # A client MUST attempt to contact the target host with the
+ # lowest-numbered priority it can reach; target hosts with the same
+ # priority SHOULD be tried in an order defined by the weight field.
+ # The range is 0-65535. Note that it is not widely implemented and
+ # should be set to zero.
+
+ attr_reader :priority
+
+ # A server selection mechanism.
+ #
+ # The weight field specifies a relative weight for entries with the
+ # same priority. Larger weights SHOULD be given a proportionately
+ # higher probability of being selected. The range of this number is
+ # 0-65535. Domain administrators SHOULD use Weight 0 when there
+ # isn't any server selection to do, to make the RR easier to read
+ # for humans (less noisy). Note that it is not widely implemented
+ # and should be set to zero.
+
+ attr_reader :weight
+
+ # The port on this target host of this service.
+ #
+ # The range is 0-65535.
+
+ attr_reader :port
+
+ # The domain name of the target host.
+ #
+ # A target of "." means that the service is decidedly not available
+ # at this domain.
+
+ attr_reader :target
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_pack("n", @priority)
+ msg.put_pack("n", @weight)
+ msg.put_pack("n", @port)
+ msg.put_name(@target, compress: false)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ priority, = msg.get_unpack("n")
+ weight, = msg.get_unpack("n")
+ port, = msg.get_unpack("n")
+ target = msg.get_name
+ return self.new(priority, weight, port, target)
+ end
+ end
+
+ ##
+ # Common implementation for SVCB-compatible resource records.
+
+ class ServiceBinding
+
+ ##
+ # Create a service binding resource record.
+
+ def initialize(priority, target, params = [])
+ @priority = priority.to_int
+ @target = Name.create(target)
+ @params = SvcParams.new(params)
+ end
+
+ ##
+ # The priority of this target host.
+ #
+ # The range is 0-65535.
+ # If set to 0, this RR is in AliasMode. Otherwise, it is in ServiceMode.
+
+ attr_reader :priority
+
+ ##
+ # The domain name of the target host.
+
+ attr_reader :target
+
+ ##
+ # The service parameters for the target host.
+
+ attr_reader :params
+
+ ##
+ # Whether this RR is in AliasMode.
+
+ def alias_mode?
+ self.priority == 0
+ end
+
+ ##
+ # Whether this RR is in ServiceMode.
+
+ def service_mode?
+ !alias_mode?
+ end
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_pack("n", @priority)
+ msg.put_name(@target, compress: false)
+ @params.encode(msg)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ priority, = msg.get_unpack("n")
+ target = msg.get_name
+ params = SvcParams.decode(msg)
+ return self.new(priority, target, params)
+ end
+ end
+
+ ##
+ # SVCB resource record [RFC9460]
+
+ class SVCB < ServiceBinding
+ TypeValue = 64
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+ end
+
+ ##
+ # HTTPS resource record [RFC9460]
+
+ class HTTPS < ServiceBinding
+ TypeValue = 65
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+ end
+ end
+ end
+ end
+
+ ##
+ # A Gem::Resolv::DNS IPv4 address.
+
+ class IPv4
+
+ Regex256 = /0
+ |1(?:[0-9][0-9]?)?
+ |2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
+ |[3-9][0-9]?/x # :nodoc:
+
+ ##
+ # Regular expression IPv4 addresses must match.
+ Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/
+
+ ##
+ # Creates a new IPv4 address from +arg+ which may be:
+ #
+ # IPv4:: returns +arg+.
+ # String:: +arg+ must match the IPv4::Regex constant
+
+ def self.create(arg)
+ case arg
+ when IPv4
+ return arg
+ when Regex
+ if (0..255) === (a = $1.to_i) &&
+ (0..255) === (b = $2.to_i) &&
+ (0..255) === (c = $3.to_i) &&
+ (0..255) === (d = $4.to_i)
+ return self.new([a, b, c, d].pack("CCCC"))
+ else
+ raise ArgumentError.new("IPv4 address with invalid value: " + arg)
+ end
+ else
+ raise ArgumentError.new("cannot interpret as IPv4 address: #{arg.inspect}")
+ end
+ end
+
+ def initialize(address) # :nodoc:
+ unless address.kind_of?(String)
+ raise ArgumentError, 'IPv4 address must be a string'
+ end
+ unless address.length == 4
+ raise ArgumentError, "IPv4 address expects 4 bytes but #{address.length} bytes"
+ end
+ @address = address
+ end
+
+ ##
+ # A String representation of this IPv4 address.
+
+ ##
+ # The raw IPv4 address as a String.
+
+ attr_reader :address
+
+ def to_s # :nodoc:
+ return sprintf("%d.%d.%d.%d", *@address.unpack("CCCC"))
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
+ end
+
+ ##
+ # Turns this IPv4 address into a Gem::Resolv::DNS::Name.
+
+ def to_name
+ return DNS::Name.create(
+ '%d.%d.%d.%d.in-addr.arpa.' % @address.unpack('CCCC').reverse)
+ end
+
+ def ==(other) # :nodoc:
+ return @address == other.address
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @address.hash
+ end
+ end
+
+ ##
+ # A Gem::Resolv::DNS IPv6 address.
+
+ class IPv6
+
+ ##
+ # IPv6 address format a:b:c:d:e:f:g:h
+ Regex_8Hex = /\A
+ (?:[0-9A-Fa-f]{1,4}:){7}
+ [0-9A-Fa-f]{1,4}
+ \z/x
+
+ ##
+ # Compressed IPv6 address format a::b
+
+ Regex_CompressedHex = /\A
+ ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
+ ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
+ \z/x
+
+ ##
+ # IPv4 mapped IPv6 address format a:b:c:d:e:f:w.x.y.z
+
+ Regex_6Hex4Dec = /\A
+ ((?:[0-9A-Fa-f]{1,4}:){6,6})
+ (\d+)\.(\d+)\.(\d+)\.(\d+)
+ \z/x
+
+ ##
+ # Compressed IPv4 mapped IPv6 address format a::b:w.x.y.z
+
+ Regex_CompressedHex4Dec = /\A
+ ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
+ ((?:[0-9A-Fa-f]{1,4}:)*)
+ (\d+)\.(\d+)\.(\d+)\.(\d+)
+ \z/x
+
+ ##
+ # IPv6 link local address format fe80:b:c:d:e:f:g:h%em1
+ Regex_8HexLinkLocal = /\A
+ [Ff][Ee]80
+ (?::[0-9A-Fa-f]{1,4}){7}
+ %[-0-9A-Za-z._~]+
+ \z/x
+
+ ##
+ # Compressed IPv6 link local address format fe80::b%em1
+
+ Regex_CompressedHexLinkLocal = /\A
+ [Ff][Ee]80:
+ (?:
+ ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
+ ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
+ |
+ :((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
+ )?
+ :[0-9A-Fa-f]{1,4}%[-0-9A-Za-z._~]+
+ \z/x
+
+ ##
+ # A composite IPv6 address Regexp.
+
+ Regex = /
+ (?:#{Regex_8Hex}) |
+ (?:#{Regex_CompressedHex}) |
+ (?:#{Regex_6Hex4Dec}) |
+ (?:#{Regex_CompressedHex4Dec}) |
+ (?:#{Regex_8HexLinkLocal}) |
+ (?:#{Regex_CompressedHexLinkLocal})
+ /x
+
+ ##
+ # Creates a new IPv6 address from +arg+ which may be:
+ #
+ # IPv6:: returns +arg+.
+ # String:: +arg+ must match one of the IPv6::Regex* constants
+
+ def self.create(arg)
+ case arg
+ when IPv6
+ return arg
+ when String
+ address = ''.b
+ if Regex_8Hex =~ arg
+ arg.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')}
+ elsif Regex_CompressedHex =~ arg
+ prefix = $1
+ suffix = $2
+ a1 = ''.b
+ a2 = ''.b
+ prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')}
+ suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')}
+ omitlen = 16 - a1.length - a2.length
+ address << a1 << "\0" * omitlen << a2
+ elsif Regex_6Hex4Dec =~ arg
+ prefix, a, b, c, d = $1, $2.to_i, $3.to_i, $4.to_i, $5.to_i
+ if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d
+ prefix.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')}
+ address << [a, b, c, d].pack('CCCC')
+ else
+ raise ArgumentError.new("not numeric IPv6 address: " + arg)
+ end
+ elsif Regex_CompressedHex4Dec =~ arg
+ prefix, suffix, a, b, c, d = $1, $2, $3.to_i, $4.to_i, $5.to_i, $6.to_i
+ if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d
+ a1 = ''.b
+ a2 = ''.b
+ prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')}
+ suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')}
+ omitlen = 12 - a1.length - a2.length
+ address << a1 << "\0" * omitlen << a2 << [a, b, c, d].pack('CCCC')
+ else
+ raise ArgumentError.new("not numeric IPv6 address: " + arg)
+ end
+ else
+ raise ArgumentError.new("not numeric IPv6 address: " + arg)
+ end
+ return IPv6.new(address)
+ else
+ raise ArgumentError.new("cannot interpret as IPv6 address: #{arg.inspect}")
+ end
+ end
+
+ def initialize(address) # :nodoc:
+ unless address.kind_of?(String) && address.length == 16
+ raise ArgumentError.new('IPv6 address must be 16 bytes')
+ end
+ @address = address
+ end
+
+ ##
+ # The raw IPv6 address as a String.
+
+ attr_reader :address
+
+ def to_s # :nodoc:
+ sprintf("%x:%x:%x:%x:%x:%x:%x:%x", *@address.unpack("nnnnnnnn")).sub(/(^|:)0(:0)+(:|$)/, '::')
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
+ end
+
+ ##
+ # Turns this IPv6 address into a Gem::Resolv::DNS::Name.
+ #--
+ # ip6.arpa should be searched too. [RFC3152]
+
+ def to_name
+ return DNS::Name.new(
+ @address.unpack("H32")[0].split(//).reverse + ['ip6', 'arpa'])
+ end
+
+ def ==(other) # :nodoc:
+ return @address == other.address
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @address.hash
+ end
+ end
+
+ ##
+ # Gem::Resolv::MDNS is a one-shot Multicast DNS (mDNS) resolver. It blindly
+ # makes queries to the mDNS addresses without understanding anything about
+ # multicast ports.
+ #
+ # Information taken form the following places:
+ #
+ # * RFC 6762
+
+ class MDNS < DNS
+
+ ##
+ # Default mDNS Port
+
+ Port = 5353
+
+ ##
+ # Default IPv4 mDNS address
+
+ AddressV4 = '224.0.0.251'
+
+ ##
+ # Default IPv6 mDNS address
+
+ AddressV6 = 'ff02::fb'
+
+ ##
+ # Default mDNS addresses
+
+ Addresses = [
+ [AddressV4, Port],
+ [AddressV6, Port],
+ ]
+
+ ##
+ # Creates a new one-shot Multicast DNS (mDNS) resolver.
+ #
+ # +config_info+ can be:
+ #
+ # nil::
+ # Uses the default mDNS addresses
+ #
+ # Hash::
+ # Must contain :nameserver or :nameserver_port like
+ # Gem::Resolv::DNS#initialize.
+
+ def initialize(config_info=nil)
+ if config_info then
+ super({ nameserver_port: Addresses }.merge(config_info))
+ else
+ super(nameserver_port: Addresses)
+ end
+ end
+
+ ##
+ # Iterates over all IP addresses for +name+ retrieved from the mDNS
+ # resolver, provided name ends with "local". If the name does not end in
+ # "local" no records will be returned.
+ #
+ # +name+ can be a Gem::Resolv::DNS::Name or a String. Retrieved addresses will
+ # be a Gem::Resolv::IPv4 or Gem::Resolv::IPv6
+
+ def each_address(name)
+ name = Gem::Resolv::DNS::Name.create(name)
+
+ return unless name[-1].to_s == 'local'
+
+ super(name)
+ end
+
+ def make_udp_requester # :nodoc:
+ nameserver_port = @config.nameserver_port
+ Requester::MDNSOneShot.new(*nameserver_port)
+ end
+
+ end
+
+ module LOC # :nodoc:
+
+ ##
+ # A Gem::Resolv::LOC::Size
+
+ class Size
+
+ # Regular expression LOC size must match.
+
+ Regex = /^(\d+\.*\d*)[m]$/
+
+ ##
+ # Creates a new LOC::Size from +arg+ which may be:
+ #
+ # LOC::Size:: returns +arg+.
+ # String:: +arg+ must match the LOC::Size::Regex constant
+
+ def self.create(arg)
+ case arg
+ when Size
+ return arg
+ when String
+ scalar = ''
+ if Regex =~ arg
+ scalar = [(($1.to_f*(1e2)).to_i.to_s[0].to_i*(2**4)+(($1.to_f*(1e2)).to_i.to_s.length-1))].pack("C")
+ else
+ raise ArgumentError.new("not a properly formed Size string: " + arg)
+ end
+ return Size.new(scalar)
+ else
+ raise ArgumentError.new("cannot interpret as Size: #{arg.inspect}")
+ end
+ end
+
+ # Internal use; use self.create.
+ def initialize(scalar)
+ @scalar = scalar
+ end
+
+ ##
+ # The raw size
+
+ attr_reader :scalar
+
+ def to_s # :nodoc:
+ s = @scalar.unpack("H2").join.to_s
+ return ((s[0].to_i)*(10**(s[1].to_i-2))).to_s << "m"
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
+ end
+
+ def ==(other) # :nodoc:
+ return @scalar == other.scalar
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @scalar.hash
+ end
+
+ end
+
+ ##
+ # A Gem::Resolv::LOC::Coord
+
+ class Coord
+
+ # Regular expression LOC Coord must match.
+
+ Regex = /^(\d+)\s(\d+)\s(\d+\.\d+)\s([NESW])$/
+
+ ##
+ # Creates a new LOC::Coord from +arg+ which may be:
+ #
+ # LOC::Coord:: returns +arg+.
+ # String:: +arg+ must match the LOC::Coord::Regex constant
+
+ def self.create(arg)
+ case arg
+ when Coord
+ return arg
+ when String
+ coordinates = ''
+ if Regex =~ arg && $1.to_f < 180
+ m = $~
+ hemi = (m[4][/[NE]/]) || (m[4][/[SW]/]) ? 1 : -1
+ coordinates = [ ((m[1].to_i*(36e5)) + (m[2].to_i*(6e4)) +
+ (m[3].to_f*(1e3))) * hemi+(2**31) ].pack("N")
+ orientation = m[4][/[NS]/] ? 'lat' : 'lon'
+ else
+ raise ArgumentError.new("not a properly formed Coord string: " + arg)
+ end
+ return Coord.new(coordinates,orientation)
+ else
+ raise ArgumentError.new("cannot interpret as Coord: #{arg.inspect}")
+ end
+ end
+
+ # Internal use; use self.create.
+ def initialize(coordinates,orientation)
+ unless coordinates.kind_of?(String)
+ raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}")
+ end
+ unless orientation.kind_of?(String) && orientation[/^lon$|^lat$/]
+ raise ArgumentError.new('Coord expects orientation to be a String argument of "lat" or "lon"')
+ end
+ @coordinates = coordinates
+ @orientation = orientation
+ end
+
+ ##
+ # The raw coordinates
+
+ attr_reader :coordinates
+
+ ## The orientation of the hemisphere as 'lat' or 'lon'
+
+ attr_reader :orientation
+
+ def to_s # :nodoc:
+ c = @coordinates.unpack("N").join.to_i
+ val = (c - (2**31)).abs
+ fracsecs = (val % 1e3).to_i.to_s
+ val = val / 1e3
+ secs = (val % 60).to_i.to_s
+ val = val / 60
+ mins = (val % 60).to_i.to_s
+ degs = (val / 60).to_i.to_s
+ posi = (c >= 2**31)
+ case posi
+ when true
+ hemi = @orientation[/^lat$/] ? "N" : "E"
+ else
+ hemi = @orientation[/^lon$/] ? "W" : "S"
+ end
+ return degs << " " << mins << " " << secs << "." << fracsecs << " " << hemi
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
+ end
+
+ def ==(other) # :nodoc:
+ return @coordinates == other.coordinates
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @coordinates.hash
+ end
+
+ end
+
+ ##
+ # A Gem::Resolv::LOC::Alt
+
+ class Alt
+
+ # Regular expression LOC Alt must match.
+
+ Regex = /^([+-]*\d+\.*\d*)[m]$/
+
+ ##
+ # Creates a new LOC::Alt from +arg+ which may be:
+ #
+ # LOC::Alt:: returns +arg+.
+ # String:: +arg+ must match the LOC::Alt::Regex constant
+
+ def self.create(arg)
+ case arg
+ when Alt
+ return arg
+ when String
+ altitude = ''
+ if Regex =~ arg
+ altitude = [($1.to_f*(1e2))+(1e7)].pack("N")
+ else
+ raise ArgumentError.new("not a properly formed Alt string: " + arg)
+ end
+ return Alt.new(altitude)
+ else
+ raise ArgumentError.new("cannot interpret as Alt: #{arg.inspect}")
+ end
+ end
+
+ # Internal use; use self.create.
+ def initialize(altitude)
+ @altitude = altitude
+ end
+
+ ##
+ # The raw altitude
+
+ attr_reader :altitude
+
+ def to_s # :nodoc:
+ a = @altitude.unpack("N").join.to_i
+ return ((a.to_f/1e2)-1e5).to_s + "m"
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
+ end
+
+ def ==(other) # :nodoc:
+ return @altitude == other.altitude
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @altitude.hash
+ end
+
+ end
+
+ end
+
+ ##
+ # Default resolver to use for Gem::Resolv class methods.
+
+ DefaultResolver = self.new
+
+ ##
+ # Replaces the resolvers in the default resolver with +new_resolvers+. This
+ # allows resolvers to be changed for resolv-replace.
+
+ def DefaultResolver.replace_resolvers new_resolvers
+ @resolvers = new_resolvers
+ end
+
+ ##
+ # Address Regexp to use for matching IP addresses.
+
+ AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/
+
+end
diff --git a/lib/rubygems/vendor/securerandom/lib/securerandom.rb b/lib/rubygems/vendor/securerandom/lib/securerandom.rb
new file mode 100644
index 0000000000..b6f1d71ad3
--- /dev/null
+++ b/lib/rubygems/vendor/securerandom/lib/securerandom.rb
@@ -0,0 +1,102 @@
+# -*- coding: us-ascii -*-
+# frozen_string_literal: true
+
+require 'random/formatter'
+
+# == Secure random number generator interface.
+#
+# This library is an interface to secure random number generators which are
+# suitable for generating session keys in HTTP cookies, etc.
+#
+# You can use this library in your application by requiring it:
+#
+# require 'rubygems/vendor/securerandom/lib/securerandom'
+#
+# It supports the following secure random number generators:
+#
+# * openssl
+# * /dev/urandom
+# * Win32
+#
+# Gem::SecureRandom is extended by the Random::Formatter module which
+# defines the following methods:
+#
+# * alphanumeric
+# * base64
+# * choose
+# * gen_random
+# * hex
+# * rand
+# * random_bytes
+# * random_number
+# * urlsafe_base64
+# * uuid
+#
+# These methods are usable as class methods of Gem::SecureRandom such as
+# +Gem::SecureRandom.hex+.
+#
+# If a secure random number generator is not available,
+# +NotImplementedError+ is raised.
+
+module Gem::SecureRandom
+
+ # The version
+ VERSION = "0.4.1"
+
+ class << self
+ # Returns a random binary string containing +size+ bytes.
+ #
+ # See Random.bytes
+ def bytes(n)
+ return gen_random(n)
+ end
+
+ # Compatibility methods for Ruby 3.2, we can remove this after dropping to support Ruby 3.2
+ def alphanumeric(n = nil, chars: ALPHANUMERIC)
+ n = 16 if n.nil?
+ choose(chars, n)
+ end if RUBY_VERSION < '3.3'
+
+ private
+
+ # :stopdoc:
+
+ # Implementation using OpenSSL
+ def gen_random_openssl(n)
+ return OpenSSL::Random.random_bytes(n)
+ end
+
+ # Implementation using system random device
+ def gen_random_urandom(n)
+ ret = Random.urandom(n)
+ unless ret
+ raise NotImplementedError, "No random device"
+ end
+ unless ret.length == n
+ raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes"
+ end
+ ret
+ end
+
+ begin
+ # Check if Random.urandom is available
+ Random.urandom(1)
+ alias gen_random gen_random_urandom
+ rescue RuntimeError
+ begin
+ require 'openssl'
+ rescue NoMethodError
+ raise NotImplementedError, "No random device"
+ else
+ alias gen_random gen_random_openssl
+ end
+ end
+
+ # :startdoc:
+
+ # Generate random data bytes for Random::Formatter
+ public :gen_random
+ end
+end
+
+Gem::SecureRandom.extend(Random::Formatter)
diff --git a/lib/rubygems/vendor/timeout/lib/timeout.rb b/lib/rubygems/vendor/timeout/lib/timeout.rb
new file mode 100644
index 0000000000..376b8c0e2b
--- /dev/null
+++ b/lib/rubygems/vendor/timeout/lib/timeout.rb
@@ -0,0 +1,201 @@
+# frozen_string_literal: true
+# Timeout long-running blocks
+#
+# == Synopsis
+#
+# require 'rubygems/vendor/timeout/lib/timeout'
+# status = Gem::Timeout.timeout(5) {
+# # Something that should be interrupted if it takes more than 5 seconds...
+# }
+#
+# == Description
+#
+# Gem::Timeout provides a way to auto-terminate a potentially long-running
+# operation if it hasn't finished in a fixed amount of time.
+#
+# == Copyright
+#
+# Copyright:: (C) 2000 Network Applied Communication Laboratory, Inc.
+# Copyright:: (C) 2000 Information-technology Promotion Agency, Japan
+
+module Gem::Timeout
+ # The version
+ VERSION = "0.4.4"
+
+ # Internal error raised to when a timeout is triggered.
+ class ExitException < Exception
+ def exception(*) # :nodoc:
+ self
+ end
+ end
+
+ # Raised by Gem::Timeout.timeout when the block times out.
+ class Error < RuntimeError
+ def self.handle_timeout(message) # :nodoc:
+ exc = ExitException.new(message)
+
+ begin
+ yield exc
+ rescue ExitException => e
+ raise new(message) if exc.equal?(e)
+ raise
+ end
+ end
+ end
+
+ # :stopdoc:
+ CONDVAR = ConditionVariable.new
+ QUEUE = Queue.new
+ QUEUE_MUTEX = Mutex.new
+ TIMEOUT_THREAD_MUTEX = Mutex.new
+ @timeout_thread = nil
+ private_constant :CONDVAR, :QUEUE, :QUEUE_MUTEX, :TIMEOUT_THREAD_MUTEX
+
+ class Request
+ attr_reader :deadline
+
+ def initialize(thread, timeout, exception_class, message)
+ @thread = thread
+ @deadline = GET_TIME.call(Process::CLOCK_MONOTONIC) + timeout
+ @exception_class = exception_class
+ @message = message
+
+ @mutex = Mutex.new
+ @done = false # protected by @mutex
+ end
+
+ def done?
+ @mutex.synchronize do
+ @done
+ end
+ end
+
+ def expired?(now)
+ now >= @deadline
+ end
+
+ def interrupt
+ @mutex.synchronize do
+ unless @done
+ @thread.raise @exception_class, @message
+ @done = true
+ end
+ end
+ end
+
+ def finished
+ @mutex.synchronize do
+ @done = true
+ end
+ end
+ end
+ private_constant :Request
+
+ def self.create_timeout_thread
+ watcher = Thread.new do
+ requests = []
+ while true
+ until QUEUE.empty? and !requests.empty? # wait to have at least one request
+ req = QUEUE.pop
+ requests << req unless req.done?
+ end
+ closest_deadline = requests.min_by(&:deadline).deadline
+
+ now = 0.0
+ QUEUE_MUTEX.synchronize do
+ while (now = GET_TIME.call(Process::CLOCK_MONOTONIC)) < closest_deadline and QUEUE.empty?
+ CONDVAR.wait(QUEUE_MUTEX, closest_deadline - now)
+ end
+ end
+
+ requests.each do |req|
+ req.interrupt if req.expired?(now)
+ end
+ requests.reject!(&:done?)
+ end
+ end
+ ThreadGroup::Default.add(watcher) unless watcher.group.enclosed?
+ watcher.name = "Gem::Timeout stdlib thread"
+ watcher.thread_variable_set(:"\0__detached_thread__", true)
+ watcher
+ end
+ private_class_method :create_timeout_thread
+
+ def self.ensure_timeout_thread_created
+ unless @timeout_thread and @timeout_thread.alive?
+ # If the Mutex is already owned we are in a signal handler.
+ # In that case, just return and let the main thread create the @timeout_thread.
+ return if TIMEOUT_THREAD_MUTEX.owned?
+ TIMEOUT_THREAD_MUTEX.synchronize do
+ unless @timeout_thread and @timeout_thread.alive?
+ @timeout_thread = create_timeout_thread
+ end
+ end
+ end
+ end
+
+ # We keep a private reference so that time mocking libraries won't break
+ # Gem::Timeout.
+ GET_TIME = Process.method(:clock_gettime)
+ private_constant :GET_TIME
+
+ # :startdoc:
+
+ # Perform an operation in a block, raising an error if it takes longer than
+ # +sec+ seconds to complete.
+ #
+ # +sec+:: Number of seconds to wait for the block to terminate. Any non-negative number
+ # or nil may be used, including Floats to specify fractional seconds. A
+ # value of 0 or +nil+ will execute the block without any timeout.
+ # Any negative number will raise an ArgumentError.
+ # +klass+:: Exception Class to raise if the block fails to terminate
+ # in +sec+ seconds. Omitting will use the default, Gem::Timeout::Error
+ # +message+:: Error message to raise with Exception Class.
+ # Omitting will use the default, "execution expired"
+ #
+ # Returns the result of the block *if* the block completed before
+ # +sec+ seconds, otherwise throws an exception, based on the value of +klass+.
+ #
+ # The exception thrown to terminate the given block cannot be rescued inside
+ # the block unless +klass+ is given explicitly. However, the block can use
+ # ensure to prevent the handling of the exception. For that reason, this
+ # method cannot be relied on to enforce timeouts for untrusted blocks.
+ #
+ # If a scheduler is defined, it will be used to handle the timeout by invoking
+ # Scheduler#timeout_after.
+ #
+ # Note that this is both a method of module Gem::Timeout, so you can <tt>include
+ # Gem::Timeout</tt> into your classes so they have a #timeout method, as well as
+ # a module method, so you can call it directly as Gem::Timeout.timeout().
+ def timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+
+ return yield(sec) if sec == nil or sec.zero?
+ raise ArgumentError, "Timeout sec must be a non-negative number" if 0 > sec
+
+ message ||= "execution expired"
+
+ if Fiber.respond_to?(:current_scheduler) && (scheduler = Fiber.current_scheduler)&.respond_to?(:timeout_after)
+ return scheduler.timeout_after(sec, klass || Error, message, &block)
+ end
+
+ Gem::Timeout.ensure_timeout_thread_created
+ perform = Proc.new do |exc|
+ request = Request.new(Thread.current, sec, exc, message)
+ QUEUE_MUTEX.synchronize do
+ QUEUE << request
+ CONDVAR.signal
+ end
+ begin
+ return yield(sec)
+ ensure
+ request.finished
+ end
+ end
+
+ if klass
+ perform.call(klass)
+ else
+ Error.handle_timeout(message, &perform)
+ end
+ end
+ module_function :timeout
+end
diff --git a/lib/rubygems/vendor/tsort/lib/tsort.rb b/lib/rubygems/vendor/tsort/lib/tsort.rb
new file mode 100644
index 0000000000..9dd7c09521
--- /dev/null
+++ b/lib/rubygems/vendor/tsort/lib/tsort.rb
@@ -0,0 +1,455 @@
+# frozen_string_literal: true
+
+#--
+# tsort.rb - provides a module for topological sorting and strongly connected components.
+#++
+#
+
+#
+# Gem::TSort implements topological sorting using Tarjan's algorithm for
+# strongly connected components.
+#
+# Gem::TSort is designed to be able to be used with any object which can be
+# interpreted as a directed graph.
+#
+# Gem::TSort requires two methods to interpret an object as a graph,
+# tsort_each_node and tsort_each_child.
+#
+# * tsort_each_node is used to iterate for all nodes over a graph.
+# * tsort_each_child is used to iterate for child nodes of a given node.
+#
+# The equality of nodes are defined by eql? and hash since
+# Gem::TSort uses Hash internally.
+#
+# == A Simple Example
+#
+# The following example demonstrates how to mix the Gem::TSort module into an
+# existing class (in this case, Hash). Here, we're treating each key in
+# the hash as a node in the graph, and so we simply alias the required
+# #tsort_each_node method to Hash's #each_key method. For each key in the
+# hash, the associated value is an array of the node's child nodes. This
+# choice in turn leads to our implementation of the required #tsort_each_child
+# method, which fetches the array of child nodes and then iterates over that
+# array using the user-supplied block.
+#
+# require 'rubygems/vendor/tsort/lib/tsort'
+#
+# class Hash
+# include Gem::TSort
+# alias tsort_each_node each_key
+# def tsort_each_child(node, &block)
+# fetch(node).each(&block)
+# end
+# end
+#
+# {1=>[2, 3], 2=>[3], 3=>[], 4=>[]}.tsort
+# #=> [3, 2, 1, 4]
+#
+# {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}.strongly_connected_components
+# #=> [[4], [2, 3], [1]]
+#
+# == A More Realistic Example
+#
+# A very simple `make' like tool can be implemented as follows:
+#
+# require 'rubygems/vendor/tsort/lib/tsort'
+#
+# class Make
+# def initialize
+# @dep = {}
+# @dep.default = []
+# end
+#
+# def rule(outputs, inputs=[], &block)
+# triple = [outputs, inputs, block]
+# outputs.each {|f| @dep[f] = [triple]}
+# @dep[triple] = inputs
+# end
+#
+# def build(target)
+# each_strongly_connected_component_from(target) {|ns|
+# if ns.length != 1
+# fs = ns.delete_if {|n| Array === n}
+# raise Gem::TSort::Cyclic.new("cyclic dependencies: #{fs.join ', '}")
+# end
+# n = ns.first
+# if Array === n
+# outputs, inputs, block = n
+# inputs_time = inputs.map {|f| File.mtime f}.max
+# begin
+# outputs_time = outputs.map {|f| File.mtime f}.min
+# rescue Errno::ENOENT
+# outputs_time = nil
+# end
+# if outputs_time == nil ||
+# inputs_time != nil && outputs_time <= inputs_time
+# sleep 1 if inputs_time != nil && inputs_time.to_i == Time.now.to_i
+# block.call
+# end
+# end
+# }
+# end
+#
+# def tsort_each_child(node, &block)
+# @dep[node].each(&block)
+# end
+# include Gem::TSort
+# end
+#
+# def command(arg)
+# print arg, "\n"
+# system arg
+# end
+#
+# m = Make.new
+# m.rule(%w[t1]) { command 'date > t1' }
+# m.rule(%w[t2]) { command 'date > t2' }
+# m.rule(%w[t3]) { command 'date > t3' }
+# m.rule(%w[t4], %w[t1 t3]) { command 'cat t1 t3 > t4' }
+# m.rule(%w[t5], %w[t4 t2]) { command 'cat t4 t2 > t5' }
+# m.build('t5')
+#
+# == Bugs
+#
+# * 'tsort.rb' is wrong name because this library uses
+# Tarjan's algorithm for strongly connected components.
+# Although 'strongly_connected_components.rb' is correct but too long.
+#
+# == References
+#
+# R. E. Tarjan, "Depth First Search and Linear Graph Algorithms",
+# <em>SIAM Journal on Computing</em>, Vol. 1, No. 2, pp. 146-160, June 1972.
+#
+
+module Gem::TSort
+
+ VERSION = "0.2.0"
+
+ class Cyclic < StandardError
+ end
+
+ # Returns a topologically sorted array of nodes.
+ # The array is sorted from children to parents, i.e.
+ # the first element has no child and the last node has no parent.
+ #
+ # If there is a cycle, Gem::TSort::Cyclic is raised.
+ #
+ # class G
+ # include Gem::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # p graph.tsort #=> [4, 2, 3, 1]
+ #
+ # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
+ # p graph.tsort # raises Gem::TSort::Cyclic
+ #
+ def tsort
+ each_node = method(:tsort_each_node)
+ each_child = method(:tsort_each_child)
+ Gem::TSort.tsort(each_node, each_child)
+ end
+
+ # Returns a topologically sorted array of nodes.
+ # The array is sorted from children to parents, i.e.
+ # the first element has no child and the last node has no parent.
+ #
+ # The graph is represented by _each_node_ and _each_child_.
+ # _each_node_ should have +call+ method which yields for each node in the graph.
+ # _each_child_ should have +call+ method which takes a node argument and yields for each child node.
+ #
+ # If there is a cycle, Gem::TSort::Cyclic is raised.
+ #
+ # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # p Gem::TSort.tsort(each_node, each_child) #=> [4, 2, 3, 1]
+ #
+ # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # p Gem::TSort.tsort(each_node, each_child) # raises Gem::TSort::Cyclic
+ #
+ def self.tsort(each_node, each_child)
+ tsort_each(each_node, each_child).to_a
+ end
+
+ # The iterator version of the #tsort method.
+ # <tt><em>obj</em>.tsort_each</tt> is similar to <tt><em>obj</em>.tsort.each</tt>, but
+ # modification of _obj_ during the iteration may lead to unexpected results.
+ #
+ # #tsort_each returns +nil+.
+ # If there is a cycle, Gem::TSort::Cyclic is raised.
+ #
+ # class G
+ # include Gem::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # graph.tsort_each {|n| p n }
+ # #=> 4
+ # # 2
+ # # 3
+ # # 1
+ #
+ def tsort_each(&block) # :yields: node
+ each_node = method(:tsort_each_node)
+ each_child = method(:tsort_each_child)
+ Gem::TSort.tsort_each(each_node, each_child, &block)
+ end
+
+ # The iterator version of the Gem::TSort.tsort method.
+ #
+ # The graph is represented by _each_node_ and _each_child_.
+ # _each_node_ should have +call+ method which yields for each node in the graph.
+ # _each_child_ should have +call+ method which takes a node argument and yields for each child node.
+ #
+ # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # Gem::TSort.tsort_each(each_node, each_child) {|n| p n }
+ # #=> 4
+ # # 2
+ # # 3
+ # # 1
+ #
+ def self.tsort_each(each_node, each_child) # :yields: node
+ return to_enum(__method__, each_node, each_child) unless block_given?
+
+ each_strongly_connected_component(each_node, each_child) {|component|
+ if component.size == 1
+ yield component.first
+ else
+ raise Cyclic.new("topological sort failed: #{component.inspect}")
+ end
+ }
+ end
+
+ # Returns strongly connected components as an array of arrays of nodes.
+ # The array is sorted from children to parents.
+ # Each elements of the array represents a strongly connected component.
+ #
+ # class G
+ # include Gem::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # p graph.strongly_connected_components #=> [[4], [2], [3], [1]]
+ #
+ # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
+ # p graph.strongly_connected_components #=> [[4], [2, 3], [1]]
+ #
+ def strongly_connected_components
+ each_node = method(:tsort_each_node)
+ each_child = method(:tsort_each_child)
+ Gem::TSort.strongly_connected_components(each_node, each_child)
+ end
+
+ # Returns strongly connected components as an array of arrays of nodes.
+ # The array is sorted from children to parents.
+ # Each elements of the array represents a strongly connected component.
+ #
+ # The graph is represented by _each_node_ and _each_child_.
+ # _each_node_ should have +call+ method which yields for each node in the graph.
+ # _each_child_ should have +call+ method which takes a node argument and yields for each child node.
+ #
+ # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # p Gem::TSort.strongly_connected_components(each_node, each_child)
+ # #=> [[4], [2], [3], [1]]
+ #
+ # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # p Gem::TSort.strongly_connected_components(each_node, each_child)
+ # #=> [[4], [2, 3], [1]]
+ #
+ def self.strongly_connected_components(each_node, each_child)
+ each_strongly_connected_component(each_node, each_child).to_a
+ end
+
+ # The iterator version of the #strongly_connected_components method.
+ # <tt><em>obj</em>.each_strongly_connected_component</tt> is similar to
+ # <tt><em>obj</em>.strongly_connected_components.each</tt>, but
+ # modification of _obj_ during the iteration may lead to unexpected results.
+ #
+ # #each_strongly_connected_component returns +nil+.
+ #
+ # class G
+ # include Gem::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # graph.each_strongly_connected_component {|scc| p scc }
+ # #=> [4]
+ # # [2]
+ # # [3]
+ # # [1]
+ #
+ # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
+ # graph.each_strongly_connected_component {|scc| p scc }
+ # #=> [4]
+ # # [2, 3]
+ # # [1]
+ #
+ def each_strongly_connected_component(&block) # :yields: nodes
+ each_node = method(:tsort_each_node)
+ each_child = method(:tsort_each_child)
+ Gem::TSort.each_strongly_connected_component(each_node, each_child, &block)
+ end
+
+ # The iterator version of the Gem::TSort.strongly_connected_components method.
+ #
+ # The graph is represented by _each_node_ and _each_child_.
+ # _each_node_ should have +call+ method which yields for each node in the graph.
+ # _each_child_ should have +call+ method which takes a node argument and yields for each child node.
+ #
+ # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # Gem::TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc }
+ # #=> [4]
+ # # [2]
+ # # [3]
+ # # [1]
+ #
+ # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # Gem::TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc }
+ # #=> [4]
+ # # [2, 3]
+ # # [1]
+ #
+ def self.each_strongly_connected_component(each_node, each_child) # :yields: nodes
+ return to_enum(__method__, each_node, each_child) unless block_given?
+
+ id_map = {}
+ stack = []
+ each_node.call {|node|
+ unless id_map.include? node
+ each_strongly_connected_component_from(node, each_child, id_map, stack) {|c|
+ yield c
+ }
+ end
+ }
+ nil
+ end
+
+ # Iterates over strongly connected component in the subgraph reachable from
+ # _node_.
+ #
+ # Return value is unspecified.
+ #
+ # #each_strongly_connected_component_from doesn't call #tsort_each_node.
+ #
+ # class G
+ # include Gem::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # graph.each_strongly_connected_component_from(2) {|scc| p scc }
+ # #=> [4]
+ # # [2]
+ #
+ # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
+ # graph.each_strongly_connected_component_from(2) {|scc| p scc }
+ # #=> [4]
+ # # [2, 3]
+ #
+ def each_strongly_connected_component_from(node, id_map={}, stack=[], &block) # :yields: nodes
+ Gem::TSort.each_strongly_connected_component_from(node, method(:tsort_each_child), id_map, stack, &block)
+ end
+
+ # Iterates over strongly connected components in a graph.
+ # The graph is represented by _node_ and _each_child_.
+ #
+ # _node_ is the first node.
+ # _each_child_ should have +call+ method which takes a node argument
+ # and yields for each child node.
+ #
+ # Return value is unspecified.
+ #
+ # #Gem::TSort.each_strongly_connected_component_from is a class method and
+ # it doesn't need a class to represent a graph which includes Gem::TSort.
+ #
+ # graph = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
+ # each_child = lambda {|n, &b| graph[n].each(&b) }
+ # Gem::TSort.each_strongly_connected_component_from(1, each_child) {|scc|
+ # p scc
+ # }
+ # #=> [4]
+ # # [2, 3]
+ # # [1]
+ #
+ def self.each_strongly_connected_component_from(node, each_child, id_map={}, stack=[]) # :yields: nodes
+ return to_enum(__method__, node, each_child, id_map, stack) unless block_given?
+
+ minimum_id = node_id = id_map[node] = id_map.size
+ stack_length = stack.length
+ stack << node
+
+ each_child.call(node) {|child|
+ if id_map.include? child
+ child_id = id_map[child]
+ minimum_id = child_id if child_id && child_id < minimum_id
+ else
+ sub_minimum_id =
+ each_strongly_connected_component_from(child, each_child, id_map, stack) {|c|
+ yield c
+ }
+ minimum_id = sub_minimum_id if sub_minimum_id < minimum_id
+ end
+ }
+
+ if node_id == minimum_id
+ component = stack.slice!(stack_length .. -1)
+ component.each {|n| id_map[n] = nil}
+ yield component
+ end
+
+ minimum_id
+ end
+
+ # Should be implemented by a extended class.
+ #
+ # #tsort_each_node is used to iterate for all nodes over a graph.
+ #
+ def tsort_each_node # :yields: node
+ raise NotImplementedError.new
+ end
+
+ # Should be implemented by a extended class.
+ #
+ # #tsort_each_child is used to iterate for child nodes of _node_.
+ #
+ def tsort_each_child(node) # :yields: child
+ raise NotImplementedError.new
+ end
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri.rb b/lib/rubygems/vendor/uri/lib/uri.rb
new file mode 100644
index 0000000000..4691b122b2
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: false
+# Gem::URI is a module providing classes to handle Uniform Resource Identifiers
+# (RFC2396[https://www.rfc-editor.org/rfc/rfc2396]).
+#
+# == Features
+#
+# * Uniform way of handling URIs.
+# * Flexibility to introduce custom Gem::URI schemes.
+# * Flexibility to have an alternate Gem::URI::Parser (or just different patterns
+# and regexp's).
+#
+# == Basic example
+#
+# require 'rubygems/vendor/uri/lib/uri'
+#
+# uri = Gem::URI("http://foo.com/posts?id=30&limit=5#time=1305298413")
+# #=> #<Gem::URI::HTTP http://foo.com/posts?id=30&limit=5#time=1305298413>
+#
+# uri.scheme #=> "http"
+# uri.host #=> "foo.com"
+# uri.path #=> "/posts"
+# uri.query #=> "id=30&limit=5"
+# uri.fragment #=> "time=1305298413"
+#
+# uri.to_s #=> "http://foo.com/posts?id=30&limit=5#time=1305298413"
+#
+# == Adding custom URIs
+#
+# module Gem::URI
+# class RSYNC < Generic
+# DEFAULT_PORT = 873
+# end
+# register_scheme 'RSYNC', RSYNC
+# end
+# #=> Gem::URI::RSYNC
+#
+# Gem::URI.scheme_list
+# #=> {"FILE"=>Gem::URI::File, "FTP"=>Gem::URI::FTP, "HTTP"=>Gem::URI::HTTP,
+# # "HTTPS"=>Gem::URI::HTTPS, "LDAP"=>Gem::URI::LDAP, "LDAPS"=>Gem::URI::LDAPS,
+# # "MAILTO"=>Gem::URI::MailTo, "RSYNC"=>Gem::URI::RSYNC}
+#
+# uri = Gem::URI("rsync://rsync.foo.com")
+# #=> #<Gem::URI::RSYNC rsync://rsync.foo.com>
+#
+# == RFC References
+#
+# A good place to view an RFC spec is http://www.ietf.org/rfc.html.
+#
+# Here is a list of all related RFC's:
+# - RFC822[https://www.rfc-editor.org/rfc/rfc822]
+# - RFC1738[https://www.rfc-editor.org/rfc/rfc1738]
+# - RFC2255[https://www.rfc-editor.org/rfc/rfc2255]
+# - RFC2368[https://www.rfc-editor.org/rfc/rfc2368]
+# - RFC2373[https://www.rfc-editor.org/rfc/rfc2373]
+# - RFC2396[https://www.rfc-editor.org/rfc/rfc2396]
+# - RFC2732[https://www.rfc-editor.org/rfc/rfc2732]
+# - RFC3986[https://www.rfc-editor.org/rfc/rfc3986]
+#
+# == Class tree
+#
+# - Gem::URI::Generic (in uri/generic.rb)
+# - Gem::URI::File - (in uri/file.rb)
+# - Gem::URI::FTP - (in uri/ftp.rb)
+# - Gem::URI::HTTP - (in uri/http.rb)
+# - Gem::URI::HTTPS - (in uri/https.rb)
+# - Gem::URI::LDAP - (in uri/ldap.rb)
+# - Gem::URI::LDAPS - (in uri/ldaps.rb)
+# - Gem::URI::MailTo - (in uri/mailto.rb)
+# - Gem::URI::Parser - (in uri/common.rb)
+# - Gem::URI::REGEXP - (in uri/common.rb)
+# - Gem::URI::REGEXP::PATTERN - (in uri/common.rb)
+# - Gem::URI::Util - (in uri/common.rb)
+# - Gem::URI::Error - (in uri/common.rb)
+# - Gem::URI::InvalidURIError - (in uri/common.rb)
+# - Gem::URI::InvalidComponentError - (in uri/common.rb)
+# - Gem::URI::BadURIError - (in uri/common.rb)
+#
+# == Copyright Info
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# Documentation::
+# Akira Yamada <akira@ruby-lang.org>
+# Dmitry V. Sabanin <sdmitry@lrn.ru>
+# Vincent Batts <vbatts@hashbangbash.com>
+# License::
+# Copyright (c) 2001 akira yamada <akira@ruby-lang.org>
+# You can redistribute it and/or modify it under the same term as Ruby.
+#
+
+module Gem::URI
+end
+
+require_relative 'uri/version'
+require_relative 'uri/common'
+require_relative 'uri/generic'
+require_relative 'uri/file'
+require_relative 'uri/ftp'
+require_relative 'uri/http'
+require_relative 'uri/https'
+require_relative 'uri/ldap'
+require_relative 'uri/ldaps'
+require_relative 'uri/mailto'
+require_relative 'uri/ws'
+require_relative 'uri/wss'
diff --git a/lib/rubygems/vendor/uri/lib/uri/common.rb b/lib/rubygems/vendor/uri/lib/uri/common.rb
new file mode 100644
index 0000000000..e9bdfa6a07
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/common.rb
@@ -0,0 +1,922 @@
+# frozen_string_literal: true
+#--
+# = uri/common.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License::
+# You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative "rfc2396_parser"
+require_relative "rfc3986_parser"
+
+module Gem::URI
+ # The default parser instance for RFC 2396.
+ RFC2396_PARSER = RFC2396_Parser.new
+ Ractor.make_shareable(RFC2396_PARSER) if defined?(Ractor)
+
+ # The default parser instance for RFC 3986.
+ RFC3986_PARSER = RFC3986_Parser.new
+ Ractor.make_shareable(RFC3986_PARSER) if defined?(Ractor)
+
+ # The default parser instance.
+ DEFAULT_PARSER = RFC3986_PARSER
+ Ractor.make_shareable(DEFAULT_PARSER) if defined?(Ractor)
+
+ # Set the default parser instance.
+ def self.parser=(parser = RFC3986_PARSER)
+ remove_const(:Parser) if defined?(::Gem::URI::Parser)
+ const_set("Parser", parser.class)
+
+ remove_const(:PARSER) if defined?(::Gem::URI::PARSER)
+ const_set("PARSER", parser)
+
+ remove_const(:REGEXP) if defined?(::Gem::URI::REGEXP)
+ remove_const(:PATTERN) if defined?(::Gem::URI::PATTERN)
+ if Parser == RFC2396_Parser
+ const_set("REGEXP", Gem::URI::RFC2396_REGEXP)
+ const_set("PATTERN", Gem::URI::RFC2396_REGEXP::PATTERN)
+ end
+
+ Parser.new.regexp.each_pair do |sym, str|
+ remove_const(sym) if const_defined?(sym, false)
+ const_set(sym, str)
+ end
+ end
+ self.parser = RFC3986_PARSER
+
+ def self.const_missing(const) # :nodoc:
+ if const == :REGEXP
+ warn "Gem::URI::REGEXP is obsolete. Use Gem::URI::RFC2396_REGEXP explicitly.", uplevel: 1 if $VERBOSE
+ Gem::URI::RFC2396_REGEXP
+ elsif value = RFC2396_PARSER.regexp[const]
+ warn "Gem::URI::#{const} is obsolete. Use Gem::URI::RFC2396_PARSER.regexp[#{const.inspect}] explicitly.", uplevel: 1 if $VERBOSE
+ value
+ elsif value = RFC2396_Parser.const_get(const)
+ warn "Gem::URI::#{const} is obsolete. Use Gem::URI::RFC2396_Parser::#{const} explicitly.", uplevel: 1 if $VERBOSE
+ value
+ else
+ super
+ end
+ end
+
+ module Util # :nodoc:
+ def make_components_hash(klass, array_hash)
+ tmp = {}
+ if array_hash.kind_of?(Array) &&
+ array_hash.size == klass.component.size - 1
+ klass.component[1..-1].each_index do |i|
+ begin
+ tmp[klass.component[i + 1]] = array_hash[i].clone
+ rescue TypeError
+ tmp[klass.component[i + 1]] = array_hash[i]
+ end
+ end
+
+ elsif array_hash.kind_of?(Hash)
+ array_hash.each do |key, value|
+ begin
+ tmp[key] = value.clone
+ rescue TypeError
+ tmp[key] = value
+ end
+ end
+ else
+ raise ArgumentError,
+ "expected Array of or Hash of components of #{klass} (#{klass.component[1..-1].join(', ')})"
+ end
+ tmp[:scheme] = klass.to_s.sub(/\A.*::/, '').downcase
+
+ return tmp
+ end
+ module_function :make_components_hash
+ end
+
+ module Schemes # :nodoc:
+ class << self
+ ReservedChars = ".+-"
+ EscapedChars = "\u01C0\u01C1\u01C2"
+ # Use Lo category chars as escaped chars for TruffleRuby, which
+ # does not allow Symbol categories as identifiers.
+
+ def escape(name)
+ unless name and name.ascii_only?
+ return nil
+ end
+ name.upcase.tr(ReservedChars, EscapedChars)
+ end
+
+ def unescape(name)
+ name.tr(EscapedChars, ReservedChars).encode(Encoding::US_ASCII).upcase
+ end
+
+ def find(name)
+ const_get(name, false) if name and const_defined?(name, false)
+ end
+
+ def register(name, klass)
+ unless scheme = escape(name)
+ raise ArgumentError, "invalid character as scheme - #{name}"
+ end
+ const_set(scheme, klass)
+ end
+
+ def list
+ constants.map { |name|
+ [unescape(name.to_s), const_get(name)]
+ }.to_h
+ end
+ end
+ end
+ private_constant :Schemes
+
+ # Registers the given +klass+ as the class to be instantiated
+ # when parsing a \Gem::URI with the given +scheme+:
+ #
+ # Gem::URI.register_scheme('MS_SEARCH', Gem::URI::Generic) # => Gem::URI::Generic
+ # Gem::URI.scheme_list['MS_SEARCH'] # => Gem::URI::Generic
+ #
+ # Note that after calling String#upcase on +scheme+, it must be a valid
+ # constant name.
+ def self.register_scheme(scheme, klass)
+ Schemes.register(scheme, klass)
+ end
+
+ # Returns a hash of the defined schemes:
+ #
+ # Gem::URI.scheme_list
+ # # =>
+ # {"MAILTO"=>Gem::URI::MailTo,
+ # "LDAPS"=>Gem::URI::LDAPS,
+ # "WS"=>Gem::URI::WS,
+ # "HTTP"=>Gem::URI::HTTP,
+ # "HTTPS"=>Gem::URI::HTTPS,
+ # "LDAP"=>Gem::URI::LDAP,
+ # "FILE"=>Gem::URI::File,
+ # "FTP"=>Gem::URI::FTP}
+ #
+ # Related: Gem::URI.register_scheme.
+ def self.scheme_list
+ Schemes.list
+ end
+
+ # :stopdoc:
+ INITIAL_SCHEMES = scheme_list
+ private_constant :INITIAL_SCHEMES
+ Ractor.make_shareable(INITIAL_SCHEMES) if defined?(Ractor)
+ # :startdoc:
+
+ # Returns a new object constructed from the given +scheme+, +arguments+,
+ # and +default+:
+ #
+ # - The new object is an instance of <tt>Gem::URI.scheme_list[scheme.upcase]</tt>.
+ # - The object is initialized by calling the class initializer
+ # using +scheme+ and +arguments+.
+ # See Gem::URI::Generic.new.
+ #
+ # Examples:
+ #
+ # values = ['john.doe', 'www.example.com', '123', nil, '/forum/questions/', nil, 'tag=networking&order=newest', 'top']
+ # Gem::URI.for('https', *values)
+ # # => #<Gem::URI::HTTPS https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top>
+ # Gem::URI.for('foo', *values, default: Gem::URI::HTTP)
+ # # => #<Gem::URI::HTTP foo://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top>
+ #
+ def self.for(scheme, *arguments, default: Generic)
+ const_name = Schemes.escape(scheme)
+
+ uri_class = INITIAL_SCHEMES[const_name]
+ uri_class ||= Schemes.find(const_name)
+ uri_class ||= default
+
+ return uri_class.new(scheme, *arguments)
+ end
+
+ #
+ # Base class for all Gem::URI exceptions.
+ #
+ class Error < StandardError; end
+ #
+ # Not a Gem::URI.
+ #
+ class InvalidURIError < Error; end
+ #
+ # Not a Gem::URI component.
+ #
+ class InvalidComponentError < Error; end
+ #
+ # Gem::URI is valid, bad usage is not.
+ #
+ class BadURIError < Error; end
+
+ # Returns a 9-element array representing the parts of the \Gem::URI
+ # formed from the string +uri+;
+ # each array element is a string or +nil+:
+ #
+ # names = %w[scheme userinfo host port registry path opaque query fragment]
+ # values = Gem::URI.split('https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top')
+ # names.zip(values)
+ # # =>
+ # [["scheme", "https"],
+ # ["userinfo", "john.doe"],
+ # ["host", "www.example.com"],
+ # ["port", "123"],
+ # ["registry", nil],
+ # ["path", "/forum/questions/"],
+ # ["opaque", nil],
+ # ["query", "tag=networking&order=newest"],
+ # ["fragment", "top"]]
+ #
+ def self.split(uri)
+ PARSER.split(uri)
+ end
+
+ # Returns a new \Gem::URI object constructed from the given string +uri+:
+ #
+ # Gem::URI.parse('https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top')
+ # # => #<Gem::URI::HTTPS https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top>
+ # Gem::URI.parse('http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top')
+ # # => #<Gem::URI::HTTP http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top>
+ #
+ # It's recommended to first Gem::URI::RFC2396_PARSER.escape string +uri+
+ # if it may contain invalid Gem::URI characters.
+ #
+ def self.parse(uri)
+ PARSER.parse(uri)
+ end
+
+ # Merges the given Gem::URI strings +str+
+ # per {RFC 2396}[https://www.rfc-editor.org/rfc/rfc2396.html].
+ #
+ # Each string in +str+ is converted to an
+ # {RFC3986 Gem::URI}[https://www.rfc-editor.org/rfc/rfc3986.html] before being merged.
+ #
+ # Examples:
+ #
+ # Gem::URI.join("http://example.com/","main.rbx")
+ # # => #<Gem::URI::HTTP http://example.com/main.rbx>
+ #
+ # Gem::URI.join('http://example.com', 'foo')
+ # # => #<Gem::URI::HTTP http://example.com/foo>
+ #
+ # Gem::URI.join('http://example.com', '/foo', '/bar')
+ # # => #<Gem::URI::HTTP http://example.com/bar>
+ #
+ # Gem::URI.join('http://example.com', '/foo', 'bar')
+ # # => #<Gem::URI::HTTP http://example.com/bar>
+ #
+ # Gem::URI.join('http://example.com', '/foo/', 'bar')
+ # # => #<Gem::URI::HTTP http://example.com/foo/bar>
+ #
+ def self.join(*str)
+ DEFAULT_PARSER.join(*str)
+ end
+
+ #
+ # == Synopsis
+ #
+ # Gem::URI::extract(str[, schemes][,&blk])
+ #
+ # == Args
+ #
+ # +str+::
+ # String to extract URIs from.
+ # +schemes+::
+ # Limit Gem::URI matching to specific schemes.
+ #
+ # == Description
+ #
+ # Extracts URIs from a string. If block given, iterates through all matched URIs.
+ # Returns nil if block given or array with matches.
+ #
+ # == Usage
+ #
+ # require "rubygems/vendor/uri/lib/uri"
+ #
+ # Gem::URI.extract("text here http://foo.example.org/bla and here mailto:test@example.com and here also.")
+ # # => ["http://foo.example.com/bla", "mailto:test@example.com"]
+ #
+ def self.extract(str, schemes = nil, &block) # :nodoc:
+ warn "Gem::URI.extract is obsolete", uplevel: 1 if $VERBOSE
+ PARSER.extract(str, schemes, &block)
+ end
+
+ #
+ # == Synopsis
+ #
+ # Gem::URI::regexp([match_schemes])
+ #
+ # == Args
+ #
+ # +match_schemes+::
+ # Array of schemes. If given, resulting regexp matches to URIs
+ # whose scheme is one of the match_schemes.
+ #
+ # == Description
+ #
+ # Returns a Regexp object which matches to Gem::URI-like strings.
+ # The Regexp object returned by this method includes arbitrary
+ # number of capture group (parentheses). Never rely on its number.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # # extract first Gem::URI from html_string
+ # html_string.slice(Gem::URI.regexp)
+ #
+ # # remove ftp URIs
+ # html_string.sub(Gem::URI.regexp(['ftp']), '')
+ #
+ # # You should not rely on the number of parentheses
+ # html_string.scan(Gem::URI.regexp) do |*matches|
+ # p $&
+ # end
+ #
+ def self.regexp(schemes = nil)# :nodoc:
+ warn "Gem::URI.regexp is obsolete", uplevel: 1 if $VERBOSE
+ PARSER.make_regexp(schemes)
+ end
+
+ TBLENCWWWCOMP_ = {} # :nodoc:
+ 256.times do |i|
+ TBLENCWWWCOMP_[-i.chr] = -('%%%02X' % i)
+ end
+ TBLENCURICOMP_ = TBLENCWWWCOMP_.dup.freeze # :nodoc:
+ TBLENCWWWCOMP_[' '] = '+'
+ TBLENCWWWCOMP_.freeze
+ TBLDECWWWCOMP_ = {} # :nodoc:
+ 256.times do |i|
+ h, l = i>>4, i&15
+ TBLDECWWWCOMP_[-('%%%X%X' % [h, l])] = -i.chr
+ TBLDECWWWCOMP_[-('%%%x%X' % [h, l])] = -i.chr
+ TBLDECWWWCOMP_[-('%%%X%x' % [h, l])] = -i.chr
+ TBLDECWWWCOMP_[-('%%%x%x' % [h, l])] = -i.chr
+ end
+ TBLDECWWWCOMP_['+'] = ' '
+ TBLDECWWWCOMP_.freeze
+
+ # Returns a URL-encoded string derived from the given string +str+.
+ #
+ # The returned string:
+ #
+ # - Preserves:
+ #
+ # - Characters <tt>'*'</tt>, <tt>'.'</tt>, <tt>'-'</tt>, and <tt>'_'</tt>.
+ # - Character in ranges <tt>'a'..'z'</tt>, <tt>'A'..'Z'</tt>,
+ # and <tt>'0'..'9'</tt>.
+ #
+ # Example:
+ #
+ # Gem::URI.encode_www_form_component('*.-_azAZ09')
+ # # => "*.-_azAZ09"
+ #
+ # - Converts:
+ #
+ # - Character <tt>' '</tt> to character <tt>'+'</tt>.
+ # - Any other character to "percent notation";
+ # the percent notation for character <i>c</i> is <tt>'%%%X' % c.ord</tt>.
+ #
+ # Example:
+ #
+ # Gem::URI.encode_www_form_component('Here are some punctuation characters: ,;?:')
+ # # => "Here+are+some+punctuation+characters%3A+%2C%3B%3F%3A"
+ #
+ # Encoding:
+ #
+ # - If +str+ has encoding Encoding::ASCII_8BIT, argument +enc+ is ignored.
+ # - Otherwise +str+ is converted first to Encoding::UTF_8
+ # (with suitable character replacements),
+ # and then to encoding +enc+.
+ #
+ # In either case, the returned string has forced encoding Encoding::US_ASCII.
+ #
+ # Related: Gem::URI.encode_uri_component (encodes <tt>' '</tt> as <tt>'%20'</tt>).
+ def self.encode_www_form_component(str, enc=nil)
+ _encode_uri_component(/[^*\-.0-9A-Z_a-z]/, TBLENCWWWCOMP_, str, enc)
+ end
+
+ # Returns a string decoded from the given \URL-encoded string +str+.
+ #
+ # The given string is first encoded as Encoding::ASCII-8BIT (using String#b),
+ # then decoded (as below), and finally force-encoded to the given encoding +enc+.
+ #
+ # The returned string:
+ #
+ # - Preserves:
+ #
+ # - Characters <tt>'*'</tt>, <tt>'.'</tt>, <tt>'-'</tt>, and <tt>'_'</tt>.
+ # - Character in ranges <tt>'a'..'z'</tt>, <tt>'A'..'Z'</tt>,
+ # and <tt>'0'..'9'</tt>.
+ #
+ # Example:
+ #
+ # Gem::URI.decode_www_form_component('*.-_azAZ09')
+ # # => "*.-_azAZ09"
+ #
+ # - Converts:
+ #
+ # - Character <tt>'+'</tt> to character <tt>' '</tt>.
+ # - Each "percent notation" to an ASCII character.
+ #
+ # Example:
+ #
+ # Gem::URI.decode_www_form_component('Here+are+some+punctuation+characters%3A+%2C%3B%3F%3A')
+ # # => "Here are some punctuation characters: ,;?:"
+ #
+ # Related: Gem::URI.decode_uri_component (preserves <tt>'+'</tt>).
+ def self.decode_www_form_component(str, enc=Encoding::UTF_8)
+ _decode_uri_component(/\+|%\h\h/, str, enc)
+ end
+
+ # Like Gem::URI.encode_www_form_component, except that <tt>' '</tt> (space)
+ # is encoded as <tt>'%20'</tt> (instead of <tt>'+'</tt>).
+ def self.encode_uri_component(str, enc=nil)
+ _encode_uri_component(/[^*\-.0-9A-Z_a-z]/, TBLENCURICOMP_, str, enc)
+ end
+
+ # Like Gem::URI.decode_www_form_component, except that <tt>'+'</tt> is preserved.
+ def self.decode_uri_component(str, enc=Encoding::UTF_8)
+ _decode_uri_component(/%\h\h/, str, enc)
+ end
+
+ # Returns a string derived from the given string +str+ with
+ # Gem::URI-encoded characters matching +regexp+ according to +table+.
+ def self._encode_uri_component(regexp, table, str, enc)
+ str = str.to_s.dup
+ if str.encoding != Encoding::ASCII_8BIT
+ if enc && enc != Encoding::ASCII_8BIT
+ str.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace)
+ str.encode!(enc, fallback: ->(x){"&##{x.ord};"})
+ end
+ str.force_encoding(Encoding::ASCII_8BIT)
+ end
+ str.gsub!(regexp, table)
+ str.force_encoding(Encoding::US_ASCII)
+ end
+ private_class_method :_encode_uri_component
+
+ # Returns a string decoding characters matching +regexp+ from the
+ # given \URL-encoded string +str+.
+ def self._decode_uri_component(regexp, str, enc)
+ raise ArgumentError, "invalid %-encoding (#{str})" if /%(?!\h\h)/.match?(str)
+ str.b.gsub(regexp, TBLDECWWWCOMP_).force_encoding(enc)
+ end
+ private_class_method :_decode_uri_component
+
+ # Returns a URL-encoded string derived from the given
+ # {Enumerable}[rdoc-ref:Enumerable@Enumerable+in+Ruby+Classes]
+ # +enum+.
+ #
+ # The result is suitable for use as form data
+ # for an \HTTP request whose <tt>Content-Type</tt> is
+ # <tt>'application/x-www-form-urlencoded'</tt>.
+ #
+ # The returned string consists of the elements of +enum+,
+ # each converted to one or more URL-encoded strings,
+ # and all joined with character <tt>'&'</tt>.
+ #
+ # Simple examples:
+ #
+ # Gem::URI.encode_www_form([['foo', 0], ['bar', 1], ['baz', 2]])
+ # # => "foo=0&bar=1&baz=2"
+ # Gem::URI.encode_www_form({foo: 0, bar: 1, baz: 2})
+ # # => "foo=0&bar=1&baz=2"
+ #
+ # The returned string is formed using method Gem::URI.encode_www_form_component,
+ # which converts certain characters:
+ #
+ # Gem::URI.encode_www_form('f#o': '/', 'b-r': '$', 'b z': '@')
+ # # => "f%23o=%2F&b-r=%24&b+z=%40"
+ #
+ # When +enum+ is Array-like, each element +ele+ is converted to a field:
+ #
+ # - If +ele+ is an array of two or more elements,
+ # the field is formed from its first two elements
+ # (and any additional elements are ignored):
+ #
+ # name = Gem::URI.encode_www_form_component(ele[0], enc)
+ # value = Gem::URI.encode_www_form_component(ele[1], enc)
+ # "#{name}=#{value}"
+ #
+ # Examples:
+ #
+ # Gem::URI.encode_www_form([%w[foo bar], %w[baz bat bah]])
+ # # => "foo=bar&baz=bat"
+ # Gem::URI.encode_www_form([['foo', 0], ['bar', :baz, 'bat']])
+ # # => "foo=0&bar=baz"
+ #
+ # - If +ele+ is an array of one element,
+ # the field is formed from <tt>ele[0]</tt>:
+ #
+ # Gem::URI.encode_www_form_component(ele[0])
+ #
+ # Example:
+ #
+ # Gem::URI.encode_www_form([['foo'], [:bar], [0]])
+ # # => "foo&bar&0"
+ #
+ # - Otherwise the field is formed from +ele+:
+ #
+ # Gem::URI.encode_www_form_component(ele)
+ #
+ # Example:
+ #
+ # Gem::URI.encode_www_form(['foo', :bar, 0])
+ # # => "foo&bar&0"
+ #
+ # The elements of an Array-like +enum+ may be mixture:
+ #
+ # Gem::URI.encode_www_form([['foo', 0], ['bar', 1, 2], ['baz'], :bat])
+ # # => "foo=0&bar=1&baz&bat"
+ #
+ # When +enum+ is Hash-like,
+ # each +key+/+value+ pair is converted to one or more fields:
+ #
+ # - If +value+ is
+ # {Array-convertible}[rdoc-ref:implicit_conversion.rdoc@Array-Convertible+Objects],
+ # each element +ele+ in +value+ is paired with +key+ to form a field:
+ #
+ # name = Gem::URI.encode_www_form_component(key, enc)
+ # value = Gem::URI.encode_www_form_component(ele, enc)
+ # "#{name}=#{value}"
+ #
+ # Example:
+ #
+ # Gem::URI.encode_www_form({foo: [:bar, 1], baz: [:bat, :bam, 2]})
+ # # => "foo=bar&foo=1&baz=bat&baz=bam&baz=2"
+ #
+ # - Otherwise, +key+ and +value+ are paired to form a field:
+ #
+ # name = Gem::URI.encode_www_form_component(key, enc)
+ # value = Gem::URI.encode_www_form_component(value, enc)
+ # "#{name}=#{value}"
+ #
+ # Example:
+ #
+ # Gem::URI.encode_www_form({foo: 0, bar: 1, baz: 2})
+ # # => "foo=0&bar=1&baz=2"
+ #
+ # The elements of a Hash-like +enum+ may be mixture:
+ #
+ # Gem::URI.encode_www_form({foo: [0, 1], bar: 2})
+ # # => "foo=0&foo=1&bar=2"
+ #
+ def self.encode_www_form(enum, enc=nil)
+ enum.map do |k,v|
+ if v.nil?
+ encode_www_form_component(k, enc)
+ elsif v.respond_to?(:to_ary)
+ v.to_ary.map do |w|
+ str = encode_www_form_component(k, enc)
+ unless w.nil?
+ str << '='
+ str << encode_www_form_component(w, enc)
+ end
+ end.join('&')
+ else
+ str = encode_www_form_component(k, enc)
+ str << '='
+ str << encode_www_form_component(v, enc)
+ end
+ end.join('&')
+ end
+
+ # Returns name/value pairs derived from the given string +str+,
+ # which must be an ASCII string.
+ #
+ # The method may be used to decode the body of Net::HTTPResponse object +res+
+ # for which <tt>res['Content-Type']</tt> is <tt>'application/x-www-form-urlencoded'</tt>.
+ #
+ # The returned data is an array of 2-element subarrays;
+ # each subarray is a name/value pair (both are strings).
+ # Each returned string has encoding +enc+,
+ # and has had invalid characters removed via
+ # {String#scrub}[rdoc-ref:String#scrub].
+ #
+ # A simple example:
+ #
+ # Gem::URI.decode_www_form('foo=0&bar=1&baz')
+ # # => [["foo", "0"], ["bar", "1"], ["baz", ""]]
+ #
+ # The returned strings have certain conversions,
+ # similar to those performed in Gem::URI.decode_www_form_component:
+ #
+ # Gem::URI.decode_www_form('f%23o=%2F&b-r=%24&b+z=%40')
+ # # => [["f#o", "/"], ["b-r", "$"], ["b z", "@"]]
+ #
+ # The given string may contain consecutive separators:
+ #
+ # Gem::URI.decode_www_form('foo=0&&bar=1&&baz=2')
+ # # => [["foo", "0"], ["", ""], ["bar", "1"], ["", ""], ["baz", "2"]]
+ #
+ # A different separator may be specified:
+ #
+ # Gem::URI.decode_www_form('foo=0--bar=1--baz', separator: '--')
+ # # => [["foo", "0"], ["bar", "1"], ["baz", ""]]
+ #
+ def self.decode_www_form(str, enc=Encoding::UTF_8, separator: '&', use__charset_: false, isindex: false)
+ raise ArgumentError, "the input of #{self.name}.#{__method__} must be ASCII only string" unless str.ascii_only?
+ ary = []
+ return ary if str.empty?
+ enc = Encoding.find(enc)
+ str.b.each_line(separator) do |string|
+ string.chomp!(separator)
+ key, sep, val = string.partition('=')
+ if isindex
+ if sep.empty?
+ val = key
+ key = +''
+ end
+ isindex = false
+ end
+
+ if use__charset_ and key == '_charset_' and e = get_encoding(val)
+ enc = e
+ use__charset_ = false
+ end
+
+ key.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_)
+ if val
+ val.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_)
+ else
+ val = +''
+ end
+
+ ary << [key, val]
+ end
+ ary.each do |k, v|
+ k.force_encoding(enc)
+ k.scrub!
+ v.force_encoding(enc)
+ v.scrub!
+ end
+ ary
+ end
+
+ private
+=begin command for WEB_ENCODINGS_
+ curl https://encoding.spec.whatwg.org/encodings.json|
+ ruby -rjson -e 'H={}
+ h={
+ "shift_jis"=>"Windows-31J",
+ "euc-jp"=>"cp51932",
+ "iso-2022-jp"=>"cp50221",
+ "x-mac-cyrillic"=>"macCyrillic",
+ }
+ JSON($<.read).map{|x|x["encodings"]}.flatten.each{|x|
+ Encoding.find(n=h.fetch(n=x["name"].downcase,n))rescue next
+ x["labels"].each{|y|H[y]=n}
+ }
+ puts "{"
+ H.each{|k,v|puts %[ #{k.dump}=>#{v.dump},]}
+ puts "}"
+'
+=end
+ WEB_ENCODINGS_ = {
+ "unicode-1-1-utf-8"=>"utf-8",
+ "utf-8"=>"utf-8",
+ "utf8"=>"utf-8",
+ "866"=>"ibm866",
+ "cp866"=>"ibm866",
+ "csibm866"=>"ibm866",
+ "ibm866"=>"ibm866",
+ "csisolatin2"=>"iso-8859-2",
+ "iso-8859-2"=>"iso-8859-2",
+ "iso-ir-101"=>"iso-8859-2",
+ "iso8859-2"=>"iso-8859-2",
+ "iso88592"=>"iso-8859-2",
+ "iso_8859-2"=>"iso-8859-2",
+ "iso_8859-2:1987"=>"iso-8859-2",
+ "l2"=>"iso-8859-2",
+ "latin2"=>"iso-8859-2",
+ "csisolatin3"=>"iso-8859-3",
+ "iso-8859-3"=>"iso-8859-3",
+ "iso-ir-109"=>"iso-8859-3",
+ "iso8859-3"=>"iso-8859-3",
+ "iso88593"=>"iso-8859-3",
+ "iso_8859-3"=>"iso-8859-3",
+ "iso_8859-3:1988"=>"iso-8859-3",
+ "l3"=>"iso-8859-3",
+ "latin3"=>"iso-8859-3",
+ "csisolatin4"=>"iso-8859-4",
+ "iso-8859-4"=>"iso-8859-4",
+ "iso-ir-110"=>"iso-8859-4",
+ "iso8859-4"=>"iso-8859-4",
+ "iso88594"=>"iso-8859-4",
+ "iso_8859-4"=>"iso-8859-4",
+ "iso_8859-4:1988"=>"iso-8859-4",
+ "l4"=>"iso-8859-4",
+ "latin4"=>"iso-8859-4",
+ "csisolatincyrillic"=>"iso-8859-5",
+ "cyrillic"=>"iso-8859-5",
+ "iso-8859-5"=>"iso-8859-5",
+ "iso-ir-144"=>"iso-8859-5",
+ "iso8859-5"=>"iso-8859-5",
+ "iso88595"=>"iso-8859-5",
+ "iso_8859-5"=>"iso-8859-5",
+ "iso_8859-5:1988"=>"iso-8859-5",
+ "arabic"=>"iso-8859-6",
+ "asmo-708"=>"iso-8859-6",
+ "csiso88596e"=>"iso-8859-6",
+ "csiso88596i"=>"iso-8859-6",
+ "csisolatinarabic"=>"iso-8859-6",
+ "ecma-114"=>"iso-8859-6",
+ "iso-8859-6"=>"iso-8859-6",
+ "iso-8859-6-e"=>"iso-8859-6",
+ "iso-8859-6-i"=>"iso-8859-6",
+ "iso-ir-127"=>"iso-8859-6",
+ "iso8859-6"=>"iso-8859-6",
+ "iso88596"=>"iso-8859-6",
+ "iso_8859-6"=>"iso-8859-6",
+ "iso_8859-6:1987"=>"iso-8859-6",
+ "csisolatingreek"=>"iso-8859-7",
+ "ecma-118"=>"iso-8859-7",
+ "elot_928"=>"iso-8859-7",
+ "greek"=>"iso-8859-7",
+ "greek8"=>"iso-8859-7",
+ "iso-8859-7"=>"iso-8859-7",
+ "iso-ir-126"=>"iso-8859-7",
+ "iso8859-7"=>"iso-8859-7",
+ "iso88597"=>"iso-8859-7",
+ "iso_8859-7"=>"iso-8859-7",
+ "iso_8859-7:1987"=>"iso-8859-7",
+ "sun_eu_greek"=>"iso-8859-7",
+ "csiso88598e"=>"iso-8859-8",
+ "csisolatinhebrew"=>"iso-8859-8",
+ "hebrew"=>"iso-8859-8",
+ "iso-8859-8"=>"iso-8859-8",
+ "iso-8859-8-e"=>"iso-8859-8",
+ "iso-ir-138"=>"iso-8859-8",
+ "iso8859-8"=>"iso-8859-8",
+ "iso88598"=>"iso-8859-8",
+ "iso_8859-8"=>"iso-8859-8",
+ "iso_8859-8:1988"=>"iso-8859-8",
+ "visual"=>"iso-8859-8",
+ "csisolatin6"=>"iso-8859-10",
+ "iso-8859-10"=>"iso-8859-10",
+ "iso-ir-157"=>"iso-8859-10",
+ "iso8859-10"=>"iso-8859-10",
+ "iso885910"=>"iso-8859-10",
+ "l6"=>"iso-8859-10",
+ "latin6"=>"iso-8859-10",
+ "iso-8859-13"=>"iso-8859-13",
+ "iso8859-13"=>"iso-8859-13",
+ "iso885913"=>"iso-8859-13",
+ "iso-8859-14"=>"iso-8859-14",
+ "iso8859-14"=>"iso-8859-14",
+ "iso885914"=>"iso-8859-14",
+ "csisolatin9"=>"iso-8859-15",
+ "iso-8859-15"=>"iso-8859-15",
+ "iso8859-15"=>"iso-8859-15",
+ "iso885915"=>"iso-8859-15",
+ "iso_8859-15"=>"iso-8859-15",
+ "l9"=>"iso-8859-15",
+ "iso-8859-16"=>"iso-8859-16",
+ "cskoi8r"=>"koi8-r",
+ "koi"=>"koi8-r",
+ "koi8"=>"koi8-r",
+ "koi8-r"=>"koi8-r",
+ "koi8_r"=>"koi8-r",
+ "koi8-ru"=>"koi8-u",
+ "koi8-u"=>"koi8-u",
+ "dos-874"=>"windows-874",
+ "iso-8859-11"=>"windows-874",
+ "iso8859-11"=>"windows-874",
+ "iso885911"=>"windows-874",
+ "tis-620"=>"windows-874",
+ "windows-874"=>"windows-874",
+ "cp1250"=>"windows-1250",
+ "windows-1250"=>"windows-1250",
+ "x-cp1250"=>"windows-1250",
+ "cp1251"=>"windows-1251",
+ "windows-1251"=>"windows-1251",
+ "x-cp1251"=>"windows-1251",
+ "ansi_x3.4-1968"=>"windows-1252",
+ "ascii"=>"windows-1252",
+ "cp1252"=>"windows-1252",
+ "cp819"=>"windows-1252",
+ "csisolatin1"=>"windows-1252",
+ "ibm819"=>"windows-1252",
+ "iso-8859-1"=>"windows-1252",
+ "iso-ir-100"=>"windows-1252",
+ "iso8859-1"=>"windows-1252",
+ "iso88591"=>"windows-1252",
+ "iso_8859-1"=>"windows-1252",
+ "iso_8859-1:1987"=>"windows-1252",
+ "l1"=>"windows-1252",
+ "latin1"=>"windows-1252",
+ "us-ascii"=>"windows-1252",
+ "windows-1252"=>"windows-1252",
+ "x-cp1252"=>"windows-1252",
+ "cp1253"=>"windows-1253",
+ "windows-1253"=>"windows-1253",
+ "x-cp1253"=>"windows-1253",
+ "cp1254"=>"windows-1254",
+ "csisolatin5"=>"windows-1254",
+ "iso-8859-9"=>"windows-1254",
+ "iso-ir-148"=>"windows-1254",
+ "iso8859-9"=>"windows-1254",
+ "iso88599"=>"windows-1254",
+ "iso_8859-9"=>"windows-1254",
+ "iso_8859-9:1989"=>"windows-1254",
+ "l5"=>"windows-1254",
+ "latin5"=>"windows-1254",
+ "windows-1254"=>"windows-1254",
+ "x-cp1254"=>"windows-1254",
+ "cp1255"=>"windows-1255",
+ "windows-1255"=>"windows-1255",
+ "x-cp1255"=>"windows-1255",
+ "cp1256"=>"windows-1256",
+ "windows-1256"=>"windows-1256",
+ "x-cp1256"=>"windows-1256",
+ "cp1257"=>"windows-1257",
+ "windows-1257"=>"windows-1257",
+ "x-cp1257"=>"windows-1257",
+ "cp1258"=>"windows-1258",
+ "windows-1258"=>"windows-1258",
+ "x-cp1258"=>"windows-1258",
+ "x-mac-cyrillic"=>"macCyrillic",
+ "x-mac-ukrainian"=>"macCyrillic",
+ "chinese"=>"gbk",
+ "csgb2312"=>"gbk",
+ "csiso58gb231280"=>"gbk",
+ "gb2312"=>"gbk",
+ "gb_2312"=>"gbk",
+ "gb_2312-80"=>"gbk",
+ "gbk"=>"gbk",
+ "iso-ir-58"=>"gbk",
+ "x-gbk"=>"gbk",
+ "gb18030"=>"gb18030",
+ "big5"=>"big5",
+ "big5-hkscs"=>"big5",
+ "cn-big5"=>"big5",
+ "csbig5"=>"big5",
+ "x-x-big5"=>"big5",
+ "cseucpkdfmtjapanese"=>"cp51932",
+ "euc-jp"=>"cp51932",
+ "x-euc-jp"=>"cp51932",
+ "csiso2022jp"=>"cp50221",
+ "iso-2022-jp"=>"cp50221",
+ "csshiftjis"=>"Windows-31J",
+ "ms932"=>"Windows-31J",
+ "ms_kanji"=>"Windows-31J",
+ "shift-jis"=>"Windows-31J",
+ "shift_jis"=>"Windows-31J",
+ "sjis"=>"Windows-31J",
+ "windows-31j"=>"Windows-31J",
+ "x-sjis"=>"Windows-31J",
+ "cseuckr"=>"euc-kr",
+ "csksc56011987"=>"euc-kr",
+ "euc-kr"=>"euc-kr",
+ "iso-ir-149"=>"euc-kr",
+ "korean"=>"euc-kr",
+ "ks_c_5601-1987"=>"euc-kr",
+ "ks_c_5601-1989"=>"euc-kr",
+ "ksc5601"=>"euc-kr",
+ "ksc_5601"=>"euc-kr",
+ "windows-949"=>"euc-kr",
+ "utf-16be"=>"utf-16be",
+ "utf-16"=>"utf-16le",
+ "utf-16le"=>"utf-16le",
+ } # :nodoc:
+ Ractor.make_shareable(WEB_ENCODINGS_) if defined?(Ractor)
+
+ # :nodoc:
+ # return encoding or nil
+ # http://encoding.spec.whatwg.org/#concept-encoding-get
+ def self.get_encoding(label)
+ Encoding.find(WEB_ENCODINGS_[label.to_str.strip.downcase]) rescue nil
+ end
+end # module Gem::URI
+
+module Gem
+
+ #
+ # Returns a \Gem::URI object derived from the given +uri+,
+ # which may be a \Gem::URI string or an existing \Gem::URI object:
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ # # Returns a new Gem::URI.
+ # uri = Gem::URI('http://github.com/ruby/ruby')
+ # # => #<Gem::URI::HTTP http://github.com/ruby/ruby>
+ # # Returns the given Gem::URI.
+ # Gem::URI(uri)
+ # # => #<Gem::URI::HTTP http://github.com/ruby/ruby>
+ #
+ # You must require 'rubygems/vendor/uri/lib/uri' to use this method.
+ #
+ def URI(uri)
+ if uri.is_a?(Gem::URI::Generic)
+ uri
+ elsif uri = String.try_convert(uri)
+ Gem::URI.parse(uri)
+ else
+ raise ArgumentError,
+ "bad argument (expected Gem::URI object or Gem::URI string)"
+ end
+ end
+ module_function :URI
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/file.rb b/lib/rubygems/vendor/uri/lib/uri/file.rb
new file mode 100644
index 0000000000..391c499716
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/file.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require_relative 'generic'
+
+module Gem::URI
+
+ #
+ # The "file" Gem::URI is defined by RFC8089.
+ #
+ class File < Generic
+ # A Default port of nil for Gem::URI::File.
+ DEFAULT_PORT = nil
+
+ #
+ # An Array of the available components for Gem::URI::File.
+ #
+ COMPONENT = [
+ :scheme,
+ :host,
+ :path
+ ].freeze
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::File object from components, with syntax checking.
+ #
+ # The components accepted are +host+ and +path+.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[host, path]</code>.
+ #
+ # A path from e.g. the File class should be escaped before
+ # being passed.
+ #
+ # Examples:
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri1 = Gem::URI::File.build(['host.example.com', '/path/file.zip'])
+ # uri1.to_s # => "file://host.example.com/path/file.zip"
+ #
+ # uri2 = Gem::URI::File.build({:host => 'host.example.com',
+ # :path => '/ruby/src'})
+ # uri2.to_s # => "file://host.example.com/ruby/src"
+ #
+ # uri3 = Gem::URI::File.build({:path => Gem::URI::RFC2396_PARSER.escape('/path/my file.txt')})
+ # uri3.to_s # => "file:///path/my%20file.txt"
+ #
+ def self.build(args)
+ tmp = Util::make_components_hash(self, args)
+ super(tmp)
+ end
+
+ # Protected setter for the host component +v+.
+ #
+ # See also Gem::URI::Generic.host=.
+ #
+ def set_host(v)
+ v = "" if v.nil? || v == "localhost"
+ @host = v
+ end
+
+ # do nothing
+ def set_port(v)
+ end
+
+ # raise InvalidURIError
+ def check_userinfo(user)
+ raise Gem::URI::InvalidURIError, "cannot set userinfo for file Gem::URI"
+ end
+
+ # raise InvalidURIError
+ def check_user(user)
+ raise Gem::URI::InvalidURIError, "cannot set user for file Gem::URI"
+ end
+
+ # raise InvalidURIError
+ def check_password(user)
+ raise Gem::URI::InvalidURIError, "cannot set password for file Gem::URI"
+ end
+
+ # do nothing
+ def set_userinfo(v)
+ end
+
+ # do nothing
+ def set_user(v)
+ end
+
+ # do nothing
+ def set_password(v)
+ end
+ end
+
+ register_scheme 'FILE', File
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/ftp.rb b/lib/rubygems/vendor/uri/lib/uri/ftp.rb
new file mode 100644
index 0000000000..7517813029
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/ftp.rb
@@ -0,0 +1,267 @@
+# frozen_string_literal: false
+# = uri/ftp.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Gem::URI
+
+ #
+ # FTP Gem::URI syntax is defined by RFC1738 section 3.2.
+ #
+ # This class will be redesigned because of difference of implementations;
+ # the structure of its path. draft-hoffman-ftp-uri-04 is a draft but it
+ # is a good summary about the de facto spec.
+ # https://datatracker.ietf.org/doc/html/draft-hoffman-ftp-uri-04
+ #
+ class FTP < Generic
+ # A Default port of 21 for Gem::URI::FTP.
+ DEFAULT_PORT = 21
+
+ #
+ # An Array of the available components for Gem::URI::FTP.
+ #
+ COMPONENT = [
+ :scheme,
+ :userinfo, :host, :port,
+ :path, :typecode
+ ].freeze
+
+ #
+ # Typecode is "a", "i", or "d".
+ #
+ # * "a" indicates a text file (the FTP command was ASCII)
+ # * "i" indicates a binary file (FTP command IMAGE)
+ # * "d" indicates the contents of a directory should be displayed
+ #
+ TYPECODE = ['a', 'i', 'd'].freeze
+
+ # Typecode prefix ";type=".
+ TYPECODE_PREFIX = ';type='.freeze
+
+ def self.new2(user, password, host, port, path,
+ typecode = nil, arg_check = true) # :nodoc:
+ # Do not use this method! Not tested. [Bug #7301]
+ # This methods remains just for compatibility,
+ # Keep it undocumented until the active maintainer is assigned.
+ typecode = nil if typecode.size == 0
+ if typecode && !TYPECODE.include?(typecode)
+ raise ArgumentError,
+ "bad typecode is specified: #{typecode}"
+ end
+
+ # do escape
+
+ self.new('ftp',
+ [user, password],
+ host, port, nil,
+ typecode ? path + TYPECODE_PREFIX + typecode : path,
+ nil, nil, nil, arg_check)
+ end
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::FTP object from components, with syntax checking.
+ #
+ # The components accepted are +userinfo+, +host+, +port+, +path+, and
+ # +typecode+.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[userinfo, host, port, path, typecode]</code>.
+ #
+ # If the path supplied is absolute, it will be escaped in order to
+ # make it absolute in the Gem::URI.
+ #
+ # Examples:
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri1 = Gem::URI::FTP.build(['user:password', 'ftp.example.com', nil,
+ # '/path/file.zip', 'i'])
+ # uri1.to_s # => "ftp://user:password@ftp.example.com/%2Fpath/file.zip;type=i"
+ #
+ # uri2 = Gem::URI::FTP.build({:host => 'ftp.example.com',
+ # :path => 'ruby/src'})
+ # uri2.to_s # => "ftp://ftp.example.com/ruby/src"
+ #
+ def self.build(args)
+
+ # Fix the incoming path to be generic URL syntax
+ # FTP path -> URL path
+ # foo/bar /foo/bar
+ # /foo/bar /%2Ffoo/bar
+ #
+ if args.kind_of?(Array)
+ args[3] = '/' + args[3].sub(/^\//, '%2F')
+ else
+ args[:path] = '/' + args[:path].sub(/^\//, '%2F')
+ end
+
+ tmp = Util::make_components_hash(self, args)
+
+ if tmp[:typecode]
+ if tmp[:typecode].size == 1
+ tmp[:typecode] = TYPECODE_PREFIX + tmp[:typecode]
+ end
+ tmp[:path] << tmp[:typecode]
+ end
+
+ return super(tmp)
+ end
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::FTP object from generic URL components with no
+ # syntax checking.
+ #
+ # Unlike build(), this method does not escape the path component as
+ # required by RFC1738; instead it is treated as per RFC2396.
+ #
+ # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+,
+ # +opaque+, +query+, and +fragment+, in that order.
+ #
+ def initialize(scheme,
+ userinfo, host, port, registry,
+ path, opaque,
+ query,
+ fragment,
+ parser = nil,
+ arg_check = false)
+ raise InvalidURIError unless path
+ path = path.sub(/^\//,'')
+ path.sub!(/^%2F/,'/')
+ super(scheme, userinfo, host, port, registry, path, opaque,
+ query, fragment, parser, arg_check)
+ @typecode = nil
+ if tmp = @path.index(TYPECODE_PREFIX)
+ typecode = @path[tmp + TYPECODE_PREFIX.size..-1]
+ @path = @path[0..tmp - 1]
+
+ if arg_check
+ self.typecode = typecode
+ else
+ self.set_typecode(typecode)
+ end
+ end
+ end
+
+ # typecode accessor.
+ #
+ # See Gem::URI::FTP::COMPONENT.
+ attr_reader :typecode
+
+ # Validates typecode +v+,
+ # returns +true+ or +false+.
+ #
+ def check_typecode(v)
+ if TYPECODE.include?(v)
+ return true
+ else
+ raise InvalidComponentError,
+ "bad typecode(expected #{TYPECODE.join(', ')}): #{v}"
+ end
+ end
+ private :check_typecode
+
+ # Private setter for the typecode +v+.
+ #
+ # See also Gem::URI::FTP.typecode=.
+ #
+ def set_typecode(v)
+ @typecode = v
+ end
+ protected :set_typecode
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the typecode +v+
+ # (with validation).
+ #
+ # See also Gem::URI::FTP.check_typecode.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("ftp://john@ftp.example.com/my_file.img")
+ # #=> #<Gem::URI::FTP ftp://john@ftp.example.com/my_file.img>
+ # uri.typecode = "i"
+ # uri
+ # #=> #<Gem::URI::FTP ftp://john@ftp.example.com/my_file.img;type=i>
+ #
+ def typecode=(typecode)
+ check_typecode(typecode)
+ set_typecode(typecode)
+ typecode
+ end
+
+ def merge(oth) # :nodoc:
+ tmp = super(oth)
+ if self != tmp
+ tmp.set_typecode(oth.typecode)
+ end
+
+ return tmp
+ end
+
+ # Returns the path from an FTP Gem::URI.
+ #
+ # RFC 1738 specifically states that the path for an FTP Gem::URI does not
+ # include the / which separates the Gem::URI path from the Gem::URI host. Example:
+ #
+ # <code>ftp://ftp.example.com/pub/ruby</code>
+ #
+ # The above Gem::URI indicates that the client should connect to
+ # ftp.example.com then cd to pub/ruby from the initial login directory.
+ #
+ # If you want to cd to an absolute directory, you must include an
+ # escaped / (%2F) in the path. Example:
+ #
+ # <code>ftp://ftp.example.com/%2Fpub/ruby</code>
+ #
+ # This method will then return "/pub/ruby".
+ #
+ def path
+ return @path.sub(/^\//,'').sub(/^%2F/,'/')
+ end
+
+ # Private setter for the path of the Gem::URI::FTP.
+ def set_path(v)
+ super("/" + v.sub(/^\//, "%2F"))
+ end
+ protected :set_path
+
+ # Returns a String representation of the Gem::URI::FTP.
+ def to_s
+ save_path = nil
+ if @typecode
+ save_path = @path
+ @path = @path + TYPECODE_PREFIX + @typecode
+ end
+ str = super
+ if @typecode
+ @path = save_path
+ end
+
+ return str
+ end
+ end
+
+ register_scheme 'FTP', FTP
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/generic.rb b/lib/rubygems/vendor/uri/lib/uri/generic.rb
new file mode 100644
index 0000000000..d0bc77dfda
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/generic.rb
@@ -0,0 +1,1592 @@
+# frozen_string_literal: true
+
+# = uri/generic.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'common'
+autoload :IPSocket, 'socket'
+autoload :IPAddr, 'ipaddr'
+
+module Gem::URI
+
+ #
+ # Base class for all Gem::URI classes.
+ # Implements generic Gem::URI syntax as per RFC 2396.
+ #
+ class Generic
+ include Gem::URI
+
+ #
+ # A Default port of nil for Gem::URI::Generic.
+ #
+ DEFAULT_PORT = nil
+
+ #
+ # Returns default port.
+ #
+ def self.default_port
+ self::DEFAULT_PORT
+ end
+
+ #
+ # Returns default port.
+ #
+ def default_port
+ self.class.default_port
+ end
+
+ #
+ # An Array of the available components for Gem::URI::Generic.
+ #
+ COMPONENT = [
+ :scheme,
+ :userinfo, :host, :port, :registry,
+ :path, :opaque,
+ :query,
+ :fragment
+ ].freeze
+
+ #
+ # Components of the Gem::URI in the order.
+ #
+ def self.component
+ self::COMPONENT
+ end
+
+ USE_REGISTRY = false # :nodoc:
+
+ def self.use_registry # :nodoc:
+ self::USE_REGISTRY
+ end
+
+ #
+ # == Synopsis
+ #
+ # See ::new.
+ #
+ # == Description
+ #
+ # At first, tries to create a new Gem::URI::Generic instance using
+ # Gem::URI::Generic::build. But, if exception Gem::URI::InvalidComponentError is raised,
+ # then it does Gem::URI::RFC2396_PARSER.escape all Gem::URI components and tries again.
+ #
+ def self.build2(args)
+ begin
+ return self.build(args)
+ rescue InvalidComponentError
+ if args.kind_of?(Array)
+ return self.build(args.collect{|x|
+ if x.is_a?(String)
+ Gem::URI::RFC2396_PARSER.escape(x)
+ else
+ x
+ end
+ })
+ elsif args.kind_of?(Hash)
+ tmp = {}
+ args.each do |key, value|
+ tmp[key] = if value
+ Gem::URI::RFC2396_PARSER.escape(value)
+ else
+ value
+ end
+ end
+ return self.build(tmp)
+ end
+ end
+ end
+
+ #
+ # == Synopsis
+ #
+ # See ::new.
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::Generic instance from components of Gem::URI::Generic
+ # with check. Components are: scheme, userinfo, host, port, registry, path,
+ # opaque, query, and fragment. You can provide arguments either by an Array or a Hash.
+ # See ::new for hash keys to use or for order of array items.
+ #
+ def self.build(args)
+ if args.kind_of?(Array) &&
+ args.size == ::Gem::URI::Generic::COMPONENT.size
+ tmp = args.dup
+ elsif args.kind_of?(Hash)
+ tmp = ::Gem::URI::Generic::COMPONENT.collect do |c|
+ if args.include?(c)
+ args[c]
+ else
+ nil
+ end
+ end
+ else
+ component = self.component rescue ::Gem::URI::Generic::COMPONENT
+ raise ArgumentError,
+ "expected Array of or Hash of components of #{self} (#{component.join(', ')})"
+ end
+
+ tmp << nil
+ tmp << true
+ return self.new(*tmp)
+ end
+
+ #
+ # == Args
+ #
+ # +scheme+::
+ # Protocol scheme, i.e. 'http','ftp','mailto' and so on.
+ # +userinfo+::
+ # User name and password, i.e. 'sdmitry:bla'.
+ # +host+::
+ # Server host name.
+ # +port+::
+ # Server port.
+ # +registry+::
+ # Registry of naming authorities.
+ # +path+::
+ # Path on server.
+ # +opaque+::
+ # Opaque part.
+ # +query+::
+ # Query data.
+ # +fragment+::
+ # Part of the Gem::URI after '#' character.
+ # +parser+::
+ # Parser for internal use [Gem::URI::DEFAULT_PARSER by default].
+ # +arg_check+::
+ # Check arguments [false by default].
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::Generic instance from ``generic'' components without check.
+ #
+ def initialize(scheme,
+ userinfo, host, port, registry,
+ path, opaque,
+ query,
+ fragment,
+ parser = DEFAULT_PARSER,
+ arg_check = false)
+ @scheme = nil
+ @user = nil
+ @password = nil
+ @host = nil
+ @port = nil
+ @path = nil
+ @query = nil
+ @opaque = nil
+ @fragment = nil
+ @parser = parser == DEFAULT_PARSER ? nil : parser
+
+ if arg_check
+ self.scheme = scheme
+ self.hostname = host
+ self.port = port
+ self.userinfo = userinfo
+ self.path = path
+ self.query = query
+ self.opaque = opaque
+ self.fragment = fragment
+ else
+ self.set_scheme(scheme)
+ self.set_host(host)
+ self.set_port(port)
+ self.set_userinfo(userinfo)
+ self.set_path(path)
+ self.query = query
+ self.set_opaque(opaque)
+ self.fragment=(fragment)
+ end
+ if registry
+ raise InvalidURIError,
+ "the scheme #{@scheme} does not accept registry part: #{registry} (or bad hostname?)"
+ end
+
+ @scheme&.freeze
+ self.set_path('') if !@path && !@opaque # (see RFC2396 Section 5.2)
+ self.set_port(self.default_port) if self.default_port && !@port
+ end
+
+ #
+ # Returns the scheme component of the Gem::URI.
+ #
+ # Gem::URI("http://foo/bar/baz").scheme #=> "http"
+ #
+ attr_reader :scheme
+
+ # Returns the host component of the Gem::URI.
+ #
+ # Gem::URI("http://foo/bar/baz").host #=> "foo"
+ #
+ # It returns nil if no host component exists.
+ #
+ # Gem::URI("mailto:foo@example.org").host #=> nil
+ #
+ # The component does not contain the port number.
+ #
+ # Gem::URI("http://foo:8080/bar/baz").host #=> "foo"
+ #
+ # Since IPv6 addresses are wrapped with brackets in URIs,
+ # this method returns IPv6 addresses wrapped with brackets.
+ # This form is not appropriate to pass to socket methods such as TCPSocket.open.
+ # If unwrapped host names are required, use the #hostname method.
+ #
+ # Gem::URI("http://[::1]/bar/baz").host #=> "[::1]"
+ # Gem::URI("http://[::1]/bar/baz").hostname #=> "::1"
+ #
+ attr_reader :host
+
+ # Returns the port component of the Gem::URI.
+ #
+ # Gem::URI("http://foo/bar/baz").port #=> 80
+ # Gem::URI("http://foo:8080/bar/baz").port #=> 8080
+ #
+ attr_reader :port
+
+ def registry # :nodoc:
+ nil
+ end
+
+ # Returns the path component of the Gem::URI.
+ #
+ # Gem::URI("http://foo/bar/baz").path #=> "/bar/baz"
+ #
+ attr_reader :path
+
+ # Returns the query component of the Gem::URI.
+ #
+ # Gem::URI("http://foo/bar/baz?search=FooBar").query #=> "search=FooBar"
+ #
+ attr_reader :query
+
+ # Returns the opaque part of the Gem::URI.
+ #
+ # Gem::URI("mailto:foo@example.org").opaque #=> "foo@example.org"
+ # Gem::URI("http://foo/bar/baz").opaque #=> nil
+ #
+ # The portion of the path that does not make use of the slash '/'.
+ # The path typically refers to an absolute path or an opaque part.
+ # (See RFC2396 Section 3 and 5.2.)
+ #
+ attr_reader :opaque
+
+ # Returns the fragment component of the Gem::URI.
+ #
+ # Gem::URI("http://foo/bar/baz?search=FooBar#ponies").fragment #=> "ponies"
+ #
+ attr_reader :fragment
+
+ # Returns the parser to be used.
+ #
+ # Unless the +parser+ is defined, DEFAULT_PARSER is used.
+ #
+ def parser
+ if !defined?(@parser) || !@parser
+ DEFAULT_PARSER
+ else
+ @parser || DEFAULT_PARSER
+ end
+ end
+
+ # Replaces self by other Gem::URI object.
+ #
+ def replace!(oth)
+ if self.class != oth.class
+ raise ArgumentError, "expected #{self.class} object"
+ end
+
+ component.each do |c|
+ self.__send__("#{c}=", oth.__send__(c))
+ end
+ end
+ private :replace!
+
+ #
+ # Components of the Gem::URI in the order.
+ #
+ def component
+ self.class.component
+ end
+
+ #
+ # Checks the scheme +v+ component against the +parser+ Regexp for :SCHEME.
+ #
+ def check_scheme(v)
+ if v && parser.regexp[:SCHEME] !~ v
+ raise InvalidComponentError,
+ "bad component(expected scheme component): #{v}"
+ end
+
+ return true
+ end
+ private :check_scheme
+
+ # Protected setter for the scheme component +v+.
+ #
+ # See also Gem::URI::Generic.scheme=.
+ #
+ def set_scheme(v)
+ @scheme = v&.downcase
+ end
+ protected :set_scheme
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the scheme component +v+
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_scheme.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com")
+ # uri.scheme = "https"
+ # uri.to_s #=> "https://my.example.com"
+ #
+ def scheme=(v)
+ check_scheme(v)
+ set_scheme(v)
+ v
+ end
+
+ #
+ # Checks the +user+ and +password+.
+ #
+ # If +password+ is not provided, then +user+ is
+ # split, using Gem::URI::Generic.split_userinfo, to
+ # pull +user+ and +password.
+ #
+ # See also Gem::URI::Generic.check_user, Gem::URI::Generic.check_password.
+ #
+ def check_userinfo(user, password = nil)
+ if !password
+ user, password = split_userinfo(user)
+ end
+ check_user(user)
+ check_password(password, user)
+
+ return true
+ end
+ private :check_userinfo
+
+ #
+ # Checks the user +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp for :USERINFO.
+ #
+ # Can not have a registry or opaque component defined,
+ # with a user component defined.
+ #
+ def check_user(v)
+ if @opaque
+ raise InvalidURIError,
+ "cannot set user with opaque"
+ end
+
+ return v unless v
+
+ if parser.regexp[:USERINFO] !~ v
+ raise InvalidComponentError,
+ "bad component(expected userinfo component or user component): #{v}"
+ end
+
+ return true
+ end
+ private :check_user
+
+ #
+ # Checks the password +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp for :USERINFO.
+ #
+ # Can not have a registry or opaque component defined,
+ # with a user component defined.
+ #
+ def check_password(v, user = @user)
+ if @opaque
+ raise InvalidURIError,
+ "cannot set password with opaque"
+ end
+ return v unless v
+
+ if !user
+ raise InvalidURIError,
+ "password component depends user component"
+ end
+
+ if parser.regexp[:USERINFO] !~ v
+ raise InvalidComponentError,
+ "bad password component"
+ end
+
+ return true
+ end
+ private :check_password
+
+ #
+ # Sets userinfo, argument is string like 'name:pass'.
+ #
+ def userinfo=(userinfo)
+ if userinfo.nil?
+ return nil
+ end
+ check_userinfo(*userinfo)
+ set_userinfo(*userinfo)
+ # returns userinfo
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the +user+ component
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_user.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://john:S3nsit1ve@my.example.com")
+ # uri.user = "sam"
+ # uri.to_s #=> "http://sam:V3ry_S3nsit1ve@my.example.com"
+ #
+ def user=(user)
+ check_user(user)
+ set_user(user)
+ # returns user
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the +password+ component
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_password.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://john:S3nsit1ve@my.example.com")
+ # uri.password = "V3ry_S3nsit1ve"
+ # uri.to_s #=> "http://john:V3ry_S3nsit1ve@my.example.com"
+ #
+ def password=(password)
+ check_password(password)
+ set_password(password)
+ # returns password
+ end
+
+ # Protected setter for the +user+ component, and +password+ if available
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.userinfo=.
+ #
+ def set_userinfo(user, password = nil)
+ unless password
+ user, password = split_userinfo(user)
+ end
+ @user = user
+ @password = password
+
+ [@user, @password]
+ end
+ protected :set_userinfo
+
+ # Protected setter for the user component +v+.
+ #
+ # See also Gem::URI::Generic.user=.
+ #
+ def set_user(v)
+ set_userinfo(v, nil)
+ v
+ end
+ protected :set_user
+
+ # Protected setter for the password component +v+.
+ #
+ # See also Gem::URI::Generic.password=.
+ #
+ def set_password(v)
+ @password = v
+ # returns v
+ end
+ protected :set_password
+
+ # Returns the userinfo +ui+ as <code>[user, password]</code>
+ # if properly formatted as 'user:password'.
+ def split_userinfo(ui)
+ return nil, nil unless ui
+ user, password = ui.split(':', 2)
+
+ return user, password
+ end
+ private :split_userinfo
+
+ # Escapes 'user:password' +v+ based on RFC 1738 section 3.1.
+ def escape_userpass(v)
+ parser.escape(v, /[@:\/]/o) # RFC 1738 section 3.1 #/
+ end
+ private :escape_userpass
+
+ # Returns the userinfo, either as 'user' or 'user:password'.
+ def userinfo
+ if @user.nil?
+ nil
+ elsif @password.nil?
+ @user
+ else
+ @user + ':' + @password
+ end
+ end
+
+ # Returns the user component (without Gem::URI decoding).
+ def user
+ @user
+ end
+
+ # Returns the password component (without Gem::URI decoding).
+ def password
+ @password
+ end
+
+ # Returns the authority info (array of user, password, host and
+ # port), if any is set. Or returns +nil+.
+ def authority
+ return @user, @password, @host, @port if @user || @password || @host || @port
+ end
+
+ # Returns the user component after Gem::URI decoding.
+ def decoded_user
+ Gem::URI.decode_uri_component(@user) if @user
+ end
+
+ # Returns the password component after Gem::URI decoding.
+ def decoded_password
+ Gem::URI.decode_uri_component(@password) if @password
+ end
+
+ #
+ # Checks the host +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp for :HOST.
+ #
+ # Can not have a registry or opaque component defined,
+ # with a host component defined.
+ #
+ def check_host(v)
+ return v unless v
+
+ if @opaque
+ raise InvalidURIError,
+ "cannot set host with registry or opaque"
+ elsif parser.regexp[:HOST] !~ v
+ raise InvalidComponentError,
+ "bad component(expected host component): #{v}"
+ end
+
+ return true
+ end
+ private :check_host
+
+ # Protected setter for the host component +v+.
+ #
+ # See also Gem::URI::Generic.host=.
+ #
+ def set_host(v)
+ @host = v
+ end
+ protected :set_host
+
+ # Protected setter for the authority info (+user+, +password+, +host+
+ # and +port+). If +port+ is +nil+, +default_port+ will be set.
+ #
+ protected def set_authority(user, password, host, port = nil)
+ @user, @password, @host, @port = user, password, host, port || self.default_port
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the host component +v+
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_host.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com")
+ # uri.host = "foo.com"
+ # uri.to_s #=> "http://foo.com"
+ #
+ def host=(v)
+ check_host(v)
+ set_host(v)
+ set_userinfo(nil)
+ v
+ end
+
+ # Extract the host part of the Gem::URI and unwrap brackets for IPv6 addresses.
+ #
+ # This method is the same as Gem::URI::Generic#host except
+ # brackets for IPv6 (and future IP) addresses are removed.
+ #
+ # uri = Gem::URI("http://[::1]/bar")
+ # uri.hostname #=> "::1"
+ # uri.host #=> "[::1]"
+ #
+ def hostname
+ v = self.host
+ v&.start_with?('[') && v.end_with?(']') ? v[1..-2] : v
+ end
+
+ # Sets the host part of the Gem::URI as the argument with brackets for IPv6 addresses.
+ #
+ # This method is the same as Gem::URI::Generic#host= except
+ # the argument can be a bare IPv6 address.
+ #
+ # uri = Gem::URI("http://foo/bar")
+ # uri.hostname = "::1"
+ # uri.to_s #=> "http://[::1]/bar"
+ #
+ # If the argument seems to be an IPv6 address,
+ # it is wrapped with brackets.
+ #
+ def hostname=(v)
+ v = "[#{v}]" if !(v&.start_with?('[') && v&.end_with?(']')) && v&.index(':')
+ self.host = v
+ end
+
+ #
+ # Checks the port +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp for :PORT.
+ #
+ # Can not have a registry or opaque component defined,
+ # with a port component defined.
+ #
+ def check_port(v)
+ return v unless v
+
+ if @opaque
+ raise InvalidURIError,
+ "cannot set port with registry or opaque"
+ elsif !v.kind_of?(Integer) && parser.regexp[:PORT] !~ v
+ raise InvalidComponentError,
+ "bad component(expected port component): #{v.inspect}"
+ end
+
+ return true
+ end
+ private :check_port
+
+ # Protected setter for the port component +v+.
+ #
+ # See also Gem::URI::Generic.port=.
+ #
+ def set_port(v)
+ v = v.empty? ? nil : v.to_i unless !v || v.kind_of?(Integer)
+ @port = v
+ end
+ protected :set_port
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the port component +v+
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_port.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com")
+ # uri.port = 8080
+ # uri.to_s #=> "http://my.example.com:8080"
+ #
+ def port=(v)
+ check_port(v)
+ set_port(v)
+ set_userinfo(nil)
+ port
+ end
+
+ def check_registry(v) # :nodoc:
+ raise InvalidURIError, "cannot set registry"
+ end
+ private :check_registry
+
+ def set_registry(v) # :nodoc:
+ raise InvalidURIError, "cannot set registry"
+ end
+ protected :set_registry
+
+ def registry=(v) # :nodoc:
+ raise InvalidURIError, "cannot set registry"
+ end
+
+ #
+ # Checks the path +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp
+ # for :ABS_PATH and :REL_PATH.
+ #
+ # Can not have a opaque component defined,
+ # with a path component defined.
+ #
+ def check_path(v)
+ # raise if both hier and opaque are not nil, because:
+ # absoluteURI = scheme ":" ( hier_part | opaque_part )
+ # hier_part = ( net_path | abs_path ) [ "?" query ]
+ if v && @opaque
+ raise InvalidURIError,
+ "path conflicts with opaque"
+ end
+
+ # If scheme is ftp, path may be relative.
+ # See RFC 1738 section 3.2.2, and RFC 2396.
+ if @scheme && @scheme != "ftp"
+ if v && v != '' && parser.regexp[:ABS_PATH] !~ v
+ raise InvalidComponentError,
+ "bad component(expected absolute path component): #{v}"
+ end
+ else
+ if v && v != '' && parser.regexp[:ABS_PATH] !~ v &&
+ parser.regexp[:REL_PATH] !~ v
+ raise InvalidComponentError,
+ "bad component(expected relative path component): #{v}"
+ end
+ end
+
+ return true
+ end
+ private :check_path
+
+ # Protected setter for the path component +v+.
+ #
+ # See also Gem::URI::Generic.path=.
+ #
+ def set_path(v)
+ @path = v
+ end
+ protected :set_path
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the path component +v+
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_path.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com/pub/files")
+ # uri.path = "/faq/"
+ # uri.to_s #=> "http://my.example.com/faq/"
+ #
+ def path=(v)
+ check_path(v)
+ set_path(v)
+ v
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the query component +v+.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com/?id=25")
+ # uri.query = "id=1"
+ # uri.to_s #=> "http://my.example.com/?id=1"
+ #
+ def query=(v)
+ return @query = nil unless v
+ raise InvalidURIError, "query conflicts with opaque" if @opaque
+
+ x = v.to_str
+ v = x.dup if x.equal? v
+ v.encode!(Encoding::UTF_8) rescue nil
+ v.delete!("\t\r\n")
+ v.force_encoding(Encoding::ASCII_8BIT)
+ raise InvalidURIError, "invalid percent escape: #{$1}" if /(%\H\H)/n.match(v)
+ v.gsub!(/(?!%\h\h|[!$-&(-;=?-_a-~])./n.freeze){'%%%02X' % $&.ord}
+ v.force_encoding(Encoding::US_ASCII)
+ @query = v
+ end
+
+ #
+ # Checks the opaque +v+ component for RFC2396 compliance and
+ # against the +parser+ Regexp for :OPAQUE.
+ #
+ # Can not have a host, port, user, or path component defined,
+ # with an opaque component defined.
+ #
+ def check_opaque(v)
+ return v unless v
+
+ # raise if both hier and opaque are not nil, because:
+ # absoluteURI = scheme ":" ( hier_part | opaque_part )
+ # hier_part = ( net_path | abs_path ) [ "?" query ]
+ if @host || @port || @user || @path # userinfo = @user + ':' + @password
+ raise InvalidURIError,
+ "cannot set opaque with host, port, userinfo or path"
+ elsif v && parser.regexp[:OPAQUE] !~ v
+ raise InvalidComponentError,
+ "bad component(expected opaque component): #{v}"
+ end
+
+ return true
+ end
+ private :check_opaque
+
+ # Protected setter for the opaque component +v+.
+ #
+ # See also Gem::URI::Generic.opaque=.
+ #
+ def set_opaque(v)
+ @opaque = v
+ end
+ protected :set_opaque
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the opaque component +v+
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_opaque.
+ #
+ def opaque=(v)
+ check_opaque(v)
+ set_opaque(v)
+ v
+ end
+
+ #
+ # Checks the fragment +v+ component against the +parser+ Regexp for :FRAGMENT.
+ #
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the fragment component +v+
+ # (with validation).
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com/?id=25#time=1305212049")
+ # uri.fragment = "time=1305212086"
+ # uri.to_s #=> "http://my.example.com/?id=25#time=1305212086"
+ #
+ def fragment=(v)
+ return @fragment = nil unless v
+
+ x = v.to_str
+ v = x.dup if x.equal? v
+ v.encode!(Encoding::UTF_8) rescue nil
+ v.delete!("\t\r\n")
+ v.force_encoding(Encoding::ASCII_8BIT)
+ v.gsub!(/(?!%\h\h|[!-~])./n){'%%%02X' % $&.ord}
+ v.force_encoding(Encoding::US_ASCII)
+ @fragment = v
+ end
+
+ #
+ # Returns true if Gem::URI is hierarchical.
+ #
+ # == Description
+ #
+ # Gem::URI has components listed in order of decreasing significance from left to right,
+ # see RFC3986 https://www.rfc-editor.org/rfc/rfc3986 1.2.3.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com/")
+ # uri.hierarchical?
+ # #=> true
+ # uri = Gem::URI.parse("mailto:joe@example.com")
+ # uri.hierarchical?
+ # #=> false
+ #
+ def hierarchical?
+ if @path
+ true
+ else
+ false
+ end
+ end
+
+ #
+ # Returns true if Gem::URI has a scheme (e.g. http:// or https://) specified.
+ #
+ def absolute?
+ if @scheme
+ true
+ else
+ false
+ end
+ end
+ alias absolute absolute?
+
+ #
+ # Returns true if Gem::URI does not have a scheme (e.g. http:// or https://) specified.
+ #
+ def relative?
+ !absolute?
+ end
+
+ #
+ # Returns an Array of the path split on '/'.
+ #
+ def split_path(path)
+ path.split("/", -1)
+ end
+ private :split_path
+
+ #
+ # Merges a base path +base+, with relative path +rel+,
+ # returns a modified base path.
+ #
+ def merge_path(base, rel)
+
+ # RFC2396, Section 5.2, 5)
+ # RFC2396, Section 5.2, 6)
+ base_path = split_path(base)
+ rel_path = split_path(rel)
+
+ # RFC2396, Section 5.2, 6), a)
+ base_path << '' if base_path.last == '..'
+ while i = base_path.index('..')
+ base_path.slice!(i - 1, 2)
+ end
+
+ if (first = rel_path.first) and first.empty?
+ base_path.clear
+ rel_path.shift
+ end
+
+ # RFC2396, Section 5.2, 6), c)
+ # RFC2396, Section 5.2, 6), d)
+ rel_path.push('') if rel_path.last == '.' || rel_path.last == '..'
+ rel_path.delete('.')
+
+ # RFC2396, Section 5.2, 6), e)
+ tmp = []
+ rel_path.each do |x|
+ if x == '..' &&
+ !(tmp.empty? || tmp.last == '..')
+ tmp.pop
+ else
+ tmp << x
+ end
+ end
+
+ add_trailer_slash = !tmp.empty?
+ if base_path.empty?
+ base_path = [''] # keep '/' for root directory
+ elsif add_trailer_slash
+ base_path.pop
+ end
+ while x = tmp.shift
+ if x == '..'
+ # RFC2396, Section 4
+ # a .. or . in an absolute path has no special meaning
+ base_path.pop if base_path.size > 1
+ else
+ # if x == '..'
+ # valid absolute (but abnormal) path "/../..."
+ # else
+ # valid absolute path
+ # end
+ base_path << x
+ tmp.each {|t| base_path << t}
+ add_trailer_slash = false
+ break
+ end
+ end
+ base_path.push('') if add_trailer_slash
+
+ return base_path.join('/')
+ end
+ private :merge_path
+
+ #
+ # == Args
+ #
+ # +oth+::
+ # Gem::URI or String
+ #
+ # == Description
+ #
+ # Destructive form of #merge.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com")
+ # uri.merge!("/main.rbx?page=1")
+ # uri.to_s # => "http://my.example.com/main.rbx?page=1"
+ #
+ def merge!(oth)
+ t = merge(oth)
+ if self == t
+ nil
+ else
+ replace!(t)
+ self
+ end
+ end
+
+ #
+ # == Args
+ #
+ # +oth+::
+ # Gem::URI or String
+ #
+ # == Description
+ #
+ # Merges two URIs.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com")
+ # uri.merge("/main.rbx?page=1")
+ # # => "http://my.example.com/main.rbx?page=1"
+ #
+ def merge(oth)
+ rel = parser.__send__(:convert_to_uri, oth)
+
+ if rel.absolute?
+ #raise BadURIError, "both Gem::URI are absolute" if absolute?
+ # hmm... should return oth for usability?
+ return rel
+ end
+
+ unless self.absolute?
+ raise BadURIError, "both Gem::URI are relative"
+ end
+
+ base = self.dup
+
+ authority = rel.authority
+
+ # RFC2396, Section 5.2, 2)
+ if (rel.path.nil? || rel.path.empty?) && !authority && !rel.query
+ base.fragment=(rel.fragment) if rel.fragment
+ return base
+ end
+
+ base.query = nil
+ base.fragment=(nil)
+
+ # RFC2396, Section 5.2, 4)
+ if authority
+ base.set_authority(*authority)
+ base.set_path(rel.path)
+ elsif base.path && rel.path
+ base.set_path(merge_path(base.path, rel.path))
+ end
+
+ # RFC2396, Section 5.2, 7)
+ base.query = rel.query if rel.query
+ base.fragment=(rel.fragment) if rel.fragment
+
+ return base
+ end # merge
+ alias + merge
+
+ # :stopdoc:
+ def route_from_path(src, dst)
+ case dst
+ when src
+ # RFC2396, Section 4.2
+ return ''
+ when %r{(?:\A|/)\.\.?(?:/|\z)}
+ # dst has abnormal absolute path,
+ # like "/./", "/../", "/x/../", ...
+ return dst.dup
+ end
+
+ src_path = src.scan(%r{[^/]*/})
+ dst_path = dst.scan(%r{[^/]*/?})
+
+ # discard same parts
+ while !dst_path.empty? && dst_path.first == src_path.first
+ src_path.shift
+ dst_path.shift
+ end
+
+ tmp = dst_path.join
+
+ # calculate
+ if src_path.empty?
+ if tmp.empty?
+ return './'
+ elsif dst_path.first.include?(':') # (see RFC2396 Section 5)
+ return './' + tmp
+ else
+ return tmp
+ end
+ end
+
+ return '../' * src_path.size + tmp
+ end
+ private :route_from_path
+ # :startdoc:
+
+ # :stopdoc:
+ def route_from0(oth)
+ oth = parser.__send__(:convert_to_uri, oth)
+ if self.relative?
+ raise BadURIError,
+ "relative Gem::URI: #{self}"
+ end
+ if oth.relative?
+ raise BadURIError,
+ "relative Gem::URI: #{oth}"
+ end
+
+ if self.scheme != oth.scheme
+ return self, self.dup
+ end
+ rel = Gem::URI::Generic.new(nil, # it is relative Gem::URI
+ self.userinfo, self.host, self.port,
+ nil, self.path, self.opaque,
+ self.query, self.fragment, parser)
+
+ if rel.userinfo != oth.userinfo ||
+ rel.host.to_s.downcase != oth.host.to_s.downcase ||
+ rel.port != oth.port
+
+ if self.userinfo.nil? && self.host.nil?
+ return self, self.dup
+ end
+
+ rel.set_port(nil) if rel.port == oth.default_port
+ return rel, rel
+ end
+ rel.set_userinfo(nil)
+ rel.set_host(nil)
+ rel.set_port(nil)
+
+ if rel.path && rel.path == oth.path
+ rel.set_path('')
+ rel.query = nil if rel.query == oth.query
+ return rel, rel
+ elsif rel.opaque && rel.opaque == oth.opaque
+ rel.set_opaque('')
+ rel.query = nil if rel.query == oth.query
+ return rel, rel
+ end
+
+ # you can modify `rel', but cannot `oth'.
+ return oth, rel
+ end
+ private :route_from0
+ # :startdoc:
+
+ #
+ # == Args
+ #
+ # +oth+::
+ # Gem::URI or String
+ #
+ # == Description
+ #
+ # Calculates relative path from oth to self.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse('http://my.example.com/main.rbx?page=1')
+ # uri.route_from('http://my.example.com')
+ # #=> #<Gem::URI::Generic /main.rbx?page=1>
+ #
+ def route_from(oth)
+ # you can modify `rel', but cannot `oth'.
+ begin
+ oth, rel = route_from0(oth)
+ rescue
+ raise $!.class, $!.message
+ end
+ if oth == rel
+ return rel
+ end
+
+ rel.set_path(route_from_path(oth.path, self.path))
+ if rel.path == './' && self.query
+ # "./?foo" -> "?foo"
+ rel.set_path('')
+ end
+
+ return rel
+ end
+
+ alias - route_from
+
+ #
+ # == Args
+ #
+ # +oth+::
+ # Gem::URI or String
+ #
+ # == Description
+ #
+ # Calculates relative path to oth from self.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse('http://my.example.com')
+ # uri.route_to('http://my.example.com/main.rbx?page=1')
+ # #=> #<Gem::URI::Generic /main.rbx?page=1>
+ #
+ def route_to(oth)
+ parser.__send__(:convert_to_uri, oth).route_from(self)
+ end
+
+ #
+ # Returns normalized Gem::URI.
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # Gem::URI("HTTP://my.EXAMPLE.com").normalize
+ # #=> #<Gem::URI::HTTP http://my.example.com/>
+ #
+ # Normalization here means:
+ #
+ # * scheme and host are converted to lowercase,
+ # * an empty path component is set to "/".
+ #
+ def normalize
+ uri = dup
+ uri.normalize!
+ uri
+ end
+
+ #
+ # Destructive version of #normalize.
+ #
+ def normalize!
+ if path&.empty?
+ set_path('/')
+ end
+ if scheme && scheme != scheme.downcase
+ set_scheme(self.scheme.downcase)
+ end
+ if host && host != host.downcase
+ set_host(self.host.downcase)
+ end
+ end
+
+ #
+ # Constructs String from Gem::URI.
+ #
+ def to_s
+ str = ''.dup
+ if @scheme
+ str << @scheme
+ str << ':'
+ end
+
+ if @opaque
+ str << @opaque
+ else
+ if @host || %w[file postgres].include?(@scheme)
+ str << '//'
+ end
+ if self.userinfo
+ str << self.userinfo
+ str << '@'
+ end
+ if @host
+ str << @host
+ end
+ if @port && @port != self.default_port
+ str << ':'
+ str << @port.to_s
+ end
+ if (@host || @port) && !@path.empty? && !@path.start_with?('/')
+ str << '/'
+ end
+ str << @path
+ if @query
+ str << '?'
+ str << @query
+ end
+ end
+ if @fragment
+ str << '#'
+ str << @fragment
+ end
+ str
+ end
+ alias to_str to_s
+
+ #
+ # Compares two URIs.
+ #
+ def ==(oth)
+ if self.class == oth.class
+ self.normalize.component_ary == oth.normalize.component_ary
+ else
+ false
+ end
+ end
+
+ # Returns the hash value.
+ def hash
+ self.component_ary.hash
+ end
+
+ # Compares with _oth_ for Hash.
+ def eql?(oth)
+ self.class == oth.class &&
+ parser == oth.parser &&
+ self.component_ary.eql?(oth.component_ary)
+ end
+
+ # Returns an Array of the components defined from the COMPONENT Array.
+ def component_ary
+ component.collect do |x|
+ self.__send__(x)
+ end
+ end
+ protected :component_ary
+
+ # == Args
+ #
+ # +components+::
+ # Multiple Symbol arguments defined in Gem::URI::HTTP.
+ #
+ # == Description
+ #
+ # Selects specified components from Gem::URI.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse('http://myuser:mypass@my.example.com/test.rbx')
+ # uri.select(:userinfo, :host, :path)
+ # # => ["myuser:mypass", "my.example.com", "/test.rbx"]
+ #
+ def select(*components)
+ components.collect do |c|
+ if component.include?(c)
+ self.__send__(c)
+ else
+ raise ArgumentError,
+ "expected of components of #{self.class} (#{self.class.component.join(', ')})"
+ end
+ end
+ end
+
+ def inspect # :nodoc:
+ "#<#{self.class} #{self}>"
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # Gem::URI or String
+ #
+ # == Description
+ #
+ # Attempts to parse other Gem::URI +oth+,
+ # returns [parsed_oth, self].
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com")
+ # uri.coerce("http://foo.com")
+ # #=> [#<Gem::URI::HTTP http://foo.com>, #<Gem::URI::HTTP http://my.example.com>]
+ #
+ def coerce(oth)
+ case oth
+ when String
+ oth = parser.parse(oth)
+ else
+ super
+ end
+
+ return oth, self
+ end
+
+ # Returns a proxy Gem::URI.
+ # The proxy Gem::URI is obtained from environment variables such as http_proxy,
+ # ftp_proxy, no_proxy, etc.
+ # If there is no proper proxy, nil is returned.
+ #
+ # If the optional parameter +env+ is specified, it is used instead of ENV.
+ #
+ # Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.)
+ # are examined, too.
+ #
+ # But http_proxy and HTTP_PROXY is treated specially under CGI environment.
+ # It's because HTTP_PROXY may be set by Proxy: header.
+ # So HTTP_PROXY is not used.
+ # http_proxy is not used too if the variable is case insensitive.
+ # CGI_HTTP_PROXY can be used instead.
+ def find_proxy(env=ENV)
+ raise BadURIError, "relative Gem::URI: #{self}" if self.relative?
+ name = self.scheme.downcase + '_proxy'
+ proxy_uri = nil
+ if name == 'http_proxy' && env.include?('REQUEST_METHOD') # CGI?
+ # HTTP_PROXY conflicts with *_proxy for proxy settings and
+ # HTTP_* for header information in CGI.
+ # So it should be careful to use it.
+ pairs = env.reject {|k, v| /\Ahttp_proxy\z/i !~ k }
+ case pairs.length
+ when 0 # no proxy setting anyway.
+ proxy_uri = nil
+ when 1
+ k, _ = pairs.shift
+ if k == 'http_proxy' && env[k.upcase] == nil
+ # http_proxy is safe to use because ENV is case sensitive.
+ proxy_uri = env[name]
+ else
+ proxy_uri = nil
+ end
+ else # http_proxy is safe to use because ENV is case sensitive.
+ proxy_uri = env.to_hash[name]
+ end
+ if !proxy_uri
+ # Use CGI_HTTP_PROXY. cf. libwww-perl.
+ proxy_uri = env["CGI_#{name.upcase}"]
+ end
+ elsif name == 'http_proxy'
+ if RUBY_ENGINE == 'jruby' && p_addr = ENV_JAVA['http.proxyHost']
+ p_port = ENV_JAVA['http.proxyPort']
+ if p_user = ENV_JAVA['http.proxyUser']
+ p_pass = ENV_JAVA['http.proxyPass']
+ proxy_uri = "http://#{p_user}:#{p_pass}@#{p_addr}:#{p_port}"
+ else
+ proxy_uri = "http://#{p_addr}:#{p_port}"
+ end
+ else
+ unless proxy_uri = env[name]
+ if proxy_uri = env[name.upcase]
+ warn 'The environment variable HTTP_PROXY is discouraged. Please use http_proxy instead.', uplevel: 1
+ end
+ end
+ end
+ else
+ proxy_uri = env[name] || env[name.upcase]
+ end
+
+ if proxy_uri.nil? || proxy_uri.empty?
+ return nil
+ end
+
+ if self.hostname
+ begin
+ addr = IPSocket.getaddress(self.hostname)
+ return nil if /\A127\.|\A::1\z/ =~ addr
+ rescue SocketError
+ end
+ end
+
+ name = 'no_proxy'
+ if no_proxy = env[name] || env[name.upcase]
+ return nil unless Gem::URI::Generic.use_proxy?(self.hostname, addr, self.port, no_proxy)
+ end
+ Gem::URI.parse(proxy_uri)
+ end
+
+ def self.use_proxy?(hostname, addr, port, no_proxy) # :nodoc:
+ hostname = hostname.downcase
+ dothostname = ".#{hostname}"
+ no_proxy.scan(/([^:,\s]+)(?::(\d+))?/) {|p_host, p_port|
+ if !p_port || port == p_port.to_i
+ if p_host.start_with?('.')
+ return false if hostname.end_with?(p_host.downcase)
+ else
+ return false if dothostname.end_with?(".#{p_host.downcase}")
+ end
+ if addr
+ begin
+ return false if IPAddr.new(p_host).include?(addr)
+ rescue IPAddr::InvalidAddressError
+ next
+ end
+ end
+ end
+ }
+ true
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/http.rb b/lib/rubygems/vendor/uri/lib/uri/http.rb
new file mode 100644
index 0000000000..99c78358ac
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/http.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: false
+# = uri/http.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Gem::URI
+
+ #
+ # The syntax of HTTP URIs is defined in RFC1738 section 3.3.
+ #
+ # Note that the Ruby Gem::URI library allows HTTP URLs containing usernames and
+ # passwords. This is not legal as per the RFC, but used to be
+ # supported in Internet Explorer 5 and 6, before the MS04-004 security
+ # update. See <URL:http://support.microsoft.com/kb/834489>.
+ #
+ class HTTP < Generic
+ # A Default port of 80 for Gem::URI::HTTP.
+ DEFAULT_PORT = 80
+
+ # An Array of the available components for Gem::URI::HTTP.
+ COMPONENT = %i[
+ scheme
+ userinfo host port
+ path
+ query
+ fragment
+ ].freeze
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::HTTP object from components, with syntax checking.
+ #
+ # The components accepted are userinfo, host, port, path, query, and
+ # fragment.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[userinfo, host, port, path, query, fragment]</code>.
+ #
+ # Example:
+ #
+ # uri = Gem::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar')
+ #
+ # uri = Gem::URI::HTTP.build([nil, "www.example.com", nil, "/path",
+ # "query", 'fragment'])
+ #
+ # Currently, if passed userinfo components this method generates
+ # invalid HTTP URIs as per RFC 1738.
+ #
+ def self.build(args)
+ tmp = Util.make_components_hash(self, args)
+ super(tmp)
+ end
+
+ # Do not allow empty host names, as they are not allowed by RFC 3986.
+ def check_host(v)
+ ret = super
+
+ if ret && v.empty?
+ raise InvalidComponentError,
+ "bad component(expected host component): #{v}"
+ end
+
+ ret
+ end
+
+ #
+ # == Description
+ #
+ # Returns the full path for an HTTP request, as required by Net::HTTP::Get.
+ #
+ # If the Gem::URI contains a query, the full path is Gem::URI#path + '?' + Gem::URI#query.
+ # Otherwise, the path is simply Gem::URI#path.
+ #
+ # Example:
+ #
+ # uri = Gem::URI::HTTP.build(path: '/foo/bar', query: 'test=true')
+ # uri.request_uri # => "/foo/bar?test=true"
+ #
+ def request_uri
+ return unless @path
+
+ url = @query ? "#@path?#@query" : @path.dup
+ url.start_with?(?/.freeze) ? url : ?/ + url
+ end
+
+ #
+ # == Description
+ #
+ # Returns the authority for an HTTP uri, as defined in
+ # https://www.rfc-editor.org/rfc/rfc3986#section-3.2.
+ #
+ #
+ # Example:
+ #
+ # Gem::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar').authority #=> "www.example.com"
+ # Gem::URI::HTTP.build(host: 'www.example.com', port: 8000, path: '/foo/bar').authority #=> "www.example.com:8000"
+ # Gem::URI::HTTP.build(host: 'www.example.com', port: 80, path: '/foo/bar').authority #=> "www.example.com"
+ #
+ def authority
+ if port == default_port
+ host
+ else
+ "#{host}:#{port}"
+ end
+ end
+
+ #
+ # == Description
+ #
+ # Returns the origin for an HTTP uri, as defined in
+ # https://www.rfc-editor.org/rfc/rfc6454.
+ #
+ #
+ # Example:
+ #
+ # Gem::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar').origin #=> "http://www.example.com"
+ # Gem::URI::HTTP.build(host: 'www.example.com', port: 8000, path: '/foo/bar').origin #=> "http://www.example.com:8000"
+ # Gem::URI::HTTP.build(host: 'www.example.com', port: 80, path: '/foo/bar').origin #=> "http://www.example.com"
+ # Gem::URI::HTTPS.build(host: 'www.example.com', path: '/foo/bar').origin #=> "https://www.example.com"
+ #
+ def origin
+ "#{scheme}://#{authority}"
+ end
+ end
+
+ register_scheme 'HTTP', HTTP
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/https.rb b/lib/rubygems/vendor/uri/lib/uri/https.rb
new file mode 100644
index 0000000000..6e8e732e1d
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/https.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: false
+# = uri/https.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'http'
+
+module Gem::URI
+
+ # The default port for HTTPS URIs is 443, and the scheme is 'https:' rather
+ # than 'http:'. Other than that, HTTPS URIs are identical to HTTP URIs;
+ # see Gem::URI::HTTP.
+ class HTTPS < HTTP
+ # A Default port of 443 for Gem::URI::HTTPS
+ DEFAULT_PORT = 443
+ end
+
+ register_scheme 'HTTPS', HTTPS
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/ldap.rb b/lib/rubygems/vendor/uri/lib/uri/ldap.rb
new file mode 100644
index 0000000000..1a08b5ab7e
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/ldap.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: false
+# = uri/ldap.rb
+#
+# Author::
+# Takaaki Tateishi <ttate@jaist.ac.jp>
+# Akira Yamada <akira@ruby-lang.org>
+# License::
+# Gem::URI::LDAP is copyrighted free software by Takaaki Tateishi and Akira Yamada.
+# You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Gem::URI
+
+ #
+ # LDAP Gem::URI SCHEMA (described in RFC2255).
+ #--
+ # ldap://<host>/<dn>[?<attrs>[?<scope>[?<filter>[?<extensions>]]]]
+ #++
+ class LDAP < Generic
+
+ # A Default port of 389 for Gem::URI::LDAP.
+ DEFAULT_PORT = 389
+
+ # An Array of the available components for Gem::URI::LDAP.
+ COMPONENT = [
+ :scheme,
+ :host, :port,
+ :dn,
+ :attributes,
+ :scope,
+ :filter,
+ :extensions,
+ ].freeze
+
+ # Scopes available for the starting point.
+ #
+ # * SCOPE_BASE - the Base DN
+ # * SCOPE_ONE - one level under the Base DN, not including the base DN and
+ # not including any entries under this
+ # * SCOPE_SUB - subtrees, all entries at all levels
+ #
+ SCOPE = [
+ SCOPE_ONE = 'one',
+ SCOPE_SUB = 'sub',
+ SCOPE_BASE = 'base',
+ ].freeze
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::LDAP object from components, with syntax checking.
+ #
+ # The components accepted are host, port, dn, attributes,
+ # scope, filter, and extensions.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[host, port, dn, attributes, scope, filter, extensions]</code>.
+ #
+ # Example:
+ #
+ # uri = Gem::URI::LDAP.build({:host => 'ldap.example.com',
+ # :dn => '/dc=example'})
+ #
+ # uri = Gem::URI::LDAP.build(["ldap.example.com", nil,
+ # "/dc=example;dc=com", "query", nil, nil, nil])
+ #
+ def self.build(args)
+ tmp = Util::make_components_hash(self, args)
+
+ if tmp[:dn]
+ tmp[:path] = tmp[:dn]
+ end
+
+ query = []
+ [:extensions, :filter, :scope, :attributes].collect do |x|
+ next if !tmp[x] && query.size == 0
+ query.unshift(tmp[x])
+ end
+
+ tmp[:query] = query.join('?')
+
+ return super(tmp)
+ end
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::LDAP object from generic Gem::URI components as per
+ # RFC 2396. No LDAP-specific syntax checking is performed.
+ #
+ # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+,
+ # +opaque+, +query+, and +fragment+, in that order.
+ #
+ # Example:
+ #
+ # uri = Gem::URI::LDAP.new("ldap", nil, "ldap.example.com", nil, nil,
+ # "/dc=example;dc=com", nil, "query", nil)
+ #
+ # See also Gem::URI::Generic.new.
+ #
+ def initialize(*arg)
+ super(*arg)
+
+ if @fragment
+ raise InvalidURIError, 'bad LDAP URL'
+ end
+
+ parse_dn
+ parse_query
+ end
+
+ # Private method to cleanup +dn+ from using the +path+ component attribute.
+ def parse_dn
+ raise InvalidURIError, 'bad LDAP URL' unless @path
+ @dn = @path[1..-1]
+ end
+ private :parse_dn
+
+ # Private method to cleanup +attributes+, +scope+, +filter+, and +extensions+
+ # from using the +query+ component attribute.
+ def parse_query
+ @attributes = nil
+ @scope = nil
+ @filter = nil
+ @extensions = nil
+
+ if @query
+ attrs, scope, filter, extensions = @query.split('?')
+
+ @attributes = attrs if attrs && attrs.size > 0
+ @scope = scope if scope && scope.size > 0
+ @filter = filter if filter && filter.size > 0
+ @extensions = extensions if extensions && extensions.size > 0
+ end
+ end
+ private :parse_query
+
+ # Private method to assemble +query+ from +attributes+, +scope+, +filter+, and +extensions+.
+ def build_path_query
+ @path = '/' + @dn
+
+ query = []
+ [@extensions, @filter, @scope, @attributes].each do |x|
+ next if !x && query.size == 0
+ query.unshift(x)
+ end
+ @query = query.join('?')
+ end
+ private :build_path_query
+
+ # Returns dn.
+ def dn
+ @dn
+ end
+
+ # Private setter for dn +val+.
+ def set_dn(val)
+ @dn = val
+ build_path_query
+ @dn
+ end
+ protected :set_dn
+
+ # Setter for dn +val+.
+ def dn=(val)
+ set_dn(val)
+ val
+ end
+
+ # Returns attributes.
+ def attributes
+ @attributes
+ end
+
+ # Private setter for attributes +val+.
+ def set_attributes(val)
+ @attributes = val
+ build_path_query
+ @attributes
+ end
+ protected :set_attributes
+
+ # Setter for attributes +val+.
+ def attributes=(val)
+ set_attributes(val)
+ val
+ end
+
+ # Returns scope.
+ def scope
+ @scope
+ end
+
+ # Private setter for scope +val+.
+ def set_scope(val)
+ @scope = val
+ build_path_query
+ @scope
+ end
+ protected :set_scope
+
+ # Setter for scope +val+.
+ def scope=(val)
+ set_scope(val)
+ val
+ end
+
+ # Returns filter.
+ def filter
+ @filter
+ end
+
+ # Private setter for filter +val+.
+ def set_filter(val)
+ @filter = val
+ build_path_query
+ @filter
+ end
+ protected :set_filter
+
+ # Setter for filter +val+.
+ def filter=(val)
+ set_filter(val)
+ val
+ end
+
+ # Returns extensions.
+ def extensions
+ @extensions
+ end
+
+ # Private setter for extensions +val+.
+ def set_extensions(val)
+ @extensions = val
+ build_path_query
+ @extensions
+ end
+ protected :set_extensions
+
+ # Setter for extensions +val+.
+ def extensions=(val)
+ set_extensions(val)
+ val
+ end
+
+ # Checks if Gem::URI has a path.
+ # For Gem::URI::LDAP this will return +false+.
+ def hierarchical?
+ false
+ end
+ end
+
+ register_scheme 'LDAP', LDAP
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/ldaps.rb b/lib/rubygems/vendor/uri/lib/uri/ldaps.rb
new file mode 100644
index 0000000000..b7a5b50e27
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/ldaps.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: false
+# = uri/ldap.rb
+#
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'ldap'
+
+module Gem::URI
+
+ # The default port for LDAPS URIs is 636, and the scheme is 'ldaps:' rather
+ # than 'ldap:'. Other than that, LDAPS URIs are identical to LDAP URIs;
+ # see Gem::URI::LDAP.
+ class LDAPS < LDAP
+ # A Default port of 636 for Gem::URI::LDAPS
+ DEFAULT_PORT = 636
+ end
+
+ register_scheme 'LDAPS', LDAPS
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/mailto.rb b/lib/rubygems/vendor/uri/lib/uri/mailto.rb
new file mode 100644
index 0000000000..7ae544d194
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/mailto.rb
@@ -0,0 +1,293 @@
+# frozen_string_literal: false
+# = uri/mailto.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Gem::URI
+
+ #
+ # RFC6068, the mailto URL scheme.
+ #
+ class MailTo < Generic
+ include RFC2396_REGEXP
+
+ # A Default port of nil for Gem::URI::MailTo.
+ DEFAULT_PORT = nil
+
+ # An Array of the available components for Gem::URI::MailTo.
+ COMPONENT = [ :scheme, :to, :headers ].freeze
+
+ # :stopdoc:
+ # "hname" and "hvalue" are encodings of an RFC 822 header name and
+ # value, respectively. As with "to", all URL reserved characters must
+ # be encoded.
+ #
+ # "#mailbox" is as specified in RFC 822 [RFC822]. This means that it
+ # consists of zero or more comma-separated mail addresses, possibly
+ # including "phrase" and "comment" components. Note that all URL
+ # reserved characters in "to" must be encoded: in particular,
+ # parentheses, commas, and the percent sign ("%"), which commonly occur
+ # in the "mailbox" syntax.
+ #
+ # Within mailto URLs, the characters "?", "=", "&" are reserved.
+
+ # ; RFC 6068
+ # hfields = "?" hfield *( "&" hfield )
+ # hfield = hfname "=" hfvalue
+ # hfname = *qchar
+ # hfvalue = *qchar
+ # qchar = unreserved / pct-encoded / some-delims
+ # some-delims = "!" / "$" / "'" / "(" / ")" / "*"
+ # / "+" / "," / ";" / ":" / "@"
+ #
+ # ; RFC3986
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+ # pct-encoded = "%" HEXDIG HEXDIG
+ HEADER_REGEXP = /\A(?<hfield>(?:%\h\h|[!$'-.0-;@-Z_a-z~])*=(?:%\h\h|[!$'-.0-;@-Z_a-z~])*)(?:&\g<hfield>)*\z/
+ # practical regexp for email address
+ # https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
+ EMAIL_REGEXP = /\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z/
+ # :startdoc:
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::MailTo object from components, with syntax checking.
+ #
+ # Components can be provided as an Array or Hash. If an Array is used,
+ # the components must be supplied as <code>[to, headers]</code>.
+ #
+ # If a Hash is used, the keys are the component names preceded by colons.
+ #
+ # The headers can be supplied as a pre-encoded string, such as
+ # <code>"subject=subscribe&cc=address"</code>, or as an Array of Arrays
+ # like <code>[['subject', 'subscribe'], ['cc', 'address']]</code>.
+ #
+ # Examples:
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # m1 = Gem::URI::MailTo.build(['joe@example.com', 'subject=Ruby'])
+ # m1.to_s # => "mailto:joe@example.com?subject=Ruby"
+ #
+ # m2 = Gem::URI::MailTo.build(['john@example.com', [['Subject', 'Ruby'], ['Cc', 'jack@example.com']]])
+ # m2.to_s # => "mailto:john@example.com?Subject=Ruby&Cc=jack@example.com"
+ #
+ # m3 = Gem::URI::MailTo.build({:to => 'listman@example.com', :headers => [['subject', 'subscribe']]})
+ # m3.to_s # => "mailto:listman@example.com?subject=subscribe"
+ #
+ def self.build(args)
+ tmp = Util.make_components_hash(self, args)
+
+ case tmp[:to]
+ when Array
+ tmp[:opaque] = tmp[:to].join(',')
+ when String
+ tmp[:opaque] = tmp[:to].dup
+ else
+ tmp[:opaque] = ''
+ end
+
+ if tmp[:headers]
+ query =
+ case tmp[:headers]
+ when Array
+ tmp[:headers].collect { |x|
+ if x.kind_of?(Array)
+ x[0] + '=' + x[1..-1].join
+ else
+ x.to_s
+ end
+ }.join('&')
+ when Hash
+ tmp[:headers].collect { |h,v|
+ h + '=' + v
+ }.join('&')
+ else
+ tmp[:headers].to_s
+ end
+ unless query.empty?
+ tmp[:opaque] << '?' << query
+ end
+ end
+
+ super(tmp)
+ end
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::MailTo object from generic URL components with
+ # no syntax checking.
+ #
+ # This method is usually called from Gem::URI::parse, which checks
+ # the validity of each component.
+ #
+ def initialize(*arg)
+ super(*arg)
+
+ @to = nil
+ @headers = []
+
+ # The RFC3986 parser does not normally populate opaque
+ @opaque = "?#{@query}" if @query && !@opaque
+
+ unless @opaque
+ raise InvalidComponentError,
+ "missing opaque part for mailto URL"
+ end
+ to, header = @opaque.split('?', 2)
+ # allow semicolon as a addr-spec separator
+ # http://support.microsoft.com/kb/820868
+ unless /\A(?:[^@,;]+@[^@,;]+(?:\z|[,;]))*\z/ =~ to
+ raise InvalidComponentError,
+ "unrecognised opaque part for mailtoURL: #{@opaque}"
+ end
+
+ if arg[10] # arg_check
+ self.to = to
+ self.headers = header
+ else
+ set_to(to)
+ set_headers(header)
+ end
+ end
+
+ # The primary e-mail address of the URL, as a String.
+ attr_reader :to
+
+ # E-mail headers set by the URL, as an Array of Arrays.
+ attr_reader :headers
+
+ # Checks the to +v+ component.
+ def check_to(v)
+ return true unless v
+ return true if v.size == 0
+
+ v.split(/[,;]/).each do |addr|
+ # check url safety as path-rootless
+ if /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*\z/ !~ addr
+ raise InvalidComponentError,
+ "an address in 'to' is invalid as Gem::URI #{addr.dump}"
+ end
+
+ # check addr-spec
+ # don't s/\+/ /g
+ addr.gsub!(/%\h\h/, Gem::URI::TBLDECWWWCOMP_)
+ if EMAIL_REGEXP !~ addr
+ raise InvalidComponentError,
+ "an address in 'to' is invalid as uri-escaped addr-spec #{addr.dump}"
+ end
+ end
+
+ true
+ end
+ private :check_to
+
+ # Private setter for to +v+.
+ def set_to(v)
+ @to = v
+ end
+ protected :set_to
+
+ # Setter for to +v+.
+ def to=(v)
+ check_to(v)
+ set_to(v)
+ v
+ end
+
+ # Checks the headers +v+ component against either
+ # * HEADER_REGEXP
+ def check_headers(v)
+ return true unless v
+ return true if v.size == 0
+ if HEADER_REGEXP !~ v
+ raise InvalidComponentError,
+ "bad component(expected opaque component): #{v}"
+ end
+
+ true
+ end
+ private :check_headers
+
+ # Private setter for headers +v+.
+ def set_headers(v)
+ @headers = []
+ if v
+ v.split('&').each do |x|
+ @headers << x.split(/=/, 2)
+ end
+ end
+ end
+ protected :set_headers
+
+ # Setter for headers +v+.
+ def headers=(v)
+ check_headers(v)
+ set_headers(v)
+ v
+ end
+
+ # Constructs String from Gem::URI.
+ def to_s
+ @scheme + ':' +
+ if @to
+ @to
+ else
+ ''
+ end +
+ if @headers.size > 0
+ '?' + @headers.collect{|x| x.join('=')}.join('&')
+ else
+ ''
+ end +
+ if @fragment
+ '#' + @fragment
+ else
+ ''
+ end
+ end
+
+ # Returns the RFC822 e-mail text equivalent of the URL, as a String.
+ #
+ # Example:
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("mailto:ruby-list@ruby-lang.org?Subject=subscribe&cc=myaddr")
+ # uri.to_mailtext
+ # # => "To: ruby-list@ruby-lang.org\nSubject: subscribe\nCc: myaddr\n\n\n"
+ #
+ def to_mailtext
+ to = Gem::URI.decode_www_form_component(@to)
+ head = ''
+ body = ''
+ @headers.each do |x|
+ case x[0]
+ when 'body'
+ body = Gem::URI.decode_www_form_component(x[1])
+ when 'to'
+ to << ', ' + Gem::URI.decode_www_form_component(x[1])
+ else
+ head << Gem::URI.decode_www_form_component(x[0]).capitalize + ': ' +
+ Gem::URI.decode_www_form_component(x[1]) + "\n"
+ end
+ end
+
+ "To: #{to}
+#{head}
+#{body}
+"
+ end
+ alias to_rfc822text to_mailtext
+ end
+
+ register_scheme 'MAILTO', MailTo
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb b/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb
new file mode 100644
index 0000000000..2bb4181649
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb
@@ -0,0 +1,547 @@
+# frozen_string_literal: false
+#--
+# = uri/common.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License::
+# You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+module Gem::URI
+ #
+ # Includes Gem::URI::REGEXP::PATTERN
+ #
+ module RFC2396_REGEXP
+ #
+ # Patterns used to parse Gem::URI's
+ #
+ module PATTERN
+ # :stopdoc:
+
+ # RFC 2396 (Gem::URI Generic Syntax)
+ # RFC 2732 (IPv6 Literal Addresses in URL's)
+ # RFC 2373 (IPv6 Addressing Architecture)
+
+ # alpha = lowalpha | upalpha
+ ALPHA = "a-zA-Z"
+ # alphanum = alpha | digit
+ ALNUM = "#{ALPHA}\\d"
+
+ # hex = digit | "A" | "B" | "C" | "D" | "E" | "F" |
+ # "a" | "b" | "c" | "d" | "e" | "f"
+ HEX = "a-fA-F\\d"
+ # escaped = "%" hex hex
+ ESCAPED = "%[#{HEX}]{2}"
+ # mark = "-" | "_" | "." | "!" | "~" | "*" | "'" |
+ # "(" | ")"
+ # unreserved = alphanum | mark
+ UNRESERVED = "\\-_.!~*'()#{ALNUM}"
+ # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
+ # "$" | ","
+ # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
+ # "$" | "," | "[" | "]" (RFC 2732)
+ RESERVED = ";/?:@&=+$,\\[\\]"
+
+ # domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
+ DOMLABEL = "(?:[#{ALNUM}](?:[-#{ALNUM}]*[#{ALNUM}])?)"
+ # toplabel = alpha | alpha *( alphanum | "-" ) alphanum
+ TOPLABEL = "(?:[#{ALPHA}](?:[-#{ALNUM}]*[#{ALNUM}])?)"
+ # hostname = *( domainlabel "." ) toplabel [ "." ]
+ HOSTNAME = "(?:#{DOMLABEL}\\.)*#{TOPLABEL}\\.?"
+
+ # :startdoc:
+ end # PATTERN
+
+ # :startdoc:
+ end # REGEXP
+
+ # Class that parses String's into Gem::URI's.
+ #
+ # It contains a Hash set of patterns and Regexp's that match and validate.
+ #
+ class RFC2396_Parser
+ include RFC2396_REGEXP
+
+ #
+ # == Synopsis
+ #
+ # Gem::URI::RFC2396_Parser.new([opts])
+ #
+ # == Args
+ #
+ # The constructor accepts a hash as options for parser.
+ # Keys of options are pattern names of Gem::URI components
+ # and values of options are pattern strings.
+ # The constructor generates set of regexps for parsing URIs.
+ #
+ # You can use the following keys:
+ #
+ # * :ESCAPED (Gem::URI::PATTERN::ESCAPED in default)
+ # * :UNRESERVED (Gem::URI::PATTERN::UNRESERVED in default)
+ # * :DOMLABEL (Gem::URI::PATTERN::DOMLABEL in default)
+ # * :TOPLABEL (Gem::URI::PATTERN::TOPLABEL in default)
+ # * :HOSTNAME (Gem::URI::PATTERN::HOSTNAME in default)
+ #
+ # == Examples
+ #
+ # p = Gem::URI::RFC2396_Parser.new(:ESCAPED => "(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})")
+ # u = p.parse("http://example.jp/%uABCD") #=> #<Gem::URI::HTTP http://example.jp/%uABCD>
+ # Gem::URI.parse(u.to_s) #=> raises Gem::URI::InvalidURIError
+ #
+ # s = "http://example.com/ABCD"
+ # u1 = p.parse(s) #=> #<Gem::URI::HTTP http://example.com/ABCD>
+ # u2 = Gem::URI.parse(s) #=> #<Gem::URI::HTTP http://example.com/ABCD>
+ # u1 == u2 #=> true
+ # u1.eql?(u2) #=> false
+ #
+ def initialize(opts = {})
+ @pattern = initialize_pattern(opts)
+ @pattern.each_value(&:freeze)
+ @pattern.freeze
+
+ @regexp = initialize_regexp(@pattern)
+ @regexp.each_value(&:freeze)
+ @regexp.freeze
+ end
+
+ # The Hash of patterns.
+ #
+ # See also #initialize_pattern.
+ attr_reader :pattern
+
+ # The Hash of Regexp.
+ #
+ # See also #initialize_regexp.
+ attr_reader :regexp
+
+ # Returns a split Gem::URI against +regexp[:ABS_URI]+.
+ def split(uri)
+ case uri
+ when ''
+ # null uri
+
+ when @regexp[:ABS_URI]
+ scheme, opaque, userinfo, host, port,
+ registry, path, query, fragment = $~[1..-1]
+
+ # Gem::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+
+ # absoluteURI = scheme ":" ( hier_part | opaque_part )
+ # hier_part = ( net_path | abs_path ) [ "?" query ]
+ # opaque_part = uric_no_slash *uric
+
+ # abs_path = "/" path_segments
+ # net_path = "//" authority [ abs_path ]
+
+ # authority = server | reg_name
+ # server = [ [ userinfo "@" ] hostport ]
+
+ if !scheme
+ raise InvalidURIError,
+ "bad Gem::URI (absolute but no scheme): #{uri}"
+ end
+ if !opaque && (!path && (!host && !registry))
+ raise InvalidURIError,
+ "bad Gem::URI (absolute but no path): #{uri}"
+ end
+
+ when @regexp[:REL_URI]
+ scheme = nil
+ opaque = nil
+
+ userinfo, host, port, registry,
+ rel_segment, abs_path, query, fragment = $~[1..-1]
+ if rel_segment && abs_path
+ path = rel_segment + abs_path
+ elsif rel_segment
+ path = rel_segment
+ elsif abs_path
+ path = abs_path
+ end
+
+ # Gem::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+
+ # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ]
+
+ # net_path = "//" authority [ abs_path ]
+ # abs_path = "/" path_segments
+ # rel_path = rel_segment [ abs_path ]
+
+ # authority = server | reg_name
+ # server = [ [ userinfo "@" ] hostport ]
+
+ else
+ raise InvalidURIError, "bad Gem::URI (is not Gem::URI?): #{uri}"
+ end
+
+ path = '' if !path && !opaque # (see RFC2396 Section 5.2)
+ ret = [
+ scheme,
+ userinfo, host, port, # X
+ registry, # X
+ path, # Y
+ opaque, # Y
+ query,
+ fragment
+ ]
+ return ret
+ end
+
+ #
+ # == Args
+ #
+ # +uri+::
+ # String
+ #
+ # == Description
+ #
+ # Parses +uri+ and constructs either matching Gem::URI scheme object
+ # (File, FTP, HTTP, HTTPS, LDAP, LDAPS, or MailTo) or Gem::URI::Generic.
+ #
+ # == Usage
+ #
+ # Gem::URI::RFC2396_PARSER.parse("ldap://ldap.example.com/dc=example?user=john")
+ # #=> #<Gem::URI::LDAP ldap://ldap.example.com/dc=example?user=john>
+ #
+ def parse(uri)
+ Gem::URI.for(*self.split(uri), self)
+ end
+
+ #
+ # == Args
+ #
+ # +uris+::
+ # an Array of Strings
+ #
+ # == Description
+ #
+ # Attempts to parse and merge a set of URIs.
+ #
+ def join(*uris)
+ uris[0] = convert_to_uri(uris[0])
+ uris.inject :merge
+ end
+
+ #
+ # :call-seq:
+ # extract( str )
+ # extract( str, schemes )
+ # extract( str, schemes ) {|item| block }
+ #
+ # == Args
+ #
+ # +str+::
+ # String to search
+ # +schemes+::
+ # Patterns to apply to +str+
+ #
+ # == Description
+ #
+ # Attempts to parse and merge a set of URIs.
+ # If no +block+ given, then returns the result,
+ # else it calls +block+ for each element in result.
+ #
+ # See also #make_regexp.
+ #
+ def extract(str, schemes = nil)
+ if block_given?
+ str.scan(make_regexp(schemes)) { yield $& }
+ nil
+ else
+ result = []
+ str.scan(make_regexp(schemes)) { result.push $& }
+ result
+ end
+ end
+
+ # Returns Regexp that is default +self.regexp[:ABS_URI_REF]+,
+ # unless +schemes+ is provided. Then it is a Regexp.union with +self.pattern[:X_ABS_URI]+.
+ def make_regexp(schemes = nil)
+ unless schemes
+ @regexp[:ABS_URI_REF]
+ else
+ /(?=(?i:#{Regexp.union(*schemes).source}):)#{@pattern[:X_ABS_URI]}/x
+ end
+ end
+
+ #
+ # :call-seq:
+ # escape( str )
+ # escape( str, unsafe )
+ #
+ # == Args
+ #
+ # +str+::
+ # String to make safe
+ # +unsafe+::
+ # Regexp to apply. Defaults to +self.regexp[:UNSAFE]+
+ #
+ # == Description
+ #
+ # Constructs a safe String from +str+, removing unsafe characters,
+ # replacing them with codes.
+ #
+ def escape(str, unsafe = @regexp[:UNSAFE])
+ unless unsafe.kind_of?(Regexp)
+ # perhaps unsafe is String object
+ unsafe = Regexp.new("[#{Regexp.quote(unsafe)}]", false)
+ end
+ str.gsub(unsafe) do
+ us = $&
+ tmp = ''
+ us.each_byte do |uc|
+ tmp << sprintf('%%%02X', uc)
+ end
+ tmp
+ end.force_encoding(Encoding::US_ASCII)
+ end
+
+ #
+ # :call-seq:
+ # unescape( str )
+ # unescape( str, escaped )
+ #
+ # == Args
+ #
+ # +str+::
+ # String to remove escapes from
+ # +escaped+::
+ # Regexp to apply. Defaults to +self.regexp[:ESCAPED]+
+ #
+ # == Description
+ #
+ # Removes escapes from +str+.
+ #
+ def unescape(str, escaped = @regexp[:ESCAPED])
+ enc = str.encoding
+ enc = Encoding::UTF_8 if enc == Encoding::US_ASCII
+ str.gsub(escaped) { [$&[1, 2]].pack('H2').force_encoding(enc) }
+ end
+
+ TO_S = Kernel.instance_method(:to_s) # :nodoc:
+ if TO_S.respond_to?(:bind_call)
+ def inspect # :nodoc:
+ TO_S.bind_call(self)
+ end
+ else
+ def inspect # :nodoc:
+ TO_S.bind(self).call
+ end
+ end
+
+ private
+
+ # Constructs the default Hash of patterns.
+ def initialize_pattern(opts = {})
+ ret = {}
+ ret[:ESCAPED] = escaped = (opts.delete(:ESCAPED) || PATTERN::ESCAPED)
+ ret[:UNRESERVED] = unreserved = opts.delete(:UNRESERVED) || PATTERN::UNRESERVED
+ ret[:RESERVED] = reserved = opts.delete(:RESERVED) || PATTERN::RESERVED
+ ret[:DOMLABEL] = opts.delete(:DOMLABEL) || PATTERN::DOMLABEL
+ ret[:TOPLABEL] = opts.delete(:TOPLABEL) || PATTERN::TOPLABEL
+ ret[:HOSTNAME] = hostname = opts.delete(:HOSTNAME)
+
+ # RFC 2396 (Gem::URI Generic Syntax)
+ # RFC 2732 (IPv6 Literal Addresses in URL's)
+ # RFC 2373 (IPv6 Addressing Architecture)
+
+ # uric = reserved | unreserved | escaped
+ ret[:URIC] = uric = "(?:[#{unreserved}#{reserved}]|#{escaped})"
+ # uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" |
+ # "&" | "=" | "+" | "$" | ","
+ ret[:URIC_NO_SLASH] = uric_no_slash = "(?:[#{unreserved};?:@&=+$,]|#{escaped})"
+ # query = *uric
+ ret[:QUERY] = query = "#{uric}*"
+ # fragment = *uric
+ ret[:FRAGMENT] = fragment = "#{uric}*"
+
+ # hostname = *( domainlabel "." ) toplabel [ "." ]
+ # reg-name = *( unreserved / pct-encoded / sub-delims ) # RFC3986
+ unless hostname
+ ret[:HOSTNAME] = hostname = "(?:[a-zA-Z0-9\\-.]|%\\h\\h)+"
+ end
+
+ # RFC 2373, APPENDIX B:
+ # IPv6address = hexpart [ ":" IPv4address ]
+ # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
+ # hexpart = hexseq | hexseq "::" [ hexseq ] | "::" [ hexseq ]
+ # hexseq = hex4 *( ":" hex4)
+ # hex4 = 1*4HEXDIG
+ #
+ # XXX: This definition has a flaw. "::" + IPv4address must be
+ # allowed too. Here is a replacement.
+ #
+ # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
+ ret[:IPV4ADDR] = ipv4addr = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"
+ # hex4 = 1*4HEXDIG
+ hex4 = "[#{PATTERN::HEX}]{1,4}"
+ # lastpart = hex4 | IPv4address
+ lastpart = "(?:#{hex4}|#{ipv4addr})"
+ # hexseq1 = *( hex4 ":" ) hex4
+ hexseq1 = "(?:#{hex4}:)*#{hex4}"
+ # hexseq2 = *( hex4 ":" ) lastpart
+ hexseq2 = "(?:#{hex4}:)*#{lastpart}"
+ # IPv6address = hexseq2 | [ hexseq1 ] "::" [ hexseq2 ]
+ ret[:IPV6ADDR] = ipv6addr = "(?:#{hexseq2}|(?:#{hexseq1})?::(?:#{hexseq2})?)"
+
+ # IPv6prefix = ( hexseq1 | [ hexseq1 ] "::" [ hexseq1 ] ) "/" 1*2DIGIT
+ # unused
+
+ # ipv6reference = "[" IPv6address "]" (RFC 2732)
+ ret[:IPV6REF] = ipv6ref = "\\[#{ipv6addr}\\]"
+
+ # host = hostname | IPv4address
+ # host = hostname | IPv4address | IPv6reference (RFC 2732)
+ ret[:HOST] = host = "(?:#{hostname}|#{ipv4addr}|#{ipv6ref})"
+ # port = *digit
+ ret[:PORT] = port = '\d*'
+ # hostport = host [ ":" port ]
+ ret[:HOSTPORT] = hostport = "#{host}(?::#{port})?"
+
+ # userinfo = *( unreserved | escaped |
+ # ";" | ":" | "&" | "=" | "+" | "$" | "," )
+ ret[:USERINFO] = userinfo = "(?:[#{unreserved};:&=+$,]|#{escaped})*"
+
+ # pchar = unreserved | escaped |
+ # ":" | "@" | "&" | "=" | "+" | "$" | ","
+ pchar = "(?:[#{unreserved}:@&=+$,]|#{escaped})"
+ # param = *pchar
+ param = "#{pchar}*"
+ # segment = *pchar *( ";" param )
+ segment = "#{pchar}*(?:;#{param})*"
+ # path_segments = segment *( "/" segment )
+ ret[:PATH_SEGMENTS] = path_segments = "#{segment}(?:/#{segment})*"
+
+ # server = [ [ userinfo "@" ] hostport ]
+ server = "(?:#{userinfo}@)?#{hostport}"
+ # reg_name = 1*( unreserved | escaped | "$" | "," |
+ # ";" | ":" | "@" | "&" | "=" | "+" )
+ ret[:REG_NAME] = reg_name = "(?:[#{unreserved}$,;:@&=+]|#{escaped})+"
+ # authority = server | reg_name
+ authority = "(?:#{server}|#{reg_name})"
+
+ # rel_segment = 1*( unreserved | escaped |
+ # ";" | "@" | "&" | "=" | "+" | "$" | "," )
+ ret[:REL_SEGMENT] = rel_segment = "(?:[#{unreserved};@&=+$,]|#{escaped})+"
+
+ # scheme = alpha *( alpha | digit | "+" | "-" | "." )
+ ret[:SCHEME] = scheme = "[#{PATTERN::ALPHA}][\\-+.#{PATTERN::ALPHA}\\d]*"
+
+ # abs_path = "/" path_segments
+ ret[:ABS_PATH] = abs_path = "/#{path_segments}"
+ # rel_path = rel_segment [ abs_path ]
+ ret[:REL_PATH] = rel_path = "#{rel_segment}(?:#{abs_path})?"
+ # net_path = "//" authority [ abs_path ]
+ ret[:NET_PATH] = net_path = "//#{authority}(?:#{abs_path})?"
+
+ # hier_part = ( net_path | abs_path ) [ "?" query ]
+ ret[:HIER_PART] = hier_part = "(?:#{net_path}|#{abs_path})(?:\\?(?:#{query}))?"
+ # opaque_part = uric_no_slash *uric
+ ret[:OPAQUE_PART] = opaque_part = "#{uric_no_slash}#{uric}*"
+
+ # absoluteURI = scheme ":" ( hier_part | opaque_part )
+ ret[:ABS_URI] = abs_uri = "#{scheme}:(?:#{hier_part}|#{opaque_part})"
+ # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ]
+ ret[:REL_URI] = rel_uri = "(?:#{net_path}|#{abs_path}|#{rel_path})(?:\\?#{query})?"
+
+ # Gem::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+ ret[:URI_REF] = "(?:#{abs_uri}|#{rel_uri})?(?:##{fragment})?"
+
+ ret[:X_ABS_URI] = "
+ (#{scheme}): (?# 1: scheme)
+ (?:
+ (#{opaque_part}) (?# 2: opaque)
+ |
+ (?:(?:
+ //(?:
+ (?:(?:(#{userinfo})@)? (?# 3: userinfo)
+ (?:(#{host})(?::(\\d*))?))? (?# 4: host, 5: port)
+ |
+ (#{reg_name}) (?# 6: registry)
+ )
+ |
+ (?!//)) (?# XXX: '//' is the mark for hostport)
+ (#{abs_path})? (?# 7: path)
+ )(?:\\?(#{query}))? (?# 8: query)
+ )
+ (?:\\#(#{fragment}))? (?# 9: fragment)
+ "
+
+ ret[:X_REL_URI] = "
+ (?:
+ (?:
+ //
+ (?:
+ (?:(#{userinfo})@)? (?# 1: userinfo)
+ (#{host})?(?::(\\d*))? (?# 2: host, 3: port)
+ |
+ (#{reg_name}) (?# 4: registry)
+ )
+ )
+ |
+ (#{rel_segment}) (?# 5: rel_segment)
+ )?
+ (#{abs_path})? (?# 6: abs_path)
+ (?:\\?(#{query}))? (?# 7: query)
+ (?:\\#(#{fragment}))? (?# 8: fragment)
+ "
+
+ ret
+ end
+
+ # Constructs the default Hash of Regexp's.
+ def initialize_regexp(pattern)
+ ret = {}
+
+ # for Gem::URI::split
+ ret[:ABS_URI] = Regexp.new('\A\s*+' + pattern[:X_ABS_URI] + '\s*\z', Regexp::EXTENDED)
+ ret[:REL_URI] = Regexp.new('\A\s*+' + pattern[:X_REL_URI] + '\s*\z', Regexp::EXTENDED)
+
+ # for Gem::URI::extract
+ ret[:URI_REF] = Regexp.new(pattern[:URI_REF])
+ ret[:ABS_URI_REF] = Regexp.new(pattern[:X_ABS_URI], Regexp::EXTENDED)
+ ret[:REL_URI_REF] = Regexp.new(pattern[:X_REL_URI], Regexp::EXTENDED)
+
+ # for Gem::URI::escape/unescape
+ ret[:ESCAPED] = Regexp.new(pattern[:ESCAPED])
+ ret[:UNSAFE] = Regexp.new("[^#{pattern[:UNRESERVED]}#{pattern[:RESERVED]}]")
+
+ # for Generic#initialize
+ ret[:SCHEME] = Regexp.new("\\A#{pattern[:SCHEME]}\\z")
+ ret[:USERINFO] = Regexp.new("\\A#{pattern[:USERINFO]}\\z")
+ ret[:HOST] = Regexp.new("\\A#{pattern[:HOST]}\\z")
+ ret[:PORT] = Regexp.new("\\A#{pattern[:PORT]}\\z")
+ ret[:OPAQUE] = Regexp.new("\\A#{pattern[:OPAQUE_PART]}\\z")
+ ret[:REGISTRY] = Regexp.new("\\A#{pattern[:REG_NAME]}\\z")
+ ret[:ABS_PATH] = Regexp.new("\\A#{pattern[:ABS_PATH]}\\z")
+ ret[:REL_PATH] = Regexp.new("\\A#{pattern[:REL_PATH]}\\z")
+ ret[:QUERY] = Regexp.new("\\A#{pattern[:QUERY]}\\z")
+ ret[:FRAGMENT] = Regexp.new("\\A#{pattern[:FRAGMENT]}\\z")
+
+ ret
+ end
+
+ # Returns +uri+ as-is if it is Gem::URI, or convert it to Gem::URI if it is
+ # a String.
+ def convert_to_uri(uri)
+ if uri.is_a?(Gem::URI::Generic)
+ uri
+ elsif uri = String.try_convert(uri)
+ parse(uri)
+ else
+ raise ArgumentError,
+ "bad argument (expected Gem::URI object or Gem::URI string)"
+ end
+ end
+
+ end # class Parser
+
+ # Backward compatibility for Gem::URI::REGEXP::PATTERN::*
+ RFC2396_Parser.new.pattern.each_pair do |sym, str|
+ unless RFC2396_REGEXP::PATTERN.const_defined?(sym, false)
+ RFC2396_REGEXP::PATTERN.const_set(sym, str)
+ end
+ end
+end # module Gem::URI
diff --git a/lib/rubygems/vendor/uri/lib/uri/rfc3986_parser.rb b/lib/rubygems/vendor/uri/lib/uri/rfc3986_parser.rb
new file mode 100644
index 0000000000..3b6961abf6
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/rfc3986_parser.rb
@@ -0,0 +1,206 @@
+# frozen_string_literal: true
+module Gem::URI
+ class RFC3986_Parser # :nodoc:
+ # Gem::URI defined in RFC3986
+ HOST = %r[
+ (?<IP-literal>\[(?:
+ (?<IPv6address>
+ (?:\h{1,4}:){6}
+ (?<ls32>\h{1,4}:\h{1,4}
+ | (?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)
+ \.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>)
+ )
+ | ::(?:\h{1,4}:){5}\g<ls32>
+ | \h{1,4}?::(?:\h{1,4}:){4}\g<ls32>
+ | (?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>
+ | (?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>
+ | (?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>
+ | (?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>
+ | (?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}
+ | (?:(?:\h{1,4}:){,6}\h{1,4})?::
+ )
+ | (?<IPvFuture>v\h++\.[!$&-.0-9:;=A-Z_a-z~]++)
+ )\])
+ | \g<IPv4address>
+ | (?<reg-name>(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])*+)
+ ]x
+
+ USERINFO = /(?:%\h\h|[!$&-.0-9:;=A-Z_a-z~])*+/
+
+ SCHEME = %r[[A-Za-z][+\-.0-9A-Za-z]*+].source
+ SEG = %r[(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/])].source
+ SEG_NC = %r[(?:%\h\h|[!$&-.0-9;=@A-Z_a-z~])].source
+ FRAGMENT = %r[(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/?])*+].source
+
+ RFC3986_URI = %r[\A
+ (?<seg>#{SEG}){0}
+ (?<Gem::URI>
+ (?<scheme>#{SCHEME}):
+ (?<hier-part>//
+ (?<authority>
+ (?:(?<userinfo>#{USERINFO.source})@)?
+ (?<host>#{HOST.source.delete(" \n")})
+ (?::(?<port>\d*+))?
+ )
+ (?<path-abempty>(?:/\g<seg>*+)?)
+ | (?<path-absolute>/((?!/)\g<seg>++)?)
+ | (?<path-rootless>(?!/)\g<seg>++)
+ | (?<path-empty>)
+ )
+ (?:\?(?<query>[^\#]*+))?
+ (?:\#(?<fragment>#{FRAGMENT}))?
+ )\z]x
+
+ RFC3986_relative_ref = %r[\A
+ (?<seg>#{SEG}){0}
+ (?<relative-ref>
+ (?<relative-part>//
+ (?<authority>
+ (?:(?<userinfo>#{USERINFO.source})@)?
+ (?<host>#{HOST.source.delete(" \n")}(?<!/))?
+ (?::(?<port>\d*+))?
+ )
+ (?<path-abempty>(?:/\g<seg>*+)?)
+ | (?<path-absolute>/\g<seg>*+)
+ | (?<path-noscheme>#{SEG_NC}++(?:/\g<seg>*+)?)
+ | (?<path-empty>)
+ )
+ (?:\?(?<query>[^#]*+))?
+ (?:\#(?<fragment>#{FRAGMENT}))?
+ )\z]x
+ attr_reader :regexp
+
+ def initialize
+ @regexp = default_regexp.each_value(&:freeze).freeze
+ end
+
+ def split(uri) #:nodoc:
+ begin
+ uri = uri.to_str
+ rescue NoMethodError
+ raise InvalidURIError, "bad Gem::URI (is not Gem::URI?): #{uri.inspect}"
+ end
+ uri.ascii_only? or
+ raise InvalidURIError, "Gem::URI must be ascii only #{uri.dump}"
+ if m = RFC3986_URI.match(uri)
+ query = m["query"]
+ scheme = m["scheme"]
+ opaque = m["path-rootless"]
+ if opaque
+ opaque << "?#{query}" if query
+ [ scheme,
+ nil, # userinfo
+ nil, # host
+ nil, # port
+ nil, # registry
+ nil, # path
+ opaque,
+ nil, # query
+ m["fragment"]
+ ]
+ else # normal
+ [ scheme,
+ m["userinfo"],
+ m["host"],
+ m["port"],
+ nil, # registry
+ (m["path-abempty"] ||
+ m["path-absolute"] ||
+ m["path-empty"]),
+ nil, # opaque
+ query,
+ m["fragment"]
+ ]
+ end
+ elsif m = RFC3986_relative_ref.match(uri)
+ [ nil, # scheme
+ m["userinfo"],
+ m["host"],
+ m["port"],
+ nil, # registry,
+ (m["path-abempty"] ||
+ m["path-absolute"] ||
+ m["path-noscheme"] ||
+ m["path-empty"]),
+ nil, # opaque
+ m["query"],
+ m["fragment"]
+ ]
+ else
+ raise InvalidURIError, "bad Gem::URI (is not Gem::URI?): #{uri.inspect}"
+ end
+ end
+
+ def parse(uri) # :nodoc:
+ Gem::URI.for(*self.split(uri), self)
+ end
+
+ def join(*uris) # :nodoc:
+ uris[0] = convert_to_uri(uris[0])
+ uris.inject :merge
+ end
+
+ # Compatibility for RFC2396 parser
+ def extract(str, schemes = nil, &block) # :nodoc:
+ warn "Gem::URI::RFC3986_PARSER.extract is obsolete. Use Gem::URI::RFC2396_PARSER.extract explicitly.", uplevel: 1 if $VERBOSE
+ RFC2396_PARSER.extract(str, schemes, &block)
+ end
+
+ # Compatibility for RFC2396 parser
+ def make_regexp(schemes = nil) # :nodoc:
+ warn "Gem::URI::RFC3986_PARSER.make_regexp is obsolete. Use Gem::URI::RFC2396_PARSER.make_regexp explicitly.", uplevel: 1 if $VERBOSE
+ RFC2396_PARSER.make_regexp(schemes)
+ end
+
+ # Compatibility for RFC2396 parser
+ def escape(str, unsafe = nil) # :nodoc:
+ warn "Gem::URI::RFC3986_PARSER.escape is obsolete. Use Gem::URI::RFC2396_PARSER.escape explicitly.", uplevel: 1 if $VERBOSE
+ unsafe ? RFC2396_PARSER.escape(str, unsafe) : RFC2396_PARSER.escape(str)
+ end
+
+ # Compatibility for RFC2396 parser
+ def unescape(str, escaped = nil) # :nodoc:
+ warn "Gem::URI::RFC3986_PARSER.unescape is obsolete. Use Gem::URI::RFC2396_PARSER.unescape explicitly.", uplevel: 1 if $VERBOSE
+ escaped ? RFC2396_PARSER.unescape(str, escaped) : RFC2396_PARSER.unescape(str)
+ end
+
+ @@to_s = Kernel.instance_method(:to_s)
+ if @@to_s.respond_to?(:bind_call)
+ def inspect
+ @@to_s.bind_call(self)
+ end
+ else
+ def inspect
+ @@to_s.bind(self).call
+ end
+ end
+
+ private
+
+ def default_regexp # :nodoc:
+ {
+ SCHEME: %r[\A#{SCHEME}\z]o,
+ USERINFO: %r[\A#{USERINFO}\z]o,
+ HOST: %r[\A#{HOST}\z]o,
+ ABS_PATH: %r[\A/#{SEG}*+\z]o,
+ REL_PATH: %r[\A(?!/)#{SEG}++\z]o,
+ QUERY: %r[\A(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/?])*+\z],
+ FRAGMENT: %r[\A#{FRAGMENT}\z]o,
+ OPAQUE: %r[\A(?:[^/].*)?\z],
+ PORT: /\A[\x09\x0a\x0c\x0d ]*+\d*[\x09\x0a\x0c\x0d ]*\z/,
+ }
+ end
+
+ def convert_to_uri(uri)
+ if uri.is_a?(Gem::URI::Generic)
+ uri
+ elsif uri = String.try_convert(uri)
+ parse(uri)
+ else
+ raise ArgumentError,
+ "bad argument (expected Gem::URI object or Gem::URI string)"
+ end
+ end
+
+ end # class Parser
+end # module Gem::URI
diff --git a/lib/rubygems/vendor/uri/lib/uri/version.rb b/lib/rubygems/vendor/uri/lib/uri/version.rb
new file mode 100644
index 0000000000..7ee577887b
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/version.rb
@@ -0,0 +1,6 @@
+module Gem::URI
+ # :stopdoc:
+ VERSION = '1.1.1'.freeze
+ VERSION_CODE = VERSION.split('.').map{|s| s.rjust(2, '0')}.join.freeze
+ # :startdoc:
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/ws.rb b/lib/rubygems/vendor/uri/lib/uri/ws.rb
new file mode 100644
index 0000000000..0dd2a7a1bb
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/ws.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: false
+# = uri/ws.rb
+#
+# Author:: Matt Muller <mamuller@amazon.com>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Gem::URI
+
+ #
+ # The syntax of WS URIs is defined in RFC6455 section 3.
+ #
+ # Note that the Ruby Gem::URI library allows WS URLs containing usernames and
+ # passwords. This is not legal as per the RFC, but used to be
+ # supported in Internet Explorer 5 and 6, before the MS04-004 security
+ # update. See <URL:http://support.microsoft.com/kb/834489>.
+ #
+ class WS < Generic
+ # A Default port of 80 for Gem::URI::WS.
+ DEFAULT_PORT = 80
+
+ # An Array of the available components for Gem::URI::WS.
+ COMPONENT = %i[
+ scheme
+ userinfo host port
+ path
+ query
+ ].freeze
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::WS object from components, with syntax checking.
+ #
+ # The components accepted are userinfo, host, port, path, and query.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[userinfo, host, port, path, query]</code>.
+ #
+ # Example:
+ #
+ # uri = Gem::URI::WS.build(host: 'www.example.com', path: '/foo/bar')
+ #
+ # uri = Gem::URI::WS.build([nil, "www.example.com", nil, "/path", "query"])
+ #
+ # Currently, if passed userinfo components this method generates
+ # invalid WS URIs as per RFC 1738.
+ #
+ def self.build(args)
+ tmp = Util.make_components_hash(self, args)
+ super(tmp)
+ end
+
+ #
+ # == Description
+ #
+ # Returns the full path for a WS Gem::URI, as required by Net::HTTP::Get.
+ #
+ # If the Gem::URI contains a query, the full path is Gem::URI#path + '?' + Gem::URI#query.
+ # Otherwise, the path is simply Gem::URI#path.
+ #
+ # Example:
+ #
+ # uri = Gem::URI::WS.build(path: '/foo/bar', query: 'test=true')
+ # uri.request_uri # => "/foo/bar?test=true"
+ #
+ def request_uri
+ return unless @path
+
+ url = @query ? "#@path?#@query" : @path.dup
+ url.start_with?(?/.freeze) ? url : ?/ + url
+ end
+ end
+
+ register_scheme 'WS', WS
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/wss.rb b/lib/rubygems/vendor/uri/lib/uri/wss.rb
new file mode 100644
index 0000000000..0b91d334bb
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/wss.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: false
+# = uri/wss.rb
+#
+# Author:: Matt Muller <mamuller@amazon.com>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'ws'
+
+module Gem::URI
+
+ # The default port for WSS URIs is 443, and the scheme is 'wss:' rather
+ # than 'ws:'. Other than that, WSS URIs are identical to WS URIs;
+ # see Gem::URI::WS.
+ class WSS < WS
+ # A Default port of 443 for Gem::URI::WSS
+ DEFAULT_PORT = 443
+ end
+
+ register_scheme 'WSS', WSS
+end
diff --git a/lib/rubygems/vendored_net_http.rb b/lib/rubygems/vendored_net_http.rb
new file mode 100644
index 0000000000..a84c52a947
--- /dev/null
+++ b/lib/rubygems/vendored_net_http.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+# Ruby 3.3 and RubyGems 3.5 is already load Gem::Timeout from lib/rubygems/net/http.rb
+# We should avoid to load it again
+require_relative "vendor/net-http/lib/net/http" unless defined?(Gem::Net::HTTP)
diff --git a/lib/rubygems/vendored_optparse.rb b/lib/rubygems/vendored_optparse.rb
new file mode 100644
index 0000000000..a5611d32f0
--- /dev/null
+++ b/lib/rubygems/vendored_optparse.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+require_relative "vendor/optparse/lib/optparse"
diff --git a/lib/rubygems/vendored_pub_grub.rb b/lib/rubygems/vendored_pub_grub.rb
new file mode 100644
index 0000000000..844d243ab3
--- /dev/null
+++ b/lib/rubygems/vendored_pub_grub.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+require_relative "vendor/pub_grub/lib/pub_grub"
diff --git a/lib/rubygems/vendored_securerandom.rb b/lib/rubygems/vendored_securerandom.rb
new file mode 100644
index 0000000000..859b6d7d7a
--- /dev/null
+++ b/lib/rubygems/vendored_securerandom.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+require_relative "vendor/securerandom/lib/securerandom"
diff --git a/lib/rubygems/vendored_timeout.rb b/lib/rubygems/vendored_timeout.rb
new file mode 100644
index 0000000000..45541928e6
--- /dev/null
+++ b/lib/rubygems/vendored_timeout.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+# Ruby 3.3 and RubyGems 3.5 is already load Gem::Timeout from lib/rubygems/timeout.rb
+# We should avoid to load it again
+require_relative "vendor/timeout/lib/timeout" unless defined?(Gem::Timeout)
diff --git a/lib/rubygems/vendored_tsort.rb b/lib/rubygems/vendored_tsort.rb
new file mode 100644
index 0000000000..c3d815650d
--- /dev/null
+++ b/lib/rubygems/vendored_tsort.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+require_relative "vendor/tsort/lib/tsort"
diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb
index ff4a7bf079..306733c1d7 100644
--- a/lib/rubygems/version.rb
+++ b/lib/rubygems/version.rb
@@ -1,167 +1,472 @@
+# frozen_string_literal: true
+
#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
+# Workaround for directly loading Gem::Version in some cases
+module Gem; end
#++
-require 'rubygems'
-
##
-# The Version class processes string versions into comparable values
+# The Version class processes string versions into comparable
+# values. A version string should normally be a series of numbers
+# separated by periods. Each part (digits separated by periods) is
+# considered its own number, and these are used for sorting. So for
+# instance, 3.10 sorts higher than 3.2 because ten is greater than
+# two.
+#
+# If any part contains letters (currently only a-z are supported) then
+# that version is considered prerelease. Versions with a prerelease
+# part in the Nth part sort less than versions with N-1
+# parts. Prerelease parts are sorted alphabetically using the normal
+# Ruby string sorting rules. If a prerelease part contains both
+# letters and numbers, it will be broken into multiple parts to
+# provide expected sort behavior (1.0.a10 becomes 1.0.a.10, and is
+# greater than 1.0.a9).
+#
+# Prereleases sort between real releases (newest to oldest):
+#
+# 1. 1.0
+# 2. 1.0.b1
+# 3. 1.0.a.2
+# 4. 0.9
+#
+# If you want to specify a version restriction that includes both prereleases
+# and regular releases of 1.x or later versions:
+#
+# s.add_dependency 'example', '>= 1.0.0.a'
+#
+# == How Software Changes
+#
+# Libraries generally change in 3 ways:
+#
+# 1. The change is an implementation detail, bug fix, security fix, or
+# optimization, and has no behavioral effect on the software using it.
+#
+# 2. The change adds new features, and software using those new features is
+# not compatible with previous versions of the library, but software using
+# previous versions of the library is compatible with the change.
+#
+# 3. The change modifies the public interface of some part of the library in
+# such a way that software that uses that part of the library must be
+# modified to work.
+#
+# == RubyGems Rational Versioning (the recommended approach)
+#
+# * Versions shall be represented by three non-negative integers, separated
+# by periods (e.g. 3.1.4). The first integer is the "major" version
+# number, the second integer is the "minor" version number, and the third
+# integer is the "patch" version number.
+#
+# * A category 1 change (implementation detail, bug fix, or security fix)
+# will increment the patch number.
+#
+# * A category 2 change (backwards compatible) will increment the minor
+# version number and reset the patch number.
+#
+# * A category 3 change (incompatible) will increment the major version number
+# and reset the minor and patch numbers.
+#
+# * Any "public" release of a gem should have a different version.
+#
+# == Optimistic Vs. Pessimistic Dependency Versioning
+#
+# Users expect to be able to specify a version constraint that gives them
+# a reasonable expectation that new versions of a library will work with
+# their software if the version constraint is true, and not work with their
+# software if the version constraint is false. In other words, the perfect
+# system will accept all compatible versions of the library and reject all
+# incompatible versions. Unfortunately, there is no perfect system, as you
+# cannot predict the future. You can never know whether a future version of
+# a library will contain which type of change.
+#
+# There are two common outlooks on dependency versioning:
+#
+# 1. Optimistic. This does not set an upper bound on a dependency. It is
+# possible that a future version of a dependency will break the software,
+# and in that case, the dependency version will need to be updated and
+# changes will need to be made.
+#
+# 2. Pessimistic. This assumes all major version changes of a dependency will
+# break the software, and that patch or minor changes of a dependency will
+# not break the software. If there is a major version of a dependency
+# released, the dependency version must be updated in order to use it, even
+# if no code changes are actually needed.
+#
+# In general, optimistic versioning is superior to pessimistic versioning.
+# Pessimistic versioning is often wrong in both directions. Dependencies can
+# release patch or minor versions that contain incompatibilities. One
+# common reason is that a security fix may require a backwards-incompatible API
+# change. In this case, even though pessimistic versioning was used, it
+# didn't even save effort, as you still need to make code changes and adjust
+# dependency versions. Similarly, for all but the smallest dependencies, just
+# because the dependency made a backwards incompatible change to one interface
+# doesn't mean the dependency made a backwards incompatible change to an
+# interface that the software is using. It is a common problem that a
+# dependency will release a new major version and the software does not require
+# any changes in order to use it. In this case, being pessimistic results in
+# additional work for no benefit.
+#
+# When a library uses pessimistic versioning of dependencies, it causes
+# significant problems if that library is not diligent about updating
+# dependency versions and any library is depending on that library.
+# For example:
+#
+# * Library A is currently on release 1.2.3.
+#
+# * Library B is at version 2.3.4 and has a pessimistic dependency on
+# library A, using ~> 1.0 (>= 1.0, < 2).
+#
+# * Library C is at version 3.4.5 and has an optimistic dependency on
+# library A, using >= 1.0.
+#
+# * Library D has optimistic dependencies on both libraries B and C.
+#
+# * Library A releases a new major version, 2.0.0, with new features, which
+# is mostly backwards compatible, but does contain some backwards
+# incompatible changes.
+#
+# * Library B would work with A 2.0.0, but cannot use it due to pessimistic
+# versioning.
+#
+# * Library C wants to use the new features in the major release of library
+# A to implement its own new features, so it does so, bumps the
+# dependency version of A to >= 2.0, and releases version 3.5.0.
+#
+# * Library D cannot upgrade to the new version of library C, because it
+# depends on library B, which has a pessimistic dependency on library A.
+#
+# * Library C releases a security fix patch version 3.5.1 to fix a
+# vulnerability present in all previous versions.
+#
+# * Library D is now in a terrible situation. It cannot upgrade to library
+# C 3.5.1, as that requires library A > 2.0, because it depends on library
+# B, which requires library A > 1.0, < 2, even though library B would work
+# fine with library A 2.0.0.
+#
+# This type of situation brought on by pessimistic versioning is unfortunately
+# both common and serious in practice.
+#
+# This is not to say that optimistic versioning never causes a problem.
+# However, with optimistic versioning, if there is a problem, it can be solved
+# with the addition of a single dependency. For example, continuing the
+# previous example:
+#
+# * Library A releases a new major version, 3.0.0, which makes backwards
+# incompatible changes that break library C.
+#
+# * Until library C releases an updated version with new changes, library
+# D only needs to set a specific dependency on library A for > 2.0, < 3,
+# until library C is updated to work with the new version of library A.
+#
+# Both optimistic versioning and pessimistic versioning have problems in
+# certain cases. However, it's significantly easier to fix optimistic
+# versioning problems than to fix pessimistic versioning problems.
+#
+# That is not to say that pessimistic versioning is never appropriate. If the
+# dependency is a library that adds a single method, where any change resulting
+# in a major version bump would probably break a library using it, then using
+# pessimistic versioning may be warranted. Additionally, if a dependency has
+# already announced or committed backwards incompatible changes that would
+# break a library's use of it, then having that library use a pessimistic
+# version constraint would likely be warranted. However, outside of
+# specific situations, you should avoid using pessimistic versioning, as the
+# costs typically exceed the benefits.
class Gem::Version
-
include Comparable
- attr_reader :ints
+ VERSION_PATTERN = '[0-9]+(?>\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?' # :nodoc:
+ ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/ # :nodoc:
+ RADIX_OPT = [9_500, 3_500, 260_000, 22_227, 24].freeze # :nodoc:
- attr_reader :version
+ ##
+ # A string representation of this Version.
+
+ def version
+ @version
+ end
+
+ alias_method :to_s, :version
##
- # Returns true if +version+ is a valid version string.
+ # True if the +version+ string matches RubyGems' requirements.
def self.correct?(version)
- case version
- when Integer, /\A\s*(\d+(\.\d+)*)*\s*\z/ then true
- else false
- end
+ version.nil? || ANCHORED_VERSION_PATTERN.match?(version.to_s)
end
##
- # Factory method to create a Version object. Input may be a Version or a
- # String. Intended to simplify client code.
+ # Factory method to create a Version object. Input may be a Version
+ # or a String. Intended to simplify client code.
#
# ver1 = Version.create('1.3.17') # -> (Version object)
# ver2 = Version.create(ver1) # -> (ver1)
- # ver3 = Version.create(nil) # -> nil
def self.create(input)
- if input.respond_to? :version then
+ if self === input # check yourself before you wreck yourself
input
- elsif input.nil? then
- nil
else
new input
end
end
+ @@all = {}
+ @@bump = {}
+ @@release = {}
+
+ def self.new(version) # :nodoc:
+ return super unless self == Gem::Version
+
+ @@all[version] ||= super
+ end
+
##
# Constructs a Version from the +version+ string. A version string is a
- # series of digits separated by dots.
+ # series of digits or ASCII letters separated by dots.
def initialize(version)
- raise ArgumentError, "Malformed version number string #{version}" unless
- self.class.correct?(version)
+ unless self.class.correct?(version)
+ raise ArgumentError, "Malformed version number string #{version}"
+ end
- self.version = version
- end
+ # If version is an empty string convert it to 0
+ version = 0 if version.nil? || (version.is_a?(String) && /\A\s*\Z/.match?(version))
- def inspect # :nodoc:
- "#<#{self.class} #{@version.inspect}>"
- end
+ @version = version.to_s
- # Dump only the raw version string, not the complete object
- def marshal_dump
- [@version]
+ # optimization to avoid allocation when given an integer, since we know
+ # it's to_s won't have any spaces or dashes
+ unless version.is_a?(Integer)
+ @version = @version.strip
+ @version.gsub!("-",".pre.")
+ end
+ @version = -@version
+ @segments = nil
+ @sort_key = compute_sort_key
end
- # Load custom marshal format
- def marshal_load(array)
- self.version = array[0]
+ ##
+ # Return a new version object where the next to the last revision
+ # number is one greater (e.g., 5.3.1 => 5.4).
+ #
+ # Pre-release (alpha) parts, e.g, 5.3.1.b.2 => 5.4, are ignored.
+
+ def bump
+ @@bump[self] ||= begin
+ segments = self.segments
+ segments.pop while segments.any? {|s| String === s }
+ segments.pop if segments.size > 1
+
+ segments[-1] = segments[-1].succ
+ self.class.new segments.join(".")
+ end
end
##
- # Strip ignored trailing zeros.
+ # A Version is only eql? to another version if it's specified to the
+ # same precision. Version "1.0" is not the same as version "1".
- def normalize
- @ints = build_array_from_version_string
+ def eql?(other)
+ self.class === other && @version == other.version
+ end
- return if @ints.length == 1
+ def hash # :nodoc:
+ canonical_segments.hash
+ end
- @ints.pop while @ints.last == 0
+ def init_with(coder) # :nodoc:
+ yaml_initialize coder.tag, coder.map
+ end
- @ints = [0] if @ints.empty?
+ def inspect # :nodoc:
+ "#<#{self.class} #{version.inspect}>"
end
##
- # Returns the text representation of the version
- #
- # return:: [String] version as string
- #
- def to_s
- @version
+ # Dump only the raw version string, not the complete object. It's a
+ # string for backwards (RubyGems 1.3.5 and earlier) compatibility.
+
+ def marshal_dump
+ [@version]
end
##
- # Returns an integer array representation of this Version.
+ # Load custom marshal format. It's a string for backwards (RubyGems
+ # 1.3.5 and earlier) compatibility.
- def to_ints
- normalize unless @ints
- @ints
+ def marshal_load(array)
+ string = array[0]
+ raise TypeError, "wrong version string" unless string.is_a?(String)
+
+ initialize string
end
- def to_yaml_properties
- ['@version']
+ def yaml_initialize(tag, map) # :nodoc:
+ @version = -map["version"]
+ @segments = nil
+ @hash = nil
end
- def version=(version)
- @version = version.to_s.strip
- normalize
+ def encode_with(coder) # :nodoc:
+ coder.add "version", @version
end
- def yaml_initialize(tag, values)
- self.version = values['version']
+ ##
+ # A version is considered a prerelease if it contains a letter.
+
+ def prerelease?
+ unless instance_variable_defined? :@prerelease
+ @prerelease = /[a-zA-Z]/.match?(version)
+ end
+ @prerelease
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.text "Gem::Version.new(#{version.inspect})"
end
##
- # Compares this version with +other+ returning -1, 0, or 1 if the other
- # version is larger, the same, or smaller than this one.
+ # The release for this version (e.g. 1.2.0.a -> 1.2.0).
+ # Non-prerelease versions return themselves.
+
+ def release
+ @@release[self] ||= if prerelease?
+ segments = self.segments
+ segments.pop while segments.any? {|s| String === s }
+ self.class.new segments.join(".")
+ else
+ self
+ end
+ end
- def <=>(other)
- return nil unless self.class === other
- return 1 unless other
- @ints <=> other.ints
+ def segments # :nodoc:
+ _segments.dup
end
##
- # A Version is only eql? to another version if it has the same version
- # string. "1.0" is not the same version as "1".
+ # A recommended version for use with a >= Requirement.
- def eql?(other)
- self.class === other and @version == other.version
+ def approximate_recommendation
+ segments = self.segments
+
+ segments.pop while segments.any? {|s| String === s }
+ segments.pop while segments.size > 2
+ segments.push 0 while segments.size < 2
+
+ recommendation = ">= #{segments.join(".")}"
+ recommendation += ".a" if prerelease?
+ recommendation
end
- def hash # :nodoc:
- @version.hash
+ ##
+ # Compares this version with +other+ returning -1, 0, or 1 if the
+ # other version is larger, the same, or smaller than this
+ # one. +other+ must be an instance of Gem::Version, comparing with
+ # other types may raise an exception.
+
+ def <=>(other)
+ if Gem::Version === other
+ # Fast path for comparison when available.
+ if @sort_key && other.sort_key
+ return @sort_key <=> other.sort_key
+ end
+
+ return 0 if @version == other.version || canonical_segments == other.canonical_segments
+
+ lhsegments = canonical_segments
+ rhsegments = other.canonical_segments
+
+ lhsize = lhsegments.size
+ rhsize = rhsegments.size
+ limit = (lhsize > rhsize ? rhsize : lhsize)
+
+ i = 0
+
+ while i < limit
+ lhs = lhsegments[i]
+ rhs = rhsegments[i]
+ i += 1
+
+ next if lhs == rhs
+ return -1 if String === lhs && Numeric === rhs
+ return 1 if Numeric === lhs && String === rhs
+
+ return lhs <=> rhs
+ end
+
+ lhs = lhsegments[i]
+
+ if lhs.nil?
+ rhs = rhsegments[i]
+
+ while i < rhsize
+ return 1 if String === rhs
+ return -1 unless rhs.zero?
+ rhs = rhsegments[i += 1]
+ end
+ else
+ while i < lhsize
+ return -1 if String === lhs
+ return 1 unless lhs.zero?
+ lhs = lhsegments[i += 1]
+ end
+ end
+
+ 0
+ elsif String === other
+ return unless self.class.correct?(other)
+ self <=> self.class.new(other)
+ end
end
- # Return a new version object where the next to the last revision
- # number is one greater. (e.g. 5.3.1 => 5.4)
- def bump
- ints = build_array_from_version_string
- ints.pop if ints.size > 1
- ints[-1] += 1
- self.class.new(ints.join("."))
+ # remove trailing zeros segments before first letter or at the end of the version
+ def canonical_segments
+ @canonical_segments ||= begin
+ # remove trailing 0 segments, using dot or letter as anchor
+ # may leave a trailing dot which will be ignored by partition_segments
+ canonical_version = @version.sub(/(?<=[a-zA-Z.])[.0]+\z/, "")
+ # remove 0 segments before the first letter in a prerelease version
+ canonical_version.sub!(/(?<=\.|\A)[0.]+(?=[a-zA-Z])/, "") if prerelease?
+ partition_segments(canonical_version)
+ end
end
- def build_array_from_version_string
- @version.to_s.scan(/\d+/).map { |s| s.to_i }
+ def freeze
+ prerelease?
+ _segments
+ canonical_segments
+ super
end
- private :build_array_from_version_string
- #:stopdoc:
+ protected
- require 'rubygems/requirement'
+ attr_reader :sort_key # :nodoc:
- # Gem::Requirement's original definition is nested in Version.
- # Although an inappropriate place, current gems specs reference the nested
- # class name explicitly. To remain compatible with old software loading
- # gemspecs, we leave a copy of original definition in Version, but define an
- # alias Gem::Requirement for use everywhere else.
+ def compute_sort_key
+ return if prerelease?
- Requirement = ::Gem::Requirement
+ segments = canonical_segments
+ return if segments.size > 5
- # :startdoc:
+ key = 0
+ RADIX_OPT.each_with_index do |radix, i|
+ seg = segments.fetch(i, 0)
+ return nil if seg >= radix
+ key = key * radix + seg
+ end
-end
+ key
+ end
+ def _segments
+ # segments is lazy so it can pick up version values that come from
+ # old marshaled versions, which don't go through marshal_load.
+ # since this version object is cached in @@all, its @segments should be frozen
+ @segments ||= partition_segments(@version)
+ end
+
+ def partition_segments(ver)
+ ver.scan(/\d+|[a-z]+/i).map! do |s|
+ /\A\d/.match?(s) ? s.to_i : -s
+ end.freeze
+ end
+end
diff --git a/lib/rubygems/version_option.rb b/lib/rubygems/version_option.rb
index 1374018913..7910fd3d1b 100644
--- a/lib/rubygems/version_option.rb
+++ b/lib/rubygems/version_option.rb
@@ -1,28 +1,32 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems'
+require_relative "../rubygems"
+##
# Mixin methods for --version and --platform Gem::Command options.
-module Gem::VersionOption
+module Gem::VersionOption
+ ##
# Add the --platform option to the option parser.
+
def add_platform_option(task = command, *wrap)
- OptionParser.accept Gem::Platform do |value|
- if value == Gem::Platform::RUBY then
+ Gem::OptionParser.accept Gem::Platform do |value|
+ if value == Gem::Platform::RUBY
value
else
Gem::Platform.new value
end
end
- add_option('--platform PLATFORM', Gem::Platform,
- "Specify the platform of gem to #{task}", *wrap) do
- |value, options|
- unless options[:added_platform] then
+ add_option("--platform PLATFORM", Gem::Platform,
+ "Specify the platform of gem to #{task}", *wrap) do |value, options|
+ unless options[:added_platform]
Gem.platforms = [Gem::Platform::RUBY]
options[:added_platform] = true
end
@@ -31,18 +35,46 @@ module Gem::VersionOption
end
end
+ ##
+ # Add the --prerelease option to the option parser.
+
+ def add_prerelease_option(*wrap)
+ add_option("--[no-]prerelease",
+ "Allow prerelease versions of a gem", *wrap) do |value, options|
+ options[:prerelease] = value
+ options[:explicit_prerelease] = true
+ end
+ end
+
+ ##
# Add the --version option to the option parser.
+
def add_version_option(task = command, *wrap)
- OptionParser.accept Gem::Requirement do |value|
- Gem::Requirement.new value
+ Gem::OptionParser.accept Gem::Requirement do |value|
+ Gem::Requirement.new(*value.split(/\s*,\s*/))
end
- add_option('-v', '--version VERSION', Gem::Requirement,
- "Specify version of gem to #{task}", *wrap) do
- |value, options|
- options[:version] = value
+ add_option("-v", "--version VERSION", Gem::Requirement,
+ "Specify version of gem to #{task}", *wrap) do |value, options|
+ # Allow handling for multiple --version operators
+ if options[:version] && !options[:version].none?
+ options[:version].concat([value])
+ else
+ options[:version] = value
+ end
+
+ explicit_prerelease_set = !options[:explicit_prerelease].nil?
+ options[:explicit_prerelease] = false unless explicit_prerelease_set
+
+ options[:prerelease] = value.prerelease? unless
+ options[:explicit_prerelease]
end
end
-end
+ ##
+ # Extract platform given on the command line
+ def get_platform_from_requirements(requirements)
+ Gem.platforms[1].to_s if requirements.key? :added_platform
+ end
+end
diff --git a/lib/rubygems/win_platform.rb b/lib/rubygems/win_platform.rb
new file mode 100644
index 0000000000..10556871b2
--- /dev/null
+++ b/lib/rubygems/win_platform.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "rbconfig"
+
+module Gem
+ ##
+ # An Array of Regexps that match windows Ruby platforms.
+
+ WIN_PATTERNS = [
+ /bccwin/i,
+ /djgpp/i,
+ /mingw/i,
+ /mswin/i,
+ /wince/i,
+ ].freeze
+
+ @@win_platform = nil
+
+ ##
+ # Is this a windows platform?
+
+ def self.win_platform?
+ if @@win_platform.nil?
+ ruby_platform = RbConfig::CONFIG["host_os"]
+ @@win_platform = !WIN_PATTERNS.find {|r| ruby_platform =~ r }.nil?
+ end
+
+ @@win_platform
+ end
+end
diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb
new file mode 100644
index 0000000000..b2547b136b
--- /dev/null
+++ b/lib/rubygems/yaml_serializer.rb
@@ -0,0 +1,845 @@
+# frozen_string_literal: true
+
+unless defined?(Psych::VERSION)
+ module Psych
+ class Exception < ::RuntimeError; end
+ class SyntaxError < Exception; end
+ class DisallowedClass < Exception; end
+ class BadAlias < Exception; end
+ class AliasesNotEnabled < BadAlias; end
+ end
+end
+
+module Gem
+ module YAMLSerializer
+ Scalar = Struct.new(:value, :tag, :anchor, keyword_init: true)
+
+ Mapping = Struct.new(:pairs, :tag, :anchor, keyword_init: true) do
+ def initialize(pairs: [], tag: nil, anchor: nil)
+ super
+ end
+ end
+
+ Sequence = Struct.new(:items, :tag, :anchor, keyword_init: true) do
+ def initialize(items: [], tag: nil, anchor: nil)
+ super
+ end
+ end
+
+ AliasRef = Struct.new(:name, keyword_init: true)
+
+ class Parser
+ MAPPING_KEY_RE = /^((?:[^#:]|:[^ ])+):(?:[ ]+(.*))?$/
+ MAX_NESTING_DEPTH = 1_000
+
+ def initialize(source)
+ @lines = source.split("\n")
+ @anchors = {}
+ @depth = 0
+ strip_document_prefix
+ end
+
+ def parse
+ return nil if @lines.empty?
+
+ root = nil
+ while @lines.any?
+ before = @lines.size
+ node = parse_node(-1)
+ @lines.shift if @lines.size == before && @lines.any?
+
+ if root.is_a?(Mapping) && node.is_a?(Mapping)
+ root.pairs.concat(node.pairs)
+ elsif root.nil?
+ root = node
+ end
+ end
+ root
+ end
+
+ private
+
+ def strip_document_prefix
+ return if @lines.empty?
+ return unless @lines[0]&.start_with?("---")
+
+ if @lines[0].strip == "---"
+ @lines.shift
+ else
+ @lines[0] = @lines[0].sub(/^---\s*/, "")
+ end
+ end
+
+ def parse_node(base_indent)
+ @depth += 1
+ raise_max_nesting! if @depth > MAX_NESTING_DEPTH
+
+ skip_blank_and_comments
+ return nil if @lines.empty?
+
+ line = @lines[0]
+ stripped = line.lstrip
+ indent = line.size - stripped.size
+ return nil if indent < base_indent
+
+ return parse_alias_ref if stripped.start_with?("*")
+
+ anchor = consume_anchor
+
+ if anchor
+ line = @lines[0]
+ stripped = line.lstrip
+ end
+
+ if stripped.start_with?("- ") || stripped == "-"
+ parse_sequence(indent, anchor)
+ elsif stripped.start_with?("\"") && stripped.end_with?("\"")
+ # We don't need to care about the following case here:
+ # 1. "value with comment" # ...
+ # 2. "key": "value"
+ #
+ # 1. must not happen because YAMLSerializer doesn't emit any
+ # comment. YAMLSerializer parses only YAML that is generated
+ # by YAMLSerializer.
+ #
+ # 2. must not happen because #parse_node isn't used non
+ # top-level mapping. Non top-level mapping always uses
+ # #parse_mapping. Top-level mapping never use the '"key":
+ # "value"' form because all top-level keys
+ # ("!ruby/object:Gem::Specification"'s keys) are known and
+ # #emit_specification doesn't quote anything.
+ parse_plain_scalar(indent, anchor)
+ elsif stripped.start_with?("'") && stripped.end_with?("'")
+ # See also the above note for double quotation.
+ parse_plain_scalar(indent, anchor)
+ elsif stripped =~ MAPPING_KEY_RE && !stripped.start_with?("!ruby/object:")
+ parse_mapping(indent, anchor)
+ elsif stripped.start_with?("!ruby/object:")
+ parse_tagged_node(indent, anchor)
+ elsif stripped.start_with?("|")
+ modifier = stripped[1..].to_s.strip
+ @lines.shift
+ register_anchor(anchor, Scalar.new(value: parse_block_scalar(indent, modifier)))
+ else
+ parse_plain_scalar(indent, anchor)
+ end
+ ensure
+ @depth -= 1
+ end
+
+ def parse_sequence(indent, anchor)
+ items = []
+ while @lines.any?
+ line = @lines[0]
+ stripped = line.lstrip
+ break unless line.size - stripped.size == indent &&
+ (stripped.start_with?("- ") || stripped == "-")
+ content = @lines.shift.lstrip[1..].strip
+ item_anchor, content = extract_item_anchor(content)
+ item = parse_sequence_item(content, indent)
+ items << register_anchor(item_anchor, item)
+ end
+ register_anchor(anchor, Sequence.new(items: items))
+ end
+
+ def parse_sequence_item(content, indent)
+ if content.start_with?("*")
+ parse_inline_alias(content)
+ elsif content.empty?
+ @lines.any? && current_indent > indent ? parse_node(indent) : nil
+ elsif content.start_with?("!ruby/object:")
+ parse_tagged_content(content.strip, indent)
+ elsif content.start_with?("!binary ")
+ parse_binary_value(content, indent)
+ elsif content.start_with?("-")
+ @lines.unshift("#{" " * (indent + 2)}#{content}")
+ parse_node(indent)
+ elsif content =~ MAPPING_KEY_RE && !content.start_with?("!ruby/object:")
+ @lines.unshift("#{" " * (indent + 2)}#{content}")
+ parse_node(indent)
+ elsif content.start_with?("|")
+ Scalar.new(value: parse_block_scalar(indent, content[1..].to_s.strip))
+ else
+ parse_inline_scalar(content, indent)
+ end
+ end
+
+ def parse_mapping(indent, anchor)
+ pairs = []
+ while @lines.any?
+ line = @lines[0]
+ stripped = line.lstrip
+ break unless line.size - stripped.size == indent &&
+ stripped =~ MAPPING_KEY_RE && !stripped.start_with?("!ruby/object:")
+ key = $1.strip
+ @lines.shift
+ val = strip_comment($2.to_s.strip)
+
+ key = decode_binary_tag(key) if key.start_with?("!binary ")
+
+ val_anchor, val = consume_value_anchor(val)
+ value = parse_mapping_value(val, indent)
+ value = register_anchor(val_anchor, value) if val_anchor
+
+ pairs << [Scalar.new(value: key), value]
+ end
+ register_anchor(anchor, Mapping.new(pairs: pairs))
+ end
+
+ def parse_mapping_value(val, indent)
+ if val.start_with?("*")
+ parse_inline_alias(val)
+ elsif val.start_with?("!ruby/object:")
+ parse_tagged_content(val.strip, indent)
+ elsif val.start_with?("!binary ")
+ parse_binary_value(val, indent)
+ elsif val.empty?
+ next_stripped = nil
+ next_indent = nil
+ if @lines.any?
+ next_stripped = @lines[0].lstrip
+ next_indent = @lines[0].size - next_stripped.size
+ end
+ if next_stripped &&
+ (next_stripped.start_with?("- ") || next_stripped == "-") &&
+ next_indent == indent
+ parse_node(indent)
+ else
+ parse_node(indent + 1)
+ end
+ elsif val == "[]"
+ Sequence.new
+ elsif val == "{}"
+ Mapping.new
+ elsif val.start_with?("|")
+ Scalar.new(value: parse_block_scalar(indent, val[1..].to_s.strip))
+ else
+ parse_inline_scalar(val, indent)
+ end
+ end
+
+ def parse_tagged_node(indent, anchor)
+ tag = @lines.shift.strip
+ nested = parse_node(indent)
+ apply_tag(nested, tag, anchor)
+ end
+
+ def parse_tagged_content(tag, indent)
+ nested = parse_node(indent)
+ apply_tag(nested, tag, nil)
+ end
+
+ def apply_tag(node, tag, anchor)
+ if node.is_a?(Mapping)
+ node.tag = tag
+ node.anchor = anchor
+ node
+ else
+ Mapping.new(pairs: [[Scalar.new(value: "value"), node]], tag: tag, anchor: anchor)
+ end
+ end
+
+ def parse_block_scalar(base_indent, modifier)
+ parts = []
+ block_indent = nil
+
+ while @lines.any?
+ line = @lines[0]
+ if line.strip.empty?
+ parts << "\n"
+ @lines.shift
+ else
+ line_indent = line.size - line.lstrip.size
+ break if line_indent <= base_indent
+ block_indent ||= line_indent
+ parts << @lines.shift[block_indent..].to_s << "\n"
+ end
+ end
+
+ res = parts.join
+ res.chomp! if modifier == "-" && res.end_with?("\n")
+ res
+ end
+
+ def parse_plain_scalar(indent, anchor)
+ result = coerce(@lines.shift.strip)
+ return register_anchor(anchor, result) if result.is_a?(Mapping) || result.is_a?(Sequence)
+
+ while result.is_a?(String) && @lines.any? &&
+ !@lines[0].strip.empty? && current_indent > indent
+ result << " " << @lines.shift.strip
+ end
+ register_anchor(anchor, Scalar.new(value: result))
+ end
+
+ def parse_inline_scalar(val, indent)
+ result = coerce(val)
+ return result if result.is_a?(Mapping) || result.is_a?(Sequence)
+
+ while result.is_a?(String) && @lines.any? &&
+ !@lines[0].strip.empty? && current_indent > indent
+ result << " " << @lines.shift.strip
+ end
+ Scalar.new(value: result)
+ end
+
+ def coerce(val, depth = 0)
+ raise_max_nesting! if depth > MAX_NESTING_DEPTH
+
+ val = val.sub(/^! /, "") if val.start_with?("! ")
+
+ if val =~ /^"(.*)"$/
+ $1.gsub(/\\["nrt\\]/) do |m|
+ case m
+ when '\\"' then '"'
+ when "\\n" then "\n"
+ when "\\r" then "\r"
+ when "\\t" then "\t"
+ when "\\\\" then "\\"
+ end
+ end
+ elsif val =~ /^'(.*)'$/
+ $1.gsub(/''/, "'")
+ elsif val == "true"
+ true
+ elsif val == "false"
+ false
+ elsif ["~", "null"].include?(val)
+ nil
+ elsif val == "{}"
+ Mapping.new
+ elsif val =~ /^\[(.*)\]$/
+ inner = $1.strip
+ return Sequence.new if inner.empty?
+ items = inner.split(/\s*,\s*/).reject(&:empty?).map {|e| Scalar.new(value: coerce(e, depth + 1)) }
+ Sequence.new(items: items)
+ elsif /\A\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2})?/.match?(val)
+ begin
+ Time.new(val)
+ rescue ArgumentError
+ # date-only format like "2024-06-15" is not supported by Time.new
+ if /\A(\d{4})-(\d{2})-(\d{2})\z/.match(val)
+ Time.utc($1.to_i, $2.to_i, $3.to_i)
+ else
+ val
+ end
+ end
+ elsif /^-?\d+$/.match?(val)
+ val.to_i
+ else
+ val
+ end
+ end
+
+ def decode_binary_tag(str)
+ content = str.sub(/\A!binary\s+/, "")
+ content = $1 if content =~ /\A"(.*)"\z/ || content =~ /\A'(.*)'\z/
+ content.unpack1("m")
+ end
+
+ def parse_binary_value(val, indent)
+ rest = val.sub(/\A!binary\s+/, "")
+ if rest.start_with?("|")
+ content = parse_block_scalar(indent, rest[1..].to_s.strip)
+ Scalar.new(value: content.unpack1("m"))
+ else
+ Scalar.new(value: decode_binary_tag(val))
+ end
+ end
+
+ def parse_alias_ref
+ AliasRef.new(name: @lines.shift.lstrip[1..].strip)
+ end
+
+ def parse_inline_alias(content)
+ AliasRef.new(name: content[1..].strip)
+ end
+
+ def current_indent
+ line = @lines[0]
+ line.size - line.lstrip.size
+ end
+
+ def consume_anchor
+ line = @lines[0]
+ stripped = line.lstrip
+ return nil unless stripped.start_with?("&") && stripped =~ /^&(\S+)\s+/
+
+ anchor = $1
+ @lines[0] = line.sub(/&#{Regexp.escape(anchor)}\s+/, "")
+ anchor
+ end
+
+ def extract_item_anchor(content)
+ return [nil, content] unless content =~ /^&(\S+)/
+
+ anchor = $1
+ [anchor, content.sub(/^&#{Regexp.escape(anchor)}\s*/, "")]
+ end
+
+ def consume_value_anchor(val)
+ return [nil, val] unless val =~ /^&(\S+)\s+/
+
+ anchor = $1
+ [anchor, val.sub(/^&#{Regexp.escape(anchor)}\s+/, "")]
+ end
+
+ def register_anchor(name, node)
+ if name
+ @anchors[name] = node
+ node.anchor = name if node.respond_to?(:anchor=)
+ end
+ node
+ end
+
+ def raise_max_nesting!
+ message = "exceeded maximum nesting depth (#{MAX_NESTING_DEPTH})"
+ if defined?(Psych::VERSION)
+ raise Psych::SyntaxError.new(nil, 0, 0, 0, message, nil)
+ else
+ raise Psych::SyntaxError, message
+ end
+ end
+
+ def skip_blank_and_comments
+ while @lines.any?
+ line = @lines[0]
+ stripped = line.lstrip
+ break unless stripped.empty? || stripped.start_with?("#")
+ @lines.shift
+ end
+ end
+
+ def strip_comment(val)
+ return val unless val.include?("#")
+ return val if val.lstrip.start_with?("#")
+
+ in_single = false
+ in_double = false
+ escape = false
+
+ val.each_char.with_index do |ch, i|
+ if escape
+ escape = false
+ next
+ end
+
+ if in_single
+ in_single = false if ch == "'"
+ elsif in_double
+ if ch == "\\"
+ escape = true
+ elsif ch == '"'
+ in_double = false
+ end
+ else
+ case ch
+ when "'" then in_single = true
+ when '"' then in_double = true
+ when "#" then return val[0...i].rstrip
+ end
+ end
+ end
+
+ val
+ end
+ end
+
+ class Builder
+ VALID_OPS = %w[= != > < >= <= ~>].freeze
+ ARRAY_FIELDS = %w[files test_files executables extra_rdoc_files].freeze
+ MAX_ALIAS_RESOLUTIONS = 1_000
+
+ def initialize(permitted_classes: [], permitted_symbols: [], aliases: true)
+ @permitted_classes = permitted_classes.map {|c| "!ruby/object:#{c}" }
+ @permitted_symbols = permitted_symbols
+ @aliases = aliases
+ @anchor_values = {}
+ @alias_count = 0
+ end
+
+ def build(node)
+ return nil if node.nil?
+
+ result = build_node(node)
+
+ if result.is_a?(Hash) && result[:tag] == "!ruby/object:Gem::Specification"
+ build_specification(result)
+ else
+ result
+ end
+ end
+
+ private
+
+ def build_node(node)
+ case node
+ when nil then nil
+ when AliasRef then resolve_alias(node)
+ when Scalar then store_anchor(node.anchor, node.value)
+ when Mapping then build_mapping(node)
+ when Sequence then store_anchor(node.anchor, node.items.map {|item| build_node(item) })
+ else node # already a Ruby object
+ end
+ end
+
+ def resolve_alias(node)
+ raise Psych::AliasesNotEnabled unless @aliases
+ @alias_count += 1
+ if @alias_count > MAX_ALIAS_RESOLUTIONS
+ raise Psych::BadAlias, "exceeded maximum alias resolutions (#{MAX_ALIAS_RESOLUTIONS})"
+ end
+ unless @anchor_values.key?(node.name)
+ klass = defined?(Psych::AnchorNotDefined) ? Psych::AnchorNotDefined : Psych::BadAlias
+ raise klass, "An alias referenced an unknown anchor: #{node.name}"
+ end
+ @anchor_values.fetch(node.name)
+ end
+
+ def store_anchor(name, value)
+ @anchor_values[name] = value if name
+ value
+ end
+
+ def build_mapping(node)
+ validate_tag!(node.tag) if node.tag
+
+ result = case node.tag
+ when "!ruby/object:Gem::Version"
+ build_version(node)
+ when "!ruby/object:Gem::Platform"
+ build_platform(node)
+ when "!ruby/object:Gem::Requirement", "!ruby/object:Gem::Version::Requirement"
+ build_requirement(node)
+ when "!ruby/object:Gem::Dependency"
+ build_dependency(node)
+ when nil
+ build_hash(node)
+ when "!ruby/object:Gem::Specification"
+ hash = build_hash(node)
+ hash[:tag] = node.tag
+ hash
+ else
+ raise ArgumentError, "undefined class/module #{node.tag.sub("!ruby/object:", "")}"
+ end
+
+ store_anchor(node.anchor, result)
+ end
+
+ def build_hash(node)
+ result = {}
+ node.pairs.each do |key_node, value_node|
+ key = key_node.is_a?(Scalar) ? key_node.value.to_s : build_node(key_node).to_s
+ value = build_node(value_node)
+
+ if ARRAY_FIELDS.include?(key)
+ value = normalize_array_field(value)
+ end
+
+ result[key] = value
+ end
+ result
+ end
+
+ def build_version(node)
+ hash = pairs_to_hash(node)
+ Gem::Version.new((hash["version"] || hash["value"]).to_s)
+ end
+
+ PLATFORM_FIELDS = %w[cpu os version].freeze
+ PLATFORM_ALLOWED_IVARS = %w[cpu os version value].freeze
+
+ def build_platform(node)
+ hash = pairs_to_hash(node)
+ if (hash.keys & PLATFORM_FIELDS).any?
+ Gem::Platform.new([hash["cpu"], hash["os"], hash["version"]])
+ elsif hash["value"].is_a?(Array)
+ # Malformed platform (e.g. sequence instead of mapping).
+ # Return the raw value so yaml_initialize handles it like Psych does.
+ hash["value"]
+ else
+ plat = Gem::Platform.allocate
+ hash.each do |k, v|
+ plat.instance_variable_set(:"@#{k}", v) if PLATFORM_ALLOWED_IVARS.include?(k)
+ end
+ plat
+ end
+ end
+
+ def build_requirement(node)
+ r = Gem::Requirement.allocate
+ hash = pairs_to_hash(node)
+ reqs = hash["requirements"] || hash["value"]
+
+ if reqs.is_a?(Array) && !reqs.empty?
+ safe_reqs = []
+ reqs.each do |item|
+ if item.is_a?(Array) && item.size == 2
+ op = item[0].to_s
+ ver = item[1]
+ if VALID_OPS.include?(op)
+ version_obj = ver.is_a?(Gem::Version) ? ver : Gem::Version.new(ver.to_s)
+ safe_reqs << [op, version_obj]
+ end
+ elsif item.is_a?(String)
+ parsed = Gem::Requirement.parse(item)
+ safe_reqs << parsed
+ end
+ rescue Gem::Requirement::BadRequirementError, Gem::Version::BadVersionError
+ # Skip malformed items silently
+ end
+ reqs = safe_reqs unless safe_reqs.empty?
+ end
+
+ r.instance_variable_set(:@requirements, reqs)
+ r
+ end
+
+ def build_dependency(node)
+ hash = pairs_to_hash(node)
+ d = Gem::Dependency.allocate
+ d.instance_variable_set(:@name, hash["name"])
+
+ d.instance_variable_set(:@requirement, hash["requirement"] || hash["version_requirements"])
+
+ raw_type = hash["type"]
+ if raw_type
+ name = raw_type.to_s.sub(/^:/, "")
+ validate_symbol!(name)
+ type = name.to_sym
+ else
+ type = :runtime
+ end
+ d.instance_variable_set(:@type, type)
+
+ d.instance_variable_set(:@prerelease, ["true", true].include?(hash["prerelease"]))
+ d.instance_variable_set(:@version_requirements, d.instance_variable_get(:@requirement))
+ d
+ end
+
+ def build_specification(hash)
+ spec = Gem::Specification.allocate
+
+ normalize_specification_version!(hash)
+ normalize_array_fields!(hash)
+
+ spec.yaml_initialize("!ruby/object:Gem::Specification", hash)
+ spec
+ end
+
+ def pairs_to_hash(node)
+ result = {}
+ node.pairs.each do |key_node, value_node|
+ key = key_node.is_a?(Scalar) ? key_node.value.to_s : build_node(key_node).to_s
+ result[key] = build_node(value_node)
+ end
+ result
+ end
+
+ def validate_tag!(tag)
+ return if @permitted_classes.include?(tag)
+ raise_disallowed_class!(tag)
+ end
+
+ def raise_disallowed_class!(tag)
+ if defined?(Psych::VERSION)
+ raise Psych::DisallowedClass.new("load", tag)
+ else
+ raise Psych::DisallowedClass, "Tried to load unspecified class: #{tag}"
+ end
+ end
+
+ def validate_symbol!(name)
+ return if @permitted_symbols.empty? || @permitted_symbols.include?(name)
+
+ label = ":#{name}"
+ if defined?(Psych::VERSION)
+ raise Psych::DisallowedClass.new("load", label)
+ else
+ raise Psych::DisallowedClass, "Tried to load unspecified class: #{label}"
+ end
+ end
+
+ def normalize_specification_version!(hash)
+ val = hash["specification_version"]
+ return unless val && !val.is_a?(Integer)
+ hash["specification_version"] = val.to_i if val.is_a?(String) && /\A\d+\z/.match?(val)
+ end
+
+ def normalize_array_fields!(hash)
+ ARRAY_FIELDS.each do |field|
+ hash[field] = normalize_array_field(hash[field]) if hash[field]
+ end
+ end
+
+ def normalize_array_field(value)
+ if value.is_a?(Hash)
+ value.values.flatten.compact
+ elsif !value.is_a?(Array) && value
+ [value].flatten.compact
+ else
+ value
+ end
+ end
+ end
+
+ class Emitter
+ def emit(obj)
+ "---#{emit_node(obj, 0)}"
+ end
+
+ private
+
+ def emit_node(obj, indent, quote: false)
+ case obj
+ when Gem::Specification then emit_specification(obj, indent)
+ when Gem::Version then emit_version(obj, indent)
+ when Gem::Platform then emit_platform(obj, indent)
+ when Gem::Requirement then emit_requirement(obj, indent)
+ when Gem::Dependency then emit_dependency(obj, indent)
+ when Hash then emit_hash(obj, indent)
+ when Array then emit_array(obj, indent)
+ when Time then emit_time(obj)
+ when String then emit_string(obj, indent, quote: quote)
+ when NilClass
+ "\n"
+ when Numeric, Symbol, TrueClass, FalseClass
+ " #{obj.inspect}\n"
+ else
+ " #{obj.to_s.inspect}\n"
+ end
+ end
+
+ def emit_specification(spec, indent)
+ parts = [" !ruby/object:Gem::Specification\n"]
+ parts << "#{pad(indent)}name:#{emit_node(spec.name, indent + 2)}"
+ parts << "#{pad(indent)}version:#{emit_node(spec.version, indent + 2)}"
+ parts << "#{pad(indent)}platform: #{spec.platform}\n"
+ if spec.platform.to_s != spec.original_platform.to_s
+ parts << "#{pad(indent)}original_platform: #{spec.original_platform}\n"
+ end
+
+ attributes = Gem::Specification.attribute_names.map(&:to_s).sort - %w[name version platform]
+ attributes.each do |name|
+ val = spec.instance_variable_get("@#{name}")
+ next if val.nil?
+ parts << "#{pad(indent)}#{name}:#{emit_node(val, indent + 2)}"
+ end
+
+ res = parts.join
+ res << "\n" unless res.end_with?("\n")
+ res
+ end
+
+ def emit_version(ver, indent)
+ " !ruby/object:Gem::Version\n" \
+ "#{pad(indent)}version: #{emit_node(ver.version.to_s, indent + 2).lstrip}"
+ end
+
+ def emit_platform(plat, indent)
+ " !ruby/object:Gem::Platform\n" \
+ "#{pad(indent)}cpu:#{emit_node(plat.cpu, indent + 2)}" \
+ "#{pad(indent)}os:#{emit_node(plat.os, indent + 2)}" \
+ "#{pad(indent)}version:#{emit_node(plat.version, indent + 2)}"
+ end
+
+ def emit_requirement(req, indent)
+ " !ruby/object:Gem::Requirement\n" \
+ "#{pad(indent)}requirements:#{emit_node(req.requirements, indent + 2)}"
+ end
+
+ def emit_dependency(dep, indent)
+ [
+ " !ruby/object:Gem::Dependency\n",
+ "#{pad(indent)}name: #{emit_node(dep.name, indent + 2).lstrip}",
+ "#{pad(indent)}requirement:#{emit_node(dep.requirement, indent + 2)}",
+ "#{pad(indent)}type: #{emit_node(dep.type, indent + 2).lstrip}",
+ "#{pad(indent)}prerelease: #{emit_node(dep.prerelease?, indent + 2).lstrip}",
+ "#{pad(indent)}version_requirements:#{emit_node(dep.requirement, indent + 2)}",
+ ].join
+ end
+
+ def emit_hash(hash, indent)
+ if hash.empty?
+ " {}\n"
+ else
+ parts = ["\n"]
+ hash.each do |k, v|
+ is_symbol = k.is_a?(Symbol) || (k.is_a?(String) && k.start_with?(":"))
+ key_str = k.is_a?(Symbol) ? k.inspect : k.to_s
+ parts << "#{pad(indent)}#{key_str}:#{emit_node(v, indent + 2, quote: is_symbol)}"
+ end
+ parts.join
+ end
+ end
+
+ def emit_array(arr, indent)
+ if arr.empty?
+ " []\n"
+ else
+ parts = ["\n"]
+ arr.each do |v|
+ parts << "#{pad(indent)}-#{emit_node(v, indent + 2)}"
+ end
+ parts.join
+ end
+ end
+
+ def emit_time(time)
+ " #{time.utc.strftime("%Y-%m-%d %H:%M:%S.%N Z")}\n"
+ end
+
+ def emit_string(str, indent, quote: false)
+ if str.include?("\n")
+ emit_block_scalar(str, indent)
+ elsif needs_quoting?(str, quote)
+ " #{str.to_s.inspect}\n"
+ else
+ " #{str}\n"
+ end
+ end
+
+ def emit_block_scalar(str, indent)
+ parts = [str.end_with?("\n") ? " |\n" : " |-\n"]
+ str.each_line do |line|
+ parts << "#{pad(indent + 2)}#{line}"
+ end
+ res = parts.join
+ res << "\n" unless res.end_with?("\n")
+ res
+ end
+
+ def needs_quoting?(str, quote)
+ quote || str.empty? ||
+ str =~ /^[!*&:@%$]/ || str =~ /^-?\d+(\.\d+)?$/ || str =~ /^[<>=-]/ ||
+ str == "true" || str == "false" || str == "nil" ||
+ str.include?(":") || str.include?("#") || str.include?("[") || str.include?("]") ||
+ str.include?("{") || str.include?("}") || str.include?(",")
+ end
+
+ def pad(indent)
+ " " * indent
+ end
+ end
+
+ module_function
+
+ def dump(obj)
+ Emitter.new.emit(obj)
+ end
+
+ def load(str, permitted_classes: [], permitted_symbols: [], aliases: true)
+ raise TypeError, "no implicit conversion of nil into String" if str.nil?
+ return nil if str.empty?
+
+ ast = Parser.new(str).parse
+ return nil if ast.nil?
+
+ Builder.new(
+ permitted_classes: permitted_classes,
+ permitted_symbols: permitted_symbols,
+ aliases: aliases
+ ).build(ast)
+ end
+ end
+end
diff --git a/lib/scanf.rb b/lib/scanf.rb
deleted file mode 100644
index cbb98b6f9f..0000000000
--- a/lib/scanf.rb
+++ /dev/null
@@ -1,703 +0,0 @@
-# scanf for Ruby
-#
-# $Release Version: 1.1.2 $
-# $Revision$
-# $Id$
-# $Author$
-#
-# A product of the Austin Ruby Codefest (Austin, Texas, August 2002)
-
-=begin
-
-=scanf for Ruby
-
-==Description
-
-scanf for Ruby is an implementation of the C function scanf(3),
-modified as necessary for Ruby compatibility.
-
-The methods provided are String#scanf, IO#scanf, and
-Kernel#scanf. Kernel#scanf is a wrapper around STDIN.scanf. IO#scanf
-can be used on any IO stream, including file handles and sockets.
-scanf can be called either with or without a block.
-
-scanf for Ruby scans an input string or stream according to a
-<b>format</b>, as described below ("Conversions"), and returns an
-array of matches between the format and the input. The format is
-defined in a string, and is similar (though not identical) to the
-formats used in Kernel#printf and Kernel#sprintf.
-
-The format may contain <b>conversion specifiers</b>, which tell scanf
-what form (type) each particular matched substring should be converted
-to (e.g., decimal integer, floating point number, literal string,
-etc.) The matches and conversions take place from left to right, and
-the conversions themselves are returned as an array.
-
-The format string may also contain characters other than those in the
-conversion specifiers. White space (blanks, tabs, or newlines) in the
-format string matches any amount of white space, including none, in
-the input. Everything else matches only itself.
-
-Scanning stops, and scanf returns, when any input character fails to
-match the specifications in the format string, or when input is
-exhausted, or when everything in the format string has been
-matched. All matches found up to the stopping point are returned in
-the return array (or yielded to the block, if a block was given).
-
-
-==Basic usage
-
- require 'scanf.rb'
-
- # String#scanf and IO#scanf take a single argument (a format string)
- array = aString.scanf("%d%s")
- array = anIO.scanf("%d%s")
-
- # Kernel#scanf reads from STDIN
- array = scanf("%d%s")
-
-==Block usage
-
-When called with a block, scanf keeps scanning the input, cycling back
-to the beginning of the format string, and yields a new array of
-conversions to the block every time the format string is matched
-(including partial matches, but not including complete failures). The
-actual return value of scanf when called with a block is an array
-containing the results of all the executions of the block.
-
- str = "123 abc 456 def 789 ghi"
- str.scanf("%d%s") { |num,str| [ num * 2, str.upcase ] }
- # => [[246, "ABC"], [912, "DEF"], [1578, "GHI"]]
-
-==Conversions
-
-The single argument to scanf is a format string, which generally
-includes one or more conversion specifiers. Conversion specifiers
-begin with the percent character ('%') and include information about
-what scanf should next scan for (string, decimal number, single
-character, etc.).
-
-There may be an optional maximum field width, expressed as a decimal
-integer, between the % and the conversion. If no width is given, a
-default of `infinity' is used (with the exception of the %c specifier;
-see below). Otherwise, given a field width of <em>n</em> for a given
-conversion, at most <em>n</em> characters are scanned in processing
-that conversion. Before conversion begins, most conversions skip
-white space in the input string; this white space is not counted
-against the field width.
-
-The following conversions are available. (See the files EXAMPLES
-and <tt>tests/scanftests.rb</tt> for examples.)
-
-[%]
- Matches a literal `%'. That is, `%%' in the format string matches a
- single input `%' character. No conversion is done, and the resulting
- '%' is not included in the return array.
-
-[d]
- Matches an optionally signed decimal integer.
-
-[u]
- Same as d.
-
-[i]
- Matches an optionally signed integer. The integer is read in base
- 16 if it begins with `0x' or `0X', in base 8 if it begins with `0',
- and in base 10 other- wise. Only characters that correspond to the
- base are recognized.
-
-[o]
- Matches an optionally signed octal integer.
-
-[x,X]
- Matches an optionally signed hexadecimal integer,
-
-[f,g,e,E]
- Matches an optionally signed floating-point number.
-
-[s]
- Matches a sequence of non-white-space character. The input string stops at
- white space or at the maximum field width, whichever occurs first.
-
-[c]
- Matches a single character, or a sequence of <em>n</em> characters if a
- field width of <em>n</em> is specified. The usual skip of leading white
- space is suppressed. To skip white space first, use an explicit space in
- the format.
-
-[<tt>[</tt>]
- Matches a nonempty sequence of characters from the specified set
- of accepted characters. The usual skip of leading white space is
- suppressed. This bracketed sub-expression is interpreted exactly like a
- character class in a Ruby regular expression. (In fact, it is placed as-is
- in a regular expression.) The matching against the input string ends with
- the appearance of a character not in (or, with a circumflex, in) the set,
- or when the field width runs out, whichever comes first.
-
-===Assignment suppression
-
-To require that a particular match occur, but without including the result
-in the return array, place the <b>assignment suppression flag</b>, which is
-the star character ('*'), immediately after the leading '%' of a format
-specifier (just before the field width, if any).
-
-==Examples
-
-See the files <tt>EXAMPLES</tt> and <tt>tests/scanftests.rb</tt>.
-
-==scanf for Ruby compared with scanf in C
-
-scanf for Ruby is based on the C function scanf(3), but with modifications,
-dictated mainly by the underlying differences between the languages.
-
-===Unimplemented flags and specifiers
-
-* The only flag implemented in scanf for Ruby is '<tt>*</tt>' (ignore
- upcoming conversion). Many of the flags available in C versions of scanf(4)
- have to do with the type of upcoming pointer arguments, and are literally
- meaningless in Ruby.
-
-* The <tt>n</tt> specifier (store number of characters consumed so far in
- next pointer) is not implemented.
-
-* The <tt>p</tt> specifier (match a pointer value) is not implemented.
-
-===Altered specifiers
-
-[o,u,x,X]
- In scanf for Ruby, all of these specifiers scan for an optionally signed
- integer, rather than for an unsigned integer like their C counterparts.
-
-===Return values
-
-scanf for Ruby returns an array of successful conversions, whereas
-scanf(3) returns the number of conversions successfully
-completed. (See below for more details on scanf for Ruby's return
-values.)
-
-==Return values
-
-Without a block, scanf returns an array containing all the conversions
-it has found. If none are found, scanf will return an empty array. An
-unsuccesful match is never ignored, but rather always signals the end
-of the scanning operation. If the first unsuccessful match takes place
-after one or more successful matches have already taken place, the
-returned array will contain the results of those successful matches.
-
-With a block scanf returns a 'map'-like array of transformations from
-the block -- that is, an array reflecting what the block did with each
-yielded result from the iterative scanf operation. (See "Block
-usage", above.)
-
-==Test suite
-
-scanf for Ruby includes a suite of unit tests (requiring the
-<tt>TestUnit</tt> package), which can be run with the command <tt>ruby
-tests/scanftests.rb</tt> or the command <tt>make test</tt>.
-
-==Current limitations and bugs
-
-When using IO#scanf under Windows, make sure you open your files in
-binary mode:
-
- File.open("filename", "rb")
-
-so that scanf can keep track of characters correctly.
-
-Support for character classes is reasonably complete (since it
-essentially piggy-backs on Ruby's regular expression handling of
-character classes), but users are advised that character class testing
-has not been exhaustive, and that they should exercise some caution
-in using any of the more complex and/or arcane character class
-idioms.
-
-
-==Technical notes
-
-===Rationale behind scanf for Ruby
-
-The impetus for a scanf implementation in Ruby comes chiefly from the fact
-that existing pattern matching operations, such as Regexp#match and
-String#scan, return all results as strings, which have to be converted to
-integers or floats explicitly in cases where what's ultimately wanted are
-integer or float values.
-
-===Design of scanf for Ruby
-
-scanf for Ruby is essentially a <format string>-to-<regular
-expression> converter.
-
-When scanf is called, a FormatString object is generated from the
-format string ("%d%s...") argument. The FormatString object breaks the
-format string down into atoms ("%d", "%5f", "blah", etc.), and from
-each atom it creates a FormatSpecifier object, which it
-saves.
-
-Each FormatSpecifier has a regular expression fragment and a "handler"
-associated with it. For example, the regular expression fragment
-associated with the format "%d" is "([-+]?\d+)", and the handler
-associated with it is a wrapper around String#to_i. scanf itself calls
-FormatString#match, passing in the input string. FormatString#match
-iterates through its FormatSpecifiers; for each one, it matches the
-corresponding regular expression fragment against the string. If
-there's a match, it sends the matched string to the handler associated
-with the FormatSpecifier.
-
-Thus, to follow up the "%d" example: if "123" occurs in the input
-string when a FormatSpecifier consisting of "%d" is reached, the "123"
-will be matched against "([-+]?\d+)", and the matched string will be
-rendered into an integer by a call to to_i.
-
-The rendered match is then saved to an accumulator array, and the
-input string is reduced to the post-match substring. Thus the string
-is "eaten" from the left as the FormatSpecifiers are applied in
-sequence. (This is done to a duplicate string; the original string is
-not altered.)
-
-As soon as a regular expression fragment fails to match the string, or
-when the FormatString object runs out of FormatSpecifiers, scanning
-stops and results accumulated so far are returned in an array.
-
-==License and copyright
-
-Copyright:: (c) 2002-2003 David Alan Black
-License:: Distributed on the same licensing terms as Ruby itself
-
-==Warranty disclaimer
-
-This software is provided "as is" and without any express or implied
-warranties, including, without limitation, the implied warranties of
-merchantibility and fitness for a particular purpose.
-
-==Credits and acknowledgements
-
-scanf for Ruby was developed as the major activity of the Austin
-Ruby Codefest (Austin, Texas, August 2002).
-
-Principal author:: David Alan Black (mailto:dblack@superlink.net)
-Co-author:: Hal Fulton (mailto:hal9000@hypermetrics.com)
-Project contributors:: Nolan Darilek, Jason Johnston
-
-Thanks to Hal Fulton for hosting the Codefest.
-
-Thanks to Matz for suggestions about the class design.
-
-Thanks to Gavin Sinclair for some feedback on the documentation.
-
-The text for parts of this document, especially the Description and
-Conversions sections, above, were adapted from the Linux Programmer's
-Manual manpage for scanf(3), dated 1995-11-01.
-
-==Bugs and bug reports
-
-scanf for Ruby is based on something of an amalgam of C scanf
-implementations and documentation, rather than on a single canonical
-description. Suggestions for features and behaviors which appear in
-other scanfs, and would be meaningful in Ruby, are welcome, as are
-reports of suspicious behaviors and/or bugs. (Please see "Credits and
-acknowledgements", above, for email addresses.)
-
-=end
-
-module Scanf
-
- class FormatSpecifier
-
- attr_reader :re_string, :matched_string, :conversion, :matched
-
- private
-
- def skip; /^\s*%\*/.match(@spec_string); end
-
- def extract_float(s); s.to_f if s &&! skip; end
- def extract_decimal(s); s.to_i if s &&! skip; end
- def extract_hex(s); s.hex if s &&! skip; end
- def extract_octal(s); s.oct if s &&! skip; end
- def extract_integer(s); Integer(s) if s &&! skip; end
- def extract_plain(s); s unless skip; end
-
- def nil_proc(s); nil; end
-
- public
-
- def to_s
- @spec_string
- end
-
- def count_space?
- /(?:\A|\S)%\*?\d*c|%\d*\[/.match(@spec_string)
- end
-
- def initialize(str)
- @spec_string = str
- h = '[A-Fa-f0-9]'
-
- @re_string, @handler =
- case @spec_string
-
- # %[[:...:]]
- when /%\*?(\[\[:[a-z]+:\]\])/
- [ "(#{$1}+)", :extract_plain ]
-
- # %5[[:...:]]
- when /%\*?(\d+)(\[\[:[a-z]+:\]\])/
- [ "(#{$2}{1,#{$1}})", :extract_plain ]
-
- # %[...]
- when /%\*?\[([^\]]*)\]/
- yes = $1
- if /^\^/.match(yes) then no = yes[1..-1] else no = '^' + yes end
- [ "([#{yes}]+)(?=[#{no}]|\\z)", :extract_plain ]
-
- # %5[...]
- when /%\*?(\d+)\[([^\]]*)\]/
- yes = $2
- w = $1
- [ "([#{yes}]{1,#{w}})", :extract_plain ]
-
- # %i
- when /%\*?i/
- [ "([-+]?(?:(?:0[0-7]+)|(?:0[Xx]#{h}+)|(?:[1-9]\\d*)))", :extract_integer ]
-
- # %5i
- when /%\*?(\d+)i/
- n = $1.to_i
- s = "("
- if n > 1 then s += "[1-9]\\d{1,#{n-1}}|" end
- if n > 1 then s += "0[0-7]{1,#{n-1}}|" end
- if n > 2 then s += "[-+]0[0-7]{1,#{n-2}}|" end
- if n > 2 then s += "[-+][1-9]\\d{1,#{n-2}}|" end
- if n > 2 then s += "0[Xx]#{h}{1,#{n-2}}|" end
- if n > 3 then s += "[-+]0[Xx]#{h}{1,#{n-3}}|" end
- s += "\\d"
- s += ")"
- [ s, :extract_integer ]
-
- # %d, %u
- when /%\*?[du]/
- [ '([-+]?\d+)', :extract_decimal ]
-
- # %5d, %5u
- when /%\*?(\d+)[du]/
- n = $1.to_i
- s = "("
- if n > 1 then s += "[-+]\\d{1,#{n-1}}|" end
- s += "\\d{1,#{$1}})"
- [ s, :extract_decimal ]
-
- # %x
- when /%\*?[Xx]/
- [ "([-+]?(?:0[Xx])?#{h}+)", :extract_hex ]
-
- # %5x
- when /%\*?(\d+)[Xx]/
- n = $1.to_i
- s = "("
- if n > 3 then s += "[-+]0[Xx]#{h}{1,#{n-3}}|" end
- if n > 2 then s += "0[Xx]#{h}{1,#{n-2}}|" end
- if n > 1 then s += "[-+]#{h}{1,#{n-1}}|" end
- s += "#{h}{1,#{n}}"
- s += ")"
- [ s, :extract_hex ]
-
- # %o
- when /%\*?o/
- [ '([-+]?[0-7]+)', :extract_octal ]
-
- # %5o
- when /%\*?(\d+)o/
- [ "([-+][0-7]{1,#{$1.to_i-1}}|[0-7]{1,#{$1}})", :extract_octal ]
-
- # %f
- when /%\*?f/
- [ '([-+]?((\d+(?>(?=[^\d.]|$)))|(\d*(\.(\d*([eE][-+]?\d+)?)))))', :extract_float ]
-
- # %5f
- when /%\*?(\d+)f/
- [ "(\\S{1,#{$1}})", :extract_float ]
-
- # %5s
- when /%\*?(\d+)s/
- [ "(\\S{1,#{$1}})", :extract_plain ]
-
- # %s
- when /%\*?s/
- [ '(\S+)', :extract_plain ]
-
- # %c
- when /\s%\*?c/
- [ "\\s*(.)", :extract_plain ]
-
- # %c
- when /%\*?c/
- [ "(.)", :extract_plain ]
-
- # %5c (whitespace issues are handled by the count_*_space? methods)
- when /%\*?(\d+)c/
- [ "(.{1,#{$1}})", :extract_plain ]
-
- # %%
- when /%%/
- [ '(\s*%)', :nil_proc ]
-
- # literal characters
- else
- [ "(#{Regexp.escape(@spec_string)})", :nil_proc ]
- end
-
- @re_string = '\A' + @re_string
- end
-
- def to_re
- Regexp.new(@re_string,Regexp::MULTILINE)
- end
-
- def match(str)
- @matched = false
- s = str.dup
- s.sub!(/\A\s+/,'') unless count_space?
- res = to_re.match(s)
- if res
- @conversion = send(@handler, res[1])
- @matched_string = @conversion.to_s
- @matched = true
- end
- res
- end
-
- def letter
- @spec_string[/%\*?\d*([a-z\[])/, 1]
- end
-
- def width
- w = @spec_string[/%\*?(\d+)/, 1]
- w && w.to_i
- end
-
- def mid_match?
- return false unless @matched
- cc_no_width = letter == '[' &&! width
- c_or_cc_width = (letter == 'c' || letter == '[') && width
- width_left = c_or_cc_width && (matched_string.size < width)
-
- return width_left || cc_no_width
- end
-
- end
-
- class FormatString
-
- attr_reader :string_left, :last_spec_tried,
- :last_match_tried, :matched_count, :space
-
- SPECIFIERS = 'diuXxofeEgsc'
- REGEX = /
- # possible space, followed by...
- (?:\s*
- # percent sign, followed by...
- %
- # another percent sign, or...
- (?:%|
- # optional assignment suppression flag
- \*?
- # optional maximum field width
- \d*
- # named character class, ...
- (?:\[\[:\w+:\]\]|
- # traditional character class, or...
- \[[^\]]*\]|
- # specifier letter.
- [#{SPECIFIERS}])))|
- # or miscellaneous characters
- [^%\s]+/ix
-
- def initialize(str)
- @specs = []
- @i = 1
- s = str.to_s
- return unless /\S/.match(s)
- @space = true if /\s\z/.match(s)
- @specs.replace s.scan(REGEX).map {|spec| FormatSpecifier.new(spec) }
- end
-
- def to_s
- @specs.join('')
- end
-
- def prune(n=matched_count)
- n.times { @specs.shift }
- end
-
- def spec_count
- @specs.size
- end
-
- def last_spec
- @i == spec_count - 1
- end
-
- def match(str)
- accum = []
- @string_left = str
- @matched_count = 0
-
- @specs.each_with_index do |spec,i|
- @i=i
- @last_spec_tried = spec
- @last_match_tried = spec.match(@string_left)
- break unless @last_match_tried
- @matched_count += 1
-
- accum << spec.conversion
-
- @string_left = @last_match_tried.post_match
- break if @string_left.empty?
- end
- return accum.compact
- end
- end
-end
-
-class IO
-
-# The trick here is doing a match where you grab one *line*
-# of input at a time. The linebreak may or may not occur
-# at the boundary where the string matches a format specifier.
-# And if it does, some rule about whitespace may or may not
-# be in effect...
-#
-# That's why this is much more elaborate than the string
-# version.
-#
-# For each line:
-# Match succeeds (non-emptily)
-# and the last attempted spec/string sub-match succeeded:
-#
-# could the last spec keep matching?
-# yes: save interim results and continue (next line)
-#
-# The last attempted spec/string did not match:
-#
-# are we on the next-to-last spec in the string?
-# yes:
-# is fmt_string.string_left all spaces?
-# yes: does current spec care about input space?
-# yes: fatal failure
-# no: save interim results and continue
-# no: continue [this state could be analyzed further]
-#
-#
-
- def scanf(str,&b)
- return block_scanf(str,&b) if b
- return [] unless str.size > 0
-
- start_position = pos rescue 0
- matched_so_far = 0
- source_buffer = ""
- result_buffer = []
- final_result = []
-
- fstr = Scanf::FormatString.new(str)
-
- loop do
- if eof || (tty? &&! fstr.match(source_buffer))
- final_result.concat(result_buffer)
- break
- end
-
- source_buffer << gets
-
- current_match = fstr.match(source_buffer)
-
- spec = fstr.last_spec_tried
-
- if spec.matched
- if spec.mid_match?
- result_buffer.replace(current_match)
- next
- end
-
- elsif (fstr.matched_count == fstr.spec_count - 1)
- if /\A\s*\z/.match(fstr.string_left)
- break if spec.count_space?
- result_buffer.replace(current_match)
- next
- end
- end
-
- final_result.concat(current_match)
-
- matched_so_far += source_buffer.size
- source_buffer.replace(fstr.string_left)
- matched_so_far -= source_buffer.size
- break if fstr.last_spec
- fstr.prune
- end
- seek(start_position + matched_so_far, IO::SEEK_SET) rescue Errno::ESPIPE
- soak_up_spaces if fstr.last_spec && fstr.space
-
- return final_result
- end
-
- private
-
- def soak_up_spaces
- c = getc
- ungetc(c) if c
- until eof ||! c || /\S/.match(c.chr)
- c = getc
- end
- ungetc(c) if (c && /\S/.match(c.chr))
- end
-
- def block_scanf(str)
- final = []
-# Sub-ideal, since another FS gets created in scanf.
-# But used here to determine the number of specifiers.
- fstr = Scanf::FormatString.new(str)
- last_spec = fstr.last_spec
- begin
- current = scanf(str)
- break if current.empty?
- final.push(yield(current))
- end until eof || fstr.last_spec_tried == last_spec
- return final
- end
-end
-
-class String
-
- def scanf(fstr,&b)
- if b
- block_scanf(fstr,&b)
- else
- fs =
- if fstr.is_a? Scanf::FormatString
- fstr
- else
- Scanf::FormatString.new(fstr)
- end
- fs.match(self)
- end
- end
-
- def block_scanf(fstr,&b)
- fs = Scanf::FormatString.new(fstr)
- str = self.dup
- final = []
- begin
- current = str.scanf(fs)
- final.push(yield(current)) unless current.empty?
- str = fs.string_left
- end until current.empty? || str.empty?
- return final
- end
-end
-
-module Kernel
- private
- def scanf(fs,&b)
- STDIN.scanf(fs,&b)
- end
-end
diff --git a/lib/securerandom.gemspec b/lib/securerandom.gemspec
new file mode 100644
index 0000000000..fe46c11013
--- /dev/null
+++ b/lib/securerandom.gemspec
@@ -0,0 +1,35 @@
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1).join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Tanaka Akira"]
+ spec.email = ["akr@fsij.org"]
+
+ spec.summary = %q{Interface for secure random number generator.}
+ spec.description = %q{Interface for secure random number generator.}
+ spec.homepage = "https://github.com/ruby/securerandom"
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0")
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.metadata["changelog_uri"] = spec.homepage + "/releases"
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ spec.files = Dir.chdir(__dir__) do
+ `git ls-files -z`.split("\x0").reject do |f|
+ (File.expand_path(f) == __FILE__) ||
+ f.start_with?(*%w[bin/ test/ spec/ features/ docs/ rakelib/ .document .git Gemfile Rakefile])
+ end
+ end
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+end
diff --git a/lib/securerandom.rb b/lib/securerandom.rb
index 0de118cb44..6079fdb5c4 100644
--- a/lib/securerandom.rb
+++ b/lib/securerandom.rb
@@ -1,182 +1,102 @@
-# = Secure random number generator interface.
+# -*- coding: us-ascii -*-
+# frozen_string_literal: true
+
+require 'random/formatter'
+
+# == Secure random number generator interface.
+#
+# This library is an interface to secure random number generators which are
+# suitable for generating session keys in HTTP cookies, etc.
#
-# This library is an interface for secure random number generator which is
-# suitable for generating session key in HTTP cookies, etc.
+# You can use this library in your application by requiring it:
#
-# It supports following secure random number generators.
+# require 'securerandom'
+#
+# It supports the following secure random number generators:
#
# * openssl
# * /dev/urandom
# * Win32
#
-# == Example
+# SecureRandom is extended by the Random::Formatter module which
+# defines the following methods:
#
-# # random hexadecimal string.
-# p SecureRandom.hex(10) #=> "52750b30ffbc7de3b362"
-# p SecureRandom.hex(10) #=> "92b15d6c8dc4beb5f559"
-# p SecureRandom.hex(11) #=> "6aca1b5c58e4863e6b81b8"
-# p SecureRandom.hex(12) #=> "94b2fff3e7fd9b9c391a2306"
-# p SecureRandom.hex(13) #=> "39b290146bea6ce975c37cfc23"
-# ...
+# * alphanumeric
+# * base64
+# * choose
+# * gen_random
+# * hex
+# * rand
+# * random_bytes
+# * random_number
+# * urlsafe_base64
+# * uuid
#
-# # random base64 string.
-# p SecureRandom.base64(10) #=> "EcmTPZwWRAozdA=="
-# p SecureRandom.base64(10) #=> "9b0nsevdwNuM/w=="
-# p SecureRandom.base64(10) #=> "KO1nIU+p9DKxGg=="
-# p SecureRandom.base64(11) #=> "l7XEiFja+8EKEtY="
-# p SecureRandom.base64(12) #=> "7kJSM/MzBJI+75j8"
-# p SecureRandom.base64(13) #=> "vKLJ0tXBHqQOuIcSIg=="
-# ...
+# These methods are usable as class methods of SecureRandom such as
+# +SecureRandom.hex+.
#
-# # random binary string.
-# p SecureRandom.random_bytes(10) #=> "\016\t{\370g\310pbr\301"
-# p SecureRandom.random_bytes(10) #=> "\323U\030TO\234\357\020\a\337"
-# ...
-
-begin
- require 'openssl'
-rescue LoadError
-end
+# If a secure random number generator is not available,
+# +NotImplementedError+ is raised.
module SecureRandom
- # SecureRandom.random_bytes generates a random binary string.
- #
- # The argument n specifies the length of the result string.
- #
- # If n is not specified, 16 is assumed.
- # It may be larger in future.
- #
- # If secure random number generator is not available,
- # NotImplementedError is raised.
- def self.random_bytes(n=nil)
- n ||= 16
- if defined? OpenSSL::Random
- return OpenSSL::Random.random_bytes(n)
- end
+ # The version
+ VERSION = "0.4.1"
- if !defined?(@has_urandom) || @has_urandom
- flags = File::RDONLY
- flags |= File::NONBLOCK if defined? File::NONBLOCK
- flags |= File::NOCTTY if defined? File::NOCTTY
- flags |= File::NOFOLLOW if defined? File::NOFOLLOW
- begin
- File.open("/dev/urandom", flags) {|f|
- unless f.stat.chardev?
- raise Errno::ENOENT
- end
- @has_urandom = true
- ret = f.readpartial(n)
- if ret.length != n
- raise NotImplementedError, "Unexpected partial read from random device"
- end
- return ret
- }
- rescue Errno::ENOENT
- @has_urandom = false
- end
+ class << self
+ # Returns a random binary string containing +size+ bytes.
+ #
+ # See Random.bytes
+ def bytes(n)
+ return gen_random(n)
end
- if !defined?(@has_win32)
- begin
- require 'Win32API'
+ # Compatibility methods for Ruby 3.2, we can remove this after dropping to support Ruby 3.2
+ def alphanumeric(n = nil, chars: ALPHANUMERIC)
+ n = 16 if n.nil?
+ choose(chars, n)
+ end if RUBY_VERSION < '3.3'
- crypt_acquire_context = Win32API.new("advapi32", "CryptAcquireContext", 'PPPII', 'L')
- @crypt_gen_random = Win32API.new("advapi32", "CryptGenRandom", 'LIP', 'L')
+ private
- hProvStr = " " * 4
- prov_rsa_full = 1
- crypt_verifycontext = 0xF0000000
+ # :stopdoc:
- if crypt_acquire_context.call(hProvStr, nil, nil, prov_rsa_full, crypt_verifycontext) == 0
- raise SystemCallError, "CryptAcquireContext failed: #{lastWin32ErrorMessage}"
- end
- @hProv, = hProvStr.unpack('L')
+ # Implementation using OpenSSL
+ def gen_random_openssl(n)
+ return OpenSSL::Random.random_bytes(n)
+ end
- @has_win32 = true
- rescue LoadError
- @has_win32 = false
+ # Implementation using system random device
+ def gen_random_urandom(n)
+ ret = Random.urandom(n)
+ unless ret
+ raise NotImplementedError, "No random device"
end
- end
- if @has_win32
- bytes = " " * n
- if @crypt_gen_random.call(@hProv, bytes.size, bytes) == 0
- raise SystemCallError, "CryptGenRandom failed: #{lastWin32ErrorMessage}"
+ unless ret.length == n
+ raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes"
end
- return bytes
+ ret
end
- raise NotImplementedError, "No random device"
- end
-
- # SecureRandom.hex generates a random hex string.
- #
- # The argument n specifies the length of the random length.
- # The length of the result string is twice of n.
- #
- # If n is not specified, 16 is assumed.
- # It may be larger in future.
- #
- # If secure random number generator is not available,
- # NotImplementedError is raised.
- def self.hex(n=nil)
- random_bytes(n).unpack("H*")[0]
- end
-
- # SecureRandom.base64 generates a random base64 string.
- #
- # The argument n specifies the length of the random length.
- # The length of the result string is about 4/3 of n.
- #
- # If n is not specified, 16 is assumed.
- # It may be larger in future.
- #
- # If secure random number generator is not available,
- # NotImplementedError is raised.
- def self.base64(n=nil)
- [random_bytes(n)].pack("m*").delete("\n")
- end
-
- # SecureRandom.random_number generates a random number.
- #
- # If an positive integer is given as n,
- # SecureRandom.random_number returns an integer:
- # 0 <= SecureRandom.random_number(n) < n.
- #
- # If 0 is given or an argument is not given,
- # SecureRandom.random_number returns an float:
- # 0.0 <= SecureRandom.random_number() < 1.0.
- def self.random_number(n=0)
- if 0 < n
- hex = n.to_s(16)
- hex = '0' + hex if (hex.length & 1) == 1
- bin = [hex].pack("H*")
- mask = bin[0].ord
- mask |= mask >> 1
- mask |= mask >> 2
- mask |= mask >> 4
+ begin
+ # Check if Random.urandom is available
+ Random.urandom(1)
+ alias gen_random gen_random_urandom
+ rescue RuntimeError
begin
- rnd = SecureRandom.random_bytes(bin.length)
- rnd[0] = (rnd[0].ord & mask).chr
- end until rnd < bin
- rnd.unpack("H*")[0].hex
- else
- # assumption: Float::MANT_DIG <= 64
- i64 = SecureRandom.random_bytes(8).unpack("Q")[0]
- Math.ldexp(i64 >> (64-Float::MANT_DIG), -Float::MANT_DIG)
+ require 'openssl'
+ rescue NoMethodError
+ raise NotImplementedError, "No random device"
+ else
+ alias gen_random gen_random_openssl
+ end
end
- end
- # Following code is based on David Garamond's GUID library for Ruby.
- def self.lastWin32ErrorMessage # :nodoc:
- get_last_error = Win32API.new("kernel32", "GetLastError", '', 'L')
- format_message = Win32API.new("kernel32", "FormatMessageA", 'LPLLPLPPPPPPPP', 'L')
- format_message_ignore_inserts = 0x00000200
- format_message_from_system = 0x00001000
+ # :startdoc:
- code = get_last_error.call
- msg = "\0" * 1024
- len = format_message.call(format_message_ignore_inserts + format_message_from_system, 0, code, 0, msg, 1024, nil, nil, nil, nil, nil, nil, nil, nil)
- msg[0, len].tr("\r", '').chomp
+ # Generate random data bytes for Random::Formatter
+ public :gen_random
end
end
+
+SecureRandom.extend(Random::Formatter)
diff --git a/lib/set.rb b/lib/set.rb
deleted file mode 100644
index f930c5e4a9..0000000000
--- a/lib/set.rb
+++ /dev/null
@@ -1,1274 +0,0 @@
-#!/usr/bin/env ruby
-#--
-# set.rb - defines the Set class
-#++
-# Copyright (c) 2002-2008 Akinori MUSHA <knu@iDaemons.org>
-#
-# Documentation by Akinori MUSHA and Gavin Sinclair.
-#
-# All rights reserved. You can redistribute and/or modify it under the same
-# terms as Ruby.
-#
-# $Id$
-#
-# == Overview
-#
-# This library provides the Set class, which deals with a collection
-# of unordered values with no duplicates. It is a hybrid of Array's
-# intuitive inter-operation facilities and Hash's fast lookup. If you
-# need to keep values ordered, use the SortedSet class.
-#
-# The method +to_set+ is added to Enumerable for convenience.
-#
-# See the Set class for an example of usage.
-
-
-#
-# Set implements a collection of unordered values with no duplicates.
-# This is a hybrid of Array's intuitive inter-operation facilities and
-# Hash's fast lookup.
-#
-# The equality of each couple of elements is determined according to
-# Object#eql? and Object#hash, since Set uses Hash as storage.
-#
-# Set is easy to use with Enumerable objects (implementing +each+).
-# Most of the initializer methods and binary operators accept generic
-# Enumerable objects besides sets and arrays. An Enumerable object
-# can be converted to Set using the +to_set+ method.
-#
-# == Example
-#
-# require 'set'
-# s1 = Set.new [1, 2] # -> #<Set: {1, 2}>
-# s2 = [1, 2].to_set # -> #<Set: {1, 2}>
-# s1 == s2 # -> true
-# s1.add("foo") # -> #<Set: {1, 2, "foo"}>
-# s1.merge([2, 6]) # -> #<Set: {6, 1, 2, "foo"}>
-# s1.subset? s2 # -> false
-# s2.subset? s1 # -> true
-#
-# == Contact
-#
-# - Akinori MUSHA <knu@iDaemons.org> (current maintainer)
-#
-class Set
- include Enumerable
-
- # Creates a new set containing the given objects.
- def self.[](*ary)
- new(ary)
- end
-
- # Creates a new set containing the elements of the given enumerable
- # object.
- #
- # If a block is given, the elements of enum are preprocessed by the
- # given block.
- def initialize(enum = nil, &block) # :yields: o
- @hash ||= Hash.new
-
- enum.nil? and return
-
- if block
- enum.each { |o| add(block[o]) }
- else
- merge(enum)
- end
- end
-
- # Copy internal hash.
- def initialize_copy(orig)
- @hash = orig.instance_eval{@hash}.dup
- end
-
- def freeze # :nodoc:
- super
- @hash.freeze
- self
- end
-
- def taint # :nodoc:
- super
- @hash.taint
- self
- end
-
- def untaint # :nodoc:
- super
- @hash.untaint
- self
- end
-
- # Returns the number of elements.
- def size
- @hash.size
- end
- alias length size
-
- # Returns true if the set contains no elements.
- def empty?
- @hash.empty?
- end
-
- # Removes all elements and returns self.
- def clear
- @hash.clear
- self
- end
-
- # Replaces the contents of the set with the contents of the given
- # enumerable object and returns self.
- def replace(enum)
- if enum.class == self.class
- @hash.replace(enum.instance_eval { @hash })
- else
- clear
- enum.each { |o| add(o) }
- end
-
- self
- end
-
- # Converts the set to an array. The order of elements is uncertain.
- def to_a
- @hash.keys
- end
-
- def flatten_merge(set, seen = Set.new)
- set.each { |e|
- if e.is_a?(Set)
- if seen.include?(e_id = e.object_id)
- raise ArgumentError, "tried to flatten recursive Set"
- end
-
- seen.add(e_id)
- flatten_merge(e, seen)
- seen.delete(e_id)
- else
- add(e)
- end
- }
-
- self
- end
- protected :flatten_merge
-
- # Returns a new set that is a copy of the set, flattening each
- # containing set recursively.
- def flatten
- self.class.new.flatten_merge(self)
- end
-
- # Equivalent to Set#flatten, but replaces the receiver with the
- # result in place. Returns nil if no modifications were made.
- def flatten!
- if detect { |e| e.is_a?(Set) }
- replace(flatten())
- else
- nil
- end
- end
-
- # Returns true if the set contains the given object.
- def include?(o)
- @hash.include?(o)
- end
- alias member? include?
-
- # Returns true if the set is a superset of the given set.
- def superset?(set)
- set.is_a?(Set) or raise ArgumentError, "value must be a set"
- return false if size < set.size
- set.all? { |o| include?(o) }
- end
-
- # Returns true if the set is a proper superset of the given set.
- def proper_superset?(set)
- set.is_a?(Set) or raise ArgumentError, "value must be a set"
- return false if size <= set.size
- set.all? { |o| include?(o) }
- end
-
- # Returns true if the set is a subset of the given set.
- def subset?(set)
- set.is_a?(Set) or raise ArgumentError, "value must be a set"
- return false if set.size < size
- all? { |o| set.include?(o) }
- end
-
- # Returns true if the set is a proper subset of the given set.
- def proper_subset?(set)
- set.is_a?(Set) or raise ArgumentError, "value must be a set"
- return false if set.size <= size
- all? { |o| set.include?(o) }
- end
-
- # Calls the given block once for each element in the set, passing
- # the element as parameter. Returns an enumerator if no block is
- # given.
- def each
- block_given? or return enum_for(__method__)
- @hash.each_key { |o| yield(o) }
- self
- end
-
- # Adds the given object to the set and returns self. Use +merge+ to
- # add many elements at once.
- def add(o)
- @hash[o] = true
- self
- end
- alias << add
-
- # Adds the given object to the set and returns self. If the
- # object is already in the set, returns nil.
- def add?(o)
- if include?(o)
- nil
- else
- add(o)
- end
- end
-
- # Deletes the given object from the set and returns self. Use +subtract+ to
- # delete many items at once.
- def delete(o)
- @hash.delete(o)
- self
- end
-
- # Deletes the given object from the set and returns self. If the
- # object is not in the set, returns nil.
- def delete?(o)
- if include?(o)
- delete(o)
- else
- nil
- end
- end
-
- # Deletes every element of the set for which block evaluates to
- # true, and returns self.
- def delete_if
- block_given? or return enum_for(__method__)
- to_a.each { |o| @hash.delete(o) if yield(o) }
- self
- end
-
- # Replaces the elements with ones returned by collect().
- def collect!
- block_given? or return enum_for(__method__)
- set = self.class.new
- each { |o| set << yield(o) }
- replace(set)
- end
- alias map! collect!
-
- # Equivalent to Set#delete_if, but returns nil if no changes were
- # made.
- def reject!
- block_given? or return enum_for(__method__)
- n = size
- delete_if { |o| yield(o) }
- size == n ? nil : self
- end
-
- # Merges the elements of the given enumerable object to the set and
- # returns self.
- def merge(enum)
- if enum.is_a?(Set)
- @hash.update(enum.instance_eval { @hash })
- else
- enum.each { |o| add(o) }
- end
-
- self
- end
-
- # Deletes every element that appears in the given enumerable object
- # and returns self.
- def subtract(enum)
- enum.each { |o| delete(o) }
- self
- end
-
- # Returns a new set built by merging the set and the elements of the
- # given enumerable object.
- def |(enum)
- dup.merge(enum)
- end
- alias + | ##
- alias union | ##
-
- # Returns a new set built by duplicating the set, removing every
- # element that appears in the given enumerable object.
- def -(enum)
- dup.subtract(enum)
- end
- alias difference - ##
-
- # Returns a new set containing elements common to the set and the
- # given enumerable object.
- def &(enum)
- n = self.class.new
- enum.each { |o| n.add(o) if include?(o) }
- n
- end
- alias intersection & ##
-
- # Returns a new set containing elements exclusive between the set
- # and the given enumerable object. (set ^ enum) is equivalent to
- # ((set | enum) - (set & enum)).
- def ^(enum)
- n = Set.new(enum)
- each { |o| if n.include?(o) then n.delete(o) else n.add(o) end }
- n
- end
-
- # Returns true if two sets are equal. The equality of each couple
- # of elements is defined according to Object#eql?.
- def ==(set)
- equal?(set) and return true
-
- set.is_a?(Set) && size == set.size or return false
-
- hash = @hash.dup
- set.all? { |o| hash.include?(o) }
- end
-
- def hash # :nodoc:
- @hash.hash
- end
-
- def eql?(o) # :nodoc:
- return false unless o.is_a?(Set)
- @hash.eql?(o.instance_eval{@hash})
- end
-
- # Classifies the set by the return value of the given block and
- # returns a hash of {value => set of elements} pairs. The block is
- # called once for each element of the set, passing the element as
- # parameter.
- #
- # e.g.:
- #
- # require 'set'
- # files = Set.new(Dir.glob("*.rb"))
- # hash = files.classify { |f| File.mtime(f).year }
- # p hash # => {2000=>#<Set: {"a.rb", "b.rb"}>,
- # # 2001=>#<Set: {"c.rb", "d.rb", "e.rb"}>,
- # # 2002=>#<Set: {"f.rb"}>}
- def classify # :yields: o
- block_given? or return enum_for(__method__)
-
- h = {}
-
- each { |i|
- x = yield(i)
- (h[x] ||= self.class.new).add(i)
- }
-
- h
- end
-
- # Divides the set into a set of subsets according to the commonality
- # defined by the given block.
- #
- # If the arity of the block is 2, elements o1 and o2 are in common
- # if block.call(o1, o2) is true. Otherwise, elements o1 and o2 are
- # in common if block.call(o1) == block.call(o2).
- #
- # e.g.:
- #
- # require 'set'
- # numbers = Set[1, 3, 4, 6, 9, 10, 11]
- # set = numbers.divide { |i,j| (i - j).abs == 1 }
- # p set # => #<Set: {#<Set: {1}>,
- # # #<Set: {11, 9, 10}>,
- # # #<Set: {3, 4}>,
- # # #<Set: {6}>}>
- def divide(&func)
- func or return enum_for(__method__)
-
- if func.arity == 2
- require 'tsort'
-
- class << dig = {} # :nodoc:
- include TSort
-
- alias tsort_each_node each_key
- def tsort_each_child(node, &block)
- fetch(node).each(&block)
- end
- end
-
- each { |u|
- dig[u] = a = []
- each{ |v| func.call(u, v) and a << v }
- }
-
- set = Set.new()
- dig.each_strongly_connected_component { |css|
- set.add(self.class.new(css))
- }
- set
- else
- Set.new(classify(&func).values)
- end
- end
-
- InspectKey = :__inspect_key__ # :nodoc:
-
- # Returns a string containing a human-readable representation of the
- # set. ("#<Set: {element1, element2, ...}>")
- def inspect
- ids = (Thread.current[InspectKey] ||= [])
-
- if ids.include?(object_id)
- return sprintf('#<%s: {...}>', self.class.name)
- end
-
- begin
- ids << object_id
- return sprintf('#<%s: {%s}>', self.class, to_a.inspect[1..-2])
- ensure
- ids.pop
- end
- end
-
- def pretty_print(pp) # :nodoc:
- pp.text sprintf('#<%s: {', self.class.name)
- pp.nest(1) {
- pp.seplist(self) { |o|
- pp.pp o
- }
- }
- pp.text "}>"
- end
-
- def pretty_print_cycle(pp) # :nodoc:
- pp.text sprintf('#<%s: {%s}>', self.class.name, empty? ? '' : '...')
- end
-end
-
-# SortedSet implements a set which elements are sorted in order. See Set.
-class SortedSet < Set
- @@setup = false
-
- class << self
- def [](*ary) # :nodoc:
- new(ary)
- end
-
- def setup # :nodoc:
- @@setup and return
-
- module_eval {
- # a hack to shut up warning
- alias old_init initialize
- remove_method :old_init
- }
- begin
- require 'rbtree'
-
- module_eval %{
- def initialize(*args, &block)
- @hash = RBTree.new
- super
- end
- }
- rescue LoadError
- module_eval %{
- def initialize(*args, &block)
- @keys = nil
- super
- end
-
- def clear
- @keys = nil
- super
- end
-
- def replace(enum)
- @keys = nil
- super
- end
-
- def add(o)
- @keys = nil
- @hash[o] = true
- self
- end
- alias << add
-
- def delete(o)
- @keys = nil
- @hash.delete(o)
- self
- end
-
- def delete_if
- block_given? or return enum_for(__method__)
- n = @hash.size
- super
- @keys = nil if @hash.size != n
- self
- end
-
- def merge(enum)
- @keys = nil
- super
- end
-
- def each
- block_given? or return enum_for(__method__)
- to_a.each { |o| yield(o) }
- self
- end
-
- def to_a
- (@keys = @hash.keys).sort! unless @keys
- @keys
- end
- }
- end
-
- @@setup = true
- end
- end
-
- def initialize(*args, &block) # :nodoc:
- SortedSet.setup
- initialize(*args, &block)
- end
-end
-
-module Enumerable
- # Makes a set from the enumerable object with given arguments.
- # Needs to +require "set"+ to use this method.
- def to_set(klass = Set, *args, &block)
- klass.new(self, *args, &block)
- end
-end
-
-# =begin
-# == RestricedSet class
-# RestricedSet implements a set with restrictions defined by a given
-# block.
-#
-# === Super class
-# Set
-#
-# === Class Methods
-# --- RestricedSet::new(enum = nil) { |o| ... }
-# --- RestricedSet::new(enum = nil) { |rset, o| ... }
-# Creates a new restricted set containing the elements of the given
-# enumerable object. Restrictions are defined by the given block.
-#
-# If the block's arity is 2, it is called with the RestrictedSet
-# itself and an object to see if the object is allowed to be put in
-# the set.
-#
-# Otherwise, the block is called with an object to see if the object
-# is allowed to be put in the set.
-#
-# === Instance Methods
-# --- restriction_proc
-# Returns the restriction procedure of the set.
-#
-# =end
-#
-# class RestricedSet < Set
-# def initialize(*args, &block)
-# @proc = block or raise ArgumentError, "missing a block"
-#
-# if @proc.arity == 2
-# instance_eval %{
-# def add(o)
-# @hash[o] = true if @proc.call(self, o)
-# self
-# end
-# alias << add
-#
-# def add?(o)
-# if include?(o) || !@proc.call(self, o)
-# nil
-# else
-# @hash[o] = true
-# self
-# end
-# end
-#
-# def replace(enum)
-# clear
-# enum.each { |o| add(o) }
-#
-# self
-# end
-#
-# def merge(enum)
-# enum.each { |o| add(o) }
-#
-# self
-# end
-# }
-# else
-# instance_eval %{
-# def add(o)
-# if @proc.call(o)
-# @hash[o] = true
-# end
-# self
-# end
-# alias << add
-#
-# def add?(o)
-# if include?(o) || !@proc.call(o)
-# nil
-# else
-# @hash[o] = true
-# self
-# end
-# end
-# }
-# end
-#
-# super(*args)
-# end
-#
-# def restriction_proc
-# @proc
-# end
-# end
-
-if $0 == __FILE__
- eval DATA.read, nil, $0, __LINE__+4
-end
-
-__END__
-
-require 'test/unit'
-
-class TC_Set < Test::Unit::TestCase
- def test_aref
- assert_nothing_raised {
- Set[]
- Set[nil]
- Set[1,2,3]
- }
-
- assert_equal(0, Set[].size)
- assert_equal(1, Set[nil].size)
- assert_equal(1, Set[[]].size)
- assert_equal(1, Set[[nil]].size)
-
- set = Set[2,4,6,4]
- assert_equal(Set.new([2,4,6]), set)
- end
-
- def test_s_new
- assert_nothing_raised {
- Set.new()
- Set.new(nil)
- Set.new([])
- Set.new([1,2])
- Set.new('a'..'c')
- }
- assert_raises(NoMethodError) {
- Set.new(false)
- }
- assert_raises(NoMethodError) {
- Set.new(1)
- }
- assert_raises(ArgumentError) {
- Set.new(1,2)
- }
-
- assert_equal(0, Set.new().size)
- assert_equal(0, Set.new(nil).size)
- assert_equal(0, Set.new([]).size)
- assert_equal(1, Set.new([nil]).size)
-
- ary = [2,4,6,4]
- set = Set.new(ary)
- ary.clear
- assert_equal(false, set.empty?)
- assert_equal(3, set.size)
-
- ary = [1,2,3]
-
- s = Set.new(ary) { |o| o * 2 }
- assert_equal([2,4,6], s.sort)
- end
-
- def test_clone
- set1 = Set.new
- set2 = set1.clone
- set1 << 'abc'
- assert_equal(Set.new, set2)
- end
-
- def test_dup
- set1 = Set[1,2]
- set2 = set1.dup
-
- assert_not_same(set1, set2)
-
- assert_equal(set1, set2)
-
- set1.add(3)
-
- assert_not_equal(set1, set2)
- end
-
- def test_size
- assert_equal(0, Set[].size)
- assert_equal(2, Set[1,2].size)
- assert_equal(2, Set[1,2,1].size)
- end
-
- def test_empty?
- assert_equal(true, Set[].empty?)
- assert_equal(false, Set[1, 2].empty?)
- end
-
- def test_clear
- set = Set[1,2]
- ret = set.clear
-
- assert_same(set, ret)
- assert_equal(true, set.empty?)
- end
-
- def test_replace
- set = Set[1,2]
- ret = set.replace('a'..'c')
-
- assert_same(set, ret)
- assert_equal(Set['a','b','c'], set)
- end
-
- def test_to_a
- set = Set[1,2,3,2]
- ary = set.to_a
-
- assert_equal([1,2,3], ary.sort)
- end
-
- def test_flatten
- # test1
- set1 = Set[
- 1,
- Set[
- 5,
- Set[7,
- Set[0]
- ],
- Set[6,2],
- 1
- ],
- 3,
- Set[3,4]
- ]
-
- set2 = set1.flatten
- set3 = Set.new(0..7)
-
- assert_not_same(set2, set1)
- assert_equal(set3, set2)
-
- # test2; destructive
- orig_set1 = set1
- set1.flatten!
-
- assert_same(orig_set1, set1)
- assert_equal(set3, set1)
-
- # test3; multiple occurrences of a set in an set
- set1 = Set[1, 2]
- set2 = Set[set1, Set[set1, 4], 3]
-
- assert_nothing_raised {
- set2.flatten!
- }
-
- assert_equal(Set.new(1..4), set2)
-
- # test4; recursion
- set2 = Set[]
- set1 = Set[1, set2]
- set2.add(set1)
-
- assert_raises(ArgumentError) {
- set1.flatten!
- }
-
- # test5; miscellaneous
- empty = Set[]
- set = Set[Set[empty, "a"],Set[empty, "b"]]
-
- assert_nothing_raised {
- set.flatten
- }
-
- set1 = empty.merge(Set["no_more", set])
-
- assert_nil(Set.new(0..31).flatten!)
-
- x = Set[Set[],Set[1,2]].flatten!
- y = Set[1,2]
-
- assert_equal(x, y)
- end
-
- def test_include?
- set = Set[1,2,3]
-
- assert_equal(true, set.include?(1))
- assert_equal(true, set.include?(2))
- assert_equal(true, set.include?(3))
- assert_equal(false, set.include?(0))
- assert_equal(false, set.include?(nil))
-
- set = Set["1",nil,"2",nil,"0","1",false]
- assert_equal(true, set.include?(nil))
- assert_equal(true, set.include?(false))
- assert_equal(true, set.include?("1"))
- assert_equal(false, set.include?(0))
- assert_equal(false, set.include?(true))
- end
-
- def test_superset?
- set = Set[1,2,3]
-
- assert_raises(ArgumentError) {
- set.superset?()
- }
-
- assert_raises(ArgumentError) {
- set.superset?(2)
- }
-
- assert_raises(ArgumentError) {
- set.superset?([2])
- }
-
- assert_equal(true, set.superset?(Set[]))
- assert_equal(true, set.superset?(Set[1,2]))
- assert_equal(true, set.superset?(Set[1,2,3]))
- assert_equal(false, set.superset?(Set[1,2,3,4]))
- assert_equal(false, set.superset?(Set[1,4]))
-
- assert_equal(true, Set[].superset?(Set[]))
- end
-
- def test_proper_superset?
- set = Set[1,2,3]
-
- assert_raises(ArgumentError) {
- set.proper_superset?()
- }
-
- assert_raises(ArgumentError) {
- set.proper_superset?(2)
- }
-
- assert_raises(ArgumentError) {
- set.proper_superset?([2])
- }
-
- assert_equal(true, set.proper_superset?(Set[]))
- assert_equal(true, set.proper_superset?(Set[1,2]))
- assert_equal(false, set.proper_superset?(Set[1,2,3]))
- assert_equal(false, set.proper_superset?(Set[1,2,3,4]))
- assert_equal(false, set.proper_superset?(Set[1,4]))
-
- assert_equal(false, Set[].proper_superset?(Set[]))
- end
-
- def test_subset?
- set = Set[1,2,3]
-
- assert_raises(ArgumentError) {
- set.subset?()
- }
-
- assert_raises(ArgumentError) {
- set.subset?(2)
- }
-
- assert_raises(ArgumentError) {
- set.subset?([2])
- }
-
- assert_equal(true, set.subset?(Set[1,2,3,4]))
- assert_equal(true, set.subset?(Set[1,2,3]))
- assert_equal(false, set.subset?(Set[1,2]))
- assert_equal(false, set.subset?(Set[]))
-
- assert_equal(true, Set[].subset?(Set[1]))
- assert_equal(true, Set[].subset?(Set[]))
- end
-
- def test_proper_subset?
- set = Set[1,2,3]
-
- assert_raises(ArgumentError) {
- set.proper_subset?()
- }
-
- assert_raises(ArgumentError) {
- set.proper_subset?(2)
- }
-
- assert_raises(ArgumentError) {
- set.proper_subset?([2])
- }
-
- assert_equal(true, set.proper_subset?(Set[1,2,3,4]))
- assert_equal(false, set.proper_subset?(Set[1,2,3]))
- assert_equal(false, set.proper_subset?(Set[1,2]))
- assert_equal(false, set.proper_subset?(Set[]))
-
- assert_equal(false, Set[].proper_subset?(Set[]))
- end
-
- def test_each
- ary = [1,3,5,7,10,20]
- set = Set.new(ary)
-
- ret = set.each { |o| }
- assert_same(set, ret)
-
- e = set.each
- assert_instance_of(Enumerator, e)
-
- assert_nothing_raised {
- set.each { |o|
- ary.delete(o) or raise "unexpected element: #{o}"
- }
-
- ary.empty? or raise "forgotten elements: #{ary.join(', ')}"
- }
- end
-
- def test_add
- set = Set[1,2,3]
-
- ret = set.add(2)
- assert_same(set, ret)
- assert_equal(Set[1,2,3], set)
-
- ret = set.add?(2)
- assert_nil(ret)
- assert_equal(Set[1,2,3], set)
-
- ret = set.add(4)
- assert_same(set, ret)
- assert_equal(Set[1,2,3,4], set)
-
- ret = set.add?(5)
- assert_same(set, ret)
- assert_equal(Set[1,2,3,4,5], set)
- end
-
- def test_delete
- set = Set[1,2,3]
-
- ret = set.delete(4)
- assert_same(set, ret)
- assert_equal(Set[1,2,3], set)
-
- ret = set.delete?(4)
- assert_nil(ret)
- assert_equal(Set[1,2,3], set)
-
- ret = set.delete(2)
- assert_equal(set, ret)
- assert_equal(Set[1,3], set)
-
- ret = set.delete?(1)
- assert_equal(set, ret)
- assert_equal(Set[3], set)
- end
-
- def test_delete_if
- set = Set.new(1..10)
- ret = set.delete_if { |i| i > 10 }
- assert_same(set, ret)
- assert_equal(Set.new(1..10), set)
-
- set = Set.new(1..10)
- ret = set.delete_if { |i| i % 3 == 0 }
- assert_same(set, ret)
- assert_equal(Set[1,2,4,5,7,8,10], set)
- end
-
- def test_collect!
- set = Set[1,2,3,'a','b','c',-1..1,2..4]
-
- ret = set.collect! { |i|
- case i
- when Numeric
- i * 2
- when String
- i.upcase
- else
- nil
- end
- }
-
- assert_same(set, ret)
- assert_equal(Set[2,4,6,'A','B','C',nil], set)
- end
-
- def test_reject!
- set = Set.new(1..10)
-
- ret = set.reject! { |i| i > 10 }
- assert_nil(ret)
- assert_equal(Set.new(1..10), set)
-
- ret = set.reject! { |i| i % 3 == 0 }
- assert_same(set, ret)
- assert_equal(Set[1,2,4,5,7,8,10], set)
- end
-
- def test_merge
- set = Set[1,2,3]
-
- ret = set.merge([2,4,6])
- assert_same(set, ret)
- assert_equal(Set[1,2,3,4,6], set)
- end
-
- def test_subtract
- set = Set[1,2,3]
-
- ret = set.subtract([2,4,6])
- assert_same(set, ret)
- assert_equal(Set[1,3], set)
- end
-
- def test_plus
- set = Set[1,2,3]
-
- ret = set + [2,4,6]
- assert_not_same(set, ret)
- assert_equal(Set[1,2,3,4,6], ret)
- end
-
- def test_minus
- set = Set[1,2,3]
-
- ret = set - [2,4,6]
- assert_not_same(set, ret)
- assert_equal(Set[1,3], ret)
- end
-
- def test_and
- set = Set[1,2,3,4]
-
- ret = set & [2,4,6]
- assert_not_same(set, ret)
- assert_equal(Set[2,4], ret)
- end
-
- def test_xor
- set = Set[1,2,3,4]
- ret = set ^ [2,4,5,5]
- assert_not_same(set, ret)
- assert_equal(Set[1,3,5], ret)
- end
-
- def test_eq
- set1 = Set[2,3,1]
- set2 = Set[1,2,3]
-
- assert_equal(set1, set1)
- assert_equal(set1, set2)
- assert_not_equal(Set[1], [1])
-
- set1 = Class.new(Set)["a", "b"]
- set2 = Set["a", "b", set1]
- set1 = set1.add(set1.clone)
-
-# assert_equal(set1, set2)
-# assert_equal(set2, set1)
- assert_equal(set2, set2.clone)
- assert_equal(set1.clone, set1)
-
- assert_not_equal(Set[Exception.new,nil], Set[Exception.new,Exception.new], "[ruby-dev:26127]")
- end
-
- # def test_hash
- # end
-
- # def test_eql?
- # end
-
- def test_classify
- set = Set.new(1..10)
- ret = set.classify { |i| i % 3 }
-
- assert_equal(3, ret.size)
- assert_instance_of(Hash, ret)
- ret.each_value { |value| assert_instance_of(Set, value) }
- assert_equal(Set[3,6,9], ret[0])
- assert_equal(Set[1,4,7,10], ret[1])
- assert_equal(Set[2,5,8], ret[2])
- end
-
- def test_divide
- set = Set.new(1..10)
- ret = set.divide { |i| i % 3 }
-
- assert_equal(3, ret.size)
- n = 0
- ret.each { |s| n += s.size }
- assert_equal(set.size, n)
- assert_equal(set, ret.flatten)
-
- set = Set[7,10,5,11,1,3,4,9,0]
- ret = set.divide { |a,b| (a - b).abs == 1 }
-
- assert_equal(4, ret.size)
- n = 0
- ret.each { |s| n += s.size }
- assert_equal(set.size, n)
- assert_equal(set, ret.flatten)
- ret.each { |s|
- if s.include?(0)
- assert_equal(Set[0,1], s)
- elsif s.include?(3)
- assert_equal(Set[3,4,5], s)
- elsif s.include?(7)
- assert_equal(Set[7], s)
- elsif s.include?(9)
- assert_equal(Set[9,10,11], s)
- else
- raise "unexpected group: #{s.inspect}"
- end
- }
- end
-
- def test_inspect
- set1 = Set[1]
-
- assert_equal('#<Set: {1}>', set1.inspect)
-
- set2 = Set[Set[0], 1, 2, set1]
- assert_equal(false, set2.inspect.include?('#<Set: {...}>'))
-
- set1.add(set2)
- assert_equal(true, set1.inspect.include?('#<Set: {...}>'))
- end
-
- # def test_pretty_print
- # end
-
- # def test_pretty_print_cycle
- # end
-end
-
-class TC_SortedSet < Test::Unit::TestCase
- def test_sortedset
- s = SortedSet[4,5,3,1,2]
-
- assert_equal([1,2,3,4,5], s.to_a)
-
- prev = nil
- s.each { |o| assert(prev < o) if prev; prev = o }
- assert_not_nil(prev)
-
- s.map! { |o| -2 * o }
-
- assert_equal([-10,-8,-6,-4,-2], s.to_a)
-
- prev = nil
- ret = s.each { |o| assert(prev < o) if prev; prev = o }
- assert_not_nil(prev)
- assert_same(s, ret)
-
- s = SortedSet.new([2,1,3]) { |o| o * -2 }
- assert_equal([-6,-4,-2], s.to_a)
-
- s = SortedSet.new(['one', 'two', 'three', 'four'])
- a = []
- ret = s.delete_if { |o| a << o; o.start_with?('t') }
- assert_same(s, ret)
- assert_equal(['four', 'one'], s.to_a)
- assert_equal(['four', 'one', 'three', 'two'], a)
-
- s = SortedSet.new(['one', 'two', 'three', 'four'])
- a = []
- ret = s.reject! { |o| a << o; o.start_with?('t') }
- assert_same(s, ret)
- assert_equal(['four', 'one'], s.to_a)
- assert_equal(['four', 'one', 'three', 'two'], a)
-
- s = SortedSet.new(['one', 'two', 'three', 'four'])
- a = []
- ret = s.reject! { |o| a << o; false }
- assert_same(nil, ret)
- assert_equal(['four', 'one', 'three', 'two'], s.to_a)
- assert_equal(['four', 'one', 'three', 'two'], a)
- end
-end
-
-class TC_Enumerable < Test::Unit::TestCase
- def test_to_set
- ary = [2,5,4,3,2,1,3]
-
- set = ary.to_set
- assert_instance_of(Set, set)
- assert_equal([1,2,3,4,5], set.sort)
-
- set = ary.to_set { |o| o * -2 }
- assert_instance_of(Set, set)
- assert_equal([-10,-8,-6,-4,-2], set.sort)
-
- set = ary.to_set(SortedSet)
- assert_instance_of(SortedSet, set)
- assert_equal([1,2,3,4,5], set.to_a)
-
- set = ary.to_set(SortedSet) { |o| o * -2 }
- assert_instance_of(SortedSet, set)
- assert_equal([-10,-8,-6,-4,-2], set.sort)
- end
-end
-
-# class TC_RestricedSet < Test::Unit::TestCase
-# def test_s_new
-# assert_raises(ArgumentError) { RestricedSet.new }
-#
-# s = RestricedSet.new([-1,2,3]) { |o| o > 0 }
-# assert_equal([2,3], s.sort)
-# end
-#
-# def test_restriction_proc
-# s = RestricedSet.new([-1,2,3]) { |o| o > 0 }
-#
-# f = s.restriction_proc
-# assert_instance_of(Proc, f)
-# assert(f[1])
-# assert(!f[0])
-# end
-#
-# def test_replace
-# s = RestricedSet.new(-3..3) { |o| o > 0 }
-# assert_equal([1,2,3], s.sort)
-#
-# s.replace([-2,0,3,4,5])
-# assert_equal([3,4,5], s.sort)
-# end
-#
-# def test_merge
-# s = RestricedSet.new { |o| o > 0 }
-# s.merge(-5..5)
-# assert_equal([1,2,3,4,5], s.sort)
-#
-# s.merge([10,-10,-8,8])
-# assert_equal([1,2,3,4,5,8,10], s.sort)
-# end
-# end
diff --git a/lib/set/subclass_compatible.rb b/lib/set/subclass_compatible.rb
new file mode 100644
index 0000000000..f43c34f6a2
--- /dev/null
+++ b/lib/set/subclass_compatible.rb
@@ -0,0 +1,347 @@
+# frozen_string_literal: true
+
+# :markup: markdown
+#
+# set/subclass_compatible.rb - Provides compatibility for set subclasses
+#
+# Copyright (c) 2002-2024 Akinori MUSHA <knu@iDaemons.org>
+#
+# Documentation by Akinori MUSHA and Gavin Sinclair.
+#
+# All rights reserved. You can redistribute and/or modify it under the same
+# terms as Ruby.
+
+
+class Set
+ # This module is automatically included in subclasses of Set, to
+ # make them backwards compatible with the pure-Ruby set implementation
+ # used before Ruby 4. Users who want to use Set subclasses without
+ # this compatibility layer should subclass from Set::CoreSet.
+ #
+ # Note that Set subclasses that access `@hash` are not compatible even
+ # with this support. Such subclasses must be updated to support Ruby 4.
+ module SubclassCompatible
+ module ClassMethods
+ def [](*ary)
+ new(ary)
+ end
+ end
+
+ # Creates a new set containing the elements of the given enumerable
+ # object.
+ #
+ # If a block is given, the elements of enum are preprocessed by the
+ # given block.
+ #
+ # Set.new([1, 2]) #=> #<Set: {1, 2}>
+ # Set.new([1, 2, 1]) #=> #<Set: {1, 2}>
+ # Set.new([1, 'c', :s]) #=> #<Set: {1, "c", :s}>
+ # Set.new(1..5) #=> #<Set: {1, 2, 3, 4, 5}>
+ # Set.new([1, 2, 3]) { |x| x * x } #=> #<Set: {1, 4, 9}>
+ def initialize(enum = nil, &block) # :yields: o
+ enum.nil? and return
+
+ if block
+ do_with_enum(enum) { |o| add(block[o]) }
+ else
+ merge(enum)
+ end
+ end
+
+ def do_with_enum(enum, &block) # :nodoc:
+ if enum.respond_to?(:each_entry)
+ enum.each_entry(&block) if block
+ elsif enum.respond_to?(:each)
+ enum.each(&block) if block
+ else
+ raise ArgumentError, "value must be enumerable"
+ end
+ end
+ private :do_with_enum
+
+ def replace(enum)
+ if enum.instance_of?(self.class)
+ super
+ else
+ do_with_enum(enum) # make sure enum is enumerable before calling clear
+ clear
+ merge(enum)
+ end
+ end
+
+ def to_set(&block)
+ return self if instance_of?(Set) && block.nil?
+ Set.new(self, &block)
+ end
+
+ def flatten_merge(set, seen = {}) # :nodoc:
+ set.each { |e|
+ if e.is_a?(Set)
+ case seen[e_id = e.object_id]
+ when true
+ raise ArgumentError, "tried to flatten recursive Set"
+ when false
+ next
+ end
+
+ seen[e_id] = true
+ flatten_merge(e, seen)
+ seen[e_id] = false
+ else
+ add(e)
+ end
+ }
+
+ self
+ end
+ protected :flatten_merge
+
+ def flatten
+ self.class.new.flatten_merge(self)
+ end
+
+ def flatten!
+ replace(flatten()) if any?(Set)
+ end
+
+ def superset?(set)
+ case
+ when set.instance_of?(self.class)
+ super
+ when set.is_a?(Set)
+ size >= set.size && set.all?(self)
+ else
+ raise ArgumentError, "value must be a set"
+ end
+ end
+ alias >= superset?
+
+ def proper_superset?(set)
+ case
+ when set.instance_of?(self.class)
+ super
+ when set.is_a?(Set)
+ size > set.size && set.all?(self)
+ else
+ raise ArgumentError, "value must be a set"
+ end
+ end
+ alias > proper_superset?
+
+ def subset?(set)
+ case
+ when set.instance_of?(self.class)
+ super
+ when set.is_a?(Set)
+ size <= set.size && all?(set)
+ else
+ raise ArgumentError, "value must be a set"
+ end
+ end
+ alias <= subset?
+
+ def proper_subset?(set)
+ case
+ when set.instance_of?(self.class)
+ super
+ when set.is_a?(Set)
+ size < set.size && all?(set)
+ else
+ raise ArgumentError, "value must be a set"
+ end
+ end
+ alias < proper_subset?
+
+ def <=>(set)
+ return unless set.is_a?(Set)
+
+ case size <=> set.size
+ when -1 then -1 if proper_subset?(set)
+ when +1 then +1 if proper_superset?(set)
+ else 0 if self.==(set)
+ end
+ end
+
+ def intersect?(set)
+ case set
+ when Set
+ if size < set.size
+ any?(set)
+ else
+ set.any?(self)
+ end
+ when Enumerable
+ set.any?(self)
+ else
+ raise ArgumentError, "value must be enumerable"
+ end
+ end
+
+ def disjoint?(set)
+ !intersect?(set)
+ end
+
+ def add?(o)
+ add(o) unless include?(o)
+ end
+
+ def delete?(o)
+ delete(o) if include?(o)
+ end
+
+ def delete_if(&block)
+ block_given? or return enum_for(__method__) { size }
+ select(&block).each { |o| delete(o) }
+ self
+ end
+
+ def keep_if(&block)
+ block_given? or return enum_for(__method__) { size }
+ reject(&block).each { |o| delete(o) }
+ self
+ end
+
+ def collect!
+ block_given? or return enum_for(__method__) { size }
+ set = self.class.new
+ each { |o| set << yield(o) }
+ replace(set)
+ end
+ alias map! collect!
+
+ def reject!(&block)
+ block_given? or return enum_for(__method__) { size }
+ n = size
+ delete_if(&block)
+ self if size != n
+ end
+
+ def select!(&block)
+ block_given? or return enum_for(__method__) { size }
+ n = size
+ keep_if(&block)
+ self if size != n
+ end
+
+ alias filter! select!
+
+ def merge(*enums, **nil)
+ enums.each do |enum|
+ if enum.instance_of?(self.class)
+ super(enum)
+ else
+ do_with_enum(enum) { |o| add(o) }
+ end
+ end
+
+ self
+ end
+
+ def subtract(enum)
+ do_with_enum(enum) { |o| delete(o) }
+ self
+ end
+
+ def |(enum)
+ dup.merge(enum)
+ end
+ alias + |
+ alias union |
+
+ def -(enum)
+ dup.subtract(enum)
+ end
+ alias difference -
+
+ def &(enum)
+ n = self.class.new
+ if enum.is_a?(Set)
+ if enum.size > size
+ each { |o| n.add(o) if enum.include?(o) }
+ else
+ enum.each { |o| n.add(o) if include?(o) }
+ end
+ else
+ do_with_enum(enum) { |o| n.add(o) if include?(o) }
+ end
+ n
+ end
+ alias intersection &
+
+ def ^(enum)
+ n = self.class.new(enum)
+ each { |o| n.add(o) unless n.delete?(o) }
+ n
+ end
+
+ def ==(other)
+ if self.equal?(other)
+ true
+ elsif other.instance_of?(self.class)
+ super
+ elsif other.is_a?(Set) && self.size == other.size
+ other.all? { |o| include?(o) }
+ else
+ false
+ end
+ end
+
+ def eql?(o) # :nodoc:
+ return false unless o.is_a?(Set)
+ super
+ end
+
+ def classify
+ block_given? or return enum_for(__method__) { size }
+
+ h = {}
+
+ each { |i|
+ (h[yield(i)] ||= self.class.new).add(i)
+ }
+
+ h
+ end
+
+ def join(separator=nil)
+ to_a.join(separator)
+ end
+
+ InspectKey = :__inspect_key__ # :nodoc:
+
+ # Returns a string containing a human-readable representation of the
+ # set ("#<Set: {element1, element2, ...}>").
+ def inspect
+ ids = (Thread.current[InspectKey] ||= [])
+
+ if ids.include?(object_id)
+ return sprintf('#<%s: {...}>', self.class.name)
+ end
+
+ ids << object_id
+ begin
+ return sprintf('#<%s: {%s}>', self.class, to_a.inspect[1..-2])
+ ensure
+ ids.pop
+ end
+ end
+
+ alias to_s inspect
+
+ def pretty_print(pp) # :nodoc:
+ pp.group(1, sprintf('#<%s:', self.class.name), '>') {
+ pp.breakable
+ pp.group(1, '{', '}') {
+ pp.seplist(self) { |o|
+ pp.pp o
+ }
+ }
+ }
+ end
+
+ def pretty_print_cycle(pp) # :nodoc:
+ pp.text sprintf('#<%s: {%s}>', self.class.name, empty? ? '' : '...')
+ end
+ end
+ private_constant :SubclassCompatible
+end
diff --git a/lib/shell.rb b/lib/shell.rb
deleted file mode 100644
index 6a64cb263f..0000000000
--- a/lib/shell.rb
+++ /dev/null
@@ -1,300 +0,0 @@
-#
-# shell.rb -
-# $Release Version: 0.7 $
-# $Revision: 1.9 $
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "e2mmap"
-
-require "thread" unless defined?(Mutex)
-
-require "forwardable"
-
-require "shell/error"
-require "shell/command-processor"
-require "shell/process-controller"
-
-class Shell
- @RCS_ID='-$Id: shell.rb,v 1.9 2002/03/04 12:01:10 keiju Exp keiju $-'
-
- include Error
- extend Exception2MessageMapper
-
-# @cascade = true
- # debug: true -> normal debug
- # debug: 1 -> eval definition debug
- # debug: 2 -> detail inspect debug
- @debug = false
- @verbose = true
-
- @debug_display_process_id = false
- @debug_display_thread_id = true
- @debug_output_mutex = Mutex.new
-
- class << Shell
- extend Forwardable
-
- attr_accessor :cascade, :debug, :verbose
-
-# alias cascade? cascade
- alias debug? debug
- alias verbose? verbose
- @verbose = true
-
- def debug=(val)
- @debug = val
- @verbose = val if val
- end
-
- def cd(path)
- new(path)
- end
-
- def default_system_path
- if @default_system_path
- @default_system_path
- else
- ENV["PATH"].split(":")
- end
- end
-
- def default_system_path=(path)
- @default_system_path = path
- end
-
- def default_record_separator
- if @default_record_separator
- @default_record_separator
- else
- $/
- end
- end
-
- def default_record_separator=(rs)
- @default_record_separator = rs
- end
-
- # os resource mutex
- mutex_methods = ["unlock", "lock", "locked?", "synchronize", "try_lock", "exclusive_unlock"]
- for m in mutex_methods
- def_delegator("@debug_output_mutex", m, "debug_output_"+m.to_s)
- end
-
- end
-
- def initialize(pwd = Dir.pwd, umask = nil)
- @cwd = File.expand_path(pwd)
- @dir_stack = []
- @umask = umask
-
- @system_path = Shell.default_system_path
- @record_separator = Shell.default_record_separator
-
- @command_processor = CommandProcessor.new(self)
- @process_controller = ProcessController.new(self)
-
- @verbose = Shell.verbose
- @debug = Shell.debug
- end
-
- attr_reader :system_path
-
- def system_path=(path)
- @system_path = path
- rehash
- end
-
- attr_accessor :umask, :record_separator
- attr_accessor :verbose, :debug
-
- def debug=(val)
- @debug = val
- @verbose = val if val
- end
-
- alias verbose? verbose
- alias debug? debug
-
- attr_reader :command_processor
- attr_reader :process_controller
-
- def expand_path(path)
- File.expand_path(path, @cwd)
- end
-
- # Most Shell commands are defined via CommandProcessor
-
- #
- # Dir related methods
- #
- # Shell#cwd/dir/getwd/pwd
- # Shell#chdir/cd
- # Shell#pushdir/pushd
- # Shell#popdir/popd
- # Shell#mkdir
- # Shell#rmdir
-
- attr_reader :cwd
- alias dir cwd
- alias getwd cwd
- alias pwd cwd
-
- attr_reader :dir_stack
- alias dirs dir_stack
-
- # If called as iterator, it restores the current directory when the
- # block ends.
- def chdir(path = nil, verbose = @verbose)
- check_point
-
- if iterator?
- notify("chdir(with block) #{path}") if verbose
- cwd_old = @cwd
- begin
- chdir(path, nil)
- yield
- ensure
- chdir(cwd_old, nil)
- end
- else
- notify("chdir #{path}") if verbose
- path = "~" unless path
- @cwd = expand_path(path)
- notify "current dir: #{@cwd}"
- rehash
- Void.new(self)
- end
- end
- alias cd chdir
-
- def pushdir(path = nil, verbose = @verbose)
- check_point
-
- if iterator?
- notify("pushdir(with block) #{path}") if verbose
- pushdir(path, nil)
- begin
- yield
- ensure
- popdir
- end
- elsif path
- notify("pushdir #{path}") if verbose
- @dir_stack.push @cwd
- chdir(path, nil)
- notify "dir stack: [#{@dir_stack.join ', '}]"
- self
- else
- notify("pushdir") if verbose
- if pop = @dir_stack.pop
- @dir_stack.push @cwd
- chdir pop
- notify "dir stack: [#{@dir_stack.join ', '}]"
- self
- else
- Shell.Fail DirStackEmpty
- end
- end
- Void.new(self)
- end
- alias pushd pushdir
-
- def popdir
- check_point
-
- notify("popdir")
- if pop = @dir_stack.pop
- chdir pop
- notify "dir stack: [#{@dir_stack.join ', '}]"
- self
- else
- Shell.Fail DirStackEmpty
- end
- Void.new(self)
- end
- alias popd popdir
-
- #
- # process management
- #
- def jobs
- @process_controller.jobs
- end
-
- def kill(sig, command)
- @process_controller.kill_job(sig, command)
- end
-
- #
- # command definitions
- #
- def Shell.def_system_command(command, path = command)
- CommandProcessor.def_system_command(command, path)
- end
-
- def Shell.undef_system_command(command)
- CommandProcessor.undef_system_command(command)
- end
-
- def Shell.alias_command(ali, command, *opts, &block)
- CommandProcessor.alias_command(ali, command, *opts, &block)
- end
-
- def Shell.unalias_command(ali)
- CommandProcessor.unalias_command(ali)
- end
-
- def Shell.install_system_commands(pre = "sys_")
- CommandProcessor.install_system_commands(pre)
- end
-
- #
- def inspect
- if debug.kind_of?(Integer) && debug > 2
- super
- else
- to_s
- end
- end
-
- def self.notify(*opts, &block)
- Shell::debug_output_synchronize do
- if opts[-1].kind_of?(String)
- yorn = verbose?
- else
- yorn = opts.pop
- end
- return unless yorn
-
- if @debug_display_thread_id
- if @debug_display_process_id
- prefix = "shell(##{Process.pid}:#{Thread.current.to_s.sub("Thread", "Th")}): "
- else
- prefix = "shell(#{Thread.current.to_s.sub("Thread", "Th")}): "
- end
- else
- prefix = "shell: "
- end
- _head = true
- STDERR.print opts.collect{|mes|
- mes = mes.dup
- yield mes if iterator?
- if _head
- _head = false
-# "shell" " + mes
- prefix + mes
- else
- " "* prefix.size + mes
- end
- }.join("\n")+"\n"
- end
- end
-
- CommandProcessor.initialize
- CommandProcessor.run_config
-end
diff --git a/lib/shell/builtin-command.rb b/lib/shell/builtin-command.rb
deleted file mode 100644
index b65056de0f..0000000000
--- a/lib/shell/builtin-command.rb
+++ /dev/null
@@ -1,160 +0,0 @@
-#
-# shell/builtin-command.rb -
-# $Release Version: 0.7 $
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "shell/filter"
-
-class Shell
- class BuiltInCommand<Filter
- def wait?
- false
- end
- def active?
- true
- end
- end
-
- class Void < BuiltInCommand
- def initialize(sh, *opts)
- super sh
- end
-
- def each(rs = nil)
- # do nothing
- end
- end
-
- class Echo < BuiltInCommand
- def initialize(sh, *strings)
- super sh
- @strings = strings
- end
-
- def each(rs = nil)
- rs = @shell.record_separator unless rs
- for str in @strings
- yield str + rs
- end
- end
- end
-
- class Cat < BuiltInCommand
- def initialize(sh, *filenames)
- super sh
- @cat_files = filenames
- end
-
- def each(rs = nil)
- if @cat_files.empty?
- super
- else
- for src in @cat_files
- @shell.foreach(src, rs){|l| yield l}
- end
- end
- end
- end
-
- class Glob < BuiltInCommand
- def initialize(sh, pattern)
- super sh
-
- @pattern = pattern
- end
-
- def each(rs = nil)
- if @pattern[0] == ?/
- @files = Dir[@pattern]
- else
- prefix = @shell.pwd+"/"
- @files = Dir[prefix+@pattern].collect{|p| p.sub(prefix, "")}
- end
- rs = @shell.record_separator unless rs
- for f in @files
- yield f+rs
- end
- end
- end
-
-# class Sort < Cat
-# def initialize(sh, *filenames)
-# super
-# end
-#
-# def each(rs = nil)
-# ary = []
-# super{|l| ary.push l}
-# for l in ary.sort!
-# yield l
-# end
-# end
-# end
-
- class AppendIO < BuiltInCommand
- def initialize(sh, io, filter)
- super sh
- @input = filter
- @io = io
- end
-
- def input=(filter)
- @input.input=filter
- for l in @input
- @io << l
- end
- end
-
- end
-
- class AppendFile < AppendIO
- def initialize(sh, to_filename, filter)
- @file_name = to_filename
- io = sh.open(to_filename, "a")
- super(sh, io, filter)
- end
-
- def input=(filter)
- begin
- super
- ensure
- @io.close
- end
- end
- end
-
- class Tee < BuiltInCommand
- def initialize(sh, filename)
- super sh
- @to_filename = filename
- end
-
- def each(rs = nil)
- to = @shell.open(@to_filename, "w")
- begin
- super{|l| to << l; yield l}
- ensure
- to.close
- end
- end
- end
-
- class Concat < BuiltInCommand
- def initialize(sh, *jobs)
- super(sh)
- @jobs = jobs
- end
-
- def each(rs = nil)
- while job = @jobs.shift
- job.each{|l| yield l}
- end
- end
- end
-end
diff --git a/lib/shell/command-processor.rb b/lib/shell/command-processor.rb
deleted file mode 100644
index 900b31a22d..0000000000
--- a/lib/shell/command-processor.rb
+++ /dev/null
@@ -1,593 +0,0 @@
-#
-# shell/command-controller.rb -
-# $Release Version: 0.7 $
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "e2mmap"
-require "thread"
-
-require "shell/error"
-require "shell/filter"
-require "shell/system-command"
-require "shell/builtin-command"
-
-class Shell
- class CommandProcessor
-# include Error
-
- #
- # initialize of Shell and related classes.
- #
- m = [:initialize, :expand_path]
- if Object.methods.first.kind_of?(String)
- NoDelegateMethods = m.collect{|x| x.id2name}
- else
- NoDelegateMethods = m
- end
-
- def self.initialize
-
- install_builtin_commands
-
- # define CommandProccessor#methods to Shell#methods and Filter#methods
- for m in CommandProcessor.instance_methods(false) - NoDelegateMethods
- add_delegate_command_to_shell(m)
- end
-
- def self.method_added(id)
- add_delegate_command_to_shell(id)
- end
- end
-
- #
- # include run file.
- #
- def self.run_config
- begin
- load File.expand_path("~/.rb_shell") if ENV.key?("HOME")
- rescue LoadError, Errno::ENOENT
- rescue
- print "load error: #{rc}\n"
- print $!.class, ": ", $!, "\n"
- for err in $@[0, $@.size - 2]
- print "\t", err, "\n"
- end
- end
- end
-
- def initialize(shell)
- @shell = shell
- @system_commands = {}
- end
-
- #
- # CommandProcessor#expand_path(path)
- # path: String
- # return: String
- # returns the absolute path for <path>
- #
- def expand_path(path)
- @shell.expand_path(path)
- end
-
- #
- # File related commands
- # Shell#foreach
- # Shell#open
- # Shell#unlink
- # Shell#test
- #
- # -
- #
- # CommandProcessor#foreach(path, rs)
- # path: String
- # rs: String - record separator
- # iterator
- # Same as:
- # File#foreach (when path is file)
- # Dir#foreach (when path is directory)
- # path is relative to pwd
- #
- def foreach(path = nil, *rs)
- path = "." unless path
- path = expand_path(path)
-
- if File.directory?(path)
- Dir.foreach(path){|fn| yield fn}
- else
- IO.foreach(path, *rs){|l| yield l}
- end
- end
-
- #
- # CommandProcessor#open(path, mode)
- # path: String
- # mode: String
- # return: File or Dir
- # Same as:
- # File#open (when path is file)
- # Dir#open (when path is directory)
- # mode has an effect only when path is a file
- #
- def open(path, mode = nil, perm = 0666, &b)
- path = expand_path(path)
- if File.directory?(path)
- Dir.open(path, &b)
- else
- if @shell.umask
- f = File.open(path, mode, perm)
- File.chmod(perm & ~@shell.umask, path)
- if block_given?
- f.each(&b)
- end
- f
- else
- f = File.open(path, mode, perm, &b)
- end
- end
- end
- # public :open
-
- #
- # CommandProcessor#unlink(path)
- # same as:
- # Dir#unlink (when path is directory)
- # File#unlink (when path is file)
- #
- def unlink(path)
- @shell.check_point
-
- path = expand_path(path)
- if File.directory?(path)
- Dir.unlink(path)
- else
- IO.unlink(path)
- end
- Void.new(@shell)
- end
-
- #
- # CommandProcessor#test(command, file1, file2)
- # CommandProcessor#[command, file1, file2]
- # command: char or String or Symbol
- # file1: String
- # file2: String(optional)
- # return: Boolean
- # same as:
- # test() (when command is char or length 1 string or symbol)
- # FileTest.command (others)
- # example:
- # sh[?e, "foo"]
- # sh[:e, "foo"]
- # sh["e", "foo"]
- # sh[:exists?, "foo"]
- # sh["exists?", "foo"]
- #
- alias top_level_test test
- def test(command, file1, file2=nil)
- file1 = expand_path(file1)
- file2 = expand_path(file2) if file2
- command = command.id2name if command.kind_of?(Symbol)
-
- case command
- when Integer
- if file2
- top_level_test(command, file1, file2)
- else
- top_level_test(command, file1)
- end
- when String
- if command.size == 1
- if file2
- top_level_test(command, file1, file2)
- else
- top_level_test(command, file1)
- end
- else
- if file2
- FileTest.send(command, file1, file2)
- else
- FileTest.send(command, file1)
- end
- end
- end
- end
- alias [] test
-
- #
- # Dir related methods
- #
- # Shell#mkdir
- # Shell#rmdir
- #
- #--
- #
- # CommandProcessor#mkdir(*path)
- # path: String
- # same as Dir.mkdir()
- #
- def mkdir(*path)
- @shell.check_point
- notify("mkdir #{path.join(' ')}")
-
- perm = nil
- if path.last.kind_of?(Integer)
- perm = path.pop
- end
- for dir in path
- d = expand_path(dir)
- if perm
- Dir.mkdir(d, perm)
- else
- Dir.mkdir(d)
- end
- File.chmod(d, 0666 & ~@shell.umask) if @shell.umask
- end
- Void.new(@shell)
- end
-
- #
- # CommandProcessor#rmdir(*path)
- # path: String
- # same as Dir.rmdir()
- #
- def rmdir(*path)
- @shell.check_point
- notify("rmdir #{path.join(' ')}")
-
- for dir in path
- Dir.rmdir(expand_path(dir))
- end
- Void.new(@shell)
- end
-
- #
- # CommandProcessor#system(command, *opts)
- # command: String
- # opts: String
- # return: SystemCommand
- # Same as system() function
- # example:
- # print sh.system("ls", "-l")
- # sh.system("ls", "-l") | sh.head > STDOUT
- #
- def system(command, *opts)
- if opts.empty?
- if command =~ /\*|\?|\{|\}|\[|\]|<|>|\(|\)|~|&|\||\\|\$|;|'|`|"|\n/
- return SystemCommand.new(@shell, find_system_command("sh"), "-c", command)
- else
- command, *opts = command.split(/\s+/)
- end
- end
- SystemCommand.new(@shell, find_system_command(command), *opts)
- end
-
- #
- # ProcessCommand#rehash
- # clear command hash table.
- #
- def rehash
- @system_commands = {}
- end
-
- #
- # ProcessCommand#transact
- #
- def check_point
- @shell.process_controller.wait_all_jobs_execution
- end
- alias finish_all_jobs check_point
-
- def transact(&block)
- begin
- @shell.instance_eval(&block)
- ensure
- check_point
- end
- end
-
- #
- # internal commands
- #
- def out(dev = STDOUT, &block)
- dev.print transact(&block)
- end
-
- def echo(*strings)
- Echo.new(@shell, *strings)
- end
-
- def cat(*filenames)
- Cat.new(@shell, *filenames)
- end
-
- # def sort(*filenames)
- # Sort.new(self, *filenames)
- # end
-
- def glob(pattern)
- Glob.new(@shell, pattern)
- end
-
- def append(to, filter)
- case to
- when String
- AppendFile.new(@shell, to, filter)
- when IO
- AppendIO.new(@shell, to, filter)
- else
- Shell.Fail Error::CantApplyMethod, "append", to.class
- end
- end
-
- def tee(file)
- Tee.new(@shell, file)
- end
-
- def concat(*jobs)
- Concat.new(@shell, *jobs)
- end
-
- # %pwd, %cwd -> @pwd
- def notify(*opts, &block)
- Shell.notify(*opts) {|mes|
- yield mes if iterator?
-
- mes.gsub!("%pwd", "#{@cwd}")
- mes.gsub!("%cwd", "#{@cwd}")
- }
- end
-
- #
- # private functions
- #
- def find_system_command(command)
- return command if /^\// =~ command
- case path = @system_commands[command]
- when String
- if exists?(path)
- return path
- else
- Shell.Fail Error::CommandNotFound, command
- end
- when false
- Shell.Fail Error::CommandNotFound, command
- end
-
- for p in @shell.system_path
- path = join(p, command)
- if FileTest.exist?(path)
- @system_commands[command] = path
- return path
- end
- end
- @system_commands[command] = false
- Shell.Fail Error::CommandNotFound, command
- end
-
- #
- # CommandProcessor.def_system_command(command, path)
- # command: String
- # path: String
- # define 'command()' method as method.
- #
- def self.def_system_command(command, path = command)
- begin
- eval((d = %Q[def #{command}(*opts)
- SystemCommand.new(@shell, '#{path}', *opts)
- end]), nil, __FILE__, __LINE__ - 1)
- rescue SyntaxError
- Shell.notify "warn: Can't define #{command} path: #{path}."
- end
- Shell.notify "Define #{command} path: #{path}.", Shell.debug?
- Shell.notify("Definition of #{command}: ", d,
- Shell.debug.kind_of?(Integer) && Shell.debug > 1)
- end
-
- def self.undef_system_command(command)
- command = command.id2name if command.kind_of?(Symbol)
- remove_method(command)
- Shell.module_eval{remove_method(command)}
- Filter.module_eval{remove_method(command)}
- self
- end
-
- # define command alias
- # ex)
- # def_alias_command("ls_c", "ls", "-C", "-F")
- # def_alias_command("ls_c", "ls"){|*opts| ["-C", "-F", *opts]}
- #
- @alias_map = {}
- def self.alias_map
- @alias_map
- end
- def self.alias_command(ali, command, *opts, &block)
- ali = ali.id2name if ali.kind_of?(Symbol)
- command = command.id2name if command.kind_of?(Symbol)
- begin
- if iterator?
- @alias_map[ali.intern] = proc
-
- eval((d = %Q[def #{ali}(*opts)
- @shell.__send__(:#{command},
- *(CommandProcessor.alias_map[:#{ali}].call *opts))
- end]), nil, __FILE__, __LINE__ - 1)
-
- else
- args = opts.collect{|opt| '"' + opt + '"'}.join(",")
- eval((d = %Q[def #{ali}(*opts)
- @shell.__send__(:#{command}, #{args}, *opts)
- end]), nil, __FILE__, __LINE__ - 1)
- end
- rescue SyntaxError
- Shell.notify "warn: Can't alias #{ali} command: #{command}."
- Shell.notify("Definition of #{ali}: ", d)
- raise
- end
- Shell.notify "Define #{ali} command: #{command}.", Shell.debug?
- Shell.notify("Definition of #{ali}: ", d,
- Shell.debug.kind_of?(Integer) && Shell.debug > 1)
- self
- end
-
- def self.unalias_command(ali)
- ali = ali.id2name if ali.kind_of?(Symbol)
- @alias_map.delete ali.intern
- undef_system_command(ali)
- end
-
- #
- # CommandProcessor.def_builtin_commands(delegation_class, command_specs)
- # delegation_class: Class or Module
- # command_specs: [[command_name, [argument,...]],...]
- # command_name: String
- # arguments: String
- # FILENAME?? -> expand_path(filename??)
- # *FILENAME?? -> filename??.collect{|f|expand_path(f)}.join(", ")
- # define command_name(argument,...) as
- # delegation_class.command_name(argument,...)
- #
- def self.def_builtin_commands(delegation_class, command_specs)
- for meth, args in command_specs
- arg_str = args.collect{|arg| arg.downcase}.join(", ")
- call_arg_str = args.collect{
- |arg|
- case arg
- when /^(FILENAME.*)$/
- format("expand_path(%s)", $1.downcase)
- when /^(\*FILENAME.*)$/
- # \*FILENAME* -> filenames.collect{|fn| expand_path(fn)}.join(", ")
- $1.downcase + '.collect{|fn| expand_path(fn)}'
- else
- arg
- end
- }.join(", ")
- d = %Q[def #{meth}(#{arg_str})
- #{delegation_class}.#{meth}(#{call_arg_str})
- end]
- Shell.notify "Define #{meth}(#{arg_str})", Shell.debug?
- Shell.notify("Definition of #{meth}: ", d,
- Shell.debug.kind_of?(Integer) && Shell.debug > 1)
- eval d
- end
- end
-
- #
- # CommandProcessor.install_system_commands(pre)
- # pre: String - command name prefix
- # defines every command which belongs in default_system_path via
- # CommandProcessor.command(). It doesn't define already defined
- # methods twice. By default, "pre_" is prefixes to each method
- # name. Characters that may not be used in a method name are
- # all converted to '_'. Definition errors are just ignored.
- #
- def self.install_system_commands(pre = "sys_")
- defined_meth = {}
- for m in Shell.methods
- defined_meth[m] = true
- end
- sh = Shell.new
- for path in Shell.default_system_path
- next unless sh.directory? path
- sh.cd path
- sh.foreach do
- |cn|
- if !defined_meth[pre + cn] && sh.file?(cn) && sh.executable?(cn)
- command = (pre + cn).gsub(/\W/, "_").sub(/^([0-9])/, '_\1')
- begin
- def_system_command(command, sh.expand_path(cn))
- rescue
- Shell.notify "warn: Can't define #{command} path: #{cn}"
- end
- defined_meth[command] = command
- end
- end
- end
- end
-
- #----------------------------------------------------------------------
- #
- # class initializing methods -
- #
- #----------------------------------------------------------------------
- def self.add_delegate_command_to_shell(id)
- id = id.intern if id.kind_of?(String)
- name = id.id2name
- if Shell.method_defined?(id)
- Shell.notify "warn: override definnition of Shell##{name}."
- Shell.notify "warn: alias Shell##{name} to Shell##{name}_org.\n"
- Shell.module_eval "alias #{name}_org #{name}"
- end
- Shell.notify "method added: Shell##{name}.", Shell.debug?
- Shell.module_eval(%Q[def #{name}(*args, &block)
- begin
- @command_processor.__send__(:#{name}, *args, &block)
- rescue Exception
- $@.delete_if{|s| /:in `__getobj__'$/ =~ s} #`
- $@.delete_if{|s| /^\\(eval\\):/ =~ s}
- raise
- end
- end], __FILE__, __LINE__)
-
- if Shell::Filter.method_defined?(id)
- Shell.notify "warn: override definnition of Shell::Filter##{name}."
- Shell.notify "warn: alias Shell##{name} to Shell::Filter##{name}_org."
- Filter.module_eval "alias #{name}_org #{name}"
- end
- Shell.notify "method added: Shell::Filter##{name}.", Shell.debug?
- Filter.module_eval(%Q[def #{name}(*args, &block)
- begin
- self | @shell.__send__(:#{name}, *args, &block)
- rescue Exception
- $@.delete_if{|s| /:in `__getobj__'$/ =~ s} #`
- $@.delete_if{|s| /^\\(eval\\):/ =~ s}
- raise
- end
- end], __FILE__, __LINE__)
- end
-
- #
- # define default builtin commands
- #
- def self.install_builtin_commands
- # method related File.
- # (exclude open/foreach/unlink)
- normal_delegation_file_methods = [
- ["atime", ["FILENAME"]],
- ["basename", ["fn", "*opts"]],
- ["chmod", ["mode", "*FILENAMES"]],
- ["chown", ["owner", "group", "*FILENAME"]],
- ["ctime", ["FILENAMES"]],
- ["delete", ["*FILENAMES"]],
- ["dirname", ["FILENAME"]],
- ["ftype", ["FILENAME"]],
- ["join", ["*items"]],
- ["link", ["FILENAME_O", "FILENAME_N"]],
- ["lstat", ["FILENAME"]],
- ["mtime", ["FILENAME"]],
- ["readlink", ["FILENAME"]],
- ["rename", ["FILENAME_FROM", "FILENAME_TO"]],
- # ["size", ["FILENAME"]],
- ["split", ["pathname"]],
- ["stat", ["FILENAME"]],
- ["symlink", ["FILENAME_O", "FILENAME_N"]],
- ["truncate", ["FILENAME", "length"]],
- ["utime", ["atime", "mtime", "*FILENAMES"]]]
-
- def_builtin_commands(File, normal_delegation_file_methods)
- alias_method :rm, :delete
-
- # method related FileTest
- def_builtin_commands(FileTest,
- FileTest.singleton_methods(false).collect{|m| [m, ["FILENAME"]]})
-
- end
-
- end
-end
diff --git a/lib/shell/error.rb b/lib/shell/error.rb
deleted file mode 100644
index 8bb96c22da..0000000000
--- a/lib/shell/error.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-#
-# shell/error.rb -
-# $Release Version: 0.7 $
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "e2mmap"
-
-class Shell
- module Error
- extend Exception2MessageMapper
- def_e2message TypeError, "wrong argument type %s (expected %s)"
-
- def_exception :DirStackEmpty, "Directory stack empty."
- def_exception :CantDefine, "Can't define method(%s, %s)."
- def_exception :CantApplyMethod, "This method(%s) does not apply to this type(%s)."
- def_exception :CommandNotFound, "Command not found(%s)."
- end
-end
-
diff --git a/lib/shell/filter.rb b/lib/shell/filter.rb
deleted file mode 100644
index 3bb683db22..0000000000
--- a/lib/shell/filter.rb
+++ /dev/null
@@ -1,109 +0,0 @@
-#
-# shell/filter.rb -
-# $Release Version: 0.7 $
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-class Shell
- #
- # Filter
- # A method to require
- # each()
- #
- class Filter
- include Enumerable
-
- def initialize(sh)
- @shell = sh # parent shell
- @input = nil # input filter
- end
-
- attr_reader :input
-
- def input=(filter)
- @input = filter
- end
-
- def each(rs = nil)
- rs = @shell.record_separator unless rs
- if @input
- @input.each(rs){|l| yield l}
- end
- end
-
- def < (src)
- case src
- when String
- cat = Cat.new(@shell, src)
- cat | self
- when IO
- self.input = src
- self
- else
- Shell.Fail Error::CantApplyMethod, "<", to.class
- end
- end
-
- def > (to)
- case to
- when String
- dst = @shell.open(to, "w")
- begin
- each(){|l| dst << l}
- ensure
- dst.close
- end
- when IO
- each(){|l| to << l}
- else
- Shell.Fail Error::CantApplyMethod, ">", to.class
- end
- self
- end
-
- def >> (to)
- begin
- Shell.cd(@shell.pwd).append(to, self)
- rescue CantApplyMethod
- Shell.Fail Error::CantApplyMethod, ">>", to.class
- end
- end
-
- def | (filter)
- filter.input = self
- if active?
- @shell.process_controller.start_job filter
- end
- filter
- end
-
- def + (filter)
- Join.new(@shell, self, filter)
- end
-
- def to_a
- ary = []
- each(){|l| ary.push l}
- ary
- end
-
- def to_s
- str = ""
- each(){|l| str.concat l}
- str
- end
-
- def inspect
- if @shell.debug.kind_of?(Integer) && @shell.debug > 2
- super
- else
- to_s
- end
- end
- end
-end
diff --git a/lib/shell/process-controller.rb b/lib/shell/process-controller.rb
deleted file mode 100644
index f2bf1d44c8..0000000000
--- a/lib/shell/process-controller.rb
+++ /dev/null
@@ -1,319 +0,0 @@
-#
-# shell/process-controller.rb -
-# $Release Version: 0.7 $
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-require "forwardable"
-
-require "thread"
-require "sync"
-
-class Shell
- class ProcessController
-
- @ProcessControllers = {}
- @ProcessControllersMonitor = Mutex.new
- @ProcessControllersCV = ConditionVariable.new
-
- @BlockOutputMonitor = Mutex.new
- @BlockOutputCV = ConditionVariable.new
-
- class<<self
- extend Forwardable
-
- def_delegator("@ProcessControllersMonitor",
- "synchronize", "process_controllers_exclusive")
-
- def active_process_controllers
- process_controllers_exclusive do
- @ProcessControllers.dup
- end
- end
-
- def activate(pc)
- process_controllers_exclusive do
- @ProcessControllers[pc] ||= 0
- @ProcessControllers[pc] += 1
- end
- end
-
- def inactivate(pc)
- process_controllers_exclusive do
- if @ProcessControllers[pc]
- if (@ProcessControllers[pc] -= 1) == 0
- @ProcessControllers.delete(pc)
- @ProcessControllersCV.signal
- end
- end
- end
- end
-
- def each_active_object
- process_controllers_exclusive do
- for ref in @ProcessControllers.keys
- yield ref
- end
- end
- end
-
- def block_output_synchronize(&b)
- @BlockOutputMonitor.synchronize(&b)
- end
-
- def wait_to_finish_all_process_controllers
- process_controllers_exclusive do
- while !@ProcessControllers.empty?
- Shell::notify("Process finishing, but active shell exists",
- "You can use Shell#transact or Shell#check_point for more safe execution.")
- if Shell.debug?
- for pc in @ProcessControllers.keys
- Shell::notify(" Not finished jobs in "+pc.shell.to_s)
- for com in pc.jobs
- com.notify(" Jobs: %id")
- end
- end
- end
- @ProcessControllersCV.wait(@ProcessControllersMonitor)
- end
- end
- end
- end
-
- # for shell-command complete finish at this process exit.
- USING_AT_EXIT_WHEN_PROCESS_EXIT = true
- at_exit do
- wait_to_finish_all_process_controllers unless $@
- end
-
- def initialize(shell)
- @shell = shell
- @waiting_jobs = []
- @active_jobs = []
- @jobs_sync = Sync.new
-
- @job_monitor = Mutex.new
- @job_condition = ConditionVariable.new
- end
-
- attr_reader :shell
-
- def jobs
- jobs = []
- @jobs_sync.synchronize(:SH) do
- jobs.concat @waiting_jobs
- jobs.concat @active_jobs
- end
- jobs
- end
-
- def active_jobs
- @active_jobs
- end
-
- def waiting_jobs
- @waiting_jobs
- end
-
- def jobs_exist?
- @jobs_sync.synchronize(:SH) do
- @active_jobs.empty? or @waiting_jobs.empty?
- end
- end
-
- def active_jobs_exist?
- @jobs_sync.synchronize(:SH) do
- @active_jobs.empty?
- end
- end
-
- def waiting_jobs_exist?
- @jobs_sync.synchronize(:SH) do
- @waiting_jobs.empty?
- end
- end
-
- # schedule a command
- def add_schedule(command)
- @jobs_sync.synchronize(:EX) do
- ProcessController.activate(self)
- if @active_jobs.empty?
- start_job command
- else
- @waiting_jobs.push(command)
- end
- end
- end
-
- # start a job
- def start_job(command = nil)
- @jobs_sync.synchronize(:EX) do
- if command
- return if command.active?
- @waiting_jobs.delete command
- else
- command = @waiting_jobs.shift
-# command.notify "job(%id) pre-start.", @shell.debug?
-
- return unless command
- end
- @active_jobs.push command
- command.start
-# command.notify "job(%id) post-start.", @shell.debug?
-
- # start all jobs that input from the job
- for job in @waiting_jobs.dup
- start_job(job) if job.input == command
- end
-# command.notify "job(%id) post2-start.", @shell.debug?
- end
- end
-
- def waiting_job?(job)
- @jobs_sync.synchronize(:SH) do
- @waiting_jobs.include?(job)
- end
- end
-
- def active_job?(job)
- @jobs_sync.synchronize(:SH) do
- @active_jobs.include?(job)
- end
- end
-
- # terminate a job
- def terminate_job(command)
- @jobs_sync.synchronize(:EX) do
- @active_jobs.delete command
- ProcessController.inactivate(self)
- if @active_jobs.empty?
- command.notify("start_jon in ierminate_jon(%id)", Shell::debug?)
- start_job
- end
- end
- end
-
- # kill a job
- def kill_job(sig, command)
- @jobs_sync.synchronize(:EX) do
- if @waiting_jobs.delete command
- ProcessController.inactivate(self)
- return
- elsif @active_jobs.include?(command)
- begin
- r = command.kill(sig)
- ProcessController.inactivate(self)
- rescue
- print "Shell: Warn: $!\n" if @shell.verbose?
- return nil
- end
- @active_jobs.delete command
- r
- end
- end
- end
-
- # wait for all jobs to terminate
- def wait_all_jobs_execution
- @job_monitor.synchronize do
- begin
- while !jobs.empty?
- @job_condition.wait(@job_monitor)
- for job in jobs
- job.notify("waiting job(%id)", Shell::debug?)
- end
- end
- ensure
- redo unless jobs.empty?
- end
- end
- end
-
- # simple fork
- def sfork(command, &block)
- pipe_me_in, pipe_peer_out = IO.pipe
- pipe_peer_in, pipe_me_out = IO.pipe
-
-
- pid = nil
- pid_mutex = Mutex.new
- pid_cv = ConditionVariable.new
-
- Thread.start do
- ProcessController.block_output_synchronize do
- STDOUT.flush
- ProcessController.each_active_object do |pc|
- for jobs in pc.active_jobs
- jobs.flush
- end
- end
-
- pid = fork {
- Thread.list.each do |th|
-# th.kill unless [Thread.main, Thread.current].include?(th)
- th.kill unless Thread.current == th
- end
-
- STDIN.reopen(pipe_peer_in)
- STDOUT.reopen(pipe_peer_out)
-
- ObjectSpace.each_object(IO) do |io|
- if ![STDIN, STDOUT, STDERR].include?(io)
- io.close unless io.closed?
- end
- end
-
- yield
- }
- end
- pid_cv.signal
-
- pipe_peer_in.close
- pipe_peer_out.close
- command.notify "job(%name:##{pid}) start", @shell.debug?
-
- begin
- _pid = nil
- command.notify("job(%id) start to waiting finish.", @shell.debug?)
- _pid = Process.waitpid(pid, nil)
- rescue Errno::ECHILD
- command.notify "warn: job(%id) was done already waitipd."
- _pid = true
- # rescue
- # STDERR.puts $!
- ensure
- command.notify("Job(%id): Wait to finish when Process finished.", @shell.debug?)
- # when the process ends, wait until the command termintes
- if USING_AT_EXIT_WHEN_PROCESS_EXIT or _pid
- else
- command.notify("notice: Process finishing...",
- "wait for Job[%id] to finish.",
- "You can use Shell#transact or Shell#check_point for more safe execution.")
- redo
- end
-
-# command.notify "job(%id) pre-pre-finish.", @shell.debug?
- @job_monitor.synchronize do
-# command.notify "job(%id) pre-finish.", @shell.debug?
- terminate_job(command)
-# command.notify "job(%id) pre-finish2.", @shell.debug?
- @job_condition.signal
- command.notify "job(%id) finish.", @shell.debug?
- end
- end
- end
-
- pid_mutex.synchronize do
- while !pid
- pid_cv.wait(pid_mutex)
- end
- end
-
- return pid, pipe_me_in, pipe_me_out
- end
- end
-end
diff --git a/lib/shell/system-command.rb b/lib/shell/system-command.rb
deleted file mode 100644
index da5d4cb898..0000000000
--- a/lib/shell/system-command.rb
+++ /dev/null
@@ -1,159 +0,0 @@
-#
-# shell/system-command.rb -
-# $Release Version: 0.7 $
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require "shell/filter"
-
-class Shell
- class SystemCommand < Filter
- def initialize(sh, command, *opts)
- if t = opts.find{|opt| !opt.kind_of?(String) && opt.class}
- Shell.Fail Error::TypeError, t.class, "String"
- end
- super(sh)
- @command = command
- @opts = opts
-
- @input_queue = Queue.new
- @pid = nil
-
- sh.process_controller.add_schedule(self)
- end
-
- attr_reader :command
- alias name command
-
- def wait?
- @shell.process_controller.waiting_job?(self)
- end
-
- def active?
- @shell.process_controller.active_job?(self)
- end
-
- def input=(inp)
- super
- if active?
- start_export
- end
- end
-
- def start
- notify([@command, *@opts].join(" "))
-
- @pid, @pipe_in, @pipe_out = @shell.process_controller.sfork(self) {
- Dir.chdir @shell.pwd
- $0 = @command
- exec(@command, *@opts)
- }
- if @input
- start_export
- end
- start_import
- end
-
- def flush
- @pipe_out.flush if @pipe_out and !@pipe_out.closed?
- end
-
- def terminate
- begin
- @pipe_in.close
- rescue IOError
- end
- begin
- @pipe_out.close
- rescue IOError
- end
- end
-
- def kill(sig)
- if @pid
- Process.kill(sig, @pid)
- end
- end
-
- def start_import
- notify "Job(%id) start imp-pipe.", @shell.debug?
- rs = @shell.record_separator unless rs
- _eop = true
- th = Thread.start {
- begin
- while l = @pipe_in.gets
- @input_queue.push l
- end
- _eop = false
- rescue Errno::EPIPE
- _eop = false
- ensure
- if !ProcessController::USING_AT_EXIT_WHEN_PROCESS_EXIT and _eop
- notify("warn: Process finishing...",
- "wait for Job[%id] to finish pipe importing.",
- "You can use Shell#transact or Shell#check_point for more safe execution.")
- redo
- end
- notify "job(%id}) close imp-pipe.", @shell.debug?
- @input_queue.push :EOF
- @pipe_in.close
- end
- }
- end
-
- def start_export
- notify "job(%id) start exp-pipe.", @shell.debug?
- _eop = true
- th = Thread.start{
- begin
- @input.each do |l|
- ProcessController::block_output_synchronize do
- @pipe_out.print l
- end
- end
- _eop = false
- rescue Errno::EPIPE, Errno::EIO
- _eop = false
- ensure
- if !ProcessController::USING_AT_EXIT_WHEN_PROCESS_EXIT and _eop
- notify("shell: warn: Process finishing...",
- "wait for Job(%id) to finish pipe exporting.",
- "You can use Shell#transact or Shell#check_point for more safe execution.")
- redo
- end
- notify "job(%id) close exp-pipe.", @shell.debug?
- @pipe_out.close
- end
- }
- end
-
- alias super_each each
- def each(rs = nil)
- while (l = @input_queue.pop) != :EOF
- yield l
- end
- end
-
- # ex)
- # if you wish to output:
- # "shell: job(#{@command}:#{@pid}) close pipe-out."
- # then
- # mes: "job(%id) close pipe-out."
- # yorn: Boolean(@shell.debug? or @shell.verbose?)
- def notify(*opts, &block)
- @shell.notify(*opts) do |mes|
- yield mes if iterator?
-
- mes.gsub!("%id", "#{@command}:##{@pid}")
- mes.gsub!("%name", "#{@command}")
- mes.gsub!("%pid", "#{@pid}")
- mes
- end
- end
- end
-end
diff --git a/lib/shell/version.rb b/lib/shell/version.rb
deleted file mode 100644
index dd50b06d55..0000000000
--- a/lib/shell/version.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-#
-# version.rb - shell version definition file
-# $Release Version: 0.7$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-class Shell
- @RELEASE_VERSION = "0.7"
- @LAST_UPDATE_DATE = "07/03/20"
-end
diff --git a/lib/shellwords.gemspec b/lib/shellwords.gemspec
new file mode 100644
index 0000000000..b601508f94
--- /dev/null
+++ b/lib/shellwords.gemspec
@@ -0,0 +1,29 @@
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1).join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Akinori MUSHA"]
+ spec.email = ["knu@idaemons.org"]
+
+ spec.summary = %q{Manipulates strings with word parsing rules of UNIX Bourne shell.}
+ spec.description = %q{Manipulates strings with word parsing rules of UNIX Bourne shell.}
+ spec.homepage = "https://github.com/ruby/shellwords"
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ srcdir, gemspec_file = File.split(__FILE__)
+ spec.files = Dir.chdir(srcdir) do
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:(?:test|spec|features)/|\.git|Rake)}) || f == gemspec_file}
+ end
+ spec.require_paths = ["lib"]
+end
diff --git a/lib/shellwords.rb b/lib/shellwords.rb
index f1300612bb..20a85ed9d2 100644
--- a/lib/shellwords.rb
+++ b/lib/shellwords.rb
@@ -1,44 +1,114 @@
-#
-# shellwords.rb: Manipulates strings a la UNIX Bourne shell
-#
-
+# frozen-string-literal: true
+##
+# == Manipulates strings like the UNIX Bourne shell
#
# This module manipulates strings according to the word parsing rules
# of the UNIX Bourne shell.
#
-# The shellwords() function was originally a port of shellwords.pl,
-# but modified to conform to POSIX / SUSv3 (IEEE Std 1003.1-2001).
+# The <tt>shellwords()</tt> function was originally a port of shellwords.pl, but
+# modified to conform to {the Shell & Utilities volume of the IEEE Std 1003.1-2008, 2016
+# Edition}[http://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html]
+#
+# === Usage
+#
+# You can use Shellwords to parse a string into a Bourne shell friendly Array.
+#
+# require 'shellwords'
+#
+# argv = Shellwords.split('three blind "mice"')
+# argv #=> ["three", "blind", "mice"]
+#
+# Once you've required Shellwords, you can use the #split alias
+# String#shellsplit.
+#
+# argv = "see how they run".shellsplit
+# argv #=> ["see", "how", "they", "run"]
+#
+# They treat quotes as special characters, so an unmatched quote will
+# cause an ArgumentError.
+#
+# argv = "they all ran after the farmer's wife".shellsplit
+# #=> ArgumentError: Unmatched quote: ...
#
-# Authors:
-# - Wakou Aoyama
-# - Akinori MUSHA <knu@iDaemons.org>
+# Shellwords also provides methods that do the opposite.
+# Shellwords.escape, or its alias, String#shellescape, escapes
+# shell metacharacters in a string for use in a command line.
#
-# Contact:
-# - Akinori MUSHA <knu@iDaemons.org> (current maintainer)
+# filename = "special's.txt"
#
+# system("cat -- #{filename.shellescape}")
+# # runs "cat -- special\\'s.txt"
+#
+# Note the '--'. Without it, cat(1) will treat the following argument
+# as a command line option if it starts with '-'. It is guaranteed
+# that Shellwords.escape converts a string to a form that a Bourne
+# shell will parse back to the original string, but it is the
+# programmer's responsibility to make sure that passing an arbitrary
+# argument to a command does no harm.
+#
+# Shellwords also comes with a core extension for Array, Array#shelljoin.
+#
+# dir = "Funny GIFs"
+# argv = %W[ls -lta -- #{dir}]
+# system(argv.shelljoin + " | less")
+# # runs "ls -lta -- Funny\\ GIFs | less"
+#
+# You can use this method to build a complete command line out of an
+# array of arguments.
+#
+# === Authors
+# * Wakou Aoyama
+# * Akinori MUSHA <knu@iDaemons.org>
+#
+# === Contact
+# * Akinori MUSHA <knu@iDaemons.org> (current maintainer)
+
module Shellwords
- #
+ # The version number string.
+ VERSION = "0.2.2"
+
# Splits a string into an array of tokens in the same way the UNIX
# Bourne shell does.
#
# argv = Shellwords.split('here are "two words"')
# argv #=> ["here", "are", "two words"]
#
- # +String#shellsplit+ is a shorthand for this function.
+ # +line+ must not contain NUL characters because of nature of
+ # +exec+ system call.
+ #
+ # Note, however, that this is not a command line parser. Shell
+ # metacharacters except for the single and double quotes and
+ # backslash are not treated as such.
+ #
+ # argv = Shellwords.split('ruby my_prog.rb | less')
+ # argv #=> ["ruby", "my_prog.rb", "|", "less"]
+ #
+ # String#shellsplit is a shortcut for this function.
#
# argv = 'here are "two words"'.shellsplit
# argv #=> ["here", "are", "two words"]
- #
def shellsplit(line)
words = []
- field = ''
- line.scan(/\G\s*(?>([^\s\\\'\"]+)|'([^\']*)'|"((?:[^\"\\]|\\.)*)"|(\\.?)|(\S))(\s|\z)?/m) do
+ field = String.new
+ line.scan(/\G\s*(?>([^\0\s\\\'\"]+)|'([^\0\']*)'|"((?:[^\0\"\\]|\\[^\0])*)"|(\\[^\0]?)|(\S))(\s|\z)?/m) do
|word, sq, dq, esc, garbage, sep|
- raise ArgumentError, "Unmatched double quote: #{line.inspect}" if garbage
- field << (word || sq || (dq || esc).gsub(/\\(?=.)/, ''))
+ if garbage
+ b = $~.begin(0)
+ line = $~[0]
+ line = "..." + line if b > 0
+ raise ArgumentError, "#{garbage == "\0" ? 'Nul character' : 'Unmatched quote'} at #{b}: #{line}"
+ end
+ # 2.2.3 Double-Quotes:
+ #
+ # The <backslash> shall retain its special meaning as an
+ # escape character only when followed by one of the following
+ # characters when considered special:
+ #
+ # $ ` " \ <newline>
+ field << (word || sq || (dq && dq.gsub(/\\([$`"\\\n])/, '\\1')) || esc.gsub(/\\(.)/, '\\1'))
if sep
words << field
- field = ''
+ field = String.new
end
end
words
@@ -52,35 +122,57 @@ module Shellwords
alias split shellsplit
end
- #
# Escapes a string so that it can be safely used in a Bourne shell
- # command line.
+ # command line. +str+ can be a non-string object that responds to
+ # +to_s+.
+ #
+ # +str+ must not contain NUL characters because of nature of +exec+
+ # system call.
#
# Note that a resulted string should be used unquoted and is not
# intended for use in double quotes nor in single quotes.
#
- # open("| grep #{Shellwords.escape(pattern)} file") { |pipe|
- # # ...
- # }
+ # argv = Shellwords.escape("It's better to give than to receive")
+ # argv #=> "It\\'s\\ better\\ to\\ give\\ than\\ to\\ receive"
+ #
+ # String#shellescape is a shorthand for this function.
#
- # +String#shellescape+ is a shorthand for this function.
+ # argv = "It's better to give than to receive".shellescape
+ # argv #=> "It\\'s\\ better\\ to\\ give\\ than\\ to\\ receive"
#
- # open("| grep #{pattern.shellescape} file") { |pipe|
- # # ...
+ # # Search files in lib for method definitions
+ # pattern = "^[ \t]*def "
+ # open("| grep -Ern -e #{pattern.shellescape} lib") { |grep|
+ # grep.each_line { |line|
+ # file, lineno, matched_line = line.split(':', 3)
+ # # ...
+ # }
# }
#
+ # It is the caller's responsibility to encode the string in the right
+ # encoding for the shell environment where this string is used.
+ #
+ # Multibyte characters are treated as multibyte characters, not as bytes.
+ #
+ # Returns an empty quoted String if +str+ has a length of zero.
def shellescape(str)
+ str = str.to_s
+
# An empty argument will be skipped, so return empty quotes.
- return "''" if str.empty?
+ return "''".dup if str.empty?
+
+ # Shellwords cannot contain NUL characters.
+ raise ArgumentError, "NUL character" if str.index("\0")
str = str.dup
- # Process as a single byte sequence because not all shell
- # implementations are multibyte aware.
- str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
+ # Treat multibyte characters as is. It is the caller's responsibility
+ # to encode the string in the right encoding for the shell
+ # environment.
+ str.gsub!(/[^A-Za-z0-9_\-.,:+\/@\n]/, "\\\\\\&")
# A LF cannot be escaped with a backslash because a backslash + LF
- # combo is regarded as line continuation and simply ignored.
+ # combo is regarded as a line continuation and simply ignored.
str.gsub!(/\n/, "'\n'")
return str
@@ -92,19 +184,26 @@ module Shellwords
alias escape shellescape
end
+ # Builds a command line string from an argument list, +array+.
#
- # Builds a command line string from an argument list +array+ joining
- # all elements escaped for Bourne shell and separated by a space.
+ # All elements are joined into a single string with fields separated by a
+ # space, where each element is escaped for the Bourne shell and stringified
+ # using +to_s+.
+ # See also Shellwords.shellescape.
#
- # open('|' + Shellwords.join(['grep', pattern, *files])) { |pipe|
- # # ...
- # }
+ # ary = ["There's", "a", "time", "and", "place", "for", "everything"]
+ # argv = Shellwords.join(ary)
+ # argv #=> "There\\'s a time and place for everything"
#
- # +Array#shelljoin+ is a shorthand for this function.
+ # Array#shelljoin is a shortcut for this function.
#
- # open('|' + ['grep', pattern, *files].shelljoin) { |pipe|
- # # ...
- # }
+ # ary = ["Don't", "rock", "the", "boat"]
+ # argv = ary.shelljoin
+ # argv #=> "Don\\'t rock the boat"
+ #
+ # You can also mix non-string objects in the elements as allowed in Array#join.
+ #
+ # output = `#{['ps', '-p', $$].shelljoin}`
#
def shelljoin(array)
array.map { |arg| shellescape(arg) }.join(' ')
@@ -118,38 +217,37 @@ module Shellwords
end
class String
- #
# call-seq:
# str.shellsplit => array
#
# Splits +str+ into an array of tokens in the same way the UNIX
- # Bourne shell does. See +Shellwords::shellsplit+ for details.
+ # Bourne shell does.
#
+ # See Shellwords.shellsplit for details.
def shellsplit
Shellwords.split(self)
end
- #
# call-seq:
# str.shellescape => string
#
# Escapes +str+ so that it can be safely used in a Bourne shell
- # command line. See +Shellwords::shellescape+ for details.
+ # command line.
#
+ # See Shellwords.shellescape for details.
def shellescape
Shellwords.escape(self)
end
end
class Array
- #
# call-seq:
# array.shelljoin => string
#
# Builds a command line string from an argument list +array+ joining
- # all elements escaped for Bourne shell and separated by a space.
- # See +Shellwords::shelljoin+ for details.
+ # all elements escaped for the Bourne shell and separated by a space.
#
+ # See Shellwords.shelljoin for details.
def shelljoin
Shellwords.join(self)
end
diff --git a/lib/singleton.gemspec b/lib/singleton.gemspec
new file mode 100644
index 0000000000..7646914905
--- /dev/null
+++ b/lib/singleton.gemspec
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1, ".").join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Yukihiro Matsumoto"]
+ spec.email = ["matz@ruby-lang.org"]
+
+ spec.summary = %q{The Singleton module implements the Singleton pattern.}
+ spec.description = spec.summary
+ spec.homepage = "https://github.com/ruby/singleton"
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
+ `git ls-files -z 2>#{IO::NULL}`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
+ end
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+end
diff --git a/lib/singleton.rb b/lib/singleton.rb
index 3c81b2d3cb..74aec8903c 100644
--- a/lib/singleton.rb
+++ b/lib/singleton.rb
@@ -1,123 +1,175 @@
+# frozen_string_literal: true
+
# The Singleton module implements the Singleton pattern.
#
-# Usage:
+# == Usage
+#
+# To use Singleton, include the module in your class.
+#
# class Klass
# include Singleton
# # ...
# end
#
-# * this ensures that only one instance of Klass lets call it
-# ``the instance'' can be created.
+# This ensures that only one instance of Klass can be created.
#
-# a,b = Klass.instance, Klass.instance
-# a == b # => true
-# Klass.new # NoMethodError - new is private ...
+# a,b = Klass.instance, Klass.instance
#
-# * ``The instance'' is created at instantiation time, in other
-# words the first call of Klass.instance(), thus
+# a == b
+# # => true
+#
+# Klass.new
+# # => NoMethodError - new is private ...
+#
+# The instance is created at upon the first call of Klass.instance().
#
# class OtherKlass
# include Singleton
# # ...
# end
-# ObjectSpace.each_object(OtherKlass){} # => 0.
#
-# * This behavior is preserved under inheritance and cloning.
+# ObjectSpace.each_object(OtherKlass){}
+# # => 0
+#
+# OtherKlass.instance
+# ObjectSpace.each_object(OtherKlass){}
+# # => 1
+#
+#
+# This behavior is preserved under inheritance and cloning.
+#
+# == Implementation
+#
+# This above is achieved by:
+#
+# * Making Klass.new and Klass.allocate private.
+#
+# * Overriding Klass.inherited(sub_klass) and Klass.clone() to ensure that the
+# Singleton properties are kept when inherited and cloned.
#
+# * Providing the Klass.instance() method that returns the same object each
+# time it is called.
#
+# * Overriding Klass._load(str) to call Klass.instance().
#
-# This is achieved by marking
-# * Klass.new and Klass.allocate - as private
+# * Overriding Klass#clone and Klass#dup to raise TypeErrors to prevent
+# cloning or duping.
#
-# Providing (or modifying) the class methods
-# * Klass.inherited(sub_klass) and Klass.clone() -
-# to ensure that the Singleton pattern is properly
-# inherited and cloned.
+# == Singleton and Marshal
#
-# * Klass.instance() - returning ``the instance''. After a
-# successful self modifying (normally the first) call the
-# method body is a simple:
+# By default Singleton's #_dump(depth) returns the empty string. Marshalling by
+# default will strip state information, e.g. instance variables from the instance.
+# Classes using Singleton can provide custom _load(str) and _dump(depth) methods
+# to retain some of the previous state of the instance.
#
-# def Klass.instance()
-# return @singleton__instance__
-# end
+# require 'singleton'
#
-# * Klass._load(str) - calling Klass.instance()
+# class Example
+# include Singleton
+# attr_accessor :keep, :strip
+# def _dump(depth)
+# # this strips the @strip information from the instance
+# Marshal.dump(@keep, depth)
+# end
+#
+# def self._load(str)
+# instance.keep = Marshal.load(str)
+# instance
+# end
+# end
#
-# * Klass._instantiate?() - returning ``the instance'' or
-# nil. This hook method puts a second (or nth) thread calling
-# Klass.instance() on a waiting loop. The return value
-# signifies the successful completion or premature termination
-# of the first, or more generally, current "instantiation thread".
+# a = Example.instance
+# a.keep = "keep this"
+# a.strip = "get rid of this"
#
+# stored_state = Marshal.dump(a)
#
-# The instance method of Singleton are
-# * clone and dup - raising TypeErrors to prevent cloning or duping
+# a.keep = nil
+# a.strip = nil
+# b = Marshal.load(stored_state)
+# p a == b # => true
+# p a.keep # => "keep this"
+# p a.strip # => nil
#
-# * _dump(depth) - returning the empty string. Marshalling strips
-# by default all state information, e.g. instance variables and
-# taint state, from ``the instance''. Providing custom _load(str)
-# and _dump(depth) hooks allows the (partially) resurrections of
-# a previous state of ``the instance''.
+module Singleton
+ # The version string
+ VERSION = "0.3.0"
-require 'thread'
+ module SingletonInstanceMethods # :nodoc:
+ # Raises a TypeError to prevent cloning.
+ def clone
+ raise TypeError, "can't clone instance of singleton #{self.class}"
+ end
-module Singleton
- # disable build-in copying methods
- def clone
- raise TypeError, "can't clone instance of singleton #{self.class}"
- end
- def dup
- raise TypeError, "can't dup instance of singleton #{self.class}"
- end
+ # Raises a TypeError to prevent duping.
+ def dup
+ raise TypeError, "can't dup instance of singleton #{self.class}"
+ end
- # default marshalling strategy
- def _dump(depth = -1)
- ''
+ # By default, do not retain any state when marshalling.
+ def _dump(depth = -1)
+ ''
+ end
end
+ include SingletonInstanceMethods
- module SingletonClassMethods
- # properly clone the Singleton pattern - did you know
- # that duping doesn't copy class methods?
- def clone
+ module SingletonClassMethods # :nodoc:
+
+ def clone # :nodoc:
Singleton.__init__(super)
end
+ # By default calls instance(). Override to retain singleton state.
def _load(str)
instance
end
+ def instance # :nodoc:
+ @singleton__instance__ || @singleton__mutex__.synchronize { @singleton__instance__ ||= new }
+ end
+
private
- # ensure that the Singleton pattern is properly inherited
def inherited(sub_klass)
super
Singleton.__init__(sub_klass)
end
+
+ def set_instance(val)
+ @singleton__instance__ = val
+ end
+
+ def set_mutex(val)
+ @singleton__mutex__ = val
+ end
+ end
+
+ def self.module_with_class_methods # :nodoc:
+ SingletonClassMethods
end
- class << Singleton
- def __init__(klass)
+ module SingletonClassProperties # :nodoc:
+
+ def self.included(c)
+ # extending an object with Singleton is a bad idea
+ c.undef_method :extend_object
+ end
+
+ def self.extended(c)
+ # extending an object with Singleton is a bad idea
+ c.singleton_class.send(:undef_method, :extend_object)
+ end
+
+ def __init__(klass) # :nodoc:
klass.instance_eval {
- @singleton__instance__ = nil
- @singleton__mutex__ = Mutex.new
+ set_instance(nil)
+ set_mutex(Thread::Mutex.new)
}
- def klass.instance
- return @singleton__instance__ if @singleton__instance__
- @singleton__mutex__.synchronize {
- return @singleton__instance__ if @singleton__instance__
- @singleton__instance__ = new()
- }
- @singleton__instance__
- end
klass
end
private
- # extending an object with Singleton is a bad idea
- undef_method :extend_object
-
def append_features(mod)
# help out people counting on transitive mixins
unless mod.instance_of?(Class)
@@ -128,186 +180,61 @@ module Singleton
def included(klass)
super
- klass.private_class_method :new, :allocate
- klass.extend SingletonClassMethods
+ klass.private_class_method :new, :allocate
+ klass.extend module_with_class_methods
Singleton.__init__(klass)
end
end
+ extend SingletonClassProperties
-end
-
-
-if __FILE__ == $0
+ ##
+ # :singleton-method: _load
+ # By default calls instance(). Override to retain singleton state.
-def num_of_instances(klass)
- "#{ObjectSpace.each_object(klass){}} #{klass} instance(s)"
+ ##
+ # :singleton-method: instance
+ # Returns the singleton instance.
end
-# The basic and most important example.
-
-class SomeSingletonClass
- include Singleton
-end
-puts "There are #{num_of_instances(SomeSingletonClass)}"
-
-a = SomeSingletonClass.instance
-b = SomeSingletonClass.instance # a and b are same object
-puts "basic test is #{a == b}"
-
-begin
- SomeSingletonClass.new
-rescue NoMethodError => mes
- puts mes
-end
-
-
+if defined?(Ractor)
+ module RactorLocalSingleton # :nodoc:
+ include Singleton::SingletonInstanceMethods
+
+ module RactorLocalSingletonClassMethods # :nodoc:
+ include Singleton::SingletonClassMethods
+ def instance
+ set_mutex(Thread::Mutex.new) if Ractor.current[mutex_key].nil?
+ return Ractor.current[instance_key] if Ractor.current[instance_key]
+ Ractor.current[mutex_key].synchronize {
+ return Ractor.current[instance_key] if Ractor.current[instance_key]
+ set_instance(new())
+ }
+ Ractor.current[instance_key]
+ end
-puts "\nThreaded example with exception and customized #_instantiate?() hook"; p
-Thread.abort_on_exception = false
+ private
-class Ups < SomeSingletonClass
- def initialize
- self.class.__sleep
- puts "initialize called by thread ##{Thread.current[:i]}"
- end
-end
+ def instance_key
+ :"__RactorLocalSingleton_instance_with_class_id_#{object_id}__"
+ end
-class << Ups
- def _instantiate?
- @enter.push Thread.current[:i]
- while false.equal?(@singleton__instance__)
- @singleton__mutex__.unlock
- sleep 0.08
- @singleton__mutex__.lock
- end
- @leave.push Thread.current[:i]
- @singleton__instance__
- end
+ def mutex_key
+ :"__RactorLocalSingleton_mutex_with_class_id_#{object_id}__"
+ end
- def __sleep
- sleep(rand(0.08))
- end
+ def set_instance(val)
+ Ractor.current[instance_key] = val
+ end
- def new
- begin
- __sleep
- raise "boom - thread ##{Thread.current[:i]} failed to create instance"
- ensure
- # simple flip-flop
- class << self
- remove_method :new
+ def set_mutex(val)
+ Ractor.current[mutex_key] = val
end
end
- end
-
- def instantiate_all
- @enter = []
- @leave = []
- 1.upto(9) {|i|
- Thread.new {
- begin
- Thread.current[:i] = i
- __sleep
- instance
- rescue RuntimeError => mes
- puts mes
- end
- }
- }
- puts "Before there were #{num_of_instances(self)}"
- sleep 3
- puts "Now there is #{num_of_instances(self)}"
- puts "#{@enter.join '; '} was the order of threads entering the waiting loop"
- puts "#{@leave.join '; '} was the order of threads leaving the waiting loop"
- end
-end
-
-
-Ups.instantiate_all
-# results in message like
-# Before there were 0 Ups instance(s)
-# boom - thread #6 failed to create instance
-# initialize called by thread #3
-# Now there is 1 Ups instance(s)
-# 3; 2; 1; 8; 4; 7; 5 was the order of threads entering the waiting loop
-# 3; 2; 1; 7; 4; 8; 5 was the order of threads leaving the waiting loop
-
-puts "\nLets see if class level cloning really works"
-Yup = Ups.clone
-def Yup.new
- begin
- __sleep
- raise "boom - thread ##{Thread.current[:i]} failed to create instance"
- ensure
- # simple flip-flop
- class << self
- remove_method :new
+ def self.module_with_class_methods
+ RactorLocalSingletonClassMethods
end
- end
-end
-Yup.instantiate_all
-
-puts "\n\n","Customized marshalling"
-class A
- include Singleton
- attr_accessor :persist, :die
- def _dump(depth)
- # this strips the @die information from the instance
- Marshal.dump(@persist,depth)
+ extend Singleton::SingletonClassProperties
end
end
-
-def A._load(str)
- instance.persist = Marshal.load(str)
- instance
-end
-
-a = A.instance
-a.persist = ["persist"]
-a.die = "die"
-a.taint
-
-stored_state = Marshal.dump(a)
-# change state
-a.persist = nil
-a.die = nil
-b = Marshal.load(stored_state)
-p a == b # => true
-p a.persist # => ["persist"]
-p a.die # => nil
-
-
-puts "\n\nSingleton with overridden default #inherited() hook"
-class Up
-end
-def Up.inherited(sub_klass)
- puts "#{sub_klass} subclasses #{self}"
-end
-
-
-class Middle < Up
- include Singleton
-end
-
-class Down < Middle; end
-
-puts "and basic \"Down test\" is #{Down.instance == Down.instance}\n
-Various exceptions"
-
-begin
- module AModule
- include Singleton
- end
-rescue TypeError => mes
- puts mes #=> Inclusion of the OO-Singleton module in module AModule
-end
-
-begin
- 'aString'.extend Singleton
-rescue NoMethodError => mes
- puts mes #=> undefined method `extend_object' for Singleton:Module
-end
-
-end
diff --git a/lib/sync.rb b/lib/sync.rb
deleted file mode 100644
index f4dea76d1f..0000000000
--- a/lib/sync.rb
+++ /dev/null
@@ -1,307 +0,0 @@
-#
-# sync.rb - 2 phase lock with counter
-# $Release Version: 1.0$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ishitsuka.com)
-#
-# --
-# Sync_m, Synchronizer_m
-# Usage:
-# obj.extend(Sync_m)
-# or
-# class Foo
-# include Sync_m
-# :
-# end
-#
-# Sync_m#sync_mode
-# Sync_m#sync_locked?, locked?
-# Sync_m#sync_shared?, shared?
-# Sync_m#sync_exclusive?, sync_exclusive?
-# Sync_m#sync_try_lock, try_lock
-# Sync_m#sync_lock, lock
-# Sync_m#sync_unlock, unlock
-#
-# Sync, Synchronizer:
-# Usage:
-# sync = Sync.new
-#
-# Sync#mode
-# Sync#locked?
-# Sync#shared?
-# Sync#exclusive?
-# Sync#try_lock(mode) -- mode = :EX, :SH, :UN
-# Sync#lock(mode) -- mode = :EX, :SH, :UN
-# Sync#unlock
-# Sync#synchronize(mode) {...}
-#
-#
-
-unless defined? Thread
- raise "Thread not available for this ruby interpreter"
-end
-
-module Sync_m
- RCS_ID='-$Header$-'
-
- # lock mode
- UN = :UN
- SH = :SH
- EX = :EX
-
- # exceptions
- class Err < StandardError
- def Err.Fail(*opt)
- fail self, sprintf(self::Message, *opt)
- end
-
- class UnknownLocker < Err
- Message = "Thread(%s) not locked."
- def UnknownLocker.Fail(th)
- super(th.inspect)
- end
- end
-
- class LockModeFailer < Err
- Message = "Unknown lock mode(%s)"
- def LockModeFailer.Fail(mode)
- if mode.id2name
- mode = id2name
- end
- super(mode)
- end
- end
- end
-
- def Sync_m.define_aliases(cl)
- cl.module_eval %q{
- alias locked? sync_locked?
- alias shared? sync_shared?
- alias exclusive? sync_exclusive?
- alias lock sync_lock
- alias unlock sync_unlock
- alias try_lock sync_try_lock
- alias synchronize sync_synchronize
- }
- end
-
- def Sync_m.append_features(cl)
- super
- # do nothing for Modules
- # make aliases for Classes.
- define_aliases(cl) unless cl.instance_of?(Module)
- self
- end
-
- def Sync_m.extend_object(obj)
- super
- obj.sync_extend
- end
-
- def sync_extend
- unless (defined? locked? and
- defined? shared? and
- defined? exclusive? and
- defined? lock and
- defined? unlock and
- defined? try_lock and
- defined? synchronize)
- Sync_m.define_aliases(class<<self;self;end)
- end
- sync_initialize
- end
-
- # accessing
- def sync_locked?
- sync_mode != UN
- end
-
- def sync_shared?
- sync_mode == SH
- end
-
- def sync_exclusive?
- sync_mode == EX
- end
-
- # locking methods.
- def sync_try_lock(mode = EX)
- return unlock if mode == UN
- @sync_mutex.synchronize do
- ret = sync_try_lock_sub(mode)
- end
- ret
- end
-
- def sync_lock(m = EX)
- return unlock if m == UN
-
- while true
- @sync_mutex.synchronize do
- if sync_try_lock_sub(m)
- return self
- else
- if sync_sh_locker[Thread.current]
- sync_upgrade_waiting.push [Thread.current, sync_sh_locker[Thread.current]]
- sync_sh_locker.delete(Thread.current)
- else
- sync_waiting.push Thread.current
- end
- @sync_mutex.sleep
- end
- end
- end
- self
- end
-
- def sync_unlock(m = EX)
- wakeup_threads = []
- @sync_mutex.synchronize do
- if sync_mode == UN
- Err::UnknownLocker.Fail(Thread.current)
- end
-
- m = sync_mode if m == EX and sync_mode == SH
-
- runnable = false
- case m
- when UN
- Err::UnknownLocker.Fail(Thread.current)
-
- when EX
- if sync_ex_locker == Thread.current
- if (self.sync_ex_count = sync_ex_count - 1) == 0
- self.sync_ex_locker = nil
- if sync_sh_locker.include?(Thread.current)
- self.sync_mode = SH
- else
- self.sync_mode = UN
- end
- runnable = true
- end
- else
- Err::UnknownLocker.Fail(Thread.current)
- end
-
- when SH
- if (count = sync_sh_locker[Thread.current]).nil?
- Err::UnknownLocker.Fail(Thread.current)
- else
- if (sync_sh_locker[Thread.current] = count - 1) == 0
- sync_sh_locker.delete(Thread.current)
- if sync_sh_locker.empty? and sync_ex_count == 0
- self.sync_mode = UN
- runnable = true
- end
- end
- end
- end
-
- if runnable
- if sync_upgrade_waiting.size > 0
- th, count = sync_upgrade_waiting.shift
- sync_sh_locker[th] = count
- th.wakeup
- wakeup_threads.push th
- else
- wait = sync_waiting
- self.sync_waiting = []
- for th in wait
- th.wakeup
- wakeup_threads.push th
- end
- end
- end
- end
- for th in wakeup_threads
- th.run
- end
- self
- end
-
- def sync_synchronize(mode = EX)
- sync_lock(mode)
- begin
- yield
- ensure
- sync_unlock
- end
- end
-
- attr_accessor :sync_mode
-
- attr_accessor :sync_waiting
- attr_accessor :sync_upgrade_waiting
- attr_accessor :sync_sh_locker
- attr_accessor :sync_ex_locker
- attr_accessor :sync_ex_count
-
- def sync_inspect
- sync_iv = instance_variables.select{|iv| /^@sync_/ =~ iv.id2name}.collect{|iv| iv.id2name + '=' + instance_eval(iv.id2name).inspect}.join(",")
- print "<#{self.class}.extend Sync_m: #{inspect}, <Sync_m: #{sync_iv}>"
- end
-
- private
-
- def sync_initialize
- @sync_mode = UN
- @sync_waiting = []
- @sync_upgrade_waiting = []
- @sync_sh_locker = Hash.new
- @sync_ex_locker = nil
- @sync_ex_count = 0
-
- @sync_mutex = Mutex.new
- end
-
- def initialize(*args)
- super
- sync_initialize
- end
-
- def sync_try_lock_sub(m)
- case m
- when SH
- case sync_mode
- when UN
- self.sync_mode = m
- sync_sh_locker[Thread.current] = 1
- ret = true
- when SH
- count = 0 unless count = sync_sh_locker[Thread.current]
- sync_sh_locker[Thread.current] = count + 1
- ret = true
- when EX
- # in EX mode, lock will upgrade to EX lock
- if sync_ex_locker == Thread.current
- self.sync_ex_count = sync_ex_count + 1
- ret = true
- else
- ret = false
- end
- end
- when EX
- if sync_mode == UN or
- sync_mode == SH && sync_sh_locker.size == 1 && sync_sh_locker.include?(Thread.current)
- self.sync_mode = m
- self.sync_ex_locker = Thread.current
- self.sync_ex_count = 1
- ret = true
- elsif sync_mode == EX && sync_ex_locker == Thread.current
- self.sync_ex_count = sync_ex_count + 1
- ret = true
- else
- ret = false
- end
- else
- Err::LockModeFailer.Fail mode
- end
- return ret
- end
-end
-Synchronizer_m = Sync_m
-
-class Sync
- include Sync_m
-end
-Synchronizer = Sync
diff --git a/lib/syntax_suggest.rb b/lib/syntax_suggest.rb
new file mode 100644
index 0000000000..1a45dfa676
--- /dev/null
+++ b/lib/syntax_suggest.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+require_relative "syntax_suggest/core_ext"
diff --git a/lib/syntax_suggest/api.rb b/lib/syntax_suggest/api.rb
new file mode 100644
index 0000000000..5054efa888
--- /dev/null
+++ b/lib/syntax_suggest/api.rb
@@ -0,0 +1,200 @@
+# frozen_string_literal: true
+
+require_relative "version"
+
+require "tmpdir"
+require "stringio"
+require "pathname"
+require "timeout"
+
+# Prism is the new parser, replacing Ripper
+require "prism"
+
+module SyntaxSuggest
+ # Used to indicate a default value that cannot
+ # be confused with another input.
+ DEFAULT_VALUE = Object.new.freeze
+
+ class Error < StandardError; end
+ TIMEOUT_DEFAULT = ENV.fetch("SYNTAX_SUGGEST_TIMEOUT", 1).to_i
+
+ # SyntaxSuggest.handle_error [Public]
+ #
+ # Takes a `SyntaxError` exception, uses the
+ # error message to locate the file. Then the file
+ # will be analyzed to find the location of the syntax
+ # error and emit that location to stderr.
+ #
+ # Example:
+ #
+ # begin
+ # require 'bad_file'
+ # rescue => e
+ # SyntaxSuggest.handle_error(e)
+ # end
+ #
+ # By default it will re-raise the exception unless
+ # `re_raise: false`. The message output location
+ # can be configured using the `io: $stderr` input.
+ #
+ # If a valid filename cannot be determined, the original
+ # exception will be re-raised (even with
+ # `re_raise: false`).
+ def self.handle_error(e, re_raise: true, io: $stderr)
+ unless e.is_a?(SyntaxError)
+ io.puts("SyntaxSuggest: Must pass a SyntaxError, got: #{e.class}")
+ raise e
+ end
+
+ file = PathnameFromMessage.new(e.message, io: io).call.name
+ raise e unless file
+
+ io.sync = true
+
+ call(
+ io: io,
+ source: file.read,
+ filename: file
+ )
+
+ raise e if re_raise
+ end
+
+ # SyntaxSuggest.call [Private]
+ #
+ # Main private interface
+ def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: DEFAULT_VALUE, timeout: TIMEOUT_DEFAULT, io: $stderr)
+ search = nil
+ filename = nil if filename == DEFAULT_VALUE
+ Timeout.timeout(timeout) do
+ record_dir ||= ENV["DEBUG"] ? "tmp" : nil
+ search = CodeSearch.new(source, record_dir: record_dir).call
+ end
+
+ blocks = search.invalid_blocks
+ DisplayInvalidBlocks.new(
+ io: io,
+ blocks: blocks,
+ filename: filename,
+ terminal: terminal,
+ code_lines: search.code_lines
+ ).call
+ rescue Timeout::Error => e
+ io.puts "Search timed out SYNTAX_SUGGEST_TIMEOUT=#{timeout}, run with SYNTAX_SUGGEST_DEBUG=1 for more info"
+ io.puts e.backtrace.first(3).join($/)
+ end
+
+ # SyntaxSuggest.record_dir [Private]
+ #
+ # Used to generate a unique directory to record
+ # search steps for debugging
+ def self.record_dir(dir)
+ time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
+ dir = Pathname(dir)
+ dir.join(time).tap { |path|
+ path.mkpath
+ alias_dir = dir.join("last")
+ FileUtils.rm_rf(alias_dir) if alias_dir.exist?
+ FileUtils.ln_sf(time, alias_dir)
+ }
+ end
+
+ # SyntaxSuggest.valid_without? [Private]
+ #
+ # This will tell you if the `code_lines` would be valid
+ # if you removed the `without_lines`. In short it's a
+ # way to detect if we've found the lines with syntax errors
+ # in our document yet.
+ #
+ # code_lines = [
+ # CodeLine.new(line: "def foo\n", index: 0)
+ # CodeLine.new(line: " def bar\n", index: 1)
+ # CodeLine.new(line: "end\n", index: 2)
+ # ]
+ #
+ # SyntaxSuggest.valid_without?(
+ # without_lines: code_lines[1],
+ # code_lines: code_lines
+ # ) # => true
+ #
+ # SyntaxSuggest.valid?(code_lines) # => false
+ def self.valid_without?(without_lines:, code_lines:)
+ lines = code_lines - Array(without_lines).flatten
+
+ lines.empty? || valid?(lines)
+ end
+
+ # SyntaxSuggest.invalid? [Private]
+ #
+ # Opposite of `SyntaxSuggest.valid?`
+ def self.invalid?(source)
+ source = source.join if source.is_a?(Array)
+ source = source.to_s
+
+ Prism.parse(source).failure?
+ end
+
+ # SyntaxSuggest.valid? [Private]
+ #
+ # Returns truthy if a given input source is valid syntax
+ #
+ # SyntaxSuggest.valid?(<<~EOM) # => true
+ # def foo
+ # end
+ # EOM
+ #
+ # SyntaxSuggest.valid?(<<~EOM) # => false
+ # def foo
+ # def bar # Syntax error here
+ # end
+ # EOM
+ #
+ # You can also pass in an array of lines and they'll be
+ # joined before evaluating
+ #
+ # SyntaxSuggest.valid?(
+ # [
+ # "def foo\n",
+ # "end\n"
+ # ]
+ # ) # => true
+ #
+ # SyntaxSuggest.valid?(
+ # [
+ # "def foo\n",
+ # " def bar\n", # Syntax error here
+ # "end\n"
+ # ]
+ # ) # => false
+ #
+ # As an FYI the CodeLine class instances respond to `to_s`
+ # so passing a CodeLine in as an object or as an array
+ # will convert it to it's code representation.
+ def self.valid?(source)
+ !invalid?(source)
+ end
+end
+
+# Integration
+require_relative "cli"
+
+# Core logic
+require_relative "code_search"
+require_relative "code_frontier"
+require_relative "explain_syntax"
+require_relative "clean_document"
+
+# Helpers
+require_relative "code_line"
+require_relative "code_block"
+require_relative "block_expand"
+require_relative "mini_stringio"
+require_relative "priority_queue"
+require_relative "unvisited_lines"
+require_relative "around_block_scan"
+require_relative "priority_engulf_queue"
+require_relative "pathname_from_message"
+require_relative "display_invalid_blocks"
+require_relative "parse_blocks_from_indent_line"
+require_relative "visitor"
+require_relative "token"
diff --git a/lib/syntax_suggest/around_block_scan.rb b/lib/syntax_suggest/around_block_scan.rb
new file mode 100644
index 0000000000..dd9af729c5
--- /dev/null
+++ b/lib/syntax_suggest/around_block_scan.rb
@@ -0,0 +1,232 @@
+# frozen_string_literal: true
+
+require_relative "scan_history"
+
+module SyntaxSuggest
+ # This class is useful for exploring contents before and after
+ # a block
+ #
+ # It searches above and below the passed in block to match for
+ # whatever criteria you give it:
+ #
+ # Example:
+ #
+ # def dog # 1
+ # puts "bark" # 2
+ # puts "bark" # 3
+ # end # 4
+ #
+ # scan = AroundBlockScan.new(
+ # code_lines: code_lines
+ # block: CodeBlock.new(lines: code_lines[1])
+ # )
+ #
+ # scan.scan_while { true }
+ #
+ # puts scan.before_index # => 0
+ # puts scan.after_index # => 3
+ #
+ class AroundBlockScan
+ def initialize(code_lines:, block:)
+ @code_lines = code_lines
+ @orig_indent = block.current_indent
+
+ @stop_after_kw = false
+ @force_add_empty = false
+ @force_add_hidden = false
+ @target_indent = nil
+
+ @scanner = ScanHistory.new(code_lines: code_lines, block: block)
+ end
+
+ # When using this flag, `scan_while` will
+ # bypass the block it's given and always add a
+ # line that responds truthy to `CodeLine#hidden?`
+ #
+ # Lines are hidden when they've been evaluated by
+ # the parser as part of a block and found to contain
+ # valid code.
+ def force_add_hidden
+ @force_add_hidden = true
+ self
+ end
+
+ # When using this flag, `scan_while` will
+ # bypass the block it's given and always add a
+ # line that responds truthy to `CodeLine#empty?`
+ #
+ # Empty lines contain no code, only whitespace such
+ # as leading spaces a newline.
+ def force_add_empty
+ @force_add_empty = true
+ self
+ end
+
+ # Tells `scan_while` to look for mismatched keyword/end-s
+ #
+ # When scanning up, if we see more keywords then end-s it will
+ # stop. This might happen when scanning outside of a method body.
+ # the first scan line up would be a keyword and this setting would
+ # trigger a stop.
+ #
+ # When scanning down, stop if there are more end-s than keywords.
+ def stop_after_kw
+ @stop_after_kw = true
+ self
+ end
+
+ # Main work method
+ #
+ # The scan_while method takes a block that yields lines above and
+ # below the block. If the yield returns true, the @before_index
+ # or @after_index are modified to include the matched line.
+ #
+ # In addition to yielding individual lines, the internals of this
+ # object give a mini DSL to handle common situations such as
+ # stopping if we've found a keyword/end mis-match in one direction
+ # or the other.
+ def scan_while
+ stop_next_up = false
+ stop_next_down = false
+
+ @scanner.scan(
+ up: ->(line, kw_count, end_count) {
+ next false if stop_next_up
+ next true if @force_add_hidden && line.hidden?
+ next true if @force_add_empty && line.empty?
+
+ if @stop_after_kw && kw_count > end_count
+ stop_next_up = true
+ end
+
+ yield line
+ },
+ down: ->(line, kw_count, end_count) {
+ next false if stop_next_down
+ next true if @force_add_hidden && line.hidden?
+ next true if @force_add_empty && line.empty?
+
+ if @stop_after_kw && end_count > kw_count
+ stop_next_down = true
+ end
+
+ yield line
+ }
+ )
+
+ self
+ end
+
+ # Scanning is intentionally conservative because
+ # we have no way of rolling back an aggressive block (at this time)
+ #
+ # If a block was stopped for some trivial reason, (like an empty line)
+ # but the next line would have caused it to be balanced then we
+ # can check that condition and grab just one more line either up or
+ # down.
+ #
+ # For example, below if we're scanning up, line 2 might cause
+ # the scanning to stop. This is because empty lines might
+ # denote logical breaks where the user intended to chunk code
+ # which is a good place to stop and check validity. Unfortunately
+ # it also means we might have a "dangling" keyword or end.
+ #
+ # 1 def bark
+ # 2
+ # 3 end
+ #
+ # If lines 2 and 3 are in the block, then when this method is
+ # run it would see it is unbalanced, but that acquiring line 1
+ # would make it balanced, so that's what it does.
+ def lookahead_balance_one_line
+ kw_count = 0
+ end_count = 0
+ lines.each do |line|
+ kw_count += 1 if line.is_kw?
+ end_count += 1 if line.is_end?
+ end
+
+ return self if kw_count == end_count # nothing to balance
+
+ @scanner.commit_if_changed # Rollback point if we don't find anything to optimize
+
+ # Try to eat up empty lines
+ @scanner.scan(
+ up: ->(line, _, _) { line.hidden? || line.empty? },
+ down: ->(line, _, _) { line.hidden? || line.empty? }
+ )
+
+ # More ends than keywords, check if we can balance expanding up
+ next_up = @scanner.next_up
+ next_down = @scanner.next_down
+ case end_count - kw_count
+ when 1
+ if next_up&.is_kw? && next_up.indent >= @target_indent
+ @scanner.scan(
+ up: ->(line, _, _) { line == next_up },
+ down: ->(line, _, _) { false }
+ )
+ @scanner.commit_if_changed
+ end
+ when -1
+ if next_down&.is_end? && next_down.indent >= @target_indent
+ @scanner.scan(
+ up: ->(line, _, _) { false },
+ down: ->(line, _, _) { line == next_down }
+ )
+ @scanner.commit_if_changed
+ end
+ end
+ # Rollback any uncommitted changes
+ @scanner.stash_changes
+
+ self
+ end
+
+ # Finds code lines at the same or greater indentation and adds them
+ # to the block
+ def scan_neighbors_not_empty
+ @target_indent = @orig_indent
+ scan_while { |line| line.not_empty? && line.indent >= @target_indent }
+ end
+
+ # Scan blocks based on indentation of next line above/below block
+ #
+ # Determines indentaion of the next line above/below the current block.
+ #
+ # Normally this is called when a block has expanded to capture all "neighbors"
+ # at the same (or greater) indentation and needs to expand out. For example
+ # the `def/end` lines surrounding a method.
+ def scan_adjacent_indent
+ before_after_indent = []
+
+ before_after_indent << (@scanner.next_up&.indent || 0)
+ before_after_indent << (@scanner.next_down&.indent || 0)
+
+ @target_indent = before_after_indent.min
+ scan_while { |line| line.not_empty? && line.indent >= @target_indent }
+
+ self
+ end
+
+ # Return the currently matched lines as a `CodeBlock`
+ #
+ # When a `CodeBlock` is created it will gather metadata about
+ # itself, so this is not a free conversion. Avoid allocating
+ # more CodeBlock's than needed
+ def code_block
+ CodeBlock.new(lines: lines)
+ end
+
+ # Returns the lines matched by the current scan as an
+ # array of CodeLines
+ def lines
+ @scanner.lines
+ end
+
+ # Manageable rspec errors
+ def inspect
+ "#<#{self.class}:0x0000123843lol >"
+ end
+ end
+end
diff --git a/lib/syntax_suggest/block_expand.rb b/lib/syntax_suggest/block_expand.rb
new file mode 100644
index 0000000000..2751ae2a64
--- /dev/null
+++ b/lib/syntax_suggest/block_expand.rb
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # This class is responsible for taking a code block that exists
+ # at a far indentaion and then iteratively increasing the block
+ # so that it captures everything within the same indentation block.
+ #
+ # def dog
+ # puts "bow"
+ # puts "wow"
+ # end
+ #
+ # block = BlockExpand.new(code_lines: code_lines)
+ # .call(CodeBlock.new(lines: code_lines[1]))
+ #
+ # puts block.to_s
+ # # => puts "bow"
+ # puts "wow"
+ #
+ #
+ # Once a code block has captured everything at a given indentation level
+ # then it will expand to capture surrounding indentation.
+ #
+ # block = BlockExpand.new(code_lines: code_lines)
+ # .call(block)
+ #
+ # block.to_s
+ # # => def dog
+ # puts "bow"
+ # puts "wow"
+ # end
+ #
+ class BlockExpand
+ def initialize(code_lines:)
+ @code_lines = code_lines
+ end
+
+ # Main interface. Expand current indentation, before
+ # expanding to a lower indentation
+ def call(block)
+ if (next_block = expand_neighbors(block))
+ next_block
+ else
+ expand_indent(block)
+ end
+ end
+
+ # Expands code to the next lowest indentation
+ #
+ # For example:
+ #
+ # 1 def dog
+ # 2 print "dog"
+ # 3 end
+ #
+ # If a block starts on line 2 then it has captured all it's "neighbors" (code at
+ # the same indentation or higher). To continue expanding, this block must capture
+ # lines one and three which are at a different indentation level.
+ #
+ # This method allows fully expanded blocks to decrease their indentation level (so
+ # they can expand to capture more code up and down). It does this conservatively
+ # as there's no undo (currently).
+ def expand_indent(block)
+ now = AroundBlockScan.new(code_lines: @code_lines, block: block)
+ .force_add_hidden
+ .stop_after_kw
+ .scan_adjacent_indent
+
+ now.lookahead_balance_one_line
+
+ now.code_block
+ end
+
+ # A neighbor is code that is at or above the current indent line.
+ #
+ # First we build a block with all neighbors. If we can't go further
+ # then we decrease the indentation threshold and expand via indentation
+ # i.e. `expand_indent`
+ #
+ # Handles two general cases.
+ #
+ # ## Case #1: Check code inside of methods/classes/etc.
+ #
+ # It's important to note, that not everything in a given indentation level can be parsed
+ # as valid code even if it's part of valid code. For example:
+ #
+ # 1 hash = {
+ # 2 name: "richard",
+ # 3 dog: "cinco",
+ # 4 }
+ #
+ # In this case lines 2 and 3 will be neighbors, but they're invalid until `expand_indent`
+ # is called on them.
+ #
+ # When we are adding code within a method or class (at the same indentation level),
+ # use the empty lines to denote the programmer intended logical chunks.
+ # Stop and check each one. For example:
+ #
+ # 1 def dog
+ # 2 print "dog"
+ # 3
+ # 4 hash = {
+ # 5 end
+ #
+ # If we did not stop parsing at empty newlines then the block might mistakenly grab all
+ # the contents (lines 2, 3, and 4) and report them as being problems, instead of only
+ # line 4.
+ #
+ # ## Case #2: Expand/grab other logical blocks
+ #
+ # Once the search algorithm has converted all lines into blocks at a given indentation
+ # it will then `expand_indent`. Once the blocks that generates are expanded as neighbors
+ # we then begin seeing neighbors being other logical blocks i.e. a block's neighbors
+ # may be another method or class (something with keywords/ends).
+ #
+ # For example:
+ #
+ # 1 def bark
+ # 2
+ # 3 end
+ # 4
+ # 5 def sit
+ # 6 end
+ #
+ # In this case if lines 4, 5, and 6 are in a block when it tries to expand neighbors
+ # it will expand up. If it stops after line 2 or 3 it may cause problems since there's a
+ # valid kw/end pair, but the block will be checked without it.
+ #
+ # We try to resolve this edge case with `lookahead_balance_one_line` below.
+ def expand_neighbors(block)
+ now = AroundBlockScan.new(code_lines: @code_lines, block: block)
+
+ # Initial scan
+ now
+ .force_add_hidden
+ .stop_after_kw
+ .scan_neighbors_not_empty
+
+ # Slurp up empties
+ now
+ .scan_while { |line| line.empty? }
+
+ # If next line is kw and it will balance us, take it
+ expanded_lines = now
+ .lookahead_balance_one_line
+ .lines
+
+ # Don't allocate a block if it won't be used
+ #
+ # If nothing was taken, return nil to indicate that status
+ # used in `def call` to determine if
+ # we need to expand up/out (`expand_indent`)
+ if block.lines == expanded_lines
+ nil
+ else
+ CodeBlock.new(lines: expanded_lines)
+ end
+ end
+
+ # Manageable rspec errors
+ def inspect
+ "#<SyntaxSuggest::CodeBlock:0x0000123843lol >"
+ end
+ end
+end
diff --git a/lib/syntax_suggest/capture/before_after_keyword_ends.rb b/lib/syntax_suggest/capture/before_after_keyword_ends.rb
new file mode 100644
index 0000000000..f53c57a4d1
--- /dev/null
+++ b/lib/syntax_suggest/capture/before_after_keyword_ends.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ module Capture
+ # Shows surrounding kw/end pairs
+ #
+ # The purpose of showing these extra pairs is due to cases
+ # of ambiguity when only one visible line is matched.
+ #
+ # For example:
+ #
+ # 1 class Dog
+ # 2 def bark
+ # 4 def eat
+ # 5 end
+ # 6 end
+ #
+ # In this case either line 2 could be missing an `end` or
+ # line 4 was an extra line added by mistake (it happens).
+ #
+ # When we detect the above problem it shows the issue
+ # as only being on line 2
+ #
+ # 2 def bark
+ #
+ # Showing "neighbor" keyword pairs gives extra context:
+ #
+ # 2 def bark
+ # 4 def eat
+ # 5 end
+ #
+ #
+ # Example:
+ #
+ # lines = BeforeAfterKeywordEnds.new(
+ # block: block,
+ # code_lines: code_lines
+ # ).call()
+ #
+ class BeforeAfterKeywordEnds
+ def initialize(code_lines:, block:)
+ @scanner = ScanHistory.new(code_lines: code_lines, block: block)
+ @original_indent = block.current_indent
+ end
+
+ def call
+ lines = []
+
+ @scanner.scan(
+ up: ->(line, kw_count, end_count) {
+ next true if line.empty?
+ break if line.indent < @original_indent
+ next true if line.indent != @original_indent
+
+ # If we're going up and have one complete kw/end pair, stop
+ if kw_count != 0 && kw_count == end_count
+ lines << line
+ break
+ end
+
+ lines << line if line.is_kw? || line.is_end?
+ true
+ },
+ down: ->(line, kw_count, end_count) {
+ next true if line.empty?
+ break if line.indent < @original_indent
+ next true if line.indent != @original_indent
+
+ # if we're going down and have one complete kw/end pair,stop
+ if kw_count != 0 && kw_count == end_count
+ lines << line
+ break
+ end
+
+ lines << line if line.is_kw? || line.is_end?
+ true
+ }
+ )
+ @scanner.stash_changes
+
+ lines
+ end
+ end
+ end
+end
diff --git a/lib/syntax_suggest/capture/falling_indent_lines.rb b/lib/syntax_suggest/capture/falling_indent_lines.rb
new file mode 100644
index 0000000000..1e046b2ba5
--- /dev/null
+++ b/lib/syntax_suggest/capture/falling_indent_lines.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ module Capture
+ # Shows the context around code provided by "falling" indentation
+ #
+ # If this is the original code lines:
+ #
+ # class OH
+ # def hello
+ # it "foo" do
+ # end
+ # end
+ #
+ # And this is the line that is captured
+ #
+ # it "foo" do
+ #
+ # It will yield its surrounding context:
+ #
+ # class OH
+ # def hello
+ # end
+ # end
+ #
+ # Example:
+ #
+ # FallingIndentLines.new(
+ # block: block,
+ # code_lines: @code_lines
+ # ).call do |line|
+ # @lines_to_output << line
+ # end
+ #
+ class FallingIndentLines
+ def initialize(code_lines:, block:)
+ @lines = nil
+ @scanner = ScanHistory.new(code_lines: code_lines, block: block)
+ @original_indent = block.current_indent
+ end
+
+ def call(&yieldable)
+ last_indent_up = @original_indent
+ last_indent_down = @original_indent
+
+ @scanner.commit_if_changed
+ @scanner.scan(
+ up: ->(line, _, _) {
+ next true if line.empty?
+
+ if line.indent < last_indent_up
+ yieldable.call(line)
+ last_indent_up = line.indent
+ end
+ true
+ },
+ down: ->(line, _, _) {
+ next true if line.empty?
+
+ if line.indent < last_indent_down
+ yieldable.call(line)
+ last_indent_down = line.indent
+ end
+ true
+ }
+ )
+ @scanner.stash_changes
+ end
+ end
+ end
+end
diff --git a/lib/syntax_suggest/capture_code_context.rb b/lib/syntax_suggest/capture_code_context.rb
new file mode 100644
index 0000000000..5de9ec09cc
--- /dev/null
+++ b/lib/syntax_suggest/capture_code_context.rb
@@ -0,0 +1,245 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ module Capture
+ end
+end
+
+require_relative "capture/falling_indent_lines"
+require_relative "capture/before_after_keyword_ends"
+
+module SyntaxSuggest
+ # Turns a "invalid block(s)" into useful context
+ #
+ # There are three main phases in the algorithm:
+ #
+ # 1. Sanitize/format input source
+ # 2. Search for invalid blocks
+ # 3. Format invalid blocks into something meaningful
+ #
+ # This class handles the third part.
+ #
+ # The algorithm is very good at capturing all of a syntax
+ # error in a single block in number 2, however the results
+ # can contain ambiguities. Humans are good at pattern matching
+ # and filtering and can mentally remove extraneous data, but
+ # they can't add extra data that's not present.
+ #
+ # In the case of known ambiguious cases, this class adds context
+ # back to the ambiguity so the programmer has full information.
+ #
+ # Beyond handling these ambiguities, it also captures surrounding
+ # code context information:
+ #
+ # puts block.to_s # => "def bark"
+ #
+ # context = CaptureCodeContext.new(
+ # blocks: block,
+ # code_lines: code_lines
+ # )
+ #
+ # lines = context.call.map(&:original)
+ # puts lines.join
+ # # =>
+ # class Dog
+ # def bark
+ # end
+ #
+ class CaptureCodeContext
+ attr_reader :code_lines
+
+ def initialize(blocks:, code_lines:)
+ @blocks = Array(blocks)
+ @code_lines = code_lines
+ @visible_lines = @blocks.map(&:visible_lines).flatten
+ @lines_to_output = @visible_lines.dup
+ end
+
+ def call
+ @blocks.each do |block|
+ capture_first_kw_end_same_indent(block)
+ capture_last_end_same_indent(block)
+ capture_before_after_kws(block)
+ capture_falling_indent(block)
+ end
+
+ sorted_lines
+ end
+
+ def sorted_lines
+ @lines_to_output.select!(&:not_empty?)
+ @lines_to_output.uniq!
+ @lines_to_output.sort!
+
+ @lines_to_output
+ end
+
+ # Shows the context around code provided by "falling" indentation
+ #
+ # Converts:
+ #
+ # it "foo" do
+ #
+ # into:
+ #
+ # class OH
+ # def hello
+ # it "foo" do
+ # end
+ # end
+ #
+ def capture_falling_indent(block)
+ Capture::FallingIndentLines.new(
+ block: block,
+ code_lines: @code_lines
+ ).call do |line|
+ @lines_to_output << line
+ end
+ end
+
+ # Shows surrounding kw/end pairs
+ #
+ # The purpose of showing these extra pairs is due to cases
+ # of ambiguity when only one visible line is matched.
+ #
+ # For example:
+ #
+ # 1 class Dog
+ # 2 def bark
+ # 4 def eat
+ # 5 end
+ # 6 end
+ #
+ # In this case either line 2 could be missing an `end` or
+ # line 4 was an extra line added by mistake (it happens).
+ #
+ # When we detect the above problem it shows the issue
+ # as only being on line 2
+ #
+ # 2 def bark
+ #
+ # Showing "neighbor" keyword pairs gives extra context:
+ #
+ # 2 def bark
+ # 4 def eat
+ # 5 end
+ #
+ def capture_before_after_kws(block)
+ return unless block.visible_lines.count == 1
+
+ around_lines = Capture::BeforeAfterKeywordEnds.new(
+ code_lines: @code_lines,
+ block: block
+ ).call
+
+ around_lines -= block.lines
+
+ @lines_to_output.concat(around_lines)
+ end
+
+ # When there is an invalid block with a keyword
+ # missing an end right before another end,
+ # it is unclear where which keyword is missing the
+ # end
+ #
+ # Take this example:
+ #
+ # class Dog # 1
+ # def bark # 2
+ # puts "woof" # 3
+ # end # 4
+ #
+ # However due to https://github.com/ruby/syntax_suggest/issues/32
+ # the problem line will be identified as:
+ #
+ # > class Dog # 1
+ #
+ # Because lines 2, 3, and 4 are technically valid code and are expanded
+ # first, deemed valid, and hidden. We need to un-hide the matching end
+ # line 4. Also work backwards and if there's a mis-matched keyword, show it
+ # too
+ def capture_last_end_same_indent(block)
+ return if block.visible_lines.length != 1
+ return unless block.visible_lines.first.is_kw?
+
+ visible_line = block.visible_lines.first
+ lines = @code_lines[visible_line.index..block.lines.last.index]
+
+ # Find first end with same indent
+ # (this would return line 4)
+ #
+ # end # 4
+ matching_end = lines.detect { |line| line.indent == block.current_indent && line.is_end? }
+ return unless matching_end
+
+ @lines_to_output << matching_end
+
+ # Work backwards from the end to
+ # see if there are mis-matched
+ # keyword/end pairs
+ #
+ # Return the first mis-matched keyword
+ # this would find line 2
+ #
+ # def bark # 2
+ # puts "woof" # 3
+ # end # 4
+ end_count = 0
+ kw_count = 0
+ kw_line = @code_lines[visible_line.index..matching_end.index].reverse.detect do |line|
+ end_count += 1 if line.is_end?
+ kw_count += 1 if line.is_kw?
+
+ !kw_count.zero? && kw_count >= end_count
+ end
+ return unless kw_line
+ @lines_to_output << kw_line
+ end
+
+ # The logical inverse of `capture_last_end_same_indent`
+ #
+ # When there is an invalid block with an `end`
+ # missing a keyword right after another `end`,
+ # it is unclear where which end is missing the
+ # keyword.
+ #
+ # Take this example:
+ #
+ # class Dog # 1
+ # puts "woof" # 2
+ # end # 3
+ # end # 4
+ #
+ # the problem line will be identified as:
+ #
+ # > end # 4
+ #
+ # This happens because lines 1, 2, and 3 are technically valid code and are expanded
+ # first, deemed valid, and hidden. We need to un-hide the matching keyword on
+ # line 1. Also work backwards and if there's a mis-matched end, show it
+ # too
+ def capture_first_kw_end_same_indent(block)
+ return if block.visible_lines.length != 1
+ return unless block.visible_lines.first.is_end?
+
+ visible_line = block.visible_lines.first
+ lines = @code_lines[block.lines.first.index..visible_line.index]
+ matching_kw = lines.reverse.detect { |line| line.indent == block.current_indent && line.is_kw? }
+ return unless matching_kw
+
+ @lines_to_output << matching_kw
+
+ kw_count = 0
+ end_count = 0
+ orphan_end = @code_lines[matching_kw.index..visible_line.index].detect do |line|
+ kw_count += 1 if line.is_kw?
+ end_count += 1 if line.is_end?
+
+ end_count >= kw_count
+ end
+
+ return unless orphan_end
+ @lines_to_output << orphan_end
+ end
+ end
+end
diff --git a/lib/syntax_suggest/clean_document.rb b/lib/syntax_suggest/clean_document.rb
new file mode 100644
index 0000000000..94c68d8ad4
--- /dev/null
+++ b/lib/syntax_suggest/clean_document.rb
@@ -0,0 +1,223 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Parses and sanitizes source into a lexically aware document
+ #
+ # Internally the document is represented by an array with each
+ # index containing a CodeLine correlating to a line from the source code.
+ #
+ # There are three main phases in the algorithm:
+ #
+ # 1. Sanitize/format input source
+ # 2. Search for invalid blocks
+ # 3. Format invalid blocks into something meaningful
+ #
+ # This class handles the first part.
+ #
+ # The reason this class exists is to format input source
+ # for better/easier/cleaner exploration.
+ #
+ # The CodeSearch class operates at the line level so
+ # we must be careful to not introduce lines that look
+ # valid by themselves, but when removed will trigger syntax errors
+ # or strange behavior.
+ #
+ # ## Join Trailing slashes
+ #
+ # Code with a trailing slash is logically treated as a single line:
+ #
+ # 1 it "code can be split" \
+ # 2 "across multiple lines" do
+ #
+ # In this case removing line 2 would add a syntax error. We get around
+ # this by internally joining the two lines into a single "line" object
+ #
+ # ## Logically Consecutive lines
+ #
+ # Code that can be broken over multiple
+ # lines such as method calls are on different lines:
+ #
+ # 1 User.
+ # 2 where(name: "schneems").
+ # 3 first
+ #
+ # Removing line 2 can introduce a syntax error. To fix this, all lines
+ # are joined into one.
+ #
+ # ## Heredocs
+ #
+ # A heredoc is an way of defining a multi-line string. They can cause many
+ # problems. If left as a single line, the parser would try to parse the contents
+ # as ruby code rather than as a string. Even without this problem, we still
+ # hit an issue with indentation:
+ #
+ # 1 foo = <<~HEREDOC
+ # 2 "Be yourself; everyone else is already taken.""
+ # 3 ― Oscar Wilde
+ # 4 puts "I look like ruby code" # but i'm still a heredoc
+ # 5 HEREDOC
+ #
+ # If we didn't join these lines then our algorithm would think that line 4
+ # is separate from the rest, has a higher indentation, then look at it first
+ # and remove it.
+ #
+ # If the code evaluates line 5 by itself it will think line 5 is a constant,
+ # remove it, and introduce a syntax errror.
+ #
+ # All of these problems are fixed by joining the whole heredoc into a single
+ # line.
+ class CleanDocument
+ def initialize(source:)
+ @document = CodeLine.from_source(source)
+ end
+
+ # Call all of the document "cleaners"
+ # and return self
+ def call
+ join_trailing_slash!
+ join_consecutive!
+ join_heredoc!
+
+ self
+ end
+
+ # Return an array of CodeLines in the
+ # document
+ def lines
+ @document
+ end
+
+ # Renders the document back to a string
+ def to_s
+ @document.join
+ end
+
+ # Smushes all heredoc lines into one line
+ #
+ # source = <<~'EOM'
+ # foo = <<~HEREDOC
+ # lol
+ # hehehe
+ # HEREDOC
+ # EOM
+ #
+ # lines = CleanDocument.new(source: source).join_heredoc!.lines
+ # expect(lines[0].to_s).to eq(source)
+ # expect(lines[1].to_s).to eq("")
+ def join_heredoc!
+ start_index_stack = []
+ heredoc_beg_end_index = []
+ lines.each do |line|
+ line.tokens.each do |token|
+ case token.type
+ when :HEREDOC_START
+ start_index_stack << line.index
+ when :HEREDOC_END
+ start_index = start_index_stack.pop
+ end_index = line.index
+ heredoc_beg_end_index << [start_index, end_index]
+ end
+ end
+ end
+
+ heredoc_groups = heredoc_beg_end_index.map { |start_index, end_index| @document[start_index..end_index] }
+
+ join_groups(heredoc_groups)
+ self
+ end
+
+ # Smushes logically "consecutive" lines
+ #
+ # source = <<~'EOM'
+ # User.
+ # where(name: 'schneems').
+ # first
+ # EOM
+ #
+ # lines = CleanDocument.new(source: source).join_consecutive!.lines
+ # expect(lines[0].to_s).to eq(source)
+ # expect(lines[1].to_s).to eq("")
+ #
+ def join_consecutive!
+ consecutive_groups = @document.select(&:consecutive?).map do |code_line|
+ take_while_including(code_line.index..) do |line|
+ line.consecutive?
+ end
+ end
+
+ join_groups(consecutive_groups)
+ self
+ end
+
+ # Join lines with a trailing slash
+ #
+ # source = <<~'EOM'
+ # it "code can be split" \
+ # "across multiple lines" do
+ # EOM
+ #
+ # lines = CleanDocument.new(source: source).join_consecutive!.lines
+ # expect(lines[0].to_s).to eq(source)
+ # expect(lines[1].to_s).to eq("")
+ def join_trailing_slash!
+ trailing_groups = @document.select(&:trailing_slash?).map do |code_line|
+ take_while_including(code_line.index..) { |x| x.trailing_slash? }
+ end
+ join_groups(trailing_groups)
+ self
+ end
+
+ # Helper method for joining "groups" of lines
+ #
+ # Input is expected to be type Array<Array<CodeLine>>
+ #
+ # The outer array holds the various "groups" while the
+ # inner array holds code lines.
+ #
+ # All code lines are "joined" into the first line in
+ # their group.
+ #
+ # To preserve document size, empty lines are placed
+ # in the place of the lines that were "joined"
+ def join_groups(groups)
+ groups.each do |lines|
+ line = lines.first
+
+ # Handle the case of multiple groups in a row
+ # if one is already replaced, move on
+ next if @document[line.index].empty?
+
+ # Join group into the first line
+ @document[line.index] = CodeLine.new(
+ tokens: lines.map(&:tokens).flatten,
+ line: lines.join,
+ index: line.index,
+ consecutive: false
+ )
+
+ # Hide the rest of the lines
+ lines[1..].each do |line|
+ # The above lines already have newlines in them, if add more
+ # then there will be double newline, use an empty line instead
+ @document[line.index] = CodeLine.new(line: "", index: line.index, tokens: [], consecutive: false)
+ end
+ end
+ self
+ end
+
+ # Helper method for grabbing elements from document
+ #
+ # Like `take_while` except when it stops
+ # iterating, it also returns the line
+ # that caused it to stop
+ def take_while_including(range = 0..)
+ take_next_and_stop = false
+ @document[range].take_while do |line|
+ next if take_next_and_stop
+
+ take_next_and_stop = !(yield line)
+ true
+ end
+ end
+ end
+end
diff --git a/lib/syntax_suggest/cli.rb b/lib/syntax_suggest/cli.rb
new file mode 100644
index 0000000000..967f77bf70
--- /dev/null
+++ b/lib/syntax_suggest/cli.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+require "pathname"
+require "optparse"
+
+module SyntaxSuggest
+ # All the logic of the exe/syntax_suggest CLI in one handy spot
+ #
+ # Cli.new(argv: ["--help"]).call
+ # Cli.new(argv: ["<path/to/file>.rb"]).call
+ # Cli.new(argv: ["<path/to/file>.rb", "--record=tmp"]).call
+ # Cli.new(argv: ["<path/to/file>.rb", "--terminal"]).call
+ #
+ class Cli
+ attr_accessor :options
+
+ # ARGV is Everything passed to the executable, does not include executable name
+ #
+ # All other intputs are dependency injection for testing
+ def initialize(argv:, exit_obj: Kernel, io: $stdout, env: ENV)
+ @options = {}
+ @parser = nil
+ options[:record_dir] = env["SYNTAX_SUGGEST_RECORD_DIR"]
+ options[:record_dir] = "tmp" if env["DEBUG"]
+ options[:terminal] = SyntaxSuggest::DEFAULT_VALUE
+
+ @io = io
+ @argv = argv
+ @exit_obj = exit_obj
+ end
+
+ def call
+ if @argv.empty?
+ # Display help if raw command
+ parser.parse! %w[--help]
+ return
+ else
+ # Mutates @argv
+ parse
+ return if options[:exit]
+ end
+
+ file_name = @argv.first
+ if file_name.nil?
+ @io.puts "No file given"
+ @exit_obj.exit(1)
+ return
+ end
+
+ file = Pathname(file_name)
+ if !file.exist?
+ @io.puts "file not found: #{file.expand_path} "
+ @exit_obj.exit(1)
+ return
+ end
+
+ @io.puts "Record dir: #{options[:record_dir]}" if options[:record_dir]
+
+ display = SyntaxSuggest.call(
+ io: @io,
+ source: file.read,
+ filename: file.expand_path,
+ terminal: options.fetch(:terminal, SyntaxSuggest::DEFAULT_VALUE),
+ record_dir: options[:record_dir]
+ )
+
+ if display.document_ok?
+ @io.puts "Syntax OK"
+ @exit_obj.exit(0)
+ else
+ @exit_obj.exit(1)
+ end
+ end
+
+ def parse
+ parser.parse!(@argv)
+
+ self
+ end
+
+ def parser
+ @parser ||= OptionParser.new do |opts|
+ opts.banner = <<~EOM
+ Usage: syntax_suggest <file> [options]
+
+ Parses a ruby source file and searches for syntax error(s) such as
+ unexpected `end', expecting end-of-input.
+
+ Example:
+
+ $ syntax_suggest dog.rb
+
+ # ...
+
+ > 10 defdog
+ > 15 end
+
+ ENV options:
+
+ SYNTAX_SUGGEST_RECORD_DIR=<dir>
+
+ Records the steps used to search for a syntax error
+ to the given directory
+
+ Options:
+ EOM
+
+ opts.version = SyntaxSuggest::VERSION
+
+ opts.on("--help", "Help - displays this message") do |v|
+ @io.puts opts
+ options[:exit] = true
+ @exit_obj.exit
+ end
+
+ opts.on("--record <dir>", "Records the steps used to search for a syntax error to the given directory") do |v|
+ options[:record_dir] = v
+ end
+
+ opts.on("--terminal", "Enable terminal highlighting") do |v|
+ options[:terminal] = true
+ end
+
+ opts.on("--no-terminal", "Disable terminal highlighting") do |v|
+ options[:terminal] = false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/syntax_suggest/code_block.rb b/lib/syntax_suggest/code_block.rb
new file mode 100644
index 0000000000..d842890300
--- /dev/null
+++ b/lib/syntax_suggest/code_block.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Multiple lines form a singular CodeBlock
+ #
+ # Source code is made of multiple CodeBlocks.
+ #
+ # Example:
+ #
+ # code_block.to_s # =>
+ # # def foo
+ # # puts "foo"
+ # # end
+ #
+ # code_block.valid? # => true
+ # code_block.in_valid? # => false
+ #
+ #
+ class CodeBlock
+ UNSET = Object.new.freeze
+ attr_reader :lines, :starts_at, :ends_at
+
+ def initialize(lines: [])
+ @lines = Array(lines)
+ @valid = UNSET
+ @deleted = false
+ @starts_at = @lines.first.number
+ @ends_at = @lines.last.number
+ end
+
+ def delete
+ @deleted = true
+ end
+
+ def deleted?
+ @deleted
+ end
+
+ def visible_lines
+ @lines.select(&:visible?).select(&:not_empty?)
+ end
+
+ def mark_invisible
+ @lines.map(&:mark_invisible)
+ end
+
+ def is_end?
+ to_s.strip == "end"
+ end
+
+ def hidden?
+ @lines.all?(&:hidden?)
+ end
+
+ # This is used for frontier ordering, we are searching from
+ # the largest indentation to the smallest. This allows us to
+ # populate an array with multiple code blocks then call `sort!`
+ # on it without having to specify the sorting criteria
+ def <=>(other)
+ out = current_indent <=> other.current_indent
+ return out if out != 0
+
+ # Stable sort
+ starts_at <=> other.starts_at
+ end
+
+ def current_indent
+ @current_indent ||= lines.select(&:not_empty?).map(&:indent).min || 0
+ end
+
+ def invalid?
+ !valid?
+ end
+
+ def valid?
+ if @valid == UNSET
+ # Performance optimization
+ #
+ # If all the lines were previously hidden
+ # and we expand to capture additional empty
+ # lines then the result cannot be invalid
+ #
+ # That means there's no reason to re-check all
+ # lines with the parser (which is expensive).
+ # Benchmark in commit message
+ @valid = if lines.all? { |l| l.hidden? || l.empty? }
+ true
+ else
+ SyntaxSuggest.valid?(lines.map(&:original).join)
+ end
+ else
+ @valid
+ end
+ end
+
+ def to_s
+ @lines.join
+ end
+ end
+end
diff --git a/lib/syntax_suggest/code_frontier.rb b/lib/syntax_suggest/code_frontier.rb
new file mode 100644
index 0000000000..38d5375ef4
--- /dev/null
+++ b/lib/syntax_suggest/code_frontier.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # The main function of the frontier is to hold the edges of our search and to
+ # evaluate when we can stop searching.
+
+ # There are three main phases in the algorithm:
+ #
+ # 1. Sanitize/format input source
+ # 2. Search for invalid blocks
+ # 3. Format invalid blocks into something meaningful
+ #
+ # The Code frontier is a critical part of the second step
+ #
+ # ## Knowing where we've been
+ #
+ # Once a code block is generated it is added onto the frontier. Then it will be
+ # sorted by indentation and frontier can be filtered. Large blocks that fully enclose a
+ # smaller block will cause the smaller block to be evicted.
+ #
+ # CodeFrontier#<<(block) # Adds block to frontier
+ # CodeFrontier#pop # Removes block from frontier
+ #
+ # ## Knowing where we can go
+ #
+ # Internally the frontier keeps track of "unvisited" lines which are exposed via `next_indent_line`
+ # when called, this method returns, a line of code with the highest indentation.
+ #
+ # The returned line of code can be used to build a CodeBlock and then that code block
+ # is added back to the frontier. Then, the lines are removed from the
+ # "unvisited" so we don't double-create the same block.
+ #
+ # CodeFrontier#next_indent_line # Shows next line
+ # CodeFrontier#register_indent_block(block) # Removes lines from unvisited
+ #
+ # ## Knowing when to stop
+ #
+ # The frontier knows how to check the entire document for a syntax error. When blocks
+ # are added onto the frontier, they're removed from the document. When all code containing
+ # syntax errors has been added to the frontier, the document will be parsable without a
+ # syntax error and the search can stop.
+ #
+ # CodeFrontier#holds_all_syntax_errors? # Returns true when frontier holds all syntax errors
+ #
+ # ## Filtering false positives
+ #
+ # Once the search is completed, the frontier may have multiple blocks that do not contain
+ # the syntax error. To limit the result to the smallest subset of "invalid blocks" call:
+ #
+ # CodeFrontier#detect_invalid_blocks
+ #
+ class CodeFrontier
+ def initialize(code_lines:, unvisited: UnvisitedLines.new(code_lines: code_lines))
+ @code_lines = code_lines
+ @unvisited = unvisited
+ @queue = PriorityEngulfQueue.new
+
+ @check_next = true
+ end
+
+ def count
+ @queue.length
+ end
+
+ # Performance optimization
+ #
+ # Parsing with ripper is expensive
+ # If we know we don't have any blocks with invalid
+ # syntax, then we know we cannot have found
+ # the incorrect syntax yet.
+ #
+ # When an invalid block is added onto the frontier
+ # check document state
+ private def can_skip_check?
+ check_next = @check_next
+ @check_next = false
+
+ if check_next
+ false
+ else
+ true
+ end
+ end
+
+ # Returns true if the document is valid with all lines
+ # removed. By default it checks all blocks in present in
+ # the frontier array, but can be used for arbitrary arrays
+ # of codeblocks as well
+ def holds_all_syntax_errors?(block_array = @queue, can_cache: true)
+ return false if can_cache && can_skip_check?
+
+ without_lines = block_array.to_a.flat_map do |block|
+ block.lines
+ end
+
+ SyntaxSuggest.valid_without?(
+ without_lines: without_lines,
+ code_lines: @code_lines
+ )
+ end
+
+ # Returns a code block with the largest indentation possible
+ def pop
+ @queue.pop
+ end
+
+ def next_indent_line
+ @unvisited.peek
+ end
+
+ def expand?
+ return false if @queue.empty?
+ return true if @unvisited.empty?
+
+ frontier_indent = @queue.peek.current_indent
+ unvisited_indent = next_indent_line.indent
+
+ if ENV["SYNTAX_SUGGEST_DEBUG"]
+ puts "```"
+ puts @queue.peek
+ puts "```"
+ puts " @frontier indent: #{frontier_indent}"
+ puts " @unvisited indent: #{unvisited_indent}"
+ end
+
+ # Expand all blocks before moving to unvisited lines
+ frontier_indent >= unvisited_indent
+ end
+
+ # Keeps track of what lines have been added to blocks and which are not yet
+ # visited.
+ def register_indent_block(block)
+ @unvisited.visit_block(block)
+ self
+ end
+
+ # When one element fully encapsulates another we remove the smaller
+ # block from the frontier. This prevents double expansions and all-around
+ # weird behavior. However this guarantee is quite expensive to maintain
+ def register_engulf_block(block)
+ end
+
+ # Add a block to the frontier
+ #
+ # This method ensures the frontier always remains sorted (in indentation order)
+ # and that each code block's lines are removed from the indentation hash so we
+ # don't re-evaluate the same line multiple times.
+ def <<(block)
+ @unvisited.visit_block(block)
+
+ @queue.push(block)
+
+ @check_next = true if block.invalid?
+
+ self
+ end
+
+ # Example:
+ #
+ # combination([:a, :b, :c, :d])
+ # # => [[:a], [:b], [:c], [:d], [:a, :b], [:a, :c], [:a, :d], [:b, :c], [:b, :d], [:c, :d], [:a, :b, :c], [:a, :b, :d], [:a, :c, :d], [:b, :c, :d], [:a, :b, :c, :d]]
+ def self.combination(array)
+ guesses = []
+ 1.upto(array.length).each do |size|
+ guesses.concat(array.combination(size).to_a)
+ end
+ guesses
+ end
+
+ # Given that we know our syntax error exists somewhere in our frontier, we want to find
+ # the smallest possible set of blocks that contain all the syntax errors
+ def detect_invalid_blocks
+ self.class.combination(@queue.to_a.select(&:invalid?)).detect do |block_array|
+ holds_all_syntax_errors?(block_array, can_cache: false)
+ end || []
+ end
+ end
+end
diff --git a/lib/syntax_suggest/code_line.rb b/lib/syntax_suggest/code_line.rb
new file mode 100644
index 0000000000..7fb1aae26a
--- /dev/null
+++ b/lib/syntax_suggest/code_line.rb
@@ -0,0 +1,222 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Represents a single line of code of a given source file
+ #
+ # This object contains metadata about the line such as
+ # amount of indentation, if it is empty or not, and
+ # lexical data, such as if it has an `end` or a keyword
+ # in it.
+ #
+ # Visibility of lines can be toggled off. Marking a line as invisible
+ # indicates that it should not be used for syntax checks.
+ # It's functionally the same as commenting it out.
+ #
+ # Example:
+ #
+ # line = CodeLine.from_source("def foo\n").first
+ # line.number => 1
+ # line.empty? # => false
+ # line.visible? # => true
+ # line.mark_invisible
+ # line.visible? # => false
+ #
+ class CodeLine
+ TRAILING_SLASH = ("\\" + $/).freeze
+
+ # Returns an array of CodeLine objects
+ # from the source string
+ def self.from_source(source)
+ source = +source
+ parse_result = Prism.parse_lex(source)
+ ast, tokens = parse_result.value
+
+ clean_comments!(source, parse_result.comments)
+
+ visitor = Visitor.new
+ visitor.visit(ast)
+ tokens.sort_by! { |token, _state| token.location.start_line }
+
+ prev_token = nil
+ tokens.map! do |token, _state|
+ prev_token = Token.new(token, prev_token, visitor)
+ end
+
+ tokens_for_line = tokens.each_with_object(Hash.new { |h, k| h[k] = [] }) { |token, hash| hash[token.line] << token }
+ source.lines.map.with_index do |line, index|
+ CodeLine.new(
+ line: line,
+ index: index,
+ tokens: tokens_for_line[index + 1],
+ consecutive: visitor.consecutive_lines.include?(index + 1)
+ )
+ end
+ end
+
+ # Remove comments that apear on their own in source. They will never be the cause
+ # of syntax errors and are just visual noise. Example:
+ #
+ # source = +<<~RUBY
+ # # Comment-only line
+ # foo # Inline comment
+ # RUBY
+ # CodeLine.clean_comments!(source, Prism.parse(source).comments)
+ # source # => "\nfoo # Inline comment\n"
+ def self.clean_comments!(source, comments)
+ # Iterate backwards since we are modifying the source in place and must preserve
+ # the offsets. Prism comments are sorted by their location in the source.
+ comments.reverse_each do |comment|
+ next if comment.trailing?
+ source.bytesplice(comment.location.start_offset, comment.location.length, "")
+ end
+ end
+
+ attr_reader :line, :index, :tokens, :line_number, :indent
+ def initialize(line:, index:, tokens:, consecutive:)
+ @tokens = tokens
+ @line = line
+ @index = index
+ @consecutive = consecutive
+ @original = line
+ @line_number = @index + 1
+ strip_line = line.dup
+ strip_line.lstrip!
+
+ @indent = if (@empty = strip_line.empty?)
+ line.length - 1 # Newline removed from strip_line is not "whitespace"
+ else
+ line.length - strip_line.length
+ end
+
+ set_kw_end
+ end
+
+ # Used for stable sort via indentation level
+ #
+ # Ruby's sort is not "stable" meaning that when
+ # multiple elements have the same value, they are
+ # not guaranteed to return in the same order they
+ # were put in.
+ #
+ # So when multiple code lines have the same indentation
+ # level, they're sorted by their index value which is unique
+ # and consistent.
+ #
+ # This is mostly needed for consistency of the test suite
+ def indent_index
+ @indent_index ||= [indent, index]
+ end
+ alias_method :number, :line_number
+
+ # Returns true if the code line is determined
+ # to contain a keyword that matches with an `end`
+ #
+ # For example: `def`, `do`, `begin`, `ensure`, etc.
+ def is_kw?
+ @is_kw
+ end
+
+ # Returns true if the code line is determined
+ # to contain an `end` keyword
+ def is_end?
+ @is_end
+ end
+
+ # Used to hide lines
+ #
+ # The search alorithm will group lines into blocks
+ # then if those blocks are determined to represent
+ # valid code they will be hidden
+ def mark_invisible
+ @line = ""
+ end
+
+ # Means the line was marked as "invisible"
+ # Confusingly, "empty" lines are visible...they
+ # just don't contain any source code other than a newline ("\n").
+ def visible?
+ !line.empty?
+ end
+
+ # Opposite or `visible?` (note: different than `empty?`)
+ def hidden?
+ !visible?
+ end
+
+ # An `empty?` line is one that was originally left
+ # empty in the source code, while a "hidden" line
+ # is one that we've since marked as "invisible"
+ def empty?
+ @empty
+ end
+
+ # Opposite of `empty?` (note: different than `visible?`)
+ def not_empty?
+ !empty?
+ end
+
+ # Renders the given line
+ #
+ # Also allows us to represent source code as
+ # an array of code lines.
+ #
+ # When we have an array of code line elements
+ # calling `join` on the array will call `to_s`
+ # on each element, which essentially converts
+ # it back into it's original source string.
+ def to_s
+ line
+ end
+
+ # When the code line is marked invisible
+ # we retain the original value of it's line
+ # this is useful for debugging and for
+ # showing extra context
+ #
+ # DisplayCodeWithLineNumbers will render
+ # all lines given to it, not just visible
+ # lines, it uses the original method to
+ # obtain them.
+ attr_reader :original
+
+ # Comparison operator, needed for equality
+ # and sorting
+ def <=>(other)
+ index <=> other.index
+ end
+
+ # Can this line be logically joined together
+ # with the following line? Determined by walking
+ # the AST
+ def consecutive?
+ @consecutive
+ end
+
+ # Determines if the given line has a trailing slash.
+ # Simply check if the line contains a backslash after
+ # the content of the last token.
+ #
+ # lines = CodeLine.from_source(<<~EOM)
+ # it "foo" \
+ # EOM
+ # expect(lines.first.trailing_slash?).to eq(true)
+ #
+ def trailing_slash?
+ return unless (last = @tokens.last)
+ @line.byteindex(TRAILING_SLASH, last.location.end_column) != nil
+ end
+
+ private def set_kw_end
+ kw_count = 0
+ end_count = 0
+
+ @tokens.each do |token|
+ kw_count += 1 if token.is_kw?
+ end_count += 1 if token.is_end?
+ end
+
+ @is_kw = (kw_count - end_count) > 0
+ @is_end = (end_count - kw_count) > 0
+ end
+ end
+end
diff --git a/lib/syntax_suggest/code_search.rb b/lib/syntax_suggest/code_search.rb
new file mode 100644
index 0000000000..7628dcd131
--- /dev/null
+++ b/lib/syntax_suggest/code_search.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Searches code for a syntax error
+ #
+ # There are three main phases in the algorithm:
+ #
+ # 1. Sanitize/format input source
+ # 2. Search for invalid blocks
+ # 3. Format invalid blocks into something meaninful
+ #
+ # This class handles the part.
+ #
+ # The bulk of the heavy lifting is done in:
+ #
+ # - CodeFrontier (Holds information for generating blocks and determining if we can stop searching)
+ # - ParseBlocksFromLine (Creates blocks into the frontier)
+ # - BlockExpand (Expands existing blocks to search more code)
+ #
+ # ## Syntax error detection
+ #
+ # When the frontier holds the syntax error, we can stop searching
+ #
+ # search = CodeSearch.new(<<~EOM)
+ # def dog
+ # def lol
+ # end
+ # EOM
+ #
+ # search.call
+ #
+ # search.invalid_blocks.map(&:to_s) # =>
+ # # => ["def lol\n"]
+ #
+ class CodeSearch
+ private
+
+ attr_reader :frontier
+
+ public
+
+ attr_reader :invalid_blocks, :record_dir, :code_lines
+
+ def initialize(source, record_dir: DEFAULT_VALUE)
+ record_dir = if record_dir == DEFAULT_VALUE
+ (ENV["SYNTAX_SUGGEST_RECORD_DIR"] || ENV["SYNTAX_SUGGEST_DEBUG"]) ? "tmp" : nil
+ else
+ record_dir
+ end
+
+ if record_dir
+ @record_dir = SyntaxSuggest.record_dir(record_dir)
+ @write_count = 0
+ end
+
+ @tick = 0
+ @source = source
+ @name_tick = Hash.new { |hash, k| hash[k] = 0 }
+ @invalid_blocks = []
+
+ @code_lines = CleanDocument.new(source: source).call.lines
+
+ @frontier = CodeFrontier.new(code_lines: @code_lines)
+ @block_expand = BlockExpand.new(code_lines: @code_lines)
+ @parse_blocks_from_indent_line = ParseBlocksFromIndentLine.new(code_lines: @code_lines)
+ end
+
+ # Used for debugging
+ def record(block:, name: "record")
+ return unless @record_dir
+ @name_tick[name] += 1
+ filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}-(#{block.starts_at}__#{block.ends_at}).txt"
+ if ENV["SYNTAX_SUGGEST_DEBUG"]
+ puts "\n\n==== #{filename} ===="
+ puts "\n```#{block.starts_at}..#{block.ends_at}"
+ puts block
+ puts "```"
+ puts " block indent: #{block.current_indent}"
+ end
+ @record_dir.join(filename).open(mode: "a") do |f|
+ document = DisplayCodeWithLineNumbers.new(
+ lines: @code_lines.select(&:visible?),
+ terminal: false,
+ highlight_lines: block.lines
+ ).call
+
+ f.write(" Block lines: #{block.starts_at..block.ends_at} (#{name}) \n\n#{document}")
+ end
+ end
+
+ def push(block, name:)
+ record(block: block, name: name)
+
+ block.mark_invisible if block.valid?
+ frontier << block
+ end
+
+ # Parses the most indented lines into blocks that are marked
+ # and added to the frontier
+ def create_blocks_from_untracked_lines
+ max_indent = frontier.next_indent_line&.indent
+
+ while (line = frontier.next_indent_line) && (line.indent == max_indent)
+ @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block|
+ push(block, name: "add")
+ end
+ end
+ end
+
+ # Given an already existing block in the frontier, expand it to see
+ # if it contains our invalid syntax
+ def expand_existing
+ block = frontier.pop
+ return unless block
+
+ record(block: block, name: "before-expand")
+
+ block = @block_expand.call(block)
+ push(block, name: "expand")
+ end
+
+ # Main search loop
+ def call
+ until frontier.holds_all_syntax_errors?
+ @tick += 1
+
+ if frontier.expand?
+ expand_existing
+ else
+ create_blocks_from_untracked_lines
+ end
+ end
+
+ @invalid_blocks.concat(frontier.detect_invalid_blocks)
+ @invalid_blocks.sort_by! { |block| block.starts_at }
+ self
+ end
+ end
+end
diff --git a/lib/syntax_suggest/core_ext.rb b/lib/syntax_suggest/core_ext.rb
new file mode 100644
index 0000000000..ffbc922eed
--- /dev/null
+++ b/lib/syntax_suggest/core_ext.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # SyntaxSuggest.module_for_detailed_message [Private]
+ #
+ # Used to monkeypatch SyntaxError via Module.prepend
+ def self.module_for_detailed_message
+ Module.new {
+ def detailed_message(highlight: true, syntax_suggest: true, **kwargs)
+ return super unless syntax_suggest
+
+ require "syntax_suggest/api" unless defined?(SyntaxSuggest::DEFAULT_VALUE)
+
+ message = super
+
+ if path
+ file = Pathname.new(path)
+ io = SyntaxSuggest::MiniStringIO.new
+
+ SyntaxSuggest.call(
+ io: io,
+ source: file.read,
+ filename: file,
+ terminal: highlight
+ )
+ annotation = io.string
+
+ annotation += "\n" unless annotation.end_with?("\n")
+
+ annotation + message
+ else
+ message
+ end
+ rescue => e
+ if ENV["SYNTAX_SUGGEST_DEBUG"]
+ $stderr.warn(e.message)
+ $stderr.warn(e.backtrace)
+ end
+
+ # Ignore internal errors
+ message
+ end
+ }
+ end
+end
+
+SyntaxError.prepend(SyntaxSuggest.module_for_detailed_message)
diff --git a/lib/syntax_suggest/display_code_with_line_numbers.rb b/lib/syntax_suggest/display_code_with_line_numbers.rb
new file mode 100644
index 0000000000..a18d62e54b
--- /dev/null
+++ b/lib/syntax_suggest/display_code_with_line_numbers.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Outputs code with highlighted lines
+ #
+ # Whatever is passed to this class will be rendered
+ # even if it is "marked invisible" any filtering of
+ # output should be done before calling this class.
+ #
+ # DisplayCodeWithLineNumbers.new(
+ # lines: lines,
+ # highlight_lines: [lines[2], lines[3]]
+ # ).call
+ # # =>
+ # 1
+ # 2 def cat
+ # > 3 Dir.chdir
+ # > 4 end
+ # 5 end
+ # 6
+ class DisplayCodeWithLineNumbers
+ TERMINAL_HIGHLIGHT = "\e[1;3m" # Bold, italics
+ TERMINAL_END = "\e[0m"
+
+ def initialize(lines:, highlight_lines: [], terminal: false)
+ @lines = Array(lines).sort
+ @terminal = terminal
+ @highlight_line_hash = Array(highlight_lines).each_with_object({}) { |line, h| h[line] = true }
+ @digit_count = @lines.last&.line_number.to_s.length
+ end
+
+ def call
+ @lines.map do |line|
+ format_line(line)
+ end.join
+ end
+
+ private def format_line(code_line)
+ # Handle trailing slash lines
+ code_line.original.lines.map.with_index do |contents, i|
+ format(
+ empty: code_line.empty?,
+ number: (code_line.number + i).to_s,
+ contents: contents,
+ highlight: @highlight_line_hash[code_line]
+ )
+ end.join
+ end
+
+ private def format(contents:, number:, empty:, highlight: false)
+ string = +""
+ string << if highlight
+ "> "
+ else
+ " "
+ end
+
+ string << number.rjust(@digit_count).to_s
+ if empty
+ string << contents
+ else
+ string << " "
+ string << TERMINAL_HIGHLIGHT if @terminal && highlight
+ string << contents
+ string << TERMINAL_END if @terminal
+ end
+ string
+ end
+ end
+end
diff --git a/lib/syntax_suggest/display_invalid_blocks.rb b/lib/syntax_suggest/display_invalid_blocks.rb
new file mode 100644
index 0000000000..5e79b3a262
--- /dev/null
+++ b/lib/syntax_suggest/display_invalid_blocks.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require_relative "capture_code_context"
+require_relative "display_code_with_line_numbers"
+
+module SyntaxSuggest
+ # Used for formatting invalid blocks
+ class DisplayInvalidBlocks
+ attr_reader :filename
+
+ def initialize(code_lines:, blocks:, io: $stderr, filename: nil, terminal: DEFAULT_VALUE)
+ @io = io
+ @blocks = Array(blocks)
+ @filename = filename
+ @code_lines = code_lines
+
+ @terminal = (terminal == DEFAULT_VALUE) ? io.isatty : terminal
+ end
+
+ def document_ok?
+ @blocks.none? { |b| !b.hidden? }
+ end
+
+ def call
+ if document_ok?
+ return self
+ end
+
+ if filename
+ @io.puts("--> #{filename}")
+ @io.puts
+ end
+ @blocks.each do |block|
+ display_block(block)
+ end
+
+ self
+ end
+
+ private def display_block(block)
+ # Build explanation
+ explain = ExplainSyntax.new(
+ code_lines: block.lines
+ ).call
+
+ # Enhance code output
+ # Also handles several ambiguious cases
+ lines = CaptureCodeContext.new(
+ blocks: block,
+ code_lines: @code_lines
+ ).call
+
+ # Build code output
+ document = DisplayCodeWithLineNumbers.new(
+ lines: lines,
+ terminal: @terminal,
+ highlight_lines: block.lines
+ ).call
+
+ # Output syntax error explanation
+ explain.errors.each do |e|
+ @io.puts e
+ end
+ @io.puts
+
+ # Output code
+ @io.puts(document)
+ end
+
+ private def code_with_context
+ lines = CaptureCodeContext.new(
+ blocks: @blocks,
+ code_lines: @code_lines
+ ).call
+
+ DisplayCodeWithLineNumbers.new(
+ lines: lines,
+ terminal: @terminal,
+ highlight_lines: @invalid_lines
+ ).call
+ end
+ end
+end
diff --git a/lib/syntax_suggest/explain_syntax.rb b/lib/syntax_suggest/explain_syntax.rb
new file mode 100644
index 0000000000..d7f5262ddb
--- /dev/null
+++ b/lib/syntax_suggest/explain_syntax.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require_relative "left_right_token_count"
+
+module SyntaxSuggest
+ class GetParseErrors
+ def self.errors(source)
+ Prism.parse(source).errors.map(&:message)
+ end
+ end
+
+ # Explains syntax errors based on their source
+ #
+ # example:
+ #
+ # source = "def foo; puts 'lol'" # Note missing end
+ # explain ExplainSyntax.new(
+ # code_lines: CodeLine.from_source(source)
+ # ).call
+ # explain.errors.first
+ # # => "Unmatched keyword, missing `end' ?"
+ #
+ # When the error cannot be determined by lexical counting
+ # then the parser is run against the input and the raw
+ # errors are returned.
+ #
+ # Example:
+ #
+ # source = "1 * " # Note missing a second number
+ # explain ExplainSyntax.new(
+ # code_lines: CodeLine.from_source(source)
+ # ).call
+ # explain.errors.first
+ # # => "syntax error, unexpected end-of-input"
+ class ExplainSyntax
+ INVERSE = {
+ "{" => "}",
+ "}" => "{",
+ "[" => "]",
+ "]" => "[",
+ "(" => ")",
+ ")" => "(",
+ "|" => "|"
+ }.freeze
+
+ def initialize(code_lines:)
+ @code_lines = code_lines
+ @left_right = LeftRightTokenCount.new
+ @missing = nil
+ end
+
+ def call
+ @code_lines.each do |line|
+ line.tokens.each do |token|
+ @left_right.count_token(token)
+ end
+ end
+
+ self
+ end
+
+ # Returns an array of missing elements
+ #
+ # For example this:
+ #
+ # ExplainSyntax.new(code_lines: lines).missing
+ # # => ["}"]
+ #
+ # Would indicate that the source is missing
+ # a `}` character in the source code
+ def missing
+ @missing ||= @left_right.missing
+ end
+
+ # Converts a missing string to
+ # an human understandable explanation.
+ #
+ # Example:
+ #
+ # explain.why("}")
+ # # => "Unmatched `{', missing `}' ?"
+ #
+ def why(miss)
+ case miss
+ when "keyword"
+ "Unmatched `end', missing keyword (`do', `def`, `if`, etc.) ?"
+ when "end"
+ "Unmatched keyword, missing `end' ?"
+ else
+ inverse = INVERSE.fetch(miss) {
+ raise "Unknown explain syntax char or key: #{miss.inspect}"
+ }
+ "Unmatched `#{inverse}', missing `#{miss}' ?"
+ end
+ end
+
+ # Returns an array of syntax error messages
+ #
+ # If no missing pairs are found it falls back
+ # on the original error messages
+ def errors
+ if missing.empty?
+ return GetParseErrors.errors(@code_lines.map(&:original).join).uniq
+ end
+
+ missing.map { |miss| why(miss) }
+ end
+ end
+end
diff --git a/lib/syntax_suggest/left_right_token_count.rb b/lib/syntax_suggest/left_right_token_count.rb
new file mode 100644
index 0000000000..e0562ba9cd
--- /dev/null
+++ b/lib/syntax_suggest/left_right_token_count.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Find mis-matched syntax based on lexical count
+ #
+ # Used for detecting missing pairs of elements
+ # each keyword needs an end, each '{' needs a '}'
+ # etc.
+ #
+ # Example:
+ #
+ # left_right = LeftRightTokenCount.new
+ # left_right.count_kw
+ # left_right.missing.first
+ # # => "end"
+ #
+ # left_right = LeftRightTokenCount.new
+ # source = "{ a: b, c: d" # Note missing '}'
+ # LexAll.new(source: source).each do |token|
+ # left_right.count_token(token)
+ # end
+ # left_right.missing.first
+ # # => "}"
+ class LeftRightTokenCount
+ def initialize
+ @kw_count = 0
+ @end_count = 0
+
+ @count_for_char = {
+ "{" => 0,
+ "}" => 0,
+ "[" => 0,
+ "]" => 0,
+ "(" => 0,
+ ")" => 0,
+ "|" => 0
+ }
+ end
+
+ def count_kw
+ @kw_count += 1
+ end
+
+ def count_end
+ @end_count += 1
+ end
+
+ # Count source code characters
+ #
+ # Example:
+ #
+ # token = CodeLine.from_source("{").first.tokens.first
+ # left_right = LeftRightTokenCount.new
+ # left_right.count_token(Token.new(token)
+ # left_right.count_for_char("{")
+ # # => 1
+ # left_right.count_for_char("}")
+ # # => 0
+ def count_token(token)
+ case token.type
+ when :STRING_CONTENT
+ # ^^^
+ # Means it's a string or a symbol `"{"` rather than being
+ # part of a data structure (like a hash) `{ a: b }`
+ # ignore it.
+ when :PERCENT_UPPER_W, :PERCENT_UPPER_I, :PERCENT_LOWER_W,
+ :PERCENT_LOWER_I, :REGEXP_BEGIN, :STRING_BEGIN
+ # ^^^
+ # Handle shorthand syntaxes like `%Q{ i am a string }`
+ #
+ # The start token will be the full thing `%Q{` but we
+ # need to count it as if it's a `{`. Any token
+ # can be used
+ char = token.value[-1]
+ @count_for_char[char] += 1 if @count_for_char.key?(char)
+ when :EMBEXPR_BEGIN
+ # ^^^
+ # Embedded string expressions like `"#{foo} <-embed"`
+ # are parsed with chars:
+ #
+ # `#{` as :EMBEXPR_BEGIN
+ # `}` as :EMBEXPR_END
+ #
+ # When we see `#{` count it as a `{` or we will
+ # have a mis-match count.
+ #
+ @count_for_char["{"] += 1
+ else
+ @end_count += 1 if token.is_end?
+ @kw_count += 1 if token.is_kw?
+ @count_for_char[token.value] += 1 if @count_for_char.key?(token.value)
+ end
+ end
+
+ def count_for_char(char)
+ @count_for_char[char]
+ end
+
+ # Returns an array of missing syntax characters
+ # or `"end"` or `"keyword"`
+ #
+ # left_right.missing
+ # # => ["}"]
+ def missing
+ out = missing_pairs
+ out << missing_pipe
+ out << missing_keyword_end
+ out.compact!
+ out
+ end
+
+ PAIRS = {
+ "{" => "}",
+ "[" => "]",
+ "(" => ")"
+ }.freeze
+
+ # Opening characters like `{` need closing characters # like `}`.
+ #
+ # When a mis-match count is detected, suggest the
+ # missing member.
+ #
+ # For example if there are 3 `}` and only two `{`
+ # return `"{"`
+ private def missing_pairs
+ PAIRS.map do |(left, right)|
+ case @count_for_char[left] <=> @count_for_char[right]
+ when 1
+ right
+ when 0
+ nil
+ when -1
+ left
+ end
+ end
+ end
+
+ # Keywords need ends and ends need keywords
+ #
+ # If we have more keywords, there's a missing `end`
+ # if we have more `end`-s, there's a missing keyword
+ private def missing_keyword_end
+ case @kw_count <=> @end_count
+ when 1
+ "end"
+ when 0
+ nil
+ when -1
+ "keyword"
+ end
+ end
+
+ # Pipes come in pairs.
+ # If there's an odd number of pipes then we
+ # are missing one
+ private def missing_pipe
+ if @count_for_char["|"].odd?
+ "|"
+ end
+ end
+ end
+end
diff --git a/lib/syntax_suggest/mini_stringio.rb b/lib/syntax_suggest/mini_stringio.rb
new file mode 100644
index 0000000000..1a82572eeb
--- /dev/null
+++ b/lib/syntax_suggest/mini_stringio.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Mini String IO [Private]
+ #
+ # Acts like a StringIO with reduced API, but without having to require that
+ # class.
+ #
+ # The original codebase emitted directly to $stderr, but now SyntaxError#detailed_message
+ # needs a string output. To accomplish that we kept the original print infrastructure in place and
+ # added this class to accumulate the print output into a string.
+ class MiniStringIO
+ EMPTY_ARG = Object.new
+
+ def initialize(isatty: $stderr.isatty)
+ @string = +""
+ @isatty = isatty
+ end
+
+ attr_reader :isatty
+ def puts(value = EMPTY_ARG, **)
+ if !value.equal?(EMPTY_ARG)
+ @string << value
+ end
+ @string << $/
+ end
+
+ attr_reader :string
+ end
+end
diff --git a/lib/syntax_suggest/parse_blocks_from_indent_line.rb b/lib/syntax_suggest/parse_blocks_from_indent_line.rb
new file mode 100644
index 0000000000..39dfca55d2
--- /dev/null
+++ b/lib/syntax_suggest/parse_blocks_from_indent_line.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # This class is responsible for generating initial code blocks
+ # that will then later be expanded.
+ #
+ # The biggest concern when guessing code blocks, is accidentally
+ # grabbing one that contains only an "end". In this example:
+ #
+ # def dog
+ # begonn # misspelled `begin`
+ # puts "bark"
+ # end
+ # end
+ #
+ # The following lines would be matched (from bottom to top):
+ #
+ # 1) end
+ #
+ # 2) puts "bark"
+ # end
+ #
+ # 3) begonn
+ # puts "bark"
+ # end
+ #
+ # At this point it has no where else to expand, and it will yield this inner
+ # code as a block
+ class ParseBlocksFromIndentLine
+ attr_reader :code_lines
+
+ def initialize(code_lines:)
+ @code_lines = code_lines
+ end
+
+ # Builds blocks from bottom up
+ def each_neighbor_block(target_line)
+ scan = AroundBlockScan.new(code_lines: code_lines, block: CodeBlock.new(lines: target_line))
+ .force_add_empty
+ .force_add_hidden
+ .scan_while { |line| line.indent >= target_line.indent }
+
+ neighbors = scan.code_block.lines
+
+ block = CodeBlock.new(lines: neighbors)
+ if neighbors.length <= 2 || block.valid?
+ yield block
+ else
+ until neighbors.empty?
+ lines = [neighbors.pop]
+ while (block = CodeBlock.new(lines: lines)) && block.invalid? && neighbors.any?
+ lines.prepend neighbors.pop
+ end
+
+ yield block if block
+ end
+ end
+ end
+ end
+end
diff --git a/lib/syntax_suggest/pathname_from_message.rb b/lib/syntax_suggest/pathname_from_message.rb
new file mode 100644
index 0000000000..ab90227427
--- /dev/null
+++ b/lib/syntax_suggest/pathname_from_message.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Converts a SyntaxError message to a path
+ #
+ # Handles the case where the filename has a colon in it
+ # such as on a windows file system: https://github.com/ruby/syntax_suggest/issues/111
+ #
+ # Example:
+ #
+ # message = "/tmp/scratch:2:in `require_relative': /private/tmp/bad.rb:1: syntax error, unexpected `end' (SyntaxError)"
+ # puts PathnameFromMessage.new(message).call.name
+ # # => "/tmp/scratch.rb"
+ #
+ class PathnameFromMessage
+ EVAL_RE = /^\(eval.*\):\d+/
+ STREAMING_RE = /^-:\d+/
+ attr_reader :name
+
+ def initialize(message, io: $stderr)
+ @line = message.lines.first
+ @parts = @line.split(":")
+ @guess = []
+ @name = nil
+ @io = io
+ end
+
+ def call
+ if skip_missing_file_name?
+ if ENV["SYNTAX_SUGGEST_DEBUG"]
+ @io.puts "SyntaxSuggest: Could not find filename from #{@line.inspect}"
+ end
+ else
+ until stop?
+ @guess << @parts.shift
+ @name = Pathname(@guess.join(":"))
+ end
+
+ if @parts.empty?
+ @io.puts "SyntaxSuggest: Could not find filename from #{@line.inspect}"
+ @name = nil
+ end
+ end
+
+ self
+ end
+
+ def stop?
+ return true if @parts.empty?
+ return false if @guess.empty?
+
+ @name&.exist?
+ end
+
+ def skip_missing_file_name?
+ @line.match?(EVAL_RE) || @line.match?(STREAMING_RE)
+ end
+ end
+end
diff --git a/lib/syntax_suggest/priority_engulf_queue.rb b/lib/syntax_suggest/priority_engulf_queue.rb
new file mode 100644
index 0000000000..2d1e9b1b63
--- /dev/null
+++ b/lib/syntax_suggest/priority_engulf_queue.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Keeps track of what elements are in the queue in
+ # priority and also ensures that when one element
+ # engulfs/covers/eats another that the larger element
+ # evicts the smaller element
+ class PriorityEngulfQueue
+ def initialize
+ @queue = PriorityQueue.new
+ end
+
+ def to_a
+ @queue.to_a
+ end
+
+ def empty?
+ @queue.empty?
+ end
+
+ def length
+ @queue.length
+ end
+
+ def peek
+ @queue.peek
+ end
+
+ def pop
+ @queue.pop
+ end
+
+ def push(block)
+ prune_engulf(block)
+ @queue << block
+ flush_deleted
+
+ self
+ end
+
+ private def flush_deleted
+ while @queue&.peek&.deleted?
+ @queue.pop
+ end
+ end
+
+ private def prune_engulf(block)
+ # If we're about to pop off the same block, we can skip deleting
+ # things from the frontier this iteration since we'll get it
+ # on the next iteration
+ return if @queue.peek && (block <=> @queue.peek) == 1
+
+ if block.starts_at != block.ends_at # A block of size 1 cannot engulf another
+ @queue.to_a.each { |b|
+ if b.starts_at >= block.starts_at && b.ends_at <= block.ends_at
+ b.delete
+ true
+ end
+ }
+ end
+ end
+ end
+end
diff --git a/lib/syntax_suggest/priority_queue.rb b/lib/syntax_suggest/priority_queue.rb
new file mode 100644
index 0000000000..1abda2a444
--- /dev/null
+++ b/lib/syntax_suggest/priority_queue.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Holds elements in a priority heap on insert
+ #
+ # Instead of constantly calling `sort!`, put
+ # the element where it belongs the first time
+ # around
+ #
+ # Example:
+ #
+ # queue = PriorityQueue.new
+ # queue << 33
+ # queue << 44
+ # queue << 1
+ #
+ # puts queue.peek # => 44
+ #
+ class PriorityQueue
+ attr_reader :elements
+
+ def initialize
+ @elements = []
+ end
+
+ def <<(element)
+ @elements << element
+ bubble_up(last_index, element)
+ end
+
+ def pop
+ exchange(0, last_index)
+ max = @elements.pop
+ bubble_down(0)
+ max
+ end
+
+ def length
+ @elements.length
+ end
+
+ def empty?
+ @elements.empty?
+ end
+
+ def peek
+ @elements.first
+ end
+
+ def to_a
+ @elements
+ end
+
+ # Used for testing, extremely not performant
+ def sorted
+ out = []
+ elements = @elements.dup
+ while (element = pop)
+ out << element
+ end
+ @elements = elements
+ out.reverse
+ end
+
+ private def last_index
+ @elements.size - 1
+ end
+
+ private def bubble_up(index, element)
+ return if index <= 0
+
+ parent_index = (index - 1) / 2
+ parent = @elements[parent_index]
+
+ return if (parent <=> element) >= 0
+
+ exchange(index, parent_index)
+ bubble_up(parent_index, element)
+ end
+
+ private def bubble_down(index)
+ child_index = (index * 2) + 1
+
+ return if child_index > last_index
+
+ not_the_last_element = child_index < last_index
+ left_element = @elements[child_index]
+ right_element = @elements[child_index + 1]
+
+ child_index += 1 if not_the_last_element && (right_element <=> left_element) == 1
+
+ return if (@elements[index] <=> @elements[child_index]) >= 0
+
+ exchange(index, child_index)
+ bubble_down(child_index)
+ end
+
+ def exchange(source, target)
+ a = @elements[source]
+ b = @elements[target]
+ @elements[source] = b
+ @elements[target] = a
+ end
+ end
+end
diff --git a/lib/syntax_suggest/scan_history.rb b/lib/syntax_suggest/scan_history.rb
new file mode 100644
index 0000000000..dc36e6ba2e
--- /dev/null
+++ b/lib/syntax_suggest/scan_history.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Scans up/down from the given block
+ #
+ # You can try out a change, stash it, or commit it to save for later
+ #
+ # Example:
+ #
+ # scanner = ScanHistory.new(code_lines: code_lines, block: block)
+ # scanner.scan(
+ # up: ->(_, _, _) { true },
+ # down: ->(_, _, _) { true }
+ # )
+ # scanner.changed? # => true
+ # expect(scanner.lines).to eq(code_lines)
+ #
+ # scanner.stash_changes
+ #
+ # expect(scanner.lines).to_not eq(code_lines)
+ class ScanHistory
+ attr_reader :before_index, :after_index
+
+ def initialize(code_lines:, block:)
+ @code_lines = code_lines
+ @history = [block]
+ refresh_index
+ end
+
+ def commit_if_changed
+ if changed?
+ @history << CodeBlock.new(lines: @code_lines[before_index..after_index])
+ end
+
+ self
+ end
+
+ # Discards any changes that have not been committed
+ def stash_changes
+ refresh_index
+ self
+ end
+
+ # Discard changes that have not been committed and revert the last commit
+ #
+ # Cannot revert the first commit
+ def revert_last_commit
+ if @history.length > 1
+ @history.pop
+ refresh_index
+ end
+
+ self
+ end
+
+ def changed?
+ @before_index != current.lines.first.index ||
+ @after_index != current.lines.last.index
+ end
+
+ # Iterates up and down
+ #
+ # Returns line, kw_count, end_count for each iteration
+ def scan(up:, down:)
+ kw_count = 0
+ end_count = 0
+
+ up_index = before_lines.reverse_each.take_while do |line|
+ kw_count += 1 if line.is_kw?
+ end_count += 1 if line.is_end?
+ up.call(line, kw_count, end_count)
+ end.last&.index
+
+ kw_count = 0
+ end_count = 0
+
+ down_index = after_lines.each.take_while do |line|
+ kw_count += 1 if line.is_kw?
+ end_count += 1 if line.is_end?
+ down.call(line, kw_count, end_count)
+ end.last&.index
+
+ @before_index = if up_index && up_index < @before_index
+ up_index
+ else
+ @before_index
+ end
+
+ @after_index = if down_index && down_index > @after_index
+ down_index
+ else
+ @after_index
+ end
+
+ self
+ end
+
+ def next_up
+ return nil if @before_index <= 0
+
+ @code_lines[@before_index - 1]
+ end
+
+ def next_down
+ return nil if @after_index >= @code_lines.length
+
+ @code_lines[@after_index + 1]
+ end
+
+ def lines
+ @code_lines[@before_index..@after_index]
+ end
+
+ private def before_lines
+ @code_lines[0...@before_index] || []
+ end
+
+ # Returns an array of all the CodeLines that exist after
+ # the currently scanned block
+ private def after_lines
+ @code_lines[@after_index.next..] || []
+ end
+
+ private def current
+ @history.last
+ end
+
+ private def refresh_index
+ @before_index = current.lines.first.index
+ @after_index = current.lines.last.index
+ self
+ end
+ end
+end
diff --git a/lib/syntax_suggest/syntax_suggest.gemspec b/lib/syntax_suggest/syntax_suggest.gemspec
new file mode 100644
index 0000000000..44e458aaad
--- /dev/null
+++ b/lib/syntax_suggest/syntax_suggest.gemspec
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+begin
+ require_relative "lib/syntax_suggest/version"
+rescue LoadError # Fallback to load version file in ruby core repository
+ require_relative "version"
+end
+
+Gem::Specification.new do |spec|
+ spec.name = "syntax_suggest"
+ spec.version = SyntaxSuggest::VERSION
+ spec.authors = ["schneems"]
+ spec.email = ["richard.schneeman+foo@gmail.com"]
+
+ spec.summary = "Find syntax errors in your source in a snap"
+ spec.description = 'When you get an "unexpected end" in your syntax this gem helps you find it'
+ spec.homepage = "https://github.com/ruby/syntax_suggest.git"
+ spec.license = "MIT"
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.3.0")
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = "https://github.com/ruby/syntax_suggest.git"
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|assets)/}) }
+ end
+ spec.bindir = "exe"
+ spec.executables = ["syntax_suggest"]
+ spec.require_paths = ["lib"]
+end
diff --git a/lib/syntax_suggest/token.rb b/lib/syntax_suggest/token.rb
new file mode 100644
index 0000000000..fc52639b1f
--- /dev/null
+++ b/lib/syntax_suggest/token.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Value object for accessing lex values
+ #
+ # This lex:
+ #
+ # [IDENTIFIER(1,0)-(1,8)("describe"), 32]
+ #
+ # Would translate into:
+ #
+ # lex.location # => (1,0)-(1,8)
+ # lex.type # => :IDENTIFIER
+ # lex.token # => "describe"
+ class Token
+ attr_reader :location, :type, :value
+
+ KW_TYPES = %i[
+ KEYWORD_IF KEYWORD_UNLESS KEYWORD_WHILE KEYWORD_UNTIL
+ KEYWORD_DEF KEYWORD_CASE KEYWORD_FOR KEYWORD_BEGIN KEYWORD_CLASS KEYWORD_MODULE KEYWORD_DO KEYWORD_DO_LOOP
+ ].to_set.freeze
+ private_constant :KW_TYPES
+
+ def initialize(prism_token, previous_prism_token, visitor)
+ @location = prism_token.location
+ @type = prism_token.type
+ @value = prism_token.value
+
+ # Prism lexes `:module` as SYMBOL_BEGIN, KEYWORD_MODULE
+ # https://github.com/ruby/prism/issues/3940
+ symbol_content = previous_prism_token&.type == :SYMBOL_BEGIN
+ @is_kw = KW_TYPES.include?(@type)
+ @is_kw = false if symbol_content || visitor.endless_def_keyword_offsets.include?(@location.start_offset)
+ @is_end = @type == :KEYWORD_END
+ end
+
+ def line
+ @location.start_line
+ end
+
+ def is_end?
+ @is_end
+ end
+
+ def is_kw?
+ @is_kw
+ end
+ end
+end
diff --git a/lib/syntax_suggest/unvisited_lines.rb b/lib/syntax_suggest/unvisited_lines.rb
new file mode 100644
index 0000000000..32808db634
--- /dev/null
+++ b/lib/syntax_suggest/unvisited_lines.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Tracks which lines various code blocks have expanded to
+ # and which are still unexplored
+ class UnvisitedLines
+ def initialize(code_lines:)
+ @unvisited = code_lines.sort_by(&:indent_index)
+ @visited_lines = {}
+ @visited_lines.compare_by_identity
+ end
+
+ def empty?
+ @unvisited.empty?
+ end
+
+ def peek
+ @unvisited.last
+ end
+
+ def pop
+ @unvisited.pop
+ end
+
+ def visit_block(block)
+ block.lines.each do |line|
+ next if @visited_lines[line]
+ @visited_lines[line] = true
+ end
+
+ while @visited_lines[@unvisited.last]
+ @unvisited.pop
+ end
+ end
+ end
+end
diff --git a/lib/syntax_suggest/version.rb b/lib/syntax_suggest/version.rb
new file mode 100644
index 0000000000..9114a079f6
--- /dev/null
+++ b/lib/syntax_suggest/version.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ VERSION = "3.0.0"
+end
diff --git a/lib/syntax_suggest/visitor.rb b/lib/syntax_suggest/visitor.rb
new file mode 100644
index 0000000000..6e25f7239c
--- /dev/null
+++ b/lib/syntax_suggest/visitor.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Walks the Prism AST to extract structural info that cannot be reliably determined from tokens
+ # alone.
+ #
+ # Such as the location of lines that must be logically joined so the search algorithm will
+ # treat them as one. Example:
+ #
+ # source = <<~RUBY
+ # User # 1
+ # .where(name: "Earlopain") # 2
+ # .first # 3
+ # RUBY
+ # ast, _tokens = Prism.parse_lex(source).value
+ # visitor = Visitor.new
+ # visitor.visit(ast)
+ # visitor.consecutive_lines # => Set[2, 1]
+ #
+ # This output means that line 1 and line 2 need to be joined with their next line.
+ #
+ # And determining the location of "endless" method definitions. For example:
+ #
+ # source = <<~RUBY
+ # def cube(x)
+ # x * x * x
+ # end
+ # def square(x) = x * x # 1
+ # RUBY
+ #
+ # ast, _tokens = Prism.parse_lex(source).value
+ # visitor = Visitor.new
+ # visitor.visit(ast)
+ # visitor.endless_def_keyword_offsets # => Set[28]
+ class Visitor < Prism::Visitor
+ attr_reader :endless_def_keyword_offsets, :consecutive_lines
+
+ def initialize
+ @endless_def_keyword_offsets = Set.new
+ @consecutive_lines = Set.new
+ end
+
+ # Called by Prism::Visitor for every method-call node in the AST
+ # (e.g. `foo.bar`, `foo.bar.baz`).
+ def visit_call_node(node)
+ receiver_loc = node.receiver&.location
+ call_operator_loc = node.call_operator_loc
+ message_loc = node.message_loc
+ if receiver_loc && call_operator_loc && message_loc
+ # dot-leading (dot on the next line)
+ # foo # line 1 - consecutive
+ # .bar # line 2
+ if receiver_loc.end_line != call_operator_loc.start_line && call_operator_loc.start_line == message_loc.start_line
+ (receiver_loc.end_line..call_operator_loc.start_line - 1).each do |line|
+ @consecutive_lines << line
+ end
+ end
+
+ # dot-trailing (dot on the same line as the receiver)
+ # foo. # line 1 - consecutive
+ # bar # line 2
+ if receiver_loc.end_line == call_operator_loc.start_line && call_operator_loc.start_line != message_loc.start_line
+ (call_operator_loc.start_line..message_loc.start_line - 1).each do |line|
+ @consecutive_lines << line
+ end
+ end
+ end
+ super
+ end
+
+ # Called by Prism::Visitor for every `def` node in the AST.
+ # Records the keyword start location for endless method definitions
+ # like `def foo = 123`. These are valid without a matching `end`,
+ # so Token must exclude them when deciding if a line is a keyword.
+ def visit_def_node(node)
+ @endless_def_keyword_offsets << node.def_keyword_loc.start_offset if node.equal_loc
+ super
+ end
+ end
+end
diff --git a/lib/tempfile.gemspec b/lib/tempfile.gemspec
new file mode 100644
index 0000000000..0b362b0a86
--- /dev/null
+++ b/lib/tempfile.gemspec
@@ -0,0 +1,33 @@
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1).join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Yukihiro Matsumoto"]
+ spec.email = ["matz@ruby-lang.org"]
+
+ spec.summary = %q{A utility class for managing temporary files.}
+ spec.description = %q{A utility class for managing temporary files.}
+ spec.homepage = "https://github.com/ruby/tempfile"
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ gemspec = File.basename(__FILE__)
+ spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL, exception: false) do |ls|
+ ls.readlines("\x0", chomp: true).reject do |f|
+ (f == gemspec) ||
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git Gemfile])
+ end
+ end
+ spec.require_paths = ["lib"]
+end
diff --git a/lib/tempfile.rb b/lib/tempfile.rb
index 601bb8d2f8..cd512bb1c5 100644
--- a/lib/tempfile.rb
+++ b/lib/tempfile.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
#
# tempfile - manipulates temporary files
#
@@ -6,210 +7,636 @@
require 'delegate'
require 'tmpdir'
-require 'thread'
-# A class for managing temporary files. This library is written to be
-# thread safe.
+# A utility class for managing temporary files.
+#
+# There are two kind of methods of creating a temporary file:
+#
+# - Tempfile.create (recommended)
+# - Tempfile.new and Tempfile.open (mostly for backward compatibility, not recommended)
+#
+# Tempfile.create creates a usual \File object.
+# The timing of file deletion is predictable.
+# Also, it supports open-and-unlink technique which
+# removes the temporary file immediately after creation.
+#
+# Tempfile.new and Tempfile.open creates a \Tempfile object.
+# The created file is removed by the GC (finalizer).
+# The timing of file deletion is not predictable.
+#
+# == Synopsis
+#
+# require 'tempfile'
+#
+# # Tempfile.create with a block
+# # The filename are chosen automatically.
+# # (You can specify the prefix and suffix of the filename by an optional argument.)
+# Tempfile.create {|f|
+# f.puts "foo"
+# f.rewind
+# f.read # => "foo\n"
+# } # The file is removed at block exit.
+#
+# # Tempfile.create without a block
+# # You need to unlink the file in non-block form.
+# f = Tempfile.create
+# f.puts "foo"
+# f.close
+# File.unlink(f.path) # You need to unlink the file.
+#
+# # Tempfile.create(anonymous: true) without a block
+# f = Tempfile.create(anonymous: true)
+# # The file is already removed because anonymous.
+# f.path # => "/tmp/" (no filename since no file)
+# f.puts "foo"
+# f.rewind
+# f.read # => "foo\n"
+# f.close
+#
+# # Tempfile.create(anonymous: true) with a block
+# Tempfile.create(anonymous: true) {|f|
+# # The file is already removed because anonymous.
+# f.path # => "/tmp/" (no filename since no file)
+# f.puts "foo"
+# f.rewind
+# f.read # => "foo\n"
+# }
+#
+# # Not recommended: Tempfile.new without a block
+# file = Tempfile.new('foo')
+# file.path # => A unique filename in the OS's temp directory,
+# # e.g.: "/tmp/foo.24722.0"
+# # This filename contains 'foo' in its basename.
+# file.write("hello world")
+# file.rewind
+# file.read # => "hello world"
+# file.close
+# file.unlink # deletes the temp file
+#
+# == About Tempfile.new and Tempfile.open
+#
+# This section does not apply to Tempfile.create because
+# it returns a File object (not a Tempfile object).
+#
+# When you create a Tempfile object,
+# it will create a temporary file with a unique filename. A Tempfile
+# objects behaves just like a File object, and you can perform all the usual
+# file operations on it: reading data, writing data, changing its permissions,
+# etc. So although this class does not explicitly document all instance methods
+# supported by File, you can in fact call any File instance method on a
+# Tempfile object.
+#
+# A Tempfile object has a finalizer to remove the temporary file.
+# This means that the temporary file is removed via GC.
+# This can cause several problems:
+#
+# - Long GC intervals and conservative GC can accumulate temporary files that are not removed.
+# - Temporary files are not removed if Ruby exits abnormally (such as SIGKILL, SEGV).
+#
+# There are legacy good practices for Tempfile.new and Tempfile.open as follows.
+#
+# === Explicit close
+#
+# When a Tempfile object is garbage collected, or when the Ruby interpreter
+# exits, its associated temporary file is automatically deleted. This means
+# that it's unnecessary to explicitly delete a Tempfile after use, though
+# it's a good practice to do so: not explicitly deleting unused Tempfiles can
+# potentially leave behind a large number of temp files on the filesystem
+# until they're garbage collected. The existence of these temp files can make
+# it harder to determine a new Tempfile filename.
+#
+# Therefore, one should always call #unlink or close in an ensure block, like
+# this:
+#
+# file = Tempfile.new('foo')
+# begin
+# # ...do something with file...
+# ensure
+# file.close
+# file.unlink # deletes the temp file
+# end
+#
+# Tempfile.create { ... } exists for this purpose and is more convenient to use.
+# Note that Tempfile.create returns a File instance instead of a Tempfile, which
+# also avoids the overhead and complications of delegation.
+#
+# Tempfile.create('foo') do |file|
+# # ...do something with file...
+# end
+#
+# === Unlink after creation
+#
+# On POSIX systems, it's possible to unlink a file right after creating it,
+# and before closing it. This removes the filesystem entry without closing
+# the file handle, so it ensures that only the processes that already had
+# the file handle open can access the file's contents. It's strongly
+# recommended that you do this if you do not want any other processes to
+# be able to read from or write to the Tempfile, and you do not need to
+# know the Tempfile's filename either.
+#
+# Also, this guarantees the temporary file is removed even if Ruby exits abnormally.
+# The OS reclaims the storage for the temporary file when the file is closed or
+# the Ruby process exits (normally or abnormally).
+#
+# For example, a practical use case for unlink-after-creation would be this:
+# you need a large byte buffer that's too large to comfortably fit in RAM,
+# e.g. when you're writing a web server and you want to buffer the client's
+# file upload data.
+#
+# `Tempfile.create(anonymous: true)` supports this behavior.
+# It also works on Windows.
+#
+# == Minor notes
+#
+# Tempfile's filename picking method is both thread-safe and inter-process-safe:
+# it guarantees that no other threads or processes will pick the same filename.
+#
+# Tempfile itself however may not be entirely thread-safe. If you access the
+# same Tempfile object from multiple threads then you should protect it with a
+# mutex.
class Tempfile < DelegateClass(File)
- MAX_TRY = 10
- @@cleanlist = []
- @@lock = Mutex.new
-
- # Creates a temporary file of mode 0600 in the temporary directory,
- # opens it with mode "w+", and returns a Tempfile object which
- # represents the created temporary file. A Tempfile object can be
- # treated just like a normal File object.
- #
- # The basename parameter is used to determine the name of a
- # temporary file. If an Array is given, the first element is used
- # as prefix string and the second as suffix string, respectively.
- # Otherwise it is treated as prefix string.
- #
- # If tmpdir is omitted, the temporary directory is determined by
- # Dir::tmpdir provided by 'tmpdir.rb'.
- # When $SAFE > 0 and the given tmpdir is tainted, it uses
- # /tmp. (Note that ENV values are tainted by default)
- def initialize(basename, *rest)
- # I wish keyword argument settled soon.
- if opts = Hash.try_convert(rest[-1])
- rest.pop
- end
- tmpdir = rest[0] || Dir::tmpdir
- if $SAFE > 0 and tmpdir.tainted?
- tmpdir = '/tmp'
- end
- lock = tmpname = nil
- n = failure = 0
- @@lock.synchronize {
- begin
- begin
- tmpname = File.join(tmpdir, make_tmpname(basename, n))
- lock = tmpname + '.lock'
- n += 1
- end while @@cleanlist.include?(tmpname) or
- File.exist?(lock) or File.exist?(tmpname)
- Dir.mkdir(lock)
- rescue
- failure += 1
- retry if failure < MAX_TRY
- raise "cannot generate tempfile `%s'" % tmpname
- end
- }
+ # The version
+ VERSION = "0.3.1"
- @data = [tmpname]
- @clean_proc = Tempfile.callback(@data)
- ObjectSpace.define_finalizer(self, @clean_proc)
+ # Creates a file in the underlying file system;
+ # returns a new \Tempfile object based on that file.
+ #
+ # If possible, consider instead using Tempfile.create, which:
+ #
+ # - Avoids the performance cost of delegation,
+ # incurred when Tempfile.new calls its superclass <tt>DelegateClass(File)</tt>.
+ # - Does not rely on a finalizer to close and unlink the file,
+ # which can be unreliable.
+ #
+ # Creates and returns file whose:
+ #
+ # - Class is \Tempfile (not \File, as in Tempfile.create).
+ # - Directory is the system temporary directory (system-dependent).
+ # - Generated filename is unique in that directory.
+ # - Permissions are <tt>0600</tt>;
+ # see {File Permissions}[rdoc-ref:File@File+Permissions].
+ # - Mode is <tt>'w+'</tt> (read/write mode, positioned at the end).
+ #
+ # The underlying file is removed when the \Tempfile object dies
+ # and is reclaimed by the garbage collector.
+ #
+ # Example:
+ #
+ # f = Tempfile.new # => #<Tempfile:/tmp/20220505-17839-1s0kt30>
+ # f.class # => Tempfile
+ # f.path # => "/tmp/20220505-17839-1s0kt30"
+ # f.stat.mode.to_s(8) # => "100600"
+ # File.exist?(f.path) # => true
+ # File.unlink(f.path) #
+ # File.exist?(f.path) # => false
+ #
+ # Argument +basename+, if given, may be one of:
+ #
+ # - A string: the generated filename begins with +basename+:
+ #
+ # Tempfile.new('foo') # => #<Tempfile:/tmp/foo20220505-17839-1whk2f>
+ #
+ # - An array of two strings <tt>[prefix, suffix]</tt>:
+ # the generated filename begins with +prefix+ and ends with +suffix+:
+ #
+ # Tempfile.new(%w/foo .jpg/) # => #<Tempfile:/tmp/foo20220505-17839-58xtfi.jpg>
+ #
+ # With arguments +basename+ and +tmpdir+, the file is created in directory +tmpdir+:
+ #
+ # Tempfile.new('foo', '.') # => #<Tempfile:./foo20220505-17839-xfstr8>
+ #
+ # Keyword arguments +mode+ and +options+ are passed directly to method
+ # {File.open}[rdoc-ref:File.open]:
+ #
+ # - The value given with +mode+ must be an integer,
+ # and may be expressed as the logical OR of constants defined in
+ # {File::Constants}[rdoc-ref:File::Constants].
+ # - For +options+, see {Open Options}[rdoc-ref:IO@Open+Options].
+ #
+ # Related: Tempfile.create.
+ #
+ def initialize(basename="", tmpdir=nil, mode: 0, **options)
+ warn "Tempfile.new doesn't call the given block.", uplevel: 1 if block_given?
- if opts.nil?
- opts = []
- else
- opts = [opts]
+ @unlinked = false
+ @mode = mode|File::RDWR|File::CREAT|File::EXCL
+ tmpfile = nil
+ ::Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
+ opts[:perm] = 0600
+ tmpfile = File.open(tmpname, @mode, **opts)
+ @opts = opts.freeze
end
- @tmpfile = File.open(tmpname, File::RDWR|File::CREAT|File::EXCL, 0600, *opts)
- @tmpname = tmpname
- @@cleanlist << @tmpname
- @data[1] = @tmpfile
- @data[2] = @@cleanlist
- super(@tmpfile)
+ super(tmpfile)
- # Now we have all the File/IO methods defined, you must not
- # carelessly put bare puts(), etc. after this.
+ @finalizer_manager = FinalizerManager.new(__getobj__.path)
+ @finalizer_manager.register(self, __getobj__)
+ end
- Dir.rmdir(lock)
+ def initialize_dup(other) # :nodoc:
+ initialize_copy_iv(other)
+ super(other)
+ @finalizer_manager.register(self, __getobj__)
end
- def make_tmpname(basename, n)
- case basename
- when Array
- prefix, suffix = *basename
- else
- prefix, suffix = basename, ''
- end
-
- t = Time.now.strftime("%Y%m%d")
- path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}-#{n}#{suffix}"
+ def initialize_clone(other) # :nodoc:
+ initialize_copy_iv(other)
+ super(other)
+ @finalizer_manager.register(self, __getobj__)
+ end
+
+ private def initialize_copy_iv(other) # :nodoc:
+ @unlinked = other.unlinked
+ @mode = other.mode
+ @opts = other.opts
+ @finalizer_manager = other.finalizer_manager
end
- private :make_tmpname
# Opens or reopens the file with mode "r+".
def open
- @tmpfile.close if @tmpfile
- @tmpfile = File.open(@tmpname, 'r+')
- @data[1] = @tmpfile
- __setobj__(@tmpfile)
+ _close
+
+ mode = @mode & ~(File::CREAT|File::EXCL)
+ __setobj__(File.open(__getobj__.path, mode, **@opts))
+
+ @finalizer_manager.register(self, __getobj__)
+
+ __getobj__
end
- def _close # :nodoc:
- @tmpfile.close if @tmpfile
- @tmpfile = nil
- @data[1] = nil if @data
+ def _close # :nodoc:
+ __getobj__.close
end
protected :_close
- #Closes the file. If the optional flag is true, unlinks the file
- # after closing.
+ # Closes the file. If +unlink_now+ is true, then the file will be unlinked
+ # (deleted) after closing. Of course, you can choose to later call #unlink
+ # if you do not unlink it now.
#
# If you don't explicitly unlink the temporary file, the removal
# will be delayed until the object is finalized.
def close(unlink_now=false)
- if unlink_now
- close!
- else
- _close
- end
+ _close
+ unlink if unlink_now
end
- # Closes and unlinks the file.
+ # Closes and unlinks (deletes) the file. Has the same effect as called
+ # <tt>close(true)</tt>.
def close!
- _close
- @clean_proc.call
- ObjectSpace.undefine_finalizer(self)
- @data = @tmpname = nil
+ close(true)
end
- # Unlinks the file. On UNIX-like systems, it is often a good idea
- # to unlink a temporary file immediately after creating and opening
- # it, because it leaves other programs zero chance to access the
- # file.
+ # Unlinks (deletes) the file from the filesystem. One should always unlink
+ # the file after using it, as is explained in the "Explicit close" good
+ # practice section in the Tempfile overview:
+ #
+ # file = Tempfile.new('foo')
+ # begin
+ # # ...do something with file...
+ # ensure
+ # file.close
+ # file.unlink # deletes the temp file
+ # end
+ #
+ # === Unlink-before-close
+ #
+ # On POSIX systems it's possible to unlink a file before closing it. This
+ # practice is explained in detail in the Tempfile overview (section
+ # "Unlink after creation"); please refer there for more information.
+ #
+ # However, unlink-before-close may not be supported on non-POSIX operating
+ # systems. Microsoft Windows is the most notable case: unlinking a non-closed
+ # file will result in an error, which this method will silently ignore. If
+ # you want to practice unlink-before-close whenever possible, then you should
+ # write code like this:
+ #
+ # file = Tempfile.new('foo')
+ # file.unlink # On Windows this silently fails.
+ # begin
+ # # ... do something with file ...
+ # ensure
+ # file.close! # Closes the file handle. If the file wasn't unlinked
+ # # because #unlink failed, then this method will attempt
+ # # to do so again.
+ # end
def unlink
- # keep this order for thread safeness
+ return if @unlinked
begin
- File.unlink(@tmpname) if File.exist?(@tmpname)
- @@cleanlist.delete(@tmpname)
- @data = @tmpname = nil
- ObjectSpace.undefine_finalizer(self)
+ File.unlink(__getobj__.path)
+ rescue Errno::ENOENT
rescue Errno::EACCES
# may not be able to unlink on Windows; just ignore
+ return
end
+
+ @finalizer_manager.unlinked = true
+
+ @unlinked = true
end
alias delete unlink
# Returns the full path name of the temporary file.
+ # This will be nil if #unlink has been called.
def path
- @tmpname
+ @unlinked ? nil : __getobj__.path
end
# Returns the size of the temporary file. As a side effect, the IO
# buffer is flushed before determining the size.
def size
- if @tmpfile
- @tmpfile.flush
- @tmpfile.stat.size
+ if !__getobj__.closed?
+ __getobj__.size # File#size calls rb_io_flush_raw()
else
- 0
+ File.size(__getobj__.path)
end
end
alias length size
- class << self
- def callback(data) # :nodoc:
- pid = $$
- Proc.new {
- if pid == $$
- path, tmpfile, cleanlist = *data
+ # :stopdoc:
+ def inspect
+ if __getobj__.closed?
+ "#<#{self.class}:#{path} (closed)>"
+ else
+ "#<#{self.class}:#{path}>"
+ end
+ end
+ alias to_s inspect
- print "removing ", path, "..." if $DEBUG
+ protected
- tmpfile.close if tmpfile
+ attr_reader :unlinked, :mode, :opts, :finalizer_manager
- # keep this order for thread safeness
- File.unlink(path) if File.exist?(path)
- cleanlist.delete(path) if cleanlist
+ class FinalizerManager # :nodoc:
+ attr_accessor :unlinked
+
+ def initialize(path)
+ @open_files = {}
+ @path = path
+ @pid = Process.pid
+ @unlinked = false
+ end
- print "done\n" if $DEBUG
- end
- }
+ def register(obj, file)
+ ObjectSpace.undefine_finalizer(obj)
+ ObjectSpace.define_finalizer(obj, self)
+ @open_files[obj.object_id] = file
end
- # If no block is given, this is a synonym for new().
+ def call(object_id)
+ @open_files.delete(object_id).close
+
+ if @open_files.empty? && !@unlinked && Process.pid == @pid
+ $stderr.puts "removing #{@path}..." if $DEBUG
+ begin
+ File.unlink(@path)
+ rescue Errno::ENOENT
+ end
+ $stderr.puts "done" if $DEBUG
+ end
+ end
+ end
+
+ class << self
+ # :startdoc:
+
+ # Creates a new Tempfile.
+ #
+ # This method is not recommended and exists mostly for backward compatibility.
+ # Please use Tempfile.create instead, which avoids the cost of delegation,
+ # does not rely on a finalizer, and also unlinks the file when given a block.
+ #
+ # Tempfile.open is still appropriate if you need the Tempfile to be unlinked
+ # by a finalizer and you cannot explicitly know where in the program the
+ # Tempfile can be unlinked safely.
+ #
+ # If no block is given, this is a synonym for Tempfile.new.
+ #
+ # If a block is given, then a Tempfile object will be constructed,
+ # and the block is run with the Tempfile object as argument. The Tempfile
+ # object will be automatically closed after the block terminates.
+ # However, the file will *not* be unlinked and needs to be manually unlinked
+ # with Tempfile#close! or Tempfile#unlink. The finalizer will try to unlink
+ # but should not be relied upon as it can keep the file on the disk much
+ # longer than intended. For instance, on CRuby, finalizers can be delayed
+ # due to conservative stack scanning and references left in unused memory.
+ #
+ # The call returns the value of the block.
+ #
+ # In any case, all arguments (<code>*args</code>) will be passed to Tempfile.new.
#
- # If a block is given, it will be passed tempfile as an argument,
- # and the tempfile will automatically be closed when the block
- # terminates. In this case, open() returns nil.
- def open(*args)
- tempfile = new(*args)
+ # Tempfile.open('foo', '/home/temp') do |f|
+ # # ... do something with f ...
+ # end
+ #
+ # # Equivalent:
+ # f = Tempfile.open('foo', '/home/temp')
+ # begin
+ # # ... do something with f ...
+ # ensure
+ # f.close
+ # end
+ def open(*args, **kw)
+ tempfile = new(*args, **kw)
if block_given?
- begin
- yield(tempfile)
- ensure
- tempfile.close
- end
+ begin
+ yield(tempfile)
+ ensure
+ tempfile.close
+ end
else
- tempfile
+ tempfile
end
end
end
end
-if __FILE__ == $0
-# $DEBUG = true
- f = Tempfile.new("foo")
- f.print("foo\n")
- f.close
- f.open
- p f.gets # => "foo\n"
- f.close!
+# Creates a file in the underlying file system;
+# returns a new \File object based on that file.
+#
+# With no block given and no arguments, creates and returns file whose:
+#
+# - Class is {File}[rdoc-ref:File] (not \Tempfile).
+# - Directory is the system temporary directory (system-dependent).
+# - Generated filename is unique in that directory.
+# - Permissions are <tt>0600</tt>;
+# see {File Permissions}[rdoc-ref:File@File+Permissions].
+# - Mode is <tt>'w+'</tt> (read/write mode, positioned at the end).
+#
+# The temporary file removal depends on the keyword argument +anonymous+ and
+# whether a block is given or not.
+# See the description about the +anonymous+ keyword argument later.
+#
+# Example:
+#
+# f = Tempfile.create # => #<File:/tmp/20220505-9795-17ky6f6>
+# f.class # => File
+# f.path # => "/tmp/20220505-9795-17ky6f6"
+# f.stat.mode.to_s(8) # => "100600"
+# f.close
+# File.exist?(f.path) # => true
+# File.unlink(f.path)
+# File.exist?(f.path) # => false
+#
+# Tempfile.create {|f|
+# f.puts "foo"
+# f.rewind
+# f.read # => "foo\n"
+# f.path # => "/tmp/20240524-380207-oma0ny"
+# File.exist?(f.path) # => true
+# } # The file is removed at block exit.
+#
+# f = Tempfile.create(anonymous: true)
+# # The file is already removed because anonymous
+# f.path # => "/tmp/" (no filename since no file)
+# f.puts "foo"
+# f.rewind
+# f.read # => "foo\n"
+# f.close
+#
+# Tempfile.create(anonymous: true) {|f|
+# # The file is already removed because anonymous
+# f.path # => "/tmp/" (no filename since no file)
+# f.puts "foo"
+# f.rewind
+# f.read # => "foo\n"
+# }
+#
+# The argument +basename+, if given, may be one of the following:
+#
+# - A string: the generated filename begins with +basename+:
+#
+# Tempfile.create('foo') # => #<File:/tmp/foo20220505-9795-1gok8l9>
+#
+# - An array of two strings <tt>[prefix, suffix]</tt>:
+# the generated filename begins with +prefix+ and ends with +suffix+:
+#
+# Tempfile.create(%w/foo .jpg/) # => #<File:/tmp/foo20220505-17839-tnjchh.jpg>
+#
+# With arguments +basename+ and +tmpdir+, the file is created in the directory +tmpdir+:
+#
+# Tempfile.create('foo', '.') # => #<File:./foo20220505-9795-1emu6g8>
+#
+# Keyword arguments +mode+ and +options+ are passed directly to the method
+# {File.open}[rdoc-ref:File.open]:
+#
+# - The value given for +mode+ must be an integer
+# and may be expressed as the logical OR of constants defined in
+# {File::Constants}[rdoc-ref:File::Constants].
+# - For +options+, see {Open Options}[rdoc-ref:IO@Open+Options].
+#
+# The keyword argument +anonymous+ specifies when the file is removed.
+#
+# - <tt>anonymous=false</tt> (default) without a block: the file is not removed.
+# - <tt>anonymous=false</tt> (default) with a block: the file is removed after the block exits.
+# - <tt>anonymous=true</tt> without a block: the file is removed before returning.
+# - <tt>anonymous=true</tt> with a block: the file is removed before the block is called.
+#
+# In the first case (<tt>anonymous=false</tt> without a block),
+# the file is not removed automatically.
+# It should be explicitly closed.
+# It can be used to rename to the desired filename.
+# If the file is not needed, it should be explicitly removed.
+#
+# The File#path method of the created file object returns the temporary directory with a trailing slash
+# when +anonymous+ is true.
+#
+# When a block is given, it creates the file as described above, passes it to the block,
+# and returns the block's value.
+# Before the returning, the file object is closed and the underlying file is removed:
+#
+# Tempfile.create {|file| file.path } # => "/tmp/20220505-9795-rkists"
+#
+# Implementation note:
+#
+# The keyword argument <tt>anonymous=true</tt> is implemented using +FILE_SHARE_DELETE+ on Windows.
+# +O_TMPFILE+ is used on Linux.
+#
+# Related: Tempfile.new.
+#
+def Tempfile.create(basename="", tmpdir=nil, mode: 0, anonymous: false, **options, &block)
+ if anonymous
+ create_anonymous(basename, tmpdir, mode: mode, **options, &block)
+ else
+ create_with_filename(basename, tmpdir, mode: mode, **options, &block)
+ end
+end
+
+class << Tempfile
+# :stopdoc:
+
+private def create_with_filename(basename="", tmpdir=nil, mode: 0, **options)
+ tmpfile = nil
+ Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
+ mode |= File::RDWR|File::CREAT|File::EXCL
+ opts[:perm] = 0600
+ tmpfile = File.open(tmpname, mode, **opts)
+ end
+ if block_given?
+ begin
+ yield tmpfile
+ ensure
+ unless tmpfile.closed?
+ if File.identical?(tmpfile, tmpfile.path)
+ unlinked = File.unlink tmpfile.path rescue nil
+ end
+ tmpfile.close
+ end
+ unless unlinked
+ begin
+ File.unlink tmpfile.path
+ rescue Errno::ENOENT
+ end
+ end
+ end
+ else
+ tmpfile
+ end
+end
+
+if RUBY_VERSION < "3.2"
+ module PathAttr # :nodoc:
+ attr_reader :path
+
+ def self.set_path(file, path)
+ file.extend(self).instance_variable_set(:@path, path)
+ end
+ end
+end
+
+private def create_anonymous(basename="", tmpdir=nil, mode: 0, **options, &block)
+ tmpfile = nil
+ tmpdir = Dir.tmpdir() if tmpdir.nil?
+ if defined?(File::TMPFILE) # O_TMPFILE since Linux 3.11
+ begin
+ tmpfile = File.open(tmpdir, File::RDWR | File::TMPFILE, 0600)
+ rescue Errno::EISDIR, Errno::ENOENT, Errno::EOPNOTSUPP
+ # kernel or the filesystem does not support O_TMPFILE
+ # fallback to create-and-unlink
+ end
+ end
+ if tmpfile.nil?
+ mode |= File::SHARE_DELETE | File::BINARY # Windows needs them to unlink the opened file.
+ tmpfile = create_with_filename(basename, tmpdir, mode: mode, **options)
+ File.unlink(tmpfile.path)
+ tmppath = tmpfile.path
+ end
+ path = File.join(tmpdir, '')
+ unless tmppath == path
+ # clear path.
+ tmpfile.autoclose = false
+ tmpfile = File.new(tmpfile.fileno, mode: File::RDWR, path: path)
+ PathAttr.set_path(tmpfile, path) if defined?(PathAttr)
+ end
+ if block
+ begin
+ yield tmpfile
+ ensure
+ tmpfile.close
+ end
+ else
+ tmpfile
+ end
+end
end
diff --git a/lib/test/unit.rb b/lib/test/unit.rb
deleted file mode 100644
index ec248c392e..0000000000
--- a/lib/test/unit.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# test/unit compatibility layer using minitest.
-
-require 'minitest/unit'
-require 'test/unit/assertions'
-require 'test/unit/testcase'
-
-module Test
- module Unit
- TEST_UNIT_IMPLEMENTATION = 'test/unit compatibility layer using minitest'
-
- def self.setup_argv(original_argv=ARGV)
- minitest_argv = []
- files = []
- reject = []
- original_argv = original_argv.dup
- while arg = original_argv.shift
- case arg
- when '-v'
- minitest_argv << '-v'
- when '-n', '--name'
- minitest_argv << arg
- minitest_argv << original_argv.shift
- when '-x'
- reject << original_argv.shift
- else
- files << arg
- end
- end
-
- if block_given?
- files = yield files
- end
-
- files.map! {|f|
- f = f.gsub(Regexp.compile(Regexp.quote(File::ALT_SEPARATOR)), File::SEPARATOR) if File::ALT_SEPARATOR
- if File.directory? f
- Dir["#{f}/**/test_*.rb"]
- elsif File.file? f
- f
- else
- raise ArgumentError, "file not found: #{f}"
- end
- }
- files.flatten!
-
- reject_pat = Regexp.union(reject.map {|r| /#{r}/ })
- files.reject! {|f| reject_pat =~ f }
-
- files.each {|f|
- d = File.dirname(File.expand_path(f))
- unless $:.include? d
- $: << d
- end
- begin
- require f
- rescue LoadError
- puts "#{f}: #{$!}"
- end
- }
-
- ARGV.replace minitest_argv
- end
- end
-end
-
-MiniTest::Unit.autorun
diff --git a/lib/test/unit/assertions.rb b/lib/test/unit/assertions.rb
deleted file mode 100644
index ac3ecf93c7..0000000000
--- a/lib/test/unit/assertions.rb
+++ /dev/null
@@ -1,122 +0,0 @@
-require 'minitest/unit'
-require 'pp'
-
-module Test
- module Unit
- module Assertions
- include MiniTest::Assertions
-
- def mu_pp(obj)
- obj.pretty_inspect.chomp
- end
-
- def assert_raise(*args, &b)
- assert_raises(*args, &b)
- end
-
- def assert_nothing_raised(*args)
- self._assertions += 1
- if Module === args.last
- msg = nil
- else
- msg = args.pop
- end
- begin
- line = __LINE__; yield
- rescue Exception => e
- bt = e.backtrace
- as = e.instance_of?(MiniTest::Assertion)
- if as
- ans = /\A#{Regexp.quote(__FILE__)}:#{line}:in /o
- bt.reject! {|line| ans =~ line}
- end
- if ((args.empty? && !as) ||
- args.any? {|a| a.instance_of?(Module) ? e.is_a?(a) : e.class == a })
- msg = message(msg) { "Exception raised:\n<#{mu_pp(e)}>" }
- raise MiniTest::Assertion, msg.call, bt
- else
- raise
- end
- end
- nil
- end
-
- def assert_nothing_thrown(msg=nil)
- begin
- 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")
- end
-
- 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)
- exp_comment = " (nsec=#{exp.nsec})"
- act_comment = " (nsec=#{act.nsec})"
- 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
-
- def assert_not_nil(exp, msg=nil)
- msg = message(msg) { "<#{mu_pp(exp)}> expected to not be nil" }
- assert(!exp.nil?, msg)
- end
-
- 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
-
- 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
-
- 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
-
- def build_message(head, template=nil, *arguments)
- template &&= template.chomp
- template.gsub(/\?/) { mu_pp(arguments.shift) }
- end
- end
- end
-end
diff --git a/lib/test/unit/testcase.rb b/lib/test/unit/testcase.rb
deleted file mode 100644
index 89aa0f34c0..0000000000
--- a/lib/test/unit/testcase.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'test/unit/assertions'
-
-module Test
- module Unit
- class TestCase < MiniTest::Unit::TestCase
- include Assertions
- def self.test_order
- :sorted
- end
- end
- end
-end
diff --git a/lib/thread.rb b/lib/thread.rb
deleted file mode 100644
index 68eeaf5b05..0000000000
--- a/lib/thread.rb
+++ /dev/null
@@ -1,367 +0,0 @@
-#
-# thread.rb - thread support classes
-# by Yukihiro Matsumoto <matz@netlab.co.jp>
-#
-# Copyright (C) 2001 Yukihiro Matsumoto
-# Copyright (C) 2000 Network Applied Communication Laboratory, Inc.
-# Copyright (C) 2000 Information-technology Promotion Agency, Japan
-#
-
-unless defined? Thread
- raise "Thread not available for this ruby interpreter"
-end
-
-unless defined? ThreadError
- class ThreadError < StandardError
- end
-end
-
-if $DEBUG
- Thread.abort_on_exception = true
-end
-
-#
-# ConditionVariable objects augment class Mutex. Using condition variables,
-# it is possible to suspend while in the middle of a critical section until a
-# resource becomes available.
-#
-# Example:
-#
-# require 'thread'
-#
-# mutex = Mutex.new
-# resource = ConditionVariable.new
-#
-# a = Thread.new {
-# mutex.synchronize {
-# # Thread 'a' now needs the resource
-# resource.wait(mutex)
-# # 'a' can now have the resource
-# }
-# }
-#
-# b = Thread.new {
-# mutex.synchronize {
-# # Thread 'b' has finished using the resource
-# resource.signal
-# }
-# }
-#
-class ConditionVariable
- #
- # Creates a new ConditionVariable
- #
- def initialize
- @waiters = []
- @waiters_mutex = Mutex.new
- end
-
- #
- # Releases the lock held in +mutex+ and waits; reacquires the lock on wakeup.
- #
- def wait(mutex)
- begin
- # TODO: mutex should not be used
- @waiters_mutex.synchronize do
- @waiters.push(Thread.current)
- end
- mutex.sleep
- end
- end
-
- #
- # Wakes up the first thread in line waiting for this lock.
- #
- def signal
- begin
- t = @waiters_mutex.synchronize { @waiters.shift }
- t.run if t
- rescue ThreadError
- retry
- end
- end
-
- #
- # Wakes up all threads waiting for this lock.
- #
- def broadcast
- # TODO: imcomplete
- waiters0 = nil
- @waiters_mutex.synchronize do
- waiters0 = @waiters.dup
- @waiters.clear
- end
- for t in waiters0
- begin
- t.run
- rescue ThreadError
- end
- end
- end
-end
-
-#
-# This class provides a way to synchronize communication between threads.
-#
-# Example:
-#
-# require 'thread'
-#
-# queue = Queue.new
-#
-# producer = Thread.new do
-# 5.times do |i|
-# sleep rand(i) # simulate expense
-# queue << i
-# puts "#{i} produced"
-# end
-# end
-#
-# consumer = Thread.new do
-# 5.times do |i|
-# value = queue.pop
-# sleep rand(i/2) # simulate expense
-# puts "consumed #{value}"
-# end
-# end
-#
-# consumer.join
-#
-class Queue
- #
- # Creates a new queue.
- #
- def initialize
- @que = []
- @waiting = []
- @que.taint # enable tainted comunication
- @waiting.taint
- self.taint
- @mutex = Mutex.new
- end
-
- #
- # Pushes +obj+ to the queue.
- #
- def push(obj)
- t = nil
- @mutex.synchronize{
- @que.push obj
- begin
- t = @waiting.shift
- t.wakeup if t
- rescue ThreadError
- retry
- end
- }
- begin
- t.run if t
- rescue ThreadError
- end
- end
-
- #
- # Alias of push
- #
- alias << push
-
- #
- # Alias of push
- #
- alias enq push
-
- #
- # Retrieves data from the queue. If the queue is empty, the calling thread is
- # suspended until data is pushed onto the queue. If +non_block+ is true, the
- # thread isn't suspended, and an exception is raised.
- #
- def pop(non_block=false)
- while true
- @mutex.synchronize{
- if @que.empty?
- raise ThreadError, "queue empty" if non_block
- @waiting.push Thread.current
- @mutex.sleep
- else
- return @que.shift
- end
- }
- end
- end
-
- #
- # Alias of pop
- #
- alias shift pop
-
- #
- # Alias of pop
- #
- alias deq pop
-
- #
- # Returns +true+ if the queue is empty.
- #
- def empty?
- @que.empty?
- end
-
- #
- # Removes all objects from the queue.
- #
- def clear
- @que.clear
- end
-
- #
- # Returns the length of the queue.
- #
- def length
- @que.length
- end
-
- #
- # Alias of length.
- #
- alias size length
-
- #
- # Returns the number of threads waiting on the queue.
- #
- def num_waiting
- @waiting.size
- end
-end
-
-#
-# This class represents queues of specified size capacity. The push operation
-# may be blocked if the capacity is full.
-#
-# See Queue for an example of how a SizedQueue works.
-#
-class SizedQueue < Queue
- #
- # Creates a fixed-length queue with a maximum size of +max+.
- #
- def initialize(max)
- raise ArgumentError, "queue size must be positive" unless max > 0
- @max = max
- @queue_wait = []
- @queue_wait.taint # enable tainted comunication
- super()
- end
-
- #
- # Returns the maximum size of the queue.
- #
- def max
- @max
- end
-
- #
- # Sets the maximum size of the queue.
- #
- def max=(max)
- diff = nil
- @mutex.synchronize {
- if max <= @max
- @max = max
- else
- diff = max - @max
- @max = max
- end
- }
- if diff
- diff.times do
- begin
- t = @queue_wait.shift
- t.run if t
- rescue ThreadError
- retry
- end
- end
- end
- max
- end
-
- #
- # Pushes +obj+ to the queue. If there is no space left in the queue, waits
- # until space becomes available.
- #
- def push(obj)
- t = nil
- @mutex.synchronize{
- while true
- break if @que.length <= @max
- @queue_wait.push Thread.current
- @mutex.sleep
- end
-
- @que.push obj
- begin
- t = @waiting.shift
- t.wakeup if t
- rescue ThreadError
- retry
- end
- }
-
- begin
- t.run if t
- rescue ThreadError
- end
- end
-
- #
- # Alias of push
- #
- alias << push
-
- #
- # Alias of push
- #
- alias enq push
-
- #
- # Retrieves data from the queue and runs a waiting thread, if any.
- #
- def pop(*args)
- retval = super
- t = nil
- @mutex.synchronize {
- if @que.length < @max
- begin
- t = @queue_wait.shift
- t.wakeup if t
- rescue ThreadError
- retry
- end
- end
- }
- begin
- t.run if t
- rescue ThreadError
- end
- retval
- end
-
- #
- # Alias of pop
- #
- alias shift pop
-
- #
- # Alias of pop
- #
- alias deq pop
-
- #
- # Returns the number of threads waiting on the queue.
- #
- def num_waiting
- @waiting.size + @queue_wait.size
- end
-end
-
-# Documentation comments:
-# - How do you make RDoc inherit documentation from superclass?
diff --git a/lib/thwait.rb b/lib/thwait.rb
deleted file mode 100644
index 029b259157..0000000000
--- a/lib/thwait.rb
+++ /dev/null
@@ -1,168 +0,0 @@
-#
-# thwait.rb - thread synchronization class
-# $Release Version: 0.9 $
-# $Revision: 1.3 $
-# by Keiju ISHITSUKA(Nihpon Rational Software Co.,Ltd.)
-#
-# --
-# feature:
-# provides synchronization for multiple threads.
-#
-# class methods:
-# * ThreadsWait.all_waits(thread1,...)
-# waits until all of specified threads are terminated.
-# if a block is supplied for the method, evaluates it for
-# each thread termination.
-# * th = ThreadsWait.new(thread1,...)
-# creates synchronization object, specifying thread(s) to wait.
-#
-# methods:
-# * th.threads
-# list threads to be synchronized
-# * th.empty?
-# is there any thread to be synchronized.
-# * th.finished?
-# is there already terminated thread.
-# * th.join(thread1,...)
-# wait for specified thread(s).
-# * th.join_nowait(threa1,...)
-# specifies thread(s) to wait. non-blocking.
-# * th.next_wait
-# waits until any of specified threads is terminated.
-# * th.all_waits
-# waits until all of specified threads are terminated.
-# if a block is supplied for the method, evaluates it for
-# each thread termination.
-#
-
-require "thread.rb"
-require "e2mmap.rb"
-
-#
-# This class watches for termination of multiple threads. Basic functionality
-# (wait until specified threads have terminated) can be accessed through the
-# class method ThreadsWait::all_waits. Finer control can be gained using
-# instance methods.
-#
-# Example:
-#
-# ThreadsWait.all_wait(thr1, thr2, ...) do |t|
-# STDERR.puts "Thread #{t} has terminated."
-# end
-#
-class ThreadsWait
- RCS_ID='-$Id: thwait.rb,v 1.3 1998/06/26 03:19:34 keiju Exp keiju $-'
-
- extend Exception2MessageMapper
- def_exception("ErrNoWaitingThread", "No threads for waiting.")
- def_exception("ErrNoFinishedThread", "No finished threads.")
-
- #
- # Waits until all specified threads have terminated. If a block is provided,
- # it is executed for each thread termination.
- #
- def ThreadsWait.all_waits(*threads) # :yield: thread
- tw = ThreadsWait.new(*threads)
- if block_given?
- tw.all_waits do |th|
- yield th
- end
- else
- tw.all_waits
- end
- end
-
- #
- # Creates a ThreadsWait object, specifying the threads to wait on.
- # Non-blocking.
- #
- def initialize(*threads)
- @threads = []
- @wait_queue = Queue.new
- join_nowait(*threads) unless threads.empty?
- end
-
- # Returns the array of threads in the wait queue.
- attr :threads
-
- #
- # Returns +true+ if there are no threads to be synchronized.
- #
- def empty?
- @threads.empty?
- end
-
- #
- # Returns +true+ if any thread has terminated.
- #
- def finished?
- !@wait_queue.empty?
- end
-
- #
- # Waits for specified threads to terminate.
- #
- def join(*threads)
- join_nowait(*threads)
- next_wait
- end
-
- #
- # Specifies the threads that this object will wait for, but does not actually
- # wait.
- #
- def join_nowait(*threads)
- threads.flatten!
- @threads.concat threads
- for th in threads
- Thread.start(th) do |t|
- begin
- t.join
- ensure
- @wait_queue.push t
- end
- end
- end
- end
-
- #
- # Waits until any of the specified threads has terminated, and returns the one
- # that does.
- #
- # If there is no thread to wait, raises +ErrNoWaitingThread+. If +nonblock+
- # is true, and there is no terminated thread, raises +ErrNoFinishedThread+.
- #
- def next_wait(nonblock = nil)
- ThreadsWait.fail ErrNoWaitingThread if @threads.empty?
- begin
- @threads.delete(th = @wait_queue.pop(nonblock))
- th
- rescue ThreadError
- ThreadsWait.fail ErrNoFinishedThread
- end
- end
-
- #
- # Waits until all of the specified threads are terminated. If a block is
- # supplied for the method, it is executed for each thread termination.
- #
- # Raises exceptions in the same manner as +next_wait+.
- #
- def all_waits
- until @threads.empty?
- th = next_wait
- yield th if block_given?
- end
- end
-end
-
-ThWait = ThreadsWait
-
-
-# Documentation comments:
-# - Source of documentation is evenly split between Nutshell, existing
-# comments, and my own rephrasing.
-# - I'm not particularly confident that the comments are all exactly correct.
-# - The history, etc., up the top appears in the RDoc output. Perhaps it would
-# be better to direct that not to appear, and put something else there
-# instead.
diff --git a/lib/time.gemspec b/lib/time.gemspec
new file mode 100644
index 0000000000..73650ab12e
--- /dev/null
+++ b/lib/time.gemspec
@@ -0,0 +1,36 @@
+name = File.basename(__FILE__, ".gemspec")
+version = ["lib", Array.new(name.count("-")+1).join("/")].find do |dir|
+ break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
+ /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
+ end rescue nil
+end
+
+Gem::Specification.new do |spec|
+ spec.name = name
+ spec.version = version
+ spec.authors = ["Tanaka Akira"]
+ spec.email = ["akr@fsij.org"]
+
+ spec.summary = %q{Extends the Time class with methods for parsing and conversion.}
+ spec.description = %q{Extends the Time class with methods for parsing and conversion.}
+ spec.homepage = "https://github.com/ruby/time"
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+ spec.metadata["changelog_uri"] = "https://github.com/ruby/time/releases"
+
+ srcdir, gemspec = File.split(__FILE__)
+ spec.files = Dir.chdir(srcdir) do
+ `git ls-files -z`.split("\x0").reject { |f|
+ f == gemspec or
+ f.start_with?(".git", "bin/", "test/", "rakelib/", "Gemfile", "Rakefile")
+ }
+ end
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+
+ spec.add_dependency "date"
+end
diff --git a/lib/time.rb b/lib/time.rb
index 3555571f22..e6aab3fa5d 100644
--- a/lib/time.rb
+++ b/lib/time.rb