From 6e63dac9628b74ff0f3de23153f3199a48391540 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 23 Mar 2026 10:35:50 +0400 Subject: [PATCH 1/2] chore: update commands --- .lldbinit | 4 +++- bootstrap | 27 +++++++++++++++++++++++++++ lldb/cmds.txt | 36 ++++++++++++++++++++++++++++++------ 3 files changed, 60 insertions(+), 7 deletions(-) create mode 100755 bootstrap diff --git a/.lldbinit b/.lldbinit index b451180..78b4cdd 100644 --- a/.lldbinit +++ b/.lldbinit @@ -1 +1,3 @@ -command script import ~/lldb/lldbinit.py \ No newline at end of file +command script import ~/lldb/lldbinit.py + +settings set target.skip-prologue false \ No newline at end of file diff --git a/bootstrap b/bootstrap new file mode 100755 index 0000000..3b262c6 --- /dev/null +++ b/bootstrap @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +cd "$(dirname "${BASH_SOURCE[0]}")" || exit + +git pull origin main + +update() { + rsync --exclude ".git/" \ + --exclude ".DS_Store" \ + --exclude "bootstrap.sh" \ + --exclude "README.md" \ + --exclude "LICENSE" \ + -avh --no-perms . ~ + source ~/.bash_profile +} + +if [[ "$1" == "--force" || "$1" == "-f" ]]; then + update +else + read -rp "This may overwrite existing files in your home directory. Are you sure? (y/n) " response + echo + if [[ "$response" =~ ^[Yy]$ ]]; then + update + fi +fi + +unset -f update diff --git a/lldb/cmds.txt b/lldb/cmds.txt index b093ab6..4a9bdc4 100644 --- a/lldb/cmds.txt +++ b/lldb/cmds.txt @@ -4,20 +4,44 @@ command alias -H "Reload ~/.lldbinit" -h "Reload ~/.lldbinit" -- reload_lldbinit # Performs a regex search on the input and performs a specific image lookup on the matched result. command regex -h "Regex search" -s "rlook UIViewController.viewDidLoad" -- rlook 's/(.+)/image lookup -rn %1/' -# Executes an experssion with Objective-C language and optimization flag. -command alias cpo expression -l objc -O -- +# Executes an expression with Objective-C language and optimization flag. +command alias cpo expression -l objc -O -- -# Executes an experssion with Objective-C language. -command alias cp expression -l objc -- +# Executes an expression with Objective-C language. +command alias cp expression -l objc -- # Executes an expression with Swift and optimization flag. -command alias spo experssion -l swift -O -- +command alias spo expression -l swift -O -- # Executes an expression with Swift. -command alias sp experssion -l swift -- +command alias sp expression -l swift -- # Executes an expression with a specified language (given by %1) parameter and optimization flag. command alias lpo expression -l %1 -O -- +# Executes an expression with a specified language (given by %1) parameter. +command alias lp expression -l %1 -- + +# Executes an expression with a specified language (given by %1) parameter without optimization. +command alias lpn expression -l %1 -- + +# Prints value in decimal format. +command alias cpd expression -f d -l objc -- + +# Prints value in hexadecimal format (ObjC). +command alias cpx expression -f x -l objc -- + +# Prints value in hexadecimal format (Swift). +command alias spx expression -f x -l swift -- + +# Prints value in binary format. +command alias cpb expression -f b -l objc -- + +# Prints value in octal format. +command alias cpoo expression -f o -l objc -- + # This command defines a regex search for 'doc' that opens the LLDB documentation page for the matched class. command regex doc 's/(.+)/script import os; os.system("open https:" + chr(47) + chr(47) + "lldb.llvm.org" + chr(47) + "python_reference" + chr(47) + "lldb.%1-class.html")/' + +# Prints values in ObjC context in hexadecimal. +command alias -H "Print values in ObjC context in hexadecimal" -h "Print in hex" -- cpx expression -f x -l objc -- \ No newline at end of file From 1e8ce909ca0ff1150d82520eef7040a24f3655de Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 4 May 2026 15:24:49 +0400 Subject: [PATCH 2/2] feat: add `udump` command --- .pre-commit-config.yaml | 7 + lldb/commands/userdefaults.py | 264 ++++++++++++++++++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 lldb/commands/userdefaults.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ace2305 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.0 + hooks: + - id: ruff-format + - id: ruff + args: [ --fix ] \ No newline at end of file diff --git a/lldb/commands/userdefaults.py b/lldb/commands/userdefaults.py new file mode 100644 index 0000000..c503ab0 --- /dev/null +++ b/lldb/commands/userdefaults.py @@ -0,0 +1,264 @@ +# +# dotfiles +# Copyright © 2026 Space Code. All rights reserved. +# + +import lldbbase as bc + + +def commands(): + """Return a list of custom LLDB commands registered in the plugin""" + return [UDumpCommand()] + + +class UDumpCommand(bc.BaseCommand): + "UDumpCommand" + + def name(self): + """The command name used in LLDB (e.g. (lldb) udump)""" + return "udump" + + def description(self): + return "Dumps NSUserDefaults contents as a formatted key-value table" + + def options(self): + return [ + bc.CommandArgument( + short="-s", + long="--suite", + arg="suite", + type="string", + default="", + help="Suite name for initWithSuiteName: (default: standardUserDefaults)", + ), + bc.CommandArgument( + short="-f", + long="--filter", + arg="filter", + type="string", + default="", + help="Show only keys containing this substring (case-intensive)", + ), + bc.CommandArgument( + short="-o", + long="--alphabetical", + arg="alphabetical", + boolean=True, + default=False, + help="Sort keys alphabetically", + ), + ] + + def run(self, args, options): + defaults = get_defaults_instance(options.suite) + + if defaults is None or defaults == "0x0": + label = options.suite or "standardUserDefaults" + print(f"error: could not get NSUserDefaults instance ({label})") + return + + entries = collect_entries(defaults, options.filter) + + if options.alphabetical: + entries.sort(key=lambda x: x[0].lower()) + + label = options.suite or "standardUserDefaults" + print_table(entries, label, options.filter) + + +def get_defaults_instance(suite): + """ + Returns the NSUserDefaults instance address for the given suite name. + Uses standardUserDefaults when suite is empty. + :param suite: Suite name string, or empty string for standard defaults + :return: Object address string (e.g. '0x600003a12340'), or None on failure + """ + + if suite: + return bc.evaluate_expression( + f'(id)[[NSUserDefaults alloc] initWithSuiteName:@"{suite}"]' + ) + return bc.evaluate_expression("(id)[NSUserDefaults standardUserDefaults]") + + +def get_dictionary_representation(defaults): + """ + Calls dictionaryRepresentation on an NSUserDefaults instance. + Returns an NSDictionary containing all currently registered key-value pairs. + :param defaults: NSUserDefaults address string + :return: NSDictionary address string + """ + + return bc.evaluate_expression( + f"(id)[(NSUserDefaults *){defaults} dictionaryRepresentation]" + ) + + +def collect_entries(defaults, filter_str): + """ + Iterates over all keys in NSUserDefaults and returns a list of (key, value) tuples. + Applies case-insensitive substring filtering when filter_str is non-empty. + :param defaults: NSUserDefaults address string + :param filter_str: Substring to filter keys by, or empty string to include all + :return: List of (key_string, value_string) tuples + """ + + dictionary = get_dictionary_representation(defaults) + + keys = bc.evaluate_expression(f"(id)[(NSDictionary *){dictionary} allKeys]") + count = bc.evaluate_integer_expression(f"[(NSArray *){keys} count]") + + entries = [] + + for i in range(count): + key_obj = bc.evaluate_expression(f"(id)[(NSArray *){keys} objectAtIndex:{i}]") + key_str = nsstring_to_str(key_obj) + + if filter_str and filter_str.lower() not in key_str.lower(): + continue + + val_obj = bc.evaluate_expression( + f"(id)[(NSDictionary *){dictionary} objectForKey:(id){key_obj}]" + ) + + val_str = format_value(val_obj) + + entries.append((key_str, val_str)) + + return entries + + +def nsstring_to_str(obj): + """ + Converts an NSString object address to a Python string via UTF8String. + :param obj: NSString address string + :return: Python string, or '' / '' on failure + """ + + if obj is None or obj == "0x0": + return "" + + value = bc.evaluate_expression_value( + f"(const char *)[(NSString *){obj} UTF8String]" + ) + + summary = value.GetSummary() + + if summary: + return summary.strip('"') + return "" + + +def get_class_name(obj): + """ + Returns the class name of any ObjC object — including tagged pointers. + + NSStringFromClass([(id)ptr class]) fails in two situations: + 1. Tagged pointers — no real heap object, ObjC dispatch crashes. + 2. Certain toll-free bridged types — LLDB loses type info on (id) cast. + + object_getClassName((id)ptr) is a plain C function from + that handles both cases via the ObjC runtime directly, without message send. + We cast the return to (const char *) because LLDB doesn't import the header + and treats the return type as unknown otherwise. + + :param obj: Object address string (e.g. '0x600003a12340') + :return: Class name string, or '' on failure + """ + value = bc.evaluate_expression_value( + f"(const char *)object_getClassName((id){obj})" + ) + summary = value.GetSummary() + if summary: + return summary.strip('"') + return "" + + +def format_value(obj): + """ + Formats an arbitrary NSObject for display. + Appends the short class name in parentheses to disambiguate types + (e.g. '1 (Bool)' vs '1 (Number)'). + :param obj: Object address string + :return: Formatted string representation + """ + if obj is None or obj == "0x0": + return "nil" + + class_name = get_class_name(obj) + description = nsstring_to_str( + bc.evaluate_expression(f"(id)[(NSObject *){obj} description]") + ) + + short = shorten_class_name(class_name) + return f"{description} ({short})" + + +def shorten_class_name(class_name): + """ + Strips common Objective-C cluster prefixes for readability. + E.g. '__NSCFBoolean' -> 'Bool', '__NSCFString' -> 'String'. + :param class_name: Full ObjC class name string + :return: Shortened display name + """ + + replacements = { + "__NSCFBoolean": "Bool", + "__NSCFString": "String", + "__NSCFNumber": "Number", + "__NSCFArray": "Array", + "__NSCFDictionary": "Dictionary", + "__NSTaggedDate": "Date", + "NSConcreteMutableData": "Data", + } + + return replacements.get(class_name, class_name) + + +def is_tagged_pointer(obj): + """ + Returns True if obj is an Objective-C tagged pointer. + On arm64, tagged pointers have the most significant bit (bit 63) set. + They store the value inline in the pointer — no heap object exists, + so ObjC message sends via LLDB expression evaluation fail with + 'no known method' errors. + :param obj: Object address string (e.g. '0xbcc1f272952fc4b8') + :return: bool + """ + try: + return bool((int(obj, 16) >> 63) & 1) + except (TypeError, ValueError): + return False + + +def print_table(entries, label, filter_str): + """ + Prints the key-value pairs as a formatted ASCII table. + :param entries: List of (key, value) tuples + :param label: Display name for the NSUserDefaults instance + :param filter_str: Active filter string (shown in footer when non-empty) + """ + if not entries: + hint = f" (filter: '{filter_str}')" if filter_str else "" + print(f"\n {label}: no keys found{hint}\n") + return + + max_key_len = min(max(len(k) for k, _ in entries), 50) + sep = "─" * (max_key_len + 2) + "┼" + "─" * 46 + + print(f"\n NSUserDefaults: {label}") + print(f" {sep}") + print(f" {'KEY':<{max_key_len}} │ VALUE") + print(f" {sep}") + + for key, value in entries: + display_key = key[:max_key_len] + display_val = value[:80] + ("…" if len(value) > 80 else "") + print(f" {display_key:<{max_key_len}} │ {display_val}") + + print(f" {sep}") + footer = f" {len(entries)} keys" + if filter_str: + footer += f" (filter: '{filter_str}')" + print(footer) + print()