Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .lldbinit
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
command script import ~/lldb/lldbinit.py
command script import ~/lldb/lldbinit.py

settings set target.skip-prologue false
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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 ]
27 changes: 27 additions & 0 deletions bootstrap
Original file line number Diff line number Diff line change
@@ -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
36 changes: 30 additions & 6 deletions lldb/cmds.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 --
264 changes: 264 additions & 0 deletions lldb/commands/userdefaults.py
Original file line number Diff line number Diff line change
@@ -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 '<nil>' / '<unreadable>' on failure
"""

if obj is None or obj == "0x0":
return "<nil>"

value = bc.evaluate_expression_value(
f"(const char *)[(NSString *){obj} UTF8String]"
)

summary = value.GetSummary()

if summary:
return summary.strip('"')
return "<unreadable>"


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 <objc/runtime.h>
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 '<unknown>' on failure
"""
value = bc.evaluate_expression_value(
f"(const char *)object_getClassName((id){obj})"
)
summary = value.GetSummary()
if summary:
return summary.strip('"')
return "<unknown>"


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()