ruby-macho 2.5.1 → 4.0.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.
@@ -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: []