UTMScriptingConfigImpl.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  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.wrappedValue as! any UTMConfiguration, 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 == .vmStopped else {
  29. throw ScriptingError.notStopped
  30. }
  31. let wrapper = UTMScriptingConfigImpl(vm.config.wrappedValue as! any UTMConfiguration)
  32. try wrapper.updateConfiguration(from: newConfiguration)
  33. try await data.save(vm: vm)
  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. "notes": config.information.notes ?? "",
  89. "architecture": config.system.architecture.rawValue,
  90. "machine": config.system.target.rawValue,
  91. "memory": config.system.memorySize,
  92. "cpuCores": config.system.cpuCount,
  93. "hypervisor": config.qemu.hasHypervisor,
  94. "uefi": config.qemu.hasUefiBoot,
  95. "directoryShareMode": qemuDirectoryShareMode(from: config.sharing.directoryShareMode).rawValue,
  96. "drives": config.drives.map({ serializeQemuDriveExisting($0) }),
  97. "networkInterfaces": config.networks.enumerated().map({ serializeQemuNetwork($1, index: $0) }),
  98. "serialPorts": config.serials.enumerated().map({ serializeQemuSerial($1, index: $0) }),
  99. ]
  100. }
  101. private func qemuDriveInterface(from interface: QEMUDriveInterface) -> UTMScriptingQemuDriveInterface {
  102. switch interface {
  103. case .none: return .none
  104. case .ide: return .ide
  105. case .scsi: return .scsi
  106. case .sd: return .sd
  107. case .mtd: return .mtd
  108. case .floppy: return .floppy
  109. case .pflash: return .pFlash
  110. case .virtio: return .virtIO
  111. case .nvme: return .nvMe
  112. case .usb: return .usb
  113. }
  114. }
  115. private func serializeQemuDriveExisting(_ config: UTMQemuConfigurationDrive) -> [AnyHashable : Any] {
  116. [
  117. "id": config.id,
  118. "removable": config.isExternal,
  119. "interface": qemuDriveInterface(from: config.interface).rawValue,
  120. "hostSize": size(of: config),
  121. ]
  122. }
  123. private func qemuNetworkMode(from mode: QEMUNetworkMode) -> UTMScriptingQemuNetworkMode {
  124. switch mode {
  125. case .emulated: return .emulated
  126. case .shared: return .shared
  127. case .host: return .host
  128. case .bridged: return .bridged
  129. }
  130. }
  131. private func serializeQemuNetwork(_ config: UTMQemuConfigurationNetwork, index: Int) -> [AnyHashable : Any] {
  132. [
  133. "index": index,
  134. "hardware": config.hardware.rawValue,
  135. "mode": qemuNetworkMode(from: config.mode).rawValue,
  136. "address": config.macAddress,
  137. "hostInterface": config.bridgeInterface ?? "",
  138. "portForwards": config.portForward.map({ serializeQemuPortForward($0) }),
  139. ]
  140. }
  141. private func networkProtocol(from protc: QEMUNetworkProtocol) -> UTMScriptingNetworkProtocol {
  142. switch protc {
  143. case .tcp: return .tcp
  144. case .udp: return .udp
  145. }
  146. }
  147. private func serializeQemuPortForward(_ config: UTMQemuConfigurationPortForward) -> [AnyHashable : Any] {
  148. [
  149. "protocol": networkProtocol(from: config.protocol).rawValue,
  150. "hostAddress": config.hostAddress ?? "",
  151. "hostPort": config.hostPort,
  152. "guestAddress": config.guestAddress ?? "",
  153. "guestPort": config.guestPort,
  154. ]
  155. }
  156. private func qemuSerialInterface(from mode: QEMUSerialMode) -> UTMScriptingSerialInterface {
  157. switch mode {
  158. case .ptty: return .ptty
  159. case .tcpServer: return .tcp
  160. default: return .unavailable
  161. }
  162. }
  163. private func serializeQemuSerial(_ config: UTMQemuConfigurationSerial, index: Int) -> [AnyHashable : Any] {
  164. [
  165. "index": index,
  166. "hardware": config.hardware?.rawValue ?? "",
  167. "interface": qemuSerialInterface(from: config.mode).rawValue,
  168. "port": config.tcpPort ?? 0,
  169. ]
  170. }
  171. private func serializeAppleConfiguration(_ config: UTMAppleConfiguration) -> [AnyHashable : Any] {
  172. [
  173. "name": config.information.name,
  174. "notes": config.information.notes ?? "",
  175. "memory": config.system.memorySize,
  176. "cpuCores": config.system.cpuCount,
  177. "directoryShares": config.sharedDirectories.enumerated().map({ serializeAppleDirectoryShare($1, index: $0) }),
  178. "drives": config.drives.map({ serializeAppleDriveExisting($0) }),
  179. "networkInterfaces": config.networks.enumerated().map({ serializeAppleNetwork($1, index: $0) }),
  180. "serialPorts": config.serials.enumerated().map({ serializeAppleSerial($1, index: $0) }),
  181. ]
  182. }
  183. private func serializeAppleDirectoryShare(_ config: UTMAppleConfigurationSharedDirectory, index: Int) -> [AnyHashable : Any] {
  184. [
  185. "index": index,
  186. "readOnly": config.isReadOnly
  187. ]
  188. }
  189. private func serializeAppleDriveExisting(_ config: UTMAppleConfigurationDrive) -> [AnyHashable : Any] {
  190. [
  191. "id": config.id,
  192. "removable": config.isExternal,
  193. "hostSize": size(of: config),
  194. ]
  195. }
  196. private func appleNetworkMode(from mode: UTMAppleConfigurationNetwork.NetworkMode) -> UTMScriptingAppleNetworkMode {
  197. switch mode {
  198. case .shared: return .shared
  199. case .bridged: return .bridged
  200. }
  201. }
  202. private func serializeAppleNetwork(_ config: UTMAppleConfigurationNetwork, index: Int) -> [AnyHashable : Any] {
  203. [
  204. "index": index,
  205. "mode": appleNetworkMode(from: config.mode).rawValue,
  206. "address": config.macAddress,
  207. "hostInterface": config.bridgeInterface ?? "",
  208. ]
  209. }
  210. private func appleSerialInterface(from mode: UTMAppleConfigurationSerial.SerialMode) -> UTMScriptingSerialInterface {
  211. switch mode {
  212. case .ptty: return .ptty
  213. default: return .unavailable
  214. }
  215. }
  216. private func serializeAppleSerial(_ config: UTMAppleConfigurationSerial, index: Int) -> [AnyHashable : Any] {
  217. [
  218. "index": index,
  219. "interface": appleSerialInterface(from: config.mode).rawValue,
  220. ]
  221. }
  222. }
  223. @MainActor
  224. extension UTMScriptingConfigImpl {
  225. 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 {
  226. var unseenIndicies = IndexSet(integersIn: array.indices)
  227. for record in records {
  228. if let index = record["index"] as? Int {
  229. guard array.indices.contains(index) else {
  230. throw ConfigurationError.indexNotFound(index: index)
  231. }
  232. try onExisting(&array[index], record)
  233. unseenIndicies.remove(index)
  234. } else {
  235. array.append(try onNew(record))
  236. }
  237. }
  238. array.remove(atOffsets: unseenIndicies)
  239. }
  240. 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 {
  241. var unseenIndicies = IndexSet(integersIn: array.indices)
  242. for record in records {
  243. if let id = record["id"] as? T.ID {
  244. guard let index = array.enumerated().first(where: { $1.id == id })?.offset else {
  245. throw ConfigurationError.identifierNotFound(id: id)
  246. }
  247. try onExisting(&array[index], record)
  248. unseenIndicies.remove(index)
  249. } else {
  250. array.append(try onNew(record))
  251. }
  252. }
  253. array.remove(atOffsets: unseenIndicies)
  254. }
  255. private func parseQemuDirectoryShareMode(_ value: AEKeyword?) -> QEMUFileShareMode? {
  256. guard let value = value, let parsed = UTMScriptingQemuDirectoryShareMode(rawValue: value) else {
  257. return Optional.none
  258. }
  259. switch parsed {
  260. case .none: return QEMUFileShareMode.none
  261. case .webDAV: return .webdav
  262. case .virtFS: return .virtfs
  263. default: return Optional.none
  264. }
  265. }
  266. private func updateQemuConfiguration(from record: [AnyHashable : Any]) throws {
  267. let config = config as! UTMQemuConfiguration
  268. if let name = record["name"] as? String, !name.isEmpty {
  269. config.information.name = name
  270. }
  271. if let notes = record["notes"] as? String, !notes.isEmpty {
  272. config.information.notes = notes
  273. }
  274. let architecture = record["architecture"] as? String
  275. let arch = QEMUArchitecture(rawValue: architecture ?? "")
  276. let machine = record["machine"] as? String
  277. let target = arch?.targetType.init(rawValue: machine ?? "")
  278. if let arch = arch, arch != config.system.architecture {
  279. let target = target ?? arch.targetType.default
  280. config.system.architecture = arch
  281. config.system.target = target
  282. config.reset(forArchitecture: arch, target: target)
  283. } else if let target = target {
  284. config.system.target = target
  285. config.reset(forArchitecture: config.system.architecture, target: target)
  286. }
  287. if let memory = record["memory"] as? Int, memory != 0 {
  288. config.system.memorySize = memory
  289. }
  290. if let cpuCores = record["cpuCores"] as? Int {
  291. config.system.cpuCount = cpuCores
  292. }
  293. if let hypervisor = record["hypervisor"] as? Bool {
  294. config.qemu.hasHypervisor = hypervisor
  295. }
  296. if let uefi = record["uefi"] as? Bool {
  297. config.qemu.hasUefiBoot = uefi
  298. }
  299. if let directoryShareMode = parseQemuDirectoryShareMode(record["directoryShareMode"] as? AEKeyword) {
  300. config.sharing.directoryShareMode = directoryShareMode
  301. }
  302. if let drives = record["drives"] as? [[AnyHashable : Any]] {
  303. try updateQemuDrives(from: drives)
  304. }
  305. if let networkInterfaces = record["networkInterfaces"] as? [[AnyHashable : Any]] {
  306. try updateQemuNetworks(from: networkInterfaces)
  307. }
  308. if let serialPorts = record["serialPorts"] as? [[AnyHashable : Any]] {
  309. try updateQemuSerials(from: serialPorts)
  310. }
  311. }
  312. private func parseQemuDriveInterface(_ value: AEKeyword?) -> QEMUDriveInterface? {
  313. guard let value = value, let parsed = UTMScriptingQemuDriveInterface(rawValue: value) else {
  314. return Optional.none
  315. }
  316. switch parsed {
  317. case .none: return QEMUDriveInterface.none
  318. case .ide: return .ide
  319. case .scsi: return .scsi
  320. case .sd: return .sd
  321. case .mtd: return .mtd
  322. case .floppy: return .floppy
  323. case .pFlash: return .pflash
  324. case .virtIO: return .virtio
  325. case .nvMe: return .nvme
  326. case .usb: return .usb
  327. default: return Optional.none
  328. }
  329. }
  330. private func updateQemuDrives(from records: [[AnyHashable : Any]]) throws {
  331. let config = config as! UTMQemuConfiguration
  332. try updateIdentifiedElements(&config.drives, with: records, onExisting: updateQemuExistingDrive, onNew: unserializeQemuDriveNew)
  333. }
  334. private func updateQemuExistingDrive(_ drive: inout UTMQemuConfigurationDrive, from record: [AnyHashable : Any]) throws {
  335. if let interface = parseQemuDriveInterface(record["interface"] as? AEKeyword) {
  336. drive.interface = interface
  337. }
  338. }
  339. private func unserializeQemuDriveNew(from record: [AnyHashable : Any]) throws -> UTMQemuConfigurationDrive {
  340. let config = config as! UTMQemuConfiguration
  341. let removable = record["removable"] as? Bool ?? false
  342. var newDrive = UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target, isExternal: removable)
  343. if let importUrl = record["source"] as? URL {
  344. newDrive.imageURL = importUrl
  345. } else if let size = record["guestSize"] as? Int {
  346. newDrive.sizeMib = size
  347. }
  348. if let interface = parseQemuDriveInterface(record["interface"] as? AEKeyword) {
  349. newDrive.interface = interface
  350. }
  351. if let raw = record["raw"] as? Bool {
  352. newDrive.isRawImage = raw
  353. }
  354. return newDrive
  355. }
  356. private func updateQemuNetworks(from records: [[AnyHashable : Any]]) throws {
  357. let config = config as! UTMQemuConfiguration
  358. try updateElements(&config.networks, with: records, onExisting: updateQemuExistingNetwork, onNew: { record in
  359. guard var newNetwork = UTMQemuConfigurationNetwork(forArchitecture: config.system.architecture, target: config.system.target) else {
  360. throw ConfigurationError.deviceNotSupported
  361. }
  362. try updateQemuExistingNetwork(&newNetwork, from: record)
  363. return newNetwork
  364. })
  365. }
  366. private func parseQemuNetworkMode(_ value: AEKeyword?) -> QEMUNetworkMode? {
  367. guard let value = value, let parsed = UTMScriptingQemuNetworkMode(rawValue: value) else {
  368. return Optional.none
  369. }
  370. switch parsed {
  371. case .emulated: return .emulated
  372. case .shared: return .shared
  373. case .host: return .host
  374. case .bridged: return .bridged
  375. default: return .none
  376. }
  377. }
  378. private func updateQemuExistingNetwork(_ network: inout UTMQemuConfigurationNetwork, from record: [AnyHashable : Any]) throws {
  379. let config = config as! UTMQemuConfiguration
  380. if let hardware = record["hardware"] as? String, let hardware = config.system.architecture.networkDeviceType.init(rawValue: hardware) {
  381. network.hardware = hardware
  382. }
  383. if let mode = parseQemuNetworkMode(record["mode"] as? AEKeyword) {
  384. network.mode = mode
  385. }
  386. if let address = record["address"] as? String, !address.isEmpty {
  387. network.macAddress = address
  388. }
  389. if let interface = record["hostInterface"] as? String, !interface.isEmpty {
  390. network.bridgeInterface = interface
  391. }
  392. if let portForwards = record["portForwards"] as? [[AnyHashable : Any]] {
  393. network.portForward = portForwards.map({ unserializeQemuPortForward(from: $0) })
  394. }
  395. }
  396. private func parseNetworkProtocol(_ value: AEKeyword?) -> QEMUNetworkProtocol? {
  397. guard let value = value, let parsed = UTMScriptingNetworkProtocol(rawValue: value) else {
  398. return Optional.none
  399. }
  400. switch parsed {
  401. case .tcp: return .tcp
  402. case .udp: return .udp
  403. default: return Optional.none
  404. }
  405. }
  406. private func unserializeQemuPortForward(from record: [AnyHashable : Any]) -> UTMQemuConfigurationPortForward {
  407. var forward = UTMQemuConfigurationPortForward()
  408. if let protoc = parseNetworkProtocol(record["protocol"] as? AEKeyword) {
  409. forward.protocol = protoc
  410. }
  411. if let hostAddress = record["hostAddress"] as? String, !hostAddress.isEmpty {
  412. forward.hostAddress = hostAddress
  413. }
  414. if let hostPort = record["hostPort"] as? Int {
  415. forward.hostPort = hostPort
  416. }
  417. if let guestAddress = record["guestAddress"] as? String, !guestAddress.isEmpty {
  418. forward.guestAddress = guestAddress
  419. }
  420. if let guestPort = record["guestPort"] as? Int {
  421. forward.guestPort = guestPort
  422. }
  423. return forward
  424. }
  425. private func updateQemuSerials(from records: [[AnyHashable : Any]]) throws {
  426. let config = config as! UTMQemuConfiguration
  427. try updateElements(&config.serials, with: records, onExisting: updateQemuExistingSerial, onNew: { record in
  428. guard var newSerial = UTMQemuConfigurationSerial(forArchitecture: config.system.architecture, target: config.system.target) else {
  429. throw ConfigurationError.deviceNotSupported
  430. }
  431. try updateQemuExistingSerial(&newSerial, from: record)
  432. return newSerial
  433. })
  434. }
  435. private func parseQemuSerialInterface(_ value: AEKeyword?) -> QEMUSerialMode? {
  436. guard let value = value, let parsed = UTMScriptingSerialInterface(rawValue: value) else {
  437. return Optional.none
  438. }
  439. switch parsed {
  440. case .ptty: return .ptty
  441. case .tcp: return .tcpServer
  442. default: return Optional.none
  443. }
  444. }
  445. private func updateQemuExistingSerial(_ serial: inout UTMQemuConfigurationSerial, from record: [AnyHashable : Any]) throws {
  446. let config = config as! UTMQemuConfiguration
  447. if let hardware = record["hardware"] as? String, let hardware = config.system.architecture.serialDeviceType.init(rawValue: hardware) {
  448. serial.hardware = hardware
  449. }
  450. if let interface = parseQemuSerialInterface(record["interface"] as? AEKeyword) {
  451. serial.mode = interface
  452. }
  453. if let port = record["port"] as? Int {
  454. serial.tcpPort = port
  455. }
  456. }
  457. private func updateAppleConfiguration(from record: [AnyHashable : Any]) throws {
  458. let config = config as! UTMAppleConfiguration
  459. if let name = record["name"] as? String, !name.isEmpty {
  460. config.information.name = name
  461. }
  462. if let notes = record["notes"] as? String, !notes.isEmpty {
  463. config.information.notes = notes
  464. }
  465. if let memory = record["memory"] as? Int, memory != 0 {
  466. config.system.memorySize = memory
  467. }
  468. if let cpuCores = record["cpuCores"] as? Int {
  469. config.system.cpuCount = cpuCores
  470. }
  471. if let directoryShares = record["directoryShares"] as? [[AnyHashable : Any]] {
  472. try updateAppleDirectoryShares(from: directoryShares)
  473. }
  474. if let drives = record["drives"] as? [[AnyHashable : Any]] {
  475. try updateAppleDrives(from: drives)
  476. }
  477. if let networkInterfaces = record["networkInterfaces"] as? [[AnyHashable : Any]] {
  478. try updateAppleNetworks(from: networkInterfaces)
  479. }
  480. if let serialPorts = record["serialPorts"] as? [[AnyHashable : Any]] {
  481. try updateAppleSerials(from: serialPorts)
  482. }
  483. }
  484. private func updateAppleDirectoryShares(from records: [[AnyHashable : Any]]) throws {
  485. let config = config as! UTMAppleConfiguration
  486. try updateElements(&config.sharedDirectories, with: records, onExisting: updateAppleExistingDirectoryShare, onNew: { record in
  487. var newShare = UTMAppleConfigurationSharedDirectory(directoryURL: nil, isReadOnly: false)
  488. try updateAppleExistingDirectoryShare(&newShare, from: record)
  489. return newShare
  490. })
  491. }
  492. private func updateAppleExistingDirectoryShare(_ share: inout UTMAppleConfigurationSharedDirectory, from record: [AnyHashable : Any]) throws {
  493. if let readOnly = record["readOnly"] as? Bool {
  494. share.isReadOnly = readOnly
  495. }
  496. }
  497. private func updateAppleDrives(from records: [[AnyHashable : Any]]) throws {
  498. let config = config as! UTMAppleConfiguration
  499. try updateIdentifiedElements(&config.drives, with: records, onExisting: { _, _ in }, onNew: unserializeAppleNewDrive)
  500. }
  501. private func unserializeAppleNewDrive(from record: [AnyHashable : Any]) throws -> UTMAppleConfigurationDrive {
  502. let removable = record["removable"] as? Bool ?? false
  503. var newDrive: UTMAppleConfigurationDrive
  504. if let size = record["guestSize"] as? Int {
  505. newDrive = UTMAppleConfigurationDrive(newSize: size)
  506. } else {
  507. newDrive = UTMAppleConfigurationDrive(existingURL: record["source"] as? URL, isExternal: removable)
  508. }
  509. return newDrive
  510. }
  511. private func updateAppleNetworks(from records: [[AnyHashable : Any]]) throws {
  512. let config = config as! UTMAppleConfiguration
  513. try updateElements(&config.networks, with: records, onExisting: updateAppleExistingNetwork, onNew: { record in
  514. var newNetwork = UTMAppleConfigurationNetwork()
  515. try updateAppleExistingNetwork(&newNetwork, from: record)
  516. return newNetwork
  517. })
  518. }
  519. private func parseAppleNetworkMode(_ value: AEKeyword?) -> UTMAppleConfigurationNetwork.NetworkMode? {
  520. guard let value = value, let parsed = UTMScriptingQemuNetworkMode(rawValue: value) else {
  521. return Optional.none
  522. }
  523. switch parsed {
  524. case .shared: return .shared
  525. case .bridged: return .bridged
  526. default: return Optional.none
  527. }
  528. }
  529. private func updateAppleExistingNetwork(_ network: inout UTMAppleConfigurationNetwork, from record: [AnyHashable : Any]) throws {
  530. if let mode = parseAppleNetworkMode(record["mode"] as? AEKeyword) {
  531. network.mode = mode
  532. }
  533. if let address = record["address"] as? String, !address.isEmpty {
  534. network.macAddress = address
  535. }
  536. if let interface = record["hostInterface"] as? String, !interface.isEmpty {
  537. network.bridgeInterface = interface
  538. }
  539. }
  540. private func updateAppleSerials(from records: [[AnyHashable : Any]]) throws {
  541. let config = config as! UTMAppleConfiguration
  542. try updateElements(&config.serials, with: records, onExisting: updateAppleExistingSerial, onNew: { record in
  543. var newSerial = UTMAppleConfigurationSerial()
  544. try updateAppleExistingSerial(&newSerial, from: record)
  545. return newSerial
  546. })
  547. }
  548. private func parseAppleSerialInterface(_ value: AEKeyword?) -> UTMAppleConfigurationSerial.SerialMode? {
  549. guard let value = value, let parsed = UTMScriptingSerialInterface(rawValue: value) else {
  550. return Optional.none
  551. }
  552. switch parsed {
  553. case .ptty: return .ptty
  554. default: return Optional.none
  555. }
  556. }
  557. private func updateAppleExistingSerial(_ serial: inout UTMAppleConfigurationSerial, from record: [AnyHashable : Any]) throws {
  558. if let interface = parseAppleSerialInterface(record["interface"] as? AEKeyword) {
  559. serial.mode = interface
  560. }
  561. }
  562. enum ConfigurationError: Error, LocalizedError {
  563. case identifierNotFound(id: any Hashable)
  564. case invalidDriveDescription
  565. case indexNotFound(index: Int)
  566. case deviceNotSupported
  567. var errorDescription: String? {
  568. switch self {
  569. case .identifierNotFound(let id): return NSLocalizedString("Identifier '\(id)' cannot be found.", comment: "UTMScriptingConfigImpl")
  570. case .invalidDriveDescription: return NSLocalizedString("Drive description is invalid.", comment: "UTMScriptingConfigImpl")
  571. case .indexNotFound(let index): return NSLocalizedString("Index \(index) cannot be found.", comment: "UTMScriptingConfigImpl")
  572. case .deviceNotSupported: return NSLocalizedString("This device is not supported by the target.", comment: "UTMScriptingConfigImpl")
  573. }
  574. }
  575. }
  576. }