mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
[AltStoreCore] Generates Source.identifier from sourceURL
This commit is contained in:
@@ -375,6 +375,7 @@
|
||||
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, ); }; };
|
||||
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 */; };
|
||||
D570841A2924680D00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -2244,6 +2246,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D586D39A28EF58B0000E101F /* AltTests.swift */,
|
||||
D56915082AD5F3E800A2B747 /* AltTests+Sources.swift */,
|
||||
D5F5AF2D28FDD2EC00C938F5 /* TestErrors.swift */,
|
||||
);
|
||||
path = AltTests;
|
||||
@@ -3291,6 +3294,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D586D39B28EF58B0000E101F /* AltTests.swift in Sources */,
|
||||
D56915092AD5F3E800A2B747 /* AltTests+Sources.swift in Sources */,
|
||||
D5F5AF2E28FDD2EC00C938F5 /* TestErrors.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
@@ -251,7 +251,7 @@ private extension DatabaseManager
|
||||
}
|
||||
|
||||
// Make sure to always update source URL to be current.
|
||||
altStoreSource.sourceURL = Source.altStoreSourceURL
|
||||
try! altStoreSource.setSourceURL(Source.altStoreSourceURL)
|
||||
|
||||
let storeApp: StoreApp
|
||||
|
||||
|
||||
@@ -11,11 +11,7 @@ import UIKit
|
||||
|
||||
public extension Source
|
||||
{
|
||||
#if ALPHA
|
||||
static let altStoreIdentifier = Bundle.Info.appbundleIdentifier
|
||||
#else
|
||||
static let altStoreIdentifier = Bundle.Info.appbundleIdentifier
|
||||
#endif
|
||||
static let altStoreIdentifier = try! Source.sourceID(from: Source.altStoreSourceURL)
|
||||
|
||||
#if STAGING
|
||||
|
||||
@@ -202,7 +198,7 @@ public class Source: NSManagedObject, Fetchable, Decodable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var identifier: String
|
||||
@NSManaged public private(set) var identifier: String
|
||||
@NSManaged public var sourceURL: URL
|
||||
|
||||
/* Source Detail */
|
||||
@@ -254,7 +250,6 @@ public class Source: NSManagedObject, Fetchable, Decodable
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case name
|
||||
case identifier
|
||||
case sourceURL
|
||||
case subtitle
|
||||
case localizedDescription = "description"
|
||||
@@ -283,11 +278,8 @@ public class Source: NSManagedObject, Fetchable, Decodable
|
||||
|
||||
do
|
||||
{
|
||||
self.sourceURL = sourceURL
|
||||
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.name = try container.decode(String.self, forKey: .name)
|
||||
self.identifier = try container.decode(String.self, forKey: .identifier)
|
||||
|
||||
// Optional Values
|
||||
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()
|
||||
{
|
||||
app.sourceIdentifier = self.identifier
|
||||
app.sortIndex = Int32(index)
|
||||
}
|
||||
self._apps = NSMutableOrderedSet(array: apps)
|
||||
@@ -321,7 +312,6 @@ public class Source: NSManagedObject, Fetchable, Decodable
|
||||
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
|
||||
for (index, item) in newsItems.enumerated()
|
||||
{
|
||||
item.sourceIdentifier = self.identifier
|
||||
item.sortIndex = Int32(index)
|
||||
}
|
||||
|
||||
@@ -343,6 +333,9 @@ public class Source: NSManagedObject, Fetchable, Decodable
|
||||
let featuredAppBundleIDs = try container.decodeIfPresent([String].self, forKey: .featuredApps)
|
||||
let featuredApps = featuredAppBundleIDs?.compactMap { appsByID[$0] }
|
||||
self.setFeaturedApps(featuredApps)
|
||||
|
||||
// Updates identifier + apps & newsItems
|
||||
try self.setSourceURL(sourceURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -397,6 +390,54 @@ public 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]?)
|
||||
{
|
||||
// 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
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Source>
|
||||
|
||||
159
AltTests/AltTests+Sources.swift
Normal file
159
AltTests/AltTests+Sources.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user