UTMScriptingConfigImpl.swift 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  1. //
  2. // Copyright © 2023 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import Foundation
  17. @objc extension UTMScriptingVirtualMachineImpl {
  18. @objc var configuration: [AnyHashable : Any] {
  19. let wrapper = UTMScriptingConfigImpl(vm.config, data: data)
  20. return wrapper.serializeConfiguration()
  21. }
  22. @objc func updateConfiguration(_ command: NSScriptCommand) {
  23. let newConfiguration = command.evaluatedArguments?["newConfiguration"] as? [AnyHashable : Any]
  24. withScriptCommand(command) { [self] in
  25. guard let newConfiguration = newConfiguration else {
  26. throw ScriptingError.invalidParameter
  27. }
  28. guard vm.state == .stopped else {
  29. throw ScriptingError.notStopped
  30. }
  31. let wrapper = UTMScriptingConfigImpl(vm.config)
  32. try wrapper.updateConfiguration(from: newConfiguration)
  33. try await data.save(vm: box)
  34. }
  35. }
  36. }
  37. @MainActor
  38. class UTMScriptingConfigImpl {
  39. private var bytesInMib: Int64 {
  40. 1048576
  41. }
  42. private(set) var config: any UTMConfiguration
  43. private weak var data: UTMData?
  44. init(_ config: any UTMConfiguration, data: UTMData? = nil) {
  45. self.config = config
  46. self.data = data
  47. }
  48. func serializeConfiguration() -> [AnyHashable : Any] {
  49. if let qemuConfig = config as? UTMQemuConfiguration {
  50. return serializeQemuConfiguration(qemuConfig)
  51. } else if let appleConfig = config as? UTMAppleConfiguration {
  52. return serializeAppleConfiguration(appleConfig)
  53. } else {
  54. fatalError()
  55. }
  56. }
  57. func updateConfiguration(from record: [AnyHashable : Any]) throws {
  58. if let _ = config as? UTMQemuConfiguration {
  59. try updateQemuConfiguration(from: record)
  60. } else if let _ = config as? UTMAppleConfiguration {
  61. try updateAppleConfiguration(from: record)
  62. } else {
  63. fatalError()
  64. }
  65. }
  66. private func size(of drive: any UTMConfigurationDrive) -> Int {
  67. guard let data = data else {
  68. return 0
  69. }
  70. guard let url = drive.imageURL else {
  71. return 0
  72. }
  73. return Int(data.computeSize(for: url) / bytesInMib)
  74. }
  75. }
  76. @MainActor
  77. extension UTMScriptingConfigImpl {
  78. private func qemuDirectoryShareMode(from mode: QEMUFileShareMode) -> UTMScriptingQemuDirectoryShareMode {
  79. switch mode {
  80. case .none: return .none
  81. case .webdav: return .webDAV
  82. case .virtfs: return .virtFS
  83. }
  84. }
  85. private func serializeQemuConfiguration(_ config: UTMQemuConfiguration) -> [AnyHashable : Any] {
  86. [
  87. "name": config.information.name,
  88. "icon": config.information.iconURL?.deletingPathExtension().lastPathComponent ?? "",
  89. "notes": config.information.notes ?? "",
  90. "architecture": config.system.architecture.rawValue,
  91. "machine": config.system.target.rawValue,
  92. "memory": config.system.memorySize,
  93. "cpuCores": config.system.cpuCount,
  94. "hypervisor": config.qemu.hasHypervisor,
  95. "uefi": config.qemu.hasUefiBoot,
  96. "directoryShareMode": qemuDirectoryShareMode(from: config.sharing.directoryShareMode).rawValue,
  97. "drives": config.drives.map({ serializeQemuDriveExisting($0) }),
  98. "networkInterfaces": config.networks.enumerated().map({ serializeQemuNetwork($1, index: $0) }),
  99. "serialPorts": config.serials.enumerated().map({ serializeQemuSerial($1, index: $0) }),
  100. "displays": config.displays.enumerated().map({ serializeQemuDisplay($1, index: $0)}),
  101. "qemuAdditionalArguments": config.qemu.additionalArguments.map({ serializeQemuAdditionalArgument($0)}),
  102. ]
  103. }
  104. private func qemuDriveInterface(from interface: QEMUDriveInterface) -> UTMScriptingQemuDriveInterface {
  105. switch interface {
  106. case .none: return .none
  107. case .ide: return .ide
  108. case .scsi: return .scsi
  109. case .sd: return .sd
  110. case .mtd: return .mtd
  111. case .floppy: return .floppy
  112. case .pflash: return .pFlash
  113. case .virtio: return .virtIO
  114. case .nvme: return .nvMe
  115. case .usb: return .usb
  116. }
  117. }
  118. private func serializeQemuDriveExisting(_ config: UTMQemuConfigurationDrive) -> [AnyHashable : Any] {
  119. [
  120. "id": config.id,
  121. "removable": config.isExternal,
  122. "interface": qemuDriveInterface(from: config.interface).rawValue,
  123. "hostSize": size(of: config),
  124. ]
  125. }
  126. private func qemuNetworkMode(from mode: QEMUNetworkMode) -> UTMScriptingQemuNetworkMode {
  127. switch mode {
  128. case .emulated: return .emulated
  129. case .shared: return .shared
  130. case .host: return .host
  131. case .bridged: return .bridged
  132. }
  133. }
  134. private func serializeQemuNetwork(_ config: UTMQemuConfigurationNetwork, index: Int) -> [AnyHashable : Any] {
  135. [
  136. "index": index,
  137. "hardware": config.hardware.rawValue,
  138. "mode": qemuNetworkMode(from: config.mode).rawValue,
  139. "address": config.macAddress,
  140. "hostInterface": config.bridgeInterface ?? "",
  141. "portForwards": config.portForward.map({ serializeQemuPortForward($0) }),
  142. ]
  143. }
  144. private func networkProtocol(from protc: QEMUNetworkProtocol) -> UTMScriptingNetworkProtocol {
  145. switch protc {
  146. case .tcp: return .tcp
  147. case .udp: return .udp
  148. }
  149. }
  150. private func serializeQemuPortForward(_ config: UTMQemuConfigurationPortForward) -> [AnyHashable : Any] {
  151. [
  152. "protocol": networkProtocol(from: config.protocol).rawValue,
  153. "hostAddress": config.hostAddress ?? "",
  154. "hostPort": config.hostPort,
  155. "guestAddress": config.guestAddress ?? "",
  156. "guestPort": config.guestPort,
  157. ]
  158. }
  159. private func qemuSerialInterface(from mode: QEMUSerialMode) -> UTMScriptingSerialInterface {
  160. switch mode {
  161. case .ptty: return .ptty
  162. case .tcpServer: return .tcp
  163. default: return .unavailable
  164. }
  165. }
  166. private func serializeQemuSerial(_ config: UTMQemuConfigurationSerial, index: Int) -> [AnyHashable : Any] {
  167. [
  168. "index": index,
  169. "hardware": config.hardware?.rawValue ?? "",
  170. "interface": qemuSerialInterface(from: config.mode).rawValue,
  171. "port": config.tcpPort ?? 0,
  172. ]
  173. }
  174. private func qemuScaler(from filter: QEMUScaler) -> UTMScriptingQemuScaler {
  175. switch filter {
  176. case .linear: return .linear
  177. case .nearest: return .nearest
  178. }
  179. }
  180. private func serializeQemuDisplay(_ config: UTMQemuConfigurationDisplay, index: Int) -> [AnyHashable : Any] {
  181. [
  182. "index": index,
  183. "hardware": config.hardware.rawValue,
  184. "dynamicResolution": config.isDynamicResolution,
  185. "nativeResolution": config.isNativeResolution,
  186. "upscalingFilter": qemuScaler(from: config.upscalingFilter).rawValue,
  187. "downscalingFilter": qemuScaler(from: config.downscalingFilter).rawValue,
  188. ]
  189. }
  190. private func serializeQemuAdditionalArgument(_ argument: QEMUArgument) -> [AnyHashable: Any] {
  191. var serializedArgument: [AnyHashable: Any] = [
  192. "argumentString": argument.string
  193. ]
  194. // Only add fileUrls if it is not nil and contains URLs
  195. if let fileUrls = argument.fileUrls, !fileUrls.isEmpty {
  196. serializedArgument["fileUrls"] = fileUrls.map({ $0 as AnyHashable })
  197. }
  198. return serializedArgument
  199. }
  200. private func serializeAppleConfiguration(_ config: UTMAppleConfiguration) -> [AnyHashable : Any] {
  201. [
  202. "name": config.information.name,
  203. "icon": config.information.iconURL?.deletingPathExtension().lastPathComponent ?? "",
  204. "notes": config.information.notes ?? "",
  205. "memory": config.system.memorySize,
  206. "cpuCores": config.system.cpuCount,
  207. "directoryShares": config.sharedDirectories.enumerated().map({ serializeAppleDirectoryShare($1, index: $0) }),
  208. "drives": config.drives.map({ serializeAppleDriveExisting($0) }),
  209. "networkInterfaces": config.networks.enumerated().map({ serializeAppleNetwork($1, index: $0) }),
  210. "serialPorts": config.serials.enumerated().map({ serializeAppleSerial($1, index: $0) }),
  211. "displays": config.displays.enumerated().map({ serializeAppleDisplay($1, index: $0)}),
  212. ]
  213. }
  214. private func serializeAppleDirectoryShare(_ config: UTMAppleConfigurationSharedDirectory, index: Int) -> [AnyHashable : Any] {
  215. [
  216. "index": index,
  217. "readOnly": config.isReadOnly
  218. ]
  219. }
  220. private func serializeAppleDriveExisting(_ config: UTMAppleConfigurationDrive) -> [AnyHashable : Any] {
  221. [
  222. "id": config.id,
  223. "removable": config.isExternal,
  224. "hostSize": size(of: config),
  225. ]
  226. }
  227. private func appleNetworkMode(from mode: UTMAppleConfigurationNetwork.NetworkMode) -> UTMScriptingAppleNetworkMode {
  228. switch mode {
  229. case .shared: return .shared
  230. case .bridged: return .bridged
  231. }
  232. }
  233. private func serializeAppleNetwork(_ config: UTMAppleConfigurationNetwork, index: Int) -> [AnyHashable : Any] {
  234. [
  235. "index": index,
  236. "mode": appleNetworkMode(from: config.mode).rawValue,
  237. "address": config.macAddress,
  238. "hostInterface": config.bridgeInterface ?? "",
  239. ]
  240. }
  241. private func appleSerialInterface(from mode: UTMAppleConfigurationSerial.SerialMode) -> UTMScriptingSerialInterface {
  242. switch mode {
  243. case .ptty: return .ptty
  244. default: return .unavailable
  245. }
  246. }
  247. private func serializeAppleSerial(_ config: UTMAppleConfigurationSerial, index: Int) -> [AnyHashable : Any] {
  248. [
  249. "index": index,
  250. "interface": appleSerialInterface(from: config.mode).rawValue,
  251. ]
  252. }
  253. private func serializeAppleDisplay(_ config: UTMAppleConfigurationDisplay, index: Int) -> [AnyHashable : Any] {
  254. [
  255. "index": index,
  256. "dynamicResolution": config.isDynamicResolution,
  257. ]
  258. }
  259. }
  260. @MainActor
  261. extension UTMScriptingConfigImpl {
  262. private func updateElements<T>(_ array: inout [T], with records: [[AnyHashable : Any]], onExisting: @MainActor (inout T, [AnyHashable : Any]) throws -> Void, onNew: @MainActor ([AnyHashable : Any]) throws -> T) throws {
  263. var unseenIndicies = IndexSet(integersIn: array.indices)
  264. for record in records {
  265. if let index = record["index"] as? Int {
  266. guard array.indices.contains(index) else {
  267. throw ConfigurationError.indexNotFound(index: index)
  268. }
  269. try onExisting(&array[index], record)
  270. unseenIndicies.remove(index)
  271. } else {
  272. array.append(try onNew(record))
  273. }
  274. }
  275. array.remove(atOffsets: unseenIndicies)
  276. }
  277. private func updateIdentifiedElements<T: Identifiable>(_ array: inout [T], with records: [[AnyHashable : Any]], onExisting: @MainActor (inout T, [AnyHashable : Any]) throws -> Void, onNew: @MainActor ([AnyHashable : Any]) throws -> T) throws {
  278. var unseenIndicies = IndexSet(integersIn: array.indices)
  279. for record in records {
  280. if let id = record["id"] as? T.ID {
  281. guard let index = array.enumerated().first(where: { $1.id == id })?.offset else {
  282. throw ConfigurationError.identifierNotFound(id: id)
  283. }
  284. try onExisting(&array[index], record)
  285. unseenIndicies.remove(index)
  286. } else {
  287. array.append(try onNew(record))
  288. }
  289. }
  290. array.remove(atOffsets: unseenIndicies)
  291. }
  292. private func parseQemuDirectoryShareMode(_ value: AEKeyword?) -> QEMUFileShareMode? {
  293. guard let value = value, let parsed = UTMScriptingQemuDirectoryShareMode(rawValue: value) else {
  294. return Optional.none
  295. }
  296. switch parsed {
  297. case .none: return QEMUFileShareMode.none
  298. case .webDAV: return .webdav
  299. case .virtFS: return .virtfs
  300. default: return Optional.none
  301. }
  302. }
  303. private func updateQemuConfiguration(from record: [AnyHashable : Any]) throws {
  304. let config = config as! UTMQemuConfiguration
  305. if let name = record["name"] as? String, !name.isEmpty {
  306. config.information.name = name
  307. }
  308. if let icon = record["icon"] as? String, !icon.isEmpty {
  309. if let url = UTMConfigurationInfo.builtinIcon(named: icon) {
  310. config.information.iconURL = url
  311. } else {
  312. throw ConfigurationError.iconNotFound(icon: icon)
  313. }
  314. }
  315. if let notes = record["notes"] as? String, !notes.isEmpty {
  316. config.information.notes = notes
  317. }
  318. let architecture = record["architecture"] as? String
  319. let arch = QEMUArchitecture(rawValue: architecture ?? "")
  320. let machine = record["machine"] as? String
  321. let target = arch?.targetType.init(rawValue: machine ?? "")
  322. if let arch = arch, arch != config.system.architecture {
  323. let target = target ?? arch.targetType.default
  324. config.system.architecture = arch
  325. config.system.target = target
  326. config.reset(forArchitecture: arch, target: target)
  327. } else if let target = target, target.rawValue != config.system.target.rawValue {
  328. config.system.target = target
  329. config.reset(forArchitecture: config.system.architecture, target: target)
  330. }
  331. if let memory = record["memory"] as? Int, memory != 0 {
  332. config.system.memorySize = memory
  333. }
  334. if let cpuCores = record["cpuCores"] as? Int {
  335. config.system.cpuCount = cpuCores
  336. }
  337. if let hypervisor = record["hypervisor"] as? Bool {
  338. config.qemu.hasHypervisor = hypervisor
  339. }
  340. if let uefi = record["uefi"] as? Bool {
  341. config.qemu.hasUefiBoot = uefi
  342. }
  343. if let directoryShareMode = parseQemuDirectoryShareMode(record["directoryShareMode"] as? AEKeyword) {
  344. config.sharing.directoryShareMode = directoryShareMode
  345. }
  346. if let drives = record["drives"] as? [[AnyHashable : Any]] {
  347. try updateQemuDrives(from: drives)
  348. }
  349. if let networkInterfaces = record["networkInterfaces"] as? [[AnyHashable : Any]] {
  350. try updateQemuNetworks(from: networkInterfaces)
  351. }
  352. if let serialPorts = record["serialPorts"] as? [[AnyHashable : Any]] {
  353. try updateQemuSerials(from: serialPorts)
  354. }
  355. if let displays = record["displays"] as? [[AnyHashable : Any]] {
  356. try updateQemuDisplays(from: displays)
  357. }
  358. if let qemuAdditionalArguments = record["qemuAdditionalArguments"] as? [[AnyHashable: Any]] {
  359. try updateQemuAdditionalArguments(from: qemuAdditionalArguments)
  360. }
  361. }
  362. private func parseQemuDriveInterface(_ value: AEKeyword?) -> QEMUDriveInterface? {
  363. guard let value = value, let parsed = UTMScriptingQemuDriveInterface(rawValue: value) else {
  364. return Optional.none
  365. }
  366. switch parsed {
  367. case .none: return QEMUDriveInterface.none
  368. case .ide: return .ide
  369. case .scsi: return .scsi
  370. case .sd: return .sd
  371. case .mtd: return .mtd
  372. case .floppy: return .floppy
  373. case .pFlash: return .pflash
  374. case .virtIO: return .virtio
  375. case .nvMe: return .nvme
  376. case .usb: return .usb
  377. default: return Optional.none
  378. }
  379. }
  380. private func updateQemuDrives(from records: [[AnyHashable : Any]]) throws {
  381. let config = config as! UTMQemuConfiguration
  382. try updateIdentifiedElements(&config.drives, with: records, onExisting: updateQemuExistingDrive, onNew: unserializeQemuDriveNew)
  383. }
  384. private func updateQemuExistingDrive(_ drive: inout UTMQemuConfigurationDrive, from record: [AnyHashable : Any]) throws {
  385. if let interface = parseQemuDriveInterface(record["interface"] as? AEKeyword) {
  386. drive.interface = interface
  387. }
  388. if let source = record["source"] as? URL {
  389. drive.imageURL = source
  390. }
  391. }
  392. private func unserializeQemuDriveNew(from record: [AnyHashable : Any]) throws -> UTMQemuConfigurationDrive {
  393. let config = config as! UTMQemuConfiguration
  394. let removable = record["removable"] as? Bool ?? false
  395. var newDrive = UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target, isExternal: removable)
  396. if let importUrl = record["source"] as? URL {
  397. newDrive.imageURL = importUrl
  398. } else if let size = record["guestSize"] as? Int {
  399. newDrive.sizeMib = size
  400. }
  401. if let interface = parseQemuDriveInterface(record["interface"] as? AEKeyword) {
  402. newDrive.interface = interface
  403. }
  404. if let raw = record["raw"] as? Bool {
  405. newDrive.isRawImage = raw
  406. }
  407. return newDrive
  408. }
  409. private func updateQemuNetworks(from records: [[AnyHashable : Any]]) throws {
  410. let config = config as! UTMQemuConfiguration
  411. try updateElements(&config.networks, with: records, onExisting: updateQemuExistingNetwork, onNew: { record in
  412. guard var newNetwork = UTMQemuConfigurationNetwork(forArchitecture: config.system.architecture, target: config.system.target) else {
  413. throw ConfigurationError.deviceNotSupported
  414. }
  415. try updateQemuExistingNetwork(&newNetwork, from: record)
  416. return newNetwork
  417. })
  418. }
  419. private func parseQemuNetworkMode(_ value: AEKeyword?) -> QEMUNetworkMode? {
  420. guard let value = value, let parsed = UTMScriptingQemuNetworkMode(rawValue: value) else {
  421. return Optional.none
  422. }
  423. switch parsed {
  424. case .emulated: return .emulated
  425. case .shared: return .shared
  426. case .host: return .host
  427. case .bridged: return .bridged
  428. default: return .none
  429. }
  430. }
  431. private func updateQemuExistingNetwork(_ network: inout UTMQemuConfigurationNetwork, from record: [AnyHashable : Any]) throws {
  432. let config = config as! UTMQemuConfiguration
  433. if let hardware = record["hardware"] as? String, let hardware = config.system.architecture.networkDeviceType.init(rawValue: hardware) {
  434. network.hardware = hardware
  435. }
  436. if let mode = parseQemuNetworkMode(record["mode"] as? AEKeyword) {
  437. network.mode = mode
  438. }
  439. if let address = record["address"] as? String, !address.isEmpty {
  440. network.macAddress = address
  441. }
  442. if let interface = record["hostInterface"] as? String, !interface.isEmpty {
  443. network.bridgeInterface = interface
  444. }
  445. if let portForwards = record["portForwards"] as? [[AnyHashable : Any]] {
  446. network.portForward = portForwards.map({ unserializeQemuPortForward(from: $0) })
  447. }
  448. }
  449. private func parseNetworkProtocol(_ value: AEKeyword?) -> QEMUNetworkProtocol? {
  450. guard let value = value, let parsed = UTMScriptingNetworkProtocol(rawValue: value) else {
  451. return Optional.none
  452. }
  453. switch parsed {
  454. case .tcp: return .tcp
  455. case .udp: return .udp
  456. default: return Optional.none
  457. }
  458. }
  459. private func unserializeQemuPortForward(from record: [AnyHashable : Any]) -> UTMQemuConfigurationPortForward {
  460. var forward = UTMQemuConfigurationPortForward()
  461. if let protoc = parseNetworkProtocol(record["protocol"] as? AEKeyword) {
  462. forward.protocol = protoc
  463. }
  464. if let hostAddress = record["hostAddress"] as? String, !hostAddress.isEmpty {
  465. forward.hostAddress = hostAddress
  466. }
  467. if let hostPort = record["hostPort"] as? Int {
  468. forward.hostPort = hostPort
  469. }
  470. if let guestAddress = record["guestAddress"] as? String, !guestAddress.isEmpty {
  471. forward.guestAddress = guestAddress
  472. }
  473. if let guestPort = record["guestPort"] as? Int {
  474. forward.guestPort = guestPort
  475. }
  476. return forward
  477. }
  478. private func updateQemuSerials(from records: [[AnyHashable : Any]]) throws {
  479. let config = config as! UTMQemuConfiguration
  480. try updateElements(&config.serials, with: records, onExisting: updateQemuExistingSerial, onNew: { record in
  481. guard var newSerial = UTMQemuConfigurationSerial(forArchitecture: config.system.architecture, target: config.system.target) else {
  482. throw ConfigurationError.deviceNotSupported
  483. }
  484. try updateQemuExistingSerial(&newSerial, from: record)
  485. return newSerial
  486. })
  487. }
  488. private func parseQemuSerialInterface(_ value: AEKeyword?) -> QEMUSerialMode? {
  489. guard let value = value, let parsed = UTMScriptingSerialInterface(rawValue: value) else {
  490. return Optional.none
  491. }
  492. switch parsed {
  493. case .ptty: return .ptty
  494. case .tcp: return .tcpServer
  495. default: return Optional.none
  496. }
  497. }
  498. private func updateQemuExistingSerial(_ serial: inout UTMQemuConfigurationSerial, from record: [AnyHashable : Any]) throws {
  499. let config = config as! UTMQemuConfiguration
  500. if let hardware = record["hardware"] as? String, let hardware = config.system.architecture.serialDeviceType.init(rawValue: hardware) {
  501. serial.hardware = hardware
  502. }
  503. if let interface = parseQemuSerialInterface(record["interface"] as? AEKeyword) {
  504. serial.mode = interface
  505. }
  506. if let port = record["port"] as? Int {
  507. serial.tcpPort = port
  508. }
  509. }
  510. private func updateQemuDisplays(from records: [[AnyHashable : Any]]) throws {
  511. let config = config as! UTMQemuConfiguration
  512. try updateElements(&config.displays, with: records, onExisting: updateQemuExistingDisplay, onNew: { record in
  513. guard var newDisplay = UTMQemuConfigurationDisplay(forArchitecture: config.system.architecture, target: config.system.target) else {
  514. throw ConfigurationError.deviceNotSupported
  515. }
  516. try updateQemuExistingDisplay(&newDisplay, from: record)
  517. return newDisplay
  518. })
  519. }
  520. private func parseQemuScaler(_ value: AEKeyword?) -> QEMUScaler? {
  521. guard let value = value, let parsed = UTMScriptingQemuScaler(rawValue: value) else {
  522. return Optional.none
  523. }
  524. switch parsed {
  525. case .linear: return .linear
  526. case .nearest: return .nearest
  527. default: return Optional.none
  528. }
  529. }
  530. private func updateQemuExistingDisplay(_ display: inout UTMQemuConfigurationDisplay, from record: [AnyHashable : Any]) throws {
  531. let config = config as! UTMQemuConfiguration
  532. if let hardware = record["hardware"] as? String, let hardware = config.system.architecture.displayDeviceType.init(rawValue: hardware) {
  533. display.hardware = hardware
  534. }
  535. if let dynamicResolution = record["dynamicResolution"] as? Bool {
  536. display.isDynamicResolution = dynamicResolution
  537. }
  538. if let nativeResolution = record["nativeResolution"] as? Bool {
  539. display.isNativeResolution = nativeResolution
  540. }
  541. if let upscalingFilter = parseQemuScaler(record["upscalingFilter"] as? AEKeyword) {
  542. display.upscalingFilter = upscalingFilter
  543. }
  544. if let downscalingFilter = parseQemuScaler(record["downscalingFilter"] as? AEKeyword) {
  545. display.downscalingFilter = downscalingFilter
  546. }
  547. }
  548. private func updateQemuAdditionalArguments(from records: [[AnyHashable: Any]]) throws {
  549. let config = config as! UTMQemuConfiguration
  550. let additionalArguments = records.compactMap { record -> QEMUArgument? in
  551. guard let argumentString = record["argumentString"] as? String else { return nil }
  552. var argument = QEMUArgument(argumentString)
  553. // fileUrls are used as required resources by QEMU.
  554. if let fileUrls = record["fileUrls"] as? [URL] {
  555. argument.fileUrls = fileUrls
  556. }
  557. return argument
  558. }
  559. // Update entire additional arguments with new one.
  560. config.qemu.additionalArguments = additionalArguments
  561. }
  562. private func updateAppleConfiguration(from record: [AnyHashable : Any]) throws {
  563. let config = config as! UTMAppleConfiguration
  564. if let name = record["name"] as? String, !name.isEmpty {
  565. config.information.name = name
  566. }
  567. if let icon = record["icon"] as? String, !icon.isEmpty {
  568. if let url = UTMConfigurationInfo.builtinIcon(named: icon) {
  569. config.information.iconURL = url
  570. } else {
  571. throw ConfigurationError.iconNotFound(icon: icon)
  572. }
  573. }
  574. if let notes = record["notes"] as? String, !notes.isEmpty {
  575. config.information.notes = notes
  576. }
  577. if let memory = record["memory"] as? Int, memory != 0 {
  578. config.system.memorySize = memory
  579. }
  580. if let cpuCores = record["cpuCores"] as? Int {
  581. config.system.cpuCount = cpuCores
  582. }
  583. if let directoryShares = record["directoryShares"] as? [[AnyHashable : Any]] {
  584. try updateAppleDirectoryShares(from: directoryShares)
  585. }
  586. if let drives = record["drives"] as? [[AnyHashable : Any]] {
  587. try updateAppleDrives(from: drives)
  588. }
  589. if let networkInterfaces = record["networkInterfaces"] as? [[AnyHashable : Any]] {
  590. try updateAppleNetworks(from: networkInterfaces)
  591. }
  592. if let serialPorts = record["serialPorts"] as? [[AnyHashable : Any]] {
  593. try updateAppleSerials(from: serialPorts)
  594. }
  595. if let displays = record["displays"] as? [[AnyHashable : Any]] {
  596. try updateAppleDisplays(from: displays)
  597. }
  598. }
  599. private func updateAppleDirectoryShares(from records: [[AnyHashable : Any]]) throws {
  600. let config = config as! UTMAppleConfiguration
  601. try updateElements(&config.sharedDirectories, with: records, onExisting: updateAppleExistingDirectoryShare, onNew: { record in
  602. var newShare = UTMAppleConfigurationSharedDirectory(directoryURL: nil, isReadOnly: false)
  603. try updateAppleExistingDirectoryShare(&newShare, from: record)
  604. return newShare
  605. })
  606. }
  607. private func updateAppleExistingDirectoryShare(_ share: inout UTMAppleConfigurationSharedDirectory, from record: [AnyHashable : Any]) throws {
  608. if let readOnly = record["readOnly"] as? Bool {
  609. share.isReadOnly = readOnly
  610. }
  611. }
  612. private func updateAppleDrives(from records: [[AnyHashable : Any]]) throws {
  613. let config = config as! UTMAppleConfiguration
  614. try updateIdentifiedElements(&config.drives, with: records, onExisting: updateAppleExistingDrive, onNew: unserializeAppleNewDrive)
  615. }
  616. private func updateAppleExistingDrive(_ drive: inout UTMAppleConfigurationDrive, from record: [AnyHashable : Any]) throws {
  617. if let source = record["source"] as? URL {
  618. drive.imageURL = source
  619. }
  620. }
  621. private func unserializeAppleNewDrive(from record: [AnyHashable : Any]) throws -> UTMAppleConfigurationDrive {
  622. let removable = record["removable"] as? Bool ?? false
  623. var newDrive: UTMAppleConfigurationDrive
  624. if let size = record["guestSize"] as? Int {
  625. newDrive = UTMAppleConfigurationDrive(newSize: size)
  626. } else {
  627. newDrive = UTMAppleConfigurationDrive(existingURL: record["source"] as? URL, isExternal: removable)
  628. }
  629. return newDrive
  630. }
  631. private func updateAppleNetworks(from records: [[AnyHashable : Any]]) throws {
  632. let config = config as! UTMAppleConfiguration
  633. try updateElements(&config.networks, with: records, onExisting: updateAppleExistingNetwork, onNew: { record in
  634. var newNetwork = UTMAppleConfigurationNetwork()
  635. try updateAppleExistingNetwork(&newNetwork, from: record)
  636. return newNetwork
  637. })
  638. }
  639. private func parseAppleNetworkMode(_ value: AEKeyword?) -> UTMAppleConfigurationNetwork.NetworkMode? {
  640. guard let value = value, let parsed = UTMScriptingQemuNetworkMode(rawValue: value) else {
  641. return Optional.none
  642. }
  643. switch parsed {
  644. case .shared: return .shared
  645. case .bridged: return .bridged
  646. default: return Optional.none
  647. }
  648. }
  649. private func updateAppleExistingNetwork(_ network: inout UTMAppleConfigurationNetwork, from record: [AnyHashable : Any]) throws {
  650. if let mode = parseAppleNetworkMode(record["mode"] as? AEKeyword) {
  651. network.mode = mode
  652. }
  653. if let address = record["address"] as? String, !address.isEmpty {
  654. network.macAddress = address
  655. }
  656. if let interface = record["hostInterface"] as? String, !interface.isEmpty {
  657. network.bridgeInterface = interface
  658. }
  659. }
  660. private func updateAppleSerials(from records: [[AnyHashable : Any]]) throws {
  661. let config = config as! UTMAppleConfiguration
  662. try updateElements(&config.serials, with: records, onExisting: updateAppleExistingSerial, onNew: { record in
  663. var newSerial = UTMAppleConfigurationSerial()
  664. try updateAppleExistingSerial(&newSerial, from: record)
  665. return newSerial
  666. })
  667. }
  668. private func parseAppleSerialInterface(_ value: AEKeyword?) -> UTMAppleConfigurationSerial.SerialMode? {
  669. guard let value = value, let parsed = UTMScriptingSerialInterface(rawValue: value) else {
  670. return Optional.none
  671. }
  672. switch parsed {
  673. case .ptty: return .ptty
  674. default: return Optional.none
  675. }
  676. }
  677. private func updateAppleExistingSerial(_ serial: inout UTMAppleConfigurationSerial, from record: [AnyHashable : Any]) throws {
  678. if let interface = parseAppleSerialInterface(record["interface"] as? AEKeyword) {
  679. serial.mode = interface
  680. }
  681. }
  682. private func updateAppleDisplays(from records: [[AnyHashable : Any]]) throws {
  683. let config = config as! UTMAppleConfiguration
  684. try updateElements(&config.displays, with: records, onExisting: updateAppleExistingDisplay, onNew: { record in
  685. var newDisplay = UTMAppleConfigurationDisplay()
  686. try updateAppleExistingDisplay(&newDisplay, from: record)
  687. return newDisplay
  688. })
  689. }
  690. private func updateAppleExistingDisplay(_ display: inout UTMAppleConfigurationDisplay, from record: [AnyHashable : Any]) throws {
  691. if let dynamicResolution = record["dynamicResolution"] as? Bool {
  692. display.isDynamicResolution = dynamicResolution
  693. }
  694. }
  695. enum ConfigurationError: Error, LocalizedError {
  696. case identifierNotFound(id: any Hashable)
  697. case invalidDriveDescription
  698. case indexNotFound(index: Int)
  699. case deviceNotSupported
  700. case iconNotFound(icon: String)
  701. var errorDescription: String? {
  702. switch self {
  703. case .identifierNotFound(let id): return String.localizedStringWithFormat(NSLocalizedString("Identifier '%@' cannot be found.", comment: "UTMScriptingConfigImpl"), String(describing: id))
  704. case .invalidDriveDescription: return NSLocalizedString("Drive description is invalid.", comment: "UTMScriptingConfigImpl")
  705. case .indexNotFound(let index): return String.localizedStringWithFormat(NSLocalizedString("Index %lld cannot be found.", comment: "UTMScriptingConfigImpl"), index)
  706. case .deviceNotSupported: return NSLocalizedString("This device is not supported by the target.", comment: "UTMScriptingConfigImpl")
  707. case .iconNotFound(let icon): return String.localizedStringWithFormat(NSLocalizedString("The icon named '%@' cannot be found in the built-in icons.", comment: "UTMScriptingConfigImpl"), icon)
  708. }
  709. }
  710. }
  711. }