mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Verifies downloaded app’s permissions match source
Renames source JSON permissions key to “appPermissions” in order to preserve backwards compatibility, since we’ve changed the schema for permissions.
This commit is contained in:
@@ -179,10 +179,10 @@
|
|||||||
BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */; };
|
BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */; };
|
||||||
BF66EE862501AE50007EE018 /* AltStoreCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
BF66EE862501AE50007EE018 /* AltStoreCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
BF66EE8C2501AEB2007EE018 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE8B2501AEB1007EE018 /* Keychain.swift */; };
|
BF66EE8C2501AEB2007EE018 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE8B2501AEB1007EE018 /* Keychain.swift */; };
|
||||||
BF66EE942501AEBC007EE018 /* ALTAppPermission.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE8E2501AEBC007EE018 /* ALTAppPermission.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
BF66EE942501AEBC007EE018 /* ALTAppPermissions.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
BF66EE952501AEBC007EE018 /* ALTSourceUserInfoKey.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
BF66EE952501AEBC007EE018 /* ALTSourceUserInfoKey.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
BF66EE962501AEBC007EE018 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */; };
|
BF66EE962501AEBC007EE018 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */; };
|
||||||
BF66EE972501AEBC007EE018 /* ALTAppPermission.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE912501AEBC007EE018 /* ALTAppPermission.m */; };
|
BF66EE972501AEBC007EE018 /* ALTAppPermissions.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */; };
|
||||||
BF66EE982501AEBC007EE018 /* ALTPatreonBenefitType.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
BF66EE982501AEBC007EE018 /* ALTPatreonBenefitType.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
BF66EE992501AEBC007EE018 /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE932501AEBC007EE018 /* ALTSourceUserInfoKey.m */; };
|
BF66EE992501AEBC007EE018 /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE932501AEBC007EE018 /* ALTSourceUserInfoKey.m */; };
|
||||||
BF66EE9D2501AEC1007EE018 /* AppProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */; };
|
BF66EE9D2501AEC1007EE018 /* AppProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */; };
|
||||||
@@ -357,6 +357,7 @@
|
|||||||
D561B2ED28EF5A4F006752E4 /* AltSign-Dynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
D561B2ED28EF5A4F006752E4 /* AltSign-Dynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
D5708417292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
|
D5708417292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
|
||||||
D570841A2924680D00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
|
D570841A2924680D00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
|
||||||
|
D571ADD02A02FC7200B24B63 /* ALTAppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */; };
|
||||||
D5728CA72A0D79D30014E73C /* OptionalProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */; };
|
D5728CA72A0D79D30014E73C /* OptionalProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */; };
|
||||||
D57968CB29CB99EF00539069 /* VibrantButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57968CA29CB99EF00539069 /* VibrantButton.swift */; };
|
D57968CB29CB99EF00539069 /* VibrantButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57968CA29CB99EF00539069 /* VibrantButton.swift */; };
|
||||||
D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; };
|
D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; };
|
||||||
@@ -749,10 +750,10 @@
|
|||||||
BF66EE802501AE50007EE018 /* AltStoreCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AltStoreCore.h; sourceTree = "<group>"; };
|
BF66EE802501AE50007EE018 /* AltStoreCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AltStoreCore.h; sourceTree = "<group>"; };
|
||||||
BF66EE812501AE50007EE018 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
BF66EE812501AE50007EE018 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
BF66EE8B2501AEB1007EE018 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
|
BF66EE8B2501AEB1007EE018 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
|
||||||
BF66EE8E2501AEBC007EE018 /* ALTAppPermission.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTAppPermission.h; sourceTree = "<group>"; };
|
BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTAppPermissions.h; sourceTree = "<group>"; };
|
||||||
BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTSourceUserInfoKey.h; sourceTree = "<group>"; };
|
BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTSourceUserInfoKey.h; sourceTree = "<group>"; };
|
||||||
BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTPatreonBenefitType.m; sourceTree = "<group>"; };
|
BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTPatreonBenefitType.m; sourceTree = "<group>"; };
|
||||||
BF66EE912501AEBC007EE018 /* ALTAppPermission.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermission.m; sourceTree = "<group>"; };
|
BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermissions.m; sourceTree = "<group>"; };
|
||||||
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTPatreonBenefitType.h; sourceTree = "<group>"; };
|
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTPatreonBenefitType.h; sourceTree = "<group>"; };
|
||||||
BF66EE932501AEBC007EE018 /* ALTSourceUserInfoKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTSourceUserInfoKey.m; sourceTree = "<group>"; };
|
BF66EE932501AEBC007EE018 /* ALTSourceUserInfoKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTSourceUserInfoKey.m; sourceTree = "<group>"; };
|
||||||
BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppProtocol.swift; sourceTree = "<group>"; };
|
BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppProtocol.swift; sourceTree = "<group>"; };
|
||||||
@@ -914,6 +915,7 @@
|
|||||||
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = "<group>"; };
|
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D55E163528776CB000A627A1 /* ComplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationView.swift; sourceTree = "<group>"; };
|
D55E163528776CB000A627A1 /* ComplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationView.swift; sourceTree = "<group>"; };
|
||||||
D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = "<group>"; };
|
D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = "<group>"; };
|
||||||
|
D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTAppPermission.swift; sourceTree = "<group>"; };
|
||||||
D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalProtocol.swift; sourceTree = "<group>"; };
|
D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalProtocol.swift; sourceTree = "<group>"; };
|
||||||
D57968CA29CB99EF00539069 /* VibrantButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantButton.swift; sourceTree = "<group>"; };
|
D57968CA29CB99EF00539069 /* VibrantButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantButton.swift; sourceTree = "<group>"; };
|
||||||
D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = "<group>"; };
|
D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = "<group>"; };
|
||||||
@@ -1428,8 +1430,8 @@
|
|||||||
children = (
|
children = (
|
||||||
BFB39B5B252BC10E00D1BE50 /* Managed.swift */,
|
BFB39B5B252BC10E00D1BE50 /* Managed.swift */,
|
||||||
D5F48B4929CD0B67002B52A4 /* AsyncManaged.swift */,
|
D5F48B4929CD0B67002B52A4 /* AsyncManaged.swift */,
|
||||||
BF66EE8E2501AEBC007EE018 /* ALTAppPermission.h */,
|
BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */,
|
||||||
BF66EE912501AEBC007EE018 /* ALTAppPermission.m */,
|
BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */,
|
||||||
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */,
|
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */,
|
||||||
BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */,
|
BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */,
|
||||||
BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */,
|
BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */,
|
||||||
@@ -1444,6 +1446,7 @@
|
|||||||
BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */,
|
BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */,
|
||||||
BF66EE9C2501AEC1007EE018 /* Fetchable.swift */,
|
BF66EE9C2501AEC1007EE018 /* Fetchable.swift */,
|
||||||
D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */,
|
D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */,
|
||||||
|
D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */,
|
||||||
);
|
);
|
||||||
path = Protocols;
|
path = Protocols;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1998,7 +2001,7 @@
|
|||||||
BF66EE982501AEBC007EE018 /* ALTPatreonBenefitType.h in Headers */,
|
BF66EE982501AEBC007EE018 /* ALTPatreonBenefitType.h in Headers */,
|
||||||
BFAECC5F2501B0BF00528F27 /* ALTConstants.h in Headers */,
|
BFAECC5F2501B0BF00528F27 /* ALTConstants.h in Headers */,
|
||||||
BFAECC5D2501B0BF00528F27 /* ALTConnection.h in Headers */,
|
BFAECC5D2501B0BF00528F27 /* ALTConnection.h in Headers */,
|
||||||
BF66EE942501AEBC007EE018 /* ALTAppPermission.h in Headers */,
|
BF66EE942501AEBC007EE018 /* ALTAppPermissions.h in Headers */,
|
||||||
BFAECC602501B0BF00528F27 /* NSError+ALTServerError.h in Headers */,
|
BFAECC602501B0BF00528F27 /* NSError+ALTServerError.h in Headers */,
|
||||||
0EE7FDC82BE8CF4800D1E390 /* ALTWrappedError.h in Headers */,
|
0EE7FDC82BE8CF4800D1E390 /* ALTWrappedError.h in Headers */,
|
||||||
BFAECC5E2501B0BF00528F27 /* CFNotificationName+AltStore.h in Headers */,
|
BFAECC5E2501B0BF00528F27 /* CFNotificationName+AltStore.h in Headers */,
|
||||||
@@ -2618,7 +2621,7 @@
|
|||||||
0E05025A2BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift in Sources */,
|
0E05025A2BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift in Sources */,
|
||||||
BF66EE8C2501AEB2007EE018 /* Keychain.swift in Sources */,
|
BF66EE8C2501AEB2007EE018 /* Keychain.swift in Sources */,
|
||||||
BF66EED42501AECA007EE018 /* AltStore5ToAltStore6.xcmappingmodel in Sources */,
|
BF66EED42501AECA007EE018 /* AltStore5ToAltStore6.xcmappingmodel in Sources */,
|
||||||
BF66EE972501AEBC007EE018 /* ALTAppPermission.m in Sources */,
|
BF66EE972501AEBC007EE018 /* ALTAppPermissions.m in Sources */,
|
||||||
BFAECC552501B0A400528F27 /* Connection.swift in Sources */,
|
BFAECC552501B0A400528F27 /* Connection.swift in Sources */,
|
||||||
BF66EEDA2501AECA007EE018 /* RefreshAttempt.swift in Sources */,
|
BF66EEDA2501AECA007EE018 /* RefreshAttempt.swift in Sources */,
|
||||||
0E05025C2BEC947000879B5C /* String+SideStore.swift in Sources */,
|
0E05025C2BEC947000879B5C /* String+SideStore.swift in Sources */,
|
||||||
@@ -2654,6 +2657,7 @@
|
|||||||
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */,
|
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */,
|
||||||
BFAECC562501B0A400528F27 /* ALTServerError+Conveniences.swift in Sources */,
|
BFAECC562501B0A400528F27 /* ALTServerError+Conveniences.swift in Sources */,
|
||||||
BFAECC592501B0A400528F27 /* Result+Conveniences.swift in Sources */,
|
BFAECC592501B0A400528F27 /* Result+Conveniences.swift in Sources */,
|
||||||
|
D571ADD02A02FC7200B24B63 /* ALTAppPermission.swift in Sources */,
|
||||||
D5E3FB9828FDFAD90034B72C /* NSError+AltStore.swift in Sources */,
|
D5E3FB9828FDFAD90034B72C /* NSError+AltStore.swift in Sources */,
|
||||||
BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */,
|
BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */,
|
||||||
BF66EEE12501AECA007EE018 /* DatabaseManager.swift in Sources */,
|
BF66EEE12501AECA007EE018 /* DatabaseManager.swift in Sources */,
|
||||||
|
|||||||
@@ -177,12 +177,17 @@ private extension AppContentViewController
|
|||||||
|
|
||||||
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
|
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
|
||||||
{
|
{
|
||||||
let dataSource = RSTArrayCollectionViewDataSource(items: self.app.permissions)
|
let dataSource = RSTArrayCollectionViewDataSource(items: Array(self.app.permissions))
|
||||||
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
|
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
|
||||||
let cell = cell as! PermissionCollectionViewCell
|
let cell = cell as! PermissionCollectionViewCell
|
||||||
cell.button.setImage(permission.type.icon, for: .normal)
|
// cell.button.setImage(permission.type.icon, for: .normal)
|
||||||
cell.button.tintColor = .label
|
// cell.button.tintColor = .label
|
||||||
cell.textLabel.text = permission.type.localizedShortName ?? permission.type.localizedName
|
// cell.textLabel.text = permission.type.localizedShortName ?? permission.type.localizedName
|
||||||
|
|
||||||
|
let icon = UIImage(systemName: permission.symbolName ?? "lock")
|
||||||
|
cell.button.setImage(icon, for: .normal)
|
||||||
|
|
||||||
|
cell.textLabel.text = permission.localizedDisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataSource
|
return dataSource
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ final class PermissionPopoverViewController: UIViewController
|
|||||||
{
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
self.nameLabel.text = self.permission.type.localizedName
|
self.nameLabel.text = self.permission.localizedName ?? self.permission.permission.rawValue
|
||||||
self.descriptionLabel.text = self.permission.usageDescription
|
self.descriptionLabel.text = self.permission.usageDescription
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ extension VerificationError
|
|||||||
|
|
||||||
case mismatchedHash = 3
|
case mismatchedHash = 3
|
||||||
case mismatchedVersion = 4
|
case mismatchedVersion = 4
|
||||||
|
|
||||||
|
case undeclaredPermissions = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
|
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
|
||||||
@@ -40,6 +42,10 @@ extension VerificationError
|
|||||||
static func mismatchedVersion(_ version: String, expectedVersion: String, app: AppProtocol) -> VerificationError {
|
static func mismatchedVersion(_ version: String, expectedVersion: String, app: AppProtocol) -> VerificationError {
|
||||||
VerificationError(code: .mismatchedVersion, app: app, version: version, expectedVersion: expectedVersion)
|
VerificationError(code: .mismatchedVersion, app: app, version: version, expectedVersion: expectedVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func undeclaredPermissions(_ permissions: [any ALTAppPermission], app: AppProtocol) -> VerificationError {
|
||||||
|
VerificationError(code: .undeclaredPermissions, app: app, permissions: permissions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VerificationError: ALTLocalizedError
|
struct VerificationError: ALTLocalizedError
|
||||||
@@ -60,6 +66,9 @@ struct VerificationError: ALTLocalizedError
|
|||||||
@UserInfoValue var version: String?
|
@UserInfoValue var version: String?
|
||||||
@UserInfoValue var expectedVersion: String?
|
@UserInfoValue var expectedVersion: String?
|
||||||
|
|
||||||
|
@UserInfoValue
|
||||||
|
var permissions: [any ALTAppPermission]?
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
//TODO: Make this automatic somehow with ALTLocalizedError
|
//TODO: Make this automatic somehow with ALTLocalizedError
|
||||||
guard self.errorFailure == nil else { return nil }
|
guard self.errorFailure == nil else { return nil }
|
||||||
@@ -129,6 +138,52 @@ struct VerificationError: ALTLocalizedError
|
|||||||
case .mismatchedVersion:
|
case .mismatchedVersion:
|
||||||
let appName = self.$app.name ?? NSLocalizedString("the app", comment: "")
|
let appName = self.$app.name ?? NSLocalizedString("the app", comment: "")
|
||||||
return String(format: NSLocalizedString("The downloaded version of %@ does not match the version specified by the source.", comment: ""), appName)
|
return String(format: NSLocalizedString("The downloaded version of %@ does not match the version specified by the source.", comment: ""), appName)
|
||||||
|
|
||||||
|
case .undeclaredPermissions:
|
||||||
|
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
||||||
|
return String(format: NSLocalizedString("%@ requires additional permissions not specified by the source.", comment: ""), appName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var recoverySuggestion: String? {
|
||||||
|
switch self.code
|
||||||
|
{
|
||||||
|
case .undeclaredPermissions:
|
||||||
|
guard let permissions, !permissions.isEmpty else { return nil }
|
||||||
|
|
||||||
|
let baseMessage = NSLocalizedString("These permissions must be declared by the source in order for AltStore to install this app:", comment: "")
|
||||||
|
|
||||||
|
let permissionsByType = Dictionary(grouping: permissions) { $0.type }
|
||||||
|
let permissionSections = [ALTAppPermissionType.entitlement, .privacy, .backgroundMode].compactMap { (type) -> String? in
|
||||||
|
guard let permissions = permissionsByType[type] else { return nil }
|
||||||
|
|
||||||
|
// "Privacy:"
|
||||||
|
var sectionText = "\(type.localizedName ?? type.rawValue):\n"
|
||||||
|
|
||||||
|
// Sort permissions + join into single string.
|
||||||
|
let sortedList = permissions.map { permission -> String in
|
||||||
|
if let localizedName = permission.localizedName
|
||||||
|
{
|
||||||
|
// "Entitlement Name (com.apple.entitlement.name)"
|
||||||
|
return "\(localizedName) (\(permission.rawValue))"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// "com.apple.entitlement.name"
|
||||||
|
return permission.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sorted { $0.localizedStandardCompare($1) == .orderedAscending } // Case-insensitive sorting
|
||||||
|
.joined(separator: "\n")
|
||||||
|
|
||||||
|
sectionText += sortedList
|
||||||
|
return sectionText
|
||||||
|
}
|
||||||
|
|
||||||
|
let recoverySuggestion = ([baseMessage] + permissionSections).joined(separator: "\n\n")
|
||||||
|
return recoverySuggestion
|
||||||
|
|
||||||
|
default: return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,16 @@ struct VerificationError: ALTLocalizedError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import RegexBuilder
|
||||||
|
|
||||||
|
private extension ALTEntitlement
|
||||||
|
{
|
||||||
|
static var ignoredEntitlements: Set<ALTEntitlement> = [
|
||||||
|
.applicationIdentifier,
|
||||||
|
.teamIdentifier
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@objc(VerifyAppOperation)
|
@objc(VerifyAppOperation)
|
||||||
final class VerifyAppOperation: ResultOperation<Void>
|
final class VerifyAppOperation: ResultOperation<Void>
|
||||||
{
|
{
|
||||||
@@ -146,6 +156,11 @@ final class VerifyAppOperation: ResultOperation<Void>
|
|||||||
try await self.verifyHash(of: app, at: ipaURL, matches: appVersion)
|
try await self.verifyHash(of: app, at: ipaURL, matches: appVersion)
|
||||||
try await self.verifyDownloadedVersion(of: app, matches: appVersion)
|
try await self.verifyDownloadedVersion(of: app, matches: appVersion)
|
||||||
|
|
||||||
|
if let storeApp = await self.context.$appVersion.app
|
||||||
|
{
|
||||||
|
try await self.verifyPermissions(of: app, match: storeApp)
|
||||||
|
}
|
||||||
|
|
||||||
self.finish(.success(()))
|
self.finish(.success(()))
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -183,4 +198,81 @@ private extension VerifyAppOperation
|
|||||||
|
|
||||||
guard version == app.version else { throw VerificationError.mismatchedVersion(app.version, expectedVersion: version, app: app) }
|
guard version == app.version else { throw VerificationError.mismatchedVersion(app.version, expectedVersion: version, app: app) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func verifyPermissions(of app: ALTApplication, @AsyncManaged match storeApp: StoreApp) async throws -> [any ALTAppPermission]
|
||||||
|
{
|
||||||
|
// Entitlements
|
||||||
|
var allEntitlements = Set(app.entitlements.keys)
|
||||||
|
for appExtension in app.appExtensions
|
||||||
|
{
|
||||||
|
allEntitlements.formUnion(appExtension.entitlements.keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out ignored entitlements.
|
||||||
|
allEntitlements = allEntitlements.filter { !ALTEntitlement.ignoredEntitlements.contains($0) }
|
||||||
|
|
||||||
|
|
||||||
|
// Background Modes
|
||||||
|
// App extensions can't have background modes, so don't need to worry about them.
|
||||||
|
let allBackgroundModes: Set<ALTAppBackgroundMode>
|
||||||
|
if let backgroundModes = app.bundle.infoDictionary?[Bundle.Info.backgroundModes] as? [String]
|
||||||
|
{
|
||||||
|
let backgroundModes = backgroundModes.lazy.map { ALTAppBackgroundMode($0) }
|
||||||
|
allBackgroundModes = Set(backgroundModes)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
allBackgroundModes = []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Privacy
|
||||||
|
let allPrivacyPermissions: Set<ALTAppPrivacyPermission>
|
||||||
|
if #available(iOS 16, *)
|
||||||
|
{
|
||||||
|
let regex = Regex {
|
||||||
|
"NS"
|
||||||
|
|
||||||
|
// Capture permission "name"
|
||||||
|
Capture {
|
||||||
|
OneOrMore(.anyGraphemeCluster)
|
||||||
|
}
|
||||||
|
|
||||||
|
"UsageDescription"
|
||||||
|
|
||||||
|
// Optional suffix
|
||||||
|
Optionally(OneOrMore(.anyGraphemeCluster))
|
||||||
|
}
|
||||||
|
|
||||||
|
let privacyPermissions = ([app] + app.appExtensions).flatMap { (app) in
|
||||||
|
let permissions = app.bundle.infoDictionary?.keys.compactMap { key -> ALTAppPrivacyPermission? in
|
||||||
|
guard let match = key.wholeMatch(of: regex) else { return nil }
|
||||||
|
|
||||||
|
let permission = ALTAppPrivacyPermission(rawValue: String(match.1))
|
||||||
|
return permission
|
||||||
|
} ?? []
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
allPrivacyPermissions = Set(privacyPermissions)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
allPrivacyPermissions = []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Verify permissions.
|
||||||
|
let sourcePermissions: Set<AnyHashable> = Set(await $storeApp.perform { $0.permissions.map { AnyHashable($0.permission) } })
|
||||||
|
let localPermissions: [any ALTAppPermission] = Array(allEntitlements) + Array(allBackgroundModes) + Array(allPrivacyPermissions)
|
||||||
|
|
||||||
|
// To pass: EVERY permission in localPermissions must also appear in sourcePermissions.
|
||||||
|
// If there is a single missing permission, throw error.
|
||||||
|
let missingPermissions: [any ALTAppPermission] = localPermissions.filter { !sourcePermissions.contains(AnyHashable($0)) }
|
||||||
|
guard missingPermissions.isEmpty else { throw VerificationError.undeclaredPermissions(missingPermissions, app: app) }
|
||||||
|
|
||||||
|
return localPermissions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[];
|
|||||||
|
|
||||||
// In this header, you should import all the public headers of your framework using statements like #import <AltStoreCore/PublicHeader.h>
|
// In this header, you should import all the public headers of your framework using statements like #import <AltStoreCore/PublicHeader.h>
|
||||||
|
|
||||||
#import <AltStoreCore/ALTAppPermission.h>
|
#import <AltStoreCore/ALTAppPermissions.h>
|
||||||
#import <AltStoreCore/ALTSourceUserInfoKey.h>
|
#import <AltStoreCore/ALTSourceUserInfoKey.h>
|
||||||
#import <AltStoreCore/ALTPatreonBenefitType.h>
|
#import <AltStoreCore/ALTPatreonBenefitType.h>
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,18 @@
|
|||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||||
|
<attribute name="appBundleID" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="permission" attributeType="String"/>
|
||||||
<attribute name="type" attributeType="String"/>
|
<attribute name="type" attributeType="String"/>
|
||||||
<attribute name="usageDescription" attributeType="String"/>
|
<attribute name="usageDescription" optional="YES" attributeType="String"/>
|
||||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="appBundleID"/>
|
||||||
|
<constraint value="permission"/>
|
||||||
|
<constraint value="type"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
|
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
|
||||||
<attribute name="appBundleID" attributeType="String"/>
|
<attribute name="appBundleID" attributeType="String"/>
|
||||||
@@ -191,7 +200,7 @@
|
|||||||
<relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/>
|
<relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/>
|
||||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
|
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
|
||||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
<relationship name="permissions" toMany="YES" deletionRule="Cascade" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||||
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
|
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
|
||||||
<uniquenessConstraints>
|
<uniquenessConstraints>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import CoreData
|
import CoreData
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
import AltSign
|
||||||
public extension ALTAppPermissionType
|
public extension ALTAppPermissionType
|
||||||
{
|
{
|
||||||
var localizedShortName: String? {
|
var localizedShortName: String? {
|
||||||
@@ -71,15 +72,29 @@ public extension ALTAppPermissionType
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(AppPermission)
|
@objc(AppPermission) @dynamicMemberLookup
|
||||||
public class AppPermission: NSManagedObject, Decodable, Fetchable
|
public class AppPermission: NSManagedObject, Decodable, Fetchable
|
||||||
{
|
{
|
||||||
/* Properties */
|
/* Properties */
|
||||||
@NSManaged public var type: ALTAppPermissionType
|
@NSManaged public var type: ALTAppPermissionType
|
||||||
@NSManaged public var usageDescription: String
|
@NSManaged public var usageDescription: String?
|
||||||
|
|
||||||
|
@nonobjc public var permission: any ALTAppPermission {
|
||||||
|
switch self.type
|
||||||
|
{
|
||||||
|
case .entitlement: return ALTEntitlement(rawValue: self._permission)
|
||||||
|
case .privacy: return ALTAppPrivacyPermission(rawValue: self._permission)
|
||||||
|
case .backgroundMode: return ALTAppBackgroundMode(rawValue: self._permission)
|
||||||
|
default: return UnknownAppPermission(rawValue: self._permission)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@NSManaged @objc(permission) private var _permission: String
|
||||||
|
|
||||||
|
// Set by StoreApp.
|
||||||
|
@NSManaged public var appBundleID: String?
|
||||||
|
|
||||||
/* Relationships */
|
/* Relationships */
|
||||||
@NSManaged public private(set) var app: StoreApp!
|
@NSManaged public internal(set) var app: StoreApp?
|
||||||
|
|
||||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
{
|
{
|
||||||
@@ -88,7 +103,10 @@ public class AppPermission: NSManagedObject, Decodable, Fetchable
|
|||||||
|
|
||||||
private enum CodingKeys: String, CodingKey
|
private enum CodingKeys: String, CodingKey
|
||||||
{
|
{
|
||||||
case type
|
case entitlement
|
||||||
|
case privacyType = "privacy"
|
||||||
|
case backgroundMode = "background"
|
||||||
|
|
||||||
case usageDescription
|
case usageDescription
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,10 +119,33 @@ public class AppPermission: NSManagedObject, Decodable, Fetchable
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
self.usageDescription = try container.decode(String.self, forKey: .usageDescription)
|
|
||||||
|
|
||||||
let rawType = try container.decode(String.self, forKey: .type)
|
self.usageDescription = try container.decodeIfPresent(String.self, forKey: .usageDescription)
|
||||||
self.type = ALTAppPermissionType(rawValue: rawType)
|
|
||||||
|
if let entitlement = try container.decodeIfPresent(String.self, forKey: .entitlement)
|
||||||
|
{
|
||||||
|
self._permission = entitlement
|
||||||
|
self.type = .entitlement
|
||||||
|
}
|
||||||
|
else if let privacyType = try container.decodeIfPresent(String.self, forKey: .privacyType)
|
||||||
|
{
|
||||||
|
self._permission = privacyType
|
||||||
|
self.type = .privacy
|
||||||
|
}
|
||||||
|
else if let backgroundMode = try container.decodeIfPresent(String.self, forKey: .backgroundMode)
|
||||||
|
{
|
||||||
|
self._permission = backgroundMode
|
||||||
|
self.type = .backgroundMode
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self._permission = ""
|
||||||
|
self.type = .unknown
|
||||||
|
|
||||||
|
// We don't want to save any unknown permissions, but can't throw error
|
||||||
|
// without making the entire decoding fail, so just delete self instead.
|
||||||
|
context.delete(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -125,3 +166,14 @@ public extension AppPermission
|
|||||||
return NSFetchRequest<AppPermission>(entityName: "AppPermission")
|
return NSFetchRequest<AppPermission>(entityName: "AppPermission")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @dynamicMemberLookup
|
||||||
|
public extension AppPermission
|
||||||
|
{
|
||||||
|
// Convenience for accessing .permission properties.
|
||||||
|
subscript<T>(dynamicMember keyPath: KeyPath<any ALTAppPermission, T>) -> T {
|
||||||
|
get {
|
||||||
|
return self.permission[keyPath: keyPath]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ extension MergeError
|
|||||||
|
|
||||||
case noVersions
|
case noVersions
|
||||||
case incorrectVersionOrder
|
case incorrectVersionOrder
|
||||||
|
case incorrectPermissions
|
||||||
}
|
}
|
||||||
|
|
||||||
static func noVersions(for app: StoreApp) -> MergeError { .init(code: .noVersions, appName: app.name, appBundleID: app.bundleIdentifier, sourceID: app.sourceIdentifier) }
|
static func noVersions(for app: StoreApp) -> MergeError { .init(code: .noVersions, appName: app.name, appBundleID: app.bundleIdentifier, sourceID: app.sourceIdentifier) }
|
||||||
static func incorrectVersionOrder(for app: StoreApp) -> MergeError { .init(code: .incorrectVersionOrder, appName: app.name, appBundleID: app.bundleIdentifier, sourceID: app.sourceIdentifier) }
|
static func incorrectVersionOrder(for app: StoreApp) -> MergeError { .init(code: .incorrectVersionOrder, appName: app.name, appBundleID: app.bundleIdentifier, sourceID: app.sourceIdentifier) }
|
||||||
|
static func incorrectPermissions(for app: StoreApp) -> MergeError { .init(code: .incorrectPermissions, appName: app.name, appBundleID: app.bundleIdentifier, sourceID: app.sourceIdentifier) }
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct MergeError: ALTLocalizedError
|
public struct MergeError: ALTLocalizedError
|
||||||
@@ -57,6 +59,15 @@ public struct MergeError: ALTLocalizedError
|
|||||||
}
|
}
|
||||||
|
|
||||||
return String(format: NSLocalizedString("The cached versions for %@ do not match the source.", comment: ""), appName)
|
return String(format: NSLocalizedString("The cached versions for %@ do not match the source.", comment: ""), appName)
|
||||||
|
|
||||||
|
case .incorrectPermissions:
|
||||||
|
var appName = NSLocalizedString("one or more apps", comment: "")
|
||||||
|
if let name = self.appName, let bundleID = self.appBundleID
|
||||||
|
{
|
||||||
|
appName = name + " (\(bundleID))"
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(format: NSLocalizedString("The cached permissions for %@ do not match the source.", comment: ""), appName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,8 +145,8 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|||||||
|
|
||||||
if let previousApp = conflict.conflictingObjects.first(where: { !$0.isInserted }) as? StoreApp
|
if let previousApp = conflict.conflictingObjects.first(where: { !$0.isInserted }) as? StoreApp
|
||||||
{
|
{
|
||||||
// Delete previous permissions (same as below).
|
// Delete previous permissions (different than below).
|
||||||
for permission in previousApp.permissions
|
for case let permission as AppPermission in previousApp._permissions where permission.app == nil
|
||||||
{
|
{
|
||||||
permission.managedObjectContext?.delete(permission)
|
permission.managedObjectContext?.delete(permission)
|
||||||
}
|
}
|
||||||
@@ -171,6 +182,8 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sortedVersionsByGlobalAppID = [String: NSOrderedSet]()
|
var sortedVersionsByGlobalAppID = [String: NSOrderedSet]()
|
||||||
|
var permissionsByGlobalAppID = [String: Set<AnyHashable>]()
|
||||||
|
|
||||||
var featuredAppIDsBySourceID = [String: [String]]()
|
var featuredAppIDsBySourceID = [String: [String]]()
|
||||||
|
|
||||||
for conflict in conflicts
|
for conflict in conflicts
|
||||||
@@ -178,28 +191,28 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|||||||
switch conflict.databaseObject
|
switch conflict.databaseObject
|
||||||
{
|
{
|
||||||
case let databaseObject as StoreApp:
|
case let databaseObject as StoreApp:
|
||||||
// Delete previous permissions
|
guard let contextApp = conflict.conflictingObjects.first as? StoreApp else { break }
|
||||||
for permission in databaseObject.permissions
|
|
||||||
|
// Permissions
|
||||||
|
let contextPermissions = Set(contextApp._permissions.lazy.compactMap { $0 as? AppPermission }.map { AnyHashable($0.permission) })
|
||||||
|
for case let databasePermission as AppPermission in databaseObject._permissions where !contextPermissions.contains(AnyHashable(databasePermission.permission))
|
||||||
{
|
{
|
||||||
permission.managedObjectContext?.delete(permission)
|
// Permission does NOT exist in context, so delete existing databasePermission.
|
||||||
|
databasePermission.managedObjectContext?.delete(databasePermission)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let contextApp = conflict.conflictingObjects.first as? StoreApp
|
// Versions
|
||||||
|
let contextVersions = NSOrderedSet(array: contextApp._versions.lazy.compactMap { $0 as? AppVersion }.map { $0.version })
|
||||||
|
for case let databaseVersion as AppVersion in databaseObject._versions where !contextVersions.contains(databaseVersion.version)
|
||||||
{
|
{
|
||||||
let contextVersions = NSOrderedSet(array: contextApp._versions.lazy.compactMap { $0 as? AppVersion }.map { $0.version })
|
// Version # does NOT exist in context, so delete existing databaseVersion.
|
||||||
|
databaseVersion.managedObjectContext?.delete(databaseVersion)
|
||||||
for case let appVersion as AppVersion in databaseObject._versions where !contextVersions.contains(appVersion.version)
|
}
|
||||||
{
|
|
||||||
// Version # does NOT exist in context, so delete existing appVersion.
|
if let globallyUniqueID = contextApp.globallyUniqueID
|
||||||
appVersion.managedObjectContext?.delete(appVersion)
|
{
|
||||||
}
|
permissionsByGlobalAppID[globallyUniqueID] = contextPermissions
|
||||||
|
sortedVersionsByGlobalAppID[globallyUniqueID] = contextVersions
|
||||||
if let globallyUniqueID = contextApp.globallyUniqueID
|
|
||||||
{
|
|
||||||
// Core Data _normally_ preserves the correct ordering of versions when merging,
|
|
||||||
// but just in case we cache the order and reorder the versions post-merge if needed.
|
|
||||||
sortedVersionsByGlobalAppID[globallyUniqueID] = contextVersions
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case let databaseObject as Source:
|
case let databaseObject as Source:
|
||||||
@@ -244,36 +257,44 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|||||||
case let databaseObject as StoreApp:
|
case let databaseObject as StoreApp:
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let appVersions: [AppVersion]
|
var appVersions = databaseObject.versions
|
||||||
|
|
||||||
if let globallyUniqueID = databaseObject.globallyUniqueID,
|
if let globallyUniqueID = databaseObject.globallyUniqueID
|
||||||
let sortedAppVersions = sortedVersionsByGlobalAppID[globallyUniqueID],
|
|
||||||
let sortedAppVersionsArray = sortedAppVersions.array as? [String],
|
|
||||||
case let databaseVersions = databaseObject.versions.map({ $0.version }),
|
|
||||||
databaseVersions != sortedAppVersionsArray
|
|
||||||
{
|
{
|
||||||
// databaseObject.versions post-merge doesn't match contextApp.versions pre-merge, so attempt to fix by re-sorting.
|
// Permissions
|
||||||
|
if let appPermissions = permissionsByGlobalAppID[globallyUniqueID],
|
||||||
let fixedAppVersions = databaseObject.versions.sorted { (versionA, versionB) in
|
case let databasePermissions = Set(databaseObject.permissions.map({ AnyHashable($0.permission) })),
|
||||||
let indexA = sortedAppVersions.index(of: versionA.version)
|
databasePermissions != appPermissions
|
||||||
let indexB = sortedAppVersions.index(of: versionB.version)
|
{
|
||||||
return indexA < indexB
|
// Sorting order doesn't matter, but elements themselves don't match so throw error.
|
||||||
|
throw MergeError.incorrectPermissions(for: databaseObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
let appVersionValues = fixedAppVersions.map { $0.version }
|
// App versions
|
||||||
guard appVersionValues == sortedAppVersionsArray else {
|
if let sortedAppVersions = sortedVersionsByGlobalAppID[globallyUniqueID],
|
||||||
// fixedAppVersions still doesn't match source's versions, so throw MergeError.
|
let sortedAppVersionsArray = sortedAppVersions.array as? [String],
|
||||||
throw MergeError.incorrectVersionOrder(for: databaseObject)
|
case let databaseVersions = databaseObject.versions.map({ $0.version }),
|
||||||
|
databaseVersions != sortedAppVersionsArray
|
||||||
|
{
|
||||||
|
// databaseObject.versions post-merge doesn't match contextApp.versions pre-merge, so attempt to fix by re-sorting.
|
||||||
|
|
||||||
|
let fixedAppVersions = databaseObject.versions.sorted { (versionA, versionB) in
|
||||||
|
let indexA = sortedAppVersions.index(of: versionA.version)
|
||||||
|
let indexB = sortedAppVersions.index(of: versionB.version)
|
||||||
|
return indexA < indexB
|
||||||
|
}
|
||||||
|
|
||||||
|
let appVersionValues = fixedAppVersions.map { $0.version }
|
||||||
|
guard appVersionValues == sortedAppVersionsArray else {
|
||||||
|
// fixedAppVersions still doesn't match source's versions, so throw MergeError.
|
||||||
|
throw MergeError.incorrectVersionOrder(for: databaseObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
appVersions = fixedAppVersions
|
||||||
}
|
}
|
||||||
|
|
||||||
appVersions = fixedAppVersions
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
appVersions = databaseObject.versions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update versions post-merging to make sure latestSupportedVersion is correct.
|
// Always update versions post-merging to make sure latestSupportedVersion is correct.
|
||||||
try databaseObject.setVersions(appVersions)
|
try databaseObject.setVersions(appVersions)
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -146,8 +146,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
|||||||
@NSManaged @objc(source) public var _source: Source?
|
@NSManaged @objc(source) public var _source: Source?
|
||||||
@NSManaged public internal(set) var featuringSource: Source?
|
@NSManaged public internal(set) var featuringSource: Source?
|
||||||
|
|
||||||
@NSManaged @objc(permissions) public var _permissions: NSOrderedSet
|
|
||||||
|
|
||||||
@NSManaged @objc(latestVersion) public private(set) var latestSupportedVersion: AppVersion?
|
@NSManaged @objc(latestVersion) public private(set) var latestSupportedVersion: AppVersion?
|
||||||
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
|
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
|
||||||
|
|
||||||
@@ -163,9 +161,10 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@nonobjc public var permissions: [AppPermission] {
|
@nonobjc public var permissions: Set<AppPermission> {
|
||||||
return self._permissions.array as! [AppPermission]
|
return self._permissions as! Set<AppPermission>
|
||||||
}
|
}
|
||||||
|
@NSManaged @objc(permissions) internal private(set) var _permissions: NSSet // Use NSSet to avoid eagerly fetching values.
|
||||||
|
|
||||||
@nonobjc public var versions: [AppVersion] {
|
@nonobjc public var versions: [AppVersion] {
|
||||||
return self._versions.array as! [AppVersion]
|
return self._versions.array as! [AppVersion]
|
||||||
@@ -216,7 +215,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
|||||||
case platformURLs
|
case platformURLs
|
||||||
case tintColor
|
case tintColor
|
||||||
case subtitle
|
case subtitle
|
||||||
case permissions
|
case permissions = "appPermissions"
|
||||||
case size
|
case size
|
||||||
case isBeta = "beta"
|
case isBeta = "beta"
|
||||||
case versions
|
case versions
|
||||||
@@ -287,7 +286,11 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
|||||||
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
||||||
|
|
||||||
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
|
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
|
||||||
self._permissions = NSOrderedSet(array: permissions)
|
for permission in permissions
|
||||||
|
{
|
||||||
|
permission.appBundleID = self.bundleIdentifier
|
||||||
|
}
|
||||||
|
self._permissions = NSSet(array: permissions)
|
||||||
|
|
||||||
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions)
|
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions)
|
||||||
{
|
{
|
||||||
@@ -367,6 +370,23 @@ internal extension StoreApp
|
|||||||
self._downloadURL = latestVersion.downloadURL
|
self._downloadURL = latestVersion.downloadURL
|
||||||
self._size = Int32(latestVersion.size)
|
self._size = Int32(latestVersion.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setPermissions(_ permissions: Set<AppPermission>)
|
||||||
|
{
|
||||||
|
for case let permission as AppPermission in self._permissions
|
||||||
|
{
|
||||||
|
if permissions.contains(permission)
|
||||||
|
{
|
||||||
|
permission.app = self
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
permission.app = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self._permissions = permissions as NSSet
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension StoreApp
|
public extension StoreApp
|
||||||
|
|||||||
89
AltStoreCore/Protocols/ALTAppPermission.swift
Normal file
89
AltStoreCore/Protocols/ALTAppPermission.swift
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
//
|
||||||
|
// ALTAppPermission.swift
|
||||||
|
// AltStoreCore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 5/3/23.
|
||||||
|
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
public extension ALTAppPermissionType
|
||||||
|
{
|
||||||
|
var localizedName: String? {
|
||||||
|
switch self
|
||||||
|
{
|
||||||
|
case .unknown: return NSLocalizedString("Permission", comment: "")
|
||||||
|
case .entitlement: return NSLocalizedString("Entitlement", comment: "")
|
||||||
|
case .privacy: return NSLocalizedString("Privacy Permission", comment: "")
|
||||||
|
case .backgroundMode: return NSLocalizedString("Background Mode", comment: "")
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol ALTAppPermission: RawRepresentable<String>, Hashable
|
||||||
|
{
|
||||||
|
var type: ALTAppPermissionType { get }
|
||||||
|
var symbolName: String? { get }
|
||||||
|
|
||||||
|
var localizedName: String? { get }
|
||||||
|
var localizedDisplayName: String { get } // Default implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension ALTAppPermission
|
||||||
|
{
|
||||||
|
var localizedDisplayName: String {
|
||||||
|
return self.localizedName ?? self.rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEqual(_ permission: any ALTAppPermission) -> Bool
|
||||||
|
{
|
||||||
|
guard let permission = permission as? Self else { return false }
|
||||||
|
return self == permission
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: Self, rhs: any ALTAppPermission) -> Bool
|
||||||
|
{
|
||||||
|
return lhs.isEqual(rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct UnknownAppPermission: ALTAppPermission
|
||||||
|
{
|
||||||
|
public var type: ALTAppPermissionType { .unknown }
|
||||||
|
public var symbolName: String? { nil }
|
||||||
|
|
||||||
|
public var localizedName: String? { nil }
|
||||||
|
|
||||||
|
public var rawValue: String
|
||||||
|
|
||||||
|
public init(rawValue: String)
|
||||||
|
{
|
||||||
|
self.rawValue = rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ALTEntitlement: ALTAppPermission
|
||||||
|
{
|
||||||
|
public var type: ALTAppPermissionType { .entitlement }
|
||||||
|
public var symbolName: String? { nil }
|
||||||
|
|
||||||
|
public var localizedName: String? { nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ALTAppPrivacyPermission: ALTAppPermission
|
||||||
|
{
|
||||||
|
public var type: ALTAppPermissionType { .privacy }
|
||||||
|
public var symbolName: String? { nil }
|
||||||
|
|
||||||
|
public var localizedName: String? { nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ALTAppBackgroundMode: ALTAppPermission
|
||||||
|
{
|
||||||
|
public var type: ALTAppPermissionType { .backgroundMode }
|
||||||
|
public var symbolName: String? { nil }
|
||||||
|
|
||||||
|
public var localizedName: String? { nil }
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTAppPermission.h
|
|
||||||
// AltStore
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 7/23/19.
|
|
||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
typedef NSString *ALTAppPermissionType NS_TYPED_EXTENSIBLE_ENUM;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypePhotos;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeCamera;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeLocation;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeContacts;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeReminders;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeAppleMusic;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeMicrophone;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeSpeechRecognition;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeBackgroundAudio;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeBackgroundFetch;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeBluetooth;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeNetwork;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeCalendars;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeTouchID;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeFaceID;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeSiri;
|
|
||||||
extern ALTAppPermissionType const ALTAppPermissionTypeMotion;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTAppPermission.m
|
|
||||||
// AltStore
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 7/23/19.
|
|
||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ALTAppPermission.h"
|
|
||||||
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypePhotos = @"photos";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeCamera = @"camera";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeLocation = @"location";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeContacts = @"contacts";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeReminders = @"reminders";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeAppleMusic = @"music";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeMicrophone = @"microphone";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeSpeechRecognition = @"speech-recognition";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeBackgroundAudio = @"background-audio";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeBackgroundFetch = @"background-fetch";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeBluetooth = @"bluetooth";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeNetwork = @"network";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeCalendars = @"calendars";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeTouchID = @"touchid";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeFaceID = @"faceid";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeSiri = @"siri";
|
|
||||||
ALTAppPermissionType const ALTAppPermissionTypeMotion = @"motion";
|
|
||||||
18
AltStoreCore/Types/ALTAppPermissions.h
Normal file
18
AltStoreCore/Types/ALTAppPermissions.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// ALTAppPermissions.h
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/23/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
typedef NSString *ALTAppPermissionType NS_TYPED_EXTENSIBLE_ENUM;
|
||||||
|
extern ALTAppPermissionType const ALTAppPermissionTypeUnknown;
|
||||||
|
extern ALTAppPermissionType const ALTAppPermissionTypeEntitlement;
|
||||||
|
extern ALTAppPermissionType const ALTAppPermissionTypePrivacy;
|
||||||
|
extern ALTAppPermissionType const ALTAppPermissionTypeBackgroundMode;
|
||||||
|
|
||||||
|
typedef NSString *ALTAppPrivacyPermission NS_TYPED_EXTENSIBLE_ENUM;
|
||||||
|
typedef NSString *ALTAppBackgroundMode NS_TYPED_EXTENSIBLE_ENUM;
|
||||||
14
AltStoreCore/Types/ALTAppPermissions.m
Normal file
14
AltStoreCore/Types/ALTAppPermissions.m
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// ALTAppPermissions.m
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/23/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "ALTAppPermissions.h"
|
||||||
|
|
||||||
|
ALTAppPermissionType const ALTAppPermissionTypeUnknown = @"unknown";
|
||||||
|
ALTAppPermissionType const ALTAppPermissionTypeEntitlement = @"entitlement";
|
||||||
|
ALTAppPermissionType const ALTAppPermissionTypePrivacy = @"privacy";
|
||||||
|
ALTAppPermissionType const ALTAppPermissionTypeBackgroundMode = @"background";
|
||||||
@@ -22,6 +22,7 @@ public extension Bundle
|
|||||||
public static let devicePairingString = "ALTPairingFile"
|
public static let devicePairingString = "ALTPairingFile"
|
||||||
public static let urlTypes = "CFBundleURLTypes"
|
public static let urlTypes = "CFBundleURLTypes"
|
||||||
public static let exportedUTIs = "UTExportedTypeDeclarations"
|
public static let exportedUTIs = "UTExportedTypeDeclarations"
|
||||||
|
public static let backgroundModes = "UIBackgroundModes"
|
||||||
|
|
||||||
public static let untetherURL = "ALTFugu14UntetherURL"
|
public static let untetherURL = "ALTFugu14UntetherURL"
|
||||||
public static let untetherRequired = "ALTFugu14UntetherRequired"
|
public static let untetherRequired = "ALTFugu14UntetherRequired"
|
||||||
|
|||||||
Reference in New Issue
Block a user