Files
Adobe-Downloader/Adobe Downloader/Utils/XHXMLParser.swift

212 lines
7.5 KiB
Swift

//
// Adobe-Downloader
//
// Created by X1a0He on 2024/10/30.
//
import Foundation
// struct Product: Identifiable {
// let id: String
// var hidden: Bool
// var displayName: String
// var sapCode: String
// var versions: [String: ProductVersion]
// var icons: [ProductIcon]
// var dependencyType: String?
// var family: String?
// var familyName: String?
// var appLineage: String?
// var type: String?
// var categories: [String]?
// struct ProductVersion {
// var sapCode: String
// var baseVersion: String
// var productVersion: String
// var apPlatform: String
// var dependencies: [Dependency]
// var buildGuid: String
// var packageCode: String?
// var productCode: String?
// var installSize: Int?
// var esdData: EsdData?
// }
// struct EsdData {
// var name: String
// var size: Int64
// var assetGuid: String
// }
// struct Dependency {
// var sapCode: String
// var version: String
// var esdDirectory: String?
// }
// struct ProductIcon {
// let size: String
// let url: String
// var dimension: Int {
// let components = size.split(separator: "x")
// if components.count == 2,
// let dimension = Int(components[0]) {
// return dimension
// }
// return 0
// }
// }
// var isValid: Bool {
// return !sapCode.isEmpty &&
// !displayName.isEmpty &&
// !versions.isEmpty
// }
// func getBestIcon() -> ProductIcon? {
// if let icon = icons.first(where: { $0.size == "192x192" }) {
// return icon
// }
// return icons.max(by: { $0.dimension < $1.dimension })
// }
// }
struct ParseResult {
var products: [String: Sap]
var cdn: String
}
class XHXMLParser {
static func parseProductsXML(xmlData: Data) throws -> ParseResult {
let xml = try XMLDocument(data: xmlData)
let allowedPlatforms = Set(["osx10-64", "osx10", "macuniversal", "macarm64"])
guard let cdn = try xml.nodes(forXPath: "//channels/channel/cdn/secure").first?.stringValue else {
throw ParserError.missingCDN
}
print("parseProductsXML - cdn: \(cdn)")
var products: [String: Sap] = [:]
let productNodes = try xml.nodes(forXPath: "//channels/channel/products/product")
let parentMap = createParentMap(xml.rootElement())
for productNode in productNodes {
guard let element = productNode as? XMLElement else { continue }
let sap = element.attribute(forName: "id")?.stringValue ?? ""
let parentElement = parentMap[parentMap[element] ?? element]
let hidden = (parentElement as? XMLElement)?.attribute(forName: "name")?.stringValue != "ccm"
let displayName = try element.nodes(forXPath: "displayName").first?.stringValue ?? ""
var productVersion = element.attribute(forName: "version")?.stringValue ?? ""
if products[sap] == nil {
let icons = try element.nodes(forXPath: "productIcons/icon").compactMap { node -> Sap.ProductIcon? in
guard let element = node as? XMLElement,
let size = element.attribute(forName: "size")?.stringValue,
let url = element.stringValue else {
return nil
}
return Sap.ProductIcon(size: size, url: url)
}
products[sap] = Sap(
hidden: hidden,
displayName: displayName,
sapCode: sap,
versions: [:],
icons: icons
)
}
let platforms = try element.nodes(forXPath: "platforms/platform")
for platformNode in platforms {
guard let platform = platformNode as? XMLElement,
let languageSet = try platform.nodes(forXPath: "languageSet").first as? XMLElement else { continue }
var baseVersion = languageSet.attribute(forName: "baseVersion")?.stringValue ?? ""
var buildGuid = languageSet.attribute(forName: "buildGuid")?.stringValue ?? ""
let appPlatform = platform.attribute(forName: "id")?.stringValue ?? ""
let dependencies = try languageSet.nodes(forXPath: "dependencies/dependency").compactMap { node -> Sap.Versions.Dependencies? in
guard let element = node as? XMLElement,
let sapCode = try element.nodes(forXPath: "sapCode").first?.stringValue,
let version = try element.nodes(forXPath: "baseVersion").first?.stringValue else {
return nil
}
return Sap.Versions.Dependencies(sapCode: sapCode, version: version)
}
if let existingVersion = products[sap]?.versions[productVersion],
allowedPlatforms.contains(existingVersion.apPlatform) {
break
}
if sap == "APRO" {
baseVersion = productVersion
let buildNodes = try xml.nodes(forXPath: "//builds/build")
for buildNode in buildNodes {
guard let buildElement = buildNode as? XMLElement,
buildElement.attribute(forName: "id")?.stringValue == sap,
buildElement.attribute(forName: "version")?.stringValue == baseVersion else {
continue
}
if let appVersion = try buildElement.nodes(forXPath: "nglLicensingInfo/appVersion").first?.stringValue {
productVersion = appVersion
break
}
}
buildGuid = try languageSet.nodes(forXPath: "urls/manifestURL").first?.stringValue ?? ""
}
if !buildGuid.isEmpty && allowedPlatforms.contains(appPlatform) {
let version = Sap.Versions(
sapCode: sap,
baseVersion: baseVersion,
productVersion: productVersion,
apPlatform: appPlatform,
dependencies: dependencies,
buildGuid: buildGuid
)
products[sap]?.versions[productVersion] = version
}
}
}
return ParseResult(products: products, cdn: cdn)
}
private static func createParentMap(_ root: XMLNode?) -> [XMLNode: XMLNode] {
var parentMap: [XMLNode: XMLNode] = [:]
func traverse(_ node: XMLNode) {
for child in node.children ?? [] {
parentMap[child] = node
traverse(child)
}
}
if let root = root {
traverse(root)
}
return parentMap
}
}
enum ParserError: Error {
case missingCDN
case invalidXML
case missingRequired
}
extension XHXMLParser {
static func parse(xmlString: String) throws -> ParseResult {
guard let data = xmlString.data(using: .utf8) else {
throw ParserError.invalidXML
}
return try parseProductsXML(xmlData: data)
}
}