From 392e0a0de124fbf0715216e3fa84a155423fe2ea Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 31 Oct 2025 14:42:26 -0400 Subject: [PATCH 1/9] fix --- src/launchpad/size/analyzers/apple.py | 1 - src/launchpad/size/cli.py | 21 -------- src/launchpad/size/models/apple.py | 69 +++++++++++++-------------- 3 files changed, 32 insertions(+), 59 deletions(-) diff --git a/src/launchpad/size/analyzers/apple.py b/src/launchpad/size/analyzers/apple.py index 648a4ac5..91650a0e 100644 --- a/src/launchpad/size/analyzers/apple.py +++ b/src/launchpad/size/analyzers/apple.py @@ -242,7 +242,6 @@ def analyze(self, artifact: AppleArtifact) -> AppleAnalysisResults: results = AppleAnalysisResults( app_info=app_info, file_analysis=file_analysis, - binary_analysis=binary_analysis, treemap=treemap, insights=insights, analysis_duration=analysis_duration, diff --git a/src/launchpad/size/cli.py b/src/launchpad/size/cli.py index 32963fa7..2f899482 100644 --- a/src/launchpad/size/cli.py +++ b/src/launchpad/size/cli.py @@ -94,9 +94,6 @@ def size_command( else: _print_results_as_table(results) - if isinstance(results, AppleAnalysisResults) and not quiet: - _print_apple_summary(results) - except Exception: console.print_exception() raise click.Abort() @@ -221,24 +218,6 @@ def _print_file_analysis_table(file_analysis: FileAnalysis) -> None: console.print() -def _print_apple_summary(results: AppleAnalysisResults) -> None: - """Print a brief summary of the analysis.""" - file_analysis = results.file_analysis - insights = results.insights - - console.print("\n[bold]Summary:[/bold]") - console.print(f"• Duration: {results.analysis_duration:.2f} seconds") - console.print(f"• App name: [cyan]{results.app_info.name}[/cyan]") - console.print(f"• Total app size: [cyan]{_format_bytes(file_analysis.total_size)}[/cyan]") - console.print(f"• File count: [cyan]{file_analysis.file_count:,}[/cyan]") - - if insights and insights.duplicate_files and insights.duplicate_files.total_savings > 0: - console.print( - f"• Potential savings from duplicates: " - f"[yellow]{_format_bytes(insights.duplicate_files.total_savings)}[/yellow]" - ) - - def _format_bytes(size: int) -> str: """Format byte size in human-readable format.""" size_float = float(size) diff --git a/src/launchpad/size/models/apple.py b/src/launchpad/size/models/apple.py index bcb7f6f5..1dd1debc 100644 --- a/src/launchpad/size/models/apple.py +++ b/src/launchpad/size/models/apple.py @@ -32,16 +32,11 @@ class AppleAnalysisResults(BaseAnalysisResults): - """Complete Apple analysis results.""" + """Apple analysis results.""" - model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) + model_config = ConfigDict(frozen=True) app_info: AppleAppInfo = Field(..., description="Apple app information") - binary_analysis: List[MachOBinaryAnalysis] = Field( - default_factory=list, - description="Apple binary analysis results", - exclude=True, - ) insights: AppleInsightResults | None = Field( description="Generated insights from the analysis", ) @@ -77,6 +72,36 @@ class AppleAppInfo(BaseAppInfo): ) +class AppleInsightResults(BaseModel): + """Collection of all insight results.""" + + model_config = ConfigDict(frozen=True) + + duplicate_files: DuplicateFilesInsightResult | None = Field(None, description="Duplicate files analysis") + large_images: LargeImageFileInsightResult | None = Field(None, description="Large image files analysis") + large_audios: LargeImageFileInsightResult | None = Field(None, description="Large audio files analysis") + large_videos: LargeVideoFileInsightResult | None = Field(None, description="Large video files analysis") + strip_binary: StripBinaryInsightResult | None = Field(None, description="Strip binary analysis") + localized_strings_minify: LocalizedStringCommentsInsightResult | None = Field( + None, description="Localized strings comments analysis" + ) + small_files: SmallFilesInsightResult | None = Field(None, description="Small files analysis") + loose_images: LooseImagesInsightResult | None = Field( + None, description="Loose images not in asset catalogs analysis" + ) + hermes_debug_info: HermesDebugInfoInsightResult | None = Field(None, description="Hermes debug info analysis") + image_optimization: ImageOptimizationInsightResult | None = Field(None, description="Image optimization analysis") + main_binary_exported_symbols: MainBinaryExportMetadataResult | None = Field( + None, description="Main binary exported symbols analysis" + ) + unnecessary_files: UnnecessaryFilesInsightResult | None = Field(None, description="Unnecessary files analysis") + audio_compression: AudioCompressionInsightResult | None = Field(None, description="Audio compression analysis") + video_compression: VideoCompressionInsightResult | None = Field(None, description="Video compression analysis") + alternate_icons_optimization: ImageOptimizationInsightResult | None = Field( + None, description="Alternate app icons optimization analysis" + ) + + @dataclass class SegmentInfo: """Extracted segment information from LIEF data.""" @@ -140,33 +165,3 @@ class SwiftMetadata: """Swift-specific metadata extracted from the binary.""" protocol_conformances: List[str] - - -class AppleInsightResults(BaseModel): - """Collection of all insight results.""" - - model_config = ConfigDict(frozen=True) - - duplicate_files: DuplicateFilesInsightResult | None = Field(None, description="Duplicate files analysis") - large_images: LargeImageFileInsightResult | None = Field(None, description="Large image files analysis") - large_audios: LargeImageFileInsightResult | None = Field(None, description="Large audio files analysis") - large_videos: LargeVideoFileInsightResult | None = Field(None, description="Large video files analysis") - strip_binary: StripBinaryInsightResult | None = Field(None, description="Strip binary analysis") - localized_strings_minify: LocalizedStringCommentsInsightResult | None = Field( - None, description="Localized strings comments analysis" - ) - small_files: SmallFilesInsightResult | None = Field(None, description="Small files analysis") - loose_images: LooseImagesInsightResult | None = Field( - None, description="Loose images not in asset catalogs analysis" - ) - hermes_debug_info: HermesDebugInfoInsightResult | None = Field(None, description="Hermes debug info analysis") - image_optimization: ImageOptimizationInsightResult | None = Field(None, description="Image optimization analysis") - main_binary_exported_symbols: MainBinaryExportMetadataResult | None = Field( - None, description="Main binary exported symbols analysis" - ) - unnecessary_files: UnnecessaryFilesInsightResult | None = Field(None, description="Unnecessary files analysis") - audio_compression: AudioCompressionInsightResult | None = Field(None, description="Audio compression analysis") - video_compression: VideoCompressionInsightResult | None = Field(None, description="Video compression analysis") - alternate_icons_optimization: ImageOptimizationInsightResult | None = Field( - None, description="Alternate app icons optimization analysis" - ) From 15612085671f5625506f7f2c29df9a9dde02a95c Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 31 Oct 2025 14:46:20 -0400 Subject: [PATCH 2/9] fix --- src/launchpad/size/analyzers/apple.py | 2 -- src/launchpad/size/models/apple.py | 6 ------ 2 files changed, 8 deletions(-) diff --git a/src/launchpad/size/analyzers/apple.py b/src/launchpad/size/analyzers/apple.py index 91650a0e..0c59d371 100644 --- a/src/launchpad/size/analyzers/apple.py +++ b/src/launchpad/size/analyzers/apple.py @@ -437,7 +437,6 @@ def _analyze_binary( linked_libraries = parser.extract_linked_libraries() swift_protocol_conformances: List[str] = [] # parser.parse_swift_protocol_conformances() objc_method_names = parser.parse_objc_method_names() - static_inits = parser.static_inits() segments = self._extract_segments_info(parser.binary) load_commands = self._extract_load_commands_info(parser.binary) dyld_info = parser.extract_dyld_info() @@ -499,7 +498,6 @@ def _analyze_binary( header_size=parser.get_header_size(), dyld_info=dyld_info, dwarf_relocations=dwarf_relocations, - static_inits=static_inits, strippable_symbols_size=strippable_symbols_size, ) diff --git a/src/launchpad/size/models/apple.py b/src/launchpad/size/models/apple.py index 1dd1debc..151090b4 100644 --- a/src/launchpad/size/models/apple.py +++ b/src/launchpad/size/models/apple.py @@ -6,8 +6,6 @@ from pathlib import Path from typing import List -import lief - from pydantic import BaseModel, ConfigDict, Field from launchpad.parsers.apple.dwarf_relocations_parser import DwarfRelocationsData @@ -146,17 +144,13 @@ class MachOBinaryAnalysis: architectures: List[str] linked_libraries: List[str] objc_method_names: List[str] - # Lief types cannot be used after the binary is closed - # so we need to extract the segment/section data into dataclasses segments: List[SegmentInfo] load_commands: List[LoadCommandInfo] swift_metadata: SwiftMetadata | None = None - # TODO(EME-432): remove lief types from this model so it's safe to use after the binary is closed symbol_info: SymbolInfo | None = None header_size: int = 0 dyld_info: DyldInfo | None = None dwarf_relocations: DwarfRelocationsData | None = None - static_inits: List[lief.Symbol | str] | None = None strippable_symbols_size: int = 0 From 85f4374076645287606e2222e35cfdcd4f0d789c Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 31 Oct 2025 14:52:01 -0400 Subject: [PATCH 3/9] fix --- src/launchpad/size/models/apple.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/launchpad/size/models/apple.py b/src/launchpad/size/models/apple.py index 151090b4..0c42e40a 100644 --- a/src/launchpad/size/models/apple.py +++ b/src/launchpad/size/models/apple.py @@ -17,6 +17,7 @@ DuplicateFilesInsightResult, HermesDebugInfoInsightResult, ImageOptimizationInsightResult, + LargeAudioFileInsightResult, LargeImageFileInsightResult, LargeVideoFileInsightResult, LocalizedStringCommentsInsightResult, @@ -77,7 +78,7 @@ class AppleInsightResults(BaseModel): duplicate_files: DuplicateFilesInsightResult | None = Field(None, description="Duplicate files analysis") large_images: LargeImageFileInsightResult | None = Field(None, description="Large image files analysis") - large_audios: LargeImageFileInsightResult | None = Field(None, description="Large audio files analysis") + large_audios: LargeAudioFileInsightResult | None = Field(None, description="Large audio files analysis") large_videos: LargeVideoFileInsightResult | None = Field(None, description="Large video files analysis") strip_binary: StripBinaryInsightResult | None = Field(None, description="Strip binary analysis") localized_strings_minify: LocalizedStringCommentsInsightResult | None = Field( From c7c02ae99ca3c57bdf54053a0c3db0702818dc2b Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 31 Oct 2025 18:05:42 -0400 Subject: [PATCH 4/9] progress --- src/launchpad/parsers/apple/macho_parser.py | 47 +++++++- src/launchpad/size/analyzers/apple.py | 4 + src/launchpad/size/models/apple.py | 20 ++++ .../size/treemap/macho_element_builder.py | 107 ++++++++++++++---- 4 files changed, 152 insertions(+), 26 deletions(-) diff --git a/src/launchpad/parsers/apple/macho_parser.py b/src/launchpad/parsers/apple/macho_parser.py index 47aa1a04..283f9506 100644 --- a/src/launchpad/parsers/apple/macho_parser.py +++ b/src/launchpad/parsers/apple/macho_parser.py @@ -11,7 +11,7 @@ import lief import sentry_sdk -from launchpad.size.models.apple import DyldInfo +from launchpad.size.models.apple import CodeSignatureInfo, DyldInfo, LinkEditInfo from ...utils.logging import get_logger from .binary_utils import parse_null_terminated_strings @@ -308,3 +308,48 @@ def extract_dyld_info(self) -> DyldInfo: chained_fixups_size=(dyld_chained_fixups.data_size if dyld_chained_fixups else 0), export_trie_size=dyld_exports_trie.data_size if dyld_exports_trie else 0, ) + + @sentry_sdk.trace + def extract_code_signature_info(self) -> CodeSignatureInfo | None: + """Extract code signature information from LC_CODE_SIGNATURE load command.""" + if not self.binary.has_code_signature: + return None + + cs = self.binary.code_signature + return CodeSignatureInfo( + size=cs.data_size, + offset=cs.data_offset, + ) + + @sentry_sdk.trace + def extract_linkedit_info(self) -> LinkEditInfo: + """Extract __LINKEDIT segment component sizes from load commands.""" + symbol_table_size = 0 + string_table_size = 0 + function_starts_size = 0 + segment_size = 0 + + is_64bit = self.binary.header.magic in [ + lief.MachO.MACHO_TYPES.MAGIC_64, + lief.MachO.MACHO_TYPES.CIGAM_64, + ] + entry_size = 16 if is_64bit else 12 + + for cmd in self.binary.commands: + if isinstance(cmd, lief.MachO.SymbolCommand): + symbol_table_size = cmd.numberof_symbols * entry_size + string_table_size = cmd.strings_size + elif isinstance(cmd, lief.MachO.FunctionStarts): + function_starts_size = cmd.data_size + + for segment in self.binary.segments: + if segment.name == "__LINKEDIT": + segment_size = segment.file_size + break + + return LinkEditInfo( + symbol_table_size=symbol_table_size, + string_table_size=string_table_size, + function_starts_size=function_starts_size, + segment_size=segment_size, + ) diff --git a/src/launchpad/size/analyzers/apple.py b/src/launchpad/size/analyzers/apple.py index 0c59d371..d7450dbd 100644 --- a/src/launchpad/size/analyzers/apple.py +++ b/src/launchpad/size/analyzers/apple.py @@ -440,6 +440,8 @@ def _analyze_binary( segments = self._extract_segments_info(parser.binary) load_commands = self._extract_load_commands_info(parser.binary) dyld_info = parser.extract_dyld_info() + code_signature_info = parser.extract_code_signature_info() + linkedit_info = parser.extract_linkedit_info() symbol_info = None dwarf_relocations = None @@ -499,6 +501,8 @@ def _analyze_binary( dyld_info=dyld_info, dwarf_relocations=dwarf_relocations, strippable_symbols_size=strippable_symbols_size, + code_signature_info=code_signature_info, + linkedit_info=linkedit_info, ) @sentry_sdk.trace diff --git a/src/launchpad/size/models/apple.py b/src/launchpad/size/models/apple.py index 0c42e40a..9d5a9cb5 100644 --- a/src/launchpad/size/models/apple.py +++ b/src/launchpad/size/models/apple.py @@ -134,6 +134,24 @@ class DyldInfo: export_trie_size: int = 0 +@dataclass +class CodeSignatureInfo: + """Code signature information extracted from LC_CODE_SIGNATURE load command.""" + + size: int = 0 + offset: int = 0 + + +@dataclass +class LinkEditInfo: + """Link edit segment components extracted from various load commands in __LINKEDIT.""" + + symbol_table_size: int = 0 + string_table_size: int = 0 + function_starts_size: int = 0 + segment_size: int = 0 + + @dataclass class MachOBinaryAnalysis: """Mach-O binary analysis results.""" @@ -153,6 +171,8 @@ class MachOBinaryAnalysis: dyld_info: DyldInfo | None = None dwarf_relocations: DwarfRelocationsData | None = None strippable_symbols_size: int = 0 + code_signature_info: CodeSignatureInfo | None = None + linkedit_info: LinkEditInfo | None = None @dataclass diff --git a/src/launchpad/size/treemap/macho_element_builder.py b/src/launchpad/size/treemap/macho_element_builder.py index a33d6d0f..eec0f9c1 100644 --- a/src/launchpad/size/treemap/macho_element_builder.py +++ b/src/launchpad/size/treemap/macho_element_builder.py @@ -435,11 +435,11 @@ def _add_segments( ) ) - dyld_children_size = 0 + linkedit_children_size = 0 if segment_name == "__LINKEDIT": - dyld_children = self._build_dyld_load_command_children(binary_analysis) - segment_children.extend(dyld_children) - dyld_children_size = sum(c.size for c in dyld_children) + linkedit_children = self._build_linkedit_children(binary_analysis) + segment_children.extend(linkedit_children) + linkedit_children_size = sum(c.size for c in linkedit_children) displayed_section_size = sum(c.size for c in segment_children) @@ -448,7 +448,7 @@ def _add_segments( seg_total_size = segment.size total_section_declared = sum(s.size for s in segment.sections) if segment.sections else 0 - segment_overhead = seg_total_size - total_section_declared - dyld_children_size + segment_overhead = seg_total_size - total_section_declared - linkedit_children_size actual_segment_size = displayed_section_size + max(0, segment_overhead) if actual_segment_size > 0: @@ -503,37 +503,94 @@ def _build_metadata_components(self, binary_analysis: MachOBinaryAnalysis) -> Li return metadata_children - def _build_dyld_load_command_children(self, binary_analysis: MachOBinaryAnalysis) -> List[TreemapElement]: - dyld_children: List[TreemapElement] = [] + def _build_linkedit_children(self, binary_analysis: MachOBinaryAnalysis) -> List[TreemapElement]: + """Build child elements for the __LINKEDIT segment. + + Includes symbol table, string table, function starts, DYLD info, and code signature. + """ + linkedit_children: List[TreemapElement] = [] + + # Add link edit components (symbol table, string table, function starts) + le = binary_analysis.linkedit_info + if le is not None: + if le.string_table_size > 0: + linkedit_children.append( + TreemapElement( + name="String Table", + size=le.string_table_size, + type=TreemapType.EXECUTABLES, + path=None, + is_dir=False, + children=[], + ) + ) + + if le.symbol_table_size > 0: + linkedit_children.append( + TreemapElement( + name="Symbol Table", + size=le.symbol_table_size, + type=TreemapType.EXECUTABLES, + path=None, + is_dir=False, + children=[], + ) + ) + + if le.function_starts_size > 0: + linkedit_children.append( + TreemapElement( + name="Function Starts", + size=le.function_starts_size, + type=TreemapType.EXECUTABLES, + path=None, + is_dir=False, + children=[], + ) + ) + + # Add DYLD info children di = binary_analysis.dyld_info - if di is None: - return dyld_children + if di is not None: + if di.chained_fixups_size > 0: + linkedit_children.append( + TreemapElement( + name="Chained Fixups", + size=di.chained_fixups_size, + type=TreemapType.DYLD, + path=None, + is_dir=False, + children=[], + ) + ) - if di.chained_fixups_size > 0: - dyld_children.append( - TreemapElement( - name="Chained Fixups", - size=di.chained_fixups_size, - type=TreemapType.DYLD, - path=None, - is_dir=False, - children=[], + if di.export_trie_size > 0: + linkedit_children.append( + TreemapElement( + name="Export Trie", + size=di.export_trie_size, + type=TreemapType.DYLD, + path=None, + is_dir=False, + children=[], + ) ) - ) - if di.export_trie_size > 0: - dyld_children.append( + # Add code signature + cs = binary_analysis.code_signature_info + if cs is not None and cs.size > 0: + linkedit_children.append( TreemapElement( - name="Export Trie", - size=di.export_trie_size, - type=TreemapType.DYLD, + name="Code Signature", + size=cs.size, + type=TreemapType.CODE_SIGNATURE, path=None, is_dir=False, children=[], ) ) - return dyld_children + return linkedit_children def _add_unmapped_region(self, binary_analysis: MachOBinaryAnalysis, binary_children: List[TreemapElement]) -> None: total_accounted = sum(c.size for c in binary_children) From d7b5decfb2ea2ad26768ca6766f5530f87222a77 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 31 Oct 2025 18:16:49 -0400 Subject: [PATCH 5/9] update --- src/launchpad/parsers/apple/macho_parser.py | 56 +++++---- src/launchpad/size/analyzers/apple.py | 4 - src/launchpad/size/models/apple.py | 36 +++--- .../size/treemap/macho_element_builder.py | 115 +++++++++--------- 4 files changed, 105 insertions(+), 106 deletions(-) diff --git a/src/launchpad/parsers/apple/macho_parser.py b/src/launchpad/parsers/apple/macho_parser.py index 283f9506..3ed9c867 100644 --- a/src/launchpad/parsers/apple/macho_parser.py +++ b/src/launchpad/parsers/apple/macho_parser.py @@ -11,7 +11,7 @@ import lief import sentry_sdk -from launchpad.size.models.apple import CodeSignatureInfo, DyldInfo, LinkEditInfo +from launchpad.size.models.apple import LinkEditInfo from ...utils.logging import get_logger from .binary_utils import parse_null_terminated_strings @@ -299,57 +299,63 @@ def find_symbol(addr: int) -> lief.Symbol | None: return symbols - @sentry_sdk.trace - def extract_dyld_info(self) -> DyldInfo: - """Extract DYLD information from LC_DYLD_INFO load commands.""" - dyld_chained_fixups = self.binary.dyld_chained_fixups - dyld_exports_trie = self.binary.dyld_exports_trie - return DyldInfo( - chained_fixups_size=(dyld_chained_fixups.data_size if dyld_chained_fixups else 0), - export_trie_size=dyld_exports_trie.data_size if dyld_exports_trie else 0, - ) - - @sentry_sdk.trace - def extract_code_signature_info(self) -> CodeSignatureInfo | None: - """Extract code signature information from LC_CODE_SIGNATURE load command.""" - if not self.binary.has_code_signature: - return None - - cs = self.binary.code_signature - return CodeSignatureInfo( - size=cs.data_size, - offset=cs.data_offset, - ) - @sentry_sdk.trace def extract_linkedit_info(self) -> LinkEditInfo: - """Extract __LINKEDIT segment component sizes from load commands.""" + """Extract all __LINKEDIT segment component sizes from load commands. + + Consolidates symbol tables, DYLD info, code signature, and segment size into one structure. + """ symbol_table_size = 0 string_table_size = 0 function_starts_size = 0 segment_size = 0 + chained_fixups_size = 0 + export_trie_size = 0 + code_signature_size = 0 + code_signature_offset = 0 + # Determine if binary is 64-bit (nlist_64 is 16 bytes, nlist is 12 bytes) is_64bit = self.binary.header.magic in [ lief.MachO.MACHO_TYPES.MAGIC_64, lief.MachO.MACHO_TYPES.CIGAM_64, ] entry_size = 16 if is_64bit else 12 + # Extract from load commands for cmd in self.binary.commands: if isinstance(cmd, lief.MachO.SymbolCommand): + # LC_SYMTAB: symbol table + string table symbol_table_size = cmd.numberof_symbols * entry_size string_table_size = cmd.strings_size elif isinstance(cmd, lief.MachO.FunctionStarts): + # LC_FUNCTION_STARTS function_starts_size = cmd.data_size + # Extract DYLD info + dyld_chained_fixups = self.binary.dyld_chained_fixups + dyld_exports_trie = self.binary.dyld_exports_trie + chained_fixups_size = dyld_chained_fixups.data_size if dyld_chained_fixups else 0 + export_trie_size = dyld_exports_trie.data_size if dyld_exports_trie else 0 + + # Extract code signature + if self.binary.has_code_signature: + cs = self.binary.code_signature + code_signature_size = cs.data_size + code_signature_offset = cs.data_offset + + # Get __LINKEDIT segment size for segment in self.binary.segments: if segment.name == "__LINKEDIT": segment_size = segment.file_size break return LinkEditInfo( + segment_size=segment_size, symbol_table_size=symbol_table_size, string_table_size=string_table_size, function_starts_size=function_starts_size, - segment_size=segment_size, + chained_fixups_size=chained_fixups_size, + export_trie_size=export_trie_size, + code_signature_size=code_signature_size, + code_signature_offset=code_signature_offset, ) diff --git a/src/launchpad/size/analyzers/apple.py b/src/launchpad/size/analyzers/apple.py index d7450dbd..00e869e8 100644 --- a/src/launchpad/size/analyzers/apple.py +++ b/src/launchpad/size/analyzers/apple.py @@ -439,8 +439,6 @@ def _analyze_binary( objc_method_names = parser.parse_objc_method_names() segments = self._extract_segments_info(parser.binary) load_commands = self._extract_load_commands_info(parser.binary) - dyld_info = parser.extract_dyld_info() - code_signature_info = parser.extract_code_signature_info() linkedit_info = parser.extract_linkedit_info() symbol_info = None @@ -498,10 +496,8 @@ def _analyze_binary( segments=segments, load_commands=load_commands, header_size=parser.get_header_size(), - dyld_info=dyld_info, dwarf_relocations=dwarf_relocations, strippable_symbols_size=strippable_symbols_size, - code_signature_info=code_signature_info, linkedit_info=linkedit_info, ) diff --git a/src/launchpad/size/models/apple.py b/src/launchpad/size/models/apple.py index 9d5a9cb5..84049468 100644 --- a/src/launchpad/size/models/apple.py +++ b/src/launchpad/size/models/apple.py @@ -127,29 +127,29 @@ class LoadCommandInfo: @dataclass -class DyldInfo: - """DYLD-specific information extracted from related DYLD load commands.""" - - chained_fixups_size: int = 0 - export_trie_size: int = 0 - - -@dataclass -class CodeSignatureInfo: - """Code signature information extracted from LC_CODE_SIGNATURE load command.""" - - size: int = 0 - offset: int = 0 +class LinkEditInfo: + """Link edit segment components extracted from various load commands in __LINKEDIT. + Consolidates all __LINKEDIT segment data including symbol tables, DYLD info, and code signature. + """ -@dataclass -class LinkEditInfo: - """Link edit segment components extracted from various load commands in __LINKEDIT.""" + # Segment + segment_size: int = 0 + # Symbol tables (from LC_SYMTAB) symbol_table_size: int = 0 string_table_size: int = 0 + + # Debugging (from LC_FUNCTION_STARTS) function_starts_size: int = 0 - segment_size: int = 0 + + # DYLD (from LC_DYLD_CHAINED_FIXUPS, LC_DYLD_EXPORTS_TRIE) + chained_fixups_size: int = 0 + export_trie_size: int = 0 + + # Code signing (from LC_CODE_SIGNATURE) + code_signature_size: int = 0 + code_signature_offset: int = 0 @dataclass @@ -168,10 +168,8 @@ class MachOBinaryAnalysis: swift_metadata: SwiftMetadata | None = None symbol_info: SymbolInfo | None = None header_size: int = 0 - dyld_info: DyldInfo | None = None dwarf_relocations: DwarfRelocationsData | None = None strippable_symbols_size: int = 0 - code_signature_info: CodeSignatureInfo | None = None linkedit_info: LinkEditInfo | None = None diff --git a/src/launchpad/size/treemap/macho_element_builder.py b/src/launchpad/size/treemap/macho_element_builder.py index eec0f9c1..71eac760 100644 --- a/src/launchpad/size/treemap/macho_element_builder.py +++ b/src/launchpad/size/treemap/macho_element_builder.py @@ -510,79 +510,78 @@ def _build_linkedit_children(self, binary_analysis: MachOBinaryAnalysis) -> List """ linkedit_children: List[TreemapElement] = [] - # Add link edit components (symbol table, string table, function starts) le = binary_analysis.linkedit_info - if le is not None: - if le.string_table_size > 0: - linkedit_children.append( - TreemapElement( - name="String Table", - size=le.string_table_size, - type=TreemapType.EXECUTABLES, - path=None, - is_dir=False, - children=[], - ) + if le is None: + return linkedit_children + + # Add symbol table and string table + if le.string_table_size > 0: + linkedit_children.append( + TreemapElement( + name="String Table", + size=le.string_table_size, + type=TreemapType.EXECUTABLES, + path=None, + is_dir=False, + children=[], ) + ) - if le.symbol_table_size > 0: - linkedit_children.append( - TreemapElement( - name="Symbol Table", - size=le.symbol_table_size, - type=TreemapType.EXECUTABLES, - path=None, - is_dir=False, - children=[], - ) + if le.symbol_table_size > 0: + linkedit_children.append( + TreemapElement( + name="Symbol Table", + size=le.symbol_table_size, + type=TreemapType.EXECUTABLES, + path=None, + is_dir=False, + children=[], ) + ) - if le.function_starts_size > 0: - linkedit_children.append( - TreemapElement( - name="Function Starts", - size=le.function_starts_size, - type=TreemapType.EXECUTABLES, - path=None, - is_dir=False, - children=[], - ) + if le.function_starts_size > 0: + linkedit_children.append( + TreemapElement( + name="Function Starts", + size=le.function_starts_size, + type=TreemapType.EXECUTABLES, + path=None, + is_dir=False, + children=[], ) + ) - # Add DYLD info children - di = binary_analysis.dyld_info - if di is not None: - if di.chained_fixups_size > 0: - linkedit_children.append( - TreemapElement( - name="Chained Fixups", - size=di.chained_fixups_size, - type=TreemapType.DYLD, - path=None, - is_dir=False, - children=[], - ) + # Add DYLD info + if le.chained_fixups_size > 0: + linkedit_children.append( + TreemapElement( + name="Chained Fixups", + size=le.chained_fixups_size, + type=TreemapType.DYLD, + path=None, + is_dir=False, + children=[], ) + ) - if di.export_trie_size > 0: - linkedit_children.append( - TreemapElement( - name="Export Trie", - size=di.export_trie_size, - type=TreemapType.DYLD, - path=None, - is_dir=False, - children=[], - ) + if le.export_trie_size > 0: + linkedit_children.append( + TreemapElement( + name="Export Trie", + size=le.export_trie_size, + type=TreemapType.DYLD, + path=None, + is_dir=False, + children=[], ) + ) # Add code signature - cs = binary_analysis.code_signature_info - if cs is not None and cs.size > 0: + if le.code_signature_size > 0: linkedit_children.append( TreemapElement( name="Code Signature", - size=cs.size, + size=le.code_signature_size, type=TreemapType.CODE_SIGNATURE, path=None, is_dir=False, From 48ae46c2877bd0b6d12171bf0a98f5d089953cba Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 31 Oct 2025 18:28:24 -0400 Subject: [PATCH 6/9] fix --- .../apple/main_binary_export_metadata.py | 6 +++--- .../size/test_treemap_generation.py | 20 ++++++++++++++++++- ...est_main_binary_export_metadata_insight.py | 20 +++++++++---------- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/launchpad/size/insights/apple/main_binary_export_metadata.py b/src/launchpad/size/insights/apple/main_binary_export_metadata.py index 3dcb0fd5..8b0d593b 100644 --- a/src/launchpad/size/insights/apple/main_binary_export_metadata.py +++ b/src/launchpad/size/insights/apple/main_binary_export_metadata.py @@ -17,11 +17,11 @@ def generate(self, input: InsightsInput) -> MainBinaryExportMetadataResult | Non if not analysis.is_main_binary: continue - dyld_info = analysis.dyld_info - if dyld_info is None: + linkedit_info = analysis.linkedit_info + if linkedit_info is None: continue - export_trie_size = dyld_info.export_trie_size + export_trie_size = linkedit_info.export_trie_size if export_trie_size >= self.MIN_EXPORTS_THRESHOLD: export_files.append( FileSavingsResult( diff --git a/tests/integration/size/test_treemap_generation.py b/tests/integration/size/test_treemap_generation.py index d8f24d0a..d3957016 100644 --- a/tests/integration/size/test_treemap_generation.py +++ b/tests/integration/size/test_treemap_generation.py @@ -361,9 +361,27 @@ def find_node_by_path(root: TreemapElement, path: str) -> TreemapElement | None: # Verify __LINKEDIT segment has_linkedit = "__LINKEDIT" in main_exe_sections assert has_linkedit - linkedit_size = main_exe_sections["__LINKEDIT"].size + linkedit = main_exe_sections["__LINKEDIT"] + linkedit_size = linkedit.size assert linkedit_size == 269360 + # Verify __LINKEDIT has children (symbol table, string table, etc.) + linkedit_children = {child.name: child for child in linkedit.children} + assert len(linkedit_children) == 6, "__LINKEDIT should have exactly 6 child components" + + # Verify expected __LINKEDIT components exist + assert "Symbol Table" in linkedit_children, "Symbol table should be present" + assert "String Table" in linkedit_children, "String table should be present" + assert "Function Starts" in linkedit_children, "Function starts should be present" + + # Verify exact sizes for __LINKEDIT components + assert linkedit_children["Symbol Table"].size == 11072, "Symbol table size mismatch" + assert linkedit_children["String Table"].size == 16584, "String table size mismatch" + assert linkedit_children["Function Starts"].size == 13584, "Function starts size mismatch" + assert linkedit_children["Chained Fixups"].size == 87616, "Chained fixups size mismatch" + assert linkedit_children["Export Trie"].size == 85056, "Export trie size mismatch" + assert linkedit_children["Code Signature"].size == 43488, "Code signature size mismatch" + # Verify Unmapped section (track size changes) has_unmapped = "Unmapped" in main_exe_sections assert has_unmapped diff --git a/tests/unit/test_main_binary_export_metadata_insight.py b/tests/unit/test_main_binary_export_metadata_insight.py index dde6dc26..4da68937 100644 --- a/tests/unit/test_main_binary_export_metadata_insight.py +++ b/tests/unit/test_main_binary_export_metadata_insight.py @@ -2,7 +2,7 @@ from launchpad.size.insights.apple.main_binary_export_metadata import MainBinaryExportMetadataInsight from launchpad.size.insights.insight import InsightsInput -from launchpad.size.models.apple import DyldInfo, MachOBinaryAnalysis +from launchpad.size.models.apple import LinkEditInfo, MachOBinaryAnalysis from launchpad.size.models.common import BaseAppInfo, FileAnalysis from launchpad.size.models.insights import MainBinaryExportMetadataResult @@ -26,7 +26,7 @@ def test_generate_with_main_binary_and_dyld_exports_trie(self): swift_metadata=None, is_main_binary=True, header_size=32, - dyld_info=DyldInfo(export_trie_size=5000), + linkedit_info=LinkEditInfo(export_trie_size=5000), ) insights_input = InsightsInput( @@ -42,7 +42,7 @@ def test_generate_with_main_binary_and_dyld_exports_trie(self): assert result.total_savings == 5000 def test_generate_with_main_binary_without_dyld_exports_trie(self): - """Test that no insight is generated when main binary has no dyld_info.""" + """Test that no insight is generated when main binary has no linkedit_info.""" main_binary_analysis = MachOBinaryAnalysis( binary_absolute_path=Path("MyApp"), binary_relative_path=Path("MyApp"), @@ -56,7 +56,7 @@ def test_generate_with_main_binary_without_dyld_exports_trie(self): swift_metadata=None, is_main_binary=True, header_size=32, - dyld_info=None, # No dyld_info + linkedit_info=None, # No linkedit_info ) insights_input = InsightsInput( @@ -85,7 +85,7 @@ def test_generate_with_no_main_binary(self): swift_metadata=None, is_main_binary=False, # Not a main binary header_size=32, - dyld_info=DyldInfo(export_trie_size=5000), # Has export trie but not main binary + linkedit_info=LinkEditInfo(export_trie_size=5000), # Has export trie but not main binary ) insights_input = InsightsInput( @@ -114,7 +114,7 @@ def test_generate_with_main_binary_but_empty_export_trie(self): swift_metadata=None, is_main_binary=True, header_size=32, - dyld_info=DyldInfo(export_trie_size=0), # Zero-size export trie + linkedit_info=LinkEditInfo(export_trie_size=0), # Zero-size export trie ) insights_input = InsightsInput( @@ -156,7 +156,7 @@ def test_generate_with_multiple_binaries_one_main(self): swift_metadata=None, is_main_binary=True, header_size=32, - dyld_info=DyldInfo(export_trie_size=8000), + linkedit_info=LinkEditInfo(export_trie_size=8000), ) # Create framework binary (non-main) @@ -173,7 +173,7 @@ def test_generate_with_multiple_binaries_one_main(self): swift_metadata=None, is_main_binary=False, header_size=32, - dyld_info=DyldInfo(export_trie_size=3000), # Framework also has export trie but won't be included + linkedit_info=LinkEditInfo(export_trie_size=3000), # Framework also has export trie but won't be included ) insights_input = InsightsInput( @@ -203,7 +203,7 @@ def test_generate_with_export_trie_below_threshold(self): swift_metadata=None, is_main_binary=True, header_size=32, - dyld_info=DyldInfo(export_trie_size=512), # Below MIN_EXPORTS_THRESHOLD (1024) + linkedit_info=LinkEditInfo(export_trie_size=512), # Below MIN_EXPORTS_THRESHOLD (1024) ) insights_input = InsightsInput( @@ -232,7 +232,7 @@ def test_generate_with_export_trie_at_threshold(self): swift_metadata=None, is_main_binary=True, header_size=32, - dyld_info=DyldInfo(export_trie_size=1024), # Exactly at MIN_EXPORTS_THRESHOLD + linkedit_info=LinkEditInfo(export_trie_size=1024), # Exactly at MIN_EXPORTS_THRESHOLD ) insights_input = InsightsInput( From f5a56204e5ff79547c4c1bf94508d898bd1f9f96 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 31 Oct 2025 18:29:14 -0400 Subject: [PATCH 7/9] omfg --- src/launchpad/parsers/apple/macho_parser.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/launchpad/parsers/apple/macho_parser.py b/src/launchpad/parsers/apple/macho_parser.py index 3ed9c867..a670032d 100644 --- a/src/launchpad/parsers/apple/macho_parser.py +++ b/src/launchpad/parsers/apple/macho_parser.py @@ -301,10 +301,7 @@ def find_symbol(addr: int) -> lief.Symbol | None: @sentry_sdk.trace def extract_linkedit_info(self) -> LinkEditInfo: - """Extract all __LINKEDIT segment component sizes from load commands. - - Consolidates symbol tables, DYLD info, code signature, and segment size into one structure. - """ + """Extract all __LINKEDIT segment component sizes from load commands.""" symbol_table_size = 0 string_table_size = 0 function_starts_size = 0 From 2dee112a62c16b77c3e3b964368a398015a870bf Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 31 Oct 2025 18:29:59 -0400 Subject: [PATCH 8/9] omfg --- src/launchpad/parsers/apple/macho_parser.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/launchpad/parsers/apple/macho_parser.py b/src/launchpad/parsers/apple/macho_parser.py index a670032d..1bf55fa5 100644 --- a/src/launchpad/parsers/apple/macho_parser.py +++ b/src/launchpad/parsers/apple/macho_parser.py @@ -318,29 +318,23 @@ def extract_linkedit_info(self) -> LinkEditInfo: ] entry_size = 16 if is_64bit else 12 - # Extract from load commands for cmd in self.binary.commands: if isinstance(cmd, lief.MachO.SymbolCommand): - # LC_SYMTAB: symbol table + string table symbol_table_size = cmd.numberof_symbols * entry_size string_table_size = cmd.strings_size elif isinstance(cmd, lief.MachO.FunctionStarts): - # LC_FUNCTION_STARTS function_starts_size = cmd.data_size - # Extract DYLD info dyld_chained_fixups = self.binary.dyld_chained_fixups dyld_exports_trie = self.binary.dyld_exports_trie chained_fixups_size = dyld_chained_fixups.data_size if dyld_chained_fixups else 0 export_trie_size = dyld_exports_trie.data_size if dyld_exports_trie else 0 - # Extract code signature if self.binary.has_code_signature: cs = self.binary.code_signature code_signature_size = cs.data_size code_signature_offset = cs.data_offset - # Get __LINKEDIT segment size for segment in self.binary.segments: if segment.name == "__LINKEDIT": segment_size = segment.file_size From ffb1ddccefc16166037a9d66ac5df4b6cdaab5ff Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 31 Oct 2025 18:45:17 -0400 Subject: [PATCH 9/9] fix --- src/launchpad/parsers/apple/macho_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/launchpad/parsers/apple/macho_parser.py b/src/launchpad/parsers/apple/macho_parser.py index 1bf55fa5..d9855f2e 100644 --- a/src/launchpad/parsers/apple/macho_parser.py +++ b/src/launchpad/parsers/apple/macho_parser.py @@ -330,7 +330,7 @@ def extract_linkedit_info(self) -> LinkEditInfo: chained_fixups_size = dyld_chained_fixups.data_size if dyld_chained_fixups else 0 export_trie_size = dyld_exports_trie.data_size if dyld_exports_trie else 0 - if self.binary.has_code_signature: + if self.binary.has_code_signature and self.binary.code_signature is not None: cs = self.binary.code_signature code_signature_size = cs.data_size code_signature_offset = cs.data_offset