From 7c6bdadf3dc1b52fc35548b00648377425ac9b54 Mon Sep 17 00:00:00 2001 From: Stossy11 <69031796+stossy11@users.noreply.github.com> Date: Sun, 13 Jul 2025 22:59:14 +1000 Subject: [PATCH 1/3] Add URL Scheme for custom app scripts --- .../xcshareddata/swiftpm/Package.resolved | 11 +++- StikJIT/JSSupport/RunJSView.swift | 10 ++- StikJIT/Views/HomeView.swift | 66 ++++++++++++++----- 3 files changed, 67 insertions(+), 20 deletions(-) diff --git a/StikDebug.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/StikDebug.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7f82837d..165bebd1 100644 --- a/StikDebug.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/StikDebug.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "3675a8b363ad866505239c8e2c7f6a0862ed02618f2dca478b2519372fb79c0c", + "originHash" : "4df9bfc6bef31f016f92a5a4a2b67290e6fd98a0453ec6e1981c9a8fb5222170", "pins" : [ { "identity" : "codeeditorview", @@ -18,6 +18,15 @@ "revision" : "5ff7f3363f7a08f77e0d761e38e6add31c2136e1", "version" : "1.8.1" } + }, + { + "identity" : "swiftui-pipify", + "kind" : "remoteSourceControl", + "location" : "https://github.com/hugeBlack/swiftui-pipify", + "state" : { + "branch" : "main", + "revision" : "a1ec2fd1781c8289bff1a8b3f664dcf21c910efb" + } } ], "version" : 3 diff --git a/StikJIT/JSSupport/RunJSView.swift b/StikJIT/JSSupport/RunJSView.swift index 4b1d11fa..5c97a926 100644 --- a/StikJIT/JSSupport/RunJSView.swift +++ b/StikJIT/JSSupport/RunJSView.swift @@ -23,9 +23,13 @@ class RunJSViewModel: ObservableObject { self.semaphore = semaphore } - func runScript(path: URL) throws { - let scriptContent = try String(contentsOf: path, encoding: .utf8) - scriptName = path.lastPathComponent + func runScript(path: URL, scriptName: String? = nil) throws { + try runScript(data: Data(contentsOf: path), name: scriptName) + } + + func runScript(data: Data, name: String? = nil) throws { + let scriptContent = String(data: data, encoding: .utf8) + scriptName = name ?? "Script" let getPidFunction: @convention(block) () -> Int = { return self.pid diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index 918f0238..8454b14d 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -357,23 +357,29 @@ struct HomeView: View { ) .onOpenURL { url in print(url.path) - if url.host != "enable-jit" { - return - } - - let components = URLComponents(url: url, resolvingAgainstBaseURL: false) - if let bundleId = components?.queryItems?.first(where: { $0.name == "bundle-id" })?.value { - if viewDidAppeared { - startJITInBackground(with: bundleId) - } else { - pendingBundleIdToEnableJIT = bundleId + switch url.host { + case "enable-jit": + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) + if let bundleId = components?.queryItems?.first(where: { $0.name == "bundle-id" })?.value { + if viewDidAppeared { + startJITInBackground(with: bundleId) + } else { + pendingBundleIdToEnableJIT = bundleId + } + } else if let pidStr = components?.queryItems?.first(where: { $0.name == "pid" })?.value, let pid = Int(pidStr) { + if viewDidAppeared { + startJITInBackground(with: pid) + } else { + pendingPIDToEnableJIT = pid + } } - } else if let pidStr = components?.queryItems?.first(where: { $0.name == "pid" })?.value, let pid = Int(pidStr) { - if viewDidAppeared { - startJITInBackground(with: pid) - } else { - pendingPIDToEnableJIT = pid + case "script": + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) + if let script = components?.queryItems?.first(where: { $0.name == "script" })?.value?.data(using: .utf8), let bundleId = components?.queryItems?.first(where: { $0.name == "bundle-id" })?.value { + startJITInBackground(with: bundleId, script: Data(base64Encoded: script)) } + default: + break } } @@ -433,14 +439,34 @@ struct HomeView: View { } } - private func startJITInBackground(with bundleID: String) { + private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback? { + return { pid, debugProxyHandle, semaphore in + jsModel = RunJSViewModel(pid: Int(pid), debugProxy: debugProxyHandle, semaphore: semaphore) + scriptViewShow = true + DispatchQueue.global(qos: .background).async { + do { + try jsModel?.runScript(data: script, name: name) + isProcessing = false + } catch { + showAlert(title: "Error Occurred While Executing the Default Script.".localized, message: error.localizedDescription, showOk: true) + } + } + } + } + + private func startJITInBackground(with bundleID: String, script: Data? = nil) { isProcessing = true + let orgValue = enablePiP + if script != nil { + enablePiP = true + } // Add log message LogManager.shared.addInfoLog("Starting Debug for \(bundleID)") DispatchQueue.global(qos: .background).async { var callback: DebugAppCallback? = nil + if enableAdvancedOptions { let mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String] if let script = mapping?[bundleID] { @@ -449,6 +475,11 @@ struct HomeView: View { callback = getJsCallback() } } + + if let script { + callback = getJsCallback(script, name: bundleID + ".js") + } + let success = JITEnableContext.shared.debugApp(withBundleID: bundleID, logger: { message in if let message = message { @@ -460,6 +491,9 @@ struct HomeView: View { DispatchQueue.main.async { LogManager.shared.addInfoLog("Debug process completed for \(bundleID)") isProcessing = false + if script != nil { + enablePiP = orgValue + } } } } From b1dd38f8a19673089757d4814a4e833fd47e4452 Mon Sep 17 00:00:00 2001 From: Stossy11 <69031796+stossy11@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:24:17 +1000 Subject: [PATCH 2/3] add PID and fix script base64 --- StikJIT/Views/HomeView.swift | 57 ++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index 8454b14d..569aba00 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -375,8 +375,19 @@ struct HomeView: View { } case "script": let components = URLComponents(url: url, resolvingAgainstBaseURL: false) - if let script = components?.queryItems?.first(where: { $0.name == "script" })?.value?.data(using: .utf8), let bundleId = components?.queryItems?.first(where: { $0.name == "bundle-id" })?.value { - startJITInBackground(with: bundleId, script: Data(base64Encoded: script)) + if let scriptBase64URL = components?.queryItems?.first(where: { $0.name == "data" })?.value?.removingPercentEncoding, + let bundleId = components?.queryItems?.first(where: { $0.name == "bundle-id" })?.value { + + let base64 = base64URLToBase64(scriptBase64URL) + if let scriptData = Data(base64Encoded: base64) { + startJITInBackground(with: bundleId, script: scriptData) + } + } else if let scriptBase64URL = components?.queryItems?.first(where: { $0.name == "data" })?.value?.removingPercentEncoding, + let pidStr = components?.queryItems?.first(where: { $0.name == "pid" })?.value { + let base64 = base64URLToBase64(scriptBase64URL) + if let scriptData = Data(base64Encoded: base64) { + startJITInBackground(with: Int(pidStr) ?? 0, script: scriptData) + } } default: break @@ -443,6 +454,7 @@ struct HomeView: View { return { pid, debugProxyHandle, semaphore in jsModel = RunJSViewModel(pid: Int(pid), debugProxy: debugProxyHandle, semaphore: semaphore) scriptViewShow = true + DispatchQueue.global(qos: .background).async { do { try jsModel?.runScript(data: script, name: name) @@ -456,18 +468,15 @@ struct HomeView: View { private func startJITInBackground(with bundleID: String, script: Data? = nil) { isProcessing = true - let orgValue = enablePiP - if script != nil { - enablePiP = true - } - // Add log message LogManager.shared.addInfoLog("Starting Debug for \(bundleID)") DispatchQueue.global(qos: .background).async { var callback: DebugAppCallback? = nil - if enableAdvancedOptions { + if let script { + callback = getJsCallback(script, name: bundleID + ".js") + } else if enableAdvancedOptions { let mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String] if let script = mapping?[bundleID] { callback = getJsCallback(for: script) @@ -476,9 +485,6 @@ struct HomeView: View { } } - if let script { - callback = getJsCallback(script, name: bundleID + ".js") - } let success = JITEnableContext.shared.debugApp(withBundleID: bundleID, logger: { message in @@ -491,14 +497,11 @@ struct HomeView: View { DispatchQueue.main.async { LogManager.shared.addInfoLog("Debug process completed for \(bundleID)") isProcessing = false - if script != nil { - enablePiP = orgValue - } } } } - private func startJITInBackground(with pid: Int) { + private func startJITInBackground(with pid: Int, script: Data? = nil) { isProcessing = true // Add log message @@ -506,7 +509,14 @@ struct HomeView: View { DispatchQueue.global(qos: .background).async { - let jsCallback: DebugAppCallback? = (enableAdvancedOptions && useDefaultScript) ? getJsCallback() : nil + var jsCallback: DebugAppCallback? = nil + + if let script { + jsCallback = getJsCallback(script, name: "Script from PID \(pid)") + } else if (enableAdvancedOptions && useDefaultScript) { + jsCallback = getJsCallback() + } + let success = JITEnableContext.shared.debugApp(withPID: Int32(pid), logger: { message in if let message = message { @@ -522,6 +532,21 @@ struct HomeView: View { } } } + + func base64URLToBase64(_ base64url: String) -> String { + var base64 = base64url + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + + // Pad with "=" to make length a multiple of 4 + let paddingLength = 4 - (base64.count % 4) + if paddingLength < 4 { + base64 += String(repeating: "=", count: paddingLength) + } + + return base64 + } + } class InstalledAppsViewModel: ObservableObject { From 39105382d865d2b07f748c9efe43cba6858cb8a5 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Wed, 16 Jul 2025 13:21:51 +0800 Subject: [PATCH 3/3] Fix pip & url scheme --- StikJIT/Utilities/Extensions.swift | 7 + StikJIT/Views/HomeView.swift | 220 +++++++++++++---------------- 2 files changed, 105 insertions(+), 122 deletions(-) diff --git a/StikJIT/Utilities/Extensions.swift b/StikJIT/Utilities/Extensions.swift index 31fd2c75..d24602b4 100644 --- a/StikJIT/Utilities/Extensions.swift +++ b/StikJIT/Utilities/Extensions.swift @@ -4,6 +4,7 @@ // // Created by s s on 2025/7/9. // +import UniformTypeIdentifiers extension FileManager { func filePath(atPath path: String, withLength length: Int) -> String? { @@ -17,3 +18,9 @@ public extension ProcessInfo { { if let boot = FileManager.default.filePath(atPath: "/System/Volumes/Preboot", withLength: 36), let file = FileManager.default.filePath(atPath: "\(boot)/boot", withLength: 96) { return access("\(file)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 } else { return (FileManager.default.filePath(atPath: "/private/preboot", withLength: 96).map { access("\($0)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 }) ?? false } }() } } + +extension UIDocumentPickerViewController { + @objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController { + return fix_init(forOpeningContentTypes: contentTypes, asCopy: true) + } +} diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index b5b9ae4b..49d2a5b0 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -9,10 +9,11 @@ import SwiftUI import UniformTypeIdentifiers import Pipify -extension UIDocumentPickerViewController { - @objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController { - return fix_init(forOpeningContentTypes: contentTypes, asCopy: true) - } +struct JITEnableConfiguration { + var bundleID: String? = nil + var pid : Int? = nil + var scriptData: Data? = nil + var scriptName : String? = nil } struct HomeView: View { @@ -37,13 +38,13 @@ struct HomeView: View { @State private var pidStr = "" @State private var viewDidAppeared = false - @State private var pendingBundleIdToEnableJIT : String? = nil - @State private var pendingPIDToEnableJIT : Int? = nil + @State private var pendingJITEnableConfiguration : JITEnableConfiguration? = nil @AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false @AppStorage("useDefaultScript") private var useDefaultScript = false @AppStorage("enablePiP") private var enablePiP = true @State var scriptViewShow = false + @State var pipRequired = false @AppStorage("DefaultScriptName") var selectedScript = "attachDetach.js" @State var jsModel: RunJSViewModel? @@ -301,12 +302,12 @@ struct HomeView: View { bundleID = selectedBundle isShowingInstalledApps = false HapticFeedbackHelper.trigger() - startJITInBackground(with: selectedBundle) + startJITInBackground(bundleID: selectedBundle) } } .pipify(isPresented: Binding( - get: { useDefaultScript && enablePiP && isProcessing }, - set: { newValue in isProcessing = newValue } + get: { pipRequired && enablePiP }, + set: { newValue in pipRequired = newValue } )) { RunJSViewPiP(model: $jsModel) } @@ -318,7 +319,6 @@ struct HomeView: View { ToolbarItem(placement: .topBarTrailing) { Button("Done") { scriptViewShow = false - isProcessing = false } } } @@ -348,7 +348,7 @@ struct HomeView: View { showAlert(title: "", message: "Invalid PID".localized, showOk: true, completion: { _ in }) return } - startJITInBackground(with: pid) + startJITInBackground(pid: pid) }, actionCancel: {_ in @@ -357,52 +357,41 @@ struct HomeView: View { ) .onOpenURL { url in print(url.path) - switch url.host { - case "enable-jit": - let components = URLComponents(url: url, resolvingAgainstBaseURL: false) - if let bundleId = components?.queryItems?.first(where: { $0.name == "bundle-id" })?.value { - if viewDidAppeared { - startJITInBackground(with: bundleId) - } else { - pendingBundleIdToEnableJIT = bundleId - } - } else if let pidStr = components?.queryItems?.first(where: { $0.name == "pid" })?.value, let pid = Int(pidStr) { - if viewDidAppeared { - startJITInBackground(with: pid) - } else { - pendingPIDToEnableJIT = pid - } - } - case "script": - let components = URLComponents(url: url, resolvingAgainstBaseURL: false) - if let scriptBase64URL = components?.queryItems?.first(where: { $0.name == "data" })?.value?.removingPercentEncoding, - let bundleId = components?.queryItems?.first(where: { $0.name == "bundle-id" })?.value { - - let base64 = base64URLToBase64(scriptBase64URL) - if let scriptData = Data(base64Encoded: base64) { - startJITInBackground(with: bundleId, script: scriptData) - } - } else if let scriptBase64URL = components?.queryItems?.first(where: { $0.name == "data" })?.value?.removingPercentEncoding, - let pidStr = components?.queryItems?.first(where: { $0.name == "pid" })?.value { - let base64 = base64URLToBase64(scriptBase64URL) - if let scriptData = Data(base64Encoded: base64) { - startJITInBackground(with: Int(pidStr) ?? 0, script: scriptData) - } + if url.host != "enable-jit" { + return + } + + var config = JITEnableConfiguration() + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) + + if let pidStr = components?.queryItems?.first(where: { $0.name == "pid" })?.value, let pid = Int(pidStr) { + config.pid = pid + } + if let bundleId = components?.queryItems?.first(where: { $0.name == "bundle-id" })?.value { + config.bundleID = bundleId + } + if let scriptBase64URL = components?.queryItems?.first(where: { $0.name == "script-data" })?.value?.removingPercentEncoding { + let base64 = base64URLToBase64(scriptBase64URL) + if let scriptData = Data(base64Encoded: base64) { + config.scriptData = scriptData } - default: - break + } + if let scriptName = components?.queryItems?.first(where: { $0.name == "script-name" })?.value { + config.scriptName = scriptName + } + + if viewDidAppeared { + startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true) + } else { + pendingJITEnableConfiguration = config } } .onAppear() { viewDidAppeared = true - if let pendingBundleIdToEnableJIT { - startJITInBackground(with: pendingBundleIdToEnableJIT) - self.pendingBundleIdToEnableJIT = nil - } - if let pendingPIDToEnableJIT { - startJITInBackground(with: pendingPIDToEnableJIT) - self.pendingPIDToEnableJIT = nil + if let config = pendingJITEnableConfiguration { + startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true) + self.pendingJITEnableConfiguration = nil } } } @@ -427,30 +416,7 @@ struct HomeView: View { // but we'll keep it empty to avoid breaking anything } - private func getJsCallback(for scriptName: String? = nil) -> DebugAppCallback? { - let name = scriptName ?? selectedScript - let selectedScriptURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - .appendingPathComponent("scripts").appendingPathComponent(name) - - if !FileManager.default.fileExists(atPath: selectedScriptURL.path) { - return nil - } - - return { pid, debugProxyHandle, semaphore in - jsModel = RunJSViewModel(pid: Int(pid), debugProxy: debugProxyHandle, semaphore: semaphore) - scriptViewShow = true - DispatchQueue.global(qos: .background).async { - do { - try jsModel?.runScript(path: selectedScriptURL) - isProcessing = false - } catch { - showAlert(title: "Error Occurred While Executing the Default Script.".localized, message: error.localizedDescription, showOk: true) - } - } - } - } - - private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback? { + private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback { return { pid, debugProxyHandle, semaphore in jsModel = RunJSViewModel(pid: Int(pid), debugProxy: debugProxyHandle, semaphore: semaphore) scriptViewShow = true @@ -458,7 +424,6 @@ struct HomeView: View { DispatchQueue.global(qos: .background).async { do { try jsModel?.runScript(data: script, name: name) - isProcessing = false } catch { showAlert(title: "Error Occurred While Executing the Default Script.".localized, message: error.localizedDescription, showOk: true) } @@ -466,70 +431,81 @@ struct HomeView: View { } } - private func startJITInBackground(with bundleID: String, script: Data? = nil) { + // launch app following this order: pid > bundleID + // load script following this order: scriptData > script file from script name > saved script for bundleID > default script + // if advanced mode is disabled the whole script loading will be skipped. If use default script is disabled default script will not be loaded + private func startJITInBackground(bundleID: String? = nil, pid : Int? = nil, scriptData: Data? = nil, scriptName : String? = nil, triggeredByURLScheme: Bool = false) { isProcessing = true // Add log message - LogManager.shared.addInfoLog("Starting Debug for \(bundleID)") + LogManager.shared.addInfoLog("Starting Debug for \(bundleID ?? String(pid ?? 0))") DispatchQueue.global(qos: .background).async { - var callback: DebugAppCallback? = nil - - if let script { - callback = getJsCallback(script, name: bundleID + ".js") - } else if enableAdvancedOptions { - let mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String] - if let script = mapping?[bundleID] { - callback = getJsCallback(for: script) - } else if useDefaultScript { - callback = getJsCallback() + var scriptData = scriptData + var scriptName = scriptName + if enableAdvancedOptions && scriptData == nil { + if scriptName == nil, let bundleID, let mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String] { + scriptName = mapping[bundleID] } + + if useDefaultScript && scriptName == nil { + scriptName = selectedScript + } + + if scriptData == nil, let scriptName { + let selectedScriptURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + .appendingPathComponent("scripts").appendingPathComponent(scriptName) + + if FileManager.default.fileExists(atPath: selectedScriptURL.path) { + do { + scriptData = try Data(contentsOf: selectedScriptURL) + } catch { + print("failed to load data from script \(error)") + } + + } + } + } else { + scriptData = nil } - let success = JITEnableContext.shared.debugApp(withBundleID: bundleID, logger: { message in - - if let message = message { - // Log messages from the JIT process - LogManager.shared.addInfoLog(message) - } - }, jsCallback: callback) + var callback: DebugAppCallback? = nil - DispatchQueue.main.async { - LogManager.shared.addInfoLog("Debug process completed for \(bundleID)") - isProcessing = false - } - } - } - - private func startJITInBackground(with pid: Int, script: Data? = nil) { - isProcessing = true - - // Add log message - LogManager.shared.addInfoLog("Starting JIT for pid \(pid)") - - DispatchQueue.global(qos: .background).async { + if let scriptData { + callback = getJsCallback(scriptData, name: scriptName ?? bundleID ?? "Script") + if triggeredByURLScheme { + usleep(500000) + } - var jsCallback: DebugAppCallback? = nil - - if let script { - jsCallback = getJsCallback(script, name: "Script from PID \(pid)") - } else if (enableAdvancedOptions && useDefaultScript) { - jsCallback = getJsCallback() + pipRequired = true } - let success = JITEnableContext.shared.debugApp(withPID: Int32(pid), logger: { message in - + let logger: LogFunc = { message in + if let message = message { // Log messages from the JIT process LogManager.shared.addInfoLog(message) } - }, jsCallback: jsCallback) + } + var success : Bool + if let pid { + success = JITEnableContext.shared.debugApp(withPID: Int32(pid), logger: logger, jsCallback: callback) + } else if let bundleID { + success = JITEnableContext.shared.debugApp(withBundleID: bundleID, logger: logger, jsCallback: callback) + } else { + DispatchQueue.main.async { + showAlert(title: "Failed to Debug App".localized, message: "Either bundle ID or PID should be specified.".localized, showOk: true) + } + success = false + } - DispatchQueue.main.async { - LogManager.shared.addInfoLog("JIT process completed for \(pid)") - showAlert(title: "Success".localized, message: String(format: "JIT has been enabled for pid %d.".localized, pid), showOk: true, messageType: .success) - isProcessing = false + if success { + DispatchQueue.main.async { + LogManager.shared.addInfoLog("Debug process completed for \(bundleID ?? String(pid ?? 0))") + } } + isProcessing = false + pipRequired = false } }