diff options
-rw-r--r-- | Package.swift | 35 | ||||
-rw-r--r-- | Sources/GopherHelpers/GopherHelpers.swift | 249 | ||||
-rw-r--r-- | Sources/swift-gopher/gopherHandler.swift | 75 | ||||
-rw-r--r-- | Sources/swift-gopher/helpers.swift | 10 | ||||
-rw-r--r-- | Sources/swift-gopher/server.swift | 2 | ||||
-rw-r--r-- | Sources/swiftGopherClient/gopherClient.swift | 129 | ||||
-rw-r--r-- | Sources/swiftGopherClient/gopherRequestResponseHandler.swift | 162 | ||||
-rw-r--r-- | Tests/swiftGopherClientTests/swiftGopherClientTests.swift | 47 |
8 files changed, 368 insertions, 341 deletions
diff --git a/Package.swift b/Package.swift index f0cb070..b073635 100644 --- a/Package.swift +++ b/Package.swift @@ -9,22 +9,19 @@ let package = Package( .library(name: "SwiftGopherClient", targets: ["swiftGopherClient"]) ], dependencies: [ - .package( - url: "https://github.com/apple/swift-nio", - from: "2.0.0" - ), + .package(url: "https://github.com/apple/swift-nio", from: "2.0.0"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.20.0") - + .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.20.0"), + ], targets: [ .target( - name: "GopherHelpers", - dependencies: [ - .product(name: "NIOCore", package: "swift-nio") - ] + name: "GopherHelpers", + dependencies: [ + .product(name: "NIOCore", package: "swift-nio") + ] ), .executableTarget( name: "swift-gopher", @@ -39,16 +36,16 @@ let package = Package( ] ), .target( - name: "swiftGopherClient", - dependencies: [ - .product(name: "NIO", package: "swift-nio"), - .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), - "GopherHelpers" - ] + name: "swiftGopherClient", + dependencies: [ + .product(name: "NIO", package: "swift-nio"), + .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), + "GopherHelpers", + ] ), .testTarget( - name: "swiftGopherClientTests", - dependencies: ["swiftGopherClient"] - ) + name: "swiftGopherClientTests", + dependencies: ["swiftGopherClient"] + ), ] ) diff --git a/Sources/GopherHelpers/GopherHelpers.swift b/Sources/GopherHelpers/GopherHelpers.swift index 22545b3..15f14cb 100644 --- a/Sources/GopherHelpers/GopherHelpers.swift +++ b/Sources/GopherHelpers/GopherHelpers.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Navan Chauhan on 12/16/23. // @@ -9,9 +9,9 @@ import Foundation import NIOCore /* - + From Wikipedia - + Canonical types 0 Text file 1 Gopher submenu @@ -43,99 +43,99 @@ import NIOCore */ public enum gopherItemType { - case text - case directory - case nameserver - case error - case binhex - case bindos - case uuencoded - case search - case telnet - case binary - case mirror - case gif - case image - case tn3270Session - case bitmap - case movie - case sound - case doc - case html - case info + case text + case directory + case nameserver + case error + case binhex + case bindos + case uuencoded + case search + case telnet + case binary + case mirror + case gif + case image + case tn3270Session + case bitmap + case movie + case sound + case doc + case html + case info } public struct gopherItem { - - public var rawLine: String - public var rawData: ByteBuffer? - public var message: String = "" - public var parsedItemType: gopherItemType = .info - public var host: String = "error.host" - public var port: Int = 1 - public var selector: String = "" - public var valid: Bool = true - - public init(rawLine: String) { - self.rawLine = rawLine - } + + public var rawLine: String + public var rawData: ByteBuffer? + public var message: String = "" + public var parsedItemType: gopherItemType = .info + public var host: String = "error.host" + public var port: Int = 1 + public var selector: String = "" + public var valid: Bool = true + + public init(rawLine: String) { + self.rawLine = rawLine + } } public func getGopherFileType(item: String) -> gopherItemType { - switch item { - case "0": - return .text - case "1": - return .directory - case "2": - return .nameserver - case "3": - return .error - case "4": - return .binhex - case "5": - return .bindos - case "6": - return .uuencoded - case "7": - return .search - case "8": - return .telnet - case "9": - return .binary - case "+": - return .mirror - case "g": - return .gif - case "I": - return .image - case "T": - return .tn3270Session - case ":": - return .bitmap - case ";": - return .movie - case "<": - return .sound - case "d": - return .doc - case "h": - return .html - case "i": - return .info - case "p": - return .image - case "r": - return .doc - case "s": - return .doc - case "P": - return .doc - case "X": - return .doc - default: - return .info - } + switch item { + case "0": + return .text + case "1": + return .directory + case "2": + return .nameserver + case "3": + return .error + case "4": + return .binhex + case "5": + return .bindos + case "6": + return .uuencoded + case "7": + return .search + case "8": + return .telnet + case "9": + return .binary + case "+": + return .mirror + case "g": + return .gif + case "I": + return .image + case "T": + return .tn3270Session + case ":": + return .bitmap + case ";": + return .movie + case "<": + return .sound + case "d": + return .doc + case "h": + return .html + case "i": + return .info + case "p": + return .image + case "r": + return .doc + case "s": + return .doc + case "P": + return .doc + case "X": + return .doc + default: + return .info + } } public func getFileType(fileExtension: String) -> gopherItemType { @@ -175,7 +175,6 @@ public func getFileType(fileExtension: String) -> gopherItemType { } } - public func fileTypeToGopherItem(fileType: gopherItemType) -> String { switch fileType { case .text: @@ -218,42 +217,42 @@ public func fileTypeToGopherItem(fileType: gopherItemType) -> String { return "h" case .info: return "i" -// case .png: -// return "p" -// case .rtf: -// return "t" -// case .wavfile: -// return "w" -// case .pdf: -// return "P" -// case .xml: -// return "x" + // case .png: + // return "p" + // case .rtf: + // return "t" + // case .wavfile: + // return "w" + // case .pdf: + // return "P" + // case .xml: + // return "x" } } public func itemToImageType(_ item: gopherItem) -> String { - switch item.parsedItemType { - case .text: - return "doc.plaintext" - case .directory: - return "folder" - case .error: - return "exclamationmark.triangle" - case .gif: - return "photo.stack" - case .image: - return "photo" - case .doc: - return "doc.richtext" - case .sound: - return "music.note" - case .bitmap: - return "photo" - case .html: - return "globe" - case .movie: - return "videoprojector" - default: - return "questionmark.square.dashed" - } + switch item.parsedItemType { + case .text: + return "doc.plaintext" + case .directory: + return "folder" + case .error: + return "exclamationmark.triangle" + case .gif: + return "photo.stack" + case .image: + return "photo" + case .doc: + return "doc.richtext" + case .sound: + return "music.note" + case .bitmap: + return "photo" + case .html: + return "globe" + case .movie: + return "videoprojector" + default: + return "questionmark.square.dashed" + } } diff --git a/Sources/swift-gopher/gopherHandler.swift b/Sources/swift-gopher/gopherHandler.swift index be06b97..cd150e3 100644 --- a/Sources/swift-gopher/gopherHandler.swift +++ b/Sources/swift-gopher/gopherHandler.swift @@ -1,8 +1,8 @@ import ArgumentParser import Foundation +import GopherHelpers import Logging import NIO -import GopherHelpers final class GopherHandler: ChannelInboundHandler { typealias InboundIn = ByteBuffer @@ -37,9 +37,11 @@ final class GopherHandler: ChannelInboundHandler { } if let remoteAddress = context.remoteAddress { - logger.info("Received request from \(remoteAddress) for '\(requestString.replacingOccurrences(of: "\r\n", with: "<GopherSequence>").replacingOccurrences(of: "\n", with: "<Linebreak>"))'") + logger.info( + "Received request from \(remoteAddress) for '\(requestString.replacingOccurrences(of: "\r\n", with: "<GopherSequence>").replacingOccurrences(of: "\n", with: "<Linebreak>"))'" + ) } else { - logger.warning("Unable to retrieve remote address") + logger.warning("Unable to retrieve remote address") } let response = processGopherRequest(requestString) @@ -110,7 +112,7 @@ final class GopherHandler: ChannelInboundHandler { // Now check if there is still a prefix if sanitizedPath.hasPrefix("/") { - sanitizedPath = String(sanitizedPath.dropFirst()) + sanitizedPath = String(sanitizedPath.dropFirst()) } let full_path = base_dir.appendingPathComponent(sanitizedPath) @@ -135,26 +137,26 @@ final class GopherHandler: ChannelInboundHandler { var basePath = URL(fileURLWithPath: gopherdata_dir).path if basePath.hasSuffix("/") { - basePath = String(basePath.dropLast()) + basePath = String(basePath.dropLast()) } let fm = FileManager.default do { - print("Reading directory: \(path.path)") - let itemsInDirectory = try fm.contentsOfDirectory(at: path, includingPropertiesForKeys: nil) - for item in itemsInDirectory { - let isDirectory = try item.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false - let name = item.lastPathComponent - if isDirectory { - items.append(generateGopherItem(item_name: "1\(name)", item_path: item)) - } else { - let fileType = getFileType(fileExtension: item.pathExtension) - let gopherFileType = fileTypeToGopherItem(fileType: fileType) - items.append(generateGopherItem(item_name: "\(gopherFileType)\(name)", item_path: item)) - } + print("Reading directory: \(path.path)") + let itemsInDirectory = try fm.contentsOfDirectory(at: path, includingPropertiesForKeys: nil) + for item in itemsInDirectory { + let isDirectory = try item.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false + let name = item.lastPathComponent + if isDirectory { + items.append(generateGopherItem(item_name: "1\(name)", item_path: item)) + } else { + let fileType = getFileType(fileExtension: item.pathExtension) + let gopherFileType = fileTypeToGopherItem(fileType: fileType) + items.append(generateGopherItem(item_name: "\(gopherFileType)\(name)", item_path: item)) } + } } catch { - print("Error reading directory: \(path.path)") + print("Error reading directory: \(path.path)") } return items } @@ -171,14 +173,14 @@ final class GopherHandler: ChannelInboundHandler { let gophermap_lines = gophermap_contents.components(separatedBy: "\n") for originalLine in gophermap_lines { // Only keep first 80 characters - var line = String(originalLine)//.prefix(80) - if "0123456789+gIT:;<dhprsPXi".contains(line.prefix(1)) && line.count > 1 { + var line = String(originalLine) //.prefix(80) + if "0123456789+gIT:;<dhprsPXi".contains(line.prefix(1)) && line.count > 1 { if line.hasSuffix("\n") { line = String(line.dropLast()) } if line.prefix(1) == "i" { gopherResponse.append("\(line)\t\terror.host\t1\r\n") - continue + continue } let regex = try! NSRegularExpression(pattern: "\\t+| {2,}") @@ -201,7 +203,7 @@ final class GopherHandler: ChannelInboundHandler { } if components.count < 3 { - continue + continue } let item_name = components[0] @@ -227,18 +229,17 @@ final class GopherHandler: ChannelInboundHandler { // Append Search if enableSearch { - let search_line = "7Search Server\t/search\t\(gopherdata_host)\t\(gopherdata_port)\r\n" - gopherResponse.append(search_line) + let search_line = "7Search Server\t/search\t\(gopherdata_host)\t\(gopherdata_port)\r\n" + gopherResponse.append(search_line) } // Append Server Info gopherResponse.append(buildVersionStringResponse()) - return gopherResponse.joined(separator: "") } - // TODO: Refactor + // TODO: Refactor func performSearch(query: String) -> String { // Really basic search implementation @@ -326,28 +327,28 @@ final class GopherHandler: ChannelInboundHandler { // Fix for "Gopher" (iOS) client sending an extra \n if request.hasSuffix("\n\n") { - request = String(request.dropLast()) + request = String(request.dropLast()) } - if request == "\r\n" { // Empty request + if request == "\r\n" { // Empty request return .string(prepareGopherMenu(path: preparePath())) } // Again, fix for the iOS client. Might as well make my own client if request.hasSuffix("\n") { - request = String(request.dropLast()) + request = String(request.dropLast()) } if request.contains("\t") { - if enableSearch { - var searchQuery = request.components(separatedBy: "\t")[1] - searchQuery = searchQuery.replacingOccurrences(of: "\r\n", with: "") - return .string(performSearch(query: searchQuery.lowercased())) - } else { - return .string("3Search is disabled on this server.\r\n") - } + if enableSearch { + var searchQuery = request.components(separatedBy: "\t")[1] + searchQuery = searchQuery.replacingOccurrences(of: "\r\n", with: "") + return .string(performSearch(query: searchQuery.lowercased())) + } else { + return .string("3Search is disabled on this server.\r\n") + } } - + //TODO: Potential Bug in Gopher implementation? curl gopher://localhost:8080/new_folder/ does not work but curl gopher://localhost:8080//new_folder/ works (tested with gopher://gopher.meulie.net//EFFector/ as well) return requestHandler(path: preparePath(path: request)) } diff --git a/Sources/swift-gopher/helpers.swift b/Sources/swift-gopher/helpers.swift index f054d17..3c70b51 100644 --- a/Sources/swift-gopher/helpers.swift +++ b/Sources/swift-gopher/helpers.swift @@ -1,11 +1,13 @@ import Foundation -let versionString = "generated and served by swift-gopher/1.0.0" // TODO: Handle automatic versioning +let versionString = "generated and served by swift-gopher/1.0.0" // TODO: Handle automatic versioning func buildVersionStringResponse() -> String { - let repeatedString = "i" + String(repeating: "-", count: 80) + "\t\terror.host\t1\r\n" - let versionResponseString = "i" + String(repeating: " ", count: 80 - versionString.count) + versionString + "\t\terror.host\t1\r\n" - return "\(repeatedString)\(versionResponseString)" + let repeatedString = "i" + String(repeating: "-", count: 80) + "\t\terror.host\t1\r\n" + let versionResponseString = + "i" + String(repeating: " ", count: 80 - versionString.count) + versionString + + "\t\terror.host\t1\r\n" + return "\(repeatedString)\(versionResponseString)" } enum ResponseType { diff --git a/Sources/swift-gopher/server.swift b/Sources/swift-gopher/server.swift index c8cf97b..2f4a863 100644 --- a/Sources/swift-gopher/server.swift +++ b/Sources/swift-gopher/server.swift @@ -16,7 +16,7 @@ struct swiftGopher: ParsableCommand { var port: Int = 8080 @Option(name: [.customShort("d"), .long], help: "Data directory to map") var gopherDataDir: String = "./example-gopherdata" - @Flag(help: "Disable full-text search feature") + @Flag(help: "Disable full-text search feature") var disableSearch: Bool = false @Flag(help: "Disable reading gophermap files to override automatic generation") var disableGophermap: Bool = false diff --git a/Sources/swiftGopherClient/gopherClient.swift b/Sources/swiftGopherClient/gopherClient.swift index eabbbc4..c7542ee 100644 --- a/Sources/swiftGopherClient/gopherClient.swift +++ b/Sources/swiftGopherClient/gopherClient.swift @@ -6,67 +6,90 @@ // import Foundation +import GopherHelpers import NIO import NIOTransportServices -import GopherHelpers +/// `GopherClient` is a class for handling network connections and requests to Gopher servers. +/// +/// It utilizes `NIOTSEventLoopGroup` on iOS/macOS (Not sure why you would run this on watchOS/tvOS but it supports that as well) for network operations, falling back to `MultiThreadedEventLoopGroup` otherwise. public class GopherClient { - private let group: EventLoopGroup + private let group: EventLoopGroup - public init() { - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { - self.group = NIOTSEventLoopGroup() - } else { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - } + /// Initializes a new instance of `GopherClient`. + /// + /// It automatically chooses the appropriate `EventLoopGroup` based on the running platform. + public init() { + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + self.group = NIOTSEventLoopGroup() + } else { + self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) } + } - deinit { - try? group.syncShutdownGracefully() - } + deinit { + try? group.syncShutdownGracefully() + } - public func sendRequest(to host: String, port: Int = 70, message: String, completion: @escaping (Result<[gopherItem], Error>) -> Void) { - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { - let bootstrap = NIOTSConnectionBootstrap(group: group) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .channelInitializer { channel in - channel.pipeline.addHandler(GopherRequestResponseHandler(message: message, completion: completion)) - } - bootstrap.connect(host: host, port: port).whenComplete { result in - switch result { - case .success(let channel): - channel.closeFuture.whenComplete { _ in - print("Connection closed") - } - case .failure(let error): - completion(.failure(error)) - } - } - } else { - let bootstrap = ClientBootstrap(group: group) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .channelInitializer { channel in - channel.pipeline.addHandler(GopherRequestResponseHandler(message: message, completion: completion)) - } - bootstrap.connect(host: host, port: port).whenComplete { result in - switch result { - case .success(let channel): - channel.closeFuture.whenComplete { _ in - print("Connection closed") - } - case .failure(let error): - completion(.failure(error)) - } - } + /// Sends a request to a Gopher server. + /// + /// - Parameters: + /// - host: The host address of the Gopher server. + /// - port: The port of the Gopher server. Defaults to 70. + /// - message: The message to be sent to the server. + /// - completion: A closure that handles the result of the request. + /// + /// The method asynchronously establishes a connection, sends the request, and calls the completion handler with the result. + public func sendRequest( + to host: String, port: Int = 70, message: String, + completion: @escaping (Result<[gopherItem], Error>) -> Void + ) { + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + let bootstrap = NIOTSConnectionBootstrap(group: group) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelInitializer { channel in + channel.pipeline.addHandler( + GopherRequestResponseHandler(message: message, completion: completion)) } - - } - - private func shutdownEventLoopGroup() { - do { - try group.syncShutdownGracefully() - } catch { - print("Error shutting down event loop group: \(error)") - } + bootstrap.connect(host: host, port: port).whenComplete { result in + switch result { + case .success(let channel): + channel.closeFuture.whenComplete { _ in + print("Connection closed") + } + case .failure(let error): + completion(.failure(error)) + } + } + } else { + let bootstrap = ClientBootstrap(group: group) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelInitializer { channel in + channel.pipeline.addHandler( + GopherRequestResponseHandler(message: message, completion: completion)) } + bootstrap.connect(host: host, port: port).whenComplete { result in + switch result { + case .success(let channel): + channel.closeFuture.whenComplete { _ in + print("Connection closed") + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + } + + /// Shuts down the event loop group, releasing any resources. + /// + /// This method is called during deinitialization to ensure clean shutdown of network resources. + private func shutdownEventLoopGroup() { + do { + try group.syncShutdownGracefully() + } catch { + print("Error shutting down event loop group: \(error)") + } + } } diff --git a/Sources/swiftGopherClient/gopherRequestResponseHandler.swift b/Sources/swiftGopherClient/gopherRequestResponseHandler.swift index d69c8c8..6cede51 100644 --- a/Sources/swiftGopherClient/gopherRequestResponseHandler.swift +++ b/Sources/swiftGopherClient/gopherRequestResponseHandler.swift @@ -6,94 +6,98 @@ // import Foundation -import NIO import GopherHelpers +import NIO final class GopherRequestResponseHandler: ChannelInboundHandler { - typealias InboundIn = ByteBuffer - typealias OutboundOut = ByteBuffer + typealias InboundIn = ByteBuffer + typealias OutboundOut = ByteBuffer - private var accumulatedData: ByteBuffer - private let message: String - private let completion: (Result<[gopherItem], Error>) -> Void + private var accumulatedData: ByteBuffer + private let message: String + private let completion: (Result<[gopherItem], Error>) -> Void - init(message: String, completion: @escaping (Result<[gopherItem], Error>) -> Void) { - self.message = message - self.completion = completion - self.accumulatedData = ByteBuffer() - } + init(message: String, completion: @escaping (Result<[gopherItem], Error>) -> Void) { + self.message = message + self.completion = completion + self.accumulatedData = ByteBuffer() + } - func channelActive(context: ChannelHandlerContext) { - var buffer = context.channel.allocator.buffer(capacity: message.utf8.count) - buffer.writeString(message) - context.writeAndFlush(self.wrapOutboundOut(buffer), promise: nil) - } + func channelActive(context: ChannelHandlerContext) { + var buffer = context.channel.allocator.buffer(capacity: message.utf8.count) + buffer.writeString(message) + context.writeAndFlush(self.wrapOutboundOut(buffer), promise: nil) + } - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - var buffer = unwrapInboundIn(data) - accumulatedData.writeBuffer(&buffer) - } - - func channelInactive(context: ChannelHandlerContext) { - if let dataCopy = accumulatedData.getSlice(at: 0, length: accumulatedData.readableBytes) { - parseGopherServerResponse(response: accumulatedData.readString(length: accumulatedData.readableBytes) ?? "", originalBytes: dataCopy) - } - } - - func errorCaught(context: ChannelHandlerContext, error: Error) { - print("Error: ", error) - context.close(promise: nil) - } - - func createGopherItem(rawLine: String, itemType: gopherItemType = .info, rawData: ByteBuffer) -> gopherItem { - var item = gopherItem(rawLine: rawLine) - item.parsedItemType = itemType - item.rawData = rawData - - if rawLine.isEmpty { - item.valid = false - } else { - let components = rawLine.components(separatedBy: "\t") - - // Handle cases where rawLine does not have any itemType in the first character - item.message = String(components[0].dropFirst()) - - if components.indices.contains(1) { - item.selector = String(components[1]) - } - - if components.indices.contains(2) { - item.host = String(components[2]) - } - - if components.indices.contains(3) { - item.port = Int(String(components[3])) ?? 70 - } - } - - return item + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + var buffer = unwrapInboundIn(data) + accumulatedData.writeBuffer(&buffer) + } + + func channelInactive(context: ChannelHandlerContext) { + if let dataCopy = accumulatedData.getSlice(at: 0, length: accumulatedData.readableBytes) { + parseGopherServerResponse( + response: accumulatedData.readString(length: accumulatedData.readableBytes) ?? "", + originalBytes: dataCopy) } - - func parseGopherServerResponse(response: String, originalBytes: ByteBuffer) { - var gopherServerResponse: [gopherItem] = [] - - print("parsing") - let carriageReturnCount = response.filter({ $0 == "\r" }).count - let newlineCarriageReturnCount = response.filter({ $0 == "\r\n" }).count - print("Carriage Returns: \(carriageReturnCount), Newline + Carriage Returns: \(newlineCarriageReturnCount)") - - for line in response.split(separator: "\r\n") { - let lineItemType = getGopherFileType(item: "\(line.first ?? " ")") - let item = createGopherItem(rawLine: String(line), itemType: lineItemType, rawData: originalBytes) - gopherServerResponse.append(item) - - } - - print("done parsing") - - completion(.success(gopherServerResponse)) + } + + func errorCaught(context: ChannelHandlerContext, error: Error) { + print("Error: ", error) + context.close(promise: nil) + } + + func createGopherItem(rawLine: String, itemType: gopherItemType = .info, rawData: ByteBuffer) + -> gopherItem + { + var item = gopherItem(rawLine: rawLine) + item.parsedItemType = itemType + item.rawData = rawData + + if rawLine.isEmpty { + item.valid = false + } else { + let components = rawLine.components(separatedBy: "\t") + + // Handle cases where rawLine does not have any itemType in the first character + item.message = String(components[0].dropFirst()) + + if components.indices.contains(1) { + item.selector = String(components[1]) + } + + if components.indices.contains(2) { + item.host = String(components[2]) + } + + if components.indices.contains(3) { + item.port = Int(String(components[3])) ?? 70 + } } -} + return item + } + + func parseGopherServerResponse(response: String, originalBytes: ByteBuffer) { + var gopherServerResponse: [gopherItem] = [] + print("parsing") + let carriageReturnCount = response.filter({ $0 == "\r" }).count + let newlineCarriageReturnCount = response.filter({ $0 == "\r\n" }).count + print( + "Carriage Returns: \(carriageReturnCount), Newline + Carriage Returns: \(newlineCarriageReturnCount)" + ) + for line in response.split(separator: "\r\n") { + let lineItemType = getGopherFileType(item: "\(line.first ?? " ")") + let item = createGopherItem( + rawLine: String(line), itemType: lineItemType, rawData: originalBytes) + gopherServerResponse.append(item) + + } + + print("done parsing") + + completion(.success(gopherServerResponse)) + } +} diff --git a/Tests/swiftGopherClientTests/swiftGopherClientTests.swift b/Tests/swiftGopherClientTests/swiftGopherClientTests.swift index a687a3e..dfa46b8 100644 --- a/Tests/swiftGopherClientTests/swiftGopherClientTests.swift +++ b/Tests/swiftGopherClientTests/swiftGopherClientTests.swift @@ -5,33 +5,34 @@ // Created by Navan Chauhan on 12/12/23. // -import XCTest import NIO +import XCTest @testable import swiftGopherClient final class GopherClientTests: XCTestCase { - - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - - func testGopherServerConnection() { - let expectation = XCTestExpectation(description: "Connect and receive response from Gopher server") - let client = GopherClient() - client.sendRequest(to: "gopher.floodgap.com", message: "\r\n") { result in - switch result { - case .success(_): - expectation.fulfill() - case .failure(let error): - print("Error \(error)") - } - } - - wait(for: [expectation], timeout: 30) + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testGopherServerConnection() { + let expectation = XCTestExpectation( + description: "Connect and receive response from Gopher server") + let client = GopherClient() + client.sendRequest(to: "gopher.floodgap.com", message: "\r\n") { result in + switch result { + case .success(_): + expectation.fulfill() + case .failure(let error): + print("Error \(error)") + } } + + wait(for: [expectation], timeout: 30) + } } |