mirror of
https://github.com/TuringSoftware/CrystalFetch.git
synced 2025-12-21 12:58:32 +08:00
246 lines
8.3 KiB
Swift
246 lines
8.3 KiB
Swift
//
|
|
// Copyright © 2025 Turing Software, LLC. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
actor MCTCatalogs {
|
|
struct Version: Equatable {
|
|
let latestCabUrl: URL?
|
|
let releases: [Release]
|
|
}
|
|
|
|
struct Release: Equatable, Hashable, Identifiable {
|
|
let build: String
|
|
let date: Date?
|
|
let cabUrl: URL
|
|
|
|
var id: Int {
|
|
hashValue
|
|
}
|
|
}
|
|
|
|
enum Windows: Int {
|
|
case windows10 = 10
|
|
case windows11 = 11
|
|
}
|
|
|
|
private(set) var versions: [Windows: Version] = [:]
|
|
|
|
init(from data: Data) async throws {
|
|
let result = try await withCheckedThrowingContinuation { continuation in
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
let parser = XMLParser(data: data)
|
|
let coordinator = MCTCatalogsParser()
|
|
coordinator.continuation = continuation
|
|
parser.delegate = coordinator
|
|
if !parser.parse() && coordinator.continuation != nil {
|
|
continuation.resume(throwing: parser.parserError ?? ESDCatalogError.unknown)
|
|
}
|
|
}
|
|
}
|
|
let results = try result.map { try Self.parseVersion($0) }
|
|
for (number, version) in results {
|
|
if let windows = Windows(rawValue: number) {
|
|
versions[windows] = version
|
|
} else {
|
|
NSLog("Ignoring unknown Windows version: %@", number)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func parseVersion(_ result: MCTCatalogsParser.Version) throws -> (number: Int, version: Version) {
|
|
let latestCabUrl: URL?
|
|
if let latestCabLink = result.latestCabLink {
|
|
latestCabUrl = URL(string: latestCabLink)!
|
|
} else {
|
|
latestCabUrl = nil
|
|
}
|
|
guard let number = Int(result.number) else {
|
|
throw ESDCatalogError.missingElement("number", "version")
|
|
}
|
|
let releases = try result.releases.map { try Self.parseRelease($0) }
|
|
return (number, Version(latestCabUrl: latestCabUrl, releases: releases))
|
|
}
|
|
|
|
private static func parseRelease(_ result: MCTCatalogsParser.Release) throws -> Release {
|
|
let date: Date?
|
|
if let dateString = result.date {
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.dateFormat = "yyyyMMdd"
|
|
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
|
|
date = dateFormatter.date(from: dateString)
|
|
} else {
|
|
date = nil
|
|
}
|
|
guard let cabLink = result.cabLink else {
|
|
throw ESDCatalogError.missingElement("cabLink", "release")
|
|
}
|
|
let cabUrl: URL = URL(string: cabLink)!
|
|
return Release(build: result.build, date: date, cabUrl: cabUrl)
|
|
}
|
|
}
|
|
|
|
fileprivate class MCTCatalogsParser: NSObject, XMLParserDelegate {
|
|
enum Inside: String {
|
|
case productsDb
|
|
case versions
|
|
case version
|
|
case latestCabLink
|
|
case releases
|
|
case release
|
|
case date
|
|
case cabLink
|
|
}
|
|
|
|
struct Version {
|
|
var number: String
|
|
var latestCabLink: String?
|
|
var releases: [Release] = []
|
|
}
|
|
|
|
struct Release {
|
|
var build: String
|
|
var date: String?
|
|
var cabLink: String?
|
|
}
|
|
|
|
var continuation: CheckedContinuation<[Version], Error>?
|
|
private var inside: Inside?
|
|
private var versions: [Version] = []
|
|
private var currentVersion: Version?
|
|
private var currentRelease: Release?
|
|
|
|
func parserDidStartDocument(_ xmlParser: XMLParser) {
|
|
inside = nil
|
|
}
|
|
|
|
func parserDidEndDocument(_ xmlParser: XMLParser) {
|
|
if let c = continuation {
|
|
continuation = nil
|
|
c.resume(returning: versions)
|
|
}
|
|
}
|
|
|
|
func parser(_ xmlParser: XMLParser, parseErrorOccurred parseError: Error) {
|
|
if let c = continuation {
|
|
continuation = nil
|
|
c.resume(throwing: parseError)
|
|
}
|
|
xmlParser.abortParsing()
|
|
}
|
|
|
|
func parser(_ xmlParser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
|
|
switch inside {
|
|
case nil:
|
|
guard expect(elementName, named: .productsDb, in: xmlParser) else {
|
|
return
|
|
}
|
|
inside = .productsDb
|
|
case .productsDb:
|
|
guard expect(elementName, named: .versions, in: xmlParser) else {
|
|
return
|
|
}
|
|
inside = .versions
|
|
case .versions:
|
|
guard expect(elementName, named: .version, in: xmlParser) else {
|
|
return
|
|
}
|
|
guard let number = attributeDict["number"] else {
|
|
parser(xmlParser, parseErrorOccurred: ESDCatalogError.attributeNotFound("number"))
|
|
return
|
|
}
|
|
inside = .version
|
|
currentVersion = Version(number: number)
|
|
case .version:
|
|
guard let seen = expect(elementName, namedOneOf: [.latestCabLink, .releases], in: xmlParser) else {
|
|
return
|
|
}
|
|
inside = seen
|
|
case .releases:
|
|
guard expect(elementName, named: .release, in: xmlParser) else {
|
|
return
|
|
}
|
|
guard let build = attributeDict["build"] else {
|
|
parser(xmlParser, parseErrorOccurred: ESDCatalogError.attributeNotFound("build"))
|
|
return
|
|
}
|
|
inside = .release
|
|
currentRelease = Release(build: build)
|
|
case .release:
|
|
guard let seen = expect(elementName, namedOneOf: [.date, .cabLink], in: xmlParser) else {
|
|
return
|
|
}
|
|
inside = seen
|
|
default:
|
|
parser(xmlParser, parseErrorOccurred: ESDCatalogError.invalidElement(elementName))
|
|
}
|
|
}
|
|
|
|
func parser(_ xmlParser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
|
|
guard elementName == inside?.rawValue else {
|
|
parser(xmlParser, parseErrorOccurred: ESDCatalogError.invalidElement(elementName))
|
|
return
|
|
}
|
|
switch inside! {
|
|
case .productsDb:
|
|
inside = nil
|
|
case .versions:
|
|
inside = .productsDb
|
|
case .version:
|
|
versions.append(currentVersion!)
|
|
currentVersion = nil
|
|
inside = .versions
|
|
case .latestCabLink, .releases:
|
|
inside = .version
|
|
case .release:
|
|
currentVersion!.releases.append(currentRelease!)
|
|
currentRelease = nil
|
|
inside = .releases
|
|
case .date, .cabLink:
|
|
inside = .release
|
|
}
|
|
}
|
|
|
|
func parser(_ xmlParser: XMLParser, foundCharacters string: String) {
|
|
if inside == .latestCabLink {
|
|
currentVersion!.latestCabLink = string
|
|
} else if inside == .date {
|
|
currentRelease!.date = string
|
|
} else if inside == .cabLink {
|
|
currentRelease!.cabLink = string
|
|
} else {
|
|
parser(xmlParser, parseErrorOccurred: ESDCatalogError.unexpectedString(string, inside?.rawValue ?? ""))
|
|
}
|
|
}
|
|
|
|
private func expect(_ elementName: String, named element: Inside, in xmlParser: XMLParser) -> Bool {
|
|
guard elementName == element.rawValue else {
|
|
parser(xmlParser, parseErrorOccurred: ESDCatalogError.invalidElement(elementName))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private func expect(_ elementName: String, namedOneOf elements: [Inside], in xmlParser: XMLParser) -> Inside? {
|
|
for element in elements {
|
|
if elementName == element.rawValue {
|
|
return element
|
|
}
|
|
}
|
|
parser(xmlParser, parseErrorOccurred: ESDCatalogError.invalidElement(elementName))
|
|
return nil
|
|
}
|
|
}
|