2021-05-20 13:11:54 -07:00
//
// D e v e l o p e r D i s k M a n a g e r . s w i f t
// A l t S e r v e r
//
// C r e a t e d b y R i l e y T e s t u t o n 2 / 1 9 / 2 1 .
// C o p y r i g h t © 2 0 2 1 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import Foundation
import AltSign
enum DeveloperDiskError : LocalizedError
{
case unknownDownloadURL
case unsupportedOperatingSystem
case downloadedDiskNotFound
var errorDescription : String ? {
switch self
{
case . unknownDownloadURL : return NSLocalizedString ( " The URL to download the Developer disk image could not be determined. " , comment : " " )
case . unsupportedOperatingSystem : return NSLocalizedString ( " The device's operating system does not support installing Developer disk images. " , comment : " " )
case . downloadedDiskNotFound : return NSLocalizedString ( " DeveloperDiskImage.dmg and its signature could not be found in the downloaded archive. " , comment : " " )
}
}
}
private extension URL
{
#if STAGING
static let developerDiskDownloadURLs = URL ( string : " https://f000.backblazeb2.com/file/altstore-staging/altserver/developerdisks.json " ) !
#else
static let developerDiskDownloadURLs = URL ( string : " https://cdn.altstore.io/file/altstore/altserver/developerdisks.json " ) !
#endif
}
private extension DeveloperDiskManager
{
struct FetchURLsResponse : Decodable
{
struct Disks : Decodable
{
var iOS : [ String : DeveloperDiskURL ] ?
var tvOS : [ String : DeveloperDiskURL ] ?
}
var version : Int
var disks : Disks
}
enum DeveloperDiskURL : Decodable
{
case archive ( URL )
case separate ( diskURL : URL , signatureURL : URL )
private enum CodingKeys : CodingKey
{
case archive
case disk
case signature
}
init ( from decoder : Decoder ) throws
{
let container = try decoder . container ( keyedBy : CodingKeys . self )
if container . contains ( . archive )
{
let archiveURL = try container . decode ( URL . self , forKey : . archive )
self = . archive ( archiveURL )
}
else
{
let diskURL = try container . decode ( URL . self , forKey : . disk )
let signatureURL = try container . decode ( URL . self , forKey : . signature )
self = . separate ( diskURL : diskURL , signatureURL : signatureURL )
}
}
}
}
class DeveloperDiskManager
{
2022-02-09 13:52:11 -08:00
private let session = URLSession ( configuration : . ephemeral )
2021-05-20 13:11:54 -07:00
func downloadDeveloperDisk ( for device : ALTDevice , completionHandler : @ escaping ( Result < ( URL , URL ) , Error > ) -> Void )
{
let osVersion = " \( device . osVersion . majorVersion ) . \( device . osVersion . minorVersion ) "
2021-10-13 12:55:07 -07:00
let osName : String
2021-05-20 13:11:54 -07:00
let osKeyPath : KeyPath < FetchURLsResponse . Disks , [ String : DeveloperDiskURL ] ? >
switch device . type
{
2021-10-13 12:55:07 -07:00
case . iphone , . ipad :
osName = " iOS "
osKeyPath = \ FetchURLsResponse . Disks . iOS
case . appletv :
osName = " tvOS "
osKeyPath = \ FetchURLsResponse . Disks . tvOS
2021-05-20 13:11:54 -07:00
default : return completionHandler ( . failure ( DeveloperDiskError . unsupportedOperatingSystem ) )
}
do
{
2021-10-13 12:55:07 -07:00
let osDirectoryURL = FileManager . default . developerDisksDirectory . appendingPathComponent ( osName )
let developerDiskDirectoryURL = osDirectoryURL . appendingPathComponent ( osVersion , isDirectory : true )
2021-05-20 13:11:54 -07:00
try FileManager . default . createDirectory ( at : developerDiskDirectoryURL , withIntermediateDirectories : true , attributes : nil )
let developerDiskURL = developerDiskDirectoryURL . appendingPathComponent ( " DeveloperDiskImage.dmg " )
let developerDiskSignatureURL = developerDiskDirectoryURL . appendingPathComponent ( " DeveloperDiskImage.dmg.signature " )
guard ! FileManager . default . fileExists ( atPath : developerDiskURL . path ) || ! FileManager . default . fileExists ( atPath : developerDiskSignatureURL . path ) else {
return completionHandler ( . success ( ( developerDiskURL , developerDiskSignatureURL ) ) )
}
func finish ( _ result : Result < ( URL , URL ) , Error > )
{
do
{
let ( diskFileURL , signatureFileURL ) = try result . get ( )
try FileManager . default . copyItem ( at : diskFileURL , to : developerDiskURL )
try FileManager . default . copyItem ( at : signatureFileURL , to : developerDiskSignatureURL )
completionHandler ( . success ( ( developerDiskURL , developerDiskSignatureURL ) ) )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
self . fetchDeveloperDiskURLs { ( result ) in
do
{
let developerDiskURLs = try result . get ( )
guard let diskURL = developerDiskURLs [ keyPath : osKeyPath ] ? [ osVersion ] else { throw DeveloperDiskError . unknownDownloadURL }
switch diskURL
{
case . archive ( let archiveURL ) : self . downloadDiskArchive ( from : archiveURL , completionHandler : finish ( _ : ) )
case . separate ( let diskURL , let signatureURL ) : self . downloadDisk ( from : diskURL , signatureURL : signatureURL , completionHandler : finish ( _ : ) )
}
}
catch
{
finish ( . failure ( error ) )
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
private extension DeveloperDiskManager
{
func fetchDeveloperDiskURLs ( completionHandler : @ escaping ( Result < FetchURLsResponse . Disks , Error > ) -> Void )
{
2022-02-09 13:52:11 -08:00
let dataTask = self . session . dataTask ( with : . developerDiskDownloadURLs ) { ( data , response , error ) in
2021-05-20 13:11:54 -07:00
do
{
guard let data = data else { throw error ! }
let response = try JSONDecoder ( ) . decode ( FetchURLsResponse . self , from : data )
completionHandler ( . success ( response . disks ) )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
dataTask . resume ( )
}
func downloadDiskArchive ( from url : URL , completionHandler : @ escaping ( Result < ( URL , URL ) , Error > ) -> Void )
{
let downloadTask = URLSession . shared . downloadTask ( with : url ) { ( fileURL , response , error ) in
do
{
guard let fileURL = fileURL else { throw error ! }
defer { try ? FileManager . default . removeItem ( at : fileURL ) }
let temporaryDirectory = FileManager . default . temporaryDirectory . appendingPathComponent ( UUID ( ) . uuidString )
try FileManager . default . createDirectory ( at : temporaryDirectory , withIntermediateDirectories : true , attributes : nil )
defer { try ? FileManager . default . removeItem ( at : temporaryDirectory ) }
try FileManager . default . unzipArchive ( at : fileURL , toDirectory : temporaryDirectory )
guard let enumerator = FileManager . default . enumerator ( at : temporaryDirectory , includingPropertiesForKeys : nil , options : [ . skipsHiddenFiles , . skipsPackageDescendants ] ) else {
throw CocoaError ( . fileNoSuchFile , userInfo : [ NSURLErrorKey : temporaryDirectory ] )
}
var tempDiskFileURL : URL ?
var tempSignatureFileURL : URL ?
for case let fileURL as URL in enumerator
{
switch fileURL . pathExtension . lowercased ( )
{
case " dmg " : tempDiskFileURL = fileURL
case " signature " : tempSignatureFileURL = fileURL
default : break
}
}
guard let diskFileURL = tempDiskFileURL , let signatureFileURL = tempSignatureFileURL else { throw DeveloperDiskError . downloadedDiskNotFound }
completionHandler ( . success ( ( diskFileURL , signatureFileURL ) ) )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
downloadTask . resume ( )
}
func downloadDisk ( from diskURL : URL , signatureURL : URL , completionHandler : @ escaping ( Result < ( URL , URL ) , Error > ) -> Void )
{
let temporaryDirectory = FileManager . default . temporaryDirectory . appendingPathComponent ( UUID ( ) . uuidString )
do { try FileManager . default . createDirectory ( at : temporaryDirectory , withIntermediateDirectories : true , attributes : nil ) }
catch { return completionHandler ( . failure ( error ) ) }
var diskFileURL : URL ?
var signatureFileURL : URL ?
var downloadError : Error ?
let dispatchGroup = DispatchGroup ( )
dispatchGroup . enter ( )
dispatchGroup . enter ( )
let diskDownloadTask = URLSession . shared . downloadTask ( with : diskURL ) { ( fileURL , response , error ) in
do
{
guard let fileURL = fileURL else { throw error ! }
let destinationURL = temporaryDirectory . appendingPathComponent ( " DeveloperDiskImage.dmg " )
try FileManager . default . copyItem ( at : fileURL , to : destinationURL )
diskFileURL = destinationURL
}
catch
{
downloadError = error
}
dispatchGroup . leave ( )
}
let signatureDownloadTask = URLSession . shared . downloadTask ( with : signatureURL ) { ( fileURL , response , error ) in
do
{
guard let fileURL = fileURL else { throw error ! }
let destinationURL = temporaryDirectory . appendingPathComponent ( " DeveloperDiskImage.dmg.signature " )
try FileManager . default . copyItem ( at : fileURL , to : destinationURL )
signatureFileURL = destinationURL
}
catch
{
downloadError = error
}
dispatchGroup . leave ( )
}
diskDownloadTask . resume ( )
signatureDownloadTask . resume ( )
dispatchGroup . notify ( queue : . global ( qos : . userInitiated ) ) {
defer {
try ? FileManager . default . removeItem ( at : temporaryDirectory )
}
guard let diskFileURL = diskFileURL , let signatureFileURL = signatureFileURL else {
return completionHandler ( . failure ( downloadError ? ? DeveloperDiskError . downloadedDiskNotFound ) )
}
completionHandler ( . success ( ( diskFileURL , signatureFileURL ) ) )
}
}
}