import Foundation /// Pure, side-effect-free parsing of `nettop` CSV output. Kept separate from the /// process-spawning so it can be unit-tested against captured fixtures. /// /// Expected invocation: `nettop -L 1 -x -n -J time,bytes_in,bytes_out` /// which yields lines like: /// /// time,,bytes_in,bytes_out, /// 23:49:56.574,apsd.571,15671580,11326466, /// 23:49:56.573,tcp4 192.168.10.194:57359<->17.57.144.184:5223,15671580,11326466, /// /// Process rows carry `Name.PID` in column 2; the connection sub-rows that follow /// inherit that process until the next process row. public enum NettopParser { public static func parseConnections(_ raw: String) -> [Connection] { var result: [Connection] = [] var currentProcess = "unknown" var currentPID: Int32 = -1 for rawLine in raw.split(separator: "\n", omittingEmptySubsequences: true) { // Don't omit empty subsequences: byte columns can legitimately be empty. let fields = rawLine.split(separator: ",", omittingEmptySubsequences: false) .map(String.init) guard fields.count >= 2 else { continue } let key = fields[1] if key.isEmpty || fields[0] == "time" { continue } // header / blank if key.contains("<->") { if let conn = parseConnectionRow( key: key, bytesIn: fields.count > 2 ? fields[2] : "", bytesOut: fields.count > 3 ? fields[3] : "", processName: currentProcess, pid: currentPID ) { result.append(conn) } } else if let (name, pid) = parseProcessKey(key) { currentProcess = name currentPID = pid } } return result } // MARK: - Rows /// `mDNSResponder.647` -> ("mDNSResponder", 647). The suffix after the final /// dot must be all digits, otherwise it's not a process header. static func parseProcessKey(_ key: String) -> (String, Int32)? { guard let dot = key.lastIndex(of: ".") else { return nil } let pidPart = key[key.index(after: dot)...] guard !pidPart.isEmpty, pidPart.allSatisfy(\.isNumber), let pid = Int32(pidPart) else { return nil } return (String(key[key.startIndex..17.57.144.184:5223` -> Connection static func parseConnectionRow( key: String, bytesIn: String, bytesOut: String, processName: String, pid: Int32 ) -> Connection? { guard let space = key.firstIndex(of: " ") else { return nil } let protoStr = String(key[key.startIndex..") else { return nil } let localStr = String(tuple[tuple.startIndex.. Endpoint { let sep: Character = isV6 ? "." : ":" guard let idx = s.lastIndex(of: sep) else { return Endpoint(host: cleanHost(s), port: nil) } let host = cleanHost(String(s[s.startIndex.. `fe80::1`. static func cleanHost(_ h: String) -> String { if let pct = h.firstIndex(of: "%") { return String(h[h.startIndex..