From 391d8b5c33f592ff75cf13969c992d508e895227 Mon Sep 17 00:00:00 2001 From: Dmitriy Borovikov Date: Fri, 2 Feb 2024 19:42:27 +0300 Subject: [PATCH] Fixed size Numeric array encode/decode implementation. --- README.md | 2 +- Sources/CDRCodable/Decoder/CDRDecoder.swift | 72 ++++++++------- .../Decoder/KeyedDecodingContainer.swift | 29 +++++- Sources/CDRCodable/Encoder/CDREncoder.swift | 41 +++++---- .../Encoder/KeyedEncodingContainer.swift | 89 ++++++++++++++----- 5 files changed, 161 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 5b7a8a3..8a4b77b 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ The following table shows the basic IDL types supported by CDRCodable and how th | String | std::string | string | ### 2. Arrays -Static size arrays is not supported by CDRCodable directly and needed custom coding. +Fixed size arrays is not supported by CDRCodable directly and needed custom coding. ### 3. Sequences CDRCodable supports sequences, which map between Swift Array and C++ std::vector container. The following table represents how the map between Swift, C++11 and IDL and is handled. diff --git a/Sources/CDRCodable/Decoder/CDRDecoder.swift b/Sources/CDRCodable/Decoder/CDRDecoder.swift index 602e4a2..0783424 100644 --- a/Sources/CDRCodable/Decoder/CDRDecoder.swift +++ b/Sources/CDRCodable/Decoder/CDRDecoder.swift @@ -18,7 +18,7 @@ final public class CDRDecoder { /// - Throws: `DecodingError.dataCorrupted(_:)` if the data is not valid public func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { let dataStore = DataStore(data: data) - switch T.self { + switch type { case is [Double].Type: return try dataStore.readArray(Double.self) as! T case is [Float].Type: return try dataStore.readArray(Float.self) as! T case is [Int].Type: return try dataStore.readArray(Int.self) as! T @@ -45,12 +45,14 @@ final public class CDRDecoder { final class DataStore { let data: Data - var index: Data.Index + let beginIndex: Data.Index + var cursor: Data.Index var codingPath: [CodingKey] = [] init(data: Data) { self.data = data - self.index = self.data.startIndex + self.beginIndex = self.data.startIndex + self.cursor = self.data.startIndex } } @@ -104,15 +106,15 @@ protocol _CDRDecodingContainer { extension DataStore { @inline(__always) func align(to aligment: Int) { - let offset = index % aligment + let offset = (cursor - beginIndex) % aligment if offset != 0 { - index = index.advanced(by: aligment - offset) + cursor = cursor.advanced(by: aligment - offset) } } @inline(__always) func checkDataEnd(_ length: Int) throws { - let nextIndex = index.advanced(by: length) + let nextIndex = cursor.advanced(by: length) guard nextIndex <= data.endIndex else { let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Unexpected end of data") throw DecodingError.dataCorrupted(context) @@ -128,30 +130,13 @@ extension DataStore { @inline(__always) func read(_ type: T.Type) throws -> T where T : Numeric { - let aligment = MemoryLayout.alignment - let offset = index % aligment - if offset != 0 { - index = index.advanced(by: aligment - offset) - } + align(to: MemoryLayout.alignment) let stride = MemoryLayout.stride try checkDataEnd(stride) defer { - index = index.advanced(by: stride) - } - return data.withUnsafeBytes{ $0.load(fromByteOffset: index, as: T.self) } - } - - @inline(__always) - func readArray(_ type: T.Type) throws -> [T] where T: Numeric { - let size = MemoryLayout.size - let count = try readCheckBlockCount(of: size) - defer { - index = index.advanced(by: count * size) - } - return Array.init(unsafeUninitializedCapacity: count) { - data.copyBytes(to: $0, from: index...) - $1 = count + cursor = cursor.advanced(by: stride) } + return data.withUnsafeBytes{ $0.load(fromByteOffset: cursor - beginIndex, as: T.self) } } @inline(__always) @@ -159,9 +144,9 @@ extension DataStore { let length = try readCheckBlockCount(of: 1) defer { - index = index.advanced(by: length) + cursor = cursor.advanced(by: length) } - guard let string = String(data: data[index.. Data { let length = try readCheckBlockCount(of: 1) defer { - index = index.advanced(by: length) + cursor = cursor.advanced(by: length) + } + return data.subdata(in: cursor..(_ type: T.Type) throws -> [T] where T: Numeric { + let size = MemoryLayout.size + let count = try readCheckBlockCount(of: size) + defer { + cursor = cursor.advanced(by: count * size) + } + return Array.init(unsafeUninitializedCapacity: count) { + let _: Int = data.copyBytes(to: $0, from: cursor...) + $1 = count + } + } + + @inline(__always) + func readFixedArray(_ type: T.Type, count: Int) throws -> [T] where T: Numeric { + align(to: MemoryLayout.alignment) + let stride = MemoryLayout.stride + try checkDataEnd(count * stride) + defer { + cursor = cursor.advanced(by: count * stride) + } + return Array.init(unsafeUninitializedCapacity: count) { + let _: Int = data.copyBytes(to: $0, from: cursor...) + $1 = count } - return data.subdata(in: index..(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { - switch T.self { + if let intValue = key.intValue, intValue > 0x10000 { + let fixedCount = intValue >> 16 + switch type { + case is [Double].Type: return try dataStore.readFixedArray(Double.self, count: fixedCount) as! T + case is [Float].Type: return try dataStore.readFixedArray(Float.self, count: fixedCount) as! T + case is [Int].Type: return try dataStore.readFixedArray(Int.self, count: fixedCount) as! T + case is [Int8].Type: return try dataStore.readFixedArray(Int8.self, count: fixedCount) as! T + case is [Int16].Type: return try dataStore.readFixedArray(Int16.self, count: fixedCount) as! T + case is [Int32].Type: return try dataStore.readFixedArray(Int32.self, count: fixedCount) as! T + case is [Int64].Type: return try dataStore.readFixedArray(Int64.self, count: fixedCount) as! T + case is [UInt].Type: return try dataStore.readFixedArray(UInt.self, count: fixedCount) as! T + case is [UInt8].Type: return try dataStore.readFixedArray(UInt8.self, count: fixedCount) as! T + case is [UInt16].Type: return try dataStore.readFixedArray(UInt16.self, count: fixedCount) as! T + case is [UInt32].Type: return try dataStore.readFixedArray(UInt32.self, count: fixedCount) as! T + case is [UInt64].Type: return try dataStore.readFixedArray(UInt64.self, count: fixedCount) as! T + case is [String].Type: + var stringArray: [String] = [] + stringArray.reserveCapacity(fixedCount) + for _ in 0.. type as fixed array") + throw DecodingError.typeMismatch(T.self, context) + } + } + + switch type { case is [Double].Type: return try dataStore.readArray(Double.self) as! T case is [Float].Type: return try dataStore.readArray(Float.self) as! T case is [Int].Type: return try dataStore.readArray(Int.self) as! T diff --git a/Sources/CDRCodable/Encoder/CDREncoder.swift b/Sources/CDRCodable/Encoder/CDREncoder.swift index 2b01107..cacd0eb 100644 --- a/Sources/CDRCodable/Encoder/CDREncoder.swift +++ b/Sources/CDRCodable/Encoder/CDREncoder.swift @@ -22,18 +22,18 @@ final public class CDREncoder { let dataStore = _CDREncoder.DataStore(capacity: capacity) switch value { - case let value as [Int]: try dataStore.encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [Int8]: try dataStore.encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [Int16]: try dataStore.encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [Int32]: try dataStore.encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [Int64]: try dataStore.encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [UInt]: try dataStore.encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [UInt8]: try dataStore.encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [UInt16]: try dataStore.encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [UInt32]: try dataStore.encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [UInt64]: try dataStore.encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [Float]: try dataStore.encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [Double]: try dataStore.encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int]: try dataStore.encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int8]: try dataStore.encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int16]: try dataStore.encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int32]: try dataStore.encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int64]: try dataStore.encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt]: try dataStore.encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt8]: try dataStore.encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt16]: try dataStore.encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt32]: try dataStore.encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt64]: try dataStore.encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Float]: try dataStore.encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Double]: try dataStore.encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) case let value as Data: try dataStore.write(count: value.count) dataStore.write(data: value) @@ -112,6 +112,13 @@ extension _CDREncoder: Encoder { } extension _CDREncoder.DataStore { + func align(_ alignment: Int) { + let offset = self.data.count % alignment + if offset != 0 { + self.data.append(contentsOf: Array(repeating: UInt8(0), count: alignment - offset)) + } + } + @inline(__always) func write(data: Data) { self.data.append(data) @@ -124,10 +131,10 @@ extension _CDREncoder.DataStore { @inline(__always) func write(value: T) where T: Numeric { - let aligment = MemoryLayout.alignment - let offset = self.data.count % aligment + let alignment = MemoryLayout.alignment + let offset = self.data.count % alignment if offset != 0 { - self.data.append(contentsOf: Array(repeating: UInt8(0), count: aligment - offset)) + self.data.append(contentsOf: Array(repeating: UInt8(0), count: alignment - offset)) } self.data.append(contentsOf: value.bytes) } @@ -141,13 +148,13 @@ extension _CDREncoder.DataStore { } @inline(__always) - func encodeNumericArray(count: Int, size: Int, pointer: UnsafeRawBufferPointer) throws { + func encodeNumericArray(count: Int, pointer: UnsafeRawBufferPointer) throws { guard let uint32 = UInt32(exactly: count) else { let context = EncodingError.Context(codingPath: [], debugDescription: "Cannot encode data of length \(count).") throw EncodingError.invalidValue(count, context) } write(value: uint32) - data.append(pointer.baseAddress!.assumingMemoryBound(to: UInt8.self), count: count * size) + data.append(pointer.baseAddress!.assumingMemoryBound(to: UInt8.self), count: pointer.count) } } diff --git a/Sources/CDRCodable/Encoder/KeyedEncodingContainer.swift b/Sources/CDRCodable/Encoder/KeyedEncodingContainer.swift index c0a8254..34a101d 100644 --- a/Sources/CDRCodable/Encoder/KeyedEncodingContainer.swift +++ b/Sources/CDRCodable/Encoder/KeyedEncodingContainer.swift @@ -20,11 +20,33 @@ extension _CDREncoder { extension _CDREncoder.KeyedContainer: KeyedEncodingContainerProtocol { @inline(__always) - private func encodeNumericArray(count: Int, size: Int, pointer: UnsafeRawBufferPointer) throws { + private func encodeNumericArray(count: Int, pointer: UnsafeRawBufferPointer) throws { try write(count: count) - dataStore.data.append(pointer.baseAddress!.assumingMemoryBound(to: UInt8.self), count: count * size) + dataStore.data.append(pointer.baseAddress!.assumingMemoryBound(to: UInt8.self), count: pointer.count) } + @inline(__always) + private func encodeFixedNumericArray(alignment: Int, count: Int, fixedCount:Int, pointer: UnsafeRawBufferPointer) throws { + guard fixedCount == count else { + let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Wrong fixed array size.") + throw EncodingError.invalidValue(count, context) + } + dataStore.align(alignment) + dataStore.data.append(pointer.baseAddress!.assumingMemoryBound(to: UInt8.self), count: pointer.count) + } + @inline(__always) + private func writeString(_ s: String) throws { + guard let data = s.data(using: .utf8) else { + let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot encode string using UTF-8 encoding.") + throw EncodingError.invalidValue(s, context) + } + let length = data.count + 1 + + try write(count: length) + dataStore.write(data: data) + dataStore.writeByte(0) + } + // Ignoring optionals as having no analog in the CDR protocol func encodeNil(forKey key: Key) throws {} func encodeIfPresent(_ value: Bool?, forKey key: Key) throws {} @@ -54,15 +76,7 @@ extension _CDREncoder.KeyedContainer: KeyedEncodingContainerProtocol { } func encode(_ value: String, forKey key: Key) throws { - guard let data = value.data(using: .utf8) else { - let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot encode string using UTF-8 encoding.") - throw EncodingError.invalidValue(value, context) - } - let length = data.count + 1 - - try write(count: length) - dataStore.write(data: data) - dataStore.writeByte(0) + try writeString(value) } func encode(_ value: T, forKey key: Key) throws where T : Numeric & Encodable { @@ -70,19 +84,48 @@ extension _CDREncoder.KeyedContainer: KeyedEncodingContainerProtocol { } func encode(_ value: T, forKey key: Key) throws where T : Encodable { + if let intValue = key.intValue, intValue > 0x10000 { + let fixedCount = intValue >> 16 + switch value { + case let value as [Int]: try encodeFixedNumericArray(alignment: MemoryLayout.alignment, count: value.count, fixedCount: fixedCount, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int8]: try encodeFixedNumericArray(alignment: MemoryLayout.alignment, count: value.count, fixedCount: fixedCount, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int16]: try encodeFixedNumericArray(alignment: MemoryLayout.alignment, count: value.count, fixedCount: fixedCount, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int32]: try encodeFixedNumericArray(alignment: MemoryLayout.alignment, count: value.count, fixedCount: fixedCount, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int64]: try encodeFixedNumericArray(alignment: MemoryLayout.alignment, count: value.count, fixedCount: fixedCount, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt]: try encodeFixedNumericArray(alignment: MemoryLayout.alignment, count: value.count, fixedCount: fixedCount, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt8]: try encodeFixedNumericArray(alignment: MemoryLayout.alignment, count: value.count, fixedCount: fixedCount, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt16]: try encodeFixedNumericArray(alignment: MemoryLayout.alignment, count: value.count, fixedCount: fixedCount, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt32]: try encodeFixedNumericArray(alignment: MemoryLayout.alignment, count: value.count, fixedCount: fixedCount, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt64]: try encodeFixedNumericArray(alignment: MemoryLayout.alignment, count: value.count, fixedCount: fixedCount, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Float]: try encodeFixedNumericArray(alignment: MemoryLayout.alignment, count: value.count, fixedCount: fixedCount, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Double]: try encodeFixedNumericArray(alignment: MemoryLayout.alignment, count: value.count, fixedCount: fixedCount, pointer: value.withUnsafeBytes{ $0 }) + case let value as [String]: + guard fixedCount == value.count else { + let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Wrong fixed array size.") + throw EncodingError.invalidValue(value.count, context) + } + for string in value { + try writeString(string) + } + default: + let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Non-collection value as fixed array") + throw EncodingError.invalidValue(value, context) + } + return + } switch value { - case let value as [Int]: try encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [Int8]: try encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [Int16]: try encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [Int32]: try encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [Int64]: try encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [UInt]: try encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [UInt8]: try encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [UInt16]: try encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [UInt32]: try encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [UInt64]: try encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [Float]: try encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) - case let value as [Double]: try encodeNumericArray(count: value.count, size: MemoryLayout.size, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int]: try encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int8]: try encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int16]: try encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int32]: try encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Int64]: try encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt]: try encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt8]: try encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt16]: try encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt32]: try encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [UInt64]: try encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Float]: try encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) + case let value as [Double]: try encodeNumericArray(count: value.count, pointer: value.withUnsafeBytes{ $0 }) case let value as Data: try write(count: value.count) dataStore.write(data: value)