diff --git a/.github/workflows/Project.yml b/.github/workflows/Project.yml new file mode 100644 index 000000000..a24247d22 --- /dev/null +++ b/.github/workflows/Project.yml @@ -0,0 +1,28 @@ +name: Project Build + +on: + push: + pull_request: + +jobs: + build: + runs-on: macos-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Install XcodeGen + run: brew install xcodegen + + - name: Generate Xcode project + run: xcodegen generate + + - name: List generated files + run: ls -R + + - name: Upload generated project + uses: actions/upload-artifact@v4 + with: + name: xcodeproj + path: "*.xcodeproj" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d0d0bea9..db21709b9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,12 @@ jobs: with: fetch-depth: 0 + - name: setup xcodegen + run: brew install xcodegen + + - name: make project + run: xcodegen generate + - name: setup xcode uses: maxim-lobanov/setup-xcode@v1 with: @@ -41,7 +47,7 @@ jobs: with: name: lara-ipa path: | - build/lara.ipa + ./lara.ipa build/xcodebuild.log - name: upload release ipa @@ -49,7 +55,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: release-ipa - path: build/lara.ipa + path: ./lara.ipa release: diff --git a/README.md b/README.md index bfebaf349..17430e423 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ Important Notes: - OTA Update Disabler - Screen Time Disabler - App Decrypt +- Clean Cache ### Coming Soon diff --git a/lara.xcodeproj/.gitignore b/lara.xcodeproj/.gitignore deleted file mode 100644 index 21166e6fd..000000000 --- a/lara.xcodeproj/.gitignore +++ /dev/null @@ -1 +0,0 @@ -xcuserdata/ \ No newline at end of file diff --git a/lara/Info.plist b/lara/Info.plist index 7f18d78f0..2db184d53 100644 --- a/lara/Info.plist +++ b/lara/Info.plist @@ -6,6 +6,18 @@ audio + CFBundleIdentifier + com.roooot.lara + CFBundleExecutable + lara + CFBundleName + Lara + CFBundlePackageType + APPL + CFBundleVersion + 2 + CFBundleShortVersionString + 0.2 UIFileSharingEnabled diff --git a/lara/classes/laramgr.swift b/lara/classes/laramgr.swift index d9004c037..8a3163af6 100644 --- a/lara/classes/laramgr.swift +++ b/lara/classes/laramgr.swift @@ -88,6 +88,7 @@ final class laramgr: ObservableObject { @Published var rcready: Bool = false @Published var rcfailed: Bool = false @Published var showrespring: Bool = false + @Published var developer: Bool = false @Published var showLogs: Bool = false diff --git a/lara/kexploit/offsets.h b/lara/kexploit/offsets.h index c49f1462f..3b4868b86 100644 --- a/lara/kexploit/offsets.h +++ b/lara/kexploit/offsets.h @@ -57,6 +57,7 @@ extern uint32_t off_proc_p_name; extern uint32_t off_proc_ro_pr_task; extern uint32_t off_proc_ro_p_ucred; extern uint32_t off_ucred_cr_label; +extern uint32_t off_ucred_cr_ref; extern uint32_t off_task_itk_space; extern uint32_t off_task_threads_next; extern uint32_t off_task_task_exc_guard; diff --git a/lara/kexploit/offsets.m b/lara/kexploit/offsets.m index 604190147..e9786f773 100644 --- a/lara/kexploit/offsets.m +++ b/lara/kexploit/offsets.m @@ -67,6 +67,7 @@ uint32_t off_proc_ro_pr_task = 0; uint32_t off_proc_ro_p_ucred = 0; uint32_t off_ucred_cr_label = 0; +uint32_t off_ucred_cr_ref = 0; uint32_t off_task_itk_space = 0; uint32_t off_task_threads_next = 0; uint32_t off_task_task_exc_guard = 0; @@ -168,6 +169,7 @@ OFFSET32(off_proc_ro_pr_task), OFFSET32(off_proc_ro_p_ucred), OFFSET32(off_ucred_cr_label), + OFFSET32(off_ucred_cr_ref), OFFSET32(off_task_itk_space), OFFSET32(off_task_threads_next), OFFSET32(off_task_task_exc_guard), @@ -354,6 +356,7 @@ void savealloffsets(void) { @"off_proc_p_name": @(off_proc_p_name), @"off_proc_ro_pr_task": @(off_proc_ro_pr_task), @"off_ucred_cr_label": @(off_ucred_cr_label), + @"off_ucred_cr_ref": @(off_ucred_cr_ref), @"off_task_itk_space": @(off_task_itk_space), @"off_task_threads_next": @(off_task_threads_next), @"off_task_task_exc_guard": @(off_task_task_exc_guard), diff --git a/lara/kexploit/pe/sbx.h b/lara/kexploit/pe/sbx.h index 7956e8c49..fc139486d 100644 --- a/lara/kexploit/pe/sbx.h +++ b/lara/kexploit/pe/sbx.h @@ -10,6 +10,7 @@ #include +uint64_t sbx_ucredbyproc(uint64_t proc); int sbx_escape(uint64_t self_proc); void sbx_setlogcallback(void (*callback)(const char *message)); uint64_t sbx_gettoken(pid_t pid); diff --git a/lara/kexploit/utils.h b/lara/kexploit/utils.h index 19a7f6c9c..f82fad67d 100644 --- a/lara/kexploit/utils.h +++ b/lara/kexploit/utils.h @@ -68,6 +68,8 @@ uint64_t proc_self(void); uint64_t task_self(void); int crashproc(const char* pid); +int proc_pause_resume(const char *name, bool resume); +int count_pids(uint64_t allproc); #ifdef __cplusplus } diff --git a/lara/kexploit/utils.m b/lara/kexploit/utils.m index ec7f8d26a..2a1f725cb 100644 --- a/lara/kexploit/utils.m +++ b/lara/kexploit/utils.m @@ -10,6 +10,7 @@ #import "xpf.h" #import "offsets.h" #import "xpaci.h" +#import "pe/sbx.h" #import #import @@ -47,6 +48,7 @@ static const uint32_t ARM_SS_OFFSET = 0x8; uint32_t TASK_TNEXT_OFFSET; uint32_t THREAD_MUPCB_OFFSET; +bool launchd = true; struct arm_saved_state64 { uint64_t x[29]; @@ -853,3 +855,50 @@ int crashproc(const char* name) { ds_kwrite64(state + offsetof(struct arm_saved_state64, sp), 0x1337133713371337); return 0; } + +int proc_pause_resume(const char *name, bool resume) { + if (!name) { + return -1; + } + + uint64_t proc = procbyname(name); + if (!proc) { + printf("(signal) process not found: %s\n", name); + return -1; + } + + uint32_t pid = ds_kread32(proc + PROC_PID_OFFSET); + int result; + + if (resume) { + result = kill(pid, SIGCONT); + } else { + result = kill(pid, SIGSTOP); + } + + if (result != 0) { + perror("(signal) kill failed"); + return -1; + } + + printf("(signal) %s %s\n", + name, + resume ? "resumed" : "paused"); + return 0; +} + +int count_pids(uint64_t allproc) { + int count = 0; + uint64_t proc = allproc; + + for (int i = 0; i < 12000 && proc; i++) { + int pid = ds_kread32(proc + off_proc_p_pid); + if (pid > 0 && pid < 99999) + count++; + uint64_t next = ds_kread64(proc + off_proc_p_list_le_next); + if (next == 0 || next == proc) + break; + proc = next; + } + return count; +} diff --git a/lara/lara-Bridging-Header.h b/lara/lara-Bridging-Header.h index f092a4222..8103570aa 100644 --- a/lara/lara-Bridging-Header.h +++ b/lara/lara-Bridging-Header.h @@ -20,6 +20,7 @@ #import "persistence.h" #import "ota.h" #import "screentime.h" +#import "themer.h" #import diff --git a/lara/views/app/settings/SettingsView.swift b/lara/views/app/settings/SettingsView.swift index 2a9b54de2..a08d4af32 100644 --- a/lara/views/app/settings/SettingsView.swift +++ b/lara/views/app/settings/SettingsView.swift @@ -8,6 +8,7 @@ import SwiftUI import UIKit import UniformTypeIdentifiers +import Combine enum method: String, CaseIterable { case vfs = "VFS" @@ -254,6 +255,10 @@ struct SettingsView: View { Toggle("Allow >10 dock icons", isOn: $rcDockUnlimited) } #endif + + Section(header: HeaderLabel(text: "Developer", icon: "gear")) { + Toggle("Developer Mode", isOn: $mgr.developer) + } } .navigationTitle("Settings") .fileImporter(isPresented: $showkcacheimport, allowedContentTypes: [.data], allowsMultipleSelection: false) { result in diff --git a/lara/views/tweaks/ToolsView.swift b/lara/views/tweaks/ToolsView.swift index d69d8eb77..1141bb075 100644 --- a/lara/views/tweaks/ToolsView.swift +++ b/lara/views/tweaks/ToolsView.swift @@ -24,6 +24,8 @@ struct ToolsView: View { @State private var pid: pid_t = getpid() @State private var status: String? @State private var crashname: String = "SpringBoard" + @State private var pausedProcesses: Set = [] + @State private var proc_sbx: UInt64 = 0 private enum tokenclass: String, CaseIterable, Identifiable { case read = "com.apple.app-sandbox.read" @@ -156,10 +158,37 @@ struct ToolsView: View { } } .disabled(crashname.isEmpty) + Button("Pause") { + crashname.withCString { _ = proc_pause_resume($0, false) } + pausedProcesses.insert(crashname) + } + .disabled(crashname.isEmpty || pausedProcesses.contains(crashname)) + + Button("Resume") { + crashname.withCString { _ = proc_pause_resume($0, true) } + pausedProcesses.remove(crashname) + } + .disabled(crashname.isEmpty || !pausedProcesses.contains(crashname)) + + Button("SBX Escape Helper") { + crashname.withCString { cstr in + proc_sbx = procbyname(cstr) + } + + if proc_sbx == 0 { + status = "Failed to get proc" + return + } + + let errorcheck = sbx_escape(proc_sbx) + status = errorcheck == 0 ? nil : "Failure" + } + .disabled(crashname.isEmpty) + } header: { - Text("Crasher") + Text("Task Manager") } footer: { - Text("Crashes the selected process") + Text("Manages The Selected Process") } Section { diff --git a/lara/views/tweaks/TweaksView.swift b/lara/views/tweaks/TweaksView.swift index 443948457..7a16461d0 100644 --- a/lara/views/tweaks/TweaksView.swift +++ b/lara/views/tweaks/TweaksView.swift @@ -59,11 +59,12 @@ struct TweaksView: View { .disabled(!mgr.vfsready) NavigationLink("OTA Updates", destination: OTAView(mgr: mgr)) NavigationLink("Screen Time", destination: ScreenTimeView(mgr: mgr)) + NavigationLink("Clean Cache", destination: CacheView()) } Section(header: HeaderLabel(text: "Broken", icon: "exclamationmark.triangle.fill")) { NavigationLink("DarkBoard", destination: DarkBoardView()) - .disabled(true) + .disabled(!mgr.developer) } NavigationLink("Extra Tools", destination: ToolsView()) diff --git a/lara/views/tweaks/cacheclean/BundleResolver.swift b/lara/views/tweaks/cacheclean/BundleResolver.swift new file mode 100644 index 000000000..931e3a6dd --- /dev/null +++ b/lara/views/tweaks/cacheclean/BundleResolver.swift @@ -0,0 +1,222 @@ +// +// BundleResolver.swift +// lara +// + +import Foundation +import UIKit + +// MARK: - Model + +struct ResolvedApp { + let dataUUID: String + let bundleID: String + let bundlePath: String + let name: String + let icon: UIImage? +} + +// MARK: - Resolver + +final class BundleResolver { + + private let fm = FileManager.default + + private let bundleRoot = "/var/containers/Bundle/Application" + private let dataRoot = "/var/mobile/Containers/Data/Application" + + // MARK: Public + + func resolveAll() -> [ResolvedApp] { + + let bundleMap = buildBundleMap() + + guard let dataContainers = try? fm.contentsOfDirectory(atPath: dataRoot) else { + return [] + } + + var results: [ResolvedApp] = [] + + for dataUUID in dataContainers { + + let dataPath = dataRoot + "/" + dataUUID + let metaPath = dataPath + "/.com.apple.mobile_container_manager.metadata.plist" + + // MARK: STEP 1 - metadata + var bundleID = + NSDictionary(contentsOfFile: metaPath)?["MCMMetadataIdentifier"] as? String + + // MARK: STEP 2 - fallback scan inside container + if bundleID == nil { + bundleID = findBundleIDInDataContainer(dataPath) + } + + // ❗ DO NOT DROP APP + let finalBundleID = bundleID ?? "unknown.\(dataUUID)" + + // MARK: STEP 3 - resolve bundle path + let resolvedBundlePath = + bundleID != nil + ? (bundleMap[finalBundleID] ?? findBundlePathFallback(bundleID: finalBundleID)) + : nil + + let finalBundlePath = resolvedBundlePath ?? "" + + // MARK: STEP 4 - name resolution (NEVER FAIL) + let finalName = readSafeName( + bundlePath: finalBundlePath.isEmpty ? dataPath : finalBundlePath, + fallback: dataUUID + ) + + // MARK: STEP 5 - icon resolution (NEVER FAIL) + let finalIcon = + finalBundlePath.isEmpty + ? UIImage(systemName: "app") + : readIcon(finalBundlePath) + + results.append( + ResolvedApp( + dataUUID: dataUUID, + bundleID: finalBundleID, + bundlePath: finalBundlePath, + name: finalName, + icon: finalIcon + ) + ) + } + + return results + } + + // MARK: Bundle Map + + private func buildBundleMap() -> [String: String] { + + var map: [String: String] = [:] + + guard let roots = try? fm.contentsOfDirectory(atPath: bundleRoot) else { + return map + } + + for root in roots { + + let rootPath = bundleRoot + "/" + root + + guard let items = try? fm.contentsOfDirectory(atPath: rootPath) else { + continue + } + + for item in items where item.hasSuffix(".app") { + + let appPath = rootPath + "/" + item + + // PRIMARY + if let meta = NSDictionary(contentsOfFile: appPath + "/.com.apple.mobile_container_manager.metadata.plist"), + let bundleID = meta["MCMMetadataIdentifier"] as? String { + map[bundleID] = appPath + continue + } + + // FALLBACK + if let info = NSDictionary(contentsOfFile: appPath + "/Info.plist"), + let bundleID = info["CFBundleIdentifier"] as? String { + map[bundleID] = appPath + } + } + } + + return map + } + + // MARK: Data container scan + + private func findBundleIDInDataContainer(_ dataPath: String) -> String? { + + guard let items = try? fm.contentsOfDirectory(atPath: dataPath) else { + return nil + } + + for item in items where item.hasSuffix(".app") { + + let infoPath = dataPath + "/" + item + "/Info.plist" + + if let info = NSDictionary(contentsOfFile: infoPath), + let bundleID = info["CFBundleIdentifier"] as? String { + return bundleID + } + } + + return nil + } + + // MARK: Bundle fallback + + private func findBundlePathFallback(bundleID: String) -> String? { + + guard let roots = try? fm.contentsOfDirectory(atPath: bundleRoot) else { + return nil + } + + for root in roots { + + let rootPath = bundleRoot + "/" + root + + guard let items = try? fm.contentsOfDirectory(atPath: rootPath) else { + continue + } + + for item in items where item.hasSuffix(".app") { + + let appPath = rootPath + "/" + item + + if let info = NSDictionary(contentsOfFile: appPath + "/Info.plist"), + let id = info["CFBundleIdentifier"] as? String, + id == bundleID { + return appPath + } + } + } + + return nil + } + + // MARK: SAFE NAME (never empty) + + private func readSafeName(bundlePath: String, fallback: String) -> String { + + let infoPath = bundlePath + "/Info.plist" + + guard let info = NSDictionary(contentsOfFile: infoPath) else { + return fallback + } + + return info["CFBundleDisplayName"] as? String ?? + info["CFBundleName"] as? String ?? + fallback + } + + // MARK: Icon + + private func readIcon(_ bundlePath: String) -> UIImage? { + + let infoPath = bundlePath + "/Info.plist" + + guard let info = NSDictionary(contentsOfFile: infoPath) else { + return UIImage(systemName: "app") + } + + if let icons = info["CFBundleIcons"] as? [String: Any], + let primary = icons["CFBundlePrimaryIcon"] as? [String: Any], + let files = primary["CFBundleIconFiles"] as? [String], + let iconName = files.last { + + let path = bundlePath + "/" + iconName + + return UIImage(contentsOfFile: path) + ?? UIImage(contentsOfFile: path + "@2x.png") + ?? UIImage(contentsOfFile: path + ".png") + } + + return UIImage(systemName: "app") + } +} diff --git a/lara/views/tweaks/cacheclean/CacheView.swift b/lara/views/tweaks/cacheclean/CacheView.swift new file mode 100644 index 000000000..f7294dad6 --- /dev/null +++ b/lara/views/tweaks/cacheclean/CacheView.swift @@ -0,0 +1,356 @@ +// +// +// CacheView.swift +// +// lara +// + +import SwiftUI +import UIKit +import WebKit +import Combine + +// MARK: - Models + +struct CacheApp: Identifiable { + let id: String + let name: String + let bundleID: String? + + let appBundlePath: String? + let dataContainerPath: String + + let icon: UIImage? + + let cacheSize: Int64 + let tmpSize: Int64 + let documentsSize: Int64 + + let cachePath: String + let tmpPath: String + let documentsPath: String +} + +struct StorageSnapshot: Identifiable { + let id = UUID() + let date = Date() + let totalBytes: Int64 +} + +struct AppRecord { + let dataUUID: String + let bundleID: String + let bundlePath: String + let name: String + let icon: UIImage? +} + +// MARK: - Manager + +final class CleanerManager: ObservableObject { + + @Published var apps: [CacheApp] = [] + @Published var snapshots: [StorageSnapshot] = [] + + @Published var isScanning = false + @Published var scanProgress: Double = 0 + @Published var statusText = "" + + @Published var totalCacheBytes: Int64 = 0 + + private let fm = FileManager.default + private let dataRoot = "/var/mobile/Containers/Data/Application" + + // Resolver (optional enrichment layer) + private let resolver = BundleResolver() + private var appDB: [String: AppRecord] = [:] + + // MARK: Build DB (SAFE) + private func buildDatabase() { + let resolved = resolver.resolveAll() + + var db: [String: AppRecord] = [:] + for app in resolved { + db[app.dataUUID] = AppRecord( + dataUUID: app.dataUUID, + bundleID: app.bundleID, + bundlePath: app.bundlePath, + name: app.name, + icon: app.icon + ) + } + + self.appDB = db + } + + // MARK: Scan + + func startScan(minSizeMB: Int64 = 1) { + + guard !isScanning else { return } + + isScanning = true + apps.removeAll() + scanProgress = 0 + totalCacheBytes = 0 + statusText = "Scanning apps..." + + DispatchQueue.global(qos: .userInitiated).async { + + self.buildDatabase() + + let containers = (try? self.fm.contentsOfDirectory(atPath: self.dataRoot)) ?? [] + + let total = max(containers.count, 1) + var processed = 0 + + var results: [CacheApp] = [] + + for uuid in containers { + + let dataPath = self.dataRoot + "/" + uuid + + let cachePath = dataPath + "/Library/Caches" + let tmpPath = dataPath + "/tmp" + let docsPath = dataPath + "/Documents" + + let cacheSize = self.folderSize(cachePath) + let tmpSize = self.folderSize(tmpPath) + let docsSize = self.folderSize(docsPath) + + let totalSize = cacheSize + tmpSize + docsSize + + // Only skip extremely small containers + if totalSize < minSizeMB * 1024 * 1024 { + processed += 1 + continue + } + + let appInfo = self.appDB[uuid] + + let name = appInfo?.name ?? "App \(uuid.suffix(6))" + let bundleID = appInfo?.bundleID + let bundlePath = appInfo?.bundlePath + let icon = appInfo?.icon + + results.append(CacheApp( + id: uuid, + name: name, + bundleID: bundleID, + appBundlePath: bundlePath, + dataContainerPath: dataPath, + icon: icon, + cacheSize: cacheSize, + tmpSize: tmpSize, + documentsSize: docsSize, + cachePath: cachePath, + tmpPath: tmpPath, + documentsPath: docsPath + )) + + processed += 1 + + DispatchQueue.main.async { + self.scanProgress = Double(processed) / Double(total) + self.statusText = "Scanning \(processed)/\(total)" + } + } + + let totalBytes = results.reduce(0) { + $0 + $1.cacheSize + $1.tmpSize + $1.documentsSize + } + + DispatchQueue.main.async { + self.apps = results.sorted { $0.cacheSize > $1.cacheSize } + self.totalCacheBytes = totalBytes + + self.snapshots.append(StorageSnapshot(totalBytes: totalBytes)) + + self.isScanning = false + self.scanProgress = 1.0 + self.statusText = "Completed (\(results.count) apps)" + } + } + } + + // MARK: Delete + + func deleteCache(_ app: CacheApp) { + try? fm.removeItem(atPath: app.cachePath) + try? fm.removeItem(atPath: app.tmpPath) + } + + func deleteAll() { + for app in apps { + deleteCache(app) + } + } + + // MARK: Web cleanup + + func cleanWKWebView() { + + let types: Set = [ + WKWebsiteDataTypeDiskCache, + WKWebsiteDataTypeMemoryCache, + WKWebsiteDataTypeCookies, + WKWebsiteDataTypeLocalStorage, + WKWebsiteDataTypeSessionStorage, + WKWebsiteDataTypeIndexedDBDatabases, + WKWebsiteDataTypeWebSQLDatabases + ] + + WKWebsiteDataStore.default().removeData( + ofTypes: types, + modifiedSince: Date(timeIntervalSince1970: 0) + ) { + DispatchQueue.main.async { + self.statusText = "WKWebView cleared" + } + } + } + + func cleanURLCache() { + URLCache.shared.removeAllCachedResponses() + } + + // MARK: Size + + private func folderSize(_ path: String) -> Int64 { + guard let e = fm.enumerator(atPath: path) else { return 0 } + + var size: Int64 = 0 + + for case let file as String in e { + + let full = (path as NSString).appendingPathComponent(file) + + if let attrs = try? fm.attributesOfItem(atPath: full), + let fileSize = attrs[.size] as? NSNumber { + size += fileSize.int64Value + } + } + + return size + } +} + +// MARK: - UI + +struct CacheView: View { + + @StateObject var mgr = CleanerManager() + + var body: some View { + + NavigationStack { + + VStack { + + Text("Clean Cache") + .font(.title2).bold() + + Text("\(mgr.totalCacheBytes / 1024 / 1024) MB Total") + .font(.title) + + ProgressView(value: mgr.scanProgress) + + Text(mgr.statusText) + .font(.caption) + + List { + + Section("Apps") { + + ForEach(mgr.apps) { app in + + HStack { + + if let icon = app.icon { + Image(uiImage: icon) + .resizable() + .frame(width: 40, height: 40) + .cornerRadius(8) + } else { + Image(systemName: "app") + } + + VStack(alignment: .leading) { + Text(app.name).bold() + Text("Cache \(app.cacheSize / 1024 / 1024) MB") + Text("Tmp \(app.tmpSize / 1024 / 1024) MB") + Text("Docs \(app.documentsSize / 1024 / 1024) MB") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .swipeActions { + Button(role: .destructive) { + mgr.deleteCache(app) + } label: { + Text("Delete") + } + } + .swipeActions(edge: .trailing) { + Button(role: .destructive) { + mgr.deleteCache(app) + } label: { + Text("Cache") + } + } + .swipeActions(edge: .leading) { + Button(role: .destructive) { + try? FileManager.default.removeItem(atPath: app.documentsPath) + if let index = mgr.apps.firstIndex(where: { $0.id == app.id }) { + mgr.apps[index] = CacheApp( + id: app.id, + name: app.name, + bundleID: app.bundleID, + appBundlePath: app.appBundlePath, + dataContainerPath: app.dataContainerPath, + icon: app.icon, + cacheSize: app.cacheSize, + tmpSize: app.tmpSize, + documentsSize: 0, + cachePath: app.cachePath, + tmpPath: app.tmpPath, + documentsPath: app.documentsPath + + ) + } + + } label: { + Text("Docs") + } + .tint(.orange) + } + } + } + + Section("Tools") { + + Button("Delete ALL Cache") { + mgr.deleteAll() + } + .foregroundStyle(.red) + + Button("Clear WKWebView") { + mgr.cleanWKWebView() + } + + Button("Clear URLCache") { + mgr.cleanURLCache() + } + + Button("Rescan") { + mgr.startScan() + } + } + } + } + .onAppear { + mgr.startScan() + } + } + } +} diff --git a/lara/views/tweaks/mobilegestalt/GestaltView.swift b/lara/views/tweaks/mobilegestalt/GestaltView.swift index 5c843b041..ca96a1d0c 100644 --- a/lara/views/tweaks/mobilegestalt/GestaltView.swift +++ b/lara/views/tweaks/mobilegestalt/GestaltView.swift @@ -28,6 +28,7 @@ enum fileloc: String, CaseIterable { let mgCurrentPath = "/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches/com.apple.MobileGestalt.plist" +@MainActor struct GestaltView: View { @AppStorage("gestaltwarn") private var gestaltwarn: Bool = true @AppStorage("mgDeviceName") private var mgDeviceName: String = "" @@ -743,19 +744,23 @@ func verifyPlist(_ plist: Any, targetPath: String) throws -> Data { let attrs = try fm.attributesOfItem(atPath: targetPath) if let current = attrs[.size] as? NSNumber, current.intValue == 0 { - Alertinator.shared.alert( - title: "Dangerous Plist State Detected", - body: "The current plist file is already 0 bytes. Overwriting has been aborted to prevent corruption." - ) + Task { @MainActor in + Alertinator.shared.alert( + title: "Dangerous Plist State Detected", + body: "The current plist file is already 0 bytes. Overwriting has been aborted to prevent corruption." + ) + } throw "Current MobileGestalt file is 0 bytes." } } guard PropertyListSerialization.propertyList(plist, isValidFor: .binary) else { - Alertinator.shared.alert( - title: "Invalid Property List", - body: "The plist is invalid and cannot be written safely." - ) + Task { @MainActor in + Alertinator.shared.alert( + title: "Invalid Property List", + body: "The plist is invalid and cannot be written safely." + ) + } throw "Invalid plist structure." } @@ -766,10 +771,12 @@ func verifyPlist(_ plist: Any, targetPath: String) throws -> Data { ) if data.isEmpty || data.count == 0 { - Alertinator.shared.alert( - title: "Refusing Empty Plist Write", - body: "The generated plist would become 0 bytes after overwrite. Operation cancelled." - ) + Task { @MainActor in + Alertinator.shared.alert( + title: "Refusing Empty Plist Write", + body: "The generated plist would become 0 bytes after overwrite. Operation cancelled." + ) + } throw "Serialized plist data is empty." } @@ -780,10 +787,12 @@ func verifyPlist(_ plist: Any, targetPath: String) throws -> Data { format: nil ) } catch { - Alertinator.shared.alert( - title: "Invalid Serialized Property List", - body: "The generated plist failed validation after serialization." - ) + Task { @MainActor in + Alertinator.shared.alert( + title: "Invalid Serialized Property List", + body: "The generated plist failed validation after serialization." + ) + } throw "Serialized plist validation failed." } diff --git a/lara.xcodeproj/project.pbxproj b/lara1.xcodeproj/project.pbxproj similarity index 100% rename from lara.xcodeproj/project.pbxproj rename to lara1.xcodeproj/project.pbxproj diff --git a/lara.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/lara1.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from lara.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to lara1.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/project.yml b/project.yml new file mode 100644 index 000000000..6d32a4a38 --- /dev/null +++ b/project.yml @@ -0,0 +1,28 @@ +name: lara + +options: + bundleIdPrefix: com.roooot + deploymentTarget: + iOS: 17.0 + +targets: + lara: + type: application + platform: iOS + + sources: + - path: lara + + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.roooot.lara + INFOPLIST_FILE: lara/Info.plist + SWIFT_OBJC_BRIDGING_HEADER: lara/lara-Bridging-Header.h + LIBRARY_SEARCH_PATHS: $(inherited) lara/lib/ + OTHER_LDFLAGS: $(inherited) -lxpf -lgrabkernel2 + SWIFT_VERSION: 5.0 + SWIFT_STRICT_CONCURRENCY: minimal + + dependencies: + - sdk: UIKit.framework + - sdk: Foundation.framework diff --git a/scripts/build_ipa.sh b/scripts/build_ipa.sh index cb166dc40..0ab17f1cf 100755 --- a/scripts/build_ipa.sh +++ b/scripts/build_ipa.sh @@ -8,7 +8,7 @@ echo "Build Started!" echo xcodebuild \ - -project lara.xcodeproj \ + -project lara1.xcodeproj \ -scheme lara \ -configuration Debug \ -sdk iphoneos \ @@ -18,27 +18,77 @@ xcodebuild \ CODE_SIGN_IDENTITY="" \ CODE_SIGN_ENTITLEMENTS="Config/lara.entitlements" \ archive \ - -archivePath "$PWD/build/lara.xcarchive" 2>&1 | xcpretty + -archivePath "$PWD/build/lara.xcarchive" + +# ----------------------------------- +# FIND APP FROM XCODE OUTPUT +# ----------------------------------- + +APP_PATH=$(find "$PWD/build/lara.xcarchive/Products/Applications" -name "*.app" -type d | head -n 1) -APP_PATH="$PWD/build/lara.xcarchive/Products/Applications/lara.app" if [ ! -d "$APP_PATH" ]; then - echo "Missing app at $APP_PATH" + echo "Missing .app in archive" exit 1 fi -rm -rf "$PWD/build/Payload" -mkdir -p "$PWD/build/Payload" -cp -R "$APP_PATH" "$PWD/build/Payload/" -plutil -replace UIFileSharingEnabled -bool YES "$PWD/build/Payload/lara.app/Info.plist" +echo "Found app: $APP_PATH" + +# ----------------------------------- +# COPY TO PROJECT ROOT (NEW BEHAVIOR) +# ----------------------------------- + +APP_ROOT="$PWD/lara.app" + +rm -rf "$APP_ROOT" +cp -R "$APP_PATH" "$APP_ROOT" + +echo "Copied app to project root: $APP_ROOT" + +# ----------------------------------- +# MODIFY INFO.PLIST +# ----------------------------------- + +plutil -replace UIFileSharingEnabled -bool YES "$APP_ROOT/Info.plist" + +# ----------------------------------- +# DETECT EXECUTABLE NAME +# ----------------------------------- + +EXEC_NAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleExecutable" "$APP_ROOT/Info.plist") + +if [ -z "$EXEC_NAME" ]; then + echo "Failed to read CFBundleExecutable" + exit 1 +fi + +echo "Executable: $EXEC_NAME" + +# ----------------------------------- +# SIGN (ldid) +# ----------------------------------- if ! command -v ldid >/dev/null 2>&1; then echo "ERROR: ldid not installed. Install with: brew install ldid" >&2 exit 1 fi -ldid -SConfig/lara.entitlements "$PWD/build/Payload/lara.app/lara" -(cd "$PWD/build" && /usr/bin/zip -qry lara.ipa Payload) + +ldid -SConfig/lara.entitlements "$APP_ROOT/$EXEC_NAME" +#cd "$APP_ROOT" +#mkdir Frameworks +#mv libgrabkernel2.dylib Frameworks/ +#mv libxpf.dylib Frameworks/ + +# ----------------------------------- +# BUILD IPA +# ----------------------------------- + +rm -rf Payload +mkdir -p Payload +cp -R "$APP_ROOT" Payload/ + +(cd "$PWD" && /usr/bin/zip -qry lara.ipa Payload) echo echo "build successful!" -echo "ipa at: build/lara.ipa" +echo "ipa at: $PWD/lara.ipa" exit 0