├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── classdumpctl.xcscheme ├── LICENSE.md ├── Makefile ├── Package.swift ├── README.md ├── Sources └── classdumpctl │ ├── ansi-color.h │ └── main.m ├── control └── entitlements.plist /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push 5 | 6 | jobs: 7 | build: 8 | runs-on: macos-14 9 | steps: 10 | - name: Checkout repo 11 | uses: actions/checkout@v4 12 | with: 13 | submodules: recursive 14 | 15 | - name: Checkout theos/theos 16 | uses: actions/checkout@v4 17 | with: 18 | repository: theos/theos 19 | submodules: recursive 20 | path: theos 21 | 22 | - name: Build with Theos 23 | env: 24 | THEOS: theos 25 | run: | 26 | # we probably don't have `ldid`, so skip codesign 27 | # https://github.com/theos/theos/pull/786 28 | make 'TARGET_CODESIGN = ' 29 | 30 | - name: Build with Swift 31 | run: | 32 | swift build 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Based on 2 | # https://github.com/theos/sdks/blob/ca52092/.github/workflows/release.yml 3 | # Also help from 4 | # https://github.com/NightwindDev/Tweak-Tutorial/blob/d39b124/oldabi.md#compiling-via-github-actions 5 | name: Release 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | release: 14 | # needed to be able to create a Release 15 | permissions: 16 | contents: write 17 | runs-on: macos-14 18 | env: 19 | THEOS: theos 20 | steps: 21 | - name: Checkout repo 22 | uses: actions/checkout@v4 23 | with: 24 | submodules: recursive 25 | 26 | - name: Checkout theos/theos 27 | uses: actions/checkout@v4 28 | with: 29 | repository: theos/theos 30 | submodules: recursive 31 | path: ${{ env.THEOS }} 32 | 33 | - name: Install package depenencies 34 | run: | 35 | brew install make xz ldid 36 | 37 | - name: Build iOS package (default) 38 | run: | 39 | gmake clean package FINALPACKAGE=1 40 | 41 | - name: Build iOS package (rootless) 42 | run: | 43 | gmake clean package FINALPACKAGE=1 THEOS_PACKAGE_SCHEME=rootless 44 | 45 | - name: Build macOS binary (default) 46 | run: | 47 | xcodebuild -workspace .swiftpm/xcode/package.xcworkspace \ 48 | -scheme classdumpctl \ 49 | -config Release \ 50 | -destination 'generic/platform=macOS' \ 51 | -derivedDataPath XcodeDerivedData \ 52 | -quiet 53 | mv XcodeDerivedData/Build/Products/Release/classdumpctl classdumpctl-mac 54 | 55 | - name: Build macOS binary (Mac Catalyst) 56 | run: | 57 | # I don't know how to build for Mac Catalyst without using Xcode. 58 | # the triple 'arm64-apple-ios-macabi' errors out when 59 | # swift-build transforms and passes the value to clang 60 | # clang: error: invalid version number in '-target arm64-apple-ios13.0-macabi' 61 | xcodebuild -workspace .swiftpm/xcode/package.xcworkspace \ 62 | -scheme classdumpctl \ 63 | -config Release \ 64 | -destination 'generic/platform=macOS,variant=Mac Catalyst' \ 65 | -derivedDataPath XcodeDerivedData \ 66 | -quiet 67 | mv XcodeDerivedData/Build/Products/Release-maccatalyst/classdumpctl classdumpctl-maccatalyst 68 | 69 | - name: Publish release 70 | env: 71 | GH_TOKEN: ${{ github.token }} 72 | run: | 73 | TAG="auto-${GITHUB_SHA:0:7}" 74 | gh release create "${TAG}" --draft \ 75 | --title "Automatic Release" \ 76 | --target "${GITHUB_SHA}" \ 77 | packages/*.deb classdumpctl-mac classdumpctl-maccatalyst 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # swift 2 | .DS_Store 3 | /.build 4 | /Packages 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/configuration/registries.json 8 | .netrc 9 | # theos 10 | .theos/ 11 | packages/ 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ClassDumpRuntime"] 2 | path = ClassDumpRuntime 3 | url = git@github.com:leptos-null/ClassDumpRuntime.git 4 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/classdumpctl.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2023 Leptos 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGET := iphone:clang:latest:8.0 2 | ARCHS := arm64 3 | 4 | include $(THEOS)/makefiles/common.mk 5 | 6 | TOOL_NAME = classdumpctl 7 | 8 | classdumpctl_FILES = Sources/classdumpctl/main.m 9 | classdumpctl_FILES += $(wildcard ClassDumpRuntime/Sources/ClassDumpRuntime/ClassDump/*/*.m) 10 | classdumpctl_FILES += $(wildcard ClassDumpRuntime/Sources/ClassDumpRuntime/ClassDump/*/*/*.m) 11 | 12 | classdumpctl_CFLAGS = -fobjc-arc -I ClassDumpRuntime/Sources/ClassDumpRuntime/include 13 | classdumpctl_CODESIGN_FLAGS = -Sentitlements.plist 14 | classdumpctl_INSTALL_PATH = /usr/local/bin 15 | 16 | include $(THEOS_MAKE_PATH)/tool.mk 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "classdumpctl", 8 | platforms: [ 9 | .iOS(.v12), 10 | .macOS(.v10_13), 11 | .watchOS(.v4), 12 | .tvOS(.v12), 13 | .macCatalyst(.v13), 14 | .visionOS(.v1), 15 | ], 16 | products: [ 17 | .executable( 18 | name: "classdumpctl", 19 | targets: ["classdumpctl"] 20 | ) 21 | ], 22 | dependencies: [ 23 | // using a local package since we already have the package 24 | // locally since we need it to build using Theos 25 | .package(path: "ClassDumpRuntime") 26 | ], 27 | targets: [ 28 | .executableTarget( 29 | name: "classdumpctl", 30 | dependencies: [ 31 | .product(name: "ClassDumpRuntime", package: "ClassDumpRuntime") 32 | ] 33 | ), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## classdumpctl 2 | 3 | `classdumpctl` is a command line tool to dump Objective-C class and protocol headers. 4 | 5 | `classdumpctl` is built on top of [ClassDumpRuntime](https://github.com/leptos-null/ClassDumpRuntime). 6 | 7 | ### Usage 8 | 9 | `classdumpctl` is designed for 3 primary uses: 10 | 11 | - Inspecting a single class or protocol at a time 12 | - use `-c ` or `-p ` 13 | - the output will be colorized by default (see `-m`) 14 | - you may need to specify `-i ` with the path to the library or framework containing the class or protocol if it's not already loaded 15 | - Dumping a single library or framework 16 | - use `-i ` to specify the path to the binary 17 | - use `-o ` to specify a directory to put the headers in 18 | - Dumping the entire `dyld_shared_cache` 19 | - use `-a` 20 | - use `-o ` 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 | --------------------------------------------------------------------------------