SOURCE RECORD · S11
S11 · 计算器 - 高精度计算工作台
ECB data request XML parsing and cache fallback
保留原行号。个人/账户信息如有删除,已用 REDACTED 标识。原文件 SHA-256 只用于识别截取时的本机原文件;不能证明编写或提交时间,也不等于脱敏下载文件的哈希。
Original line numbers are retained. Redactions are explicitly marked. The original full-file hash identifies a captured local file, not its creation/submission time or the redacted download. Public-file hashes are listed separately. This is selected source, not a whole-repository or binary absence finding.
Project key: calc
Original relative path: Packages/PrecisionCalcKit/Sources/CalcSystemIntegration/SystemIntegration.swift
Captured: 2026-09-12T18:57:15+08:00
Original full-file SHA256: 1fb932a35430ec0bec8fc612bbbb251283e749c30dad8df59702221fc951a623
Evidence level: local source excerpt
Build correspondence: not yet established with an Apple-received binary.
Line numbers refer to the captured original file. Gaps are explicitly marked. No source code was changed.
Original lines 76 to 150
0076 public let source: String
0077 public let baseCurrency: String
0078 public let rates: [String: BigDecimal]
0079 public let retrievedAt: Date
0080
0081 public init(date: Date, source: String, baseCurrency: String = "EUR", rates: [String: BigDecimal], retrievedAt: Date = Date()) {
0082 self.date = date; self.source = source; self.baseCurrency = baseCurrency; self.rates = rates; self.retrievedAt = retrievedAt
0083 }
0084 }
0085
0086 public protocol ExchangeRateProvider: Sendable { func latestRates() async throws -> ExchangeRateSnapshot }
0087
0088 public enum ExchangeRateError: Error, Sendable, Hashable, LocalizedError {
0089 case invalidResponse, missingCurrency(String), invalidXML, noCache
0090 public var errorDescription: String? {
0091 switch self { case .invalidResponse: "汇率服务响应无效"; case .missingCurrency(let code): "参考源没有提供 \(code) 汇率"; case .invalidXML: "无法解析参考汇率数据"; case .noCache: "没有可用的本地汇率快照" }
0092 }
0093 }
0094
0095 public struct ECBExchangeRateProvider: ExchangeRateProvider {
0096 public let endpoint: URL
0097 public let session: URLSession
0098 public init(endpoint: URL = URL(string: "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml") ?? URL(fileURLWithPath: "/invalid-ecb-endpoint"), session: URLSession = .shared) { self.endpoint = endpoint; self.session = session }
0099
0100 public func latestRates() async throws -> ExchangeRateSnapshot {
0101 var request = URLRequest(url: endpoint, timeoutInterval: 10)
0102 request.setValue("PrecisionCalc/1.0", forHTTPHeaderField: "User-Agent")
0103 let (data, response) = try await session.data(for: request)
0104 guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { throw ExchangeRateError.invalidResponse }
0105 return try Self.parse(data)
0106 }
0107
0108 public static func parse(_ data: Data) throws -> ExchangeRateSnapshot {
0109 let delegate = ECBParserDelegate()
0110 let parser = XMLParser(data: data)
0111 parser.delegate = delegate
0112 guard parser.parse(), !delegate.rates.isEmpty else { throw ExchangeRateError.invalidXML }
0113 var rates = delegate.rates
0114 rates["EUR"] = .one
0115 return ExchangeRateSnapshot(date: delegate.date ?? Date(), source: "European Central Bank", rates: rates)
0116 }
0117 }
0118
0119 private final class ECBParserDelegate: NSObject, XMLParserDelegate, @unchecked Sendable {
0120 var rates: [String: BigDecimal] = [:]
0121 var date: Date?
0122 func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String: String] = [:]) {
0123 if let value = attributeDict["time"] { date = ISO8601DateFormatter.precisionCalcDay.date(from: value) }
0124 if let currency = attributeDict["currency"], let rate = attributeDict["rate"], let decimal = try? BigDecimal(parsing: rate) { rates[currency] = decimal }
0125 }
0126 }
0127
0128 public actor CachedExchangeRateProvider: ExchangeRateProvider {
0129 private let upstream: any ExchangeRateProvider
0130 private let defaults: UserDefaults
0131 private let key = "precisioncalc.exchange-rate.snapshot"
0132 public init(upstream: any ExchangeRateProvider, suiteName: String = "group.com.calculatorx.shared") {
0133 self.upstream = upstream
0134 self.defaults = UserDefaults(suiteName: suiteName) ?? .standard
0135 }
0136 public func latestRates() async throws -> ExchangeRateSnapshot {
0137 do {
0138 let snapshot = try await upstream.latestRates()
0139 if let data = try? JSONEncoder().encode(snapshot) { defaults.set(data, forKey: key) }
0140 return snapshot
0141 } catch {
0142 guard let data = defaults.data(forKey: key), let snapshot = try? JSONDecoder().decode(ExchangeRateSnapshot.self, from: data) else { throw error }
0143 return snapshot
0144 }
0145 }
0146 }
0147
0148 public struct BundledSnapshotProvider: ExchangeRateProvider {
0149 public init() {}
0150 public func latestRates() async throws -> ExchangeRateSnapshot {