heapinfo 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 38cb3f9332f88d77ee939beeec7ef2e1563e92fb
4
- data.tar.gz: 897f2130e7167fcae28c678df332b4aa0c8be682
3
+ metadata.gz: bb558249510b9c160a5f0f699d702a17b1189624
4
+ data.tar.gz: dacc4780724a2254d8137a23d2f920734db9034d
5
5
  SHA512:
6
- metadata.gz: 3fcb02ec66361586e3f5c00466f266dd1c388b0b27651c3ce030f88ab7d084b080cf7296bd71bbc32dc96a1a41e65f4318d73456c2d6126f6b37a9659e72bcb6
7
- data.tar.gz: 7884b477325650a2baf197a303387f580d4cb6de6100691b9e985842b23b8ea863afce4f07b1385a65cf5ce189aff3b70979b00681d39b6863fdde337375d62f
6
+ metadata.gz: 8fce3e42275b5895a2bec83dbe31d216d1d5a7506905e4d0e527f449c4418f9af00d6f9133d8b1074bb293e7a33ac0557833539562d68dd7a7cfc22d04404885
7
+ data.tar.gz: 1d036055d53f75e3d9639af6274682b29f5d8f6b275d4644d2619fa29ce4bbed559871ea9caf4b72c66fe81a82dcf4bd0496de239fd88b807df8b50757790ef5
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  [![Build Status](https://travis-ci.org/david942j/heapinfo.svg?branch=master)](https://travis-ci.org/david942j/heapinfo)
2
+ ![](http://ruby-gem-downloads-badge.herokuapp.com/heapinfo?type=total)
2
3
  [![Code Climate](https://codeclimate.com/github/david942j/heapinfo/badges/gpa.svg)](https://codeclimate.com/github/david942j/heapinfo)
3
4
  [![Issue Count](https://codeclimate.com/github/david942j/heapinfo/badges/issue_count.svg)](https://codeclimate.com/github/david942j/heapinfo)
4
5
  [![Test Coverage](https://codeclimate.com/github/david942j/heapinfo/badges/coverage.svg)](https://codeclimate.com/github/david942j/heapinfo/coverage)
data/lib/heapinfo.rb CHANGED
@@ -11,12 +11,13 @@ require 'fileutils'
11
11
  module HeapInfo
12
12
  # Directory for writing some tmp files when working,
13
13
  # make sure /tmp is writable
14
- TMP_DIR = '/tmp/.heapinfo'
15
- FileUtils.mkdir_p TMP_DIR
14
+ TMP_DIR = '/tmp/.heapinfo'.freeze
15
+ FileUtils.mkdir_p(TMP_DIR)
16
16
 
17
- # Entry point for using HeapInfo.
18
- # Show segments info of the process after loaded
19
- # @param [String, Fixnum] prog The program name of victim. If a number is given, seem as pid (useful when multi-processes exist)
17
+ # Entry point for using {HeapInfo}.
18
+ # Show segments info of the process after loaded.
19
+ # @param [String, Fixnum] prog
20
+ # The program name of victim. If a number is given, seem as pid (useful when multi-processes exist).
20
21
  # @param [Hash] options Give library's file name.
21
22
  # @option options [String, Regexp] :libc file name of glibc, default is <tt>/libc[^\w]/</tt>
22
23
  # @option options [String, Regexp] :ld file name of dynamic linker/loader, default is <tt>/\/ld-.+\.so/</tt>
@@ -34,7 +35,7 @@ module HeapInfo
34
35
  # # => "/lib/x86_64-linux-gnu/libc-2.19.so"
35
36
  # p h.ld.name
36
37
  # # => "/lib/x86_64-linux-gnu/ld-2.19.so"
37
- #
38
+ #
38
39
  # @example
39
40
  # h = heapinfo(27605, libc: 'libc.so.6', ld: 'ld-linux-x86-64.so.2')
40
41
  # # pid 27605 is run by custom loader
@@ -53,9 +54,8 @@ end
53
54
  # @return [HeapInfo::Process]
54
55
  # @param [Mixed] args see #HeapInfo::heapinfo for more information
55
56
  def heapinfo(*args)
56
- ::HeapInfo::heapinfo(*args)
57
+ ::HeapInfo.heapinfo(*args)
57
58
  end
58
-
59
59
 
60
60
  require 'heapinfo/helper'
61
61
  require 'heapinfo/nil'
@@ -11,34 +11,36 @@ module HeapInfo
11
11
  attr_reader :unsorted_bin
12
12
  # @return [Array<HeapInfo::Smallbin>] Smallbins in an array.
13
13
  attr_reader :smallbin
14
- # @return [Integer] The <tt>system_mem</tt>
14
+ # @return [Integer] The +system_mem+ in arena.
15
15
  attr_reader :system_mem
16
16
  # attr_reader :largebin, :last_remainder
17
17
 
18
- # Instantiate a <tt>HeapInfo::Arena</tt> object.
18
+ # Instantiate a {HeapInfo::Arena} object.
19
19
  #
20
20
  # @param [Integer] base Base address of arena.
21
21
  # @param [Integer] size_t Either 8 or 4
22
22
  # @param [Proc] dumper For dump more data
23
23
  def initialize(base, size_t, dumper)
24
- @base, @size_t, @dumper = base, size_t, dumper
24
+ @base = base
25
+ @size_t = size_t
26
+ @dumper = dumper
25
27
  reload!
26
28
  end
27
29
 
28
30
  # Refresh all attributes.
29
- # Retrive data using <tt>@dumper</tt>, load bins, top chunk etc.
31
+ # Retrive data using +@dumper+, load bins, top chunk etc.
30
32
  # @return [HeapInfo::Arena] self
31
33
  def reload!
32
34
  top_ptr_offset = @base + 8 + size_t * 10
33
35
  top_ptr = Helper.unpack(size_t, @dumper.call(top_ptr_offset, size_t))
34
36
  @fastbin = []
35
- return self if top_ptr == 0 # arena not init yet
37
+ return self if top_ptr.zero? # arena not init yet
36
38
  @top_chunk = Chunk.new size_t, top_ptr, @dumper
37
39
  @last_remainder = Chunk.new size_t, top_ptr_offset + 8, @dumper
38
40
  # this offset diff after 2.23
39
- @system_mem = 2.times.map do |off|
41
+ @system_mem = Array.new(2) do |off|
40
42
  Helper.unpack(size_t, @dumper.call(top_ptr_offset + 258 * size_t + 16 + off * size_t, size_t))
41
- end.find { |val| val >= 0x21000 and (val & 0xfff) == 0 }
43
+ end.find { |val| val >= 0x21000 && (val & 0xfff).zero? }
42
44
  @fastbin = Array.new(7) do |idx|
43
45
  f = Fastbin.new(size_t, @base + 8 - size_t * 2 + size_t * idx, @dumper, head: true)
44
46
  f.index = idx
@@ -67,7 +69,8 @@ module HeapInfo
67
69
  res
68
70
  end
69
71
 
70
- private
72
+ private
73
+
71
74
  attr_reader :size_t
72
75
  end
73
76
 
@@ -77,10 +80,10 @@ module HeapInfo
77
80
  attr_reader :fd
78
81
  # @return [Integer] index
79
82
  attr_accessor :index
80
-
81
- # Instantiate a <tt>HeapInfo::Fastbin</tt> object.
83
+
84
+ # Instantiate a {HeapInfo::Fastbin} object.
82
85
  #
83
- # @param [Mixed] args See <tt>HeapInfo::Chunk</tt> for more information.
86
+ # @param [Mixed] args See {HeapInfo::Chunk} for more information.
84
87
  def initialize(*args)
85
88
  super
86
89
  @fd = Helper.unpack(size_t, @data[0, @size_t])
@@ -95,7 +98,9 @@ module HeapInfo
95
98
  # For pretty inspect.
96
99
  # @return [String] Title with color codes.
97
100
  def title
98
- "%s%s: " % [Helper.color(Helper.class_name(self), sev: :bin), index.nil? ? nil : "[#{Helper.color("%#x" % idx_to_size)}]"]
101
+ class_name = Helper.color(Helper.class_name(self), sev: :bin)
102
+ size_str = index.nil? ? nil : "[#{Helper.color(format('%#x', idx_to_size))}]"
103
+ "#{class_name}#{size_str}: "
99
104
  end
100
105
 
101
106
  # Pretty inspect.
@@ -104,15 +109,15 @@ module HeapInfo
104
109
  title + list.map do |ptr|
105
110
  next "(#{ptr})\n" if ptr.is_a? Symbol
106
111
  next " => (nil)\n" if ptr.nil?
107
- " => %s" % Helper.color("%#x" % ptr)
112
+ format(' => %s', Helper.color(format('%#x', ptr)))
108
113
  end.join
109
114
  end
110
115
 
111
- # @return [Array<Integer, Symbol, NilClass>] single link list of <tt>fd</tt> chain.
116
+ # @return [Array<Integer, Symbol, NilClass>] single link list of +fd+ chain.
112
117
  # Last element will be:
113
- # - <tt>:loop</tt> if loop detectded
114
- # - <tt>:invalid</tt> invalid address detected
115
- # - <tt>nil</tt> end with zero address (normal case)
118
+ # - +:loop+ if loop detectded
119
+ # - +:invalid+ invalid address detected
120
+ # - +nil+ end with zero address (normal case)
116
121
  def list
117
122
  dup = {}
118
123
  ptr = @fd
@@ -127,19 +132,20 @@ module HeapInfo
127
132
  ret << nil
128
133
  end
129
134
 
130
- # @param [Integer] ptr Get the <tt>fd</tt> value of chunk at <tt>ptr</tt>.
131
- # @return [Integer] The <tt>fd</tt>.
135
+ # @param [Integer] ptr Get the +fd+ value of chunk at +ptr+.
136
+ # @return [Integer] The +fd+.
132
137
  def fd_of(ptr)
133
138
  addr_of(ptr, 2)
134
139
  end
135
140
 
136
- # @param [Integer] ptr Get the <tt>bk</tt> value of chunk at <tt>ptr</tt>.
137
- # @return [Integer] The <tt>bk</tt>.
141
+ # @param [Integer] ptr Get the +bk+ value of chunk at +ptr+.
142
+ # @return [Integer] The +bk+.
138
143
  def bk_of(ptr)
139
144
  addr_of(ptr, 3)
140
145
  end
141
146
 
142
- private
147
+ private
148
+
143
149
  def addr_of(ptr, offset)
144
150
  t = dump(ptr + size_t * offset, size_t)
145
151
  return nil if t.nil?
@@ -152,63 +158,60 @@ module HeapInfo
152
158
  # @return [Integer]
153
159
  attr_reader :bk
154
160
 
155
- # Instantiate a <tt>HeapInfo::UnsortedBin</tt> object.
161
+ # Instantiate a {HeapInfo::UnsortedBin} object.
156
162
  #
157
- # @param [Mixed] args See <tt>HeapInfo::Chunk</tt> for more information.
163
+ # @param [Mixed] args See {HeapInfo::Chunk} for more information.
158
164
  def initialize(*args)
159
165
  super
160
166
  @bk = Helper.unpack(size_t, @data[@size_t, @size_t])
161
167
  end
162
168
 
163
- # @option [Integer] size At most expand size. For <tt>size = 2</tt>, the expand list would be <tt>bk, bk, bin, fd, fd</tt>.
169
+ # @param [Integer] size
170
+ # At most expand size. For +size = 2+, the expand list would be +bk, bk, bin, fd, fd+.
164
171
  # @return [String] unsorted bin layouts wrapper with color codes.
165
172
  def inspect(size: 2)
166
173
  list = link_list(size)
167
- return '' if list.size <= 1 and Helper.class_name(self) != 'UnsortedBin' # bad..
174
+ return '' if list.size <= 1 && Helper.class_name(self) != 'UnsortedBin' # bad..
168
175
  title + pretty_list(list) + "\n"
169
176
  end
170
177
 
171
178
  # Wrapper the double-linked list with color codes.
172
- # @param [Array<Integer>] list The list from <tt>#link_list</tt>.
179
+ # @param [Array<Integer>] list The list from +#link_list+.
173
180
  # @return [String] Wrapper with color codes.
174
181
  def pretty_list(list)
175
182
  center = nil
176
183
  list.map.with_index do |c, idx|
177
- next center = Helper.color("[self]", sev: :bin) if c == @base
184
+ next center = Helper.color('[self]', sev: :bin) if c == @base
185
+ color_c = Helper.color(format('%#x', c))
178
186
  fwd = fd_of(c)
179
- next "%s(invalid)" % Helper.color("%#x" % c) if fwd.nil? # invalid c
187
+ next "#{color_c}(invalid)" if fwd.nil? # invalid c
180
188
  bck = bk_of(c)
181
189
  if center.nil? # bk side
182
- next Helper.color("%s%s" % [
183
- Helper.color("%#x" % c),
184
- fwd == list[idx+1] ? nil : "(%#x)" % fwd,
185
- ])
186
- else #fd side
187
- next Helper.color("%s%s" % [
188
- bck == list[idx-1] ? nil : "(%#x)" % bck,
189
- Helper.color("%#x" % c),
190
- ])
190
+ Helper.color(format('%s%s', color_c, fwd == list[idx + 1] ? nil : format('(%#x)', fwd)))
191
+ else # fd side
192
+ Helper.color(format('%s%s', bck == list[idx - 1] ? nil : format('(%#x)', bck), color_c))
191
193
  end
192
- end.join(" === ")
194
+ end.join(' === ')
193
195
  end
194
196
 
195
197
  # Return the double link list with bin in the center.
196
198
  #
197
- # The list will like <tt>[..., bk of bk, bk of bin, bin, fd of bin, fd of fd, ...]</tt>.
198
- # @param [Integer] expand_size At most expand size. For <tt>size = 2</tt>, the expand list would be <tt>bk, bk, bin, fd, fd</tt>.
199
+ # The list will like +[..., bk of bk, bk of bin, bin, fd of bin, fd of fd, ...]+.
200
+ # @param [Integer] expand_size
201
+ # At most expand size. For +size = 2+, the expand list would be +bk, bk, bin, fd, fd+.
199
202
  # @return [Array<Integer>] The linked list.
200
203
  def link_list(expand_size)
201
204
  list = [@base]
202
205
  # fd
203
- work = Proc.new do |ptr, nxt, append|
206
+ work = proc do |ptr, nxt, append|
204
207
  sz = 0
205
208
  dup = {}
206
- while ptr != @base and sz < expand_size
209
+ while ptr != @base && sz < expand_size
207
210
  append.call ptr
208
211
  break if ptr.nil? # invalid pointer
209
212
  break if dup[ptr] # looped
210
213
  dup[ptr] = true
211
- ptr = self.send(nxt, ptr)
214
+ ptr = send(nxt, ptr)
212
215
  sz += 1
213
216
  end
214
217
  end
@@ -220,7 +223,6 @@ module HeapInfo
220
223
 
221
224
  # Class for record smallbin type chunk.
222
225
  class Smallbin < UnsortedBin
223
-
224
226
  # Mapping index of smallbin to chunk size.
225
227
  # @return [Integer] size
226
228
  def idx_to_size
@@ -1,9 +1,8 @@
1
1
  require 'digest'
2
2
  module HeapInfo
3
-
4
3
  # Self implment file-base cache manager.
5
4
  #
6
- # Values are recorded in files based on <tt>Marshal</tt>
5
+ # Values are recorded in files based on <tt>Marshal</tt>.
7
6
  module Cache
8
7
  # Directory for caching files.
9
8
  # e.g. HeapInfo will record main_arena_offset for glibc(s)
@@ -24,8 +23,8 @@ module HeapInfo
24
23
  # @return [Boolean] true
25
24
  def self.write(key, value)
26
25
  filepath = realpath key
27
- FileUtils.mkdir_p(File.dirname filepath)
28
- IO.binwrite(filepath, Marshal::dump(value))
26
+ FileUtils.mkdir_p(File.dirname(filepath))
27
+ IO.binwrite(filepath, Marshal.dump(value))
29
28
  true
30
29
  end
31
30
 
@@ -35,8 +34,8 @@ module HeapInfo
35
34
  # @return [Object, NilClass] value that recorded, return <tt>nil</tt> when cache miss.
36
35
  def self.read(key)
37
36
  filepath = realpath key
38
- return unless File.file? filepath
39
- Marshal::load IO.binread filepath
37
+ return unless File.file?(filepath)
38
+ Marshal.load(IO.binread(filepath))
40
39
  rescue
41
40
  nil # handle if file content invalid
42
41
  end
@@ -44,24 +43,24 @@ module HeapInfo
44
43
  # @param [String] key
45
44
  # @return [String] Prepend with <tt>CACHE_DIR</tt>
46
45
  def self.realpath(key)
47
- raise ArgumentError.new('Invalid key(file path)') if key =~ /[^\w\/]/
46
+ raise ArgumentError, 'Invalid key(file path)' if key =~ %r{[^\w/]}
48
47
  File.join(CACHE_DIR, key)
49
48
  end
50
49
 
51
- # @return [Object] Not important.
52
- def self.load
50
+ # @return [void]
51
+ def self.init
53
52
  FileUtils.mkdir_p(CACHE_DIR)
54
53
  rescue
55
54
  # To prevent ~/ is not writable.
56
- self.send :remove_const, :CACHE_DIR
57
- self.const_set :CACHE_DIR, File.join(HeapInfo::TMP_DIR, '.cache/heapinfo')
55
+ send(:remove_const, :CACHE_DIR)
56
+ const_set(:CACHE_DIR, File.join(HeapInfo::TMP_DIR, '.cache/heapinfo'))
58
57
  end
59
58
 
60
59
  # Clear the cache directory.
61
- # @return [Object] Not important.
60
+ # @return [void]
62
61
  def self.clear_all
63
62
  FileUtils.rm_rf CACHE_DIR
64
63
  end
65
- self.load
64
+ init
66
65
  end
67
66
  end
@@ -10,24 +10,24 @@ module HeapInfo
10
10
  # @return [Integer] Base address of this chunk
11
11
  attr_reader :base
12
12
 
13
- # Instantiate a <tt>HeapInfo::Chunk</tt> object
13
+ # Instantiate a {HeapInfo::Chunk} object
14
14
  #
15
15
  # @param [Integer] size_t 4 or 8
16
16
  # @param [Integer] base Start address of this chunk
17
17
  # @param [Proc] dumper For dump more information of this chunk
18
- # @param [Boolean] head For specific if is fake chunk in <tt>arena</tt>. If <tt>head</tt> is <tt>true</tt>, will not load <tt>size</tt> and <tt>prev_size</tt> (since it's meaningless)
18
+ # @param [Boolean] head
19
+ # For specific if is fake chunk in +arena+.
20
+ # If +head+ is +true+, will not load +size+ and +prev_size+ (since it's meaningless)
19
21
  # @example
20
- # HeapInfo::Chunk.new 8, 0x602000, ->(addr, len) { [0,0x21, 0xda4a].pack("Q*")[addr-0x602000, len] }
22
+ # HeapInfo::Chunk.new 8, 0x602000, ->(addr, len) { [0,0x21, 0xda4a].pack('Q*')[addr-0x602000, len] }
21
23
  # # create a chunk with chunk size 0x21
22
24
  def initialize(size_t, base, dumper, head: false)
23
- raise ArgumentError.new('size_t can be either 4 or 8') unless [4, 8].include? size_t
24
- self.class.send(:define_method, :dump){|*args| dumper.call(*args)}
25
+ raise ArgumentError, 'size_t can be either 4 or 8' unless [4, 8].include?(size_t)
26
+ self.class.send(:define_method, :dump) { |*args| dumper.call(*args) }
25
27
  @size_t = size_t
26
28
  @base = base
27
29
  sz = dump(@base, size_t * 2)
28
- if head # no need to read size if is bin
29
- return @data = dump(@base + size_t * 2, size_t * 4)
30
- end
30
+ return @data = dump(@base + size_t * 2, size_t * 4) if head # no need to read size if is bin
31
31
  @prev_size = Helper.unpack(size_t, sz[0, size_t])
32
32
  @size = Helper.unpack(size_t, sz[size_t..-1])
33
33
  r_size = [size - size_t * 2, size_t * 4].min # don't read too much data
@@ -35,14 +35,14 @@ module HeapInfo
35
35
  @data = dump(@base + size_t * 2, r_size)
36
36
  end
37
37
 
38
- # Hook <tt>#to_s</tt> for pretty printing
38
+ # Hook +#to_s+ for pretty printing
39
39
  # @return [String]
40
40
  def to_s
41
- ret = Helper.color("#<%s:%#x>\n" % [self.class.to_s, @base], sev: :klass) +
42
- "flags = [#{flags.map{|f|Helper.color(":#{f}", sev: :sym)}.join(',')}]\n" +
43
- "size = #{Helper.color "%#x" % size} (#{bintype})\n"
44
- ret += "prev_size = #{Helper.color "%#x" % @prev_size}\n" unless flags.include? :prev_inuse
45
- ret += "data = #{Helper.color @data.inspect}#{'...' if @data.length < size - size_t * 2}\n"
41
+ ret = Helper.color(format("#<%s:%#x>\n", self.class.to_s, @base), sev: :klass)
42
+ ret += "flags = [#{flags.map { |f| Helper.color(":#{f}", sev: :sym) }.join(',')}]\n"
43
+ ret += "size = #{Helper.color(format('%#x', size))} (#{bintype})\n"
44
+ ret += "prev_size = #{Helper.color(format('%#x', @prev_size))}\n" unless flags.include? :prev_inuse
45
+ ret += "data = #{Helper.color(@data.inspect)}#{'...' if @data.length < size - size_t * 2}\n"
46
46
  ret
47
47
  end
48
48
 
@@ -55,29 +55,29 @@ module HeapInfo
55
55
  def flags
56
56
  mask = @size - size
57
57
  flag = []
58
- flag << :non_main_arena unless mask & 4 == 0
59
- flag << :mmapped unless mask & 2 == 0
60
- flag << :prev_inuse unless mask & 1 == 0
58
+ flag << :non_main_arena unless (mask & 4).zero?
59
+ flag << :mmapped unless (mask & 2).zero?
60
+ flag << :prev_inuse unless (mask & 1).zero?
61
61
  flag
62
62
  end
63
63
 
64
64
  # Ask if chunk not belongs to main arena.
65
65
  #
66
- # @return [Boolean] <tt>true|false</tt> if chunk not belongs to main arena
66
+ # @return [Boolean] +true|false+ if chunk not belongs to main arena
67
67
  def non_main_arena?
68
68
  flags.include? :non_main_arena
69
69
  end
70
70
 
71
71
  # Ask if chunk is mmapped.
72
72
  #
73
- # @return [Boolean] <tt>true|false</tt> if chunk is mmapped
73
+ # @return [Boolean] +true|false+ if chunk is mmapped
74
74
  def mmapped?
75
75
  flags.include? :mmapped
76
76
  end
77
77
 
78
78
  # Ask if chunk has set the prev-inuse bit.
79
79
  #
80
- # @return [Boolean] <tt>true|false</tt> if the <tt>prev_inuse</tt> bit has been set
80
+ # @return [Boolean] +true|false+ if the +prev_inuse+ bit has been set
81
81
  def prev_inuse?
82
82
  flags.include? :prev_inuse
83
83
  end
@@ -89,7 +89,7 @@ module HeapInfo
89
89
  end
90
90
 
91
91
  # Bin type of this chunk
92
- # @return [Symbol] Bin type is simply determined according to <tt>#size</tt>
92
+ # @return [Symbol] Bin type is simply determined according to +#size+
93
93
  # @example
94
94
  # [c.size, c.size_t]
95
95
  # # => [80, 8]
@@ -110,8 +110,8 @@ module HeapInfo
110
110
  return :unknown if sz < @size_t * 4
111
111
  return :fast if sz <= @size_t * 16
112
112
  return :small if sz <= @size_t * 0x7e
113
- return :large if sz <= @size_t * 0x3ffe # is this correct?
114
- return :mmap
113
+ return :large if sz <= @size_t * 0x3ffe # is this correct?
114
+ :mmap
115
115
  end
116
116
  end
117
117
  end
@@ -1,17 +1,28 @@
1
1
  module HeapInfo
2
2
  # Self-defined array for collecting chunk(s)
3
3
  class Chunks
4
- # Instantiate a <tt>HeapInfo::Chunks</tt> object
4
+ include Enumerable
5
+ # Instantiate a <tt>HeapInfo::Chunks</tt> object.
5
6
  def initialize
6
7
  @chunks = []
7
8
  end
8
9
 
9
- def method_missing(method_sym, *arguments, &block) # :nodoc:
10
- return super unless @chunks.respond_to? method_sym
11
- @chunks.send(method_sym, *arguments, &block)
10
+ # @return [void]
11
+ def <<(val)
12
+ @chunks << val
12
13
  end
13
14
 
14
- # Hook <tt>#to_s</tt> for pretty printing.
15
+ # @return [Integer]
16
+ def size
17
+ @chunks.size
18
+ end
19
+
20
+ # @return [void]
21
+ def each(&block)
22
+ @chunks.each(&block)
23
+ end
24
+
25
+ # Hook +#to_s+ for pretty printing.
15
26
  # @return [String]
16
27
  def to_s
17
28
  @chunks.map(&:to_s).join("\n")