Skip to content

Instantly share code, notes, and snippets.

@VaslD
Last active July 6, 2023 17:31
Show Gist options
  • Save VaslD/65f8f7ebc8a43fc525e9e12ac19b4549 to your computer and use it in GitHub Desktop.
Save VaslD/65f8f7ebc8a43fc525e9e12ac19b4549 to your computer and use it in GitHub Desktop.
#!/usr/bin/swift
import Foundation
import Security
let manager = FileManager.default
// Print usage if called with no arguments.
var inputs = CommandLine.arguments
if let command = inputs.first {
let executable = Bundle.main.executableURL!.standardized.path
let commandResolved: String
if command.starts(with: "/") {
commandResolved = URL(fileURLWithPath: command).standardized.path
} else {
commandResolved = URL(fileURLWithPath: "\(manager.currentDirectoryPath)/\(command)").standardized.path
}
if executable == commandResolved {
inputs.removeFirst()
}
}
guard !inputs.isEmpty else {
print("Usage: profiler profile.mobileprovision")
exit(EX_USAGE)
}
// Collect existing provisioning profiles from Xcode.
let systemProfiles = try manager.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("MobileDevice", isDirectory: true)
.appendingPathComponent("Provisioning Profiles", isDirectory: true)
var profiles = [String: ProvisioningProfile]()
for file in try manager.contentsOfDirectory(at: systemProfiles, includingPropertiesForKeys: nil) {
guard !file.lastPathComponent.starts(with: ".") else {
continue
}
guard let profile = try? ProvisioningProfile(file) else {
print("Read Error:", file.path)
continue
}
profiles[profile.application] = profile
}
// Collect user-supplied provisioning profiles.
var upgrades = [URL: ProvisioningProfile]()
for item in inputs {
var isDirectory: ObjCBool = false
// Ignore unavailable paths.
guard manager.fileExists(atPath: item, isDirectory: &isDirectory) else {
print("Not Found: ", item)
continue
}
var itemURL = URL(fileURLWithPath: item)
if !isDirectory.boolValue {
// Decode profile if path is a regular file.
if itemURL.pathExtension.lowercased() != "zip" {
guard let profile = try? ProvisioningProfile(itemURL) else {
print("Read Error: ", item)
continue
}
upgrades[itemURL] = profile
continue
}
// Extract archives if necessary.
let temporary = manager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let extractor = Process()
extractor.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
extractor.arguments = ["-x", "-k", itemURL.path, temporary.path]
try extractor.run()
extractor.waitUntilExit()
guard extractor.terminationStatus == 0 else {
print("Bad Archive:", item)
continue
}
let contents = try manager.contentsOfDirectory(at: temporary, includingPropertiesForKeys: nil)
if contents.count == 1,
try contents.first!.resourceValues(forKeys: [.isDirectoryKey]).isDirectory! {
itemURL = contents.first!
} else {
itemURL = temporary
}
}
// Descend if path is a directory.
// Extracting from archives always creates a directory.
for file in try manager.contentsOfDirectory(at: itemURL, includingPropertiesForKeys: nil) {
guard !file.lastPathComponent.starts(with: ".") else {
continue
}
guard let profile = try? ProvisioningProfile(file) else {
print("Read Error: ", item)
continue
}
upgrades[file] = profile
}
}
// Compare and upgrade provisioning profiles.
for (new, upgrade) in upgrades {
// Existing profiles are upgraded only if user-supplied are newer.
if let old = profiles[upgrade.application] {
guard old.expires < upgrade.expires else {
print("Not Upgrade:", new.path)
continue
}
let target = systemProfiles.appendingPathComponent(
old.id.uuidString.lowercased() + ".mobileprovision",
isDirectory: false
)
try manager.removeItem(at: target)
}
// Copy to Xcode provisioning profiles location.
let target = systemProfiles.appendingPathComponent(
upgrade.id.uuidString.lowercased() + ".mobileprovision",
isDirectory: false
)
do { try manager.copyItem(at: new, to: target) }
catch { print("Write Error:", target.path) }
}
// - MARK: Provisioning Profile
struct ProvisioningProfile: Identifiable {
let id: UUID
let application: String
let expires: Date
init(_ id: UUID, _ application: String, _ expires: Date) {
self.id = id
self.application = application
self.expires = expires
}
init(_ file: URL) throws {
// *.mobileprovision files are Cryptographic Message Syntax (CMS) encoded Property Lists (plist).
// CMS is a single-file containing both the original content and a crypto signature. We only need the plist.
var decoder: CMSDecoder!
guard CMSDecoderCreate(&decoder) == noErr else {
throw CocoaError(.coderReadCorrupt)
}
let fileData = try Data(contentsOf: file, options: .mappedIfSafe)
try fileData.withUnsafeBytes {
guard CMSDecoderUpdateMessage(decoder, $0.baseAddress!, $0.count) == noErr else {
throw CocoaError(.coderInvalidValue)
}
guard CMSDecoderFinalizeMessage(decoder) == noErr else {
throw CocoaError(.coderInvalidValue)
}
}
var profileData: CFData!
guard CMSDecoderCopyContent(decoder, &profileData) == noErr else {
throw CocoaError(.coderInvalidValue)
}
guard let profile = try PropertyListSerialization.propertyList(from: profileData as Data,
format: nil) as? [String: Any] else {
throw CocoaError(.coderValueNotFound)
}
let id = UUID(uuidString: profile["UUID"] as! String)!
let app = (profile["Entitlements"] as! [String: Any])["application-identifier"] as! String
let createdAt = profile["CreationDate"] as! Date
let validDays = profile["TimeToLive"] as! TimeInterval
self = ProvisioningProfile(id, app, createdAt + 86400 * validDays)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment