diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 8ce1a538..106256f6 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -371,6 +371,7 @@ D5418F172AD740890014ABD6 /* AppScreenshotCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5418F162AD740890014ABD6 /* AppScreenshotCollectionViewCell.swift */; }; D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */; }; D552B1D82A042A740066216F /* AppPermissionsCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D552B1D72A042A740066216F /* AppPermissionsCard.swift */; }; + D552EB062AF453F900A3AB4D /* URL+Normalized.swift in Sources */ = {isa = PBXBuildFile; fileRef = D552EB052AF453F900A3AB4D /* URL+Normalized.swift */; }; D55467B82A8D5E2600F4CE90 /* AppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */; }; D55467C52A8D72C300F4CE90 /* ActiveAppsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */; }; D561B2EB28EF5A4F006752E4 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; }; @@ -1041,6 +1042,7 @@ D5418F162AD740890014ABD6 /* AppScreenshotCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppScreenshotCollectionViewCell.swift; sourceTree = ""; }; D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = ""; }; D552B1D72A042A740066216F /* AppPermissionsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermissionsCard.swift; sourceTree = ""; }; + D552EB052AF453F900A3AB4D /* URL+Normalized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Normalized.swift"; sourceTree = ""; }; D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = ""; }; D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveAppsWidget.swift; sourceTree = ""; }; D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Regex+Permissions.swift"; sourceTree = ""; }; @@ -1753,6 +1755,7 @@ D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */, D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */, D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */, + D552EB052AF453F900A3AB4D /* URL+Normalized.swift */, ); path = Extensions; sourceTree = ""; @@ -3077,6 +3080,7 @@ BF66EECD2501AECA007EE018 /* StoreAppPolicy.swift in Sources */, BF66EEE82501AED0007EE018 /* UserDefaults+AltStore.swift in Sources */, BF340E9A250AD39500A192CB /* ViewApp.intentdefinition in Sources */, + D552EB062AF453F900A3AB4D /* URL+Normalized.swift in Sources */, BFAECC522501B0A400528F27 /* CodableError.swift in Sources */, D5F9821D2AB900060045751F /* AppScreenshot.swift in Sources */, BF66EE9E2501AEC1007EE018 /* Fetchable.swift in Sources */, diff --git a/AltStoreCore/Extensions/URL+Normalized.swift b/AltStoreCore/Extensions/URL+Normalized.swift new file mode 100644 index 00000000..7b4e161f --- /dev/null +++ b/AltStoreCore/Extensions/URL+Normalized.swift @@ -0,0 +1,60 @@ +// +// URL+Normalized.swift +// AltStoreCore +// +// Created by Riley Testut on 11/2/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation + +public extension URL +{ + func normalized() throws -> String + { + // Based on https://encyclopedia.pub/entry/29841 + + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: true) else { throw URLError(.badURL, userInfo: [NSURLErrorKey: self, NSURLErrorFailingURLErrorKey: self]) } + + 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://" + self.absoluteString) else { throw URLError(.cannotFindHost, userInfo: [NSURLErrorKey: self, NSURLErrorFailingURLErrorKey: self]) } + components = updatedComponents + } + + // 1. Don't use percent encoding + guard let host = components.host else { throw URLError(.cannotFindHost, userInfo: [NSURLErrorKey: self, NSURLErrorFailingURLErrorKey: self]) } + + // 2. Ignore scheme + var normalizedURL = host + + // 3. Add port (if not default) + if let port = components.port, port != 80 && port != 443 + { + normalizedURL += ":" + 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. + normalizedURL += path // path has leading `/` + + // 6. Convert to lowercase + normalizedURL = normalizedURL.lowercased() + + // 7. Remove trailing `/` + if normalizedURL.hasSuffix("/") + { + normalizedURL.removeLast() + } + + // 8. Remove leading "www" + if normalizedURL.hasPrefix("www.") + { + normalizedURL.removeFirst(4) + } + + return normalizedURL + } +} diff --git a/AltStoreCore/Model/Source.swift b/AltStoreCore/Model/Source.swift index 61fa9afd..b72e90e6 100644 --- a/AltStoreCore/Model/Source.swift +++ b/AltStoreCore/Model/Source.swift @@ -392,50 +392,8 @@ 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 + let sourceID = try sourceURL.normalized() + return sourceID } func setFeaturedApps(_ featuredApps: [StoreApp]?)