Addresses a multi-agent code review (26 confirmed findings + a URLSession leak) across correctness, concurrency, security, and cleanup: - MongoDocument.id: deterministic fallback (was UUID() on every access) to stop SwiftUI List/ForEach identity churn; getAll pagination drives its cursor off the real _id via documentId, so a missing _id stops paging instead of looping on a fabricated cursor. - View models: per-load generation guard (+ captured-tab guard on ONU detail) so a cancelled or overlapping load can't commit stale results or land data on the wrong tab. - JSONValue.intValue: guard Int(d) against NaN/inf/out-of-range (crash). - ServerTrustEvaluator: hash the real SubjectPublicKeyInfo (prepend the algorithm-specific ASN.1 SPKI header) so a standard openssl-captured public-key pin actually matches; unknown key types fail closed. - AppEnvironment.configure: clear stale cookies + Keychain creds on host change. - FirmwareFile.isCompatible: empty metadata no longer passes as compatible-for-all. - APIClient: invalidate URLSession on deinit (per-configure/probe leak). - Dashboard best-effort sections keep last-good on transient failure; unified optical thresholds; LoginView prefill-once; populatedBuckets unique ForEach id; Format rounding rollover; dead-code/docstring/dedup cleanups. Verification pending on the Mac (no Swift toolchain on the Linux dev box). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
61 lines
3.3 KiB
Swift
61 lines
3.3 KiB
Swift
import Foundation
|
|
|
|
/// One uploaded ONU firmware image, from `GET /files/onu-firmware/`.
|
|
struct FirmwareFile: MongoDocument {
|
|
let raw: JSONValue
|
|
init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
|
|
init(raw: JSONValue) { self.raw = raw }
|
|
|
|
var id: String { raw["_id"]?.stringValue ?? filename ?? raw.compactDescription }
|
|
var filename: String? { raw["filename"]?.stringValue ?? raw["_id"]?.stringValue }
|
|
var version: String? { raw["metadata"]?["Version"]?.stringValue }
|
|
var manufacturer: String? { raw["metadata"]?["Compatible Manufacturer"]?.stringValue }
|
|
var models: [String] { raw["metadata"]?["Compatible Model"]?.arrayValue?.compactMap(\.stringValue) ?? [] }
|
|
var sizeBytes: Double? { raw["length"]?.doubleValue }
|
|
var uploadDate: String? { raw["uploadDate"]?.stringValue }
|
|
|
|
/// True only when this image POSITIVELY targets the given ONU: manufacturer
|
|
/// matches the ONU vendor (when both are known) AND the ONU's equipment id is
|
|
/// one of the compatible models (exact, case-insensitive — substring matching
|
|
/// would wrongly pass e.g. an "XGS2110" image for an "FTXGS2110B" ONU).
|
|
///
|
|
/// An image with NO declared compatibility metadata (empty manufacturer/models),
|
|
/// or when the ONU's equipment id is unknown, is treated as NOT compatible — for
|
|
/// a destructive flash, an un-typed image must be chosen deliberately via the
|
|
/// sheet's "Show all" escape hatch rather than appearing as compatible-for-all.
|
|
func isCompatible(vendor: String?, equipmentId: String?) -> Bool {
|
|
if let vendor, let manufacturer, !manufacturer.isEmpty,
|
|
manufacturer.caseInsensitiveCompare(vendor) != .orderedSame { return false }
|
|
guard let equipmentId, !models.isEmpty else { return false }
|
|
return models.contains { $0.caseInsensitiveCompare(equipmentId) == .orderedSame }
|
|
}
|
|
}
|
|
|
|
/// Procedure 7 (per-ONU firmware upgrade) as a pure transform so it can be
|
|
/// unit-tested before it ever touches a live device.
|
|
enum FirmwareUpgrade {
|
|
/// Mutate a full ONU-CFG document to stage `filename`/`version` in the ONU's
|
|
/// **inactive** firmware bank and point at it. Writes to the inactive bank so
|
|
/// the running image is preserved (§6.3): `FW Bank Ptr` 0→1, 1→0, anything
|
|
/// else (incl. 65535 unset) → 1. Returns the new document + the slot written,
|
|
/// or nil if the document isn't a recognizable ONU-CFG.
|
|
static func apply(to config: JSONValue, filename: String, version: String) -> (document: JSONValue, slot: Int)? {
|
|
guard var root = config.objectValue, var onu = root["ONU"]?.objectValue else { return nil }
|
|
|
|
let currentPtr = onu["FW Bank Ptr"]?.intValue ?? 65535
|
|
let slot = (currentPtr == 1) ? 0 : 1
|
|
|
|
var files = onu["FW Bank Files"]?.arrayValue ?? []
|
|
var versions = onu["FW Bank Versions"]?.arrayValue ?? []
|
|
while files.count <= slot { files.append(.string("")) }
|
|
while versions.count <= slot { versions.append(.string("")) }
|
|
files[slot] = .string(filename)
|
|
versions[slot] = .string(version)
|
|
|
|
onu["FW Bank Files"] = .array(files)
|
|
onu["FW Bank Versions"] = .array(versions)
|
|
onu["FW Bank Ptr"] = .int(slot)
|
|
root["ONU"] = .object(onu)
|
|
return (.object(root), slot)
|
|
}
|
|
}
|