From 80e8b965839eb87446f08ee21b5fdce81c475f58 Mon Sep 17 00:00:00 2001 From: Trevor Rudolph Date: Tue, 5 Oct 2021 00:22:27 -0400 Subject: [PATCH] Add XGCompiler class Based on devkitppc --- .../XGAssemblyCodeExtensions.swift | 16 ++ Objects/managers/GoDShellManager.swift | 55 +--- Objects/managers/XGCompiler.swift | 272 ++++++++++++++++++ Objects/managers/XGCompilerData.swift | 142 +++++++++ enums/XGFiles.swift | 20 ++ enums/XGResources.swift | 2 +- extensions/XGCompilerExtension.swift | 93 ++++++ 7 files changed, 554 insertions(+), 46 deletions(-) create mode 100644 Objects/managers/XGCompiler.swift create mode 100644 Objects/managers/XGCompilerData.swift create mode 100644 extensions/XGCompilerExtension.swift diff --git a/Objects/file formats/XGAssemblyCodeExtensions.swift b/Objects/file formats/XGAssemblyCodeExtensions.swift index dff3cac0..3e0217a7 100644 --- a/Objects/file formats/XGAssemblyCodeExtensions.swift +++ b/Objects/file formats/XGAssemblyCodeExtensions.swift @@ -132,6 +132,22 @@ extension XGAssembly { } #endif + class func replaceRamASM(RAMOffset: Int, newASM asm: [UInt32]) { + var offset = RAMOffset + if offset > 0x80000000 { + offset -= 0x80000000 + } + #if GAME_PBR + replaceASM(startOffset: offset - kDolToRAMOffsetDifference, newASM: asm) + #else + if game == .XD, region == .US, offset > kRELtoRAMOffsetDifference { + replaceRELASM(startOffset: offset - kRELtoRAMOffsetDifference, newASM: asm) + } else { + replaceASM(startOffset: offset - kDolToRAMOffsetDifference, newASM: asm) + } + #endif + } + class func replaceRamASM(RAMOffset: Int, newASM asm: [XGASM]) { var offset = RAMOffset if offset > 0x80000000 { diff --git a/Objects/managers/GoDShellManager.swift b/Objects/managers/GoDShellManager.swift index 0f774cee..58b188e3 100755 --- a/Objects/managers/GoDShellManager.swift +++ b/Objects/managers/GoDShellManager.swift @@ -11,6 +11,11 @@ class GoDShellManager { enum Commands { case wit case wimgt + case gcc + case ld + case nm + case objdump + case objcopy case gcitool case gcitoolReplace case pbrSaveTool @@ -21,6 +26,11 @@ class GoDShellManager { switch self { case .wit: return .wit case .wimgt: return .wimgt + case .gcc: return .gcc + case .ld: return .ld + case .nm: return .nm + case .objdump: return .objdump + case .objcopy: return .objcopy case .gcitool: return .tool("gcitool") case .gcitoolReplace: return .tool("gcitool_replace") case .pbrSaveTool: return .tool("pbrsavetool") @@ -79,49 +89,4 @@ class GoDShellManager { return output } - @discardableResult - static func runAsync(_ command: Commands, args: String? = nil, inputRedirectFile: XGFiles? = nil, outputRedirectFile: XGFiles? = nil, errorRedirectFile: XGFiles? = nil) -> GoDProcess? { - guard command.file.exists else { - printg("command, \(command.file.fileName), doesn't exist\n\(command.file.path) not found") - return nil - } - - let process = Process() - process.executableURL = URL(fileURLWithPath: command.file.path) - if let args = args { - let escaped = args.replacingOccurrences(of: "\\ ", with: "") - process.arguments = escaped.split(separator: " ").compactMap(String.init).map({ (arg) -> String in - return arg.replacingOccurrences(of: "", with: " ") - }) - } - - if let inputFile = inputRedirectFile, inputFile.exists { - let fileHandle = FileHandle(forReadingAtPath: inputFile.path) - process.standardInput = fileHandle - } - - if let outputFile = outputRedirectFile { - let fileHandle = FileHandle(forWritingAtPath: outputFile.path) - process.standardOutput = fileHandle - } else { - process.standardOutput = nil - } - - if let errorFile = errorRedirectFile { - let fileHandle = FileHandle(forWritingAtPath: errorFile.path) - process.standardError = fileHandle - } else { - process.standardError = nil - } - - do { - try process.run() - return GoDProcess(process: process) - } catch let error { - printg("Shell error:", error) - } - return nil - } - - } diff --git a/Objects/managers/XGCompiler.swift b/Objects/managers/XGCompiler.swift new file mode 100644 index 00000000..6497dc85 --- /dev/null +++ b/Objects/managers/XGCompiler.swift @@ -0,0 +1,272 @@ +import Foundation + +enum XGCompilerTarget { + case ogc + case bare +} + +class XGCompiler: NSObject { + class func compileBinary(startOffset: Int, type: XGCompilerTarget = .bare, sources: [String], linkerScripts: [String], directoryPath: String) -> (instructions: [UInt32], exports: [String:UInt32], patches: [UInt32:UInt32]) { + var offset = startOffset + if offset < 0x80000000 { + offset += 0x80000000 + } + + let directory = URL(fileURLWithPath: directoryPath) + + let compiledFileURL = directory.appendingPathComponent("code.elf") + try? FileManager.default.removeItem(at: compiledFileURL) + + var args = sources.map{ directory.appendingPathComponent($0).path.escapedPath }.joined(separator: " ") + args += " -o \(compiledFileURL.path.escapedPath)" + args += " -I\(directoryPath.escapedPath)" + args += " -O1 -mcpu=750 -meabi -mhard-float" + args += " -mno-sdata -Wno-builtin-declaration-mismatch" + args += " -ffast-math -flto -fwhole-program -fdata-sections -fkeep-static-functions" + args += " -fno-tree-loop-distribute-patterns -fno-zero-initialized-in-bss" + args += " -Wl,-Map=\(directory.appendingPathComponent("code.map").path.escapedPath)" + args += linkerScripts.map { + var path = directory.appendingPathComponent($0).path.escapedPath + if $0.prefix(1) == "/" { + path = $0.escapedPath + } + return " -T \(path)" + }.joined(separator: " ") + switch type { + case .ogc: + args += " -I/opt/devkitpro/libogc/include -DGEKKO -mogc" + args += " -L/opt/devkitpro/libogc/lib/cube -ldb -lbba -lmad -lasnd -logc -lm" + args += " -Wl,--gc-sections -Wl,--section-start,.init=\(offset.hexString())" + case .bare: + args += " -nolibc -nostdlib -nodefaultlibs" + args += " -Wl,--gc-sections -Wl,--section-start,.text=\(offset.hexString())" + } + + // print(args) + GoDShellManager.run(.gcc, args: args, printOutput: false) + + var out = GoDShellManager.run(.objcopy, args: "--remove-section .comment \(compiledFileURL.path.escapedPath)", printOutput: false) + print(out!.dropLast()) + + out = GoDShellManager.run(.nm, args: "-a \(compiledFileURL.path.escapedPath)", printOutput: false) + let exports = XGCompiler.getExports(out!) + let imports = XGCompiler.getImports(out!) + + let patches = XGCompiler.getPatches(sources.map{ directory.appendingPathComponent($0).path }, imports: imports, exports: exports) + + if settings.verbose { + out = GoDShellManager.run(.objdump, args: "-D \(compiledFileURL.path.escapedPath)", printOutput: false) + printg("generated asm: \(out!.dropLast())") + } + + let codeBinaryFileURL = directory.appendingPathComponent("code.bin") + GoDShellManager.run(.objcopy, args: "-O binary \(compiledFileURL.path.escapedPath) \(codeBinaryFileURL.path.escapedPath)", printOutput: false) + + guard var codeBinaryData = try? Data(contentsOf: codeBinaryFileURL).rawBytes else { + fatalError("Could not read output compilation output") + } + + // pad to the nearest word + let words = codeBinaryData.count / 4 + let rem = codeBinaryData.count - (words * 4) + let padding = 4 - rem + codeBinaryData.append(contentsOf: [UInt8](repeating: 0, count: padding)) + + let instructions = codeBinaryData.withUnsafeBytes { + Array($0.bindMemory(to: UInt32.self)).map(UInt32.init(bigEndian:)) + } + + return (instructions, exports, patches) + } + + class func compileCode(startOffset: Int, code: String, include: String = "", externSymbols: [String:UInt32] = [:]) -> (instructions: [UInt32], exports: [String:UInt32], patches: [UInt32:UInt32]) { + let funcName = "__func" + + var offset = startOffset + if offset < 0x80000000 { + offset += 0x80000000 + } + + let tmpDir = try? XGTemporaryDirectory() + // tmpDir!.keepDirectory = true + let tmpDirPath = tmpDir!.directoryURL.path + let folder = XGFolders.path(tmpDirPath) + + let includeFile = XGFiles.nameAndFolder("code.h", folder) + XGUtility.saveString(XGCompilerConstants.dolphinIncludes+include, toFile: includeFile) + + let codeFile = XGFiles.nameAndFolder("code.c", folder) + XGUtility.saveString(""" + #include "code.h" + void \(funcName)() { + \(code) + } + """, toFile: codeFile) + + let linkFile = XGFiles.nameAndFolder("ngc.ld", folder) + XGUtility.saveString(generateLinkerScript(offset: offset, name: funcName), toFile: linkFile) + + let mapFile = XGFiles.nameAndFolder("map.ld", folder) + XGUtility.saveString(generateMapScript(), toFile: mapFile) + + let externFile = XGFiles.nameAndFolder("extern.ld", folder) + XGUtility.saveString(generateExternScript(externSymbols), toFile: externFile) + + return XGCompiler.compileBinary(startOffset: startOffset, type: .bare, sources: ["code.c"], linkerScripts: ["ngc.ld", "map.ld", "external.ld"], directoryPath: tmpDirPath) + } + + class func getPatches(_ sources: [String], imports: [String:UInt32], exports: [String:UInt32]) -> [UInt32:UInt32] { + var patches : [UInt32:UInt32] = [:] + + let pattern = "^PATCH_CALL\\((.*), (.*)\\)" + let regex = try! NSRegularExpression(pattern: pattern) + + for source in sources { + let contents = try! String(contentsOf: URL(fileURLWithPath: source)) + let lines = contents.components(separatedBy: CharacterSet.newlines) + + for line in lines { + let lineRange = NSRange(location: 0, length: line.utf16.count) + guard let match = regex.firstMatch(in: line, options: [], range: lineRange) else { continue } + + let locationRange = Range(match.range(at: 1), in: line)! + let location = String(line[locationRange]) + + var address : UInt32 + if let val = imports[location] { + address = val + } else { + address = UInt32(location.dropFirst(2), radix: 16)! + } + + let symbolRange = Range(match.range(at: 2), in: line)! + let symbol = String(line[symbolRange]) + + let code = exports[symbol]! + + patches[address] = code + } + + } + + return patches + } + + class func applyPatches(_ patches: [UInt32:UInt32]) { + for (absCallLocation, absCallTarget) in patches { + let callLocation = Int(absCallLocation) - 0x80000000 + let callTarget = Int(absCallTarget) - 0x80000000 + print("Patching function call at \(absCallLocation.hexString())") + XGAssembly.replaceRamASM(RAMOffset: callLocation, newASM: [.bl(callTarget)]) + } + } + + class func getExports(_ text: String) -> [String:UInt32] { + return XGCompiler.parseNameMangling(text, include: ["T", "t", "d"]) + } + + class func getImports(_ text: String) -> [String:UInt32] { + return XGCompiler.parseNameMangling(text, include: ["A"]) + } + + class func parseNameMangling(_ text: String, include: [Character]) -> [String:UInt32] { + let lines = text.components(separatedBy: CharacterSet.newlines) + + var symbols: [String:UInt32] = [:] + + for line in lines { + // skip empty lines and unnamed symbols + if line.length == 0 || line.components(separatedBy: .whitespacesAndNewlines).filter({!$0.isEmpty}).count != 3 { + continue + } + let scanner = Scanner(string: line) + + let startAddress = UInt32(scanner.scanUInt64(representation: .hexadecimal)!) + let symbolType = scanner.scanCharacter()! + let symbolName = scanner.scanUpToString("\n")! + + if !include.contains(symbolType) { + continue + } + symbols[symbolName] = startAddress + } + + return symbols + } + + class func getDolphinMap() -> String { + let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first + let appURLs = try? FileManager.default.contentsOfDirectory(at: appSupportURL!, includingPropertiesForKeys: nil) + let appURL = appURLs!.filter{ ["Dolphin", "dolphin-emu"].contains($0.lastPathComponent) }.first + return appURL!.appendingPathComponent("Maps/GXXE01.map").path + } + + class func parseDolphinMap() -> [String:UInt32] { + let path = XGCompiler.getDolphinMap() + let text = try! String(contentsOfFile: path, encoding: String.Encoding.utf8) + let lines = text.components(separatedBy: CharacterSet.newlines) + + var symbols: [String:UInt32] = [:] + + for line in lines { + if line.first == "." || line.count == 0 { + continue + } + let charset = CharacterSet(charactersIn: ":/<>,'%?&") + if line.rangeOfCharacter(from: charset) != nil { + continue + } + + let scanner = Scanner(string: line) + + let startAddress = UInt32(scanner.scanUInt64(representation: .hexadecimal)!) + let _ /* symbolLength */ = scanner.scanUInt64(representation: .hexadecimal)! + let _ /* virtualAddress */ = scanner.scanUInt64(representation: .hexadecimal)! + let _ /* number */ = scanner.scanInt(representation: .decimal)! + let symbolName = String(scanner.string[scanner.currentIndex...]) + + symbols[symbolName] = startAddress + } + + return symbols + } + + class func checkForCompiler() throws { + + } + + class func generateExternScript(_ externSymbols: [String:UInt32] = [:]) -> String { + var symbols = [String]() + for (symbol, address) in externSymbols { + symbols.append("\(symbol) = \(address.hexString());") + } + + return symbols.joined(separator: "\n") + } + + class func generateMapScript() -> String { + let symbolMap = parseDolphinMap() + return generateExternScript(symbolMap) + } + + class func generateLinkerScript(offset: Int, name: String) -> String { + return """ + OUTPUT_FORMAT("elf32-powerpc", "elf32-powerpc", "elf32-powerpc") + OUTPUT_ARCH(powerpc:common) + ENTRY(\(name)) + + SECTIONS { + . = \(offset.hexString()); + + .text : { + *(.text) + } =0 + + . = ALIGN(32); + .data : { + SORT(CONSTRUCTORS) + } + } + """ + } +} diff --git a/Objects/managers/XGCompilerData.swift b/Objects/managers/XGCompilerData.swift new file mode 100644 index 00000000..c089deb4 --- /dev/null +++ b/Objects/managers/XGCompilerData.swift @@ -0,0 +1,142 @@ +enum XGCompilerConstants { + static let dolphinIncludes = """ +typedef unsigned int u32; +typedef unsigned short u16; +typedef unsigned char u8; +typedef unsigned char bool; + +#define true 1 +#define false 0 + +#define VI_DISPLAY_PIX_SZ 2 + +#define VI_INTERLACE 0 +#define VI_NON_INTERLACE 1 +#define VI_PROGRESSIVE 2 + +#define VI_NTSC 0 +#define VI_PAL 1 +#define VI_MPAL 2 +#define VI_DEBUG 3 +#define VI_DEBUG_PAL 4 +#define VI_EURGB60 5 + +#define VI_TVMODE(FMT, INT) ( ((FMT) << 2) + (INT) ) + +#define VI_MAX_WIDTH_NTSC 720 +#define VI_MAX_HEIGHT_NTSC 480 + +typedef enum +{ + VI_TVMODE_NTSC_INT = VI_TVMODE(VI_NTSC, VI_INTERLACE), + VI_TVMODE_NTSC_DS = VI_TVMODE(VI_NTSC, VI_NON_INTERLACE), + VI_TVMODE_NTSC_PROG = VI_TVMODE(VI_NTSC, VI_PROGRESSIVE), + + VI_TVMODE_PAL_INT = VI_TVMODE(VI_PAL, VI_INTERLACE), + VI_TVMODE_PAL_DS = VI_TVMODE(VI_PAL, VI_NON_INTERLACE), + + VI_TVMODE_EURGB60_INT = VI_TVMODE(VI_EURGB60, VI_INTERLACE), + VI_TVMODE_EURGB60_DS = VI_TVMODE(VI_EURGB60, VI_NON_INTERLACE), + + VI_TVMODE_MPAL_INT = VI_TVMODE(VI_MPAL, VI_INTERLACE), + VI_TVMODE_MPAL_DS = VI_TVMODE(VI_MPAL, VI_NON_INTERLACE), + + VI_TVMODE_DEBUG_INT = VI_TVMODE(VI_DEBUG, VI_INTERLACE), + + VI_TVMODE_DEBUG_PAL_INT = VI_TVMODE(VI_DEBUG_PAL, VI_INTERLACE), + VI_TVMODE_DEBUG_PAL_DS = VI_TVMODE(VI_DEBUG_PAL, VI_NON_INTERLACE) +} VITVMode; + +typedef enum +{ + VI_XFBMODE_SF = 0, + VI_XFBMODE_DF +} VIXFBMode; + +typedef struct _GXRenderModeObj +{ + VITVMode viTVmode; + u16 fbWidth; // no xscale from efb to xfb + u16 efbHeight; // embedded frame buffer + u16 xfbHeight; // external frame buffer, may yscale efb + u16 viXOrigin; + u16 viYOrigin; + u16 viWidth; + u16 viHeight; + VIXFBMode xFBmode; // whether single-field or double-field in + // XFB. + u8 field_rendering; // rendering fields or frames? + u8 aa; // antialiasing on? + u8 sample_pattern[12][2]; // aa sample pattern + u8 vfilter[7]; // vertical filter coefficients +} GXRenderModeObj; + +void DCStoreRange(void* addr, u32 nBytes); + +void VIInit(); +void VIFlush(); +void VISetBlack(bool black); +void VIConfigure(GXRenderModeObj* rm); +void VIConfigurePan(u16 PanPosX, u16 PanPosY, u16 PanSizeX, u16 PanSizeY); +void VIWaitForRetrace(); +void VISetNextFrameBuffer(void *fb); + +#define VIPadFrameBufferWidth(width) ((u16)(((u16)(width) + 15) & ~15)) + +void OSInit(); +void *OSGetArenaHi(); +void *OSGetArenaLo(); +void OSSetArenaHi(void* newHi); +void OSSetArenaLo(void* newLo); +void OSReport(const char* text, ...); + +#define OSRoundUp32B(x) (((u32)(x) + 32 - 1) & ~(32 - 1)) +#define OSRoundDown32B(x) (((u32)(x)) & ~(32 - 1)) + +void DBInit(); + +void __init_registers(); +void __init_hardware(); +void __init_data(); +void __init_user(); +""" + + static var dolphinSymbols: String { + guard game == .XD else { + printg("This has not been implemented for Colosseum yet.") + return "" + } + + guard region == .US else { + printg("This has not yet been implemented for this region:", region.name) + return "" + } + + return """ +DCStoreRange = 0x800aaca0; + +VIInit = 0x800b8b30; +VIFlush = 0x800ba044; +VISetBlack = 0x800ba1e0; +VIConfigure = 0x800b94a8; +VIConfigurePan = 0x800b9cb0; +VIWaitForRetrace = 0x800b8fe0; +VISetNextFrameBuffer = 0x800ba174; + +OSInit = 0x800a9838; +OSGetArenaHi = 0x800aa950; +OSGetArenaLo = 0x800aa958; +OSSetArenaHi = 0x800aa960; +OSSetArenaLo = 0x800aa968; +OSReport = 0x800abc80; + +DBInit = 0x800b2754; + +__init_registers = 0x800032b0; +__init_hardware = 0x80003400; +__init_data = 0x80003340; +__init_user = 0x800b26c0; +""" + + } +} diff --git a/enums/XGFiles.swift b/enums/XGFiles.swift index b1da8672..0d108268 100644 --- a/enums/XGFiles.swift +++ b/enums/XGFiles.swift @@ -82,6 +82,11 @@ indirect enum XGFiles { case log(Date) case wit case wimgt + case gcc + case ld + case nm + case objdump + case objcopy case tool(String) case embedded(String) case gameFile(String) @@ -143,6 +148,11 @@ indirect enum XGFiles { case .iso : return XGISO.inputISOFile?.fileName ?? "game" + XGFileTypes.iso.fileExtension case .wit : return environment == .Windows ? "wit.exe" : "wit" case .wimgt : return environment == .Windows ? "wimgt.exe" : "wimgt" + case .gcc : return environment == .Windows ? "powerpc-eabi-gcc.exe": "powerpc-eabi-gcc" + case .ld : return environment == .Windows ? "powerpc-eabi-ld.exe": "powerpc-eabi-ld" + case .nm : return environment == .Windows ? "powerpc-eabi-nm.exe": "powerpc-eabi-nm" + case .objdump : return environment == .Windows ? "powerpc-eabi-objdump.exe": "powerpc-eabi-objdump" + case .objcopy : return environment == .Windows ? "powerpc-eabi-objcopy.exe": "powerpc-eabi-objcopy" case .tool(let s) : return s + (environment == .Windows ? ".exe" : "") case .embedded(let s) : return s case .gameFile(let s) : return s @@ -189,6 +199,11 @@ indirect enum XGFiles { case .json : return .JSON case .wit : return .Wiimm case .wimgt : return .Wiimm + case .gcc : return .DevKitPPC + case .ld : return .DevKitPPC + case .nm : return .DevKitPPC + case .objdump : return .DevKitPPC + case .objcopy : return .DevKitPPC case .tool("pbrsavetool"): return .SaveFiles case .tool : return .Resources case .embedded : return .Documents @@ -587,7 +602,9 @@ indirect enum XGFolders { case LZSS case Reference case Resources + case Compiler case Wiimm + case DevKitPPC case ISO case Logs case ISOExport(String) @@ -614,7 +631,9 @@ indirect enum XGFolders { case .LZSS : return "LZSS" case .Reference : return "Reference" case .Resources : return "Resources" + case .Compiler : return "Compiler" case .Wiimm : return "Wiimm" + case .DevKitPPC : return "bin" case .ISO : return XGISO.inputISOFile?.folder.name ?? "ISO" case .Logs : return "Logs" case .ISOExport(let name): return name @@ -651,6 +670,7 @@ indirect enum XGFolders { case .Trainers : path = XGFolders.Images.path case .Types : path = XGFolders.Images.path case .Wiimm : path = XGFolders.Resources.path + case .DevKitPPC : path = "/opt/devkitpro/devkitPPC" case .nameAndFolder(_, let f): path = f.path case .ISO : return XGISO.inputISOFile?.folder.path ?? documentsPath + "/" + self.name case .path(let s): return s diff --git a/enums/XGResources.swift b/enums/XGResources.swift index 802f7cd5..10d12a0e 100644 --- a/enums/XGResources.swift +++ b/enums/XGResources.swift @@ -143,7 +143,7 @@ enum XGResources { do { try FileManager.default.copyItem(atPath: srcPath, toPath: dstPath) } catch let error { - printg("Error copying resource:", error) + printg("Error copying resource \(srcPath) to \(dstPath):", error) } } } diff --git a/extensions/XGCompilerExtension.swift b/extensions/XGCompilerExtension.swift new file mode 100644 index 00000000..83c2187c --- /dev/null +++ b/extensions/XGCompilerExtension.swift @@ -0,0 +1,93 @@ +import Foundation +import FoundationNetworking + +extension XGCompiler { + class func Blanco() -> Int { + return 0 + } +} + +extension FileManager { + /// Create a new empty temporary directory. Caller must delete. + func createTemporaryDirectory(inDirectory directory: URL? = nil, name: String? = nil) throws -> URL { + let directoryName = name ?? UUID().uuidString + let parentDirectoryURL = directory ?? temporaryDirectory + let directoryURL = parentDirectoryURL.appendingPathComponent(directoryName) + try createDirectory(at: directoryURL, withIntermediateDirectories: false) + return directoryURL + } + + /// Get a new temporary filename. Caller must delete. + func temporaryFileURL(inDirectory directory: URL? = nil) -> URL { + let filename = UUID().uuidString + let directoryURL = directory ?? temporaryDirectory + return directoryURL.appendingPathComponent(filename) + } + + /// A file URL for the current directory + var currentDirectory: URL { + URL(fileURLWithPath: currentDirectoryPath) + } +} + +final class XGTemporaryDirectory { + let directoryURL: URL + /// Set true to keep the directory after this `TemporaryDirectory` object expires + var keepDirectory = false + + /// Create a new temporary directory somewhere in the filesystem that by default will be deleted + /// along with its contents when the object goes out of scope. + init() throws { + directoryURL = try FileManager.default.createTemporaryDirectory() + } + + /// Wrap an existing directory that, by default, will not be deleted when this object goes out of scope. + init(url: URL) { + directoryURL = url + keepDirectory = true + } + + deinit { + if !keepDirectory { + try? FileManager.default.removeItem(at: directoryURL) + } + } + + /// Get a path for a temp file in this object's directory. File doesn't exist, directory does. + func createFile(name: String? = nil) throws -> URL { + if let name = name { + return directoryURL.appendingPathComponent(name) + } + return FileManager.default.temporaryFileURL(inDirectory: directoryURL) + } + + /// Get a path for a subdirectory in this object's directory. + /// The new `TemporaryDirectory` is not auto-delete by default. + func createDirectory(name: String? = nil) throws -> XGTemporaryDirectory { + let url = try FileManager.default.createTemporaryDirectory(inDirectory: directoryURL, name: name) + return XGTemporaryDirectory(url: url) + } +} + +extension URLSession { + func synchronousDataTask(urlrequest: URLRequest) -> (data: Data?, response: URLResponse?, error: Error?) { + var data: Data? + var response: URLResponse? + var error: Error? + + let semaphore = DispatchSemaphore(value: 0) + + let dataTask = self.dataTask(with: urlrequest) { + data = $0 + response = $1 + error = $2 + + semaphore.signal() + } + dataTask.resume() + + _ = semaphore.wait(timeout: .distantFuture) + + return (data, response, error) + } +}