diff options
| author | Dustin Mierau <dustin@mierau.me> | 2024-01-12 09:50:18 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2024-01-12 09:50:18 -0800 |
| commit | 2f210991684c87073dd7534d2d0416d91cf50275 (patch) | |
| tree | 03dc2edb225ab583fbc286de37f927194377a085 /Hotline | |
| parent | 768bc9136c91b5dc31eef96088e7d1796243d81b (diff) | |
Experimenting with a simpler stream/delegate bsaed socket library for Tracker listing for now. Added consume methods to Data and [UInt8] arrays to make reading bytes off the wire easier to read..
Diffstat (limited to 'Hotline')
| -rw-r--r-- | Hotline/Hotline/HotlineTrackerClient.swift | 300 | ||||
| -rw-r--r-- | Hotline/Models/Hotline.swift | 12 | ||||
| -rw-r--r-- | Hotline/Shared/NetSocket.swift | 227 | ||||
| -rw-r--r-- | Hotline/Utility/FoundationExtensions.swift | 261 | ||||
| -rw-r--r-- | Hotline/macOS/TrackerView.swift | 25 |
5 files changed, 634 insertions, 191 deletions
diff --git a/Hotline/Hotline/HotlineTrackerClient.swift b/Hotline/Hotline/HotlineTrackerClient.swift index 350db43..bd5c937 100644 --- a/Hotline/Hotline/HotlineTrackerClient.swift +++ b/Hotline/Hotline/HotlineTrackerClient.swift @@ -1,12 +1,6 @@ import Foundation import Network -enum HotlineTrackerStatus: Int { - case disconnected - case connecting - case connected -} - struct HotlineTracker: Identifiable, Equatable { let id: UUID = UUID() var address: String @@ -22,197 +16,129 @@ struct HotlineTracker: Identifiable, Equatable { } } +enum HotlineTrackerStatus: Int { + case disconnected + case connecting + case connected +} + +private enum HotlineTrackerStage { + case magic + case header + case listing + case done +} + class HotlineTrackerClient { - static let magicPacket = Data([ + static let MagicPacket: [UInt8] = [ 0x48, 0x54, 0x52, 0x4B, // 'HTRK' 0x00, 0x01 // Version - ]) + ] private var tracker: HotlineTracker private var connectionStatus: HotlineTrackerStatus = .disconnected private var servers: [HotlineServer] = [] + + private var socket: NetSocket = NetSocket() + private var stage: HotlineTrackerStage = .magic - private var serverAddress: NWEndpoint.Host - private var serverPort: NWEndpoint.Port - private var connection: NWConnection? - private var bytes = Data() - private var maxDataLength: Int = 0 + private var serverAddress: String + private var serverPort: Int + private var expectedDataLength: Int = 0 private var serverCount: Int = 0 + private var fetchContinuation: CheckedContinuation<[HotlineServer], any Error>? + init() { let t = HotlineTracker("hltracker.com") self.tracker = t - self.serverAddress = NWEndpoint.Host(t.address) - self.serverPort = NWEndpoint.Port(rawValue: t.port)! + self.serverAddress = t.address + self.serverPort = Int(t.port) + self.socket.delegate = self } init(tracker: HotlineTracker) { self.tracker = tracker - self.serverAddress = NWEndpoint.Host(tracker.address) - self.serverPort = NWEndpoint.Port(rawValue: tracker.port)! + self.serverAddress = tracker.address + self.serverPort = Int(tracker.port) + self.socket.delegate = self } - func fetchServers(address: String, port: Int, callback: (([HotlineServer]) -> Void)? = nil) async -> [HotlineServer] { - self.serverAddress = NWEndpoint.Host(address) - self.serverPort = NWEndpoint.Port(rawValue: UInt16(port))! + @MainActor func fetchServers(address: String, port: Int) async throws -> [HotlineServer] { + self.serverAddress = address + self.serverPort = Int(port) self.reset() - return await withCheckedContinuation { [weak self] continuation in - self?.connect { [weak self] in - continuation.resume(returning: self?.servers ?? []) - } + return try await withCheckedThrowingContinuation { [weak self] continuation in + self?.fetchContinuation = continuation + self?.connect() } } - private func reset() { - self.maxDataLength = 0 + @MainActor func close() { + self.socket.close() + } + + // MARK: - + + @MainActor private func reset() { + self.expectedDataLength = 0 self.serverCount = 0 self.servers = [] } - private func connect(_ callback: (() -> Void)? = nil) { - self.connection = NWConnection(host: self.serverAddress, port: self.serverPort, using: .tcp) - self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in - switch newState { - case .ready: - self?.connectionStatus = .connected - self?.sendMagic() - case .cancelled: - self?.connectionStatus = .disconnected - DispatchQueue.main.async { - callback?() - } - case .failed(let err): - print("HotlineTrackerClient: Connection error \(err)") - self?.connectionStatus = .disconnected - DispatchQueue.main.async { - callback?() - } - default: - return - } - } + @MainActor private func connect() { + self.socket.close() self.connectionStatus = .connecting - self.connection?.start(queue: .global()) - } - - func disconnect() { - self.connection?.cancel() - self.connection = nil + self.socket.connect(host: self.serverAddress, port: self.serverPort) + self.socket.write(HotlineTrackerClient.MagicPacket) } - private func sendMagic() { - guard let c = connection else { - print("HotlineTrackerClient: invalid connection to send magic.") + @MainActor private func receiveMagic() { + guard self.stage == .magic, self.socket.available >= HotlineTrackerClient.MagicPacket.count else { return } - // let packet: [UInt8] = [0x48, 0x54, 0x52, 0x4B, 0x00, self.serverVersion] + let magic: [UInt8] = self.socket.read(count: HotlineTrackerClient.MagicPacket.count) - c.send(content: HotlineTrackerClient.magicPacket, completion: .contentProcessed { [weak self] (error) in - if let err = error { - print("HotlineTrackerClient: sending magic failed \(err)") - return - } - - self?.receiveMagic() - }) - } - - private func receiveMagic() { - guard let c = connection else { - print("HotlineTrackerClient: invalid connection to receive magic.") + if magic != HotlineTrackerClient.MagicPacket { + self.socket.close() return } - c.receive(minimumIncompleteLength: 6, maximumLength: 6) { [weak self] (data, context, isComplete, error) in - guard let self = self, let data = data else { - return - } - - if data.isEmpty || !data.elementsEqual(HotlineTrackerClient.magicPacket) { - print("HotlineTrackerClient: invalid magic response") - self.disconnect() - return - } - - if let error = error { - print("HotlineTrackerClient: receive error \(error)") - } - else { - self.receiveHeader() - } - } + self.stage = .header + self.receiveHeader() } - private func receiveHeader() { - guard let c = connection else { - print("HotlineTrackerClient: invalid connection to receive header.") + @MainActor private func receiveHeader() { + guard self.stage == .header, self.socket.available >= 8 else { return } - c.receive(minimumIncompleteLength: 8, maximumLength: 8) { [weak self] (data, context, isComplete, error) in - guard let self = self else { - return - } - - if let error = error { - print("HotlineTrackerClient: receive error \(error)") - self.disconnect() - return - } - - if let data = data, !data.isEmpty { - self.maxDataLength = Int(data[2]) * 0xFF + Int(data[3]) - self.maxDataLength -= 4 - self.serverCount = Int(data[4]) * 256 + Int(data[5]) - } - - if let error = error { - print("HotlineTrackerClient: receive error \(error)") - } - else { - self.receiveListing() - } + let header: [UInt8] = self.socket.read(count: 8) + if header.count != 8 { + return } + + self.expectedDataLength = Int(header[2]) * 0xFF + Int(header[3]) + self.expectedDataLength -= 4 + self.serverCount = Int(header[4]) * 256 + Int(header[5]) + + self.stage = .listing + self.receiveListing() } - private func receiveListing() { - guard let c = connection else { - print("HotlineTrackerClient: invalid connection to receive data.") + @MainActor private func receiveListing() { + guard self.stage == .listing, self.socket.available >= self.expectedDataLength else { return } - c.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { [weak self] (data, context, isComplete, error) in - guard let self = self else { - return - } - - if let data = data, !data.isEmpty { - self.bytes.append(contentsOf: data) - - if bytes.count >= maxDataLength { - self.parseListing() - print("HotlineTrackerClient: Found \(self.servers.count) servers on tracker \(self.serverAddress):\(self.serverPort)") - self.disconnect() - return - } - } - - if let error = error { - print("HotlineTrackerClient: receive error \(error)") - self.disconnect() - } - else { - print("HotlineTrackerClient: not complete") - self.receiveListing() - } - } + self.parseListing(self.socket.read(count: self.expectedDataLength)) } - private func parseListing() { + @MainActor private func parseListing(_ listingBytes: [UInt8]) { // IP address (4 bytes) // Port number (2 bytes) // Number of users (2 bytes) @@ -222,41 +148,77 @@ class HotlineTrackerClient { // Description size (1 byte) // Description (description size) + var bytes: [UInt8] = listingBytes let trackerSeparatorRegex = /^[-]+$/ var foundServers: [HotlineServer] = [] - var cursor = 0 for _ in 1...self.serverCount { - if self.bytes.count < cursor + 12 { - print("HotlineTrackerClient: Data isn't long enough for next server") - break - } - if - let ip_1 = self.bytes.readUInt8(at: cursor), - let ip_2 = self.bytes.readUInt8(at: cursor + 1), - let ip_3 = self.bytes.readUInt8(at: cursor + 2), - let ip_4 = self.bytes.readUInt8(at: cursor + 3), - let port = self.bytes.readUInt16(at: cursor + 4), - let userCount = self.bytes.readUInt16(at: cursor + 6) { - - let (serverName, nameByteCount) = self.bytes.readPString(at: cursor + 10) - let (serverDescription, descByteCount) = self.bytes.readPString(at: cursor + 10 + nameByteCount) + let ip_1 = bytes.consumeUInt8(), + let ip_2 = bytes.consumeUInt8(), + let ip_3 = bytes.consumeUInt8(), + let ip_4 = bytes.consumeUInt8(), + let port = bytes.consumeUInt16(), + let _ = bytes.consumeBytes(2), + let userCount = bytes.consumeUInt16(), + let serverName = bytes.consumePString(), + let serverDescription = bytes.consumePString() { - if let name = serverName, - let desc = serverDescription { - // Ignore servers that are just used as dividers in the tracker listing. - let validName = try? trackerSeparatorRegex.prefixMatch(in: name) - if validName == nil { - let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: name, description: desc) - foundServers.append(server) - } + // Ignore servers that are just used as dividers in the tracker listing. + let validName = try? trackerSeparatorRegex.prefixMatch(in: serverName) + if validName == nil { + let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: serverName, description: serverDescription) + foundServers.append(server) } - - cursor += 10 + nameByteCount + descByteCount + } + else { + print("HotlineTrackerClient: Data isn't long enough for next server") + break } } self.servers = foundServers + + self.stage = .done + self.socket.close() + } +} + +// MARK: - + +extension HotlineTrackerClient: NetSocketDelegate { + @MainActor func netsocketConnected(socket: NetSocket) { + self.connectionStatus = .connected + } + + @MainActor func netsocketDisconnected(socket: NetSocket, error: Error?) { + self.stage = .magic + self.connectionStatus = .disconnected + + let servers = self.servers + self.reset() + + if let continuation = self.fetchContinuation { + self.fetchContinuation = nil + if let err = error { + continuation.resume(throwing: err) + } + else { + continuation.resume(returning: servers) + } + } + } + + @MainActor func netsocketReceived(socket: NetSocket, bytes: [UInt8]) { + switch self.stage { + case .magic: + self.receiveMagic() + case .header: + self.receiveHeader() + case .listing: + self.receiveListing() + case .done: + break + } } } diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift index e62ac15..445d09e 100644 --- a/Hotline/Models/Hotline.swift +++ b/Hotline/Models/Hotline.swift @@ -223,13 +223,13 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { // MARK: - @MainActor func getServerList(tracker: String, port: Int = HotlinePorts.DefaultTrackerPort) async -> [Server] { - let fetchedServers: [HotlineServer] = await self.trackerClient.fetchServers(address: tracker, port: port) - var servers: [Server] = [] - for s in fetchedServers { - if let serverName = s.name { - servers.append(Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users))) + if let fetchedServers: [HotlineServer] = try? await self.trackerClient.fetchServers(address: tracker, port: port) { + for s in fetchedServers { + if let serverName = s.name { + servers.append(Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users))) + } } } @@ -237,7 +237,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { } @MainActor func disconnectTracker() { - self.trackerClient.disconnect() + self.trackerClient.close() } @MainActor func login(server: Server, username: String, iconID: Int, callback: ((Bool) -> Void)? = nil) { diff --git a/Hotline/Shared/NetSocket.swift b/Hotline/Shared/NetSocket.swift new file mode 100644 index 0000000..e474f5a --- /dev/null +++ b/Hotline/Shared/NetSocket.swift @@ -0,0 +1,227 @@ + +// NetSocket.swift +// A simple delegate based buffered read/write TCP socket. +// Created by Dustin Mierau + +import Foundation + +protocol NetSocketDelegate: AnyObject { + func netsocketConnected(socket: NetSocket) + func netsocketDisconnected(socket: NetSocket, error: Error?) + func netsocketReceived(socket: NetSocket, bytes: [UInt8]) + func netsocketSent(socket: NetSocket, count: Int) +} + +extension NetSocketDelegate { + func netsocketConnected(socket: NetSocket) {} + func netsocketDisconnected(socket: NetSocket, error: Error?) {} + func netsocketReceived(socket: NetSocket, bytes: [UInt8]) {} + func netsocketSent(socket: NetSocket, count: Int) {} +} + +enum NetSocketStatus { + case disconnected + case connecting + case connected +} + +final class NetSocket: NSObject, StreamDelegate { + weak var delegate: NetSocketDelegate? = nil + + private var output: OutputStream? = nil + private var input: InputStream? = nil + + private var outputBuffer: [UInt8] = [] + private var inputBuffer: [UInt8] = [] + + private var readBuffer: [UInt8] = Array(repeating: 0, count: 4 * 1024) + + public func peek() -> [UInt8] { self.inputBuffer } + public var available: Int { self.inputBuffer.count } + + private var status: NetSocketStatus = .disconnected + + override init() {} + + @MainActor public func connect(host: String, port: Int) { + self.close() + + var outputStream: OutputStream? = nil + var inputStream: InputStream? = nil + + self.status = .connecting + + Stream.getStreamsToHost(withName: host, port: port, inputStream: &inputStream, outputStream: &outputStream) + + self.input = inputStream + self.output = outputStream + + inputStream?.delegate = self + outputStream?.delegate = self + + inputStream?.schedule(in: .current, forMode: .common) + outputStream?.schedule(in: .current, forMode: .common) + + inputStream?.open() + outputStream?.open() + } + + @MainActor public func close(_ err: Error? = nil) { + let disconnected = (self.status != .disconnected) + + self.status = .disconnected + + self.input?.delegate = nil + self.output?.delegate = nil + self.input?.close() + self.output?.close() + self.input?.remove(from: .current, forMode: .common) + self.output?.remove(from: .current, forMode: .common) + self.input = nil + self.output = nil + self.inputBuffer = [] + self.outputBuffer = [] + + if disconnected { + self.delegate?.netsocketDisconnected(socket: self, error: err) + } + + print("NetSocket: Closed") + } + + @MainActor public func write(_ data: Data) { + guard let output = self.output else { + return + } + + self.outputBuffer.append(contentsOf: data) + + if output.hasSpaceAvailable { + self.writeBufferToStream() + } + } + + @MainActor public func write(_ data: [UInt8]) { + guard let output = self.output else { + return + } + + self.outputBuffer.append(contentsOf: data) + + if output.hasSpaceAvailable { + self.writeBufferToStream() + } + } + + @MainActor public func read(count: Int) -> [UInt8] { + guard self.inputBuffer.count > 0, count > 0 else { + return [] + } + + let amountToRead = min(count, self.inputBuffer.count) + let dataRead: [UInt8] = Array(self.inputBuffer[0..<amountToRead]) + self.inputBuffer.removeFirst(amountToRead) + + return dataRead + } + + @MainActor public func read(count: Int) -> Data { + guard self.inputBuffer.count > 0, count > 0 else { + return Data() + } + + let amountToRead = min(count, self.inputBuffer.count) + + let dataRead: Data = Data(self.inputBuffer[0..<amountToRead]) + self.inputBuffer.removeFirst(amountToRead) + + return dataRead + } + + @MainActor public func readAll() -> [UInt8] { + guard self.inputBuffer.count > 0 else { + return [] + } + + let dataRead: [UInt8] = Array(self.inputBuffer) + self.inputBuffer = [] + + return dataRead + } + + @MainActor public func readAll() -> Data { + guard self.inputBuffer.count > 0 else { + return Data() + } + + let dataRead: Data = Data(self.inputBuffer) + self.inputBuffer = [] + + return dataRead + } + + @MainActor private func writeBufferToStream() { + guard let output = self.output, self.outputBuffer.count > 0 else { + return + } + + let bytesWritten = output.write(self.outputBuffer, maxLength: self.outputBuffer.count) + if bytesWritten > 0 { + self.outputBuffer.removeFirst(bytesWritten) + self.delegate?.netsocketSent(socket: self, count: bytesWritten) + } + else if bytesWritten == -1 { + self.close(output.streamError) + } + } + + @MainActor private func readStreamToBuffer() { + guard let input = self.input else { + return + } + + let bytesRead = input.read(&self.readBuffer, maxLength: self.readBuffer.capacity) + if bytesRead > 0 { + self.inputBuffer.append(contentsOf: readBuffer[0..<bytesRead]) + self.delegate?.netsocketReceived(socket: self, bytes: self.inputBuffer) + } + else if bytesRead == -1 { + self.close(input.streamError) + } + } + + // MARK: - + + @MainActor func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + guard let input = self.input, let output = self.output else { + return + } + + switch eventCode { + case .openCompleted: + if input.streamStatus == .open && output.streamStatus == .open { + if self.status == .connecting { + print("NetSocket: Connected") + self.status = .connected + self.delegate?.netsocketConnected(socket: self) + } + } + break + case .hasBytesAvailable: + print("NetSocket: Data available") + self.readStreamToBuffer() + case .hasSpaceAvailable: + print("NetSocket: Space available") + self.writeBufferToStream() + case .endEncountered: + print("NetSocket: End encounterd") + self.close() + case .errorOccurred: + print("NetSocket: Error encounterd", input.streamError.debugDescription, output.streamError.debugDescription) + let err = input.streamError ?? output.streamError + self.close(err) + default: + break + } + } +} diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift index 3183b39..edb8bee 100644 --- a/Hotline/Utility/FoundationExtensions.swift +++ b/Hotline/Utility/FoundationExtensions.swift @@ -5,6 +5,267 @@ enum Endianness { case little } +extension Array where Element == UInt8 { + init(_ val: UInt8) { + self.init() + self.appendUInt8(val) + } + + init(_ val: UInt16) { + self.init() + self.appendUInt16(val) + } + + init(_ val: UInt32) { + self.init() + self.appendUInt32(val) + } + + init(_ val: UInt64) { + self.init() + self.appendUInt64(val) + } + + mutating func consumeUInt8() -> UInt8? { + guard let val = self.readUInt8(at: 0) else { + return nil + } + + self.removeFirst(1) + return val + } + + mutating func consumeUInt16() -> UInt16? { + guard let val = self.readUInt16(at: 0) else { + return nil + } + + self.removeFirst(2) + return val + } + + mutating func consumeUInt32() -> UInt32? { + guard let val = self.readUInt32(at: 0) else { + return nil + } + + self.removeFirst(4) + return val + } + + mutating func consumeUInt64() -> UInt64? { + guard let val = self.readUInt64(at: 0) else { + return nil + } + + self.removeFirst(8) + return val + } + + mutating func consumeBytes(_ length: Int) -> Data? { + guard let val = self.readData(at: 0, length: length) else { + return nil + } + + self.removeFirst(length) + return val + } + + mutating func consumeDate() -> Date? { + guard let date = self.readDate(at: 0) else { + return nil + } + + self.removeFirst(2 + 2 + 4) + return date + } + + mutating func consumePString() -> String? { + let (str, len) = self.readPString(at: 0) + guard let str = str else { + return nil + } + if len == 0 { + return "" + } + + self.removeFirst(len) + return str + } + + mutating func consumeString(_ length: Int) -> String? { + guard let val = self.readString(at: 0, length: length) else { + return nil + } + + self.removeFirst(length) + return val + } + + func readUInt8(at offset: Int) -> UInt8? { + guard offset >= 0, offset + 1 <= self.count else { + return nil + } + return self[offset] + } + + func readUInt16(at offset: Int) -> UInt16? { + guard offset >= 0, offset + 2 <= self.count else { + return nil + } + + return (UInt16(self[offset]) << 8) + UInt16(self[offset + 1]) + } + + func readUInt32(at offset: Int) -> UInt32? { + guard offset >= 0, offset + 4 <= self.count else { + return nil + } + + return (UInt32(self[offset]) << 24) + (UInt32(self[offset + 1]) << 16) + (UInt32(self[offset + 2]) << 8) + UInt32(self[offset + 3]) + } + + func readUInt64(at offset: Int) -> UInt64? { + guard offset >= 0, offset + 8 <= self.count else { + return nil + } + + let leftSide: UInt64 = (UInt64(self[offset]) << 56) + + (UInt64(self[offset + 1]) << 48) + + (UInt64(self[offset + 2]) << 40) + + (UInt64(self[offset + 3]) << 32) + + let rightSide: UInt64 = (UInt64(self[offset + 4]) << 24) + + (UInt64(self[offset + 5]) << 16) + + (UInt64(self[offset + 6]) << 8) + + UInt64(self[offset + 7]) + + return leftSide + rightSide + } + + func readDate(at offset: Int) -> Date? { + guard offset >= 0, offset + 2 + 2 + 4 <= self.count else { + return nil + } + + if + let year = self.readUInt16(at: offset), + let ms = self.readUInt16(at: offset + 2), + let secs = self.readUInt32(at: offset + 2 + 2) { + return convertHotlineDate(year: year, seconds: secs, milliseconds: ms) + } + + return nil + } + + func readData(at offset: Int, length: Int) -> Data? { + guard offset >= 0, offset + length <= self.count else { + return nil + } + return Data(self[offset..<(offset + length)]) + } + + func readString(at offset: Int, length: Int) -> String? { + guard let subdata = self.readData(at: offset, length: length) else { + return nil + } + + if subdata.count == 0 { + return "" + } + + let allowedEncodings = [ + NSUTF8StringEncoding, + NSShiftJISStringEncoding, + NSUnicodeStringEncoding, + NSWindowsCP1251StringEncoding + ] + + var decodedNSString: NSString? + let rawValue = NSString.stringEncoding(for: subdata, encodingOptions: [.allowLossyKey: false], convertedString: &decodedNSString, usedLossyConversion: nil) + + if allowedEncodings.contains(rawValue) { + return decodedNSString as? String + } + + else if rawValue > 1 { + print("ENCODING FOUND \(rawValue)") + } + + var macStr = String(data: subdata, encoding: .macOSRoman) + if macStr == nil { + macStr = String(data: subdata, encoding: .nonLossyASCII) + } + + return macStr + } + + func readPString(at offset: Int) -> (String?, Int) { + guard offset >= 0, offset + 1 <= self.count else { + return (nil, 0) + } + let len = Int(self.readUInt8(at: offset)!) + guard offset + 1 + len <= self.count else { + return (nil, 0) + } + return (self.readString(at: offset+1, length: len), 1 + len) + } + + func readLongPString(at offset: Int) -> (String?, Int) { + guard offset >= 0, offset + 2 <= self.count else { + return (nil, 0) + } + let len = Int(self.readUInt16(at: offset)!) + guard len > 0 else { + return ("", 0) + } + guard offset + 2 + len <= self.count else { + return (nil, 0) + } + return (self.readString(at: offset+2, length: len), len) + } + + mutating func appendUInt8(_ value: UInt8, endianness: Endianness = .big) { + let val = endianness == .big ? value.bigEndian : value.littleEndian + self.append(val) + } + + mutating func appendUInt16(_ value: UInt16, endianness: Endianness = .big) { + let val = endianness == .big ? value.bigEndian : value.littleEndian + let bytes: [UInt8] = [ + UInt8((val >> 8) & 0xFF), + UInt8(val & 0xFF) + ] + self.append(contentsOf: bytes) + } + + mutating func appendUInt32(_ value: UInt32, endianness: Endianness = .big) { + let val = endianness == .big ? value.bigEndian : value.littleEndian + let bytes: [UInt8] = [ + UInt8((val >> 24) & 0xFF), + UInt8((val >> 16) & 0xFF), + UInt8((val >> 8) & 0xFF), + UInt8(val & 0xFF) + ] + self.append(contentsOf: bytes) + } + + mutating func appendUInt64(_ value: UInt64, endianness: Endianness = .big) { + let val: UInt64 = endianness == .big ? value.bigEndian : value.littleEndian + let bytes: [UInt8] = [ + UInt8((val >> 56) & 0xFF), + UInt8((val >> 48) & 0xFF), + UInt8((val >> 40) & 0xFF), + UInt8((val >> 32) & 0xFF), + UInt8((val >> 24) & 0xFF), + UInt8((val >> 16) & 0xFF), + UInt8((val >> 8) & 0xFF), + UInt8(val & 0xFF) + ] + self.append(contentsOf: bytes) + } +} + extension Data { init(_ val: UInt8) { self.init() diff --git a/Hotline/macOS/TrackerView.swift b/Hotline/macOS/TrackerView.swift index d53b73e..6b8f330 100644 --- a/Hotline/macOS/TrackerView.swift +++ b/Hotline/macOS/TrackerView.swift @@ -81,27 +81,20 @@ class TrackerItem: Identifiable, Hashable { let client = HotlineTrackerClient() - self.loading = true -// self.servers = [] - - let fetchedServers: [HotlineServer] = await client.fetchServers(address: bookmark.address, port: HotlinePorts.DefaultTrackerPort) - - client.disconnect() - var newItems: [TrackerItem] = [] + if let fetchedServers: [HotlineServer] = try? await client.fetchServers(address: bookmark.address, port: HotlinePorts.DefaultTrackerPort) { + var newItems: [TrackerItem] = [] - for s in fetchedServers { - if let serverName = s.name { - - let server = Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users)) -// let item = TrackerItem(server: server) - - newItems.append(TrackerItem(server: server)) + for s in fetchedServers { + if let serverName = s.name { + let server = Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users)) + newItems.append(TrackerItem(server: server)) + } } - } - self.servers = newItems + self.servers = newItems + } self.loading = false } |