diff --git a/.gitignore b/.gitignore index d332b2de0..47d0c9425 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ .ruby-version .idea/ .vscode/ +mise.toml # macOS metadata file .DS_Store diff --git a/Gemfile.lock b/Gemfile.lock index 355a76fcf..45d656b79 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -67,4 +67,4 @@ DEPENDENCIES simplecov-cobertura BUNDLED WITH - 2.3.5 + 4.0.6 diff --git a/lib/macho/fat_file.rb b/lib/macho/fat_file.rb index 637e1c5aa..0439ab5f7 100644 --- a/lib/macho/fat_file.rb +++ b/lib/macho/fat_file.rb @@ -165,7 +165,7 @@ def populate_fields # All load commands responsible for loading dylibs in the file's Mach-O's. # @return [Array] an array of DylibCommands def dylib_load_commands - machos.map(&:dylib_load_commands).flatten + machos.flat_map(&:dylib_load_commands) end # Changes the file's dylib ID to `new_id`. If the file is not a dylib, @@ -199,7 +199,7 @@ def linked_dylibs # Individual architectures in a fat binary can link to different subsets # of libraries, but at this point we want to have the full picture, i.e. # the union of all libraries used by all architectures. - machos.map(&:linked_dylibs).flatten.uniq + machos.flat_map(&:linked_dylibs).uniq end # Changes all dependent shared library install names from `old_name` to @@ -229,7 +229,7 @@ def change_install_name(old_name, new_name, options = {}) # @see MachOFile#rpaths def rpaths # Can individual architectures have different runtime paths? - machos.map(&:rpaths).flatten.uniq + machos.flat_map(&:rpaths).uniq end # Change the runtime path `old_path` to `new_path` in the file's Mach-Os. diff --git a/lib/macho/macho_file.rb b/lib/macho/macho_file.rb index 15dadbcf7..1c1702b04 100644 --- a/lib/macho/macho_file.rb +++ b/lib/macho/macho_file.rb @@ -147,7 +147,7 @@ def cpusubtype # @return [Array] an array of load commands # corresponding to `name` def command(name) - load_commands.select { |lc| lc.type == name.to_sym } + @load_commands_by_type.fetch(name.to_sym, []) end alias [] command @@ -245,6 +245,7 @@ def delete_command(lc, options = {}) # The exception to this rule is when methods like {#add_command} and # {#delete_command} have been called with `repopulate = false`. def populate_fields + clear_memoization_cache @header = populate_mach_header @load_commands = populate_load_commands end @@ -252,14 +253,14 @@ def populate_fields # All load commands responsible for loading dylibs. # @return [Array] an array of DylibCommands def dylib_load_commands - load_commands.select { |lc| LoadCommands::DYLIB_LOAD_COMMANDS.include?(lc.type) } + @dylib_load_commands ||= load_commands.select { |lc| LoadCommands::DYLIB_LOAD_COMMANDS.include?(lc.type) } end # All segment load commands in the Mach-O. # @return [Array] if the Mach-O is 32-bit # @return [Array] if the Mach-O is 64-bit def segments - if magic32? + @segments ||= if magic32? command(:LC_SEGMENT) else command(:LC_SEGMENT_64) @@ -271,26 +272,7 @@ def segments # @note This is **not** the same as {#alignment}! # @note See `get_align` and `get_align_64` in `cctools/misc/lipo.c` def segment_alignment - # special cases: 12 for x86/64/PPC/PP64, 14 for ARM/ARM64 - return 12 if %i[i386 x86_64 ppc ppc64].include?(cputype) - return 14 if %i[arm arm64].include?(cputype) - - cur_align = Sections::MAX_SECT_ALIGN - - segments.each do |segment| - if filetype == :object - # start with the smallest alignment, and work our way up - align = magic32? ? 2 : 3 - segment.sections.each do |section| - align = section.align unless section.align <= align - end - else - align = segment.guess_align - end - cur_align = align if align < cur_align - end - - cur_align + @segment_alignment ||= calculate_segment_alignment end # The Mach-O's dylib ID, or `nil` if not a dylib. @@ -338,7 +320,7 @@ def linked_dylibs # library, but at this point we're really only interested in a list of # unique libraries this Mach-O file links to, thus: `uniq`. (This is also # for consistency with `FatFile` that merges this list across all archs.) - dylib_load_commands.map(&:name).map(&:to_s).uniq + @linked_dylibs ||= dylib_load_commands.map { |lc| lc.name.to_s }.uniq end # Changes the shared library `old_name` to `new_name` @@ -368,7 +350,7 @@ def change_install_name(old_name, new_name, _options = {}) # All runtime paths searched by the dynamic linker for the Mach-O. # @return [Array] an array of all runtime paths def rpaths - command(:LC_RPATH).map(&:path).map(&:to_s) + @rpaths ||= command(:LC_RPATH).map { |lc| lc.path.to_s } end # Changes the runtime path `old_path` to `new_path` @@ -475,6 +457,18 @@ def to_h private + # Clears all memoized values. Called when the file is repopulated. + # @return [void] + # @api private + def clear_memoization_cache + @linked_dylibs = nil + @rpaths = nil + @dylib_load_commands = nil + @segments = nil + @load_commands_by_type = nil + @segment_alignment = nil + end + # The file's Mach-O header structure. # @return [Headers::MachHeader] if the Mach-O is 32-bit # @return [Headers::MachHeader64] if the Mach-O is 64-bit @@ -589,6 +583,7 @@ def populate_load_commands permissive = options.fetch(:permissive, false) offset = header.class.bytesize load_commands = [] + @load_commands_by_type = Hash.new { |h, k| h[k] = [] } header.ncmds.times do fmt = Utils.specialize_format("L=", endianness) @@ -609,12 +604,39 @@ def populate_load_commands command = klass.new_from_bin(view) load_commands << command + @load_commands_by_type[command.type] << command offset += command.cmdsize end load_commands end + # Calculate the segment alignment for the Mach-O. Guesses conservatively. + # @return [Integer] the alignment, as a power of 2 + # @api private + def calculate_segment_alignment + # special cases: 12 for x86/64/PPC/PP64, 14 for ARM/ARM64 + return 12 if %i[i386 x86_64 ppc ppc64].include?(cputype) + return 14 if %i[arm arm64].include?(cputype) + + cur_align = Sections::MAX_SECT_ALIGN + + segments.each do |segment| + if filetype == :object + # start with the smallest alignment, and work our way up + align = magic32? ? 2 : 3 + segment.sections.each do |section| + align = section.align unless section.align <= align + end + else + align = segment.guess_align + end + cur_align = align if align < cur_align + end + + cur_align + end + # The low file offset (offset to first section data). # @return [Integer] the offset # @api private