ruby-macho 4.1.0 → 6.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.
@@ -60,7 +60,12 @@ module MachO
60
60
 
61
61
  @filename = filename
62
62
  @options = opts
63
- @raw_data = File.binread(@filename)
63
+ File.open(@filename, "rb") do |file|
64
+ @raw_data = file.read(Headers::MachHeader.bytesize)
65
+ @raw_data ||= ""
66
+ populate_mach_header if !opts.fetch(:decompress, false) || !Utils.compressed_magic?(@raw_data.unpack1("N"))
67
+ @raw_data << file.read.to_s
68
+ end
64
69
  populate_fields
65
70
  end
66
71
 
@@ -147,7 +152,7 @@ module MachO
147
152
  # @return [Array<LoadCommands::LoadCommand>] an array of load commands
148
153
  # corresponding to `name`
149
154
  def command(name)
150
- load_commands.select { |lc| lc.type == name.to_sym }
155
+ @load_commands_by_type.fetch(name.to_sym, []).dup
151
156
  end
152
157
 
153
158
  alias [] command
@@ -245,6 +250,7 @@ module MachO
245
250
  # The exception to this rule is when methods like {#add_command} and
246
251
  # {#delete_command} have been called with `repopulate = false`.
247
252
  def populate_fields
253
+ clear_memoization_cache
248
254
  @header = populate_mach_header
249
255
  @load_commands = populate_load_commands
250
256
  end
@@ -252,7 +258,8 @@ module MachO
252
258
  # All load commands responsible for loading dylibs.
253
259
  # @return [Array<LoadCommands::DylibCommand>] an array of DylibCommands
254
260
  def dylib_load_commands
255
- load_commands.select { |lc| LoadCommands::DYLIB_LOAD_COMMANDS.include?(lc.type) }
261
+ @dylib_load_commands ||= load_commands.select { |lc| LoadCommands::DYLIB_LOAD_COMMANDS.include?(lc.type) }
262
+ @dylib_load_commands.dup
256
263
  end
257
264
 
258
265
  # All segment load commands in the Mach-O.
@@ -271,26 +278,7 @@ module MachO
271
278
  # @note This is **not** the same as {#alignment}!
272
279
  # @note See `get_align` and `get_align_64` in `cctools/misc/lipo.c`
273
280
  def segment_alignment
274
- # special cases: 12 for x86/64/PPC/PP64, 14 for ARM/ARM64
275
- return 12 if %i[i386 x86_64 ppc ppc64].include?(cputype)
276
- return 14 if %i[arm arm64].include?(cputype)
277
-
278
- cur_align = Sections::MAX_SECT_ALIGN
279
-
280
- segments.each do |segment|
281
- if filetype == :object
282
- # start with the smallest alignment, and work our way up
283
- align = magic32? ? 2 : 3
284
- segment.sections.each do |section|
285
- align = section.align unless section.align <= align
286
- end
287
- else
288
- align = segment.guess_align
289
- end
290
- cur_align = align if align < cur_align
291
- end
292
-
293
- cur_align
281
+ @segment_alignment ||= calculate_segment_alignment
294
282
  end
295
283
 
296
284
  # The Mach-O's dylib ID, or `nil` if not a dylib.
@@ -338,7 +326,8 @@ module MachO
338
326
  # library, but at this point we're really only interested in a list of
339
327
  # unique libraries this Mach-O file links to, thus: `uniq`. (This is also
340
328
  # for consistency with `FatFile` that merges this list across all archs.)
341
- dylib_load_commands.map(&:name).map(&:to_s).uniq
329
+ @linked_dylibs ||= dylib_load_commands.map { |lc| lc.name.to_s }.uniq
330
+ @linked_dylibs.dup
342
331
  end
343
332
 
344
333
  # Changes the shared library `old_name` to `new_name`
@@ -368,7 +357,8 @@ module MachO
368
357
  # All runtime paths searched by the dynamic linker for the Mach-O.
369
358
  # @return [Array<String>] an array of all runtime paths
370
359
  def rpaths
371
- command(:LC_RPATH).map(&:path).map(&:to_s)
360
+ @rpaths ||= command(:LC_RPATH).map { |lc| lc.path.to_s }
361
+ @rpaths.dup
372
362
  end
373
363
 
374
364
  # Changes the runtime path `old_path` to `new_path`
@@ -448,6 +438,13 @@ module MachO
448
438
  rpath_cmds.reverse_each { |cmd| delete_command(cmd) }
449
439
  end
450
440
 
441
+ # Replaces the embedded signature with a pure-Ruby ad-hoc signature.
442
+ # @param identifier [String, nil] the signing identifier
443
+ # @return [void]
444
+ def codesign!(identifier: nil)
445
+ CodeSigning::AdhocSigner.new(self, identifier || CodeSigning.identifier(self, filename)).sign!
446
+ end
447
+
451
448
  # Write all Mach-O data to the given filename.
452
449
  # @param filename [String] the file to write to
453
450
  # @return [void]
@@ -475,6 +472,17 @@ module MachO
475
472
 
476
473
  private
477
474
 
475
+ # Clears all memoized values. Called when the file is repopulated.
476
+ # @return [void]
477
+ # @api private
478
+ def clear_memoization_cache
479
+ @linked_dylibs = nil
480
+ @rpaths = nil
481
+ @dylib_load_commands = nil
482
+ @load_commands_by_type = nil
483
+ @segment_alignment = nil
484
+ end
485
+
478
486
  # The file's Mach-O header structure.
479
487
  # @return [Headers::MachHeader] if the Mach-O is 32-bit
480
488
  # @return [Headers::MachHeader64] if the Mach-O is 64-bit
@@ -583,16 +591,27 @@ module MachO
583
591
 
584
592
  # All load commands in the file.
585
593
  # @return [Array<LoadCommands::LoadCommand>] an array of load commands
594
+ # @raise [TruncatedFileError] if the declared load command data is incomplete
586
595
  # @raise [LoadCommandError] if an unknown load command is encountered
596
+ # @raise [LoadCommandSizeError] if a load command's size is invalid
587
597
  # @api private
588
598
  def populate_load_commands
589
599
  permissive = options.fetch(:permissive, false)
590
600
  offset = header.class.bytesize
601
+ load_commands_end = offset + sizeofcmds
602
+ raise TruncatedFileError if load_commands_end > @raw_data.bytesize
603
+
591
604
  load_commands = []
605
+ @load_commands_by_type = Hash.new { |h, k| h[k] = [] }
592
606
 
593
607
  header.ncmds.times do
608
+ raise TruncatedFileError if offset + LoadCommands::LoadCommand.bytesize > load_commands_end
609
+
594
610
  fmt = Utils.specialize_format("L=", endianness)
595
611
  cmd = @raw_data.slice(offset, 4).unpack1(fmt)
612
+ cmdsize = @raw_data.slice(offset + 4, 4).unpack1(fmt)
613
+ raise LoadCommandSizeError, cmdsize if cmdsize % 4 != 0 || offset + cmdsize > load_commands_end
614
+
596
615
  cmd_sym = LoadCommands::LOAD_COMMANDS[cmd]
597
616
 
598
617
  raise LoadCommandError, cmd unless cmd_sym || permissive
@@ -605,16 +624,45 @@ module MachO
605
624
  LoadCommands::LoadCommand
606
625
  end
607
626
 
627
+ raise LoadCommandSizeError, cmdsize if cmdsize < klass.bytesize
628
+
608
629
  view = MachOView.new(self, @raw_data, endianness, offset)
609
630
  command = klass.new_from_bin(view)
610
631
 
611
632
  load_commands << command
633
+ @load_commands_by_type[command.type] << command
612
634
  offset += command.cmdsize
613
635
  end
614
636
 
615
637
  load_commands
616
638
  end
617
639
 
640
+ # Calculate the segment alignment for the Mach-O. Guesses conservatively.
641
+ # @return [Integer] the alignment, as a power of 2
642
+ # @api private
643
+ def calculate_segment_alignment
644
+ # special cases: 12 for x86/64/PPC/PP64, 14 for ARM/ARM64
645
+ return 12 if %i[i386 x86_64 ppc ppc64].include?(cputype)
646
+ return 14 if %i[arm arm64].include?(cputype)
647
+
648
+ cur_align = Sections::MAX_SECT_ALIGN
649
+
650
+ segments.each do |segment|
651
+ if filetype == :object
652
+ # start with the smallest alignment, and work our way up
653
+ align = magic32? ? 2 : 3
654
+ segment.sections.each do |section|
655
+ align = section.align unless section.align <= align
656
+ end
657
+ else
658
+ align = segment.guess_align
659
+ end
660
+ cur_align = align if align < cur_align
661
+ end
662
+
663
+ cur_align
664
+ end
665
+
618
666
  # The low file offset (offset to first section data).
619
667
  # @return [Integer] the offset
620
668
  # @api private
@@ -622,6 +670,9 @@ module MachO
622
670
  offset = @raw_data.size
623
671
 
624
672
  segments.each do |seg|
673
+ offset = seg.fileoff if seg.nsects.zero? && seg.fileoff.positive? &&
674
+ seg.filesize.positive? && seg.fileoff < offset
675
+
625
676
  seg.sections.each do |sect|
626
677
  next if sect.empty?
627
678
  next if sect.type?(:S_ZEROFILL)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MachO
4
- # A general purpose pseudo-structure. Described in detail in docs/machostructure-dsl.md.
4
+ # A general purpose pseudo-structure. Described in detail in machostructure-dsl-docs.md.
5
5
  # @abstract
6
6
  class MachOStructure
7
7
  # Constants used for parsing MachOStructure fields
data/lib/macho.rb CHANGED
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
-
5
3
  require_relative "macho/utils"
6
4
  require_relative "macho/structure"
7
5
  require_relative "macho/view"
8
6
  require_relative "macho/headers"
7
+ require_relative "macho/code_signing"
9
8
  require_relative "macho/load_commands"
10
9
  require_relative "macho/sections"
11
10
  require_relative "macho/macho_file"
@@ -16,7 +15,7 @@ require_relative "macho/tools"
16
15
  # The primary namespace for ruby-macho.
17
16
  module MachO
18
17
  # release version
19
- VERSION = "4.1.0"
18
+ VERSION = "6.0.0"
20
19
 
21
20
  # Opens the given filename as a MachOFile or FatFile, depending on its magic.
22
21
  # @param filename [String] the file being opened
@@ -42,20 +41,20 @@ module MachO
42
41
  file
43
42
  end
44
43
 
45
- # Signs the dylib using an ad-hoc identity.
46
- # Necessary after making any changes to a dylib, since otherwise
47
- # changing a signed file invalidates its signature.
44
+ # Signs a thin or fat Mach-O using an ad-hoc identity.
45
+ # Necessary after changing signed Mach-O data because the signature covers
46
+ # the header, load commands and all bytes preceding the signature.
48
47
  # @param filename [String] the file being opened
49
48
  # @return [void]
50
- # @raise [ModificationError] if the operation fails
49
+ # @raise [CodeSigningError] if the operation fails
51
50
  def self.codesign!(filename)
52
- raise ArgumentError, "codesign binary is not available on Linux" if RUBY_PLATFORM !~ /darwin/
53
51
  raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)
54
52
 
55
- _, _, status = Open3.capture3("codesign", "--sign", "-", "--force",
56
- "--preserve-metadata=entitlements,requirements,flags,runtime",
57
- filename)
58
-
59
- raise CodeSigningError, "#{filename}: signing failed!" unless status.success?
53
+ file = MachO.open(filename)
54
+ file.codesign!
55
+ file.write!
56
+ nil
57
+ rescue MachOError => e
58
+ raise CodeSigningError, "#{filename}: signing failed: #{e.message}"
60
59
  end
61
60
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-macho
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - William Woodruff
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-06-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: A library for viewing and manipulating Mach-O files in Ruby.
14
13
  email: william@yossarian.net
@@ -20,6 +19,7 @@ files:
20
19
  - LICENSE
21
20
  - README.md
22
21
  - lib/macho.rb
22
+ - lib/macho/code_signing.rb
23
23
  - lib/macho/exceptions.rb
24
24
  - lib/macho/fat_file.rb
25
25
  - lib/macho/headers.rb
@@ -35,7 +35,7 @@ licenses:
35
35
  - MIT
36
36
  metadata:
37
37
  rubygems_mfa_required: 'true'
38
- post_install_message:
38
+ funding_uri: https://github.com/sponsors/Homebrew
39
39
  rdoc_options: []
40
40
  require_paths:
41
41
  - lib
@@ -43,15 +43,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '2.6'
46
+ version: '3.2'
47
47
  required_rubygems_version: !ruby/object:Gem::Requirement
48
48
  requirements:
49
49
  - - ">="
50
50
  - !ruby/object:Gem::Version
51
51
  version: '0'
52
52
  requirements: []
53
- rubygems_version: 3.4.10
54
- signing_key:
53
+ rubygems_version: 4.0.10
55
54
  specification_version: 4
56
55
  summary: ruby-macho - Mach-O file analyzer.
57
56
  test_files: []