mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-16 18:23:53 +01:00
Merge branch 'revised_source_json'
# Conflicts: # AltStore.xcodeproj/project.pbxproj # AltStore/App Detail/AppContentViewController.swift # AltStore/App Detail/AppViewController.swift # AltStore/Base.lproj/Main.storyboard # AltStoreCore/Model/DatabaseManager.swift
This commit is contained in:
@@ -23,6 +23,7 @@ extension SourceError
|
||||
case duplicate
|
||||
|
||||
case missingPermissionUsageDescription
|
||||
case missingScreenshotSize
|
||||
}
|
||||
|
||||
static func unsupported(_ source: Source) -> SourceError { SourceError(code: .unsupported, source: source) }
|
||||
@@ -36,6 +37,10 @@ extension SourceError
|
||||
static func missingPermissionUsageDescription(for permission: any ALTAppPermission, app: StoreApp, source: Source) -> SourceError {
|
||||
SourceError(code: .missingPermissionUsageDescription, source: source, app: app, permission: permission)
|
||||
}
|
||||
|
||||
static func missingScreenshotSize(for screenshot: AppScreenshot, source: Source) -> SourceError {
|
||||
SourceError(code: .missingScreenshotSize, source: source, app: screenshot.app, screenshotURL: screenshot.imageURL)
|
||||
}
|
||||
}
|
||||
|
||||
struct SourceError: ALTLocalizedError
|
||||
@@ -59,6 +64,9 @@ struct SourceError: ALTLocalizedError
|
||||
@UserInfoValue
|
||||
var permission: (any ALTAppPermission)?
|
||||
|
||||
@UserInfoValue
|
||||
var screenshotURL: URL?
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self.code
|
||||
{
|
||||
@@ -112,6 +120,14 @@ struct SourceError: ALTLocalizedError
|
||||
let permissionType = permission.type.localizedName ?? NSLocalizedString("Permission", comment: "")
|
||||
let failureReason = String(format: NSLocalizedString("The %@ '%@' for %@ is missing a usage description.", comment: ""), permissionType.lowercased(), permission.rawValue, appName)
|
||||
return failureReason
|
||||
|
||||
case .missingScreenshotSize:
|
||||
let appName = self.$app.name ?? String(format: NSLocalizedString("an app in source “%@”", comment: ""), self.$source.name)
|
||||
let baseMessage = String(format: NSLocalizedString("An iPad screenshot for %@ does not specify its size", comment: ""), appName)
|
||||
guard let screenshotURL else { return baseMessage + "." }
|
||||
|
||||
let failureReason = baseMessage + ": \(screenshotURL.absoluteString)"
|
||||
return failureReason
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,35 @@ class FetchSourceOperation: ResultOperation<Source>
|
||||
decoder.managedObjectContext = childContext
|
||||
decoder.sourceURL = self.sourceURL
|
||||
|
||||
let source = try decoder.decode(Source.self, from: data)
|
||||
let source: Source
|
||||
|
||||
do
|
||||
{
|
||||
source = try decoder.decode(Source.self, from: data)
|
||||
}
|
||||
catch let error as DecodingError
|
||||
{
|
||||
let nsError = error as NSError
|
||||
guard let codingPath = nsError.userInfo[ALTNSCodingPathKey] as? [CodingKey] else { throw error }
|
||||
|
||||
let rawComponents = codingPath.map { $0.intValue?.description ?? $0.stringValue }
|
||||
let pathDescription = rawComponents.joined(separator: " > ")
|
||||
|
||||
var userInfo = nsError.userInfo
|
||||
|
||||
if let debugDescription = nsError.localizedDebugDescription
|
||||
{
|
||||
let detailedDescription = debugDescription + "\n\n" + pathDescription
|
||||
userInfo[NSDebugDescriptionErrorKey] = detailedDescription
|
||||
}
|
||||
else
|
||||
{
|
||||
userInfo[NSDebugDescriptionErrorKey] = pathDescription
|
||||
}
|
||||
|
||||
throw NSError(domain: nsError.domain, code: nsError.code, userInfo: userInfo)
|
||||
}
|
||||
|
||||
let identifier = source.identifier
|
||||
|
||||
try self.verify(source, response: response)
|
||||
@@ -181,6 +209,12 @@ private extension FetchSourceOperation
|
||||
// Privacy permissions MUST have a usage description.
|
||||
guard permission.usageDescription != nil else { throw SourceError.missingPermissionUsageDescription(for: permission.permission, app: app, source: source) }
|
||||
}
|
||||
|
||||
for screenshot in app.screenshots(for: .ipad)
|
||||
{
|
||||
// All iPad screenshots MUST have an explicit size.
|
||||
guard screenshot.size != nil else { throw SourceError.missingScreenshotSize(for: screenshot, source: source) }
|
||||
}
|
||||
}
|
||||
|
||||
if let previousSourceID = self.$source.identifier
|
||||
|
||||
@@ -212,41 +212,23 @@ private extension VerifyAppOperation
|
||||
|
||||
|
||||
// Privacy
|
||||
let allPrivacyPermissions: Set<ALTAppPrivacyPermission>
|
||||
if #available(iOS 16, *)
|
||||
{
|
||||
let regex = Regex {
|
||||
"NS"
|
||||
|
||||
// Capture permission "name"
|
||||
Capture {
|
||||
OneOrMore(.anyGraphemeCluster)
|
||||
let allPrivacyPermissions = ([app] + app.appExtensions).flatMap { (app) in
|
||||
let permissions = app.bundle.infoDictionary?.keys.compactMap { key -> ALTAppPrivacyPermission? in
|
||||
if #available(iOS 16, *)
|
||||
{
|
||||
guard key.wholeMatch(of: Regex.privacyPermission) != nil else { return nil }
|
||||
}
|
||||
else
|
||||
{
|
||||
guard key.contains("UsageDescription") else { return nil }
|
||||
}
|
||||
|
||||
"UsageDescription"
|
||||
|
||||
// Optional suffix
|
||||
Optionally(OneOrMore(.anyGraphemeCluster))
|
||||
}
|
||||
let permission = ALTAppPrivacyPermission(rawValue: key)
|
||||
return permission
|
||||
} ?? []
|
||||
|
||||
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)
|
||||
return permissions
|
||||
}
|
||||
else
|
||||
{
|
||||
allPrivacyPermissions = []
|
||||
}
|
||||
|
||||
|
||||
// Verify permissions.
|
||||
let sourcePermissions: Set<AnyHashable> = Set(await $storeApp.perform { $0.permissions.map { AnyHashable($0.permission) } })
|
||||
@@ -254,8 +236,37 @@ private extension VerifyAppOperation
|
||||
|
||||
// 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) }
|
||||
let missingPermissions: [any ALTAppPermission] = localPermissions.filter { permission in
|
||||
if sourcePermissions.contains(AnyHashable(permission))
|
||||
{
|
||||
// `permission` exists in source, so return false.
|
||||
return false
|
||||
}
|
||||
else if permission.type == .privacy
|
||||
{
|
||||
guard #available(iOS 16, *) else {
|
||||
// Assume all privacy permissions _are_ included in source on pre-iOS 16 devices.
|
||||
return false
|
||||
}
|
||||
|
||||
// Special-handling for legacy privacy permissions.
|
||||
if let match = permission.rawValue.firstMatch(of: Regex.privacyPermission),
|
||||
case let legacyPermission = ALTAppPrivacyPermission(rawValue: String(match.1)),
|
||||
sourcePermissions.contains(AnyHashable(legacyPermission))
|
||||
{
|
||||
// The legacy name of this permission exists in the source, so return false.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Source doesn't contain permission or its legacy name, so assume it is missing.
|
||||
return true
|
||||
}
|
||||
|
||||
guard missingPermissions.isEmpty else {
|
||||
// There is at least one undeclared permission, so throw error.
|
||||
throw VerificationError.undeclaredPermissions(missingPermissions, app: app)
|
||||
}
|
||||
|
||||
return localPermissions
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user