diff --git a/Sources/OracleNIO/Constants.swift b/Sources/OracleNIO/Constants.swift index 9e69ff3..069f982 100644 --- a/Sources/OracleNIO/Constants.swift +++ b/Sources/OracleNIO/Constants.swift @@ -173,10 +173,10 @@ enum Constants { static let TNS_JSON_FLAG_HASH_ID_UINT16: UInt16 = 0x0200 static let TNS_JSON_FLAG_NUM_FNAMES_UINT16: UInt16 = 0x0400 static let TNS_JSON_FLAG_FNAMES_SEG_UINT32: UInt16 = 0x0800 - static let TNS_JSON_FLAG_TINY_NODES_STAT = 0x2000 + static let TNS_JSON_FLAG_TINY_NODES_STAT: UInt16 = 0x2000 static let TNS_JSON_FLAG_TREE_SEG_UINT32: UInt16 = 0x1000 static let TNS_JSON_FLAG_REL_OFFSET_MODE: UInt16 = 0x01 - static let TNS_JSON_FLAG_INLINE_LEAF = 0x02 + static let TNS_JSON_FLAG_INLINE_LEAF: UInt16 = 0x02 static let TNS_JSON_FLAG_LEN_IN_PCODE = 0x04 static let TNS_JSON_FLAG_NUM_FNAMES_UINT32: UInt16 = 0x08 static let TNS_JSON_FLAG_IS_SCALAR: UInt16 = 0x10 diff --git a/Sources/OracleNIO/Data/JSON/OracleJSON+Decoding.swift b/Sources/OracleNIO/Data/JSON/OracleJSON+Decoding.swift index c0055ae..4e737b9 100644 --- a/Sources/OracleNIO/Data/JSON/OracleJSON+Decoding.swift +++ b/Sources/OracleNIO/Data/JSON/OracleJSON+Decoding.swift @@ -16,6 +16,12 @@ import NIOCore import struct Foundation.Date +extension OracleJSON: Decodable where Value: Decodable { + public init(from decoder: any Decoder) throws { + self.value = try .init(from: decoder) + } +} + extension OracleJSON: OracleDecodable where Value: Decodable { public init( from buffer: inout ByteBuffer, diff --git a/Sources/OracleNIO/Data/JSON/OracleJSON+Encoding.swift b/Sources/OracleNIO/Data/JSON/OracleJSON+Encoding.swift new file mode 100644 index 0000000..569f0da --- /dev/null +++ b/Sources/OracleNIO/Data/JSON/OracleJSON+Encoding.swift @@ -0,0 +1,571 @@ +import NIOCore + +import struct Foundation.Date + +extension OracleJSON: Encodable where Value: Encodable { + public func encode(to encoder: any Encoder) throws { + try value.encode(to: encoder) + } +} + +extension OracleJSON: OracleThrowingDynamicTypeEncodable where Value: Encodable { + public var oracleType: OracleDataType { + .json + } + + public func encode( + into buffer: inout ByteBuffer, + context: OracleEncodingContext + ) throws { + let storage = try _OracleJSONEncoder().encode(value) + var writer = OracleJSONWriter() + try writer.encode( + storage, into: &buffer, + maxFieldNameSize: context.jsonMaximumFieldNameSize + ) + } + + public init(_ value: Value) { + self.value = value + } +} + +final class _OracleJSONEncoder: Encoder { + let codingPath: [any CodingKey] + var userInfo: [CodingUserInfoKey: Any] = [:] + + fileprivate var object: JSONObject? + fileprivate var array: JSONArray? + var singleValue: OracleJSONStorage? + + var value: OracleJSONStorage? { + if let object { + return .container(object.values) + } + if let array { + return .array(array.values) + } + return self.singleValue + } + + init() { + self.codingPath = [] + } + + fileprivate init( + codingPath: [any CodingKey] = [], + userInfo: [CodingUserInfoKey: Any], + value: OracleJSONStorage? = nil, + object: JSONObject? = nil, + array: JSONArray? = nil + ) { + self.codingPath = codingPath + self.userInfo = userInfo + self.singleValue = value + self.object = object + self.array = array + } + + func encode(_ value: some Encodable) throws -> OracleJSONStorage { + try value.encode(to: self) + return self.value ?? .none + } + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { + if let object { + let container = OracleKeyedEncodingContainer(codingPath: codingPath, encoder: self, object: object) + return KeyedEncodingContainer(container) + } + + precondition(singleValue == nil && array == nil) + + object = .init() + let container = OracleKeyedEncodingContainer(codingPath: codingPath, encoder: self, object: object!) + return KeyedEncodingContainer(container) + } + + func unkeyedContainer() -> any UnkeyedEncodingContainer { + if let array { + return OracleUnkeyedEncodingContainer(codingPath: codingPath, encoder: self, array: array) + } + + precondition(singleValue == nil && object == nil) + + array = .init() + return OracleUnkeyedEncodingContainer(codingPath: codingPath, encoder: self, array: array!) + } + + func singleValueContainer() -> any SingleValueEncodingContainer { + precondition(array == nil && object == nil) + return OracleSingleValueEncodingContainer(codingPath: codingPath, encoder: self) + } +} + +struct OracleKeyedEncodingContainer: KeyedEncodingContainerProtocol { + let codingPath: [any CodingKey] + let encoder: _OracleJSONEncoder + fileprivate let object: JSONObject + + func encodeNil(forKey key: Key) throws {} + + func encode(_ value: Bool, forKey key: Key) throws { + object.set(.bool(value), for: key.stringValue) + } + + func encode(_ value: String, forKey key: Key) throws { + object.set(.string(value), for: key.stringValue) + } + + func encode(_ value: Double, forKey key: Key) throws { + object.set(.double(value), for: key.stringValue) + } + + func encode(_ value: Float, forKey key: Key) throws { + object.set(.float(value), for: key.stringValue) + } + + func encode(_ value: Int, forKey key: Key) throws { + try encodeFixedWidthInteger(value, for: key) + } + + func encode(_ value: Int8, forKey key: Key) throws { + try encodeFixedWidthInteger(value, for: key) + } + + func encode(_ value: Int16, forKey key: Key) throws { + try encodeFixedWidthInteger(value, for: key) + } + + func encode(_ value: Int32, forKey key: Key) throws { + try encodeFixedWidthInteger(value, for: key) + } + + func encode(_ value: Int64, forKey key: Key) throws { + try encodeFixedWidthInteger(value, for: key) + } + + func encode(_ value: UInt, forKey key: Key) throws { + try encodeFixedWidthInteger(value, for: key) + } + + func encode(_ value: UInt8, forKey key: Key) throws { + try encodeFixedWidthInteger(value, for: key) + } + + func encode(_ value: UInt16, forKey key: Key) throws { + try encodeFixedWidthInteger(value, for: key) + } + + func encode(_ value: UInt32, forKey key: Key) throws { + try encodeFixedWidthInteger(value, for: key) + } + + func encode(_ value: UInt64, forKey key: Key) throws { + try encodeFixedWidthInteger(value, for: key) + } + + func encode(_ value: T, forKey key: Key) throws where T : Encodable { + switch value { + case let value as Date: + self.object.set(.date(value), for: key.stringValue) + case let value as IntervalDS: + self.object.set(.intervalDS(value), for: key.stringValue) + case let value as OracleVectorInt8: + self.object.set(.vectorInt8(value), for: key.stringValue) + case let value as OracleVectorFloat32: + self.object.set(.vectorFloat32(value), for: key.stringValue) + case let value as OracleVectorFloat64: + self.object.set(.vectorFloat64(value), for: key.stringValue) + default: break + } + + let newPath = self.codingPath + [key] + let newEncoder = _OracleJSONEncoder(codingPath: newPath, userInfo: encoder.userInfo) + try value.encode(to: newEncoder) + + guard let value = newEncoder.value else { + preconditionFailure() + } + + self.object.set(value, for: key.stringValue) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { + let newPath = codingPath + [key] + let object = object.setObject(for: key.stringValue) + let nestedContainer = OracleKeyedEncodingContainer( + codingPath: newPath, + encoder: encoder, + object: object + ) + return KeyedEncodingContainer(nestedContainer) + } + + func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer { + let newPath = codingPath + [key] + let array = object.setArray(for: key.stringValue) + let nestedContainer = OracleUnkeyedEncodingContainer( + codingPath: newPath, + encoder: encoder, + array: array + ) + return nestedContainer + } + + func superEncoder() -> any Encoder { + encoder + } + + func superEncoder(forKey key: Key) -> any Encoder { + encoder + } +} + +extension OracleKeyedEncodingContainer { + @inline(__always) + private func encodeFixedWidthInteger(_ value: T, for key: Key) throws { + guard let value = Int(exactly: value) else { + throw EncodingError.invalidValue( + value, + .init( + codingPath: self.codingPath, + debugDescription: "Number \(value) does not fit in \(Int.self)." + ) + ) + } + self.object.set(.int(value), for: key.stringValue) + } +} + + +struct OracleUnkeyedEncodingContainer: UnkeyedEncodingContainer { + var count: Int { encoder.array!.array.count } + let codingPath: [any CodingKey] + let encoder: _OracleJSONEncoder + fileprivate let array: JSONArray + + func encodeNil() throws {} + + func encode(_ value: Bool) throws { + self.array.append(.bool(value)) + } + + func encode(_ value: String) throws { + self.array.append(.string(value)) + } + + func encode(_ value: Double) throws { + self.array.append(.double(value)) + } + + func encode(_ value: Float) throws { + self.array.append(.float(value)) + } + + func encode(_ value: Int) throws { + try self.encodeFixedWidthInteger(value) + } + + func encode(_ value: Int8) throws { + try self.encodeFixedWidthInteger(value) + } + + func encode(_ value: Int16) throws { + try self.encodeFixedWidthInteger(value) + } + + func encode(_ value: Int32) throws { + try self.encodeFixedWidthInteger(value) + } + + func encode(_ value: Int64) throws { + try self.encodeFixedWidthInteger(value) + } + + func encode(_ value: UInt) throws { + try self.encodeFixedWidthInteger(value) + } + + func encode(_ value: UInt8) throws { + try self.encodeFixedWidthInteger(value) + } + + func encode(_ value: UInt16) throws { + try self.encodeFixedWidthInteger(value) + } + + func encode(_ value: UInt32) throws { + try self.encodeFixedWidthInteger(value) + } + + func encode(_ value: UInt64) throws { + try self.encodeFixedWidthInteger(value) + } + + func encode(_ value: some Encodable) throws { + switch value { + case let value as Date: + self.array.append(.date(value)) + case let value as IntervalDS: + self.array.append(.intervalDS(value)) + case let value as OracleVectorInt8: + self.array.append(.vectorInt8(value)) + case let value as OracleVectorFloat32: + self.array.append(.vectorFloat32(value)) + case let value as OracleVectorFloat64: + self.array.append(.vectorFloat64(value)) + default: break + } + + let newPath = self.codingPath + [ArrayKey(index: self.count)] + let newEncoder = _OracleJSONEncoder(codingPath: newPath, userInfo: encoder.userInfo) + try value.encode(to: newEncoder) + + guard let value = newEncoder.value else { + preconditionFailure() + } + + self.array.append(value) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer { + let object = self.array.appendObject() + let newPath = self.encoder.codingPath + [ArrayKey(index: self.count)] + let nestedContainer = OracleKeyedEncodingContainer( + codingPath: newPath, encoder: encoder, object: object + ) + return KeyedEncodingContainer(nestedContainer) + } + + func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { + let array = self.encoder.array!.appendArray() + let newPath = self.codingPath + [ArrayKey(index: self.count)] + let nestedContainer = OracleUnkeyedEncodingContainer( + codingPath: newPath, encoder: encoder, array: array + ) + return nestedContainer + } + + func superEncoder() -> any Encoder { + preconditionFailure() + } +} + +extension OracleUnkeyedEncodingContainer { + @inline(__always) + private func encodeFixedWidthInteger(_ value: T) throws { + guard let value = Int(exactly: value) else { + throw EncodingError.invalidValue( + value, + .init( + codingPath: self.codingPath, + debugDescription: "Number \(value) does not fit in \(Int.self)." + ) + ) + } + self.array.append(.int(value)) + } +} + + +struct OracleSingleValueEncodingContainer: SingleValueEncodingContainer { + let codingPath: [any CodingKey] + let encoder: _OracleJSONEncoder + + mutating func encodeNil() throws {} + + func encode(_ value: Bool) throws { + self.encoder.singleValue = .bool(value) + } + + func encode(_ value: String) throws { + self.encoder.singleValue = .string(value) + } + + func encode(_ value: Float) throws { + self.encoder.singleValue = .float(value) + } + + func encode(_ value: Double) throws { + self.encoder.singleValue = .double(value) + } + + func encode(_ value: Int) throws { + try encodeFixedWidthInteger(value) + } + + func encode(_ value: Int8) throws { + try encodeFixedWidthInteger(value) + } + + func encode(_ value: Int16) throws { + try encodeFixedWidthInteger(value) + } + + func encode(_ value: Int32) throws { + try encodeFixedWidthInteger(value) + } + + func encode(_ value: Int64) throws { + try encodeFixedWidthInteger(value) + } + + func encode(_ value: UInt) throws { + try encodeFixedWidthInteger(value) + } + + func encode(_ value: UInt8) throws { + try encodeFixedWidthInteger(value) + } + + func encode(_ value: UInt16) throws { + try encodeFixedWidthInteger(value) + } + + func encode(_ value: UInt32) throws { + try encodeFixedWidthInteger(value) + } + + func encode(_ value: UInt64) throws { + try encodeFixedWidthInteger(value) + } + + func encode(_ value: some Encodable) throws { + switch value { + case let value as Date: + self.encoder.singleValue = .date(value) + case let value as IntervalDS: + self.encoder.singleValue = .intervalDS(value) + case let value as OracleVectorInt8: + self.encoder.singleValue = .vectorInt8(value) + case let value as OracleVectorFloat32: + self.encoder.singleValue = .vectorFloat32(value) + case let value as OracleVectorFloat64: + self.encoder.singleValue = .vectorFloat64(value) + default: break + } + + try value.encode(to: encoder) + } +} + +extension OracleSingleValueEncodingContainer { + @inline(__always) + private func encodeFixedWidthInteger(_ value: T) throws { + guard let value = Int(exactly: value) else { + throw EncodingError.invalidValue( + value, + .init( + codingPath: self.codingPath, + debugDescription: "Number \(value) does not fit in \(Int.self)." + ) + ) + } + self.encoder.singleValue = .int(value) + } +} + + +// MARK: Implementation details + +private enum JSONFuture { + case value(OracleJSONStorage) + case array(JSONArray) + case object(JSONObject) +} + +private final class JSONArray { + private(set) var array: [JSONFuture] = [] + + init() { + self.array.reserveCapacity(10) + } + + @inline(__always) + func append(_ element: OracleJSONStorage) { + self.array.append(.value(element)) + } + + @inline(__always) + func appendArray() -> JSONArray { + let array = JSONArray() + self.array.append(.array(array)) + return array + } + + @inline(__always) + func appendObject() -> JSONObject { + let object = JSONObject() + self.array.append(.object(object)) + return object + } + + var values: [OracleJSONStorage] { + self.array.map { + switch $0 { + case .value(let value): + value + case .array(let array): + .array(array.values) + case .object(let object): + .container(object.values) + } + } + } +} + +private final class JSONObject { + private(set) var object: [String: JSONFuture] = [:] + + init() { + self.object.reserveCapacity(20) + } + + @inline(__always) + func set(_ value: OracleJSONStorage, for key: String) { + self.object[key] = .value(value) + } + + @inline(__always) + func setArray(for key: String) -> JSONArray { + if case .array(let array) = self.object[key] { + return array + } + + if case .object = self.object[key] { + preconditionFailure(#"A keyed container has already been created for "\#(key)"."#) + } + + let array = JSONArray() + object[key] = .array(array) + return array + } + + @inline(__always) + func setObject(for key: String) -> JSONObject { + if case .object(let object) = self.object[key] { + return object + } + + if case .array = self.object[key] { + preconditionFailure(#"A unkeyed container has already been created for "\#(key)"."#) + } + + let object = JSONObject() + self.object[key] = .object(object) + return object + } + + var values: [String: OracleJSONStorage] { + self.object.mapValues { + switch $0 { + case .value(let value): + value + case .array(let array): + .array(array.values) + case .object(let object): + .container(object.values) + } + } + } +} diff --git a/Sources/OracleNIO/Data/JSON/OracleJSONWriter.swift b/Sources/OracleNIO/Data/JSON/OracleJSONWriter.swift new file mode 100644 index 0000000..c66c8ee --- /dev/null +++ b/Sources/OracleNIO/Data/JSON/OracleJSONWriter.swift @@ -0,0 +1,419 @@ +import NIOCore + +struct OracleJSONWriter { + var maxFieldNameSize: Int = 255 + var fieldNames: [String: FieldName] = [:] + var shortFieldNamesSegment = FieldNameSegment() + var longFieldNamesSegment = FieldNameSegment() + var fieldIDSize = 0 + + mutating func encode(_ value: OracleJSONStorage, into buffer: inout ByteBuffer, maxFieldNameSize: Int) throws { + self.maxFieldNameSize = maxFieldNameSize + var flags = try self.determineFlags(for: value) + + var treeSegment = TreeSegment(buffer: ByteBuffer(), writer: self) + try treeSegment.encodeNode(value) + if treeSegment.buffer.writerIndex > 65535 { + flags |= Constants.TNS_JSON_FLAG_TREE_SEG_UINT32 + } + + // write initial header + buffer.writeInteger(Constants.TNS_JSON_MAGIC_BYTE_1) + buffer.writeInteger(Constants.TNS_JSON_MAGIC_BYTE_2) + buffer.writeInteger(Constants.TNS_JSON_MAGIC_BYTE_3) + if !longFieldNamesSegment.names.isEmpty { + buffer.writeInteger(Constants.TNS_JSON_VERSION_MAX_FNAME_65535) + } else { + buffer.writeInteger(Constants.TNS_JSON_VERSION_MAX_FNAME_255) + } + buffer.writeInteger(flags) + + // write extended header (when value is not scalar) + if !shortFieldNamesSegment.names.isEmpty { + writeExtendedHeader(into: &buffer) + } + + // write size of tree segment + if treeSegment.buffer.writerIndex < 65535 { + buffer.writeInteger(UInt16(treeSegment.buffer.writerIndex)) + } else { + buffer.writeInteger(UInt32(treeSegment.buffer.writerIndex)) + } + + // write remainder of header and any data (when value is not scalar) + if !shortFieldNamesSegment.names.isEmpty { + + // write number of "tiny" nodes (always zero) + buffer.writeInteger(0, as: UInt16.self) + + // write field name segment + writeFieldNameSegment(shortFieldNamesSegment, into: &buffer) + if !longFieldNamesSegment.names.isEmpty { + writeFieldNameSegment(longFieldNamesSegment, into: &buffer) + } + + } + + // write tree segment data + buffer.writeImmutableBuffer(treeSegment.buffer) + } + + private func writeFieldNameSegment(_ segment: FieldNameSegment, into buffer: inout ByteBuffer) { + // write array of hash ids + for name in segment.names { + if name.nameBytes.count <= 255 { + buffer.writeInteger(UInt8(name.hashID & 0xff)) + } else { + buffer.writeInteger(UInt16(name.hashID & 0xffff)) + } + } + + // write array of field name offsets for the short field names + for name in segment.names { + if segment.buffer.writerIndex < 65535 { + buffer.writeInteger(UInt16(name.offset)) + } else { + buffer.writeInteger(UInt32(name.offset)) + } + } + + // write field names + if segment.buffer.writerIndex > 0 { + buffer.writeImmutableBuffer(segment.buffer) + } + } + + private mutating func writeExtendedHeader(into buffer: inout ByteBuffer) { + var secondaryFlags: UInt16 = 0 + + // write number of short field names + if fieldIDSize == 1 { + buffer.writeInteger(UInt8(shortFieldNamesSegment.names.count)) + } else if fieldIDSize == 2 { + buffer.writeInteger(UInt16(shortFieldNamesSegment.names.count)) + } else { + buffer.writeInteger(UInt32(shortFieldNamesSegment.names.count)) + } + + // write size of short field names segment + if shortFieldNamesSegment.buffer.writerIndex < 65535 { + buffer.writeInteger(UInt16(shortFieldNamesSegment.buffer.writerIndex)) + } else { + buffer.writeInteger(UInt32(shortFieldNamesSegment.buffer.writerIndex)) + } + + // write fields for long field names segment, if applicable + if !longFieldNamesSegment.names.isEmpty { + if longFieldNamesSegment.buffer.writerIndex < 65535 { + secondaryFlags = Constants.TNS_JSON_FLAG_SEC_FNAMES_SEG_UINT16 + } + buffer.writeInteger(secondaryFlags) + buffer.writeInteger(UInt32(longFieldNamesSegment.names.count)) + buffer.writeInteger(UInt32(longFieldNamesSegment.buffer.writerIndex)) + } + } + + private mutating func determineFlags(for value: OracleJSONStorage) throws -> UInt16 { + // if value is a single scalar, nothing more needs to be done + var flags = Constants.TNS_JSON_FLAG_INLINE_LEAF + switch value { + case .array, .container: + break + default: + flags |= Constants.TNS_JSON_FLAG_IS_SCALAR + return flags + } + + // examine all values recursively to determine the unique set of field + // names and whether they need to be added to the long field names + // segment (> 255 bytes) or short field names segment (<= 255 bytes) + self.fieldNames = [:] + self.shortFieldNamesSegment = FieldNameSegment(buffer: ByteBuffer(), names: []) + try self.examineNode(value) + + // perform processing of field names segments and determine the total + // number of unique field names in the value + if !shortFieldNamesSegment.names.isEmpty { + shortFieldNamesSegment.processNames(fieldIDOffset: 0) + } + if !longFieldNamesSegment.names.isEmpty { + longFieldNamesSegment.processNames(fieldIDOffset: shortFieldNamesSegment.names.count) + } + + + // determine remaining flags and field id size + let fieldNamesCount = shortFieldNamesSegment.names.count + longFieldNamesSegment.names.count + flags |= Constants.TNS_JSON_FLAG_HASH_ID_UINT8 | Constants.TNS_JSON_FLAG_TINY_NODES_STAT + if fieldNamesCount > 65535 { + flags |= Constants.TNS_JSON_FLAG_NUM_FNAMES_UINT32 + self.fieldIDSize = 4 + } else if fieldNamesCount > 255 { + flags |= Constants.TNS_JSON_FLAG_NUM_FNAMES_UINT16 + self.fieldIDSize = 2 + } else { + self.fieldIDSize = 1 + } + if self.shortFieldNamesSegment.buffer.writerIndex > 65535 { + flags |= Constants.TNS_JSON_FLAG_FNAMES_SEG_UINT32 + } + return flags + } + + private mutating func examineNode(_ node: OracleJSONStorage) throws { + switch node { + case .array(let array): + for element in array { + try self.examineNode(element) + } + case .container(let dictionary): + for (key, value) in dictionary { + if !fieldNames.keys.contains(key) { + try self.addFieldName(key) + } + try self.examineNode(value) + } + default: + return + } + } + + private mutating func addFieldName(_ name: String) throws { + var fieldName = try FieldName(name: name, maxFieldNameSize: maxFieldNameSize) + if fieldName.nameBytes.count <= 255 { + shortFieldNamesSegment.addName(&fieldName) + fieldNames[name] = fieldName + } else { + longFieldNamesSegment.addName(&fieldName) + fieldNames[name] = fieldName + } + } +} + +struct FieldName { + let hashID: Int + let nameBytes: [UInt8] + let name: String + var offset = 0 + var fieldID = 0 + + /// Calculates the hash id to use for the field name. + /// + /// This is based on Bernstein's hash function. + static func calculateHashID(for name: [UInt8]) -> Int { + var hashID = 0x811C9DC5 + for c in name { + hashID = (hashID ^ Int(c)) * 16777619 + } + return hashID + } + + init(name: String, maxFieldNameSize: Int) throws { + self.name = name + self.nameBytes = Array(name.utf8) + if self.nameBytes.count > maxFieldNameSize { + throw EncodingError.invalidValue( + name, + EncodingError.Context( + codingPath: [], + debugDescription: "Field name too long" + ) + ) + } + self.hashID = Self.calculateHashID(for: nameBytes) + } + + /// Returns the sort key to use when sorting field names. + func sortKey() -> (Int, Int, [UInt8]) { + return (hashID & 0xFF, nameBytes.count, nameBytes) + } +} + +struct FieldNameSegment { + var buffer = ByteBuffer() + var names: [FieldName] = [] + + mutating func addName(_ name: inout FieldName) { + name.offset = buffer.writerIndex + if name.nameBytes.count <= 255 { + buffer.writeInteger(UInt8(name.nameBytes.count)) + } else { + buffer.writeInteger(UInt16(name.nameBytes.count)) + } + buffer.writeBytes(name.nameBytes) + names.append(name) + } + + mutating func processNames(fieldIDOffset: Int) { + names.sort { (lhs: FieldName, rhs: FieldName) in + let lhs = lhs.sortKey() + let rhs = rhs.sortKey() + if lhs.0 < rhs.0 { + return true + } + if lhs.0 == rhs.0 && lhs.1 < rhs.1 { + return true + } + if lhs.0 == rhs.0 && lhs.1 == rhs.1 { + for (l, r) in zip(lhs.2, rhs.2) { + if l < r { + return true + } + if l > r { + return false + } + } + } + return false + } + for i in names.indices { + names[i].fieldID = fieldIDOffset + i + 1 + } + } +} + +struct TreeSegment { + var buffer: ByteBuffer + var writer: OracleJSONWriter + + mutating func encodeNode(_ value: OracleJSONStorage) throws { + switch value { + case .none: + buffer.writeInteger(Constants.TNS_JSON_TYPE_NULL) + + case .bool(let value): + if value { + buffer.writeInteger(Constants.TNS_JSON_TYPE_TRUE) + } else { + buffer.writeInteger(Constants.TNS_JSON_TYPE_FALSE) + } + + // TODO: revisit numeric conversions + case .int(let value): + buffer.writeInteger(Constants.TNS_JSON_TYPE_NUMBER_LENGTH_UINT8) + OracleNumeric.encodeNumeric(value, into: &buffer) + case .float(let value): + buffer.writeInteger(Constants.TNS_JSON_TYPE_NUMBER_LENGTH_UINT8) + OracleNumeric.encodeNumeric(value, into: &buffer) + case .double(let value): + buffer.writeInteger(Constants.TNS_JSON_TYPE_NUMBER_LENGTH_UINT8) + OracleNumeric.encodeNumeric(value, into: &buffer) + + case .date(let value): + buffer.writeInteger(Constants.TNS_JSON_TYPE_TIMESTAMP_TZ) + value.encode(into: &buffer, context: .default) + + case .intervalDS(let value): + buffer.writeInteger(Constants.TNS_JSON_TYPE_INTERVAL_DS) + value.encode(into: &buffer, context: .default) + + case .string(let value): + let bytes = Array(value.utf8) + switch bytes.count { + case ..<256: + buffer.writeInteger(Constants.TNS_JSON_TYPE_STRING_LENGTH_UINT8) + try buffer.writeLengthPrefixed(as: UInt8.self) { $0.writeBytes(bytes) } + case ..<65536: + buffer.writeInteger(Constants.TNS_JSON_TYPE_STRING_LENGTH_UINT16) + try buffer.writeLengthPrefixed(as: UInt16.self) { $0.writeBytes(bytes) } + default: + buffer.writeInteger(Constants.TNS_JSON_TYPE_STRING_LENGTH_UINT32) + try buffer.writeLengthPrefixed(as: UInt32.self) { $0.writeBytes(bytes) } + } + + case .vectorInt8(let vector): + buffer.writeInteger(Constants.TNS_JSON_TYPE_EXTENDED) + buffer.writeInteger(Constants.TNS_JSON_TYPE_VECTOR) + try buffer.writeLengthPrefixed(as: UInt32.self) { partial in + let start = partial.writerIndex + _encodeOracleVectorHeader(elements: UInt32(vector.count), format: OracleVectorInt8.vectorFormat, into: &partial) + return partial.writerIndex - start + } + vector._encodeRaw(into: &buffer, context: .default) + case .vectorFloat32(let vector): + buffer.writeInteger(Constants.TNS_JSON_TYPE_EXTENDED) + buffer.writeInteger(Constants.TNS_JSON_TYPE_VECTOR) + try buffer.writeLengthPrefixed(as: UInt32.self) { partial in + let start = partial.writerIndex + _encodeOracleVectorHeader( + elements: UInt32(vector.count), + format: OracleVectorFloat32.vectorFormat, + into: &partial + ) + return partial.writerIndex - start + } + vector._encodeRaw(into: &buffer, context: .default) + case .vectorFloat64(let vector): + buffer.writeInteger(Constants.TNS_JSON_TYPE_EXTENDED) + buffer.writeInteger(Constants.TNS_JSON_TYPE_VECTOR) + try buffer.writeLengthPrefixed(as: UInt32.self) { partial in + let start = partial.writerIndex + _encodeOracleVectorHeader( + elements: UInt32(vector.count), + format: OracleVectorFloat64.vectorFormat, + into: &partial + ) + return partial.writerIndex - start + } + vector._encodeRaw(into: &buffer, context: .default) + + case .array(let array): + try encodeArray(array) + + case .container(let dictionary): + try encodeObject(dictionary) + } + } + + mutating func encodeContainer(nodeType: UInt8, count: Int) { + var nodeType = nodeType + nodeType |= 0x20 // use UInt32 for offsets + if count > 65535 { + nodeType |= 0x10 // count is UInt32 + } else if count > 255 { + nodeType |= 0x08 // count is UInt16 + } + buffer.writeInteger(nodeType) + if count < 256 { + buffer.writeInteger(UInt8(count)) + } else if count < 65536 { + buffer.writeInteger(UInt16(count)) + } else { + buffer.writeInteger(UInt32(count)) + } + } + + mutating func encodeArray(_ array: [OracleJSONStorage]) throws { + encodeContainer(nodeType: Constants.TNS_JSON_TYPE_ARRAY, count: array.count) + var offset = buffer.writerIndex + buffer.reserveCapacity(minimumWritableBytes: array.count * MemoryLayout.size) + for element in array { + buffer.writeInteger(UInt32(buffer.writerIndex), endianness: .big) + offset += MemoryLayout.size + try encodeNode(element) + } + } + + mutating func encodeObject(_ dictionary: [String: OracleJSONStorage]) throws { + encodeContainer(nodeType: Constants.TNS_JSON_TYPE_OBJECT, count: dictionary.count) + var fieldIDOffset = buffer.writerIndex + var valueOffset = buffer.writerIndex + dictionary.count * writer.fieldIDSize + let finalOffset = valueOffset + dictionary.count * MemoryLayout.size + buffer.reserveCapacity(minimumWritableBytes: finalOffset - buffer.writerIndex) + for (key, value) in dictionary { + guard let fieldName = writer.fieldNames[key] else { + throw EncodingError.invalidValue(key, EncodingError.Context(codingPath: [], debugDescription: "Unknown field name: \(key)")) + } + if writer.fieldIDSize == 1 { + buffer.setInteger(UInt8(fieldName.fieldID), at: fieldIDOffset, endianness: .big) + } else if writer.fieldIDSize == 2 { + buffer.setInteger(UInt16(fieldName.fieldID), at: fieldIDOffset, endianness: .big) + } else { + buffer.setInteger(UInt32(fieldName.fieldID), at: fieldIDOffset, endianness: .big) + } + buffer.setInteger(UInt32(buffer.writerIndex), at: valueOffset, endianness: .big) + fieldIDOffset += writer.fieldIDSize + valueOffset += MemoryLayout.size + try self.encodeNode(value) + } + } +} diff --git a/Sources/OracleNIO/OracleCodable.swift b/Sources/OracleNIO/OracleCodable.swift index aa4dd82..e50fc43 100644 --- a/Sources/OracleNIO/OracleCodable.swift +++ b/Sources/OracleNIO/OracleCodable.swift @@ -225,6 +225,8 @@ public struct OracleEncodingContext: Sendable { /// A ``OracleJSONEncoder`` used to encode the object to JSON. public var jsonEncoder: JSONEncoder + var jsonMaximumFieldNameSize: Int = 255 + /// Creates a ``OracleEncodingContext`` with the given ``OracleJSONEncoder``. /// diff --git a/Tests/OracleNIOTests/Data/OracleJSONParserTests.swift b/Tests/OracleNIOTests/Data/OracleJSONParserTests.swift index b7b4747..509a965 100644 --- a/Tests/OracleNIOTests/Data/OracleJSONParserTests.swift +++ b/Tests/OracleNIOTests/Data/OracleJSONParserTests.swift @@ -189,7 +189,7 @@ ])) } - static let scalarValueArguments: [((inout ByteBuffer) throws -> Void, OracleJSONStorage)] = + static let scalarValueArguments: [(@Sendable (inout ByteBuffer) throws -> Void, OracleJSONStorage)] = [ ({ buffer in buffer.writeInteger(Constants.TNS_JSON_TYPE_NULL) }, .none), ({ buffer in buffer.writeInteger(Constants.TNS_JSON_TYPE_TRUE) }, .bool(true)), @@ -300,7 +300,7 @@ @Test(arguments: scalarValueArguments) func scalarValue( - writer: @escaping (inout ByteBuffer) throws -> Void, expected: OracleJSONStorage + writer: @Sendable @escaping (inout ByteBuffer) throws -> Void, expected: OracleJSONStorage ) throws { var buffer = ByteBuffer(bytes: header) buffer.writeInteger( diff --git a/Tests/OracleNIOTests/Data/OracleJSONWriterTests.swift b/Tests/OracleNIOTests/Data/OracleJSONWriterTests.swift new file mode 100644 index 0000000..bb1d672 --- /dev/null +++ b/Tests/OracleNIOTests/Data/OracleJSONWriterTests.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the OracleNIO open source project +// +// Copyright (c) 2024 Timo Zacherl and the OracleNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE for license information +// See CONTRIBUTORS.md for the list of OracleNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.0) +import NIOCore +import Testing + +@testable import OracleNIO + +import struct Foundation.Date + +@Suite struct OracleJSONWriterTests { + @Test func encodeString() throws { + // expected + // 0000 : FF 4A 5A 01 00 10 00 06 |.JZ.....| + // 0008 : 05 76 61 6C 75 65 |.value | + var buffer = ByteBuffer() + var writer = OracleJSONWriter() + try writer.encode(.string("value"), into: &buffer, maxFieldNameSize: 255) + let result = try OracleJSONParser.parse(from: &buffer) + #expect(result == .string("value")) + } +} +#endif