From dfc5803c1d263e048a45ec1f22bcc72b3967428e Mon Sep 17 00:00:00 2001 From: cryptoAlgorithm Date: Wed, 8 Jun 2022 23:02:46 +0800 Subject: [PATCH] Basic untyped dict decoding This is currently very badly written, mostly as a POC at this stage. --- .../xcshareddata/xcschemes/ETFKit.xcscheme | 91 +++++ README.md | 4 +- Sources/ETFKit/Decoder/ETFDecoder.swift | 37 +++ .../Decoder/ETFKeyedDecodingContainer.swift | 312 ++++++++++++++++++ Sources/ETFKit/ETFKit.swift | 128 ++++++- Sources/ETFKit/Extensions/Data+.swift | 14 + Tests/ETFKitTests/ETFKitTests.swift | 16 +- 7 files changed, 596 insertions(+), 6 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/ETFKit.xcscheme create mode 100644 Sources/ETFKit/Decoder/ETFDecoder.swift create mode 100644 Sources/ETFKit/Decoder/ETFKeyedDecodingContainer.swift create mode 100644 Sources/ETFKit/Extensions/Data+.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ETFKit.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ETFKit.xcscheme new file mode 100644 index 0000000..db39168 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/ETFKit.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 5bc462a..eb90c11 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # ETFKit -A description of this package. +Decode/encode Erlang External Term format (ETF) version 131. + +> This is a work in progress. diff --git a/Sources/ETFKit/Decoder/ETFDecoder.swift b/Sources/ETFKit/Decoder/ETFDecoder.swift new file mode 100644 index 0000000..95cfe6b --- /dev/null +++ b/Sources/ETFKit/Decoder/ETFDecoder.swift @@ -0,0 +1,37 @@ +// +// ETFDecoder.swift +// +// +// Created by Vincent Kwok on 8/6/22. +// + +import Foundation + +open class ETFDecoder { + open func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { + return try type.init(from: _ETFDecoder()) + } +} + +internal class _ETFDecoder: Decoder { + internal(set) public var codingPath: [CodingKey] + + /// Contextual user-provided information for use during encoding. + public var userInfo: [CodingUserInfoKey : Any] = [:] + + func container(keyedBy: Key.Type) throws -> KeyedDecodingContainer { + fatalError() + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + fatalError() + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + fatalError() + } + + init(at codingPath: [CodingKey] = []) { + self.codingPath = codingPath + } +} diff --git a/Sources/ETFKit/Decoder/ETFKeyedDecodingContainer.swift b/Sources/ETFKit/Decoder/ETFKeyedDecodingContainer.swift new file mode 100644 index 0000000..a565433 --- /dev/null +++ b/Sources/ETFKit/Decoder/ETFKeyedDecodingContainer.swift @@ -0,0 +1,312 @@ +// +// ETFKeyedDecodingContainer.swift +// +// +// Created by Vincent Kwok on 8/6/22. +// + +import Foundation + +/*internal struct _ETFKeyedDecodingContainer : KeyedDecodingContainerProtocol { + typealias Key = K + + // MARK: Properties + + /// A reference to the decoder we're reading from. + private let decoder: _ETFDecoder + + /// A reference to the container we're reading from. + private let container: [String : Any] + + /// The path of coding keys taken to get to this point in decoding. + private(set) public var codingPath: [CodingKey] + + // MARK: - Initialization + + /// Initializes `self` by referencing the given decoder and container. + internal init(referencing decoder: _ETFDecoder, wrapping container: [String : Any]) { + self.decoder = decoder + self.codingPath = decoder.codingPath + } + + // MARK: - KeyedDecodingContainerProtocol Methods + + public var allKeys: [Key] { + return self.container.keys.compactMap { Key(stringValue: $0) } + } + + public func contains(_ key: Key) -> Bool { + return self.container[key.stringValue] != nil + } + + private func _errorDescription(of key: CodingKey) -> String { + return "\(key) (\"\(key.stringValue)\")" + } + + public func decodeNil(forKey key: Key) throws -> Bool { + if let entry = self.container[key.stringValue] { + return entry is NSNull + } else { + return true + } + } + + public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let value = try self.decoder.unbox(entry, as: Bool.self) else { + throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) + } + + return value + } + + public func decode(_ type: Int.Type, forKey key: Key) throws -> Int { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let value = try self.decoder.unbox(entry, as: Int.self) else { + throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) + } + + return value + } + + public func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + + + return value + } + + public func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + + + return value + } + + public func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + + + return value + } + + public func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + + + return value + } + + public func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + + + return value + } + + public func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + + + return value + } + + public func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + + + return value + } + + public func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + + + return value + } + + public func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + + + return value + } + + public func decode(_ type: Float.Type, forKey key: Key) throws -> Float { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + + + return value + } + + public func decode(_ type: Double.Type, forKey key: Key) throws -> Double { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + + + return value + } + + public func decode(_ type: String.Type, forKey key: Key) throws -> String { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + + + return value + } + + public func decode(_ type: T.Type, forKey key: Key) throws -> T { + guard let entry = self.container[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + + + return value + } + + public func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let value = self.container[key.stringValue] else { + throw DecodingError.keyNotFound( + key, + DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get \(KeyedDecodingContainer.self) -- no value found for key \"\(key.stringValue)\"" + ) + ) + } + + guard let dictionary = value as? [String : Any] else { + throw DecodingError._typeMismatch(at: self.codingPath, expectation: [String : Any].self, reality: value) + } + + let container = _XMLKeyedDecodingContainer(referencing: self.decoder, wrapping: dictionary) + return KeyedDecodingContainer(container) + } + + public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let value = self.container[key.stringValue] else { + throw DecodingError.keyNotFound( + key, + DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get UnkeyedDecodingContainer -- no value found for key \"\(key.stringValue)\"" + ) + ) + } + + guard let array = value as? [Any] else { + throw DecodingError._typeMismatch(at: self.codingPath, expectation: [Any].self, reality: value) + } + + return _XMLUnkeyedDecodingContainer(referencing: self.decoder, wrapping: array) + } + + private func _superDecoder(forKey key: CodingKey) throws -> Decoder { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value: Any = self.container[key.stringValue] ?? NSNull() + return _XMLDecoder(referencing: value, at: self.decoder.codingPath, options: self.decoder.options) + } + + public func superDecoder() throws -> Decoder { + return try _superDecoder(forKey: ) + } + + public func superDecoder(forKey key: Key) throws -> Decoder { + return try _superDecoder(forKey: key) + } +} +*/ diff --git a/Sources/ETFKit/ETFKit.swift b/Sources/ETFKit/ETFKit.swift index dcc8d5b..65e6e31 100644 --- a/Sources/ETFKit/ETFKit.swift +++ b/Sources/ETFKit/ETFKit.swift @@ -1,6 +1,130 @@ +import Foundation + public struct ETFKit { - public private(set) var text = "Hello, World!" + static let VERSION = 131 + + enum ETFTags: Int { + case SMALL_INT = 97 + case INTEGER = 98 + case FLOAT = 99 + case LIST = 108 + case BINARY = 109 + case MAP = 116 + } + + enum ETFDecodingError: Error { + case MismatchingVersion(String) + case MismatchingTag(String) + case UnhandledTag(String) + } + + internal static func parseDict(data: Data) throws -> [String : Any] { + // Decode header + guard data[0] == ETFKit.VERSION else { + throw ETFDecodingError.MismatchingTag("Expected version \(ETFKit.VERSION), got \(data[0])") + } + + var idx = 1 + return try decodingMap(data: data, from: &idx) + } +} + +extension ETFKit { + fileprivate static func decodingValue(data: Data, from idx: inout Int) throws -> UInt8 { + guard data[idx] == ETFTags.SMALL_INT.rawValue else { + throw ETFDecodingError.MismatchingTag("Expected tag \(ETFTags.SMALL_INT), got \(data[idx])") + } + idx += 2 + return data[idx - 1] + } + fileprivate static func decodingValue(data: Data, from idx: inout Int) throws -> Int32 { + guard data[idx] == ETFTags.INTEGER.rawValue else { + throw ETFDecodingError.MismatchingTag("Expected tag \(ETFTags.INTEGER), got \(data[idx])") + } + idx += 5 + return data.subdata(in: idx-4.. Double { + throw ETFDecodingError.MismatchingTag("") + } + fileprivate static func decodingValue(data: Data, from idx: inout Int) throws -> String { + guard data[idx] == ETFTags.BINARY.rawValue else { + throw ETFDecodingError.MismatchingTag("Expected tag \(ETFTags.BINARY), got \(data[idx])") + } + idx += 1 + let dataStart = idx + 4 + let to = dataStart + Int(data.subdata(in: idx.. [Any] { + guard data[idx] == ETFTags.LIST.rawValue else { + throw ETFDecodingError.MismatchingTag("Expected tag \(ETFTags.LIST), got \(data[idx])") + } + + idx += 1 + var len = Int(data.subdata(in: idx.. 0 { + switch ETFTags(rawValue: Int(data[idx])) { + case .SMALL_INT: + let val: UInt8 = try decodingValue(data: data, from: &idx) + arr.append(val) + case .INTEGER: + let val: Int32 = try decodingValue(data: data, from: &idx) + arr.append(val) + case .MAP: + arr.append(try decodingMap(data: data, from: &idx)) + case .LIST: + arr.append(try decodingArray(data: data, from: &idx)) + case .BINARY: + let val: String = try decodingValue(data: data, from: &idx) + arr.append(val) + default: + throw ETFDecodingError.UnhandledTag("Tag \(data[idx]) is not handled") + } + len -= 1 + } + return arr + } + + fileprivate static func decodingMap(data: Data, from idx: inout Int) throws -> [String : Any] { + guard data[idx] == ETFTags.MAP.rawValue else { + throw ETFDecodingError.MismatchingTag("Expected tag \(ETFTags.MAP), got \(data[idx])") + } + + idx += 4 + var pairs = data[idx] + var dict: [String : Any] = [:] + idx += 1 + + while pairs > 0 { + let key: String = try decodingValue(data: data, from: &idx) + switch ETFTags(rawValue: Int(data[idx])) { + case .SMALL_INT: + let val: UInt8 = try decodingValue(data: data, from: &idx) + dict.updateValue(val, forKey: key) + case .INTEGER: + let val: Int32 = try decodingValue(data: data, from: &idx) + dict.updateValue(val, forKey: key) + case .MAP: + dict.updateValue(try decodingMap(data: data, from: &idx), forKey: key) + case .LIST: + dict.updateValue(try decodingArray(data: data, from: &idx), forKey: key) + case .BINARY: + // Binaries are always strings + let val: String = try decodingValue(data: data, from: &idx) + dict.updateValue(val, forKey: key) + default: + throw ETFDecodingError.UnhandledTag("Tag \(data[idx]) is not handled") + } + pairs -= 1 + } - public init() { + return dict } } diff --git a/Sources/ETFKit/Extensions/Data+.swift b/Sources/ETFKit/Extensions/Data+.swift new file mode 100644 index 0000000..4480a7c --- /dev/null +++ b/Sources/ETFKit/Extensions/Data+.swift @@ -0,0 +1,14 @@ +// +// Data+.swift +// +// +// Created by Vincent Kwok on 8/6/22. +// + +import Foundation + +extension Data { + internal func toUInt32() -> Int32 { + Int32(bigEndian: self.withUnsafeBytes { $0.pointee }) + } +} diff --git a/Tests/ETFKitTests/ETFKitTests.swift b/Tests/ETFKitTests/ETFKitTests.swift index 1d3ac63..c49f80b 100644 --- a/Tests/ETFKitTests/ETFKitTests.swift +++ b/Tests/ETFKitTests/ETFKitTests.swift @@ -1,11 +1,21 @@ import XCTest @testable import ETFKit -final class ETFKitTests: XCTestCase { - func testExample() throws { +final class DecodeTests: XCTestCase { + // These tests are just here for testing + func testObject() throws { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - XCTAssertEqual(ETFKit().text, "Hello, World!") + // XCTAssertEqual(ETFKit().text, "Hello, World!") + print(String(describing: try ETFKit.parseDict(data: Data(base64Encoded: "g3QAAAACbQAAAAFhYjp7/1VtAAAAAWJtAAABWWNpb2V3aGpmZ2lvZXdqZmlvZXdqaW9mYWV3dWlmaGF3ZWlvZmhhaXV3ZW9oZml1YWV3aGZ1aXdhZW5idWl2aHdhZXVpdmh3ZWF1aWZoaW93ZWFoZmlvd2VhamZpb3dlYWpmaW9ld2phaW9mandlYWlvZndlYWlvaGlvaGllb2F2d2huYXdldWlvdm5iaHV3ZWFiaG51aW9ld2FoZm5pb3dhZWhudmlvZndlYWhmb2phZXdvaWZqZXdhaW9mamF3ZWlvdm5oZWl3b2FoamZpb2p3YWZpb3dhZWpmb2lld2FvZmF3ZWZhd2VmaXdvZmpod2Vpb2hmaWVvd2hmaW9ld2hmb2lld2pmaW9ld2hucWdvaWhlbnF3b2dpaHF3ZWlvZ2poZXdxaW9naGV3aW9xaGdpb2Vxd2hnaW9ld2hxZ2lvZXdxaGdpb2V3cWhnaW9ld3FoZ2lvcWV3Zw==")!))) + } + + func testBigEndian() throws { + print(String(describing: try ETFKit.parseDict(data: Data(base64Encoded: "g3QAAAACbQAAAAFhYjp7/1VtAAAAAWJtAAAAAWM=")!))) + } + + func testList() throws { + print(String(describing: try ETFKit.parseDict(data: Data(base64Encoded: "g3QAAAABbQAAAAFhbAAAAANtAAAAAWJtAAAAAWNtAAAAAWRq")!))) } }