Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions Sources/Models/AppBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,14 @@ public struct AppBundle: Sendable, Identifiable, Hashable, Equatable {
}

private func loadExtensions() -> Set<AppBundle> {
let pluginsURL = fileURL.appendingPathComponent("PlugIns")
guard let contents = try? FileManager.default.contentsOfDirectory(
at: pluginsURL,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles]
) else {
return []
// App extensions live in PlugIns/; ExtensionKit extensions (EXAppExtensionAttributes,
// e.g. PhotoKit background upload) live in Extensions/.
let contents = ["PlugIns", "Extensions"].flatMap { folder in
(try? FileManager.default.contentsOfDirectory(
at: fileURL.appendingPathComponent(folder),
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles]
)) ?? []
}
let extensions = contents
.filter { $0.pathExtension.lowercased() == "appex" }
Expand Down
23 changes: 23 additions & 0 deletions Tests/SideSignTests/SideSignTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright 漏 2026 SideSign. All rights reserved.
//

import Foundation
import Testing
@testable import SideSign
import CodeSignKit
Expand Down Expand Up @@ -98,4 +99,26 @@ struct SideSignTests {
let decompressed = try reader.readCurrentFile()
#expect(decompressed == testContent)
}

@Test
func appExtensionsIncludeExtensionKitExtensions() throws {
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: tempDir) }

func makeBundle(_ url: URL, identifier: String) throws {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
let plist: [String: Any] = ["CFBundleIdentifier": identifier, "CFBundleName": url.deletingPathExtension().lastPathComponent]
let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
try data.write(to: url.appendingPathComponent("Info.plist"))
}

let appURL = tempDir.appendingPathComponent("Test.app")
try makeBundle(appURL, identifier: "com.example.test")
// Classic app extension (NSExtension) and ExtensionKit extension (EXAppExtensionAttributes).
try makeBundle(appURL.appendingPathComponent("PlugIns/Widget.appex"), identifier: "com.example.test.widget")
try makeBundle(appURL.appendingPathComponent("Extensions/Upload.appex"), identifier: "com.example.test.upload")

let app = try #require(AppBundle(fileURL: appURL))
#expect(Set(app.appExtensions.map(\.bundleIdentifier)) == ["com.example.test.widget", "com.example.test.upload"])
}
}