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:
@@ -27,9 +27,18 @@
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<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="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"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="permission"/>
|
||||
<constraint value="type"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
|
||||
<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="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="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="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
|
||||
<uniquenessConstraints>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import CoreData
|
||||
import UIKit
|
||||
|
||||
import AltSign
|
||||
public extension ALTAppPermissionType
|
||||
{
|
||||
var localizedShortName: String? {
|
||||
@@ -71,15 +72,29 @@ public extension ALTAppPermissionType
|
||||
}
|
||||
}
|
||||
|
||||
@objc(AppPermission)
|
||||
@objc(AppPermission) @dynamicMemberLookup
|
||||
public class AppPermission: NSManagedObject, Decodable, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@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 */
|
||||
@NSManaged public private(set) var app: StoreApp!
|
||||
@NSManaged public internal(set) var app: StoreApp?
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
@@ -88,7 +103,10 @@ public class AppPermission: NSManagedObject, Decodable, Fetchable
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case type
|
||||
case entitlement
|
||||
case privacyType = "privacy"
|
||||
case backgroundMode = "background"
|
||||
|
||||
case usageDescription
|
||||
}
|
||||
|
||||
@@ -101,10 +119,33 @@ public class AppPermission: NSManagedObject, Decodable, Fetchable
|
||||
do
|
||||
{
|
||||
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.type = ALTAppPermissionType(rawValue: rawType)
|
||||
self.usageDescription = try container.decodeIfPresent(String.self, forKey: .usageDescription)
|
||||
|
||||
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
|
||||
{
|
||||
@@ -125,3 +166,14 @@ public extension 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 incorrectVersionOrder
|
||||
case incorrectPermissions
|
||||
}
|
||||
|
||||
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 incorrectPermissions(for app: StoreApp) -> MergeError { .init(code: .incorrectPermissions, appName: app.name, appBundleID: app.bundleIdentifier, sourceID: app.sourceIdentifier) }
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
{
|
||||
// Delete previous permissions (same as below).
|
||||
for permission in previousApp.permissions
|
||||
// Delete previous permissions (different than below).
|
||||
for case let permission as AppPermission in previousApp._permissions where permission.app == nil
|
||||
{
|
||||
permission.managedObjectContext?.delete(permission)
|
||||
}
|
||||
@@ -171,6 +182,8 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
}
|
||||
|
||||
var sortedVersionsByGlobalAppID = [String: NSOrderedSet]()
|
||||
var permissionsByGlobalAppID = [String: Set<AnyHashable>]()
|
||||
|
||||
var featuredAppIDsBySourceID = [String: [String]]()
|
||||
|
||||
for conflict in conflicts
|
||||
@@ -178,28 +191,28 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
switch conflict.databaseObject
|
||||
{
|
||||
case let databaseObject as StoreApp:
|
||||
// Delete previous permissions
|
||||
for permission in databaseObject.permissions
|
||||
guard let contextApp = conflict.conflictingObjects.first as? StoreApp else { break }
|
||||
|
||||
// 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 })
|
||||
|
||||
for case let appVersion as AppVersion in databaseObject._versions where !contextVersions.contains(appVersion.version)
|
||||
{
|
||||
// Version # does NOT exist in context, so delete existing appVersion.
|
||||
appVersion.managedObjectContext?.delete(appVersion)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// Version # does NOT exist in context, so delete existing databaseVersion.
|
||||
databaseVersion.managedObjectContext?.delete(databaseVersion)
|
||||
}
|
||||
|
||||
if let globallyUniqueID = contextApp.globallyUniqueID
|
||||
{
|
||||
permissionsByGlobalAppID[globallyUniqueID] = contextPermissions
|
||||
sortedVersionsByGlobalAppID[globallyUniqueID] = contextVersions
|
||||
}
|
||||
|
||||
case let databaseObject as Source:
|
||||
@@ -244,36 +257,44 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
case let databaseObject as StoreApp:
|
||||
do
|
||||
{
|
||||
let appVersions: [AppVersion]
|
||||
var appVersions = databaseObject.versions
|
||||
|
||||
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
|
||||
if let globallyUniqueID = databaseObject.globallyUniqueID
|
||||
{
|
||||
// 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
|
||||
// Permissions
|
||||
if let appPermissions = permissionsByGlobalAppID[globallyUniqueID],
|
||||
case let databasePermissions = Set(databaseObject.permissions.map({ AnyHashable($0.permission) })),
|
||||
databasePermissions != appPermissions
|
||||
{
|
||||
// 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 }
|
||||
guard appVersionValues == sortedAppVersionsArray else {
|
||||
// fixedAppVersions still doesn't match source's versions, so throw MergeError.
|
||||
throw MergeError.incorrectVersionOrder(for: databaseObject)
|
||||
// App versions
|
||||
if 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.
|
||||
|
||||
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)
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -146,8 +146,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
@NSManaged @objc(source) public var _source: 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(versions) public private(set) var _versions: NSOrderedSet
|
||||
|
||||
@@ -163,9 +161,10 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
}
|
||||
}
|
||||
|
||||
@nonobjc public var permissions: [AppPermission] {
|
||||
return self._permissions.array as! [AppPermission]
|
||||
@nonobjc public var permissions: Set<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] {
|
||||
return self._versions.array as! [AppVersion]
|
||||
@@ -216,7 +215,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
case platformURLs
|
||||
case tintColor
|
||||
case subtitle
|
||||
case permissions
|
||||
case permissions = "appPermissions"
|
||||
case size
|
||||
case isBeta = "beta"
|
||||
case versions
|
||||
@@ -287,7 +286,11 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -367,6 +370,23 @@ internal extension StoreApp
|
||||
self._downloadURL = latestVersion.downloadURL
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user