elftools 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -105,12 +105,19 @@ module ELFTools
105
105
  #========= method about sections
106
106
 
107
107
  # Number of sections in this file.
108
+ #
109
+ # A file with more sections than the ELF header can count records a zero
110
+ # there and states the number in the first section header instead, which a
111
+ # file with no section headers at all records as well.
108
112
  # @return [Integer] The desired number.
109
113
  # @example
110
114
  # elf.num_sections
111
115
  # #=> 29
112
116
  def num_sections
113
- header.e_shnum
117
+ count = header.e_shnum.to_i
118
+ return count unless count.zero? && !header.e_shoff.to_i.zero?
119
+
120
+ first_section_header.sh_size.to_i
114
121
  end
115
122
 
116
123
  # Acquire the section named as +name+.
@@ -197,15 +204,25 @@ module ELFTools
197
204
  # elf.section_name_table.name
198
205
  # #=> '.shstrtab'
199
206
  def section_name_table
200
- section_at(header.e_shstrndx)
207
+ index = header.e_shstrndx.to_i
208
+ # An index too large for the ELF header is stated in the first section
209
+ # header instead.
210
+ index = first_section_header.sh_link.to_i if index == Constants::SHN_XINDEX
211
+ section_at(index)
201
212
  end
202
213
 
203
214
  #========= method about segments
204
215
 
205
216
  # Number of segments in this file.
217
+ #
218
+ # A file with more segments than the ELF header can count states the number
219
+ # in the first section header instead, as it does for the sections.
206
220
  # @return [Integer] The desited number.
207
221
  def num_segments
208
- header.e_phnum
222
+ count = header.e_phnum.to_i
223
+ return count unless count == Constants::PN_XNUM
224
+
225
+ first_section_header.sh_info.to_i
209
226
  end
210
227
 
211
228
  # Iterate all segments.
@@ -373,15 +390,35 @@ module ELFTools
373
390
 
374
391
  # bad idea..
375
392
  def loaded_headers
376
- explore = lambda do |obj|
377
- return obj if obj.is_a?(::ELFTools::Structs::ELFStruct)
378
- return obj.map(&explore) if obj.is_a?(Array)
393
+ # By identity, so that a graph that reaches the same thing by many paths,
394
+ # or leads round in circles, is walked once rather than over and over.
395
+ headers_in(self, {}.compare_by_identity).flatten
396
+ end
379
397
 
380
- obj.instance_variables.map do |s|
381
- explore.call(obj.instance_variable_get(s))
382
- end
383
- end
384
- explore.call(self).flatten
398
+ # The structures reachable from an object, whatever holds them.
399
+ # @param [Object] obj The object.
400
+ # @param [Hash] seen What has been reached already.
401
+ # @return [Array] The structures, nested.
402
+ def headers_in(obj, seen)
403
+ # A class is not one of the things a file records.
404
+ return [] if seen[obj] || obj.is_a?(Module)
405
+
406
+ seen[obj] = true
407
+ return obj if obj.is_a?(::ELFTools::Structs::ELFStruct)
408
+
409
+ held_by(obj).map { |held| headers_in(held, seen) }
410
+ end
411
+
412
+ # What an object holds, which a walk of it goes on to.
413
+ # @param [Object] obj The object.
414
+ # @return [Enumerable] What it holds.
415
+ def held_by(obj)
416
+ return obj if obj.is_a?(Array)
417
+ # A hash is held by its values, which is where the tags and the symbols
418
+ # read through the tags are remembered.
419
+ return obj.each_value if obj.is_a?(Hash)
420
+
421
+ obj.instance_variables.map { |name| obj.instance_variable_get(name) }
385
422
  end
386
423
 
387
424
  def identify
@@ -404,6 +441,18 @@ module ELFTools
404
441
  raise ELFDataError, format('Invalid EI_DATA "\x%02x"', ei_data) if endian.nil?
405
442
  end
406
443
 
444
+ # The first section header, which is where a file too large for the counts
445
+ # the ELF header records states them.
446
+ # @return [ELFTools::Structs::ELF_Shdr] The header.
447
+ def first_section_header
448
+ @first_section_header ||= begin
449
+ stream.pos = header.e_shoff.to_i
450
+ shdr = Structs::ELF_Shdr.new(endian:, offset: stream.pos)
451
+ shdr.elf_class = elf_class
452
+ shdr.read(stream)
453
+ end
454
+ end
455
+
407
456
  def create_section(n)
408
457
  stream.pos = header.e_shoff + n * header.e_shentsize
409
458
  shdr = Structs::ELF_Shdr.new(endian:, offset: stream.pos)
@@ -413,6 +462,7 @@ module ELFTools
413
462
  offset_from_vma: method(:offset_from_vma),
414
463
  section_name_table: method(:section_name_table),
415
464
  section_at: method(:section_at),
465
+ sections: method(:sections),
416
466
  machine: header.e_machine.to_i)
417
467
  end
418
468
 
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elftools/constants'
4
+ require 'elftools/relocation'
5
+ require 'elftools/structs'
6
+
7
+ module ELFTools
8
+ # The relocations a file packs into a bitmap instead of recording one by one.
9
+ #
10
+ # Almost every relocation of a file that is loaded anywhere only adds the
11
+ # load bias to a word, which takes an entry recording an address, a type that
12
+ # is the same every time, and an addend that repeats what the word already
13
+ # holds. A file may pack them instead, as a run of addresses in ascending
14
+ # order, and spend a bit rather than an entry on each.
15
+ #
16
+ # An even entry is an address, and relocates the word there. An odd entry is
17
+ # a bitmap of the words following the last address, a set bit relocating one
18
+ # of them. Nothing records a type, because every relocation here is the one
19
+ # {ELFTools::Constants::R.relative} names.
20
+ class RelativeRelocations
21
+ # Instantiate a {ELFTools::RelativeRelocations} object.
22
+ # @param [#pos=, #read] stream Streaming object.
23
+ # @param [Range<Integer>] bytes The file offsets the table occupies.
24
+ # @param [Integer] elf_class 32 or 64, the width of an entry.
25
+ # @param [Symbol] endian +:little+ or +:big+.
26
+ # @param [Integer] machine
27
+ # The machine of the file, which decides what these relocations are of.
28
+ def initialize(stream, bytes, elf_class:, endian:, machine:)
29
+ @stream = stream
30
+ @bytes = bytes
31
+ @elf_class = elf_class
32
+ @endian = endian
33
+ @machine = machine
34
+ end
35
+
36
+ # The relocations the table packs.
37
+ # @return [Array<ELFTools::Relocation>]
38
+ # The relocations, in the ascending order the table records them.
39
+ def to_a
40
+ type = Constants::R.relative(@machine)
41
+ addresses.map do |address, from|
42
+ fields = Structs::Fields.from(Structs::ELF_Rel, { r_offset: address, r_info: 0 },
43
+ elf_class: @elf_class, endian: @endian, offset: from)
44
+ relocation = Relocation.new(fields, @stream, machine: @machine)
45
+ # Through the relocation, so that the type is laid out in +r_info+ the
46
+ # way the machine lays it out.
47
+ relocation.type = type if type
48
+ relocation
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ # How many bytes an entry takes, which is the width of an address.
55
+ # @return [Integer] The number.
56
+ def width
57
+ @elf_class / 8
58
+ end
59
+
60
+ # Every address the table relocates, and the entry it was read from.
61
+ # @return [Array<Array(Integer, Integer)>] The addresses.
62
+ def addresses
63
+ found = []
64
+ # Where a bitmap counts from, which an address moves to just past itself.
65
+ here = 0
66
+ entries.each do |entry, from|
67
+ if entry.even?
68
+ found << [entry, from]
69
+ here = entry + width
70
+ else
71
+ # The lowest bit says the entry is a bitmap rather than an address.
72
+ (1...(width * 8)).each { |bit| found << [here + ((bit - 1) * width), from] if entry[bit] == 1 }
73
+ here += ((width * 8) - 1) * width
74
+ end
75
+ end
76
+ found
77
+ end
78
+
79
+ # What the table records, and where each was read from.
80
+ # @return [Array<Array(Integer, Integer)>] The entries.
81
+ def entries
82
+ @stream.pos = @bytes.begin
83
+ format = "#{width == 8 ? 'Q' : 'L'}#{@endian == :big ? '>' : '<'}"
84
+ @stream.read(@bytes.size).to_s.unpack("#{format}*").each_with_index.map { |e, i| [e, @bytes.begin + (i * width)] }
85
+ end
86
+ end
87
+ end
@@ -1,28 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'elftools/constants'
4
+ require 'elftools/structs'
5
+ require 'elftools/util'
4
6
 
5
7
  module ELFTools
6
8
  # A relocation entry.
7
9
  #
8
10
  # Can be either a REL or RELA relocation.
9
11
  class Relocation
10
- attr_reader :header # @return [ELFTools::Structs::ELF_Rel, ELFTools::Structs::ELF_Rela] Rel(a) header.
11
12
  attr_reader :stream # @return [#pos=, #read] Streaming object.
12
13
 
13
14
  # Instantiate a {Relocation} object.
14
- # @param [ELFTools::Structs::ELF_Rel, ELFTools::Structs::ELF_Rela] header
15
- # The relocation header.
15
+ # @param [ELFTools::Structs::ELF_Rel, ELFTools::Structs::ELF_Rela, ELFTools::Structs::Fields] header
16
+ # The relocation header, or what the file records in one.
17
+ # {ELFTools::Structs::Fields} answers what the relocation records until
18
+ # something asks for {#header} itself, which builds the structure then.
16
19
  # @param [#pos=, #read] stream The streaming object.
17
20
  # @param [Integer] machine
18
21
  # The machine of the ELF file, which decides what {#type} means and how
19
22
  # {#header} records it.
20
23
  def initialize(header, stream, machine: nil)
21
- @header = header
24
+ @fields = header.is_a?(Structs::Fields) ? header : Structs::Fields.of(header)
22
25
  @stream = stream
23
26
  @machine = machine
24
27
  end
25
28
 
29
+ # The structure the file records this relocation in.
30
+ #
31
+ # One is built here for a relocation read without it, which is what
32
+ # assigning to a field of one needs and what {ELFTools::ELFFile#patches}
33
+ # reports the changes of.
34
+ # @return [ELFTools::Structs::ELF_Rel, ELFTools::Structs::ELF_Rela] The structure.
35
+ def header
36
+ @fields.struct
37
+ end
38
+
26
39
  # Which symbol this relocation is against, as an index into the symbol
27
40
  # table.
28
41
  # @return [Integer] The symbol index.
@@ -37,6 +50,24 @@ module ELFTools
37
50
  sym_and_type.last
38
51
  end
39
52
 
53
+ # Sets which symbol this relocation is against.
54
+ # @param [Integer] index The symbol index.
55
+ # @raise [ArgumentError] If the bits recording it cannot hold it.
56
+ # @example
57
+ # relocation.symbol_index = 3
58
+ def symbol_index=(index)
59
+ @fields[:r_info] = info_of(Util.fits!(index, index_bits, 'Symbol index'), type)
60
+ end
61
+
62
+ # Sets what this relocation does.
63
+ # @param [Integer] type The relocation type.
64
+ # @raise [ArgumentError] If the bits recording it cannot hold it.
65
+ # @example
66
+ # relocation.type = ELFTools::Constants::R::X86_64::R_X86_64_JUMP_SLOT
67
+ def type=(type)
68
+ @fields[:r_info] = info_of(symbol_index, Util.fits!(type, type_bits, 'Relocation type'))
69
+ end
70
+
40
71
  # The name of {#type}.
41
72
  #
42
73
  # Every architecture numbers relocation types on its own, so the name is
@@ -51,6 +82,37 @@ module ELFTools
51
82
 
52
83
  private
53
84
 
85
+ # The +r_info+ recording a symbol index and a type, laid out the way this
86
+ # file lays it out. The inverse of {#sym_and_type}.
87
+ # @return [Integer] The +r_info+.
88
+ def info_of(index, type)
89
+ return mips64_info_of(index, type) if mips64?
90
+
91
+ (index << mask_bit) | type
92
+ end
93
+
94
+ # The +r_info+ as the 64-bit MIPS ABI records it, leaving the bytes the
95
+ # ABI keeps for itself as they were.
96
+ # @return [Integer] The +r_info+.
97
+ def mips64_info_of(index, type)
98
+ info = @fields[:r_info]
99
+ return (index << 32) | (info & 0xffff_ff00) | type if @fields.endian == :big
100
+
101
+ (info & 0x00ff_ffff_0000_0000) | (type << 56) | index
102
+ end
103
+
104
+ # How many bits record a symbol index.
105
+ # @return [Integer] The number.
106
+ def index_bits
107
+ mips64? ? 32 : (@fields.elf_class - mask_bit)
108
+ end
109
+
110
+ # How many bits record a relocation type.
111
+ # @return [Integer] The number.
112
+ def type_bits
113
+ mips64? ? 8 : mask_bit
114
+ end
115
+
54
116
  # What +r_info+ records, i.e. a symbol index and a relocation type. Most
55
117
  # machines split the field in half between the two, one lays it out its
56
118
  # own way.
@@ -58,14 +120,15 @@ module ELFTools
58
120
  def sym_and_type
59
121
  return mips64_sym_and_type if mips64?
60
122
 
61
- [header.r_info >> mask_bit, header.r_info & ((1 << mask_bit) - 1)]
123
+ info = @fields[:r_info]
124
+ [info >> mask_bit, info & ((1 << mask_bit) - 1)]
62
125
  end
63
126
 
64
127
  # Whether the file records relocations the way the 64-bit MIPS ABI does,
65
128
  # which is the one layout that departs from halving +r_info+.
66
129
  # @return [Boolean] The answer.
67
130
  def mips64?
68
- @machine == Constants::EM_MIPS && header.elf_class == 64
131
+ @machine == Constants::EM_MIPS && @fields.elf_class == 64
69
132
  end
70
133
 
71
134
  # Reads +r_info+ as the 64-bit MIPS ABI records it, i.e. a symbol index of
@@ -81,14 +144,14 @@ module ELFTools
81
144
  # 0x0718050000000008 #=> [8, 7]
82
145
  # @return [Array(Integer, Integer)] The symbol index and the type.
83
146
  def mips64_sym_and_type
84
- info = header.r_info.to_i
85
- return [info >> 32, info & 0xff] if header.class.self_endian == :big
147
+ info = @fields[:r_info]
148
+ return [info >> 32, info & 0xff] if @fields.endian == :big
86
149
 
87
150
  [info & 0xffff_ffff, info >> 56]
88
151
  end
89
152
 
90
153
  def mask_bit
91
- header.elf_class == 32 ? 8 : 32
154
+ @fields.elf_class == 32 ? 8 : 32
92
155
  end
93
156
  end
94
157
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elftools/relative_relocations'
4
+ require 'elftools/sections/section'
5
+
6
+ module ELFTools
7
+ module Sections
8
+ # Class of the section packing the relocations that only add the load bias.
9
+ #
10
+ # This section is usually named .relr.dyn, and records the very relocations
11
+ # the +DT_RELR+ tag points at.
12
+ class RelativeRelocationSection < Section
13
+ # Instantiate a {RelativeRelocationSection} object.
14
+ # @param [ELFTools::Structs::ELF_Shdr] header
15
+ # See {Section#initialize} for more information.
16
+ # @param [#pos=, #read] stream
17
+ # See {Section#initialize} for more information.
18
+ # @param [Integer] machine
19
+ # The machine of the ELF file, which decides what these relocations
20
+ # are of. This should be +e_machine+ of the ELF header.
21
+ def initialize(header, stream, machine: nil, **_kwargs)
22
+ @machine = machine
23
+ super
24
+ end
25
+
26
+ # The relocations the section packs.
27
+ # @return [Array<ELFTools::Relocation>]
28
+ # The relocations, in the ascending order the section records them.
29
+ # @example
30
+ # section.relocations.map(&:type_name).uniq
31
+ # #=> ['R_X86_64_RELATIVE']
32
+ def relocations
33
+ @relocations ||= RelativeRelocations.new(
34
+ stream, header.sh_offset.to_i...(header.sh_offset.to_i + header.sh_size.to_i),
35
+ elf_class: header.elf_class, endian: header.class.self_endian, machine: @machine
36
+ ).to_a
37
+ end
38
+
39
+ # How many relocations the section packs.
40
+ # @return [Integer] The number.
41
+ def num_relocations
42
+ relocations.size
43
+ end
44
+ end
45
+ end
46
+ end
@@ -78,12 +78,23 @@ module ELFTools
78
78
  private
79
79
 
80
80
  def create_relocation(n)
81
- stream.pos = header.sh_offset + n * header.sh_entsize
82
81
  klass = rela? ? Structs::ELF_Rela : Structs::ELF_Rel
83
- rel = klass.new(endian: header.class.self_endian, offset: stream.pos)
84
- rel.elf_class = header.elf_class
85
- rel.read(stream)
86
- Relocation.new(rel, stream, machine: @machine)
82
+ fields = Structs::Fields.new(klass, stream, table_offset + (n * entsize),
83
+ elf_class: header.elf_class, endian: header.class.self_endian)
84
+ Relocation.new(fields, stream, machine: @machine)
85
+ end
86
+
87
+ # Where this table starts in the file, asked of the header once rather
88
+ # than once for every relocation read.
89
+ # @return [Integer] The file offset.
90
+ def table_offset
91
+ @table_offset ||= header.sh_offset.to_i
92
+ end
93
+
94
+ # How many bytes this table spaces its entries by.
95
+ # @return [Integer] The number.
96
+ def entsize
97
+ @entsize ||= header.sh_entsize.to_i
87
98
  end
88
99
  end
89
100
  end
@@ -46,6 +46,37 @@ module ELFTools
46
46
  stream.read(header.sh_size)
47
47
  end
48
48
 
49
+ # Is this section written to while the file runs?
50
+ # @return [Boolean] True or false.
51
+ # @example
52
+ # elf.section_by_name('.data').writable?
53
+ # #=> true
54
+ def writable?
55
+ header.sh_flags.allbits?(Constants::SHF_WRITE)
56
+ end
57
+
58
+ # Is this section executed?
59
+ # @return [Boolean] True or false.
60
+ # @example
61
+ # elf.section_by_name('.text').executable?
62
+ # #=> true
63
+ def executable?
64
+ header.sh_flags.allbits?(Constants::SHF_EXECINSTR)
65
+ end
66
+
67
+ # Does this section take memory while the file runs?
68
+ #
69
+ # The ones that do are what a file is loaded by, the rest being what is
70
+ # recorded about it, its symbol names and its debugging information
71
+ # among them.
72
+ # @return [Boolean] True or false.
73
+ # @example
74
+ # elf.section_by_name('.symtab').allocated?
75
+ # #=> false
76
+ def allocated?
77
+ header.sh_flags.allbits?(Constants::SHF_ALLOC)
78
+ end
79
+
49
80
  # Is this a null section?
50
81
  # @return [Boolean] No it's not.
51
82
  def null?
@@ -7,9 +7,13 @@ require 'elftools/sections/section'
7
7
  require 'elftools/sections/dynamic_section'
8
8
  require 'elftools/sections/note_section'
9
9
  require 'elftools/sections/null_section'
10
+ require 'elftools/sections/relative_relocation_section'
10
11
  require 'elftools/sections/relocation_section'
11
12
  require 'elftools/sections/str_tab_section'
12
13
  require 'elftools/sections/sym_tab_section'
14
+ require 'elftools/sections/version_definition_section'
15
+ require 'elftools/sections/version_need_section'
16
+ require 'elftools/sections/version_section'
13
17
 
14
18
  module ELFTools
15
19
  # Defines different types of sections in this module.
@@ -27,6 +31,10 @@ module ELFTools
27
31
  when Constants::SHT_NULL then NullSection
28
32
  when Constants::SHT_NOTE then NoteSection
29
33
  when Constants::SHT_RELA, Constants::SHT_REL then RelocationSection
34
+ when Constants::SHT_RELR then RelativeRelocationSection
35
+ when Constants::SHT_GNU_versym then VersionSection
36
+ when Constants::SHT_GNU_verneed then VersionNeedSection
37
+ when Constants::SHT_GNU_verdef then VersionDefinitionSection
30
38
  when Constants::SHT_STRTAB then StrTabSection
31
39
  when Constants::SHT_SYMTAB, Constants::SHT_DYNSYM then SymTabSection
32
40
  else Section
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'elftools/sections/section'
4
4
  require 'elftools/sections/symbol'
5
+ require 'elftools/structs'
6
+ require 'elftools/version_tables'
5
7
 
6
8
  module ELFTools
7
9
  module Sections
@@ -16,14 +18,15 @@ module ELFTools
16
18
  # See {Section#initialize} for more information.
17
19
  # @param [#pos=, #read] stream
18
20
  # See {Section#initialize} for more information.
19
- # @param [Proc] section_at
20
- # The method for fetching other sections by index.
21
- # This lambda should be {ELFTools::ELFFile#section_at}.
21
+ # @param [Proc] sections
22
+ # The method for fetching the sections, which is where the names and
23
+ # the versions of these symbols are recorded. This lambda should be
24
+ # {ELFTools::ELFFile#sections}.
22
25
  # @param [Integer] machine
23
26
  # The machine of the ELF file, which decides what the fields of a
24
27
  # symbol mean. This should be +e_machine+ of the ELF header.
25
- def initialize(header, stream, section_at: nil, machine: nil, **_kwargs)
26
- @section_at = section_at
28
+ def initialize(header, stream, sections: nil, machine: nil, **_kwargs)
29
+ @sections = sections
27
30
  @machine = machine
28
31
  # For faster #symbol_by_name
29
32
  super
@@ -91,16 +94,67 @@ module ELFTools
91
94
  # Lazy loaded.
92
95
  # @return [ELFTools::Sections::StrTabSection] The string table section.
93
96
  def symstr
94
- @symstr ||= @section_at.call(header.sh_link)
97
+ @symstr ||= @sections.call[header.sh_link]
95
98
  end
96
99
 
97
100
  private
98
101
 
99
102
  def create_symbol(n)
100
- stream.pos = header.sh_offset + n * header.sh_entsize
101
- sym = Structs::ELF_sym[header.elf_class].new(endian: header.class.self_endian, offset: stream.pos)
102
- sym.read(stream)
103
- Symbol.new(sym, stream, symstr: method(:symstr), machine: @machine)
103
+ Symbol.new(
104
+ Structs::Fields.new(Structs::ELF_sym[header.elf_class], stream, table_offset + (n * entsize),
105
+ elf_class: header.elf_class, endian: header.class.self_endian),
106
+ stream, symstr: symstr_reader, machine: @machine, version: -> { version_at(n) }
107
+ )
108
+ end
109
+
110
+ # Where this table starts in the file, asked of the header once rather
111
+ # than once for every symbol read.
112
+ # @return [Integer] The file offset.
113
+ def table_offset
114
+ @table_offset ||= header.sh_offset.to_i
115
+ end
116
+
117
+ # How many bytes this table spaces its entries by.
118
+ # @return [Integer] The number.
119
+ def entsize
120
+ @entsize ||= header.sh_entsize.to_i
121
+ end
122
+
123
+ # What reads the section these symbols are named in, kept so that
124
+ # reading the table does not make one for every symbol.
125
+ # @return [Method] The method.
126
+ def symstr_reader
127
+ @symstr_reader ||= method(:symstr)
128
+ end
129
+
130
+ # The version the +n+-th symbol binds to.
131
+ # @return [ELFTools::VersionTables::Version, nil]
132
+ # The version, +nil+ unless this is the table a file is loaded by and
133
+ # the file records versions at all.
134
+ def version_at(n)
135
+ return if versions.nil?
136
+
137
+ VersionTables.version(versions.version_at(n), versions_by_index)
138
+ end
139
+
140
+ # The section recording which version each of these symbols binds to,
141
+ # which is the one naming this table.
142
+ # @return [ELFTools::Sections::VersionSection, nil] The section.
143
+ def versions
144
+ return @versions if defined?(@versions)
145
+
146
+ @versions = @sections&.call&.find do |sec|
147
+ sec.is_a?(VersionSection) && @sections.call[sec.header.sh_link].equal?(self)
148
+ end
149
+ end
150
+
151
+ # The name each index the symbols record names.
152
+ # @return [Hash{Integer => String}] The names.
153
+ def versions_by_index
154
+ @versions_by_index ||= VersionTables.names(
155
+ @sections.call.grep(VersionNeedSection).flat_map(&:requirements),
156
+ @sections.call.grep(VersionDefinitionSection).flat_map(&:definitions)
157
+ )
104
158
  end
105
159
  end
106
160
  end