From c808fa9ab35fde98d592acf74e74055f5f4a18c5 Mon Sep 17 00:00:00 2001 From: Navan Chauhan Date: Sat, 16 Dec 2023 22:00:38 -0700 Subject: Refactor --- Package.resolved | 9 + Package.swift | 15 +- Sources/GopherHelpers/GopherHelpers.swift | 259 +++++++++++++++++++++ Sources/swift-gopher/fileTypes.swift | 133 ----------- Sources/swift-gopher/gopherHandler.swift | 1 + Sources/swift-gopher/helpers.swift | 7 +- Sources/swiftGopherClient/gopherClient.swift | 50 ++-- .../gopherRequestResponseHandler.swift | 37 +-- Sources/swiftGopherClient/gopherTypes.swift | 122 ---------- 9 files changed, 333 insertions(+), 300 deletions(-) create mode 100644 Sources/GopherHelpers/GopherHelpers.swift delete mode 100644 Sources/swift-gopher/fileTypes.swift delete mode 100644 Sources/swiftGopherClient/gopherTypes.swift diff --git a/Package.resolved b/Package.resolved index dd381cf..99aed36 100644 --- a/Package.resolved +++ b/Package.resolved @@ -62,6 +62,15 @@ "revision" : "702cd7c56d5d44eeba73fdf83918339b26dc855c", "version" : "2.62.0" } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "ebf8b9c365a6ce043bf6e6326a04b15589bd285e", + "version" : "1.20.0" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 0e72b20..f0cb070 100644 --- a/Package.swift +++ b/Package.swift @@ -16,10 +16,16 @@ let package = Package( .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") + ], targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "GopherHelpers", + dependencies: [ + .product(name: "NIOCore", package: "swift-nio") + ] + ), .executableTarget( name: "swift-gopher", dependencies: [ @@ -29,12 +35,15 @@ let package = Package( ), .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "Logging", package: "swift-log"), + "GopherHelpers", ] ), .target( name: "swiftGopherClient", dependencies: [ - .product(name: "NIO", package: "swift-nio") + .product(name: "NIO", package: "swift-nio"), + .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), + "GopherHelpers" ] ), .testTarget( diff --git a/Sources/GopherHelpers/GopherHelpers.swift b/Sources/GopherHelpers/GopherHelpers.swift new file mode 100644 index 0000000..22545b3 --- /dev/null +++ b/Sources/GopherHelpers/GopherHelpers.swift @@ -0,0 +1,259 @@ +// +// File.swift +// +// +// Created by Navan Chauhan on 12/16/23. +// + +import Foundation +import NIOCore + +/* + + From Wikipedia + + Canonical types + 0 Text file + 1 Gopher submenu + 2 CCSO Nameserver + 3 Error code returned by a Gopher server to indicate failure + 4 BinHex-encoded file (primarily for Macintosh computers) + 5 DOS file + 6 uuencoded file + 7 Gopher full-text search + 8 Telnet + 9 Binary file + + Mirror or alternate server (for load balancing or in case of primary server downtime) + g GIF file + I Image file + T Telnet 3270 + gopher+ types + : Bitmap image + ; Movie file + < Sound file + Non-canonical types + d Doc. Seen used alongside PDF's and .DOC's + h HTML file + i Informational message, widely used.[25] + p image file "(especially the png format)" + r document rtf file "rich text Format") + s Sound file (especially the WAV format) + P document pdf file "Portable Document Format") + X document xml file "eXtensive Markup Language" ) + */ + +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 +} + +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 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 + } +} + +public func getFileType(fileExtension: String) -> gopherItemType { + switch fileExtension { + case "txt": + return .text + case "md": + return .text + case "html": + return .html + case "pdf": + return .doc + case "png": + return .image + case "gif": + return .gif + case "jpg": + return .image + case "jpeg": + return .image + case "mp3": + return .sound + case "wav": + return .sound + case "mp4": + return .movie + case "mov": + return .movie + case "avi": + return .movie + case "rtf": + return .doc + case "xml": + return .doc + default: + return .binary + } +} + + +public func fileTypeToGopherItem(fileType: gopherItemType) -> String { + switch fileType { + case .text: + return "0" + case .directory: + return "1" + case .nameserver: + return "2" + case .error: + return "3" + case .binhex: + return "4" + case .bindos: + return "5" + case .uuencoded: + return "6" + case .search: + return "7" + case .telnet: + return "8" + case .binary: + return "9" + case .mirror: + return "+" + case .tn3270Session: + return "T" + case .gif: + return "g" + case .image: + return "I" + case .bitmap: + return "b" + case .movie: + return "M" + case .sound: + return "s" + case .doc: + return "d" + case .html: + 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" + } +} + +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" + } +} diff --git a/Sources/swift-gopher/fileTypes.swift b/Sources/swift-gopher/fileTypes.swift deleted file mode 100644 index 221b7b2..0000000 --- a/Sources/swift-gopher/fileTypes.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// fileTypes.swift -// -// -// Created by Navan Chauhan on 12/3/23. -// - -import Foundation - -enum ResponseType { - case string(String) - case data(Data) -} - -enum gopherFileType { - case text - case directory - case nameserver - case error - case binhex - case bindos - case uuencoded - case indexSearch - case telnet - case binary - case redundantServer - case tn3270Session - case gif - case image - case bitmap - case movie - case sound - case doc - case html - case message - case png - case rtf - case wavfile - case pdf - case xml -} - -func getFileType(fileExtension: String) -> gopherFileType { - switch fileExtension { - case "txt": - return .text - case "md": - return .text - case "html": - return .html - case "pdf": - return .pdf - case "png": - return .png - case "gif": - return .gif - case "jpg": - return .image - case "jpeg": - return .image - case "mp3": - return .sound - case "wav": - return .wavfile - case "mp4": - return .movie - case "mov": - return .movie - case "avi": - return .movie - case "rtf": - return .rtf - case "xml": - return .xml - default: - return .binary - } -} - -func fileTypeToGopherItem(fileType: gopherFileType) -> String { - switch fileType { - case .text: - return "0" - case .directory: - return "1" - case .nameserver: - return "2" - case .error: - return "3" - case .binhex: - return "4" - case .bindos: - return "5" - case .uuencoded: - return "6" - case .indexSearch: - return "7" - case .telnet: - return "8" - case .binary: - return "9" - case .redundantServer: - return "+" - case .tn3270Session: - return "T" - case .gif: - return "g" - case .image: - return "I" - case .bitmap: - return "b" - case .movie: - return "M" - case .sound: - return "s" - case .doc: - return "d" - case .html: - return "h" - case .message: - return "i" - case .png: - return "p" - case .rtf: - return "t" - case .wavfile: - return "w" - case .pdf: - return "P" - case .xml: - return "x" - } -} diff --git a/Sources/swift-gopher/gopherHandler.swift b/Sources/swift-gopher/gopherHandler.swift index 00a3a30..be06b97 100644 --- a/Sources/swift-gopher/gopherHandler.swift +++ b/Sources/swift-gopher/gopherHandler.swift @@ -2,6 +2,7 @@ import ArgumentParser import Foundation import Logging import NIO +import GopherHelpers final class GopherHandler: ChannelInboundHandler { typealias InboundIn = ByteBuffer diff --git a/Sources/swift-gopher/helpers.swift b/Sources/swift-gopher/helpers.swift index caf841b..f054d17 100644 --- a/Sources/swift-gopher/helpers.swift +++ b/Sources/swift-gopher/helpers.swift @@ -6,4 +6,9 @@ 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)" -} \ No newline at end of file +} + +enum ResponseType { + case string(String) + case data(Data) +} diff --git a/Sources/swiftGopherClient/gopherClient.swift b/Sources/swiftGopherClient/gopherClient.swift index e508285..eabbbc4 100644 --- a/Sources/swiftGopherClient/gopherClient.swift +++ b/Sources/swiftGopherClient/gopherClient.swift @@ -7,12 +7,18 @@ import Foundation import NIO +import NIOTransportServices +import GopherHelpers public class GopherClient { private let group: EventLoopGroup public init() { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + 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 { @@ -20,22 +26,40 @@ public class GopherClient { } public func sendRequest(to host: String, port: Int = 70, message: String, completion: @escaping (Result<[gopherItem], Error>) -> Void) { - let bootstrap = ClientBootstrap(group: group) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .channelInitializer { channel in - channel.pipeline.addHandler(GopherRequestResponseHandler(message: message, completion: completion)) + 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)) + } } - - bootstrap.connect(host: host, port: port).whenComplete { result in - switch result { - case .success(let channel): - channel.closeFuture.whenComplete { _ in - print("Connection closed") + } 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)) } - case .failure(let error): - completion(.failure(error)) } } + } private func shutdownEventLoopGroup() { diff --git a/Sources/swiftGopherClient/gopherRequestResponseHandler.swift b/Sources/swiftGopherClient/gopherRequestResponseHandler.swift index 3d15d66..d69c8c8 100644 --- a/Sources/swiftGopherClient/gopherRequestResponseHandler.swift +++ b/Sources/swiftGopherClient/gopherRequestResponseHandler.swift @@ -7,6 +7,7 @@ import Foundation import NIO +import GopherHelpers final class GopherRequestResponseHandler: ChannelInboundHandler { typealias InboundIn = ByteBuffer @@ -31,17 +32,12 @@ final class GopherRequestResponseHandler: ChannelInboundHandler { func channelRead(context: ChannelHandlerContext, data: NIOAny) { var buffer = unwrapInboundIn(data) accumulatedData.writeBuffer(&buffer) - if let receivedString = buffer.getString(at: 0, length: buffer.readableBytes) { - print("Received from server: \(receivedString)") - } - //completion(.success(receivedString)) - //context.close(promise: nil) } func channelInactive(context: ChannelHandlerContext) { - // Parse GopherServerResponse - parseGopherServerResponse(response: accumulatedData.readString(length: accumulatedData.readableBytes) ?? "") - //completion(.success(accumulatedData.readString(length: accumulatedData.readableBytes) ?? "")) + 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) { @@ -49,9 +45,10 @@ final class GopherRequestResponseHandler: ChannelInboundHandler { context.close(promise: nil) } - func createGopherItem(rawLine: String, itemType: gopherItemType = .info) -> gopherItem { + 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 @@ -77,7 +74,7 @@ final class GopherRequestResponseHandler: ChannelInboundHandler { return item } - func parseGopherServerResponse(response: String) { + func parseGopherServerResponse(response: String, originalBytes: ByteBuffer) { var gopherServerResponse: [gopherItem] = [] print("parsing") @@ -87,8 +84,7 @@ final class GopherRequestResponseHandler: ChannelInboundHandler { for line in response.split(separator: "\r\n") { let lineItemType = getGopherFileType(item: "\(line.first ?? " ")") - let item = createGopherItem(rawLine: String(line), itemType: lineItemType) - print(item.message) + let item = createGopherItem(rawLine: String(line), itemType: lineItemType, rawData: originalBytes) gopherServerResponse.append(item) } @@ -96,23 +92,8 @@ final class GopherRequestResponseHandler: ChannelInboundHandler { print("done parsing") completion(.success(gopherServerResponse)) - - //completion(.success(response)) } } -public struct gopherItem { - - public var rawLine: String - 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 - } -} + diff --git a/Sources/swiftGopherClient/gopherTypes.swift b/Sources/swiftGopherClient/gopherTypes.swift deleted file mode 100644 index efec92e..0000000 --- a/Sources/swiftGopherClient/gopherTypes.swift +++ /dev/null @@ -1,122 +0,0 @@ -// -// gopherTypes.swift -// -// -// Created by Navan Chauhan on 12/12/23. -// - -import Foundation - -/* - - From Wikipedia - - Canonical types - 0 Text file - 1 Gopher submenu - 2 CCSO Nameserver - 3 Error code returned by a Gopher server to indicate failure - 4 BinHex-encoded file (primarily for Macintosh computers) - 5 DOS file - 6 uuencoded file - 7 Gopher full-text search - 8 Telnet - 9 Binary file - + Mirror or alternate server (for load balancing or in case of primary server downtime) - g GIF file - I Image file - T Telnet 3270 - gopher+ types - : Bitmap image - ; Movie file - < Sound file - Non-canonical types - d Doc. Seen used alongside PDF's and .DOC's - h HTML file - i Informational message, widely used.[25] - p image file "(especially the png format)" - r document rtf file "rich text Format") - s Sound file (especially the WAV format) - P document pdf file "Portable Document Format") - X document xml file "eXtensive Markup Language" ) - */ - -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 -} - -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 - } -} -- cgit v1.2.3