├── .gitignore ├── .markdownlint.yml ├── .swiftlint.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── mypy.ini ├── resources ├── app-info.png ├── device-info.png ├── file-tree.png ├── keyWindow-detail.png ├── keyWindow-simple.png └── keyWindow.png └── src ├── app.py ├── commands.py ├── cookie.py ├── device.py ├── file.py ├── iLLDB.py ├── lldbhelper ├── SBValue.py ├── __init__.py └── lldb_command_base.py ├── objc.py ├── objc ├── cat.m ├── cookie_delete.m └── cookie_read.m ├── swift ├── app.swift ├── deviceIOS.swift ├── deviceMacOS.swift ├── file.swift └── tree.swift ├── ud.py ├── ui.py └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /.markdownlint.yml: -------------------------------------------------------------------------------- 1 | # for reference, see https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml 2 | 3 | # Default state for all rules 4 | default: true 5 | 6 | # MD024/no-duplicate-heading/no-duplicate-header: Multiple headings with the same content 7 | MD024: false 8 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | #- block_based_kvo 3 | #- class_delegate_protocol 4 | #- closing_brace 5 | #- closure_parameter_position 6 | #- colon 7 | #- comma 8 | #- comment_spacing 9 | #- compiler_protocol_init 10 | #- computed_accessors_order 11 | #- control_statement 12 | #- custom_rules 13 | #- cyclomatic_complexity 14 | #- deployment_target 15 | #- discouraged_direct_init 16 | #- duplicate_enum_cases 17 | #- duplicate_imports 18 | #- dynamic_inline 19 | #- empty_enum_arguments 20 | #- empty_parameters 21 | #- empty_parentheses_with_trailing_closure 22 | #- file_length 23 | #- for_where 24 | - force_cast 25 | #- force_try 26 | #- function_body_length 27 | #- function_parameter_count 28 | #- generic_type_name 29 | #- identifier_name 30 | #- implicit_getter 31 | #- inclusive_language 32 | #- inert_defer 33 | #- is_disjoint 34 | #- large_tuple 35 | #- leading_whitespace 36 | #- legacy_cggeometry_functions 37 | #- legacy_constant 38 | #- legacy_constructor 39 | #- legacy_hashing 40 | #- legacy_nsgeometry_functions 41 | #- line_length 42 | #- mark 43 | #- multiple_closures_with_trailing_closure 44 | #- nesting 45 | #- no_fallthrough_only 46 | #- no_space_in_method_call 47 | #- notification_center_detachment 48 | #- nsobject_prefer_isequal 49 | #- opening_brace 50 | #- operator_whitespace 51 | #- orphaned_doc_comment 52 | #- private_over_fileprivate 53 | #- private_unit_test 54 | #- protocol_property_accessors_order 55 | #- reduce_boolean 56 | #- redundant_discardable_let 57 | #- redundant_objc_attribute 58 | #- redundant_optional_initialization 59 | #- redundant_set_access_control 60 | #- redundant_string_enum_value 61 | #- redundant_void_return 62 | #- return_arrow_whitespace 63 | #- shorthand_operator 64 | #- statement_position 65 | #- superfluous_disable_command 66 | #- switch_case_alignment 67 | #- syntactic_sugar 68 | #- todo 69 | #- trailing_comma 70 | #- trailing_newline 71 | #- trailing_semicolon 72 | #- trailing_whitespace 73 | #- type_body_length 74 | #- type_name 75 | #- unneeded_break_in_switch 76 | #- unused_capture_list 77 | #- unused_closure_parameter 78 | #- unused_control_flow_label 79 | #- unused_enumerated 80 | #- unused_optional_binding 81 | #- unused_setter_value 82 | #- valid_ibinspectable 83 | #- vertical_parameter_alignment 84 | #- vertical_whitespace 85 | #- void_return 86 | #- weak_delegate 87 | #- xctfail_message 88 | 89 | 90 | opt_in_rules: 91 | - anyobject_protocol 92 | - array_init 93 | - attributes 94 | - balanced_xctest_lifecycle 95 | - capture_variable 96 | - closure_body_length 97 | - closure_end_indentation 98 | - closure_spacing 99 | - collection_alignment 100 | #- conditional_returns_on_newline 101 | - contains_over_filter_count 102 | - contains_over_filter_is_empty 103 | - contains_over_first_not_nil 104 | - contains_over_range_nil_comparison 105 | - convenience_type 106 | - discarded_notification_center_observer 107 | - discouraged_assert 108 | #- discouraged_object_literal 109 | - discouraged_optional_boolean 110 | - discouraged_optional_collection 111 | - empty_collection_literal 112 | - empty_count 113 | - empty_string 114 | - empty_xctest_method 115 | - enum_case_associated_values_count 116 | - expiring_todo 117 | #- explicit_acl 118 | #- explicit_enum_raw_value 119 | - explicit_init 120 | #- explicit_self 121 | #- explicit_top_level_acl 122 | #- explicit_type_interface 123 | #- extension_access_modifier 124 | - fallthrough 125 | - fatal_error_message 126 | #- file_header 127 | - file_name 128 | - file_name_no_space 129 | - file_types_order 130 | - first_where 131 | - flatmap_over_map_reduce 132 | - force_unwrapping 133 | - function_default_parameter_at_end 134 | - ibinspectable_in_extension 135 | - identical_operands 136 | - implicit_return 137 | #- implicitly_unwrapped_optional 138 | #- indentation_width 139 | - joined_default_parameter 140 | - last_where 141 | - legacy_multiple 142 | #- legacy_objc_type 143 | - legacy_random 144 | - let_var_whitespace 145 | - literal_expression_end_indentation 146 | - lower_acl_than_parent 147 | - missing_docs 148 | - modifier_order 149 | #- multiline_arguments 150 | #- multiline_arguments_brackets 151 | #- multiline_function_chains 152 | #- multiline_literal_brackets 153 | #- multiline_parameters 154 | #- multiline_parameters_brackets 155 | - nimble_operator 156 | #- no_extension_access_modifier 157 | #- no_grouping_extension 158 | - nslocalizedstring_key 159 | - nslocalizedstring_require_bundle 160 | #- number_separator 161 | #- object_literal 162 | - operator_usage_whitespace 163 | - optional_enum_case_matching 164 | - overridden_super_call 165 | - override_in_extension 166 | - pattern_matching_keywords 167 | - prefer_nimble 168 | - prefer_self_type_over_type_of_self 169 | - prefer_zero_over_explicit_init 170 | #- prefixed_toplevel_constant 171 | - private_action 172 | - private_outlet 173 | - private_subject 174 | #- prohibited_interface_builder 175 | - prohibited_super_call 176 | - quick_discouraged_call 177 | - quick_discouraged_focused_test 178 | - quick_discouraged_pending_test 179 | - raw_value_for_camel_cased_codable_enum 180 | - reduce_into 181 | - redundant_nil_coalescing 182 | - redundant_type_annotation 183 | #- required_deinit 184 | - required_enum_case 185 | - single_test_class 186 | - sorted_first_last 187 | #- sorted_imports 188 | - static_operator 189 | - strict_fileprivate 190 | #- strong_iboutlet 191 | - switch_case_on_newline 192 | - test_case_accessibility 193 | - toggle_bool 194 | - trailing_closure 195 | - type_contents_order 196 | - unavailable_function 197 | - unneeded_parentheses_in_closure_argument 198 | - unowned_variable_capture 199 | - untyped_error_in_catch 200 | - unused_declaration 201 | - unused_import 202 | - vertical_parameter_alignment_on_call 203 | #- vertical_whitespace_between_cases 204 | #- vertical_whitespace_closing_braces 205 | #- vertical_whitespace_opening_braces 206 | - xct_specific_matcher 207 | - yoda_condition 208 | 209 | excluded: 210 | - Pods 211 | - Carthage 212 | - SourcePackages 213 | - Generated 214 | - BuildTools 215 | 216 | line_length: 217 | warning: 300 218 | error: 500 219 | 220 | identifier_name: 221 | min_length: 222 | warning: 1 223 | excluded: 224 | - id 225 | - URL 226 | - GlobalAPIKey 227 | 228 | file_name: 229 | excluded: 230 | - main.swift 231 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnPaste": true, 4 | "[python]": { 5 | "editor.defaultFormatter": "ms-python.autopep8" 6 | }, 7 | "flake8.args": [ 8 | "--ignore=E226 E401 E402 E741", 9 | "--max-line-length=150" 10 | ], 11 | "autopep8.args": [ 12 | "--ignore=E402", 13 | "--max-line-length=150", 14 | "--aggressive" 15 | ], 16 | "mypy-type-checker.args": [ 17 | "--config-file=mypy.ini" 18 | ], 19 | "mypy.configFile": "mypy.ini", 20 | "terminal.integrated.env.osx": { 21 | "PYTHONPATH": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python" 22 | }, 23 | "python.analysis.extraPaths": [ 24 | "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python" 25 | ], 26 | "markdown.extension.toc.updateOnSave": false, 27 | // "python.linting.mypyEnabled": true, 28 | // "python.linting.enabled": true, 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 p-x9 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iLLDB 2 | 3 | LLDB Extension for iOS App Development 4 | 5 | 6 | 7 | [![Github issues](https://img.shields.io/github/issues/p-x9/iLLDB)](https://github.com/p-x9/iLLDB/issues) 8 | [![Github forks](https://img.shields.io/github/forks/p-x9/iLLDB)](https://github.com/p-x9/iLLDB/network/members) 9 | [![Github stars](https://img.shields.io/github/stars/p-x9/iLLDB)](https://github.com/p-x9/iLLDB/stargazers) 10 | [![Github top language](https://img.shields.io/github/languages/top/p-x9/iLLDB)](https://github.com/p-x9/iLLDB/) 11 | 12 | ## Feature 13 | 14 | - [Show view hierarchy](#ui-hierarchy) 15 | - [Easy operation of UserDefaults](#userdefaults) 16 | - [Show device information](#device-info) 17 | - [Show App information](#app-info) 18 | - [Show file hierarchy](#file-hierarchy) 19 | - [Open directory in Finder (Simulator Only)](#open-directory-in-finder-app-simulator-only) 20 | - [Show file contents](#show-file-contents) 21 | - [Easy operation of HTTP Cookie](#http-cookie) 22 | - [Show Cookie Values](#read-cookie-value) 23 | - [Delete Cookie](#delete-cookie) 24 | - [Objective-C Runtime](#objective-c-runtime) 25 | - [Show inheritance hierarchy of object's class](#show-inheritance-hierarchy-of-objects-class) 26 | - [Show list of methods of object's class](#show-a-list-of-methods-of-objects-class) 27 | - [Show list of properties of object's class](#show-a-list-of-proerties-of-objects-class) 28 | - [Show list of ivars of object's class](#show-a-list-of-ivars-of-objects-class) 29 | 30 | ## Set up 31 | 32 | 1. clone this repository 33 | 2. Add the following line to ~/.lldbinit 34 | 35 | ```sh 36 | command script import {PATH TO iLLDB}/src/iLLDB.py 37 | ``` 38 | 39 | ## Usage 40 | 41 | ### UI hierarchy 42 | 43 | ```sh 44 | (lldb) ui tree -h 45 | usage: tree 46 | [-h] 47 | [-d] 48 | [-s] 49 | [--depth DEPTH] 50 | [--with-address] 51 | [--window WINDOW] 52 | [--view VIEW] 53 | [--vc VC] 54 | [--layer LAYER] 55 | 56 | optional arguments: 57 | -h, --help 58 | show this help message and exit 59 | -d, --detail 60 | Enable detailed mode (default: False) 61 | -s, --simple 62 | Enable simpled mode (default: False) 63 | --depth DEPTH 64 | Maximum depth to be displayed (default: None) 65 | --with-address 66 | Print address of ui (default: False) 67 | --window WINDOW 68 | Specify the target window (default: None) 69 | --view VIEW 70 | Specify the target view (property or address) (default: None) 71 | --vc VC 72 | Specify the target viewController (property or address) (default: None) 73 | --layer LAYER 74 | Specify the target CALayer (property or address) (default: None) 75 | ``` 76 | 77 | #### Example 78 | 79 | - Show keyWindow hierarchy 80 | 81 | ```sh 82 | ui tree 83 | ``` 84 | 85 | ![KeyWindow](./resources/keyWindow.png) 86 | 87 | ```sh 88 | ui tree -s # simple 89 | ``` 90 | 91 | ![KeyWindow](./resources/keyWindow-simple.png) 92 | 93 | ```sh 94 | ui tree -d # detail 95 | ``` 96 | 97 | ![KeyWindow](./resources/keyWindow-detail.png) 98 | 99 | - Show the hierarchy of a specific view 100 | 101 | ```sh 102 | ui tree -view {property name of view} 103 | ``` 104 | 105 | - Show the hierarchy of a specific viewController 106 | 107 | ```sh 108 | ui tree -vc {property name of viewController} 109 | ``` 110 | 111 | - Show the hierarchy of a specific window 112 | 113 | ```sh 114 | ui tree -window {property name of window} 115 | ``` 116 | 117 | - Show the hierarchy of a specific layer 118 | 119 | ```sh 120 | ui tree -layer {property name of layer} 121 | ``` 122 | 123 | ### UserDefaults 124 | 125 | ```sh 126 | usage: 127 | [-h] 128 | {read,write,delete,read-all,delete-all} 129 | ... 130 | UserDefault debugging 131 | optional arguments: 132 | -h, --help 133 | show this help message and exit 134 | Subcommands: 135 | {read,write,delete,read-all,delete-all} 136 | read 137 | read UserDefault value 138 | write 139 | write UserDefault value 140 | delete 141 | delete UserDefault value 142 | read-all 143 | read all UserDefault value 144 | delete-all 145 | delete all UserDefault value 146 | ``` 147 | 148 | #### read 149 | 150 | ```sh 151 | ud read "key" 152 | ``` 153 | 154 | #### write 155 | 156 | ```sh 157 | ud write "key" "value" 158 | ``` 159 | 160 | #### delete 161 | 162 | ```sh 163 | ud delete "key" 164 | ``` 165 | 166 | #### read all 167 | 168 | ```sh 169 | ud read-all 170 | ``` 171 | 172 | #### delete all 173 | 174 | ```sh 175 | ud delete-all 176 | ``` 177 | 178 | ### Device Info 179 | 180 | Displays device information. 181 | 182 | ```sh 183 | device info 184 | ``` 185 | 186 | ![device info](./resources/device-info.png) 187 | 188 | ### App Info 189 | 190 | Displays App information. 191 | 192 | ```sh 193 | app info 194 | ``` 195 | 196 | ![app info](./resources/app-info.png) 197 | 198 | ### File hierarchy 199 | 200 | ```sh 201 | (lldb) file tree -h 202 | usage: tree 203 | [-h] 204 | [-p PATH] 205 | [-b] 206 | [-l] 207 | [--documents] 208 | [--tmp] 209 | [--depth DEPTH] 210 | optional arguments: 211 | -h, --help 212 | show this help message and exit 213 | -p PATH, --path PATH 214 | path (default: None) 215 | -b, --bundle 216 | bundle directory (default: False) 217 | -l, --library 218 | library directory (default: False) 219 | --documents 220 | documents directory (default: False) 221 | --tmp 222 | tmp directory (default: False) 223 | --depth DEPTH 224 | Maximum depth to be displayed (default: None) 225 | ``` 226 | 227 | #### Example 228 | 229 | - Display the contents of the Bundle directory 230 | 231 | ```sh 232 | file tree --bundle 233 | ``` 234 | 235 | - Display the contents of the Library directory 236 | 237 | ```sh 238 | file tree --library 239 | ``` 240 | 241 | - Display the contents of the Documents directory 242 | 243 | ```sh 244 | file tree --documents 245 | ``` 246 | 247 | - Display the contents of the tmp directory 248 | 249 | ```sh 250 | file tree --tmp 251 | ``` 252 | 253 | - Display the contents of a specific directory 254 | 255 | ```sh 256 | file tree --path {url} 257 | ``` 258 | 259 | ![file tree](./resources/file-tree.png) 260 | 261 | ### Open directory in Finder App (Simulator Only) 262 | 263 | ```sh 264 | (lldb) file open -h 265 | usage: open 266 | [-h] 267 | [-p PATH] 268 | [-b] 269 | [-l] 270 | [--documents] 271 | [--tmp] 272 | optional arguments: 273 | -h, --help 274 | show this help message and exit 275 | -p PATH, --path PATH 276 | path (default: None) 277 | -b, --bundle 278 | bundle directory (default: False) 279 | -l, --library 280 | library directory (default: False) 281 | --documents 282 | documents directory (default: False) 283 | --tmp 284 | tmp directory (default: False) 285 | ``` 286 | 287 | ### Show file Contents 288 | 289 | ```sh 290 | (lldb) file cat -h 291 | usage: cat 292 | [-h] 293 | [--mode MODE] 294 | path 295 | positional arguments: 296 | path 297 | path 298 | optional arguments: 299 | -h, --help 300 | show this help message and exit 301 | --mode MODE 302 | mode [text, plist] (default: text) 303 | ``` 304 | 305 | #### Example 306 | 307 | - text file 308 | 309 | ```sh 310 | file cat "path" 311 | ``` 312 | 313 | - plist file 314 | 315 | ```sh 316 | file cat "path" --mode plist 317 | ``` 318 | 319 | ### HTTP Cookie 320 | 321 | #### Read Cookie Value 322 | 323 | Displays the value of the HTTP cookie information. 324 | 325 | ```sh 326 | (lldb) cookie read -h 327 | usage: read 328 | [-h] 329 | [--group-id GROUP_ID] 330 | [--domain DOMAIN] 331 | [--name NAME] 332 | [--path PATH] 333 | optional arguments: 334 | -h, --help 335 | show this help message and exit 336 | --group-id GROUP_ID 337 | AppGroup identifier for cookie storage (default: None) 338 | --domain DOMAIN 339 | Domain for Cookie (default: None) 340 | --name NAME 341 | Name for Cookie (default: None) 342 | --path PATH 343 | Path for Cookie (default: None) 344 | ``` 345 | 346 | ##### Example 347 | 348 | - Show all cookies 349 | 350 | ```sh 351 | cookie read 352 | ``` 353 | 354 | - Show only cookies for specific domains 355 | 356 | ```sh 357 | cookie read --domain example.com 358 | ``` 359 | 360 | - Show only cookies with a specific name from a specific domain 361 | 362 | ```sh 363 | cookie read --domain example.com --name KEYNAME 364 | ``` 365 | 366 | #### Delete Cookie 367 | 368 | Delete cookie value. 369 | 370 | After executing the command, you will be asked to confirm before deleting. 371 | If you type "Yes", the deletion will be executed as is. 372 | 373 | ```sh 374 | (lldb) cookie delete -h 375 | usage: delete 376 | [-h] 377 | [--group-id GROUP_ID] 378 | [--domain DOMAIN] 379 | [--name NAME] 380 | [--path PATH] 381 | optional arguments: 382 | -h, --help 383 | show this help message and exit 384 | --group-id GROUP_ID 385 | AppGroup identifier for cookie storage (default: None) 386 | --domain DOMAIN 387 | Domain for Cookie (default: None) 388 | --name NAME 389 | Name for Cookie (default: None) 390 | --path PATH 391 | Path for Cookie (default: None) 392 | ``` 393 | 394 | ##### Example 395 | 396 | - Delete all cookies 397 | 398 | ```sh 399 | cookie delete 400 | ``` 401 | 402 | - Delete only cookies for specific domains 403 | 404 | ```sh 405 | cookie delete --domain example.com 406 | ``` 407 | 408 | - Delete only cookies with a specific name from a specific domain 409 | 410 | ```sh 411 | cookie delete --domain example.com --name KEYNAME 412 | ``` 413 | 414 | ### Objective-C Runtime 415 | 416 | Commands for debugging with Objective-C Runtime 417 | 418 | #### Show inheritance hierarchy of object's class 419 | 420 | ```sh 421 | (lldb) objc inherits -h 422 | usage: inherits 423 | [-h] 424 | object 425 | positional arguments: 426 | object 427 | object 428 | optional arguments: 429 | -h, --help 430 | show this help message and exit 431 | ``` 432 | 433 | ##### Example 434 | 435 | ```sh 436 | (lldb)objc inherits UIWindow() 437 | # NSObject -> UIResponder -> UIView -> UIWindow 438 | ``` 439 | 440 | #### Show a list of methods of object's class 441 | 442 | ```sh 443 | (lldb) objc methods -h 444 | usage: methods 445 | [-h] 446 | [--class CLASS_NAME] 447 | [-c] 448 | [-i] 449 | object 450 | positional arguments: 451 | object 452 | object 453 | optional arguments: 454 | -h, --help 455 | show this help message and exit 456 | --class CLASS_NAME 457 | Specify a target class in the inheritance hierarchy (default: None) 458 | -c, --class-only 459 | Show only class methods (default: False) 460 | -i, --instance-only 461 | Show only instance methods (default: False) 462 | ``` 463 | 464 | #### Show a list of proerties of object's class 465 | 466 | ```sh 467 | (lldb) objc properties -h 468 | usage: properties 469 | [-h] 470 | [--class CLASS_NAME] 471 | object 472 | positional arguments: 473 | object 474 | object 475 | optional arguments: 476 | -h, --help 477 | show this help message and exit 478 | --class CLASS_NAME 479 | Specify a target class in the inheritance hierarchy (default: None) 480 | ``` 481 | 482 | #### Show a list of ivars of object's class 483 | 484 | ```sh 485 | (lldb) objc ivars -h 486 | usage: ivars 487 | [-h] 488 | [--class CLASS_NAME] 489 | object 490 | positional arguments: 491 | object 492 | object 493 | optional arguments: 494 | -h, --help 495 | show this help message and exit 496 | --class CLASS_NAME 497 | Specify a target class in the inheritance hierarchy (default: None) 498 | ``` 499 | 500 | ## License 501 | 502 | iLLDB is released under the MIT License. See [LICENSE](./LICENSE) 503 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | no_implicit_optional = True 3 | strict_optional = True 4 | ignore_missing_imports = True 5 | show_column_numbers = True 6 | 7 | follow_imports = skip 8 | warn_return_any = True 9 | check_untyped_defs = True 10 | disallow_untyped_calls = True 11 | disallow_untyped_defs = True 12 | -------------------------------------------------------------------------------- /resources/app-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-x9/iLLDB/37edc17398173867b2dba75a4767faf51f7eef76/resources/app-info.png -------------------------------------------------------------------------------- /resources/device-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-x9/iLLDB/37edc17398173867b2dba75a4767faf51f7eef76/resources/device-info.png -------------------------------------------------------------------------------- /resources/file-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-x9/iLLDB/37edc17398173867b2dba75a4767faf51f7eef76/resources/file-tree.png -------------------------------------------------------------------------------- /resources/keyWindow-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-x9/iLLDB/37edc17398173867b2dba75a4767faf51f7eef76/resources/keyWindow-detail.png -------------------------------------------------------------------------------- /resources/keyWindow-simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-x9/iLLDB/37edc17398173867b2dba75a4767faf51f7eef76/resources/keyWindow-simple.png -------------------------------------------------------------------------------- /resources/keyWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-x9/iLLDB/37edc17398173867b2dba75a4767faf51f7eef76/resources/keyWindow.png -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | import lldb 2 | import shlex 3 | import argparse 4 | from typing import Union, cast 5 | from lldbhelper import LLDBCommandBase 6 | import util 7 | 8 | 9 | def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None: 10 | AppCommnad.register_lldb_command(debugger, AppCommnad.__module__) 11 | 12 | 13 | class AppCommnad(LLDBCommandBase): 14 | 15 | @classmethod 16 | def cmdname(cls) -> str: 17 | return 'app' 18 | 19 | @classmethod 20 | def description(cls) -> str: 21 | return 'Application debugging. [iLLDB]' 22 | 23 | def create_argparser(self) -> argparse.ArgumentParser: 24 | parser = argparse.ArgumentParser(description="App debugging", 25 | formatter_class=util.HelpFormatter) 26 | subparsers = parser.add_subparsers(title="Subcommands", dest="subcommand") 27 | 28 | subparsers.add_parser("info", 29 | help="Show App info", 30 | formatter_class=util.HelpFormatter) 31 | 32 | return parser 33 | 34 | def __call__( 35 | self, 36 | debugger: lldb.SBDebugger, 37 | command: str, 38 | exe_ctx: lldb.SBExecutionContext, 39 | result: lldb.SBCommandReturnObject 40 | ) -> None: 41 | args: Union[list[str], argparse.Namespace] = shlex.split(command, posix=False) 42 | args = self.argparser.parse_args(cast(list[str], args)) 43 | 44 | args = cast(argparse.Namespace, args) 45 | if args.subcommand == "info": 46 | self.info(args, debugger, result) 47 | else: 48 | self.argparser.print_help() 49 | 50 | def info( 51 | self, 52 | args: argparse.Namespace, 53 | debugger: lldb.SBDebugger, 54 | result: lldb.SBCommandReturnObject 55 | ) -> None: 56 | script = util.read_script_file('swift/app.swift') 57 | script += """ 58 | printAppInfo() 59 | """ 60 | 61 | _ = util.exp_script( 62 | debugger, 63 | script 64 | ) 65 | -------------------------------------------------------------------------------- /src/commands.py: -------------------------------------------------------------------------------- 1 | import lldb 2 | import shlex 3 | import util 4 | 5 | 6 | @lldb.command('mirror', doc='Display child elements using Mirror. [iLLDB]') 7 | def mirror( 8 | debugger: lldb.SBDebugger, 9 | command: str, 10 | exe_ctx: lldb.SBExecutionContext, 11 | result: lldb.SBCommandReturnObject, 12 | internal_dict: dict 13 | ) -> None: 14 | """ 15 | Display the child elements of an object using the Mirror class in Swift. 16 | 17 | Args: 18 | debugger (lldb.SBDebugger): The LLDB debugger object. 19 | command (str): The command string passed to the mirror command. 20 | exe_ctx (lldb.SBExecutionContext): The execution context of the command. 21 | result (lldb.SBCommandReturnObject): The object used to return the result of the command. 22 | internal_dict (dict): The internal dictionary of the command. 23 | """ 24 | args = shlex.split(command, posix=False) 25 | if len(args) < 1: 26 | return 27 | script = f""" 28 | import Foundation 29 | let mirror = Mirror(reflecting: {args[0]}) 30 | mirror.children.enumerated().forEach {{ 31 | print("\\($1.label ?? "[\\($0)]"): \\(String(reflecting: type(of: $1.value))) = \\($1.value)") 32 | }} 33 | """ 34 | util.exp_script( 35 | debugger, 36 | script, 37 | lang=lldb.eLanguageTypeSwift 38 | ) 39 | -------------------------------------------------------------------------------- /src/cookie.py: -------------------------------------------------------------------------------- 1 | import lldb 2 | import shlex 3 | import argparse 4 | from typing import Union, cast 5 | from lldbhelper import LLDBCommandBase 6 | import util 7 | 8 | 9 | def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None: 10 | CookieCommnad.register_lldb_command(debugger, CookieCommnad.__module__) 11 | 12 | 13 | class CookieCommnad(LLDBCommandBase): 14 | 15 | @classmethod 16 | def cmdname(cls) -> str: 17 | return 'cookie' 18 | 19 | @classmethod 20 | def description(cls) -> str: 21 | return 'HTTP Cookie debugging. [iLLDB]' 22 | 23 | def create_argparser(self) -> argparse.ArgumentParser: 24 | description = "HTTP Cookie debugging" 25 | parser = argparse.ArgumentParser(description=description, 26 | formatter_class=util.HelpFormatter) 27 | subparsers = parser.add_subparsers(title="Subcommands", dest="subcommand") 28 | 29 | # read 30 | read_command = subparsers.add_parser("read", 31 | help="Read Cookie value", 32 | formatter_class=util.HelpFormatter) 33 | read_command.add_argument("--group-id", type=str, help="AppGroup identifier for cookie storage") 34 | read_command.add_argument("--domain", type=str, help="Domain for Cookie") 35 | read_command.add_argument("--name", type=str, help="Name for Cookie") 36 | read_command.add_argument("--path", type=str, help="Path for Cookie") 37 | 38 | # delete 39 | delete_command = subparsers.add_parser("delete", 40 | help="Delete Cookie", 41 | formatter_class=util.HelpFormatter) 42 | delete_command.add_argument("--group-id", type=str, help="AppGroup identifier for cookie storage") 43 | delete_command.add_argument("--domain", type=str, help="Domain for Cookie") 44 | delete_command.add_argument("--name", type=str, help="Name for Cookie") 45 | delete_command.add_argument("--path", type=str, help="Path for Cookie") 46 | 47 | return parser 48 | 49 | def __call__( 50 | self, 51 | debugger: lldb.SBDebugger, 52 | command: str, 53 | exe_ctx: lldb.SBExecutionContext, 54 | result: lldb.SBCommandReturnObject 55 | ) -> None: 56 | args: Union[list[str], argparse.Namespace] = shlex.split(command, posix=False) 57 | args = self.argparser.parse_args(cast(list[str], args)) 58 | 59 | args = cast(argparse.Namespace, args) 60 | if args.subcommand == "read": 61 | self.read(args, debugger, result) 62 | elif args.subcommand == "delete": 63 | self.read(args, debugger, result) 64 | confirm = input('The above cookies will be deleted. Please type "Yes" if OK\n') 65 | if confirm == "Yes": 66 | self.delete(args, debugger, result) 67 | else: 68 | print("Cancelled") 69 | else: 70 | self.argparser.print_help() 71 | 72 | def read(self, args: argparse.Namespace, debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> None: 73 | script = "" 74 | if args.group_id is not None: 75 | script += f'NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"{args.group_id}"];\n' 76 | else: 77 | script += "NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];\n" 78 | 79 | script += util.read_script_file('objc/cookie_read.m') 80 | 81 | # domain 82 | if args.domain is not None: 83 | script = script.replace("", args.domain) 84 | else: 85 | script = script.replace("@\"\"", "NULL") 86 | 87 | # name 88 | if args.name is not None: 89 | script = script.replace("", args.name) 90 | else: 91 | script = script.replace("@\"\"", "NULL") 92 | 93 | # path 94 | if args.path is not None: 95 | script = script.replace("", args.path) 96 | else: 97 | script = script.replace("@\"\"", "NULL") 98 | 99 | _ = util.exp_script( 100 | debugger, 101 | script, 102 | lang=lldb.eLanguageTypeObjC 103 | ) 104 | 105 | def delete(self, args: argparse.Namespace, debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> None: 106 | script = "" 107 | if args.group_id is not None: 108 | script += f'NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"{args.group_id}"];\n' 109 | else: 110 | script += "NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];\n" 111 | 112 | script += util.read_script_file('objc/cookie_delete.m') 113 | 114 | # domain 115 | if args.domain is not None: 116 | script = script.replace("", args.domain) 117 | else: 118 | script = script.replace("@\"\"", "NULL") 119 | 120 | # name 121 | if args.name is not None: 122 | script = script.replace("", args.name) 123 | else: 124 | script = script.replace("@\"\"", "NULL") 125 | 126 | # path 127 | if args.path is not None: 128 | script = script.replace("", args.path) 129 | else: 130 | script = script.replace("@\"\"", "NULL") 131 | 132 | _ = util.exp_script( 133 | debugger, 134 | script, 135 | lang=lldb.eLanguageTypeObjC 136 | ) 137 | -------------------------------------------------------------------------------- /src/device.py: -------------------------------------------------------------------------------- 1 | import lldb 2 | import shlex 3 | import argparse 4 | from typing import Union, cast 5 | from lldbhelper import LLDBCommandBase 6 | import util 7 | 8 | 9 | def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None: 10 | DeviceCommnad.register_lldb_command(debugger, DeviceCommnad.__module__) 11 | 12 | 13 | class DeviceCommnad(LLDBCommandBase): 14 | @classmethod 15 | def cmdname(cls) -> str: 16 | return 'device' 17 | 18 | @classmethod 19 | def description(cls) -> str: 20 | return "Device debugging. [iLLDB]" 21 | 22 | def create_argparser(self) -> argparse.ArgumentParser: 23 | parser = argparse.ArgumentParser(description="Device debugging", 24 | formatter_class=util.HelpFormatter) 25 | subparsers = parser.add_subparsers(title="Subcommands", dest="subcommand") 26 | 27 | subparsers.add_parser("info", 28 | help="Show device info", 29 | formatter_class=util.HelpFormatter) 30 | 31 | return parser 32 | 33 | def __call__( 34 | self, 35 | debugger: lldb.SBDebugger, 36 | command: str, 37 | exe_ctx: lldb.SBExecutionContext, 38 | result: lldb.SBCommandReturnObject 39 | ) -> None: 40 | args: Union[list[str], argparse.Namespace] = shlex.split(command, posix=False) 41 | args = self.argparser.parse_args(cast(list[str], args)) 42 | 43 | args = cast(argparse.Namespace, args) 44 | if args.subcommand == "info": 45 | self.info(args, debugger, result) 46 | else: 47 | self.argparser.print_help() 48 | 49 | def info( 50 | self, 51 | args: argparse.Namespace, 52 | debugger: lldb.SBDebugger, 53 | result: lldb.SBCommandReturnObject 54 | ) -> None: 55 | swift_file_name = "deviceMacOS" if util.isAppKit(debugger) else "deviceIOS" 56 | 57 | script = util.read_script_file(f'swift/{swift_file_name}.swift') 58 | script += """ 59 | printDeviceInfo() 60 | """ 61 | 62 | _ = util.exp_script( 63 | debugger, 64 | script 65 | ) 66 | -------------------------------------------------------------------------------- /src/file.py: -------------------------------------------------------------------------------- 1 | import lldb 2 | import shlex 3 | import argparse 4 | import subprocess 5 | from typing import Union, cast 6 | from lldbhelper import LLDBCommandBase 7 | import util 8 | 9 | 10 | def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None: 11 | FileCommnad.register_lldb_command(debugger, FileCommnad.__module__) 12 | 13 | 14 | class FileCommnad(LLDBCommandBase): 15 | 16 | @classmethod 17 | def cmdname(cls) -> str: 18 | return 'file' 19 | 20 | @classmethod 21 | def description(cls) -> str: 22 | return 'File debugging. [iLLDB]' 23 | 24 | def create_argparser(self) -> argparse.ArgumentParser: 25 | parser = argparse.ArgumentParser(description="File debugging", 26 | formatter_class=util.HelpFormatter) 27 | subparsers = parser.add_subparsers(title="Subcommands", 28 | dest="subcommand") 29 | 30 | tree_command = subparsers.add_parser("tree", 31 | help="Show file hierarchie", 32 | formatter_class=util.HelpFormatter) 33 | tree_command.add_argument("-p", "--path", type=str, help="path") 34 | tree_command.add_argument("-b", "--bundle", action="store_true", help="bundle directory") 35 | tree_command.add_argument("-l", "--library", action="store_true", help="library directory") 36 | tree_command.add_argument("--documents", action="store_true", help="documents directory") 37 | tree_command.add_argument("--tmp", action="store_true", help="tmp directory") 38 | tree_command.add_argument("--depth", type=int, help="Maximum depth to be displayed") 39 | 40 | open_command = subparsers.add_parser("open", 41 | help="Open directory with Finder (Simulator Only)", 42 | formatter_class=util.HelpFormatter) 43 | open_command.add_argument("-p", "--path", type=str, required=False, help="path") 44 | open_command.add_argument("-b", "--bundle", action="store_true", help="bundle directory") 45 | open_command.add_argument("-l", "--library", action="store_true", help="library directory") 46 | open_command.add_argument("--documents", action="store_true", help="documents directory") 47 | open_command.add_argument("--tmp", action="store_true", help="tmp directory") 48 | 49 | cat_command = subparsers.add_parser("cat", 50 | help="The content of the specified file is retrieved and displayed", 51 | formatter_class=util.HelpFormatter) 52 | cat_command.add_argument("path", 53 | type=str, 54 | help="path") 55 | cat_command.add_argument("--mode", type=str, default='text', help="mode [text, plist]") 56 | 57 | return parser 58 | 59 | def __call__( 60 | self, 61 | debugger: lldb.SBDebugger, 62 | command: str, 63 | exe_ctx: lldb.SBExecutionContext, 64 | result: lldb.SBCommandReturnObject 65 | ) -> None: 66 | args: Union[list[str], argparse.Namespace] = shlex.split(command, posix=False) 67 | args = self.argparser.parse_args(cast(list[str], args)) 68 | args = cast(argparse.Namespace, args) 69 | 70 | if args.subcommand == "tree": 71 | self.tree(args, debugger, result) 72 | elif args.subcommand == "open": 73 | self.open(args, debugger, result) 74 | elif args.subcommand == "cat": 75 | self.cat(args, debugger, result) 76 | else: 77 | self.argparser.print_help() 78 | 79 | def tree(self, args: argparse.Namespace, debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> None: 80 | script = util.read_script_file('swift/file.swift') 81 | 82 | depth = 'nil' 83 | if args.depth is not None: 84 | try: 85 | depth = str(int(args.depth)) 86 | except ValueError: 87 | pass 88 | 89 | if args.bundle: 90 | script += f"listFilesInDirectory(Bundle.main.bundleURL, depth: {depth})" 91 | elif args.library: 92 | script += f"listFilesInDirectory(FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!, depth: {depth})" 93 | elif args.documents: 94 | script += f"listFilesInDirectory(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!, depth: {depth})" 95 | elif args.tmp: 96 | script += f"listFilesInDirectory(FileManager.default.temporaryDirectory, depth: {depth})" 97 | elif args.path: 98 | path: str = args.path 99 | if not path.startswith('"') and not path.endswith('"'): 100 | path = f"\"{path}\"" 101 | script += f"listFilesInDirectory(URL(fileURLWithPath: {path}), depth: {depth})" 102 | 103 | _ = util.exp_script( 104 | debugger, 105 | script 106 | ) 107 | 108 | def open(self, args: argparse.Namespace, debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> None: 109 | if not (util.isIOSSimulator(debugger) or util.isMacOS(debugger)): 110 | print("Supported only simulator or macOS") 111 | return 112 | 113 | shell = "open -R " 114 | 115 | script = "" 116 | if args.bundle: 117 | script += "Bundle.main.bundlePath" 118 | elif args.library: 119 | script += "FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!.path" 120 | elif args.documents: 121 | script += "FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path" 122 | elif args.tmp: 123 | script += "FileManager.default.temporaryDirectory.path" 124 | elif args.path: 125 | path: str = args.path 126 | if not path.startswith('"') and not path.endswith('"'): 127 | path = f"\"{path}\"" 128 | shell += f"{path}" 129 | 130 | if script != "": 131 | ret = util.exp_script(debugger, script) 132 | if ret: 133 | print(ret.asStr()) 134 | shell += f"{ret.GetObjectDescription()}" 135 | 136 | subprocess.run(shell, shell=True) 137 | 138 | def cat(self, args: argparse.Namespace, debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> None: 139 | script = util.read_script_file('objc/cat.m') 140 | script = script.replace("", args.path) 141 | script = script.replace("", args.mode) 142 | 143 | _ = util.exp_script( 144 | debugger, 145 | script, 146 | lang=lldb.eLanguageTypeObjC 147 | ) 148 | -------------------------------------------------------------------------------- /src/iLLDB.py: -------------------------------------------------------------------------------- 1 | import lldb 2 | import os 3 | import lldbhelper # noqa: F401 4 | 5 | iLLDB_VERSION = "0.8.0" 6 | 7 | 8 | def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None: 9 | file_path = os.path.realpath(__file__) 10 | dir_name = os.path.dirname(file_path) 11 | load_commands(dir_name, debugger) 12 | print(f"[iLLDB] loaded: Version {iLLDB_VERSION}") 13 | 14 | 15 | def load_commands(dir_name: str, debugger: lldb.SBDebugger) -> None: 16 | ignored_files = {"iLLDB.py"} 17 | 18 | for file in os.listdir(dir_name): 19 | full_path = dir_name + '/' + file 20 | 21 | if file in ignored_files: 22 | continue 23 | elif file.endswith('.py'): 24 | cmd = 'command script import ' 25 | elif file.endswith('.h'): 26 | cmd = 'command source -e0 -s1 ' 27 | elif os.path.isdir(full_path): 28 | load_commands(full_path, debugger) 29 | continue 30 | else: 31 | continue 32 | 33 | debugger.HandleCommand(cmd + full_path) 34 | -------------------------------------------------------------------------------- /src/lldbhelper/SBValue.py: -------------------------------------------------------------------------------- 1 | import lldb 2 | from typing import Optional 3 | 4 | 5 | def asInt(self: lldb.SBValue) -> Optional[int]: 6 | if self.IsValid(): 7 | try: 8 | value_str = self.GetValue() 9 | return int(value_str, 0) 10 | except BaseException: 11 | return None 12 | else: 13 | return None 14 | 15 | 16 | def asFloat(self: lldb.SBValue) -> Optional[float]: 17 | if self.IsValid(): 18 | try: 19 | value_str = self.GetValue() 20 | return float(value_str) 21 | except BaseException: 22 | return None 23 | else: 24 | return None 25 | 26 | 27 | def asBool(self: lldb.SBValue) -> Optional[bool]: 28 | if self.IsValid(): 29 | value_str = self.GetValue() 30 | if value_str == 'true' or value_str == 'TRUE' or value_str == 'YES': 31 | return True 32 | elif value_str == 'false' or value_str == 'FALSE' or value_str == 'NO': 33 | return False 34 | 35 | intValue = asInt(self) 36 | if intValue is not None: 37 | return intValue > 0 38 | else: 39 | return None 40 | else: 41 | return None 42 | 43 | 44 | def asStr(self: lldb.SBValue) -> Optional[str]: 45 | if self.IsValid(): 46 | summary: str = self.GetSummary() 47 | if summary is None: 48 | return None 49 | if summary.startswith('"') and summary.endswith('"') and len(summary) > 1: 50 | summary = summary[1:-1] 51 | elif summary.startswith('@"') and summary.endswith('"') and len(summary) > 2: 52 | summary = summary[2:-1] 53 | return summary 54 | else: 55 | return None 56 | 57 | 58 | lldb.SBValue.asInt = asInt 59 | lldb.SBValue.asFloat = asFloat 60 | lldb.SBValue.asBool = asBool 61 | lldb.SBValue.asStr = asStr 62 | -------------------------------------------------------------------------------- /src/lldbhelper/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['LLDBCommandBase'] 2 | 3 | from lldbhelper.lldb_command_base import LLDBCommandBase 4 | -------------------------------------------------------------------------------- /src/lldbhelper/lldb_command_base.py: -------------------------------------------------------------------------------- 1 | import lldb 2 | from abc import ABC, abstractmethod 3 | import argparse 4 | 5 | # ref: https://lldb.llvm.org/use/python-reference.html#create-a-new-lldb-command-using-a-python-function 6 | 7 | 8 | class LLDBCommandBase(ABC): 9 | """ 10 | Abstract base class for creating LLDB commands. 11 | 12 | This class provides a framework for creating LLDB commands. 13 | It defines abstract methods for command name, description, argument parser creation, and command execution. 14 | It also provides methods for registering the command with LLDB and retrieving help information. 15 | """ 16 | 17 | @classmethod 18 | @abstractmethod 19 | def cmdname(cls) -> str: 20 | """ 21 | Abstract method that returns the name of the command. 22 | 23 | Returns: 24 | str: The name of the command. 25 | """ 26 | pass 27 | 28 | @classmethod 29 | @abstractmethod 30 | def description(cls) -> str: 31 | """ 32 | Abstract method that returns the description of the command. 33 | 34 | Returns: 35 | str: The description of the command. 36 | """ 37 | pass 38 | 39 | @classmethod 40 | def register_lldb_command(cls, debugger: lldb.SBDebugger, module_name: str) -> None: 41 | """ 42 | Registers the command with LLDB. 43 | 44 | This method sets the command's description and adds it to the debugger. 45 | 46 | Args: 47 | debugger (lldb.SBDebugger): The LLDB debugger. 48 | module_name (str): The name of the module. 49 | 50 | Returns: 51 | None 52 | """ 53 | cls.__doc__ = cls.description() 54 | 55 | command = f"command script add -o -c {module_name}.{cls.__name__} {cls.cmdname()}" 56 | debugger.HandleCommand(command) 57 | 58 | @abstractmethod 59 | def create_argparser(self) -> argparse.ArgumentParser: 60 | """ 61 | Abstract method that creates and returns an argument parser for the command. 62 | 63 | Returns: 64 | argparse.ArgumentParser: The argument parser for the command. 65 | """ 66 | pass 67 | 68 | def __init__(self, debugger: lldb.SBDebugger, internal_dict: dict): 69 | """ 70 | Initializes the LLDBCommandBase instance. 71 | 72 | Args: 73 | debugger (lldb.SBDebugger): The LLDB debugger. 74 | internal_dict (dict): The internal dictionary. 75 | 76 | Returns: 77 | None 78 | """ 79 | self.argparser = self.create_argparser() 80 | 81 | @abstractmethod 82 | def __call__( 83 | self, 84 | debugger: lldb.SBDebugger, 85 | command: str, 86 | exe_ctx: lldb.SBExecutionContext, 87 | result: lldb.SBCommandReturnObject 88 | ) -> None: 89 | """ 90 | Abstract method that executes the command logic. 91 | 92 | Args: 93 | debugger (lldb.SBDebugger): The LLDB debugger. 94 | command (str): The command string. 95 | exe_ctx (lldb.SBExecutionContext): The execution context. 96 | result (lldb.SBCommandReturnObject): The command result. 97 | 98 | Returns: 99 | None 100 | """ 101 | pass 102 | 103 | def get_short_help(self) -> str: 104 | """ 105 | Returns the short help message for the command. 106 | Default implementation is that `description` is returned. 107 | 108 | Returns: 109 | str: The short help message. 110 | """ 111 | return self.description() 112 | 113 | def get_long_help(self) -> str: 114 | """ 115 | Returns the long help message for the command. 116 | Default implementation is that the argparser help message is returned. 117 | 118 | Returns: 119 | str: The long help message. 120 | """ 121 | return self.argparser.format_help() 122 | -------------------------------------------------------------------------------- /src/objc.py: -------------------------------------------------------------------------------- 1 | import lldb 2 | import shlex 3 | import argparse 4 | from typing import Union, Optional, cast 5 | from lldbhelper import LLDBCommandBase 6 | import util 7 | 8 | 9 | def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None: 10 | ObjcCommnad.register_lldb_command(debugger, ObjcCommnad.__module__) 11 | 12 | 13 | class ObjcCommnad(LLDBCommandBase): 14 | 15 | @classmethod 16 | def cmdname(cls) -> str: 17 | return 'objc' 18 | 19 | @classmethod 20 | def description(cls) -> str: 21 | return 'Objective-C runtime debugging. [iLLDB]' 22 | 23 | def create_argparser(self) -> argparse.ArgumentParser: 24 | parser = argparse.ArgumentParser( 25 | description="Objective-C runtime debugging", 26 | formatter_class=util.HelpFormatter 27 | ) 28 | subparsers = parser.add_subparsers(title="Subcommands", dest="subcommand") 29 | 30 | # inherits 31 | inherits_command = subparsers.add_parser("inherits", 32 | help="Show class hierarchy of object", 33 | formatter_class=util.HelpFormatter) 34 | inherits_command.add_argument("object", 35 | type=str, 36 | help="object") 37 | 38 | # methods 39 | method_command = subparsers.add_parser("methods", 40 | help="Show method list of object", 41 | formatter_class=util.HelpFormatter) 42 | method_command.add_argument("object", 43 | type=str, 44 | help="object") 45 | method_command.add_argument("--class", 46 | dest='class_name', 47 | type=str, 48 | help="Specify a target class in the inheritance hierarchy") 49 | method_command.add_argument("-c", "--class-only", action="store_true", help="Show only class methods") 50 | method_command.add_argument("-i", "--instance-only", action="store_true", help="Show only instance methods") 51 | 52 | # properties 53 | properties_command = subparsers.add_parser("properties", 54 | help="Show property list of object", 55 | formatter_class=util.HelpFormatter) 56 | properties_command.add_argument("object", 57 | type=str, 58 | help="object") 59 | properties_command.add_argument("--class", 60 | dest='class_name', 61 | type=str, 62 | help="Specify a target class in the inheritance hierarchy") 63 | 64 | # ivars 65 | ivars_command = subparsers.add_parser("ivars", 66 | help="Show ivar list of object", 67 | formatter_class=util.HelpFormatter) 68 | ivars_command.add_argument("object", 69 | type=str, 70 | help="object") 71 | ivars_command.add_argument("--class", 72 | dest='class_name', 73 | type=str, 74 | help="Specify a target class in the inheritance hierarchy") 75 | 76 | return parser 77 | 78 | def __call__( 79 | self, 80 | debugger: lldb.SBDebugger, 81 | command: str, 82 | exe_ctx: lldb.SBExecutionContext, 83 | result: lldb.SBCommandReturnObject 84 | ) -> None: 85 | args: Union[list[str], argparse.Namespace] = shlex.split(command, posix=False) 86 | args = self.argparser.parse_args(cast(list[str], args)) 87 | args = cast(argparse.Namespace, args) 88 | 89 | if args.subcommand == "methods": 90 | self.methods(args, debugger, result) 91 | elif args.subcommand == "inherits": 92 | self.inherits(args, debugger, result) 93 | elif args.subcommand == "properties": 94 | self.properties(args, debugger, result) 95 | elif args.subcommand == "ivars": 96 | self.ivars(args, debugger, result) 97 | else: 98 | self.argparser.print_help() 99 | 100 | def inherits( 101 | self, 102 | args: argparse.Namespace, 103 | debugger: lldb.SBDebugger, 104 | result: lldb.SBCommandReturnObject 105 | ) -> None: 106 | inherits = self.class_inherits(debugger, args.object) 107 | result.AppendMessage(' -> '.join(inherits)) 108 | 109 | def methods( 110 | self, 111 | args: argparse.Namespace, 112 | debugger: lldb.SBDebugger, 113 | result: lldb.SBCommandReturnObject 114 | ) -> None: 115 | class_info = self.class_info( 116 | debugger, 117 | args.object, 118 | args.class_name 119 | ) 120 | 121 | if class_info is None: 122 | if args.class_name is not None: 123 | result.AppendMessage("Invalid class name") 124 | else: 125 | result.AppendMessage('Invalid object') 126 | return 127 | 128 | show_all = (not args.class_only and not args.instance_only) 129 | text = "" 130 | if args.class_only or show_all: 131 | text += "Class Methods\n" 132 | text += '\n'.join(map(lambda m: f" {m.name}", class_info.class_methods)) 133 | text += '\n' 134 | 135 | if args.instance_only or show_all: 136 | text += "Instance Methods\n" 137 | text += '\n'.join(map(lambda m: f" {m.name}", class_info.instance_methods)) 138 | text += '\n' 139 | 140 | result.AppendMessage(text) 141 | 142 | def properties( 143 | self, 144 | args: argparse.Namespace, 145 | debugger: lldb.SBDebugger, 146 | result: lldb.SBCommandReturnObject 147 | ) -> None: 148 | class_info = self.class_info( 149 | debugger, 150 | args.object, 151 | args.class_name 152 | ) 153 | 154 | if class_info is None: 155 | if args.class_name is not None: 156 | result.AppendMessage("Invalid class name") 157 | else: 158 | result.AppendMessage('Invalid object') 159 | return 160 | 161 | text = "Properties\n" 162 | text += '\n'.join(map(lambda p: f" {p.name}", class_info.properties)) 163 | text += '\n' 164 | 165 | result.AppendMessage(text) 166 | 167 | def ivars( 168 | self, 169 | args: argparse.Namespace, 170 | debugger: lldb.SBDebugger, 171 | result: lldb.SBCommandReturnObject 172 | ) -> None: 173 | ivars = self.class_ivars( 174 | debugger, 175 | args.object, 176 | args.class_name 177 | ) 178 | 179 | text = "Ivars:\n" 180 | text += '\n'.join(map(lambda v: f" {v}", ivars)) 181 | result.AppendMessage(text) 182 | 183 | def class_info( 184 | self, 185 | debugger: lldb.SBDebugger, 186 | object: str, 187 | class_name: Optional[str] 188 | ) -> Optional['ClassInfo']: 189 | """ 190 | Retrieves information about a specific class in the Objective-C runtime. 191 | 192 | Args: 193 | debugger (lldb.SBDebugger): 194 | An instance of the LLDB debugger. 195 | object (str): 196 | The name of the object for which to retrieve class information. 197 | class_name (Optional[str]): 198 | The name of the class for which to retrieve information. 199 | If not provided, the last class in the object's class hierarchy will be used. 200 | 201 | Returns: 202 | Optional['ClassInfo']: 203 | The parsed class information, including class methods, instance methods, and properties. 204 | Returns None if the class name is invalid or the object is invalid. 205 | """ 206 | inherits = self.class_inherits(debugger, object) 207 | if class_name is not None and class_name not in inherits: 208 | return None 209 | if class_name is None: 210 | class_name = inherits[-1] 211 | 212 | script: str 213 | 214 | language: int = lldb.eLanguageTypeObjC 215 | if util.currentLanguage(debugger) == lldb.eLanguageTypeSwift: 216 | script = f""" 217 | {object}.perform(Selector(("__methodDescriptionForClass:")), with: NSClassFromString("{class_name}")) 218 | """ 219 | language = lldb.eLanguageTypeSwift 220 | else: 221 | script = f""" 222 | (NSString *)[{object} __methodDescriptionForClass: NSClassFromString(@"{class_name}")]; 223 | """ 224 | 225 | ret = util.exp_script( 226 | debugger, 227 | script, 228 | lang=language 229 | ) 230 | 231 | return ClassInfoParser.parse(ret.GetObjectDescription()) 232 | 233 | def class_inherits( 234 | self, 235 | debugger: lldb.SBDebugger, 236 | object: str, 237 | ) -> list[str]: 238 | """ 239 | Retrieve the class hierarchy of an object in Objective-C or Swift. 240 | 241 | Args: 242 | debugger (lldb.SBDebugger): An instance of `lldb.SBDebugger` used for debugging. 243 | object (str): The name of the object for which to retrieve the class hierarchy. 244 | 245 | Returns: 246 | list[str]: A list of class names representing the class hierarchy of the object. 247 | """ 248 | 249 | script: str 250 | 251 | language: int = lldb.eLanguageTypeObjC_plus_plus 252 | if util.currentLanguage(debugger) == lldb.eLanguageTypeSwift: 253 | script = f""" 254 | var currentClass: AnyClass? = object_getClass({object}) 255 | var result = String(describing: type(of: {object})) 256 | 257 | while true {{ 258 | if let current = currentClass, 259 | let superClass = class_getSuperclass(current) {{ 260 | result = "\\(String(describing: superClass)), " + result 261 | currentClass = superClass 262 | }} else {{ 263 | break 264 | }} 265 | }} 266 | 267 | result 268 | """ 269 | language = lldb.eLanguageTypeSwift 270 | else: 271 | script = f""" 272 | Class currentClass = (Class)object_getClass({object}); 273 | NSMutableString *result = [NSMutableString stringWithFormat:@"%s", (char *)class_getName(currentClass)]; 274 | 275 | while ((currentClass = (Class)class_getSuperclass(currentClass))) {{ 276 | [result insertString:[NSString stringWithFormat:@"%s, ", (char *)class_getName(currentClass)] atIndex:0]; 277 | }} 278 | result; 279 | """ 280 | 281 | ret = util.exp_script( 282 | debugger, 283 | script, 284 | lang=language 285 | ) 286 | 287 | if ret: 288 | result: str = ret.asStr() 289 | return result.split(', ') 290 | else: 291 | return [] 292 | 293 | def class_ivars( 294 | self, 295 | debugger: lldb.SBDebugger, 296 | object: str, 297 | class_name: Optional[str] 298 | ) -> list['IVar']: 299 | inherits = self.class_inherits(debugger, object) 300 | if class_name is not None and class_name not in inherits: 301 | return [] 302 | if class_name is None: 303 | class_name = inherits[-1] 304 | 305 | script = '' 306 | 307 | language: int = lldb.eLanguageTypeObjC 308 | if util.currentLanguage(debugger) == lldb.eLanguageTypeSwift: 309 | script = f""" 310 | {object}.perform(Selector(("__ivarDescriptionForClass:")), with: NSClassFromString("{class_name}")) 311 | """ 312 | language = lldb.eLanguageTypeSwift 313 | else: 314 | script = f""" 315 | (NSString *)[{object} __ivarDescriptionForClass: NSClassFromString(@"{class_name}")]; 316 | """ 317 | 318 | ret = util.exp_script( 319 | debugger, 320 | script, 321 | lang=language 322 | ) 323 | 324 | return IVarParser.parse(ret.GetObjectDescription()) 325 | 326 | 327 | from dataclasses import dataclass 328 | import re 329 | 330 | 331 | @dataclass 332 | class Method: 333 | name: str 334 | ptr: str 335 | 336 | def isClassMethod(self) -> bool: 337 | return self.name[0] == '+' 338 | 339 | def __str__(self) -> str: 340 | return f"{self.name} ({self.ptr})" 341 | 342 | 343 | @dataclass 344 | class Property: 345 | name: str 346 | dynamic: Optional[str] 347 | 348 | def __str__(self) -> str: 349 | if self.dynamic: 350 | return f"{self.name} ({self.dynamic})" 351 | return f"{self.name}" 352 | 353 | 354 | @dataclass 355 | class ClassInfo: 356 | """ 357 | A data class that represents information about a class in Objective-C. 358 | 359 | Attributes: 360 | class_methods (list[Method]): 361 | A list of Method objects representing the class methods of the class. 362 | instance_methods (list[Method]): 363 | A list of Method objects representing the instance methods of the class. 364 | properties (list[Property]): 365 | A list of Property objects representing the properties of the class. 366 | """ 367 | 368 | class_methods: list[Method] 369 | instance_methods: list[Method] 370 | properties: list[Property] 371 | 372 | def __str__(self) -> str: 373 | """ 374 | Returns a string representation of the class information. 375 | 376 | Returns: 377 | str: A string representation of the class information. 378 | """ 379 | class_methods_description = "\n".join(list(map(lambda m: f" {m}", self.class_methods))) 380 | instance_methods_description = "\n".join(list(map(lambda m: f" {m}", self.instance_methods))) 381 | properties_description = "\n".join(list(map(lambda p: f" {p}", self.properties))) 382 | 383 | description = f"Class Methods:\n{class_methods_description}" + "\n \n" 384 | description += f"Instance Methods:\n{instance_methods_description}" + "\n \n" 385 | description += f"Properties:\n{properties_description}" 386 | 387 | return description 388 | 389 | 390 | class ClassInfoParser: 391 | """ 392 | This class is responsible for parsing a string representation of class information and converting it into a `ClassInfo` object. 393 | 394 | Example Usage: 395 | ```python 396 | string = " + (void)classMethod1;\n - (void)instanceMethod1;\n @property (nonatomic, strong) NSString *property1;\n" 397 | class_info = ClassInfoParser.parse(string) 398 | print(class_info) 399 | ``` 400 | Expected Output: 401 | ``` 402 | Class Methods: 403 | + (void)classMethod1 404 | Instance Methods: 405 | - (void)instanceMethod1 406 | Properties: 407 | property1 408 | ``` 409 | 410 | Methods: 411 | - parse(string: str) -> ClassInfo: 412 | Parses the given string representation of class information and returns a `ClassInfo` object. 413 | - parse_methods(lines: List[str], is_static: bool = False) -> List[Method]: 414 | Parses the given list of lines and extracts the class methods or instance methods based on the value of the `is_static` parameter. 415 | - parse_properties(lines: List[str]) -> List[Property]: 416 | Parses the given list of lines and extracts the properties. 417 | """ 418 | 419 | @classmethod 420 | def parse(cls, string: str) -> ClassInfo: 421 | """ 422 | Parses the given string representation of class information and returns a `ClassInfo` object. 423 | 424 | Args: 425 | string (str): The string representation of class information. 426 | 427 | Returns: 428 | ClassInfo: The parsed `ClassInfo` object. 429 | """ 430 | lines = string.splitlines() 431 | class_methods = cls.parse_methods(lines, is_static=True) 432 | instance_methods = cls.parse_methods(lines, is_static=False) 433 | properties = cls.parse_properties(lines) 434 | 435 | return ClassInfo( 436 | class_methods=class_methods, 437 | instance_methods=instance_methods, 438 | properties=properties 439 | ) 440 | 441 | @classmethod 442 | def parse_methods( 443 | cls, 444 | lines: list[str], 445 | is_static: bool = False 446 | ) -> list[Method]: 447 | """ 448 | Parses the given list of lines and extracts the class methods or instance methods based on the value of the `is_static` parameter. 449 | 450 | Args: 451 | lines (list[str]): The list of lines to parse. 452 | is_static (bool, optional): Indicates whether to parse class methods (True) or instance methods (False). Defaults to False. 453 | 454 | Returns: 455 | list[Method]: The list of parsed methods. 456 | """ 457 | operator = '+' if is_static else '-' 458 | lines = list(filter(lambda l: l[0:5] == f' {operator} (', lines)) 459 | lines = list(map(lambda l: l[2:], lines)) 460 | methods = list[Method]() 461 | 462 | for line in lines: 463 | line = line.replace(' ', '') 464 | components = line.split('; ') 465 | name = components[0] + ';' 466 | ptr = components[1].replace('(', '').replace(')', '') 467 | 468 | methods.append(Method(name, ptr)) 469 | 470 | return methods 471 | 472 | @classmethod 473 | def parse_properties(cls, lines: list[str]) -> list[Property]: 474 | """ 475 | Parses the given list of lines and extracts the properties. 476 | 477 | Args: 478 | lines (list[str]): The list of lines to parse. 479 | 480 | Returns: 481 | list[Property]: The list of parsed properties. 482 | """ 483 | lines = list(filter(lambda l: '@property' in l, lines)) 484 | lines = list(map(lambda l: l[2:], lines)) 485 | properties = list[Property]() 486 | 487 | for line in lines: 488 | line = line.replace(' ', '') 489 | components = line.split('; ') 490 | name = components[0] + ';' 491 | name = name.replace(';;', ';') 492 | 493 | dynamic: Optional[str] = None 494 | if len(components) > 1: 495 | dynamic = components[1].replace('( ', '').replace(' )', '') 496 | 497 | properties.append(Property(name, dynamic)) 498 | 499 | return properties 500 | 501 | 502 | @dataclass 503 | class IVar: 504 | """ 505 | Represents an instance variable in Objective-C. 506 | 507 | Attributes: 508 | name (str): The name of the instance variable. 509 | type (str): The type of the instance variable. 510 | value (str): The value of the instance variable. 511 | """ 512 | 513 | name: str 514 | type: str 515 | value: str 516 | 517 | def __str__(self) -> str: 518 | """ 519 | Returns a string representation of the instance variable. 520 | 521 | Returns: 522 | str: The string representation of the instance variable. 523 | """ 524 | return f"{self.name} = ({self.type}) {self.value}" 525 | 526 | 527 | class IVarParser: 528 | """ 529 | A class for parsing a string representation of Objective-C instance variables and converting them into a list of IVar objects. 530 | """ 531 | 532 | @classmethod 533 | def parse(cls, string: str) -> list: 534 | """ 535 | Parses a string representation of Objective-C instance variables and returns a list of IVar objects. 536 | 537 | Args: 538 | string (str): The string representation of Objective-C instance variables. 539 | 540 | Returns: 541 | list: A list of IVar objects. 542 | 543 | Example: 544 | string = " _debugName (NSString*): @"UIWindow-0x10950ea70-0"" 545 | ivars = IVarParser.parse(string) 546 | for ivar in ivars: 547 | print(ivar.name) 548 | print(ivar.type) 549 | print(ivar.value) 550 | 551 | Output: 552 | _debugName 553 | NSString * 554 | @"UIWindow-0x10950ea70-0"" 555 | """ 556 | lines = string.splitlines() 557 | 558 | ivars = list[IVar]() 559 | 560 | for i, line in enumerate(lines): 561 | if line.startswith(' '): 562 | ivars[-1].value += f"\n{line}" 563 | elif line.startswith(' }'): 564 | ivars[-1].value += "\n }" 565 | elif line.startswith(' '): 566 | components = line.split(' ') 567 | name = components[0].replace(' ', '') 568 | 569 | type_match = re.search(r'\((.*?)\)', line) 570 | if type_match is not None: 571 | type = type_match.group(1) 572 | value = line[type_match.end() + 2:] 573 | ivars.append(IVar(name, type, value)) 574 | 575 | return ivars 576 | -------------------------------------------------------------------------------- /src/objc/cat.m: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | @import CoreFoundation; 3 | 4 | // #import 5 | // #import 6 | 7 | NSString *path = @""; 8 | NSString *mode = @""; 9 | 10 | NSFileManager *fileManager = [NSFileManager defaultManager]; 11 | 12 | BOOL isDirectory = NO; 13 | BOOL exists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory]; 14 | 15 | if (!exists) { 16 | printf("`%s` is not existed", (char *)[path UTF8String]); 17 | } else if (isDirectory) { 18 | printf("`%s` is a directory", (char *)[path UTF8String]); 19 | } else { 20 | if ([mode isEqualToString:@"plist"]) { 21 | CFURLRef fileURL = CFURLCreateWithFileSystemPath( 22 | kCFAllocatorDefault, (CFStringRef)path, kCFURLPOSIXPathStyle, false); 23 | CFReadStreamRef stream = 24 | CFReadStreamCreateWithFile(kCFAllocatorDefault, fileURL); 25 | CFReadStreamOpen(stream); 26 | 27 | CFPropertyListRef plist = CFPropertyListCreateWithStream( 28 | kCFAllocatorDefault, stream, 0, kCFPropertyListImmutable, NULL, NULL); 29 | 30 | if (CFGetTypeID(plist) == CFDictionaryGetTypeID()) { 31 | NSDictionary *plistDictionary = (__bridge NSDictionary *)plist; 32 | printf("%s\n", (char *)[[plistDictionary description] UTF8String]); 33 | } else { 34 | printf("cannot loaded"); 35 | } 36 | CFRelease(plist); 37 | CFReadStreamClose(stream); 38 | CFRelease(stream); 39 | CFRelease(fileURL); 40 | 41 | } else { 42 | NSError *error = nil; 43 | NSString *fileContents = 44 | [NSString stringWithContentsOfFile:path 45 | encoding:NSUTF8StringEncoding 46 | error:&error]; 47 | if (fileContents) { 48 | printf("%s\n", (char *)[fileContents UTF8String]); 49 | } else { 50 | printf("%s\n", (char *)[[error localizedDescription] UTF8String]); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/objc/cookie_delete.m: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | // #import 4 | // NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 5 | 6 | NSString *domain = @""; 7 | NSString *name = @""; 8 | NSString *path = @""; 9 | 10 | NSArray *allCookies = [storage cookies]; 11 | int matchedCount = 0; 12 | 13 | for (NSHTTPCookie *cookie in allCookies) { 14 | BOOL domainMatched = 15 | (domain == nil) || [cookie.domain isEqualToString:domain]; 16 | BOOL nameMatched = (name == nil) || [cookie.name isEqualToString:name]; 17 | BOOL pathMatched = (path == nil) || [cookie.path isEqualToString:path]; 18 | 19 | if (domainMatched && nameMatched && pathMatched) { 20 | [storage deleteCookie:cookie]; 21 | matchedCount++; 22 | } 23 | } 24 | 25 | if (matchedCount == 0) { 26 | printf("%d Cookies was deleted\n", matchedCount); 27 | } 28 | -------------------------------------------------------------------------------- /src/objc/cookie_read.m: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | // #import 4 | // NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 5 | 6 | NSString *domain = @""; 7 | NSString *name = @""; 8 | NSString *path = @""; 9 | 10 | NSArray *allCookies = [storage cookies]; 11 | int matchedCount = 0; 12 | 13 | printf("Cookie Information:\n"); 14 | 15 | for (NSHTTPCookie *cookie in allCookies) { 16 | BOOL domainMatched = 17 | (domain == nil) || [cookie.domain isEqualToString:domain]; 18 | BOOL nameMatched = (name == nil) || [cookie.name isEqualToString:name]; 19 | BOOL pathMatched = (path == nil) || [cookie.path isEqualToString:path]; 20 | 21 | if (domainMatched && nameMatched && pathMatched) { 22 | printf("-------------------\n"); 23 | 24 | printf(" Name: %s\n", (char *)[cookie.name UTF8String]); 25 | printf(" Value: %s\n", (char *)[cookie.value UTF8String]); 26 | printf(" Domain: %s\n", (char *)[cookie.domain UTF8String]); 27 | printf(" Path: %s\n", (char *)[cookie.path UTF8String]); 28 | printf(" Secure: %s\n", cookie.secure ? "True" : "False"); 29 | 30 | NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; 31 | [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; 32 | NSString *formattedDate = [dateFormatter stringFromDate:cookie.expiresDate]; 33 | printf(" Expires: %s\n", (char *)[formattedDate UTF8String]); 34 | 35 | printf("-------------------\n"); 36 | matchedCount++; 37 | } 38 | } 39 | 40 | if (matchedCount == 0) { 41 | printf(" None\n"); 42 | } 43 | -------------------------------------------------------------------------------- /src/swift/app.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func printAppInfo() { 4 | guard let appInfo = Bundle.main.infoDictionary else { 5 | return 6 | } 7 | print("[App Info]") 8 | if let appName = appInfo["CFBundleName"] as? String { 9 | print("App Name: \(appName)") 10 | } 11 | if let appVersion = appInfo["CFBundleShortVersionString"] as? String { 12 | print("App Version: \(appVersion)") 13 | } 14 | if let appBuild = appInfo["CFBundleVersion"] as? String { 15 | print("App Build Number: \(appBuild)") 16 | } 17 | if let bundleIdentifier = appInfo["CFBundleIdentifier"] as? String { 18 | print("Bundle Identifier: \(bundleIdentifier)") 19 | } 20 | if let executableName = appInfo["CFBundleExecutable"] as? String { 21 | print("Executable Name: \(executableName)") 22 | } 23 | if let bundleDisplayName = appInfo["CFBundleDisplayName"] as? String { 24 | print("Display Name: \(bundleDisplayName)") 25 | } 26 | if let bundleIconFile = appInfo["CFBundleIconFile"] as? String { 27 | print("Icon File: \(bundleIconFile)") 28 | } 29 | if let bundleIconFiles = appInfo["CFBundleIconFiles"] as? [String] { 30 | print("Icon Files: \(bundleIconFiles)") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/swift/deviceIOS.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | var screenSize: CGSize { 4 | guard let window = UIApplication.shared.connectedScenes.first as? UIWindowScene else { 5 | return .zero 6 | } 7 | return window.screen.bounds.size 8 | } 9 | 10 | func printDeviceInfo() { 11 | let currentDevice = UIDevice.current 12 | 13 | print("[Device Info]") 14 | print("Name: \(currentDevice.name)") 15 | print("Model: \(currentDevice.model)") 16 | print("IsSimulator: \(TARGET_OS_SIMULATOR != 0)") 17 | print("System Name: \(currentDevice.systemName)") 18 | print("System Version: \(currentDevice.systemVersion)") 19 | print("Screen: \(screenSize.width) x \(screenSize.height)") 20 | print("Locale: \(Locale.current)") 21 | if let identifierForVendor = currentDevice.identifierForVendor { 22 | print("Id For Vendor: \(identifierForVendor.uuidString)") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/swift/deviceMacOS.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SystemConfiguration 3 | 4 | func printDeviceInfo() { 5 | let processInfo = ProcessInfo.processInfo 6 | 7 | print("[Device Info]") 8 | if let name = SCDynamicStoreCopyComputerName(nil, nil) { 9 | print("Name: \(name)") 10 | } 11 | 12 | if let model = sysctlByString(key: "hw.model") { 13 | print("Model: \(model)") 14 | } 15 | 16 | print("System Name: macOS") 17 | print("System Version: \(processInfo.operatingSystemVersionString)") 18 | 19 | if let cpu = sysctlByString(key: "machdep.cpu.brand_string") { 20 | print("CPU: \(cpu)") 21 | } 22 | 23 | print("Locale: \(Locale.current)") 24 | } 25 | 26 | func sysctlByString(key: String) -> String? { 27 | var size: size_t = 0 28 | sysctlbyname(key, nil, &size, nil, 0) 29 | var value = [CChar](repeating: 0, count: Int(size)) 30 | sysctlbyname(key, &value, &size, nil, 0) 31 | 32 | return String(cString: value) 33 | } 34 | -------------------------------------------------------------------------------- /src/swift/file.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func listFilesInDirectory(_ directoryURL: URL, indentation: String = "", depth: Int? = nil) { 4 | if indentation.isEmpty { 5 | let isDirectory = (try? directoryURL.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true 6 | print("\(isDirectory ? "📁 " : "📄 ")" + directoryURL.lastPathComponent) 7 | } 8 | 9 | let currentDepth = indentation.replacingOccurrences(of: "│", with: " ").count / 3 10 | if let depth, currentDepth >= depth { 11 | return 12 | } 13 | 14 | do { 15 | let fileManager = FileManager.default 16 | let contents = try fileManager.contentsOfDirectory(at: directoryURL, includingPropertiesForKeys: nil, options: []) 17 | 18 | for (index, url) in contents.enumerated() { 19 | let isDirectory = (try? url.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true 20 | let isLast = index == contents.count - 1 21 | 22 | var fileHierarchy = "\(indentation)\(isLast ? "└─ " : "├─ ")" 23 | 24 | if isDirectory { 25 | fileHierarchy += "📁 \(url.lastPathComponent)" 26 | print(fileHierarchy) 27 | 28 | listFilesInDirectory(url, indentation: indentation + (isLast ? " " : "│ "), depth: depth) 29 | } else { 30 | fileHierarchy += "📄 \(url.lastPathComponent)" 31 | print(fileHierarchy) 32 | } 33 | } 34 | } catch { 35 | print(error.localizedDescription) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/swift/tree.swift: -------------------------------------------------------------------------------- 1 | // import UIKit 2 | // typealias NSUIView = UIView 3 | // typealias NSUIViewController = UIViewController 4 | // typealias NSUIWindow = UIWindow 5 | // typealias NSUIApplication = UIApplication 6 | 7 | func windowHierarchy( 8 | _ window: NSUIWindow?, 9 | indentation: String = "", 10 | isLast: Bool = true, 11 | mode: String = "normal", 12 | depth: Int? = nil, 13 | address: Bool = false 14 | ) { 15 | guard let window = window else { return } 16 | 17 | let currentDepth = indentation.replacingOccurrences(of: "│", with: " ").count / 3 18 | if let depth, currentDepth > depth { 19 | return 20 | } 21 | 22 | var result = "" 23 | if isLast { 24 | result += "\(indentation)└─" 25 | } else { 26 | result += "\(indentation)├─" 27 | } 28 | 29 | var windowDescription: String 30 | switch mode { 31 | case "simple": windowDescription = "\(String(describing: type(of: window)))" 32 | case "detail": windowDescription = "\(window)" 33 | default: 34 | let frameDescription = frameDescription(window.frame) 35 | windowDescription = "\(String(describing: type(of: window))) \(frameDescription)" 36 | } 37 | if address && mode != "detail" { windowDescription += ": \(addressDescription(of: window))" } 38 | 39 | result += windowDescription 40 | print(result) 41 | 42 | var rootViewController: NSUIViewController? 43 | if window.responds(to: Selector(("rootViewController"))) { // for iOS 44 | rootViewController = window.perform(Selector(("rootViewController"))).takeUnretainedValue() as? NSUIViewController 45 | } else if window.responds(to: Selector(("contentViewController"))) { // for macOS 46 | rootViewController = window.perform(Selector(("contentViewController"))).takeUnretainedValue() as? NSUIViewController 47 | } 48 | 49 | viewControllerHierarchy( 50 | rootViewController, 51 | indentation: indentation + (isLast ? " " : "│ "), 52 | isLast: true, 53 | mode: mode, 54 | depth: depth, 55 | address: address 56 | ) 57 | } 58 | 59 | func viewControllerHierarchy( 60 | _ viewController: NSUIViewController?, 61 | indentation: String = "", 62 | isLast: Bool = true, 63 | mode: String = "normal", 64 | depth: Int? = nil, 65 | address: Bool = false 66 | ) { 67 | guard let viewController = viewController else { return } 68 | 69 | let currentDepth = indentation.replacingOccurrences(of: "│", with: " ").count / 3 70 | if let depth, currentDepth > depth { 71 | return 72 | } 73 | 74 | var result = "" 75 | if isLast { 76 | result += "\(indentation)└─" 77 | } else { 78 | result += "\(indentation)├─" 79 | } 80 | 81 | var viewControllerDescription: String 82 | switch mode { 83 | case "simple": viewControllerDescription = "\(String(describing: type(of: viewController)))" 84 | case "detail": viewControllerDescription = "\(viewController)" 85 | default: 86 | let frameDescription = frameDescription(viewController.view.frame) 87 | viewControllerDescription = "\(String(describing: type(of: viewController))) \(frameDescription))" 88 | } 89 | if address && mode != "detail" { viewControllerDescription += ": \(addressDescription(of: viewController))" } 90 | 91 | result += viewControllerDescription 92 | 93 | print(result) 94 | 95 | let children = viewController.children 96 | for (index, childViewController) in children.enumerated() { 97 | let isLastChild = index == children.count - 1 && viewController.view.subviews.isEmpty 98 | viewControllerHierarchy( 99 | childViewController, 100 | indentation: indentation + (isLast ? " " : "│ "), 101 | isLast: isLastChild, 102 | mode: mode, 103 | depth: depth, 104 | address: address 105 | ) 106 | } 107 | 108 | for (index, subview) in viewController.view.subviews.enumerated() { 109 | let isLastSubview = index == viewController.view.subviews.count - 1 110 | viewHierarchy( 111 | subview, 112 | indentation: indentation + (isLast ? " " : "│ "), 113 | isLast: isLastSubview, 114 | mode: mode, 115 | depth: depth, 116 | address: address 117 | ) 118 | } 119 | } 120 | 121 | func viewHierarchy( 122 | _ view: NSUIView?, 123 | indentation: String = "", 124 | isLast: Bool = true, 125 | mode: String = "normal", 126 | depth: Int? = nil, 127 | address: Bool = false 128 | ) { 129 | guard let view = view else { return } 130 | 131 | let currentDepth = indentation.replacingOccurrences(of: "│", with: " ").count / 3 132 | if let depth, currentDepth > depth { 133 | return 134 | } 135 | 136 | var result = "" 137 | if isLast { 138 | result += "\(indentation)└─" 139 | } else { 140 | result += "\(indentation)├─" 141 | } 142 | 143 | var viewDescription: String 144 | switch mode { 145 | case "simple": 146 | viewDescription = "\(String(describing: type(of: view)))" 147 | case "detail": viewDescription = "\(view)" 148 | default: 149 | let frameDescription = frameDescription(view.frame) 150 | viewDescription = "\(String(describing: type(of: view))) \(frameDescription)" 151 | } 152 | if address && mode != "detail" { viewDescription += ": \(addressDescription(of: view))" } 153 | 154 | result += viewDescription 155 | 156 | print(result) 157 | 158 | for (index, subview) in view.subviews.enumerated() { 159 | let isLastSubview = index == view.subviews.count - 1 160 | viewHierarchy( 161 | subview, 162 | indentation: indentation + (isLast ? " " : "│ "), 163 | isLast: isLastSubview, 164 | mode: mode, 165 | depth: depth, 166 | address: address 167 | ) 168 | } 169 | } 170 | 171 | func layerHierarchy( 172 | _ layer: CALayer?, 173 | indentation: String = "", 174 | isLast: Bool = true, 175 | mode: String = "normal", 176 | depth: Int? = nil, 177 | address: Bool = false 178 | ) { 179 | guard let layer = layer else { return } 180 | 181 | let currentDepth = indentation.replacingOccurrences(of: "│", with: " ").count / 3 182 | if let depth, currentDepth > depth { 183 | return 184 | } 185 | 186 | var result = "" 187 | if isLast { 188 | result += "\(indentation)└─" 189 | } else { 190 | result += "\(indentation)├─" 191 | } 192 | 193 | var layerDescription: String 194 | switch mode { 195 | case "simple": layerDescription = "\(String(describing: type(of: layer)))" 196 | case "detail": layerDescription = "\(layer)" 197 | default: 198 | let frameDescription = frameDescription(layer.frame) 199 | layerDescription = "\(String(describing: type(of: layer))) \(frameDescription)" 200 | } 201 | if address && mode != "detail" { layerDescription += ": \(addressDescription(of: layer))" } 202 | 203 | result += layerDescription 204 | 205 | print(result) 206 | 207 | guard let sublayers = layer.sublayers else { return } 208 | for (index, sublayer) in sublayers.enumerated() { 209 | let isLastSublayer = index == sublayers.count - 1 210 | layerHierarchy( 211 | sublayer, 212 | indentation: indentation + (isLast ? " " : "│ "), 213 | isLast: isLastSublayer, 214 | mode: mode, 215 | depth: depth, 216 | address: address 217 | ) 218 | } 219 | } 220 | 221 | func frameDescription(_ frame: CGRect) -> String { 222 | String( 223 | format: "(%.1f, %.1f, %.1f, %.1f)", 224 | arguments: [ 225 | frame.minX, 226 | frame.minY, 227 | frame.width, 228 | frame.height 229 | ] 230 | ) 231 | } 232 | 233 | func addressDescription(of object: AnyObject) -> String { 234 | let unmanaged = Unmanaged.passUnretained(object) 235 | return unmanaged.toOpaque().debugDescription 236 | } 237 | -------------------------------------------------------------------------------- /src/ud.py: -------------------------------------------------------------------------------- 1 | import lldb 2 | import shlex 3 | import argparse 4 | from typing import Union, cast 5 | from lldbhelper import LLDBCommandBase 6 | import util 7 | 8 | 9 | def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None: 10 | UserDefaultsCommnad.register_lldb_command(debugger, UserDefaultsCommnad.__module__) 11 | 12 | 13 | class UserDefaultsCommnad(LLDBCommandBase): 14 | 15 | @classmethod 16 | def cmdname(cls) -> str: 17 | return 'ud' 18 | 19 | @classmethod 20 | def description(cls) -> str: 21 | return 'UserDefault debugging. [iLLDB]' 22 | 23 | def create_argparser(self) -> argparse.ArgumentParser: 24 | description = "UserDefault debugging" 25 | parser = argparse.ArgumentParser(description=description, 26 | formatter_class=util.HelpFormatter) 27 | subparsers = parser.add_subparsers(title="Subcommands", dest="subcommand") 28 | 29 | # read 30 | read_command = subparsers.add_parser("read", 31 | help="Read UserDefault value", 32 | formatter_class=util.HelpFormatter) 33 | read_command.add_argument("--suite", type=str, help="Suite for UserDefault") 34 | read_command.add_argument("key", 35 | type=str, 36 | help="key") 37 | 38 | # write 39 | write_command = subparsers.add_parser("write", 40 | help="Write UserDefault value", 41 | formatter_class=util.HelpFormatter) 42 | write_command.add_argument("--suite", type=str, help="Suite for UserDefault") 43 | write_command.add_argument("key", 44 | type=str, 45 | help="Key") 46 | write_command.add_argument("value", 47 | type=str, 48 | help="Value to set") 49 | write_command.add_argument("--type", 50 | type=str, 51 | default='str', 52 | help="Type of value to set['str', 'number']") 53 | 54 | # delete 55 | delete_command = subparsers.add_parser("delete", 56 | help="Delete UserDefault value", 57 | formatter_class=util.HelpFormatter) 58 | delete_command.add_argument("--suite", type=str, help="Suite for UserDefault") 59 | delete_command.add_argument("key", 60 | type=str, 61 | help="Key") 62 | 63 | read_all = subparsers.add_parser("read-all", 64 | help="Read all UserDefault value", 65 | formatter_class=util.HelpFormatter) 66 | read_all.add_argument("--suite", type=str, help="Suite for UserDefault") 67 | 68 | delete_all = subparsers.add_parser("delete-all", 69 | help="Delete all UserDefault value", 70 | formatter_class=util.HelpFormatter) 71 | delete_all.add_argument("--suite", type=str, help="Suite for UserDefault") 72 | 73 | return parser 74 | 75 | def __call__( 76 | self, 77 | debugger: lldb.SBDebugger, 78 | command: str, 79 | exe_ctx: lldb.SBExecutionContext, 80 | result: lldb.SBCommandReturnObject 81 | ) -> None: 82 | args: Union[list[str], argparse.Namespace] = shlex.split(command, posix=False) 83 | args = self.argparser.parse_args(cast(list[str], args)) 84 | args = cast(argparse.Namespace, args) 85 | 86 | if args.subcommand is None: 87 | self.argparser.print_help() 88 | exit(0) 89 | 90 | script = "" 91 | if args.suite is not None: 92 | script += """ 93 | @import Foundation; 94 | NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName]; 95 | """ 96 | else: 97 | script += """ 98 | @import Foundation; 99 | NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 100 | """ 101 | 102 | if args.subcommand == "read": 103 | key = args.key 104 | script += f"""({{ 105 | id val = [userDefaults stringForKey:@\"{key}\"]; 106 | if (val != nil) {{ 107 | printf("\\"%s\\"", (char*)[val UTF8String]); 108 | }} else {{ 109 | val = [userDefaults objectForKey:@\"{key}\"]; 110 | }} 111 | val; 112 | }}) 113 | """ 114 | elif args.subcommand == "write": 115 | key = args.key 116 | type = args.type 117 | value = args.value 118 | if type == "number": 119 | script += f"[userDefaults setObject:@({value}) forKey:@\"{key}\"];" 120 | else: 121 | script += f"[userDefaults setObject:@\"{value}\" forKey:@\"{key}\"];" 122 | elif args.subcommand == "delete": 123 | key = args.key 124 | script += f"[userDefaults removeObjectForKey:@\"{key}\"];" 125 | elif args.subcommand == "read-all": 126 | script += "[userDefaults dictionaryRepresentation];" 127 | elif args.subcommand == "delete-all": 128 | script += r""" 129 | NSDictionary *allUserDefaults = [userDefaults dictionaryRepresentation]; 130 | 131 | for (NSString *key in [allUserDefaults allKeys]) { 132 | [userDefaults removeObjectForKey:key]; 133 | } 134 | """ 135 | else: 136 | self.argparser.print_help() 137 | exit(0) 138 | 139 | ret = util.exp_script( 140 | debugger, 141 | script, 142 | lang=lldb.eLanguageTypeObjC_plus_plus 143 | ) 144 | if ret: 145 | result.AppendMessage(ret.GetObjectDescription()) 146 | -------------------------------------------------------------------------------- /src/ui.py: -------------------------------------------------------------------------------- 1 | import lldb 2 | import shlex 3 | import argparse 4 | from typing import Union, cast 5 | from lldbhelper import LLDBCommandBase 6 | import util 7 | 8 | 9 | def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None: 10 | UICommnad.register_lldb_command(debugger, UICommnad.__module__) 11 | 12 | 13 | class UICommnad(LLDBCommandBase): 14 | 15 | @classmethod 16 | def cmdname(cls) -> str: 17 | return 'ui' 18 | 19 | @classmethod 20 | def description(cls) -> str: 21 | return 'UI debugging. [iLLDB]' 22 | 23 | def create_argparser(self) -> argparse.ArgumentParser: 24 | parser = argparse.ArgumentParser(description="UI debugging", 25 | formatter_class=util.HelpFormatter) 26 | subparsers = parser.add_subparsers(title="Subcommands", dest="subcommand") 27 | 28 | tree_command = subparsers.add_parser("tree", 29 | help="Show view hierarchie", 30 | formatter_class=util.HelpFormatter) 31 | tree_command.add_argument("-d", "--detail", action="store_true", help="Enable detailed mode") 32 | tree_command.add_argument("-s", "--simple", action="store_true", help="Enable simpled mode") 33 | tree_command.add_argument("--depth", type=int, help="Maximum depth to be displayed") 34 | tree_command.add_argument("--with-address", action="store_true", help="Print address of ui") 35 | 36 | tree_command.add_argument("--window", type=str, help="Specify the target window") 37 | tree_command.add_argument("--view", type=str, help="Specify the target view (property or address)") 38 | tree_command.add_argument("--vc", type=str, help="Specify the target viewController (property or address)") 39 | tree_command.add_argument("--layer", type=str, help="Specify the target CALayer (property or address)") 40 | 41 | return parser 42 | 43 | def __call__( 44 | self, 45 | debugger: lldb.SBDebugger, 46 | command: str, 47 | exe_ctx: lldb.SBExecutionContext, 48 | result: lldb.SBCommandReturnObject 49 | ) -> None: 50 | args: Union[list[str], argparse.Namespace] = shlex.split(command, posix=False) 51 | args = self.argparser.parse_args(cast(list[str], args)) 52 | args = cast(argparse.Namespace, args) 53 | 54 | if args.subcommand == "tree": 55 | self.tree(args, debugger, result) 56 | else: 57 | self.argparser.print_help() 58 | 59 | def tree(self, args: argparse.Namespace, debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> None: 60 | mode = 'normal' 61 | if args.detail: 62 | mode = 'detail' 63 | if args.simple: 64 | mode = 'simple' 65 | 66 | depth = 'nil' 67 | if args.depth is not None: 68 | try: 69 | depth = str(int(args.depth)) 70 | except ValueError: 71 | pass 72 | 73 | with_address = 'true' if args.with_address else 'false' 74 | 75 | script = '' 76 | 77 | if util.isUIKit(debugger): 78 | script += """ 79 | import UIKit 80 | typealias NSUIView = UIView 81 | typealias NSUIViewController = UIViewController 82 | typealias NSUIWindow = UIWindow 83 | typealias NSUIApplication = UIApplication 84 | """ 85 | elif util.isAppKit(debugger): 86 | script += """ 87 | import AppKit 88 | typealias NSUIView = NSView 89 | typealias NSUIViewController = NSViewController 90 | typealias NSUIWindow = NSWindow 91 | typealias NSUIApplication = NSApplication 92 | """ 93 | 94 | self.resolve_adress(args) 95 | 96 | script += util.read_script_file('swift/tree.swift') 97 | if args.window is not None: 98 | script += f"\n windowHierarchy({args.window}, mode: \"{mode}\", depth: {depth}, address: {with_address})" 99 | elif args.view is not None: 100 | script += f"\n viewHierarchy({args.view}, mode: \"{mode}\", depth: {depth}, address: {with_address})" 101 | elif args.vc is not None: 102 | script += f"\n viewControllerHierarchy({args.vc}, mode: \"{mode}\", depth: {depth}, address: {with_address})" 103 | elif args.layer is not None: 104 | script += f"\n layerHierarchy({args.layer}, mode: \"{mode}\", depth: {depth}, address: {with_address})" 105 | else: 106 | script += f"\n windowHierarchy(NSUIApplication.shared.keyWindow, mode: \"{mode}\", depth: {depth}, address: {with_address})" 107 | 108 | _ = util.exp_script( 109 | debugger, 110 | script 111 | ) 112 | 113 | def resolve_adress(self, args: argparse.Namespace) -> None: 114 | try: 115 | if args.window is not None and int(args.window, 16): 116 | args.window = f"Unmanaged.fromOpaque(.init(bitPattern: {args.window})!).takeUnretainedValue()" 117 | elif args.view is not None and int(args.view, 16): 118 | args.view = f"Unmanaged.fromOpaque(.init(bitPattern: {args.view})!).takeUnretainedValue()" 119 | elif args.vc is not None and int(args.vc, 16): 120 | args.vc = f"Unmanaged.fromOpaque(.init(bitPattern: {args.vc})!).takeUnretainedValue()" 121 | elif args.layer is not None and int(args.layer, 16): 122 | args.layer = f"Unmanaged.fromOpaque(.init(bitPattern: {args.layer})!).takeUnretainedValue()" 123 | except ValueError: 124 | pass 125 | -------------------------------------------------------------------------------- /src/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | import lldb 3 | import argparse 4 | from typing import Optional 5 | from lldbhelper import SBValue # noqa: F401 6 | 7 | 8 | def exp_script( 9 | debugger: lldb.SBDebugger, 10 | script: str, 11 | lang: int = lldb.eLanguageTypeSwift) -> Optional[lldb.SBValue]: 12 | 13 | frame: lldb.SBFrame = ( 14 | debugger.GetSelectedTarget() 15 | .GetProcess() 16 | .GetSelectedThread() 17 | .GetSelectedFrame() 18 | ) 19 | 20 | if not frame: 21 | return None 22 | 23 | options = lldb.SBExpressionOptions() 24 | options.SetLanguage(lang) 25 | options.SetIgnoreBreakpoints(True) 26 | options.SetTrapExceptions(False) 27 | options.SetTryAllThreads(True) 28 | options.SetFetchDynamicValue(lldb.eNoDynamicValues) 29 | options.SetUnwindOnError(True) 30 | options.SetGenerateDebugInfo(True) 31 | 32 | value: lldb.SBValue = frame.EvaluateExpression(script, options) 33 | error: lldb.SBError = value.GetError() 34 | 35 | if error.Success() or error.value == 0x1001: # success or unknown error 36 | return value 37 | else: 38 | print(error) 39 | return None 40 | 41 | 42 | def read_script_file(file_name: str) -> str: 43 | file_path = os.path.realpath(__file__) 44 | dir_name = os.path.dirname(file_path) 45 | 46 | file = open(f'{dir_name}/{file_name}', mode='r') 47 | text = file.read() 48 | file.close() 49 | 50 | return text 51 | 52 | 53 | def isIOSSimulator(debugger: lldb.SBDebugger) -> bool: 54 | script = """ 55 | @import Foundation; 56 | NSString *name = [[[NSProcessInfo processInfo] environment] objectForKey:@"SIMULATOR_DEVICE_NAME"]; 57 | name; 58 | """ 59 | ret = exp_script(debugger, script, lang=lldb.eLanguageTypeObjC) 60 | if ret: 61 | result: Optional[str] = ret.asStr() 62 | return result is not None 63 | else: 64 | return False 65 | 66 | 67 | def isAppKit(debugger: lldb.SBDebugger) -> bool: 68 | script = """ 69 | @import Foundation; 70 | Class app = NSClassFromString(@"NSApplication"); 71 | BOOL val = (BOOL)(app != nil) 72 | val 73 | """ 74 | ret = exp_script(debugger, script, lang=lldb.eLanguageTypeObjC) 75 | 76 | if ret: 77 | result: Optional[bool] = ret.asBool() 78 | return False if result is None else result 79 | else: 80 | return False 81 | 82 | 83 | def isUIKit(debugger: lldb.SBDebugger) -> bool: 84 | script = """ 85 | @import Foundation; 86 | Class app = NSClassFromString(@"UIApplication"); 87 | BOOL val = (BOOL)(app != nil) 88 | val; 89 | """ 90 | ret = exp_script(debugger, script, lang=lldb.eLanguageTypeObjC) 91 | if ret: 92 | result: Optional[bool] = ret.asBool() 93 | return False if result is None else result 94 | else: 95 | return False 96 | 97 | 98 | def isMacOS(debugger: lldb.SBDebugger) -> bool: 99 | model = sysctlbyname(debugger, "hw.model") 100 | if model: 101 | return 'Mac' in model and not isIOSSimulator(debugger) 102 | else: 103 | return isAppKit(debugger) 104 | 105 | 106 | def isIOS(debugger: lldb.SBDebugger) -> bool: 107 | machine = sysctlbyname(debugger, "hw.machine") 108 | if machine: 109 | return 'iP' in machine or isIOSSimulator(debugger) 110 | else: 111 | return isUIKit(debugger) 112 | 113 | 114 | def sysctlbyname(debugger: lldb.SBDebugger, key: str) -> Optional[str]: 115 | script = f""" 116 | @import Foundation; 117 | int sysctlbyname(const char *, void *, size_t *, void *, size_t); 118 | 119 | size_t size = 0; 120 | sysctlbyname("{key}", NULL, &size, NULL, 0); 121 | char *machine = (char *)malloc(size); 122 | sysctlbyname("{key}", machine, &size, NULL, 0); 123 | 124 | NSString *result = [NSString stringWithUTF8String:machine]; 125 | free(machine); 126 | result; 127 | """ 128 | 129 | ret = exp_script(debugger, script, lang=lldb.eLanguageTypeObjC) 130 | if ret: 131 | result: Optional[str] = ret.asStr() 132 | return result 133 | else: 134 | return None 135 | 136 | 137 | def currentLanguage(debugger: lldb.SBDebugger) -> int: 138 | return ( # type: ignore[no-any-return] 139 | debugger.GetSelectedTarget() 140 | .GetProcess() 141 | .GetSelectedThread() 142 | .GetSelectedFrame() 143 | .GetCompileUnit() 144 | .GetLanguage() 145 | ) 146 | 147 | 148 | class HelpFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter): 149 | pass 150 | --------------------------------------------------------------------------------