diff options
| author | Dustin Mierau <dustin@mierau.me> | 2024-07-24 12:32:41 -0700 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2024-07-24 12:32:41 -0700 |
| commit | 8633db7e6ca9f38345d7be0ab90193d6325904bf (patch) | |
| tree | a950663e587c0bb0b051a61882ca9df6949a2446 /Hotline | |
| parent | 844317f8a3f5c7b11394653c8563c6e9c24d7e85 (diff) | |
First pass at uploads and a big refactoring of transfer clients. Also now showing download progress bar on file icon in dock (like Safari).
Diffstat (limited to 'Hotline')
| -rw-r--r-- | Hotline/Hotline/HotlineClient.swift | 17 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineExtensions.swift | 782 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineFileClient.swift | 665 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineProtocol.swift | 14 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineTransferClient.swift | 1244 | ||||
| -rw-r--r-- | Hotline/Models/FilePreview.swift | 16 | ||||
| -rw-r--r-- | Hotline/Models/Hotline.swift | 206 | ||||
| -rw-r--r-- | Hotline/Models/TransferInfo.swift | 1 | ||||
| -rw-r--r-- | Hotline/Utility/FoundationExtensions.swift | 525 | ||||
| -rw-r--r-- | Hotline/macOS/FilesView.swift | 59 | ||||
| -rw-r--r-- | Hotline/macOS/ServerView.swift | 1 |
11 files changed, 2267 insertions, 1263 deletions
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??") |