import Foundation import CryptoKit // Golomb-Coded Set (GCS) filter utilities for sync. // Hashing: // - Packet ID is 16 bytes (see PacketIdUtil). For GCS mapping, use h64 = first 8 bytes of SHA-256 over the 16-byte ID. // - Map to [1, M) by computing (h64 % M) and remapping 0 -> 1 to avoid zero-length deltas. // Encoding (v1): // - Sort mapped values ascending; encode deltas (first is v0, then vi - v{i-1}) as positive integers x >= 1. // - Golomb-Rice with parameter P: q = (x - 1) >> P encoded as unary (q ones then a zero), then write P-bit remainder r = (x - 1) & ((1<
Int {
let f = max(0.000001, min(0.25, targetFpr))
// ceil(log2(1/f))
let p = Int(ceil(log2(1.0 / f)))
return max(1, p)
}
// Estimate max elements that fit in size bytes: bits per element ~= P + 2 (approx)
static func estimateMaxElements(sizeBytes: Int, p: Int) -> Int {
let bits = max(8, sizeBytes * 8)
let per = max(3, p + 2)
return max(1, bits / per)
}
static func buildFilter(ids: [Data], maxBytes: Int, targetFpr: Double) -> Params {
let p = deriveP(targetFpr: targetFpr)
guard !ids.isEmpty else {
return Params(p: p, m: 1, data: Data())
}
let cap = estimateMaxElements(sizeBytes: maxBytes, p: p)
let selected = Array(ids.prefix(cap))
let range = max(1, hashRange(count: selected.count, p: p))
let modulo = UInt64(range)
var mapped = selected
.map { h64($0) }
.map { mapHash($0, modulo: modulo) }
.sorted()
mapped = normalizeMappedValues(mapped, modulo: modulo)
if mapped.isEmpty {
return Params(p: p, m: range, data: Data())
}
var encoded = encode(sorted: mapped, p: p)
var trimmedCount = mapped.count
while encoded.count > maxBytes && trimmedCount > 0 {
if trimmedCount == 1 {
mapped.removeAll()
encoded = Data()
break
}
trimmedCount = max(1, (trimmedCount * 9) / 10)
mapped = Array(mapped.prefix(trimmedCount))
encoded = encode(sorted: mapped, p: p)
}
return Params(p: p, m: range, data: encoded)
}
static func decodeToSortedSet(p: Int, m: UInt32, data: Data) -> [UInt64] {
var values: [UInt64] = []
let reader = BitReader(data)
var acc: UInt64 = 0
while true {
guard let q = reader.readUnary() else { break }
guard let r = reader.readBits(count: p) else { break }
let x = (UInt64(q) << UInt64(p)) + UInt64(r) + 1
acc &+= x
if acc >= UInt64(m) { break }
values.append(acc)
}
return values
}
static func contains(sortedValues: [UInt64], candidate: UInt64) -> Bool {
var lo = 0
var hi = sortedValues.count - 1
while lo <= hi {
let mid = (lo + hi) >> 1
let v = sortedValues[mid]
if v == candidate { return true }
if v < candidate { lo = mid + 1 } else { hi = mid - 1 }
}
return false
}
static func bucket(for id: Data, modulus m: UInt32) -> UInt64 {
let modulo = UInt64(max(1, m))
guard modulo > 1 else { return 0 }
return mapHash(h64(id), modulo: modulo)
}
private static func h64(_ id16: Data) -> UInt64 {
var hasher = SHA256()
hasher.update(data: id16)
let d = hasher.finalize()
let db = Data(d)
var x: UInt64 = 0
let take = min(8, db.count)
for i in 0..