ruby-macho 2.5.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,42 +1,284 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MachO
4
- # A general purpose pseudo-structure.
4
+ # A general purpose pseudo-structure. Described in detail in docs/machostructure-dsl.md.
5
5
  # @abstract
6
6
  class MachOStructure
7
- # The String#unpack format of the data structure.
8
- # @return [String] the unpacking format
9
- # @api private
10
- FORMAT = ""
11
-
12
- # The size of the data structure, in bytes.
13
- # @return [Integer] the size, in bytes
14
- # @api private
15
- SIZEOF = 0
16
-
17
- # @return [Integer] the size, in bytes, of the represented structure.
18
- def self.bytesize
19
- self::SIZEOF
7
+ # Constants used for parsing MachOStructure fields
8
+ module Fields
9
+ # 1. All fields with empty strings and zeros aren't used
10
+ # to calculate the format and sizeof variables.
11
+ # 2. All fields with nil should provide those values manually
12
+ # via the :size parameter.
13
+
14
+ # association of field types to byte size
15
+ # @api private
16
+ BYTE_SIZE = {
17
+ # Binary slices
18
+ :string => nil,
19
+ :null_padded_string => nil,
20
+ :int32 => 4,
21
+ :uint32 => 4,
22
+ :uint64 => 8,
23
+ # Classes
24
+ :view => 0,
25
+ :lcstr => 4,
26
+ :two_level_hints_table => 0,
27
+ :tool_entries => 4,
28
+ }.freeze
29
+
30
+ # association of field types with ruby format codes
31
+ # Binary format codes can be found here:
32
+ # https://docs.ruby-lang.org/en/2.6.0/String.html#method-i-unpack
33
+ #
34
+ # The equals sign is used to manually change endianness using
35
+ # the Utils#specialize_format() method.
36
+ # @api private
37
+ FORMAT_CODE = {
38
+ # Binary slices
39
+ :string => "a",
40
+ :null_padded_string => "Z",
41
+ :int32 => "l=",
42
+ :uint32 => "L=",
43
+ :uint64 => "Q=",
44
+ # Classes
45
+ :view => "",
46
+ :lcstr => "L=",
47
+ :two_level_hints_table => "",
48
+ :tool_entries => "L=",
49
+ }.freeze
50
+
51
+ # A list of classes that must get initialized
52
+ # To add a new class append it here and add the init method to the def_class_reader method
53
+ # @api private
54
+ CLASSES_TO_INIT = %i[lcstr tool_entries two_level_hints_table].freeze
55
+
56
+ # A list of fields that don't require arguments in the initializer
57
+ # Used to calculate MachOStructure#min_args
58
+ # @api private
59
+ NO_ARG_REQUIRED = %i[two_level_hints_table].freeze
20
60
  end
21
61
 
22
- # @param endianness [Symbol] either `:big` or `:little`
23
- # @param bin [String] the string to be unpacked into the new structure
24
- # @return [MachO::MachOStructure] the resulting structure
25
- # @api private
26
- def self.new_from_bin(endianness, bin)
27
- format = Utils.specialize_format(self::FORMAT, endianness)
62
+ # map of field names to indices
63
+ @field_idxs = {}
64
+
65
+ # array of fields sizes
66
+ @size_list = []
28
67
 
29
- new(*bin.unpack(format))
68
+ # array of field format codes
69
+ @fmt_list = []
70
+
71
+ # minimum number of required arguments
72
+ @min_args = 0
73
+
74
+ # @param args [Array[Value]] list of field parameters
75
+ def initialize(*args)
76
+ raise ArgumentError, "Invalid number of arguments" if args.size < self.class.min_args
77
+
78
+ @values = args
30
79
  end
31
80
 
32
81
  # @return [Hash] a hash representation of this {MachOStructure}.
33
82
  def to_h
34
83
  {
35
84
  "structure" => {
36
- "format" => self.class::FORMAT,
85
+ "format" => self.class.format,
37
86
  "bytesize" => self.class.bytesize,
38
87
  },
39
88
  }
40
89
  end
90
+
91
+ class << self
92
+ attr_reader :min_args
93
+
94
+ # @param endianness [Symbol] either `:big` or `:little`
95
+ # @param bin [String] the string to be unpacked into the new structure
96
+ # @return [MachO::MachOStructure] the resulting structure
97
+ # @api private
98
+ def new_from_bin(endianness, bin)
99
+ format = Utils.specialize_format(self.format, endianness)
100
+
101
+ new(*bin.unpack(format))
102
+ end
103
+
104
+ def format
105
+ @format ||= @fmt_list.join
106
+ end
107
+
108
+ def bytesize
109
+ @bytesize ||= @size_list.sum
110
+ end
111
+
112
+ private
113
+
114
+ # @param subclass [Class] subclass type
115
+ # @api private
116
+ def inherited(subclass) # rubocop:disable Lint/MissingSuper
117
+ # Clone all class instance variables
118
+ field_idxs = @field_idxs.dup
119
+ size_list = @size_list.dup
120
+ fmt_list = @fmt_list.dup
121
+ min_args = @min_args.dup
122
+
123
+ # Add those values to the inheriting class
124
+ subclass.class_eval do
125
+ @field_idxs = field_idxs
126
+ @size_list = size_list
127
+ @fmt_list = fmt_list
128
+ @min_args = min_args
129
+ end
130
+ end
131
+
132
+ # @param name [Symbol] name of internal field
133
+ # @param type [Symbol] type of field in terms of binary size
134
+ # @param options [Hash] set of additonal options
135
+ # Expected options
136
+ # :size [Integer] size in bytes
137
+ # :mask [Integer] bitmask
138
+ # :unpack [String] string format
139
+ # :default [Value] default value
140
+ # :to_s [Boolean] flag for generating #to_s
141
+ # :endian [Symbol] optionally specify :big or :little endian
142
+ # :padding [Symbol] optionally specify :null padding
143
+ # @api private
144
+ def field(name, type, **options)
145
+ raise ArgumentError, "Invalid field type #{type}" unless Fields::FORMAT_CODE.key?(type)
146
+
147
+ # Get field idx for size_list and fmt_list
148
+ idx = if @field_idxs.key?(name)
149
+ @field_idxs[name]
150
+ else
151
+ @min_args += 1 unless options.key?(:default) || Fields::NO_ARG_REQUIRED.include?(type)
152
+ @field_idxs[name] = @field_idxs.size
153
+ @size_list << nil
154
+ @fmt_list << nil
155
+ @field_idxs.size - 1
156
+ end
157
+
158
+ # Update string type if padding is specified
159
+ type = :null_padded_string if type == :string && options[:padding] == :null
160
+
161
+ # Add to size_list and fmt_list
162
+ @size_list[idx] = Fields::BYTE_SIZE[type] || options[:size]
163
+ @fmt_list[idx] = if options[:endian]
164
+ Utils.specialize_format(Fields::FORMAT_CODE[type], options[:endian])
165
+ else
166
+ Fields::FORMAT_CODE[type]
167
+ end
168
+ @fmt_list[idx] += options[:size].to_s if options.key?(:size)
169
+
170
+ # Generate methods
171
+ if Fields::CLASSES_TO_INIT.include?(type)
172
+ def_class_reader(name, type, idx)
173
+ elsif options.key?(:mask)
174
+ def_mask_reader(name, idx, options[:mask])
175
+ elsif options.key?(:unpack)
176
+ def_unpack_reader(name, idx, options[:unpack])
177
+ elsif options.key?(:default)
178
+ def_default_reader(name, idx, options[:default])
179
+ else
180
+ def_reader(name, idx)
181
+ end
182
+
183
+ def_to_s(name) if options[:to_s]
184
+ end
185
+
186
+ #
187
+ # Method Generators
188
+ #
189
+
190
+ # Generates a reader method for classes that need to be initialized.
191
+ # These classes are defined in the Fields::CLASSES_TO_INIT array.
192
+ # @param name [Symbol] name of internal field
193
+ # @param type [Symbol] type of field in terms of binary size
194
+ # @param idx [Integer] the index of the field value in the @values array
195
+ # @api private
196
+ def def_class_reader(name, type, idx)
197
+ case type
198
+ when :lcstr
199
+ define_method(name) do
200
+ instance_variable_defined?("@#{name}") ||
201
+ instance_variable_set("@#{name}", LoadCommands::LoadCommand::LCStr.new(self, @values[idx]))
202
+
203
+ instance_variable_get("@#{name}")
204
+ end
205
+ when :two_level_hints_table
206
+ define_method(name) do
207
+ instance_variable_defined?("@#{name}") ||
208
+ instance_variable_set("@#{name}", LoadCommands::TwolevelHintsCommand::TwolevelHintsTable.new(view, htoffset, nhints))
209
+
210
+ instance_variable_get("@#{name}")
211
+ end
212
+ when :tool_entries
213
+ define_method(name) do
214
+ instance_variable_defined?("@#{name}") ||
215
+ instance_variable_set("@#{name}", LoadCommands::BuildVersionCommand::ToolEntries.new(view, @values[idx]))
216
+
217
+ instance_variable_get("@#{name}")
218
+ end
219
+ end
220
+ end
221
+
222
+ # Generates a reader method for fields that need to be bitmasked.
223
+ # @param name [Symbol] name of internal field
224
+ # @param idx [Integer] the index of the field value in the @values array
225
+ # @param mask [Integer] the bitmask
226
+ # @api private
227
+ def def_mask_reader(name, idx, mask)
228
+ define_method(name) do
229
+ instance_variable_defined?("@#{name}") ||
230
+ instance_variable_set("@#{name}", @values[idx] & ~mask)
231
+
232
+ instance_variable_get("@#{name}")
233
+ end
234
+ end
235
+
236
+ # Generates a reader method for fields that need further unpacking.
237
+ # @param name [Symbol] name of internal field
238
+ # @param idx [Integer] the index of the field value in the @values array
239
+ # @param unpack [String] the format code used for futher binary unpacking
240
+ # @api private
241
+ def def_unpack_reader(name, idx, unpack)
242
+ define_method(name) do
243
+ instance_variable_defined?("@#{name}") ||
244
+ instance_variable_set("@#{name}", @values[idx].unpack(unpack))
245
+
246
+ instance_variable_get("@#{name}")
247
+ end
248
+ end
249
+
250
+ # Generates a reader method for fields that have default values.
251
+ # @param name [Symbol] name of internal field
252
+ # @param idx [Integer] the index of the field value in the @values array
253
+ # @param default [Value] the default value
254
+ # @api private
255
+ def def_default_reader(name, idx, default)
256
+ define_method(name) do
257
+ instance_variable_defined?("@#{name}") ||
258
+ instance_variable_set("@#{name}", @values.size > idx ? @values[idx] : default)
259
+
260
+ instance_variable_get("@#{name}")
261
+ end
262
+ end
263
+
264
+ # Generates an attr_reader like method for a field.
265
+ # @param name [Symbol] name of internal field
266
+ # @param idx [Integer] the index of the field value in the @values array
267
+ # @api private
268
+ def def_reader(name, idx)
269
+ define_method(name) do
270
+ @values[idx]
271
+ end
272
+ end
273
+
274
+ # Generates the to_s method based on the named field.
275
+ # @param name [Symbol] name of the field
276
+ # @api private
277
+ def def_to_s(name)
278
+ define_method(:to_s) do
279
+ send(name).to_s
280
+ end
281
+ end
282
+ end
41
283
  end
42
284
  end
data/lib/macho/tools.rb CHANGED
@@ -51,6 +51,8 @@ module MachO
51
51
  # @param options [Hash]
52
52
  # @option options [Boolean] :strict (true) whether or not to fail loudly
53
53
  # with an exception if the change cannot be performed
54
+ # @option options [Boolean] :uniq (false) whether or not to change duplicate
55
+ # rpaths simultaneously
54
56
  # @return [void]
55
57
  def self.change_rpath(filename, old_path, new_path, options = {})
56
58
  file = MachO.open(filename)
@@ -80,6 +82,8 @@ module MachO
80
82
  # @param options [Hash]
81
83
  # @option options [Boolean] :strict (true) whether or not to fail loudly
82
84
  # with an exception if the change cannot be performed
85
+ # @option options [Boolean] :uniq (false) whether or not to delete duplicate
86
+ # rpaths simultaneously
83
87
  # @return [void]
84
88
  def self.delete_rpath(filename, old_path, options = {})
85
89
  file = MachO.open(filename)
data/lib/macho/utils.rb CHANGED
@@ -121,5 +121,12 @@ module MachO
121
121
  def self.big_magic?(num)
122
122
  [Headers::MH_MAGIC, Headers::MH_MAGIC_64].include? num
123
123
  end
124
+
125
+ # Compares the given number to the known magic number for a compressed Mach-O slice.
126
+ # @param num [Integer] the number being checked
127
+ # @return [Boolean] whether `num` is a valid compressed header magic number
128
+ def self.compressed_magic?(num)
129
+ num == Headers::COMPRESSED_MAGIC
130
+ end
124
131
  end
125
132
  end
data/lib/macho/view.rb CHANGED
@@ -3,6 +3,9 @@
3
3
  module MachO
4
4
  # A representation of some unspecified Mach-O data.
5
5
  class MachOView
6
+ # @return [MachOFile] that this view belongs to
7
+ attr_reader :macho_file
8
+
6
9
  # @return [String] the raw Mach-O data
7
10
  attr_reader :raw_data
8
11
 
@@ -13,10 +16,12 @@ module MachO
13
16
  attr_reader :offset
14
17
 
15
18
  # Creates a new MachOView.
19
+ # @param macho_file [MachOFile] the file this view slice is from
16
20
  # @param raw_data [String] the raw Mach-O data
17
21
  # @param endianness [Symbol] the endianness of the data
18
22
  # @param offset [Integer] the offset of the relevant data
19
- def initialize(raw_data, endianness, offset)
23
+ def initialize(macho_file, raw_data, endianness, offset)
24
+ @macho_file = macho_file
20
25
  @raw_data = raw_data
21
26
  @endianness = endianness
22
27
  @offset = offset
@@ -29,5 +34,9 @@ module MachO
29
34
  "offset" => offset,
30
35
  }
31
36
  end
37
+
38
+ def inspect
39
+ "#<#{self.class}:0x#{(object_id << 1).to_s(16)} @endianness=#{@endianness.inspect}, @offset=#{@offset.inspect}, length=#{@raw_data.length}>"
40
+ end
32
41
  end
33
42
  end
data/lib/macho.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "open3"
4
4
 
5
+ require_relative "macho/utils"
5
6
  require_relative "macho/structure"
6
7
  require_relative "macho/view"
7
8
  require_relative "macho/headers"
@@ -10,13 +11,12 @@ require_relative "macho/sections"
10
11
  require_relative "macho/macho_file"
11
12
  require_relative "macho/fat_file"
12
13
  require_relative "macho/exceptions"
13
- require_relative "macho/utils"
14
14
  require_relative "macho/tools"
15
15
 
16
16
  # The primary namespace for ruby-macho.
17
17
  module MachO
18
18
  # release version
19
- VERSION = "2.5.1"
19
+ VERSION = "4.0.0"
20
20
 
21
21
  # Opens the given filename as a MachOFile or FatFile, depending on its magic.
22
22
  # @param filename [String] the file being opened
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-macho
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.1
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - William Woodruff
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-15 00:00:00.000000000 Z
11
+ date: 2023-07-25 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A library for viewing and manipulating Mach-O files in Ruby.
14
14
  email: william@yossarian.net
@@ -33,8 +33,9 @@ files:
33
33
  homepage: https://github.com/Homebrew/ruby-macho
34
34
  licenses:
35
35
  - MIT
36
- metadata: {}
37
- post_install_message:
36
+ metadata:
37
+ rubygems_mfa_required: 'true'
38
+ post_install_message:
38
39
  rdoc_options: []
39
40
  require_paths:
40
41
  - lib
@@ -42,15 +43,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
42
43
  requirements:
43
44
  - - ">="
44
45
  - !ruby/object:Gem::Version
45
- version: '2.5'
46
+ version: '2.6'
46
47
  required_rubygems_version: !ruby/object:Gem::Requirement
47
48
  requirements:
48
49
  - - ">="
49
50
  - !ruby/object:Gem::Version
50
51
  version: '0'
51
52
  requirements: []
52
- rubygems_version: 3.0.3
53
- signing_key:
53
+ rubygems_version: 3.4.10
54
+ signing_key:
54
55
  specification_version: 4
55
56
  summary: ruby-macho - Mach-O file analyzer.
56
57
  test_files: []