emerge 0.3.0 → 0.5.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.
@@ -0,0 +1,325 @@
1
+ require 'macho'
2
+
3
+ module EmergeCLI
4
+ class MachOParser
5
+ TYPE_METADATA_KIND_MASK = 0x7 << 3
6
+ TYPE_METADATA_KIND_SHIFT = 3
7
+
8
+ # Bind Codes
9
+ BIND_OPCODE_MASK = 0xF0
10
+ BIND_IMMEDIATE_MASK = 0x0F
11
+ BIND_OPCODE_DONE = 0x00
12
+ BIND_OPCODE_SET_DYLIB_ORDINAL_IMM = 0x10
13
+ BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB = 0x20
14
+ BIND_OPCODE_SET_DYLIB_SPECIAL_IMM = 0x30
15
+ BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM = 0x40
16
+ BIND_OPCODE_SET_TYPE_IMM = 0x50
17
+ BIND_OPCODE_SET_ADDEND_SLEB = 0x60
18
+ BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB = 0x70
19
+ BIND_OPCODE_ADD_ADDR_ULEB = 0x80
20
+ BIND_OPCODE_DO_BIND = 0x90
21
+ BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB = 0xA0
22
+ BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED = 0xB0
23
+ BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB = 0xC0
24
+
25
+ UINT64_SIZE = 8
26
+ UINT64_MAX_VALUE = 0xFFFFFFFFFFFFFFFF
27
+
28
+ def load_binary(binary_path)
29
+ @macho_file = MachO::MachOFile.new(binary_path)
30
+ @binary_data = File.binread(binary_path)
31
+ end
32
+
33
+ def read_linkedit_data_command
34
+ chained_fixups_command = nil
35
+ @macho_file.load_commands.each do |lc|
36
+ chained_fixups_command = lc if lc.type == :LC_DYLD_CHAINED_FIXUPS
37
+ end
38
+
39
+ if chained_fixups_command.nil?
40
+ Logger.debug 'No LC_DYLD_CHAINED_FIXUPS found'
41
+ return false, []
42
+ end
43
+
44
+ # linkedit_data_command
45
+ _, _, dataoff, datasize = @binary_data[chained_fixups_command.offset, 16].unpack('L<L<L<L<')
46
+
47
+ header = @binary_data[dataoff, datasize].unpack('L<L<L<L<L<L<L<')
48
+ # dyld_chained_fixups_header
49
+ _, _, imports_offset, symbols_offset, imports_count,
50
+ imports_format, = header
51
+
52
+ imports_start = dataoff + imports_offset
53
+ symbols_start = dataoff + symbols_offset
54
+
55
+ imported_symbols = []
56
+
57
+ import_size, name_offset_proc =
58
+ case imports_format
59
+ when 1, nil # DYLD_CHAINED_IMPORT
60
+ [4, ->(ptr) { ptr.unpack1('L<') >> 9 }]
61
+ when 2 # DYLD_CHAINED_IMPORT_ADDEND
62
+ [8, ->(ptr) { ptr.unpack1('L<') >> 9 }]
63
+ when 3 # DYLD_CHAINED_IMPORT_ADDEND64
64
+ [16, ->(ptr) { ptr.unpack1('Q<') >> 32 }]
65
+ end
66
+
67
+ # Extract imported symbol names
68
+ imports_count.times do |i|
69
+ import_offset = imports_start + (i * import_size)
70
+ name_offset = name_offset_proc.call(@binary_data[import_offset, import_size])
71
+ name_start = symbols_start + name_offset
72
+ name = read_null_terminated_string(@binary_data[name_start..])
73
+ imported_symbols << name
74
+ end
75
+
76
+ [true, imported_symbols]
77
+ end
78
+
79
+ def read_dyld_info_only_command
80
+ dyld_info_only_command = nil
81
+ @macho_file.load_commands.each do |lc|
82
+ dyld_info_only_command = lc if lc.type == :LC_DYLD_INFO_ONLY
83
+ end
84
+
85
+ if dyld_info_only_command.nil?
86
+ Logger.debug 'No LC_DYLD_INFO_ONLY found'
87
+ return []
88
+ end
89
+
90
+ bound_symbols = []
91
+ start_address = dyld_info_only_command.bind_off
92
+ end_address = dyld_info_only_command.bind_off + dyld_info_only_command.bind_size
93
+ current_address = start_address
94
+
95
+ current_symbol = BoundSymbol.new(segment_offset: 0, library: nil, offset: 0, symbol: '')
96
+ while current_address < end_address
97
+ results, current_address, current_symbol = read_next_symbol(@binary_data, current_address, end_address,
98
+ current_symbol)
99
+
100
+ # Dup items to avoid pointer issues
101
+ results.each do |res|
102
+ bound_symbols << res.dup
103
+ end
104
+ end
105
+
106
+ # Filter only swift symbols starting with _$s
107
+ swift_symbols = bound_symbols.select { |bound_symbol| bound_symbol.symbol.start_with?('_$s') }
108
+
109
+ load_commands = @macho_file.load_commands.select { |lc| lc.type == :LC_SEGMENT_64 || lc.type == :LC_SEGMENT } # rubocop:disable Naming/VariableNumber
110
+
111
+ swift_symbols.each do |swift_symbol|
112
+ swift_symbol.address = load_commands[swift_symbol.segment_offset].vmaddr + swift_symbol.offset
113
+ end
114
+
115
+ swift_symbols
116
+ end
117
+
118
+ def find_protocols_in_swift_proto(use_chained_fixups, imported_symbols, bound_symbols, search_symbols)
119
+ found_section = nil
120
+ @macho_file.segments.each do |segment|
121
+ segment.sections.each do |section|
122
+ if section.segname.strip == '__TEXT' && section.sectname.strip == '__swift5_proto'
123
+ found_section = section
124
+ break
125
+ end
126
+ end
127
+ end
128
+
129
+ unless found_section
130
+ Logger.error 'The __swift5_proto section was not found.'
131
+ return false
132
+ end
133
+
134
+ start = found_section.offset
135
+ size = found_section.size
136
+ offsets_list = parse_list(@binary_data, start, size)
137
+
138
+ offsets_list.each do |relative_offset, offset_start|
139
+ type_file_address = offset_start + relative_offset
140
+ if type_file_address <= 0 || type_file_address >= @binary_data.size
141
+ Logger.error 'Invalid protocol conformance offset'
142
+ next
143
+ end
144
+
145
+ # ProtocolConformanceDescriptor -> ProtocolDescriptor
146
+ protocol_descriptor = read_little_endian_signed_integer(@binary_data, type_file_address)
147
+
148
+ # # ProtocolConformanceDescriptor -> ConformanceFlags
149
+ conformance_flags = read_little_endian_signed_integer(@binary_data, type_file_address + 12)
150
+ kind = (conformance_flags & TYPE_METADATA_KIND_MASK) >> TYPE_METADATA_KIND_SHIFT
151
+
152
+ next unless kind == 0
153
+
154
+ indirect_relative_offset = get_indirect_relative_offset(type_file_address, protocol_descriptor)
155
+
156
+ bound_symbol = bound_symbols.find { |symbol| symbol.address == indirect_relative_offset }
157
+ if bound_symbol
158
+ return true if search_symbols.include?(bound_symbol.symbol)
159
+ elsif use_chained_fixups
160
+ descriptor_offset = protocol_descriptor & ~1
161
+ jump_ptr = type_file_address + descriptor_offset
162
+
163
+ address = @binary_data[jump_ptr, 4].unpack1('I<')
164
+ symbol_name = imported_symbols[address]
165
+ return true if search_symbols.include?(symbol_name)
166
+ end
167
+ end
168
+ false
169
+ end
170
+
171
+ private
172
+
173
+ def read_next_symbol(binary_data, current_address, end_address, current_symbol)
174
+ while current_address < end_address
175
+ first_byte = read_byte(binary_data, current_address)
176
+ current_address += 1
177
+ immediate = first_byte & BIND_IMMEDIATE_MASK
178
+ opcode = first_byte & BIND_OPCODE_MASK
179
+
180
+ case opcode
181
+ when BIND_OPCODE_DONE
182
+ result = current_symbol.dup
183
+ current_symbol.segment_offset = 0
184
+ current_symbol.library = 0
185
+ current_symbol.offset = 0
186
+ current_symbol.symbol = ''
187
+ return [result], current_address, current_symbol
188
+ when BIND_OPCODE_SET_DYLIB_ORDINAL_IMM
189
+ current_symbol.library = [immediate].pack('L').unpack1('L')
190
+ when BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM
191
+ current_symbol.symbol = read_null_terminated_string(binary_data[current_address..])
192
+ # Increase current pointer
193
+ current_address += current_symbol.symbol.size + 1
194
+ when BIND_OPCODE_ADD_ADDR_ULEB
195
+ offset, new_current_address = read_uleb(@binary_data, current_address)
196
+ current_symbol.offset = (current_symbol.offset + offset) & UINT64_MAX_VALUE
197
+ current_address = new_current_address
198
+ when BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB
199
+ offset, new_current_address = read_uleb(@binary_data, current_address)
200
+ current_symbol.offset = (current_symbol.offset + offset + UINT64_SIZE) & UINT64_MAX_VALUE
201
+ current_address = new_current_address
202
+ return [current_symbol], current_address, current_symbol
203
+ when BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB
204
+ offset, current_address = read_uleb(@binary_data, current_address)
205
+ current_symbol.segment_offset = immediate
206
+ current_symbol.offset = offset
207
+ when BIND_OPCODE_SET_ADDEND_SLEB
208
+ _, current_address = read_uleb(@binary_data, current_address)
209
+ when BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED
210
+ result = current_symbol.dup
211
+ current_symbol.offset = (
212
+ current_symbol.offset + (immediate * UINT64_SIZE) + UINT64_SIZE
213
+ ) & UINT64_MAX_VALUE
214
+ return [result], current_address, current_symbol
215
+ when BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB
216
+ count, current_address = read_uleb(@binary_data, current_address)
217
+ skipping, current_address = read_uleb(@binary_data, current_address)
218
+
219
+ results = []
220
+ count.times do
221
+ results << current_symbol.dup
222
+ current_symbol.offset = (current_symbol.offset + skipping + UINT64_SIZE) & UINT64_MAX_VALUE
223
+ end
224
+
225
+ return results, current_address, current_symbol
226
+ when BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB
227
+ count, current_address = read_uleb(@binary_data, current_address)
228
+ current_symbol.library = count
229
+ when BIND_OPCODE_DO_BIND
230
+ result = current_symbol.dup
231
+ current_symbol.offset = (current_symbol.offset + UINT64_SIZE) & UINT64_MAX_VALUE
232
+ return [result], current_address, current_symbol
233
+ end
234
+ end
235
+ [[], current_address, current_symbol]
236
+ end
237
+
238
+ def read_byte(binary_data, address)
239
+ binary_data[address, 1].unpack1('C')
240
+ end
241
+
242
+ def read_little_endian_signed_integer(binary_data, address)
243
+ binary_data[address, 4].unpack1('l<')
244
+ end
245
+
246
+ def read_uleb(binary_data, address)
247
+ next_byte = 0
248
+ size = 0
249
+ result = 0
250
+
251
+ loop do
252
+ next_byte = read_byte(binary_data, address)
253
+ address += 1
254
+ bytes = next_byte & 0x7F
255
+ shifted = bytes << (size * 7)
256
+
257
+ size += 1
258
+ result |= shifted
259
+ break if next_byte.nobits?(0x80)
260
+ end
261
+
262
+ [result, address]
263
+ end
264
+
265
+ def read_null_terminated_string(data)
266
+ data.unpack1('Z*')
267
+ end
268
+
269
+ def vm_address(file_offset, macho)
270
+ load_commands = macho.load_commands.select { |lc| lc.type == :LC_SEGMENT_64 || lc.type == :LC_SEGMENT } # rubocop:disable Naming/VariableNumber
271
+ load_commands.each do |lc|
272
+ next unless file_offset >= lc.fileoff && file_offset < (lc.fileoff + lc.filesize)
273
+ unless lc.respond_to?(:sections)
274
+ Logger.error 'Load command does not support sections function'
275
+ next
276
+ end
277
+
278
+ lc.sections.each do |section|
279
+ if file_offset >= section.offset && file_offset < (section.offset) + section.size
280
+ return section.addr + (file_offset - section.offset)
281
+ end
282
+ end
283
+ end
284
+ nil
285
+ end
286
+
287
+ def parse_list(bytes, start, size)
288
+ data_pointer = bytes[start..]
289
+ file_offset = start
290
+ pointer_size = 4
291
+ class_pointers = []
292
+
293
+ (size / pointer_size).to_i.times do
294
+ pointer = data_pointer.unpack1('l<')
295
+ class_pointers << [pointer, file_offset]
296
+ data_pointer = data_pointer[pointer_size..]
297
+ file_offset += pointer_size
298
+ end
299
+
300
+ class_pointers
301
+ end
302
+
303
+ def get_indirect_relative_offset(type_file_address, protocol_descriptor)
304
+ vm_start = vm_address(type_file_address, @macho_file)
305
+ return nil if vm_start.nil?
306
+ if (vm_start + protocol_descriptor).odd?
307
+ (vm_start + protocol_descriptor) & ~1
308
+ elsif vm_start + protocol_descriptor > 0
309
+ vm_start + protocol_descriptor
310
+ end
311
+ end
312
+ end
313
+
314
+ class BoundSymbol
315
+ attr_accessor :segment_offset, :library, :offset, :symbol, :address
316
+
317
+ def initialize(segment_offset:, library:, offset:, symbol:)
318
+ @segment_offset = segment_offset
319
+ @library = library
320
+ @offset = offset
321
+ @symbol = symbol
322
+ @address = 0
323
+ end
324
+ end
325
+ end
data/lib/utils/network.rb CHANGED
@@ -17,16 +17,16 @@ module EmergeCLI
17
17
  @internet = Async::HTTP::Internet.new
18
18
  end
19
19
 
20
- def get(path:, headers: {})
21
- request(:get, path, nil, headers)
20
+ def get(path:, headers: {}, query: nil, max_retries: MAX_RETRIES)
21
+ request(:get, path, nil, headers, query, max_retries)
22
22
  end
23
23
 
24
- def post(path:, body:, headers: {}, query: nil)
25
- request(:post, path, body, headers, query)
24
+ def post(path:, body:, headers: {}, query: nil, max_retries: MAX_RETRIES)
25
+ request(:post, path, body, headers, query, max_retries)
26
26
  end
27
27
 
28
- def put(path:, body:, headers: {})
29
- request(:put, path, body, headers)
28
+ def put(path:, body:, headers: {}, max_retries: MAX_RETRIES)
29
+ request(:put, path, body, headers, nil, max_retries)
30
30
  end
31
31
 
32
32
  def close
@@ -35,7 +35,7 @@ module EmergeCLI
35
35
 
36
36
  private
37
37
 
38
- def request(method, path, body, custom_headers, query = nil)
38
+ def request(method, path, body, custom_headers, query = nil, max_retries = MAX_RETRIES)
39
39
  uri = if path.start_with?('http')
40
40
  URI.parse(path)
41
41
  else
@@ -71,10 +71,10 @@ module EmergeCLI
71
71
  response
72
72
  rescue StandardError => e
73
73
  retries += 1
74
- if retries <= MAX_RETRIES
74
+ if retries <= max_retries
75
75
  delay = RETRY_DELAY * retries
76
76
  error_message = e.message
77
- Logger.warn "Request failed (attempt #{retries}/#{MAX_RETRIES}): #{error_message}"
77
+ Logger.warn "Request failed (attempt #{retries}/#{max_retries}): #{error_message}"
78
78
  Logger.warn "Retrying in #{delay} seconds..."
79
79
 
80
80
  begin
@@ -87,7 +87,9 @@ module EmergeCLI
87
87
  sleep delay
88
88
  retry
89
89
  else
90
- Logger.error "Request failed after #{MAX_RETRIES} attempts: #{absolute_uri} #{e.message}"
90
+ unless max_retries == 0
91
+ Logger.error "Request failed after #{max_retries} attempts: #{absolute_uri} #{e.message}"
92
+ end
91
93
  raise e
92
94
  end
93
95
  end
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module EmergeCLI
2
- VERSION = '0.3.0'.freeze
2
+ VERSION = '0.5.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: emerge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emerge Tools
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-13 00:00:00.000000000 Z
11
+ date: 2025-01-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-http
@@ -24,6 +24,26 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.86.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: CFPropertyList
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.3'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 2.3.2
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '2.3'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 2.3.2
27
47
  - !ruby/object:Gem::Dependency
28
48
  name: chunky_png
29
49
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +86,20 @@ dependencies:
66
86
  - - "~>"
67
87
  - !ruby/object:Gem::Version
68
88
  version: 0.2.1
89
+ - !ruby/object:Gem::Dependency
90
+ name: ruby-macho
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 4.1.0
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 4.1.0
69
103
  - !ruby/object:Gem::Dependency
70
104
  name: ruby_tree_sitter
71
105
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +114,20 @@ dependencies:
80
114
  - - "~>"
81
115
  - !ruby/object:Gem::Version
82
116
  version: '1.9'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rubyzip
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: 2.3.0
124
+ type: :runtime
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: 2.3.0
83
131
  - !ruby/object:Gem::Dependency
84
132
  name: tty-prompt
85
133
  requirement: !ruby/object:Gem::Requirement
@@ -133,11 +181,21 @@ files:
133
181
  - CHANGELOG.md
134
182
  - README.md
135
183
  - exe/emerge
184
+ - lib/commands/autofixes/exported_symbols.rb
185
+ - lib/commands/autofixes/minify_strings.rb
186
+ - lib/commands/autofixes/strip_binary_symbols.rb
187
+ - lib/commands/build_distribution/download_and_install.rb
188
+ - lib/commands/build_distribution/validate_app.rb
136
189
  - lib/commands/config/orderfiles/orderfiles_ios.rb
137
190
  - lib/commands/config/snapshots/snapshots_ios.rb
138
191
  - lib/commands/global_options.rb
139
192
  - lib/commands/integrate/fastlane.rb
193
+ - lib/commands/order_files/download_order_files.rb
194
+ - lib/commands/order_files/validate_linkmaps.rb
195
+ - lib/commands/order_files/validate_xcode_project.rb
140
196
  - lib/commands/reaper/reaper.rb
197
+ - lib/commands/snapshots/validate_app.rb
198
+ - lib/commands/upload/build.rb
141
199
  - lib/commands/upload/snapshots/client_libraries/default.rb
142
200
  - lib/commands/upload/snapshots/client_libraries/paparazzi.rb
143
201
  - lib/commands/upload/snapshots/client_libraries/roborazzi.rb
@@ -151,6 +209,7 @@ files:
151
209
  - lib/utils/git_result.rb
152
210
  - lib/utils/github.rb
153
211
  - lib/utils/logger.rb
212
+ - lib/utils/macho_parser.rb
154
213
  - lib/utils/network.rb
155
214
  - lib/utils/profiler.rb
156
215
  - lib/utils/project_detector.rb
@@ -160,6 +219,8 @@ files:
160
219
  - parsers/libtree-sitter-java-linux-x86_64.so
161
220
  - parsers/libtree-sitter-kotlin-darwin-arm64.dylib
162
221
  - parsers/libtree-sitter-kotlin-linux-x86_64.so
222
+ - parsers/libtree-sitter-objc-darwin-arm64.dylib
223
+ - parsers/libtree-sitter-objc-linux-x86_64.so
163
224
  - parsers/libtree-sitter-swift-darwin-arm64.dylib
164
225
  - parsers/libtree-sitter-swift-linux-x86_64.so
165
226
  homepage: https://github.com/EmergeTools/emerge-cli
@@ -170,7 +231,7 @@ metadata:
170
231
  source_code_uri: https://github.com/EmergeTools/emerge-cli
171
232
  changelog_uri: https://github.com/EmergeTools/emerge-cli/blob/main/CHANGELOG.md
172
233
  rubygems_mfa_required: 'true'
173
- post_install_message:
234
+ post_install_message:
174
235
  rdoc_options: []
175
236
  require_paths:
176
237
  - lib
@@ -186,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
247
  version: '0'
187
248
  requirements: []
188
249
  rubygems_version: 3.5.11
189
- signing_key:
250
+ signing_key:
190
251
  specification_version: 4
191
252
  summary: Emerge CLI
192
253
  test_files: []