├── Gemfile ├── .gitignore ├── .travis.yml ├── test ├── fixtures │ ├── test.torrent │ └── python.torrent ├── bittorrent_test.rb ├── encoding_test.rb ├── utf8_decoding_test.rb ├── shoulda_macros │ ├── encoding.rb │ └── decoding.rb ├── benchmark │ ├── encoding.rb │ └── decoding.rb ├── environment.rb └── decoding_test.rb ├── .yardopts ├── .github └── main.workflow ├── lib ├── bencode │ ├── core_ext │ │ ├── object.rb │ │ ├── io.rb │ │ ├── integer.rb │ │ ├── array.rb │ │ ├── hash.rb │ │ └── string.rb │ ├── decode.rb │ └── parser.rb └── bencode.rb ├── LICENSE ├── README.md ├── bencode.gemspec └── Rakefile /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | *.gem 3 | .idea 4 | Gemfile.lock 5 | .yardoc 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | - 2.1.1 6 | -------------------------------------------------------------------------------- /test/fixtures/test.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dasch/ruby-bencode/HEAD/test/fixtures/test.torrent -------------------------------------------------------------------------------- /test/fixtures/python.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dasch/ruby-bencode/HEAD/test/fixtures/python.torrent -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --title "BEncode Documentation" 3 | --readme README.md 4 | lib/**/*.rb - 5 | LICENSE 6 | -------------------------------------------------------------------------------- /test/bittorrent_test.rb: -------------------------------------------------------------------------------- 1 | require 'environment' 2 | 3 | describe "bittorrent" do 4 | it "should load a bencoded torrent file" do 5 | BEncode.load_file("test/fixtures/python.torrent") 6 | pass 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/encoding_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'environment' 3 | 4 | describe "encoding" do 5 | it_should_encode "i42e", 42 6 | it_should_encode "3:foo", "foo" 7 | it_should_encode "5:café", "café" 8 | end 9 | -------------------------------------------------------------------------------- /test/utf8_decoding_test.rb: -------------------------------------------------------------------------------- 1 | require 'environment' 2 | 3 | describe "utf encoding" do 4 | it "should be able to handle UTF8-encoded data" do 5 | BEncode.load_file('test/fixtures/test.torrent') 6 | pass 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.github/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "Code Style" { 2 | resolves = ["Rubocop checks"] 3 | on = "push" 4 | } 5 | 6 | action "Rubocop checks" { 7 | uses = "gimenete/rubocop-action@1.0" 8 | secrets = ["GITHUB_TOKEN"] 9 | } 10 | -------------------------------------------------------------------------------- /test/shoulda_macros/encoding.rb: -------------------------------------------------------------------------------- 1 | 2 | class MiniTest::Test 3 | def self.it_should_encode(expected, value) 4 | it "should encode #{value.inspect} into #{expected.inspect}" do 5 | assert_equal expected, value.bencode 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/bencode/core_ext/object.rb: -------------------------------------------------------------------------------- 1 | 2 | class Object 3 | # 4 | # Raises an exception. Subclasses of Object must themselves 5 | # define meaningful #bencode methods. 6 | def bencode 7 | raise BEncode::EncodeError, "cannot bencode #{self.class}" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/shoulda_macros/decoding.rb: -------------------------------------------------------------------------------- 1 | 2 | class MiniTest::Test 3 | def self.it_should_decode(expected, encoded, opts = {}) 4 | it "should decode #{encoded.inspect} into #{expected.inspect}" do 5 | assert_equal expected, BEncode.load(encoded, opts) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/benchmark/encoding.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__)+"/../../lib") 2 | require 'benchmark' 3 | require 'bencode' 4 | 5 | Benchmark.bmbm do |x| 6 | x.report("Encoding an array of integers") do 7 | 100.times do 8 | ((1..50).to_a * 100).bencode 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/environment.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'minitest/autorun' 5 | require 'shoulda' 6 | 7 | require File.dirname(__FILE__) + '/../lib/bencode' 8 | Dir.glob(File.dirname(__FILE__) + '/shoulda_macros/*.rb').each do |macro| 9 | require macro 10 | end 11 | -------------------------------------------------------------------------------- /lib/bencode/core_ext/io.rb: -------------------------------------------------------------------------------- 1 | class IO 2 | def self.bdecode(filename) 3 | open(filename, 'rb') {|io| io.bdecode} 4 | end 5 | 6 | def self.bencode(filename) 7 | open(filename, 'rb') {|io| io.bencode} 8 | end 9 | 10 | def bdecode 11 | read.chomp.bdecode 12 | end 13 | 14 | def bencode 15 | read.chomp.bencode 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/bencode/core_ext/integer.rb: -------------------------------------------------------------------------------- 1 | 2 | class Integer 3 | # 4 | # Bencodes the Integer object. Bencoded integers are represented 5 | # as +ixe+, where +x+ is the integer with an optional 6 | # hyphen prepended, indicating negativity. 7 | # 8 | # 42.bencode #=> "i42e" 9 | # -7.bencode #=> "i-7e" 10 | def bencode 11 | "i#{self}e" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/bencode.rb: -------------------------------------------------------------------------------- 1 | 2 | # Support for loading and dumping bencoded data. 3 | # 4 | # See {BEncode.load} and {BEncode.dump}. 5 | module BEncode 6 | VERSION = "1.0.0" 7 | 8 | class DecodeError < StandardError 9 | end 10 | 11 | class EncodeError < StandardError 12 | end 13 | end 14 | 15 | glob = File.join(File.dirname(__FILE__), 'bencode/**/*.rb') 16 | 17 | Dir[glob].sort.each {|file| require file } 18 | -------------------------------------------------------------------------------- /lib/bencode/core_ext/array.rb: -------------------------------------------------------------------------------- 1 | 2 | class Array 3 | # 4 | # Bencodes the Array object. Bencoded arrays are represented as 5 | # +lxe+, where +x+ is zero or more bencoded objects. 6 | # 7 | # [1, "foo"].bencode #=> "li1e3:fooe" 8 | # 9 | def bencode 10 | begin 11 | "l#{map{|obj| obj.bencode }.join}e" 12 | rescue BEncode::EncodeError 13 | raise BEncode::EncodeError, "array items must be encodable" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/benchmark/decoding.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__)+"/../../lib") 2 | require 'benchmark' 3 | require 'bencode' 4 | 5 | Benchmark.bmbm do |x| 6 | x.report("Decoding an integer") do 7 | 100_000.times do 8 | BEncode.load("i42e") 9 | end 10 | end 11 | 12 | x.report("Decoding an string") do 13 | 100_000.times do 14 | BEncode.load("6:foobar") 15 | end 16 | end 17 | x.report("Decoding a long integer array") do 18 | 100.times do 19 | BEncode.load("l" + ("i42e" * 1000) + "e") 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/bencode/core_ext/hash.rb: -------------------------------------------------------------------------------- 1 | 2 | class Hash 3 | # 4 | # Bencodes the Hash object. Bencoded hashes are represented as 5 | # +dxe+, where +x+ is zero or a power of two bencoded objects. 6 | # each key is immediately followed by its associated value. 7 | # All keys must be strings. The keys of the bencoded hash will 8 | # be in lexicographical order. 9 | def bencode 10 | pairs = sort.map{|key, val| [key.to_s.bencode, val.bencode] } 11 | "d#{pairs.join}e" 12 | rescue NoMethodError 13 | raise BEncode::EncodeError, "dictionary keys must be strings" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/decoding_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'environment' 3 | 4 | describe "decoding" do 5 | it_should_decode 42, "i42e" 6 | it_should_decode 0, "i0e" 7 | it_should_decode -42, "i-42e" 8 | 9 | it_should_decode "foo", "3:foo" 10 | it_should_decode "", "0:" 11 | 12 | it_should_decode [1, 2, 3], "li1ei2ei3ee" 13 | 14 | hsh = {"foo" => "bar", "baz" => "qux"} 15 | it_should_decode hsh, "d3:foo3:bar3:baz3:quxe" 16 | 17 | it_should_decode 42, "i42eBOGUS", :ignore_trailing_junk => true 18 | 19 | it_should_decode "café", "5:café" 20 | it_should_decode ["你好", "中文"], "l6:你好6:中文e" 21 | end 22 | -------------------------------------------------------------------------------- /lib/bencode/core_ext/string.rb: -------------------------------------------------------------------------------- 1 | 2 | class String 3 | # 4 | # Bencodes the String object. Bencoded strings are represented 5 | # as x:y, where +y+ is the string and +x+ 6 | # is the length of the string. 7 | # 8 | # "foo".bencode #=> "3:foo" 9 | # "".bencode #=> "0:" 10 | # 11 | def bencode 12 | "#{bytesize}:#{self}" 13 | end 14 | 15 | # 16 | # Bdecodes the String object and returns the data serialized 17 | # through bencoding. 18 | # 19 | # "li1ei2ei3ee".bdecode #=> [1, 2, 3] 20 | # 21 | def bdecode 22 | BEncode.load(self) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2009 Daniel Schierbeck 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/bencode/decode.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'strscan' 3 | 4 | module BEncode 5 | # Encodes the Ruby object +obj+ into a bencoded string. 6 | # 7 | # @param [Hash, Array, Integer, String] obj the object to encode 8 | # @return [String] a bencoded string 9 | # @raise [EncodeError] if +obj+ is not a supported object type 10 | def self.dump(obj) 11 | obj.bencode 12 | end 13 | 14 | # Decodes +str+ into a Ruby structure. 15 | # 16 | # @param [String] str a bencoded string 17 | # @option opts [Boolean] :ignore_trailing_junk (false) whether 18 | # to ignore invalid bencode at the end of +str+ 19 | # @return [Object] a Ruby object 20 | # @raise [DecodeError] if +str+ is malformed 21 | def self.load(str, opts = {}) 22 | scanner = BEncode::Parser.new(str) 23 | obj = scanner.parse! 24 | raise BEncode::DecodeError unless (opts[:ignore_trailing_junk] || scanner.eos?) 25 | obj 26 | end 27 | 28 | # Decodes the file located at +path+. 29 | # 30 | # @param [String] path path to the bencoded file 31 | # @option (see .load) 32 | # @return (see .load) 33 | def self.load_file(path, opts = {}) 34 | File.open(path, 'rb') do |io| 35 | load(io, opts) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ruby bencode binding 2 | ==================== 3 | 4 | This is a simple library for reading and writing bencoded data. 5 | 6 | 7 | What is bencode? 8 | ---------------- 9 | 10 | Bencode is a simple data serialization format used by the popular 11 | [BitTorrent](http://bittorrent.org/) P2P file sharing system. 12 | 13 | It contains only four data types, namely: 14 | 15 | - byte strings 16 | - integers 17 | - lists 18 | - dictionaries 19 | 20 | 21 | Examples 22 | -------- 23 | 24 | Encoding objects is as simple as calling `#bencode` on them: 25 | 26 | ```ruby 27 | "foo bar".bencode # => "7:foo bar" 28 | 42.bencode # => "i42e" 29 | [1, 2, 3].bencode # => "li1ei2ei3ee" 30 | {"foo" => 1, "bar" => -10}.bencode # => "d3:bari-10e3:fooi1ee" 31 | ``` 32 | 33 | Decoding a complete data stream is as easy as calling `BEncode.load(data)`. 34 | 35 | Decoding a data stream in chunks works as follows: 36 | 37 | ```ruby 38 | irb(main):007:0> stream = BEncode::Parser.new(StringIO.new "d3:foo3:bared3:baz3:quxe") 39 | => #> 40 | irb(main):008:0> stream.parse! 41 | => {"foo"=>"bar"} 42 | irb(main):009:0> stream.parse! 43 | => {"baz"=>"qux"} 44 | ``` 45 | 46 | License 47 | ------- 48 | 49 | Released under the MIT license. 50 | 51 | 52 | Contributors 53 | ------------ 54 | 55 | - Daniel Schierbeck ([dasch](https://github.com/dasch)) 56 | - Mike Hodgson ([mikehodgson](https://github.com/mikehodgson)) 57 | - Andrew Danforth 58 | - Eric Himmelreich 59 | - Allen Madsen ([blatyo](https://github.com/blatyo)) 60 | - Ian Levesque ([ianlevesque](https://github.com/ianlevesque)) 61 | - Dirk Geurs ([Dirklectisch](https://github.com/Dirklectisch)) 62 | - Xinyue Lu ([msg7086](https://github.com/msg7086)) 63 | -------------------------------------------------------------------------------- /bencode.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.specification_version = 2 if s.respond_to? :specification_version= 3 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 4 | s.rubygems_version = '1.3.5' 5 | 6 | ## Leave these as is they will be modified for you by the rake gemspec task. 7 | ## If your rubyforge_project name is different, then edit it and comment out 8 | ## the sub! line in the Rakefile 9 | s.name = 'bencode' 10 | s.version = '1.0.0' 11 | s.date = '2024-08-12' 12 | 13 | s.summary = "Encode and decode bencoded data" 14 | s.description = "A simple encoder and decoder for the BitTorrent serialization format." 15 | 16 | s.authors = ["Daniel Schierbeck"] 17 | s.email = "daniel.schierbeck@gmail.com" 18 | s.homepage = "http://github.com/dasch/ruby-bencode" 19 | s.license = 'MIT' 20 | 21 | s.require_paths = %w[lib] 22 | 23 | s.rdoc_options = ["--charset=UTF-8"] 24 | s.extra_rdoc_files = %w[README.md LICENSE] 25 | 26 | s.add_development_dependency('rdoc') 27 | s.add_development_dependency('rake') 28 | s.add_development_dependency('minitest') 29 | s.add_development_dependency('shoulda') 30 | 31 | # = MANIFEST = 32 | s.files = %w[ 33 | Gemfile 34 | LICENSE 35 | README.md 36 | Rakefile 37 | bencode.gemspec 38 | lib/bencode.rb 39 | lib/bencode/core_ext/array.rb 40 | lib/bencode/core_ext/hash.rb 41 | lib/bencode/core_ext/integer.rb 42 | lib/bencode/core_ext/io.rb 43 | lib/bencode/core_ext/object.rb 44 | lib/bencode/core_ext/string.rb 45 | lib/bencode/decode.rb 46 | lib/bencode/parser.rb 47 | test/benchmark/decoding.rb 48 | test/benchmark/encoding.rb 49 | test/bittorrent_test.rb 50 | test/decoding_test.rb 51 | test/encoding_test.rb 52 | test/environment.rb 53 | test/fixtures/python.torrent 54 | test/fixtures/test.torrent 55 | test/shoulda_macros/decoding.rb 56 | test/shoulda_macros/encoding.rb 57 | test/utf8_decoding_test.rb 58 | ] 59 | # = MANIFEST = 60 | 61 | s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ } 62 | end 63 | -------------------------------------------------------------------------------- /lib/bencode/parser.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | 3 | module BEncode 4 | class Parser 5 | attr_reader :stream, :encoding 6 | 7 | def initialize(stream) 8 | @stream = 9 | if stream.kind_of?(IO) || stream.kind_of?(StringIO) 10 | stream 11 | elsif stream.respond_to? :string 12 | StringIO.new stream.string 13 | elsif stream.respond_to? :to_s 14 | StringIO.new stream.to_s 15 | end 16 | @encoding = @stream.external_encoding || Encoding::default_external 17 | end 18 | 19 | def parse! 20 | case peek 21 | when ?i then parse_integer! 22 | when ?l then parse_list! 23 | when ?d then parse_dict! 24 | when ?0 .. ?9 then parse_string! 25 | end 26 | end 27 | 28 | def eos? 29 | stream.eof? 30 | end 31 | 32 | private 33 | 34 | def parse_integer! 35 | stream.getc 36 | num = stream.gets("e") or raise BEncode::DecodeError 37 | num.chop.to_i 38 | end 39 | 40 | def parse_list! 41 | stream.getc 42 | ary = [] 43 | ary.push(parse!) until peek == ?e 44 | stream.getc 45 | ary 46 | end 47 | 48 | def parse_dict! 49 | stream.getc 50 | hsh = {} 51 | until peek == ?e 52 | key = parse! 53 | 54 | unless key.is_a? String or key.is_a? Integer 55 | raise BEncode::DecodeError, "key must be a string or number" 56 | end 57 | 58 | val = parse! 59 | 60 | hsh.store(key.to_s, val) 61 | end 62 | stream.getc 63 | hsh 64 | end 65 | 66 | def parse_string! 67 | num = stream.gets(":") or 68 | raise BEncode::DecodeError, "invalid string length (no colon)" 69 | 70 | begin 71 | length = num.chop.to_i 72 | return "" if length == 0 # Workaround for Rubinius bug 73 | str = stream.read(length).force_encoding(encoding) 74 | rescue 75 | raise BEncode::DecodeError, "invalid string length" 76 | end 77 | 78 | str 79 | end 80 | 81 | def peek 82 | c = stream.getc 83 | stream.ungetc(c) 84 | c 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'date' 4 | require 'rake/testtask' 5 | require 'rdoc/task' 6 | 7 | ############################################################################# 8 | # 9 | # Helper functions 10 | # 11 | ############################################################################# 12 | 13 | def name 14 | @name ||= Dir['*.gemspec'].first.split('.').first 15 | end 16 | 17 | def version 18 | line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/] 19 | line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1] 20 | end 21 | 22 | def date 23 | Date.today.to_s 24 | end 25 | 26 | def rubyforge_project 27 | name 28 | end 29 | 30 | def gemspec_file 31 | "#{name}.gemspec" 32 | end 33 | 34 | def gem_file 35 | "#{name}-#{version}.gem" 36 | end 37 | 38 | def replace_header(head, header_name) 39 | head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"} 40 | end 41 | 42 | ############################################################################# 43 | # 44 | # Standard tasks 45 | # 46 | ############################################################################# 47 | 48 | task :default => :test 49 | 50 | Rake::TestTask.new(:test) do |test| 51 | test.libs << 'lib' << 'test' 52 | test.pattern = 'test/**/*_test.rb' 53 | test.verbose = false 54 | end 55 | 56 | desc "Run all the benchmarks" 57 | task :benchmark do 58 | Dir["test/benchmark/*.rb"].each do |file| 59 | puts ">> Running #{file}" 60 | puts %x[ruby #{file}] 61 | puts 62 | end 63 | end 64 | 65 | Rake::RDocTask.new do |rdoc| 66 | rdoc.rdoc_dir = 'rdoc' 67 | rdoc.title = "#{name} #{version}" 68 | rdoc.rdoc_files.include('README*') 69 | rdoc.rdoc_files.include('lib/**/*.rb') 70 | end 71 | 72 | desc "Open an irb session preloaded with this library" 73 | task :console do 74 | sh "irb -rubygems -r ./lib/#{name}.rb" 75 | end 76 | 77 | ############################################################################# 78 | # 79 | # Packaging tasks 80 | # 81 | ############################################################################# 82 | 83 | desc "Create tag v#{version} and build and push #{gem_file} to Rubygems" 84 | task :release => :build do 85 | unless `git branch` =~ /^\* master$/ 86 | puts "You must be on the master branch to release!" 87 | exit! 88 | end 89 | sh "git commit --allow-empty -a -m 'Release #{version}'" 90 | sh "git tag v#{version}" 91 | sh "git push origin master" 92 | sh "git push origin v#{version}" 93 | sh "gem push pkg/#{name}-#{version}.gem" 94 | end 95 | 96 | desc "Build #{gem_file} into the pkg directory" 97 | task :build => :gemspec do 98 | sh "mkdir -p pkg" 99 | sh "gem build #{gemspec_file}" 100 | sh "mv #{gem_file} pkg" 101 | end 102 | 103 | desc "Generate #{gemspec_file}" 104 | task :gemspec => :validate do 105 | # read spec file and split out manifest section 106 | spec = File.read(gemspec_file) 107 | head, manifest, tail = spec.split(" # = MANIFEST =\n") 108 | 109 | # replace name version and date 110 | replace_header(head, :name) 111 | replace_header(head, :version) 112 | replace_header(head, :date) 113 | #comment this out if your rubyforge_project has a different name 114 | replace_header(head, :rubyforge_project) 115 | 116 | # determine file list from git ls-files 117 | files = `git ls-files`. 118 | split("\n"). 119 | sort. 120 | reject { |file| file =~ /^\./ }. 121 | reject { |file| file =~ /^(rdoc|pkg)/ }. 122 | map { |file| " #{file}" }. 123 | join("\n") 124 | 125 | # piece file back together and write 126 | manifest = " s.files = %w[\n#{files}\n ]\n" 127 | spec = [head, manifest, tail].join(" # = MANIFEST =\n") 128 | File.open(gemspec_file, 'w') { |io| io.write(spec) } 129 | puts "Updated #{gemspec_file}" 130 | end 131 | 132 | desc "Validate #{gemspec_file}" 133 | task :validate do 134 | libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"] 135 | unless libfiles.empty? 136 | puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir." 137 | exit! 138 | end 139 | unless Dir['VERSION*'].empty? 140 | puts "A `VERSION` file at root level violates Gem best practices." 141 | exit! 142 | end 143 | end 144 | --------------------------------------------------------------------------------