Last active
July 6, 2023 17:31
-
-
Save VaslD/65f8f7ebc8a43fc525e9e12ac19b4549 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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