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 )
{
2022-03-01 16:03:03 -08:00
do
2021-05-20 13:11:54 -07:00
{
2022-03-01 16:03:03 -08:00
guard let osName = device . type . osName else { throw DeveloperDiskError . unsupportedOperatingSystem }
2021-10-13 12:55:07 -07:00
2022-03-01 16:03:03 -08:00
let osKeyPath : KeyPath < FetchURLsResponse . Disks , [ String : DeveloperDiskURL ] ? >
switch device . type
{
case . iphone , . ipad : osKeyPath = \ FetchURLsResponse . Disks . iOS
case . appletv : osKeyPath = \ FetchURLsResponse . Disks . tvOS
default : throw DeveloperDiskError . unsupportedOperatingSystem
}
var osVersion = device . osVersion
osVersion . patchVersion = 0 // P a t c h i s i r r e l e v a n t f o r d e v e l o p e r d i s k s
2021-10-13 12:55:07 -07:00
let osDirectoryURL = FileManager . default . developerDisksDirectory . appendingPathComponent ( osName )
2022-03-01 16:03:03 -08:00
let developerDiskDirectoryURL = osDirectoryURL . appendingPathComponent ( osVersion . stringValue , 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 " )
2022-03-01 16:03:03 -08:00
let isCachedDiskCompatible = self . isDeveloperDiskCompatible ( with : device )
if isCachedDiskCompatible && FileManager . default . fileExists ( atPath : developerDiskURL . path ) && FileManager . default . fileExists ( atPath : developerDiskSignatureURL . path )
{
// T h e d e v e l o p e r d i s k i s c a c h e d a n d w e ' v e c o n f i r m e d i t w o r k s , s o r e - u s e i t .
2021-05-20 13:11:54 -07:00
return completionHandler ( . success ( ( developerDiskURL , developerDiskSignatureURL ) ) )
}
func finish ( _ result : Result < ( URL , URL ) , Error > )
{
do
{
let ( diskFileURL , signatureFileURL ) = try result . get ( )
2022-03-01 16:03:03 -08:00
if FileManager . default . fileExists ( atPath : developerDiskURL . path )
{
try FileManager . default . removeItem ( at : developerDiskURL )
}
if FileManager . default . fileExists ( atPath : developerDiskSignatureURL . path )
{
try FileManager . default . removeItem ( at : developerDiskSignatureURL )
}
2021-05-20 13:11:54 -07:00
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 ( )
2022-03-01 16:03:03 -08:00
guard let diskURL = developerDiskURLs [ keyPath : osKeyPath ] ? [ osVersion . stringValue ] else { throw DeveloperDiskError . unknownDownloadURL }
2021-05-20 13:11:54 -07:00
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 ) )
}
}
2022-03-01 16:03:03 -08:00
func setDeveloperDiskCompatible ( _ isCompatible : Bool , with device : ALTDevice )
{
guard let id = self . developerDiskCompatibilityID ( for : device ) else { return }
UserDefaults . standard . set ( isCompatible , forKey : id )
}
func isDeveloperDiskCompatible ( with device : ALTDevice ) -> Bool
{
guard let id = self . developerDiskCompatibilityID ( for : device ) else { return false }
let isCompatible = UserDefaults . standard . bool ( forKey : id )
return isCompatible
}
2021-05-20 13:11:54 -07:00
}
private extension DeveloperDiskManager
{
2022-03-01 16:03:03 -08:00
func developerDiskCompatibilityID ( for device : ALTDevice ) -> String ?
{
guard let osName = device . type . osName else { return nil }
var osVersion = device . osVersion
osVersion . patchVersion = 0 // P a t c h i s i r r e l e v a n t f o r d e v e l o p e r d i s k s
let id = [ " ALTDeveloperDiskCompatible " , osName , device . osVersion . stringValue ] . joined ( separator : " _ " )
return id
}
2021-05-20 13:11:54 -07:00
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 ) ) )
}
}
}