CrystalFetch/Source/ESDCatalog/MCTCatalogs.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
}
}