You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/SessionUtilitiesKit/Utilities/BencodeEncoder.swift

401 lines
14 KiB
Swift

// 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<T: Encodable>(_ 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<T>(_ 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<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey {
let container: KeyedContainer<Key> = 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<Key> 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<T>(_ 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<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
let container = _BencodeEncoder.KeyedContainer<NestedKey>(
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<T>(_ 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<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
let container = _BencodeEncoder.KeyedContainer<NestedKey>(
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<T>(_ 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<UInt8>: 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<T>(_ 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<UInt8>: 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<T>(_ 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<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]
}
private protocol _DictionaryProtocol {
typealias Key = String
associatedtype Value: Encodable
func map<T>(_ 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 {}