aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hotline.xcodeproj/project.pbxproj12
-rw-r--r--Hotline.xcodeproj/xcshareddata/xcschemes/Hotline.xcscheme92
-rw-r--r--Hotline/Hotline/HotlineClient.swift17
-rw-r--r--Hotline/Hotline/HotlineExtensions.swift782
-rw-r--r--Hotline/Hotline/HotlineFileClient.swift665
-rw-r--r--Hotline/Hotline/HotlineProtocol.swift14
-rw-r--r--Hotline/Hotline/HotlineTransferClient.swift1244
-rw-r--r--Hotline/Models/FilePreview.swift16
-rw-r--r--Hotline/Models/Hotline.swift206
-rw-r--r--Hotline/Models/TransferInfo.swift1
-rw-r--r--Hotline/Utility/FoundationExtensions.swift525
-rw-r--r--Hotline/macOS/FilesView.swift59
-rw-r--r--Hotline/macOS/ServerView.swift1
13 files changed, 2367 insertions, 1267 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj
index bbf6d4b..4c4f92c 100644
--- a/Hotline.xcodeproj/project.pbxproj
+++ b/Hotline.xcodeproj/project.pbxproj
@@ -30,7 +30,7 @@
DA55AC752BE4888300034857 /* InstantMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA55AC742BE4888300034857 /* InstantMessage.swift */; };
DA55AC772BE589F700034857 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA55AC762BE589F700034857 /* AboutView.swift */; platformFilters = (macos, ); };
DA55AC792BE6A1AD00034857 /* RegularExpressions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA55AC782BE6A1AD00034857 /* RegularExpressions.swift */; };
- DA5753682B33E88A00FAC277 /* HotlineFileClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5753672B33E88A00FAC277 /* HotlineFileClient.swift */; };
+ DA5753682B33E88A00FAC277 /* HotlineTransferClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5753672B33E88A00FAC277 /* HotlineTransferClient.swift */; };
DA57536C2B36BA1D00FAC277 /* TextDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA57536B2B36BA1D00FAC277 /* TextDocument.swift */; };
DA6300972B24036B0034CBFD /* HotlineClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6300962B24036B0034CBFD /* HotlineClient.swift */; };
DA65499A2BEC280E00EDB697 /* ServerMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6549992BEC280E00EDB697 /* ServerMessageView.swift */; };
@@ -73,6 +73,7 @@
DAC002192B21630900A6C290 /* SwiftUIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC002182B21630900A6C290 /* SwiftUIExtensions.swift */; };
DAC3D97E2BC0F1ED00A727C9 /* Application-iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC3D97D2BC0F1ED00A727C9 /* Application-iOS.swift */; platformFilter = ios; };
DAC3D9832BC33FD000A727C9 /* ApplicationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC3D9822BC33FD000A727C9 /* ApplicationState.swift */; };
+ DAC87F072C5010E80060FADF /* HotlineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC87F062C5010E80060FADF /* HotlineExtensions.swift */; };
DADDB28B2B22B31F0024040D /* Tracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADDB28A2B22B31F0024040D /* Tracker.swift */; };
DADDB28D2B22B5920024040D /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADDB28C2B22B5920024040D /* Server.swift */; };
DADDB28F2B238D850024040D /* Hotline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADDB28E2B238D850024040D /* Hotline.swift */; };
@@ -113,7 +114,7 @@
DA55AC742BE4888300034857 /* InstantMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantMessage.swift; sourceTree = "<group>"; };
DA55AC762BE589F700034857 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
DA55AC782BE6A1AD00034857 /* RegularExpressions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularExpressions.swift; sourceTree = "<group>"; };
- DA5753672B33E88A00FAC277 /* HotlineFileClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFileClient.swift; sourceTree = "<group>"; };
+ DA5753672B33E88A00FAC277 /* HotlineTransferClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineTransferClient.swift; sourceTree = "<group>"; };
DA57536B2B36BA1D00FAC277 /* TextDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextDocument.swift; sourceTree = "<group>"; };
DA6300962B24036B0034CBFD /* HotlineClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineClient.swift; sourceTree = "<group>"; };
DA6549992BEC280E00EDB697 /* ServerMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMessageView.swift; sourceTree = "<group>"; };
@@ -155,6 +156,7 @@
DAC002182B21630900A6C290 /* SwiftUIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExtensions.swift; sourceTree = "<group>"; };
DAC3D97D2BC0F1ED00A727C9 /* Application-iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Application-iOS.swift"; path = "Sounds/Application-iOS.swift"; sourceTree = "<group>"; };
DAC3D9822BC33FD000A727C9 /* ApplicationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationState.swift; sourceTree = "<group>"; };
+ DAC87F062C5010E80060FADF /* HotlineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineExtensions.swift; sourceTree = "<group>"; };
DADDB28A2B22B31F0024040D /* Tracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tracker.swift; sourceTree = "<group>"; };
DADDB28C2B22B5920024040D /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
DADDB28E2B238D850024040D /* Hotline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hotline.swift; sourceTree = "<group>"; };
@@ -276,8 +278,9 @@
children = (
DA6300962B24036B0034CBFD /* HotlineClient.swift */,
DA9CAFCA2B126E3300CDA197 /* HotlineTrackerClient.swift */,
- DA5753672B33E88A00FAC277 /* HotlineFileClient.swift */,
+ DA5753672B33E88A00FAC277 /* HotlineTransferClient.swift */,
DA4F2BF72B16A17200D8ADDC /* HotlineProtocol.swift */,
+ DAC87F062C5010E80060FADF /* HotlineExtensions.swift */,
);
path = Hotline;
sourceTree = "<group>";
@@ -482,7 +485,7 @@
DA77253F2B21176D006C5ABB /* NewsView.swift in Sources */,
DA872B132BDDBF78008B1012 /* HotlinePanel.swift in Sources */,
DA4F2BF82B16A17200D8ADDC /* HotlineProtocol.swift in Sources */,
- DA5753682B33E88A00FAC277 /* HotlineFileClient.swift in Sources */,
+ DA5753682B33E88A00FAC277 /* HotlineTransferClient.swift in Sources */,
DAB4D8822B4C8FED0048A05C /* FileIconView.swift in Sources */,
DA65499A2BEC280E00EDB697 /* ServerMessageView.swift in Sources */,
DAAEE66F2B47625600A5BA07 /* FilePreviewImageView.swift in Sources */,
@@ -494,6 +497,7 @@
DAE734F92B2E4185000C56F6 /* ServerView.swift in Sources */,
DA2D98122BF29D5F0027E4BD /* NewsItemView.swift in Sources */,
DADDB28B2B22B31F0024040D /* Tracker.swift in Sources */,
+ DAC87F072C5010E80060FADF /* HotlineExtensions.swift in Sources */,
DAE734FD2B2E65E9000C56F6 /* MessageBoardView.swift in Sources */,
DA6980832BFFD06C003E434B /* BookmarkDocument.swift in Sources */,
DAB4D87E2B4C8BCA0048A05C /* FilePreviewTextView.swift in Sources */,
diff --git a/Hotline.xcodeproj/xcshareddata/xcschemes/Hotline.xcscheme b/Hotline.xcodeproj/xcshareddata/xcschemes/Hotline.xcscheme
new file mode 100644
index 0000000..2ba63f6
--- /dev/null
+++ b/Hotline.xcodeproj/xcshareddata/xcschemes/Hotline.xcscheme
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "1540"
+ version = "1.7">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES"
+ buildArchitectures = "Automatic">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DA9CAFB62B126D5700CDA197"
+ BuildableName = "Hotline.app"
+ BlueprintName = "Hotline"
+ ReferencedContainer = "container:Hotline.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ shouldAutocreateTestPlan = "YES">
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DA9CAFB62B126D5700CDA197"
+ BuildableName = "Hotline.app"
+ BlueprintName = "Hotline"
+ ReferencedContainer = "container:Hotline.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <CommandLineArguments>
+ <CommandLineArgument
+ argument = "-com.apple.CoreData.CloudKitDebug 1"
+ isEnabled = "NO">
+ </CommandLineArgument>
+ <CommandLineArgument
+ argument = "-com.apple.CoreData.Logging.stderr 0"
+ isEnabled = "YES">
+ </CommandLineArgument>
+ <CommandLineArgument
+ argument = "-com.apple.CoreData.ConcurrencyDebug 1"
+ isEnabled = "YES">
+ </CommandLineArgument>
+ </CommandLineArguments>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DA9CAFB62B126D5700CDA197"
+ BuildableName = "Hotline.app"
+ BlueprintName = "Hotline"
+ ReferencedContainer = "container:Hotline.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift
index 9bc1e56..d31aa96 100644
--- a/Hotline/Hotline/HotlineClient.swift
+++ b/Hotline/Hotline/HotlineClient.swift
@@ -784,6 +784,23 @@ class HotlineClient: NetSocketDelegate {
}
}
+ @MainActor func sendUploadFile(name fileName: String, path filePath: [String], callback: ((Bool, UInt32?) -> Void)? = nil) {
+ var t = HotlineTransaction(type: .uploadFile)
+ t.setFieldString(type: .fileName, val: fileName)
+ t.setFieldPath(type: .filePath, val: filePath)
+
+ self.sendPacket(t) { reply, err in
+ guard err == nil,
+ let transferReferenceField = reply.getField(type: .referenceNumber),
+ let referenceNumber = transferReferenceField.getUInt32() else {
+ callback?(false, nil)
+ return
+ }
+
+ callback?(true, referenceNumber)
+ }
+ }
+
@MainActor func sendDownloadBanner(callback: ((Bool, UInt32?, Int?) -> Void)? = nil) {
let t = HotlineTransaction(type: .downloadBanner)
diff --git a/Hotline/Hotline/HotlineExtensions.swift b/Hotline/Hotline/HotlineExtensions.swift
new file mode 100644
index 0000000..ac16a39
--- /dev/null
+++ b/Hotline/Hotline/HotlineExtensions.swift
@@ -0,0 +1,782 @@
+import Foundation
+
+enum LineEnding {
+ case lf // Unix-style (\n)
+ case crlf // Windows-style (\r\n)
+ case cr // Classic Mac-style (\r)
+}
+
+extension URL {
+ func urlForResourceFork() -> URL {
+ self.appendingPathComponent("..namedfork/rsrc")
+ }
+}
+
+extension String {
+ func convertingLineEndings(to targetEnding: LineEnding) -> String {
+ let lf = "\n"
+ let crlf = "\r\n"
+ let cr = "\r"
+
+ // Normalize all line endings to LF (\n)
+ let normalizedString = self.replacingOccurrences(of: cr, with: lf).replacingOccurrences(of: crlf, with: lf)
+
+ // Replace normalized LF (\n) line endings with the target line ending
+ switch targetEnding {
+ case .lf:
+ return normalizedString
+ case .crlf:
+ return normalizedString.replacingOccurrences(of: lf, with: crlf)
+ case .cr:
+ return normalizedString.replacingOccurrences(of: lf, with: cr)
+ }
+ }
+
+ func replyToString() -> String {
+ if self.range(of: "^Re:", options: [.regularExpression, .caseInsensitive]) != nil {
+ return String(self)
+ }
+ return "Re: \(self)"
+ }
+
+ func fourCharCode() -> FourCharCode {
+ guard self.count == 4 else {
+ return 0
+ }
+
+ return self.utf16.reduce(0, {$0 << 8 + FourCharCode($1)})
+ }
+}
+
+extension FileManager {
+ func updateFileProgress(_ fileURL: URL, progress: Double) {
+ let filePath = fileURL.path(percentEncoded: false)
+ let fileAttributes = try? self.attributesOfItem(atPath: filePath)
+
+// let extendedAttributes = fileAttributes[]
+
+ print("ATTRIBUTES: ", fileAttributes)
+
+// let blah = Progress(totalUnitCount: 100)
+//
+// blah.fileTotalCount = 1
+// blah.fileOperationKind = .downloading
+// blah.fileCompletedCount = 0
+ }
+
+ func getHFSTypeAndCreator(_ fileURL: URL) throws -> (hfsCreator: UInt32, hfsType: UInt32) {
+ let filePath = fileURL.path(percentEncoded: false)
+ let fileAttributes: [FileAttributeKey: Any] = try self.attributesOfItem(atPath: filePath)
+
+ var creator: UInt32 = 0
+ if let creatorCode: NSNumber = fileAttributes[.hfsCreatorCode] as? NSNumber {
+ creator = creatorCode.uint32Value
+ }
+
+ var type: UInt32 = 0
+ if let typeCode: NSNumber = fileAttributes[.hfsTypeCode] as? NSNumber {
+ type = typeCode.uint32Value
+ }
+
+ return (hfsCreator: creator, hfsType: type)
+ }
+
+ func getCreatedAndModifiedDates(_ fileURL: URL) -> (createdDate: Date, modifiedDate: Date) {
+ let filePath = fileURL.path(percentEncoded: false)
+
+ guard let fileAttributes: [FileAttributeKey: Any] = try? self.attributesOfItem(atPath: filePath),
+ let createdNSDate: NSDate = fileAttributes[.creationDate] as? NSDate,
+ let modifiedNSDate: NSDate = fileAttributes[.modificationDate] as? NSDate else {
+ return (createdDate: Date(), modifiedDate: Date())
+ }
+
+ return (createdDate: createdNSDate as Date, modifiedDate: modifiedNSDate as Date)
+ }
+
+ func setExtendedFileAttribute(_ fileURL: URL, name: String, value: Data) throws {
+ try fileURL.withUnsafeFileSystemRepresentation { fileSystemPath in
+ let result = value.withUnsafeBytes { [count = value.count] in
+ setxattr(fileSystemPath, name, $0.baseAddress, count, 0, 0)
+ }
+
+ guard result >= 0 else { throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: [NSLocalizedDescriptionKey: String(cString: strerror(errno))]) }
+ }
+ }
+
+ func removeExtendedFileAttribute(_ fileURL: URL, name: String) throws {
+ try fileURL.withUnsafeFileSystemRepresentation { fileSystemPath in
+ let result = removexattr(fileSystemPath, name, 0)
+ guard result >= 0 else { throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: [NSLocalizedDescriptionKey: String(cString: strerror(errno))]) }
+ }
+ }
+
+ func getExtendedFileAttribute(_ fileURL: URL, name: String) throws -> Data {
+ let data = try fileURL.withUnsafeFileSystemRepresentation { fileSystemPath in
+ let length = getxattr(fileSystemPath, name, nil, 0, 0, 0)
+ guard length >= 0 else { throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: [NSLocalizedDescriptionKey: String(cString: strerror(errno))]) }
+
+
+ var data = Data(count: length)
+ if length == 0 {
+ return data
+ }
+
+ let result = data.withUnsafeMutableBytes { [count = data.count] in
+ getxattr(fileSystemPath, name, $0.baseAddress, count, 0, 0)
+ }
+ guard result >= 0 else { throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: [NSLocalizedDescriptionKey: String(cString: strerror(errno))]) }
+
+ return data
+ }
+
+ return data
+ }
+
+ func getFileForkSizes(_ fileURL: URL) throws -> (dataForkSize: UInt32, resourceForkSize: UInt32) {
+ let filePath = fileURL.path(percentEncoded: false)
+ guard self.fileExists(atPath: filePath) else {
+ throw CocoaError(.fileNoSuchFile)
+ }
+
+ // Get data fork size.
+ var dataForkSize: UInt32 = 0
+ let dataFileAttributes: [FileAttributeKey: Any]? = try? self.attributesOfItem(atPath: fileURL.path(percentEncoded: false))
+ if let dataNSSizeAttribute: NSNumber = dataFileAttributes?[FileAttributeKey.size] as? NSNumber {
+ dataForkSize = UInt32(dataNSSizeAttribute.int64Value)
+ }
+ else {
+ throw CocoaError(.fileReadCorruptFile)
+ }
+
+ // Get resource fork size.
+ var resourceForkSize: UInt32 = 0
+ let resourceFileURL: URL = fileURL.appendingPathComponent("..namedfork/rsrc")
+ let resourceFilePath: String = fileURL.path(percentEncoded: false)
+ if self.fileExists(atPath: resourceFilePath) {
+ let resourceFileAttributes: [FileAttributeKey: Any]? = try? self.attributesOfItem(atPath: resourceFileURL.path(percentEncoded: false))
+ if let resourceNSSizeAttribute: NSNumber = resourceFileAttributes?[FileAttributeKey.size] as? NSNumber {
+ resourceForkSize = UInt32(resourceNSSizeAttribute.int64Value)
+ }
+ }
+
+ return (dataForkSize: dataForkSize, resourceForkSize: resourceForkSize)
+ }
+
+ func getFlattenedFileSize(_ fileURL: URL) -> UInt64? {
+ var fileIsDirectory: ObjCBool = false
+ let filePath: String = fileURL.path(percentEncoded: false)
+
+ guard fileURL.isFileURL,
+ self.fileExists(atPath: filePath, isDirectory: &fileIsDirectory),
+ fileIsDirectory.boolValue == false else {
+ return nil
+ }
+
+ guard let fileName = fileURL.lastPathComponent.data(using: .macOSRoman) else {
+ return nil
+ }
+
+ var totalSize: UInt64 = 0
+
+ // Add flat file header size.
+ totalSize += UInt64(HotlineFileHeader.DataSize)
+
+ // Add information fork header size.
+ totalSize += UInt64(HotlineFileForkHeader.DataSize)
+
+ // Add information fork size.
+ totalSize += UInt64(HotlineFileInfoFork.BaseDataSize)
+ totalSize += UInt64(fileName.count)
+
+ // Add file fork sizes.
+ if let forkSizes = try? self.getFileForkSizes(fileURL) {
+ // Add data fork size.
+ totalSize += UInt64(HotlineFileForkHeader.DataSize)
+ totalSize += UInt64(forkSizes.dataForkSize)
+
+ // Add resource fork size.
+ if forkSizes.resourceForkSize > 0 {
+ totalSize += UInt64(HotlineFileForkHeader.DataSize)
+ totalSize += UInt64(forkSizes.resourceForkSize)
+ }
+ }
+
+
+ // Add data fork size.
+// var dataSize: UInt64 = 0
+// let dataFileAttributes: [FileAttributeKey: Any]? = try? self.attributesOfItem(atPath: fileURL.path(percentEncoded: false))
+// if let dataSizeAttribute: UInt64 = dataFileAttributes?[FileAttributeKey.size] as? UInt64 {
+// dataSize = UInt64(dataSizeAttribute)
+// }
+//
+// totalSize += dataSize
+//
+// // Add resource fork size.
+// var resourceForkSize: UInt64 = 0
+// let resourceFileURL: URL = fileURL.appendingPathComponent("..namedfork/rsrc")
+// let resourceFilePath: String = fileURL.path(percentEncoded: false)
+// if self.fileExists(atPath: resourceFilePath) {
+// let resourceFileAttributes: [FileAttributeKey: Any]? = try? self.attributesOfItem(atPath: resourceFileURL.path(percentEncoded: false))
+// if let resourceSizeAttribute: UInt64 = resourceFileAttributes?[FileAttributeKey.size] as? UInt64 {
+// resourceForkSize = UInt64(resourceSizeAttribute)
+// print("FOUND RESOURCE FORK: \(resourceForkSize)")
+// }
+// }
+//
+// totalSize += resourceForkSize
+
+ return totalSize
+ }
+}
+
+extension Date {
+ init?(year: UInt16, seconds: UInt32, milliseconds: UInt16) {
+ var components = DateComponents()
+ components.timeZone = .gmt
+ components.year = Int(year)
+ components.month = 1
+ components.day = 1
+ components.second = 0
+
+ guard let baseDate = Calendar.current.date(from: components) else {
+ return nil
+ }
+
+ self = baseDate.advanced(by: TimeInterval(seconds))
+ }
+
+ func hotlineDateComponents() -> (year: UInt16, seconds: UInt32, milliseconds: UInt16)? {
+ let epochTime = self.timeIntervalSince1970
+ let gmtDate = Date(timeIntervalSince1970: epochTime)
+
+ let calendar = Calendar.current
+ var components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second, .nanosecond], from: gmtDate)
+ components.month = 1
+ components.day = 1
+ components.hour = 0
+ components.minute = 0
+ components.second = 0
+ components.nanosecond = 0
+
+ guard let startOfYear = calendar.date(from: components),
+ let year = components.year else {
+ return nil
+ }
+
+ return (year: UInt16(year), seconds: UInt32(gmtDate.timeIntervalSince(startOfYear)), milliseconds: UInt16(0))
+ }
+}
+
+extension Array where Element == UInt8 {
+ init(_ val: UInt8) {
+ self.init()
+ self.appendUInt8(val)
+ }
+
+ init(_ val: UInt16) {
+ self.init()
+ self.appendUInt16(val)
+ }
+
+ init(_ val: UInt32) {
+ self.init()
+ self.appendUInt32(val)
+ }
+
+ init(_ val: UInt64) {
+ self.init()
+ self.appendUInt64(val)
+ }
+
+ mutating func consumeUInt8() -> UInt8? {
+ guard let val = self.readUInt8(at: 0) else {
+ return nil
+ }
+
+ self.removeFirst(1)
+ return val
+ }
+
+ mutating func consumeUInt16() -> UInt16? {
+ guard let val = self.readUInt16(at: 0) else {
+ return nil
+ }
+
+ self.removeFirst(2)
+ return val
+ }
+
+ mutating func consumeUInt32() -> UInt32? {
+ guard let val = self.readUInt32(at: 0) else {
+ return nil
+ }
+
+ self.removeFirst(4)
+ return val
+ }
+
+ mutating func consumeUInt64() -> UInt64? {
+ guard let val = self.readUInt64(at: 0) else {
+ return nil
+ }
+
+ self.removeFirst(8)
+ return val
+ }
+
+ mutating func consume(_ length: Int) -> Bool {
+ guard length <= self.count else {
+ return false
+ }
+
+ self.removeFirst(length)
+ return true
+ }
+
+ mutating func consumeBytes(_ length: Int) -> Data? {
+ guard let val: Data = self.readData(at: 0, length: length) else {
+ return nil
+ }
+
+ self.removeFirst(length)
+ return val
+ }
+
+ mutating func consumeBytes(_ length: Int) -> [UInt8]? {
+ guard let val: [UInt8] = self.readData(at: 0, length: length) else {
+ return nil
+ }
+
+ self.removeFirst(length)
+ return val
+ }
+
+ mutating func consumeDate() -> Date? {
+ guard let date = self.readDate(at: 0) else {
+ return nil
+ }
+
+ self.removeFirst(2 + 2 + 4)
+ return date
+ }
+
+ mutating func consumePString() -> String? {
+ let (str, len) = self.readPString(at: 0)
+ guard let str = str else {
+ return nil
+ }
+ if len == 0 {
+ return ""
+ }
+
+ self.removeFirst(len)
+ return str
+ }
+
+ mutating func consumeLongPString() -> String? {
+ let (str, len) = self.readLongPString(at: 0)
+ guard let str = str else {
+ return nil
+ }
+ if len == 0 {
+ return ""
+ }
+
+ self.removeFirst(len)
+ return str
+ }
+
+ mutating func consumeString(_ length: Int) -> String? {
+ guard let val = self.readString(at: 0, length: length) else {
+ return nil
+ }
+
+ self.removeFirst(length)
+ return val
+ }
+
+ func readUInt8(at offset: Int) -> UInt8? {
+ guard offset >= 0, offset + 1 <= self.count else {
+ return nil
+ }
+ return self[offset]
+ }
+
+ func readUInt16(at offset: Int) -> UInt16? {
+ guard offset >= 0, offset + 2 <= self.count else {
+ return nil
+ }
+
+ return (UInt16(self[offset]) << 8) + UInt16(self[offset + 1])
+ }
+
+ func readUInt32(at offset: Int) -> UInt32? {
+ guard offset >= 0, offset + 4 <= self.count else {
+ return nil
+ }
+
+ return (UInt32(self[offset]) << 24) + (UInt32(self[offset + 1]) << 16) + (UInt32(self[offset + 2]) << 8) + UInt32(self[offset + 3])
+ }
+
+ func readUInt64(at offset: Int) -> UInt64? {
+ guard offset >= 0, offset + 8 <= self.count else {
+ return nil
+ }
+
+ let a: UInt64 = (UInt64(self[offset]) << 56) +
+ (UInt64(self[offset + 1]) << 48) +
+ (UInt64(self[offset + 2]) << 40) +
+ (UInt64(self[offset + 3]) << 32)
+
+ let b: UInt64 = (UInt64(self[offset + 4]) << 24) +
+ (UInt64(self[offset + 5]) << 16) +
+ (UInt64(self[offset + 6]) << 8) +
+ UInt64(self[offset + 7])
+
+ return a + b
+ }
+
+ func readDate(at offset: Int) -> Date? {
+ guard offset >= 0, offset + 2 + 2 + 4 <= self.count else {
+ return nil
+ }
+
+ if
+ let year = self.readUInt16(at: offset),
+ let ms = self.readUInt16(at: offset + 2),
+ let secs = self.readUInt32(at: offset + 2 + 2) {
+ return Date(year: year, seconds: secs, milliseconds: ms)
+// return convertHotlineDate(year: year, seconds: secs, milliseconds: ms)
+ }
+
+ return nil
+ }
+
+ func readData(at offset: Int, length: Int) -> Data? {
+ guard offset >= 0, offset + length <= self.count else {
+ return nil
+ }
+ return Data(self[offset..<(offset + length)])
+ }
+
+ func readData(at offset: Int, length: Int) -> [UInt8]? {
+ guard offset >= 0, offset + length <= self.count else {
+ return nil
+ }
+ return Array(self[offset..<(offset + length)])
+ }
+
+ func readString(at offset: Int, length: Int) -> String? {
+ guard let subdata: Data = self.readData(at: offset, length: length) else {
+ return nil
+ }
+
+ if subdata.count == 0 {
+ return ""
+ }
+
+ let allowedEncodings = [
+ NSUTF8StringEncoding,
+ NSShiftJISStringEncoding,
+ NSUnicodeStringEncoding,
+ NSWindowsCP1251StringEncoding
+ ]
+
+ var decodedNSString: NSString?
+ let rawValue = NSString.stringEncoding(for: subdata, encodingOptions: [.allowLossyKey: false], convertedString: &decodedNSString, usedLossyConversion: nil)
+
+ if allowedEncodings.contains(rawValue) {
+ return decodedNSString as? String
+ }
+
+ else if rawValue > 1 {
+ print("ENCODING FOUND \(rawValue)")
+ }
+
+ var macStr = String(data: subdata, encoding: .macOSRoman)
+ if macStr == nil {
+ macStr = String(data: subdata, encoding: .nonLossyASCII)
+ }
+
+ return macStr
+ }
+
+ func readPString(at offset: Int) -> (String?, Int) {
+ guard offset >= 0, offset + 1 <= self.count else {
+ return (nil, 0)
+ }
+ let len = Int(self.readUInt8(at: offset)!)
+ guard offset + 1 + len <= self.count else {
+ return (nil, 0)
+ }
+ return (self.readString(at: offset+1, length: len), 1 + len)
+ }
+
+ func readLongPString(at offset: Int) -> (String?, Int) {
+ guard offset >= 0, offset + 2 <= self.count else {
+ return (nil, 0)
+ }
+ let len = Int(self.readUInt16(at: offset)!)
+ guard len > 0 else {
+ return ("", 0)
+ }
+ guard offset + 2 + len <= self.count else {
+ return (nil, 0)
+ }
+ return (self.readString(at: offset+2, length: len), len)
+ }
+
+ mutating func appendUInt8(_ value: UInt8, endianness: Endianness = .big) {
+ let val = endianness == .big ? value.bigEndian : value.littleEndian
+ self.append(val)
+ }
+
+ mutating func appendUInt16(_ value: UInt16, endianness: Endianness = .big) {
+ let val = endianness == .big ? value.bigEndian : value.littleEndian
+ let bytes: [UInt8] = [
+ UInt8(val & 0x00FF),
+ UInt8((val >> 8) & 0x00FF),
+ ]
+ self.append(contentsOf: bytes)
+ }
+
+ mutating func appendUInt32(_ value: UInt32, endianness: Endianness = .big) {
+ let val = endianness == .big ? value.bigEndian : value.littleEndian
+ let bytes: [UInt8] = [
+ UInt8(val & 0x000000FF),
+ UInt8((val >> 8) & 0x000000FF),
+ UInt8((val >> 16) & 0x000000FF),
+ UInt8((val >> 24) & 0x000000FF),
+ ]
+ self.append(contentsOf: bytes)
+ }
+
+ mutating func appendUInt64(_ value: UInt64, endianness: Endianness = .big) {
+ let val: UInt64 = endianness == .big ? value.bigEndian : value.littleEndian
+ let bytes: [UInt8] = [
+ UInt8(val & 0x00000000000000FF),
+ UInt8((val >> 8) & 0x00000000000000FF),
+ UInt8((val >> 16) & 0x00000000000000FF),
+ UInt8((val >> 24) & 0x00000000000000FF),
+ UInt8((val >> 32) & 0x00000000000000FF),
+ UInt8((val >> 40) & 0x00000000000000FF),
+ UInt8((val >> 48) & 0x00000000000000FF),
+ UInt8((val >> 56) & 0x00000000000000FF),
+ ]
+ self.append(contentsOf: bytes)
+ }
+
+ mutating func appendData(_ data: Data) {
+ self.append(contentsOf: data)
+ }
+
+ mutating func appendData(_ data: [UInt8]) {
+ self.append(contentsOf: data)
+ }
+
+ mutating func hotlineEncrypt() {
+ for i in (0..<self.count).reversed() {
+ self[i] = 0xFF - self[i]
+ }
+ }
+
+ func hotlineEncrypted() -> [UInt8] {
+ var cpy = [UInt8](self)
+
+
+
+ for i in (0..<cpy.count).reversed() {
+ cpy[i] = 0xFF - cpy[i]
+ }
+ return cpy
+ }
+}
+
+extension Data {
+ init(_ val: UInt8) {
+ self.init()
+ self.appendUInt8(val)
+ }
+
+ init(_ val: UInt16) {
+ self.init()
+ self.appendUInt16(val)
+ }
+
+ init(_ val: UInt32) {
+ self.init()
+ self.appendUInt32(val)
+ }
+
+ func readUInt8(at offset: Int) -> UInt8? {
+ guard offset >= 0, offset + 1 <= self.count else {
+ return nil
+ }
+ return self[offset]
+ }
+
+ func readUInt16(at offset: Int) -> UInt16? {
+ guard offset >= 0, offset + 2 <= self.count else {
+ return nil
+ }
+
+ return (UInt16(self[offset]) << 8) + UInt16(self[offset + 1])
+ }
+
+ func readUInt32(at offset: Int) -> UInt32? {
+ guard offset >= 0, offset + 4 <= self.count else {
+ return nil
+ }
+
+ return (UInt32(self[offset]) << 24) + (UInt32(self[offset + 1]) << 16) + (UInt32(self[offset + 2]) << 8) + UInt32(self[offset + 3])
+ }
+
+ func readUInt64(at offset: Int) -> UInt64? {
+ guard offset >= 0, offset + 8 <= self.count else {
+ return nil
+ }
+
+ return withUnsafeBytes { $0.load(as: UInt64.self ) }
+ }
+
+ func readDate(at offset: Int) -> Date? {
+ guard offset >= 0, offset + 2 + 2 + 4 <= self.count else {
+ return nil
+ }
+
+ if
+ let year = self.readUInt16(at: offset),
+ let ms = self.readUInt16(at: offset + 2),
+ let secs = self.readUInt32(at: offset + 2 + 2) {
+ return Date(year: year, seconds: secs, milliseconds: ms)
+ }
+
+ return nil
+ }
+
+ mutating func appendDate(_ date: Date) {
+ var year: UInt16 = 0
+ var msecs: UInt16 = 0
+ var secs: UInt32 = 0
+
+ if let components = date.hotlineDateComponents() {
+ year = components.year
+ secs = components.seconds
+ msecs = components.milliseconds
+ }
+
+ self.appendUInt16(year)
+ self.appendUInt16(msecs)
+ self.appendUInt32(secs)
+ }
+
+ func readData(at offset: Int, length: Int) -> Data? {
+ guard offset >= 0, offset + length <= self.count else {
+ return nil
+ }
+ return self.subdata(in: offset..<(offset + length))
+ }
+
+ func readString(at offset: Int, length: Int) -> String? {
+ let subdata = self[offset..<(offset + length)]
+ if subdata.count == 0 {
+ return ""
+ }
+
+ let allowedEncodings = [
+ NSUTF8StringEncoding,
+ NSShiftJISStringEncoding,
+ NSUnicodeStringEncoding,
+ NSWindowsCP1251StringEncoding
+ ]
+
+ var decodedNSString: NSString?
+ let rawValue = NSString.stringEncoding(for: subdata, encodingOptions: [.allowLossyKey: false], convertedString: &decodedNSString, usedLossyConversion: nil)
+
+ if allowedEncodings.contains(rawValue) {
+ return decodedNSString as? String
+ }
+
+ else if rawValue > 1 {
+ print("ENCODING FOUND \(rawValue)")
+ }
+
+ var macStr = String(data: subdata, encoding: .macOSRoman)
+ if macStr == nil {
+ macStr = String(data: subdata, encoding: .nonLossyASCII)
+ }
+
+ return macStr
+ }
+
+ func readPString(at offset: Int) -> (String?, Int) {
+ guard offset >= 0, offset + 1 <= self.count else {
+ return (nil, 0)
+ }
+ let len = Int(self.readUInt8(at: offset)!)
+ guard offset + 1 + len <= self.count else {
+ return (nil, 0)
+ }
+ return (self.readString(at: offset+1, length: len), 1 + len)
+ }
+
+ func readLongPString(at offset: Int) -> (String?, Int) {
+ guard offset >= 0, offset + 2 <= self.count else {
+ return (nil, 0)
+ }
+ let len = Int(self.readUInt16(at: offset)!)
+ guard len > 0 else {
+ return ("", 0)
+ }
+ guard offset + 2 + len <= self.count else {
+ return (nil, 0)
+ }
+ return (self.readString(at: offset+2, length: len), len)
+ }
+
+
+ mutating func appendUInt8(_ value: UInt8, endianness: Endianness = .big) {
+ var val = endianness == .big ? value.bigEndian : value.littleEndian
+ append(&val, count: MemoryLayout<UInt8>.size)
+ }
+
+ mutating func appendUInt16(_ value: UInt16, endianness: Endianness = .big) {
+ var val = endianness == .big ? value.bigEndian : value.littleEndian
+ Swift.withUnsafeBytes(of: &val) { buffer in
+ append(buffer.bindMemory(to: UInt8.self))
+ }
+// append(&val, count: MemoryLayout<UInt16>.size)
+ }
+
+ mutating func appendUInt32(_ value: UInt32, endianness: Endianness = .big) {
+ var val = endianness == .big ? value.bigEndian : value.littleEndian
+ Swift.withUnsafeBytes(of: &val) { buffer in
+ append(buffer.bindMemory(to: UInt8.self))
+ }
+// append(&val, count: MemoryLayout<UInt32>.size)
+ }
+
+ mutating func appendZeros(count: Int) {
+ append(contentsOf: Array<UInt8>(repeating: 0, count: count))
+ }
+
+ mutating func appendString(_ value: String, encoding: String.Encoding) {
+ guard let encodedString = value.data(using: encoding) else {
+ return
+ }
+
+ append(encodedString)
+ }
+}
+
+extension FourCharCode {
+ func fourCharCode() -> String {
+ let bytes = [
+ UInt8((self >> 24) & 0xFF),
+ UInt8((self >> 16) & 0xFF),
+ UInt8((self >> 8) & 0xFF),
+ UInt8(self & 0xFF)
+ ]
+ return String(bytes: bytes, encoding: .ascii) ?? ""
+ }
+}
diff --git a/Hotline/Hotline/HotlineFileClient.swift b/Hotline/Hotline/HotlineFileClient.swift
deleted file mode 100644
index 8202a61..0000000
--- a/Hotline/Hotline/HotlineFileClient.swift
+++ /dev/null
@@ -1,665 +0,0 @@
-import Foundation
-import Network
-import UniformTypeIdentifiers
-
-enum HotlineFileClientError: Error {
- case failedToConnect
- case failedToDownload
-}
-
-enum HotlineFileClientStatus: Equatable {
- case unconnected
- case connecting
- case connected
- case progress(Double)
- case completed
- case failed(HotlineFileClientError)
-}
-
-protocol HotlineFileClientDelegate: AnyObject {
- func hotlineFileStatusChanged(client: HotlineFileClient, reference: UInt32, status: HotlineFileClientStatus, timeRemaining: TimeInterval)
- func hotlineFileReceivedInfo(client: HotlineFileClient, reference: UInt32, info: HotlineFileInfoFork)
- func hotlineFileDownloadedData(client: HotlineFileClient, reference: UInt32, data: Data)
- func hotlineFileDownloadedFile(client: HotlineFileClient, reference: UInt32, at: URL)
-}
-
-extension HotlineFileClientDelegate {
- func hotlineFileStatusChanged(client: HotlineFileClient, reference: UInt32, status: HotlineFileClientStatus, timeRemaining: TimeInterval) {}
- func hotlineFileReceivedInfo(client: HotlineFileClient, reference: UInt32, info: HotlineFileInfoFork) {}
- func hotlineFileDownloadedData(client: HotlineFileClient, reference: UInt32, data: Data) {}
- func hotlineFileDownloadedFile(client: HotlineFileClient, reference: UInt32, at: URL) {}
-}
-
-enum HotlineFileTransferType {
- case file
- case preview
-}
-
-enum HotlineFileTransferStage: Int {
- case fileHeader = 1
- case fileForkHeader = 2
- case fileInfoFork = 3
- case fileDataFork = 4
- case fileResourceFork = 5
- case fileUnsupportedFork = 6
-}
-
-struct HotlineFileHeader {
- static let DataSize: Int = 4 + 2 + 16 + 2
-
- let format: UInt32
- let version: UInt16
- let forkCount: UInt16
-
- init?(from data: Data) {
- guard data.count >= HotlineFileHeader.DataSize else {
- return nil
- }
-
- self.format = data.readUInt32(at: 0)!
- self.version = data.readUInt16(at: 4)!
- // 16 bytes of reserved data sits here. Skip it.
- // self.reserved = data.readData(at: 4 + 2, length: 16)
- self.forkCount = data.readUInt16(at: 4 + 2 + 16)!
- }
-}
-
-struct HotlineFileForkHeader {
- static let DataSize: Int = 4 + 4 + 4 + 4
-
- let forkType: UInt32
- let compressionType: UInt32
- let dataSize: UInt32
-
- init?(from data: Data) {
- guard data.count >= HotlineFileForkHeader.DataSize else {
- return nil
- }
-
- self.forkType = data.readUInt32(at: 0)!
- self.compressionType = data.readUInt32(at: 4)!
- // 4 bytes of reserved data sits here. Skip it.
- // self.reserved = data.readUInt32(at: 4 + 4)!
- self.dataSize = data.readUInt32(at: 4 + 4 + 4)!
- }
-}
-
-struct HotlineFileInfoFork {
- static let BaseDataSize: Int = 4 + 4 + 4 + 4 + 4 + 32 + 8 + 8 + 2 + 2
-
- let platform: UInt32
- let type: UInt32
- let creator: UInt32
- let flags: UInt32
- let platformFlags: UInt32
- let createdDate: Date
- let modifiedDate: Date
- let nameScript: UInt16
- let name: String
- let comment: String?
- var headerSize: Int
-
- init?(from data: Data) {
- // Make sure we have at least enough data to read basic header data
- guard data.count >= HotlineFileInfoFork.BaseDataSize else {
- return nil
- }
-
- if
- let platform = data.readUInt32(at: 0),
- let type = data.readUInt32(at: 4),
- let creator = data.readUInt32(at: 4 + 4),
- let flags = data.readUInt32(at: 4 + 4 + 4),
- let platformFlags = data.readUInt32(at: 4 + 4 + 4 + 4),
- // 32 bytes of reserved data sits here. Skip it.
- let nameScript = data.readUInt16(at: 4 + 4 + 4 + 4 + 4 + 32 + 8 + 8) {
-
- let createdDate = data.readDate(at: 4 + 4 + 4 + 4 + 4 + 32) ?? Date.now
- let modifiedDate = data.readDate(at: 4 + 4 + 4 + 4 + 4 + 32 + 8) ?? Date.now
-
- let (n, nl) = data.readLongPString(at: 4 + 4 + 4 + 4 + 4 + 32 + 8 + 8 + 2)
- if let name = n {
- self.platform = platform
- self.type = type
- self.creator = creator
- self.flags = flags
- self.platformFlags = platformFlags
- self.createdDate = createdDate
- self.modifiedDate = modifiedDate
- self.nameScript = nameScript
- self.name = name
-
- var calculatedHeaderSize: Int = HotlineFileInfoFork.BaseDataSize + nl
- var commentRead: String? = nil
- if data.count >= HotlineFileInfoFork.BaseDataSize + nl + 2 {
- let commentLength = data.readUInt16(at: HotlineFileInfoFork.BaseDataSize + nl)!
- var commentCorrupted = false
-
- // Some servers have incorrect data length for the INFO fork
- // the length they send is what it should be but don't include
- // the comment length in the actual data, so we end up with mismatched
- // lengths. So here we test if the length we read is actually 'DA'
- // or the first part of the "DATA" fork header.
- // Needless to say, stuff like this makes for sad code but this ain't so bad.
- if commentLength == 0x4441 {
- commentCorrupted = true
- }
-
- if !commentCorrupted {
- let (c, cl) = data.readLongPString(at: HotlineFileInfoFork.BaseDataSize + nl)
- calculatedHeaderSize += 2
- if cl > 0 {
- calculatedHeaderSize += Int(cl)
- if let ct = c, cl > 0 {
- commentRead = ct
- }
- }
- }
- }
-
- self.comment = commentRead
- self.headerSize = calculatedHeaderSize
- return
- }
- }
-
- return nil
- }
-}
-
-class HotlineFileClient {
- let serverAddress: NWEndpoint.Host
- let serverPort: NWEndpoint.Port
- let referenceNumber: UInt32
- let referenceDataSize: UInt32
- let transferType: HotlineFileTransferType
-
- weak var delegate: HotlineFileClientDelegate? = nil
-
- var status: HotlineFileClientStatus = .unconnected {
- didSet {
- DispatchQueue.main.async {
- self.delegate?.hotlineFileStatusChanged(client: self, reference: self.referenceNumber, status: self.status, timeRemaining: self.fileProgress?.estimatedTimeRemaining ?? 0.0)
- }
- }
- }
-
- private var connection: NWConnection?
- private var transferStage: HotlineFileTransferStage = .fileHeader
-
- private var fileBytes = Data()
- private var fileResourceBytes = Data()
-
- private var fileHeader: HotlineFileHeader? = nil
- private var fileCurrentForkHeader: HotlineFileForkHeader? = nil
- private var fileCurrentForkBytesLeft: Int = 0
- private var fileInfoFork: HotlineFileInfoFork? = nil
- private var fileHandle: FileHandle? = nil
- private var filePath: String? = nil
- private var fileBytesDownloaded: Int = 0
- private var fileProgress: Progress? = nil
-
-// private var previewCallback: ((Data?) -> Void)? = nil
-// private var fileCallback: ((URL?) -> Void)? = nil
-
- init(address: String, port: UInt16, reference: UInt32, size: UInt32, type: HotlineFileTransferType = .file) {
- self.serverAddress = NWEndpoint.Host(address)
- self.serverPort = NWEndpoint.Port(rawValue: port + 1)!
- self.referenceNumber = reference
- self.referenceDataSize = size
- self.transferType = type
- self.transferStage = .fileHeader
- }
-
- deinit {
- self.invalidate()
- }
-
- func downloadToMemory() {
- guard self.status == .unconnected else {
- return
- }
-
- self.connect()
- }
-
- func downloadToFile() {
- guard self.status == .unconnected else {
- return
- }
-
- self.filePath = nil
- self.connect()
- }
-
- func downloadToFile(to fileURL: URL) {
- guard self.status == .unconnected else {
- return
- }
-
- self.filePath = fileURL.path
- self.connect()
- }
-
- func cancel(deleteIncompleteFile: Bool = true) {
- self.delegate = nil
-
- if self.status == .unconnected {
- return
- }
-
- // Close file before we try to potentionally delete it.
- if let fh = self.fileHandle {
- try? fh.close()
- self.fileHandle = nil
- }
-
- if deleteIncompleteFile, let downloadPath = self.filePath {
- print("HotlineFileClient: Deleting file fragment at", downloadPath)
- if FileManager.default.isDeletableFile(atPath: downloadPath) {
- try? FileManager.default.removeItem(atPath: downloadPath)
- }
- self.filePath = nil
- }
-
- self.invalidate()
-
- print("HotlineFileClient: Cancelled transfer")
- }
-
- // MARK: -
-
- private func connect() {
- self.connection = NWConnection(host: self.serverAddress, port: self.serverPort, using: .tcp)
- self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in
- switch newState {
- case .ready:
- self?.status = .connected
- self?.sendMagic()
- case .waiting(let err):
- print("HotlineFileClient: Waiting", err)
- case .cancelled:
- print("HotlineFileClient: Cancelled")
- self?.invalidate()
- case .failed(let err):
- print("HotlineFileClient: Connection error \(err)")
- switch self?.status {
- case .connecting:
- print("FAILED TO CONNECT")
- self?.invalidate()
- self?.status = .failed(.failedToConnect)
- case .connected, .progress(_):
- print("FAILED TO DOWNLLOAD")
- self?.invalidate()
- self?.status = .failed(.failedToDownload)
- default:
- break
- }
- default:
- return
- }
- }
-
- self.status = .connecting
- self.connection?.start(queue: .global())
- }
-
- func invalidate() {
- if let c = self.connection {
- c.stateUpdateHandler = nil
- c.cancel()
-
- self.connection = nil
- }
-
- self.fileBytes = Data()
-
- if let fh = self.fileHandle {
- try? fh.close()
- self.fileHandle = nil
- }
- }
-
- private func sendMagic() {
- guard let c = connection, self.status == .connected else {
- self.invalidate()
- print("HotlineFileClient: invalid connection to send header.")
- return
- }
-
- var headerData = Data()
- headerData.appendUInt32("HTXF".fourCharCode())
- headerData.appendUInt32(self.referenceNumber)
- headerData.appendUInt32(0)
- headerData.appendUInt32(0)
-
- c.send(content: headerData, completion: .contentProcessed { [weak self] (error) in
- guard let self = self else {
- return
- }
-
- guard error == nil else {
- self.status = .failed(.failedToConnect)
- self.invalidate()
- return
- }
-
- self.status = .progress(0.0)
-
- switch self.transferType {
- case .file:
- self.receiveFile()
- case .preview:
- self.receivePreviewData()
- }
- })
- }
-
- private func receiveFile() {
- guard let c = self.connection else {
- return
- }
-
- c.receive(minimumIncompleteLength: 1, maximumLength: Int(UInt16.max)) { [weak self] (data, context, isComplete, error) in
- guard let self = self else {
- return
- }
-
- guard error == nil else {
- self.status = .failed(.failedToDownload)
- self.invalidate()
- return
- }
-
- if let newData = data, !newData.isEmpty {
- self.fileBytesDownloaded += newData.count
- self.fileBytes.append(newData)
- self.fileProgress?.completedUnitCount = Int64(self.fileBytesDownloaded)
- self.status = .progress(Double(self.fileBytesDownloaded) / Double(self.referenceDataSize))
- }
-
- // See if we need header data still.
- var keepProcessing = false
- repeat {
- keepProcessing = false
-
- switch self.transferStage {
- case .fileHeader:
- if let header = HotlineFileHeader(from: self.fileBytes) {
- self.fileBytes.removeSubrange(0..<HotlineFileHeader.DataSize)
- self.fileHeader = header
- self.transferStage = .fileForkHeader
- keepProcessing = true
- }
- case .fileForkHeader:
- if let forkHeader = HotlineFileForkHeader(from: self.fileBytes) {
-// let fileForkHeader = HotlineFileForkHeader(from: self.fileBytes)
- self.fileBytes.removeSubrange(0..<HotlineFileForkHeader.DataSize)
- self.fileCurrentForkHeader = forkHeader
- self.fileCurrentForkBytesLeft = Int(forkHeader.dataSize)
-
- if forkHeader.forkType == "INFO".fourCharCode() {
- print("INFO FORK!")
- self.transferStage = .fileInfoFork
- }
- else if forkHeader.forkType == "DATA".fourCharCode() {
- print("DATA FORK!")
- self.transferStage = .fileDataFork
- }
- else if forkHeader.forkType == "MACR".fourCharCode() {
- print("RESOURCE FORK!")
- self.fileResourceBytes = Data()
-// if self.prepareResourceFile() {
-// print("BOOYAH")
-// }
- self.transferStage = .fileResourceFork
- }
- else {
- print("UNKNOWN FORK??")
- self.transferStage = .fileUnsupportedFork
- }
-
- keepProcessing = true
- }
- case .fileInfoFork:
- if let infoForkDataSize = self.fileCurrentForkHeader?.dataSize, self.fileBytes.count >= infoForkDataSize {
- let infoForkData = self.fileBytes.subdata(in: 0..<Int(infoForkDataSize))
- if let infoFork = HotlineFileInfoFork(from: infoForkData) {
- self.fileInfoFork = infoFork
-
- self.fileBytes.removeSubrange(0..<infoFork.headerSize)
-
- self.transferStage = .fileForkHeader
- self.fileCurrentForkBytesLeft -= Int(infoForkDataSize)
- self.fileCurrentForkHeader = nil
-
- print("INFO FORK STUFF:", infoFork)
-
- // Create file in Downloads folder if we don't have a destination already.
- if !self.prepareDownloadFile(name: infoFork.name) {
- print("FAILED TO CREATE FILE ON DISK")
- }
-
- let reference = self.referenceNumber
- DispatchQueue.main.async {
- self.delegate?.hotlineFileReceivedInfo(client: self, reference: reference, info: infoFork)
- }
- }
-
- keepProcessing = true
- }
- case .fileDataFork:
- if self.fileBytes.count > 0 {
- if let f = self.fileHandle {
- do {
- var dataToWrite = self.fileBytes
-
- if dataToWrite.count >= self.fileCurrentForkBytesLeft {
- dataToWrite = self.fileBytes.subdata(in: 0..<self.fileCurrentForkBytesLeft)
- self.fileBytes.removeSubrange(0..<self.fileCurrentForkBytesLeft)
-
- self.transferStage = .fileForkHeader
- self.fileCurrentForkBytesLeft = 0
- self.fileCurrentForkHeader = nil
-
- keepProcessing = true
- }
- else {
- self.fileCurrentForkBytesLeft -= dataToWrite.count
- self.fileBytes = Data()
- }
-
- try f.write(contentsOf: dataToWrite)
- }
- catch {
- print("DOWNLOAD WRITE ERROR", error)
- }
- }
- }
- case .fileResourceFork:
- if self.fileBytes.count > 0 {
-// if let f = self.fileResourceHandle {
-// do {
- var dataToWrite = self.fileBytes
-
- if dataToWrite.count >= self.fileCurrentForkBytesLeft {
- dataToWrite = self.fileBytes.subdata(in: 0..<self.fileCurrentForkBytesLeft)
- self.fileBytes.removeSubrange(0..<self.fileCurrentForkBytesLeft)
-
- self.transferStage = .fileForkHeader
- self.fileCurrentForkBytesLeft = 0
- self.fileCurrentForkHeader = nil
-
- keepProcessing = true
- }
- else {
- self.fileCurrentForkBytesLeft -= dataToWrite.count
- self.fileBytes = Data()
- }
-
- print("WRITING TO RESOURCE FORK", dataToWrite.count)
-
- self.fileResourceBytes.append(dataToWrite)
-// try f.write(contentsOf: dataToWrite)
-// }
-// catch {
-// print("DOWNLOAD WRITE ERROR", error)
-// }
-// }
- }
- case .fileUnsupportedFork:
- if self.fileBytes.count > 0 {
- var dataToWrite = self.fileBytes
-
- if dataToWrite.count >= self.fileCurrentForkBytesLeft {
- dataToWrite = self.fileBytes.subdata(in: 0..<self.fileCurrentForkBytesLeft)
- self.fileBytes.removeSubrange(0..<self.fileCurrentForkBytesLeft)
-
- self.transferStage = .fileForkHeader
- self.fileCurrentForkBytesLeft = 0
- self.fileCurrentForkHeader = nil
-
- keepProcessing = true
- }
- else {
- self.fileCurrentForkBytesLeft -= dataToWrite.count
- self.fileBytes = Data()
- }
- }
- }
- } while keepProcessing
-
- if self.fileBytesDownloaded < Int(self.referenceDataSize) {
- // If we still have more to download, then receive more data.
- self.receiveFile()
- return
- }
- else {
- self.invalidate()
-
- if self.fileResourceBytes.count > 0 {
- let _ = self.writeResourceFork()
- self.fileResourceBytes = Data()
- }
-
- self.status = .completed
-
- if let downloadPath = self.filePath {
- DispatchQueue.main.sync {
- print("POSTING DOWNLOAD FILE FINISHED", downloadPath)
-
- var downloadURL = URL(filePath: downloadPath)
- downloadURL.resolveSymlinksInPath()
- print("FINAL PATH", downloadURL.path)
-
- let reference = self.referenceNumber
- self.delegate?.hotlineFileDownloadedFile(client: self, reference: reference, at: downloadURL)
-
- #if os(macOS)
- // Bounce dock icon when download completes. Weird this is the only API to do so.
- DistributedNotificationCenter.default().post(name: .init("com.apple.DownloadFileFinished"), object: downloadURL.path)
- #endif
- }
- }
- }
- }
- }
-
- private func receivePreviewData() {
- guard let c = self.connection else {
- print("UH OH!")
- return
- }
-
- c.receive(minimumIncompleteLength: 1, maximumLength: Int(UInt16.max)) { [weak self] (data, context, isComplete, error) in
- guard let self = self else {
- return
- }
-
- guard error == nil else {
- self.status = .failed(.failedToDownload)
- self.invalidate()
- return
- }
-
- if let newData = data, !newData.isEmpty {
- self.fileBytesDownloaded += newData.count
- self.fileBytes.append(newData)
- self.status = .progress(Double(self.fileBytesDownloaded) / Double(self.referenceDataSize))
- print("HotlineFileClient: Download progress", self.fileBytesDownloaded, self.referenceDataSize, isComplete)
- }
-
- if self.fileBytesDownloaded < Int(self.referenceDataSize) {
- self.receivePreviewData()
- }
- else {
- print("HotlineFileClient: Complete")
- let reference = self.referenceNumber
- let data = self.fileBytes
-
- self.status = .completed
- self.invalidate()
-
- DispatchQueue.main.sync {
- self.delegate?.hotlineFileDownloadedData(client: self, reference: reference, data: data)
- }
- }
- }
- }
-
- // MARK: - Utility
-
- private func writeResourceFork() -> Bool {
- guard let filePath = self.filePath else {
- return false
- }
-
- var resolvedFileURL = URL(filePath: filePath)
- resolvedFileURL.resolveSymlinksInPath()
-
- let resourceFilePath = resolvedFileURL.appendingPathComponent("..namedfork/rsrc")
-
- do {
- try self.fileResourceBytes.write(to: resourceFilePath)
- }
- catch {
- return false
- }
-
- return true
- }
-
- private func prepareDownloadFile(name: String) -> Bool {
- if let filePath = self.filePath {
- if let h = FileHandle(forWritingAtPath: filePath) {
- self.filePath = filePath
- self.fileHandle = h
- self.fileProgress?.fileURL = URL(filePath: filePath).resolvingSymlinksInPath()
- return true
- }
- }
- else {
- let folderURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
- let filePath = folderURL.generateUniqueFilePath(filename: name)
-
- var fileAttributes: [FileAttributeKey : Any] = [:]
- if let creatorCode = self.fileInfoFork?.creator {
- fileAttributes[.hfsCreatorCode] = creatorCode as NSNumber
- }
- if let typeCode = self.fileInfoFork?.type {
- fileAttributes[.hfsTypeCode] = typeCode as NSNumber
- }
-
- if FileManager.default.createFile(atPath: filePath, contents: nil, attributes: fileAttributes) {
- if let h = FileHandle(forWritingAtPath: filePath) {
- self.filePath = filePath
- self.fileHandle = h
- self.fileProgress?.fileURL = URL(filePath: filePath).resolvingSymlinksInPath()
- return true
- }
- }
- }
-
- return false
- }
-}
diff --git a/Hotline/Hotline/HotlineProtocol.swift b/Hotline/Hotline/HotlineProtocol.swift
index 314133d..ff91672 100644
--- a/Hotline/Hotline/HotlineProtocol.swift
+++ b/Hotline/Hotline/HotlineProtocol.swift
@@ -970,17 +970,3 @@ enum HotlineTransactionType: UInt16 {
case unknown = 15000
}
-
-// MARK: - Utilities
-
-func convertHotlineDate(year:UInt16, seconds: UInt32, milliseconds: UInt16) -> Date? {
- var components = DateComponents()
- components.timeZone = .gmt
- components.year = Int(year)
- components.month = 1
- components.day = 1
- components.second = 0
-
- let baseDate = Calendar.current.date(from: components)
- return baseDate?.advanced(by: TimeInterval(seconds))
-}
diff --git a/Hotline/Hotline/HotlineTransferClient.swift b/Hotline/Hotline/HotlineTransferClient.swift
new file mode 100644
index 0000000..6c5db84
--- /dev/null
+++ b/Hotline/Hotline/HotlineTransferClient.swift
@@ -0,0 +1,1244 @@
+import Foundation
+import Network
+import UniformTypeIdentifiers
+
+enum HotlineFileClientError: Error {
+ case failedToConnect
+ case failedToDownload
+ case failedToUpload
+}
+
+enum HotlineFileFork {
+ case none
+ case info
+ case data
+ case resource
+ case unsupported
+}
+
+enum HotlineTransferStatus: Equatable {
+ case unconnected
+ case connecting
+ case connected
+ case progress(Double)
+ case completed
+ case failed(HotlineFileClientError)
+}
+
+protocol HotlineTransferDelegate: AnyObject {
+ func hotlineTransferStatusChanged(client: HotlineTransferClient, reference: UInt32, status: HotlineTransferStatus, timeRemaining: TimeInterval)
+}
+
+protocol HotlineFileDownloadClientDelegate: HotlineTransferDelegate {
+ func hotlineFileDownloadReceivedInfo(client: HotlineFileDownloadClient, reference: UInt32, info: HotlineFileInfoFork)
+ func hotlineFileDownloadComplete(client: HotlineFileDownloadClient, reference: UInt32, at: URL)
+}
+
+protocol HotlineFilePreviewClientDelegate: HotlineTransferDelegate {
+ func hotlineFilePreviewComplete(client: HotlineFilePreviewClient, reference: UInt32, data: Data)
+}
+
+protocol HotlineFileUploadClientDelegate: HotlineTransferDelegate {
+ func hotlineFileUploadComplete(client: HotlineFileUploadClient, reference: UInt32)
+}
+
+//enum HotlineFileTransferType {
+// case fileDownload
+// case filePreview
+// case fileUpload
+//}
+
+enum HotlineFileTransferStage: Int {
+ case fileHeader = 1
+ case fileForkHeader = 2
+ case fileInfoFork = 3
+ case fileDataFork = 4
+ case fileResourceFork = 5
+ case fileUnsupportedFork = 6
+}
+
+enum HotlineFileUploadStage: Int {
+ case magic = 1
+ case fileHeader = 2
+ case fileInfoForkHeader = 3
+ case fileInfoFork = 4
+ case fileDataForkHeader = 5
+ case fileDataFork = 6
+ case fileResourceForkHeader = 7
+ case fileResourceFork = 8
+ case fileComplete = 9
+}
+
+protocol HotlineTransferClient {
+ var serverAddress: NWEndpoint.Host { get }
+ var serverPort: NWEndpoint.Port { get }
+ var referenceNumber: UInt32 { get }
+ var status: HotlineTransferStatus { get set }
+
+ func start()
+ func cancel()
+}
+
+// MARK: -
+
+class HotlineFileUploadClient: HotlineTransferClient {
+ let serverAddress: NWEndpoint.Host
+ let serverPort: NWEndpoint.Port
+ let referenceNumber: UInt32
+
+ weak var delegate: HotlineFileUploadClientDelegate? = nil
+
+ private var connection: NWConnection?
+ private var stage: HotlineFileUploadStage = .magic
+ private var payloadSize: UInt32 = 0
+ private let fileURL: URL
+ private let fileResourceURL: URL?
+ private var fileHandle: FileHandle? = nil
+ private var bytesSent: Int = 0
+ private let infoForkData: Data
+ private let dataForkSize: UInt32
+ private let resourceForkSize: UInt32
+
+ var status: HotlineTransferStatus = .unconnected {
+ didSet {
+ DispatchQueue.main.async {
+ self.delegate?.hotlineTransferStatusChanged(client: self, reference: self.referenceNumber, status: self.status, timeRemaining: 0.0)
+ }
+ }
+ }
+
+ init?(upload fileURL: URL, address: String, port: UInt16, reference: UInt32) {
+ guard let payloadSize = FileManager.default.getFlattenedFileSize(fileURL) else {
+ return nil
+ }
+
+ guard let infoFork = HotlineFileInfoFork(file: fileURL) else {
+ return nil
+ }
+
+ guard let forkSizes = try? FileManager.default.getFileForkSizes(fileURL) else {
+ return nil
+ }
+
+ self.serverAddress = NWEndpoint.Host(address)
+ self.serverPort = NWEndpoint.Port(rawValue: port + 1)!
+ self.referenceNumber = reference
+ self.stage = .magic
+ self.payloadSize = UInt32(payloadSize)
+ self.fileURL = fileURL
+ self.infoForkData = infoFork.data()
+ self.dataForkSize = forkSizes.dataForkSize
+ self.resourceForkSize = forkSizes.resourceForkSize
+ if forkSizes.resourceForkSize > 0 {
+ self.fileResourceURL = fileURL.urlForResourceFork()
+ }
+ else {
+ self.fileResourceURL = nil
+ }
+ }
+
+ deinit {
+ self.invalidate()
+ }
+
+ func start() {
+ guard self.status == .unconnected else {
+ return
+ }
+
+ let _ = self.fileURL.startAccessingSecurityScopedResource()
+ let _ = self.fileResourceURL?.stopAccessingSecurityScopedResource()
+
+ self.bytesSent = 0
+ self.connect()
+ }
+
+ func cancel() {
+ self.delegate = nil
+
+ if self.status == .unconnected {
+ return
+ }
+
+ self.invalidate()
+
+ print("HotlineFileUploadClient: Cancelled upload")
+ }
+
+ private func connect() {
+ self.connection = NWConnection(host: self.serverAddress, port: self.serverPort, using: .tcp)
+ self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in
+ switch newState {
+ case .ready:
+ self?.status = .connected
+ self?.stage = .magic
+ self?.send()
+ case .waiting(let err):
+ print("HotlineFileClient: Waiting", err)
+ case .cancelled:
+ print("HotlineFileClient: Cancelled")
+ self?.invalidate()
+ case .failed(let err):
+ print("HotlineFileClient: Connection error \(err)")
+ switch self?.status {
+ case .connecting:
+ print("HotlineFileClient: Failed to connect to file transfer server.")
+ self?.invalidate()
+ self?.status = .failed(.failedToConnect)
+ case .connected, .progress(_):
+ print("HotlineFileClient: Failed to finish transfer.")
+ self?.invalidate()
+ self?.status = .failed(.failedToUpload)
+ default:
+ break
+ }
+ default:
+ return
+ }
+ }
+
+ self.status = .connecting
+ self.connection?.start(queue: .global())
+ }
+
+ private func invalidate() {
+ if let c = self.connection {
+ c.stateUpdateHandler = nil
+ c.cancel()
+
+ self.connection = nil
+ }
+
+ self.stage = .magic
+
+ if let fh = self.fileHandle {
+ try? fh.close()
+ self.fileHandle = nil
+ }
+
+ self.fileURL.stopAccessingSecurityScopedResource()
+ self.fileResourceURL?.stopAccessingSecurityScopedResource()
+ }
+
+ private func sendFileData(_ data: Data) {
+ guard let c = self.connection else {
+ self.invalidate()
+ print("HotlineFileUploadClient: invalid connection to send data.")
+ return
+ }
+
+ let dataSent: Int = data.count
+ c.send(content: data, completion: .contentProcessed({ [weak self] error in
+ guard let client = self,
+ error == nil else {
+ self?.status = .failed(.failedToConnect)
+ self?.invalidate()
+ return
+ }
+
+
+ client.bytesSent += dataSent
+ client.status = .progress(Double(client.bytesSent) / Double(client.payloadSize))
+
+ client.send()
+ }))
+ }
+
+ private func send() {
+ guard let _ = self.connection else {
+ self.invalidate()
+ print("HotlineFileUploadClient: Invalid connection to send.")
+ return
+ }
+
+ switch self.stage {
+ case .magic:
+ print("Upload: Starting upload for \(self.fileURL)")
+ print("Upload: Sending magic")
+ self.status = .progress(0.0)
+
+ var magicData = Data()
+ magicData.appendUInt32("HTXF".fourCharCode())
+ magicData.appendUInt32(self.referenceNumber)
+ magicData.appendUInt32(self.payloadSize)
+ magicData.appendUInt32(0)
+ self.stage = .fileHeader
+ self.sendFileData(magicData)
+
+ case .fileHeader:
+ print("Upload: Sending file header")
+ if let header = HotlineFileHeader(file: self.fileURL) {
+ self.stage = .fileInfoForkHeader
+ self.sendFileData(header.data())
+ }
+
+ case .fileInfoForkHeader:
+ print("Upload: Sending info fork header")
+ let header = HotlineFileForkHeader(type: "INFO".fourCharCode(), dataSize: UInt32(self.infoForkData.count))
+ self.stage = .fileInfoFork
+ self.sendFileData(header.data())
+
+ case .fileInfoFork:
+ print("Upload: Sending info fork")
+ self.stage = .fileDataForkHeader
+ self.sendFileData(self.infoForkData)
+
+ case .fileDataForkHeader:
+ guard self.dataForkSize > 0 else {
+ print("Upload: Data fork empty, skipping to resource fork")
+ self.stage = .fileResourceForkHeader
+ fallthrough
+ }
+
+ do {
+ let fh = try FileHandle(forReadingFrom: self.fileURL)
+ self.fileHandle = fh
+ self.stage = .fileDataFork
+
+ let header = HotlineFileForkHeader(type: "DATA".fourCharCode(), dataSize: self.dataForkSize)
+
+ print("Upload: Sending data fork header \(self.dataForkSize)")
+ self.sendFileData(header.data())
+ }
+ catch {
+ print("Upload: Error opening data fork", error)
+ self.invalidate()
+ return
+ }
+
+ case .fileDataFork:
+ guard self.dataForkSize > 0,
+ let fh = self.fileHandle else {
+ print("Upload: Data fork empty, skipping to resource fork")
+ self.stage = .fileResourceForkHeader
+ try? self.fileHandle?.close()
+ self.fileHandle = nil
+ fallthrough
+ }
+
+ do {
+ let fileData = try fh.read(upToCount: 4 * 1024)
+ if fileData == nil || fileData?.isEmpty == true {
+ print("Upload: Finished data fork")
+ self.stage = .fileResourceForkHeader
+ try? fh.close()
+ self.fileHandle = nil
+ fallthrough
+ }
+
+ print("Upload: Sending Data Fork \(String(describing: fileData?.count))")
+ self.sendFileData(fileData!)
+ }
+ catch {
+ self.invalidate()
+ print("Upload: Error reading data fork", error)
+ return
+ }
+
+ case .fileResourceForkHeader:
+ guard self.resourceForkSize > 0,
+ let resourceURL = self.fileResourceURL else {
+ print("Upload: Skipping resource fork header")
+ self.stage = .fileComplete
+ fallthrough
+ }
+
+ print("Upload: Sending resource fork header")
+ guard let fh = try? FileHandle(forReadingFrom: resourceURL) else {
+ print("Upload: Error reading resource fork")
+ self.invalidate()
+ return
+ }
+
+ let header = HotlineFileForkHeader(type: "MACR".fourCharCode(), dataSize: self.resourceForkSize)
+
+ self.fileHandle = fh
+ self.stage = .fileResourceFork
+ self.sendFileData(header.data())
+
+ case .fileResourceFork:
+ guard self.resourceForkSize > 0,
+ let fh = self.fileHandle else {
+ print("Upload: Resource fork empty, skipping to completion")
+ self.stage = .fileComplete
+ try? self.fileHandle?.close()
+ self.fileHandle = nil
+ fallthrough
+ }
+
+ do {
+ let resourceData = try fh.read(upToCount: 4 * 1024)
+ if resourceData == nil || resourceData?.isEmpty == true {
+ print("Upload: Finished resource fork")
+ self.stage = .fileComplete
+ try? self.fileHandle?.close()
+ self.fileHandle = nil
+ fallthrough
+ }
+
+ print("Upload: Sending resource fork \(String(describing: resourceData?.count))")
+ self.sendFileData(resourceData!)
+ }
+ catch {
+ self.invalidate()
+ print("Upload: Error reading resource fork", error)
+ return
+ }
+ break
+
+ case .fileComplete:
+ print("Upload: Complete!")
+ self.status = .completed
+ self.invalidate()
+
+ DispatchQueue.main.sync {
+ self.delegate?.hotlineFileUploadComplete(client: self, reference: self.referenceNumber)
+ }
+ }
+ }
+}
+
+// MARK: -
+
+class HotlineFilePreviewClient: HotlineTransferClient {
+ let serverAddress: NWEndpoint.Host
+ let serverPort: NWEndpoint.Port
+ let referenceNumber: UInt32
+ let referenceDataSize: UInt32
+
+ weak var delegate: HotlineFilePreviewClientDelegate? = nil
+
+ var status: HotlineTransferStatus = .unconnected {
+ didSet {
+ DispatchQueue.main.async {
+ self.delegate?.hotlineTransferStatusChanged(client: self, reference: self.referenceNumber, status: self.status, timeRemaining: 0.0)
+ }
+ }
+ }
+
+ private var connection: NWConnection?
+ private var transferStage: HotlineFileTransferStage = .fileHeader
+ private var fileBytes = Data()
+ private var fileBytesTransferred: Int = 0
+
+ init(address: String, port: UInt16, reference: UInt32, size: UInt32) {
+ self.serverAddress = NWEndpoint.Host(address)
+ self.serverPort = NWEndpoint.Port(rawValue: port + 1)!
+ self.referenceNumber = reference
+ self.referenceDataSize = size
+ }
+
+ deinit {
+ self.invalidate()
+ }
+
+ func start() {
+ guard self.status == .unconnected else {
+ return
+ }
+
+ self.connect()
+ }
+
+ func cancel() {
+ self.delegate = nil
+
+ if self.status == .unconnected {
+ return
+ }
+
+ self.invalidate()
+
+ print("HotlineFilePreviewClient: Cancelled preview transfer")
+ }
+
+ private func invalidate() {
+ if let c = self.connection {
+ c.stateUpdateHandler = nil
+ c.cancel()
+
+ self.connection = nil
+ }
+
+ self.fileBytes = Data()
+ }
+
+ private func connect() {
+ self.connection = NWConnection(host: self.serverAddress, port: self.serverPort, using: .tcp)
+ self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in
+ switch newState {
+ case .ready:
+ self?.status = .connected
+ self?.sendMagic()
+ case .waiting(let err):
+ print("HotlineFilePreviewClient: Waiting", err)
+ case .cancelled:
+ print("HotlineFilePreviewClient: Cancelled")
+ self?.invalidate()
+ case .failed(let err):
+ print("HotlineFilePreviewClient: Connection error \(err)")
+ switch self?.status {
+ case .connecting:
+ print("HotlineFilePreviewClient: Failed to connect to file transfer server.")
+ self?.invalidate()
+ self?.status = .failed(.failedToConnect)
+ case .connected, .progress(_):
+ print("HotlineFilePreviewClient: Failed to finish transfer.")
+ self?.invalidate()
+ self?.status = .failed(.failedToDownload)
+ default:
+ break
+ }
+ default:
+ return
+ }
+ }
+
+ self.status = .connecting
+ self.connection?.start(queue: .global())
+ }
+
+ private func sendMagic() {
+ guard let c = connection, self.status == .connected else {
+ self.invalidate()
+ print("HotlineFileClient: invalid connection to send header.")
+ return
+ }
+
+ var headerData = Data()
+ headerData.appendUInt32("HTXF".fourCharCode())
+ headerData.appendUInt32(self.referenceNumber)
+ headerData.appendUInt32(0)
+ headerData.appendUInt32(0)
+
+ c.send(content: headerData, completion: .contentProcessed { [weak self] (error) in
+ guard let self = self else {
+ return
+ }
+
+ guard error == nil else {
+ self.status = .failed(.failedToConnect)
+ self.invalidate()
+ return
+ }
+
+ self.status = .progress(0.0)
+ self.receive()
+ })
+ }
+
+ private func receive() {
+ guard let c = self.connection else {
+ return
+ }
+
+ c.receive(minimumIncompleteLength: 1, maximumLength: Int(UInt16.max)) { [weak self] (data, context, isComplete, error) in
+ guard let self = self else {
+ return
+ }
+
+ guard error == nil else {
+ self.status = .failed(.failedToDownload)
+ self.invalidate()
+ return
+ }
+
+ if let newData = data, !newData.isEmpty {
+ self.fileBytesTransferred += newData.count
+ self.fileBytes.append(newData)
+ self.status = .progress(Double(self.fileBytesTransferred) / Double(self.referenceDataSize))
+ print("HotlineFilePreviewClient: Download progress", self.fileBytesTransferred, self.referenceDataSize, isComplete)
+ }
+
+ if self.fileBytesTransferred < Int(self.referenceDataSize) {
+ self.receive()
+ }
+ else {
+ print("HotlineFilePreviewClient: Complete")
+ let data = self.fileBytes
+
+ self.status = .completed
+ self.invalidate()
+
+ let reference = self.referenceNumber
+ DispatchQueue.main.sync {
+ self.delegate?.hotlineFilePreviewComplete(client: self, reference: reference, data: data)
+ }
+ }
+ }
+ }
+}
+
+// MARK: -
+
+class HotlineFileDownloadClient: HotlineTransferClient {
+ let serverAddress: NWEndpoint.Host
+ let serverPort: NWEndpoint.Port
+ let referenceNumber: UInt32
+
+ private var connection: NWConnection?
+ private var transferStage: HotlineFileTransferStage = .fileHeader
+
+ weak var delegate: HotlineFileDownloadClientDelegate? = nil
+
+ var status: HotlineTransferStatus = .unconnected {
+ didSet {
+ DispatchQueue.main.async {
+ self.delegate?.hotlineTransferStatusChanged(client: self, reference: self.referenceNumber, status: self.status, timeRemaining: 0.0)
+ }
+ }
+ }
+
+ private let referenceDataSize: UInt32
+ private var fileBytes = Data()
+ private var fileResourceBytes = Data()
+
+ private var fileHeader: HotlineFileHeader? = nil
+ private var fileCurrentForkHeader: HotlineFileForkHeader? = nil
+ private var fileCurrentForkBytesLeft: Int = 0
+ private var fileInfoFork: HotlineFileInfoFork? = nil
+ private var fileHandle: FileHandle? = nil
+ private var filePath: String? = nil
+ private var fileBytesTransferred: Int = 0
+ private var fileProgress: Progress
+
+ init(address: String, port: UInt16, reference: UInt32, size: UInt32) {
+ self.serverAddress = NWEndpoint.Host(address)
+ self.serverPort = NWEndpoint.Port(rawValue: port + 1)!
+ self.referenceNumber = reference
+ self.referenceDataSize = size
+ self.transferStage = .fileHeader
+ self.fileProgress = Progress(totalUnitCount: Int64(size))
+ }
+
+ deinit {
+ self.invalidate()
+ }
+
+ func start() {
+ guard self.status == .unconnected else {
+ return
+ }
+
+ self.filePath = nil
+ self.connect()
+ }
+
+ func start(to fileURL: URL) {
+ guard self.status == .unconnected else {
+ return
+ }
+
+ self.filePath = fileURL.path
+ self.connect()
+ }
+
+ func cancel() {
+ self.delegate = nil
+
+ if self.status == .unconnected {
+ return
+ }
+
+ // Close file before we try to potentionally delete it.
+ if let fh = self.fileHandle {
+ try? fh.close()
+ self.fileHandle = nil
+ }
+
+ if let downloadPath = self.filePath {
+ print("HotlineFileClient: Deleting file fragment at", downloadPath)
+ if FileManager.default.isDeletableFile(atPath: downloadPath) {
+ try? FileManager.default.removeItem(atPath: downloadPath)
+ }
+ self.filePath = nil
+ }
+
+ self.invalidate()
+
+ print("HotlineFileClient: Cancelled transfer")
+ }
+
+ private func connect() {
+ self.connection = NWConnection(host: self.serverAddress, port: self.serverPort, using: .tcp)
+ self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in
+ switch newState {
+ case .ready:
+ self?.status = .connected
+ self?.sendMagic()
+ case .waiting(let err):
+ print("HotlineFileClient: Waiting", err)
+ case .cancelled:
+ print("HotlineFileClient: Cancelled")
+ self?.invalidate()
+ case .failed(let err):
+ print("HotlineFileClient: Connection error \(err)")
+ switch self?.status {
+ case .connecting:
+ print("HotlineFileClient: Failed to connect to file transfer server.")
+ self?.invalidate()
+ self?.status = .failed(.failedToConnect)
+ case .connected, .progress(_):
+ print("HotlineFileClient: Failed to finish transfer.")
+ self?.invalidate()
+ self?.status = .failed(.failedToDownload)
+ default:
+ break
+ }
+ default:
+ return
+ }
+ }
+
+ self.status = .connecting
+ self.connection?.start(queue: .global())
+ }
+
+ func invalidate() {
+ if let c = self.connection {
+ c.stateUpdateHandler = nil
+ c.cancel()
+
+ self.connection = nil
+ }
+
+ self.fileBytes = Data()
+
+ if let fh = self.fileHandle {
+ try? fh.close()
+ self.fileHandle = nil
+ }
+
+ self.fileProgress.unpublish()
+ }
+
+ private func sendMagic() {
+ guard let c = connection, self.status == .connected else {
+ self.invalidate()
+ print("HotlineFileClient: invalid connection to send header.")
+ return
+ }
+
+ var headerData = Data()
+ headerData.appendUInt32("HTXF".fourCharCode())
+ headerData.appendUInt32(self.referenceNumber)
+ headerData.appendUInt32(0)
+ headerData.appendUInt32(0)
+
+ c.send(content: headerData, completion: .contentProcessed { [weak self] (error) in
+ guard let self = self else {
+ return
+ }
+
+ guard error == nil else {
+ self.status = .failed(.failedToConnect)
+ self.invalidate()
+ return
+ }
+
+ self.status = .progress(0.0)
+ self.receiveFile()
+ })
+ }
+
+ private func receiveFile() {
+ guard let c = self.connection else {
+ return
+ }
+
+ c.receive(minimumIncompleteLength: 1, maximumLength: Int(UInt16.max)) { [weak self] (data, context, isComplete, error) in
+ guard let self = self else {
+ return
+ }
+
+ guard error == nil else {
+ self.status = .failed(.failedToDownload)
+ self.invalidate()
+ return
+ }
+
+ if let newData = data, !newData.isEmpty {
+ self.fileBytesTransferred += newData.count
+ self.fileBytes.append(newData)
+ self.fileProgress.completedUnitCount = Int64(self.fileBytesTransferred)
+ self.status = .progress(Double(self.fileBytesTransferred) / Double(self.referenceDataSize))
+ }
+
+ // See if we need header data still.
+ var keepProcessing = false
+ repeat {
+ keepProcessing = false
+
+ switch self.transferStage {
+ case .fileHeader:
+ if let header = HotlineFileHeader(from: self.fileBytes) {
+ self.fileBytes.removeSubrange(0..<HotlineFileHeader.DataSize)
+ self.fileHeader = header
+ self.transferStage = .fileForkHeader
+ keepProcessing = true
+ }
+ case .fileForkHeader:
+ if let forkHeader = HotlineFileForkHeader(from: self.fileBytes) {
+// let fileForkHeader = HotlineFileForkHeader(from: self.fileBytes)
+ self.fileBytes.removeSubrange(0..<HotlineFileForkHeader.DataSize)
+ self.fileCurrentForkHeader = forkHeader
+ self.fileCurrentForkBytesLeft = Int(forkHeader.dataSize)
+
+ if forkHeader.isInfoFork {
+ print("HotlineFileClient: Downloading info fork")
+ self.transferStage = .fileInfoFork
+ }
+ else if forkHeader.isDataFork {
+ print("HotlineFileClient: Downloading data fork")
+ self.transferStage = .fileDataFork
+ }
+ else if forkHeader.isResourceFork {
+ print("HotlineFileClient: Downloading resource fork")
+ self.fileResourceBytes = Data()
+ self.transferStage = .fileResourceFork
+ }
+ else {
+ print("HotlineFileClient: Downloading unsupported fork")
+ self.transferStage = .fileUnsupportedFork
+ }
+
+ keepProcessing = true
+ }
+ case .fileInfoFork:
+ if let infoForkDataSize = self.fileCurrentForkHeader?.dataSize, self.fileBytes.count >= infoForkDataSize {
+ let infoForkData = self.fileBytes.subdata(in: 0..<Int(infoForkDataSize))
+ if let infoFork = HotlineFileInfoFork(from: infoForkData) {
+ self.fileInfoFork = infoFork
+
+ self.fileBytes.removeSubrange(0..<infoFork.headerSize)
+
+ self.transferStage = .fileForkHeader
+ self.fileCurrentForkBytesLeft -= Int(infoForkDataSize)
+ self.fileCurrentForkHeader = nil
+
+ print("INFO FORK STUFF:", infoFork)
+
+ // Create file in Downloads folder if we don't have a destination already.
+ if !self.prepareDownloadFile(name: infoFork.name) {
+ print("FAILED TO CREATE FILE ON DISK")
+ }
+
+ let reference = self.referenceNumber
+ DispatchQueue.main.async {
+ self.delegate?.hotlineFileDownloadReceivedInfo(client: self, reference: reference, info: infoFork)
+ }
+ }
+
+ keepProcessing = true
+ }
+ case .fileDataFork:
+ if self.fileBytes.count > 0 {
+ if let f = self.fileHandle {
+ do {
+ var dataToWrite = self.fileBytes
+
+ if dataToWrite.count >= self.fileCurrentForkBytesLeft {
+ dataToWrite = self.fileBytes.subdata(in: 0..<self.fileCurrentForkBytesLeft)
+ self.fileBytes.removeSubrange(0..<self.fileCurrentForkBytesLeft)
+
+ self.transferStage = .fileForkHeader
+ self.fileCurrentForkBytesLeft = 0
+ self.fileCurrentForkHeader = nil
+
+ keepProcessing = true
+ }
+ else {
+ self.fileCurrentForkBytesLeft -= dataToWrite.count
+ self.fileBytes = Data()
+ }
+
+ try f.write(contentsOf: dataToWrite)
+ }
+ catch {
+ print("DOWNLOAD WRITE ERROR", error)
+ }
+ }
+ }
+ case .fileResourceFork:
+ if self.fileBytes.count > 0 {
+// if let f = self.fileResourceHandle {
+// do {
+ var dataToWrite = self.fileBytes
+
+ if dataToWrite.count >= self.fileCurrentForkBytesLeft {
+ dataToWrite = self.fileBytes.subdata(in: 0..<self.fileCurrentForkBytesLeft)
+ self.fileBytes.removeSubrange(0..<self.fileCurrentForkBytesLeft)
+
+ self.transferStage = .fileForkHeader
+ self.fileCurrentForkBytesLeft = 0
+ self.fileCurrentForkHeader = nil
+
+ keepProcessing = true
+ }
+ else {
+ self.fileCurrentForkBytesLeft -= dataToWrite.count
+ self.fileBytes = Data()
+ }
+
+ print("WRITING TO RESOURCE FORK", dataToWrite.count)
+
+ self.fileResourceBytes.append(dataToWrite)
+// try f.write(contentsOf: dataToWrite)
+// }
+// catch {
+// print("DOWNLOAD WRITE ERROR", error)
+// }
+// }
+ }
+ case .fileUnsupportedFork:
+ if self.fileBytes.count > 0 {
+ var dataToWrite = self.fileBytes
+
+ if dataToWrite.count >= self.fileCurrentForkBytesLeft {
+ dataToWrite = self.fileBytes.subdata(in: 0..<self.fileCurrentForkBytesLeft)
+ self.fileBytes.removeSubrange(0..<self.fileCurrentForkBytesLeft)
+
+ self.transferStage = .fileForkHeader
+ self.fileCurrentForkBytesLeft = 0
+ self.fileCurrentForkHeader = nil
+
+ keepProcessing = true
+ }
+ else {
+ self.fileCurrentForkBytesLeft -= dataToWrite.count
+ self.fileBytes = Data()
+ }
+ }
+ }
+ } while keepProcessing
+
+ if self.fileBytesTransferred < Int(self.referenceDataSize) {
+ // If we still have more to download, then receive more data.
+ self.receiveFile()
+ return
+ }
+ else {
+ self.invalidate()
+
+ if self.fileResourceBytes.count > 0 {
+ let _ = self.writeResourceFork()
+ self.fileResourceBytes = Data()
+ }
+
+ self.status = .completed
+
+ if let downloadPath = self.filePath {
+ DispatchQueue.main.sync {
+ print("POSTING DOWNLOAD FILE FINISHED", downloadPath)
+
+ var downloadURL = URL(filePath: downloadPath)
+ downloadURL.resolveSymlinksInPath()
+ print("FINAL PATH", downloadURL.path)
+
+ self.delegate?.hotlineFileDownloadComplete(client: self, reference: self.referenceNumber, at: downloadURL)
+
+ #if os(macOS)
+ // Bounce dock icon when download completes. Weird this is the only API to do so.
+ DistributedNotificationCenter.default().post(name: .init("com.apple.DownloadFileFinished"), object: downloadURL.path)
+ #endif
+ }
+ }
+ }
+ }
+ }
+
+ private func writeResourceFork() -> Bool {
+ guard let filePath = self.filePath else {
+ return false
+ }
+
+ var resolvedFileURL = URL(filePath: filePath)
+ resolvedFileURL.resolveSymlinksInPath()
+
+ let resourceFilePath = resolvedFileURL.appendingPathComponent("..namedfork/rsrc")
+
+ do {
+ try self.fileResourceBytes.write(to: resourceFilePath)
+ }
+ catch {
+ return false
+ }
+
+ return true
+ }
+
+ private func prepareDownloadFile(name: String) -> Bool {
+ var filePath: String
+
+ if self.filePath != nil {
+ filePath = self.filePath!
+ }
+ else {
+ let folderURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
+ filePath = folderURL.generateUniqueFilePath(filename: name)
+ }
+
+ var fileAttributes: [FileAttributeKey : Any] = [:]
+ if let creatorCode = self.fileInfoFork?.creator {
+ fileAttributes[.hfsCreatorCode] = creatorCode as NSNumber
+ }
+ if let typeCode = self.fileInfoFork?.type {
+ fileAttributes[.hfsTypeCode] = typeCode as NSNumber
+ }
+ if let createdDate = self.fileInfoFork?.createdDate {
+ fileAttributes[.creationDate] = createdDate as NSDate
+ }
+ if let modifiedDate = self.fileInfoFork?.modifiedDate {
+ fileAttributes[.modificationDate] = modifiedDate as NSDate
+ }
+
+ if FileManager.default.createFile(atPath: filePath, contents: nil, attributes: fileAttributes) {
+ if let h = FileHandle(forWritingAtPath: filePath) {
+ self.filePath = filePath
+ self.fileHandle = h
+ self.fileProgress.fileURL = URL(filePath: filePath).resolvingSymlinksInPath()
+ self.fileProgress.fileOperationKind = .downloading
+ self.fileProgress.publish()
+ return true
+ }
+ }
+
+ return false
+ }
+}
+
+// MARK: -
+
+struct HotlineFileHeader {
+ static let DataSize: Int = 4 + 2 + 16 + 2
+
+ let format: UInt32
+ let version: UInt16
+ let forkCount: UInt16
+
+ init?(from data: Data) {
+ guard data.count >= HotlineFileHeader.DataSize else {
+ return nil
+ }
+
+ self.format = data.readUInt32(at: 0)!
+ self.version = data.readUInt16(at: 4)!
+ // 16 bytes of reserved data sits here. Skip it.
+ // self.reserved = data.readData(at: 4 + 2, length: 16)
+ self.forkCount = data.readUInt16(at: 4 + 2 + 16)!
+ }
+
+ init?(file fileURL: URL) {
+ guard FileManager.default.fileExists(atPath: fileURL.path(percentEncoded: false)) else {
+ return nil
+ }
+
+ self.format = "FILP".fourCharCode()
+ self.version = 1
+
+ let resourceURL = fileURL.appendingPathComponent("..namedfork/rsrc")
+ if FileManager.default.fileExists(atPath: resourceURL.path(percentEncoded: false)) {
+ self.forkCount = 2
+ }
+ else {
+ self.forkCount = 1
+ }
+ }
+
+ func data() -> Data {
+ var d = Data()
+
+ d.appendUInt32(self.format)
+ d.appendUInt16(self.version)
+ d.appendZeros(count: 16)
+ d.appendUInt16(self.forkCount)
+
+ return d
+ }
+}
+
+// MARK: -
+
+struct HotlineFileForkHeader {
+ static let DataSize: Int = 4 + 4 + 4 + 4
+
+ let forkType: UInt32
+ let compressionType: UInt32
+ let dataSize: UInt32
+
+ init(type: UInt32, dataSize: UInt32) {
+ self.forkType = type
+ self.compressionType = 0
+ self.dataSize = dataSize
+ }
+
+ init?(from data: Data) {
+ guard data.count >= HotlineFileForkHeader.DataSize else {
+ return nil
+ }
+
+ self.forkType = data.readUInt32(at: 0)!
+ self.compressionType = data.readUInt32(at: 4)!
+ // 4 bytes of reserved data sits here. Skip it.
+ // self.reserved = data.readUInt32(at: 4 + 4)!
+ self.dataSize = data.readUInt32(at: 4 + 4 + 4)!
+ }
+
+ func data() -> Data {
+ var d = Data()
+
+ d.appendUInt32(self.forkType)
+ d.appendUInt32(self.compressionType)
+ d.appendUInt32(0)
+ d.appendUInt32(self.dataSize)
+
+ return d
+ }
+
+ var isInfoFork: Bool {
+ return self.forkType == "INFO".fourCharCode()
+ }
+
+ var isDataFork: Bool {
+ return self.forkType == "DATA".fourCharCode()
+ }
+
+ var isResourceFork: Bool {
+ return self.forkType == "MACR".fourCharCode()
+ }
+}
+
+// MARK: -
+
+struct HotlineFileInfoFork {
+ static let BaseDataSize: Int = 4 + 4 + 4 + 4 + 4 + 32 + 8 + 8 + 2 + 2
+
+ let platform: UInt32
+ let type: UInt32
+ let creator: UInt32
+ let flags: UInt32
+ let platformFlags: UInt32
+ let createdDate: Date
+ let modifiedDate: Date
+ let nameScript: UInt16
+ let name: String
+ let comment: String?
+ var headerSize: Int
+
+ init?(file fileURL: URL) {
+ guard FileManager.default.fileExists(atPath: fileURL.path(percentEncoded: false)) else {
+ return nil
+ }
+
+ self.platform = "AMAC".fourCharCode()
+
+ if let hfsInfo = try? FileManager.default.getHFSTypeAndCreator(fileURL) {
+ self.type = hfsInfo.hfsType
+ self.creator = hfsInfo.hfsCreator
+ }
+ else {
+ self.type = 0
+ self.creator = 0
+ }
+
+ self.flags = 0
+ self.platformFlags = 0
+
+ let dateInfo = FileManager.default.getCreatedAndModifiedDates(fileURL)
+ self.createdDate = dateInfo.createdDate
+ self.modifiedDate = dateInfo.modifiedDate
+
+ self.nameScript = 0
+ self.name = fileURL.lastPathComponent
+
+ self.comment = nil
+
+ self.headerSize = 0
+ }
+
+ init?(from data: Data) {
+ // Make sure we have at least enough data to read basic header data
+ guard data.count >= HotlineFileInfoFork.BaseDataSize else {
+ return nil
+ }
+
+ if
+ let platform = data.readUInt32(at: 0),
+ let type = data.readUInt32(at: 4),
+ let creator = data.readUInt32(at: 4 + 4),
+ let flags = data.readUInt32(at: 4 + 4 + 4),
+ let platformFlags = data.readUInt32(at: 4 + 4 + 4 + 4),
+ // 32 bytes of reserved data sits here. Skip it.
+ let nameScript = data.readUInt16(at: 4 + 4 + 4 + 4 + 4 + 32 + 8 + 8) {
+
+ let createdDate = data.readDate(at: 4 + 4 + 4 + 4 + 4 + 32) ?? Date.now
+ let modifiedDate = data.readDate(at: 4 + 4 + 4 + 4 + 4 + 32 + 8) ?? Date.now
+
+ let (n, nl) = data.readLongPString(at: 4 + 4 + 4 + 4 + 4 + 32 + 8 + 8 + 2)
+ if let name = n {
+ self.platform = platform
+ self.type = type
+ self.creator = creator
+ self.flags = flags
+ self.platformFlags = platformFlags
+ self.createdDate = createdDate
+ self.modifiedDate = modifiedDate
+ self.nameScript = nameScript
+ self.name = name
+
+ var calculatedHeaderSize: Int = HotlineFileInfoFork.BaseDataSize + nl
+ var commentRead: String? = nil
+ if data.count >= HotlineFileInfoFork.BaseDataSize + nl + 2 {
+ let commentLength = data.readUInt16(at: HotlineFileInfoFork.BaseDataSize + nl)!
+ var commentCorrupted = false
+
+ // Some servers have incorrect data length for the INFO fork
+ // the length they send is what it should be but don't include
+ // the comment length in the actual data, so we end up with mismatched
+ // lengths. So here we test if the length we read is actually 'DA'
+ // or the first part of the "DATA" fork header.
+ // Needless to say, stuff like this makes for sad code but this ain't so bad.
+ if commentLength == 0x4441 {
+ commentCorrupted = true
+ }
+
+ if !commentCorrupted {
+ let (c, cl) = data.readLongPString(at: HotlineFileInfoFork.BaseDataSize + nl)
+ calculatedHeaderSize += 2
+ if cl > 0 {
+ calculatedHeaderSize += Int(cl)
+ if let ct = c, cl > 0 {
+ commentRead = ct
+ }
+ }
+ }
+ }
+
+ self.comment = commentRead
+ self.headerSize = calculatedHeaderSize
+ return
+ }
+ }
+
+ return nil
+ }
+
+ func data() -> Data {
+ var d = Data()
+
+ d.appendUInt32(self.platform)
+ d.appendUInt32(self.type)
+ d.appendUInt32(self.creator)
+ d.appendUInt32(self.flags)
+ d.appendUInt32(self.platformFlags)
+ d.appendZeros(count: 32)
+ d.appendDate(self.createdDate)
+ d.appendDate(self.modifiedDate)
+ d.appendUInt16(self.nameScript)
+
+ d.appendUInt16(UInt16(self.name.count))
+ d.appendString(self.name, encoding: .macOSRoman)
+
+ return d
+ }
+}
diff --git a/Hotline/Models/FilePreview.swift b/Hotline/Models/FilePreview.swift
index d558c4c..3960a0d 100644
--- a/Hotline/Models/FilePreview.swift
+++ b/Hotline/Models/FilePreview.swift
@@ -15,9 +15,9 @@ enum FilePreviewType: Equatable {
}
@Observable
-final class FilePreview: HotlineFileClientDelegate {
+final class FilePreview: HotlineFilePreviewClientDelegate {
@ObservationIgnored let info: PreviewFileInfo
- @ObservationIgnored var client: HotlineFileClient? = nil
+ @ObservationIgnored var client: HotlineFilePreviewClient? = nil
var state: FilePreviewState = .unloaded
var progress: Double = 0.0
@@ -49,20 +49,20 @@ final class FilePreview: HotlineFileClientDelegate {
init(info: PreviewFileInfo) {
self.info = info
- self.client = HotlineFileClient(address: info.address, port: UInt16(info.port), reference: info.id, size: UInt32(info.size), type: .preview)
+ self.client = HotlineFilePreviewClient(address: info.address, port: UInt16(info.port), reference: info.id, size: UInt32(info.size))
self.client?.delegate = self
}
func download() {
- self.client?.downloadToMemory()
+ self.client?.start()
}
func cancel() {
- self.client?.cancel(deleteIncompleteFile: true)
+ self.client?.cancel()
}
- func hotlineFileStatusChanged(client: HotlineFileClient, reference: UInt32, status: HotlineFileClientStatus, timeRemaining: TimeInterval) {
- print("FILE STATUS CHANGED", status)
+ func hotlineTransferStatusChanged(client: any HotlineTransferClient, reference: UInt32, status: HotlineTransferStatus, timeRemaining: TimeInterval) {
+ print("FilePreview: Download status changed:", status)
switch status {
case .unconnected:
@@ -86,7 +86,7 @@ final class FilePreview: HotlineFileClientDelegate {
}
}
- func hotlineFileDownloadedData(client: HotlineFileClient, reference: UInt32, data: Data) {
+ func hotlineFilePreviewComplete(client: HotlineFilePreviewClient, reference: UInt32, data: Data) {
self.state = .loaded
self.data = data
diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift
index c57434a..69a11d3 100644
--- a/Hotline/Models/Hotline.swift
+++ b/Hotline/Models/Hotline.swift
@@ -1,7 +1,7 @@
import SwiftUI
@Observable
-class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
+class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelegate, HotlineFilePreviewClientDelegate, HotlineFileUploadClientDelegate {
let id: UUID = UUID()
let trackerClient: HotlineTrackerClient
let client: HotlineClient
@@ -137,11 +137,11 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
var accountsLoaded: Bool = false
var instantMessages: [UInt16:[InstantMessage]] = [:]
var transfers: [TransferInfo] = []
- var downloads: [HotlineFileClient] = []
+ var downloads: [HotlineTransferClient] = []
var unreadInstantMessages: [UInt16:UInt16] = [:]
var unreadPublicChat: Bool = false
- @ObservationIgnored var bannerClient: HotlineFileClient?
+ @ObservationIgnored var bannerClient: HotlineFilePreviewClient?
#if os(macOS)
var bannerImage: NSImage? = nil
#elseif os(iOS)
@@ -502,13 +502,15 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
if
let self = self,
+// let server = self.server,
let address = self.server?.address,
let port = self.server?.port,
let referenceNumber = downloadReferenceNumber,
let transferSize = downloadTransferSize {
- print("DOWNLOADING TO MEMORY")
- let fileClient = HotlineFileClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize), type: .file)
+ let fileClient = HotlineFileDownloadClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize))
+// let previewClient = HotlineFilePreviewClient(server: self.server, reference: referenceNumber, size: UInt32(transferSize), type: .fileDownload)
+// let fileClient = HotlineFileClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize), type: .fileDownload)
fileClient.delegate = self
self.downloads.append(fileClient)
@@ -516,7 +518,7 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
transfer.downloadCallback = callback
self.transfers.append(transfer)
- fileClient.downloadToFile()
+ fileClient.start()
}
}
}
@@ -542,8 +544,7 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
let referenceNumber = downloadReferenceNumber,
let transferSize = downloadTransferSize {
- print("DOWNLOADING TO MEMORY")
- let fileClient = HotlineFileClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize), type: .file)
+ let fileClient = HotlineFileDownloadClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize))
fileClient.delegate = self
self.downloads.append(fileClient)
@@ -552,10 +553,80 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
transfer.progressCallback = progressCallback
self.transfers.append(transfer)
- fileClient.downloadToFile(to: fileURL)
+ fileClient.start(to: fileURL)
}
}
}
+
+ @MainActor func uploadFile(url fileURL: URL, path: [String], complete callback: ((TransferInfo) -> Void)? = nil) {
+ let fileName = fileURL.lastPathComponent
+
+ guard fileURL.isFileURL, !fileName.isEmpty else {
+ print("NOT A FILE URL?")
+ return
+ }
+
+ let filePath = fileURL.path(percentEncoded: false)
+
+ var fileIsDirectory: ObjCBool = false
+ guard FileManager.default.fileExists(atPath: filePath, isDirectory: &fileIsDirectory),
+ fileIsDirectory.boolValue == false else {
+ print("FILE IS A DIRECTORY?")
+ return
+ }
+
+ var fileSize: UInt = 0
+
+ // Data size
+ let fileAttributes = try? FileManager.default.attributesOfItem(atPath: filePath)
+ if let sizeAttribute = fileAttributes?[.size] as? NSNumber {
+ print("DATA SIZE \(sizeAttribute.uintValue)")
+ fileSize += sizeAttribute.uintValue
+ }
+
+ // Resource size
+ let resourceURL = fileURL.appendingPathComponent("..namedfork/rsrc")
+
+
+ print("RESOURCE PATH \(resourceURL)")
+ let resourceAttributes = try? FileManager.default.attributesOfItem(atPath: resourceURL.path(percentEncoded: false))
+ if let sizeAttribute = resourceAttributes?[.size] as? NSNumber {
+ print("RESOURCE SIZE \(sizeAttribute.uintValue)")
+ fileSize += sizeAttribute.uintValue
+ }
+
+ print("FILE SIZE? \(fileSize)")
+
+ guard fileSize > 0 else {
+ print("FILE IS EMPTY??")
+ return
+ }
+
+ print("FILE SIZE: \(fileSize) NAME: \(fileName) PATH: \(path)")
+
+ self.client.sendUploadFile(name: fileName, path: path) { [weak self] success, uploadReferenceNumber in
+ print("UPLOAD REFERENCE: \(String(describing: uploadReferenceNumber))")
+
+ if let self = self,
+ let address = self.server?.address,
+ let port = self.server?.port,
+ let referenceNumber = uploadReferenceNumber,
+ let fileClient = HotlineFileUploadClient(upload: fileURL, address: address, port: UInt16(port), reference: referenceNumber) {
+
+ print("GOING TO UPLOAD")
+
+ fileClient.delegate = self
+ self.downloads.append(fileClient)
+
+ let transfer = TransferInfo(id: referenceNumber, title: fileName, size: fileSize)
+ transfer.uploadCallback = callback
+ self.transfers.append(transfer)
+
+ fileClient.start()
+ }
+ }
+
+ }
@MainActor func getFileDetails(_ fileName: String, path: [String]) async -> FileDetails? {
var fullPath: [String] = []
@@ -611,27 +682,6 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
let transferSize = downloadTransferSize {
info = PreviewFileInfo(id: referenceNumber, address: address, port: port, size: transferSize, name: fileName)
-
-// let fileClient = HotlineFileClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize), type: .preview)
-// fileClient.delegate = self
-// self.downloads.append(fileClient)
-
-// if addTransfer {
-// let transfer = TransferInfo(id: referenceNumber, title: fileName, size: UInt(transferSize))
-// transfer.previewCallback = callback
-// self.transfers.append(transfer)
-// }
-
-// fileClient.downloadToMemory()
-
- print("DOWNLOADING TO MEMORY")
-// fileClient.downloadToMemory({ [weak self] fileData in
-// print("DOWNLOADED PREVIEW DATA", fileData?.count)
-// self?.downloads.removeAll { $0.referenceNumber == referenceNumber }
-// callback?(fileData != nil, fileData)
-// })
-
-// self.downloads.append(fileClient)
}
callback?(info)
@@ -651,7 +701,7 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
if let i = self.downloads.firstIndex(where: { $0.referenceNumber == id }) {
let fileClient = self.downloads.remove(at: i)
- fileClient.cancel(deleteIncompleteFile: true)
+ fileClient.cancel()
}
}
@@ -662,7 +712,7 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
self.downloads = []
for fileClient in downloads {
- fileClient.cancel(deleteIncompleteFile: true)
+ fileClient.cancel()
}
}
@@ -696,9 +746,9 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
let port = self.server?.port,
let referenceNumber = downloadReferenceNumber,
let transferSize = downloadTransferSize {
- self.bannerClient = HotlineFileClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize), type: .preview)
+ self.bannerClient = HotlineFilePreviewClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize))
self.bannerClient?.delegate = self
- self.bannerClient?.downloadToMemory()
+ self.bannerClient?.start()
}
}
}
@@ -856,9 +906,9 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
}
- // MARK: - Hotline File Delegate
+ // MARK: - Hotline Transfer Delegate
- func hotlineFileStatusChanged(client: HotlineFileClient, reference: UInt32, status: HotlineFileClientStatus, timeRemaining: TimeInterval) {
+ func hotlineTransferStatusChanged(client: HotlineTransferClient, reference: UInt32, status: HotlineTransferStatus, timeRemaining: TimeInterval) {
switch status {
case .unconnected:
break
@@ -880,7 +930,7 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
transfer.failed = true
transfer.timeRemaining = 0.0
}
- if let b = self.bannerClient, reference == b.referenceNumber {
+ if let b = self.bannerClient, b.referenceNumber == reference {
b.delegate = nil
self.bannerClient = nil
}
@@ -893,13 +943,55 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
}
}
- func hotlineFileReceivedInfo(client: HotlineFileClient, reference: UInt32, info: HotlineFileInfoFork) {
+ func hotlineFileDownloadReceivedInfo(client: HotlineFileDownloadClient, reference: UInt32, info: HotlineFileInfoFork) {
if let transfer = self.transfers.first(where: { $0.id == reference }) {
transfer.title = info.name
}
}
- func hotlineFileDownloadedData(client: HotlineFileClient, reference: UInt32, data: Data) {
+// func hotlineFileReceivedInfo(client: HotlineFileClient, reference: UInt32, info: HotlineFileInfoFork) {
+// if let transfer = self.transfers.first(where: { $0.id == reference }) {
+// transfer.title = info.name
+// }
+// }
+
+// func hotlineTransferStatusChanged(client: HotlineTransferClient, status: HotlineTransferStatus, timeRemaining: TimeInterval) {
+// switch status {
+// case .unconnected:
+// break
+// case .connecting:
+// break
+// case .connected:
+// break
+// case .progress(let progress):
+// if let transfer = self.transfers.first(where: { $0.id == client.referenceNumber }) {
+// transfer.progress = progress
+// transfer.timeRemaining = timeRemaining
+// transfer.progressCallback?(transfer, progress)
+// }
+// case .failed(_):
+// if let i = self.downloads.firstIndex(where: { $0.referenceNumber == client.referenceNumber }) {
+// self.downloads.remove(at: i)
+// }
+// if let transfer = self.transfers.first(where: { $0.id == client.referenceNumber }) {
+// transfer.failed = true
+// transfer.timeRemaining = 0.0
+// }
+// if let b = self.bannerClient, reference == b.referenceNumber {
+// b.delegate = nil
+// self.bannerClient = nil
+// }
+// case .completed:
+// if let transfer = self.transfers.first(where: { $0.id == client.referenceNumber }) {
+// transfer.completed = true
+// transfer.timeRemaining = 0.0
+// }
+// break
+// }
+// }
+
+
+ func hotlineFilePreviewComplete(client: HotlineFilePreviewClient, reference: UInt32, data: Data) {
if let b = self.bannerClient, b.referenceNumber == reference {
#if os(macOS)
self.bannerImage = NSImage(data: data)
@@ -919,7 +1011,27 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
}
}
- func hotlineFileDownloadedFile(client: HotlineFileClient, reference: UInt32, at: URL) {
+// func hotlineFileDownloadedData(client: HotlineFileClient, reference: UInt32, data: Data) {
+// if let b = self.bannerClient, b.referenceNumber == reference {
+// #if os(macOS)
+// self.bannerImage = NSImage(data: data)
+// #elseif os(iOS)
+// self.bannerImage = UIImage(data: data)
+// #endif
+// }
+// else
+// if let i = self.transfers.firstIndex(where: { $0.id == reference }) {
+// let transfer = self.transfers[i]
+// transfer.previewCallback?(transfer, data)
+// self.transfers.remove(at: i)
+// }
+//
+// if let i = self.downloads.firstIndex(where: { $0.referenceNumber == reference }) {
+// self.downloads.remove(at: i)
+// }
+// }
+
+ func hotlineFileDownloadComplete(client: HotlineFileDownloadClient, reference: UInt32, at: URL) {
if let i = self.transfers.firstIndex(where: { $0.id == reference }) {
let transfer = self.transfers[i]
transfer.fileURL = at
@@ -934,6 +1046,20 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
}
}
+ func hotlineFileUploadComplete(client: HotlineFileUploadClient, reference: UInt32) {
+ if let i = self.transfers.firstIndex(where: { $0.id == reference }) {
+ let transfer = self.transfers[i]
+ transfer.uploadCallback?(transfer)
+ if Prefs.shared.playSounds && Prefs.shared.playFileTransferCompleteSound {
+ SoundEffectPlayer.shared.playSoundEffect(.transferComplete)
+ }
+ }
+
+ if let i = self.downloads.firstIndex(where: { $0.referenceNumber == reference }) {
+ self.downloads.remove(at: i)
+ }
+ }
+
// MARK: - Utilities
func updateServerTitle() {
diff --git a/Hotline/Models/TransferInfo.swift b/Hotline/Models/TransferInfo.swift
index 8b8d97a..50b23f5 100644
--- a/Hotline/Models/TransferInfo.swift
+++ b/Hotline/Models/TransferInfo.swift
@@ -16,6 +16,7 @@ class TransferInfo: Identifiable, Equatable, Hashable {
var progressCallback: ((TransferInfo, Double) -> Void)? = nil
var downloadCallback: ((TransferInfo, URL) -> Void)? = nil
+ var uploadCallback: ((TransferInfo) -> Void)? = nil
var previewCallback: ((TransferInfo, Data) -> Void)? = nil
init(id: UInt32, title: String, size: UInt) {
diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift
index b5a2c34..9fc9ae7 100644
--- a/Hotline/Utility/FoundationExtensions.swift
+++ b/Hotline/Utility/FoundationExtensions.swift
@@ -6,40 +6,8 @@ enum Endianness {
case little
}
-enum LineEnding {
- case lf // Unix-style (\n)
- case crlf // Windows-style (\r\n)
- case cr // Classic Mac-style (\r)
-}
-
extension String {
- func convertingLineEndings(to targetEnding: LineEnding) -> String {
- let lf = "\n"
- let crlf = "\r\n"
- let cr = "\r"
-
- // Normalize all line endings to LF (\n)
- let normalizedString = self.replacingOccurrences(of: cr, with: lf).replacingOccurrences(of: crlf, with: lf)
-
- // Replace normalized LF (\n) line endings with the target line ending
- switch targetEnding {
- case .lf:
- return normalizedString
- case .crlf:
- return normalizedString.replacingOccurrences(of: lf, with: crlf)
- case .cr:
- return normalizedString.replacingOccurrences(of: lf, with: cr)
- }
- }
-
- func replyToString() -> String {
- if self.range(of: "^Re:", options: [.regularExpression, .caseInsensitive]) != nil {
- return String(self)
- }
- return "Re: \(self)"
- }
-
func convertToAttributedStringWithLinks() -> AttributedString {
let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: self)
let matches = self.ranges(of: RegularExpressions.relaxedLink)
@@ -100,500 +68,7 @@ extension String {
}
}
-extension Array where Element == UInt8 {
- init(_ val: UInt8) {
- self.init()
- self.appendUInt8(val)
- }
-
- init(_ val: UInt16) {
- self.init()
- self.appendUInt16(val)
- }
-
- init(_ val: UInt32) {
- self.init()
- self.appendUInt32(val)
- }
-
- init(_ val: UInt64) {
- self.init()
- self.appendUInt64(val)
- }
-
- mutating func consumeUInt8() -> UInt8? {
- guard let val = self.readUInt8(at: 0) else {
- return nil
- }
-
- self.removeFirst(1)
- return val
- }
-
- mutating func consumeUInt16() -> UInt16? {
- guard let val = self.readUInt16(at: 0) else {
- return nil
- }
-
- self.removeFirst(2)
- return val
- }
-
- mutating func consumeUInt32() -> UInt32? {
- guard let val = self.readUInt32(at: 0) else {
- return nil
- }
-
- self.removeFirst(4)
- return val
- }
-
- mutating func consumeUInt64() -> UInt64? {
- guard let val = self.readUInt64(at: 0) else {
- return nil
- }
-
- self.removeFirst(8)
- return val
- }
-
- mutating func consume(_ length: Int) -> Bool {
- guard length <= self.count else {
- return false
- }
-
- self.removeFirst(length)
- return true
- }
-
- mutating func consumeBytes(_ length: Int) -> Data? {
- guard let val: Data = self.readData(at: 0, length: length) else {
- return nil
- }
-
- self.removeFirst(length)
- return val
- }
-
- mutating func consumeBytes(_ length: Int) -> [UInt8]? {
- guard let val: [UInt8] = self.readData(at: 0, length: length) else {
- return nil
- }
-
- self.removeFirst(length)
- return val
- }
-
- mutating func consumeDate() -> Date? {
- guard let date = self.readDate(at: 0) else {
- return nil
- }
-
- self.removeFirst(2 + 2 + 4)
- return date
- }
-
- mutating func consumePString() -> String? {
- let (str, len) = self.readPString(at: 0)
- guard let str = str else {
- return nil
- }
- if len == 0 {
- return ""
- }
-
- self.removeFirst(len)
- return str
- }
-
- mutating func consumeLongPString() -> String? {
- let (str, len) = self.readLongPString(at: 0)
- guard let str = str else {
- return nil
- }
- if len == 0 {
- return ""
- }
-
- self.removeFirst(len)
- return str
- }
-
- mutating func consumeString(_ length: Int) -> String? {
- guard let val = self.readString(at: 0, length: length) else {
- return nil
- }
-
- self.removeFirst(length)
- return val
- }
-
- func readUInt8(at offset: Int) -> UInt8? {
- guard offset >= 0, offset + 1 <= self.count else {
- return nil
- }
- return self[offset]
- }
-
- func readUInt16(at offset: Int) -> UInt16? {
- guard offset >= 0, offset + 2 <= self.count else {
- return nil
- }
-
- return (UInt16(self[offset]) << 8) + UInt16(self[offset + 1])
- }
-
- func readUInt32(at offset: Int) -> UInt32? {
- guard offset >= 0, offset + 4 <= self.count else {
- return nil
- }
-
- return (UInt32(self[offset]) << 24) + (UInt32(self[offset + 1]) << 16) + (UInt32(self[offset + 2]) << 8) + UInt32(self[offset + 3])
- }
-
- func readUInt64(at offset: Int) -> UInt64? {
- guard offset >= 0, offset + 8 <= self.count else {
- return nil
- }
-
- let a: UInt64 = (UInt64(self[offset]) << 56) +
- (UInt64(self[offset + 1]) << 48) +
- (UInt64(self[offset + 2]) << 40) +
- (UInt64(self[offset + 3]) << 32)
-
- let b: UInt64 = (UInt64(self[offset + 4]) << 24) +
- (UInt64(self[offset + 5]) << 16) +
- (UInt64(self[offset + 6]) << 8) +
- UInt64(self[offset + 7])
-
- return a + b
- }
-
- func readDate(at offset: Int) -> Date? {
- guard offset >= 0, offset + 2 + 2 + 4 <= self.count else {
- return nil
- }
-
- if
- let year = self.readUInt16(at: offset),
- let ms = self.readUInt16(at: offset + 2),
- let secs = self.readUInt32(at: offset + 2 + 2) {
- return convertHotlineDate(year: year, seconds: secs, milliseconds: ms)
- }
-
- return nil
- }
-
- func readData(at offset: Int, length: Int) -> Data? {
- guard offset >= 0, offset + length <= self.count else {
- return nil
- }
- return Data(self[offset..<(offset + length)])
- }
-
- func readData(at offset: Int, length: Int) -> [UInt8]? {
- guard offset >= 0, offset + length <= self.count else {
- return nil
- }
- return Array(self[offset..<(offset + length)])
- }
-
- func readString(at offset: Int, length: Int) -> String? {
- guard let subdata: Data = self.readData(at: offset, length: length) else {
- return nil
- }
-
- if subdata.count == 0 {
- return ""
- }
-
- let allowedEncodings = [
- NSUTF8StringEncoding,
- NSShiftJISStringEncoding,
- NSUnicodeStringEncoding,
- NSWindowsCP1251StringEncoding
- ]
- var decodedNSString: NSString?
- let rawValue = NSString.stringEncoding(for: subdata, encodingOptions: [.allowLossyKey: false], convertedString: &decodedNSString, usedLossyConversion: nil)
-
- if allowedEncodings.contains(rawValue) {
- return decodedNSString as? String
- }
-
- else if rawValue > 1 {
- print("ENCODING FOUND \(rawValue)")
- }
-
- var macStr = String(data: subdata, encoding: .macOSRoman)
- if macStr == nil {
- macStr = String(data: subdata, encoding: .nonLossyASCII)
- }
-
- return macStr
- }
-
- func readPString(at offset: Int) -> (String?, Int) {
- guard offset >= 0, offset + 1 <= self.count else {
- return (nil, 0)
- }
- let len = Int(self.readUInt8(at: offset)!)
- guard offset + 1 + len <= self.count else {
- return (nil, 0)
- }
- return (self.readString(at: offset+1, length: len), 1 + len)
- }
-
- func readLongPString(at offset: Int) -> (String?, Int) {
- guard offset >= 0, offset + 2 <= self.count else {
- return (nil, 0)
- }
- let len = Int(self.readUInt16(at: offset)!)
- guard len > 0 else {
- return ("", 0)
- }
- guard offset + 2 + len <= self.count else {
- return (nil, 0)
- }
- return (self.readString(at: offset+2, length: len), len)
- }
-
- mutating func appendUInt8(_ value: UInt8, endianness: Endianness = .big) {
- let val = endianness == .big ? value.bigEndian : value.littleEndian
- self.append(val)
- }
-
- mutating func appendUInt16(_ value: UInt16, endianness: Endianness = .big) {
- let val = endianness == .big ? value.bigEndian : value.littleEndian
- let bytes: [UInt8] = [
- UInt8(val & 0x00FF),
- UInt8((val >> 8) & 0x00FF),
- ]
- self.append(contentsOf: bytes)
- }
-
- mutating func appendUInt32(_ value: UInt32, endianness: Endianness = .big) {
- let val = endianness == .big ? value.bigEndian : value.littleEndian
- let bytes: [UInt8] = [
- UInt8(val & 0x000000FF),
- UInt8((val >> 8) & 0x000000FF),
- UInt8((val >> 16) & 0x000000FF),
- UInt8((val >> 24) & 0x000000FF),
- ]
- self.append(contentsOf: bytes)
- }
-
- mutating func appendUInt64(_ value: UInt64, endianness: Endianness = .big) {
- let val: UInt64 = endianness == .big ? value.bigEndian : value.littleEndian
- let bytes: [UInt8] = [
- UInt8(val & 0x00000000000000FF),
- UInt8((val >> 8) & 0x00000000000000FF),
- UInt8((val >> 16) & 0x00000000000000FF),
- UInt8((val >> 24) & 0x00000000000000FF),
- UInt8((val >> 32) & 0x00000000000000FF),
- UInt8((val >> 40) & 0x00000000000000FF),
- UInt8((val >> 48) & 0x00000000000000FF),
- UInt8((val >> 56) & 0x00000000000000FF),
- ]
- self.append(contentsOf: bytes)
- }
-
- mutating func appendData(_ data: Data) {
- self.append(contentsOf: data)
- }
-
- mutating func appendData(_ data: [UInt8]) {
- self.append(contentsOf: data)
- }
-
- mutating func hotlineEncrypt() {
- for i in (0..<self.count).reversed() {
- self[i] = 0xFF - self[i]
- }
- }
-
- func hotlineEncrypted() -> [UInt8] {
- var cpy = [UInt8](self)
-
-
-
- for i in (0..<cpy.count).reversed() {
- cpy[i] = 0xFF - cpy[i]
- }
- return cpy
- }
-}
-
-extension Data {
- init(_ val: UInt8) {
- self.init()
- self.appendUInt8(val)
- }
-
- init(_ val: UInt16) {
- self.init()
- self.appendUInt16(val)
- }
-
- init(_ val: UInt32) {
- self.init()
- self.appendUInt32(val)
- }
-
- func readUInt8(at offset: Int) -> UInt8? {
- guard offset >= 0, offset + 1 <= self.count else {
- return nil
- }
- return self[offset]
- }
-
- func readUInt16(at offset: Int) -> UInt16? {
- guard offset >= 0, offset + 2 <= self.count else {
- return nil
- }
-
- return (UInt16(self[offset]) << 8) + UInt16(self[offset + 1])
- }
-
- func readUInt32(at offset: Int) -> UInt32? {
- guard offset >= 0, offset + 4 <= self.count else {
- return nil
- }
-
- return (UInt32(self[offset]) << 24) + (UInt32(self[offset + 1]) << 16) + (UInt32(self[offset + 2]) << 8) + UInt32(self[offset + 3])
- }
-
- func readUInt64(at offset: Int) -> UInt64? {
- guard offset >= 0, offset + 8 <= self.count else {
- return nil
- }
-
- return withUnsafeBytes { $0.load(as: UInt64.self ) }
- }
-
- func readDate(at offset: Int) -> Date? {
- guard offset >= 0, offset + 2 + 2 + 4 <= self.count else {
- return nil
- }
-
- if
- let year = self.readUInt16(at: offset),
- let ms = self.readUInt16(at: offset + 2),
- let secs = self.readUInt32(at: offset + 2 + 2) {
- return convertHotlineDate(year: year, seconds: secs, milliseconds: ms)
- }
-
- return nil
- }
-
- func readData(at offset: Int, length: Int) -> Data? {
- guard offset >= 0, offset + length <= self.count else {
- return nil
- }
- return self.subdata(in: offset..<(offset + length))
- }
-
- func readString(at offset: Int, length: Int) -> String? {
- let subdata = self[offset..<(offset + length)]
- if subdata.count == 0 {
- return ""
- }
-
- let allowedEncodings = [
- NSUTF8StringEncoding,
- NSShiftJISStringEncoding,
- NSUnicodeStringEncoding,
- NSWindowsCP1251StringEncoding
- ]
-
- var decodedNSString: NSString?
- let rawValue = NSString.stringEncoding(for: subdata, encodingOptions: [.allowLossyKey: false], convertedString: &decodedNSString, usedLossyConversion: nil)
-
- if allowedEncodings.contains(rawValue) {
- return decodedNSString as? String
- }
-
- else if rawValue > 1 {
- print("ENCODING FOUND \(rawValue)")
- }
-
- var macStr = String(data: subdata, encoding: .macOSRoman)
- if macStr == nil {
- macStr = String(data: subdata, encoding: .nonLossyASCII)
- }
-
- return macStr
- }
-
- func readPString(at offset: Int) -> (String?, Int) {
- guard offset >= 0, offset + 1 <= self.count else {
- return (nil, 0)
- }
- let len = Int(self.readUInt8(at: offset)!)
- guard offset + 1 + len <= self.count else {
- return (nil, 0)
- }
- return (self.readString(at: offset+1, length: len), 1 + len)
- }
-
- func readLongPString(at offset: Int) -> (String?, Int) {
- guard offset >= 0, offset + 2 <= self.count else {
- return (nil, 0)
- }
- let len = Int(self.readUInt16(at: offset)!)
- guard len > 0 else {
- return ("", 0)
- }
- guard offset + 2 + len <= self.count else {
- return (nil, 0)
- }
- return (self.readString(at: offset+2, length: len), len)
- }
-
-
- mutating func appendUInt8(_ value: UInt8, endianness: Endianness = .big) {
- var val = endianness == .big ? value.bigEndian : value.littleEndian
- append(&val, count: MemoryLayout<UInt8>.size)
- }
-
- mutating func appendUInt16(_ value: UInt16, endianness: Endianness = .big) {
- var val = endianness == .big ? value.bigEndian : value.littleEndian
- Swift.withUnsafeBytes(of: &val) { buffer in
- append(buffer.bindMemory(to: UInt8.self))
- }
-// append(&val, count: MemoryLayout<UInt16>.size)
- }
-
- mutating func appendUInt32(_ value: UInt32, endianness: Endianness = .big) {
- var val = endianness == .big ? value.bigEndian : value.littleEndian
- Swift.withUnsafeBytes(of: &val) { buffer in
- append(buffer.bindMemory(to: UInt8.self))
- }
-// append(&val, count: MemoryLayout<UInt32>.size)
- }
-}
-
-extension String {
- func fourCharCode() -> FourCharCode {
- guard self.count == 4 else {
- return 0
- }
-
- return self.utf16.reduce(0, {$0 << 8 + FourCharCode($1)})
- }
-}
-
-extension FourCharCode {
- func fourCharCode() -> String {
- let bytes = [
- UInt8((self >> 24) & 0xFF),
- UInt8((self >> 16) & 0xFF),
- UInt8((self >> 8) & 0xFF),
- UInt8(self & 0xFF)
- ]
- return String(bytes: bytes, encoding: .ascii) ?? ""
- }
-}
extension Binding where Value: OptionSet, Value == Value.Element {
func bindedValue(_ options: Value) -> Bool {
diff --git a/Hotline/macOS/FilesView.swift b/Hotline/macOS/FilesView.swift
index 2afa1a8..e34c7b5 100644
--- a/Hotline/macOS/FilesView.swift
+++ b/Hotline/macOS/FilesView.swift
@@ -133,6 +133,7 @@ struct FilesView: View {
@State private var selection: FileInfo?
@State private var fileDetails: FileDetails?
+ @State private var uploadFileSelectorDisplayed: Bool = false
private func openPreviewWindow(_ previewInfo: PreviewFileInfo) {
switch previewInfo.previewType {
@@ -143,7 +144,6 @@ struct FilesView: View {
default:
return
}
-// let _ = FilePreviewWindowController(info: previewInfo)
}
@MainActor private func getFileInfo(_ file: FileInfo) {
@@ -164,6 +164,15 @@ struct FilesView: View {
model.downloadFile(file.name, path: file.path)
}
+ @MainActor private func uploadFile(file fileURL: URL, to path: [String]) {
+ model.uploadFile(url: fileURL, path: path) { info in
+ Task {
+ // Refresh file listing to display newly uploaded file.
+ let _ = await model.getFileList(path: path)
+ }
+ }
+ }
+
@MainActor private func previewFile(_ file: FileInfo) {
guard file.isPreviewable else {
return
@@ -290,15 +299,6 @@ struct FilesView: View {
}
}
.toolbar {
-// ToolbarItem(placement: .primaryAction) {
-// Button {
-// } label: {
-// Label("Delete", systemImage: "trash")
-// }
-// .help("Delete")
-// .disabled(true)
-// }
-
ToolbarItem(placement: .primaryAction) {
Button {
if let selectedFile = selection, selectedFile.isPreviewable {
@@ -325,6 +325,15 @@ struct FilesView: View {
ToolbarItem(placement: .primaryAction) {
Button {
+ uploadFileSelectorDisplayed = true
+ } label: {
+ Label("Upload", systemImage: "arrow.up")
+ }
+ .help("Upload")
+ }
+
+ ToolbarItem(placement: .primaryAction) {
+ Button {
if let selectedFile = selection, !selectedFile.isFolder {
downloadFile(selectedFile)
}
@@ -339,6 +348,36 @@ struct FilesView: View {
.sheet(item: $fileDetails ) { item in
FileDetailsView(fd: item)
}
+ .fileImporter(isPresented: $uploadFileSelectorDisplayed, allowedContentTypes: [.data], allowsMultipleSelection: false, onCompletion: { results in
+ switch results {
+ case .success(let fileURLS):
+ guard fileURLS.count > 0 else {
+ return
+ }
+
+ let fileURL = fileURLS.first!
+
+ print(fileURL)
+
+ var uploadPath: [String] = []
+
+ if let selection = selection {
+ if selection.isFolder {
+ uploadPath = selection.path
+ }
+ else {
+ uploadPath = Array<String>(selection.path)
+ uploadPath.removeLast()
+ }
+ }
+
+ print("UPLOAD PATH: \(uploadPath)")
+ uploadFile(file: fileURL, to: uploadPath)
+
+ case .failure(let error):
+ print(error)
+ }
+ })
}
}
diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift
index 80a756c..5b5b8e3 100644
--- a/Hotline/macOS/ServerView.swift
+++ b/Hotline/macOS/ServerView.swift
@@ -505,7 +505,6 @@ struct ServerView: View {
guard !server.address.isEmpty else {
return
}
-
model.login(server: server, username: Prefs.shared.username, iconID: Prefs.shared.userIconID) { success in
if !success {
print("FAILED LOGIN??")