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")!)))
}
}