diff --git a/StikJIT/Views/MainTabView.swift b/StikJIT/Views/MainTabView.swift index 8caf282..1e38374 100644 --- a/StikJIT/Views/MainTabView.swift +++ b/StikJIT/Views/MainTabView.swift @@ -57,7 +57,7 @@ struct MainTabView: View { TabDescriptor(id: "deviceinfo", title: "Device Info", systemImage: "iphone.and.arrow.forward") { AnyView(DeviceInfoView()) } ] if FeatureFlags.showBetaTabs { - tabs.append(TabDescriptor(id: "profiles", title: "Profiles", systemImage: "magazine.fill") { AnyView(ProfileView()) }) + tabs.append(TabDescriptor(id: "profiles", title: "App Expiry", systemImage: "calendar.badge.clock") { AnyView(ProfileView()) }) tabs.append(TabDescriptor(id: "processes", title: "Processes", systemImage: "rectangle.stack.person.crop") { AnyView(ProcessInspectorView()) }) tabs.append(TabDescriptor(id: "devicelibrary", title: "Devices", systemImage: "list.bullet.rectangle") { AnyView(DeviceLibraryView()) }) if FeatureFlags.isLocationSpoofingEnabled { diff --git a/StikJIT/Views/ProfileView.swift b/StikJIT/Views/ProfileView.swift index 92b33f7..d783db7 100644 --- a/StikJIT/Views/ProfileView.swift +++ b/StikJIT/Views/ProfileView.swift @@ -120,36 +120,17 @@ struct ProfileView: View { @State private var exportDoc = ProfileDocument() @State private var isImporterPresented = false - @State private var searchText = "" @State private var alert = false @State private var alertTitle = "" @State private var alertMsg = "" @State private var alertSuccess = false - - @State private var profiles: [Profile] = [] - @AppStorage("cachedProfilesData") private var cachedProfilesData: Data? + @State private var entries: [AppProfileStatus] = [] + @State private var expandedApps: Set = [] + @State private var notMatchedProfiles: [AppProfileStatus] = [] + @State private var confirmRemove = false @State private var removeTargetName: String = "" @State private var removeTargetUUID: String = "" - @State private var expandedAppIds: Set = [] - - // Computed property to group profiles by appId and sort by expiration date - private var groupedProfiles: [String: [Profile]] { - Dictionary(grouping: profiles, by: { $0.appId }) - .mapValues { profiles in - profiles.sorted { profile1, profile2 in - // Sort by expiration date descending (latest first) - guard let date1 = profile1.expirationDate else { return false } - guard let date2 = profile2.expirationDate else { return true } - return date1 > date2 - } - } - } - - private var sortedAppIds: [String] { - groupedProfiles.keys.sorted() - } - @AppStorage("customAccentColor") private var customAccentColorHex: String = "" @AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue @@ -163,7 +144,6 @@ struct ProfileView: View { ZStack { ThemedBackground(style: backgroundStyle) .ignoresSafeArea() - ScrollView { VStack(spacing: 20) { infoCard @@ -171,7 +151,6 @@ struct ProfileView: View { .padding(.horizontal, 20) .padding(.vertical, 30) } - if alert { CustomErrorView(title: alertTitle, message: alertMsg, @@ -199,7 +178,7 @@ struct ProfileView: View { } } - .navigationTitle("Profiles") + .navigationTitle("App Expiry") .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { @@ -209,7 +188,7 @@ struct ProfileView: View { } Button { - Task { await loadProfiles(force: true) } + Task { await loadData(force: true) } } label: { Label("Reload", systemImage: "arrow.clockwise") } @@ -217,7 +196,7 @@ struct ProfileView: View { } } - .onAppear { Task { await loadProfiles() } } + .onAppear { Task { await loadData() } } .fileImporter( isPresented: $isImporterPresented, allowedContentTypes: [UTType(filenameExtension: "mobileprovision")!], @@ -245,93 +224,125 @@ struct ProfileView: View { // MARK: - UI Sections private var infoCard: some View { - VStack(alignment: .leading, spacing: 14) { + VStack { + HStack { + Text("Sideloaded Apps") + .font(.subheadline) + .foregroundStyle(.secondary) + Spacer() + } - if profiles.isEmpty { - Text(working ? "Loading Profiles" : "No profile available") - } else { - ForEach(sortedAppIds, id: \.self) { appId in - if let appProfiles = groupedProfiles[appId] { - VStack(alignment: .leading, spacing: 8) { - // App ID header with expand/collapse control - HStack { - Text(appId) - .font(.headline) - .foregroundColor(.primary) - Spacer() - if appProfiles.count > 1 { - Button { - withAnimation(.easeInOut(duration: 0.3)) { - if expandedAppIds.contains(appId) { - expandedAppIds.remove(appId) - } else { - expandedAppIds.insert(appId) - } - } - } label: { - HStack(spacing: 4) { - Image(systemName: expandedAppIds.contains(appId) ? "chevron.up" : "chevron.down") - } - } - } - } - .padding(.top, appId != sortedAppIds.first ? 8 : 0) - - // Profiles for this app (only first if collapsed) - let displayProfiles: [Profile] = expandedAppIds.contains(appId) ? appProfiles : Array(appProfiles.prefix(1)) - ForEach(Array(displayProfiles.enumerated()), id: \.element.uuid) { pair in - let index = pair.offset - let entry = pair.element - HStack { - VStack(alignment: .leading, spacing: 6) { - Text(entry.appName).bold() - Text(entry.uuid).lineLimit(1) - Text("Expires: \(entry.formattedDate)") - .foregroundStyle(index == 0 ? entry.dateColor : Color.secondary) - } - .font(.caption.monospaced()) - .foregroundColor(.secondary) - .textSelection(.enabled) - Spacer() - HStack(spacing: 10) { - profileActionButton(icon: "square.and.arrow.down", color: accentColor) { - saveProfile(profile: entry) - } - profileActionButton(icon: "trash", color: .refreshRed) { - removeProfilePrompt(entry: entry) - } - } - } - .padding(.vertical, 4) - .transition(.opacity.combined(with: .move(edge: .top))) - if entry.uuid != displayProfiles.last?.uuid { - Divider().background(Color.white.opacity(0.08)) - } - } - .animation(.easeInOut(duration: 0.3), value: expandedAppIds) - } - - // Divider between app groups - if appId != sortedAppIds.last { + LazyVStack(alignment: .leading, spacing: 14) { + if entries.isEmpty { + Text(working ? "Loading Apps" : "No sideloaded apps found") + } else { + ForEach(entries) { entry in + appRow(for: entry) + if entry.id != entries.last?.id { Divider() .background(Color.white.opacity(0.12)) .padding(.vertical, 4) } } } + + + } + .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) + + if !notMatchedProfiles.isEmpty { + HStack { + Text("Other Profiles") + .padding(.top, 10.0) + .font(.subheadline) + .foregroundStyle(.secondary) + Spacer() + } + LazyVStack(alignment: .leading, spacing: 14) { + ForEach(notMatchedProfiles) { entry in + appRow(for: entry) + if entry.id != notMatchedProfiles.last?.id { + Divider() + .background(Color.white.opacity(0.12)) + .padding(.vertical, 4) + } + } + } + .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) + } + } + } + + private func appRow(for entry: AppProfileStatus) -> some View { + VStack(alignment: .leading, spacing: 10) { + HStack(alignment: .firstTextBaseline) { + Text(entry.name) + .font(.headline) + .foregroundStyle(.primary) + Spacer() + if let match = entry.bestMatchingProfile { + Text("Expires: \(match.profile.formattedDate)") + .foregroundStyle(match.profile.dateColor) + .font(.caption) + } else { + Text("No matching profile") + .font(.caption) + .foregroundColor(.refreshRed) + } + } + Text(entry.id) + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + .textSelection(.enabled) + if let recent = entry.mostRecentProfile { + profileRow(match: recent, isMostRecent: true) + } + if let best = entry.bestMatchingProfile, best.profile.uuid != entry.mostRecentProfile?.profile.uuid { + profileRow(match: best, isMostRecent: false) + } + if entry.profileMatches.count > 1 { + if expandedApps.contains(entry.id) { + ForEach(entry.profileMatches.dropFirst(recentAndBestCount(for: entry)), id: \.profile.uuid) { match in + profileRow(match: match, isMostRecent: false) + } + } + Button { + withAnimation(.easeInOut(duration: 0.25)) { + if expandedApps.contains(entry.id) { + expandedApps.remove(entry.id) + } else { + expandedApps.insert(entry.id) + } + } + } label: { + Label(expandedApps.contains(entry.id) ? "Hide older profiles" : "Show older profiles", + systemImage: expandedApps.contains(entry.id) ? "chevron.up" : "chevron.down") + .font(.caption) + .labelStyle(.titleAndIcon) + .foregroundStyle(.secondary) + } } } - .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) } private func profileActionButton(icon: String, color: Color, action: @escaping () -> Void) -> some View { @@ -343,54 +354,126 @@ struct ProfileView: View { .buttonStyle(.borderless) } - private func removeProfilePrompt(entry: Profile) { - removeTargetName = entry.appName - removeTargetUUID = entry.uuid - confirmRemove = true - } - - private func restoreCachedProfilesIfNeeded() async -> Bool { - guard profiles.isEmpty, let cachedProfilesData else { return false } - - let cachedDatas: [Data]? = try? await Task.detached(priority: .userInitiated) { - try? JSONDecoder().decode([Data].self, from: cachedProfilesData) - }.value - - guard let cachedDatas, !cachedDatas.isEmpty else { return false } - let restoredProfiles = cachedDatas.map { Profile(data: $0) } - await MainActor.run { - guard self.profiles.isEmpty else { return } - self.profiles = restoredProfiles - self.working = false + private func recentAndBestCount(for entry: AppProfileStatus) -> Int { + let distinctBest : Int + if let bestMatchingProfile = entry.bestMatchingProfile, let recent = entry.mostRecentProfile { + distinctBest = (bestMatchingProfile.profile.uuid != recent.profile.uuid) ? 1 : 0 + } else { + distinctBest = 0 } - return true + + return 1 + distinctBest } - func loadProfiles(force: Bool = false) async { - if !force { - if !profiles.isEmpty { - await MainActor.run { working = false } - return + private func profileRow(match: ProfileMatch, isMostRecent: Bool) -> some View { + VStack(alignment: .leading, spacing: 6) { + HStack { + Text(isMostRecent ? "Most Recent Profile" : "Profile") + .font(.subheadline.bold()) + .foregroundStyle(.primary) + Spacer() + Text(match.profile.formattedDate) + .font(.caption.monospaced()) + .foregroundStyle(match.profile.dateColor) + } + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(match.profile.appName) + .font(.caption) + .foregroundStyle(.secondary) + Text("UUID: \(match.profile.uuid)") + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + .textSelection(.enabled) + } + Spacer() + HStack { + profileActionButton(icon: "square.and.arrow.down", color: accentColor) { + saveProfile(profile: match.profile) + } + profileActionButton(icon: "trash", color: .refreshRed) { + removeProfilePrompt(entry: match.profile) + } + } + } + if !match.missingEntitlements.isEmpty { + VStack(alignment: .leading, spacing: 2) { + Text("Missing entitlements:") + .font(.caption) + .foregroundColor(.refreshRed) + ForEach(Array(formattedMissingLines(from: match.missingEntitlements).enumerated()), id: \.offset) { _, line in + Text(line) + .font(.caption.monospaced()) + .foregroundColor(.refreshRed) + } + } } - if await restoreCachedProfilesIfNeeded() { return } } - + .padding(10) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color.white.opacity(0.05)) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .strokeBorder(match.missingEntitlements.isEmpty ? Color.white.opacity(0.1) : Color.refreshRed.opacity(0.6), lineWidth: 1) + ) + ) + .background( + match.missingEntitlements.isEmpty ? Color.clear : Color.refreshRed.opacity(0.08) + ) + .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + } + + // MARK: - Data loading + + private func loadData(force: Bool = false) async { + if !force, !entries.isEmpty { + working = false + return + } await MainActor.run { working = true } do { - let profileDatas = try await Task.detached(priority: .userInitiated) { - try JITEnableContext.shared.fetchAllProfiles() + let (profiles, apps) = try await Task.detached(priority: .userInitiated) { () throws -> ([Profile], [SideloadedApp]) in + let profileDatas = try JITEnableContext.shared.fetchAllProfiles() + let parsedProfiles = profileDatas.map { Profile(data: $0) } + let rawApps = try JITEnableContext.shared.getSideloadedApps() + let parsedApps = rawApps.compactMap { item -> SideloadedApp? in + guard let dict = item as? [String: Any] else { return nil } + return SideloadedApp(dictionary: dict) + } + return (parsedProfiles, parsedApps) }.value - let parsedProfiles = profileDatas.map { Profile(data: $0) } - let encodedProfiles = try? JSONEncoder().encode(profileDatas) + let groupedProfiles = Dictionary(grouping: profiles) { profileIdentifier(from: $0) } + .mapValues { profiles in + profiles.sorted { lhs, rhs in + let leftDate = lhs.expirationDate ?? .distantPast + let rightDate = rhs.expirationDate ?? .distantPast + return leftDate > rightDate + } + } + + let wildcardGroups = groupedProfiles.filter { $0.key.contains("*") } + let appsSorted = apps.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + let computedEntries = appsSorted.map { app in + let matches = profileMatches(for: app, groupedProfiles: groupedProfiles, wildcardProfiles: wildcardGroups) + return AppProfileStatus(app: app, profileMatches: matches) + } + + let appIds = apps.compactMap { $0.applicationIdentifier } + notMatchedProfiles = groupedProfiles + .filter { !appIds.contains($0.key) } + .map { appId, profiles in + AppProfileStatus(appId: appId, profiles: profiles) + } + await MainActor.run { - self.profiles = parsedProfiles - self.cachedProfilesData = encodedProfiles + self.entries = computedEntries self.working = false } } catch { await MainActor.run { + alertTitle = "Failed to load" alertMsg = error.localizedDescription - alertTitle = "Failed to Fetch Profiles" alertSuccess = false alert = true working = false @@ -398,6 +481,117 @@ struct ProfileView: View { } } + + private func profileMatches(for app: SideloadedApp, groupedProfiles: [String: [Profile]], wildcardProfiles: [String: [Profile]]) -> [ProfileMatch] { + let targetIdentifier = app.applicationIdentifier ?? app.bundleId + var collected: [Profile] = [] + if let direct = groupedProfiles[targetIdentifier] { + collected.append(contentsOf: direct) + } + if targetIdentifier != app.bundleId, let bundleMatches = groupedProfiles[app.bundleId] { + collected.append(contentsOf: bundleMatches) + } + for (pattern, profiles) in wildcardProfiles { + if wildcardMatch(pattern, value: targetIdentifier) || wildcardMatch(pattern, value: app.bundleId) { + collected.append(contentsOf: profiles) + } + } + var seen = Set() + let uniqueProfiles = collected.filter { profile in + guard !profile.uuid.isEmpty else { return false } + let inserted = seen.insert(profile.uuid).inserted + return inserted + } + let matches = uniqueProfiles.map { profile -> ProfileMatch in + let profileEntitlements = entitlements(from: profile) + let missing = missingEntitlements(appEntitlements: app.entitlements, profileEntitlements: profileEntitlements) + return ProfileMatch(profile: profile, missingEntitlements: missing) + } + return matches.sorted { lhs, rhs in + let leftDate = lhs.profile.expirationDate ?? .distantPast + let rightDate = rhs.profile.expirationDate ?? .distantPast + return leftDate > rightDate + } + } + + // MARK: - Entitlement helpers + + private func profileIdentifier(from profile: Profile) -> String { + entitlements(from: profile)["application-identifier"] as? String ?? profile.appId + } + + private func entitlements(from profile: Profile) -> [String: Any] { + profile.plistDict?["Entitlements"] as? [String: Any] ?? [:] + } + + private func missingEntitlements(appEntitlements: [String: Any], profileEntitlements: [String: Any]) -> [MissingNode] { + var missing: [MissingNode] = [] + for (key, appValue) in appEntitlements { + guard let profileValue = profileEntitlements[key] else { + missing.append(MissingNode(name: key, children: [])) + continue + } + if let node = diffEntitlement(name: key, appValue: appValue, profileValue: profileValue) { + missing.append(node) + } + } + return missing.sorted { $0.name < $1.name } + } + + private func diffEntitlement(name: String, appValue: Any, profileValue: Any) -> MissingNode? { + switch (profileValue, appValue) { + case let (p as String, a as String): + return wildcardMatch(p, value: a) ? nil : MissingNode(name: name, children: []) + case let (p as [Any], a as [Any]): + let aStrings = a.compactMap { normalizedString(from: $0) } + let pStrings = p.compactMap { normalizedString(from: $0) } + let missingValues = aStrings.filter { target in + !pStrings.contains(where: { wildcardMatch($0, value: target) }) + } + guard !missingValues.isEmpty else { return nil } + let children = missingValues.map { MissingNode(name: $0, children: []) } + return MissingNode(name: name, children: children) + case let (p as [Any], a): + let aString = normalizedString(from: a) + let pStrings = p.compactMap { normalizedString(from: $0) } + if let aString, pStrings.contains(where: { wildcardMatch($0, value: aString) }) { + return nil + } + return MissingNode(name: name, children: aString.map { [MissingNode(name: $0, children: [])] } ?? []) + case let (p as [String: Any], a as [String: Any]): + let childMissing = missingEntitlements(appEntitlements: a, profileEntitlements: p) + return childMissing.isEmpty ? nil : MissingNode(name: name, children: childMissing) + case let (p as NSNumber, a as NSNumber): + return p == a ? nil : MissingNode(name: name, children: []) + case let (p as Bool, a as Bool): + return p == a ? nil : MissingNode(name: name, children: []) + default: + if let pObj = profileValue as? NSObject, let aObj = appValue as? NSObject, pObj == aObj { + return nil + } + return MissingNode(name: name, children: []) + } + } + + private func wildcardMatch(_ pattern: String, value: String) -> Bool { + let escaped = NSRegularExpression.escapedPattern(for: pattern) + .replacingOccurrences(of: "\\*", with: ".*") + let regex = "^" + escaped + "$" + return value.range(of: regex, options: .regularExpression) != nil + } + + private func normalizedString(from value: Any) -> String? { + if let str = value as? String { return str } + if let num = value as? NSNumber { return num.stringValue } + return nil + } + + private func removeProfilePrompt(entry: Profile) { + removeTargetName = entry.appName + removeTargetUUID = entry.uuid + confirmRemove = true + } + func saveProfile(profile: Profile) { exportFileName = "\(profile.appName).mobileprovision" exportDoc = ProfileDocument(data: profile.data) @@ -413,7 +607,7 @@ struct ProfileView: View { alertSuccess = true alert = true // Reload profiles after removal - await loadProfiles(force: true) + await loadData(force: true) } catch { alertMsg = error.localizedDescription alertTitle = "Failed to Remove Profile" @@ -448,7 +642,7 @@ struct ProfileView: View { alert = true // Reload profiles after adding - await loadProfiles(force: true) + await loadData(force: true) } catch { alertMsg = error.localizedDescription alertTitle = "Failed to Add Profile" @@ -457,5 +651,90 @@ struct ProfileView: View { } working = false } - +} + +private struct MissingNode: Identifiable { + let id = UUID() + let name: String + let children: [MissingNode] +} + +private func formattedMissingLines(from nodes: [MissingNode]) -> [String] { + var lines: [String] = [] + for node in nodes { + lines.append(node.name) + lines.append(contentsOf: formattedMissingChildren(node.children, indentLevel: 1)) + } + return lines +} + +private func formattedMissingChildren(_ children: [MissingNode], indentLevel: Int) -> [String] { + var lines: [String] = [] + let prefix = String(repeating: " ", count: max(indentLevel - 1, 0)) + "┗ " + for child in children { + lines.append(prefix + child.name) + lines.append(contentsOf: formattedMissingChildren(child.children, indentLevel: indentLevel + 1)) + } + return lines +} + +private struct SideloadedApp: Identifiable { + let id: String + let name: String + let bundleId: String + let entitlements: [String: Any] + + var applicationIdentifier: String? { + entitlements["application-identifier"] as? String + } + + init?(dictionary: [String: Any]) { + guard let bundleId = dictionary["CFBundleIdentifier"] as? String else { return nil } + self.bundleId = bundleId + self.id = bundleId + let displayName = dictionary["CFBundleDisplayName"] as? String + let name = dictionary["CFBundleName"] as? String + self.name = displayName ?? name ?? bundleId + self.entitlements = dictionary["Entitlements"] as? [String: Any] ?? [:] + } +} + +private struct ProfileMatch: Identifiable { + let id: String + let profile: Profile + let missingEntitlements: [MissingNode] + + init(profile: Profile, missingEntitlements: [MissingNode]) { + self.id = profile.uuid + self.profile = profile + self.missingEntitlements = missingEntitlements + } +} + +private struct AppProfileStatus: Identifiable { + let id: String + let name: String + let bundleId: String + let entitlements: [String: Any] + let profileMatches: [ProfileMatch] + + var mostRecentProfile: ProfileMatch? { profileMatches.first } + var bestMatchingProfile: ProfileMatch? { profileMatches.first(where: { $0.missingEntitlements.isEmpty }) } + + init(app: SideloadedApp, profileMatches: [ProfileMatch]) { + self.id = app.id + self.name = app.name + self.bundleId = app.bundleId + self.entitlements = app.entitlements + self.profileMatches = profileMatches + + } + + init(appId: String, profiles:[Profile]) { + self.id = appId + self.name = "-" + self.bundleId = appId + self.profileMatches = profiles.map { ProfileMatch(profile: $0, missingEntitlements: []) } + self.entitlements = [:] + } } diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index 3f7119d..2c593cf 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -91,9 +91,8 @@ struct SettingsView: View { TabOption(id: "scripts", title: "Scripts", detail: "Manage automation scripts", icon: "scroll", isBeta: false) ] options.append(TabOption(id: "deviceinfo", title: "Device Info", detail: "View detailed device metadata", icon: "iphone.and.arrow.forward", isBeta: false)) - if FeatureFlags.showBetaTabs { - options.append(TabOption(id: "profiles", title: "Profiles", detail: "Install/remove profiles", icon: "magazine", isBeta: true)) - } + options.append(TabOption(id: "profiles", title: "App Expiry", detail: "Check app expiration date, install/remove profiles", icon: "calendar.badge.clock", isBeta: false)) + if FeatureFlags.showBetaTabs { options.append(TabOption(id: "processes", title: "Processes", detail: "Inspect running apps", icon: "rectangle.stack.person.crop", isBeta: true)) options.append(TabOption(id: "devicelibrary", title: "Devices", detail: "Manage external devices", icon: "list.bullet.rectangle", isBeta: true)) diff --git a/StikJIT/idevice/JITEnableContext.h b/StikJIT/idevice/JITEnableContext.h index 949cb30..68a3175 100644 --- a/StikJIT/idevice/JITEnableContext.h +++ b/StikJIT/idevice/JITEnableContext.h @@ -67,6 +67,7 @@ typedef void (^SyslogErrorHandler)(NSError *error); - (NSDictionary*)getAppListWithError:(NSError**)error; - (NSDictionary*)getAllAppsWithError:(NSError**)error; - (NSDictionary*)getHiddenSystemAppsWithError:(NSError**)error; +- (NSArray*)getSideloadedAppsWithError:(NSError**)error; @end @interface JITEnableContext(Syslog) diff --git a/StikJIT/idevice/applist.m b/StikJIT/idevice/applist.m index 8c52b9b..768e440 100644 --- a/StikJIT/idevice/applist.m +++ b/StikJIT/idevice/applist.m @@ -149,6 +149,65 @@ static NSDictionary *buildAppDictionary(void *apps, return result; } +static NSArray* getSideloadedApps(IdeviceProviderHandle *provider, + NSString **error) { + InstallationProxyClientHandle *client = NULL; + IdeviceFfiError* err = installation_proxy_connect(provider, &client); + if (err) { + *error = [NSString stringWithFormat:@"Failed to connect to installation proxy: %s", err->message]; + idevice_error_free(err); + return nil; + } + + plist_t *apps = NULL; + size_t count = 0; + err = installation_proxy_get_apps(client, NULL, NULL, 0, (void*)&apps, &count); + if (err) { + *error = [NSString stringWithFormat:@"Failed to get apps: %s", err->message]; + idevice_error_free(err); + installation_proxy_client_free(client); + return nil; + } + + NSMutableArray* result = [NSMutableArray new]; + + for (size_t i = 0; i < count; i++) { + plist_t app = ((plist_t *)apps)[i]; + + plist_t profileValidatedNode = 0; + if(!(profileValidatedNode = plist_dict_get_item(app, "ProfileValidated"))) { + continue; + } + + char* bin = 0; + uint32_t size = 0; + plist_to_bin(app, &bin, &size); + if(!bin || size == 0) { + continue; + } + + NSData* d = [NSData dataWithBytes:bin length:size]; + NSError* err; + NSDictionary* dict = [NSPropertyListSerialization propertyListWithData:d options:0 format:nil error:&err]; + plist_mem_free(bin); + + if(err) { + continue; + } + + [result addObject:dict]; + + } + + installation_proxy_client_free(client); + for(int i = 0; i < count; ++i) { + plist_free(apps[i]); + } + idevice_data_free((uint8_t *)apps, sizeof(plist_t)*count); + + return result; +} + static NSDictionary *performAppQuery(IdeviceProviderHandle *provider, BOOL requireGetTaskAllow, NSString **error, @@ -265,6 +324,21 @@ UIImage* getAppIcon(IdeviceProviderHandle* provider, NSString* bundleID, NSStrin return apps; } +- (NSArray*)getSideloadedAppsWithError:(NSError**)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; + } + + NSString* errorStr = nil; + NSArray* apps = getSideloadedApps(provider, &errorStr); + if (errorStr) { + *error = [self errorWithStr:errorStr code:-17]; + return nil; + } + return apps; +} + - (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error { [self ensureHeartbeatWithError:error]; if(*error) { diff --git a/StikJIT/idevice/idevice.h b/StikJIT/idevice/idevice.h index d452c71..7524bb2 100644 --- a/StikJIT/idevice/idevice.h +++ b/StikJIT/idevice/idevice.h @@ -80,6 +80,8 @@ typedef struct AppServiceHandle AppServiceHandle; typedef struct CoreDeviceProxyHandle CoreDeviceProxyHandle; +typedef struct CrashReportCopyMobileHandle CrashReportCopyMobileHandle; + /** * Opaque handle to a DebugProxyClient */ @@ -94,6 +96,8 @@ typedef struct DiagnosticsServiceHandle DiagnosticsServiceHandle; typedef struct HeartbeatClientHandle HeartbeatClientHandle; +typedef struct HouseArrestClientHandle HouseArrestClientHandle; + /** * Opaque C-compatible handle to an Idevice connection */ @@ -121,6 +125,8 @@ typedef struct LockdowndClientHandle LockdowndClientHandle; typedef struct MisagentClientHandle MisagentClientHandle; +typedef struct NotificationProxyClientHandle NotificationProxyClientHandle; + typedef struct OsTraceRelayClientHandle OsTraceRelayClientHandle; typedef struct OsTraceRelayReceiverHandle OsTraceRelayReceiverHandle; @@ -150,6 +156,8 @@ typedef struct RsdHandshakeHandle RsdHandshakeHandle; */ typedef struct ScreenshotClientHandle ScreenshotClientHandle; +typedef struct ScreenshotrClientHandle ScreenshotrClientHandle; + typedef struct SpringBoardServicesClientHandle SpringBoardServicesClientHandle; typedef struct SysdiagnoseStreamHandle SysdiagnoseStreamHandle; @@ -175,6 +183,11 @@ typedef struct IdeviceFfiError { const char *message; } IdeviceFfiError; +/** + * Stub to avoid header problems + */ +typedef void *plist_t; + /** * File information structure for C bindings */ @@ -265,11 +278,6 @@ typedef struct DebugserverCommandHandle { uintptr_t argv_count; } DebugserverCommandHandle; -/** - * Stub to avoid header problems - */ -typedef void *plist_t; - typedef struct SyslogLabel { const char *subsystem; const char *category; @@ -333,6 +341,14 @@ typedef struct CRsdServiceArray { size_t count; } CRsdServiceArray; +/** + * Represents a screenshot data buffer + */ +typedef struct ScreenshotData { + uint8_t *data; + uintptr_t length; +} ScreenshotData; + /** * Creates a new Idevice connection * @@ -446,6 +462,14 @@ struct IdeviceFfiError *idevice_start_session(struct IdeviceHandle *idevice, */ void idevice_free(struct IdeviceHandle *idevice); +/** + * Frees a stream handle + * + * # Safety + * Pass a valid handle allocated by this library + */ +void idevice_stream_free(struct ReadWriteOpaque *stream_handle); + /** * Frees a string allocated by this library * @@ -470,6 +494,27 @@ void idevice_string_free(char *string); */ void idevice_data_free(uint8_t *data, uintptr_t len); +/** + * Frees an array of plists allocated by this library + * + * # Safety + * `data` must be a pointer to data allocated by this library, + * NOT data allocated by libplist. + */ +void idevice_plist_array_free(plist_t *plists, uintptr_t len); + +/** + * Frees a slice of pointers allocated by this library that had an underlying + * vec creation. + * + * The following functions use an underlying vec and are safe to use: + * - idevice_usbmuxd_get_devices + * + * # Safety + * Pass a valid pointer passed by the Vec creating functions + */ +void idevice_outer_slice_free(void *slice, uintptr_t len); + /** * Connects the adapter to a specific port * @@ -592,6 +637,23 @@ struct IdeviceFfiError *adapter_recv(struct AdapterStreamHandle *handle, struct IdeviceFfiError *afc_client_connect(struct IdeviceProviderHandle *provider, struct AfcClientHandle **client); +/** + * Connects to the AFC2 service using a TCP provider + * + * # Arguments + * * [`provider`] - An IdeviceProvider + * * [`client`] - On success, will be set to point to a newly allocated AfcClient handle + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `provider` must be a valid pointer to a handle allocated by this library + * `client` must be a valid, non-null pointer to a location where the handle will be stored + */ +struct IdeviceFfiError *afc2_client_connect(struct IdeviceProviderHandle *provider, + struct AfcClientHandle **client); + /** * Creates a new AfcClient from an existing Idevice connection * @@ -786,12 +848,13 @@ struct IdeviceFfiError *afc_file_open(struct AfcClientHandle *client, struct IdeviceFfiError *afc_file_close(struct AfcFileHandle *handle); /** - * Reads data from an open file + * Reads data from an open file. This advances the cursor of the file. * * # Arguments * * [`handle`] - File handle to read from * * [`data`] - Will be set to point to the read data - * * [`length`] - Will be set to the length of the read data + * * [`len`] - Number of bytes to read from the file + * * [`bytes_read`] - The number of bytes read from the file * * # Returns * An IdeviceFfiError on error, null on success @@ -799,7 +862,75 @@ struct IdeviceFfiError *afc_file_close(struct AfcFileHandle *handle); * # Safety * All pointers must be valid and non-null */ -struct IdeviceFfiError *afc_file_read(struct AfcFileHandle *handle, uint8_t **data, size_t *length); +struct IdeviceFfiError *afc_file_read(struct AfcFileHandle *handle, + uint8_t **data, + uintptr_t len, + size_t *bytes_read); + +/** + * Reads all data from an open file. + * + * # Arguments + * * [`handle`] - File handle to read from + * * [`data`] - Will be set to point to the read data + * * [`length`] - The number of bytes read from the file + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * All pointers must be valid and non-null + */ +struct IdeviceFfiError *afc_file_read_entire(struct AfcFileHandle *handle, + uint8_t **data, + size_t *length); + +/** + * Moves the read/write cursor in an open file. + * + * # Arguments + * * [`handle`] - File handle whose cursor should be moved + * * [`offset`] - Distance to move the cursor, interpreted based on `whence` + * * [`whence`] - Origin used for the seek operation: + * * `0` — Seek from the start of the file (`SeekFrom::Start`) + * * `1` — Seek from the current cursor position (`SeekFrom::Current`) + * * `2` — Seek from the end of the file (`SeekFrom::End`) + * * [`new_pos`] - Output parameter; will be set to the new absolute cursor position + * + * # Returns + * An [`IdeviceFfiError`] on error, or null on success. + * + * # Safety + * All pointers must be valid and non-null. + * + * # Notes + * * If `whence` is invalid, this function returns `FfiInvalidArg`. + * * The AFC protocol may restrict seeking beyond certain bounds; such errors + * are reported through the returned [`IdeviceFfiError`]. + */ +struct IdeviceFfiError *afc_file_seek(struct AfcFileHandle *handle, + int64_t offset, + int whence, + int64_t *new_pos); + +/** + * Returns the current read/write cursor position of an open file. + * + * # Arguments + * * [`handle`] - File handle whose cursor should be queried + * * [`pos`] - Output parameter; will be set to the current absolute cursor position + * + * # Returns + * An [`IdeviceFfiError`] on error, or null on success. + * + * # Safety + * All pointers must be valid and non-null. + * + * # Notes + * This function is equivalent to performing a seek operation with + * `SeekFrom::Current(0)` internally. + */ +struct IdeviceFfiError *afc_file_tell(struct AfcFileHandle *handle, int64_t *pos); /** * Writes data to an open file @@ -1452,6 +1583,148 @@ void core_device_proxy_free(struct CoreDeviceProxyHandle *handle); */ void adapter_free(struct AdapterHandle *handle); +/** + * Automatically creates and connects to the crash report copy mobile service, + * returning a client handle + * + * # Arguments + * * [`provider`] - An IdeviceProvider + * * [`client`] - On success, will be set to point to a newly allocated handle + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `provider` must be a valid pointer to a handle allocated by this library + * `client` must be a valid, non-null pointer to a location where the handle will be stored + */ +struct IdeviceFfiError *crash_report_client_connect(struct IdeviceProviderHandle *provider, + struct CrashReportCopyMobileHandle **client); + +/** + * Creates a new CrashReportCopyMobile client from an existing Idevice connection + * + * # Arguments + * * [`socket`] - An IdeviceSocket handle + * * [`client`] - On success, will be set to point to a newly allocated handle + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `socket` must be a valid pointer to a handle allocated by this library + * `client` must be a valid, non-null pointer to a location where the handle will be stored + */ +struct IdeviceFfiError *crash_report_client_new(struct IdeviceHandle *socket, + struct CrashReportCopyMobileHandle **client); + +/** + * Lists crash report files in the specified directory + * + * # Arguments + * * [`client`] - A valid CrashReportCopyMobile handle + * * [`dir_path`] - Optional directory path (NULL for root "/") + * * [`entries`] - Will be set to point to an array of C strings + * * [`count`] - Will be set to the number of entries + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * All pointers must be valid and non-null + * `dir_path` may be NULL (defaults to root) + * Caller must free the returned array with `afc_free_directory_entries` + */ +struct IdeviceFfiError *crash_report_client_ls(struct CrashReportCopyMobileHandle *client, + const char *dir_path, + char ***entries, + size_t *count); + +/** + * Downloads a crash report file from the device + * + * # Arguments + * * [`client`] - A valid CrashReportCopyMobile handle + * * [`log_name`] - Name of the log file to download (C string) + * * [`data`] - Will be set to point to the file contents + * * [`length`] - Will be set to the size of the data + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * All pointers must be valid and non-null + * `log_name` must be a valid C string + * Caller must free the returned data with `idevice_data_free` + */ +struct IdeviceFfiError *crash_report_client_pull(struct CrashReportCopyMobileHandle *client, + const char *log_name, + uint8_t **data, + size_t *length); + +/** + * Removes a crash report file from the device + * + * # Arguments + * * [`client`] - A valid CrashReportCopyMobile handle + * * [`log_name`] - Name of the log file to remove (C string) + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + * `log_name` must be a valid C string + */ +struct IdeviceFfiError *crash_report_client_remove(struct CrashReportCopyMobileHandle *client, + const char *log_name); + +/** + * Converts this client to an AFC client for advanced file operations + * + * # Arguments + * * [`client`] - A valid CrashReportCopyMobile handle (will be consumed) + * * [`afc_client`] - On success, will be set to an AFC client handle + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer (will be freed after this call) + * `afc_client` must be a valid, non-null pointer where the new AFC client will be stored + */ +struct IdeviceFfiError *crash_report_client_to_afc(struct CrashReportCopyMobileHandle *client, + struct AfcClientHandle **afc_client); + +/** + * Triggers a flush of crash logs from system storage + * + * This connects to the crashreportmover service to move crash logs + * into the AFC-accessible directory. Should be called before listing logs. + * + * # Arguments + * * [`provider`] - An IdeviceProvider + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `provider` must be a valid pointer to a handle allocated by this library + */ +struct IdeviceFfiError *crash_report_flush(struct IdeviceProviderHandle *provider); + +/** + * Frees a CrashReportCopyMobile client handle + * + * # Arguments + * * [`handle`] - The handle to free + * + * # Safety + * `handle` must be a valid pointer to the handle that was allocated by this library, + * or NULL (in which case this function does nothing) + */ +void crash_report_client_free(struct CrashReportCopyMobileHandle *handle); + /** * Creates a new DebugserverCommand * @@ -2205,6 +2478,92 @@ struct IdeviceFfiError *heartbeat_get_marco(struct HeartbeatClientHandle *client */ void heartbeat_client_free(struct HeartbeatClientHandle *handle); +/** + * Connects to the House Arrest service using a TCP provider + * + * # Arguments + * * [`provider`] - An IdeviceProvider + * * [`client`] - On success, will be set to point to a newly allocated HouseArrestClient handle + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `provider` must be a valid pointer to a handle allocated by this library + * `client` must be a valid, non-null pointer to a location where the handle will be stored + */ +struct IdeviceFfiError *house_arrest_client_connect(struct IdeviceProviderHandle *provider, + struct HouseArrestClientHandle **client); + +/** + * Creates a new HouseArrestClient from an existing Idevice connection + * + * # Arguments + * * [`socket`] - An IdeviceSocket handle + * * [`client`] - On success, will be set to point to a newly allocated HouseArrestClient handle + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `socket` must be a valid pointer to a handle allocated by this library + * `client` must be a valid, non-null pointer to a location where the handle will be stored + */ +struct IdeviceFfiError *house_arrest_client_new(struct IdeviceHandle *socket, + struct HouseArrestClientHandle **client); + +/** + * Vends a container for an app + * + * # Arguments + * * [`client`] - The House Arrest client + * * [`bundle_id`] - The bundle ID to vend for + * * [`afc_client`] - The new AFC client for the underlying connection + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a allocated by this library + * `bundle_id` must be a NULL-terminated string + * `afc_client` must be a valid, non-null pointer where the new AFC client will be stored + */ +struct IdeviceFfiError *house_arrest_vend_container(struct HouseArrestClientHandle *client, + const char *bundle_id, + struct AfcClientHandle **afc_client); + +/** + * Vends documents for an app + * + * # Arguments + * * [`client`] - The House Arrest client + * * [`bundle_id`] - The bundle ID to vend for + * * [`afc_client`] - The new AFC client for the underlying connection + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a allocated by this library + * `bundle_id` must be a NULL-terminated string + * `afc_client` must be a valid, non-null pointer where the new AFC client will be stored + */ +struct IdeviceFfiError *house_arrest_vend_documents(struct HouseArrestClientHandle *client, + const char *bundle_id, + struct AfcClientHandle **afc_client); + +/** + * Frees an HouseArrestClient handle + * + * # Arguments + * * [`handle`] - The handle to free + * + * # Safety + * `handle` must be a valid pointer to the handle that was allocated by this library, + * or NULL (in which case this function does nothing) + */ +void house_arrest_client_free(struct HouseArrestClientHandle *handle); + /** * Automatically creates and connects to Installation Proxy, returning a client handle * @@ -2554,6 +2913,43 @@ struct IdeviceFfiError *lockdownd_get_value(struct LockdowndClientHandle *client const char *domain, plist_t *out_plist); +/** + * Tells the device to enter recovery mode + * + * # Arguments + * * `client` - A valid LockdowndClient handle + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + */ +struct IdeviceFfiError *lockdownd_enter_recovery(struct LockdowndClientHandle *client); + +/** + * Sets a value in lockdownd + * + * # Arguments + * * `client` - A valid LockdowndClient handle + * * `key` - The key to set (null-terminated string) + * * `value` - The value to set as a plist + * * `domain` - The domain to set in (null-terminated string, optional) + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + * `key` must be a valid null-terminated string + * `value` must be a valid plist + * `domain` must be a valid null-terminated string or NULL + */ +struct IdeviceFfiError *lockdownd_set_value(struct LockdowndClientHandle *client, + const char *key, + plist_t value, + const char *domain); + /** * Frees a LockdowndClient handle * @@ -3033,6 +3429,148 @@ struct IdeviceFfiError *image_mounter_mount_personalized_with_callback(struct Im void *context), void *context); +/** + * Automatically creates and connects to Notification Proxy, returning a client handle + * + * # Arguments + * * [`provider`] - An IdeviceProvider + * * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `provider` must be a valid pointer to a handle allocated by this library + * `client` must be a valid, non-null pointer to a location where the handle will be stored + */ +struct IdeviceFfiError *notification_proxy_connect(struct IdeviceProviderHandle *provider, + struct NotificationProxyClientHandle **client); + +/** + * Creates a new NotificationProxyClient from an existing Idevice connection + * + * # Arguments + * * [`socket`] - An IdeviceSocket handle + * * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `socket` must be a valid pointer to a handle allocated by this library. The socket is consumed, + * and may not be used again. + * `client` must be a valid, non-null pointer to a location where the handle will be stored + */ +struct IdeviceFfiError *notification_proxy_new(struct IdeviceHandle *socket, + struct NotificationProxyClientHandle **client); + +/** + * Posts a notification to the device + * + * # Arguments + * * `client` - A valid NotificationProxyClient handle + * * `name` - C string containing the notification name + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + * `name` must be a valid null-terminated C string + */ +struct IdeviceFfiError *notification_proxy_post(struct NotificationProxyClientHandle *client, + const char *name); + +/** + * Observes a specific notification + * + * # Arguments + * * `client` - A valid NotificationProxyClient handle + * * `name` - C string containing the notification name to observe + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + * `name` must be a valid null-terminated C string + */ +struct IdeviceFfiError *notification_proxy_observe(struct NotificationProxyClientHandle *client, + const char *name); + +/** + * Observes multiple notifications at once + * + * # Arguments + * * `client` - A valid NotificationProxyClient handle + * * `names` - A null-terminated array of C strings containing notification names + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + * `names` must be a valid pointer to a null-terminated array of null-terminated C strings + */ +struct IdeviceFfiError *notification_proxy_observe_multiple(struct NotificationProxyClientHandle *client, + const char *const *names); + +/** + * Receives the next notification from the device + * + * # Arguments + * * `client` - A valid NotificationProxyClient handle + * * `name_out` - On success, will be set to a newly allocated C string containing the notification name + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + * `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string` + */ +struct IdeviceFfiError *notification_proxy_receive(struct NotificationProxyClientHandle *client, + char **name_out); + +/** + * Receives the next notification with a timeout + * + * # Arguments + * * `client` - A valid NotificationProxyClient handle + * * `interval` - Timeout in seconds to wait for a notification + * * `name_out` - On success, will be set to a newly allocated C string containing the notification name + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + * `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string` + */ +struct IdeviceFfiError *notification_proxy_receive_with_timeout(struct NotificationProxyClientHandle *client, + uint64_t interval, + char **name_out); + +/** + * Frees a string returned by notification_proxy_receive + * + * # Safety + * `s` must be a valid pointer returned from `notification_proxy_receive` + */ +void notification_proxy_free_string(char *s); + +/** + * Frees a handle + * + * # Arguments + * * [`handle`] - The handle to free + * + * # Safety + * `handle` must be a valid pointer to the handle that was allocated by this library, + * or NULL (in which case this function does nothing) + */ +void notification_proxy_client_free(struct NotificationProxyClientHandle *handle); + /** * Connects to the relay with the given provider * @@ -3444,6 +3982,65 @@ void rsd_free_services(struct CRsdServiceArray *services); */ void rsd_handshake_free(struct RsdHandshakeHandle *handle); +/** + * Connects to screenshotr service using provider + * + * # Arguments + * * [`provider`] - An IdeviceProvider + * * [`client`] - On success, will be set to point to a newly allocated ScreenshotrClient handle + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `provider` must be a valid pointer to a handle allocated by this library + * `client` must be a valid, non-null pointer to a location where the handle will be stored + */ +struct IdeviceFfiError *screenshotr_connect(struct IdeviceProviderHandle *provider, + struct ScreenshotrClientHandle **client); + +/** + * Takes a screenshot from the device + * + * # Arguments + * * `client` - A valid ScreenshotrClient handle + * * `screenshot` - Pointer to store the screenshot data + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + * `screenshot` must be a valid pointer to store the screenshot data + * The caller is responsible for freeing the screenshot data using screenshotr_screenshot_free + */ +struct IdeviceFfiError *screenshotr_take_screenshot(struct ScreenshotrClientHandle *client, + struct ScreenshotData *screenshot); + +/** + * Frees screenshot data + * + * # Arguments + * * `screenshot` - The screenshot data to free + * + * # Safety + * `screenshot` must be a valid ScreenshotData that was allocated by screenshotr_take_screenshot + * or NULL (in which case this function does nothing) + */ +void screenshotr_screenshot_free(struct ScreenshotData screenshot); + +/** + * Frees a ScreenshotrClient handle + * + * # Arguments + * * [`handle`] - The handle to free + * + * # Safety + * `handle` must be a valid pointer to the handle that was allocated by this library, + * or NULL (in which case this function does nothing) + */ +void screenshotr_client_free(struct ScreenshotrClientHandle *handle); + /** * Connects to the Springboard service using a provider * @@ -3499,6 +4096,78 @@ struct IdeviceFfiError *springboard_services_get_icon(struct SpringBoardServices void **out_result, size_t *out_result_len); +/** + * Gets the home screen wallpaper preview as PNG image + * + * # Arguments + * * `client` - A valid SpringBoardServicesClient handle + * * `out_result` - On success, will be set to point to newly allocated png image + * * `out_result_len` - On success, will contain the size of the data in bytes + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + * `out_result` and `out_result_len` must be valid, non-null pointers + */ +struct IdeviceFfiError *springboard_services_get_home_screen_wallpaper_preview(struct SpringBoardServicesClientHandle *client, + void **out_result, + size_t *out_result_len); + +/** + * Gets the lock screen wallpaper preview as PNG image + * + * # Arguments + * * `client` - A valid SpringBoardServicesClient handle + * * `out_result` - On success, will be set to point to newly allocated png image + * * `out_result_len` - On success, will contain the size of the data in bytes + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + * `out_result` and `out_result_len` must be valid, non-null pointers + */ +struct IdeviceFfiError *springboard_services_get_lock_screen_wallpaper_preview(struct SpringBoardServicesClientHandle *client, + void **out_result, + size_t *out_result_len); + +/** + * Gets the current interface orientation of the device + * + * # Arguments + * * `client` - A valid SpringBoardServicesClient handle + * * `out_orientation` - On success, will contain the orientation value (0-4) + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + * `out_orientation` must be a valid, non-null pointer + */ +struct IdeviceFfiError *springboard_services_get_interface_orientation(struct SpringBoardServicesClientHandle *client, + uint8_t *out_orientation); + +/** + * Gets the home screen icon layout metrics + * + * # Arguments + * * `client` - A valid SpringBoardServicesClient handle + * * `res` - On success, will point to a plist dictionary node containing the metrics + * + * # Returns + * An IdeviceFfiError on error, null on success + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + * `res` must be a valid, non-null pointer + */ +struct IdeviceFfiError *springboard_services_get_homescreen_icon_metrics(struct SpringBoardServicesClientHandle *client, + plist_t *res); + /** * Frees an SpringBoardServicesClient handle * @@ -4079,6 +4748,8 @@ extern "C" PLIST_ERR_PARSE = -3, /**< parsing of the input format failed */ PLIST_ERR_NO_MEM = -4, /**< not enough memory to handle the operation */ PLIST_ERR_IO = -5, /**< I/O error */ + PLIST_ERR_CIRCULAR_REF = -6, /**< circular reference detected */ + PLIST_ERR_MAX_NESTING = -7, /**< maximum nesting depth exceeded */ PLIST_ERR_UNKNOWN = -255 /**< an unspecified error occurred */ } plist_err_t; @@ -4333,6 +5004,12 @@ extern "C" */ PLIST_API void plist_array_next_item(plist_t node, plist_array_iter iter, plist_t *item); + /** + * Free #PLIST_ARRAY iterator. + * + * @param iter Iterator to free. + */ + PLIST_API void plist_array_free_iter(plist_array_iter iter); /******************************************** * * @@ -4370,6 +5047,13 @@ extern "C" */ PLIST_API void plist_dict_next_item(plist_t node, plist_dict_iter iter, char **key, plist_t *val); + /** + * Free #PLIST_DICT iterator. + * + * @param iter Iterator to free. + */ + PLIST_API void plist_dict_free_iter(plist_dict_iter iter); + /** * Get key associated key to an item. Item must be member of a dictionary. * diff --git a/StikJIT/idevice/libidevice_ffi.a b/StikJIT/idevice/libidevice_ffi.a index 86e7231..81a63a3 100644 Binary files a/StikJIT/idevice/libidevice_ffi.a and b/StikJIT/idevice/libidevice_ffi.a differ