UTMQemuConfigurationDrive.swift 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. //
  2. // Copyright © 2022 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. /// Settings for single QEMU disk device
  18. struct UTMQemuConfigurationDrive: UTMConfigurationDrive {
  19. static let latestInterfaceVersion = 1
  20. /// If not removable, this is the name of the file in the bundle.
  21. var imageName: String?
  22. /// Size of the image when creating a new image (in MiB). Not saved.
  23. var sizeMib: Int = 0
  24. /// If true, the drive image will be mounted as read-only.
  25. var isReadOnly: Bool = false
  26. /// If true, a bookmark is stored in the package.
  27. var isExternal: Bool = false
  28. /// If valid, will point to the actual location of the drive image. Not saved.
  29. var imageURL: URL?
  30. /// Unique identifier for this drive
  31. private(set) var id: String = UUID().uuidString
  32. /// Type of the image.
  33. var imageType: QEMUDriveImageType = .none
  34. /// Interface of the image (only valid when type is CD/Disk).
  35. var interface: QEMUDriveInterface = .none
  36. /// Interface version for backwards compatibility
  37. var interfaceVersion: Int = Self.latestInterfaceVersion
  38. /// If true, the created image will be raw format and not QCOW2. Not saved.
  39. var isRawImage: Bool = false
  40. /// If initialized, returns a default interface for an image type. Not saved.
  41. var defaultInterfaceForImageType: ((QEMUDriveImageType) -> QEMUDriveInterface)?
  42. enum CodingKeys: String, CodingKey {
  43. case imageName = "ImageName"
  44. case imageType = "ImageType"
  45. case interface = "Interface"
  46. case interfaceVersion = "InterfaceVersion"
  47. case identifier = "Identifier"
  48. case isReadOnly = "ReadOnly"
  49. }
  50. init() {
  51. }
  52. init(from decoder: Decoder) throws {
  53. guard let dataURL = decoder.userInfo[.dataURL] as? URL else {
  54. throw UTMConfigurationError.invalidDataURL
  55. }
  56. let values = try decoder.container(keyedBy: CodingKeys.self)
  57. if let imageName = try values.decodeIfPresent(String.self, forKey: .imageName) {
  58. self.imageName = imageName
  59. imageURL = dataURL.appendingPathComponent(imageName)
  60. isExternal = false
  61. } else {
  62. isExternal = true
  63. }
  64. isReadOnly = try values.decodeIfPresent(Bool.self, forKey: .isReadOnly) ?? isExternal
  65. imageType = try values.decode(QEMUDriveImageType.self, forKey: .imageType)
  66. interface = try values.decode(QEMUDriveInterface.self, forKey: .interface)
  67. interfaceVersion = try values.decodeIfPresent(Int.self, forKey: .interfaceVersion) ?? 0
  68. id = try values.decode(String.self, forKey: .identifier)
  69. }
  70. func encode(to encoder: Encoder) throws {
  71. var container = encoder.container(keyedBy: CodingKeys.self)
  72. if !isExternal {
  73. try container.encodeIfPresent(imageURL?.lastPathComponent, forKey: .imageName)
  74. }
  75. try container.encode(isReadOnly, forKey: .isReadOnly)
  76. try container.encode(imageType, forKey: .imageType)
  77. if imageType == .cd || imageType == .disk {
  78. try container.encode(interface, forKey: .interface)
  79. } else {
  80. try container.encode(QEMUDriveInterface.none, forKey: .interface)
  81. }
  82. try container.encode(interfaceVersion, forKey: .interfaceVersion)
  83. try container.encode(id, forKey: .identifier)
  84. }
  85. func hash(into hasher: inout Hasher) {
  86. imageName?.hash(into: &hasher)
  87. sizeMib.hash(into: &hasher)
  88. isReadOnly.hash(into: &hasher)
  89. isExternal.hash(into: &hasher)
  90. id.hash(into: &hasher)
  91. imageType.hash(into: &hasher)
  92. interface.hash(into: &hasher)
  93. interfaceVersion.hash(into: &hasher)
  94. isRawImage.hash(into: &hasher)
  95. }
  96. func clone() -> UTMQemuConfigurationDrive {
  97. var cloned = self
  98. cloned.id = UUID().uuidString
  99. return cloned
  100. }
  101. }
  102. // MARK: - Default interface
  103. extension UTMQemuConfigurationDrive {
  104. static func defaultInterface(forArchitecture architecture: QEMUArchitecture, target: any QEMUTarget, imageType: QEMUDriveImageType) -> QEMUDriveInterface {
  105. let rawTarget = target.rawValue
  106. if rawTarget.hasPrefix("virt-") || rawTarget == "virt" {
  107. if imageType == .cd {
  108. return .usb
  109. } else {
  110. return .virtio
  111. }
  112. } else if architecture == .sparc || architecture == .sparc64 {
  113. return .scsi
  114. } else {
  115. return .ide
  116. }
  117. }
  118. }
  119. // MARK: - Conversion of old config format
  120. extension UTMQemuConfigurationDrive {
  121. init(migrating oldConfig: UTMLegacyQemuConfiguration, at index: Int) {
  122. self.init()
  123. imageName = oldConfig.driveImagePath(for: index)
  124. imageType = convertImageType(from: oldConfig.driveImageType(for: index))
  125. interface = convertInterface(from: oldConfig.driveInterfaceType(for: index))
  126. interfaceVersion = 0
  127. isExternal = oldConfig.driveRemovable(for: index)
  128. isReadOnly = isExternal
  129. var oldId = oldConfig.driveName(for: index) ?? UUID().uuidString
  130. if oldId.hasPrefix("drive") {
  131. oldId.removeFirst(5)
  132. }
  133. if oldId.isEmpty {
  134. oldId = UUID().uuidString
  135. }
  136. id = oldId
  137. let dataURL = oldConfig.existingPath?.appendingPathComponent(QEMUPackageFileName.images.rawValue)
  138. if let imageName = imageName {
  139. imageURL = dataURL?.appendingPathComponent(imageName)
  140. }
  141. }
  142. private func convertImageType(from type: UTMDiskImageType) -> QEMUDriveImageType {
  143. switch type {
  144. case .none:
  145. return .none
  146. case .disk:
  147. return .disk
  148. case .CD:
  149. return .cd
  150. case .BIOS:
  151. return .bios
  152. case .kernel:
  153. return .linuxKernel
  154. case .initrd:
  155. return .linuxInitrd
  156. case .DTB:
  157. return .linuxDtb
  158. case .max:
  159. return .none
  160. @unknown default:
  161. return .none
  162. }
  163. }
  164. private func convertInterface(from str: String?) -> QEMUDriveInterface {
  165. if str == "ide" {
  166. return .ide
  167. } else if str == "scsi" {
  168. return .scsi
  169. } else if str == "sd" {
  170. return .sd
  171. } else if str == "mtd" {
  172. return .mtd
  173. } else if str == "floppy" {
  174. return .floppy
  175. } else if str == "pflash" {
  176. return .pflash
  177. } else if str == "virtio" {
  178. return .virtio
  179. } else if str == "nvme" {
  180. return .nvme
  181. } else if str == "usb" {
  182. return .usb
  183. } else {
  184. return .none
  185. }
  186. }
  187. }
  188. // MARK: - New drive
  189. extension UTMQemuConfigurationDrive {
  190. init(forArchitecture architecture: QEMUArchitecture, target: any QEMUTarget, isExternal: Bool = false) {
  191. self.isExternal = isExternal
  192. self.imageType = isExternal ? .cd : .disk
  193. self.isRawImage = false
  194. self.imageName = nil
  195. self.sizeMib = 10240
  196. self.isReadOnly = isExternal
  197. self.imageURL = nil
  198. self.id = UUID().uuidString
  199. self.defaultInterfaceForImageType = { Self.defaultInterface(forArchitecture: architecture, target: target, imageType: $0) }
  200. self.interface = defaultInterfaceForImageType!(imageType)
  201. self.interfaceVersion = Self.latestInterfaceVersion
  202. }
  203. }