` to specify a directory to put the headers in
21 | - uses concurrency to process all images in the shared cache quickly (see `-j`)
22 |
23 | It can also do more. See the full options listing below:
24 |
25 | ```
26 | Usage: classdumpctl [options]
27 | Options:
28 | -a, --dyld_shared_cache Interact in the dyld_shared_cache
29 | by default, dump all classes in the cache
30 | -l, --list List all classes in the specified image
31 | if specified with -a/--dyld_shared_cache
32 | lists all images in the dyld_shared_cache
33 | -o , --output=
Use path as the output directory
34 | if specified with -a/--dyld_shared_cache
35 | the file structure of the cache is written to
36 | the specified directory, otherwise all classes found
37 | are written to this directory at the top level
38 | -m , --color= Set color settings, one of the below
39 | default: color output using ASNI color escapes only if output is to a TTY
40 | never: no output is colored
41 | always: color output to files, pipes, and TTYs using ASNI color escapes
42 | html-hljs: output in HTML format annotated with hljs classes
43 | html-lsp: output in HTML format annotated with LSP classes
44 | -i , --image=
Reference the mach-o image at path
45 | by default, dump all classes in this image
46 | otherwise may specify --class or --protocol
47 | -c , --class= Dump class to stdout (unless -o is specified)
48 | -p , --protocol= Dump protocol to stdout (unless -o is specified)
49 | -j , --jobs= Allow N jobs at once
50 | only applicable when specified with -a/--dyld_shared_cache
51 | (defaults to number of processing core available)
52 |
53 | --strip-protocol-conformance[=flag] Hide properties and methods that are required
54 | by a protocol the type conforms to
55 | (defaults to false)
56 | --strip-overrides[=flag] Hide properties and methods that are inherited
57 | from the class hierachy
58 | (defaults to false)
59 | --strip-duplicates[=flag] Hide duplicate occurrences of a property or method
60 | (defaults to false)
61 | --strip-synthesized[=flag] Hide methods and ivars that are synthesized from a property
62 | (defaults to true)
63 | --strip-ctor-method[=flag] Hide `.cxx_construct` method
64 | (defaults to false)
65 | --strip-dtor-method[=flag] Hide `.cxx_destruct` method
66 | (defaults to false)
67 | --add-symbol-comments[=flag] Add comments above each eligible declaration
68 | with the symbol name and image path the object is found in
69 | (defaults to false)
70 | ```
71 |
--------------------------------------------------------------------------------
/Sources/classdumpctl/ansi-color.h:
--------------------------------------------------------------------------------
1 | //
2 | // ansi-color.h
3 | // classdumpctl
4 | //
5 | // Created by Leptos on 8/24/24.
6 | // Copyright © 2024 Leptos. All rights reserved.
7 | //
8 |
9 | #ifndef ANSI_COLOR_h
10 | #define ANSI_COLOR_h
11 |
12 | /* ANSI color escapes:
13 | * "\033[Em"
14 | * where E is the encoding, and the rest are literals, for example:
15 | * if 'E' -> "0;30" the full string is "\033[0;30m"
16 | * E -> "0" for reset
17 | *
18 | * E -> "T;MC"
19 | * T values:
20 | * 0 for regular
21 | * 1 for bold
22 | * 2 for faint
23 | * 3 for italic
24 | * 4 for underline
25 | * M values:
26 | * 3 for foreground normal
27 | * 4 for background normal
28 | * 9 for foreground bright
29 | * 10 for background bright
30 | * C values:
31 | * 0 for black
32 | * 1 for red
33 | * 2 for green
34 | * 3 for yellow
35 | * 4 for blue
36 | * 5 for purple
37 | * 6 for cyan
38 | * 7 for white
39 | */
40 |
41 | #define ANSI_GRAPHIC_RENDITION(e) "\033[" e "m"
42 | #define ANSI_GRAPHIC_RESET_CODE "0"
43 | #define ANSI_GRAPHIC_COLOR(t, m, c) ANSI_GRAPHIC_RENDITION(t ";" m c)
44 |
45 | #define ANSI_GRAPHIC_COLOR_TYPE_REGULAR "0"
46 | #define ANSI_GRAPHIC_COLOR_TYPE_BOLD "1"
47 | #define ANSI_GRAPHIC_COLOR_TYPE_FAINT "2"
48 | #define ANSI_GRAPHIC_COLOR_TYPE_ITALIC "3"
49 | #define ANSI_GRAPHIC_COLOR_TYPE_UNDERLINE "4"
50 |
51 | #define ANSI_GRAPHIC_COLOR_ATTRIBUTE_FOREGROUND_NORMAL "3"
52 | #define ANSI_GRAPHIC_COLOR_ATTRIBUTE_BACKGROUND_NORMAL "4"
53 | #define ANSI_GRAPHIC_COLOR_ATTRIBUTE_FOREGROUND_BRIGHT "9"
54 | #define ANSI_GRAPHIC_COLOR_ATTRIBUTE_BACKGROUND_BRIGHT "10"
55 |
56 | #define ANSI_GRAPHIC_COLOR_CODE_BLACK "0"
57 | #define ANSI_GRAPHIC_COLOR_CODE_RED "1"
58 | #define ANSI_GRAPHIC_COLOR_CODE_GREEN "2"
59 | #define ANSI_GRAPHIC_COLOR_CODE_YELLOW "3"
60 | #define ANSI_GRAPHIC_COLOR_CODE_BLUE "4"
61 | #define ANSI_GRAPHIC_COLOR_CODE_PURPLE "5"
62 | #define ANSI_GRAPHIC_COLOR_CODE_CYAN "6"
63 | #define ANSI_GRAPHIC_COLOR_CODE_WHITE "7"
64 |
65 |
66 | #endif /* ANSI_COLOR_h */
67 |
--------------------------------------------------------------------------------
/Sources/classdumpctl/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // classdumpctl
4 | //
5 | // Created by Leptos on 1/10/23.
6 | // Copyright © 2023 Leptos. All rights reserved.
7 | //
8 |
9 | #import
10 | #import
11 | #import
12 | #import
13 |
14 | #import "ansi-color.h"
15 |
16 |
17 | typedef NS_ENUM(NSUInteger, CDOutputColorMode) {
18 | CDOutputColorModeDefault,
19 | CDOutputColorModeNever,
20 | CDOutputColorModeAlways,
21 | CDOutputColorModeHtmlHljs,
22 | CDOutputColorModeHtmlLsp,
23 |
24 | CDOutputColorModeCaseCount
25 | };
26 |
27 | typedef NS_ENUM(NSUInteger, CDOptionBoolValue) {
28 | CDOptionBoolValueStripProtocolConformance = 0x100,
29 | CDOptionBoolValueStripOverrides,
30 | CDOptionBoolValueStripDuplicates,
31 | CDOptionBoolValueStripSynthesized,
32 | CDOptionBoolValueStripCtorMethod,
33 | CDOptionBoolValueStripDtorMethod,
34 | CDOptionBoolValueAddSymbolImageComments,
35 |
36 | CDOptionBoolValueCaseEnd
37 | };
38 |
39 | static void printUsage(const char *progname) {
40 | printf("Usage: %s [options]\n"
41 | "Options:\n"
42 | " -a, --dyld_shared_cache Interact in the dyld_shared_cache\n"
43 | " by default, dump all classes in the cache\n"
44 | " -l, --list List all classes in the specified image\n"
45 | " if specified with -a/--dyld_shared_cache\n"
46 | " lists all images in the dyld_shared_cache\n"
47 | " -o , --output=
Use path as the output directory\n"
48 | " if specified with -a/--dyld_shared_cache\n"
49 | " the file structure of the cache is written to\n"
50 | " the specified directory, otherwise all classes found\n"
51 | " are written to this directory at the top level\n"
52 | " -m , --color= Set color settings, one of the below\n"
53 | " default: color output using ASNI color escapes only if output is to a TTY\n"
54 | " never: no output is colored\n"
55 | " always: color output to files, pipes, and TTYs using ASNI color escapes\n"
56 | " html-hljs: output in HTML format annotated with hljs classes\n"
57 | " html-lsp: output in HTML format annotated with LSP classes\n"
58 | " -i , --image=
Reference the mach-o image at path\n"
59 | " by default, dump all classes in this image\n"
60 | " otherwise may specify --class or --protocol\n"
61 | " -c , --class= Dump class to stdout (unless -o is specified)\n"
62 | " -p , --protocol= Dump protocol to stdout (unless -o is specified)\n"
63 | " -j , --jobs= Allow N jobs at once\n"
64 | " only applicable when specified with -a/--dyld_shared_cache\n"
65 | " (defaults to number of processing core available)\n"
66 | "\n"
67 | " --strip-protocol-conformance[=flag] Hide properties and methods that are required\n"
68 | " by a protocol the type conforms to\n"
69 | " (defaults to false)\n"
70 | " --strip-overrides[=flag] Hide properties and methods that are inherited\n"
71 | " from the class hierachy\n"
72 | " (defaults to false)\n"
73 | " --strip-duplicates[=flag] Hide duplicate occurrences of a property or method\n"
74 | " (defaults to false)\n"
75 | " --strip-synthesized[=flag] Hide methods and ivars that are synthesized from a property\n"
76 | " (defaults to true)\n"
77 | " --strip-ctor-method[=flag] Hide `.cxx_construct` method\n"
78 | " (defaults to false)\n"
79 | " --strip-dtor-method[=flag] Hide `.cxx_destruct` method\n"
80 | " (defaults to false)\n"
81 | " --add-symbol-comments[=flag] Add comments above each eligible declaration\n"
82 | " with the symbol name and image path the object is found in\n"
83 | " (defaults to false)\n"
84 | "", progname);
85 | }
86 |
87 | static CDClassModel *safelyGenerateModelForClass(Class const cls, IMP const blankIMP) {
88 | Method const initializeMthd = class_getClassMethod(cls, @selector(initialize));
89 | method_setImplementation(initializeMthd, blankIMP);
90 |
91 | return [CDClassModel modelWithClass:cls];
92 | }
93 |
94 | static NSString *ansiEscapedColorThemeForSemanticString(CDSemanticString *const semanticString) {
95 | NSMutableString *build = [NSMutableString string];
96 | // start with a reset - if there were attributes set before we start writing
97 | // it might be confusing, when we eventually do reset later
98 | if (semanticString.length > 0) {
99 | [build appendString:@ANSI_GRAPHIC_RENDITION(ANSI_GRAPHIC_RESET_CODE)];
100 | }
101 | [semanticString enumerateLongestEffectiveRangesUsingBlock:^(NSString *string, CDSemanticType type) {
102 | NSString *ansiRendition = nil;
103 | switch (type) {
104 | case CDSemanticTypeComment:
105 | ansiRendition = @ANSI_GRAPHIC_RENDITION(ANSI_GRAPHIC_COLOR_TYPE_FAINT);
106 | break;
107 | case CDSemanticTypeKeyword:
108 | ansiRendition = @ANSI_GRAPHIC_COLOR(ANSI_GRAPHIC_COLOR_TYPE_REGULAR,
109 | ANSI_GRAPHIC_COLOR_ATTRIBUTE_FOREGROUND_NORMAL,
110 | ANSI_GRAPHIC_COLOR_CODE_RED);
111 | break;
112 | case CDSemanticTypeRecordName:
113 | ansiRendition = @ANSI_GRAPHIC_COLOR(ANSI_GRAPHIC_COLOR_TYPE_REGULAR,
114 | ANSI_GRAPHIC_COLOR_ATTRIBUTE_FOREGROUND_NORMAL,
115 | ANSI_GRAPHIC_COLOR_CODE_CYAN);
116 | break;
117 | case CDSemanticTypeClass:
118 | ansiRendition = @ANSI_GRAPHIC_COLOR(ANSI_GRAPHIC_COLOR_TYPE_REGULAR,
119 | ANSI_GRAPHIC_COLOR_ATTRIBUTE_FOREGROUND_NORMAL,
120 | ANSI_GRAPHIC_COLOR_CODE_CYAN);
121 | break;
122 | case CDSemanticTypeProtocol:
123 | ansiRendition = @ANSI_GRAPHIC_COLOR(ANSI_GRAPHIC_COLOR_TYPE_REGULAR,
124 | ANSI_GRAPHIC_COLOR_ATTRIBUTE_FOREGROUND_NORMAL,
125 | ANSI_GRAPHIC_COLOR_CODE_CYAN);
126 | break;
127 | case CDSemanticTypeNumeric:
128 | ansiRendition = @ANSI_GRAPHIC_COLOR(ANSI_GRAPHIC_COLOR_TYPE_REGULAR,
129 | ANSI_GRAPHIC_COLOR_ATTRIBUTE_FOREGROUND_NORMAL,
130 | ANSI_GRAPHIC_COLOR_CODE_PURPLE);
131 | break;
132 | default:
133 | break;
134 | }
135 | if (ansiRendition != nil) {
136 | [build appendString:ansiRendition];
137 | }
138 | [build appendString:string];
139 | if (ansiRendition != nil) {
140 | [build appendString:@ANSI_GRAPHIC_RENDITION(ANSI_GRAPHIC_RESET_CODE)];
141 | }
142 | }];
143 | return build;
144 | }
145 |
146 | static NSString *sanitizeForHTML(NSString *input) {
147 | NSMutableString *build = [NSMutableString string];
148 | // thanks to https://www.w3.org/International/questions/qa-escapes#use
149 | NSDictionary *replacementMap = @{
150 | @"<": @"<",
151 | @">": @">",
152 | @"&": @"&",
153 | @"\"": @""",
154 | @"'": @"'",
155 | };
156 | [input enumerateSubstringsInRange:NSMakeRange(0, input.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
157 | [build appendString:(replacementMap[substring] ?: substring)];
158 | }];
159 | return build;
160 | }
161 |
162 | static NSString *hljsHtmlForSemanticString(CDSemanticString *const semanticString) {
163 | NSMutableString *build = [NSMutableString string];
164 | // https://highlightjs.readthedocs.io/en/latest/css-classes-reference.html
165 | [semanticString enumerateLongestEffectiveRangesUsingBlock:^(NSString *string, CDSemanticType type) {
166 | NSString *htmlCls = nil;
167 | switch (type) {
168 | case CDSemanticTypeComment:
169 | htmlCls = @"hljs-comment";
170 | break;
171 | case CDSemanticTypeKeyword:
172 | htmlCls = @"hljs-keyword";
173 | break;
174 | case CDSemanticTypeVariable:
175 | htmlCls = @"hljs-variable";
176 | break;
177 | case CDSemanticTypeRecordName:
178 | htmlCls = @"hljs-type";
179 | break;
180 | case CDSemanticTypeClass:
181 | // hljs-class is deprecated
182 | htmlCls = @"hljs-title class";
183 | break;
184 | case CDSemanticTypeProtocol:
185 | // hljs does not officially define `hljs-title.protocol`
186 | // however `hljs-title` is still a class that themes should style
187 | htmlCls = @"hljs-title protocol";
188 | break;
189 | case CDSemanticTypeNumeric:
190 | htmlCls = @"hljs-number";
191 | break;
192 | default:
193 | break;
194 | }
195 | if (htmlCls != nil) {
196 | [build appendString:@""];
199 | }
200 | [build appendString:sanitizeForHTML(string)];
201 | if (htmlCls != nil) {
202 | [build appendString:@""];
203 | }
204 | }];
205 | return build;
206 | }
207 |
208 | static NSString *lspHtmlForSemanticString(CDSemanticString *const semanticString) {
209 | NSMutableString *build = [NSMutableString string];
210 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#semanticTokenTypes
211 | // https://github.com/leptos-null/LspHighlight/blob/5bce00c/Sources/LspHighlight/LspHighlight.swift#L285
212 | [semanticString enumerateLongestEffectiveRangesUsingBlock:^(NSString *string, CDSemanticType type) {
213 | NSString *htmlCls = nil;
214 | switch (type) {
215 | case CDSemanticTypeComment:
216 | htmlCls = @"lsp-type-comment";
217 | break;
218 | case CDSemanticTypeKeyword:
219 | htmlCls = @"lsp-type-keyword";
220 | break;
221 | case CDSemanticTypeVariable:
222 | htmlCls = @"lsp-type-variable";
223 | break;
224 | case CDSemanticTypeRecordName:
225 | htmlCls = @"lsp-type-struct";
226 | break;
227 | case CDSemanticTypeClass:
228 | htmlCls = @"lsp-type-class";
229 | break;
230 | case CDSemanticTypeProtocol:
231 | htmlCls = @"lsp-type-type";
232 | break;
233 | case CDSemanticTypeNumeric:
234 | htmlCls = @"lsp-type-number";
235 | break;
236 | default:
237 | break;
238 | }
239 | if (htmlCls != nil) {
240 | [build appendString:@""];
243 | }
244 | [build appendString:sanitizeForHTML(string)];
245 | if (htmlCls != nil) {
246 | [build appendString:@""];
247 | }
248 | }];
249 | return build;
250 | }
251 |
252 | static NSString *linesForSemanticStringColorMode(CDSemanticString *const semanticString, CDOutputColorMode const colorMode, BOOL const isOutputTTY) {
253 | BOOL shouldColor = NO;
254 | switch (colorMode) {
255 | case CDOutputColorModeDefault:
256 | shouldColor = isOutputTTY;
257 | break;
258 | case CDOutputColorModeNever:
259 | shouldColor = NO;
260 | break;
261 | case CDOutputColorModeAlways:
262 | shouldColor = YES;
263 | break;
264 | case CDOutputColorModeHtmlHljs:
265 | return hljsHtmlForSemanticString(semanticString);
266 | case CDOutputColorModeHtmlLsp:
267 | return lspHtmlForSemanticString(semanticString);
268 | default:
269 | NSCAssert(NO, @"Unknown case: %lu", (unsigned long)colorMode);
270 | break;
271 | }
272 | if (shouldColor) {
273 | return ansiEscapedColorThemeForSemanticString(semanticString);
274 | }
275 | return [semanticString string];
276 | }
277 |
278 | /// - Returns: `0` if `value` should be handled as `NO`,
279 | /// `1` if `value` should be handled as `YES`,
280 | /// `-1` if there's an error processing `value`
281 | static int parseOptargBool(const char *const value) {
282 | // no value means enable the flag
283 | if (value == NULL) { return 1; }
284 |
285 | if (strcmp(value, "0") == 0) { return 0; }
286 | if (strcmp(value, "1") == 0) { return 1; }
287 | if (strcmp(value, "no") == 0) { return 0; }
288 | if (strcmp(value, "yes") == 0) { return 1; }
289 | if (strcmp(value, "NO") == 0) { return 0; }
290 | if (strcmp(value, "YES") == 0) { return 1; }
291 | if (strcmp(value, "N") == 0) { return 0; }
292 | if (strcmp(value, "Y") == 0) { return 1; }
293 | if (strcmp(value, "n") == 0) { return 0; }
294 | if (strcmp(value, "y") == 0) { return 1; }
295 | if (strcmp(value, "off") == 0) { return 0; }
296 | if (strcmp(value, "on") == 0) { return 1; }
297 | if (strcmp(value, "false") == 0) { return 0; }
298 | if (strcmp(value, "true") == 0) { return 1; }
299 | if (strcmp(value, "FALSE") == 0) { return 0; }
300 | if (strcmp(value, "TRUE") == 0) { return 1; }
301 |
302 | return -1;
303 | }
304 |
305 | int main(int argc, char *argv[]) {
306 | BOOL dyldSharedCacheFlag = NO;
307 | BOOL listFlag = NO;
308 | NSString *outputDir = nil;
309 | CDOutputColorMode outputColorMode = CDOutputColorModeDefault;
310 | NSMutableArray *requestImageList = [NSMutableArray array];
311 | NSMutableArray *requestClassList = [NSMutableArray array];
312 | NSMutableArray *requestProtocolList = [NSMutableArray array];
313 | NSUInteger maxJobs = NSProcessInfo.processInfo.processorCount;
314 |
315 | CDGenerationOptions *const generationOptions = [CDGenerationOptions new];
316 | generationOptions.stripSynthesized = YES;
317 |
318 | struct option const options[] = {
319 | { "dyld_shared_cache", no_argument, NULL, 'a' },
320 | { "list", no_argument, NULL, 'l' },
321 | { "output", required_argument, NULL, 'o' },
322 | { "color", required_argument, NULL, 'm' },
323 | { "image", required_argument, NULL, 'i' },
324 | { "class", required_argument, NULL, 'c' },
325 | { "protocol", required_argument, NULL, 'p' },
326 | { "jobs", required_argument, NULL, 'j' },
327 |
328 | { "strip-protocol-conformance", optional_argument, NULL, CDOptionBoolValueStripProtocolConformance },
329 | { "strip-overrides", optional_argument, NULL, CDOptionBoolValueStripOverrides },
330 | { "strip-duplicates", optional_argument, NULL, CDOptionBoolValueStripDuplicates },
331 | { "strip-synthesized", optional_argument, NULL, CDOptionBoolValueStripSynthesized },
332 | { "strip-ctor-method", optional_argument, NULL, CDOptionBoolValueStripCtorMethod },
333 | { "strip-dtor-method", optional_argument, NULL, CDOptionBoolValueStripDtorMethod },
334 | { "add-symbol-comments", optional_argument, NULL, CDOptionBoolValueAddSymbolImageComments },
335 |
336 | { NULL, 0, NULL, 0 }
337 | };
338 |
339 | int optionIndex = 0;
340 | int ch;
341 | while ((ch = getopt_long(argc, argv, ":alo:m:i:c:p:j:", options, &optionIndex)) != -1) {
342 | switch (ch) {
343 | case CDOptionBoolValueStripProtocolConformance:
344 | case CDOptionBoolValueStripOverrides:
345 | case CDOptionBoolValueStripDuplicates:
346 | case CDOptionBoolValueStripSynthesized:
347 | case CDOptionBoolValueStripCtorMethod:
348 | case CDOptionBoolValueStripDtorMethod:
349 | case CDOptionBoolValueAddSymbolImageComments: {
350 | struct option const *const option = options + optionIndex;
351 | // test if we want to consume the next argument.
352 | // `optional_argument` only provides `optarg` if the
353 | // command line paramter is in the format "--name=value",
354 | // this code allows us to consume "--name" "value".
355 | // We have to validate "value", otherwise we might accidently
356 | // consume "--name" "--flag"
357 | if (optarg == NULL && optind < argc) {
358 | int const parse = parseOptargBool(argv[optind]);
359 | // sucessful parse - consume next arg
360 | if (parse >= 0) {
361 | optarg = argv[optind];
362 | optind++;
363 | }
364 | }
365 | int const parse = parseOptargBool(optarg);
366 | if (parse < 0) {
367 | fprintf(stderr, "Unknown value for --%s: '%s', expected 'yes', 'no'\n", option->name, optarg);
368 | return 1;
369 | }
370 |
371 | BOOL const flag = (parse != 0);
372 | switch (ch) {
373 | case CDOptionBoolValueStripProtocolConformance:
374 | generationOptions.stripProtocolConformance = flag;
375 | break;
376 | case CDOptionBoolValueStripOverrides:
377 | generationOptions.stripOverrides = flag;
378 | break;
379 | case CDOptionBoolValueStripDuplicates:
380 | generationOptions.stripDuplicates = flag;
381 | break;
382 | case CDOptionBoolValueStripSynthesized:
383 | generationOptions.stripSynthesized = flag;
384 | break;
385 | case CDOptionBoolValueStripCtorMethod:
386 | generationOptions.stripCtorMethod = flag;
387 | break;
388 | case CDOptionBoolValueStripDtorMethod:
389 | generationOptions.stripDtorMethod = flag;
390 | break;
391 | case CDOptionBoolValueAddSymbolImageComments:
392 | generationOptions.addSymbolImageComments = flag;
393 | break;
394 | default:
395 | break;
396 | }
397 | } break;
398 | case 'a':
399 | dyldSharedCacheFlag = YES;
400 | break;
401 | case 'l':
402 | listFlag = YES;
403 | break;
404 | case 'o':
405 | outputDir = @(optarg);
406 | break;
407 | case 'm': {
408 | const char *stringyOption = optarg;
409 | if (stringyOption == NULL) {
410 | printUsage(argv[0]);
411 | return 1;
412 | } else if (strcmp(stringyOption, "default") == 0) {
413 | outputColorMode = CDOutputColorModeDefault;
414 | } else if (strcmp(stringyOption, "never") == 0) {
415 | outputColorMode = CDOutputColorModeNever;
416 | } else if (strcmp(stringyOption, "none") == 0) { // alias
417 | outputColorMode = CDOutputColorModeNever;
418 | } else if (strcmp(stringyOption, "always") == 0) {
419 | outputColorMode = CDOutputColorModeAlways;
420 | } else if (strcmp(stringyOption, "ansi") == 0) { // alias
421 | outputColorMode = CDOutputColorModeAlways;
422 | } else if (strcmp(stringyOption, "html-hljs") == 0) {
423 | outputColorMode = CDOutputColorModeHtmlHljs;
424 | } else if (strcmp(stringyOption, "html-lsp") == 0) {
425 | outputColorMode = CDOutputColorModeHtmlLsp;
426 | } else {
427 | printUsage(argv[0]);
428 | return 1;
429 | }
430 | } break;
431 | case 'i':
432 | [requestImageList addObject:@(optarg)];
433 | break;
434 | case 'c':
435 | [requestClassList addObject:@(optarg)];
436 | break;
437 | case 'p':
438 | [requestProtocolList addObject:@(optarg)];
439 | break;
440 | case 'j':
441 | maxJobs = strtoul(optarg, NULL, 10);
442 | break;
443 | default: {
444 | printUsage(argv[0]);
445 | return 1;
446 | } break;
447 | }
448 | }
449 |
450 | BOOL const hasImageRequests = (requestImageList.count > 0);
451 | BOOL const hasSpecificDumpRequests = (requestClassList.count > 0) || (requestProtocolList.count > 0);
452 | if (!hasImageRequests && !hasSpecificDumpRequests && !dyldSharedCacheFlag) {
453 | printUsage(argv[0]);
454 | return 1;
455 | }
456 |
457 | IMP const blankIMP = imp_implementationWithBlock(^{ }); // returns void, takes no parameters
458 |
459 | // just doing this once before we potentially delete some class initializers
460 | [[CDClassModel modelWithClass:NSClassFromString(@"NSObject")] semanticLinesWithOptions:generationOptions];
461 | [[CDProtocolModel modelWithProtocol:NSProtocolFromString(@"NSObject")] semanticLinesWithOptions:generationOptions];
462 |
463 | if (hasImageRequests && !hasSpecificDumpRequests && (outputDir == nil)) {
464 | fprintf(stderr, "-o/--output required to dump all classes in an image\n");
465 | return 1;
466 | }
467 | if ((hasImageRequests || hasSpecificDumpRequests) && outputDir != nil) {
468 | NSFileManager *const fileManager = NSFileManager.defaultManager;
469 | BOOL isDir = NO;
470 | if ([fileManager fileExistsAtPath:outputDir isDirectory:&isDir]) {
471 | if (!isDir) {
472 | fprintf(stderr, "%s is not a directory\n", outputDir.fileSystemRepresentation);
473 | return 1;
474 | }
475 | } else {
476 | NSError *dirError = nil;
477 | if (![fileManager createDirectoryAtPath:outputDir withIntermediateDirectories:YES attributes:nil error:&dirError]) {
478 | NSLog(@"createDirectoryError: %@", dirError);
479 | return 1;
480 | }
481 | }
482 | }
483 |
484 | for (NSString *requestImage in requestImageList) {
485 | dlerror(); // clear
486 | void *imageHandle = dlopen(requestImage.fileSystemRepresentation, RTLD_NOW);
487 | const char *dlerr = dlerror();
488 | if (dlerr != NULL) {
489 | fprintf(stderr, "dlerror: %s\n", dlerr);
490 | }
491 | if (imageHandle == NULL) {
492 | continue;
493 | }
494 |
495 | if (listFlag || !hasSpecificDumpRequests) {
496 | unsigned int classCount = 0;
497 | const char **classNames = objc_copyClassNamesForImage(requestImage.fileSystemRepresentation, &classCount);
498 | for (unsigned int classIndex = 0; classIndex < classCount; classIndex++) {
499 | if (listFlag) {
500 | printf("%s\n", classNames[classIndex]);
501 | continue;
502 | }
503 | Class const cls = objc_getClass(classNames[classIndex]);
504 | CDClassModel *model = safelyGenerateModelForClass(cls, blankIMP);
505 | CDSemanticString *semanticString = [model semanticLinesWithOptions:generationOptions];
506 | NSString *lines = linesForSemanticStringColorMode(semanticString, outputColorMode, NO);
507 | NSString *headerName = [NSStringFromClass(cls) stringByAppendingPathExtension:@"h"];
508 |
509 | NSString *headerPath = [outputDir stringByAppendingPathComponent:headerName];
510 |
511 | NSError *writeError = nil;
512 | if (![lines writeToFile:headerPath atomically:NO encoding:NSUTF8StringEncoding error:&writeError]) {
513 | NSLog(@"writeToFileError: %@", writeError);
514 | }
515 | }
516 | }
517 | // we don't close `imageHandle` since we might dump specific classes later
518 | }
519 |
520 | BOOL const isOutputTTY = (outputDir == nil) && isatty(STDOUT_FILENO);
521 |
522 | for (NSString *requestClassName in requestClassList) {
523 | Class const cls = NSClassFromString(requestClassName);
524 | if (cls == nil) {
525 | fprintf(stderr, "Class named %s not found\n", requestClassName.UTF8String);
526 | continue;
527 | }
528 | CDClassModel *model = safelyGenerateModelForClass(cls, blankIMP);
529 | if (model == nil) {
530 | fprintf(stderr, "Unable to message class named %s\n", requestClassName.UTF8String);
531 | continue;
532 | }
533 | CDSemanticString *string = [model semanticLinesWithOptions:generationOptions];
534 | NSString *lines = linesForSemanticStringColorMode(string, outputColorMode, isOutputTTY);
535 | NSData *encodedLines = [lines dataUsingEncoding:NSUTF8StringEncoding];
536 |
537 | if (outputDir != nil) {
538 | NSString *headerName = [requestClassName stringByAppendingPathExtension:@"h"];
539 | NSString *headerPath = [outputDir stringByAppendingPathComponent:headerName];
540 |
541 | [encodedLines writeToFile:headerPath atomically:NO];
542 | } else {
543 | [NSFileHandle.fileHandleWithStandardOutput writeData:encodedLines];
544 | }
545 | }
546 |
547 | for (NSString *requestProtocolName in requestProtocolList) {
548 | Protocol *const prcl = NSProtocolFromString(requestProtocolName);
549 | if (prcl == nil) {
550 | fprintf(stderr, "Protocol named %s not found\n", requestProtocolName.UTF8String);
551 | continue;
552 | }
553 | CDProtocolModel *model = [CDProtocolModel modelWithProtocol:prcl];
554 | CDSemanticString *string = [model semanticLinesWithOptions:generationOptions];
555 | NSString *lines = linesForSemanticStringColorMode(string, outputColorMode, isOutputTTY);
556 | NSData *encodedLines = [lines dataUsingEncoding:NSUTF8StringEncoding];
557 |
558 | if (outputDir != nil) {
559 | NSString *headerName = [requestProtocolName stringByAppendingPathExtension:@"h"];
560 | NSString *headerPath = [outputDir stringByAppendingPathComponent:headerName];
561 |
562 | [encodedLines writeToFile:headerPath atomically:NO];
563 | } else {
564 | [NSFileHandle.fileHandleWithStandardOutput writeData:encodedLines];
565 | }
566 | }
567 |
568 | if (dyldSharedCacheFlag) {
569 | NSArray *const imagePaths = [CDUtilities dyldSharedCacheImagePaths];
570 | if (listFlag) {
571 | for (NSString *imagePath in imagePaths) {
572 | printf("%s\n", imagePath.fileSystemRepresentation);
573 | }
574 | return 0;
575 | }
576 |
577 | if (outputDir == nil) {
578 | fprintf(stderr, "-o/--output required to dump all classes in the dyld_shared_cache\n");
579 | return 1;
580 | }
581 |
582 | NSFileManager *const fileManager = NSFileManager.defaultManager;
583 |
584 | if ([fileManager fileExistsAtPath:outputDir]) {
585 | fprintf(stderr, "%s already exists\n", outputDir.fileSystemRepresentation);
586 | return 1;
587 | }
588 |
589 | NSMutableDictionary *const pidToPath = [NSMutableDictionary dictionaryWithCapacity:maxJobs];
590 |
591 | NSUInteger activeJobs = 0;
592 | NSUInteger badExitCount = 0;
593 | NSUInteger finishedImageCount = 0;
594 |
595 | NSUInteger const imagePathCount = imagePaths.count;
596 | for (NSUInteger imageIndex = 0; (imageIndex < imagePathCount) || (activeJobs > 0); imageIndex++) {
597 | BOOL const hasImagePath = (imageIndex < imagePathCount);
598 |
599 | if (!hasImagePath || (activeJobs >= maxJobs)) {
600 | int childStatus = 0;
601 | pid_t const childPid = wait(&childStatus);
602 | activeJobs--;
603 |
604 | if (childPid < 0) {
605 | perror("wait");
606 | return 1;
607 | }
608 | NSNumber *key = @(childPid);
609 | NSString *path = pidToPath[key];
610 | [pidToPath removeObjectForKey:key];
611 | finishedImageCount++;
612 |
613 | if (WIFEXITED(childStatus)) {
614 | int const exitStatus = WEXITSTATUS(childStatus);
615 | if (exitStatus != 0) {
616 | printf("Child for '%s' exited with status %d\n", path.fileSystemRepresentation, exitStatus);
617 | badExitCount++;
618 | }
619 | } else if (WIFSIGNALED(childStatus)) {
620 | printf("Child for '%s' signaled with signal %d\n", path.fileSystemRepresentation, WTERMSIG(childStatus));
621 | badExitCount++;
622 | } else {
623 | printf("Child for '%s' did not finish cleanly\n", path.fileSystemRepresentation);
624 | badExitCount++;
625 | }
626 | printf(" %lu/%lu\r", finishedImageCount, imagePathCount);
627 | fflush(stdout); // important to flush after using '\r', but also critical to flush (if needed) before calling `fork`
628 | }
629 | if (hasImagePath) {
630 | NSString *imagePath = imagePaths[imageIndex];
631 |
632 | pid_t const forkStatus = fork();
633 | if (forkStatus < 0) {
634 | perror("fork");
635 | return 1;
636 | }
637 | if (forkStatus == 0) {
638 | // child
639 | NSString *topDir = [outputDir stringByAppendingPathComponent:imagePath];
640 |
641 | NSError *error = nil;
642 | if (![fileManager createDirectoryAtPath:topDir withIntermediateDirectories:YES attributes:nil error:&error]) {
643 | NSLog(@"createDirectoryAtPathError: %@", error);
644 | return 1;
645 | }
646 | NSString *logPath = [topDir stringByAppendingPathComponent:@"log.txt"];
647 |
648 | int const logHandle = open(logPath.fileSystemRepresentation, O_WRONLY | O_CREAT | O_EXCL, 0644);
649 | assert(logHandle >= 0);
650 | dup2(logHandle, STDOUT_FILENO);
651 | dup2(logHandle, STDERR_FILENO);
652 |
653 | dlerror(); // clear
654 | void *imageHandle = dlopen(imagePath.fileSystemRepresentation, RTLD_NOW);
655 | const char *dlerr = dlerror();
656 | if (dlerr != NULL) {
657 | fprintf(stderr, "dlerror: %s\n", dlerr);
658 | }
659 | if (imageHandle == NULL) {
660 | return 1;
661 | }
662 |
663 | // use a group so we can make sure all the work items finish before we exit the program
664 | dispatch_group_t const linesWriteGroup = dispatch_group_create();
665 | // perform file system writes on another thread so we don't unnecessarily block our CPU work
666 | dispatch_queue_t const linesWriteQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
667 |
668 | unsigned int classCount = 0;
669 | const char **classNames = objc_copyClassNamesForImage(imagePath.fileSystemRepresentation, &classCount);
670 | for (unsigned int classIndex = 0; classIndex < classCount; classIndex++) {
671 | Class const cls = objc_getClass(classNames[classIndex]);
672 | // creating the model and generating the "lines" both use
673 | // functions that grab the objc runtime lock, so putting either of
674 | // these on another thread is not efficient, as they would just be blocked
675 | CDClassModel *model = safelyGenerateModelForClass(cls, blankIMP);
676 | if (model == nil) {
677 | continue;
678 | }
679 | CDSemanticString *semanticString = [model semanticLinesWithOptions:generationOptions];
680 |
681 | NSString *lines = linesForSemanticStringColorMode(semanticString, outputColorMode, NO);
682 | NSString *headerName = [NSStringFromClass(cls) stringByAppendingPathExtension:@"h"];
683 |
684 | dispatch_group_async(linesWriteGroup, linesWriteQueue, ^{
685 | NSString *headerPath = [topDir stringByAppendingPathComponent:headerName];
686 | [lines writeToFile:headerPath atomically:NO encoding:NSUTF8StringEncoding error:NULL];
687 | });
688 | }
689 |
690 | dispatch_group_wait(linesWriteGroup, DISPATCH_TIME_FOREVER);
691 |
692 | free(classNames);
693 | dlclose(imageHandle);
694 |
695 | close(logHandle);
696 | unlink(logPath.fileSystemRepresentation);
697 |
698 | return 0; // exit child process
699 | }
700 |
701 | pidToPath[@(forkStatus)] = imagePath;
702 | activeJobs++;
703 | }
704 | }
705 |
706 | printf("%lu images in dyld_shared_cache\n", (unsigned long)imagePaths.count);
707 | printf("Failed to load %lu images\n", (unsigned long)badExitCount);
708 | }
709 | return 0;
710 | }
711 |
--------------------------------------------------------------------------------
/control:
--------------------------------------------------------------------------------
1 | Package: null.leptos.classdumpctl
2 | Name: classdumpctl
3 | Version: 0.0.1
4 | Architecture: iphoneos-arm
5 | Description: Dump Objective-C class and protocol headers
6 | Maintainer: Leptos
7 | Author: Leptos
8 | Section: System
9 | Tag: role::hacker
10 |
--------------------------------------------------------------------------------
/entitlements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.system-groups
6 |
7 | systemgroup.com.apple.powerlog
8 |
9 | com.apple.springboard-ui.client
10 |
11 | platform-application
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------