This commit is contained in:
Stephen B 2025-10-18 17:04:01 -04:00
parent e31594e693
commit 31cd4ecf53
24 changed files with 294 additions and 1388 deletions

View file

@ -1,11 +0,0 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -1,87 +0,0 @@
{
"images" : [
{
"filename" : "StikJIT.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "StikJIT_Dark.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "DarkUI.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "LightUI.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

View file

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "StikJIT.png",
"filename" : "StikDebug New-iOS-Default-1024x1024@1x.png",
"idiom" : "universal",
"scale" : "1x"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 552 KiB

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "System.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

158
StikJIT/Geode.js Normal file
View file

@ -0,0 +1,158 @@
function littleEndianHexStringToNumber(hexStr) {
const bytes = [];
for (let i = 0; i < hexStr.length; i += 2) {
bytes.push(parseInt(hexStr.substr(i, 2), 16));
}
let num = 0n;
for (let i = 4; i >= 0; i--) {
num = (num << 8n) | BigInt(bytes[i]);
}
return num;
}
function numberToLittleEndianHexString(num) {
const bytes = [];
for (let i = 0; i < 5; i++) {
bytes.push(Number(num & 0xFFn));
num >>= 8n;
}
while (bytes.length < 8) {
bytes.push(0);
}
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
}
function littleEndianHexToU32(hexStr) {
return parseInt(hexStr.match(/../g).reverse().join(''), 16);
}
function extractBrkImmediate(u32) {
return (u32 >> 5) & 0xFFFF;
}
let pid = get_pid();
log(`pid = ${pid}`);
let attachResponse = send_command(`vAttach;${pid.toString(16)}`);
log(`attach_response = ${attachResponse}`);
let validBreakpoints = 0;
let totalBreakpoints = 0;
let invalidBreakpoints = 0;
while (invalidBreakpoints < 10) {
totalBreakpoints++;
log(`Handling breakpoint ${totalBreakpoints} (valid: ${validBreakpoints})`);
let brkResponse = send_command(`c`);
log(`brkResponse = ${brkResponse}`);
let tidMatch = /T[0-9a-f]+thread:(?<tid>[0-9a-f]+);/.exec(brkResponse);
let tid = tidMatch ? tidMatch.groups['tid'] : null;
let pcMatch = /20:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let pc = pcMatch ? pcMatch.groups['reg'] : null;
let x0Match = /00:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let x0 = x0Match ? x0Match.groups['reg'] : null;
let x1Match = /01:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let x1 = x1Match ? x1Match.groups['reg'] : null;
if (!tid || !pc || !x0) {
log(`Failed to extract registers: tid=${tid}, pc=${pc}, x0=${x0}, x1=${x1}`);
invalidBreakpoints++;
continue;
}
const pcNum = littleEndianHexStringToNumber(pc);
const x0Num = littleEndianHexStringToNumber(x0);
const x1Num = x1 ? littleEndianHexStringToNumber(x1) : 0n;
log(`tid = ${tid}, pc = ${pcNum.toString(16)}, x0 = ${x0Num.toString(16)}, x1 = ${x1Num.toString(16)}`);
let instructionResponse = send_command(`m${pcNum.toString(16)},4`);
log(`instruction at pc: ${instructionResponse}`);
let instrU32 = littleEndianHexToU32(instructionResponse);
let brkImmediate = extractBrkImmediate(instrU32);
log(`BRK immediate: 0x${brkImmediate.toString(16)} (${brkImmediate})`);
if (brkImmediate !== 0x69 && brkImmediate !== 0x70 && brkImmediate !== 0x71) {
log(`Skipping: BRK immediate not 0x69 or 0x70 or 0x71 (was 0x${brkImmediate.toString(16)})`);
invalidBreakpoints++;
continue;
}
invalidBreakpoints = 0;
if (brkImmediate === 0x69) { // usual jit mapping
log(`Received command to process JIT mapping (0x69)`);
let jitPageAddress = x0Num;
let size = x1Num > 0n ? x1Num : 0x10000n; // allocate 64 KB if x1 somehow is 0
log(`Got RX page address: 0x${jitPageAddress.toString(16)}, preparing region with 0x${size.toString(16)} bytes!`);
let prepareJITPageResponse = prepare_memory_region(Number(jitPageAddress), Number(size)); // Unsure if this is specific to iOS26 but this func doesnt take in a BigInt, resulting in an error unless converted to a number. I'm not sure how other scripts don't have this problem.
log(`prepareJITPageResponse = ${prepareJITPageResponse}`);
let pcPlus4 = numberToLittleEndianHexString(pcNum + 4n);
let pcPlus4Response = send_command(`P20=${pcPlus4};thread:${tid};`);
log(`pcPlus4Response = ${pcPlus4Response}`);
validBreakpoints++;
} else if (brkImmediate === 0x70) { // patching instructs
log(`Received command to patch instructions (0x70)`);
let x2Match = /02:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let x2 = x2Match ? x2Match.groups['reg'] : null;
if (!x1 || !x2) {
log(`Missing x1 or x2 for function patching`);
continue;
}
let destAddr = x0Num;
let srcAddr = x1Num;
let size = x2 ? littleEndianHexStringToNumber(x2) : 10n;
log(`Patching: dest=0x${destAddr.toString(16)}, src=0x${srcAddr.toString(16)}, size=0x${size.toString(16)}`);
// Unsure exactly why, but anything over 4 MB freezes the app for some reason, so we will set a soft limit
if (size > 0x400000n) {
log(`Size too large (0x${size.toString(16)}), skipping`);
let pcPlus4 = numberToLittleEndianHexString(pcNum + 4n);
let pcPlus4Response = send_command(`P20=${pcPlus4};thread:${tid};`);
log(`pcPlus4Response = ${pcPlus4Response}`);
validBreakpoints++;
continue;
}
try {
// m (read) = `m${curPointer.toString(16)},<size>`
// M (write) = `M${curPointer.toString(16)},<size>:<your hex instructions goes here>`
const CHUNK_SIZE = 0x4000n; // 16 KB
for (let i = 0n; i < size; i += CHUNK_SIZE) {
let chunkSize = i + CHUNK_SIZE <= size ? CHUNK_SIZE : size - i;
let readAddr = srcAddr + i;
let writeAddr = destAddr + i;
let readRes = send_command(`m${readAddr.toString(16)},${chunkSize.toString(16)}`);
if (readRes && readRes.length > 0) {
let writeResponse = send_command(`M${writeAddr.toString(16)},${chunkSize.toString(16)}:${readRes}`);
if (writeResponse !== "OK") {
log(`Write failed at offset ${i.toString(16)}`);
break;
}
}
if (Number(i / CHUNK_SIZE) % 10 === 0) {
log(`Progress: 0x${i.toString(16)}/0x${size.toString(16)}`);
}
}
log(`Memory write completed!`);
} catch (e) {
log(`Memory write failed: ${e}`);
}
let pcPlus4 = numberToLittleEndianHexString(pcNum + 4n);
let pcPlus4Response = send_command(`P20=${pcPlus4};thread:${tid};`);
log(`pcPlus4Response = ${pcPlus4Response}`);
validBreakpoints++;
} else if (brkImmediate === 0x71) { // detach, might be unnecessary
break;
}
log(`Completed breakpoint ${validBreakpoints}`);
}
log(`Stopping script (Received 0x71 or too many invalid breakpoints)`);
let detachResponse = send_command(`D`);
log(`detachResponse = ${detachResponse}`);

View file

@ -0,0 +1,3 @@
<svg width="496" height="785" viewBox="0 0 496 785" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.1989 397.271C77.3786 402.079 151.833 415.575 181.63 445.587C184.132 448.108 187.858 449.467 191.54 448.804C198.512 447.548 204.146 442.93 206.19 436.797L206.615 435.521C207.544 432.733 207.043 429.717 205.25 427.295L190.71 407.655L378.316 417.041C398.811 418.127 421.817 423.733 440.945 428.631L166.859 770.574C143.273 800.092 105.607 780.765 119.335 744.572L224.238 459.922H25.3456C10.5608 459.922 0.00012064 449.731 0 436.024C0 428.295 3.16799 421.266 9.15236 413.533L22.1989 397.271ZM329.14 14.6772C352.726 -15.1932 390.392 4.13405 376.662 40.3297L271.761 324.978H470.652C485.438 324.978 496 335.521 496 348.875C496 356.957 492.831 363.985 486.848 371.366L479.259 380.832C478.595 379.5 477.756 378.296 476.707 377.295C476.043 376.661 475.077 376.439 474.12 376.565C438.773 381.223 365.499 386.813 343.574 373.238L348.703 366.312C349.875 364.729 350.357 362.812 350.058 360.928L349.982 360.448C349.232 355.723 345.956 351.63 341.216 349.495C339.383 348.669 337.294 348.501 335.321 349C296.141 358.904 200.318 373.092 105.593 356.031C93.4479 354.919 76.4146 353.371 58.6918 351.783L329.14 14.6772Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,3 @@
<svg width="1025" height="1024" viewBox="0 0 1025 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M260.5 252H508.5V0H516.5V252H764.5V0H772.5V252H1024.5V260H772.5V508H1024.5V516H772.5V764H1024.5V772H772.5V1024H764.5V772H516.5V1024H508.5V772H260.5V1024H252.5V772H0.5V764H252.5V516H0.5V508H252.5V260H0.5V252H252.5V0H260.5V252ZM260.5 764H508.5V516H260.5V764ZM516.5 764H764.5V516H516.5V764ZM260.5 508H508.5V260H260.5V508ZM516.5 508H764.5V260H516.5V508Z" fill="#D9D9D9"/>
</svg>

After

Width:  |  Height:  |  Size: 488 B

View file

@ -0,0 +1,52 @@
{
"fill" : {
"linear-gradient" : [
"display-p3:0.38110,0.78809,0.97949,1.00000",
"display-p3:0.20813,0.42505,0.94629,1.00000"
]
},
"groups" : [
{
"blur-material" : 0.5,
"layers" : [
{
"image-name" : "1Bolt.svg",
"name" : "1Bolt"
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.7
}
},
{
"layers" : [
{
"fill" : {
"solid" : "extended-gray:1.00000,0.20000"
},
"image-name" : "2Grid.svg",
"name" : "2Grid"
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

View file

@ -78,6 +78,12 @@ struct HomeView: View {
private var ddiMounted: Bool { isMounted() }
private var canConnectByApp: Bool { pairingFileExists && ddiMounted }
private var pairingFileLikelyInvalid: Bool {
(pairingFileExists || pairingFilePresentOnDisk) &&
!isValidatingPairingFile &&
!ddiMounted &&
!heartbeatOK
}
private let pairingFileURL = URL.documentsDirectory.appendingPathComponent("pairingFile.plist")
@ -248,6 +254,12 @@ struct HomeView: View {
autoScriptData = data
autoScriptName = "script2.js"
}
} else if appName == "Geode" {
if let url = Bundle.main.url(forResource: "Geode", withExtension: "js"),
let data = try? Data(contentsOf: url) {
autoScriptData = data
autoScriptName = "script2.js"
}
} else if appName == "MeloNX" {
if let url = Bundle.main.url(forResource: "melo", withExtension: "js"),
let data = try? Data(contentsOf: url) {
@ -397,7 +409,7 @@ struct HomeView: View {
}
.disabled(isProcessing || isValidatingPairingFile)
if pairingFileExists && enableAdvancedOptions {
if pairingFileExists && enableAdvancedOptions && !pairingFileLikelyInvalid {
Button(action: { showPIDSheet = true }) {
secondaryButtonLabel(icon: "number.circle", title: "Connect by PID")
}
@ -428,7 +440,7 @@ struct HomeView: View {
return .init(
title: "Pairing file needs attention",
subtitle: "We found a pairing file but couldn’t read it.",
icon: "doc.badge.exclamationmark",
icon: "exclamationmark.triangle.fill",
tint: .yellow
)
}
@ -439,6 +451,14 @@ struct HomeView: View {
tint: .orange
)
}
if pairingFileLikelyInvalid {
return .init(
title: "Pairing file may be invalid",
subtitle: "We couldn’t reach your device. Create a new pairing file on your trusted computer, then try again.",
icon: "exclamationmark.triangle.fill",
tint: .yellow
)
}
if !ddiMounted {
return .init(
title: "Mount the Developer Disk Image",
@ -473,7 +493,8 @@ struct HomeView: View {
private var primaryActionTitle: String {
if isValidatingPairingFile { return "Validating…" }
if !pairingFileExists { return pairingFilePresentOnDisk ? "Re-import Pairing File" : "Import Pairing File" }
if !pairingFileExists { return pairingFilePresentOnDisk ? "Import New Pairing File" : "Import Pairing File" }
if pairingFileLikelyInvalid { return "Import New Pairing File" }
if !ddiMounted { return "Mount Developer Disk Image" }
return "Connect by App"
}
@ -481,6 +502,7 @@ struct HomeView: View {
private var primaryActionIcon: String {
if isValidatingPairingFile { return "hourglass" }
if !pairingFileExists { return pairingFilePresentOnDisk ? "arrow.clockwise" : "doc.badge.plus" }
if pairingFileLikelyInvalid { return "arrow.clockwise" }
if !ddiMounted { return "externaldrive" }
return "cable.connector.horizontal"
}
@ -497,6 +519,14 @@ struct HomeView: View {
actionTitle: nil,
action: nil
)
} else if pairingFileLikelyInvalid {
pairingItem = ChecklistItem(
title: "Pairing file",
subtitle: "We couldn’t reach your device. Create a new pairing file on your trusted computer.",
status: .attention,
actionTitle: "Import New",
action: { isShowingPairingFilePicker = true }
)
} else if pairingFileExists {
pairingItem = ChecklistItem(
title: "Pairing file",
@ -508,9 +538,9 @@ struct HomeView: View {
} else if pairingFilePresentOnDisk {
pairingItem = ChecklistItem(
title: "Pairing file",
subtitle: "We couldn’t read the pairing file that’s on disk. Re-import it from your trusted computer.",
subtitle: "We couldn’t read the pairing file that’s on disk. Create a new pairing file on your trusted computer.",
status: .attention,
actionTitle: "Re-import",
actionTitle: "Import New",
action: { isShowingPairingFilePicker = true }
)
} else {
@ -523,14 +553,23 @@ struct HomeView: View {
)
}
let hideOtherIndicators = pairingFileLikelyInvalid || (pairingFilePresentOnDisk && !pairingFileExists && !isValidatingPairingFile)
if hideOtherIndicators {
return [pairingItem]
}
return [
pairingItem,
ChecklistItem(
title: "Developer Disk Image",
subtitle: ddiMounted ? "Mounted successfully." : "",
subtitle: ddiMounted
? "Mounted successfully."
: (pairingFileLikelyInvalid
? "The Developer Disk Image can’t mount because the pairing file looks invalid. Create a new pairing file."
: ""),
status: ddiMounted ? .ready : .attention,
actionTitle: nil,
action: nil
actionTitle: pairingFileLikelyInvalid ? "Import New" : nil,
action: pairingFileLikelyInvalid ? { isShowingPairingFilePicker = true } : nil
),
ChecklistItem(
title: "VPN tunnel",
@ -910,6 +949,10 @@ struct HomeView: View {
private func primaryActionTapped() {
guard !isValidatingPairingFile else { return }
if pairingFileLikelyInvalid {
isShowingPairingFilePicker = true
return
}
if pairingFileExists {
if !ddiMounted {
showAlert(title: "Device Not Mounted".localized, message: "The Developer Disk Image has not been mounted yet. Check in settings for more information.".localized, showOk: true) { _ in }

View file

@ -41,10 +41,6 @@ struct MainTabView: View {
.tabItem { Label("Scripts", systemImage: "scroll") }
.tag(1)
IPAAppManagerView()
.tabItem { Label("Testing", systemImage: "square.grid.2x2.fill") }
.tag(2)
DeviceInfoView()
.tabItem { Label("Device Info", systemImage: "info.circle.fill") }
.tag(3)

View file

@ -209,7 +209,7 @@ struct SettingsView: View {
glassCard {
VStack(spacing: 16) {
VStack {
Image("StikJIT")
Image("StikDebug")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 80, height: 80)

View file

@ -10,6 +10,26 @@ import UniformTypeIdentifiers
import UIKit
// MARK: - Shared glass card wrapper (renamed to avoid conflicts)
struct MaterialCard<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
content
.padding(20)
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.strokeBorder(Color.white.opacity(0.15), lineWidth: 1)
)
)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4)
}
}
@ViewBuilder
func appGlassCard<Content: View>(@ViewBuilder _ content: () -> Content) -> some View {

File diff suppressed because it is too large Load diff