[AltStoreCore] Generates Source.identifier from sourceURL

This commit is contained in:
Riley Testut
2023-10-10 17:39:20 -05:00
committed by Magesh K
parent e33a40ecb1
commit d7384cfae9
4 changed files with 238 additions and 13 deletions

View File

@@ -375,6 +375,7 @@
D561B2EB28EF5A4F006752E4 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; }; D561B2EB28EF5A4F006752E4 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; };
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, ); }; };
D56915072AD5E91B00A2B747 /* Regex+Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */; }; D56915072AD5E91B00A2B747 /* Regex+Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */; };
D56915092AD5F3E800A2B747 /* AltTests+Sources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56915082AD5F3E800A2B747 /* AltTests+Sources.swift */; };
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 */; }; D571ADD02A02FC7200B24B63 /* ALTAppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */; };
@@ -1037,6 +1038,7 @@
D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = "<group>"; }; D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = "<group>"; };
D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveAppsWidget.swift; sourceTree = "<group>"; }; D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveAppsWidget.swift; sourceTree = "<group>"; };
D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Regex+Permissions.swift"; sourceTree = "<group>"; }; D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Regex+Permissions.swift"; sourceTree = "<group>"; };
D56915082AD5F3E800A2B747 /* AltTests+Sources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AltTests+Sources.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>"; };
D571ADCD2A02FA7400B24B63 /* SourceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceError.swift; sourceTree = "<group>"; }; D571ADCD2A02FA7400B24B63 /* SourceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceError.swift; sourceTree = "<group>"; };
D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTAppPermission.swift; sourceTree = "<group>"; }; D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTAppPermission.swift; sourceTree = "<group>"; };
@@ -2244,6 +2246,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D586D39A28EF58B0000E101F /* AltTests.swift */, D586D39A28EF58B0000E101F /* AltTests.swift */,
D56915082AD5F3E800A2B747 /* AltTests+Sources.swift */,
D5F5AF2D28FDD2EC00C938F5 /* TestErrors.swift */, D5F5AF2D28FDD2EC00C938F5 /* TestErrors.swift */,
); );
path = AltTests; path = AltTests;
@@ -3291,6 +3294,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D586D39B28EF58B0000E101F /* AltTests.swift in Sources */, D586D39B28EF58B0000E101F /* AltTests.swift in Sources */,
D56915092AD5F3E800A2B747 /* AltTests+Sources.swift in Sources */,
D5F5AF2E28FDD2EC00C938F5 /* TestErrors.swift in Sources */, D5F5AF2E28FDD2EC00C938F5 /* TestErrors.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

View File

@@ -251,7 +251,7 @@ private extension DatabaseManager
} }
// Make sure to always update source URL to be current. // Make sure to always update source URL to be current.
altStoreSource.sourceURL = Source.altStoreSourceURL try! altStoreSource.setSourceURL(Source.altStoreSourceURL)
let storeApp: StoreApp let storeApp: StoreApp

View File

@@ -11,11 +11,7 @@ import UIKit
public extension Source public extension Source
{ {
#if ALPHA static let altStoreIdentifier = try! Source.sourceID(from: Source.altStoreSourceURL)
static let altStoreIdentifier = Bundle.Info.appbundleIdentifier
#else
static let altStoreIdentifier = Bundle.Info.appbundleIdentifier
#endif
#if STAGING #if STAGING
@@ -202,7 +198,7 @@ public class Source: NSManagedObject, Fetchable, Decodable
{ {
/* Properties */ /* Properties */
@NSManaged public var name: String @NSManaged public var name: String
@NSManaged public var identifier: String @NSManaged public private(set) var identifier: String
@NSManaged public var sourceURL: URL @NSManaged public var sourceURL: URL
/* Source Detail */ /* Source Detail */
@@ -254,7 +250,6 @@ public class Source: NSManagedObject, Fetchable, Decodable
private enum CodingKeys: String, CodingKey private enum CodingKeys: String, CodingKey
{ {
case name case name
case identifier
case sourceURL case sourceURL
case subtitle case subtitle
case localizedDescription = "description" case localizedDescription = "description"
@@ -283,11 +278,8 @@ public class Source: NSManagedObject, Fetchable, Decodable
do do
{ {
self.sourceURL = sourceURL
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name) self.name = try container.decode(String.self, forKey: .name)
self.identifier = try container.decode(String.self, forKey: .identifier)
// Optional Values // Optional Values
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle) self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
@@ -313,7 +305,6 @@ public class Source: NSManagedObject, Fetchable, Decodable
for (index, app) in apps.enumerated() for (index, app) in apps.enumerated()
{ {
app.sourceIdentifier = self.identifier
app.sortIndex = Int32(index) app.sortIndex = Int32(index)
} }
self._apps = NSMutableOrderedSet(array: apps) self._apps = NSMutableOrderedSet(array: apps)
@@ -321,7 +312,6 @@ public class Source: NSManagedObject, Fetchable, Decodable
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? [] let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
for (index, item) in newsItems.enumerated() for (index, item) in newsItems.enumerated()
{ {
item.sourceIdentifier = self.identifier
item.sortIndex = Int32(index) item.sortIndex = Int32(index)
} }
@@ -343,6 +333,9 @@ public class Source: NSManagedObject, Fetchable, Decodable
let featuredAppBundleIDs = try container.decodeIfPresent([String].self, forKey: .featuredApps) let featuredAppBundleIDs = try container.decodeIfPresent([String].self, forKey: .featuredApps)
let featuredApps = featuredAppBundleIDs?.compactMap { appsByID[$0] } let featuredApps = featuredAppBundleIDs?.compactMap { appsByID[$0] }
self.setFeaturedApps(featuredApps) self.setFeaturedApps(featuredApps)
// Updates identifier + apps & newsItems
try self.setSourceURL(sourceURL)
} }
catch catch
{ {
@@ -397,6 +390,54 @@ public extension Source
internal extension Source internal extension Source
{ {
class func sourceID(from sourceURL: URL) throws -> String
{
// Based on https://encyclopedia.pub/entry/29841
guard var components = URLComponents(url: sourceURL, resolvingAgainstBaseURL: true) else { throw URLError(.badURL, userInfo: [NSURLErrorKey: sourceURL]) }
if components.scheme == nil && components.host == nil
{
// Special handling for URLs without explicit scheme & incorrectly assumed to have nil host (e.g. "altstore.io/my/path")
guard let updatedComponents = URLComponents(string: "https://" + sourceURL.absoluteString) else { throw URLError(.cannotFindHost, userInfo: [NSURLErrorKey: sourceURL]) }
components = updatedComponents
}
// 1. Don't use percent encoding
guard let host = components.host else { throw URLError(.cannotFindHost, userInfo: [NSURLErrorKey: sourceURL]) }
// 2. Ignore scheme
var standardizedID = host
// 3. Add port (if not default)
if let port = components.port, port != 80 && port != 443
{
standardizedID += ":" + String(port)
}
// 4. Add path without fragment or query parameters
// 5. Remove duplicate slashes
let path = components.path.replacingOccurrences(of: "//", with: "/") // Only remove duplicate slashes from path, not entire URL.
standardizedID += path // path has leading `/`
// 6. Convert to lowercase
standardizedID = standardizedID.lowercased()
// 7. Remove trailing `/`
if standardizedID.hasSuffix("/")
{
standardizedID.removeLast()
}
// 8. Remove leading "www"
if standardizedID.hasPrefix("www.")
{
standardizedID.removeFirst(4)
}
return standardizedID
}
func setFeaturedApps(_ featuredApps: [StoreApp]?) func setFeaturedApps(_ featuredApps: [StoreApp]?)
{ {
// Explicitly update relationships for all apps to ensure featuredApps merges correctly. // Explicitly update relationships for all apps to ensure featuredApps merges correctly.
@@ -418,6 +459,27 @@ internal extension Source
} }
} }
public extension Source
{
func setSourceURL(_ sourceURL: URL) throws
{
let identifier = try Source.sourceID(from: sourceURL)
self.identifier = identifier
self.sourceURL = sourceURL
for app in self.apps
{
app.sourceIdentifier = identifier
}
for newsItem in self.newsItems
{
newsItem.sourceIdentifier = identifier
}
}
}
public extension Source public extension Source
{ {
@nonobjc class func fetchRequest() -> NSFetchRequest<Source> @nonobjc class func fetchRequest() -> NSFetchRequest<Source>

View File

@@ -0,0 +1,159 @@
//
// AltTests+Sources.swift
// AltTests
//
// Created by Riley Testut on 10/10/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import XCTest
@testable import AltStoreCore
extension AltTests
{
func testSourceID() throws
{
let url = Source.altStoreSourceURL
let sourceID = try Source.sourceID(from: url)
XCTAssertEqual(sourceID, "apps.altstore.io")
}
@available(iOS 17, *)
func testSourceIDWithPercentEncoding() throws
{
let url = URL(string: "apple.com/MY invalid•path/")!
let sourceID = try Source.sourceID(from: url)
XCTAssertEqual(sourceID, "apple.com/my invalid•path")
}
func testSourceIDWithDifferentSchemes() throws
{
let url1 = URL(string: "http://rileytestut.com")!
let url2 = URL(string: "https://rileytestut.com")!
let sourceID = try Source.sourceID(from: url1)
XCTAssertEqual(sourceID, "rileytestut.com")
let sourceID2 = try Source.sourceID(from: url2)
XCTAssertEqual(sourceID, sourceID2)
}
func testSourceIDWithNonDefaultPort() throws
{
let url = URL(string: "http://localhost:8008/apps.json")!
let sourceID = try Source.sourceID(from: url)
XCTAssertEqual(sourceID, "localhost:8008/apps.json")
}
func testSourceIDWithFragmentsAndQueries() throws
{
var components = URLComponents(string: "https://disney.com/altstore/apps")!
components.fragment = "get started"
components.queryItems = [URLQueryItem(name: "id", value: "1234")]
let url1 = components.url!
components.queryItems = [URLQueryItem(name: "id", value: "5678")]
let url2 = components.url!
XCTAssertNotEqual(url1, url2)
let sourceID = try Source.sourceID(from: url1)
XCTAssertEqual(sourceID, "disney.com/altstore/apps")
let sourceID2 = try Source.sourceID(from: url2)
XCTAssertEqual(sourceID, sourceID2)
}
func testSourceIDWithDuplicateSlashes() throws
{
let url1 = URL(string: "http://rileytestut.co.nz//secret/altstore//apps.json")!
let url2 = URL(string: "http://rileytestut.co.nz/secret/altstore/apps.json//")!
let sourceID = try Source.sourceID(from: url1)
XCTAssertEqual(sourceID, "rileytestut.co.nz/secret/altstore/apps.json")
let sourceID2 = try Source.sourceID(from: url2)
XCTAssertEqual(sourceID, sourceID2)
}
func testSourceIDWithMixedCase() throws
{
let href = "https://rileyTESTUT.co.nz/test/PATH/ApPs.json"
let url1 = URL(string: href)!
let url2 = URL(string: href.lowercased())!
let sourceID = try Source.sourceID(from: url1)
XCTAssertEqual(sourceID, "rileytestut.co.nz/test/path/apps.json")
let sourceID2 = try Source.sourceID(from: url2)
XCTAssertEqual(sourceID, sourceID2)
}
func testSourceIDWithTrailingSlash() throws
{
let url1 = URL(string: "http://apps.altstore.io/")!
let url2 = URL(string: "http://apps.altstore.io")!
let sourceID = try Source.sourceID(from: url1)
XCTAssertEqual(sourceID, "apps.altstore.io")
let sourceID2 = try Source.sourceID(from: url2)
XCTAssertEqual(sourceID, sourceID2)
}
func testSourceIDWithLeadingWWW() throws
{
let url1 = URL(string: "http://www.GBA4iOSApp.com")!
let url2 = URL(string: "http://gba4iosapp.com")!
let sourceID = try Source.sourceID(from: url1)
XCTAssertEqual(sourceID, "gba4iosapp.com")
let sourceID2 = try Source.sourceID(from: url2)
XCTAssertEqual(sourceID, sourceID2)
}
func testSourceIDWithAllRules() throws
{
let url1 = URL(string: "fTp://WWW.apps.APPLE.com:4004//altstore apps/source.JSON?user=test@altstore.io#welcome//")!
let url2 = URL(string: "ftp://apps.apple.com:4004/altstore apps/source.json?user=anothertest@altstore.io#welcome")!
let sourceID = try Source.sourceID(from: url1)
XCTAssertEqual(sourceID, "apps.apple.com:4004/altstore apps/source.json")
let sourceID2 = try Source.sourceID(from: url2)
XCTAssertEqual(sourceID, sourceID2)
}
func testSourceIDWithEmoji() throws
{
let url1 = URL(string: "http://xn--g5h5981o.com")! // 🤷.com
let sourceID1 = try Source.sourceID(from: url1)
XCTAssertEqual(sourceID1, "🤷♂.com")
let url2 = URL(string: "http://www.xn--7r8h.io")! // www.💜.io
let sourceID2 = try Source.sourceID(from: url2)
XCTAssertEqual(sourceID2, "💜.io")
}
func testSourceIDWithRelativeURL() throws
{
let baseURL = URL(string: "https://rileytestut.com")!
let path = "altstore/apps.json"
let url1 = URL(string: path, relativeTo: baseURL)!
let url2 = baseURL.appendingPathComponent(path)
let sourceID = try Source.sourceID(from: url1)
XCTAssertEqual(sourceID, "rileytestut.com/altstore/apps.json")
let sourceID2 = try Source.sourceID(from: url2)
XCTAssertEqual(sourceID, sourceID2)
}
}