PONGo_ios/APIQuery.swift
Jon Vanvik 203465913c Apply code-review fixes: identity/pagination, load races, TLS pin, leaks
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>
2026-05-31 16:36:49 +02:00

75 lines
3.1 KiB
Swift

import Foundation
/// Builder for the MCMS list-endpoint query parameters.
///
/// Supported by most list endpoints: `query`, `projection`, `sort`, `limit`
/// (server default 1000), `skip`, `next` (cursor = a document `_id`), `distinct`.
/// Stats/log endpoints additionally require `start-time` and accept `end-time`
/// (UTC `yyyy-MM-dd HH:mm:ss`).
struct APIQuery {
/// Custom Mongo query as `key=value` pairs (joined with commas by the API).
var query: [String: String] = [:]
/// Field inclusion/exclusion: `1` include, `0` exclude.
var projection: [String: Int] = [:]
/// Sort field direction (`1` asc, `-1` desc).
var sort: [String: Int] = [:]
var limit: Int?
var skip: Int?
/// Pagination cursor: excludes all items at/before this `_id`.
var next: String?
/// Attribute to fetch unique values for.
var distinct: String?
var startTime: Date?
var endTime: Date?
static let empty = APIQuery()
private static let timeFormatter: DateFormatter = {
let f = DateFormatter()
f.locale = Locale(identifier: "en_US_POSIX")
f.timeZone = TimeZone(identifier: "UTC")
f.dateFormat = "yyyy-MM-dd HH:mm:ss"
return f
}()
/// NOTE: `query`/`projection`/`sort` are flattened with literal `=`/`,`
/// separators that MCMS uses to split pairs. Keys/values are NOT escaped, so a
/// key or value that itself contains `=` or `,` will be mis-parsed into the
/// wrong filter. Current callers only use simple equality on safe field names;
/// keep it that way (or escape per the MCMS parser's rules) see field guide §3.8.
func queryItems() -> [URLQueryItem] {
var items: [URLQueryItem] = []
if !query.isEmpty {
let joined = query.map { "\($0)=\($1)" }.joined(separator: ",")
items.append(.init(name: "query", value: joined))
}
if !projection.isEmpty {
let joined = projection.map { "\($0)=\($1)" }.joined(separator: ",")
items.append(.init(name: "projection", value: joined))
}
if !sort.isEmpty {
let joined = sort.map { "\($0)=\($1)" }.joined(separator: ",")
items.append(.init(name: "sort", value: joined))
}
if let limit { items.append(.init(name: "limit", value: "\(limit)")) }
if let skip { items.append(.init(name: "skip", value: "\(skip)")) }
if let next { items.append(.init(name: "next", value: next)) }
if let distinct { items.append(.init(name: "distinct", value: distinct)) }
if let startTime {
items.append(.init(name: "start-time", value: Self.timeFormatter.string(from: startTime)))
}
if let endTime {
items.append(.init(name: "end-time", value: Self.timeFormatter.string(from: endTime)))
}
return items
}
/// Convenience for a recent time window on stats/log endpoints.
static func window(lastHours hours: Int, limit: Int? = nil) -> APIQuery {
var q = APIQuery()
q.startTime = Calendar.current.date(byAdding: .hour, value: -hours, to: Date())
q.limit = limit
return q
}
}