aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/HotlineApp.swift26
-rw-r--r--Hotline/Item.swift18
-rw-r--r--Hotline/Network/HotlineClient.swift241
-rw-r--r--Hotline/Network/HotlineProtocol.swift118
-rw-r--r--Hotline/Network/HotlineTracker.swift48
-rw-r--r--Hotline/TrackerView.swift139
-rw-r--r--Hotline/Utility/DataExtensions.swift42
7 files changed, 513 insertions, 119 deletions
diff --git a/Hotline/HotlineApp.swift b/Hotline/HotlineApp.swift
index f351e67..da49fd0 100644
--- a/Hotline/HotlineApp.swift
+++ b/Hotline/HotlineApp.swift
@@ -3,23 +3,23 @@ import SwiftData
@main
struct HotlineApp: App {
- var sharedModelContainer: ModelContainer = {
- let schema = Schema([
- Item.self,
- ])
- let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
-
- do {
- return try ModelContainer(for: schema, configurations: [modelConfiguration])
- } catch {
- fatalError("Could not create ModelContainer: \(error)")
- }
- }()
+// var sharedModelContainer: ModelContainer = {
+// let schema = Schema([
+// Item.self,
+// ])
+// let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
+//
+// do {
+// return try ModelContainer(for: schema, configurations: [modelConfiguration])
+// } catch {
+// fatalError("Could not create ModelContainer: \(error)")
+// }
+// }()
var body: some Scene {
WindowGroup {
TrackerView()
}
- .modelContainer(sharedModelContainer)
+// .modelContainer(sharedModelContainer)
}
}
diff --git a/Hotline/Item.swift b/Hotline/Item.swift
deleted file mode 100644
index 4fa6e0c..0000000
--- a/Hotline/Item.swift
+++ /dev/null
@@ -1,18 +0,0 @@
-//
-// Item.swift
-// Hotline
-//
-// Created by Dustin Mierau on 11/25/23.
-//
-
-import Foundation
-import SwiftData
-
-@Model
-final class Item {
- var timestamp: Date
-
- init(timestamp: Date) {
- self.timestamp = timestamp
- }
-}
diff --git a/Hotline/Network/HotlineClient.swift b/Hotline/Network/HotlineClient.swift
new file mode 100644
index 0000000..d3b6c98
--- /dev/null
+++ b/Hotline/Network/HotlineClient.swift
@@ -0,0 +1,241 @@
+import Foundation
+import Network
+
+enum HotlineClientStatus: Int {
+ case disconnected
+ case connecting
+ case connected
+}
+
+class HotlineClient : ObservableObject {
+ static let shared = HotlineClient()
+
+ static let handshakePacket = Data([
+ 0x54, 0x52, 0x54, 0x50, // 'TRTP' protocol ID
+ 0x48, 0x4F, 0x54, 0x4C, // Sub-protocol ID
+ 0x00, 0x01, // Version
+ 0x00, 0x02, // Sub-version
+ ])
+
+ @Published var connectionStatus: HotlineClientStatus = .disconnected
+
+ var server: HotlineServer?
+ var connection: NWConnection?
+ var bytes = Data()
+ var handshakeComplete = false
+
+ init() {
+
+ }
+
+ func connect(to server: HotlineServer) {
+ self.server = server
+
+ let serverAddress = NWEndpoint.Host(server.address)
+ let serverPort = NWEndpoint.Port(rawValue: server.port)!
+
+ self.connection = NWConnection(host: serverAddress, port: serverPort, using: .tcp)
+ self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in
+ switch newState {
+ case .ready:
+ print("HotlineClient: connection ready!")
+ DispatchQueue.main.async {
+ self?.connectionStatus = .connected
+ }
+ self?.sendHandshake()
+ case .cancelled:
+ print("HotlineClient: connection cancelled")
+ DispatchQueue.main.async {
+ self?.connectionStatus = .disconnected
+ }
+ case .failed(let err):
+ print("HotlineClient: connection error \(err)")
+ DispatchQueue.main.async {
+ self?.connectionStatus = .disconnected
+ }
+ default:
+ print("HotlineClient: unhandled connection state \(newState)")
+ }
+ }
+
+ DispatchQueue.main.async {
+ self.connectionStatus = .connecting
+ }
+ self.connection?.start(queue: .global())
+ }
+
+ private func disconnect() {
+ guard let c = connection else {
+ print("HotlineClient: already disconnected")
+ return
+ }
+
+ c.cancel()
+ self.connection = nil
+ }
+
+ private func sendHandshake() {
+ guard let c = connection else {
+ print("HotlineClient: invalid connection to send handshake.")
+ return
+ }
+
+ c.send(content: HotlineClient.handshakePacket, completion: .contentProcessed { [weak self] (error) in
+ if let err = error {
+ print("HotlineClient: sending magic failed \(err)")
+ return
+ }
+
+ print("HotlineClient: sent handshake packet!")
+
+ self?.receiveHandshake()
+ })
+ }
+
+ private func receiveHandshake() {
+ guard let c = connection else {
+ print("HotlineTracker: invalid connection to receive magic.")
+ return
+ }
+
+ print("HotlineClient: receiving handshake...")
+ c.receive(minimumIncompleteLength: 8, maximumLength: 8) { [weak self] (data, context, isComplete, error) in
+ guard let self = self, let data = data else {
+ return
+ }
+
+ if data.isEmpty {
+ print("HotlineClient: empty handshake response")
+ self.disconnect()
+ return
+ }
+
+ let protocolID = data.readUInt32(at: 0)!
+ if protocolID != 0x54525450 { // 'TRTP'
+ print("HotlineClient: invalid handshake protocol ID \(protocolID)")
+ self.disconnect()
+ return
+ }
+
+ let errorCode = data.readUInt32(at: 4)!
+ if errorCode != 0 { // 0 == no error
+ print("HotlineClient: handshake error", errorCode)
+ self.disconnect()
+ return
+ }
+
+ print("HotlineClient: completed handshake")
+ self.sendLogin()
+ }
+ }
+
+ private func sendLogin() {
+ guard let c = connection else {
+ print("HotlineClient: no connection for transaction.")
+ return
+ }
+
+ c.send(content: HotlineClient.handshakePacket, completion: .contentProcessed { [weak self] (error) in
+ if let err = error {
+ print("HotlineClient: sending login failed \(err)")
+ return
+ }
+
+ print("HotlineClient: sent handshake packet!")
+
+
+ })
+ }
+
+ private func receiveTransaction() {
+ guard let c = connection else {
+ print("HotlineClient: no connection for transaction.")
+ return
+ }
+
+ print("HotlineClient: waiting on transaction header...")
+ c.receive(minimumIncompleteLength: HotlineTransaction.headerSize, maximumLength: HotlineTransaction.headerSize) { [weak self] (data, context, isComplete, error) in
+ guard let self = self else {
+ return
+ }
+
+ if let error = error {
+ print("HotlineClient: receive error \(error)")
+ self.disconnect()
+ return
+ }
+
+ if let data = data, !data.isEmpty {
+ print("HotlineClient: received \(data.count) header bytes")
+
+ let transaction = self.parseTransaction(data: data)
+ if var t = transaction {
+ if t.dataSize > 0 {
+ c.receive(minimumIncompleteLength: Int(t.dataSize), maximumLength: Int(t.dataSize)) { [weak self] (data, context, isComplete, error) in
+ guard let self = self else {
+ return
+ }
+
+ if let data = data, !data.isEmpty {
+ t.parameterCount = data.readUInt16(at: 0)!
+
+ if t.parameterCount > 0 {
+ t.parameters = []
+ var dataCursor = 2
+ for _ in 0..<t.parameterCount {
+ if
+ let fieldID = data.readUInt16(at: dataCursor),
+ let fieldSize = data.readUInt16(at: dataCursor + 2),
+ let fieldData = data.readData(at: dataCursor + 4, length: Int(fieldSize)) {
+ t.parameters?.append(HotlineTransactionParameter(id: fieldID, dataSize: fieldSize, data: fieldData))
+
+ dataCursor += 4 + Int(fieldSize)
+ }
+ }
+ }
+ }
+
+ self.processTransaction(t)
+ }
+ }
+ else {
+ self.processTransaction(t)
+ }
+ }
+
+// print("HotlineTracker: server count = \(self.serverCount)")
+ }
+
+// self.receiveListing()
+// }
+ }
+ }
+
+ private func processTransaction(_ transaction: HotlineTransaction) {
+ print("HotlineClient processing transaction \(transaction.type) with \(transaction.parameterCount) parameters")
+ }
+
+ private func parseTransaction(data: Data) -> HotlineTransaction? {
+ if
+ let flags = data.readUInt8(at: 0),
+ let isReply = data.readUInt8(at: 1),
+ let type = data.readUInt16(at: 2),
+ let id = data.readUInt32(at: 4),
+ let errorCode = data.readUInt32(at: 8),
+ let transactionSize = data.readUInt32(at: 12),
+ let dataSize = data.readUInt32(at: 16) {
+
+ return HotlineTransaction(
+ flags: flags,
+ isReply: isReply,
+ type: HotlineTransactionType(rawValue: type) ?? HotlineTransactionType.unknown,
+ id: id,
+ errorCode: errorCode,
+ totalSize: transactionSize,
+ dataSize: dataSize
+ )
+ }
+
+ return nil
+ }
+}
diff --git a/Hotline/Network/HotlineProtocol.swift b/Hotline/Network/HotlineProtocol.swift
new file mode 100644
index 0000000..787675e
--- /dev/null
+++ b/Hotline/Network/HotlineProtocol.swift
@@ -0,0 +1,118 @@
+import Foundation
+
+struct HotlineServer: Identifiable, Hashable {
+ let id = UUID()
+ let address: String
+ let port: UInt16
+ let users: UInt16
+ let name: String?
+ let description: String?
+
+ static func == (lhs: HotlineServer, rhs: HotlineServer) -> Bool {
+ return lhs.id == rhs.id
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(self.id)
+ }
+}
+
+struct HotlineTransactionParameter {
+ let id: UInt16
+ let dataSize: UInt16
+ let data: Data
+}
+
+struct HotlineTransaction {
+ static let headerSize = 20
+
+ let flags: UInt8
+ let isReply: UInt8
+ let type: HotlineTransactionType
+ let id: UInt32
+ let errorCode: UInt32
+ let totalSize: UInt32
+ let dataSize: UInt32
+
+ var parameterCount: UInt16 = 0
+ var parameters: [HotlineTransactionParameter]? = nil
+
+ func encode(to data: inout Data) {
+ data.appendUInt8(self.flags)
+ data.appendUInt8(self.isReply)
+ data.appendUInt16(self.type.rawValue)
+ data.appendUInt32(self.id)
+ data.appendUInt32(self.errorCode)
+ data.appendUInt32(self.totalSize)
+ data.appendUInt32(self.dataSize)
+
+ if let p = self.parameters, p.count > 0 {
+ data.appendUInt16(UInt16(p.count))
+ for param in p {
+ data.appendUInt16(param.id)
+ data.appendUInt16(param.dataSize)
+ data.append(param.data)
+ }
+ }
+ }
+}
+
+enum HotlineTransactionType: UInt16 {
+ case unknown = 0
+ case error = 100
+ case getMessages = 101
+ case newMessage = 102
+ case oldPostNews = 103
+ case serverMessage = 104
+ case sendChat = 105
+ case chatMessage = 106
+ case login = 107
+ case sendInstantMessage = 108
+ case showAgreement = 109
+ case disconnectUser = 110
+ case disconnectMessage = 111
+ case inviteToNewChat = 112
+ case inviteToChat = 113
+ case rejectChatInvite = 114
+ case joinChat = 115
+ case leaveChat = 116
+ case notifyChatOfUserChange = 117
+ case notifyChatOfUserDelete = 118
+ case notifyChatSubject = 119
+ case setChatSubject = 120
+ case agreed = 121
+ case serverBanner = 122
+ case getFileNameList = 200
+ case downloadFile = 202
+ case uploadFile = 203
+ case deleteFile = 204
+ case newFolder = 205
+ case getFileInfo = 206
+ case setFileInfo = 207
+ case moveFile = 208
+ case makeFileAlias = 209
+ case downloadFolder = 210
+ case downloadInfo = 211
+ case downloadBanner = 212
+ case uploadFolder = 213
+ case getUserNameList = 300
+ case notifyOfUserChange = 301
+ case notifyOfUserDelete = 302
+ case getClientInfoText = 303
+ case setClientUserInfo = 304
+ case newUser = 350
+ case deleteUser = 351
+ case getUser = 352
+ case setUser = 353
+ case userAccess = 354
+ case userBroadcast = 355
+ case getNewsCategoryNameList = 370
+ case getNewsArticleNameList = 371
+ case deleteNewsItem = 380
+ case newNewsFolder = 381
+ case newNewsCategory = 382
+ case getNewsArticleData = 400
+ case postNewsArticle = 410
+ case deleteNewsArticle = 411
+}
+
diff --git a/Hotline/Network/HotlineTracker.swift b/Hotline/Network/HotlineTracker.swift
index 9b21ab2..2c44bc8 100644
--- a/Hotline/Network/HotlineTracker.swift
+++ b/Hotline/Network/HotlineTracker.swift
@@ -1,15 +1,6 @@
import Foundation
import Network
-struct HotlineServer: Identifiable {
- var id = UUID()
- let address: String
- let port: UInt16
- let users: UInt16
- let name: String?
- let description: String?
-}
-
enum HotlineTrackerStatus: Int {
case disconnected
case connecting
@@ -19,7 +10,6 @@ enum HotlineTrackerStatus: Int {
class HotlineTracker : ObservableObject {
let serverAddress: NWEndpoint.Host
let serverPort: NWEndpoint.Port = NWEndpoint.Port(rawValue: 5498)!
- let callback: ([HotlineServer]) -> Void
static let magicPacket = Data([
0x48, 0x54, 0x52, 0x4B, // 'HTRK'
@@ -35,9 +25,8 @@ class HotlineTracker : ObservableObject {
@Published var connectionStatus: HotlineTrackerStatus = .disconnected
@Published var servers: [HotlineServer] = []
- init(address: String, callback: @escaping ([HotlineServer]) -> Void) {
+ init(address: String) {
self.serverAddress = NWEndpoint.Host(address)
- self.callback = callback
}
func fetch() {
@@ -56,20 +45,28 @@ class HotlineTracker : ObservableObject {
switch newState {
case .ready:
print("READY TO SEND AND RECEIVE DATA")
- self?.connectionStatus = .connected
+ DispatchQueue.main.async {
+ self?.connectionStatus = .connected
+ }
self?.sendMagic()
case .cancelled:
print("CONNECTION CANCELLED")
- self?.connectionStatus = .disconnected
+ DispatchQueue.main.async {
+ self?.connectionStatus = .disconnected
+ }
case .failed(let err):
print("CONNECTION ERROR \(err)")
- self?.connectionStatus = .disconnected
+ DispatchQueue.main.async {
+ self?.connectionStatus = .disconnected
+ }
default:
print("CONNECTION OTHER THING")
}
}
- self.connectionStatus = .connecting
+ DispatchQueue.main.async {
+ self.connectionStatus = .connecting
+ }
self.connection?.start(queue: .global())
}
@@ -89,7 +86,7 @@ class HotlineTracker : ObservableObject {
return
}
-// let packet: [UInt8] = [0x48, 0x54, 0x52, 0x4B, 0x00, self.serverVersion]
+ // let packet: [UInt8] = [0x48, 0x54, 0x52, 0x4B, 0x00, self.serverVersion]
c.send(content: HotlineTracker.magicPacket, completion: .contentProcessed { [weak self] (error) in
if let err = error {
@@ -120,9 +117,9 @@ class HotlineTracker : ObservableObject {
self.disconnect()
return
}
-// if let data = data, !data.isEmpty {
+ // if let data = data, !data.isEmpty {
print("HotlineTracker: received magic response!")
-// }
+ // }
if let error = error {
print("HotlineTracker: receive error \(error)")
@@ -153,7 +150,7 @@ class HotlineTracker : ObservableObject {
if let data = data, !data.isEmpty {
print("HotlineTracker: received \(data.count) header bytes")
-
+
self.maxDataLength = Int(data[2]) * 0xFF + Int(data[3])
self.maxDataLength -= 4
print("HotlineTracker: message size = \(self.maxDataLength)")
@@ -182,7 +179,7 @@ class HotlineTracker : ObservableObject {
guard let self = self else {
return
}
-
+
if let data = data, !data.isEmpty {
print("HotlineTracker: received \(data.count) bytes")
self.bytes.append(contentsOf: data)
@@ -216,7 +213,7 @@ class HotlineTracker : ObservableObject {
// Description size (1 byte)
// Description (description size)
- var servers: [HotlineServer] = []
+ var foundServers: [HotlineServer] = []
var cursor = 0
for _ in 1...self.serverCount {
@@ -243,7 +240,7 @@ class HotlineTracker : ObservableObject {
print("SERVER: \(server)")
- servers.append(server)
+ foundServers.append(server)
cursor += 11 + nameLength + 1 + descLength
}
@@ -255,9 +252,8 @@ class HotlineTracker : ObservableObject {
}
DispatchQueue.main.async {
- print("CALLING CALLBACK")
- self.servers = servers
- self.callback(servers)
+// print("CALLING CALLBACK")
+ self.servers = foundServers
}
}
diff --git a/Hotline/TrackerView.swift b/Hotline/TrackerView.swift
index 04498b1..0bc9762 100644
--- a/Hotline/TrackerView.swift
+++ b/Hotline/TrackerView.swift
@@ -2,68 +2,115 @@ import SwiftUI
import SwiftData
struct TrackerView: View {
- @Environment(\.modelContext) private var modelContext
- @Query private var items: [Item]
+ // @Environment(\.modelContext) private var modelContext
+ // @Query private var items: [Item]
- @StateObject var tracker = HotlineTracker(address: "hltracker.com", callback: { s in
- print("ALL DONE")
- })
+ @StateObject var tracker = HotlineTracker(address: "hltracker.com")
+ @State private var selectedServer: HotlineServer?
+
+ private var client: HotlineClient?
var body: some View {
- ScrollView {
- VStack(alignment: .leading) {
- ForEach(tracker.servers) { server in
+ List(selection: $selectedServer) {
+ ForEach(tracker.servers) { server in
+ HStack {
+ Text(server.name!).bold()
+ Spacer()
HStack {
- VStack(alignment: .leading) {
- Text(server.name!).bold()
- Image(systemName: "person.fill").opacity(0.3)
- Text("\(server.users)").font(.system(size: 12))
- Text(server.description!).foregroundColor(.secondary).font(.system(size: 14))
- }
- .padding(EdgeInsets(top: 15, leading: 18, bottom: 15, trailing: 18))
- Spacer()
+ Text("\(server.users)").font(.system(size: 12)).bold().opacity(0.3)
+ // Image(systemName: "person.fill").font(.system(size: 12)).opacity(0.3)
}
- .background(.white)
- .cornerRadius(16)
- .padding(EdgeInsets(top: 5, leading: 8, bottom: 5, trailing: 8))
- .frame(minWidth: 0, maxWidth: .infinity)
+ .padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
+ .background(Color(white: 0.94))
+ .cornerRadius(20.0)
}
+// .contentShape(Rectangle())
+ .listRowSeparator(.hidden)
+ .tag(server)
+ // .padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 0))
+// .onTapGesture {
+// self.selectedServer = server
+// }
}
- .padding()
}
- .background(Color(white: 0.9))
-// .toolbar {
-// ToolbarItem(placement: .navigationBarTrailing) {
-// EditButton()
-// }
-// ToolbarItem {
-// Button(action: addItem) {
-// Label("Add Item", systemImage: "plus")
-// }
-// }
-// }
+ .listStyle(.plain)
+ .frame(maxWidth: .infinity)
.task {
tracker.fetch()
}
- }
-
- private func addItem() {
- withAnimation {
- let newItem = Item(timestamp: Date())
- modelContext.insert(newItem)
- }
- }
-
- private func deleteItems(offsets: IndexSet) {
- withAnimation {
- for index in offsets {
- modelContext.delete(items[index])
+ .sheet(item: $selectedServer) { item in
+ VStack(alignment: .leading) {
+ Text(item.name!).bold().dynamicTypeSize(.xxLarge).padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0))
+ Text(item.description!).opacity(0.4).dynamicTypeSize(.xLarge).padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0))
+ Text(item.address).opacity(0.2).dynamicTypeSize(.medium)
+ Spacer()
+ HStack(alignment: .center) {
+ Button("Connect") {
+ print("WHAT", "HELLO", selectedServer!.address)
+ HotlineClient.shared.connect(to: selectedServer!)
+// client = HotlineClient(server: selectedServer)
+ }
+ .bold()
+ .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24))
+ .frame(maxWidth: .infinity)
+ .foregroundColor(.white)
+ .background(Color(.black))
+ .cornerRadius(8.0)
+ }
}
+ .padding(EdgeInsets(top: 28.0, leading: 24.0, bottom: 24.0, trailing: 24.0))
+ .presentationDetents([.fraction(0.4)])
+ .presentationDragIndicator(.visible)
}
+// List(tracker.servers) { server in
+// HStack {
+// Text(server.name!).bold()
+// Spacer()
+// HStack {
+// Text("\(server.users)").font(.system(size: 12)).bold().opacity(0.3)
+//// Image(systemName: "person.fill").font(.system(size: 12)).opacity(0.3)
+// }
+// .padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
+// .background(Color(white: 0.94))
+// .cornerRadius(20.0)
+// }
+// .contentShape(Rectangle())
+// .listRowSeparator(.hidden)
+//// .padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 0))
+// .onTapGesture {
+// self.selectedServer = server
+// }
+// }
+// .listStyle(.plain)
+// .frame(maxWidth: .infinity)
+// .task {
+// tracker.fetch()
+// }
+// .sheet(item: $selectedServer) { item in
+// VStack(alignment: .leading) {
+// Text(item.name!).bold().dynamicTypeSize(.xxLarge).padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0))
+// Text(item.description!).opacity(0.4).dynamicTypeSize(.xLarge)
+// Spacer()
+// HStack(alignment: .center) {
+// Button("Connect") {
+// print("WHAT")
+// }
+// .bold()
+// .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24))
+// .frame(maxWidth: .infinity)
+// .foregroundColor(.white)
+// .background(Color(.black))
+// .cornerRadius(8.0)
+// }
+// }
+// .padding(EdgeInsets(top: 28.0, leading: 24.0, bottom: 24.0, trailing: 24.0))
+// .presentationDetents([.fraction(0.3)])
+// .presentationDragIndicator(.visible)
+// }
}
}
#Preview {
TrackerView()
- .modelContainer(for: Item.self, inMemory: true)
+ // .modelContainer(for: Item.self, inMemory: true)
}
diff --git a/Hotline/Utility/DataExtensions.swift b/Hotline/Utility/DataExtensions.swift
index 9530416..3f7101b 100644
--- a/Hotline/Utility/DataExtensions.swift
+++ b/Hotline/Utility/DataExtensions.swift
@@ -29,26 +29,36 @@ extension Data {
return (endianness == .big) ? value.bigEndian : value.littleEndian
}
- func read<T: FixedWidthInteger>(type: T.Type, at offset: Int) -> T? {
- guard offset >= 0, offset + MemoryLayout<T>.size <= self.count else {
- return nil // Ensure the offset is within the Data's range
- }
-
- return self.withUnsafeBytes { rawBufferPointer in
- let pointer = rawBufferPointer.baseAddress!
- .advanced(by: offset)
- .assumingMemoryBound(to: T.self)
-
-// switch endianness {
-// case .big:
- return pointer.pointee.bigEndian
-// case .little:
-// return pointer.pointee.littleEndian
-// }
+ func readData(at offset: Int, length: Int) -> Data? {
+ guard offset >= 0, offset + length <= self.count else {
+ return nil
}
+ return self[offset..<(offset + length)]
}
func readString(at offset: Int, length: Int, encoding: String.Encoding) -> String? {
return String(data: self[offset..<(offset + length)], encoding: encoding)
}
+
+
+ mutating func appendUInt8(_ value: UInt8, endianness: Endianness = .big) {
+ var val = endianness == .big ? value.bigEndian : value.littleEndian
+ append(&val, count: MemoryLayout<UInt8>.size)
+ }
+
+ mutating func appendUInt16(_ value: UInt16, endianness: Endianness = .big) {
+ var val = endianness == .big ? value.bigEndian : value.littleEndian
+ Swift.withUnsafeBytes(of: &val) { buffer in
+ append(buffer.bindMemory(to: UInt8.self))
+ }
+// append(&val, count: MemoryLayout<UInt16>.size)
+ }
+
+ mutating func appendUInt32(_ value: UInt32, endianness: Endianness = .big) {
+ var val = endianness == .big ? value.bigEndian : value.littleEndian
+ Swift.withUnsafeBytes(of: &val) { buffer in
+ append(buffer.bindMemory(to: UInt8.self))
+ }
+// append(&val, count: MemoryLayout<UInt32>.size)
+ }
}