Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

187 lines
7.1 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import MacToolsPluginKit
struct PluginPackageManifest: Codable, Equatable {
struct Capabilities: Codable, Equatable {
let primaryPanel: Bool
let componentPanel: Bool
let configuration: Bool
init(primaryPanel: Bool = false, componentPanel: Bool = false, configuration: Bool = false) {
self.primaryPanel = primaryPanel
self.componentPanel = componentPanel
self.configuration = configuration
}
}
let id: String
let displayName: String
let version: String
let minHostVersion: String
let pluginKitVersion: Int
let bundleRelativePath: String
let factoryClass: String?
let capabilities: Capabilities
let permissions: [String]
let category: String?
let releaseChannel: String?
let localizedMetadata: [String: PluginLocalizedMetadata]?
init(
id: String,
displayName: String,
version: String,
minHostVersion: String,
pluginKitVersion: Int = PluginPackageManifestLoader.supportedPluginKitVersion,
bundleRelativePath: String,
factoryClass: String? = nil,
capabilities: Capabilities = Capabilities(),
permissions: [String] = [],
category: String? = nil,
releaseChannel: String? = nil,
localizedMetadata: [String: PluginLocalizedMetadata]? = nil
) {
self.id = id
self.displayName = displayName
self.version = version
self.minHostVersion = minHostVersion
self.pluginKitVersion = pluginKitVersion
self.bundleRelativePath = bundleRelativePath
self.factoryClass = factoryClass
self.capabilities = capabilities
self.permissions = permissions
self.category = category
self.releaseChannel = releaseChannel
self.localizedMetadata = localizedMetadata
}
var localizedDisplayName: String {
PluginLocalizationMatcher.localizedMetadata(from: localizedMetadata ?? [:])?.displayName ?? displayName
}
var localizedSummary: String? {
PluginLocalizationMatcher.localizedMetadata(from: localizedMetadata ?? [:])?.summary
}
}
enum PluginPackageManifestError: LocalizedError, Equatable {
case missingManifest(URL)
case unreadableManifest(URL)
case invalidIdentifier(String)
case invalidVersion(String)
case invalidBundleRelativePath(String)
case unsupportedPluginKitVersion(Int)
case incompatibleHostVersion(required: String, current: String)
var errorDescription: String? {
switch self {
case let .missingManifest(url):
return AppL10n.pluginsFormat("plugin.error.manifest.missingFormat", defaultValue: "插件缺少 manifest%@", url.path)
case let .unreadableManifest(url):
return AppL10n.pluginsFormat("plugin.error.manifest.unreadableFormat", defaultValue: "插件 manifest 无法读取:%@", url.path)
case let .invalidIdentifier(id):
return AppL10n.pluginsFormat("plugin.error.manifest.invalidIdentifierFormat", defaultValue: "插件 ID 不合法:%@", id)
case let .invalidVersion(version):
return AppL10n.pluginsFormat("plugin.error.manifest.invalidVersionFormat", defaultValue: "插件版本号不合法:%@", version)
case let .invalidBundleRelativePath(path):
return AppL10n.pluginsFormat("plugin.error.manifest.invalidBundlePathFormat", defaultValue: "插件入口路径不合法:%@", path)
case let .unsupportedPluginKitVersion(version):
return AppL10n.pluginsFormat("plugin.error.manifest.unsupportedSDKFormat", defaultValue: "插件 SDK 版本不支持:%d", version)
case let .incompatibleHostVersion(required, current):
return AppL10n.pluginsFormat(
"plugin.error.manifest.incompatibleHostFormat",
defaultValue: "插件需要 MacTools %@ 或更高版本,当前版本为 %@。",
required,
current
)
}
}
}
enum PluginPackageManifestLoader {
static let fileName = "plugin.json"
static let supportedPluginKitVersion = PluginKitCompatibility.currentVersion
static func load(
from packageURL: URL,
hostVersion: String = AppMetadata.shortVersion ?? "0"
) throws -> PluginPackageManifest {
let manifest = try readManifest(from: packageURL)
try validate(manifest, hostVersion: hostVersion)
return manifest
}
static func decode(from packageURL: URL) throws -> PluginPackageManifest {
try readManifest(from: packageURL)
}
static func validate(_ manifest: PluginPackageManifest, hostVersion: String) throws {
try validatePackageIdentity(manifest)
guard manifest.pluginKitVersion == supportedPluginKitVersion else {
throw PluginPackageManifestError.unsupportedPluginKitVersion(manifest.pluginKitVersion)
}
guard PluginVersionComparator.isVersion(hostVersion, atLeast: manifest.minHostVersion) else {
throw PluginPackageManifestError.incompatibleHostVersion(
required: manifest.minHostVersion,
current: hostVersion
)
}
}
static func validatePackageIdentity(_ manifest: PluginPackageManifest) throws {
guard isValidPluginID(manifest.id) else {
throw PluginPackageManifestError.invalidIdentifier(manifest.id)
}
guard isValidVersion(manifest.version) else {
throw PluginPackageManifestError.invalidVersion(manifest.version)
}
guard isValidVersion(manifest.minHostVersion) else {
throw PluginPackageManifestError.invalidVersion(manifest.minHostVersion)
}
guard
!manifest.bundleRelativePath.isEmpty,
!manifest.bundleRelativePath.hasPrefix("/"),
!manifest.bundleRelativePath.split(separator: "/").contains("..")
else {
throw PluginPackageManifestError.invalidBundleRelativePath(manifest.bundleRelativePath)
}
}
private static func isValidPluginID(_ id: String) -> Bool {
let pattern = #"^[A-Za-z0-9][A-Za-z0-9._-]{1,126}[A-Za-z0-9]$"#
return id.range(of: pattern, options: .regularExpression) != nil
}
private static func isValidVersion(_ version: String) -> Bool {
let pattern = #"^[0-9]+(?:\.[0-9]+){0,2}$"#
return version.range(of: pattern, options: .regularExpression) != nil
}
private static func readManifest(from packageURL: URL) throws -> PluginPackageManifest {
let manifestURL = packageURL.appendingPathComponent(fileName, isDirectory: false)
guard FileManager.default.fileExists(atPath: manifestURL.path) else {
throw PluginPackageManifestError.missingManifest(manifestURL)
}
let data: Data
do {
data = try Data(contentsOf: manifestURL)
} catch {
throw PluginPackageManifestError.unreadableManifest(manifestURL)
}
do {
return try JSONDecoder().decode(PluginPackageManifest.self, from: data)
} catch {
throw PluginPackageManifestError.unreadableManifest(manifestURL)
}
}
}