elf_utils 0.3.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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.standard.yml +3 -0
  4. data/CODE_OF_CONDUCT.md +132 -0
  5. data/CONTRIBUTING.md +55 -0
  6. data/Gemfile +23 -0
  7. data/LICENSE.txt +21 -0
  8. data/MAINTAINERS.md +3 -0
  9. data/README.md +126 -0
  10. data/Rakefile +76 -0
  11. data/SECURITY.md +57 -0
  12. data/elf_utils.gemspec +41 -0
  13. data/ext/elf_utils/elf_utils.c +53 -0
  14. data/ext/elf_utils/extconf.rb +3 -0
  15. data/lib/elf_utils/elf_file.rb +312 -0
  16. data/lib/elf_utils/section/base.rb +77 -0
  17. data/lib/elf_utils/section/debug_abbrev/abbreviation.rb +171 -0
  18. data/lib/elf_utils/section/debug_abbrev/abbreviation_table.rb +27 -0
  19. data/lib/elf_utils/section/debug_abbrev.rb +15 -0
  20. data/lib/elf_utils/section/debug_addr.rb +9 -0
  21. data/lib/elf_utils/section/debug_arange.rb +54 -0
  22. data/lib/elf_utils/section/debug_info/compilation_unit.rb +189 -0
  23. data/lib/elf_utils/section/debug_info/debug_str_offsets_ref.rb +15 -0
  24. data/lib/elf_utils/section/debug_info/debug_str_ref.rb +17 -0
  25. data/lib/elf_utils/section/debug_info/die/base.rb +130 -0
  26. data/lib/elf_utils/section/debug_info/die.rb +470 -0
  27. data/lib/elf_utils/section/debug_info/die_ref.rb +22 -0
  28. data/lib/elf_utils/section/debug_info/header.rb +26 -0
  29. data/lib/elf_utils/section/debug_info.rb +93 -0
  30. data/lib/elf_utils/section/debug_line/line_number_program/header.rb +48 -0
  31. data/lib/elf_utils/section/debug_line/line_number_program/state_machine.rb +206 -0
  32. data/lib/elf_utils/section/debug_line/line_number_program.rb +134 -0
  33. data/lib/elf_utils/section/debug_line.rb +35 -0
  34. data/lib/elf_utils/section/debug_ranges.rb +22 -0
  35. data/lib/elf_utils/section/debug_str_offsets.rb +16 -0
  36. data/lib/elf_utils/section/dynsym.rb +14 -0
  37. data/lib/elf_utils/section/strtab.rb +9 -0
  38. data/lib/elf_utils/section/symtab.rb +11 -0
  39. data/lib/elf_utils/section.rb +50 -0
  40. data/lib/elf_utils/segment/base.rb +72 -0
  41. data/lib/elf_utils/segment.rb +9 -0
  42. data/lib/elf_utils/string_pread.rb +18 -0
  43. data/lib/elf_utils/symbol.rb +144 -0
  44. data/lib/elf_utils/types/dwarf/expression.rb +34 -0
  45. data/lib/elf_utils/types/dwarf.rb +639 -0
  46. data/lib/elf_utils/types/dwarf32/v2.rb +44 -0
  47. data/lib/elf_utils/types/dwarf32/v3.rb +40 -0
  48. data/lib/elf_utils/types/dwarf32/v4.rb +41 -0
  49. data/lib/elf_utils/types/dwarf32/v5.rb +44 -0
  50. data/lib/elf_utils/types/dwarf32.rb +12 -0
  51. data/lib/elf_utils/types/dwarf64/v3.rb +42 -0
  52. data/lib/elf_utils/types/dwarf64/v4.rb +43 -0
  53. data/lib/elf_utils/types/dwarf64/v5.rb +46 -0
  54. data/lib/elf_utils/types/dwarf64.rb +8 -0
  55. data/lib/elf_utils/types/sleb128.rb +66 -0
  56. data/lib/elf_utils/types/uleb128.rb +56 -0
  57. data/lib/elf_utils/types/unit_length.rb +51 -0
  58. data/lib/elf_utils/types.rb +328 -0
  59. data/lib/elf_utils/version.rb +5 -0
  60. data/lib/elf_utils.rb +83 -0
  61. data/sig/elf_utils.rbs +4 -0
  62. metadata +120 -0
@@ -0,0 +1,206 @@
1
+ class ElfUtils::Section::DebugLine
2
+ class LineNumberProgram::StateMachine
3
+ include CTypes::Helpers
4
+
5
+ StandardOpCodes = ElfUtils::Types::Dwarf::StandardOpCodes
6
+ ExtendedOpCodes = ElfUtils::Types::Dwarf::ExtendedOpCodes
7
+ ULEB128 = ElfUtils::Types::ULEB128
8
+ SLEB128 = ElfUtils::Types::SLEB128
9
+
10
+ StateRegisters = Struct.new("StateRegisters", :address, :op_index, :file,
11
+ :line, :column, :is_stmt, :basic_block, :end_sequence, :prologue_end,
12
+ :epilogue_begin, :isa, :discriminator, keyword_init: true) do
13
+ def initialize(args)
14
+ # save off values we need for `#advance`
15
+ @maximum_operations_per_instruction =
16
+ args.delete(:maximum_operations_per_instruction)
17
+ @minimum_instruction_length = args.delete(:minimum_instruction_length)
18
+
19
+ # add the hard-coded defaults
20
+ args.merge!(
21
+ address: 0,
22
+ op_index: 0,
23
+ file: 1,
24
+ line: 1,
25
+ column: 0,
26
+ basic_block: false,
27
+ end_sequence: false,
28
+ prologue_end: false,
29
+ epilogue_begin: false,
30
+ isa: 0,
31
+ discriminator: 0
32
+ )
33
+
34
+ # save off the initial values to support `#reset`
35
+ @initial_values = args
36
+ super
37
+ end
38
+
39
+ # advance `address` & `op_index` by the provided `operation_advance`
40
+ def advance(operation_advance)
41
+ self.address += @minimum_instruction_length *
42
+ ((op_index + operation_advance) /
43
+ @maximum_operations_per_instruction)
44
+ self.op_index = (op_index + operation_advance) %
45
+ @maximum_operations_per_instruction
46
+ end
47
+
48
+ # reset the registers to their initial values
49
+ def reset
50
+ @initial_values.each { |k, v| self[k] = v }
51
+ end
52
+
53
+ def flags
54
+ flags = []
55
+ %i[is_stmt basic_block end_sequence prologue_end epilogue_begin]
56
+ .each { |f| flags << f if send(f) }
57
+ flags
58
+ end
59
+
60
+ def to_s
61
+ "0x%016x %6d %6d %6d %3d %13d %s" % [
62
+ address, line, column, file, isa, discriminator, flags.join(" ")
63
+ ]
64
+ end
65
+ end
66
+
67
+ def initialize(program)
68
+ @opcode_base = program.header.opcode_base
69
+ @standard_opcode_lengths = program.standard_opcode_lengths
70
+ @special_opcodes = build_special_opcode_table(program.header)
71
+ @addr_type = program.file.elf_type(:Addr)
72
+
73
+ # initial state
74
+ @initial_registers = StateRegisters.new(
75
+ minimum_instruction_length: program.header.minimum_instruction_length,
76
+ maximum_operations_per_instruction: (program.header.version >= 4) ?
77
+ program.header.maximum_operations_per_instruction : 1,
78
+ is_stmt: program.header.default_is_stmt == 1
79
+ ).freeze
80
+ end
81
+
82
+ def process(opcodes)
83
+ buf = opcodes
84
+ opcode_base = @opcode_base
85
+ matrix = []
86
+ registers = @initial_registers.dup
87
+
88
+ # TODO merge unpack & eval steps for extended & special opcodes
89
+ until buf.empty?
90
+ byte = uint8.unpack(buf)
91
+ if byte == 0
92
+ opcode, args, buf = unpack_extended_op(buf[1..])
93
+ eval_extended_opcode(matrix, registers, opcode, args)
94
+ elsif byte < opcode_base
95
+ buf = eval_standard_opcode(buf, matrix, registers)
96
+ else
97
+ opcode, buf = byte, buf[1..]
98
+ opcode = @special_opcodes[opcode]
99
+ eval_special_opcode(matrix, registers, opcode)
100
+ end
101
+ end
102
+
103
+ matrix
104
+ end
105
+
106
+ private
107
+
108
+ # construct a special opcode table to lookup the values for each special
109
+ # opcode
110
+ def build_special_opcode_table(program_header)
111
+ opcode_base = program_header.opcode_base
112
+
113
+ (opcode_base..255).each_with_object([]) do |opcode, o|
114
+ adjusted_opcode = opcode - opcode_base
115
+
116
+ o[opcode] = {
117
+ line_increment: program_header.line_base +
118
+ (adjusted_opcode % program_header.line_range),
119
+ operation_advance: adjusted_opcode / program_header.line_range
120
+ }
121
+ o
122
+ end
123
+ end
124
+
125
+ def unpack_extended_op(buf)
126
+ size, buf = ULEB128.unpack_one(buf)
127
+ opcode, buf = uint8.unpack_one(buf)
128
+ args, buf = if size > 1
129
+ string(size - 1, trim: false).unpack_one(buf)
130
+ else
131
+ ["", buf]
132
+ end
133
+ [ExtendedOpCodes[opcode] || opcode, args, buf]
134
+ end
135
+
136
+ def eval_special_opcode(matrix, reg, opcode)
137
+ reg.advance(opcode[:operation_advance])
138
+ reg.line += opcode[:line_increment]
139
+ matrix << reg.dup
140
+ reg.basic_block = false
141
+ reg.prologue_end = false
142
+ reg.epilogue_begin = false
143
+ reg.discriminator = 0
144
+ end
145
+
146
+ def eval_standard_opcode(buf, matrix, reg)
147
+ opcode, buf = uint8.unpack_one(buf)
148
+ case StandardOpCodes[opcode]
149
+ when :copy
150
+ matrix << reg.dup
151
+ reg.discriminator = 0
152
+ reg.basic_block = false
153
+ reg.prologue_end = false
154
+ reg.epilogue_begin = false
155
+ when :advance_pc
156
+ operation_advance, buf = ULEB128.unpack_one(buf)
157
+ reg.advance(operation_advance)
158
+ when :advance_line
159
+ lines, buf = SLEB128.unpack_one(buf)
160
+ reg.line += lines
161
+ when :set_file
162
+ reg.file, buf = ULEB128.unpack_one(buf)
163
+ when :set_column
164
+ reg.column, buf = ULEB128.unpack_one(buf)
165
+ when :negate_stmt
166
+ reg.is_stmt = !reg.is_stmt
167
+ when :set_basic_block
168
+ reg.basic_block = true
169
+ when :const_add_pc
170
+ reg.advance(@special_opcodes[255][:operation_advance])
171
+ when :fixed_advance_pc
172
+ offset, buf = uint16.unpack_one(buf)
173
+ reg.address += offset
174
+ reg.op_index = 0
175
+ when :set_prologue_end
176
+ reg.prologue_end = true
177
+ when :set_epilogue_begin
178
+ reg.epilogue_begin = true
179
+ when :set_isa
180
+ reg.isa, buf = ULEB128.unpack_one(buf)
181
+ else
182
+ # for unsupported opcodes, skip the arguments
183
+ _, buf = array(ULEB128, @standard_opcode_lengths[opcode])
184
+ .unpack_one(buf)
185
+ end
186
+
187
+ buf
188
+ end
189
+
190
+ def eval_extended_opcode(matrix, reg, opcode, args)
191
+ case opcode
192
+ when :end_sequence
193
+ reg.end_sequence = true
194
+ matrix << reg.dup
195
+ reg.reset
196
+ when :set_address
197
+ reg.address = @addr_type.unpack(args)
198
+ reg.op_index = 0
199
+ when :set_discriminator
200
+ reg.discriminator = ULEB128.unpack(args)
201
+ else
202
+ raise ElfUtils::Error, "unknown opcode: %p %p" % [opcode, args]
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,134 @@
1
+ class ElfUtils::Section::DebugLine
2
+ class LineNumberProgram
3
+ include CTypes::Helpers
4
+
5
+ def initialize(file:, offset:, header:, body:)
6
+ @file = file
7
+ @offset = offset
8
+ @header = header
9
+ @body = body
10
+ end
11
+ attr_reader :body, :file, :header
12
+
13
+ def addr_type
14
+ @header.addr_type
15
+ end
16
+
17
+ def standard_opcode_lengths
18
+ unpack_header_tail unless @standard_opcode_lengths
19
+ @standard_opcode_lengths
20
+ end
21
+
22
+ def directories
23
+ unpack_header_tail unless @directories
24
+ @directories
25
+ end
26
+
27
+ def file_names
28
+ unpack_header_tail unless @file_names
29
+ @file_names
30
+ end
31
+
32
+ def file_name(index)
33
+ file_name = file_names[index]
34
+ dir = directories[file_name[:directory_index]]
35
+ File.join(dir[:path], file_name[:path])
36
+ end
37
+
38
+ def address_table
39
+ @address_table ||= begin
40
+ state = StateMachine.new(self)
41
+ state.process(body).each_with_object({}) do |entry, o|
42
+ o[entry.address] = entry.freeze
43
+ end.freeze
44
+ end
45
+ end
46
+
47
+ def source_location(addr)
48
+ entry = address_table[addr] || return
49
+ {
50
+ file: file_name(entry.file),
51
+ line: entry.line,
52
+ column: entry.column
53
+ }
54
+ end
55
+
56
+ private
57
+
58
+ def unpack_header_tail
59
+ buf = header._rest
60
+ @standard_opcode_lengths, buf = array(uint8, header.opcode_base - 1)
61
+ .unpack_one(buf)
62
+ @standard_opcode_lengths.unshift(nil)
63
+
64
+ # unpack directory entries
65
+ if header.version < 5
66
+ @directories, buf = unpack_terminated_entries(buf,
67
+ Header::INCLUDE_DIRECTORIES_FORMAT)
68
+
69
+ # Prior to DWARF v5, the include directories were 1-indexed. Insert a
70
+ # nil at index 0, shifting all the entires up one index.
71
+ @directories.unshift(nil)
72
+ else
73
+ format, count, buf = unpack_format_and_count(buf)
74
+ @directories, buf = unpack_count_entries(buf, format, count)
75
+ end
76
+
77
+ # unpack file_name entries
78
+ if header.version < 5
79
+ @file_names, = unpack_terminated_entries(buf, Header::FILE_NAMES_FORMAT)
80
+ @file_names.unshift(nil)
81
+ else
82
+ format, count, buf = unpack_format_and_count(buf)
83
+ @file_names, = unpack_count_entries(buf, format, count)
84
+ end
85
+ end
86
+
87
+ def unpack_format_and_count(buf)
88
+ format_count, buf = uint8.unpack_one(buf)
89
+ format, buf = array(Header::EntryFormat, format_count).unpack_one(buf)
90
+ count, buf = ElfUtils::Types::ULEB128.unpack_one(buf)
91
+ [format, count, buf]
92
+ end
93
+
94
+ def unpack_count_entries(buf, format, count)
95
+ entries = count.times.map do
96
+ entry, buf = unpack_entry(buf, format)
97
+ entry
98
+ end
99
+ [entries, buf]
100
+ end
101
+
102
+ def unpack_terminated_entries(buf, format)
103
+ entries = []
104
+ until buf[0] == "\0"
105
+ entry, buf = unpack_entry(buf, format)
106
+ entries << entry
107
+ end
108
+ [entries, buf[1..]]
109
+ end
110
+
111
+ def unpack_entry(buf, format)
112
+ entry = {}
113
+ format.each do |field|
114
+ case field.form
115
+ when :string
116
+ entry[field.type], buf = string.terminated.unpack_one(buf)
117
+ when :line_strp
118
+ offset, buf = addr_type.unpack_one(buf)
119
+ entry[field.type] = @file.section(".debug_line_str")[offset]
120
+ when :udata
121
+ entry[field.type], buf = ElfUtils::Types::ULEB128.unpack_one(buf)
122
+ when :data16
123
+ entry[field.type], buf = string(16, trim: false).unpack_one(buf)
124
+ else
125
+ raise ElfUtils::Error, "unsupported entry field form: %p" % [field]
126
+ end
127
+ end
128
+ [entry, buf]
129
+ end
130
+ end
131
+ end
132
+
133
+ require_relative "line_number_program/header"
134
+ require_relative "line_number_program/state_machine"
@@ -0,0 +1,35 @@
1
+ module ElfUtils
2
+ class Section::DebugLine < Section::Base
3
+ def line_number_programs
4
+ out = []
5
+ buf = bytes
6
+ until buf.empty?
7
+ program, buf = unpack_line_number_program(buf)
8
+ out << program
9
+ end
10
+ out
11
+ end
12
+
13
+ def line_number_program(offset)
14
+ program, = unpack_line_number_program(bytes[offset..])
15
+ program
16
+ end
17
+
18
+ private
19
+
20
+ def unpack_line_number_program(buf)
21
+ header, rest = LineNumberProgram::Header
22
+ .with_endian(@file.endian)
23
+ .unpack_one(buf)
24
+ header.freeze
25
+ header_size = buf.size - rest.size
26
+ body_size = header.unit.unit_size - header_size
27
+ body = rest[0, body_size]
28
+ program = LineNumberProgram
29
+ .new(file: @file, offset:, header: header.inner, body:)
30
+ [program, buf[header.unit.unit_size..]]
31
+ end
32
+ end
33
+ end
34
+
35
+ require_relative "debug_line/line_number_program"
@@ -0,0 +1,22 @@
1
+ module ElfUtils
2
+ class Section::DebugRanges < Section::Base
3
+ include CTypes::Helpers
4
+
5
+ # get the range list at a given offset
6
+ def get(offset:, base:)
7
+ array(array(@file.addr_type, 2), terminator: [0, 0])
8
+ .unpack(bytes[offset..])
9
+ .map do |first, last|
10
+ if first == @file.addr_type.max
11
+ # TODO figure out how to generate a test file that makes use of
12
+ # DW_AT_ranges & has a base address range so we can add tests for
13
+ # this.
14
+ base = last
15
+ nil
16
+ else
17
+ (first + base)..(last + base - 1)
18
+ end
19
+ end.compact
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ module ElfUtils
2
+ class Section::DebugStrOffsets < Section::Base
3
+ def get(base:, offset:, format:)
4
+ type = case format
5
+ when :dwarf32
6
+ CTypes::UInt32.with_endian(@file.endian)
7
+ when :dwarf64
8
+ CTypes::UInt64.with_endian(@file.endian)
9
+ else
10
+ raise Error, "unsupported format: %p" % [format]
11
+ end
12
+ pos = base + offset * type.size
13
+ type.unpack(bytes[pos..])
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "../symbol"
2
+
3
+ module ElfUtils
4
+ class Section::Dynsym < Section::Base
5
+ def symbols
6
+ @symbols ||= begin
7
+ dynstr = @file.section(".dynstr")
8
+ @file.elf_type(:Sym).unpack_all(bytes).map do |sym|
9
+ Symbol.new(@file, sym, dynstr)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ module ElfUtils
2
+ class Section::Strtab < Section::Base
3
+ def [](index)
4
+ @strings ||= bytes
5
+ str = @strings[index..].unpack1("Z*")
6
+ str.empty? ? nil : str
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ require_relative "../symbol"
2
+
3
+ module ElfUtils
4
+ class Section::Symtab < Section::Base
5
+ def symbols
6
+ @symbols ||= @file.elf_type(:Sym).unpack_all(bytes).map do |sym|
7
+ Symbol.new(@file, sym)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,50 @@
1
+ module ElfUtils
2
+ module Section
3
+ def self.from_header(elf_file, header)
4
+ case header.sh_type
5
+ when :strtab
6
+ return Section::Strtab.new(elf_file, header)
7
+ when :symtab
8
+ return Section::Symtab.new(elf_file, header)
9
+ when :dynsym
10
+ return Section::Dynsym.new(elf_file, header)
11
+ end
12
+
13
+ name = elf_file.shstrtab[header.sh_name]
14
+ case name
15
+ when ".debug_info"
16
+ Section::DebugInfo.new(elf_file, header)
17
+ when ".debug_abbrev"
18
+ Section::DebugAbbrev.new(elf_file, header)
19
+ when ".debug_str"
20
+ Section::Strtab.new(elf_file, header)
21
+ when ".debug_str_offsets"
22
+ Section::DebugStrOffsets.new(elf_file, header)
23
+ when ".debug_aranges"
24
+ Section::DebugArange.new(elf_file, header)
25
+ when ".debug_line"
26
+ Section::DebugLine.new(elf_file, header)
27
+ when ".debug_line_str"
28
+ Section::Strtab.new(elf_file, header)
29
+ when ".debug_addr"
30
+ Section::DebugAddr.new(elf_file, header)
31
+ when ".debug_ranges"
32
+ Section::DebugRanges.new(elf_file, header)
33
+ else
34
+ Section::Base.new(elf_file, header)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ require_relative "section/base"
41
+ require_relative "section/strtab"
42
+ require_relative "section/symtab"
43
+ require_relative "section/dynsym"
44
+ require_relative "section/debug_info"
45
+ require_relative "section/debug_abbrev"
46
+ require_relative "section/debug_str_offsets"
47
+ require_relative "section/debug_arange"
48
+ require_relative "section/debug_line"
49
+ require_relative "section/debug_addr"
50
+ require_relative "section/debug_ranges"
@@ -0,0 +1,72 @@
1
+ module ElfUtils
2
+ class Segment::Base
3
+ def initialize(file, header)
4
+ @file = file
5
+ @header = header
6
+ @offset = 0
7
+ end
8
+
9
+ def type
10
+ @header.p_type
11
+ end
12
+
13
+ def addr
14
+ @header.p_vaddr + @offset
15
+ end
16
+
17
+ def offset
18
+ @header.p_offset
19
+ end
20
+
21
+ def filesize
22
+ @header.p_filesz
23
+ end
24
+
25
+ def size
26
+ @header.p_memsz
27
+ end
28
+
29
+ def align
30
+ @header.p_align
31
+ end
32
+
33
+ def flags
34
+ @header.p_flags
35
+ end
36
+
37
+ def to_range
38
+ addr...(addr + size)
39
+ end
40
+
41
+ def inspect
42
+ @header.inspect
43
+ end
44
+
45
+ def sections
46
+ a = addr...(addr + size)
47
+ @file.sections.select do |section|
48
+ next unless section.alloc?
49
+ a.include?(section.addr) ||
50
+ a.include?(section.addr + section.size - 1)
51
+ end
52
+ end
53
+
54
+ # relocate this segement and all sections within it
55
+ #
56
+ # @param addr address for section; `nil` clears relocation
57
+ def relocate(addr)
58
+ if addr.nil?
59
+ sections.relocate(nil)
60
+ @offset = 0
61
+ return
62
+ end
63
+
64
+ # calculate the offset, then relocate the sections by that offset
65
+ offset = addr - @header.p_vaddr
66
+ sections.each do |section|
67
+ section.relocate(offset, relative: true)
68
+ end
69
+ @offset = offset
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,9 @@
1
+ module ElfUtils
2
+ module Segment
3
+ def self.from_header(elf_file, header)
4
+ Segment::Base.new(elf_file, header)
5
+ end
6
+ end
7
+ end
8
+
9
+ require_relative "segment/base"
@@ -0,0 +1,18 @@
1
+ module ElfUtils
2
+ # provide a #pread method for String
3
+ #
4
+ # This refinement is to allow String instances to be used as the underlying
5
+ # IO object for ElfFile. Intended as a lighter-weight approach than
6
+ # IOString.
7
+ module StringPread
8
+ refine String do
9
+ # read up to `maxlen` bytes from the String starting at `offset`
10
+ # @param maxlen [Integer] maximum number of bytes to read
11
+ # @param offset [Integer] offset to begin reading at
12
+ # @return [String, nil] bytes read or nil
13
+ def pread(maxlen, offset)
14
+ byteslice(offset, maxlen)
15
+ end
16
+ end
17
+ end
18
+ end