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) } }