diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/LogCompressor.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/LogCompressor.swift deleted file mode 100644 index 554be1564..000000000 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/LogCompressor.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// LogCompressor.swift -// -// -// Created by Jamil Bou Kheir on 3/28/24. -// - -import AppleArchive -import Foundation -import System - -/// This module handles the business work of interacting with the AppleArchive framework to do the actual -/// compression. It's used from both the app and tunnel process in nearly the same way, save for how the -/// writeStream is opened. -/// -/// In the tunnel process, the writeStream is a custom stream derived from our TunnelArchiveByteStream, -/// which keeps state around the writing of compressed bytes in order to handle sending chunks back to the -/// app process. -/// -/// In the app process, the writeStream is derived from a passed file path where the Apple Archive -/// framework handles writing for us -- no custom byte stream instance is needed. -/// -/// Once the writeStream is opened, the remaining operations are the same for both. -public struct LogCompressor { - enum CompressionError: Error { - case unableToReadSourceDirectory - case unableToInitialize - } - - public init() {} - - public func start( - source directory: FilePath, - to file: FilePath - ) throws { - let stream = ArchiveByteStream.fileStream( - path: file, - mode: .writeOnly, - options: [.create], - permissions: FilePermissions(rawValue: 0o644) - ) - - try compress(source: directory, writeStream: stream) - } - - // Compress to a given writeStream which was opened either from a FilePath or - // TunnelArchiveByteStream - private func compress( - source path: FilePath, - writeStream: ArchiveByteStream? - ) throws { - let headerKeys = "TYP,PAT,LNK,DEV,DAT,UID,GID,MOD,FLG,MTM,BTM,CTM" - - guard let writeStream = writeStream, - let compressionStream = - ArchiveByteStream.compressionStream( - using: .lzfse, - writingTo: writeStream - ), - let encodeStream = - ArchiveStream.encodeStream( - writingTo: compressionStream - ), - let keySet = ArchiveHeader.FieldKeySet(headerKeys) - else { - throw CompressionError.unableToInitialize - } - - defer { - try? encodeStream.close() - try? compressionStream.close() - try? writeStream.close() - } - - try encodeStream.writeDirectoryContents( - archiveFrom: path, - keySet: keySet - ) - } -} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/LogExporter.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/LogExporter.swift index ac669efc9..ec18c85da 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/LogExporter.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/LogExporter.swift @@ -48,7 +48,7 @@ import System // 2. Create tunnel log archive from tunnel process let tunnelLogURL = sharedLogFolderURL - .appendingPathComponent("tunnel.aar") + .appendingPathComponent("tunnel.zip") fileManager.createFile(atPath: tunnelLogURL.path, contents: nil) let fileHandle = try FileHandle(forWritingTo: tunnelLogURL) @@ -79,19 +79,19 @@ import System } // 4. Create app log archive - let appLogURL = sharedLogFolderURL.appendingPathComponent("app.aar") - try LogCompressor().start( - source: toPath(logFolderURL), - to: toPath(appLogURL) + let appLogURL = sharedLogFolderURL.appendingPathComponent("app.zip") + try ZipService.createZip( + source: logFolderURL, + to: appLogURL ) // Remove existing archive if it exists try? fileManager.removeItem(at: archiveURL) // Write final log archive - try LogCompressor().start( - source: toPath(sharedLogFolderURL), - to: toPath(archiveURL) + try ZipService.createZip( + source: sharedLogFolderURL, + to: archiveURL ) // Remove intermediate log archives @@ -109,7 +109,10 @@ import System } static func export(to archiveURL: URL) async throws { - guard let logFolderURL = SharedAccess.logFolderURL + guard let logFolderURL = SharedAccess.logFolderURL, + let connlibLogFolderURL = SharedAccess.connlibLogFolderURL, + let cacheFolderURL = SharedAccess.cacheFolderURL + else { throw ExportError.invalidSourceDirectory } @@ -117,10 +120,21 @@ import System // Remove existing archive if it exists try? fileManager.removeItem(at: archiveURL) + let latestSymlink = connlibLogFolderURL.appendingPathComponent("latest") + let tempSymlink = cacheFolderURL.appendingPathComponent( + "latest") + + // Move the `latest` symlink out of the way before creating the archive. + // Apple's implementation of zip appears to not be able to handle symlinks well + let _ = try? FileManager.default.moveItem(at: latestSymlink, to: tempSymlink) + defer { + let _ = try? FileManager.default.moveItem(at: tempSymlink, to: latestSymlink) + } + // Write final log archive - try LogCompressor().start( - source: toPath(logFolderURL), - to: toPath(archiveURL) + try ZipService.createZip( + source: logFolderURL, + to: archiveURL ) } @@ -128,7 +142,7 @@ import System // directory and then the OS will move it into place when the ShareSheet // is dismissed. static func tempFile() -> URL { - let fileName = "firezone_logs_\(now()).aar" + let fileName = "firezone_logs_\(now()).zip" return fileManager.temporaryDirectory.appendingPathComponent(fileName) } } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/ZipService.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/ZipService.swift new file mode 100644 index 000000000..91e5b0e0a --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/ZipService.swift @@ -0,0 +1,57 @@ +// Inspired from https://gist.github.com/dreymonde/793a8a7c2ed5443b1594f528bb7c88a7 + +import Foundation + +// MARK: - Extensions + +extension URL { + var isDirectory: Bool { + (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true + } +} + +// MARK: - Errors + +enum CreateZipError: Swift.Error { + case urlNotADirectory(URL) + case failedToCreateZIP(Swift.Error) + case failedToMoveZIP(Swift.Error) +} + +// MARK: - ZipService + +public final class ZipService { + + public static func createZip( + source directoryURL: URL, + to zipFinalURL: URL, + ) throws { + // see URL extension above + guard directoryURL.isDirectory else { + throw CreateZipError.urlNotADirectory(directoryURL) + } + + var fileManagerError: Swift.Error? + var coordinatorError: NSError? + + NSFileCoordinator().coordinate( + readingItemAt: directoryURL, + options: .forUploading, + error: &coordinatorError + ) { zipAccessURL in + do { + try FileManager.default.moveItem(at: zipAccessURL, to: zipFinalURL) + } catch { + fileManagerError = error + } + } + + if let error = coordinatorError { + throw CreateZipError.failedToCreateZIP(error) + } + + if let error = fileManagerError { + throw CreateZipError.failedToMoveZIP(error) + } + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SettingsView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SettingsView.swift index 1a23a575a..a234eeae1 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SettingsView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SettingsView.swift @@ -596,7 +596,7 @@ public struct SettingsView: View { let savePanel = NSSavePanel() savePanel.prompt = "Save" savePanel.nameFieldLabel = "Save log archive to:" - let fileName = "firezone_logs_\(LogExporter.now()).aar" + let fileName = "firezone_logs_\(LogExporter.now()).zip" savePanel.nameFieldStringValue = fileName diff --git a/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift index 0d89e73fc..1ac9e5c73 100644 --- a/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift +++ b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift @@ -244,16 +244,28 @@ class PacketTunnelProvider: NEPacketTunnelProvider { case .idle: guard let logFolderURL = SharedAccess.logFolderURL, - let logFolderPath = FilePath(logFolderURL) + let cacheFolderURL = SharedAccess.cacheFolderURL, + let connlibLogFolderURL = SharedAccess.connlibLogFolderURL else { completionHandler(nil) return } - let tunnelLogArchive = TunnelLogArchive(source: logFolderPath) + let tunnelLogArchive = TunnelLogArchive(source: logFolderURL) + + let latestSymlink = connlibLogFolderURL.appendingPathComponent("latest") + let tempSymlink = cacheFolderURL.appendingPathComponent( + "latest") do { + // Move the `latest` symlink out of the way before creating the archive. + // Apple's implementation of zip appears to not be able to handle symlinks well + let _ = try? FileManager.default.moveItem(at: latestSymlink, to: tempSymlink) + defer { + let _ = try? FileManager.default.moveItem(at: tempSymlink, to: latestSymlink) + } + try tunnelLogArchive.archive() } catch { Log.error(error) diff --git a/swift/apple/FirezoneNetworkExtension/TunnelLogArchive.swift b/swift/apple/FirezoneNetworkExtension/TunnelLogArchive.swift index 934b750bb..c0befdbdf 100644 --- a/swift/apple/FirezoneNetworkExtension/TunnelLogArchive.swift +++ b/swift/apple/FirezoneNetworkExtension/TunnelLogArchive.swift @@ -4,7 +4,6 @@ // LICENSE: Apache-2.0 // -import AppleArchive import FirezoneKit import Foundation import System @@ -24,8 +23,7 @@ import System /// /// Currently this limit is set to 1 MB (chosen somewhat arbitrarily based on limited information found on the /// web), but can be easily enlarged in the future to reduce the number of IPC calls required to consume -/// the entire archive. The LZFSE compression algorithm used by default in the Apple Archive Framework is -/// quite efficient -- compression ratios for our logs can be as high as 100:1 using this format. +/// the entire archive. class TunnelLogArchive { enum ArchiveError: Error { case unableToWriteArchive @@ -37,13 +35,13 @@ class TunnelLogArchive { let archiveURL = FileManager .default .temporaryDirectory - .appendingPathComponent("logs.aar") + .appendingPathComponent("logs.zip") var offset: UInt64 = 0 var fileHandle: FileHandle? - var source: FilePath + var source: URL - init(source: FilePath) { + init(source: URL) { self.source = source } @@ -52,16 +50,11 @@ class TunnelLogArchive { } func archive() throws { - guard let archivePath = FilePath(self.archiveURL) - else { - throw ArchiveError.unableToWriteArchive - } - try? FileManager.default.removeItem(at: self.archiveURL) - try LogCompressor().start( + try ZipService.createZip( source: source, - to: archivePath + to: self.archiveURL ) } diff --git a/website/src/components/Changelog/Apple.tsx b/website/src/components/Changelog/Apple.tsx index ef058fb65..17650e9f7 100644 --- a/website/src/components/Changelog/Apple.tsx +++ b/website/src/components/Changelog/Apple.tsx @@ -29,6 +29,9 @@ export default function Apple() { Fixes an issue where certain log files would not be recreated after logs were cleared. + + Uses `.zip` to compress logs instead of Apple Archive. +