XCode project for app, moved app project to folder
61
SideStoreApp/Sources/Cargo/Commands/Build.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
import ArgumentParser
|
||||
import SwiftLintFramework
|
||||
|
||||
extension SwiftLint {
|
||||
struct Build: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(abstract: "Print lint warnings and errors")
|
||||
|
||||
@OptionGroup
|
||||
var common: LintOrAnalyzeArguments
|
||||
@Option(help: pathOptionDescription(for: .build))
|
||||
var path: String?
|
||||
@Flag(help: quietOptionDescription(for: .build))
|
||||
var quiet = false
|
||||
@Option(help: "The directory of the cache used when linting.")
|
||||
var cachePath: String?
|
||||
@Flag(help: "Ignore cache when linting.")
|
||||
var noCache = false
|
||||
@Flag(help: "Run all rules, even opt-in and disabled ones, ignoring `only_rules`.")
|
||||
var enableAllRules = false
|
||||
@Argument(help: pathsArgumentDescription(for: .build))
|
||||
var paths = [String]()
|
||||
|
||||
func run() async throws {
|
||||
let allPaths: [String]
|
||||
if let path {
|
||||
queuedPrintError("""
|
||||
warning: The --path option is deprecated. Pass the path(s) to lint last to the swiftlint command.
|
||||
""")
|
||||
allPaths = [path] + paths
|
||||
} else if !paths.isEmpty {
|
||||
allPaths = paths
|
||||
} else {
|
||||
allPaths = [""] // Lint files in current working directory if no paths were specified.
|
||||
}
|
||||
let options = LintOrAnalyzeOptions(
|
||||
mode: .build,
|
||||
paths: allPaths,
|
||||
configurationFiles: common.config,
|
||||
strict: common.leniency == .strict,
|
||||
lenient: common.leniency == .lenient,
|
||||
forceExclude: common.forceExclude,
|
||||
useExcludingByPrefix: common.useAlternativeExcluding,
|
||||
useScriptInputFiles: common.useScriptInputFiles,
|
||||
benchmark: common.benchmark,
|
||||
reporter: common.reporter,
|
||||
quiet: quiet,
|
||||
output: common.output,
|
||||
progress: common.progress,
|
||||
cachePath: cachePath,
|
||||
ignoreCache: noCache,
|
||||
enableAllRules: enableAllRules,
|
||||
autocorrect: common.fix,
|
||||
format: common.format,
|
||||
compilerLogPath: nil,
|
||||
compileCommands: nil,
|
||||
inProcessSourcekit: common.inProcessSourcekit
|
||||
)
|
||||
try await LintOrAnalyzeCommand.run(options)
|
||||
}
|
||||
}
|
||||
}
|
||||
22
SideStoreApp/Sources/Cargo/Commands/Cargo.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
struct Cargo: AsyncParsableCommand {
|
||||
static let configuration: CommandConfiguration = {
|
||||
if let directory = ProcessInfo.processInfo.environment["BUILD_WORKSPACE_DIRECTORY"] {
|
||||
FileManager.default.changeCurrentDirectoryPath(directory)
|
||||
}
|
||||
|
||||
return CommandConfiguration(
|
||||
commandName: "cargo",
|
||||
abstract: "A tool to build `rust` projects with `cargo`.",
|
||||
version: Version.value,
|
||||
subcommands: [
|
||||
Build.self,
|
||||
Version.self
|
||||
],
|
||||
defaultSubcommand: Build.self
|
||||
)
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
extension RulesFilter.ExcludingOptions {
|
||||
static func excludingOptions(byCommandLineOptions rulesFilterOptions: RulesFilterOptions) -> Self {
|
||||
var excludingOptions: Self = []
|
||||
|
||||
switch rulesFilterOptions.ruleEnablement {
|
||||
case .enabled:
|
||||
excludingOptions.insert(.disabled)
|
||||
case .disabled:
|
||||
excludingOptions.insert(.enabled)
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
if rulesFilterOptions.correctable {
|
||||
excludingOptions.insert(.uncorrectable)
|
||||
}
|
||||
|
||||
return excludingOptions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import ArgumentParser
|
||||
|
||||
enum RuleEnablementOptions: String, EnumerableFlag {
|
||||
case enabled, disabled
|
||||
|
||||
static func name(for value: RuleEnablementOptions) -> NameSpecification {
|
||||
return .shortAndLong
|
||||
}
|
||||
|
||||
static func help(for value: RuleEnablementOptions) -> ArgumentHelp? {
|
||||
return "Only show \(value.rawValue) rules"
|
||||
}
|
||||
}
|
||||
|
||||
struct RulesFilterOptions: ParsableArguments {
|
||||
@Flag(exclusivity: .exclusive)
|
||||
var ruleEnablement: RuleEnablementOptions?
|
||||
@Flag(name: .shortAndLong, help: "Only display correctable rules")
|
||||
var correctable = false
|
||||
}
|
||||
23
SideStoreApp/Sources/Cargo/Commands/Version.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
import ArgumentParser
|
||||
import SwiftLintFramework
|
||||
|
||||
extension Cargo {
|
||||
struct Version: ParsableCommand {
|
||||
@Flag(help: "Display full version info")
|
||||
var verbose = false
|
||||
|
||||
static let configuration = CommandConfiguration(abstract: "Display the current version of Cargo")
|
||||
|
||||
static var value: String { "TODO" }
|
||||
|
||||
func run() throws {
|
||||
if verbose, let buildID = ExecutableInfo.buildID {
|
||||
print("Version:", Self.value)
|
||||
print("Build ID:", buildID)
|
||||
} else {
|
||||
print(Self.value)
|
||||
}
|
||||
ExitHelper.successfullyExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
61
SideStoreApp/Sources/Cargo/swiftlint/Commands/Analyze.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
import ArgumentParser
|
||||
import SwiftLintFramework
|
||||
|
||||
extension SwiftLint {
|
||||
struct Analyze: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(abstract: "Run analysis rules")
|
||||
|
||||
@OptionGroup
|
||||
var common: LintOrAnalyzeArguments
|
||||
@Option(help: pathOptionDescription(for: .analyze))
|
||||
var path: String?
|
||||
@Flag(help: quietOptionDescription(for: .analyze))
|
||||
var quiet = false
|
||||
@Option(help: "The path of the full xcodebuild log to use when running AnalyzerRules.")
|
||||
var compilerLogPath: String?
|
||||
@Option(help: "The path of a compilation database to use when running AnalyzerRules.")
|
||||
var compileCommands: String?
|
||||
@Argument(help: pathsArgumentDescription(for: .analyze))
|
||||
var paths = [String]()
|
||||
|
||||
func run() async throws {
|
||||
let allPaths: [String]
|
||||
if let path {
|
||||
queuedPrintError("""
|
||||
warning: The --path option is deprecated. Pass the path(s) to analyze last to the swiftlint command.
|
||||
""")
|
||||
allPaths = [path] + paths
|
||||
} else if !paths.isEmpty {
|
||||
allPaths = paths
|
||||
} else {
|
||||
allPaths = [""] // Analyze files in current working directory if no paths were specified.
|
||||
}
|
||||
let options = LintOrAnalyzeOptions(
|
||||
mode: .analyze,
|
||||
paths: allPaths,
|
||||
useSTDIN: false,
|
||||
configurationFiles: common.config,
|
||||
strict: common.leniency == .strict,
|
||||
lenient: common.leniency == .lenient,
|
||||
forceExclude: common.forceExclude,
|
||||
useExcludingByPrefix: common.useAlternativeExcluding,
|
||||
useScriptInputFiles: common.useScriptInputFiles,
|
||||
benchmark: common.benchmark,
|
||||
reporter: common.reporter,
|
||||
quiet: quiet,
|
||||
output: common.output,
|
||||
progress: common.progress,
|
||||
cachePath: nil,
|
||||
ignoreCache: true,
|
||||
enableAllRules: false,
|
||||
autocorrect: common.fix,
|
||||
format: common.format,
|
||||
compilerLogPath: compilerLogPath,
|
||||
compileCommands: compileCommands,
|
||||
inProcessSourcekit: common.inProcessSourcekit
|
||||
)
|
||||
|
||||
try await LintOrAnalyzeCommand.run(options)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
extension RulesFilter.ExcludingOptions {
|
||||
static func excludingOptions(byCommandLineOptions rulesFilterOptions: RulesFilterOptions) -> Self {
|
||||
var excludingOptions: Self = []
|
||||
|
||||
switch rulesFilterOptions.ruleEnablement {
|
||||
case .enabled:
|
||||
excludingOptions.insert(.disabled)
|
||||
case .disabled:
|
||||
excludingOptions.insert(.enabled)
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
if rulesFilterOptions.correctable {
|
||||
excludingOptions.insert(.uncorrectable)
|
||||
}
|
||||
|
||||
return excludingOptions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import ArgumentParser
|
||||
|
||||
enum RuleEnablementOptions: String, EnumerableFlag {
|
||||
case enabled, disabled
|
||||
|
||||
static func name(for value: RuleEnablementOptions) -> NameSpecification {
|
||||
return .shortAndLong
|
||||
}
|
||||
|
||||
static func help(for value: RuleEnablementOptions) -> ArgumentHelp? {
|
||||
return "Only show \(value.rawValue) rules"
|
||||
}
|
||||
}
|
||||
|
||||
struct RulesFilterOptions: ParsableArguments {
|
||||
@Flag(exclusivity: .exclusive)
|
||||
var ruleEnablement: RuleEnablementOptions?
|
||||
@Flag(name: .shortAndLong, help: "Only display correctable rules")
|
||||
var correctable = false
|
||||
}
|
||||
43
SideStoreApp/Sources/Cargo/swiftlint/Commands/Docs.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import SwiftLintFramework
|
||||
|
||||
extension SwiftLint {
|
||||
struct Docs: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Open SwiftLint documentation website in the default web browser"
|
||||
)
|
||||
|
||||
@Argument(help: "The identifier of the rule to open the documentation for")
|
||||
var ruleID: String?
|
||||
|
||||
func run() throws {
|
||||
var subPage = ""
|
||||
if let ruleID {
|
||||
if primaryRuleList.list[ruleID] == nil {
|
||||
queuedPrintError("There is no rule named '\(ruleID)'. Opening rule directory instead.")
|
||||
subPage = "rule-directory.html"
|
||||
} else {
|
||||
subPage = ruleID + ".html"
|
||||
}
|
||||
}
|
||||
open(URL(string: "https://realm.github.io/SwiftLint/\(subPage)")!)
|
||||
ExitHelper.successfullyExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func open(_ url: URL) {
|
||||
let process = Process()
|
||||
#if os(Linux)
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/env", isDirectory: false)
|
||||
let command = "xdg-open"
|
||||
process.arguments = [command, url.absoluteString]
|
||||
try? process.run()
|
||||
#else
|
||||
process.launchPath = "/usr/bin/env"
|
||||
let command = "open"
|
||||
process.arguments = [command, url.absoluteString]
|
||||
process.launch()
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import SwiftLintFramework
|
||||
|
||||
extension SwiftLint {
|
||||
struct GenerateDocs: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Generates markdown documentation for selected group of rules"
|
||||
)
|
||||
|
||||
@Option(help: "The directory where the documentation should be saved")
|
||||
var path = "rule_docs"
|
||||
@Option(help: "The path to a SwiftLint configuration file")
|
||||
var config: String?
|
||||
@OptionGroup
|
||||
var rulesFilterOptions: RulesFilterOptions
|
||||
|
||||
func run() throws {
|
||||
let configuration = Configuration(configurationFiles: [config].compactMap({ $0 }))
|
||||
let rulesFilter = RulesFilter(enabledRules: configuration.rules)
|
||||
let rules = rulesFilter.getRules(excluding: .excludingOptions(byCommandLineOptions: rulesFilterOptions))
|
||||
|
||||
try RuleListDocumentation(rules)
|
||||
.write(to: URL(fileURLWithPath: path, isDirectory: true))
|
||||
ExitHelper.successfullyExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
64
SideStoreApp/Sources/Cargo/swiftlint/Commands/Lint.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
import ArgumentParser
|
||||
import SwiftLintFramework
|
||||
|
||||
extension SwiftLint {
|
||||
struct Lint: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(abstract: "Print lint warnings and errors")
|
||||
|
||||
@OptionGroup
|
||||
var common: LintOrAnalyzeArguments
|
||||
@Option(help: pathOptionDescription(for: .lint))
|
||||
var path: String?
|
||||
@Flag(help: "Lint standard input.")
|
||||
var useSTDIN = false
|
||||
@Flag(help: quietOptionDescription(for: .lint))
|
||||
var quiet = false
|
||||
@Option(help: "The directory of the cache used when linting.")
|
||||
var cachePath: String?
|
||||
@Flag(help: "Ignore cache when linting.")
|
||||
var noCache = false
|
||||
@Flag(help: "Run all rules, even opt-in and disabled ones, ignoring `only_rules`.")
|
||||
var enableAllRules = false
|
||||
@Argument(help: pathsArgumentDescription(for: .lint))
|
||||
var paths = [String]()
|
||||
|
||||
func run() async throws {
|
||||
let allPaths: [String]
|
||||
if let path {
|
||||
queuedPrintError("""
|
||||
warning: The --path option is deprecated. Pass the path(s) to lint last to the swiftlint command.
|
||||
""")
|
||||
allPaths = [path] + paths
|
||||
} else if !paths.isEmpty {
|
||||
allPaths = paths
|
||||
} else {
|
||||
allPaths = [""] // Lint files in current working directory if no paths were specified.
|
||||
}
|
||||
let options = LintOrAnalyzeOptions(
|
||||
mode: .lint,
|
||||
paths: allPaths,
|
||||
useSTDIN: useSTDIN,
|
||||
configurationFiles: common.config,
|
||||
strict: common.leniency == .strict,
|
||||
lenient: common.leniency == .lenient,
|
||||
forceExclude: common.forceExclude,
|
||||
useExcludingByPrefix: common.useAlternativeExcluding,
|
||||
useScriptInputFiles: common.useScriptInputFiles,
|
||||
benchmark: common.benchmark,
|
||||
reporter: common.reporter,
|
||||
quiet: quiet,
|
||||
output: common.output,
|
||||
progress: common.progress,
|
||||
cachePath: cachePath,
|
||||
ignoreCache: noCache,
|
||||
enableAllRules: enableAllRules,
|
||||
autocorrect: common.fix,
|
||||
format: common.format,
|
||||
compilerLogPath: nil,
|
||||
compileCommands: nil,
|
||||
inProcessSourcekit: common.inProcessSourcekit
|
||||
)
|
||||
try await LintOrAnalyzeCommand.run(options)
|
||||
}
|
||||
}
|
||||
}
|
||||
123
SideStoreApp/Sources/Cargo/swiftlint/Commands/Rules.swift
Normal file
@@ -0,0 +1,123 @@
|
||||
import ArgumentParser
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#else
|
||||
#error("Unsupported platform")
|
||||
#endif
|
||||
import Foundation
|
||||
@_spi(TestHelper)
|
||||
import SwiftLintFramework
|
||||
import SwiftyTextTable
|
||||
|
||||
extension SwiftLint {
|
||||
struct Rules: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(abstract: "Display the list of rules and their identifiers")
|
||||
|
||||
@Option(help: "The path to a SwiftLint configuration file")
|
||||
var config: String?
|
||||
@OptionGroup
|
||||
var rulesFilterOptions: RulesFilterOptions
|
||||
@Flag(name: .shortAndLong, help: "Display full configuration details")
|
||||
var verbose = false
|
||||
@Argument(help: "The rule identifier to display description for")
|
||||
var ruleID: String?
|
||||
|
||||
func run() throws {
|
||||
if let ruleID {
|
||||
guard let rule = primaryRuleList.list[ruleID] else {
|
||||
throw SwiftLintError.usageError(description: "No rule with identifier: \(ruleID)")
|
||||
}
|
||||
|
||||
rule.description.printDescription()
|
||||
return
|
||||
}
|
||||
|
||||
let configuration = Configuration(configurationFiles: [config].compactMap({ $0 }))
|
||||
let rulesFilter = RulesFilter(enabledRules: configuration.rules)
|
||||
let rules = rulesFilter.getRules(excluding: .excludingOptions(byCommandLineOptions: rulesFilterOptions))
|
||||
let table = TextTable(ruleList: rules, configuration: configuration, verbose: verbose)
|
||||
print(table.render())
|
||||
ExitHelper.successfullyExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension RuleDescription {
|
||||
func printDescription() {
|
||||
print("\(consoleDescription)")
|
||||
|
||||
guard !triggeringExamples.isEmpty else { return }
|
||||
|
||||
func indent(_ string: String) -> String {
|
||||
return string.components(separatedBy: "\n")
|
||||
.map { " \($0)" }
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
print("\nTriggering Examples (violation is marked with '↓'):")
|
||||
for (index, example) in triggeringExamples.enumerated() {
|
||||
print("\nExample #\(index + 1)\n\n\(indent(example.code))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SwiftyTextTable
|
||||
|
||||
private extension TextTable {
|
||||
init(ruleList: RuleList, configuration: Configuration, verbose: Bool) {
|
||||
let columns = [
|
||||
TextTableColumn(header: "identifier"),
|
||||
TextTableColumn(header: "opt-in"),
|
||||
TextTableColumn(header: "correctable"),
|
||||
TextTableColumn(header: "enabled in your config"),
|
||||
TextTableColumn(header: "kind"),
|
||||
TextTableColumn(header: "analyzer"),
|
||||
TextTableColumn(header: "uses sourcekit"),
|
||||
TextTableColumn(header: "configuration")
|
||||
]
|
||||
self.init(columns: columns)
|
||||
let sortedRules = ruleList.list.sorted { $0.0 < $1.0 }
|
||||
func truncate(_ string: String) -> String {
|
||||
let stringWithNoNewlines = string.replacingOccurrences(of: "\n", with: "\\n")
|
||||
let minWidth = "configuration".count - "...".count
|
||||
let configurationStartColumn = 140
|
||||
let maxWidth = verbose ? Int.max : Terminal.currentWidth()
|
||||
let truncatedEndIndex = stringWithNoNewlines.index(
|
||||
stringWithNoNewlines.startIndex,
|
||||
offsetBy: max(minWidth, maxWidth - configurationStartColumn),
|
||||
limitedBy: stringWithNoNewlines.endIndex
|
||||
)
|
||||
if let truncatedEndIndex {
|
||||
return stringWithNoNewlines[..<truncatedEndIndex] + "..."
|
||||
}
|
||||
return stringWithNoNewlines
|
||||
}
|
||||
for (ruleID, ruleType) in sortedRules {
|
||||
let rule = ruleType.init()
|
||||
let configuredRule = configuration.configuredRule(forID: ruleID)
|
||||
addRow(values: [
|
||||
ruleID,
|
||||
(rule is OptInRule) ? "yes" : "no",
|
||||
(rule is CorrectableRule) ? "yes" : "no",
|
||||
configuredRule != nil ? "yes" : "no",
|
||||
ruleType.description.kind.rawValue,
|
||||
(rule is AnalyzerRule) ? "yes" : "no",
|
||||
(rule is SourceKitFreeRule) ? "no" : "yes",
|
||||
truncate((configuredRule ?? rule).configurationDescription)
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct Terminal {
|
||||
static func currentWidth() -> Int {
|
||||
var size = winsize()
|
||||
#if os(Linux)
|
||||
_ = ioctl(CInt(STDOUT_FILENO), UInt(TIOCGWINSZ), &size)
|
||||
#else
|
||||
_ = ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)
|
||||
#endif
|
||||
return Int(size.ws_col)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
struct SwiftLint: AsyncParsableCommand {
|
||||
static let configuration: CommandConfiguration = {
|
||||
if let directory = ProcessInfo.processInfo.environment["BUILD_WORKSPACE_DIRECTORY"] {
|
||||
FileManager.default.changeCurrentDirectoryPath(directory)
|
||||
}
|
||||
|
||||
return CommandConfiguration(
|
||||
commandName: "swiftlint",
|
||||
abstract: "A tool to enforce Swift style and conventions.",
|
||||
version: Version.value,
|
||||
subcommands: [
|
||||
Analyze.self,
|
||||
Docs.self,
|
||||
GenerateDocs.self,
|
||||
Lint.self,
|
||||
Rules.self,
|
||||
Version.self
|
||||
],
|
||||
defaultSubcommand: Lint.self
|
||||
)
|
||||
}()
|
||||
}
|
||||
23
SideStoreApp/Sources/Cargo/swiftlint/Commands/Version.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
import ArgumentParser
|
||||
import SwiftLintFramework
|
||||
|
||||
extension SwiftLint {
|
||||
struct Version: ParsableCommand {
|
||||
@Flag(help: "Display full version info")
|
||||
var verbose = false
|
||||
|
||||
static let configuration = CommandConfiguration(abstract: "Display the current version of SwiftLint")
|
||||
|
||||
static var value: String { SwiftLintFramework.Version.current.value }
|
||||
|
||||
func run() throws {
|
||||
if verbose, let buildID = ExecutableInfo.buildID {
|
||||
print("Version:", Self.value)
|
||||
print("Build ID:", buildID)
|
||||
} else {
|
||||
print(Self.value)
|
||||
}
|
||||
ExitHelper.successfullyExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import CollectionConcurrencyKit
|
||||
import Foundation
|
||||
import SourceKittenFramework
|
||||
import SwiftLintFramework
|
||||
|
||||
private actor CounterActor {
|
||||
private var count = 0
|
||||
|
||||
func next() -> Int {
|
||||
count += 1
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
||||
private func scriptInputFiles() throws -> [SwiftLintFile] {
|
||||
let inputFileKey = "SCRIPT_INPUT_FILE_COUNT"
|
||||
guard let countString = ProcessInfo.processInfo.environment[inputFileKey] else {
|
||||
throw SwiftLintError.usageError(description: "\(inputFileKey) variable not set")
|
||||
}
|
||||
|
||||
guard let count = Int(countString) else {
|
||||
throw SwiftLintError.usageError(description: "\(inputFileKey) did not specify a number")
|
||||
}
|
||||
|
||||
return (0..<count).compactMap { fileNumber in
|
||||
do {
|
||||
let environment = ProcessInfo.processInfo.environment
|
||||
let variable = "SCRIPT_INPUT_FILE_\(fileNumber)"
|
||||
guard let path = environment[variable] else {
|
||||
throw SwiftLintError.usageError(description: "Environment variable not set: \(variable)")
|
||||
}
|
||||
if path.bridge().isSwiftFile() {
|
||||
return SwiftLintFile(pathDeferringReading: path)
|
||||
}
|
||||
return nil
|
||||
} catch {
|
||||
queuedPrintError(String(describing: error))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if os(Linux)
|
||||
private func autoreleasepool<T>(block: () -> T) -> T { return block() }
|
||||
#endif
|
||||
|
||||
extension Configuration {
|
||||
func visitLintableFiles(with visitor: LintableFilesVisitor, storage: RuleStorage) async throws -> [SwiftLintFile] {
|
||||
let files = try await Signposts.record(name: "Configuration.VisitLintableFiles.GetFiles") {
|
||||
try await getFiles(with: visitor)
|
||||
}
|
||||
let groupedFiles = try Signposts.record(name: "Configuration.VisitLintableFiles.GroupFiles") {
|
||||
try groupFiles(files, visitor: visitor)
|
||||
}
|
||||
let lintersForFile = Signposts.record(name: "Configuration.VisitLintableFiles.LintersForFile") {
|
||||
groupedFiles.map { file in
|
||||
linters(for: [file.key: file.value], visitor: visitor)
|
||||
}
|
||||
}
|
||||
let duplicateFileNames = Signposts.record(name: "Configuration.VisitLintableFiles.DuplicateFileNames") {
|
||||
lintersForFile.map(\.duplicateFileNames)
|
||||
}
|
||||
let collected = await Signposts.record(name: "Configuration.VisitLintableFiles.Collect") {
|
||||
await zip(lintersForFile, duplicateFileNames).asyncMap { linters, duplicateFileNames in
|
||||
await collect(linters: linters, visitor: visitor, storage: storage,
|
||||
duplicateFileNames: duplicateFileNames)
|
||||
}
|
||||
}
|
||||
let result = await Signposts.record(name: "Configuration.VisitLintableFiles.Visit") {
|
||||
await collected.asyncMap { linters, duplicateFileNames in
|
||||
await visit(linters: linters, visitor: visitor, duplicateFileNames: duplicateFileNames)
|
||||
}
|
||||
}
|
||||
return result.flatMap { $0 }
|
||||
}
|
||||
|
||||
private func groupFiles(_ files: [SwiftLintFile], visitor: LintableFilesVisitor) throws
|
||||
-> [Configuration: [SwiftLintFile]] {
|
||||
if files.isEmpty && !visitor.allowZeroLintableFiles {
|
||||
throw SwiftLintError.usageError(
|
||||
description: "No lintable files found at paths: '\(visitor.paths.joined(separator: ", "))'"
|
||||
)
|
||||
}
|
||||
|
||||
var groupedFiles = [Configuration: [SwiftLintFile]]()
|
||||
for file in files {
|
||||
let fileConfiguration = configuration(for: file)
|
||||
let fileConfigurationRootPath = fileConfiguration.rootDirectory.bridge()
|
||||
|
||||
// Files whose configuration specifies they should be excluded will be skipped
|
||||
let shouldSkip = fileConfiguration.excludedPaths.contains { excludedRelativePath in
|
||||
let excludedPath = fileConfigurationRootPath.appendingPathComponent(excludedRelativePath)
|
||||
let filePathComponents = file.path?.bridge().pathComponents ?? []
|
||||
let excludedPathComponents = excludedPath.bridge().pathComponents
|
||||
return filePathComponents.starts(with: excludedPathComponents)
|
||||
}
|
||||
|
||||
if !shouldSkip {
|
||||
groupedFiles[fileConfiguration, default: []].append(file)
|
||||
}
|
||||
}
|
||||
|
||||
return groupedFiles
|
||||
}
|
||||
|
||||
private func outputFilename(for path: String, duplicateFileNames: Set<String>) -> String {
|
||||
let basename = path.bridge().lastPathComponent
|
||||
if !duplicateFileNames.contains(basename) {
|
||||
return basename
|
||||
}
|
||||
|
||||
var pathComponents = path.bridge().pathComponents
|
||||
for component in rootDirectory.bridge().pathComponents where pathComponents.first == component {
|
||||
pathComponents.removeFirst()
|
||||
}
|
||||
|
||||
return pathComponents.joined(separator: "/")
|
||||
}
|
||||
|
||||
private func linters(for filesPerConfiguration: [Configuration: [SwiftLintFile]],
|
||||
visitor: LintableFilesVisitor) -> [Linter] {
|
||||
let fileCount = filesPerConfiguration.reduce(0) { $0 + $1.value.count }
|
||||
|
||||
var linters = [Linter]()
|
||||
linters.reserveCapacity(fileCount)
|
||||
for (config, files) in filesPerConfiguration {
|
||||
let newConfig: Configuration
|
||||
if visitor.cache != nil {
|
||||
newConfig = config.withPrecomputedCacheDescription()
|
||||
} else {
|
||||
newConfig = config
|
||||
}
|
||||
linters += files.map { visitor.linter(forFile: $0, configuration: newConfig) }
|
||||
}
|
||||
return linters
|
||||
}
|
||||
|
||||
private func collect(linters: [Linter],
|
||||
visitor: LintableFilesVisitor,
|
||||
storage: RuleStorage,
|
||||
duplicateFileNames: Set<String>) async -> ([CollectedLinter], Set<String>) {
|
||||
let counter = CounterActor()
|
||||
let total = linters.filter(\.isCollecting).count
|
||||
let progress = ProgressBar(count: total)
|
||||
if visitor.showProgressBar && total > 0 {
|
||||
await progress.initialize()
|
||||
}
|
||||
let collect = { (linter: Linter) -> CollectedLinter? in
|
||||
let skipFile = visitor.shouldSkipFile(atPath: linter.file.path)
|
||||
if !visitor.quiet && linter.isCollecting {
|
||||
if visitor.showProgressBar {
|
||||
await progress.printNext()
|
||||
} else if let filePath = linter.file.path {
|
||||
let outputFilename = self.outputFilename(for: filePath, duplicateFileNames: duplicateFileNames)
|
||||
let collected = await counter.next()
|
||||
if skipFile {
|
||||
queuedPrintError("""
|
||||
Skipping '\(outputFilename)' (\(collected)/\(total)) \
|
||||
because its compiler arguments could not be found
|
||||
""")
|
||||
} else {
|
||||
queuedPrintError("Collecting '\(outputFilename)' (\(collected)/\(total))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard !skipFile else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return autoreleasepool {
|
||||
linter.collect(into: storage)
|
||||
}
|
||||
}
|
||||
|
||||
let collectedLinters = await visitor.parallel ?
|
||||
linters.concurrentCompactMap(collect) :
|
||||
linters.asyncCompactMap(collect)
|
||||
return (collectedLinters, duplicateFileNames)
|
||||
}
|
||||
|
||||
private func visit(linters: [CollectedLinter],
|
||||
visitor: LintableFilesVisitor,
|
||||
duplicateFileNames: Set<String>) async -> [SwiftLintFile] {
|
||||
let counter = CounterActor()
|
||||
let progress = ProgressBar(count: linters.count)
|
||||
if visitor.showProgressBar {
|
||||
await progress.initialize()
|
||||
}
|
||||
let visit = { (linter: CollectedLinter) -> SwiftLintFile in
|
||||
if !visitor.quiet {
|
||||
if visitor.showProgressBar {
|
||||
await progress.printNext()
|
||||
} else if let filePath = linter.file.path {
|
||||
let outputFilename = self.outputFilename(for: filePath, duplicateFileNames: duplicateFileNames)
|
||||
let visited = await counter.next()
|
||||
queuedPrintError("\(visitor.action) '\(outputFilename)' (\(visited)/\(linters.count))")
|
||||
}
|
||||
}
|
||||
|
||||
await Signposts.record(name: "Configuration.Visit", span: .file(linter.file.path ?? "")) {
|
||||
await visitor.block(linter)
|
||||
}
|
||||
return linter.file
|
||||
}
|
||||
return await visitor.parallel ?
|
||||
linters.concurrentMap(visit) :
|
||||
linters.asyncMap(visit)
|
||||
}
|
||||
|
||||
fileprivate func getFiles(with visitor: LintableFilesVisitor) async throws -> [SwiftLintFile] {
|
||||
if visitor.useSTDIN {
|
||||
let stdinData = FileHandle.standardInput.readDataToEndOfFile()
|
||||
if let stdinString = String(data: stdinData, encoding: .utf8) {
|
||||
return [SwiftLintFile(contents: stdinString)]
|
||||
}
|
||||
throw SwiftLintError.usageError(description: "stdin isn't a UTF8-encoded string")
|
||||
} else if visitor.useScriptInputFiles {
|
||||
let files = try scriptInputFiles()
|
||||
guard visitor.forceExclude else {
|
||||
return files
|
||||
}
|
||||
|
||||
let scriptInputPaths = files.compactMap { $0.path }
|
||||
let filesToLint = visitor.useExcludingByPrefix ?
|
||||
filterExcludedPathsByPrefix(in: scriptInputPaths) :
|
||||
filterExcludedPaths(in: scriptInputPaths)
|
||||
return filesToLint.map(SwiftLintFile.init(pathDeferringReading:))
|
||||
}
|
||||
if !visitor.quiet {
|
||||
let filesInfo: String
|
||||
if visitor.paths.isEmpty || visitor.paths == [""] {
|
||||
filesInfo = "in current working directory"
|
||||
} else {
|
||||
filesInfo = "at paths \(visitor.paths.joined(separator: ", "))"
|
||||
}
|
||||
|
||||
queuedPrintError("\(visitor.action) Swift files \(filesInfo)")
|
||||
}
|
||||
return visitor.paths.flatMap {
|
||||
self.lintableFiles(inPath: $0, forceExclude: visitor.forceExclude,
|
||||
excludeByPrefix: visitor.useExcludingByPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
func visitLintableFiles(options: LintOrAnalyzeOptions, cache: LinterCache? = nil, storage: RuleStorage,
|
||||
visitorBlock: @escaping (CollectedLinter) async -> Void) async throws -> [SwiftLintFile] {
|
||||
let visitor = try LintableFilesVisitor.create(options, cache: cache,
|
||||
allowZeroLintableFiles: allowZeroLintableFiles,
|
||||
block: visitorBlock)
|
||||
return try await visitLintableFiles(with: visitor, storage: storage)
|
||||
}
|
||||
|
||||
// MARK: LintOrAnalyze Command
|
||||
|
||||
init(options: LintOrAnalyzeOptions) {
|
||||
self.init(
|
||||
configurationFiles: options.configurationFiles,
|
||||
enableAllRules: options.enableAllRules,
|
||||
cachePath: options.cachePath
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct DuplicateCollector {
|
||||
var all = Set<String>()
|
||||
var duplicates = Set<String>()
|
||||
}
|
||||
|
||||
private extension Collection where Element == Linter {
|
||||
var duplicateFileNames: Set<String> {
|
||||
let collector = reduce(into: DuplicateCollector()) { result, linter in
|
||||
if let filename = linter.file.path?.bridge().lastPathComponent {
|
||||
if result.all.contains(filename) {
|
||||
result.duplicates.insert(filename)
|
||||
}
|
||||
|
||||
result.all.insert(filename)
|
||||
}
|
||||
}
|
||||
return collector.duplicates
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import Foundation
|
||||
|
||||
extension ProcessInfo {
|
||||
var isLikelyXcodeCloudEnvironment: Bool {
|
||||
// https://developer.apple.com/documentation/xcode/environment-variable-reference
|
||||
let requiredKeys: Set = [
|
||||
"CI",
|
||||
"CI_BUILD_ID",
|
||||
"CI_BUILD_NUMBER",
|
||||
"CI_BUNDLE_ID",
|
||||
"CI_COMMIT",
|
||||
"CI_DERIVED_DATA_PATH",
|
||||
"CI_PRODUCT",
|
||||
"CI_PRODUCT_ID",
|
||||
"CI_PRODUCT_PLATFORM",
|
||||
"CI_PROJECT_FILE_PATH",
|
||||
"CI_START_CONDITION",
|
||||
"CI_TEAM_ID",
|
||||
"CI_WORKFLOW",
|
||||
"CI_WORKSPACE",
|
||||
"CI_XCODE_PROJECT",
|
||||
"CI_XCODE_SCHEME",
|
||||
"CI_XCODEBUILD_ACTION"
|
||||
]
|
||||
|
||||
return requiredKeys.isSubset(of: environment.keys)
|
||||
}
|
||||
}
|
||||
52
SideStoreApp/Sources/Cargo/swiftlint/Helpers/Benchmark.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
import Foundation
|
||||
import SwiftLintFramework
|
||||
|
||||
struct BenchmarkEntry {
|
||||
let id: String
|
||||
let time: Double
|
||||
}
|
||||
|
||||
struct Benchmark {
|
||||
private let name: String
|
||||
private var entries = [BenchmarkEntry]()
|
||||
|
||||
init(name: String) {
|
||||
self.name = name
|
||||
}
|
||||
|
||||
mutating func record(id: String, time: Double) {
|
||||
guard id != "custom_rules" else { return }
|
||||
entries.append(BenchmarkEntry(id: id, time: time))
|
||||
}
|
||||
|
||||
mutating func record(file: SwiftLintFile, from start: Date) {
|
||||
record(id: file.path ?? "<nopath>", time: -start.timeIntervalSinceNow)
|
||||
}
|
||||
|
||||
func save() {
|
||||
// Decomposed to improve compile times
|
||||
let entriesDict: [String: Double] = entries.reduce(into: [String: Double]()) { accu, idAndTime in
|
||||
accu[idAndTime.id] = (accu[idAndTime.id] ?? 0) + idAndTime.time
|
||||
}
|
||||
let entriesKeyValues: [(String, Double)] = entriesDict.sorted { $0.1 < $1.1 }
|
||||
let lines: [String] = entriesKeyValues.map { id, time -> String in
|
||||
return "\(numberFormatter.string(from: NSNumber(value: time))!): \(id)"
|
||||
}
|
||||
let string: String = lines.joined(separator: "\n") + "\n"
|
||||
let url = URL(fileURLWithPath: "benchmark_\(name)_\(timestamp).txt", isDirectory: false)
|
||||
try? string.data(using: .utf8)?.write(to: url, options: [.atomic])
|
||||
}
|
||||
}
|
||||
|
||||
private let numberFormatter: NumberFormatter = {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .decimal
|
||||
formatter.minimumFractionDigits = 3
|
||||
return formatter
|
||||
}()
|
||||
|
||||
private let timestamp: String = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy_MM_dd_HH_mm_ss"
|
||||
return formatter.string(from: Date())
|
||||
}()
|
||||
@@ -0,0 +1,108 @@
|
||||
import Foundation
|
||||
|
||||
struct CompilerArgumentsExtractor {
|
||||
static func allCompilerInvocations(compilerLogs: String) -> [[String]] {
|
||||
var compilerInvocations = [[String]]()
|
||||
compilerLogs.enumerateLines { line, _ in
|
||||
if let swiftcIndex = line.range(of: "swiftc ")?.upperBound, line.contains(" -module-name ") {
|
||||
let invocation = parseCLIArguments(String(line[swiftcIndex...]))
|
||||
.expandingResponseFiles
|
||||
.filteringCompilerArguments
|
||||
compilerInvocations.append(invocation)
|
||||
}
|
||||
}
|
||||
return compilerInvocations
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func parseCLIArguments(_ string: String) -> [String] {
|
||||
let escapedSpacePlaceholder = "\u{0}"
|
||||
let scanner = Scanner(string: string)
|
||||
var str = ""
|
||||
var didStart = false
|
||||
while let result = scanner.scanUpToString("\"") {
|
||||
if didStart {
|
||||
str += result.replacingOccurrences(of: " ", with: escapedSpacePlaceholder)
|
||||
str += " "
|
||||
} else {
|
||||
str += result
|
||||
}
|
||||
_ = scanner.scanString("\"")
|
||||
didStart.toggle()
|
||||
}
|
||||
return str.trimmingCharacters(in: .whitespaces)
|
||||
.replacingOccurrences(of: "\\ ", with: escapedSpacePlaceholder)
|
||||
.components(separatedBy: " ")
|
||||
.map { $0.replacingOccurrences(of: escapedSpacePlaceholder, with: " ") }
|
||||
}
|
||||
|
||||
/**
|
||||
Partially filters compiler arguments from `xcodebuild` to something that SourceKit/Clang will accept.
|
||||
|
||||
- parameter args: Compiler arguments, as parsed from `xcodebuild`.
|
||||
|
||||
- returns: A tuple of partially filtered compiler arguments in `.0`, and whether or not there are
|
||||
more flags to remove in `.1`.
|
||||
*/
|
||||
private func partiallyFilter(arguments args: [String]) -> ([String], Bool) {
|
||||
guard let indexOfFlagToRemove = args.firstIndex(of: "-output-file-map") else {
|
||||
return (args, false)
|
||||
}
|
||||
var args = args
|
||||
args.remove(at: args.index(after: indexOfFlagToRemove))
|
||||
args.remove(at: indexOfFlagToRemove)
|
||||
return (args, true)
|
||||
}
|
||||
|
||||
extension Array where Element == String {
|
||||
/// Return the full list of compiler arguments, replacing any response files with their contents.
|
||||
fileprivate var expandingResponseFiles: [String] {
|
||||
return flatMap { arg -> [String] in
|
||||
guard arg.starts(with: "@") else {
|
||||
return [arg]
|
||||
}
|
||||
let responseFile = String(arg.dropFirst())
|
||||
return (try? String(contentsOf: URL(fileURLWithPath: responseFile, isDirectory: false))).flatMap {
|
||||
$0.trimmingCharacters(in: .newlines)
|
||||
.components(separatedBy: "\n")
|
||||
.expandingResponseFiles
|
||||
} ?? [arg]
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns filtered compiler arguments from `xcodebuild` to something that SourceKit/Clang will accept.
|
||||
var filteringCompilerArguments: [String] {
|
||||
var args = self
|
||||
if args.first == "swiftc" {
|
||||
args.removeFirst()
|
||||
}
|
||||
|
||||
// https://github.com/realm/SwiftLint/issues/3365
|
||||
args = args.map { $0.replacingOccurrences(of: "\\=", with: "=") }
|
||||
args = args.map { $0.replacingOccurrences(of: "\\ ", with: " ") }
|
||||
args.append(contentsOf: ["-D", "DEBUG"])
|
||||
var shouldContinueToFilterArguments = true
|
||||
while shouldContinueToFilterArguments {
|
||||
(args, shouldContinueToFilterArguments) = partiallyFilter(arguments: args)
|
||||
}
|
||||
|
||||
return args.filter {
|
||||
![
|
||||
"-parseable-output",
|
||||
"-incremental",
|
||||
"-serialize-diagnostics",
|
||||
"-emit-dependencies",
|
||||
"-use-frontend-parseable-output"
|
||||
].contains($0)
|
||||
}.map {
|
||||
if $0 == "-O" {
|
||||
return "-Onone"
|
||||
} else if $0 == "-DNDEBUG=1" {
|
||||
return "-DDEBUG=1"
|
||||
}
|
||||
return $0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#if os(Linux)
|
||||
import Glibc
|
||||
#endif
|
||||
|
||||
enum ExitHelper {
|
||||
static func successfullyExit() {
|
||||
#if os(Linux)
|
||||
// Workaround for https://github.com/apple/swift/issues/59961
|
||||
Glibc.exit(0)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import ArgumentParser
|
||||
|
||||
enum LeniencyOptions: String, EnumerableFlag {
|
||||
case strict, lenient
|
||||
|
||||
static func help(for value: LeniencyOptions) -> ArgumentHelp? {
|
||||
switch value {
|
||||
case .strict:
|
||||
return "Upgrades warnings to serious violations (errors)."
|
||||
case .lenient:
|
||||
return "Downgrades serious violations to warnings, warning threshold is disabled."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Common Arguments
|
||||
|
||||
struct LintOrAnalyzeArguments: ParsableArguments {
|
||||
@Option(help: "The path to one or more SwiftLint configuration files, evaluated as a parent-child hierarchy.")
|
||||
var config = [String]()
|
||||
@Flag(name: [.long, .customLong("autocorrect")], help: "Correct violations whenever possible.")
|
||||
var fix = false
|
||||
@Flag(help: """
|
||||
Should reformat the Swift files using the same mechanism used by Xcode (via SourceKit).
|
||||
Only applied with `--fix`/`--autocorrect`.
|
||||
""")
|
||||
var format = false
|
||||
@Flag(help: "Use an alternative algorithm to exclude paths for `excluded`, which may be faster in some cases.")
|
||||
var useAlternativeExcluding = false
|
||||
@Flag(help: "Read SCRIPT_INPUT_FILE* environment variables as files.")
|
||||
var useScriptInputFiles = false
|
||||
@Flag(exclusivity: .exclusive)
|
||||
var leniency: LeniencyOptions?
|
||||
@Flag(help: "Exclude files in config `excluded` even if their paths are explicitly specified.")
|
||||
var forceExclude = false
|
||||
@Flag(help: "Save benchmarks to `benchmark_files.txt` and `benchmark_rules.txt`.")
|
||||
var benchmark = false
|
||||
@Option(help: "The reporter used to log errors and warnings.")
|
||||
var reporter: String?
|
||||
@Flag(help: "Use the in-process version of SourceKit.")
|
||||
var inProcessSourcekit = false
|
||||
@Option(help: "The file where violations should be saved. Prints to stdout by default.")
|
||||
var output: String?
|
||||
@Flag(help: "Show a live-updating progress bar instead of each file being processed.")
|
||||
var progress = false
|
||||
}
|
||||
|
||||
// MARK: - Common Argument Help
|
||||
|
||||
// It'd be great to be able to parameterize an `@OptionGroup` so we could move these options into
|
||||
// `LintOrAnalyzeArguments`.
|
||||
|
||||
func pathOptionDescription(for mode: LintOrAnalyzeMode) -> ArgumentHelp {
|
||||
ArgumentHelp(visibility: .hidden)
|
||||
}
|
||||
|
||||
func pathsArgumentDescription(for mode: LintOrAnalyzeMode) -> ArgumentHelp {
|
||||
"List of paths to the files or directories to \(mode.imperative)."
|
||||
}
|
||||
|
||||
func quietOptionDescription(for mode: LintOrAnalyzeMode) -> ArgumentHelp {
|
||||
"Don't print status logs like '\(mode.verb.capitalized) <file>' & 'Done \(mode.verb)'."
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
import Dispatch
|
||||
import Foundation
|
||||
@_spi(TestHelper)
|
||||
import SwiftLintFramework
|
||||
|
||||
enum LintOrAnalyzeMode {
|
||||
case lint, analyze
|
||||
|
||||
var imperative: String {
|
||||
switch self {
|
||||
case .lint:
|
||||
return "lint"
|
||||
case .analyze:
|
||||
return "analyze"
|
||||
}
|
||||
}
|
||||
|
||||
var verb: String {
|
||||
switch self {
|
||||
case .lint:
|
||||
return "linting"
|
||||
case .analyze:
|
||||
return "analyzing"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LintOrAnalyzeCommand {
|
||||
static func run(_ options: LintOrAnalyzeOptions) async throws {
|
||||
if options.inProcessSourcekit {
|
||||
queuedPrintError(
|
||||
"""
|
||||
warning: The --in-process-sourcekit option is deprecated. \
|
||||
SwiftLint now always uses an in-process SourceKit.
|
||||
"""
|
||||
)
|
||||
}
|
||||
try await Signposts.record(name: "LintOrAnalyzeCommand.run") {
|
||||
try await options.autocorrect ? autocorrect(options) : lintOrAnalyze(options)
|
||||
}
|
||||
ExitHelper.successfullyExit()
|
||||
}
|
||||
|
||||
private static func lintOrAnalyze(_ options: LintOrAnalyzeOptions) async throws {
|
||||
let builder = LintOrAnalyzeResultBuilder(options)
|
||||
let files = try await collectViolations(builder: builder)
|
||||
try Signposts.record(name: "LintOrAnalyzeCommand.PostProcessViolations") {
|
||||
try postProcessViolations(files: files, builder: builder)
|
||||
}
|
||||
}
|
||||
|
||||
private static func collectViolations(builder: LintOrAnalyzeResultBuilder) async throws -> [SwiftLintFile] {
|
||||
let options = builder.options
|
||||
let visitorMutationQueue = DispatchQueue(label: "io.realm.swiftlint.lintVisitorMutation")
|
||||
return try await builder.configuration.visitLintableFiles(options: options, cache: builder.cache,
|
||||
storage: builder.storage) { linter in
|
||||
let currentViolations: [StyleViolation]
|
||||
if options.benchmark {
|
||||
CustomRuleTimer.shared.activate()
|
||||
let start = Date()
|
||||
let (violationsBeforeLeniency, currentRuleTimes) = linter
|
||||
.styleViolationsAndRuleTimes(using: builder.storage)
|
||||
currentViolations = applyLeniency(options: options, violations: violationsBeforeLeniency)
|
||||
visitorMutationQueue.sync {
|
||||
builder.fileBenchmark.record(file: linter.file, from: start)
|
||||
currentRuleTimes.forEach { builder.ruleBenchmark.record(id: $0, time: $1) }
|
||||
builder.violations += currentViolations
|
||||
}
|
||||
} else {
|
||||
currentViolations = applyLeniency(options: options,
|
||||
violations: linter.styleViolations(using: builder.storage))
|
||||
visitorMutationQueue.sync {
|
||||
builder.violations += currentViolations
|
||||
}
|
||||
}
|
||||
linter.file.invalidateCache()
|
||||
builder.report(violations: currentViolations, realtimeCondition: true)
|
||||
}
|
||||
}
|
||||
|
||||
private static func postProcessViolations(files: [SwiftLintFile], builder: LintOrAnalyzeResultBuilder) throws {
|
||||
let options = builder.options
|
||||
let configuration = builder.configuration
|
||||
if isWarningThresholdBroken(configuration: configuration, violations: builder.violations)
|
||||
&& !options.lenient {
|
||||
builder.violations.append(
|
||||
createThresholdViolation(threshold: configuration.warningThreshold!)
|
||||
)
|
||||
builder.report(violations: [builder.violations.last!], realtimeCondition: true)
|
||||
}
|
||||
builder.report(violations: builder.violations, realtimeCondition: false)
|
||||
let numberOfSeriousViolations = builder.violations.filter({ $0.severity == .error }).count
|
||||
if !options.quiet {
|
||||
printStatus(violations: builder.violations, files: files, serious: numberOfSeriousViolations,
|
||||
verb: options.verb)
|
||||
}
|
||||
if options.benchmark {
|
||||
builder.fileBenchmark.save()
|
||||
for (id, time) in CustomRuleTimer.shared.dump() {
|
||||
builder.ruleBenchmark.record(id: id, time: time)
|
||||
}
|
||||
builder.ruleBenchmark.save()
|
||||
if !options.quiet, let memoryUsage = memoryUsage() {
|
||||
queuedPrintError(memoryUsage)
|
||||
}
|
||||
}
|
||||
try builder.cache?.save()
|
||||
guard numberOfSeriousViolations == 0 else { exit(2) }
|
||||
}
|
||||
|
||||
private static func printStatus(violations: [StyleViolation], files: [SwiftLintFile], serious: Int, verb: String) {
|
||||
let pluralSuffix = { (collection: [Any]) -> String in
|
||||
return collection.count != 1 ? "s" : ""
|
||||
}
|
||||
queuedPrintError(
|
||||
"Done \(verb)! Found \(violations.count) violation\(pluralSuffix(violations)), " +
|
||||
"\(serious) serious in \(files.count) file\(pluralSuffix(files))."
|
||||
)
|
||||
}
|
||||
|
||||
private static func isWarningThresholdBroken(configuration: Configuration,
|
||||
violations: [StyleViolation]) -> Bool {
|
||||
guard let warningThreshold = configuration.warningThreshold else { return false }
|
||||
let numberOfWarningViolations = violations.filter({ $0.severity == .warning }).count
|
||||
return numberOfWarningViolations >= warningThreshold
|
||||
}
|
||||
|
||||
private static func createThresholdViolation(threshold: Int) -> StyleViolation {
|
||||
let description = RuleDescription(
|
||||
identifier: "warning_threshold",
|
||||
name: "Warning Threshold",
|
||||
description: "Number of warnings thrown is above the threshold",
|
||||
kind: .lint
|
||||
)
|
||||
return StyleViolation(
|
||||
ruleDescription: description,
|
||||
severity: .error,
|
||||
location: Location(file: "", line: 0, character: 0),
|
||||
reason: "Number of warnings exceeded threshold of \(threshold).")
|
||||
}
|
||||
|
||||
private static func applyLeniency(options: LintOrAnalyzeOptions, violations: [StyleViolation]) -> [StyleViolation] {
|
||||
switch (options.lenient, options.strict) {
|
||||
case (false, false):
|
||||
return violations
|
||||
|
||||
case (true, false):
|
||||
return violations.map {
|
||||
if $0.severity == .error {
|
||||
return $0.with(severity: .warning)
|
||||
} else {
|
||||
return $0
|
||||
}
|
||||
}
|
||||
|
||||
case (false, true):
|
||||
return violations.map {
|
||||
if $0.severity == .warning {
|
||||
return $0.with(severity: .error)
|
||||
} else {
|
||||
return $0
|
||||
}
|
||||
}
|
||||
|
||||
case (true, true):
|
||||
queuedFatalError("Invalid command line options: 'lenient' and 'strict' are mutually exclusive.")
|
||||
}
|
||||
}
|
||||
|
||||
private static func autocorrect(_ options: LintOrAnalyzeOptions) async throws {
|
||||
let storage = RuleStorage()
|
||||
let configuration = Configuration(options: options)
|
||||
let correctionsBuilder = CorrectionsBuilder()
|
||||
let files = try await configuration
|
||||
.visitLintableFiles(options: options, cache: nil, storage: storage) { linter in
|
||||
if options.format {
|
||||
switch configuration.indentation {
|
||||
case .tabs:
|
||||
linter.format(useTabs: true, indentWidth: 4)
|
||||
case .spaces(let count):
|
||||
linter.format(useTabs: false, indentWidth: count)
|
||||
}
|
||||
}
|
||||
|
||||
let corrections = linter.correct(using: storage)
|
||||
if !corrections.isEmpty && !options.quiet {
|
||||
if options.useSTDIN {
|
||||
queuedPrint(linter.file.contents)
|
||||
} else {
|
||||
if options.progress {
|
||||
await correctionsBuilder.append(corrections)
|
||||
} else {
|
||||
let correctionLogs = corrections.map(\.consoleDescription)
|
||||
queuedPrint(correctionLogs.joined(separator: "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !options.quiet {
|
||||
if options.progress {
|
||||
let corrections = await correctionsBuilder.corrections
|
||||
if !corrections.isEmpty {
|
||||
let correctionLogs = corrections.map(\.consoleDescription)
|
||||
options.writeToOutput(correctionLogs.joined(separator: "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
let pluralSuffix = { (collection: [Any]) -> String in
|
||||
return collection.count != 1 ? "s" : ""
|
||||
}
|
||||
queuedPrintError("Done correcting \(files.count) file\(pluralSuffix(files))!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LintOrAnalyzeOptions {
|
||||
let mode: LintOrAnalyzeMode
|
||||
let paths: [String]
|
||||
let useSTDIN: Bool
|
||||
let configurationFiles: [String]
|
||||
let strict: Bool
|
||||
let lenient: Bool
|
||||
let forceExclude: Bool
|
||||
let useExcludingByPrefix: Bool
|
||||
let useScriptInputFiles: Bool
|
||||
let benchmark: Bool
|
||||
let reporter: String?
|
||||
let quiet: Bool
|
||||
let output: String?
|
||||
let progress: Bool
|
||||
let cachePath: String?
|
||||
let ignoreCache: Bool
|
||||
let enableAllRules: Bool
|
||||
let autocorrect: Bool
|
||||
let format: Bool
|
||||
let compilerLogPath: String?
|
||||
let compileCommands: String?
|
||||
let inProcessSourcekit: Bool
|
||||
|
||||
var verb: String {
|
||||
if autocorrect {
|
||||
return "correcting"
|
||||
} else {
|
||||
return mode.verb
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LintOrAnalyzeResultBuilder {
|
||||
var fileBenchmark = Benchmark(name: "files")
|
||||
var ruleBenchmark = Benchmark(name: "rules")
|
||||
var violations = [StyleViolation]()
|
||||
let storage = RuleStorage()
|
||||
let configuration: Configuration
|
||||
let reporter: Reporter.Type
|
||||
let cache: LinterCache?
|
||||
let options: LintOrAnalyzeOptions
|
||||
|
||||
init(_ options: LintOrAnalyzeOptions) {
|
||||
let config = Signposts.record(name: "LintOrAnalyzeCommand.ParseConfiguration") {
|
||||
Configuration(options: options)
|
||||
}
|
||||
configuration = config
|
||||
reporter = reporterFrom(identifier: options.reporter ?? config.reporter)
|
||||
if options.ignoreCache || ProcessInfo.processInfo.isLikelyXcodeCloudEnvironment {
|
||||
cache = nil
|
||||
} else {
|
||||
cache = LinterCache(configuration: config)
|
||||
}
|
||||
self.options = options
|
||||
|
||||
if let outFile = options.output {
|
||||
do {
|
||||
try Data().write(to: URL(fileURLWithPath: outFile))
|
||||
} catch {
|
||||
queuedPrintError("Could not write to file at path \(outFile)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func report(violations: [StyleViolation], realtimeCondition: Bool) {
|
||||
if (reporter.isRealtime && (!options.progress || options.output != nil)) == realtimeCondition {
|
||||
let report = reporter.generateReport(violations)
|
||||
if !report.isEmpty {
|
||||
options.writeToOutput(report)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension LintOrAnalyzeOptions {
|
||||
func writeToOutput(_ string: String) {
|
||||
guard let outFile = output else {
|
||||
queuedPrint(string)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let outFileURL = URL(fileURLWithPath: outFile)
|
||||
let fileUpdater = try FileHandle(forUpdating: outFileURL)
|
||||
fileUpdater.seekToEndOfFile()
|
||||
fileUpdater.write(Data((string + "\n").utf8))
|
||||
fileUpdater.closeFile()
|
||||
} catch {
|
||||
queuedPrintError("Could not write to file at path \(outFile)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private actor CorrectionsBuilder {
|
||||
private(set) var corrections: [Correction] = []
|
||||
|
||||
func append(_ corrections: [Correction]) {
|
||||
self.corrections.append(contentsOf: corrections)
|
||||
}
|
||||
}
|
||||
|
||||
private func memoryUsage() -> String? {
|
||||
#if os(Linux)
|
||||
return nil
|
||||
#else
|
||||
var info = mach_task_basic_info()
|
||||
let basicInfoCount = MemoryLayout<mach_task_basic_info>.stride / MemoryLayout<natural_t>.stride
|
||||
var count = mach_msg_type_number_t(basicInfoCount)
|
||||
|
||||
let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: basicInfoCount) {
|
||||
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
|
||||
}
|
||||
}
|
||||
|
||||
if kerr == KERN_SUCCESS {
|
||||
let bytes = Measurement<UnitInformationStorage>(value: Double(info.resident_size), unit: .bytes)
|
||||
let formatted = ByteCountFormatter().string(from: bytes)
|
||||
return "Memory used: \(formatted)"
|
||||
} else {
|
||||
let errorMessage = String(cString: mach_error_string(kerr), encoding: .ascii)
|
||||
return "Error with task_info(): \(errorMessage ?? "unknown")"
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import Foundation
|
||||
import SourceKittenFramework
|
||||
import SwiftLintFramework
|
||||
|
||||
typealias File = String
|
||||
typealias Arguments = [String]
|
||||
|
||||
class CompilerInvocations {
|
||||
static func buildLog(compilerInvocations: [[String]]) -> CompilerInvocations {
|
||||
return ArrayCompilerInvocations(invocations: compilerInvocations)
|
||||
}
|
||||
|
||||
static func compilationDatabase(compileCommands: [File: Arguments]) -> CompilerInvocations {
|
||||
return CompilationDatabaseInvocations(compileCommands: compileCommands)
|
||||
}
|
||||
|
||||
/// Default implementation
|
||||
func arguments(forFile path: String?) -> Arguments { [] }
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private class ArrayCompilerInvocations: CompilerInvocations {
|
||||
private let invocationsByArgument: [String: [Arguments]]
|
||||
|
||||
init(invocations: [Arguments]) {
|
||||
// Store invocations by the path, so next when we'll be asked for arguments,
|
||||
// we'll be able to return them faster
|
||||
self.invocationsByArgument = invocations.reduce(into: [:]) { result, arguments in
|
||||
arguments.forEach { result[$0, default: []].append(arguments) }
|
||||
}
|
||||
}
|
||||
|
||||
override func arguments(forFile path: String?) -> Arguments {
|
||||
return path.flatMap { path in
|
||||
return invocationsByArgument[path]?.first
|
||||
} ?? []
|
||||
}
|
||||
}
|
||||
|
||||
private class CompilationDatabaseInvocations: CompilerInvocations {
|
||||
private let compileCommands: [File: Arguments]
|
||||
|
||||
init(compileCommands: [File: Arguments]) {
|
||||
self.compileCommands = compileCommands
|
||||
}
|
||||
|
||||
override func arguments(forFile path: String?) -> Arguments {
|
||||
return path.flatMap { path in
|
||||
return compileCommands[path] ??
|
||||
compileCommands[path.path(relativeTo: FileManager.default.currentDirectoryPath)]
|
||||
} ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LintOrAnalyzeModeWithCompilerArguments {
|
||||
case lint
|
||||
case analyze(allCompilerInvocations: CompilerInvocations)
|
||||
}
|
||||
|
||||
private func resolveParamsFiles(args: [String]) -> [String] {
|
||||
return args.reduce(into: []) { (allArgs: inout [String], arg: String) -> Void in
|
||||
if arg.hasPrefix("@"), let contents = try? String(contentsOfFile: String(arg.dropFirst())) {
|
||||
allArgs.append(contentsOf: resolveParamsFiles(args: contents.split(separator: "\n").map(String.init)))
|
||||
} else {
|
||||
allArgs.append(arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LintableFilesVisitor {
|
||||
let paths: [String]
|
||||
let action: String
|
||||
let useSTDIN: Bool
|
||||
let quiet: Bool
|
||||
let showProgressBar: Bool
|
||||
let useScriptInputFiles: Bool
|
||||
let forceExclude: Bool
|
||||
let useExcludingByPrefix: Bool
|
||||
let cache: LinterCache?
|
||||
let parallel: Bool
|
||||
let allowZeroLintableFiles: Bool
|
||||
let mode: LintOrAnalyzeModeWithCompilerArguments
|
||||
let block: (CollectedLinter) async -> Void
|
||||
|
||||
private init(paths: [String], action: String, useSTDIN: Bool, quiet: Bool, showProgressBar: Bool,
|
||||
useScriptInputFiles: Bool, forceExclude: Bool, useExcludingByPrefix: Bool,
|
||||
cache: LinterCache?, compilerInvocations: CompilerInvocations?,
|
||||
allowZeroLintableFiles: Bool, block: @escaping (CollectedLinter) async -> Void) {
|
||||
self.paths = resolveParamsFiles(args: paths)
|
||||
self.action = action
|
||||
self.useSTDIN = useSTDIN
|
||||
self.quiet = quiet
|
||||
self.showProgressBar = showProgressBar
|
||||
self.useScriptInputFiles = useScriptInputFiles
|
||||
self.forceExclude = forceExclude
|
||||
self.useExcludingByPrefix = useExcludingByPrefix
|
||||
self.cache = cache
|
||||
if let compilerInvocations {
|
||||
self.mode = .analyze(allCompilerInvocations: compilerInvocations)
|
||||
// SourceKit had some changes in 5.6 that makes it ~100x more expensive
|
||||
// to process files concurrently. By processing files serially, it's
|
||||
// only 2x slower than before.
|
||||
self.parallel = SwiftVersion.current < .fiveDotSix
|
||||
} else {
|
||||
self.mode = .lint
|
||||
self.parallel = true
|
||||
}
|
||||
self.block = block
|
||||
self.allowZeroLintableFiles = allowZeroLintableFiles
|
||||
}
|
||||
|
||||
static func create(_ options: LintOrAnalyzeOptions,
|
||||
cache: LinterCache?,
|
||||
allowZeroLintableFiles: Bool,
|
||||
block: @escaping (CollectedLinter) async -> Void)
|
||||
throws -> LintableFilesVisitor {
|
||||
try Signposts.record(name: "LintableFilesVisitor.Create") {
|
||||
let compilerInvocations: CompilerInvocations?
|
||||
if options.mode == .lint {
|
||||
compilerInvocations = nil
|
||||
} else {
|
||||
compilerInvocations = try loadCompilerInvocations(options)
|
||||
}
|
||||
|
||||
return LintableFilesVisitor(
|
||||
paths: options.paths, action: options.verb.bridge().capitalized,
|
||||
useSTDIN: options.useSTDIN, quiet: options.quiet,
|
||||
showProgressBar: options.progress,
|
||||
useScriptInputFiles: options.useScriptInputFiles,
|
||||
forceExclude: options.forceExclude,
|
||||
useExcludingByPrefix: options.useExcludingByPrefix,
|
||||
cache: cache,
|
||||
compilerInvocations: compilerInvocations,
|
||||
allowZeroLintableFiles: allowZeroLintableFiles, block: block
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func shouldSkipFile(atPath path: String?) -> Bool {
|
||||
switch self.mode {
|
||||
case .lint:
|
||||
return false
|
||||
case let .analyze(compilerInvocations):
|
||||
let compilerArguments = compilerInvocations.arguments(forFile: path)
|
||||
return compilerArguments.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
func linter(forFile file: SwiftLintFile, configuration: Configuration) -> Linter {
|
||||
switch self.mode {
|
||||
case .lint:
|
||||
return Linter(file: file, configuration: configuration, cache: cache)
|
||||
case let .analyze(compilerInvocations):
|
||||
let compilerArguments = compilerInvocations.arguments(forFile: file.path)
|
||||
return Linter(file: file, configuration: configuration, compilerArguments: compilerArguments)
|
||||
}
|
||||
}
|
||||
|
||||
private static func loadCompilerInvocations(_ options: LintOrAnalyzeOptions) throws -> CompilerInvocations {
|
||||
if let path = options.compilerLogPath {
|
||||
guard let compilerInvocations = self.loadLogCompilerInvocations(path) else {
|
||||
throw SwiftLintError.usageError(description: "Could not read compiler log at path: '\(path)'")
|
||||
}
|
||||
|
||||
return .buildLog(compilerInvocations: compilerInvocations)
|
||||
} else if let path = options.compileCommands {
|
||||
do {
|
||||
return .compilationDatabase(compileCommands: try self.loadCompileCommands(path))
|
||||
} catch {
|
||||
throw SwiftLintError.usageError(
|
||||
description: "Could not read compilation database at path: '\(path)' \(error.localizedDescription)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
throw SwiftLintError.usageError(description: "Could not read compiler invocations")
|
||||
}
|
||||
|
||||
private static func loadLogCompilerInvocations(_ path: String) -> [[String]]? {
|
||||
if let data = FileManager.default.contents(atPath: path),
|
||||
let logContents = String(data: data, encoding: .utf8) {
|
||||
if logContents.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
return CompilerArgumentsExtractor.allCompilerInvocations(compilerLogs: logContents)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func loadCompileCommands(_ path: String) throws -> [File: Arguments] {
|
||||
guard let fileContents = FileManager.default.contents(atPath: path) else {
|
||||
throw CompileCommandsLoadError.nonExistentFile(path)
|
||||
}
|
||||
|
||||
if path.hasSuffix(".yaml") || path.hasSuffix(".yml") {
|
||||
// Assume this is a SwiftPM yaml file
|
||||
return try SwiftPMCompilationDB.parse(yaml: fileContents)
|
||||
}
|
||||
|
||||
guard let object = try? JSONSerialization.jsonObject(with: fileContents),
|
||||
let compileDB = object as? [[String: Any]] else {
|
||||
throw CompileCommandsLoadError.malformedCommands(path)
|
||||
}
|
||||
|
||||
// Convert the compilation database to a dictionary, with source files as keys and compiler arguments as values.
|
||||
//
|
||||
// Compilation databases are an array of dictionaries. Each dict has "file" and "arguments" keys.
|
||||
var commands = [File: Arguments]()
|
||||
for (index, entry) in compileDB.enumerated() {
|
||||
guard let file = entry["file"] as? String else {
|
||||
throw CompileCommandsLoadError.malformedFile(path, index)
|
||||
}
|
||||
|
||||
guard let arguments = entry["arguments"] as? [String] else {
|
||||
throw CompileCommandsLoadError.malformedArguments(path, index)
|
||||
}
|
||||
|
||||
guard arguments.contains(file) else {
|
||||
throw CompileCommandsLoadError.missingFileInArguments(path, index, arguments)
|
||||
}
|
||||
|
||||
commands[file] = arguments.filteringCompilerArguments
|
||||
}
|
||||
|
||||
return commands
|
||||
}
|
||||
}
|
||||
|
||||
private enum CompileCommandsLoadError: LocalizedError {
|
||||
case nonExistentFile(String)
|
||||
case malformedCommands(String)
|
||||
case malformedFile(String, Int)
|
||||
case malformedArguments(String, Int)
|
||||
case missingFileInArguments(String, Int, [String])
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case let .nonExistentFile(path):
|
||||
return "Could not read compile commands file at '\(path)'"
|
||||
case let .malformedCommands(path):
|
||||
return "Compile commands file at '\(path)' isn't in the correct format"
|
||||
case let .malformedFile(path, index):
|
||||
return "Missing or invalid (must be a string) 'file' key in \(path) at index \(index)"
|
||||
case let .malformedArguments(path, index):
|
||||
return "Missing or invalid (must be an array of strings) 'arguments' key in \(path) at index \(index)"
|
||||
case let .missingFileInArguments(path, index, arguments):
|
||||
return "Entry in \(path) at index \(index) has 'arguments' which do not contain the 'file': \(arguments)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import Dispatch
|
||||
import Foundation
|
||||
import SwiftLintFramework
|
||||
|
||||
// Inspired by https://github.com/jkandzi/Progress.swift
|
||||
actor ProgressBar {
|
||||
private var index = 1
|
||||
private var lastPrintedTime: TimeInterval = 0.0
|
||||
private let startTime = uptime()
|
||||
private let count: Int
|
||||
|
||||
init(count: Int) {
|
||||
self.count = count
|
||||
}
|
||||
|
||||
func initialize() {
|
||||
// When progress is printed, the previous line is reset, so print an empty line before anything else
|
||||
queuedPrintError("")
|
||||
}
|
||||
|
||||
func printNext() {
|
||||
guard index <= count else { return }
|
||||
|
||||
let currentTime = uptime()
|
||||
if currentTime - lastPrintedTime > 0.1 || index == count {
|
||||
let lineReset = "\u{1B}[1A\u{1B}[K"
|
||||
let bar = makeBar()
|
||||
let timeEstimate = makeTimeEstimate(currentTime: currentTime)
|
||||
let lineContents = "\(index) of \(count) \(bar) \(timeEstimate)"
|
||||
queuedPrintError("\(lineReset)\(lineContents)")
|
||||
lastPrintedTime = currentTime
|
||||
}
|
||||
|
||||
index += 1
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func makeBar() -> String {
|
||||
let barLength = 30
|
||||
let completedBarElements = Int(Double(barLength) * (Double(index) / Double(count)))
|
||||
let barArray = Array(repeating: "=", count: completedBarElements) +
|
||||
Array(repeating: " ", count: barLength - completedBarElements)
|
||||
return "[\(barArray.joined())]"
|
||||
}
|
||||
|
||||
private func makeTimeEstimate(currentTime: TimeInterval) -> String {
|
||||
let totalTime = currentTime - startTime
|
||||
let itemsPerSecond = Double(index) / totalTime
|
||||
let estimatedTimeRemaining = Double(count - index) / itemsPerSecond
|
||||
let estimatedTimeRemainingString = "\(Int(estimatedTimeRemaining))s"
|
||||
return "ETA: \(estimatedTimeRemainingString) (\(Int(itemsPerSecond)) files/s)"
|
||||
}
|
||||
}
|
||||
|
||||
#if os(Linux)
|
||||
// swiftlint:disable:next identifier_name
|
||||
private let NSEC_PER_SEC = 1_000_000_000
|
||||
#endif
|
||||
|
||||
private func uptime() -> TimeInterval {
|
||||
Double(DispatchTime.now().uptimeNanoseconds) / Double(NSEC_PER_SEC)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
@_spi(TestHelper)
|
||||
import SwiftLintFramework
|
||||
|
||||
extension RulesFilter {
|
||||
struct ExcludingOptions: OptionSet {
|
||||
let rawValue: Int
|
||||
|
||||
static let enabled = Self(rawValue: 1 << 0)
|
||||
static let disabled = Self(rawValue: 1 << 1)
|
||||
static let uncorrectable = Self(rawValue: 1 << 2)
|
||||
}
|
||||
}
|
||||
|
||||
class RulesFilter {
|
||||
private let allRules: RuleList
|
||||
private let enabledRules: [Rule]
|
||||
|
||||
init(allRules: RuleList = primaryRuleList, enabledRules: [Rule]) {
|
||||
self.allRules = allRules
|
||||
self.enabledRules = enabledRules
|
||||
}
|
||||
|
||||
func getRules(excluding excludingOptions: ExcludingOptions) -> RuleList {
|
||||
if excludingOptions.isEmpty {
|
||||
return allRules
|
||||
}
|
||||
|
||||
let filtered: [Rule.Type] = allRules.list.compactMap { ruleID, ruleType in
|
||||
let enabledRule = enabledRules.first { rule in
|
||||
type(of: rule).description.identifier == ruleID
|
||||
}
|
||||
let isRuleEnabled = enabledRule != nil
|
||||
|
||||
if excludingOptions.contains(.enabled) && isRuleEnabled {
|
||||
return nil
|
||||
}
|
||||
if excludingOptions.contains(.disabled) && !isRuleEnabled {
|
||||
return nil
|
||||
}
|
||||
if excludingOptions.contains(.uncorrectable) && !(ruleType is CorrectableRule.Type) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ruleType
|
||||
}
|
||||
|
||||
return RuleList(rules: filtered)
|
||||
}
|
||||
}
|
||||
73
SideStoreApp/Sources/Cargo/swiftlint/Helpers/Signposts.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
#if canImport(os)
|
||||
import os.signpost
|
||||
private let timelineLog = OSLog(subsystem: "io.realm.swiftlint", category: "Timeline")
|
||||
private let fileLog = OSLog(subsystem: "io.realm.swiftlint", category: "File")
|
||||
#endif
|
||||
|
||||
struct Signposts {
|
||||
enum Span {
|
||||
case timeline, file(String)
|
||||
}
|
||||
|
||||
static func record<R>(name: StaticString, span: Span = .timeline, body: () throws -> R) rethrows -> R {
|
||||
#if canImport(os)
|
||||
let log: OSLog
|
||||
let description: String?
|
||||
switch span {
|
||||
case .timeline:
|
||||
log = timelineLog
|
||||
description = nil
|
||||
case .file(let file):
|
||||
log = fileLog
|
||||
description = file
|
||||
}
|
||||
let signpostID = OSSignpostID(log: log)
|
||||
if let description {
|
||||
os_signpost(.begin, log: log, name: name, signpostID: signpostID, "%{public}s", description)
|
||||
} else {
|
||||
os_signpost(.begin, log: log, name: name, signpostID: signpostID)
|
||||
}
|
||||
|
||||
let result = try body()
|
||||
if let description {
|
||||
os_signpost(.end, log: log, name: name, signpostID: signpostID, "%{public}s", description)
|
||||
} else {
|
||||
os_signpost(.end, log: log, name: name, signpostID: signpostID)
|
||||
}
|
||||
return result
|
||||
#else
|
||||
return try body()
|
||||
#endif
|
||||
}
|
||||
|
||||
static func record<R>(name: StaticString, span: Span = .timeline, body: () async throws -> R) async rethrows -> R {
|
||||
#if canImport(os)
|
||||
let log: OSLog
|
||||
let description: String?
|
||||
switch span {
|
||||
case .timeline:
|
||||
log = timelineLog
|
||||
description = nil
|
||||
case .file(let file):
|
||||
log = fileLog
|
||||
description = file
|
||||
}
|
||||
let signpostID = OSSignpostID(log: log)
|
||||
if let description {
|
||||
os_signpost(.begin, log: log, name: name, signpostID: signpostID, "%{public}s", description)
|
||||
} else {
|
||||
os_signpost(.begin, log: log, name: name, signpostID: signpostID)
|
||||
}
|
||||
|
||||
let result = try await body()
|
||||
if let description {
|
||||
os_signpost(.end, log: log, name: name, signpostID: signpostID, "%{public}s", description)
|
||||
} else {
|
||||
os_signpost(.end, log: log, name: name, signpostID: signpostID)
|
||||
}
|
||||
return result
|
||||
#else
|
||||
return try await body()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
enum SwiftLintError: LocalizedError {
|
||||
case usageError(description: String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .usageError(let description):
|
||||
return description
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
private struct SwiftPMCommand: Codable {
|
||||
let tool: String
|
||||
let module: String?
|
||||
let sources: [String]?
|
||||
let args: [String]?
|
||||
let importPaths: [String]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case tool
|
||||
case module = "module-name"
|
||||
case sources
|
||||
case args = "other-args"
|
||||
case importPaths = "import-paths"
|
||||
}
|
||||
}
|
||||
|
||||
private struct SwiftPMNode: Codable {}
|
||||
|
||||
private struct SwiftPMNodes: Codable {
|
||||
let nodes: [String: SwiftPMNode]
|
||||
}
|
||||
|
||||
struct SwiftPMCompilationDB: Codable {
|
||||
private let commands: [String: SwiftPMCommand]
|
||||
|
||||
static func parse(yaml: Data) throws -> [File: Arguments] {
|
||||
let decoder = YAMLDecoder()
|
||||
let compilationDB: SwiftPMCompilationDB
|
||||
|
||||
if ProcessInfo.processInfo.environment["TEST_SRCDIR"] != nil {
|
||||
// Running tests
|
||||
let nodes = try decoder.decode(SwiftPMNodes.self, from: yaml)
|
||||
let suffix = "/Source/swiftlint/"
|
||||
let pathToReplace = Array(nodes.nodes.keys.filter({ node in
|
||||
node.hasSuffix(suffix)
|
||||
}))[0].dropLast(suffix.count - 1)
|
||||
let stringFileContents = String(data: yaml, encoding: .utf8)!
|
||||
.replacingOccurrences(of: pathToReplace, with: "")
|
||||
compilationDB = try decoder.decode(Self.self, from: stringFileContents)
|
||||
} else {
|
||||
compilationDB = try decoder.decode(Self.self, from: yaml)
|
||||
}
|
||||
|
||||
let swiftCompilerCommands = compilationDB.commands
|
||||
.filter { $0.value.tool == "swift-compiler" }
|
||||
let allSwiftSources = swiftCompilerCommands
|
||||
.flatMap { $0.value.sources ?? [] }
|
||||
.filter { $0.hasSuffix(".swift") }
|
||||
return Dictionary(uniqueKeysWithValues: allSwiftSources.map { swiftSource in
|
||||
let command = swiftCompilerCommands
|
||||
.values
|
||||
.first { $0.sources?.contains(swiftSource) == true }
|
||||
|
||||
guard let command,
|
||||
let module = command.module,
|
||||
let sources = command.sources,
|
||||
let arguments = command.args,
|
||||
let importPaths = command.importPaths
|
||||
else {
|
||||
return (swiftSource, [])
|
||||
}
|
||||
|
||||
let args = ["-module-name", module] +
|
||||
sources +
|
||||
arguments.filteringCompilerArguments +
|
||||
["-I"] + importPaths
|
||||
|
||||
return (swiftSource, args)
|
||||
})
|
||||
}
|
||||
}
|
||||
59
SideStoreApp/Sources/Cargo/xcframework/BuildSetting.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// BuildSettings.swift
|
||||
// Cargo
|
||||
//
|
||||
// Created by Joseph Mattiello on 02/28/23.
|
||||
// Copyright © 2023 Joseph Mattiello. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
//set -eu;
|
||||
//
|
||||
//BUILT_SRC="./em_proxy/$LIB_FILE_NAME.a"
|
||||
//ln -f -- "$BUILT_SRC" "$TARGET_BUILD_DIR/$EXECUTABLE_PATH" || cp "$BUILT_SRC" "$TARGET_BUILD_DIR/$EXECUTABLE_PATH"
|
||||
//echo "$BUILT_SRC -> $TARGET_BUILD_DIR/$EXECUTABLE_PATH"
|
||||
|
||||
//# generated with cargo-xcode 1.5.0
|
||||
//# modified to use prebuilt binaries
|
||||
//
|
||||
//set -eu;
|
||||
//
|
||||
//BUILT_SRC="./minimuxer/$LIB_FILE_NAME.a"
|
||||
//ln -f -- "$BUILT_SRC" "$TARGET_BUILD_DIR/$EXECUTABLE_PATH" || cp "$BUILT_SRC" "$TARGET_BUILD_DIR/$EXECUTABLE_PATH"
|
||||
//echo "$BUILT_SRC -> $TARGET_BUILD_DIR/$EXECUTABLE_PATH"
|
||||
//
|
||||
//# xcode generates dep file, but for its own path, so append our rename to it
|
||||
// #DEP_FILE_SRC="minimuxer/target/${CARGO_XCODE_TARGET_TRIPLE}/release/${CARGO_XCODE_CARGO_DEP_FILE_NAME}"
|
||||
// #if [ -f "$DEP_FILE_SRC" ]; then
|
||||
//# DEP_FILE_DST="${DERIVED_FILE_DIR}/${CARGO_XCODE_TARGET_ARCH}-${EXECUTABLE_NAME}.d"
|
||||
//# cp -f "$DEP_FILE_SRC" "$DEP_FILE_DST"
|
||||
//# echo >> "$DEP_FILE_DST" "$SCRIPT_OUTPUT_FILE_0: $BUILT_SRC"
|
||||
//#fi
|
||||
//
|
||||
//# lipo script needs to know all the platform-specific files that have been built
|
||||
//# archs is in the file name, so that paths don't stay around after archs change
|
||||
//# must match input for LipoScript
|
||||
// #FILE_LIST="${DERIVED_FILE_DIR}/${ARCHS}-${EXECUTABLE_NAME}.xcfilelist"
|
||||
// #touch "$FILE_LIST"
|
||||
// #if ! egrep -q "$SCRIPT_OUTPUT_FILE_0" "$FILE_LIST" ; then
|
||||
//# echo >> "$FILE_LIST" "$SCRIPT_OUTPUT_FILE_0"
|
||||
//#fi
|
||||
|
||||
|
||||
import ArgumentParser
|
||||
|
||||
/// A representation of a build setting in an Xcode project, e.g.
|
||||
/// `IPHONEOS_DEPLOYMENT_TARGET=13.0`
|
||||
struct BuildSetting: ExpressibleByArgument {
|
||||
/// The name of the build setting, e.g. `IPHONEOS_DEPLOYMENT_TARGET`
|
||||
let name: String
|
||||
/// The value of the build setting
|
||||
let value: String
|
||||
|
||||
init?(argument: String) {
|
||||
let components = argument.components(separatedBy: "=")
|
||||
guard components.count == 2 else { return nil }
|
||||
name = components[0].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
value = components[1].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
71
SideStoreApp/Sources/Cargo/xcframework/Command+Options.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// Command+Options.swift
|
||||
// Cargo
|
||||
//
|
||||
// Created by Joseph Mattiello on 02/28/23.
|
||||
// Copyright © 2023 Joseph Mattiello. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import PackageModel
|
||||
|
||||
extension Command {
|
||||
struct Options: ParsableArguments {
|
||||
// MARK: - Package Loading
|
||||
|
||||
@Option(help: ArgumentHelp("The location of the Package", valueName: "directory"))
|
||||
var packagePath = "."
|
||||
|
||||
// MARK: - Building
|
||||
|
||||
@Option(help: ArgumentHelp("The location of the build/cache directory to use", valueName: "directory"))
|
||||
var buildPath = ".build"
|
||||
|
||||
@Option(help: ArgumentHelp("Build with a specific configuration", valueName: "debug|release"))
|
||||
var configuration = PackageModel.BuildConfiguration.release
|
||||
|
||||
@Flag(inversion: .prefixedNo, help: "Whether to clean before we build")
|
||||
var clean = true
|
||||
|
||||
@Flag(inversion: .prefixedNo, help: "Whether to include debug symbols in the built XCFramework")
|
||||
var debugSymbols = true
|
||||
|
||||
@Flag(help: "Prints the available products and targets")
|
||||
var listProducts = false
|
||||
|
||||
@Option(help: "The path to a .xcconfig file that can be used to override Xcode build settings. Relative to the package path.")
|
||||
var xcconfig: String?
|
||||
|
||||
@Flag(help: "Enables Library Evolution for the whole build stack. Normally we apply it only to the targets listed to be built to work around issues with projects that don't support it.")
|
||||
var stackEvolution: Bool = false
|
||||
|
||||
@Option(help: ArgumentHelp("Arbitrary Xcode build settings that are passed directly to the `xcodebuild` invocation. Can be specified multiple times.", valueName: "NAME=VALUE"))
|
||||
var xcSetting: [BuildSetting] = []
|
||||
|
||||
// MARK: - Output Options
|
||||
|
||||
@Option(
|
||||
help: ArgumentHelp(
|
||||
"A list of platforms you want to build for. Can be specified multiple times."
|
||||
+ " Default is to build for all platforms supported in your Package.swift, or all Apple platforms (except for maccatalyst platform) if omitted",
|
||||
valueName: TargetPlatform.allCases.map { $0.rawValue }.joined(separator: "|")
|
||||
)
|
||||
)
|
||||
var platform: [TargetPlatform] = []
|
||||
|
||||
@Option(help: ArgumentHelp("Where to place the compiled library", valueName: "directory"))
|
||||
var output = "."
|
||||
|
||||
@Flag(help: .hidden)
|
||||
var githubAction: Bool = false
|
||||
|
||||
// MARK: - Targets
|
||||
|
||||
@Argument(help: "An optional list of products (or targets) to build. Defaults to building all `.library` products")
|
||||
var products: [String] = []
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ParsableArguments Extensions
|
||||
|
||||
extension PackageModel.BuildConfiguration: ExpressibleByArgument {}
|
||||
142
SideStoreApp/Sources/Cargo/xcframework/Command.swift
Normal file
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// Command.swift
|
||||
// Cargo
|
||||
//
|
||||
// Created by Joseph Mattiello on 02/28/23.
|
||||
// Copyright © 2023 Joseph Mattiello. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import PackageLoading
|
||||
import PackageModel
|
||||
import TSCBasic
|
||||
import Workspace
|
||||
import Xcodeproj
|
||||
|
||||
struct Command: ParsableCommand {
|
||||
// MARK: - Configuration
|
||||
|
||||
static var configuration = CommandConfiguration(
|
||||
abstract: "Builds a `rust` package using `cargo`.",
|
||||
discussion:
|
||||
"""
|
||||
|
||||
""",
|
||||
version: "1.1.0"
|
||||
)
|
||||
|
||||
// MARK: - Arguments
|
||||
|
||||
@OptionGroup()
|
||||
var options: Options
|
||||
|
||||
// MARK: - Execution
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
func run() throws {
|
||||
// load all/validate of the package info
|
||||
let package = try PackageInfo(options: options)
|
||||
|
||||
// validate that package to make sure we can generate it
|
||||
let validation = package.validationErrors()
|
||||
if validation.isEmpty == false {
|
||||
for error in validation {
|
||||
print(error.isFatal ? "Error:" : "Warning:", error.errorDescription!)
|
||||
}
|
||||
if validation.contains(where: { $0.isFatal }) {
|
||||
Darwin.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// generate the Xcode project file
|
||||
let generator = ProjectGenerator(package: package)
|
||||
|
||||
let platforms = try package.supportedPlatforms()
|
||||
|
||||
// get what we're building
|
||||
try generator.writeDistributionXcconfig()
|
||||
let project = try generator.generate()
|
||||
|
||||
// printing packages?
|
||||
if options.listProducts {
|
||||
package.printAllProducts(project: project)
|
||||
Darwin.exit(0)
|
||||
}
|
||||
|
||||
// get valid packages and their SDKs
|
||||
let productNames = try package.validProductNames(project: project)
|
||||
let sdks = platforms.flatMap { $0.sdks }
|
||||
|
||||
// we've applied the xcconfig to everything, but some dependencies (*cough* swift-nio)
|
||||
// have build errors, so we remove it from targets we're not building
|
||||
if options.stackEvolution == false {
|
||||
try project.enableDistribution(targets: productNames, xcconfig: AbsolutePath(package.distributionBuildXcconfig.path).relative(to: AbsolutePath(package.rootDirectory.path)))
|
||||
}
|
||||
|
||||
// save the project
|
||||
try project.save(to: generator.projectPath)
|
||||
|
||||
// start building
|
||||
let builder = XcodeBuilder(project: project, projectPath: generator.projectPath, package: package, options: options)
|
||||
|
||||
// clean first
|
||||
if options.clean {
|
||||
try builder.clean()
|
||||
}
|
||||
|
||||
// all of our targets for each platform, then group the resulting .frameworks by target
|
||||
var frameworkFiles: [String: [XcodeBuilder.BuildResult]] = [:]
|
||||
|
||||
for sdk in sdks {
|
||||
try builder.build(targets: productNames, sdk: sdk)
|
||||
.forEach { pair in
|
||||
if frameworkFiles[pair.key] == nil {
|
||||
frameworkFiles[pair.key] = []
|
||||
}
|
||||
frameworkFiles[pair.key]?.append(pair.value)
|
||||
}
|
||||
}
|
||||
|
||||
var xcframeworkFiles: [(String, Foundation.URL)] = []
|
||||
|
||||
// then we merge the resulting frameworks
|
||||
try frameworkFiles
|
||||
.forEach { pair in
|
||||
xcframeworkFiles.append((pair.key, try builder.merge(target: pair.key, buildResults: pair.value)))
|
||||
}
|
||||
|
||||
// zip it up if thats what they want
|
||||
if options.zip {
|
||||
let zipper = Zipper(package: package)
|
||||
let zipped = try xcframeworkFiles
|
||||
.flatMap { pair -> [Foundation.URL] in
|
||||
let zip = try zipper.zip(target: pair.0, version: self.options.zipVersion, file: pair.1)
|
||||
let checksum = try zipper.checksum(file: zip)
|
||||
try zipper.clean(file: pair.1)
|
||||
|
||||
return [zip, checksum]
|
||||
}
|
||||
|
||||
// notify the action if we have one
|
||||
if options.githubAction {
|
||||
let zips = zipped.map { $0.path }.joined(separator: "\n")
|
||||
let data = Data(zips.utf8)
|
||||
let url = Foundation.URL(fileURLWithPath: options.buildPath).appendingPathComponent("xcframework-zipfile.url")
|
||||
try data.write(to: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
private enum Error: Swift.Error, LocalizedError {
|
||||
case noProducts
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .noProducts: return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// Collection-Extensions.swift
|
||||
// swift-create-xcframework
|
||||
//
|
||||
// Created by Rob Amos on 9/5/20.
|
||||
//
|
||||
|
||||
extension Collection {
|
||||
var nonEmpty: Self? {
|
||||
isEmpty ? nil : self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// PackageDescription+Extensions.swift
|
||||
// swift-create-xcframework
|
||||
//
|
||||
// Created by Rob Amos on 7/5/20.
|
||||
//
|
||||
|
||||
import PackageModel
|
||||
|
||||
extension ProductType {
|
||||
var isLibrary: Bool {
|
||||
if case .library = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
extension Manifest {
|
||||
var libraryProductNames: [String] {
|
||||
products
|
||||
.compactMap { product in
|
||||
guard product.type.isLibrary else { return nil }
|
||||
return product.name
|
||||
}
|
||||
}
|
||||
}
|
||||
113
SideStoreApp/Sources/Cargo/xcframework/Platforms.swift
Normal file
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// Platforms.swift
|
||||
// Cargo
|
||||
//
|
||||
// Created by Joseph Mattiello on 02/28/23.
|
||||
// Copyright © 2023 Joseph Mattiello. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
import PackageModel
|
||||
|
||||
enum TargetPlatform: String, ExpressibleByArgument, CaseIterable {
|
||||
case ios
|
||||
case macos
|
||||
case maccatalyst
|
||||
case tvos
|
||||
case watchos
|
||||
|
||||
init?(argument: String) {
|
||||
self.init(rawValue: argument.lowercased())
|
||||
}
|
||||
|
||||
var platformName: String {
|
||||
switch self {
|
||||
case .ios: "ios"
|
||||
case .macos: "macos"
|
||||
case .maccatalyst: "macos"
|
||||
case .tvos: "tvos"
|
||||
case .watchos: "watchos"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Target SDKs
|
||||
|
||||
struct SDK {
|
||||
let destination: String
|
||||
let archiveName: String
|
||||
let releaseFolder: String
|
||||
let buildSettings: [String: String]?
|
||||
}
|
||||
|
||||
var sdks: [SDK] {
|
||||
switch self {
|
||||
case .ios:
|
||||
return [
|
||||
SDK(
|
||||
destination: "generic/platform=iOS",
|
||||
archiveName: "iphoneos.xcarchive",
|
||||
releaseFolder: "Release-iphoneos",
|
||||
buildSettings: nil
|
||||
),
|
||||
SDK(
|
||||
destination: "generic/platform=iOS Simulator",
|
||||
archiveName: "iphonesimulator.xcarchive",
|
||||
releaseFolder: "Release-iphonesimulator",
|
||||
buildSettings: nil
|
||||
)
|
||||
]
|
||||
|
||||
case .macos:
|
||||
return [
|
||||
SDK(
|
||||
destination: "generic/platform=macOS,name=Any Mac",
|
||||
archiveName: "macos.xcarchive",
|
||||
releaseFolder: "Release",
|
||||
buildSettings: nil
|
||||
)
|
||||
]
|
||||
|
||||
case .maccatalyst:
|
||||
return [
|
||||
SDK(
|
||||
destination: "generic/platform=macOS,variant=Mac Catalyst",
|
||||
archiveName: "maccatalyst.xcarchive",
|
||||
releaseFolder: "Release-maccatalyst",
|
||||
buildSettings: ["SUPPORTS_MACCATALYST": "YES"]
|
||||
)
|
||||
]
|
||||
|
||||
case .tvos:
|
||||
return [
|
||||
SDK(
|
||||
destination: "generic/platform=tvOS",
|
||||
archiveName: "appletvos.xcarchive",
|
||||
releaseFolder: "Release-appletvos",
|
||||
buildSettings: nil
|
||||
),
|
||||
SDK(
|
||||
destination: "generic/platform=tvOS Simulator",
|
||||
archiveName: "appletvsimulator.xcarchive",
|
||||
releaseFolder: "Release-appletvsimulator",
|
||||
buildSettings: nil
|
||||
)
|
||||
]
|
||||
|
||||
case .watchos:
|
||||
return [
|
||||
SDK(
|
||||
destination: "generic/platform=watchOS",
|
||||
archiveName: "watchos.xcarchive",
|
||||
releaseFolder: "Release-watchos",
|
||||
buildSettings: nil
|
||||
),
|
||||
SDK(
|
||||
destination: "generic/platform=watchOS Simulator",
|
||||
archiveName: "watchsimulator.xcarchive",
|
||||
releaseFolder: "Release-watchsimulator",
|
||||
buildSettings: nil
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
SideStoreApp/Sources/Cargo/xcframework/main.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
//
|
||||
// main.swift
|
||||
// Cargo
|
||||
//
|
||||
// Created by Joseph Mattiello on 02/28/23.
|
||||
// Copyright © 2023 Joseph Mattiello. All rights reserved.
|
||||
//
|
||||
|
||||
Command.main()
|
||||
19
SideStoreApp/Sources/EmotionalDamage/EmotionalDamage.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// EmotionalDamage.swift
|
||||
// EmotionalDamage
|
||||
//
|
||||
// Created by Jackson Coxson on 10/26/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import em_proxy
|
||||
|
||||
public func start_em_proxy(bind_addr: String) {
|
||||
let host = NSString(string: bind_addr)
|
||||
let host_pointer = UnsafeMutablePointer<CChar>(mutating: host.utf8String)
|
||||
_ = start_emotional_damage(host_pointer)
|
||||
}
|
||||
|
||||
public func stop_em_proxy() {
|
||||
stop_emotional_damage()
|
||||
}
|
||||
113
SideStoreApp/Sources/MiniMuxerSwift/MiniMuxer.swift
Normal file
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// minimuxer.swift
|
||||
// minimuxer
|
||||
//
|
||||
// Created by Jackson Coxson on 10/27/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
@_exported import minimuxer
|
||||
|
||||
public enum Uhoh: Error {
|
||||
case Good
|
||||
case Bad(code: Int32)
|
||||
}
|
||||
|
||||
public func start_minimuxer(pairing_file: String) -> Int32 {
|
||||
let pf = NSString(string: pairing_file)
|
||||
let pf_pointer = UnsafeMutablePointer<CChar>(mutating: pf.utf8String)
|
||||
let u = NSString(string: getDocumentsDirectory().absoluteString)
|
||||
let u_ptr = UnsafeMutablePointer<CChar>(mutating: u.utf8String)
|
||||
return minimuxer_c_start(pf_pointer, u_ptr)
|
||||
}
|
||||
|
||||
public func set_usbmuxd_socket() {
|
||||
target_minimuxer_address()
|
||||
}
|
||||
|
||||
public func debug_app(app_id: String) throws -> Uhoh {
|
||||
let ai = NSString(string: app_id)
|
||||
let ai_pointer = UnsafeMutablePointer<CChar>(mutating: ai.utf8String)
|
||||
#if true // Retries
|
||||
var res = minimuxer_debug_app(ai_pointer)
|
||||
var attempts = 10
|
||||
while attempts != 0, res != 0 {
|
||||
os_log("(JIT) ATTEMPTS: %@", type: .debug, attempts)
|
||||
res = minimuxer_debug_app(ai_pointer)
|
||||
attempts -= 1
|
||||
}
|
||||
#else
|
||||
let res = minimuxer_debug_app(ai_pointer)
|
||||
#endif
|
||||
if res != 0 {
|
||||
throw Uhoh.Bad(code: res)
|
||||
}
|
||||
return Uhoh.Good
|
||||
}
|
||||
|
||||
public func install_provisioning_profile(plist: Data) throws -> Uhoh {
|
||||
let pls = String(decoding: plist, as: UTF8.self)
|
||||
print(pls)
|
||||
print(plist)
|
||||
#if false // Retries
|
||||
var res = minimuxer_install_provisioning_profile(x, UInt32(plist.count))
|
||||
var attempts = 10
|
||||
while attempts != 0, res != 0 {
|
||||
print("(INSTALL) ATTEMPTS: \(attempts)")
|
||||
res = minimuxer_install_provisioning_profile(x, UInt32(plist.count))
|
||||
attempts -= 1
|
||||
}
|
||||
#else
|
||||
let x = plist.withUnsafeBytes { buf in UnsafeMutableRawPointer(mutating: buf) }
|
||||
#endif
|
||||
let res = minimuxer_install_provisioning_profile(x, UInt32(plist.count))
|
||||
if res != 0 {
|
||||
throw Uhoh.Bad(code: res)
|
||||
}
|
||||
return Uhoh.Good
|
||||
}
|
||||
|
||||
public func remove_provisioning_profile(id: String) throws -> Uhoh {
|
||||
let id_ns = NSString(string: id)
|
||||
let id_pointer = UnsafeMutablePointer<CChar>(mutating: id_ns.utf8String)
|
||||
#if false // Retries
|
||||
var res = minimuxer_remove_provisioning_profile(id_pointer)
|
||||
var attempts = 10
|
||||
while attempts != 0, res != 0 {
|
||||
print("(REMOVE PROFILE) ATTEMPTS: \(attempts)")
|
||||
res = minimuxer_remove_provisioning_profile(id_pointer)
|
||||
attempts -= 1
|
||||
}
|
||||
#else
|
||||
let res = minimuxer_remove_provisioning_profile(id_pointer)
|
||||
#endif
|
||||
if res != 0 {
|
||||
throw Uhoh.Bad(code: res)
|
||||
}
|
||||
return Uhoh.Good
|
||||
}
|
||||
|
||||
public func remove_app(app_id: String) throws -> Uhoh {
|
||||
let ai = NSString(string: app_id)
|
||||
let ai_pointer = UnsafeMutablePointer<CChar>(mutating: ai.utf8String)
|
||||
let res = minimuxer_remove_app(ai_pointer)
|
||||
if res != 0 {
|
||||
throw Uhoh.Bad(code: res)
|
||||
}
|
||||
return Uhoh.Good
|
||||
}
|
||||
|
||||
public func auto_mount_dev_image() {
|
||||
let u = NSString(string: getDocumentsDirectory().absoluteString)
|
||||
let u_ptr = UnsafeMutablePointer<CChar>(mutating: u.utf8String)
|
||||
minimuxer_auto_mount(u_ptr)
|
||||
}
|
||||
|
||||
func getDocumentsDirectory() -> URL {
|
||||
// find all possible documents directories for this user
|
||||
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
||||
|
||||
// just send back the first one, which ought to be the only one
|
||||
return paths[0]
|
||||
}
|
||||
10
SideStoreApp/Sources/Shared/AltConstants.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// AltConstants.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Mattiello on 2/28/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public let ALTDeviceListeningSocket: UInt16 = 28151
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// CFNotificationName+SideStore.swift
|
||||
// SideKit
|
||||
//
|
||||
// Created by Joseph Mattiello on 02/28/23.
|
||||
// Copyright © 2023 Joseph Mattiello. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public let ALTWiredServerConnectionAvailableRequest = CFNotificationName("io.altstore.Request.WiredServerConnectionAvailable" as CFString)
|
||||
public let ALTWiredServerConnectionAvailableResponse = CFNotificationName("io.altstore.Response.WiredServerConnectionAvailable" as CFString)
|
||||
public let ALTWiredServerConnectionStartRequest = CFNotificationName("io.altstore.Request.WiredServerConnectionStart" as CFString)
|
||||
101
SideStoreApp/Sources/Shared/Connections/Connection.swift
Normal file
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// Connection.swift
|
||||
// AltKit
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import SideKit
|
||||
|
||||
public protocol SideConnection: Connection {
|
||||
func __send(_ data: Data, completionHandler: @escaping (Bool, Error?) -> Void)
|
||||
func __receiveData(expectedSize: Int, completionHandler: @escaping (Data?, Error?) -> Void)
|
||||
}
|
||||
|
||||
public extension SideConnection {
|
||||
func send(_ data: Data, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void) {
|
||||
__send(data) { success, error in
|
||||
let result = Result(success, error).mapError { (failure: Error) -> ALTServerError in
|
||||
guard let nwError = failure as? NWError else { return ALTServerError(failure) }
|
||||
return ALTServerError.lostConnection(underlyingError: nwError)
|
||||
}
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
|
||||
func receiveData(expectedSize: Int, completionHandler: @escaping (Result<Data, ALTServerError>) -> Void) {
|
||||
__receiveData(expectedSize: expectedSize) { data, error in
|
||||
let result = Result(data, error).mapError { (failure: Error) -> ALTServerError in
|
||||
guard let nwError = failure as? NWError else { return ALTServerError(failure) }
|
||||
return ALTServerError.lostConnection(underlyingError: nwError)
|
||||
}
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
|
||||
func send<T: Encodable>(_ response: T, shouldDisconnect: Bool = false, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void) {
|
||||
func finish(_ result: Result<Void, ALTServerError>) {
|
||||
completionHandler(result)
|
||||
|
||||
if shouldDisconnect {
|
||||
// Add short delay to prevent us from dropping connection too quickly.
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
|
||||
self.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try JSONEncoder().encode(response)
|
||||
let responseSize = withUnsafeBytes(of: Int32(data.count)) { Data($0) }
|
||||
|
||||
send(responseSize) { result in
|
||||
switch result {
|
||||
case let .failure(error): finish(.failure(error))
|
||||
case .success:
|
||||
self.send(data) { result in
|
||||
switch result {
|
||||
case let .failure(error): finish(.failure(error))
|
||||
case .success: finish(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
finish(.failure(.invalidResponse(underlyingError: error)))
|
||||
}
|
||||
}
|
||||
|
||||
func receiveRequest(completionHandler: @escaping (Result<ServerRequest, ALTServerError>) -> Void) {
|
||||
let size = MemoryLayout<Int32>.size
|
||||
|
||||
print("Receiving request size from connection:", self)
|
||||
receiveData(expectedSize: size) { result in
|
||||
do {
|
||||
let data = try result.get()
|
||||
|
||||
let expectedSize = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
|
||||
print("Receiving request from connection: \(self)... (\(expectedSize) bytes)")
|
||||
|
||||
self.receiveData(expectedSize: expectedSize) { result in
|
||||
do {
|
||||
let data = try result.get()
|
||||
let request = try JSONDecoder().decode(ServerRequest.self, from: data)
|
||||
|
||||
print("Received request:", request)
|
||||
completionHandler(.success(request))
|
||||
} catch {
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
SideStoreApp/Sources/Shared/Connections/ConnectionManager.swift
Normal file
@@ -0,0 +1,158 @@
|
||||
//
|
||||
// ConnectionManager.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 5/23/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import SideKit
|
||||
|
||||
public protocol RequestHandler {
|
||||
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
|
||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
|
||||
|
||||
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
|
||||
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
|
||||
|
||||
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
|
||||
|
||||
func handleEnableUnsignedCodeExecutionRequest(_ request: EnableUnsignedCodeExecutionRequest, for connection: Connection, completionHandler: @escaping (Result<EnableUnsignedCodeExecutionResponse, Error>) -> Void)
|
||||
}
|
||||
|
||||
public protocol ConnectionHandler: AnyObject {
|
||||
associatedtype ConnectionType = Connection
|
||||
var connectionHandler: ((ConnectionType) -> Void)? { get set }
|
||||
var disconnectionHandler: ((ConnectionType) -> Void)? { get set }
|
||||
|
||||
func startListening()
|
||||
func stopListening()
|
||||
}
|
||||
|
||||
public class ConnectionManager<RequestHandlerType: RequestHandler, ConnectionType: NetworkConnection & AnyObject, ConnectionHandlerType: ConnectionHandler> where ConnectionHandlerType.ConnectionType == ConnectionType {
|
||||
public let requestHandler: RequestHandlerType
|
||||
public let connectionHandlers: [ConnectionHandlerType]
|
||||
|
||||
public var isStarted = false
|
||||
|
||||
private var connections = [ConnectionType]()
|
||||
private let connectionsLock = NSLock()
|
||||
|
||||
public init(requestHandler: RequestHandlerType, connectionHandlers: [ConnectionHandlerType]) {
|
||||
self.requestHandler = requestHandler
|
||||
self.connectionHandlers = connectionHandlers
|
||||
|
||||
for handler in connectionHandlers {
|
||||
handler.connectionHandler = { [weak self] connection in
|
||||
self?.prepare(connection)
|
||||
}
|
||||
|
||||
handler.disconnectionHandler = { [weak self] connection in
|
||||
self?.disconnect(connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func start() {
|
||||
guard !isStarted else { return }
|
||||
|
||||
for connectionHandler in connectionHandlers {
|
||||
connectionHandler.startListening()
|
||||
}
|
||||
|
||||
isStarted = true
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
guard isStarted else { return }
|
||||
|
||||
for connectionHandler in connectionHandlers {
|
||||
connectionHandler.stopListening()
|
||||
}
|
||||
|
||||
isStarted = false
|
||||
}
|
||||
}
|
||||
|
||||
private extension ConnectionManager {
|
||||
func prepare(_ connection: ConnectionType) {
|
||||
connectionsLock.lock()
|
||||
defer { self.connectionsLock.unlock() }
|
||||
|
||||
guard !connections.contains(where: { $0 === connection }) else { return }
|
||||
connections.append(connection)
|
||||
|
||||
handleRequest(for: connection)
|
||||
}
|
||||
|
||||
func disconnect(_ connection: ConnectionType) {
|
||||
connectionsLock.lock()
|
||||
defer { self.connectionsLock.unlock() }
|
||||
|
||||
guard let index = connections.firstIndex(where: { $0 === connection }) else { return }
|
||||
connections.remove(at: index)
|
||||
}
|
||||
|
||||
func handleRequest(for connection: ConnectionType) {
|
||||
func finish<T: ServerMessageProtocol>(_ result: Result<T, Error>) {
|
||||
do {
|
||||
let response = try result.get()
|
||||
connection.send(response, shouldDisconnect: true) { result in
|
||||
print("Sent response \(response) with result:", result)
|
||||
}
|
||||
} catch {
|
||||
let response = ErrorResponse(error: ALTServerError(error))
|
||||
connection.send(response, shouldDisconnect: true) { result in
|
||||
print("Sent error response \(response) with result:", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connection.receiveRequest { result in
|
||||
print("Received request with result:", result)
|
||||
|
||||
switch result {
|
||||
case let .failure(error): finish(Result<ErrorResponse, Error>.failure(error))
|
||||
|
||||
case let .success(.anisetteData(request)):
|
||||
self.requestHandler.handleAnisetteDataRequest(request, for: connection) { result in
|
||||
finish(result)
|
||||
}
|
||||
|
||||
case let .success(.prepareApp(request)):
|
||||
self.requestHandler.handlePrepareAppRequest(request, for: connection) { result in
|
||||
finish(result)
|
||||
}
|
||||
|
||||
case .success(.beginInstallation): break
|
||||
|
||||
case let .success(.installProvisioningProfiles(request)):
|
||||
self.requestHandler.handleInstallProvisioningProfilesRequest(request, for: connection) { result in
|
||||
finish(result)
|
||||
}
|
||||
|
||||
case let .success(.removeProvisioningProfiles(request)):
|
||||
self.requestHandler.handleRemoveProvisioningProfilesRequest(request, for: connection) { result in
|
||||
finish(result)
|
||||
}
|
||||
|
||||
case let .success(.removeApp(request)):
|
||||
self.requestHandler.handleRemoveAppRequest(request, for: connection) { result in
|
||||
finish(result)
|
||||
}
|
||||
|
||||
case let .success(.enableUnsignedCodeExecution(request)):
|
||||
self.requestHandler.handleEnableUnsignedCodeExecutionRequest(request, for: connection) { result in
|
||||
finish(result)
|
||||
}
|
||||
|
||||
case .success(.unknown):
|
||||
finish(Result<ErrorResponse, Error>.failure(ALTServerError.unknownRequest))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// NetworkConnection.swift
|
||||
// AltKit
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import SideKit
|
||||
|
||||
public class NetworkConnection: NSObject, SideConnection {
|
||||
public let nwConnection: NWConnection
|
||||
|
||||
public init(_ nwConnection: NWConnection) {
|
||||
self.nwConnection = nwConnection
|
||||
}
|
||||
|
||||
public func __send(_ data: Data, completionHandler: @escaping (Bool, Error?) -> Void) {
|
||||
nwConnection.send(content: data, completion: .contentProcessed { error in
|
||||
completionHandler(error == nil, error)
|
||||
})
|
||||
}
|
||||
|
||||
public func __receiveData(expectedSize: Int, completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
nwConnection.receive(minimumIncompleteLength: expectedSize, maximumLength: expectedSize) { data, _, _, error in
|
||||
guard data != nil || error != nil else {
|
||||
return completionHandler(nil, ALTServerError.lostConnection(underlyingError: error))
|
||||
}
|
||||
|
||||
completionHandler(data, error)
|
||||
}
|
||||
}
|
||||
|
||||
public func disconnect() {
|
||||
switch nwConnection.state {
|
||||
case .cancelled, .failed: break
|
||||
default: nwConnection.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension NetworkConnection {
|
||||
override var description: String {
|
||||
"\(nwConnection.endpoint) (Network)"
|
||||
}
|
||||
}
|
||||
468
SideStoreApp/Sources/Shared/Connections/ServerProtocol.swift
Normal file
@@ -0,0 +1,468 @@
|
||||
//
|
||||
// ServerProtocol.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 5/24/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltSign
|
||||
import SideKit
|
||||
|
||||
public let ALTServerServiceType = "_altserver._tcp"
|
||||
|
||||
protocol ServerMessageProtocol: Codable
|
||||
{
|
||||
var version: Int { get }
|
||||
var identifier: String { get }
|
||||
}
|
||||
|
||||
public enum ServerRequest: Decodable
|
||||
{
|
||||
case anisetteData(AnisetteDataRequest)
|
||||
case prepareApp(PrepareAppRequest)
|
||||
case beginInstallation(BeginInstallationRequest)
|
||||
case installProvisioningProfiles(InstallProvisioningProfilesRequest)
|
||||
case removeProvisioningProfiles(RemoveProvisioningProfilesRequest)
|
||||
case removeApp(RemoveAppRequest)
|
||||
case enableUnsignedCodeExecution(EnableUnsignedCodeExecutionRequest)
|
||||
case unknown(identifier: String, version: Int)
|
||||
|
||||
var identifier: String {
|
||||
switch self
|
||||
{
|
||||
case .anisetteData(let request): return request.identifier
|
||||
case .prepareApp(let request): return request.identifier
|
||||
case .beginInstallation(let request): return request.identifier
|
||||
case .installProvisioningProfiles(let request): return request.identifier
|
||||
case .removeProvisioningProfiles(let request): return request.identifier
|
||||
case .removeApp(let request): return request.identifier
|
||||
case .enableUnsignedCodeExecution(let request): return request.identifier
|
||||
case .unknown(let identifier, _): return identifier
|
||||
}
|
||||
}
|
||||
|
||||
var version: Int {
|
||||
switch self
|
||||
{
|
||||
case .anisetteData(let request): return request.version
|
||||
case .prepareApp(let request): return request.version
|
||||
case .beginInstallation(let request): return request.version
|
||||
case .installProvisioningProfiles(let request): return request.version
|
||||
case .removeProvisioningProfiles(let request): return request.version
|
||||
case .removeApp(let request): return request.version
|
||||
case .enableUnsignedCodeExecution(let request): return request.version
|
||||
case .unknown(_, let version): return version
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case identifier
|
||||
case version
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let version = try container.decode(Int.self, forKey: .version)
|
||||
|
||||
let identifier = try container.decode(String.self, forKey: .identifier)
|
||||
switch identifier
|
||||
{
|
||||
case "AnisetteDataRequest":
|
||||
let request = try AnisetteDataRequest(from: decoder)
|
||||
self = .anisetteData(request)
|
||||
|
||||
case "PrepareAppRequest":
|
||||
let request = try PrepareAppRequest(from: decoder)
|
||||
self = .prepareApp(request)
|
||||
|
||||
case "BeginInstallationRequest":
|
||||
let request = try BeginInstallationRequest(from: decoder)
|
||||
self = .beginInstallation(request)
|
||||
|
||||
case "InstallProvisioningProfilesRequest":
|
||||
let request = try InstallProvisioningProfilesRequest(from: decoder)
|
||||
self = .installProvisioningProfiles(request)
|
||||
|
||||
case "RemoveProvisioningProfilesRequest":
|
||||
let request = try RemoveProvisioningProfilesRequest(from: decoder)
|
||||
self = .removeProvisioningProfiles(request)
|
||||
|
||||
case "RemoveAppRequest":
|
||||
let request = try RemoveAppRequest(from: decoder)
|
||||
self = .removeApp(request)
|
||||
|
||||
case "EnableUnsignedCodeExecutionRequest":
|
||||
let request = try EnableUnsignedCodeExecutionRequest(from: decoder)
|
||||
self = .enableUnsignedCodeExecution(request)
|
||||
|
||||
default:
|
||||
self = .unknown(identifier: identifier, version: version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ServerResponse: Decodable
|
||||
{
|
||||
case anisetteData(AnisetteDataResponse)
|
||||
case installationProgress(InstallationProgressResponse)
|
||||
case installProvisioningProfiles(InstallProvisioningProfilesResponse)
|
||||
case removeProvisioningProfiles(RemoveProvisioningProfilesResponse)
|
||||
case removeApp(RemoveAppResponse)
|
||||
case enableUnsignedCodeExecution(EnableUnsignedCodeExecutionResponse)
|
||||
case error(ErrorResponse)
|
||||
case unknown(identifier: String, version: Int)
|
||||
|
||||
var identifier: String {
|
||||
switch self
|
||||
{
|
||||
case .anisetteData(let response): return response.identifier
|
||||
case .installationProgress(let response): return response.identifier
|
||||
case .installProvisioningProfiles(let response): return response.identifier
|
||||
case .removeProvisioningProfiles(let response): return response.identifier
|
||||
case .removeApp(let response): return response.identifier
|
||||
case .enableUnsignedCodeExecution(let response): return response.identifier
|
||||
case .error(let response): return response.identifier
|
||||
case .unknown(let identifier, _): return identifier
|
||||
}
|
||||
}
|
||||
|
||||
var version: Int {
|
||||
switch self
|
||||
{
|
||||
case .anisetteData(let response): return response.version
|
||||
case .installationProgress(let response): return response.version
|
||||
case .installProvisioningProfiles(let response): return response.version
|
||||
case .removeProvisioningProfiles(let response): return response.version
|
||||
case .removeApp(let response): return response.version
|
||||
case .enableUnsignedCodeExecution(let response): return response.version
|
||||
case .error(let response): return response.version
|
||||
case .unknown(_, let version): return version
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case identifier
|
||||
case version
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let version = try container.decode(Int.self, forKey: .version)
|
||||
|
||||
let identifier = try container.decode(String.self, forKey: .identifier)
|
||||
switch identifier
|
||||
{
|
||||
case "AnisetteDataResponse":
|
||||
let response = try AnisetteDataResponse(from: decoder)
|
||||
self = .anisetteData(response)
|
||||
|
||||
case "InstallationProgressResponse":
|
||||
let response = try InstallationProgressResponse(from: decoder)
|
||||
self = .installationProgress(response)
|
||||
|
||||
case "InstallProvisioningProfilesResponse":
|
||||
let response = try InstallProvisioningProfilesResponse(from: decoder)
|
||||
self = .installProvisioningProfiles(response)
|
||||
|
||||
case "RemoveProvisioningProfilesResponse":
|
||||
let response = try RemoveProvisioningProfilesResponse(from: decoder)
|
||||
self = .removeProvisioningProfiles(response)
|
||||
|
||||
case "RemoveAppResponse":
|
||||
let response = try RemoveAppResponse(from: decoder)
|
||||
self = .removeApp(response)
|
||||
|
||||
case "EnableUnsignedCodeExecutionResponse":
|
||||
let response = try EnableUnsignedCodeExecutionResponse(from: decoder)
|
||||
self = .enableUnsignedCodeExecution(response)
|
||||
|
||||
case "ErrorResponse":
|
||||
let response = try ErrorResponse(from: decoder)
|
||||
self = .error(response)
|
||||
|
||||
default:
|
||||
self = .unknown(identifier: identifier, version: version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// _Don't_ provide generic SuccessResponse, as that would prevent us
|
||||
// from easily changing response format for a request in the future.
|
||||
public struct ErrorResponse: ServerMessageProtocol
|
||||
{
|
||||
public var version = 2
|
||||
public var identifier = "ErrorResponse"
|
||||
|
||||
public var error: ALTServerError {
|
||||
return (self.serverError?.underlyingError as? ALTServerError)!
|
||||
}
|
||||
private var serverError: ALTServerError?
|
||||
|
||||
// Legacy (v1)
|
||||
private var errorCode: ALTServerError.RawValue
|
||||
|
||||
public init(error: ALTServerError)
|
||||
{
|
||||
self.serverError = ALTServerError(error)
|
||||
self.errorCode = error.errorCode
|
||||
}
|
||||
}
|
||||
|
||||
public struct AnisetteDataRequest: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "AnisetteDataRequest"
|
||||
|
||||
public init()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public struct AnisetteDataResponse: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "AnisetteDataResponse"
|
||||
|
||||
public var anisetteData: ALTAnisetteData
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case identifier
|
||||
case version
|
||||
case anisetteData
|
||||
}
|
||||
|
||||
public init(anisetteData: ALTAnisetteData)
|
||||
{
|
||||
self.anisetteData = anisetteData
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.version = try container.decode(Int.self, forKey: .version)
|
||||
self.identifier = try container.decode(String.self, forKey: .identifier)
|
||||
|
||||
let json = try container.decode([String: String].self, forKey: .anisetteData)
|
||||
|
||||
// if let anisetteData = ALTAnisetteData() //(json: json)
|
||||
// {
|
||||
// self.anisetteData = anisetteData
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
throw DecodingError.dataCorruptedError(forKey: CodingKeys.anisetteData, in: container, debugDescription: "Couuld not parse anisette data from JSON")
|
||||
// }
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws
|
||||
{
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.version, forKey: .version)
|
||||
try container.encode(self.identifier, forKey: .identifier)
|
||||
|
||||
// let json = self.anisetteData.json()
|
||||
// try container.encode(json, forKey: .anisetteData)
|
||||
}
|
||||
}
|
||||
|
||||
public struct PrepareAppRequest: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "PrepareAppRequest"
|
||||
|
||||
public var udid: String
|
||||
public var contentSize: Int
|
||||
|
||||
public var fileURL: URL?
|
||||
|
||||
public init(udid: String, contentSize: Int, fileURL: URL? = nil)
|
||||
{
|
||||
self.udid = udid
|
||||
self.contentSize = contentSize
|
||||
self.fileURL = fileURL
|
||||
}
|
||||
}
|
||||
|
||||
public struct BeginInstallationRequest: ServerMessageProtocol
|
||||
{
|
||||
public var version = 3
|
||||
public var identifier = "BeginInstallationRequest"
|
||||
|
||||
// If activeProfiles is non-nil, then AltServer should remove all profiles except active ones.
|
||||
public var activeProfiles: Set<String>?
|
||||
|
||||
public var bundleIdentifier: String?
|
||||
|
||||
public init(activeProfiles: Set<String>?, bundleIdentifier: String?)
|
||||
{
|
||||
self.activeProfiles = activeProfiles
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
}
|
||||
}
|
||||
|
||||
public struct InstallationProgressResponse: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "InstallationProgressResponse"
|
||||
|
||||
public var progress: Double
|
||||
|
||||
public init(progress: Double)
|
||||
{
|
||||
self.progress = progress
|
||||
}
|
||||
}
|
||||
|
||||
public struct InstallProvisioningProfilesRequest: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "InstallProvisioningProfilesRequest"
|
||||
|
||||
public var udid: String
|
||||
public var provisioningProfiles: Set<ALTProvisioningProfile>
|
||||
|
||||
// If activeProfiles is non-nil, then AltServer should remove all profiles except active ones.
|
||||
public var activeProfiles: Set<String>?
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case identifier
|
||||
case version
|
||||
case udid
|
||||
case provisioningProfiles
|
||||
case activeProfiles
|
||||
}
|
||||
|
||||
public init(udid: String, provisioningProfiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?)
|
||||
{
|
||||
self.udid = udid
|
||||
self.provisioningProfiles = provisioningProfiles
|
||||
self.activeProfiles = activeProfiles
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.version = try container.decode(Int.self, forKey: .version)
|
||||
self.identifier = try container.decode(String.self, forKey: .identifier)
|
||||
self.udid = try container.decode(String.self, forKey: .udid)
|
||||
|
||||
let rawProvisioningProfiles = try container.decode([Data].self, forKey: .provisioningProfiles)
|
||||
let provisioningProfiles = try rawProvisioningProfiles.map { (data) -> ALTProvisioningProfile in
|
||||
guard let profile = ALTProvisioningProfile(data: data) else {
|
||||
throw DecodingError.dataCorruptedError(forKey: CodingKeys.provisioningProfiles, in: container, debugDescription: "Could not parse provisioning profile from data.")
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
self.provisioningProfiles = Set(provisioningProfiles)
|
||||
self.activeProfiles = try container.decodeIfPresent(Set<String>.self, forKey: .activeProfiles)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws
|
||||
{
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.version, forKey: .version)
|
||||
try container.encode(self.identifier, forKey: .identifier)
|
||||
try container.encode(self.udid, forKey: .udid)
|
||||
|
||||
try container.encode(self.provisioningProfiles.map { $0.data }, forKey: .provisioningProfiles)
|
||||
try container.encodeIfPresent(self.activeProfiles, forKey: .activeProfiles)
|
||||
}
|
||||
}
|
||||
|
||||
public struct InstallProvisioningProfilesResponse: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "InstallProvisioningProfilesResponse"
|
||||
|
||||
public init()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public struct RemoveProvisioningProfilesRequest: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "RemoveProvisioningProfilesRequest"
|
||||
|
||||
public var udid: String
|
||||
public var bundleIdentifiers: Set<String>
|
||||
|
||||
public init(udid: String, bundleIdentifiers: Set<String>)
|
||||
{
|
||||
self.udid = udid
|
||||
self.bundleIdentifiers = bundleIdentifiers
|
||||
}
|
||||
}
|
||||
|
||||
public struct RemoveProvisioningProfilesResponse: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "RemoveProvisioningProfilesResponse"
|
||||
|
||||
public init()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public struct RemoveAppRequest: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "RemoveAppRequest"
|
||||
|
||||
public var udid: String
|
||||
public var bundleIdentifier: String
|
||||
|
||||
public init(udid: String, bundleIdentifier: String)
|
||||
{
|
||||
self.udid = udid
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
}
|
||||
}
|
||||
|
||||
public struct RemoveAppResponse: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "RemoveAppResponse"
|
||||
|
||||
public init()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public struct EnableUnsignedCodeExecutionRequest: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "EnableUnsignedCodeExecutionRequest"
|
||||
|
||||
public var udid: String
|
||||
public var processID: Int?
|
||||
public var processName: String?
|
||||
|
||||
public init(udid: String, processID: Int? = nil, processName: String? = nil)
|
||||
{
|
||||
self.udid = udid
|
||||
self.processID = processID
|
||||
self.processName = processName
|
||||
}
|
||||
}
|
||||
|
||||
public struct EnableUnsignedCodeExecutionResponse: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "EnableUnsignedCodeExecutionResponse"
|
||||
|
||||
public init()
|
||||
{
|
||||
}
|
||||
}
|
||||
138
SideStoreApp/Sources/Shared/Connections/XPCConnection.swift
Normal file
@@ -0,0 +1,138 @@
|
||||
//
|
||||
// XPCConnection.swift
|
||||
// AltKit
|
||||
//
|
||||
// Created by Riley Testut on 6/15/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SideKit
|
||||
|
||||
@objc private protocol XPCConnectionProxy {
|
||||
func ping(completionHandler: @escaping () -> Void)
|
||||
func receive(_ data: Data, completionHandler: @escaping (Bool, Error?) -> Void)
|
||||
}
|
||||
|
||||
public extension XPCConnection {
|
||||
static let unc0verMachServiceName = "cy:io.altstore.altdaemon"
|
||||
static let odysseyMachServiceName = "lh:io.altstore.altdaemon"
|
||||
|
||||
static let machServiceNames = [unc0verMachServiceName, odysseyMachServiceName]
|
||||
}
|
||||
|
||||
public class XPCConnection: NSObject, SideConnection {
|
||||
public let xpcConnection: NSXPCConnection
|
||||
|
||||
private let queue = DispatchQueue(label: "io.altstore.XPCConnection")
|
||||
private let dispatchGroup = DispatchGroup()
|
||||
private var semaphore: DispatchSemaphore?
|
||||
private var buffer = Data(capacity: 1024)
|
||||
|
||||
private var error: Error?
|
||||
|
||||
public init(_ xpcConnection: NSXPCConnection) {
|
||||
let proxyInterface = NSXPCInterface(with: XPCConnectionProxy.self)
|
||||
xpcConnection.remoteObjectInterface = proxyInterface
|
||||
xpcConnection.exportedInterface = proxyInterface
|
||||
|
||||
self.xpcConnection = xpcConnection
|
||||
|
||||
super.init()
|
||||
|
||||
xpcConnection.interruptionHandler = {
|
||||
self.error = ALTServerError.lostConnection(underlyingError: nil)
|
||||
}
|
||||
|
||||
xpcConnection.exportedObject = self
|
||||
xpcConnection.resume()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private extension XPCConnection {
|
||||
func makeProxy(errorHandler: @escaping (Error) -> Void) -> XPCConnectionProxy {
|
||||
let proxy = xpcConnection.remoteObjectProxyWithErrorHandler { error in
|
||||
print("Error messaging remote object proxy:", error)
|
||||
self.error = error
|
||||
errorHandler(error)
|
||||
} as! XPCConnectionProxy
|
||||
|
||||
return proxy
|
||||
}
|
||||
}
|
||||
|
||||
public extension XPCConnection {
|
||||
func connect(completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
let proxy = makeProxy { error in
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
|
||||
proxy.ping {
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
xpcConnection.invalidate()
|
||||
}
|
||||
|
||||
func __send(_ data: Data, completionHandler: @escaping (Bool, Error?) -> Void) {
|
||||
guard error == nil else { return completionHandler(false, error) }
|
||||
|
||||
let proxy = makeProxy { error in
|
||||
completionHandler(false, error)
|
||||
}
|
||||
|
||||
proxy.receive(data) { success, error in
|
||||
completionHandler(success, error)
|
||||
}
|
||||
}
|
||||
|
||||
func __receiveData(expectedSize: Int, completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
guard error == nil else { return completionHandler(nil, error) }
|
||||
|
||||
queue.async {
|
||||
let copiedBuffer = self.buffer // Copy buffer to prevent runtime crashes.
|
||||
guard copiedBuffer.count >= expectedSize else {
|
||||
self.semaphore = DispatchSemaphore(value: 0)
|
||||
DispatchQueue.global().async {
|
||||
_ = self.semaphore?.wait(timeout: .now() + 1.0)
|
||||
self.__receiveData(expectedSize: expectedSize, completionHandler: completionHandler)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let data = copiedBuffer.prefix(expectedSize)
|
||||
self.buffer = copiedBuffer.dropFirst(expectedSize)
|
||||
|
||||
completionHandler(data, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension XPCConnection {
|
||||
override var description: String {
|
||||
"\(xpcConnection.endpoint) (XPC)"
|
||||
}
|
||||
}
|
||||
|
||||
extension XPCConnection: XPCConnectionProxy {
|
||||
fileprivate func ping(completionHandler: @escaping () -> Void) {
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
fileprivate func receive(_ data: Data, completionHandler: @escaping (Bool, Error?) -> Void) {
|
||||
queue.async {
|
||||
self.buffer.append(data)
|
||||
|
||||
self.semaphore?.signal()
|
||||
self.semaphore = nil
|
||||
|
||||
completionHandler(true, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
69
SideStoreApp/Sources/Shared/Extensions/Bundle+AltStore.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// Bundle+AltStore.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/30/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Bundle {
|
||||
enum Info {
|
||||
public static let deviceID = "ALTDeviceID"
|
||||
public static let serverID = "ALTServerID"
|
||||
public static let certificateID = "ALTCertificateID"
|
||||
public static let appGroups = "ALTAppGroups"
|
||||
public static let altBundleID = "ALTBundleIdentifier"
|
||||
public static let orgbundleIdentifier = "com.SideStore"
|
||||
public static let appbundleIdentifier = orgbundleIdentifier + ".SideStore"
|
||||
public static let devicePairingString = "ALTPairingFile"
|
||||
public static let urlTypes = "CFBundleURLTypes"
|
||||
public static let exportedUTIs = "UTExportedTypeDeclarations"
|
||||
|
||||
public static let untetherURL = "ALTFugu14UntetherURL"
|
||||
public static let untetherRequired = "ALTFugu14UntetherRequired"
|
||||
public static let untetherMinimumiOSVersion = "ALTFugu14UntetherMinimumVersion"
|
||||
public static let untetherMaximumiOSVersion = "ALTFugu14UntetherMaximumVersion"
|
||||
}
|
||||
}
|
||||
|
||||
public extension Bundle {
|
||||
var infoPlistURL: URL {
|
||||
let infoPlistURL = bundleURL.appendingPathComponent("Info.plist")
|
||||
return infoPlistURL
|
||||
}
|
||||
|
||||
var provisioningProfileURL: URL {
|
||||
let provisioningProfileURL = bundleURL.appendingPathComponent("embedded.mobileprovision")
|
||||
return provisioningProfileURL
|
||||
}
|
||||
|
||||
var certificateURL: URL {
|
||||
let certificateURL = bundleURL.appendingPathComponent("ALTCertificate.p12")
|
||||
return certificateURL
|
||||
}
|
||||
|
||||
var altstorePlistURL: URL {
|
||||
let altstorePlistURL = bundleURL.appendingPathComponent("AltStore.plist")
|
||||
return altstorePlistURL
|
||||
}
|
||||
}
|
||||
|
||||
public extension Bundle {
|
||||
static var baseAltStoreAppGroupID = "group.com.SideStore.SideStore"
|
||||
|
||||
var appGroups: [String] {
|
||||
infoDictionary?[Bundle.Info.appGroups] as? [String] ?? []
|
||||
}
|
||||
|
||||
var altstoreAppGroup: String? {
|
||||
let appGroup = appGroups.first { $0.contains(Bundle.baseAltStoreAppGroupID) }
|
||||
return appGroup
|
||||
}
|
||||
|
||||
var completeInfoDictionary: [String: Any]? {
|
||||
let infoPlistURL = self.infoPlistURL
|
||||
return NSDictionary(contentsOf: infoPlistURL) as? [String: Any]
|
||||
}
|
||||
}
|
||||
128
SideStoreApp/Sources/Shared/Extensions/NSError+AltStore.swift
Normal file
@@ -0,0 +1,128 @@
|
||||
//
|
||||
// NSError+AltStore.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 3/11/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension NSError {
|
||||
@objc(alt_localizedFailure)
|
||||
var localizedFailure: String? {
|
||||
let localizedFailure = (userInfo[NSLocalizedFailureErrorKey] as? String) ?? (NSError.userInfoValueProvider(forDomain: domain)?(self, NSLocalizedFailureErrorKey) as? String)
|
||||
return localizedFailure
|
||||
}
|
||||
|
||||
@objc(alt_localizedDebugDescription)
|
||||
var localizedDebugDescription: String? {
|
||||
let debugDescription = (userInfo[NSDebugDescriptionErrorKey] as? String) ?? (NSError.userInfoValueProvider(forDomain: domain)?(self, NSDebugDescriptionErrorKey) as? String)
|
||||
return debugDescription
|
||||
}
|
||||
|
||||
@objc(alt_errorWithLocalizedFailure:)
|
||||
func withLocalizedFailure(_ failure: String) -> NSError {
|
||||
var userInfo = self.userInfo
|
||||
userInfo[NSLocalizedFailureErrorKey] = failure
|
||||
|
||||
if let failureReason = localizedFailureReason {
|
||||
userInfo[NSLocalizedFailureReasonErrorKey] = failureReason
|
||||
} else if localizedFailure == nil && localizedFailureReason == nil && localizedDescription.contains(localizedErrorCode) {
|
||||
// Default localizedDescription, so replace with just the localized error code portion.
|
||||
userInfo[NSLocalizedFailureReasonErrorKey] = "(\(localizedErrorCode).)"
|
||||
} else {
|
||||
userInfo[NSLocalizedFailureReasonErrorKey] = localizedDescription
|
||||
}
|
||||
|
||||
if let localizedDescription = NSError.userInfoValueProvider(forDomain: domain)?(self, NSLocalizedDescriptionKey) as? String {
|
||||
userInfo[NSLocalizedDescriptionKey] = localizedDescription
|
||||
}
|
||||
|
||||
// Don't accidentally remove localizedDescription from dictionary
|
||||
// userInfo[NSLocalizedDescriptionKey] = NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSLocalizedDescriptionKey) as? String
|
||||
|
||||
if let recoverySuggestion = localizedRecoverySuggestion {
|
||||
userInfo[NSLocalizedRecoverySuggestionErrorKey] = recoverySuggestion
|
||||
}
|
||||
|
||||
let error = NSError(domain: domain, code: code, userInfo: userInfo)
|
||||
return error
|
||||
}
|
||||
|
||||
func sanitizedForCoreData() -> NSError {
|
||||
var userInfo = self.userInfo
|
||||
userInfo[NSLocalizedFailureErrorKey] = localizedFailure
|
||||
userInfo[NSLocalizedDescriptionKey] = localizedDescription
|
||||
userInfo[NSLocalizedFailureReasonErrorKey] = localizedFailureReason
|
||||
userInfo[NSLocalizedRecoverySuggestionErrorKey] = localizedRecoverySuggestion
|
||||
|
||||
// Remove userInfo values that don't conform to NSSecureEncoding.
|
||||
userInfo = userInfo.filter { _, value in
|
||||
(value as AnyObject) is NSSecureCoding
|
||||
}
|
||||
|
||||
// Sanitize underlying errors.
|
||||
if let underlyingError = userInfo[NSUnderlyingErrorKey] as? Error {
|
||||
let sanitizedError = (underlyingError as NSError).sanitizedForCoreData()
|
||||
userInfo[NSUnderlyingErrorKey] = sanitizedError
|
||||
}
|
||||
|
||||
if #available(iOS 14.5, macOS 11.3, *), let underlyingErrors = userInfo[NSMultipleUnderlyingErrorsKey] as? [Error] {
|
||||
let sanitizedErrors = underlyingErrors.map { ($0 as NSError).sanitizedForCoreData() }
|
||||
userInfo[NSMultipleUnderlyingErrorsKey] = sanitizedErrors
|
||||
}
|
||||
|
||||
let error = NSError(domain: domain, code: code, userInfo: userInfo)
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
public extension Error {
|
||||
var underlyingError: Error? {
|
||||
let underlyingError = (self as NSError).userInfo[NSUnderlyingErrorKey] as? Error
|
||||
return underlyingError
|
||||
}
|
||||
|
||||
var localizedErrorCode: String {
|
||||
let localizedErrorCode = String(format: NSLocalizedString("%@ error %@", comment: ""), (self as NSError).domain, (self as NSError).code as NSNumber)
|
||||
return localizedErrorCode
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ALTLocalizedError: LocalizedError, CustomNSError {
|
||||
var failure: String? { get }
|
||||
|
||||
var underlyingError: Error? { get }
|
||||
}
|
||||
|
||||
public extension ALTLocalizedError {
|
||||
var errorUserInfo: [String: Any] {
|
||||
let userInfo = ([
|
||||
NSLocalizedDescriptionKey: errorDescription,
|
||||
NSLocalizedFailureReasonErrorKey: failureReason,
|
||||
NSLocalizedFailureErrorKey: failure,
|
||||
NSUnderlyingErrorKey: underlyingError
|
||||
] as [String: Any?]).compactMapValues { $0 }
|
||||
return userInfo
|
||||
}
|
||||
|
||||
var underlyingError: Error? {
|
||||
// Error's default implementation calls errorUserInfo,
|
||||
// but ALTLocalizedError.errorUserInfo calls underlyingError.
|
||||
// Return nil to prevent infinite recursion.
|
||||
nil
|
||||
}
|
||||
|
||||
var errorDescription: String? {
|
||||
guard let errorFailure = failure else { return (underlyingError as NSError?)?.localizedDescription }
|
||||
guard let failureReason = failureReason else { return errorFailure }
|
||||
|
||||
let errorDescription = errorFailure + " " + failureReason
|
||||
return errorDescription
|
||||
}
|
||||
|
||||
var failureReason: String? { (underlyingError as NSError?)?.localizedDescription }
|
||||
var recoverySuggestion: String? { (underlyingError as NSError?)?.localizedRecoverySuggestion }
|
||||
var helpAnchor: String? { (underlyingError as NSError?)?.helpAnchor }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// NSXPCConnection+MachServices.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 9/22/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc private protocol XPCPrivateAPI {
|
||||
init(machServiceName: String)
|
||||
init(machServiceName: String, options: NSXPCConnection.Options)
|
||||
}
|
||||
|
||||
public extension NSXPCConnection {
|
||||
class func makeConnection(machServiceName: String) -> NSXPCConnection {
|
||||
let connection = unsafeBitCast(self, to: XPCPrivateAPI.Type.self).init(machServiceName: machServiceName, options: .privileged)
|
||||
return unsafeBitCast(connection, to: NSXPCConnection.self)
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSXPCListener {
|
||||
class func makeListener(machServiceName: String) -> NSXPCListener {
|
||||
let listener = unsafeBitCast(self, to: XPCPrivateAPI.Type.self).init(machServiceName: machServiceName)
|
||||
return unsafeBitCast(listener, to: NSXPCListener.self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// Result+Conveniences.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/22/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Result {
|
||||
var value: Success? {
|
||||
switch self {
|
||||
case let .success(value): return value
|
||||
case .failure: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var error: Failure? {
|
||||
switch self {
|
||||
case .success: return nil
|
||||
case let .failure(error): return error
|
||||
}
|
||||
}
|
||||
|
||||
init(_ value: Success?, _ error: Failure?) {
|
||||
switch (value, error) {
|
||||
case let (value?, _): self = .success(value)
|
||||
case let (_, error?): self = .failure(error)
|
||||
case (nil, nil): preconditionFailure("Either value or error must be non-nil")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Result where Success == Void {
|
||||
init(_ success: Bool, _ error: Failure?) {
|
||||
if success {
|
||||
self = .success(())
|
||||
} else if let error = error {
|
||||
self = .failure(error)
|
||||
} else {
|
||||
preconditionFailure("Error must be non-nil if success is false")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Result {
|
||||
init<T, U>(_ values: (T?, U?), _ error: Failure?) where Success == (T, U) {
|
||||
if let value1 = values.0, let value2 = values.1 {
|
||||
self = .success((value1, value2))
|
||||
} else if let error = error {
|
||||
self = .failure(error)
|
||||
} else {
|
||||
preconditionFailure("Error must be non-nil if either provided values are nil")
|
||||
}
|
||||
}
|
||||
}
|
||||
21
SideStoreApp/Sources/Shared/XPC/AltXPCProtocol.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// AltXPCProtocol.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Joseph Mattiello on 02/28/23.
|
||||
// Copyright © 2023 Joseph Mattiello. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public typealias AltXPCProtocol = SideXPCProtocol
|
||||
|
||||
@objc
|
||||
public protocol SideXPCProtocol {
|
||||
func ping(completionHandler: @escaping () -> Void)
|
||||
func requestAnisetteData(completionHandler: @escaping (ALTAnisetteData?, Error?) -> Void)
|
||||
}
|
||||
|
||||
@objc public class ALTAnisetteData: NSObject {
|
||||
// implementation
|
||||
}
|
||||
10
SideStoreApp/Sources/SideBackup/AltBackup.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
111
SideStoreApp/Sources/SideBackup/AppDelegate.swift
Normal file
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// AltBackup
|
||||
//
|
||||
// Created by Riley Testut on 5/11/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension AppDelegate {
|
||||
static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
|
||||
static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
|
||||
|
||||
static let operationDidFinishNotification = Notification.Name("io.altstore.BackupOperationFinished")
|
||||
|
||||
static let operationResultKey = "result"
|
||||
}
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
private var currentBackupReturnURL: URL?
|
||||
|
||||
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil)
|
||||
|
||||
let viewController = ViewController()
|
||||
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window?.rootViewController = viewController
|
||||
window?.makeKeyAndVisible()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
|
||||
open(url)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate {
|
||||
func open(_ url: URL) -> Bool {
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
||||
guard let command = components.host?.lowercased() else { return false }
|
||||
|
||||
switch command {
|
||||
case "backup":
|
||||
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
|
||||
currentBackupReturnURL = returnURL
|
||||
NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil)
|
||||
|
||||
return true
|
||||
|
||||
case "restore":
|
||||
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
|
||||
currentBackupReturnURL = returnURL
|
||||
NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil)
|
||||
|
||||
return true
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
@objc func operationDidFinish(_ notification: Notification) {
|
||||
defer { self.currentBackupReturnURL = nil }
|
||||
|
||||
guard
|
||||
let returnURL = currentBackupReturnURL,
|
||||
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
|
||||
else { return }
|
||||
|
||||
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
components.path = "/success"
|
||||
|
||||
case let .failure(error as NSError):
|
||||
components.path = "/failure"
|
||||
components.queryItems = ["errorDomain": error.domain,
|
||||
"errorCode": String(error.code),
|
||||
"errorDescription": error.localizedDescription].map { URLQueryItem(name: $0, value: $1) }
|
||||
}
|
||||
|
||||
guard let responseURL = components.url else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(responseURL, options: [:]) { success in
|
||||
print("Sent response to app with success:", success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.518",
|
||||
"green" : "0.502",
|
||||
"red" : "0.004"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.404",
|
||||
"green" : "0.322",
|
||||
"red" : "0.008"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.750",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
249
SideStoreApp/Sources/SideBackup/BackupController.swift
Normal file
@@ -0,0 +1,249 @@
|
||||
//
|
||||
// BackupController.swift
|
||||
// AltBackup
|
||||
//
|
||||
// Created by Riley Testut on 5/12/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension ErrorUserInfoKey {
|
||||
static let sourceFile: String = "alt_sourceFile"
|
||||
static let sourceFileLine: String = "alt_sourceFileLine"
|
||||
}
|
||||
|
||||
extension Error {
|
||||
var sourceDescription: String? {
|
||||
guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else {
|
||||
return nil
|
||||
}
|
||||
return "(\((sourceFile as NSString).lastPathComponent), Line \(sourceFileLine))"
|
||||
}
|
||||
}
|
||||
|
||||
struct BackupError: ALTLocalizedError {
|
||||
enum Code {
|
||||
case invalidBundleID
|
||||
case appGroupNotFound(String?)
|
||||
case randomError // Used for debugging.
|
||||
}
|
||||
|
||||
let code: Code
|
||||
|
||||
let sourceFile: String
|
||||
let sourceFileLine: Int
|
||||
|
||||
var failure: String?
|
||||
|
||||
var failureReason: String? {
|
||||
switch code {
|
||||
case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "")
|
||||
case let .appGroupNotFound(appGroup):
|
||||
if let appGroup = appGroup {
|
||||
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
|
||||
} else {
|
||||
return NSLocalizedString("The AltStore app group could not be found.", comment: "")
|
||||
}
|
||||
case .randomError: return NSLocalizedString("A random error occured.", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var errorUserInfo: [String: Any] {
|
||||
let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: errorDescription,
|
||||
NSLocalizedFailureReasonErrorKey: failureReason,
|
||||
NSLocalizedFailureErrorKey: failure,
|
||||
ErrorUserInfoKey.sourceFile: sourceFile,
|
||||
ErrorUserInfoKey.sourceFileLine: sourceFileLine]
|
||||
return userInfo.compactMapValues { $0 }
|
||||
}
|
||||
|
||||
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line) {
|
||||
self.code = code
|
||||
failure = description
|
||||
sourceFile = file
|
||||
sourceFileLine = line
|
||||
}
|
||||
}
|
||||
|
||||
class BackupController: NSObject {
|
||||
private let fileCoordinator = NSFileCoordinator(filePresenter: nil)
|
||||
private let operationQueue = OperationQueue()
|
||||
|
||||
override init() {
|
||||
operationQueue.name = "AltBackup-BackupQueue"
|
||||
}
|
||||
|
||||
func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
do {
|
||||
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
|
||||
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: ""))
|
||||
}
|
||||
|
||||
guard
|
||||
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
|
||||
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
|
||||
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: "")) }
|
||||
|
||||
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
|
||||
|
||||
// Use temporary directory to prevent messing up successful backup with incomplete one.
|
||||
let temporaryAppBackupDirectory = backupsDirectory.appendingPathComponent("Temp", isDirectory: true).appendingPathComponent(UUID().uuidString)
|
||||
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
|
||||
|
||||
let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: [])
|
||||
let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing])
|
||||
fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: operationQueue) { error in
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
do {
|
||||
let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App")
|
||||
try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
|
||||
|
||||
if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path) {
|
||||
try FileManager.default.removeItem(at: backupDocumentsDirectory)
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: documentsDirectory.path) {
|
||||
try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory)
|
||||
}
|
||||
|
||||
print("Copied Documents directory from \(documentsDirectory) to \(backupDocumentsDirectory)")
|
||||
|
||||
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
|
||||
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
|
||||
|
||||
if FileManager.default.fileExists(atPath: backupLibraryDirectory.path) {
|
||||
try FileManager.default.removeItem(at: backupLibraryDirectory)
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: libraryDirectory.path) {
|
||||
try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory)
|
||||
}
|
||||
|
||||
print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)")
|
||||
}
|
||||
|
||||
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup {
|
||||
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: ""))
|
||||
}
|
||||
|
||||
let backupAppGroupURL = temporaryAppBackupDirectory.appendingPathComponent(appGroup)
|
||||
|
||||
// There are several system hidden files that we don't have permission to read, so we just skip all hidden files in app group directories.
|
||||
try self.copyDirectoryContents(at: appGroupURL, to: backupAppGroupURL, options: [.skipsHiddenFiles])
|
||||
}
|
||||
|
||||
// Replace previous backup with new backup.
|
||||
_ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory)
|
||||
|
||||
print("Replaced previous backup with new backup:", temporaryAppBackupDirectory)
|
||||
|
||||
completionHandler(.success(()))
|
||||
} catch {
|
||||
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) } catch { print("Failed to remove temporary directory.", error) }
|
||||
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
do {
|
||||
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
|
||||
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: ""))
|
||||
}
|
||||
|
||||
guard
|
||||
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
|
||||
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
|
||||
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to access backup.", comment: "")) }
|
||||
|
||||
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
|
||||
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
|
||||
|
||||
let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: [])
|
||||
fileCoordinator.coordinate(with: [readingIntent], queue: operationQueue) { error in
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
let mainGroupBackupDirectory = appBackupDirectory.appendingPathComponent("App")
|
||||
|
||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
|
||||
|
||||
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
|
||||
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
|
||||
|
||||
try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory)
|
||||
try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory)
|
||||
|
||||
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup {
|
||||
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: ""))
|
||||
}
|
||||
|
||||
let backupAppGroupURL = appBackupDirectory.appendingPathComponent(appGroup)
|
||||
try self.copyDirectoryContents(at: backupAppGroupURL, to: appGroupURL)
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
} catch {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension BackupController {
|
||||
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws {
|
||||
guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return }
|
||||
|
||||
if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path) {
|
||||
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options) {
|
||||
let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false
|
||||
let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
|
||||
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: destinationURL)
|
||||
} catch CocoaError.fileWriteNoPermission where isDirectory {
|
||||
try copyDirectoryContents(at: fileURL, to: destinationURL, options: options)
|
||||
continue
|
||||
} catch {
|
||||
print(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
|
||||
print("Copied item from \(fileURL) to \(destinationURL)")
|
||||
} catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" {
|
||||
// Ignore errors for /Documents/Inbox
|
||||
print("Failed to copy Inbox directory:", error)
|
||||
} catch {
|
||||
print(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<namedColor name="Background">
|
||||
<color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
67
SideStoreApp/Sources/SideBackup/Info.plist
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ALTAppGroups</key>
|
||||
<array>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
<string>group.com.SideStore.SideStore</string>
|
||||
</array>
|
||||
<key>ALTBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>AltBackup General</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>altbackup</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
14
SideStoreApp/Sources/SideBackup/UIColor+AltBackup.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// UIColor+AltBackup.swift
|
||||
// AltBackup
|
||||
//
|
||||
// Created by Riley Testut on 5/11/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIColor {
|
||||
static let altstoreBackground = UIColor(named: "Background")!
|
||||
static let altstoreText = UIColor(named: "Text")!
|
||||
}
|
||||
189
SideStoreApp/Sources/SideBackup/ViewController.swift
Normal file
@@ -0,0 +1,189 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// AltBackup
|
||||
//
|
||||
// Created by Riley Testut on 5/11/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension Bundle {
|
||||
var appName: String? {
|
||||
let appName =
|
||||
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ??
|
||||
Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String
|
||||
return appName
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewController {
|
||||
enum BackupOperation {
|
||||
case backup
|
||||
case restore
|
||||
}
|
||||
}
|
||||
|
||||
class ViewController: UIViewController {
|
||||
private let backupController = BackupController()
|
||||
|
||||
private var currentOperation: BackupOperation? {
|
||||
didSet {
|
||||
DispatchQueue.main.async {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var textLabel: UILabel!
|
||||
private var detailTextLabel: UILabel!
|
||||
private var activityIndicatorView: UIActivityIndicatorView!
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
.lightContent
|
||||
}
|
||||
|
||||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
||||
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.backup), name: AppDelegate.startBackupNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.restore), name: AppDelegate.startRestoreNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = .altstoreBackground
|
||||
|
||||
textLabel = UILabel(frame: .zero)
|
||||
textLabel.font = UIFont.preferredFont(forTextStyle: .title2)
|
||||
textLabel.textColor = .altstoreText
|
||||
textLabel.textAlignment = .center
|
||||
textLabel.numberOfLines = 0
|
||||
|
||||
detailTextLabel = UILabel(frame: .zero)
|
||||
detailTextLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
detailTextLabel.textColor = .altstoreText
|
||||
detailTextLabel.textAlignment = .center
|
||||
detailTextLabel.numberOfLines = 0
|
||||
|
||||
activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge)
|
||||
activityIndicatorView.color = .altstoreText
|
||||
activityIndicatorView.startAnimating()
|
||||
|
||||
#if DEBUG
|
||||
let button1 = UIButton(type: .system)
|
||||
button1.setTitle("Backup", for: .normal)
|
||||
button1.setTitleColor(.white, for: .normal)
|
||||
button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
|
||||
|
||||
let button2 = UIButton(type: .system)
|
||||
button2.setTitle("Restore", for: .normal)
|
||||
button2.setTitleColor(.white, for: .normal)
|
||||
button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
|
||||
|
||||
let arrangedSubviews = [textLabel!, detailTextLabel!, activityIndicatorView!, button1, button2]
|
||||
#else
|
||||
let arrangedSubviews = [textLabel!, detailTextLabel!, activityIndicatorView!]
|
||||
#endif
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.spacing = 22
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .center
|
||||
view.addSubview(stackView)
|
||||
|
||||
NSLayoutConstraint.activate([stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
stackView.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1.0),
|
||||
view.safeAreaLayoutGuide.trailingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 1.0)])
|
||||
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
private extension ViewController {
|
||||
@objc func backup() {
|
||||
currentOperation = .backup
|
||||
|
||||
backupController.performBackup { result in
|
||||
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
|
||||
|
||||
let title = String(format: NSLocalizedString("%@ could not be backed up.", comment: ""), appName)
|
||||
self.process(result, errorTitle: title)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func restore() {
|
||||
currentOperation = .restore
|
||||
|
||||
backupController.restoreBackup { result in
|
||||
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
|
||||
|
||||
let title = String(format: NSLocalizedString("%@ could not be restored.", comment: ""), appName)
|
||||
self.process(result, errorTitle: title)
|
||||
}
|
||||
}
|
||||
|
||||
func update() {
|
||||
switch currentOperation {
|
||||
case .backup:
|
||||
textLabel.text = NSLocalizedString("Backing up app data…", comment: "")
|
||||
detailTextLabel.isHidden = true
|
||||
activityIndicatorView.startAnimating()
|
||||
|
||||
case .restore:
|
||||
textLabel.text = NSLocalizedString("Restoring app data…", comment: "")
|
||||
detailTextLabel.isHidden = true
|
||||
activityIndicatorView.startAnimating()
|
||||
|
||||
case .none:
|
||||
textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""),
|
||||
Bundle.main.appName ?? NSLocalizedString("App", comment: ""))
|
||||
|
||||
detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in SideStore to continue using it.", comment: ""),
|
||||
Bundle.main.appName ?? NSLocalizedString("this app", comment: ""))
|
||||
|
||||
detailTextLabel.isHidden = false
|
||||
activityIndicatorView.stopAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ViewController {
|
||||
func process(_ result: Result<Void, Error>, errorTitle: String) {
|
||||
DispatchQueue.main.async {
|
||||
switch result {
|
||||
case .success: break
|
||||
case let .failure(error as NSError):
|
||||
let message: String
|
||||
|
||||
if let sourceDescription = error.sourceDescription {
|
||||
message = error.localizedDescription + "\n\n" + sourceDescription
|
||||
} else {
|
||||
message = error.localizedDescription
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: errorTitle, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: AppDelegate.operationDidFinishNotification, object: nil, userInfo: [AppDelegate.operationResultKey: result])
|
||||
}
|
||||
}
|
||||
|
||||
@objc func didEnterBackground(_: Notification) {
|
||||
// Reset UI once we've left app (but not before).
|
||||
currentOperation = nil
|
||||
}
|
||||
}
|
||||
59
SideStoreApp/Sources/SideDaemon/AltDaemon-Bridging-Header.h
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// Shared
|
||||
#import "ALTConstants.h"
|
||||
#import "ALTConnection.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
#import "CFNotificationName+AltStore.h"
|
||||
|
||||
// libproc
|
||||
int proc_pidpath(int pid, void * buffer, uint32_t buffersize);
|
||||
|
||||
// Security.framework
|
||||
CF_ENUM(uint32_t) {
|
||||
kSecCSInternalInformation = 1 << 0,
|
||||
kSecCSSigningInformation = 1 << 1,
|
||||
kSecCSRequirementInformation = 1 << 2,
|
||||
kSecCSDynamicInformation = 1 << 3,
|
||||
kSecCSContentInformation = 1 << 4,
|
||||
kSecCSSkipResourceDirectory = 1 << 5,
|
||||
kSecCSCalculateCMSDigest = 1 << 6,
|
||||
};
|
||||
|
||||
OSStatus SecStaticCodeCreateWithPath(CFURLRef path, uint32_t flags, void ** __nonnull CF_RETURNS_RETAINED staticCode);
|
||||
OSStatus SecCodeCopySigningInformation(void *code, uint32_t flags, CFDictionaryRef * __nonnull CF_RETURNS_RETAINED information);
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AKDevice : NSObject
|
||||
|
||||
@property (class, readonly) AKDevice *currentDevice;
|
||||
|
||||
@property (strong, readonly) NSString *serialNumber;
|
||||
@property (strong, readonly) NSString *uniqueDeviceIdentifier;
|
||||
@property (strong, readonly) NSString *serverFriendlyDescription;
|
||||
|
||||
@end
|
||||
|
||||
@interface AKAppleIDSession : NSObject
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier;
|
||||
|
||||
- (NSDictionary<NSString *, NSString *> *)appleIDHeadersForRequest:(NSURLRequest *)request;
|
||||
|
||||
@end
|
||||
|
||||
@interface LSApplicationWorkspace : NSObject
|
||||
|
||||
@property (class, readonly) LSApplicationWorkspace *defaultWorkspace;
|
||||
|
||||
- (BOOL)installApplication:(NSURL *)fileURL withOptions:(nullable NSDictionary<NSString *, id> *)options error:(NSError *_Nullable *)error;
|
||||
- (BOOL)uninstallApplication:(NSString *)bundleIdentifier withOptions:(nullable NSDictionary *)options;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
22
SideStoreApp/Sources/SideDaemon/AltDaemon.entitlements
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>application-identifier</key>
|
||||
<string>$(DEVELOPMENT_TEAM).$(ORG_IDENTIFIER).AltDaemon</string>
|
||||
<key>get-task-allow</key>
|
||||
<true/>
|
||||
<key>platform-application</key>
|
||||
<true/>
|
||||
<key>com.apple.authkit.client.private</key>
|
||||
<true/>
|
||||
<key>com.apple.private.mobileinstall.allowedSPI</key>
|
||||
<array>
|
||||
<string>Install</string>
|
||||
<string>Uninstall</string>
|
||||
<string>InstallForLaunchServices</string>
|
||||
<string>UninstallForLaunchServices</string>
|
||||
<string>InstallLocalProvisioned</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
60
SideStoreApp/Sources/SideDaemon/AnisetteDataManager.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// AnisetteDataManager.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
private extension UserDefaults {
|
||||
@objc var localUserID: String? {
|
||||
get { string(forKey: #keyPath(UserDefaults.localUserID)) }
|
||||
set { set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
|
||||
}
|
||||
}
|
||||
|
||||
struct AnisetteDataManager {
|
||||
static let shared = AnisetteDataManager()
|
||||
|
||||
private let dateFormatter = ISO8601DateFormatter()
|
||||
|
||||
private init() {
|
||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW)
|
||||
}
|
||||
|
||||
func requestAnisetteData() throws -> ALTAnisetteData {
|
||||
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
let akAppleIDSession = unsafeBitCast(NSClassFromString("AKAppleIDSession")!, to: AKAppleIDSession.Type.self)
|
||||
let akDevice = unsafeBitCast(NSClassFromString("AKDevice")!, to: AKDevice.Type.self)
|
||||
|
||||
let session = akAppleIDSession.init(identifier: "com.apple.gs.xcode.auth")
|
||||
let headers = session.appleIDHeaders(for: request)
|
||||
|
||||
let device = akDevice.current
|
||||
let date = dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
|
||||
|
||||
var localUserID = UserDefaults.standard.localUserID
|
||||
if localUserID == nil {
|
||||
localUserID = UUID().uuidString
|
||||
UserDefaults.standard.localUserID = localUserID
|
||||
}
|
||||
|
||||
let anisetteData = ALTAnisetteData(machineID: headers["X-Apple-I-MD-M"] ?? "",
|
||||
oneTimePassword: headers["X-Apple-I-MD"] ?? "",
|
||||
localUserID: headers["X-Apple-I-MD-LU"] ?? localUserID ?? "",
|
||||
routingInfo: UInt64(headers["X-Apple-I-MD-RINFO"] ?? "") ?? 0,
|
||||
deviceUniqueIdentifier: device.uniqueDeviceIdentifier,
|
||||
deviceSerialNumber: device.serialNumber,
|
||||
deviceDescription: "<MacBookPro15,1> <Mac OS X;10.15.2;19C57> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>",
|
||||
date: date,
|
||||
locale: .current,
|
||||
timeZone: .current)
|
||||
return anisetteData
|
||||
}
|
||||
}
|
||||
116
SideStoreApp/Sources/SideDaemon/AppManager.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// AppManager.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
private extension URL {
|
||||
static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true)
|
||||
}
|
||||
|
||||
private extension CFNotificationName {
|
||||
static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString)
|
||||
}
|
||||
|
||||
struct AppManager {
|
||||
static let shared = AppManager()
|
||||
|
||||
private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated)
|
||||
private let profilesQueue = OperationQueue()
|
||||
|
||||
private let fileCoordinator = NSFileCoordinator()
|
||||
|
||||
private init() {
|
||||
profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue"
|
||||
profilesQueue.qualityOfService = .userInitiated
|
||||
}
|
||||
|
||||
func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles _: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
appQueue.async {
|
||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||
|
||||
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String: Any]
|
||||
let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) }
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
|
||||
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
appQueue.async {
|
||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||
lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil)
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||
fileCoordinator.coordinate(with: [intent], queue: profilesQueue) { error in
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
let installingBundleIDs = Set(profiles.map(\.bundleIdentifier))
|
||||
|
||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||
|
||||
// Remove all inactive profiles (if active profiles are provided), and the previous profiles.
|
||||
for fileURL in profileURLs {
|
||||
// Use memory mapping to reduce peak memory usage and stay within limit.
|
||||
guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue }
|
||||
|
||||
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile) {
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
} else {
|
||||
print("Ignoring:", profile.bundleIdentifier, profile.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
for profile in profiles {
|
||||
let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased())
|
||||
try profile.data.write(to: destinationURL, options: .atomic)
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
} catch {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
|
||||
// Notify system to prevent accidentally untrusting developer certificate.
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
|
||||
}
|
||||
}
|
||||
|
||||
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||
fileCoordinator.coordinate(with: [intent], queue: profilesQueue) { error in
|
||||
do {
|
||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||
|
||||
for fileURL in profileURLs {
|
||||
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
|
||||
|
||||
if bundleIdentifiers.contains(profile.bundleIdentifier) {
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
} catch {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
|
||||
// Notify system to prevent accidentally untrusting developer certificate.
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
109
SideStoreApp/Sources/SideDaemon/DaemonRequestHandler.swift
Normal file
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// DaemonRequestHandler.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Shared
|
||||
import SideKit
|
||||
|
||||
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
|
||||
|
||||
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
|
||||
connectionHandlers: [XPCConnectionHandler()])
|
||||
|
||||
extension DaemonConnectionManager {
|
||||
static var shared: ConnectionManager {
|
||||
connectionManager
|
||||
}
|
||||
}
|
||||
|
||||
struct DaemonRequestHandler: RequestHandler {
|
||||
func handleAnisetteDataRequest(_: AnisetteDataRequest, for _: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void) {
|
||||
do {
|
||||
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
|
||||
|
||||
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
||||
completionHandler(.success(response))
|
||||
} catch {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void) {
|
||||
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
|
||||
|
||||
print("Awaiting begin installation request...")
|
||||
|
||||
connection.receiveRequest { result in
|
||||
print("Received begin installation request with result:", result)
|
||||
|
||||
do {
|
||||
guard case let .beginInstallation(request) = try result.get() else { throw ALTServerError(.unknownRequest) }
|
||||
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
|
||||
|
||||
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { result in
|
||||
let result = result.map { InstallationProgressResponse(progress: 1.0) }
|
||||
print("Installed app with result:", result)
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
} catch {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for _: Connection,
|
||||
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void) {
|
||||
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { result in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
|
||||
|
||||
let response = InstallProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for _: Connection,
|
||||
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void) {
|
||||
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { result in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Removed profiles:", request.bundleIdentifiers)
|
||||
|
||||
let response = RemoveProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveAppRequest(_ request: RemoveAppRequest, for _: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void) {
|
||||
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { result in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
print("Failed to remove app \(request.bundleIdentifier):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Removed app:", request.bundleIdentifier)
|
||||
|
||||
let response = RemoveAppResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
84
SideStoreApp/Sources/SideDaemon/XPCConnectionHandler.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// XPCConnectionHandler.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 9/14/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
class XPCConnectionHandler: NSObject, ConnectionHandler {
|
||||
var connectionHandler: ((Connection) -> Void)?
|
||||
var disconnectionHandler: ((Connection) -> Void)?
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility)
|
||||
private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) }
|
||||
|
||||
deinit {
|
||||
self.stopListening()
|
||||
}
|
||||
|
||||
func startListening() {
|
||||
for listener in listeners {
|
||||
listener.delegate = self
|
||||
listener.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func stopListening() {
|
||||
listeners.forEach { $0.suspend() }
|
||||
}
|
||||
}
|
||||
|
||||
private extension XPCConnectionHandler {
|
||||
func disconnect(_ connection: Connection) {
|
||||
connection.disconnect()
|
||||
|
||||
disconnectionHandler?(connection)
|
||||
}
|
||||
}
|
||||
|
||||
extension XPCConnectionHandler: NSXPCListenerDelegate {
|
||||
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
|
||||
let maximumPathLength = 4 * UInt32(MAXPATHLEN)
|
||||
|
||||
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength))
|
||||
defer { pathBuffer.deallocate() }
|
||||
|
||||
proc_pidpath(newConnection.processIdentifier, pathBuffer, maximumPathLength)
|
||||
|
||||
let path = String(cString: pathBuffer)
|
||||
let fileURL = URL(fileURLWithPath: path)
|
||||
|
||||
var code: UnsafeMutableRawPointer?
|
||||
defer { code.map { Unmanaged<AnyObject>.fromOpaque($0).release() } }
|
||||
|
||||
var status = SecStaticCodeCreateWithPath(fileURL as CFURL, 0, &code)
|
||||
guard status == 0 else { return false }
|
||||
|
||||
var signingInfo: CFDictionary?
|
||||
defer { signingInfo.map { Unmanaged<AnyObject>.passUnretained($0).release() } }
|
||||
|
||||
status = SecCodeCopySigningInformation(code, kSecCSInternalInformation | kSecCSSigningInformation, &signingInfo)
|
||||
guard status == 0 else { return false }
|
||||
|
||||
// Only accept connections from AltStore.
|
||||
guard
|
||||
let codeSigningInfo = signingInfo as? [String: Any],
|
||||
let bundleIdentifier = codeSigningInfo["identifier"] as? String,
|
||||
bundleIdentifier.contains(Bundle.Info.appbundleIdentifier)
|
||||
else { return false }
|
||||
|
||||
let connection = XPCConnection(newConnection)
|
||||
newConnection.invalidationHandler = { [weak self, weak connection] in
|
||||
guard let self = self, let connection = connection else { return }
|
||||
self.disconnect(connection)
|
||||
}
|
||||
|
||||
connectionHandler?(connection)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
14
SideStoreApp/Sources/SideDaemon/main.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// main.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/2/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
autoreleasepool {
|
||||
DaemonConnectionManager.shared.start()
|
||||
RunLoop.current.run()
|
||||
}
|
||||
10
SideStoreApp/Sources/SideDaemon/package/DEBIAN/control
Normal file
@@ -0,0 +1,10 @@
|
||||
Package: com.rileytestut.altdaemon
|
||||
Name: AltDaemon
|
||||
Depends:
|
||||
Version: 1.0
|
||||
Architecture: iphoneos-arm
|
||||
Description: AltDaemon allows AltStore to install and refresh apps without a computer.
|
||||
Maintainer: Riley Testut
|
||||
Author: Riley Testut
|
||||
Homepage: https://altstore.io
|
||||
Section: System
|
||||
2
SideStoreApp/Sources/SideDaemon/package/DEBIAN/postinst
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
launchctl load /Library/LaunchDaemons/com.rileytestut.altdaemon.plist
|
||||
2
SideStoreApp/Sources/SideDaemon/package/DEBIAN/preinst
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist >> /dev/null 2>&1
|
||||
2
SideStoreApp/Sources/SideDaemon/package/DEBIAN/prerm
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.rileytestut.altdaemon</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/bin/env</string>
|
||||
<string>_MSSafeMode=1</string>
|
||||
<string>_SafeMode=1</string>
|
||||
<string>/usr/bin/AltDaemon</string>
|
||||
</array>
|
||||
<key>UserName</key>
|
||||
<string>mobile</string>
|
||||
<key>KeepAlive</key>
|
||||
<false/>
|
||||
<key>RunAtLoad</key>
|
||||
<false/>
|
||||
<key>MachServices</key>
|
||||
<dict>
|
||||
<key>cy:io.altstore.altdaemon</key>
|
||||
<true/>
|
||||
<key>lh:io.altstore.altdaemon</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
SideStoreApp/Sources/SideDaemon/package/usr/bin/AltDaemon
Executable file
19
SideStoreApp/Sources/SidePatcher/SidePatcher.h
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// SidePatcher.h
|
||||
// SidePatcher
|
||||
//
|
||||
// Created by Riley Testut on 10/18/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SideAppPatcher : NSObject
|
||||
|
||||
- (BOOL)patchAppBinaryAtURL:(NSURL *)appFileURL withBinaryAtURL:(NSURL *)patchFileURL error:(NSError *_Nullable *)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
143
SideStoreApp/Sources/SidePatcher/SidePatcher.m
Normal file
@@ -0,0 +1,143 @@
|
||||
//
|
||||
// SidePatcher.m
|
||||
// SidePatcher
|
||||
//
|
||||
// Created by Riley Testut on 10/18/21.
|
||||
// Copied with minor modifications from sample code provided by Linus Henze.
|
||||
//
|
||||
|
||||
#import <SidePatcher/SidePatcher.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@import RoxasUIKit;
|
||||
|
||||
#define CPU_SUBTYPE_PAC 0x80000000
|
||||
#define FAT_MAGIC 0xcafebabe
|
||||
|
||||
#define ROUND_TO_PAGE(val) (((val % 0x4000) == 0) ? val : (val + (0x4000 - (val & 0x3FFF))))
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint32_t cpuType;
|
||||
uint32_t cpuSubType;
|
||||
// Incomplete, we don't need anything else
|
||||
} MachOHeader;
|
||||
|
||||
typedef struct {
|
||||
uint32_t cpuType;
|
||||
uint32_t cpuSubType;
|
||||
uint32_t fileOffset;
|
||||
uint32_t size;
|
||||
uint32_t alignment;
|
||||
} FatArch;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint32_t archCount;
|
||||
FatArch archs[0];
|
||||
} FatHeader;
|
||||
|
||||
// Given two MachO files, return a FAT file with the following properties:
|
||||
// 1. installd will still see the original MachO and validate it's code signature
|
||||
// 2. The kernel will only see the injected MachO instead
|
||||
//
|
||||
// Only arm64e for now
|
||||
void *injectApp(void *originalApp, size_t originalAppSize, void *appToInject, size_t appToInjectSize, size_t *outputSize) {
|
||||
*outputSize = 0;
|
||||
|
||||
// First validate the App to inject: It must be an arm64e application
|
||||
if (appToInjectSize < sizeof(MachOHeader)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
MachOHeader *injectedHeader = (MachOHeader*) appToInject;
|
||||
if (injectedHeader->cpuType != CPU_TYPE_ARM64) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (injectedHeader->cpuSubType != (CPU_SUBTYPE_ARM64E | CPU_SUBTYPE_PAC)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Ok, the App to inject is ok
|
||||
// Now build a fat header
|
||||
size_t originalAppSizeRounded = ROUND_TO_PAGE(originalAppSize);
|
||||
size_t appToInjectSizeRounded = ROUND_TO_PAGE(appToInjectSize);
|
||||
size_t totalSize = 0x4000 /* Fat Header + Alignment */ + originalAppSizeRounded + appToInjectSizeRounded;
|
||||
|
||||
void *fatBuf = malloc(totalSize);
|
||||
if (fatBuf == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bzero(fatBuf, totalSize);
|
||||
|
||||
FatHeader *fatHeader = (FatHeader*) fatBuf;
|
||||
fatHeader->magic = htonl(FAT_MAGIC);
|
||||
fatHeader->archCount = htonl(2);
|
||||
|
||||
// Write first arch (original app)
|
||||
fatHeader->archs[0].cpuType = htonl(CPU_TYPE_ARM64);
|
||||
fatHeader->archs[0].cpuSubType = htonl(CPU_SUBTYPE_ARM64E); /* Note that this is not a valid cpu subtype */
|
||||
fatHeader->archs[0].fileOffset = htonl(0x4000);
|
||||
fatHeader->archs[0].size = htonl(originalAppSize);
|
||||
fatHeader->archs[0].alignment = htonl(0xE);
|
||||
|
||||
// Write second arch (injected app)
|
||||
fatHeader->archs[1].cpuType = htonl(CPU_TYPE_ARM64);
|
||||
fatHeader->archs[1].cpuSubType = htonl(CPU_SUBTYPE_ARM64E | CPU_SUBTYPE_PAC);
|
||||
fatHeader->archs[1].fileOffset = htonl(0x4000 + originalAppSizeRounded);
|
||||
fatHeader->archs[1].size = htonl(appToInjectSize);
|
||||
fatHeader->archs[1].alignment = htonl(0xE);
|
||||
|
||||
// Ok, now write the MachOs
|
||||
memcpy(fatBuf + 0x4000, originalApp, originalAppSize);
|
||||
memcpy(fatBuf + 0x4000 + originalAppSizeRounded, appToInject, appToInjectSize);
|
||||
|
||||
// We're done!
|
||||
*outputSize = totalSize;
|
||||
return fatBuf;
|
||||
}
|
||||
|
||||
@implementation SideAppPatcher
|
||||
|
||||
- (BOOL)patchAppBinaryAtURL:(NSURL *)appFileURL withBinaryAtURL:(NSURL *)patchFileURL error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NSMutableData *originalApp = [NSMutableData dataWithContentsOfURL:appFileURL options:0 error:error];
|
||||
if (originalApp == nil)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSMutableData *injectedApp = [NSMutableData dataWithContentsOfURL:patchFileURL options:0 error:error];
|
||||
if (injectedApp == nil)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
size_t outputSize = 0;
|
||||
void *output = injectApp(originalApp.mutableBytes, originalApp.length, injectedApp.mutableBytes, injectedApp.length, &outputSize);
|
||||
if (output == NULL)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
// If injectApp fails, it means the patch app is in the wrong format.
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{NSURLErrorKey: patchFileURL}];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSData *outputData = [NSData dataWithBytesNoCopy:output length:outputSize freeWhenDone:YES];
|
||||
if (![outputData writeToURL:appFileURL options:NSDataWritingAtomic error:error])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// SidePatcher.h
|
||||
// SidePatcher
|
||||
//
|
||||
// Created by Joseph Mattiello on 03/01/23.
|
||||
// Copyright © 2023 Provenance Emu. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for SideAppPatcher.
|
||||
FOUNDATION_EXPORT double SideAppPatcherVersionNumber;
|
||||
|
||||
//! Project version string for SideAppPatcher.
|
||||
FOUNDATION_EXPORT const unsigned char SideAppPatcherVersionString[];
|
||||
|
||||
|
||||
# pragma mark - SidePatcher
|
||||
#import <SidePatcher/_SidePatcher.h>
|
||||
@@ -0,0 +1 @@
|
||||
../../SidePatcher.h
|
||||
369
SideStoreApp/Sources/SideStore/AppDelegate.swift
Normal file
@@ -0,0 +1,369 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/9/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Intents
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
import AltSign
|
||||
import SideStoreCore
|
||||
import SideStoreAppKit
|
||||
import EmotionalDamage
|
||||
import RoxasUIKit
|
||||
|
||||
@UIApplicationMain
|
||||
final class AppDelegate: SideStoreAppDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
@available(iOS 14, *)
|
||||
private var intentHandler: IntentHandler {
|
||||
get { _intentHandler as! IntentHandler }
|
||||
set { _intentHandler = newValue }
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
private var viewAppIntentHandler: ViewAppIntentHandler {
|
||||
get { _viewAppIntentHandler as! ViewAppIntentHandler }
|
||||
set { _viewAppIntentHandler = newValue }
|
||||
}
|
||||
|
||||
private lazy var _intentHandler: Any = {
|
||||
guard #available(iOS 14, *) else { fatalError() }
|
||||
return IntentHandler()
|
||||
}()
|
||||
|
||||
private lazy var _viewAppIntentHandler: Any = {
|
||||
guard #available(iOS 14, *) else { fatalError() }
|
||||
return ViewAppIntentHandler()
|
||||
}()
|
||||
|
||||
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Register default settings before doing anything else.
|
||||
UserDefaults.registerDefaults()
|
||||
|
||||
DatabaseManager.shared.start { error in
|
||||
if let error = error {
|
||||
print("Failed to start DatabaseManager. Error:", error as Any)
|
||||
} else {
|
||||
print("Started DatabaseManager.")
|
||||
}
|
||||
}
|
||||
|
||||
AnalyticsManager.shared.start()
|
||||
|
||||
setTintColor()
|
||||
|
||||
SecureValueTransformer.register()
|
||||
|
||||
if UserDefaults.standard.firstLaunch == nil {
|
||||
Keychain.shared.reset()
|
||||
UserDefaults.standard.firstLaunch = Date()
|
||||
}
|
||||
|
||||
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
|
||||
|
||||
#if DEBUG || BETA
|
||||
UserDefaults.standard.isDebugModeEnabled = true
|
||||
#endif
|
||||
|
||||
prepareForBackgroundFetch()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_: UIApplication) {
|
||||
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
|
||||
|
||||
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
|
||||
|
||||
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
|
||||
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
|
||||
switch result {
|
||||
case .success: break
|
||||
case let .failure(error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_: UIApplication) {
|
||||
AppManager.shared.update()
|
||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||
}
|
||||
|
||||
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
|
||||
open(url)
|
||||
}
|
||||
|
||||
func application(_: UIApplication, handlerFor intent: INIntent) -> Any? {
|
||||
guard #available(iOS 14, *) else { return nil }
|
||||
|
||||
switch intent {
|
||||
case is RefreshAllIntent: return intentHandler
|
||||
case is ViewAppIntent: return viewAppIntentHandler
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
extension AppDelegate {
|
||||
func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
func application(_: UIApplication, didDiscardSceneSessions _: Set<UISceneSession>) {
|
||||
// Called when the user discards a scene session.
|
||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate {
|
||||
func setTintColor() {
|
||||
window?.tintColor = .altPrimary
|
||||
}
|
||||
|
||||
func open(_ url: URL) -> Bool {
|
||||
if url.isFileURL {
|
||||
guard url.pathExtension.lowercased() == "ipa" else { return false }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url])
|
||||
}
|
||||
|
||||
return true
|
||||
} else {
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
||||
guard let host = components.host?.lowercased() else { return false }
|
||||
|
||||
switch host {
|
||||
case "patreon":
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
case "appbackupresponse":
|
||||
let result: Result<Void, Error>
|
||||
|
||||
switch url.path.lowercased() {
|
||||
case "/success": result = .success(())
|
||||
case "/failure":
|
||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:]
|
||||
guard
|
||||
let errorDomain = queryItems["errorDomain"],
|
||||
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
|
||||
let errorDescription = queryItems["errorDescription"]
|
||||
else { return false }
|
||||
|
||||
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
|
||||
result = .failure(error)
|
||||
|
||||
default: return false
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
|
||||
|
||||
return true
|
||||
|
||||
case "install":
|
||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
||||
guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
case "source":
|
||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
||||
guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate {
|
||||
private func prepareForBackgroundFetch() {
|
||||
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
|
||||
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
|
||||
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
#endif
|
||||
}
|
||||
|
||||
func application(_: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||
{
|
||||
let tokenParts = deviceToken.map { data -> String in
|
||||
String(format: "%02.2hhx", data)
|
||||
}
|
||||
|
||||
let token = tokenParts.joined()
|
||||
print("Push Token:", token)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||
self.application(application, performFetchWithCompletionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func application(_: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification {
|
||||
let threeHours: TimeInterval = 3 * 60 * 60
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("App Refresh Tip", comment: "")
|
||||
content.body = NSLocalizedString("The more you open SideStore, the more chances it's given to refresh apps in the background.", comment: "")
|
||||
|
||||
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
UserDefaults.standard.presentedLaunchReminderNotification = true
|
||||
}
|
||||
|
||||
BackgroundTaskManager.shared.performExtendedBackgroundTask { taskResult, taskCompletionHandler in
|
||||
if let error = taskResult.error {
|
||||
print("Error starting extended background task. Aborting.", error)
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
taskCompletionHandler()
|
||||
return
|
||||
}
|
||||
|
||||
if !DatabaseManager.shared.isStarted {
|
||||
DatabaseManager.shared.start { error in
|
||||
if error != nil {
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
taskCompletionHandler()
|
||||
} else {
|
||||
self.performBackgroundFetch { backgroundFetchResult in
|
||||
backgroundFetchCompletionHandler(backgroundFetchResult)
|
||||
} refreshAppsCompletionHandler: { _ in
|
||||
taskCompletionHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.performBackgroundFetch { backgroundFetchResult in
|
||||
backgroundFetchCompletionHandler(backgroundFetchResult)
|
||||
} refreshAppsCompletionHandler: { _ in
|
||||
taskCompletionHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
||||
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) {
|
||||
fetchSources { result in
|
||||
switch result {
|
||||
case .failure: backgroundFetchCompletionHandler(.failed)
|
||||
case .success: backgroundFetchCompletionHandler(.newData)
|
||||
}
|
||||
|
||||
if !UserDefaults.standard.isBackgroundRefreshEnabled {
|
||||
refreshAppsCompletionHandler(.success([:]))
|
||||
}
|
||||
}
|
||||
|
||||
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
||||
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
|
||||
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate {
|
||||
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void) {
|
||||
AppManager.shared.fetchSources { result in
|
||||
do {
|
||||
let (sources, context) = try result.get()
|
||||
|
||||
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
||||
previousUpdatesFetchRequest.includesPendingChanges = false
|
||||
previousUpdatesFetchRequest.resultType = .dictionaryResultType
|
||||
previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)]
|
||||
|
||||
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
|
||||
previousNewsItemsFetchRequest.includesPendingChanges = false
|
||||
previousNewsItemsFetchRequest.resultType = .dictionaryResultType
|
||||
previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)]
|
||||
|
||||
let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]]
|
||||
let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]]
|
||||
|
||||
try context.save()
|
||||
|
||||
let updatesFetchRequest = InstalledApp.updatesFetchRequest()
|
||||
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
||||
|
||||
let updates = try context.fetch(updatesFetchRequest)
|
||||
let newsItems = try context.fetch(newsItemsFetchRequest)
|
||||
|
||||
for update in updates {
|
||||
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
|
||||
guard let storeApp = update.storeApp, let version = storeApp.version else { continue }
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("New Update Available", comment: "")
|
||||
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, version)
|
||||
content.sound = .default
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
}
|
||||
|
||||
for newsItem in newsItems {
|
||||
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
|
||||
guard !newsItem.isSilent else { continue }
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
if let app = newsItem.storeApp {
|
||||
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
|
||||
} else {
|
||||
content.title = NSLocalizedString("SideStore News", comment: "")
|
||||
}
|
||||
|
||||
content.body = newsItem.title
|
||||
content.sound = .default
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.applicationIconBadgeNumber = updates.count
|
||||
}
|
||||
|
||||
completionHandler(.success(sources))
|
||||
} catch {
|
||||
print("Error fetching apps:", error)
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
218
SideStoreApp/Sources/SideStore/LaunchViewController.swift
Normal file
@@ -0,0 +1,218 @@
|
||||
//
|
||||
// LaunchViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/30/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import EmotionalDamage
|
||||
import minimuxer
|
||||
import MiniMuxerSwift
|
||||
import SideStoreAppKit
|
||||
import RoxasUIKit
|
||||
import UIKit
|
||||
|
||||
import SideStoreCore
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate {
|
||||
private var didFinishLaunching = false
|
||||
|
||||
private var destinationViewController: UIViewController!
|
||||
|
||||
override var launchConditions: [RSTLaunchCondition] {
|
||||
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { completionHandler in
|
||||
DatabaseManager.shared.start(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
return [isDatabaseStarted]
|
||||
}
|
||||
|
||||
override var childForStatusBarStyle: UIViewController? {
|
||||
self.children.first
|
||||
}
|
||||
|
||||
override var childForStatusBarHidden: UIViewController? {
|
||||
self.children.first
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
defer {
|
||||
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
||||
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
||||
}
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_: Bool) {
|
||||
super.viewDidAppear(true)
|
||||
#if !targetEnvironment(simulator)
|
||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||
|
||||
guard let pf = fetchPairingFile() else {
|
||||
displayError("Device pairing file not found.")
|
||||
return
|
||||
}
|
||||
start_minimuxer_threads(pf)
|
||||
#endif
|
||||
}
|
||||
|
||||
func fetchPairingFile() -> String? {
|
||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||
let fm = FileManager.default
|
||||
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
||||
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
|
||||
print("Loaded ALTPairingFile from \(documentsPath.path)")
|
||||
return contents
|
||||
} else if
|
||||
let appResourcePath = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"),
|
||||
fm.fileExists(atPath: appResourcePath.path),
|
||||
let data = fm.contents(atPath: appResourcePath.path),
|
||||
let contents = String(data: data, encoding: .utf8),
|
||||
!contents.isEmpty
|
||||
{
|
||||
print("Loaded ALTPairingFile from \(appResourcePath.path)")
|
||||
return contents
|
||||
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here") {
|
||||
print("Loaded ALTPairingFile from Info.plist")
|
||||
return plistString
|
||||
} else {
|
||||
// Show an alert explaining the pairing file
|
||||
// Create new Alert
|
||||
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://wiki.sidestore.io/guides/install#pairing-process", preferredStyle: .alert)
|
||||
|
||||
// Create OK button with action handler
|
||||
let ok = UIAlertAction(title: "OK", style: .default, handler: { _ in
|
||||
// Try to load it from a file picker
|
||||
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
|
||||
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
|
||||
types.append(.xml)
|
||||
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
|
||||
documentPickerController.shouldShowFileExtensions = true
|
||||
documentPickerController.delegate = self
|
||||
self.present(documentPickerController, animated: true, completion: nil)
|
||||
})
|
||||
|
||||
// Add OK button to a dialog message
|
||||
dialogMessage.addAction(ok)
|
||||
|
||||
// Present Alert to
|
||||
present(dialogMessage, animated: true, completion: nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func displayError(_ msg: String) {
|
||||
print(msg)
|
||||
// Create a new alert
|
||||
let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
|
||||
|
||||
// Present alert to user
|
||||
present(dialogMessage, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
let url = urls[0]
|
||||
let isSecuredURL = url.startAccessingSecurityScopedResource() == true
|
||||
|
||||
do {
|
||||
// Read to a string
|
||||
let data1 = try Data(contentsOf: urls[0])
|
||||
let pairing_string = String(bytes: data1, encoding: .utf8)
|
||||
if pairing_string == nil {
|
||||
displayError("Unable to read pairing file")
|
||||
}
|
||||
|
||||
// Save to a file for next launch
|
||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||
let fm = FileManager.default
|
||||
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
||||
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
|
||||
|
||||
// Start minimuxer now that we have a file
|
||||
start_minimuxer_threads(pairing_string!)
|
||||
|
||||
} catch {
|
||||
displayError("Unable to read pairing file")
|
||||
}
|
||||
|
||||
if isSecuredURL {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
controller.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func documentPickerWasCancelled(_: UIDocumentPickerViewController) {
|
||||
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
|
||||
}
|
||||
|
||||
func start_minimuxer_threads(_ pairing_file: String) {
|
||||
set_usbmuxd_socket()
|
||||
#if false // Retries
|
||||
var res = start_minimuxer(pairing_file: pairing_file)
|
||||
var attempts = 10
|
||||
while attempts != 0, res != 0 {
|
||||
print("start_minimuxer `res` != 0, retry #\(attempts)")
|
||||
res = start_minimuxer(pairing_file: pairing_file)
|
||||
attempts -= 1
|
||||
}
|
||||
#else
|
||||
let res = start_minimuxer(pairing_file: pairing_file)
|
||||
#endif
|
||||
if res != 0 {
|
||||
displayError("minimuxer failed to start. Incorrect arguments were passed.")
|
||||
}
|
||||
auto_mount_dev_image()
|
||||
}
|
||||
}
|
||||
|
||||
extension LaunchViewController {
|
||||
override func handleLaunchError(_ error: Error) {
|
||||
do {
|
||||
throw error
|
||||
} catch let error as NSError {
|
||||
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
|
||||
|
||||
let errorDescription: String
|
||||
|
||||
if #available(iOS 14.5, *) {
|
||||
let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
|
||||
errorDescription = errorMessages.joined(separator: "\n\n")
|
||||
} else {
|
||||
errorDescription = error.debugDescription
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { _ in
|
||||
self.handleLaunchConditions()
|
||||
}))
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
override func finishLaunching() {
|
||||
super.finishLaunching()
|
||||
|
||||
guard !didFinishLaunching else { return }
|
||||
|
||||
AppManager.shared.update()
|
||||
AppManager.shared.updatePatronsIfNeeded()
|
||||
PatreonAPI.shared.refreshPatreonAccount()
|
||||
|
||||
// Add view controller as child (rather than presenting modally)
|
||||
// so tint adjustment + card presentations works correctly.
|
||||
destinationViewController.view.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height)
|
||||
destinationViewController.view.alpha = 0.0
|
||||
addChild(destinationViewController)
|
||||
view.addSubview(destinationViewController.view, pinningEdgesWith: .zero)
|
||||
destinationViewController.didMove(toParent: self)
|
||||
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.destinationViewController.view.alpha = 1.0
|
||||
}
|
||||
|
||||
didFinishLaunching = true
|
||||
}
|
||||
}
|
||||
BIN
SideStoreApp/Sources/SideStore/Resources/AltBackup.ipa
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 846 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 997 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1 @@
|
||||
{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]}
|
||||