└── objc_api_visibility.pl /objc_api_visibility.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # objc_api_visibility.pl 3 | # by Ryan Petrich, inspired by Dustin Howett's Logos 4 | # Reads all Objective-C method names from an installed SDK, determines their visibility and optionally compares against an iOS app binary 5 | 6 | sub read_methods { 7 | my $cmd = shift; 8 | my $when_found = shift; 9 | my $current_class; 10 | open(LS_CMD, "$cmd |") or die "Can't run '$cmd'\n$!\n"; 11 | while () { 12 | my $line = $_; 13 | foreach my $statement (split(/;|(\/\/)/, $line)) { 14 | if ($statement =~ /\G([+-])\s*\(\s*(.*?)\s*\)(?=\s*[\w:])/gc) { 15 | my $return = $2; 16 | # If line does not include semicolon, continue fetching lines until we get one 17 | if ($line !~ m/;/) { 18 | $statement =~ s/\n$//; 19 | while () { 20 | $line = $_; 21 | foreach my $component (split(/;|(\/\/)|(\/\*)/, $line)) { 22 | my $trimmed = $component; 23 | $trimmed =~ s/^\s+//; 24 | $trimmed =~ s/\s+$//; 25 | if (length($trimmed)) { 26 | $statement .= " $trimmed"; 27 | } 28 | } 29 | last if $line =~ m/;/; 30 | } 31 | # then rematch so that /G is populated 32 | $statement =~ /\G([+-])\s*\(\s*(.*?)\s*\)(?=\s*[\w:])/gc; 33 | } 34 | # Method definition 35 | my @sel_parts = (); 36 | while ($statement =~ /\G\s*([\$\w]*)(\s*:\s*(\((.+?)\))?\s*([\$\w]+?)\b)?\s*((\/\*)?((__OSX_AVAILABLE)|(NS_DEPRECATED)|(NS_AVAILABLE)).*)?/gc) { 37 | # Read selector component 38 | push(@sel_parts, $1); 39 | last if !$2; 40 | } 41 | my $sel = join(':', @sel_parts); 42 | $when_found->($sel, $current_class); 43 | } elsif ($statement =~ /\@((interface)|(protocol))/) { 44 | # Class/Protocol definition 45 | my @components = split(/\s/, $_); 46 | $current_class = @components[1]; 47 | } elsif ($statement =~ /\@property\s*\((.*)\).*?\s\*?(\S+)(\s*(\/\*)?(__OSX|NS)_AVAILABLE.*)?\s*$/) { 48 | # Property definition 49 | my $getter = $2; 50 | if ($statement =~ /\@property\s*\(.*\).*?\(\^(\S*?)\)/) { 51 | # Fix some macro silliness 52 | $getter = $1; 53 | } 54 | my $setter; 55 | if ($getter =~ /^_/) { 56 | # Properties that begin with underscore are special cased 57 | $setter = substr $getter, 1; 58 | $setter = '_set' . ucfirst($setter) . ':'; 59 | } else { 60 | $setter = 'set' . ucfirst($getter) . ':'; 61 | } 62 | my $readable = 1; 63 | my $writable = 1; 64 | foreach my $attribute (split(/,\s*/, $1)) { 65 | # Parse attributes 66 | if ($attribute =~ /^readonly$/) { 67 | $writable = 0; 68 | } elsif ($attribute =~ /^writeonly$/) { 69 | $readable = 0; 70 | } elsif ($attribute =~ /^getter=(\S*)$/) { 71 | $getter = $1; 72 | } elsif ($attribute =~ /^setter=(\S*)$/) { 73 | $setter = $1; 74 | } 75 | } 76 | $when_found->($getter, $current_class) if $readable; 77 | $when_found->($setter, $current_class) if $writable; 78 | } 79 | } 80 | } 81 | close(LS_CMD); 82 | } 83 | 84 | # Find SDK path 85 | my $xcode = `xcode-select -print-path`; 86 | chomp $xcode; 87 | my @paths = glob("$xcode/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS?.?.sdk"); 88 | die("Cannot find an iPhoneOS.platform SDK!\n") if scalar(@paths) == 0; 89 | my $sdk_path = @paths[0]; 90 | 91 | # Build map of method names used in the SDK 92 | my %method_map; 93 | foreach my $binary(split("\0", `find "$sdk_path/System/Library" -maxdepth 4 -perm -111 -type f -print0`)) { 94 | # Class dump to find all methods 95 | read_methods "class-dump-z -N -N -y \"$sdk_path\" \"$binary\"", sub { 96 | # ...mark each as private and add class names 97 | my $method = shift; 98 | my $class_name = shift; 99 | $method_map{$method}->{'visibility'} = 'private'; 100 | $method_map{$method}->{'classes'}->{$class_name} = 1; 101 | } 102 | } 103 | foreach my $header(split("\0", `find "$sdk_path/System/Library/Frameworks" -name "*.h" -type f -print0`)) { 104 | # Find methods declared in headers 105 | read_methods "cat $header", sub { 106 | # ...mark each as public 107 | my $method = shift; 108 | $method_map{$method}->{'visibility'} = 'public'; 109 | } 110 | } 111 | 112 | # Generate list of names to output 113 | my @names_to_output; 114 | if ($#ARGV == 0) { 115 | # Read selector table from the first argument 116 | my $method_table = `otool -s __TEXT __objc_methname "$ARGV[0]" | sed -n '3,\$p' | cut -c10-`; 117 | @names_to_output = split(/\x00/, scalar reverse (reverse unpack('(a4)*', pack('(H8)*', split(/\s+/, $method_table))))); 118 | } else { 119 | # If no first argument, list all 120 | @names_to_output = keys %method_map; 121 | } 122 | 123 | # Ouput as TSV with the following fields visibility, method name, class names sorted by method name 124 | foreach my $method(sort @names_to_output) { 125 | my $visibility = $method_map{$method}->{'visibility'}; 126 | $visibility = 'new' if length($visibility) == 0; 127 | my $classes = join("\t", keys(%{ $method_map{$method}->{'classes'} })); 128 | print "$visibility\t$method\t$classes\n"; 129 | } 130 | --------------------------------------------------------------------------------