diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..deee644 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,43 @@ +{ + // Displayed name + "name": "Message-Utils", + + // Image to be used + "image": "swift", + + // Customization + "customizations": { + "vscode": { + "extensions": [ + "sswg.swift-lang", + "fabiospampinato.vscode-highlight", + "fabiospampinato.vscode-todo-plus" + ] + } + }, + + // Env in container + "containerEnv": { + + }, + + // Mounts in container + "mounts": [ + { + "source": "${localWorkspaceFolder}", + "target": "/workspace", + "type": "bind" + } + ], + + // The WorkspaceFolder inside container + "workspaceFolder": "/workspace", + + // RunArgs + "runArgs": [ + "--name", + "Swift-MessageUtils" + ] + + +} \ No newline at end of file diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..94df151 --- /dev/null +++ b/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MessageUtils", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "MessageUtils", + targets: ["MessageUtils"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "MessageUtils"), + .testTarget( + name: "MessageUtilsTests", + dependencies: ["MessageUtils"] + ), + ] +) diff --git a/Sources/MessageUtils/Enums/DevType.swift b/Sources/MessageUtils/Enums/DevType.swift new file mode 100644 index 0000000..e3ee372 --- /dev/null +++ b/Sources/MessageUtils/Enums/DevType.swift @@ -0,0 +1,4 @@ +public enum DeviceType: UInt8 { + case EDGE_SENSOR = 0 + case SCANNER_SENSOR = 1 +} \ No newline at end of file diff --git a/Sources/MessageUtils/Enums/MessageType.swift b/Sources/MessageUtils/Enums/MessageType.swift new file mode 100644 index 0000000..48ecabf --- /dev/null +++ b/Sources/MessageUtils/Enums/MessageType.swift @@ -0,0 +1,8 @@ +public enum MessageType: UInt8 { + case KEEPALIVE = 0 + case DATA = 1 + case INFO = 2 + case WARNING = 50 + case ERROR = 100 + case CRITICAL = 255 +} \ No newline at end of file diff --git a/Sources/MessageUtils/Enums/SignType.swift b/Sources/MessageUtils/Enums/SignType.swift new file mode 100644 index 0000000..cde819d --- /dev/null +++ b/Sources/MessageUtils/Enums/SignType.swift @@ -0,0 +1,3 @@ +public enum SignType: UInt32 { + case P521 = 10 +} \ No newline at end of file diff --git a/Sources/MessageUtils/Errors/UtilsErrors.swift b/Sources/MessageUtils/Errors/UtilsErrors.swift new file mode 100644 index 0000000..975a424 --- /dev/null +++ b/Sources/MessageUtils/Errors/UtilsErrors.swift @@ -0,0 +1,13 @@ +public enum SerializationError : Error { + + case SIGNATURE_NOT_SUPPORTED + +} + +public enum CommonError : Error { + case SIGNATURE_NOT_SUPPORTED +} + +public enum DeserializationError: Error { + case UNMATCHING_SIGNATURE_TYPE +} \ No newline at end of file diff --git a/Sources/MessageUtils/MessageUtils.swift b/Sources/MessageUtils/MessageUtils.swift new file mode 100644 index 0000000..b7be363 --- /dev/null +++ b/Sources/MessageUtils/MessageUtils.swift @@ -0,0 +1,203 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book +import Foundation + +public func serializeV1(msg: Message) throws -> Data { + + let MESSAGE_CAPACITY: Int = try countBytes(msg: msg) + + var serializedData: Data = Data(count: MESSAGE_CAPACITY) + + // Serialize Header + + // UGLY: extrapolate in functions + serializedData[0] = msg.version + serializedData[1] = msg.messageType.rawValue + serializedData[2] = msg.devType.rawValue + serializedData[3] = msg.RESERVED + serializedData[4...7] = msg.signType.rawValue.data + // First 8 bytes + + serializedData[8...15] = msg.timestamp.data + // 8 Bytes + + serializedData[16...31] = msg.devID.data + // 16 bytes + + serializedData[32...39] = msg.location.x.data + // 8 Bytes + + serializedData[40...47] = msg.location.y.data + // 8 Bytes + + serializedData[48...55] = msg.location.z.data + // 8 Bytes + + var index = 56 + + for field in msg.fields { + serializedData[index...(index + 3)] = UInt32(field.key.count).data + serializedData[(index + 4)...(index + 7)] = UInt32(field.key.count).data + + index += 8 + + serializedData[index..<(index + field.key.count)] = Data(field.key) + + index += field.key.count + + serializedData[index..<(index + field.value.count)] = Data(field.value) + + index += field.value.count + } + + let paddingBytes = (8 - (index % 8)) % 8 + + serializedData[index..<(index + paddingBytes)] = Data(count: paddingBytes) + index += paddingBytes + + // Add STOP FIELDS + serializedData[index..<(index + 8)] = UInt64(0).data + print(UInt64(0).data.base64EncodedString()) + index += 8 + serializedData[index.. Message { + + // Serialize Header + + // UGLY: extrapolate in functions + let version: UInt8 = serializedData[0] + let messageType: MessageType = MessageType(rawValue: serializedData[1])! + let devType: DeviceType = DeviceType(rawValue: serializedData[2])! + let RESERVED: UInt8 = serializedData[3] + let signType: SignType = SignType(rawValue: serializedData[4...7].uint32)! + // First 8 bytes + + let signBytes = try signatureBytes(signature: signType) + + let timestamp = serializedData[8...15].timestamp + // 8 Bytes + + let devID = serializedData[16...31].uint128 + // 16 bytes + + let locationX = serializedData[32...39].uint64 + // 8 Bytes + + let locationY = serializedData[40...47].uint64 + // 8 Bytes + + let locationZ = serializedData[48...55].uint64 + // 8 Bytes + + var index = 56 + + // Deserializing Fields + var MORE_FIELDS = true + var fields: [Field] = [] + + while MORE_FIELDS { + + let nextChunk = serializedData[index..<(index + 8)] + let fieldKeyCount = serializedData[index...(index + 4)].uint32 + let fieldValueCount = serializedData[(index + 4)..<(index + 8)].uint32 + + index += 8 + + let a = nextChunk.uint64 + + if nextChunk.uint64 == 0 { + MORE_FIELDS = false + continue + } + + let key = serializedData[index..<(index + Int(fieldKeyCount))].map { value in + return value + } + index += Int(fieldKeyCount) + let value = serializedData[index..<(index + Int(fieldValueCount))].map { value in + return value + } + index += Int(fieldValueCount) + + fields.append(Field(key: key, value: value)) + + } + + let paddingBytes = (8 - (index % 8)) % 8 + + // Skip padding bytes + index += paddingBytes + + let signature = serializedData[index.. Int { + + /// + /// Author: Christian Risi + /// + /// This is computed as all the bits for the fixed fields + /// + /// In this case we have: + /// - 4 B --> Generic Info + /// - 4 B --> Timestamp + /// - 8 B --> Device ID + /// - 12 B --> Device Location + /// - 8 B --> FIELDS STOP + let INITIAL_CAPACITY_BYTES: Int = 36 + let FIELD_HEADER_CAPACITY_BYTES: Int = 4 + + var fieldReveservedCapacity: Int = 0 + + for field in msg.fields { + fieldReveservedCapacity += FIELD_HEADER_CAPACITY_BYTES + field.key.count + field.value.count + } + + // UGLY: We are assuming P521 signature + let SIGNATURE_CAPACITY_BYTES: Int = try signatureBytes(signature: msg.signType) + + return (INITIAL_CAPACITY_BYTES + fieldReveservedCapacity + SIGNATURE_CAPACITY_BYTES) + +} + +public func signatureBytes(signature: SignType) throws -> Int { + switch signature { + + case .P521: + return 132 + + default: + throw CommonError.SIGNATURE_NOT_SUPPORTED + } +} diff --git a/Sources/MessageUtils/Structs/Field.swift b/Sources/MessageUtils/Structs/Field.swift new file mode 100644 index 0000000..1bbd049 --- /dev/null +++ b/Sources/MessageUtils/Structs/Field.swift @@ -0,0 +1,8 @@ +import Foundation + +public struct Field { + + public let key: [UInt8] + public let value: [UInt8] + +} diff --git a/Sources/MessageUtils/Structs/Location.swift b/Sources/MessageUtils/Structs/Location.swift new file mode 100644 index 0000000..542d9b7 --- /dev/null +++ b/Sources/MessageUtils/Structs/Location.swift @@ -0,0 +1,7 @@ +public struct Location { + + public let x: UInt64 + public let y: UInt64 + public let z: UInt64 + +} \ No newline at end of file diff --git a/Sources/MessageUtils/Structs/Message.swift b/Sources/MessageUtils/Structs/Message.swift new file mode 100644 index 0000000..5144b62 --- /dev/null +++ b/Sources/MessageUtils/Structs/Message.swift @@ -0,0 +1,37 @@ +import Foundation + +public struct Message{ + + public let version: UInt8 + public let messageType: MessageType + public let devType: DeviceType + public let RESERVED: UInt8 + public let signType: SignType + + public let timestamp : Date + public let devID : UInt128 + public let location: Location + public let fields: [Field] + public let signature: [UInt8] + + + public func toString() -> String{ + var description = "" + description += "MESSAGE -------\n" + description += "V: \t\(self.version)\n" + description += "Message Type: \t\(self.messageType.rawValue)\n" + description += "Device Type: \t\(self.devType.rawValue)\n" + description += "RESERVED: \t\(self.RESERVED)\n" + description += "Signature Type: \t\(self.signType.rawValue)\n" + description += "Timestamp: \t\(self.timestamp)\n" + description += "Device ID: \t\(self.devID)\n" + description += "Location: \tX: \(self.location.x)\tY: \(self.location.y)\tZ: \(self.location.z)\n" + description += "Fields: \n" + for field in self.fields { + description += "\t\(String(data: Data(field.key), encoding: .ascii) ?? "UNABLE TO DECODE"): \(String(data: Data(field.value), encoding: .ascii) ?? "UNABLE TO DECODE")" + } + description += "Signature: \t\(self.signature)\n" + + return description + } +} \ No newline at end of file diff --git a/Sources/MessageUtils/Utils/DataCompatibleP.swift b/Sources/MessageUtils/Utils/DataCompatibleP.swift new file mode 100644 index 0000000..fb815c6 --- /dev/null +++ b/Sources/MessageUtils/Utils/DataCompatibleP.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol DataCompatibleP { + var data : Data {get} +} \ No newline at end of file diff --git a/Sources/MessageUtils/Utils/DataToUtils.swift b/Sources/MessageUtils/Utils/DataToUtils.swift new file mode 100644 index 0000000..bb7ff39 --- /dev/null +++ b/Sources/MessageUtils/Utils/DataToUtils.swift @@ -0,0 +1,32 @@ +import Foundation + +public extension Data { + + var uint8 : UInt8 { + return self.withUnsafeBytes{ $0.bindMemory(to: UInt8.self) }[0] + } + + var uint16: UInt16 { + return self.withUnsafeBytes{ $0.bindMemory(to: UInt16.self) }[0] + } + + var uint32: UInt32 { + return self.withUnsafeBytes{ $0.bindMemory(to: UInt32.self) }[0] + } + + var uint64: UInt64 { + return self.withUnsafeBytes{ $0.bindMemory(to: UInt64.self) }[0] + } + + var uint128: UInt128 { + return self.withUnsafeBytes{ $0.bindMemory(to: UInt128.self) }[0] + } + + var uint: UInt { + return self.withUnsafeBytes{ $0.bindMemory(to: UInt.self) }[0] + } + + var timestamp: Date { + return self.withUnsafeBytes{ $0.bindMemory(to: Date.self) }[0] + } +} \ No newline at end of file diff --git a/Sources/MessageUtils/Utils/MessageBytes.md b/Sources/MessageUtils/Utils/MessageBytes.md new file mode 100644 index 0000000..79320c9 --- /dev/null +++ b/Sources/MessageUtils/Utils/MessageBytes.md @@ -0,0 +1,52 @@ +# Message Structure +``` ++-+ 64 - bits---+----------------+-------------+----------------+-----------------------+ +|0| Version 7 | Message Type 8 | Dev Type 8 | RESERVED 16 | Sign Type 32 | ++-+-------------+----------------+-------------+----------------+-----------------------+ +| Timestamp 64 | ++---------------------------------------------------------------------------------------+ +| DevID 128 bits | +| | ++---------------------------------------------------------------------------------------+ +| Location 192 bits | +| | +| | ++---------------------------------------------------------------------------------------+ +\ / + | Fields -----------------------------------------------------------------------------| +/ \ ++---------------------------------------------------------------------------------------+ +| 0 Padding 64 - n | ++---------------------------------------------------------------------------------------+ +| Signature up to-512 bits | +| | +| | +| | +| | +| | +| | +| | ++---------------------------------------------------------------------------------------+ +``` + +# Fields +each field can be at most 8 * 2^32 bits of length: +- 34359738368 bits +- 4294967296 Bytes + +## Key +**MUST** be a `String` + +## Value +It's up to the Application to decide whether the value is +a string or another type of datum + +``` ++-- 64 - bits--------------------------+----------------------------------------+ +| Key-Length 32 | Value-Length 32 | ++--------------------------------------+----------------------------------------+ +\ Key / + |-----------------------------------------------------------------------------| +/ Value \ ++-------------------------------------------------------------------------------+ +``` \ No newline at end of file diff --git a/Sources/MessageUtils/Utils/ToDataUtils.swift b/Sources/MessageUtils/Utils/ToDataUtils.swift new file mode 100644 index 0000000..fdc62a4 --- /dev/null +++ b/Sources/MessageUtils/Utils/ToDataUtils.swift @@ -0,0 +1,43 @@ +import Foundation + +extension UInt8: DataCompatibleP { + public var data: Data { + var obj = self + return Data(bytes: &obj, count: MemoryLayout.stride) + } +} + +extension UInt16: DataCompatibleP { + public var data: Data { + var obj = self + return Data(bytes: &obj, count: MemoryLayout.stride) + } +} + +extension UInt32: DataCompatibleP { + public var data: Data { + var obj = self + return Data(bytes: &obj, count: MemoryLayout.stride) + } +} + +extension UInt64: DataCompatibleP { + public var data: Data { + var obj = self + return Data(bytes: &obj, count: MemoryLayout.stride) + } +} + +extension UInt128: DataCompatibleP { + public var data: Data { + var obj = self + return Data(bytes: &obj, count: MemoryLayout.stride) + } +} + +extension Date: DataCompatibleP { + public var data: Data { + var obj = self + return Data(bytes: &obj, count: MemoryLayout.stride) + } +} diff --git a/Tests/MessageUtilsTests/MessageUtilsTests.swift b/Tests/MessageUtilsTests/MessageUtilsTests.swift new file mode 100644 index 0000000..039c4db --- /dev/null +++ b/Tests/MessageUtilsTests/MessageUtilsTests.swift @@ -0,0 +1,87 @@ +import Foundation + +import Testing +@testable import MessageUtils + + + +@Test func serializeMessage() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + let msg = Message( + version: 1, + messageType: .KEEPALIVE, + devType: .EDGE_SENSOR, + RESERVED: 0, + signType: .P521, + timestamp: Date(), + devID: 1, + location: Location(x: 10,y: 20,z: 1), + fields: [], + signature: Array(repeating: 255, count: 132) + ) + + let data = try serializeV1(msg: msg) + +} + + +@Test func serializeMessageWithField() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + let msg = Message( + version: 1, + messageType: .KEEPALIVE, + devType: .EDGE_SENSOR, + RESERVED: 0, + signType: .P521, + timestamp: Date(), + devID: 1, + location: Location(x: 10,y: 20,z: 1), + fields: [ + Field(key: Array("valueOfLife".utf8), value: Array("42".utf8)), + Field(key: Array("valueOfLife".utf8), value: Array("42".utf8)) + ], + signature: Array(repeating: 255, count: 132) + ) + + let data = try serializeV1(msg: msg) + +} + +@Test func serializeDeserializeMessage() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + let msg = Message( + version: 1, + messageType: .KEEPALIVE, + devType: .EDGE_SENSOR, + RESERVED: 0, + signType: .P521, + timestamp: Date(), + devID: 1, + location: Location(x: 10,y: 20,z: 1), + fields: [ + Field(key: Array("valueOfLife".utf8), value: Array("42".utf8)), + Field(key: Array("valueOfLife".utf8), value: Array("42".utf8)) + ], + signature: Array(repeating: 255, count: 132) + ) + + print(msg.toString()) + + let data = try serializeV1(msg: msg) + + let bytes = data.map { value in + return value + } + + + + print(bytes.count % 8 == 0) + + + let msg2 = try derializeV1(serializedData: data) + print(msg2.toString()) + + +} + +