aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Package.swift35
-rw-r--r--Sources/GopherHelpers/GopherHelpers.swift249
-rw-r--r--Sources/swift-gopher/gopherHandler.swift75
-rw-r--r--Sources/swift-gopher/helpers.swift10
-rw-r--r--Sources/swift-gopher/server.swift2
-rw-r--r--Sources/swiftGopherClient/gopherClient.swift129
-rw-r--r--Sources/swiftGopherClient/gopherRequestResponseHandler.swift162
-rw-r--r--Tests/swiftGopherClientTests/swiftGopherClientTests.swift47
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)
+ }
}