From 1b7e8bcac693069ef602c879d9dd8bc2d0dae7b9 Mon Sep 17 00:00:00 2001 From: Difei Zhao Date: Sun, 18 Oct 2026 09:19:01 -0700 Subject: [PATCH 1/3] fix: discover app extensions in Extensions directory --- Sources/Models/AppBundle.swift | 28 +++-- Tests/SideSignTests/AppBundleTests.swift | 129 +++++++++++++++++++++++ 2 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 Tests/SideSignTests/AppBundleTests.swift diff --git a/Sources/Models/AppBundle.swift b/Sources/Models/AppBundle.swift index acf0c02..d86e001 100644 --- a/Sources/Models/AppBundle.swift +++ b/Sources/Models/AppBundle.swift @@ -117,17 +117,25 @@ public struct AppBundle: Sendable, Identifiable, Hashable, Equatable { } private func loadExtensions() -> Set { - let pluginsURL = fileURL.appendingPathComponent("PlugIns") - guard let contents = try? FileManager.default.contentsOfDirectory( - at: pluginsURL, - includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles] - ) else { - return [] + let fileManager = FileManager.default + let extensionDirectories = ["PlugIns", "Extensions"].map { + fileURL.appendingPathComponent($0) } - let extensions = contents - .filter { $0.pathExtension.lowercased() == "appex" } - .compactMap { AppBundle(fileURL: $0) } + + let extensions = extensionDirectories.flatMap { directoryURL -> [AppBundle] in + guard let contents = try? fileManager.contentsOfDirectory( + at: directoryURL, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) else { + return [] + } + + return contents + .filter { $0.pathExtension.lowercased() == "appex" } + .compactMap { AppBundle(fileURL: $0) } + } + return Set(extensions) } diff --git a/Tests/SideSignTests/AppBundleTests.swift b/Tests/SideSignTests/AppBundleTests.swift new file mode 100644 index 0000000..d0d3055 --- /dev/null +++ b/Tests/SideSignTests/AppBundleTests.swift @@ -0,0 +1,129 @@ +import Foundation +import Testing +@testable import SideSign + +@Suite("App Bundle Extension Discovery Tests") +struct AppBundleTests { + + @Test + func discoversExtensionsFromBothDirectoriesAndSkipsInvalidEntries() throws { + let fixture = try BundleFixture() + defer { fixture.remove() } + + try fixture.addBundle(named: "Plugin.appex", in: "PlugIns", identifier: "com.example.plugin") + try fixture.addBundle(named: "Extension.appex", in: "Extensions", identifier: "com.example.extension") + try fixture.addBundle(named: "Uppercase.APPEX", in: "Extensions", identifier: "com.example.uppercase") + try fixture.addBundle(named: ".Hidden.appex", in: "Extensions", identifier: "com.example.hidden") + try fixture.addBundle(named: "Invalid.appex", in: "Extensions", identifier: nil) + try fixture.addBundle(named: "NotAnExtension.bundle", in: "Extensions", identifier: "com.example.bundle") + try fixture.addFile(named: "File.appex", in: "Extensions") + try fixture.addBundle(named: "NestedChild.appex", in: "Extensions/Nested", identifier: "com.example.nested") + + let app = try #require(AppBundle(fileURL: fixture.appURL)) + let discovered = app.appExtensions + + #expect(Set(discovered.map(\.bundleIdentifier)) == [ + "com.example.extension", + "com.example.plugin", + "com.example.uppercase" + ]) + #expect(app.allAppBundles.map(\.bundleIdentifier) == [ + "com.example.app", + "com.example.extension", + "com.example.plugin", + "com.example.uppercase" + ]) + #expect(app.appBundle(withBundleIdentifier: "com.example.plugin")?.fileURL.lastPathComponent == "Plugin.appex") + #expect(app.appBundle(withBundleIdentifier: "com.example.extension")?.fileURL.lastPathComponent == "Extension.appex") + #expect(app.appBundle(withBundleIdentifier: "com.example.nested") == nil) + #expect(app.appBundle(withBundleIdentifier: "com.example.app")?.fileURL == fixture.appURL) + } + + @Test + func discoversExtensionsWhenPlugInsDirectoryIsMissing() throws { + let fixture = try BundleFixture() + defer { fixture.remove() } + try fixture.addBundle(named: "Extension.appex", in: "Extensions", identifier: "com.example.extension") + + let app = try #require(AppBundle(fileURL: fixture.appURL)) + + #expect(app.appExtensions.map(\.bundleIdentifier) == ["com.example.extension"]) + } + + @Test + func discoversExtensionsWhenExtensionsDirectoryIsMissing() throws { + let fixture = try BundleFixture() + defer { fixture.remove() } + try fixture.addBundle(named: "Plugin.appex", in: "PlugIns", identifier: "com.example.plugin") + + let app = try #require(AppBundle(fileURL: fixture.appURL)) + + #expect(app.appExtensions.map(\.bundleIdentifier) == ["com.example.plugin"]) + } + + @Test + func returnsNoExtensionsWhenNeitherDirectoryExists() throws { + let fixture = try BundleFixture() + defer { fixture.remove() } + + let app = try #require(AppBundle(fileURL: fixture.appURL)) + + #expect(app.appExtensions.isEmpty) + } + + @Test(arguments: ["PlugIns", "Extensions"]) + func unreadableDirectoryDoesNotHideOtherDirectory(unreadableDirectory: String) throws { + let fixture = try BundleFixture() + defer { fixture.remove() } + let readableDirectory = unreadableDirectory == "PlugIns" ? "Extensions" : "PlugIns" + try fixture.addFile(named: unreadableDirectory, in: nil) + try fixture.addBundle(named: "Extension.appex", in: readableDirectory, identifier: "com.example.extension") + + let app = try #require(AppBundle(fileURL: fixture.appURL)) + + #expect(app.appExtensions.map(\.bundleIdentifier) == ["com.example.extension"]) + } +} + +private struct BundleFixture { + let rootURL: URL + let appURL: URL + + init() throws { + let fileManager = FileManager.default + rootURL = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString) + appURL = rootURL.appendingPathComponent("Sample.app") + try fileManager.createDirectory(at: appURL, withIntermediateDirectories: true) + try Self.writeInfoPlist(to: appURL, identifier: "com.example.app") + } + + func addBundle(named name: String, in directoryName: String, identifier: String?) throws { + let directoryURL = appURL.appendingPathComponent(directoryName) + let bundleURL = directoryURL.appendingPathComponent(name) + try FileManager.default.createDirectory(at: bundleURL, withIntermediateDirectories: true) + if let identifier { + try Self.writeInfoPlist(to: bundleURL, identifier: identifier) + } + } + + func addFile(named name: String, in directoryName: String?) throws { + let directoryURL = directoryName.map { appURL.appendingPathComponent($0) } ?? appURL + try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) + try Data().write(to: directoryURL.appendingPathComponent(name)) + } + + func remove() { + try? FileManager.default.removeItem(at: rootURL) + } + + private static func writeInfoPlist(to bundleURL: URL, identifier: String) throws { + let plist: [String: Any] = [ + "CFBundleIdentifier": identifier, + "CFBundleName": bundleURL.deletingPathExtension().lastPathComponent, + "CFBundleShortVersionString": "1.0", + "CFBundleVersion": "1" + ] + let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) + try data.write(to: bundleURL.appendingPathComponent("Info.plist")) + } +} From 95491361b49078b4e1298362aac80934c7712ede Mon Sep 17 00:00:00 2001 From: Difei Zhao Date: Sun, 18 Oct 2026 09:21:53 -0700 Subject: [PATCH 2/3] test: import Foundation for archive fixtures --- Tests/SideSignTests/SideSignTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/SideSignTests/SideSignTests.swift b/Tests/SideSignTests/SideSignTests.swift index 81e54fe..969d109 100644 --- a/Tests/SideSignTests/SideSignTests.swift +++ b/Tests/SideSignTests/SideSignTests.swift @@ -6,6 +6,7 @@ // Copyright © 2026 SideSign. All rights reserved. // +import Foundation import Testing @testable import SideSign import CodeSignKit From 14082dde804e03fbd93de7e34b6edb570d4bb9af Mon Sep 17 00:00:00 2001 From: Difei Zhao Date: Sun, 18 Oct 2026 09:29:22 -0700 Subject: [PATCH 3/3] Simplify extension discovery --- Sources/Models/AppBundle.swift | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/Sources/Models/AppBundle.swift b/Sources/Models/AppBundle.swift index d86e001..c9b6b11 100644 --- a/Sources/Models/AppBundle.swift +++ b/Sources/Models/AppBundle.swift @@ -117,25 +117,16 @@ public struct AppBundle: Sendable, Identifiable, Hashable, Equatable { } private func loadExtensions() -> Set { - let fileManager = FileManager.default - let extensionDirectories = ["PlugIns", "Extensions"].map { - fileURL.appendingPathComponent($0) - } - - let extensions = extensionDirectories.flatMap { directoryURL -> [AppBundle] in - guard let contents = try? fileManager.contentsOfDirectory( - at: directoryURL, + let contents = ["PlugIns", "Extensions"].flatMap { folder in + (try? FileManager.default.contentsOfDirectory( + at: fileURL.appendingPathComponent(folder), includingPropertiesForKeys: nil, options: [.skipsHiddenFiles] - ) else { - return [] - } - - return contents - .filter { $0.pathExtension.lowercased() == "appex" } - .compactMap { AppBundle(fileURL: $0) } + )) ?? [] } - + let extensions = contents + .filter { $0.pathExtension.lowercased() == "appex" } + .compactMap { AppBundle(fileURL: $0) } return Set(extensions) }