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.
+