Files
SideStore/AltStoreCore/Model/StoreApp.swift
Riley Testut 45ed24bfb5 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.
2023-05-12 18:26:24 -05:00

305 lines
11 KiB
Swift

//
// StoreApp.swift
// AltStore
//
// Created by Riley Testut on 5/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import Roxas
import AltSign
public extension StoreApp
{
#if ALPHA
static let altstoreAppID = "com.rileytestut.AltStore.Alpha"
#elseif BETA
static let altstoreAppID = "com.rileytestut.AltStore.Beta"
#else
static let altstoreAppID = "com.rileytestut.AltStore"
#endif
static let dolphinAppID = "me.oatmealdome.dolphinios-njb"
}
@objc(StoreApp)
public class StoreApp: NSManagedObject, Decodable, Fetchable
{
/* Properties */
@NSManaged public private(set) var name: String
@NSManaged public private(set) var bundleIdentifier: String
@NSManaged public private(set) var subtitle: String?
@NSManaged public private(set) var developerName: String
@NSManaged public private(set) var localizedDescription: String
@NSManaged @objc(size) internal var _size: Int32
@NSManaged public private(set) var iconURL: URL
@NSManaged public private(set) var screenshotURLs: [URL]
@NSManaged @objc(version) public private(set) var latestVersionString: String
@NSManaged @objc(versionDate) internal private(set) var _versionDate: Date
@NSManaged @objc(versionDescription) internal private(set) var _versionDescription: String?
@NSManaged @objc(downloadURL) internal var _downloadURL: URL
@NSManaged public private(set) var tintColor: UIColor?
@NSManaged public private(set) var isBeta: Bool
@objc public internal(set) var sourceIdentifier: String? {
get {
self.willAccessValue(forKey: #keyPath(sourceIdentifier))
defer { self.didAccessValue(forKey: #keyPath(sourceIdentifier)) }
let sourceIdentifier = self.primitiveSourceIdentifier
return sourceIdentifier
}
set {
self.willChangeValue(forKey: #keyPath(sourceIdentifier))
self.primitiveSourceIdentifier = newValue
self.didChangeValue(forKey: #keyPath(sourceIdentifier))
for version in self.versions
{
version.sourceID = newValue
}
}
}
@NSManaged private var primitiveSourceIdentifier: String?
@NSManaged public var sortIndex: Int32
/* Relationships */
@NSManaged public var installedApp: InstalledApp?
@NSManaged public var newsItems: Set<NewsItem>
@NSManaged @objc(source) public var _source: Source?
@NSManaged public internal(set) var featuringSource: Source?
@NSManaged @objc(latestVersion) public private(set) var latestSupportedVersion: AppVersion?
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
@nonobjc public var source: Source? {
set {
self._source = newValue
self.sourceIdentifier = newValue?.identifier
}
get {
return self._source
}
}
@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]
}
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
private enum CodingKeys: String, CodingKey
{
case name
case bundleIdentifier
case developerName
case localizedDescription
case version
case versionDescription
case versionDate
case iconURL
case screenshotURLs
case downloadURL
case tintColor
case subtitle
case permissions = "appPermissions"
case size
case isBeta = "beta"
case versions
}
public required init(from decoder: Decoder) throws
{
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
// Must initialize with context in order for child context saves to work correctly.
super.init(entity: StoreApp.entity(), insertInto: context)
do
{
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.bundleIdentifier = try container.decode(String.self, forKey: .bundleIdentifier)
self.developerName = try container.decode(String.self, forKey: .developerName)
self.localizedDescription = try container.decode(String.self, forKey: .localizedDescription)
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
self.iconURL = try container.decode(URL.self, forKey: .iconURL)
self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor)
{
guard let tintColor = UIColor(hexString: tintColorHex) else {
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
}
self.tintColor = tintColor
}
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
for permission in permissions
{
permission.appBundleID = self.bundleIdentifier
}
self._permissions = NSSet(array: permissions)
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions)
{
for version in versions
{
version.appBundleID = self.bundleIdentifier
}
try self.setVersions(versions)
}
else
{
let version = try container.decode(String.self, forKey: .version)
let versionDate = try container.decode(Date.self, forKey: .versionDate)
let versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
let downloadURL = try container.decode(URL.self, forKey: .downloadURL)
let size = try container.decode(Int32.self, forKey: .size)
let appVersion = AppVersion.makeAppVersion(version: version,
date: versionDate,
localizedDescription: versionDescription,
downloadURL: downloadURL,
size: Int64(size),
appBundleID: self.bundleIdentifier,
in: context)
try self.setVersions([appVersion])
}
}
catch
{
if let context = self.managedObjectContext
{
context.delete(self)
}
throw error
}
}
}
internal extension StoreApp
{
func setVersions(_ versions: [AppVersion]) throws
{
guard let latestVersion = versions.first else {
throw MergeError.noVersions(for: self)
}
self._versions = NSOrderedSet(array: versions)
let latestSupportedVersion = versions.first(where: { $0.isSupported })
self.latestSupportedVersion = latestSupportedVersion
for case let version as AppVersion in self._versions
{
if version == latestSupportedVersion
{
version.latestSupportedVersionApp = self
}
else
{
// Ensure we replace any previous relationship when merging.
version.latestSupportedVersionApp = nil
}
}
// Preserve backwards compatibility by assigning legacy property values.
self.latestVersionString = latestVersion.version
self._versionDate = latestVersion.date
self._versionDescription = latestVersion.localizedDescription
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
{
var latestAvailableVersion: AppVersion? {
return self._versions.firstObject as? AppVersion
}
var globallyUniqueID: String? {
guard let sourceIdentifier = self.sourceIdentifier else { return nil }
let globallyUniqueID = self.bundleIdentifier + "|" + sourceIdentifier
return globallyUniqueID
}
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
{
return NSFetchRequest<StoreApp>(entityName: "StoreApp")
}
class func makeAltStoreApp(in context: NSManagedObjectContext) -> StoreApp
{
let app = StoreApp(context: context)
app.name = "AltStore"
app.bundleIdentifier = StoreApp.altstoreAppID
app.developerName = "Riley Testut"
app.localizedDescription = "AltStore is an alternative App Store."
app.iconURL = URL(string: "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png")!
app.screenshotURLs = []
app.sourceIdentifier = Source.altStoreIdentifier
let appVersion = AppVersion.makeAppVersion(version: "1.0",
date: Date(),
downloadURL: URL(string: "http://rileytestut.com")!,
size: 0,
appBundleID: app.bundleIdentifier,
sourceID: Source.altStoreIdentifier,
in: context)
try? app.setVersions([appVersion])
#if BETA
app.isBeta = true
#endif
return app
}
}