Run virtual machines on iOS
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

665 lines
27 KiB

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