Torrent UDP Tracker

This commit is contained in:
Ben Davis
2017-07-06 18:33:07 +02:00
parent 14a1c5536b
commit c6280c6db9
13 changed files with 804 additions and 152 deletions
+17 -5
View File
@@ -12,6 +12,8 @@
867491B4C409A91D4D72CCE7 /* Pods_BitTorrent_BitTorrentExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D97F9FB9F74204574FF6840B /* Pods_BitTorrent_BitTorrentExample.framework */; };
B50B24F71F0A553F00C23E7C /* UDPConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50B24F61F0A553B00C23E7C /* UDPConnectionTests.swift */; };
B50B24F91F0A554A00C23E7C /* UDPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50B24F81F0A554A00C23E7C /* UDPConnection.swift */; };
B51D6C091F0C180600E1E3AB /* TorrentUDPTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51D6C031F0C17AE00E1E3AB /* TorrentUDPTrackerTests.swift */; };
B51D6C0A1F0C180D00E1E3AB /* TorrentUDPTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51D6C061F0C17C000E1E3AB /* TorrentUDPTracker.swift */; };
B537CF061F03148B0084089B /* HTTPConnectionStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = B537CF051F03148B0084089B /* HTTPConnectionStub.swift */; };
B537CF461F031AD20084089B /* TestText.torrent in Resources */ = {isa = PBXBuildFile; fileRef = B5E9B0E31F02F9E700EF58E3 /* TestText.torrent */; };
B537CF491F031C3D0084089B /* BEncode.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B54D0C1A1CA53983004343BD /* BEncode.framework */; };
@@ -34,6 +36,7 @@
B585AB781C3833450093FA41 /* BitTorrent.h in Headers */ = {isa = PBXBuildFile; fileRef = B585AB771C3833450093FA41 /* BitTorrent.h */; settings = {ATTRIBUTES = (Public, ); }; };
B585AB7F1C3833450093FA41 /* BitTorrent.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B585AB741C3833450093FA41 /* BitTorrent.framework */; };
B585AB841C3833450093FA41 /* BitTorrentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B585AB831C3833450093FA41 /* BitTorrentTests.swift */; };
B59E1B281F0E6EA3007753CE /* BitTorrentTestMacros.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59E1B261F0E6E5F007753CE /* BitTorrentTestMacros.swift */; };
B5BD7FD61F03032400621BC2 /* TorrentHTTPTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BD7FD51F03032400621BC2 /* TorrentHTTPTrackerTests.swift */; };
B5E977961CAFB46B0038EBE7 /* String+URLEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E977951CAFB46B0038EBE7 /* String+URLEncode.swift */; };
B5E9B0D81F02E6F800EF58E3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E9B0D71F02E6F800EF58E3 /* AppDelegate.swift */; };
@@ -119,6 +122,8 @@
A8680B0EA21B5A20E03DEE6D /* Pods_BitTorrentTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BitTorrentTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
B50B24F61F0A553B00C23E7C /* UDPConnectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPConnectionTests.swift; sourceTree = "<group>"; };
B50B24F81F0A554A00C23E7C /* UDPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPConnection.swift; sourceTree = "<group>"; };
B51D6C031F0C17AE00E1E3AB /* TorrentUDPTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentUDPTrackerTests.swift; sourceTree = "<group>"; };
B51D6C061F0C17C000E1E3AB /* TorrentUDPTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentUDPTracker.swift; sourceTree = "<group>"; };
B537CF051F03148B0084089B /* HTTPConnectionStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HTTPConnectionStub.swift; path = "HTTP Networking/HTTPConnectionStub.swift"; sourceTree = "<group>"; };
B54D0C141CA53983004343BD /* BEncode.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = BEncode.xcodeproj; path = Submodules/BEncodeSwift/BEncode.xcodeproj; sourceTree = "<group>"; };
B54D0C231CA56A22004343BD /* TorrentMetaInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TorrentMetaInfo.swift; path = Models/TorrentMetaInfo.swift; sourceTree = "<group>"; };
@@ -133,7 +138,7 @@
B551C9541F0B9D3D004115CB /* GCDAsyncUdpSocketStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GCDAsyncUdpSocketStub.swift; sourceTree = "<group>"; };
B5530DB11F03063300F71CCD /* HTTPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HTTPConnection.swift; path = "HTTP Networking/HTTPConnection.swift"; sourceTree = "<group>"; };
B5530DB41F03063E00F71CCD /* HTTPConnectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HTTPConnectionTests.swift; path = "HTTP Networking/HTTPConnectionTests.swift"; sourceTree = "<group>"; };
B55317DB1F02FC4D00909ADF /* TorrentHTTPTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TorrentHTTPTracker.swift; path = Tracker/TorrentHTTPTracker.swift; sourceTree = "<group>"; };
B55317DB1F02FC4D00909ADF /* TorrentHTTPTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentHTTPTracker.swift; sourceTree = "<group>"; };
B55317DF1F02FE1500909ADF /* URLEncodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = URLEncodeTests.swift; path = "HTTP Networking/URLEncodeTests.swift"; sourceTree = "<group>"; };
B558F4821F0A647D00438BB4 /* InternetProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetProtocol.swift; sourceTree = "<group>"; };
B558F4841F0A73D000438BB4 /* InternetProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetProtocolTests.swift; sourceTree = "<group>"; };
@@ -144,7 +149,8 @@
B585AB7E1C3833450093FA41 /* BitTorrentTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BitTorrentTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
B585AB831C3833450093FA41 /* BitTorrentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BitTorrentTests.swift; path = BitTorrentTests/BitTorrentTests.swift; sourceTree = SOURCE_ROOT; };
B585AB851C3833450093FA41 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B5BD7FD51F03032400621BC2 /* TorrentHTTPTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TorrentHTTPTrackerTests.swift; path = Tracker/TorrentHTTPTrackerTests.swift; sourceTree = "<group>"; };
B59E1B261F0E6E5F007753CE /* BitTorrentTestMacros.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BitTorrentTestMacros.swift; path = Utilities/BitTorrentTestMacros.swift; sourceTree = "<group>"; };
B5BD7FD51F03032400621BC2 /* TorrentHTTPTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentHTTPTrackerTests.swift; sourceTree = "<group>"; };
B5E977951CAFB46B0038EBE7 /* String+URLEncode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "String+URLEncode.swift"; path = "HTTP Networking/String+URLEncode.swift"; sourceTree = "<group>"; };
B5E9B0D51F02E6F800EF58E3 /* BitTorrentExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BitTorrentExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
B5E9B0D71F02E6F800EF58E3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -154,7 +160,7 @@
B5E9B0E31F02F9E700EF58E3 /* TestText.torrent */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestText.torrent; sourceTree = "<group>"; };
B5E9B0E41F02F9E700EF58E3 /* text.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = text.txt; sourceTree = "<group>"; };
B5F81E481F0436D600B25C70 /* TorrentPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentPeer.swift; sourceTree = "<group>"; };
B5F81E4A1F04399800B25C70 /* TorrentTrackerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TorrentTrackerResponse.swift; path = Tracker/TorrentTrackerResponse.swift; sourceTree = "<group>"; };
B5F81E4A1F04399800B25C70 /* TorrentTrackerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentTrackerResponse.swift; sourceTree = "<group>"; };
D437CE230D6B283EED6C1A3E /* Pods-BitTorrent.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BitTorrent.debug.xcconfig"; path = "Pods/Target Support Files/Pods-BitTorrent/Pods-BitTorrent.debug.xcconfig"; sourceTree = "<group>"; };
D97F9FB9F74204574FF6840B /* Pods_BitTorrent_BitTorrentExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BitTorrent_BitTorrentExample.framework; sourceTree = BUILT_PRODUCTS_DIR; };
EDC27D7221FFFF5E25D0A77F /* Pods-BitTorrent-BitTorrentExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BitTorrent-BitTorrentExample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-BitTorrent-BitTorrentExample/Pods-BitTorrent-BitTorrentExample.debug.xcconfig"; sourceTree = "<group>"; };
@@ -286,11 +292,13 @@
B55317DA1F02FC3000909ADF /* Tracker */ = {
isa = PBXGroup;
children = (
B55317DB1F02FC4D00909ADF /* TorrentHTTPTracker.swift */,
B5F81E4A1F04399800B25C70 /* TorrentTrackerResponse.swift */,
B55317DB1F02FC4D00909ADF /* TorrentHTTPTracker.swift */,
B5BD7FD51F03032400621BC2 /* TorrentHTTPTrackerTests.swift */,
B51D6C061F0C17C000E1E3AB /* TorrentUDPTracker.swift */,
B51D6C031F0C17AE00E1E3AB /* TorrentUDPTrackerTests.swift */,
);
name = Tracker;
path = Tracker;
sourceTree = "<group>";
};
B585AB6A1C3833450093FA41 = {
@@ -339,6 +347,7 @@
children = (
B585AB851C3833450093FA41 /* Info.plist */,
B56A8A061C83539300426AC8 /* TestHelpers.swift */,
B59E1B261F0E6E5F007753CE /* BitTorrentTestMacros.swift */,
);
path = BitTorrentTests;
sourceTree = "<group>";
@@ -741,6 +750,7 @@
B54D0C7B1CA69FD8004343BD /* Data+sha1.swift in Sources */,
B5530DB21F03063300F71CCD /* HTTPConnection.swift in Sources */,
B50B24F91F0A554A00C23E7C /* UDPConnection.swift in Sources */,
B51D6C0A1F0C180D00E1E3AB /* TorrentUDPTracker.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -748,6 +758,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B51D6C091F0C180600E1E3AB /* TorrentUDPTrackerTests.swift in Sources */,
B537CF061F03148B0084089B /* HTTPConnectionStub.swift in Sources */,
B5BD7FD61F03032400621BC2 /* TorrentHTTPTrackerTests.swift in Sources */,
B556D5AE1F0BA1FD00277B8D /* GCDAsyncUdpSocketStub.swift in Sources */,
@@ -758,6 +769,7 @@
B55317E01F02FE1500909ADF /* URLEncodeTests.swift in Sources */,
B558F4851F0A73D000438BB4 /* InternetProtocolTests.swift in Sources */,
B50B24F71F0A553F00C23E7C /* UDPConnectionTests.swift in Sources */,
B59E1B281F0E6EA3007753CE /* BitTorrentTestMacros.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+1 -3
View File
@@ -46,9 +46,7 @@ struct TorrentPeerInfo {
$0.pointee
}
let port = Int(portu16)
print("Found peer: \(ip1).\(ip2).\(ip3).\(ip4):\(port)")
let peer = TorrentPeerInfo(ip: "\(ip1).\(ip2).\(ip3).\(ip4)", port: port, peerId: nil)
result.append(peer)
}
+3 -23
View File
@@ -8,26 +8,6 @@
import Foundation
protocol TorrentTrackerDelegate: class {
func torrentTracker(_ sender: TorrentHTTPTracker, receivedResponse: TorrentHTTPTrackerResponse)
func torrentTracker(_ sender: TorrentHTTPTracker, receivedErrorMessage: String)
}
enum TorrentHTTPTrackerEvent {
case started, stopped, completed
var name: String {
switch self {
case .started:
return "started"
case .stopped:
return "stopped"
case .completed:
return "completed"
}
}
}
class TorrentHTTPTracker {
let metaInfo: TorrentMetaInfo
@@ -42,7 +22,7 @@ class TorrentHTTPTracker {
func announceClient(with peerId: String,
port: Int,
event: TorrentHTTPTrackerEvent = .started,
event: TorrentTrackerEvent = .started,
infoHash: Data,
numberOfBytesRemaining: Int,
numberOfBytesUploaded: Int,
@@ -68,9 +48,9 @@ class TorrentHTTPTracker {
}
if let data = response.responseData {
if let result = TorrentHTTPTrackerResponse(data: data) {
if let result = TorrentTrackerResponse(bencode: data) {
self!.delegate?.torrentTracker(self!, receivedResponse: result)
} else if let errorMessage = TorrentHTTPTrackerResponse.errorMessage(fromResponseData: data) {
} else if let errorMessage = TorrentTrackerResponse.errorMessage(fromResponseData: data) {
self!.delegate?.torrentTracker(self!, receivedErrorMessage: errorMessage)
}
}
@@ -12,17 +12,17 @@ import XCTest
class TorrentTrackerDelegateSpy: TorrentTrackerDelegate {
var receivedResponseCalled = false
var receivedResponseParameter: TorrentHTTPTrackerResponse? = nil
var receivedResponseParameter: TorrentTrackerResponse? = nil
var receivedErrorMessageCalled = false
var receivedErrorMessageParameter: String? = nil
func torrentTracker(_ sender: TorrentHTTPTracker, receivedResponse response: TorrentHTTPTrackerResponse) {
func torrentTracker(_ sender: Any, receivedResponse response: TorrentTrackerResponse) {
receivedResponseCalled = true
receivedResponseParameter = response
}
func torrentTracker(_ sender: TorrentHTTPTracker, receivedErrorMessage errorMessage: String) {
func torrentTracker(_ sender: Any, receivedErrorMessage errorMessage: String) {
receivedErrorMessageCalled = true
receivedErrorMessageParameter = errorMessage
}
@@ -63,7 +63,7 @@ class TorrentHTTPTrackerTests: XCTestCase {
sut.delegate = delegateSpy
}
func performAnnounce(withEvent event: TorrentHTTPTrackerEvent) {
func performAnnounce(withEvent event: TorrentTrackerEvent) {
sut.announceClient(with: "peerId",
port: 123,
event: event,
@@ -9,7 +9,42 @@
import Foundation
import BEncode
struct TorrentHTTPTrackerResponse {
protocol TorrentTrackerDelegate: class {
func torrentTracker(_ sender: Any, receivedResponse response: TorrentTrackerResponse)
func torrentTracker(_ sender: Any, receivedErrorMessage errorMessage: String)
}
enum TorrentTrackerEvent {
case none, started, stopped, completed
var name: String {
switch self {
case .none:
return "none"
case .started:
return "started"
case .stopped:
return "stopped"
case .completed:
return "completed"
}
}
var udpDataRepresentation: Data {
switch self {
case .none:
return UInt32(0).toData()
case .started:
return UInt32(2).toData()
case .stopped:
return UInt32(3).toData()
case .completed:
return UInt32(1).toData()
}
}
}
struct TorrentTrackerResponse {
let peers: [TorrentPeerInfo]
let numberOfPeersComplete: Int // Seeders
@@ -40,9 +75,9 @@ struct TorrentHTTPTrackerResponse {
}
}
extension TorrentHTTPTrackerResponse {
extension TorrentTrackerResponse {
init?(data: Data) {
init?(bencode data: Data) {
let bencode: [String: Any]
do {
bencode = try BEncoder.decodeStringKeyedDictionary(data)
@@ -79,7 +114,7 @@ extension TorrentHTTPTrackerResponse {
}
}
extension TorrentHTTPTrackerResponse {
extension TorrentTrackerResponse {
static func errorMessage(fromResponseData data: Data) -> String? {
+161
View File
@@ -0,0 +1,161 @@
//
// TorrentUDPTracker.swift
// BitTorrent
//
// Created by Ben Davis on 04/07/2017.
// Copyright © 2017 Ben Davis. All rights reserved.
//
import Foundation
import BEncode
private let PROTOCOL_ID = UInt64(0x41727101980).toData() // magic constant (protocol_id)
private let CONNECTION_ACTION = UInt32(0).toData()
private let ANNOUNCE_ACTION = UInt32(1).toData()
private let ERROR_ACTION = UInt32(3).toData()
class TorrentUDPTracker {
var enableLogging = false
weak var delegate: TorrentTrackerDelegate?
private let announceURL: URL
private var discoveredHostIpAddress: String?
private let udpConnection: UDPConnectionProtocol
private var pendingAnnounce: ((_ transactionId: Data, _ connectionId: Data)->Void)?
private var pendingTransactionId: Data?
init(announceURL: URL, port: UInt16, udpConnection: UDPConnectionProtocol = UDPConnection()) {
self.announceURL = announceURL
self.udpConnection = udpConnection
udpConnection.delegate = self
udpConnection.startListening(on: port)
}
func announceClient(with peerId: String,
port: Int,
event: TorrentTrackerEvent = .started,
infoHash: Data,
numberOfBytesRemaining: Int,
numberOfBytesUploaded: Int,
numberOfBytesDownloaded: Int,
numberOfPeersToFetch: Int) {
guard let host = getHostIpAddress() else { return }
let announcePort = UInt16(announceURL.port ?? 80)
log("Will connect to UDP tracker: \(host):\(announcePort)")
let transactionId = makeTransactionId()
let payload = makeConnectionPayload(with: transactionId)
udpConnection.send(payload, toHost: host, port: announcePort, timeout: 10)
pendingAnnounce = { [weak self] (responseTransactionId, connectionId) in
guard let strongSelf = self else { return }
guard responseTransactionId == transactionId else { return }
self?.log("Will announce to UDP tracker: \(host):\(announcePort)")
var payload = connectionId // 0 64-bit integer connection_id
payload += ANNOUNCE_ACTION // 8 32-bit integer action 1 // announce
payload += strongSelf.makeTransactionId() // 12 32-bit integer transaction_id
payload += infoHash // 16 20-byte string info_hash
payload += peerId.data(using: .ascii)! // 36 20-byte string peer_id
payload += UInt64(numberOfBytesDownloaded).toData() // 56 64-bit integer downloaded
payload += UInt64(numberOfBytesRemaining).toData() // 64 64-bit integer left
payload += UInt64(numberOfBytesUploaded).toData() // 72 64-bit integer uploaded
payload += event.udpDataRepresentation // 80 32-bit integer event
payload += UInt32(0).toData() // 84 32-bit integer IP address 0 // default
payload += UInt32(0).toData() // 88 32-bit integer key 0 // default
payload += UInt32(numberOfPeersToFetch).toData() // 92 32-bit integer num_want -1 // default
payload += UInt16(port).toData() // 96 16-bit integer port
strongSelf.udpConnection.send(payload, toHost: host, port: announcePort, timeout: 10)
}
}
private func getHostIpAddress() -> String? {
guard discoveredHostIpAddress == nil else {
return discoveredHostIpAddress
}
let result = InternetProtocol.getIPAddress(of: announceURL.host!)
discoveredHostIpAddress = result
return result
}
private func makeConnectionPayload(with transactionId: Data) -> Data {
return PROTOCOL_ID + CONNECTION_ACTION + transactionId
}
private func makeTransactionId() -> Data {
let result = arc4random().toData()
pendingTransactionId = result
return result
}
}
extension TorrentUDPTracker: UDPConnectionDelegate {
func udpConnection(_ sender: UDPConnectionProtocol, receivedData data: Data, fromHost host: String) {
let action = Data(data[0..<4])
log("Got response from UDP tracker \(host)")
if action == CONNECTION_ACTION {
log("UDP tracker \(host) accepted connection")
parseConnectionResponse(data)
} else if action == ANNOUNCE_ACTION {
log("UDP tracker \(host) responded to announce")
parseAnnounceResponse(data)
} else if action == ERROR_ACTION {
log("UDP tracker \(host) gave error")
parseErrorResponse(data)
}
}
func parseConnectionResponse(_ response: Data) {
let transactionId = Data(response[4..<8])
let connectionId = Data(response[8..<16])
pendingAnnounce?(transactionId, connectionId)
pendingAnnounce = nil
}
private func parseAnnounceResponse(_ response: Data) {
let transactionId = Data(response[4..<8])
guard pendingTransactionId == transactionId else { return }
let interval = response[8..<12].toUInt32()
let leechers = response[12..<16].toUInt32()
let seeders = response[16..<20].toUInt32()
let peers = TorrentPeerInfo.peersInfoFromBinaryModel(response[20..<response.count])
let response = TorrentTrackerResponse(peers: peers,
numberOfPeersComplete: Int(seeders),
numberOfPeersIncomplete: Int(leechers),
interval: Int(interval))
self.delegate?.torrentTracker(self, receivedResponse: response)
}
private func parseErrorResponse(_ response: Data) {
if let errorMessage = String(data: response[8..<response.count], encoding: .utf8) {
delegate?.torrentTracker(self, receivedErrorMessage: errorMessage)
}
}
}
extension TorrentUDPTracker {
func log(_ items: Any...) {
if enableLogging { print(items) }
}
}
@@ -0,0 +1,370 @@
//
// TorrentUDPTrackerTests.swift
// BitTorrent
//
// Created by Ben Davis on 04/07/2017.
// Copyright © 2017 Ben Davis. All rights reserved.
//
import XCTest
@testable import BitTorrent
import BEncode
class UDPConnectionStub: UDPConnectionProtocol {
weak var delegate: UDPConnectionDelegate?
var startListeningCalled = false
var startListeningParameter: UInt16?
func startListening(on port: UInt16) {
startListeningCalled = true
startListeningParameter = port
}
var sendCallCount = 0
var sendDataParameters: (data: Data, host: String, port: UInt16, timeout: TimeInterval)?
func send(_ data: Data, toHost host: String, port: UInt16, timeout: TimeInterval) {
sendCallCount += 1
sendDataParameters = (data, host, port, timeout)
}
}
class TorrentUDPTrackerTests: XCTestCase {
var sut: TorrentUDPTracker!
var torrentTrackerDelegateSpy: TorrentTrackerDelegateSpy!
var udpConnection: UDPConnectionStub!
let port: UInt16 = 123
override func setUp() {
super.setUp()
let url = URL(string: "udp://localhost:123/announce")!
udpConnection = UDPConnectionStub()
torrentTrackerDelegateSpy = TorrentTrackerDelegateSpy()
sut = TorrentUDPTracker(announceURL: url, port: 123, udpConnection: udpConnection)
sut.delegate = torrentTrackerDelegateSpy
}
func performAnnounce(withEvent event: TorrentTrackerEvent) {
sut.announceClient(with: "peerId12345678901234",
port: 789,
event: event,
infoHash: Data(bytes: [ 1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0 ]),
numberOfBytesRemaining: 456,
numberOfBytesUploaded: 1234,
numberOfBytesDownloaded: 4321,
numberOfPeersToFetch: 321)
}
func test_startsListeningOnPort() {
XCTAssert(udpConnection.startListeningCalled)
XCTAssertEqual(udpConnection.startListeningParameter, port)
}
func test_hostIsResolvedFromURL() {
performAnnounce(withEvent: .started)
XCTAssertEqual(udpConnection.sendCallCount, 1)
if let parameters = udpConnection.sendDataParameters {
XCTAssertEqual(parameters.host, "127.0.0.1")
XCTAssertEqual(parameters.port, 123)
}
}
func test_connectMessageSentToHost() {
performAnnounce(withEvent: .started)
let expectedProtocolId = UInt64(0x41727101980).toData()
let expectedAction = UInt32(0).toData()
XCTAssertEqual(udpConnection.sendCallCount, 1)
if let parameters = udpConnection.sendDataParameters {
XCTAssertEqual(parameters.data.count, 16)
let protocolId = Data(parameters.data[0..<8])
XCTAssertEqual(protocolId, expectedProtocolId)
let action = Data(parameters.data[8..<12])
XCTAssertEqual(action, expectedAction)
XCTAssertEqual(parameters.host, "127.0.0.1")
XCTAssertEqual(parameters.port, 123)
}
}
func test_announceSentOnConnectionAccepted() {
// Given
performAnnounce(withEvent: .started)
// When
let expectedAction = UInt32(1).toData()
let expectedConnectionId = simulateAcceptConnection()
// Then
XCTAssertEqual(udpConnection.sendCallCount, 2)
if let parameters = udpConnection.sendDataParameters {
let connectionId = Data(parameters.data[0..<8])
XCTAssertEqual(connectionId, expectedConnectionId)
let action = Data(parameters.data[8..<12])
XCTAssertEqual(action, expectedAction)
}
}
func test_announcePayload() {
let peerId = "peerId12345678901234"
let exampleEvent = TorrentTrackerEvent.started
let examplePort = 789
let expectedInfoHash = Data(bytes: [ 1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0 ])
let numberOfBytesRemaining = 456
let numberOfBytesUploaded = 1234
let numberOfBytesDownloaded = 4321
let numberOfPeersToFetch = 321
// Given
sut.announceClient(with: peerId,
port: examplePort,
event: exampleEvent,
infoHash: expectedInfoHash,
numberOfBytesRemaining: numberOfBytesRemaining,
numberOfBytesUploaded: numberOfBytesUploaded,
numberOfBytesDownloaded: numberOfBytesDownloaded,
numberOfPeersToFetch: numberOfPeersToFetch)
// When
let expectedConnectionId = simulateAcceptConnection()
let expectedAction = UInt32(1).toData()
let expectedPeerId = peerId.data(using: .ascii)!
let expectedDownloaded = UInt64(numberOfBytesDownloaded).toData()
let expectedLeft = UInt64(numberOfBytesRemaining).toData()
let expectedUploaded = UInt64(numberOfBytesUploaded).toData()
let expectedEvent = exampleEvent.udpDataRepresentation
let expectedIPAddress = UInt32(0).toData() // default value
let expectedNumWant = UInt32(numberOfPeersToFetch).toData()
let expectedPort = UInt16(examplePort).toData()
// Then
XCTAssertEqual(udpConnection.sendCallCount, 2)
if let data = udpConnection.sendDataParameters?.data {
XCTAssertEqual(data.count, 98)
let connectionId = Data(data[0..<8])
let action = Data(data[8..<12])
// let transactionId = Data(data[12..<16])
let infoHash = Data(data[16..<36])
let peerId = Data(data[36..<56])
let downloaded = Data(data[56..<64])
let left = Data(data[64..<72])
let uploaded = Data(data[72..<80])
let event = Data(data[80..<84])
let ipAddress = Data(data[84..<88])
// let key = Data(data[88..<92])
let numWant = Data(data[92..<96])
let port = Data(data[96..<98])
XCTAssertEqual(connectionId, expectedConnectionId)
XCTAssertEqual(action, expectedAction)
XCTAssertEqual(infoHash, expectedInfoHash)
XCTAssertEqual(peerId, expectedPeerId)
XCTAssertEqual(downloaded, expectedDownloaded)
XCTAssertEqual(left, expectedLeft)
XCTAssertEqual(uploaded, expectedUploaded)
XCTAssertEqual(event, expectedEvent)
XCTAssertEqual(ipAddress, expectedIPAddress)
XCTAssertEqual(numWant, expectedNumWant)
XCTAssertEqual(port, expectedPort)
}
}
func test_basicResponseParsing() {
performAnnounce(withEvent: .started)
_ = simulateAcceptConnection()
let interval = 1
let seeders = 2
let leechers = 3
simulateAnnounceResponse(interval: interval,
leechers: leechers,
seeders: seeders,
peers: Data())
XCTAssert(torrentTrackerDelegateSpy.receivedResponseCalled)
guard let response = torrentTrackerDelegateSpy.receivedResponseParameter else { return }
XCTAssertEqual(response.interval, 1)
XCTAssertEqual(response.numberOfPeersComplete, seeders)
XCTAssertEqual(response.numberOfPeersIncomplete, leechers)
XCTAssertEqual(response.peers, [])
}
func test_parsingPeers() {
performAnnounce(withEvent: .started)
_ = simulateAcceptConnection()
let peers = examplePeersResponse(with: [
(127,0,0,1, 15383),
(216,58,198,14, 4321),
])
simulateAnnounceResponse(interval: 1,
leechers: 2,
seeders: 3,
peers: peers)
XCTAssert(torrentTrackerDelegateSpy.receivedResponseCalled)
guard let response = torrentTrackerDelegateSpy.receivedResponseParameter else { return }
XCTAssertEqual(response.peers.count, 2)
XCTAssertEqual(response.peers.first!.ip, "127.0.0.1")
XCTAssertEqual(response.peers.first!.port, 15383)
XCTAssertNil(response.peers.first!.peerId)
XCTAssertEqual(response.peers.last!.ip, "216.58.198.14")
XCTAssertEqual(response.peers.last!.port, 4321)
XCTAssertNil(response.peers.last!.peerId)
}
func examplePeersResponse(with peers: [(UInt8, UInt8, UInt8, UInt8, UInt16)]) -> Data {
var result = Data()
for peer in peers {
let ip = Data(bytes: [peer.0, peer.1, peer.2, peer.3])
result.append(ip)
let port = peer.4.toData()
result.append(port)
}
return result
}
func test_announceMessageForOldTransactionIdIsIgnored() {
// Given
performAnnounce(withEvent: .started)
_ = simulateAcceptConnection()
guard let connectionParameters = udpConnection.sendDataParameters else { return }
let oldTransactionId = connectionParameters.data[4..<8]
// When
performAnnounce(withEvent: .started)
_ = simulateAcceptConnection()
simulateAnnounceResponse(interval: 1, leechers: 2, seeders: 3, peers: Data(), transactionId: oldTransactionId)
// Then
XCTAssertFalse(torrentTrackerDelegateSpy.receivedResponseCalled)
}
func test_connectionMessageForOldTransactionIdIsIgnored() {
// Given
performAnnounce(withEvent: .started)
guard let connectionParameters = udpConnection.sendDataParameters else { return }
let oldTransactionId = connectionParameters.data[4..<8]
// When
performAnnounce(withEvent: .started)
let connectionId = arc4random().toData() + arc4random().toData()
let actionData = UInt32(0).toData() // Action 0 = connection
let connectionResponse = actionData + oldTransactionId + connectionId
udpConnection.delegate?.udpConnection(udpConnection,
receivedData: connectionResponse,
fromHost: "127.0.0.1")
// Then
XCTAssertEqual(udpConnection.sendCallCount, 2)
}
// MARK: -
func simulateAnnounceResponse(interval: Int,
leechers: Int,
seeders: Int,
peers: Data) {
guard let announceParameters = udpConnection.sendDataParameters else { return }
let transactionId = announceParameters.data[12..<16]
simulateAnnounceResponse(interval: interval,
leechers: leechers,
seeders: seeders,
peers: peers,
transactionId: transactionId)
}
func simulateAnnounceResponse(interval: Int,
leechers: Int,
seeders: Int,
peers: Data,
transactionId: Data) {
var announceResponse = UInt32(1).toData()
announceResponse += transactionId
announceResponse += UInt32(interval).toData()
announceResponse += UInt32(leechers).toData()
announceResponse += UInt32(seeders).toData()
announceResponse += peers
udpConnection.delegate?.udpConnection(udpConnection,
receivedData: announceResponse,
fromHost: "127.0.0.1")
}
func simulateAcceptConnection() -> Data {
let connectionId = arc4random().toData() + arc4random().toData()
guard let connectionParameters = udpConnection.sendDataParameters else {
return connectionId
}
let transactionId = connectionParameters.data[12..<16]
let actionData = UInt32(0).toData() // Action 0 = connection
let connectionResponse = actionData + transactionId + connectionId
udpConnection.delegate?.udpConnection(udpConnection,
receivedData: connectionResponse,
fromHost: "127.0.0.1")
return connectionId
}
// MARK: - Error handling
func delegateCalledOnError() {
simulateErrorResponse(withError: "Error Message")
XCTAssert(torrentTrackerDelegateSpy.receivedErrorMessageCalled)
XCTAssertEqual(torrentTrackerDelegateSpy.receivedErrorMessageParameter!, "Error Message")
}
func simulateErrorResponse(withError errorString: String) {
guard let connectionParameters = udpConnection.sendDataParameters else { return }
let transactionId = connectionParameters.data[4..<8]
let connectionResponse = UInt32(3).toData() + // Action 3 = error
transactionId + // Responding to transaction
errorString.data(using: .utf8)! // Error message
udpConnection.delegate?.udpConnection(udpConnection,
receivedData: connectionResponse,
fromHost: "127.0.0.1")
}
}
@@ -8,112 +8,94 @@
import Foundation
let IOS_CELLULAR_INTERFACE_NAME = "pdp_ip0"
let IOS_WIFI_INTERFACE_NAME = "en0"
let IP_ADDR_IPv4_INTERFACE_NAME = "ipv4"
let IP_ADDR_IPv6_INTERFACE_NAME = "ipv6"
func ipAddress(fromSockAddrData data: Data) -> String? {
let socketAddress = data.withUnsafeBytes() { (pointer: UnsafePointer<sockaddr_in>) in
return pointer.pointee
}
guard let resultCString = inet_ntoa(socketAddress.sin_addr) else {
return nil
}
return String(cString: resultCString)
}
func port(fromSockAddrData data: Data) -> UInt16 {
let socketAddress = data.withUnsafeBytes() { (pointer: UnsafePointer<sockaddr_in>) in
return pointer.pointee
}
return socketAddress.sin_port
}
// + (NSString*)ipAddressFromSockAddrData:(NSData*)data {
// struct sockaddr_in * socketAddress = (struct sockaddr_in *)data.bytes;
// struct in_addr ipAsStruct = ((struct sockaddr_in)*(socketAddress)).sin_addr;
// char *buff;
// buff = inet_ntoa(ipAsStruct);
// NSString *ip = [NSString stringWithUTF8String:buff];
// return ip;
// }
//
// + (uint16_t)portFromSockAddrData:(NSData*)data {
// struct sockaddr_in * socketAddressPtr = (struct sockaddr_in *)data.bytes;
// struct sockaddr_in socketAddress = ((struct sockaddr_in)*(socketAddressPtr));
// return socketAddress.sin_port;
//}
func getIPAddress(of hostname: String) -> String? {
struct InternetProtocol {
guard let hostnameCString = hostname.cString(using: .ascii),
let hostEntry = gethostbyname(hostnameCString)?.pointee,
let hostAddressList = hostEntry.h_addr_list?.pointee else {
return nil
}
static let IOS_CELLULAR_INTERFACE_NAME = "pdp_ip0"
static let IOS_WIFI_INTERFACE_NAME = "en0"
static let IP_ADDR_IPv4_INTERFACE_NAME = "ipv4"
static let IP_ADDR_IPv6_INTERFACE_NAME = "ipv6"
let firstHostAddress = hostAddressList.withMemoryRebound(to: in_addr.self, capacity: 1) { $0.pointee }
let firstHostAddressCString = inet_ntoa(firstHostAddress)!
return String(cString: firstHostAddressCString)
}
func getLocalIPAddress(preferIPv4: Bool = true) -> String? {
// Prefer wifi over cellular
let searchArray = preferIPv4 ?
[
IOS_WIFI_INTERFACE_NAME + "/" + IP_ADDR_IPv4_INTERFACE_NAME,
IOS_WIFI_INTERFACE_NAME + "/" + IP_ADDR_IPv6_INTERFACE_NAME,
IOS_CELLULAR_INTERFACE_NAME + "/" + IP_ADDR_IPv4_INTERFACE_NAME,
IOS_CELLULAR_INTERFACE_NAME + "/" + IP_ADDR_IPv6_INTERFACE_NAME,
] :
[
IOS_WIFI_INTERFACE_NAME + "/" + IP_ADDR_IPv6_INTERFACE_NAME,
IOS_WIFI_INTERFACE_NAME + "/" + IP_ADDR_IPv4_INTERFACE_NAME,
IOS_CELLULAR_INTERFACE_NAME + "/" + IP_ADDR_IPv6_INTERFACE_NAME,
IOS_CELLULAR_INTERFACE_NAME + "/" + IP_ADDR_IPv4_INTERFACE_NAME,
]
let addresses = getLocalIPAddresses()
for searchItem in searchArray {
if let result = addresses[searchItem] {
return result
static func ipAddress(fromSockAddrData data: Data) -> String? {
let socketAddress = data.withUnsafeBytes() { (pointer: UnsafePointer<sockaddr_in>) in
return pointer.pointee
}
guard let resultCString = inet_ntoa(socketAddress.sin_addr) else {
return nil
}
return String(cString: resultCString)
}
return nil
}
func getLocalIPAddresses() -> [String: String] {
var addresses: [String: String] = [:]
// Get list of all network interfaces on the local machine:
var ifaddrsPointer : UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&ifaddrsPointer) == 0, let ifaddrsList = ifaddrsPointer?.pointee else {
return [:]
static func port(fromSockAddrData data: Data) -> UInt16 {
let socketAddress = data.withUnsafeBytes() { (pointer: UnsafePointer<sockaddr_in>) in
return pointer.pointee
}
return socketAddress.sin_port
}
// For each network interface ...
for ifaddrs in ifaddrsList {
if !ifaddrs.isUpAndRunning || ifaddrs.isLoopbackNet {
continue
static func getIPAddress(of hostname: String) -> String? {
guard let hostnameCString = hostname.cString(using: .ascii),
let hostEntry = gethostbyname(hostnameCString)?.pointee,
let hostAddressList = hostEntry.h_addr_list?.pointee else {
return nil
}
if ifaddrs.isIpv4 || ifaddrs.isIpv6 {
if let addressString = ifaddrs.convertToIPString(), let name = ifaddrs.nameAndTypeString() {
addresses[name] = addressString
}
}
let firstHostAddress = hostAddressList.withMemoryRebound(to: in_addr.self, capacity: 1) { $0.pointee }
let firstHostAddressCString = inet_ntoa(firstHostAddress)!
return String(cString: firstHostAddressCString)
}
freeifaddrs(ifaddrsPointer)
static func getLocalIPAddress(preferIPv4: Bool = true) -> String? {
// Prefer wifi over cellular
let searchArray = [
ifaddrs.nameAndTypeString(from: IOS_WIFI_INTERFACE_NAME, isIpv4: preferIPv4),
ifaddrs.nameAndTypeString(from: IOS_WIFI_INTERFACE_NAME, isIpv4: !preferIPv4),
ifaddrs.nameAndTypeString(from: IOS_CELLULAR_INTERFACE_NAME, isIpv4: preferIPv4),
ifaddrs.nameAndTypeString(from: IOS_CELLULAR_INTERFACE_NAME, isIpv4: !preferIPv4),
]
let addresses = getLocalIPAddresses()
for searchItem in searchArray {
if let result = addresses[searchItem] {
return result
}
}
return nil
}
return addresses
static func getLocalIPAddresses() -> [String: String] {
var addresses: [String: String] = [:]
// Get list of all network interfaces on the local machine:
var ifaddrsPointer : UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&ifaddrsPointer) == 0, let ifaddrsList = ifaddrsPointer?.pointee else {
return [:]
}
// For each network interface ...
for ifaddrs in ifaddrsList {
if !ifaddrs.isUpAndRunning || ifaddrs.isLoopbackNet {
continue
}
if ifaddrs.isIpv4 || ifaddrs.isIpv6 {
if let addressString = ifaddrs.convertToIPString(), let name = ifaddrs.nameAndTypeString() {
addresses[name] = addressString
}
}
}
freeifaddrs(ifaddrsPointer)
return addresses
}
}
public class ifaddrsIterator: IteratorProtocol {
public typealias Element = ifaddrs
@@ -173,7 +155,13 @@ extension ifaddrs {
return nil
}
return "\(name) - " + (isIpv4 ? IP_ADDR_IPv4_INTERFACE_NAME : IP_ADDR_IPv6_INTERFACE_NAME)
return ifaddrs.nameAndTypeString(from: name, isIpv4: isIpv4)
}
static func nameAndTypeString(from name: String, isIpv4: Bool) -> String {
return name + "/" + (isIpv4 ?
InternetProtocol.IP_ADDR_IPv4_INTERFACE_NAME :
InternetProtocol.IP_ADDR_IPv6_INTERFACE_NAME)
}
}
@@ -12,36 +12,36 @@ import XCTest
class InternetProtocolTests: XCTestCase {
func test_canDecodeLocalhost() {
let result = getIPAddress(of: "localhost")
let result = InternetProtocol.getIPAddress(of: "localhost")
XCTAssertNotNil(result)
XCTAssertEqual(result!, "127.0.0.1")
}
func test_invalidHostnameReturnsNil() {
let result = getIPAddress(of: "asldfjhablskhdbj")
let result = InternetProtocol.getIPAddress(of: "asldfjhablskhdbj")
XCTAssertNil(result)
}
func test_nonAsciiHostnameReturnsNil() {
let result = getIPAddress(of: "🙁")
let result = InternetProtocol.getIPAddress(of: "🙁")
XCTAssertNil(result)
}
func test_canDecodeGoogle() {
let result = getIPAddress(of: "google.com")
let result = InternetProtocol.getIPAddress(of: "google.com")
XCTAssertNotNil(result)
}
func test_canDecodeIPv4AddressFromData() {
let data = Data(bytes: [16,2,122,105,127,0,0,1,0,0,0,0,0,0,0,0])
let result = ipAddress(fromSockAddrData: data)
let result = InternetProtocol.ipAddress(fromSockAddrData: data)
XCTAssertNotNil(result)
XCTAssertEqual(result, "127.0.0.1")
}
func test_canDecodeSocketPortFromData() {
let data = Data(bytes: [16,2,122,105,127,0,0,1,0,0,0,0,0,0,0,0])
let result = port(fromSockAddrData: data)
let result = InternetProtocol.port(fromSockAddrData: data)
XCTAssertNotNil(result)
XCTAssertEqual(result, 27002)
}
+10 -10
View File
@@ -9,32 +9,32 @@
import Foundation
import CocoaAsyncSocket
protocol UDPConnectionProtocol: class {
weak var delegate: UDPConnectionDelegate? { set get }
func startListening(on port: UInt16)
func send(_ data: Data, toHost host: String, port: UInt16, timeout: TimeInterval)
}
protocol UDPConnectionDelegate: class {
func udpConnection(_ sender: UDPConnection, receivedData data: Data, fromHost host: String)
func udpConnection(_ sender: UDPConnectionProtocol, receivedData data: Data, fromHost host: String)
}
/// This class is a thin wrapper around the socket library to protect against changes
/// in its interface, and to allow me to replace CocoaAsyncSocket with a swift framework
/// one day.
class UDPConnection: NSObject {
class UDPConnection: NSObject, UDPConnectionProtocol {
weak var delegate: UDPConnectionDelegate?
private let socket: GCDAsyncUdpSocket
// Designated init for testing
init(socket: GCDAsyncUdpSocket) {
init(socket: GCDAsyncUdpSocket = GCDAsyncUdpSocket()) {
self.socket = socket
super.init()
socket.setDelegate(self)
socket.synchronouslySetDelegateQueue(.main)
}
// Useful init which should be used
override convenience init() {
self.init(socket: GCDAsyncUdpSocket())
}
deinit {
socket.close()
}
@@ -56,7 +56,7 @@ extension UDPConnection: GCDAsyncUdpSocketDelegate {
fromAddress address: Data,
withFilterContext filterContext: Any?) {
let hostString = ipAddress(fromSockAddrData: address)!
let hostString = InternetProtocol.ipAddress(fromSockAddrData: address)!
delegate?.udpConnection(self, receivedData: data, fromHost: hostString)
}
}
@@ -13,8 +13,8 @@ import CocoaAsyncSocket
class UDPConnectionDelegateTestingStub: UDPConnectionDelegate {
var receivedDataCalled = false
var receivedDataParameters: (sender: UDPConnection, data: Data, host: String)?
func udpConnection(_ sender: UDPConnection, receivedData data: Data, fromHost host: String) {
var receivedDataParameters: (sender: UDPConnectionProtocol, data: Data, host: String)?
func udpConnection(_ sender: UDPConnectionProtocol, receivedData data: Data, fromHost host: String) {
receivedDataCalled = true
receivedDataParameters = (sender, data, host)
}
@@ -66,7 +66,7 @@ class UDPConnectionTests: XCTestCase {
sut.udpSocket(socket, didReceive: packetData, fromAddress: addressData, withFilterContext: nil)
XCTAssert(delegate.receivedDataCalled)
XCTAssertEqual(delegate.receivedDataParameters?.sender, sut)
XCTAssert(delegate.receivedDataParameters?.sender as AnyObject === sut)
XCTAssertEqual(delegate.receivedDataParameters?.data, packetData)
XCTAssertEqual(delegate.receivedDataParameters?.host, "127.0.0.1")
}
@@ -0,0 +1,108 @@
//
// BEncodeTestMacros.swift
// BEncode
//
// Created by Ben Davis on 22/08/2016.
// Copyright © 2016 bendavisapps. All rights reserved.
//
import XCTest
enum BEncoderTestError: Error {
case InvalidType
}
public func XCTAssertEqual<T>(_ expression1: @autoclosure () throws -> [[T]],
_ expression2: @autoclosure () throws -> [[T]],
_ message: @autoclosure () -> String = "",
file: StaticString = #file,
line: UInt = #line) {
let array1: [[T]] = try! expression1()
let array2: [[T]] = try! expression2()
XCTAssertEqual(array1.count, array2.count)
for i in 0..<array1.count {
let element1 = array1[i]
let element2 = array2[i]
XCTAssertEqual(element1, element2)
}
}
public func XCTAssertEqual<T>(_ expression1: @autoclosure () throws -> [T],
_ expression2: @autoclosure () throws -> [T],
_ message: @autoclosure () -> String = "",
file: StaticString = #file,
line: UInt = #line) {
let array1: [T] = try! expression1()
let array2: [T] = try! expression2()
XCTAssertEqual(array1.count, array2.count)
for i in 0..<array1.count {
let element1 = array1[i]
let element2 = array2[i]
if let element1 = element1 as? Int, let element2 = element2 as? Int {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? String, let element2 = element2 as? String {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? Data, let element2 = element2 as? Data {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? [T], let element2 = element2 as? [T] {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? [String:T], let element2 = element2 as? [String:T] {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? [Data:T], let element2 = element2 as? [Data:T] {
XCTAssertEqual(element1, element2)
}
}
}
public func XCTAssertEqual<T, E>(_ expression1: @autoclosure () throws -> [E: T],
_ expression2: @autoclosure () throws -> [E: T],
_ message: @autoclosure () -> String = "",
file: StaticString = #file,
line: UInt = #line) {
let dictionary1: [E: T] = try! expression1()
let dictionary2: [E: T] = try! expression2()
XCTAssertEqual(dictionary1.count, dictionary2.count)
let keys1 = [E](dictionary1.keys)
let keys2 = [E](dictionary2.keys)
XCTAssertEqual(keys1, keys2)
for key in keys1 {
let element1 = dictionary1[key]
let element2 = dictionary2[key]
if let element1 = element1 as? Int, let element2 = element2 as? Int {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? String, let element2 = element2 as? String {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? Data, let element2 = element2 as? Data {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? [T], let element2 = element2 as? [T] {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? [String:T], let element2 = element2 as? [String:T] {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? [Data:T], let element2 = element2 as? [Data:T] {
XCTAssertEqual(element1, element2)
}
}
}
fileprivate func XCTAssertEqual(_ element1: Any, element2: Any) throws {
if let element1 = element1 as? Int, let element2 = element2 as? Int {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? String, let element2 = element2 as? String {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? Data, let element2 = element2 as? Data {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? [Any], let element2 = element2 as? [Any] {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? [String:Any], let element2 = element2 as? [String:Any] {
XCTAssertEqual(element1, element2)
} else if let element1 = element1 as? [Data:Any], let element2 = element2 as? [Data:Any] {
XCTAssertEqual(element1, element2)
} else {
throw BEncoderTestError.InvalidType
}
}