Profiles tab rework, now App Expiry tab, update idevice

This commit is contained in:
Huge_Black 2026-02-22 14:33:30 +08:00
parent e9d9f3578e
commit 16f6615e66
7 changed files with 1192 additions and 155 deletions

View file

@ -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 {

View file

@ -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<String> = []
@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<String> = []
// 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<String>()
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 = [:]
}
}

View file

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

View file

@ -67,6 +67,7 @@ typedef void (^SyslogErrorHandler)(NSError *error);
- (NSDictionary<NSString*, NSString*>*)getAppListWithError:(NSError**)error;
- (NSDictionary<NSString*, NSString*>*)getAllAppsWithError:(NSError**)error;
- (NSDictionary<NSString*, NSString*>*)getHiddenSystemAppsWithError:(NSError**)error;
- (NSArray<NSDictionary*>*)getSideloadedAppsWithError:(NSError**)error;
@end
@interface JITEnableContext(Syslog)

View file

@ -149,6 +149,65 @@ static NSDictionary<NSString*, NSString*> *buildAppDictionary(void *apps,
return result;
}
static NSArray<NSDictionary*>* 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<NSDictionary*>* 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<NSString*, NSString*> *performAppQuery(IdeviceProviderHandle *provider,
BOOL requireGetTaskAllow,
NSString **error,
@ -265,6 +324,21 @@ UIImage* getAppIcon(IdeviceProviderHandle* provider, NSString* bundleID, NSStrin
return apps;
}
- (NSArray<NSDictionary*>*)getSideloadedAppsWithError:(NSError**)error {
[self ensureHeartbeatWithError:error];
if(*error) {
return nil;
}
NSString* errorStr = nil;
NSArray<NSDictionary*>* 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) {

View file

@ -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.
*

Binary file not shown.