// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. // // stringlint:disable import Foundation // MARK: - BencodeEncoder public struct BencodeEncoder { public var userInfo: [CodingUserInfoKey: Any] public init(userInfo: [CodingUserInfoKey: Any] = [:], using dependencies: Dependencies = Dependencies()) { self.userInfo = userInfo self.userInfo[Dependencies.userInfoKey] = dependencies } public func encode(_ value: T) throws -> Data { let encoder: _BencodeEncoder = _BencodeEncoder(userInfo: userInfo) try value.encode(to: encoder) switch encoder.container { case let bencodeContainer as _BencodeEncoder.UnkeyedContainer: return (encoder.data + (bencodeContainer.additionalData ?? Data())) default: return encoder.data } } } // MARK: - AdditionalData Support public extension UnkeyedEncodingContainer { mutating func encodeAdditionalData(_ data: T) throws where T: Encodable, T: DataProtocol { let finalData: Data = try { switch data { case let value as Data: return value case let value as [UInt8]: return Data(value) default: throw EncodingError.invalidValue( data, EncodingError.Context( codingPath: codingPath, debugDescription: "unable to encode additional data" ) ) } }() switch self { case let bencodeContainer as _BencodeEncoder.UnkeyedContainer: bencodeContainer.additionalData = finalData default: try self.encode(finalData) } } } // MARK: - _BencodeEncodingContainer protocol _BencodeEncodingContainer: AnyObject { var data: Data { get } } // MARK: - _BencodeEncoder final class _BencodeEncoder { var codingPath: [CodingKey] = [] var userInfo: [CodingUserInfoKey: Any] = [:] fileprivate var container: _BencodeEncodingContainer? var data: Data { return container?.data ?? Data() } init(codingPath: [CodingKey] = [], userInfo: [CodingUserInfoKey: Any]) { self.codingPath = codingPath self.userInfo = userInfo } } extension _BencodeEncoder: Encoder { func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { let container: KeyedContainer = KeyedContainer(codingPath: codingPath, userInfo: userInfo) self.container = container return KeyedEncodingContainer(container) } func unkeyedContainer() -> UnkeyedEncodingContainer { let container = UnkeyedContainer(codingPath: self.codingPath, userInfo: self.userInfo) self.container = container return container } func singleValueContainer() -> SingleValueEncodingContainer { let container = SingleValueContainer(codingPath: self.codingPath, userInfo: self.userInfo) self.container = container return container } } // MARK: - KeyedContainer private extension _BencodeEncoder { class KeyedContainer where Key: CodingKey { private var storage: [String: (any _BencodeEncodingContainer)] = [:] var codingPath: [CodingKey] var userInfo: [CodingUserInfoKey: Any] init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey: Any]) { self.codingPath = codingPath self.userInfo = userInfo } } } extension _BencodeEncoder.KeyedContainer: _BencodeEncodingContainer { var data: Data { ( Data(Bencode.Element.dictIndicator.rawValue.utf8.map { UInt8($0) }) + storage .sorted(by: { $0.key < $1.key }) // Should be in lexicographical order .map { key, value in ( try! _BencodeEncoder.SingleValueContainer.encodedString(key, codingPath: codingPath) + value.data ) } .reduce(Data(), +) + Data(Bencode.Element.endIndicator.rawValue.utf8.map { UInt8($0) }) ) } } extension _BencodeEncoder.KeyedContainer: KeyedEncodingContainerProtocol { func encodeNil(forKey key: Key) throws {} // Just omit nil elements func encode(_ value: T, forKey key: Key) throws where T: Encodable { var container = self.nestedSingleValueContainer(forKey: key) try container.encode(value) } private func nestedSingleValueContainer(forKey key: Key) -> SingleValueEncodingContainer { let container = _BencodeEncoder.SingleValueContainer( codingPath: codingPath + [key], userInfo: userInfo ) storage[key.stringValue] = container return container } func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { let container = _BencodeEncoder.UnkeyedContainer( codingPath: codingPath + [key], userInfo: userInfo ) storage[key.stringValue] = container return container } func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey: CodingKey { let container = _BencodeEncoder.KeyedContainer( codingPath: codingPath + [key], userInfo: userInfo ) storage[key.stringValue] = container return KeyedEncodingContainer(container) } func superEncoder() -> Encoder { _BencodeEncoder( codingPath: codingPath + [Bencode.SuperCodingKey()], userInfo: userInfo ) } func superEncoder(forKey key: Key) -> Encoder { _BencodeEncoder( codingPath: codingPath + [key], userInfo: userInfo ) } } // MARK: - UnkeyedContainer extension _BencodeEncoder { final class UnkeyedContainer { private var storage: [any _BencodeEncodingContainer] = [] var codingPath: [CodingKey] var userInfo: [CodingUserInfoKey: Any] var additionalData: Data? var count: Int { return storage.count } init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey: Any]) { self.codingPath = codingPath self.userInfo = userInfo } } } extension _BencodeEncoder.UnkeyedContainer: _BencodeEncodingContainer { var data: Data { return ( Data(Bencode.Element.listIndicator.rawValue.utf8.map { UInt8($0) }) + storage.map { $0.data }.reduce(Data(), +) + Data(Bencode.Element.endIndicator.rawValue.utf8.map { UInt8($0) }) ) } } extension _BencodeEncoder.UnkeyedContainer: UnkeyedEncodingContainer { func encodeNil() throws {} // Just omit nil elements func encode(_ value: T) throws where T: Encodable { var container = nestedSingleValueContainer() try container.encode(value) } private func nestedSingleValueContainer() -> SingleValueEncodingContainer { let container = _BencodeEncoder.SingleValueContainer( codingPath: codingPath + [Bencode.UnkeyedCodingKey(intValue: storage.count)], userInfo: userInfo ) storage.append(container) return container } func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey: CodingKey { let container = _BencodeEncoder.KeyedContainer( codingPath: codingPath + [Bencode.UnkeyedCodingKey(intValue: storage.count)], userInfo: userInfo ) storage.append(container) return KeyedEncodingContainer(container) } func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { let container = _BencodeEncoder.UnkeyedContainer( codingPath: codingPath + [Bencode.UnkeyedCodingKey(intValue: storage.count)], userInfo: userInfo ) storage.append(container) return container } func superEncoder() -> Encoder { _BencodeEncoder( codingPath: codingPath + [Bencode.UnkeyedCodingKey(intValue: storage.count)], userInfo: userInfo ) } } // MARK: - SingleValueContainer extension _BencodeEncoder { final class SingleValueContainer { fileprivate var storage: Data = Data() var codingPath: [CodingKey] var userInfo: [CodingUserInfoKey: Any] init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey: Any]) { self.codingPath = codingPath self.userInfo = userInfo } } } extension _BencodeEncoder.SingleValueContainer: _BencodeEncodingContainer { var data: Data { storage } } extension _BencodeEncoder.SingleValueContainer: SingleValueEncodingContainer { func encodeNil() throws { throw EncodingError.invalidValue(0, EncodingError.Context( codingPath: codingPath, debugDescription: "Null values are not supported" )) } func encode(_ value: Int) throws { let intValue: String = "\(Bencode.Element.intIndicator.rawValue)\(value)\(Bencode.Element.endIndicator.rawValue)" storage = Data(intValue.utf8.map { UInt8($0) }) } func encode(_ value: String) throws { storage = try _BencodeEncoder.SingleValueContainer.encodedString(value, codingPath: codingPath) } func encode(_ value: T) throws where T: Encodable { // Encode non-primative encodable types as dictionaries switch value { case let intValue as Int: try encode(intValue) case let stringValue as String: try encode(stringValue) case let dataValue as Data: storage = encodedData(dataValue) case let rawDataValue as Array: storage = encodedRawArray(rawDataValue) case let arrayValue as any _ArrayProtocol: storage = try encodedArray(arrayValue) case let dictValue as any _DictionaryProtocol: storage = try encodedDict(dictValue) default: let encoder = _BencodeEncoder(codingPath: codingPath, userInfo: userInfo) try value.encode(to: encoder) storage = encoder.data } } // MARK: - Explicit type encoding fileprivate static func encodedString(_ value: String, codingPath: [CodingKey]) throws -> Data { let encodedString: Data = try value.data(using: .ascii) ?? { throw EncodingError.invalidValue(value, EncodingError.Context( codingPath: codingPath, debugDescription: "Could not convert string to ASCII data" )) }() let prefix: String = "\(encodedString.count)\(Bencode.Element.separator.rawValue)" return (Data(prefix.utf8.map { UInt8($0) }) + encodedString) } private func encodedData(_ value: Data) -> Data { // Data should be in the same format as a String let prefix: String = "\(value.count)\(Bencode.Element.separator.rawValue)" return (Data(prefix.utf8.map { UInt8($0) }) + value) } private func encodedRawArray(_ value: [UInt8]) -> Data { // Bytes arrays should be in the same format as a String let prefix: String = "\(value.count)\(Bencode.Element.separator.rawValue)" return (Data(prefix.utf8.map { UInt8($0) }) + value) } private func encodedArray(_ value: T) throws -> Data where T: _ArrayProtocol, T.Element: Encodable { let result: [Data] = try value .compactMap { element in switch element { case let dataValue as Data: return encodedData(dataValue) case let rawDataValue as Array: return encodedRawArray(rawDataValue) default: let elementEncoder: _BencodeEncoder = _BencodeEncoder( codingPath: codingPath, userInfo: userInfo ) try element.encode(to: elementEncoder) return elementEncoder.data } } return ( Data(Bencode.Element.listIndicator.rawValue.utf8.map { UInt8($0) }) + Data(result.joined()) + Data(Bencode.Element.endIndicator.rawValue.utf8.map { UInt8($0) }) ) } private func encodedDict(_ value: T) throws -> Data where T: _DictionaryProtocol, T.Key == String, T.Value: Encodable { let encodedData: [Data] = try value .sorted(by: { $0.key < $1.key }) // Should be in lexicographical order .map { key, item in let itemEncoder = _BencodeEncoder( codingPath: codingPath, userInfo: userInfo ) try item.encode(to: itemEncoder) return ( try _BencodeEncoder.SingleValueContainer.encodedString(key, codingPath: codingPath) + itemEncoder.data ) } return ( Data(Bencode.Element.dictIndicator.rawValue.utf8.map { UInt8($0) }) + Data(encodedData.joined()) + Data(Bencode.Element.endIndicator.rawValue.utf8.map { UInt8($0) }) ) } } // MARK: - Convenience Protocols private protocol _ArrayProtocol { associatedtype Element: Encodable func compactMap(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] } private protocol _DictionaryProtocol { typealias Key = String associatedtype Value: Encodable func map(_ transform: ((key: Key, value: Value)) throws -> T) rethrows -> [T] func sorted(by areInIncreasingOrder: ((key: Key, value: Value), (key: Key, value: Value)) throws -> Bool) rethrows -> [(key: Key, value: Value)] } extension Array: _ArrayProtocol where Element: Encodable {} extension Dictionary: _DictionaryProtocol where Key == String, Value: Encodable {}