openvmm_entry/
cli_args.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! CLI argument parsing.
5//!
6//! Code in this module must not instantiate any complex VM objects!
7//!
8//! In other words, this module is only responsible for marshalling raw CLI
9//! strings into typed Rust structs/enums, and should consist of entirely _pure
10//! functions_.
11//!
12//! e.g: instead of opening a `File` directly, parse the specified file path
13//! into a `PathBuf`, and allow later parts of the init flow to handle opening
14//! the file.
15
16// NOTE: This module itself is not pub, but the Options struct below is
17//       re-exported as pub in main to make this lint fire. It won't fire on
18//       anything else on this file though.
19#![warn(missing_docs)]
20
21use anyhow::Context;
22use clap::Parser;
23use clap::ValueEnum;
24use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
25use openvmm_defs::config::DeviceVtl;
26use openvmm_defs::config::PcatBootDevice;
27use openvmm_defs::config::Vtl2BaseAddressType;
28use openvmm_defs::config::X2ApicConfig;
29use std::ffi::OsString;
30use std::net::SocketAddr;
31use std::path::PathBuf;
32use std::str::FromStr;
33use thiserror::Error;
34
35/// OpenVMM virtual machine monitor.
36///
37/// This is not yet a stable interface and may change radically between
38/// versions.
39#[derive(Parser)]
40pub struct Options {
41    /// processor count
42    #[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
43    pub processors: u32,
44
45    /// guest RAM size
46    #[clap(
47        short = 'm',
48        long,
49        value_name = "SIZE",
50        default_value = "1GB",
51        value_parser = parse_memory
52    )]
53    pub memory: u64,
54
55    /// use shared memory segment
56    #[clap(short = 'M', long)]
57    pub shared_memory: bool,
58
59    /// prefetch guest RAM
60    #[clap(long)]
61    pub prefetch: bool,
62
63    /// back guest RAM with a file instead of anonymous memory.
64    /// The file is created/opened and sized to the guest RAM size.
65    /// Enables snapshot save (fsync) and restore (open + mmap).
66    #[clap(long, value_name = "FILE", conflicts_with = "private_memory")]
67    pub memory_backing_file: Option<PathBuf>,
68
69    /// Restore VM from a snapshot directory (implies file-backed memory from
70    /// the snapshot's memory.bin). Cannot be used with --memory-backing-file.
71    #[clap(long, value_name = "DIR", conflicts_with = "memory_backing_file")]
72    pub restore_snapshot: Option<PathBuf>,
73
74    /// use private anonymous memory for guest RAM
75    #[clap(long, conflicts_with_all = ["memory_backing_file", "restore_snapshot"])]
76    pub private_memory: bool,
77
78    /// enable transparent huge pages for guest RAM (Linux only, requires --private-memory)
79    #[clap(long, requires("private_memory"))]
80    pub thp: bool,
81
82    /// start in paused state
83    #[clap(short = 'P', long)]
84    pub paused: bool,
85
86    /// kernel image (when using linux direct boot)
87    #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
88    pub kernel: OptionalPathBuf,
89
90    /// initrd image (when using linux direct boot)
91    #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
92    pub initrd: OptionalPathBuf,
93
94    /// extra kernel command line args
95    #[clap(short = 'c', long, value_name = "STRING")]
96    pub cmdline: Vec<String>,
97
98    /// enable HV#1 capabilities
99    #[clap(long)]
100    pub hv: bool,
101
102    /// Use a full device tree instead of ACPI tables for ARM64 Linux direct
103    /// boot. By default, ARM64 uses ACPI mode (stub DT + EFI + ACPI tables).
104    /// This flag selects the legacy DT-only path. Rejected on x86.
105    #[clap(long, conflicts_with_all = ["uefi", "pcat", "igvm"])]
106    pub device_tree: bool,
107
108    /// enable vtl2 - only supported in WHP and simulated without hypervisor support currently
109    ///
110    /// Currently implies --get.
111    #[clap(long, requires("hv"))]
112    pub vtl2: bool,
113
114    /// Add GET and related devices for using the OpenHCL paravisor to the
115    /// highest enabled VTL.
116    #[clap(long, requires("hv"))]
117    pub get: bool,
118
119    /// Disable GET and related devices for using the OpenHCL paravisor, even
120    /// when --vtl2 is passed.
121    #[clap(long, conflicts_with("get"))]
122    pub no_get: bool,
123
124    /// disable the VTL0 alias map presented to VTL2 by default
125    #[clap(long, requires("vtl2"))]
126    pub no_alias_map: bool,
127
128    /// enable isolation emulation
129    #[clap(long, requires("vtl2"))]
130    pub isolation: Option<IsolationCli>,
131
132    /// the hybrid vsock listener path
133    #[clap(long, value_name = "PATH", alias = "vsock-path")]
134    pub vmbus_vsock_path: Option<String>,
135
136    /// the VTL2 hybrid vsock listener path
137    #[clap(long, value_name = "PATH", requires("vtl2"), alias = "vtl2-vsock-path")]
138    pub vmbus_vtl2_vsock_path: Option<String>,
139
140    /// the late map vtl0 ram access policy when vtl2 is enabled
141    #[clap(long, requires("vtl2"), default_value = "halt")]
142    pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
143
144    /// disable in-hypervisor enlightenment implementation (where possible)
145    #[clap(long)]
146    pub no_enlightenments: bool,
147
148    /// disable the in-hypervisor APIC and use the user-mode one (where possible)
149    #[clap(long)]
150    pub user_mode_apic: bool,
151
152    /// attach a disk (can be passed multiple times)
153    #[clap(long_help = r#"
154e.g: --disk memdiff:file:/path/to/disk.vhd
155
156syntax: <path> | kind:<arg>[,flag,opt=arg,...]
157
158valid disk kinds:
159    `mem:<len>`                    memory backed disk
160        <len>: length of ramdisk, e.g.: `1G`
161    `memdiff:<disk>`               memory backed diff disk
162        <disk>: lower disk, e.g.: `file:base.img`
163    `file:<path>[;create=<len>]`   file-backed disk
164        <path>: path to file
165    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
166    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
167    `autocache:<key>:<disk>`       auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
168    `blob:<type>:<url>`            HTTP blob (read-only)
169        <type>: `flat` or `vhd1`
170    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
171        <cipher>: `xts-aes-256`
172    `prwrap:<disk>`                persistent reservations wrapper
173
174flags:
175    `ro`                           open disk as read-only
176    `dvd`                          specifies that device is cd/dvd and it is read_only
177    `vtl2`                         assign this disk to VTL2
178    `uh`                           relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
179    `uh-nvme`                      relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
180
181options:
182    `pcie_port=<name>`             present the disk using pcie under the specified port, incompatible with `dvd`, `vtl2`, `uh`, and `uh-nvme`
183"#)]
184    #[clap(long, value_name = "FILE")]
185    pub disk: Vec<DiskCli>,
186
187    /// attach a disk via an NVMe controller
188    #[clap(long_help = r#"
189e.g: --nvme memdiff:file:/path/to/disk.vhd
190
191syntax: <path> | kind:<arg>[,flag,opt=arg,...]
192
193valid disk kinds:
194    `mem:<len>`                    memory backed disk
195        <len>: length of ramdisk, e.g.: `1G`
196    `memdiff:<disk>`               memory backed diff disk
197        <disk>: lower disk, e.g.: `file:base.img`
198    `file:<path>[;create=<len>]`   file-backed disk
199        <path>: path to file
200    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
201    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
202    `autocache:<key>:<disk>`       auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
203    `blob:<type>:<url>`            HTTP blob (read-only)
204        <type>: `flat` or `vhd1`
205    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
206        <cipher>: `xts-aes-256`
207    `prwrap:<disk>`                persistent reservations wrapper
208
209flags:
210    `ro`                           open disk as read-only
211    `vtl2`                         assign this disk to VTL2
212    `uh`                           relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
213    `uh-nvme`                      relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
214
215options:
216    `pcie_port=<name>`             present the disk using pcie under the specified port, incompatible with `vtl2`, `uh`, and `uh-nvme`
217"#)]
218    #[clap(long)]
219    pub nvme: Vec<DiskCli>,
220
221    /// attach a disk via a virtio-blk controller
222    #[clap(long_help = r#"
223e.g: --virtio-blk memdiff:file:/path/to/disk.vhd
224
225syntax: <path> | kind:<arg>[,flag,opt=arg,...]
226
227valid disk kinds:
228    `mem:<len>`                    memory backed disk
229        <len>: length of ramdisk, e.g.: `1G`
230    `memdiff:<disk>`               memory backed diff disk
231        <disk>: lower disk, e.g.: `file:base.img`
232    `file:<path>`                  file-backed disk
233        <path>: path to file
234
235flags:
236    `ro`                           open disk as read-only
237
238options:
239    `pcie_port=<name>`             present the disk using pcie under the specified port
240"#)]
241    #[clap(long = "virtio-blk")]
242    pub virtio_blk: Vec<DiskCli>,
243
244    /// number of sub-channels for the SCSI controller
245    #[clap(long, value_name = "COUNT", default_value = "0")]
246    pub scsi_sub_channels: u16,
247
248    /// expose a virtual NIC
249    #[clap(long)]
250    pub nic: bool,
251
252    /// expose a virtual NIC with the given backend (consomme | dio | tap | none)
253    ///
254    /// Prefix with `uh:` to add this NIC via Mana emulation through OpenHCL,
255    /// `vtl2:` to assign this NIC to VTL2, or `pcie_port=<port_name>:` to
256    /// expose the NIC over emulated PCIe at the specified port.
257    #[clap(long)]
258    pub net: Vec<NicConfigCli>,
259
260    /// expose a virtual NIC using the Windows kernel-mode vmswitch.
261    ///
262    /// Specify the switch ID or "default" for the default switch.
263    #[clap(long, value_name = "SWITCH_ID")]
264    pub kernel_vmnic: Vec<String>,
265
266    /// expose a graphics device
267    #[clap(long)]
268    pub gfx: bool,
269
270    /// support a graphics device in vtl2
271    #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
272    pub vtl2_gfx: bool,
273
274    /// listen for vnc connections. implied by gfx.
275    #[clap(long)]
276    pub vnc: bool,
277
278    /// VNC port number
279    #[clap(long, value_name = "PORT", default_value = "5900")]
280    pub vnc_port: u16,
281
282    /// set the APIC ID offset, for testing APIC IDs that don't match VP index
283    #[cfg(guest_arch = "x86_64")]
284    #[clap(long, default_value_t)]
285    pub apic_id_offset: u32,
286
287    /// the maximum number of VPs per socket
288    #[clap(long)]
289    pub vps_per_socket: Option<u32>,
290
291    /// enable or disable SMT (hyperthreading) (auto | force | off)
292    #[clap(long, default_value = "auto")]
293    pub smt: SmtConfigCli,
294
295    /// configure x2apic (auto | supported | off | on)
296    #[cfg(guest_arch = "x86_64")]
297    #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
298    pub x2apic: X2ApicConfig,
299
300    /// COM1 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
301    #[clap(long, value_name = "SERIAL")]
302    pub com1: Option<SerialConfigCli>,
303
304    /// COM2 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
305    #[clap(long, value_name = "SERIAL")]
306    pub com2: Option<SerialConfigCli>,
307
308    /// COM3 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
309    #[clap(long, value_name = "SERIAL")]
310    pub com3: Option<SerialConfigCli>,
311
312    /// COM4 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
313    #[clap(long, value_name = "SERIAL")]
314    pub com4: Option<SerialConfigCli>,
315
316    /// vmbus com1 serial binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
317    #[structopt(long, value_name = "SERIAL")]
318    pub vmbus_com1_serial: Option<SerialConfigCli>,
319
320    /// vmbus com2 serial binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
321    #[structopt(long, value_name = "SERIAL")]
322    pub vmbus_com2_serial: Option<SerialConfigCli>,
323
324    /// Only allow guest to host serial traffic
325    #[clap(long)]
326    pub serial_tx_only: bool,
327
328    /// debugcon binding (port:serial, where port is a u16, and serial is (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none))
329    #[clap(long, value_name = "SERIAL")]
330    pub debugcon: Option<DebugconSerialConfigCli>,
331
332    /// boot UEFI firmware
333    #[clap(long, short = 'e')]
334    pub uefi: bool,
335
336    /// UEFI firmware file
337    #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
338    pub uefi_firmware: OptionalPathBuf,
339
340    /// enable UEFI debugging on COM1
341    #[clap(long, requires("uefi"))]
342    pub uefi_debug: bool,
343
344    /// enable memory protections in UEFI
345    #[clap(long, requires("uefi"))]
346    pub uefi_enable_memory_protections: bool,
347
348    /// set PCAT boot order as comma-separated string of boot device types
349    /// (e.g: floppy,hdd,optical,net).
350    ///
351    /// If less than 4 entries are added, entries are added according to their
352    /// default boot order (optical,hdd,net,floppy)
353    ///
354    /// e.g: passing "floppy,optical" will result in a boot order equivalent to
355    /// "floppy,optical,hdd,net".
356    ///
357    /// Passing duplicate types is an error.
358    #[clap(long, requires("pcat"))]
359    pub pcat_boot_order: Option<PcatBootOrderCli>,
360
361    /// Boot with PCAT BIOS firmware and piix4 devices
362    #[clap(long, conflicts_with("uefi"))]
363    pub pcat: bool,
364
365    /// PCAT firmware file
366    #[clap(long, requires("pcat"), value_name = "FILE")]
367    pub pcat_firmware: Option<PathBuf>,
368
369    /// boot IGVM file
370    #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
371    pub igvm: Option<PathBuf>,
372
373    /// specify igvm vtl2 relocation type
374    /// (absolute=\<addr\>, disable, auto=\<filesize,or memory size\>, vtl2=\<filesize,or memory size\>,)
375    #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
376    pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
377
378    /// add a virtio_9p device (e.g. myfs,C:\)
379    ///
380    /// Prefix with `pcie_port=<port_name>:` to expose the device over
381    /// emulated PCIe at the specified port.
382    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
383    pub virtio_9p: Vec<FsArgs>,
384
385    /// output debug info from the 9p server
386    #[clap(long)]
387    pub virtio_9p_debug: bool,
388
389    /// add a virtio_fs device (e.g. myfs,C:\,uid=1000,gid=2000)
390    ///
391    /// Prefix with `pcie_port=<port_name>:` to expose the device over
392    /// emulated PCIe at the specified port.
393    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path,[options]")]
394    pub virtio_fs: Vec<FsArgsWithOptions>,
395
396    /// add a virtio_fs device for sharing memory (e.g. myfs,\SectionDirectoryPath)
397    ///
398    /// Prefix with `pcie_port=<port_name>:` to expose the device over
399    /// emulated PCIe at the specified port.
400    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
401    pub virtio_fs_shmem: Vec<FsArgs>,
402
403    /// add a virtio_fs device under either the PCI or MMIO bus, or whatever the hypervisor supports (pci | mmio | auto)
404    #[clap(long, value_name = "BUS", default_value = "auto")]
405    pub virtio_fs_bus: VirtioBusCli,
406
407    /// virtio PMEM device
408    ///
409    /// Prefix with `pcie_port=<port_name>:` to expose the device over
410    /// emulated PCIe at the specified port.
411    #[clap(long, value_name = "[pcie_port=PORT:]PATH")]
412    pub virtio_pmem: Option<VirtioPmemArgs>,
413
414    /// add a virtio entropy (RNG) device
415    #[clap(long)]
416    pub virtio_rng: bool,
417
418    /// add a virtio-rng device under either the PCI or MMIO bus, or whatever the hypervisor supports (pci | mmio | vpci | auto)
419    #[clap(long, value_name = "BUS", default_value = "auto")]
420    pub virtio_rng_bus: VirtioBusCli,
421
422    /// attach the virtio-rng device to the specified PCIe port (overrides --virtio-rng-bus)
423    #[clap(long, value_name = "PORT", requires("virtio_rng"))]
424    pub virtio_rng_pcie_port: Option<String>,
425
426    /// virtio console device backed by a serial backend (/dev/hvc0 in guest)
427    ///
428    /// Accepts serial config (console | stderr | listen=\<path\> |
429    /// file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> |
430    /// term[=\<program\>]\[,name=\<windowtitle\>\] | none)
431    #[clap(long)]
432    pub virtio_console: Option<SerialConfigCli>,
433
434    /// attach the virtio-console device to the specified PCIe port
435    #[clap(long, value_name = "PORT", requires("virtio_console"))]
436    pub virtio_console_pcie_port: Option<String>,
437
438    /// add a virtio vsock device with the given Unix socket base path
439    #[clap(long, value_name = "PATH")]
440    pub virtio_vsock_path: Option<String>,
441
442    /// expose a virtio network with the given backend (dio | vmnic | tap |
443    /// none)
444    ///
445    /// Prefix with `uh:` to add this NIC via Mana emulation through OpenHCL,
446    /// `vtl2:` to assign this NIC to VTL2, or `pcie_port=<port_name>:` to
447    /// expose the NIC over emulated PCIe at the specified port.
448    #[clap(long)]
449    pub virtio_net: Vec<NicConfigCli>,
450
451    /// send log output from the worker process to a file instead of stderr. the file will be overwritten.
452    #[clap(long, value_name = "PATH")]
453    pub log_file: Option<PathBuf>,
454
455    /// run as a ttrpc server on the specified Unix socket
456    #[clap(long, value_name = "SOCKETPATH")]
457    pub ttrpc: Option<PathBuf>,
458
459    /// run as a grpc server on the specified Unix socket
460    #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
461    pub grpc: Option<PathBuf>,
462
463    /// do not launch child processes
464    #[clap(long)]
465    pub single_process: bool,
466
467    /// device to assign (can be passed multiple times)
468    #[cfg(windows)]
469    #[clap(long, value_name = "PATH")]
470    pub device: Vec<String>,
471
472    /// instead of showing the frontpage the VM will shutdown instead
473    #[clap(long, requires("uefi"))]
474    pub disable_frontpage: bool,
475
476    /// add a vtpm device
477    #[clap(long)]
478    pub tpm: bool,
479
480    /// the mesh worker host name.
481    ///
482    /// Used internally for debugging and diagnostics.
483    #[clap(long, default_value = "control", hide(true))]
484    #[expect(clippy::option_option)]
485    pub internal_worker: Option<Option<String>>,
486
487    /// redirect the VTL 0 vmbus control plane to a proxy in VTL 2.
488    #[clap(long, requires("vtl2"))]
489    pub vmbus_redirect: bool,
490
491    /// limit the maximum protocol version allowed by vmbus; used for testing purposes
492    #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
493    pub vmbus_max_version: Option<u32>,
494
495    /// The disk to use for the VMGS.
496    ///
497    /// If this is not provided, guest state will be stored in memory.
498    #[clap(long_help = r#"
499e.g: --vmgs memdiff:file:/path/to/file.vmgs
500
501syntax: <path> | kind:<arg>[,flag]
502
503valid disk kinds:
504    `mem:<len>`                     memory backed disk
505        <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
506    `memdiff:<disk>[;create=<len>]` memory backed diff disk
507        <disk>: lower disk, e.g.: `file:base.img`
508    `file:<path>`                   file-backed disk
509        <path>: path to file
510
511flags:
512    `fmt`                           reprovision the VMGS before boot
513    `fmt-on-fail`                   reprovision the VMGS before boot if it is corrupted
514"#)]
515    #[clap(long)]
516    pub vmgs: Option<VmgsCli>,
517
518    /// Use GspById guest state encryption policy with a test seed
519    #[clap(long, requires("vmgs"))]
520    pub test_gsp_by_id: bool,
521
522    /// VGA firmware file
523    #[clap(long, requires("pcat"), value_name = "FILE")]
524    pub vga_firmware: Option<PathBuf>,
525
526    /// enable secure boot
527    #[clap(long)]
528    pub secure_boot: bool,
529
530    /// use secure boot template
531    #[clap(long)]
532    pub secure_boot_template: Option<SecureBootTemplateCli>,
533
534    /// custom uefi nvram json file
535    #[clap(long, value_name = "PATH")]
536    pub custom_uefi_json: Option<PathBuf>,
537
538    /// the path to a named pipe (Windows) or Unix socket (Linux) to relay to the connected
539    /// tty.
540    ///
541    /// This is a hidden argument used internally.
542    #[clap(long, hide(true))]
543    pub relay_console_path: Option<PathBuf>,
544
545    /// the title of the console window spawned from the relay console.
546    ///
547    /// This is a hidden argument used internally.
548    #[clap(long, hide(true))]
549    pub relay_console_title: Option<String>,
550
551    /// enable in-hypervisor gdb debugger
552    #[clap(long, value_name = "PORT")]
553    pub gdb: Option<u16>,
554
555    /// enable emulated MANA devices with the given network backend (see --net)
556    ///
557    /// Prefix with `pcie_port=<port_name>:` to expose the nic over emulated PCIe
558    /// at the specified port.
559    #[clap(long)]
560    pub mana: Vec<NicConfigCli>,
561
562    /// use a specific hypervisor interface
563    #[clap(long)]
564    pub hypervisor: Option<String>,
565
566    /// (dev utility) boot linux using a custom (raw) DSDT table.
567    ///
568    /// This is a _very_ niche utility, and it's unlikely you'll need to use it.
569    ///
570    /// e.g: this flag helped bring up certain Hyper-V Generation 1 legacy
571    /// devices without needing to port the associated ACPI code into OpenVMM's
572    /// DSDT builder.
573    #[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
574    pub custom_dsdt: Option<PathBuf>,
575
576    /// attach an ide drive (can be passed multiple times)
577    ///
578    /// Each ide controller has two channels. Each channel can have up to two
579    /// attachments.
580    ///
581    /// If the `s` flag is not passed then the drive will we be attached to the
582    /// primary ide channel if space is available. If two attachments have already
583    /// been added to the primary channel then the drive will be attached to the
584    /// secondary channel.
585    #[clap(long_help = r#"
586e.g: --ide memdiff:file:/path/to/disk.vhd
587
588syntax: <path> | kind:<arg>[,flag,opt=arg,...]
589
590valid disk kinds:
591    `mem:<len>`                    memory backed disk
592        <len>: length of ramdisk, e.g.: `1G`
593    `memdiff:<disk>`               memory backed diff disk
594        <disk>: lower disk, e.g.: `file:base.img`
595    `file:<path>[;create=<len>]`   file-backed disk
596        <path>: path to file
597    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
598    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
599    `blob:<type>:<url>`            HTTP blob (read-only)
600        <type>: `flat` or `vhd1`
601    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
602        <cipher>: `xts-aes-256`
603
604additional wrapper kinds (e.g., `autocache`, `prwrap`) are also supported;
605this list is not exhaustive.
606
607flags:
608    `ro`                           open disk as read-only
609    `s`                            attach drive to secondary ide channel
610    `dvd`                          specifies that device is cd/dvd and it is read_only
611"#)]
612    #[clap(long, value_name = "FILE", requires("pcat"))]
613    pub ide: Vec<IdeDiskCli>,
614
615    /// attach a floppy drive (should be able to be passed multiple times). VM must be generation 1 (no UEFI)
616    ///
617    #[clap(long_help = r#"
618e.g: --floppy memdiff:file:/path/to/disk.vfd,ro
619
620syntax: <path> | kind:<arg>[,flag,opt=arg,...]
621
622valid disk kinds:
623    `mem:<len>`                    memory backed disk
624        <len>: length of ramdisk, e.g.: `1G`
625    `memdiff:<disk>`               memory backed diff disk
626        <disk>: lower disk, e.g.: `file:base.img`
627    `file:<path>[;create=<len>]`   file-backed disk
628        <path>: path to file
629    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
630    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
631    `blob:<type>:<url>`            HTTP blob (read-only)
632        <type>: `flat` or `vhd1`
633    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
634        <cipher>: `xts-aes-256`
635
636flags:
637    `ro`                           open disk as read-only
638"#)]
639    #[clap(long, value_name = "FILE", requires("pcat"))]
640    pub floppy: Vec<FloppyDiskCli>,
641
642    /// enable guest watchdog device
643    #[clap(long)]
644    pub guest_watchdog: bool,
645
646    /// enable OpenHCL's guest crash dump device, targeting the specified path
647    #[clap(long)]
648    pub openhcl_dump_path: Option<PathBuf>,
649
650    /// halt the VM when the guest requests a reset, instead of resetting it
651    #[clap(long)]
652    pub halt_on_reset: bool,
653
654    /// write saved state .proto files to the specified path
655    #[clap(long)]
656    pub write_saved_state_proto: Option<PathBuf>,
657
658    /// specify the IMC hive file for booting Windows
659    #[clap(long)]
660    pub imc: Option<PathBuf>,
661
662    /// Expose MCR device
663    #[clap(long)]
664    pub mcr: bool, // TODO MCR: support closed source CLI flags
665
666    /// expose a battery device
667    #[clap(long)]
668    pub battery: bool,
669
670    /// set the uefi console mode
671    #[clap(long)]
672    pub uefi_console_mode: Option<UefiConsoleModeCli>,
673
674    /// set the EFI diagnostics log level
675    #[clap(long_help = r#"
676Set the EFI diagnostics log level.
677
678options:
679    default                        default (ERROR and WARN only)
680    info                           info (ERROR, WARN, and INFO)
681    full                           full (all log levels)
682"#)]
683    #[clap(long, requires("uefi"))]
684    pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
685
686    /// Perform a default boot even if boot entries exist and fail
687    #[clap(long)]
688    pub default_boot_always_attempt: bool,
689
690    /// Attach a PCI Express root complex to the VM
691    #[clap(long_help = r#"
692Attach root complexes to the VM.
693
694Examples:
695    # Attach root complex rc0 on segment 0 with bus and MMIO ranges
696    --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
697
698Syntax: <name>[,opt=arg,...]
699
700Options:
701    `segment=<value>`              configures the PCI Express segment, default 0
702    `start_bus=<value>`            lowest valid bus number, default 0
703    `end_bus=<value>`              highest valid bus number, default 255
704    `low_mmio=<size>`              low MMIO window size, default 64M
705    `high_mmio=<size>`             high MMIO window size, default 1G
706"#)]
707    #[clap(long, conflicts_with("pcat"))]
708    pub pcie_root_complex: Vec<PcieRootComplexCli>,
709
710    /// Attach a PCI Express root port to the VM
711    #[clap(long_help = r#"
712Attach root ports to root complexes.
713
714Examples:
715    # Attach root port rc0rp0 to root complex rc0
716    --pcie-root-port rc0:rc0rp0
717
718    # Attach root port rc0rp1 to root complex rc0 with hotplug support
719    --pcie-root-port rc0:rc0rp1,hotplug
720
721Syntax: <root_complex_name>:<name>[,hotplug]
722
723Options:
724    `hotplug`                      enable hotplug support for this root port
725"#)]
726    #[clap(long, conflicts_with("pcat"))]
727    pub pcie_root_port: Vec<PcieRootPortCli>,
728
729    /// Attach a PCI Express switch to the VM
730    #[clap(long_help = r#"
731Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
732
733Examples:
734    # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
735    --pcie-switch rp0:switch0,num_downstream_ports=4
736
737    # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
738    --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
739
740    # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
741    --pcie-switch rp0:switch0
742    --pcie-switch switch0-downstream-0:switch1
743    --pcie-switch switch1-downstream-1:switch2
744
745    # Enable hotplug on all downstream switch ports of switch0
746    --pcie-switch rp0:switch0,hotplug
747
748Syntax: <port_name>:<name>[,opt,opt=arg,...]
749
750    port_name can be:
751        - Root port name (e.g., "rp0") to connect directly to a root port
752        - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
753
754Options:
755    `hotplug`                       enable hotplug support for all downstream switch ports
756    `num_downstream_ports=<value>`  number of downstream ports, default 4
757"#)]
758    #[clap(long, conflicts_with("pcat"))]
759    pub pcie_switch: Vec<GenericPcieSwitchCli>,
760
761    /// Attach a PCIe remote device to a downstream port
762    #[clap(long_help = r#"
763Attach PCIe devices to root ports or downstream switch ports
764which are implemented in a simulator running in a remote process.
765
766Examples:
767    # Attach to root port rc0rp0 with default socket
768    --pcie-remote rc0rp0
769
770    # Attach with custom socket address
771    --pcie-remote rc0rp0,socket=0.0.0.0:48914
772
773    # Specify HU and controller identifiers
774    --pcie-remote rc0rp0,hu=1,controller=0
775
776    # Multiple devices on different ports
777    --pcie-remote rc0rp0,socket=0.0.0.0:48914
778    --pcie-remote rc0rp1,socket=0.0.0.0:48915
779
780Syntax: <port_name>[,opt=arg,...]
781
782Options:
783    `socket=<address>`              TCP socket (default: localhost:48914)
784    `hu=<value>`                    Hardware unit identifier (default: 0)
785    `controller=<value>`            Controller identifier (default: 0)
786"#)]
787    #[clap(long, conflicts_with("pcat"))]
788    pub pcie_remote: Vec<PcieRemoteCli>,
789}
790
791#[derive(Clone, Debug, PartialEq)]
792pub struct FsArgs {
793    pub tag: String,
794    pub path: String,
795    pub pcie_port: Option<String>,
796}
797
798impl FromStr for FsArgs {
799    type Err = anyhow::Error;
800
801    fn from_str(s: &str) -> Result<Self, Self::Err> {
802        let (pcie_port, s) = parse_pcie_port_prefix(s);
803        let mut s = s.split(',');
804        let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
805            anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>");
806        };
807        Ok(Self {
808            tag: tag.to_owned(),
809            path: path.to_owned(),
810            pcie_port,
811        })
812    }
813}
814
815#[derive(Clone, Debug, PartialEq)]
816pub struct FsArgsWithOptions {
817    /// The file system tag.
818    pub tag: String,
819    /// The root path.
820    pub path: String,
821    /// The extra options, joined with ';'.
822    pub options: String,
823    /// Optional PCIe port name.
824    pub pcie_port: Option<String>,
825}
826
827impl FromStr for FsArgsWithOptions {
828    type Err = anyhow::Error;
829
830    fn from_str(s: &str) -> Result<Self, Self::Err> {
831        let (pcie_port, s) = parse_pcie_port_prefix(s);
832        let mut s = s.split(',');
833        let (Some(tag), Some(path)) = (s.next(), s.next()) else {
834            anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>[,<options>]");
835        };
836        let options = s.collect::<Vec<_>>().join(";");
837        Ok(Self {
838            tag: tag.to_owned(),
839            path: path.to_owned(),
840            options,
841            pcie_port,
842        })
843    }
844}
845
846#[derive(Copy, Clone, clap::ValueEnum)]
847pub enum VirtioBusCli {
848    Auto,
849    Mmio,
850    Pci,
851    Vpci,
852}
853
854/// Parse an optional `pcie_port=<name>:` prefix from a CLI argument string.
855///
856/// Returns `(Some(port_name), rest)` if the prefix is present, or
857/// `(None, original)` if not.
858fn parse_pcie_port_prefix(s: &str) -> (Option<String>, &str) {
859    if let Some(rest) = s.strip_prefix("pcie_port=") {
860        if let Some((port, rest)) = rest.split_once(':') {
861            if !port.is_empty() {
862                return (Some(port.to_string()), rest);
863            }
864        }
865    }
866    (None, s)
867}
868
869#[derive(Clone, Debug, PartialEq)]
870pub struct VirtioPmemArgs {
871    pub path: String,
872    pub pcie_port: Option<String>,
873}
874
875impl FromStr for VirtioPmemArgs {
876    type Err = anyhow::Error;
877
878    fn from_str(s: &str) -> Result<Self, Self::Err> {
879        let (pcie_port, s) = parse_pcie_port_prefix(s);
880        if s.is_empty() {
881            anyhow::bail!("expected [pcie_port=<port>:]<path>");
882        }
883        Ok(Self {
884            path: s.to_owned(),
885            pcie_port,
886        })
887    }
888}
889
890#[derive(clap::ValueEnum, Clone, Copy)]
891pub enum SecureBootTemplateCli {
892    Windows,
893    UefiCa,
894}
895
896fn parse_memory(s: &str) -> anyhow::Result<u64> {
897    if s == "VMGS_DEFAULT" {
898        Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
899    } else {
900        || -> Option<u64> {
901            let mut b = s.as_bytes();
902            if s.ends_with('B') {
903                b = &b[..b.len() - 1]
904            }
905            if b.is_empty() {
906                return None;
907            }
908            let multi = match b[b.len() - 1] as char {
909                'T' => Some(1024 * 1024 * 1024 * 1024),
910                'G' => Some(1024 * 1024 * 1024),
911                'M' => Some(1024 * 1024),
912                'K' => Some(1024),
913                _ => None,
914            };
915            if multi.is_some() {
916                b = &b[..b.len() - 1]
917            }
918            let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
919            Some(n * multi.unwrap_or(1))
920        }()
921        .with_context(|| format!("invalid memory size '{0}'", s))
922    }
923}
924
925/// Parse a number from a string that could be prefixed with 0x to indicate hex.
926fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
927    match s.strip_prefix("0x") {
928        Some(rest) => u64::from_str_radix(rest, 16),
929        None => s.parse::<u64>(),
930    }
931}
932
933#[derive(Clone, Debug, PartialEq)]
934pub enum DiskCliKind {
935    // mem:<len>
936    Memory(u64),
937    // memdiff:<kind>
938    MemoryDiff(Box<DiskCliKind>),
939    // sql:<path>[;create=<len>]
940    Sqlite {
941        path: PathBuf,
942        create_with_len: Option<u64>,
943    },
944    // sqldiff:<path>[;create]:<kind>
945    SqliteDiff {
946        path: PathBuf,
947        create: bool,
948        disk: Box<DiskCliKind>,
949    },
950    // autocache:[key]:<kind>
951    AutoCacheSqlite {
952        cache_path: String,
953        key: Option<String>,
954        disk: Box<DiskCliKind>,
955    },
956    // prwrap:<kind>
957    PersistentReservationsWrapper(Box<DiskCliKind>),
958    // file:<path>[;create=<len>]
959    File {
960        path: PathBuf,
961        create_with_len: Option<u64>,
962    },
963    // blob:<type>:<url>
964    Blob {
965        kind: BlobKind,
966        url: String,
967    },
968    // crypt:<cipher>:<key_file>:<kind>
969    Crypt {
970        cipher: DiskCipher,
971        key_file: PathBuf,
972        disk: Box<DiskCliKind>,
973    },
974    // delay:<delay_ms>:<kind>
975    DelayDiskWrapper {
976        delay_ms: u64,
977        disk: Box<DiskCliKind>,
978    },
979}
980
981#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
982pub enum DiskCipher {
983    #[clap(name = "xts-aes-256")]
984    XtsAes256,
985}
986
987#[derive(Copy, Clone, Debug, PartialEq)]
988pub enum BlobKind {
989    Flat,
990    Vhd1,
991}
992
993fn parse_path_and_len(arg: &str) -> anyhow::Result<(PathBuf, Option<u64>)> {
994    Ok(match arg.split_once(';') {
995        Some((path, len)) => {
996            let Some(len) = len.strip_prefix("create=") else {
997                anyhow::bail!("invalid syntax after ';', expected 'create=<len>'")
998            };
999
1000            let len = parse_memory(len)?;
1001
1002            (path.into(), Some(len))
1003        }
1004        None => (arg.into(), None),
1005    })
1006}
1007
1008impl FromStr for DiskCliKind {
1009    type Err = anyhow::Error;
1010
1011    fn from_str(s: &str) -> anyhow::Result<Self> {
1012        let disk = match s.split_once(':') {
1013            // convenience support for passing bare paths as file disks
1014            None => {
1015                let (path, create_with_len) = parse_path_and_len(s)?;
1016                DiskCliKind::File {
1017                    path,
1018                    create_with_len,
1019                }
1020            }
1021            Some((kind, arg)) => match kind {
1022                "mem" => DiskCliKind::Memory(parse_memory(arg)?),
1023                "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
1024                "sql" => {
1025                    let (path, create_with_len) = parse_path_and_len(arg)?;
1026                    DiskCliKind::Sqlite {
1027                        path,
1028                        create_with_len,
1029                    }
1030                }
1031                "sqldiff" => {
1032                    let (path_and_opts, kind) =
1033                        arg.split_once(':').context("expected path[;opts]:kind")?;
1034                    let disk = Box::new(kind.parse()?);
1035                    match path_and_opts.split_once(';') {
1036                        Some((path, create)) => {
1037                            if create != "create" {
1038                                anyhow::bail!("invalid syntax after ';', expected 'create'")
1039                            }
1040                            DiskCliKind::SqliteDiff {
1041                                path: path.into(),
1042                                create: true,
1043                                disk,
1044                            }
1045                        }
1046                        None => DiskCliKind::SqliteDiff {
1047                            path: path_and_opts.into(),
1048                            create: false,
1049                            disk,
1050                        },
1051                    }
1052                }
1053                "autocache" => {
1054                    let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
1055                    let cache_path = std::env::var("OPENVMM_AUTO_CACHE_PATH")
1056                        .context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
1057                    DiskCliKind::AutoCacheSqlite {
1058                        cache_path,
1059                        key: (!key.is_empty()).then(|| key.to_string()),
1060                        disk: Box::new(kind.parse()?),
1061                    }
1062                }
1063                "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
1064                "file" => {
1065                    let (path, create_with_len) = parse_path_and_len(arg)?;
1066                    DiskCliKind::File {
1067                        path,
1068                        create_with_len,
1069                    }
1070                }
1071                "blob" => {
1072                    let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
1073                    let blob_kind = match blob_kind {
1074                        "flat" => BlobKind::Flat,
1075                        "vhd1" => BlobKind::Vhd1,
1076                        _ => anyhow::bail!("unknown blob kind {blob_kind}"),
1077                    };
1078                    DiskCliKind::Blob {
1079                        kind: blob_kind,
1080                        url: url.to_string(),
1081                    }
1082                }
1083                "crypt" => {
1084                    let (cipher, (key, kind)) = arg
1085                        .split_once(':')
1086                        .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
1087                        .context("expected cipher:key_file:kind")?;
1088                    DiskCliKind::Crypt {
1089                        cipher: ValueEnum::from_str(cipher, false)
1090                            .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
1091                        key_file: PathBuf::from(key),
1092                        disk: Box::new(kind.parse()?),
1093                    }
1094                }
1095                kind => {
1096                    // here's a fun edge case: what if the user passes `--disk d:\path\to\disk.img`?
1097                    //
1098                    // in this case, we actually want to treat that leading `d:` as part of the
1099                    // path, rather than as a disk with `kind == 'd'`
1100                    let (path, create_with_len) = parse_path_and_len(s)?;
1101                    if path.has_root() {
1102                        DiskCliKind::File {
1103                            path,
1104                            create_with_len,
1105                        }
1106                    } else {
1107                        anyhow::bail!("invalid disk kind {kind}");
1108                    }
1109                }
1110            },
1111        };
1112        Ok(disk)
1113    }
1114}
1115
1116#[derive(Clone)]
1117pub struct VmgsCli {
1118    pub kind: DiskCliKind,
1119    pub provision: ProvisionVmgs,
1120}
1121
1122#[derive(Copy, Clone)]
1123pub enum ProvisionVmgs {
1124    OnEmpty,
1125    OnFailure,
1126    True,
1127}
1128
1129impl FromStr for VmgsCli {
1130    type Err = anyhow::Error;
1131
1132    fn from_str(s: &str) -> anyhow::Result<Self> {
1133        let (kind, opt) = s
1134            .split_once(',')
1135            .map(|(k, o)| (k, Some(o)))
1136            .unwrap_or((s, None));
1137        let kind = kind.parse()?;
1138
1139        let provision = match opt {
1140            None => ProvisionVmgs::OnEmpty,
1141            Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
1142            Some("fmt") => ProvisionVmgs::True,
1143            Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
1144        };
1145
1146        Ok(VmgsCli { kind, provision })
1147    }
1148}
1149
1150// <kind>[,ro]
1151#[derive(Clone)]
1152pub struct DiskCli {
1153    pub vtl: DeviceVtl,
1154    pub kind: DiskCliKind,
1155    pub read_only: bool,
1156    pub is_dvd: bool,
1157    pub underhill: Option<UnderhillDiskSource>,
1158    pub pcie_port: Option<String>,
1159}
1160
1161#[derive(Copy, Clone)]
1162pub enum UnderhillDiskSource {
1163    Scsi,
1164    Nvme,
1165}
1166
1167impl FromStr for DiskCli {
1168    type Err = anyhow::Error;
1169
1170    fn from_str(s: &str) -> anyhow::Result<Self> {
1171        let mut opts = s.split(',');
1172        let kind = opts.next().unwrap().parse()?;
1173
1174        let mut read_only = false;
1175        let mut is_dvd = false;
1176        let mut underhill = None;
1177        let mut vtl = DeviceVtl::Vtl0;
1178        let mut pcie_port = None;
1179        for opt in opts {
1180            let mut s = opt.split('=');
1181            let opt = s.next().unwrap();
1182            match opt {
1183                "ro" => read_only = true,
1184                "dvd" => {
1185                    is_dvd = true;
1186                    read_only = true;
1187                }
1188                "vtl2" => {
1189                    vtl = DeviceVtl::Vtl2;
1190                }
1191                "uh" => underhill = Some(UnderhillDiskSource::Scsi),
1192                "uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
1193                "pcie_port" => {
1194                    let port = s.next();
1195                    if port.is_none_or(|p| p.is_empty()) {
1196                        anyhow::bail!("`pcie_port` requires a port name");
1197                    }
1198                    pcie_port = Some(String::from(port.unwrap()));
1199                }
1200                opt => anyhow::bail!("unknown option: '{opt}'"),
1201            }
1202        }
1203
1204        if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
1205            anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
1206        }
1207
1208        if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
1209            anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
1210        }
1211
1212        Ok(DiskCli {
1213            vtl,
1214            kind,
1215            read_only,
1216            is_dvd,
1217            underhill,
1218            pcie_port,
1219        })
1220    }
1221}
1222
1223// <kind>[,ro,s]
1224#[derive(Clone)]
1225pub struct IdeDiskCli {
1226    pub kind: DiskCliKind,
1227    pub read_only: bool,
1228    pub channel: Option<u8>,
1229    pub device: Option<u8>,
1230    pub is_dvd: bool,
1231}
1232
1233impl FromStr for IdeDiskCli {
1234    type Err = anyhow::Error;
1235
1236    fn from_str(s: &str) -> anyhow::Result<Self> {
1237        let mut opts = s.split(',');
1238        let kind = opts.next().unwrap().parse()?;
1239
1240        let mut read_only = false;
1241        let mut channel = None;
1242        let mut device = None;
1243        let mut is_dvd = false;
1244        for opt in opts {
1245            let mut s = opt.split('=');
1246            let opt = s.next().unwrap();
1247            match opt {
1248                "ro" => read_only = true,
1249                "p" => channel = Some(0),
1250                "s" => channel = Some(1),
1251                "0" => device = Some(0),
1252                "1" => device = Some(1),
1253                "dvd" => {
1254                    is_dvd = true;
1255                    read_only = true;
1256                }
1257                _ => anyhow::bail!("unknown option: '{opt}'"),
1258            }
1259        }
1260
1261        Ok(IdeDiskCli {
1262            kind,
1263            read_only,
1264            channel,
1265            device,
1266            is_dvd,
1267        })
1268    }
1269}
1270
1271// <kind>[,ro]
1272#[derive(Clone, Debug, PartialEq)]
1273pub struct FloppyDiskCli {
1274    pub kind: DiskCliKind,
1275    pub read_only: bool,
1276}
1277
1278impl FromStr for FloppyDiskCli {
1279    type Err = anyhow::Error;
1280
1281    fn from_str(s: &str) -> anyhow::Result<Self> {
1282        if s.is_empty() {
1283            anyhow::bail!("empty disk spec");
1284        }
1285        let mut opts = s.split(',');
1286        let kind = opts.next().unwrap().parse()?;
1287
1288        let mut read_only = false;
1289        for opt in opts {
1290            let mut s = opt.split('=');
1291            let opt = s.next().unwrap();
1292            match opt {
1293                "ro" => read_only = true,
1294                _ => anyhow::bail!("unknown option: '{opt}'"),
1295            }
1296        }
1297
1298        Ok(FloppyDiskCli { kind, read_only })
1299    }
1300}
1301
1302#[derive(Clone)]
1303pub struct DebugconSerialConfigCli {
1304    pub port: u16,
1305    pub serial: SerialConfigCli,
1306}
1307
1308impl FromStr for DebugconSerialConfigCli {
1309    type Err = String;
1310
1311    fn from_str(s: &str) -> Result<Self, Self::Err> {
1312        let Some((port, serial)) = s.split_once(',') else {
1313            return Err("invalid format (missing comma between port and serial)".into());
1314        };
1315
1316        let port: u16 = parse_number(port)
1317            .map_err(|_| "could not parse port".to_owned())?
1318            .try_into()
1319            .map_err(|_| "port must be 16-bit")?;
1320        let serial: SerialConfigCli = serial.parse()?;
1321
1322        Ok(Self { port, serial })
1323    }
1324}
1325
1326/// (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
1327#[derive(Clone, Debug, PartialEq)]
1328pub enum SerialConfigCli {
1329    None,
1330    Console,
1331    NewConsole(Option<PathBuf>, Option<String>),
1332    Stderr,
1333    Pipe(PathBuf),
1334    Tcp(SocketAddr),
1335    File(PathBuf),
1336}
1337
1338impl FromStr for SerialConfigCli {
1339    type Err = String;
1340
1341    fn from_str(s: &str) -> Result<Self, Self::Err> {
1342        let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
1343
1344        let first_key = match keyvalues.first() {
1345            Some(first_pair) => first_pair.0.as_str(),
1346            None => Err("invalid serial configuration: no values supplied")?,
1347        };
1348        let first_value = keyvalues.first().unwrap().1.as_ref();
1349
1350        let ret = match first_key {
1351            "none" => SerialConfigCli::None,
1352            "console" => SerialConfigCli::Console,
1353            "stderr" => SerialConfigCli::Stderr,
1354            "file" => match first_value {
1355                Some(path) => SerialConfigCli::File(path.into()),
1356                None => Err("invalid serial configuration: file requires a value")?,
1357            },
1358            "term" => {
1359                // If user supplies a name key, use it to title the window
1360                let window_name = keyvalues.iter().find(|(key, _)| key == "name");
1361                let window_name = match window_name {
1362                    Some((_, Some(name))) => Some(name.clone()),
1363                    _ => None,
1364                };
1365
1366                SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
1367            }
1368            "listen" => match first_value {
1369                Some(path) => {
1370                    if let Some(tcp) = path.strip_prefix("tcp:") {
1371                        let addr = tcp
1372                            .parse()
1373                            .map_err(|err| format!("invalid tcp address: {err}"))?;
1374                        SerialConfigCli::Tcp(addr)
1375                    } else {
1376                        SerialConfigCli::Pipe(path.into())
1377                    }
1378                }
1379                None => Err(
1380                    "invalid serial configuration: listen requires a value of tcp:addr or pipe",
1381                )?,
1382            },
1383            _ => {
1384                return Err(format!(
1385                    "invalid serial configuration: '{}' is not a known option",
1386                    first_key
1387                ));
1388            }
1389        };
1390
1391        Ok(ret)
1392    }
1393}
1394
1395impl SerialConfigCli {
1396    /// Parse a comma separated list of key=value options into a vector of
1397    /// key/value pairs.
1398    fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1399        let mut ret = Vec::new();
1400
1401        // For each comma separated item in the supplied list
1402        for item in s.split(',') {
1403            // Split on the = for key and value
1404            // If no = is found, treat key as key and value as None
1405            let mut eqsplit = item.split('=');
1406            let key = eqsplit.next();
1407            let value = eqsplit.next();
1408
1409            if let Some(key) = key {
1410                ret.push((key.to_owned(), value.map(|x| x.to_owned())));
1411            } else {
1412                // An empty key is invalid
1413                return Err("invalid key=value pair in serial config".into());
1414            }
1415        }
1416        Ok(ret)
1417    }
1418}
1419
1420#[derive(Clone, Debug, PartialEq)]
1421pub enum EndpointConfigCli {
1422    None,
1423    Consomme { cidr: Option<String> },
1424    Dio { id: Option<String> },
1425    Tap { name: String },
1426}
1427
1428impl FromStr for EndpointConfigCli {
1429    type Err = String;
1430
1431    fn from_str(s: &str) -> Result<Self, Self::Err> {
1432        let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
1433            ["none"] => EndpointConfigCli::None,
1434            ["consomme", s @ ..] => EndpointConfigCli::Consomme {
1435                cidr: s.first().map(|&s| s.to_owned()),
1436            },
1437            ["dio", s @ ..] => EndpointConfigCli::Dio {
1438                id: s.first().map(|s| (*s).to_owned()),
1439            },
1440            ["tap", name] => EndpointConfigCli::Tap {
1441                name: (*name).to_owned(),
1442            },
1443            _ => return Err("invalid network backend".into()),
1444        };
1445
1446        Ok(ret)
1447    }
1448}
1449
1450#[derive(Clone, Debug, PartialEq)]
1451pub struct NicConfigCli {
1452    pub vtl: DeviceVtl,
1453    pub endpoint: EndpointConfigCli,
1454    pub max_queues: Option<u16>,
1455    pub underhill: bool,
1456    pub pcie_port: Option<String>,
1457}
1458
1459impl FromStr for NicConfigCli {
1460    type Err = String;
1461
1462    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
1463        let mut vtl = DeviceVtl::Vtl0;
1464        let mut max_queues = None;
1465        let mut underhill = false;
1466        let mut pcie_port = None;
1467        while let Some((opt, rest)) = s.split_once(':') {
1468            if let Some((opt, val)) = opt.split_once('=') {
1469                match opt {
1470                    "queues" => {
1471                        max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
1472                    }
1473                    "pcie_port" => {
1474                        if val.is_empty() {
1475                            return Err("`pcie_port=` requires port name argument".into());
1476                        }
1477                        pcie_port = Some(val.to_string());
1478                    }
1479                    _ => break,
1480                }
1481            } else {
1482                match opt {
1483                    "vtl2" => {
1484                        vtl = DeviceVtl::Vtl2;
1485                    }
1486                    "uh" => underhill = true,
1487                    _ => break,
1488                }
1489            }
1490            s = rest;
1491        }
1492
1493        if underhill && vtl != DeviceVtl::Vtl0 {
1494            return Err("`uh` is incompatible with `vtl2`".into());
1495        }
1496
1497        if pcie_port.is_some() && (underhill || vtl != DeviceVtl::Vtl0) {
1498            return Err("`pcie_port` is incompatible with `uh` and `vtl2`".into());
1499        }
1500
1501        let endpoint = s.parse()?;
1502        Ok(NicConfigCli {
1503            vtl,
1504            endpoint,
1505            max_queues,
1506            underhill,
1507            pcie_port,
1508        })
1509    }
1510}
1511
1512#[derive(Debug, Error)]
1513#[error("unknown VTL2 relocation type: {0}")]
1514pub struct UnknownVtl2RelocationType(String);
1515
1516fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
1517    match s {
1518        "disable" => Ok(Vtl2BaseAddressType::File),
1519        s if s.starts_with("auto=") => {
1520            let s = s.strip_prefix("auto=").unwrap_or_default();
1521            let size = if s == "filesize" {
1522                None
1523            } else {
1524                let size = parse_memory(s).map_err(|e| {
1525                    UnknownVtl2RelocationType(format!(
1526                        "unable to parse memory size from {} for 'auto=' type, {e}",
1527                        e
1528                    ))
1529                })?;
1530                Some(size)
1531            };
1532            Ok(Vtl2BaseAddressType::MemoryLayout { size })
1533        }
1534        s if s.starts_with("absolute=") => {
1535            let s = s.strip_prefix("absolute=");
1536            let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
1537                UnknownVtl2RelocationType(format!(
1538                    "unable to parse number from {} for 'absolute=' type",
1539                    e
1540                ))
1541            })?;
1542            Ok(Vtl2BaseAddressType::Absolute(addr))
1543        }
1544        s if s.starts_with("vtl2=") => {
1545            let s = s.strip_prefix("vtl2=").unwrap_or_default();
1546            let size = if s == "filesize" {
1547                None
1548            } else {
1549                let size = parse_memory(s).map_err(|e| {
1550                    UnknownVtl2RelocationType(format!(
1551                        "unable to parse memory size from {} for 'vtl2=' type, {e}",
1552                        e
1553                    ))
1554                })?;
1555                Some(size)
1556            };
1557            Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
1558        }
1559        _ => Err(UnknownVtl2RelocationType(s.to_owned())),
1560    }
1561}
1562
1563#[derive(Debug, Copy, Clone, PartialEq)]
1564pub enum SmtConfigCli {
1565    Auto,
1566    Force,
1567    Off,
1568}
1569
1570#[derive(Debug, Error)]
1571#[error("expected auto, force, or off")]
1572pub struct BadSmtConfig;
1573
1574impl FromStr for SmtConfigCli {
1575    type Err = BadSmtConfig;
1576
1577    fn from_str(s: &str) -> Result<Self, Self::Err> {
1578        let r = match s {
1579            "auto" => Self::Auto,
1580            "force" => Self::Force,
1581            "off" => Self::Off,
1582            _ => return Err(BadSmtConfig),
1583        };
1584        Ok(r)
1585    }
1586}
1587
1588#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
1589fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
1590    let r = match s {
1591        "auto" => X2ApicConfig::Auto,
1592        "supported" => X2ApicConfig::Supported,
1593        "off" => X2ApicConfig::Unsupported,
1594        "on" => X2ApicConfig::Enabled,
1595        _ => return Err("expected auto, supported, off, or on"),
1596    };
1597    Ok(r)
1598}
1599
1600#[derive(Debug, Copy, Clone, ValueEnum)]
1601pub enum Vtl0LateMapPolicyCli {
1602    Off,
1603    Log,
1604    Halt,
1605    Exception,
1606}
1607
1608#[derive(Debug, Copy, Clone, ValueEnum)]
1609pub enum IsolationCli {
1610    Vbs,
1611}
1612
1613#[derive(Debug, Copy, Clone, PartialEq)]
1614pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
1615
1616impl FromStr for PcatBootOrderCli {
1617    type Err = &'static str;
1618
1619    fn from_str(s: &str) -> Result<Self, Self::Err> {
1620        let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
1621        let mut order = Vec::new();
1622
1623        for item in s.split(',') {
1624            let device = match item {
1625                "optical" => PcatBootDevice::Optical,
1626                "hdd" => PcatBootDevice::HardDrive,
1627                "net" => PcatBootDevice::Network,
1628                "floppy" => PcatBootDevice::Floppy,
1629                _ => return Err("unknown boot device type"),
1630            };
1631
1632            let default_pos = default_order
1633                .iter()
1634                .position(|x| x == &Some(device))
1635                .ok_or("cannot pass duplicate boot devices")?;
1636
1637            order.push(default_order[default_pos].take().unwrap());
1638        }
1639
1640        order.extend(default_order.into_iter().flatten());
1641        assert_eq!(order.len(), 4);
1642
1643        Ok(Self(order.try_into().unwrap()))
1644    }
1645}
1646
1647#[derive(Copy, Clone, Debug, ValueEnum)]
1648pub enum UefiConsoleModeCli {
1649    Default,
1650    Com1,
1651    Com2,
1652    None,
1653}
1654
1655#[derive(Copy, Clone, Debug, Default, ValueEnum)]
1656pub enum EfiDiagnosticsLogLevelCli {
1657    #[default]
1658    Default,
1659    Info,
1660    Full,
1661}
1662
1663#[derive(Clone, Debug, PartialEq)]
1664pub struct PcieRootComplexCli {
1665    pub name: String,
1666    pub segment: u16,
1667    pub start_bus: u8,
1668    pub end_bus: u8,
1669    pub low_mmio: u32,
1670    pub high_mmio: u64,
1671}
1672
1673impl FromStr for PcieRootComplexCli {
1674    type Err = anyhow::Error;
1675
1676    fn from_str(s: &str) -> Result<Self, Self::Err> {
1677        const DEFAULT_PCIE_CRS_LOW_SIZE: u32 = 64 * 1024 * 1024; // 64M
1678        const DEFAULT_PCIE_CRS_HIGH_SIZE: u64 = 1024 * 1024 * 1024; // 1G
1679
1680        let mut opts = s.split(',');
1681        let name = opts.next().context("expected root complex name")?;
1682        if name.is_empty() {
1683            anyhow::bail!("must provide a root complex name");
1684        }
1685
1686        let mut segment = 0;
1687        let mut start_bus = 0;
1688        let mut end_bus = 255;
1689        let mut low_mmio = DEFAULT_PCIE_CRS_LOW_SIZE;
1690        let mut high_mmio = DEFAULT_PCIE_CRS_HIGH_SIZE;
1691        for opt in opts {
1692            let mut s = opt.split('=');
1693            let opt = s.next().context("expected option")?;
1694            match opt {
1695                "segment" => {
1696                    let seg_str = s.next().context("expected segment number")?;
1697                    segment = u16::from_str(seg_str).context("failed to parse segment number")?;
1698                }
1699                "start_bus" => {
1700                    let bus_str = s.next().context("expected start bus number")?;
1701                    start_bus =
1702                        u8::from_str(bus_str).context("failed to parse start bus number")?;
1703                }
1704                "end_bus" => {
1705                    let bus_str = s.next().context("expected end bus number")?;
1706                    end_bus = u8::from_str(bus_str).context("failed to parse end bus number")?;
1707                }
1708                "low_mmio" => {
1709                    let low_mmio_str = s.next().context("expected low MMIO size")?;
1710                    low_mmio = parse_memory(low_mmio_str)
1711                        .context("failed to parse low MMIO size")?
1712                        .try_into()?;
1713                }
1714                "high_mmio" => {
1715                    let high_mmio_str = s.next().context("expected high MMIO size")?;
1716                    high_mmio =
1717                        parse_memory(high_mmio_str).context("failed to parse high MMIO size")?;
1718                }
1719                opt => anyhow::bail!("unknown option: '{opt}'"),
1720            }
1721        }
1722
1723        if start_bus >= end_bus {
1724            anyhow::bail!("start_bus must be less than or equal to end_bus");
1725        }
1726
1727        Ok(PcieRootComplexCli {
1728            name: name.to_string(),
1729            segment,
1730            start_bus,
1731            end_bus,
1732            low_mmio,
1733            high_mmio,
1734        })
1735    }
1736}
1737
1738#[derive(Clone, Debug, PartialEq)]
1739pub struct PcieRootPortCli {
1740    pub root_complex_name: String,
1741    pub name: String,
1742    pub hotplug: bool,
1743}
1744
1745impl FromStr for PcieRootPortCli {
1746    type Err = anyhow::Error;
1747
1748    fn from_str(s: &str) -> Result<Self, Self::Err> {
1749        let mut opts = s.split(',');
1750        let names = opts.next().context("expected root port identifiers")?;
1751        if names.is_empty() {
1752            anyhow::bail!("must provide root port identifiers");
1753        }
1754
1755        let mut s = names.split(':');
1756        let rc_name = s.next().context("expected name of parent root complex")?;
1757        let rp_name = s.next().context("expected root port name")?;
1758
1759        if let Some(extra) = s.next() {
1760            anyhow::bail!("unexpected token: '{extra}'")
1761        }
1762
1763        let mut hotplug = false;
1764
1765        // Parse optional flags
1766        for opt in opts {
1767            match opt {
1768                "hotplug" => hotplug = true,
1769                _ => anyhow::bail!("unexpected option: '{opt}'"),
1770            }
1771        }
1772
1773        Ok(PcieRootPortCli {
1774            root_complex_name: rc_name.to_string(),
1775            name: rp_name.to_string(),
1776            hotplug,
1777        })
1778    }
1779}
1780
1781#[derive(Clone, Debug, PartialEq)]
1782pub struct GenericPcieSwitchCli {
1783    pub port_name: String,
1784    pub name: String,
1785    pub num_downstream_ports: u8,
1786    pub hotplug: bool,
1787}
1788
1789impl FromStr for GenericPcieSwitchCli {
1790    type Err = anyhow::Error;
1791
1792    fn from_str(s: &str) -> Result<Self, Self::Err> {
1793        let mut opts = s.split(',');
1794        let names = opts.next().context("expected switch identifiers")?;
1795        if names.is_empty() {
1796            anyhow::bail!("must provide switch identifiers");
1797        }
1798
1799        let mut s = names.split(':');
1800        let port_name = s.next().context("expected name of parent port")?;
1801        let switch_name = s.next().context("expected switch name")?;
1802
1803        if let Some(extra) = s.next() {
1804            anyhow::bail!("unexpected token: '{extra}'")
1805        }
1806
1807        let mut num_downstream_ports = 4u8; // Default value
1808        let mut hotplug = false;
1809
1810        for opt in opts {
1811            let mut kv = opt.split('=');
1812            let key = kv.next().context("expected option name")?;
1813
1814            match key {
1815                "num_downstream_ports" => {
1816                    let value = kv.next().context("expected option value")?;
1817                    if let Some(extra) = kv.next() {
1818                        anyhow::bail!("unexpected token: '{extra}'")
1819                    }
1820                    num_downstream_ports = value.parse().context("invalid num_downstream_ports")?;
1821                }
1822                "hotplug" => {
1823                    if kv.next().is_some() {
1824                        anyhow::bail!("hotplug option does not take a value")
1825                    }
1826                    hotplug = true;
1827                }
1828                _ => anyhow::bail!("unknown option: '{key}'"),
1829            }
1830        }
1831
1832        Ok(GenericPcieSwitchCli {
1833            port_name: port_name.to_string(),
1834            name: switch_name.to_string(),
1835            num_downstream_ports,
1836            hotplug,
1837        })
1838    }
1839}
1840
1841/// CLI configuration for a PCIe remote device.
1842#[derive(Clone, Debug, PartialEq)]
1843pub struct PcieRemoteCli {
1844    /// Name of the PCIe downstream port to attach to.
1845    pub port_name: String,
1846    /// TCP socket address for the remote simulator.
1847    pub socket_addr: Option<String>,
1848    /// Hardware unit identifier for plug request.
1849    pub hu: u16,
1850    /// Controller identifier for plug request.
1851    pub controller: u16,
1852}
1853
1854impl FromStr for PcieRemoteCli {
1855    type Err = anyhow::Error;
1856
1857    fn from_str(s: &str) -> Result<Self, Self::Err> {
1858        let mut opts = s.split(',');
1859        let port_name = opts.next().context("expected port name")?;
1860        if port_name.is_empty() {
1861            anyhow::bail!("must provide a port name");
1862        }
1863
1864        let mut socket_addr = None;
1865        let mut hu = 0u16;
1866        let mut controller = 0u16;
1867
1868        for opt in opts {
1869            let mut kv = opt.split('=');
1870            let key = kv.next().context("expected option name")?;
1871            let value = kv.next();
1872
1873            match key {
1874                "socket" => {
1875                    let addr = value.context("socket requires an address")?;
1876                    if let Some(extra) = kv.next() {
1877                        anyhow::bail!("unexpected token: '{extra}'")
1878                    }
1879                    if addr.is_empty() {
1880                        anyhow::bail!("socket address cannot be empty");
1881                    }
1882                    socket_addr = Some(addr.to_string());
1883                }
1884                "hu" => {
1885                    let val = value.context("hu requires a value")?;
1886                    if let Some(extra) = kv.next() {
1887                        anyhow::bail!("unexpected token: '{extra}'")
1888                    }
1889                    hu = val.parse().context("failed to parse hu")?;
1890                }
1891                "controller" => {
1892                    let val = value.context("controller requires a value")?;
1893                    if let Some(extra) = kv.next() {
1894                        anyhow::bail!("unexpected token: '{extra}'")
1895                    }
1896                    controller = val.parse().context("failed to parse controller")?;
1897                }
1898                _ => anyhow::bail!("unknown option: '{key}'"),
1899            }
1900        }
1901
1902        Ok(PcieRemoteCli {
1903            port_name: port_name.to_string(),
1904            socket_addr,
1905            hu,
1906            controller,
1907        })
1908    }
1909}
1910
1911/// Read a environment variable that may / may-not have a target-specific
1912/// prefix. e.g: `default_value_from_arch_env("FOO")` would first try and read
1913/// from `FOO`, and if that's not found, it will try `X86_64_FOO`.
1914///
1915/// Must return an `OsString`, in order to be compatible with `clap`'s
1916/// default_value code. As such - to encode the absence of the env-var, an empty
1917/// OsString is returned.
1918fn default_value_from_arch_env(name: &str) -> OsString {
1919    let prefix = if cfg!(guest_arch = "x86_64") {
1920        "X86_64"
1921    } else if cfg!(guest_arch = "aarch64") {
1922        "AARCH64"
1923    } else {
1924        return Default::default();
1925    };
1926    let prefixed = format!("{}_{}", prefix, name);
1927    std::env::var_os(name)
1928        .or_else(|| std::env::var_os(prefixed))
1929        .unwrap_or_default()
1930}
1931
1932/// Workaround to use `Option<PathBuf>` alongside [`default_value_from_arch_env`]
1933#[derive(Clone)]
1934pub struct OptionalPathBuf(pub Option<PathBuf>);
1935
1936impl From<&std::ffi::OsStr> for OptionalPathBuf {
1937    fn from(s: &std::ffi::OsStr) -> Self {
1938        OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
1939    }
1940}
1941
1942#[cfg(test)]
1943// UNSAFETY: Needed to set and remove environment variables in tests
1944#[expect(unsafe_code)]
1945mod tests {
1946    use super::*;
1947
1948    fn with_env_var<F, R>(name: &str, value: &str, f: F) -> R
1949    where
1950        F: FnOnce() -> R,
1951    {
1952        // SAFETY:
1953        // Safe in a testing context because it won't be changed concurrently
1954        unsafe {
1955            std::env::set_var(name, value);
1956        }
1957        let result = f();
1958        // SAFETY:
1959        // Safe in a testing context because it won't be changed concurrently
1960        unsafe {
1961            std::env::remove_var(name);
1962        }
1963        result
1964    }
1965
1966    #[test]
1967    fn test_parse_file_disk_with_create() {
1968        let s = "file:test.vhd;create=1G";
1969        let disk = DiskCliKind::from_str(s).unwrap();
1970
1971        match disk {
1972            DiskCliKind::File {
1973                path,
1974                create_with_len,
1975            } => {
1976                assert_eq!(path, PathBuf::from("test.vhd"));
1977                assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); // 1G
1978            }
1979            _ => panic!("Expected File variant"),
1980        }
1981    }
1982
1983    #[test]
1984    fn test_parse_direct_file_with_create() {
1985        let s = "test.vhd;create=1G";
1986        let disk = DiskCliKind::from_str(s).unwrap();
1987
1988        match disk {
1989            DiskCliKind::File {
1990                path,
1991                create_with_len,
1992            } => {
1993                assert_eq!(path, PathBuf::from("test.vhd"));
1994                assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); // 1G
1995            }
1996            _ => panic!("Expected File variant"),
1997        }
1998    }
1999
2000    #[test]
2001    fn test_parse_memory_disk() {
2002        let s = "mem:1G";
2003        let disk = DiskCliKind::from_str(s).unwrap();
2004        match disk {
2005            DiskCliKind::Memory(size) => {
2006                assert_eq!(size, 1024 * 1024 * 1024); // 1G
2007            }
2008            _ => panic!("Expected Memory variant"),
2009        }
2010    }
2011
2012    #[test]
2013    fn test_parse_pcie_disk() {
2014        assert_eq!(
2015            DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
2016            Some("p0".to_string())
2017        );
2018        assert_eq!(
2019            DiskCli::from_str("file:path.vhdx,pcie_port=p0")
2020                .unwrap()
2021                .pcie_port,
2022            Some("p0".to_string())
2023        );
2024        assert_eq!(
2025            DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
2026                .unwrap()
2027                .pcie_port,
2028            Some("p0".to_string())
2029        );
2030
2031        // Missing port name
2032        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
2033
2034        // Incompatible with various other disk fields
2035        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
2036        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
2037        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
2038    }
2039
2040    #[test]
2041    fn test_parse_memory_diff_disk() {
2042        let s = "memdiff:file:base.img";
2043        let disk = DiskCliKind::from_str(s).unwrap();
2044        match disk {
2045            DiskCliKind::MemoryDiff(inner) => match *inner {
2046                DiskCliKind::File {
2047                    path,
2048                    create_with_len,
2049                } => {
2050                    assert_eq!(path, PathBuf::from("base.img"));
2051                    assert_eq!(create_with_len, None);
2052                }
2053                _ => panic!("Expected File variant inside MemoryDiff"),
2054            },
2055            _ => panic!("Expected MemoryDiff variant"),
2056        }
2057    }
2058
2059    #[test]
2060    fn test_parse_sqlite_disk() {
2061        let s = "sql:db.sqlite;create=2G";
2062        let disk = DiskCliKind::from_str(s).unwrap();
2063        match disk {
2064            DiskCliKind::Sqlite {
2065                path,
2066                create_with_len,
2067            } => {
2068                assert_eq!(path, PathBuf::from("db.sqlite"));
2069                assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
2070            }
2071            _ => panic!("Expected Sqlite variant"),
2072        }
2073
2074        // Test without create option
2075        let s = "sql:db.sqlite";
2076        let disk = DiskCliKind::from_str(s).unwrap();
2077        match disk {
2078            DiskCliKind::Sqlite {
2079                path,
2080                create_with_len,
2081            } => {
2082                assert_eq!(path, PathBuf::from("db.sqlite"));
2083                assert_eq!(create_with_len, None);
2084            }
2085            _ => panic!("Expected Sqlite variant"),
2086        }
2087    }
2088
2089    #[test]
2090    fn test_parse_sqlite_diff_disk() {
2091        // Test with create option
2092        let s = "sqldiff:diff.sqlite;create:file:base.img";
2093        let disk = DiskCliKind::from_str(s).unwrap();
2094        match disk {
2095            DiskCliKind::SqliteDiff { path, create, disk } => {
2096                assert_eq!(path, PathBuf::from("diff.sqlite"));
2097                assert!(create);
2098                match *disk {
2099                    DiskCliKind::File {
2100                        path,
2101                        create_with_len,
2102                    } => {
2103                        assert_eq!(path, PathBuf::from("base.img"));
2104                        assert_eq!(create_with_len, None);
2105                    }
2106                    _ => panic!("Expected File variant inside SqliteDiff"),
2107                }
2108            }
2109            _ => panic!("Expected SqliteDiff variant"),
2110        }
2111
2112        // Test without create option
2113        let s = "sqldiff:diff.sqlite:file:base.img";
2114        let disk = DiskCliKind::from_str(s).unwrap();
2115        match disk {
2116            DiskCliKind::SqliteDiff { path, create, disk } => {
2117                assert_eq!(path, PathBuf::from("diff.sqlite"));
2118                assert!(!create);
2119                match *disk {
2120                    DiskCliKind::File {
2121                        path,
2122                        create_with_len,
2123                    } => {
2124                        assert_eq!(path, PathBuf::from("base.img"));
2125                        assert_eq!(create_with_len, None);
2126                    }
2127                    _ => panic!("Expected File variant inside SqliteDiff"),
2128                }
2129            }
2130            _ => panic!("Expected SqliteDiff variant"),
2131        }
2132    }
2133
2134    #[test]
2135    fn test_parse_autocache_sqlite_disk() {
2136        // Test with environment variable set
2137        let disk = with_env_var("OPENVMM_AUTO_CACHE_PATH", "/tmp/cache", || {
2138            DiskCliKind::from_str("autocache::file:disk.vhd").unwrap()
2139        });
2140        assert!(matches!(
2141            disk,
2142            DiskCliKind::AutoCacheSqlite {
2143                cache_path,
2144                key,
2145                disk: _disk,
2146            } if cache_path == "/tmp/cache" && key.is_none()
2147        ));
2148
2149        // Test without environment variable
2150        assert!(DiskCliKind::from_str("autocache::file:disk.vhd").is_err());
2151    }
2152
2153    #[test]
2154    fn test_parse_disk_errors() {
2155        assert!(DiskCliKind::from_str("invalid:").is_err());
2156        assert!(DiskCliKind::from_str("memory:extra").is_err());
2157
2158        // Test sqlite: without environment variable
2159        assert!(DiskCliKind::from_str("sqlite:").is_err());
2160    }
2161
2162    #[test]
2163    fn test_parse_errors() {
2164        // Invalid memory size
2165        assert!(DiskCliKind::from_str("mem:invalid").is_err());
2166
2167        // Invalid syntax for SQLiteDiff
2168        assert!(DiskCliKind::from_str("sqldiff:path").is_err());
2169
2170        // Missing OPENVMM_AUTO_CACHE_PATH for AutoCacheSqlite
2171        // SAFETY:
2172        // Safe in a testing context because it won't be changed concurrently
2173        unsafe {
2174            std::env::remove_var("OPENVMM_AUTO_CACHE_PATH");
2175        }
2176        assert!(DiskCliKind::from_str("autocache:key:file:disk.vhd").is_err());
2177
2178        // Invalid blob kind
2179        assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
2180
2181        // Invalid cipher
2182        assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
2183
2184        // Invalid format for crypt (missing parts)
2185        assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
2186
2187        // Invalid disk kind
2188        assert!(DiskCliKind::from_str("invalid:path").is_err());
2189
2190        // Missing create size
2191        assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
2192    }
2193
2194    #[test]
2195    fn test_fs_args_from_str() {
2196        let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
2197        assert_eq!(args.tag, "tag1");
2198        assert_eq!(args.path, "/path/to/fs");
2199
2200        // Test error cases
2201        assert!(FsArgs::from_str("tag1").is_err());
2202        assert!(FsArgs::from_str("tag1,/path,extra").is_err());
2203    }
2204
2205    #[test]
2206    fn test_fs_args_with_options_from_str() {
2207        let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
2208        assert_eq!(args.tag, "tag1");
2209        assert_eq!(args.path, "/path/to/fs");
2210        assert_eq!(args.options, "opt1;opt2");
2211
2212        // Test without options
2213        let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
2214        assert_eq!(args.tag, "tag1");
2215        assert_eq!(args.path, "/path/to/fs");
2216        assert_eq!(args.options, "");
2217
2218        // Test error case
2219        assert!(FsArgsWithOptions::from_str("tag1").is_err());
2220    }
2221
2222    #[test]
2223    fn test_serial_config_from_str() {
2224        assert_eq!(
2225            SerialConfigCli::from_str("none").unwrap(),
2226            SerialConfigCli::None
2227        );
2228        assert_eq!(
2229            SerialConfigCli::from_str("console").unwrap(),
2230            SerialConfigCli::Console
2231        );
2232        assert_eq!(
2233            SerialConfigCli::from_str("stderr").unwrap(),
2234            SerialConfigCli::Stderr
2235        );
2236
2237        // Test file config
2238        let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
2239        if let SerialConfigCli::File(path) = file_config {
2240            assert_eq!(path.to_str().unwrap(), "/path/to/file");
2241        } else {
2242            panic!("Expected File variant");
2243        }
2244
2245        // Test term config with name, but no specific path
2246        match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
2247            SerialConfigCli::NewConsole(None, Some(name)) => {
2248                assert_eq!(name, "MyTerm");
2249            }
2250            _ => panic!("Expected NewConsole variant with name"),
2251        }
2252
2253        // Test term config without name, but no specific path
2254        match SerialConfigCli::from_str("term").unwrap() {
2255            SerialConfigCli::NewConsole(None, None) => (),
2256            _ => panic!("Expected NewConsole variant without name"),
2257        }
2258
2259        // Test term config with name
2260        match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
2261            SerialConfigCli::NewConsole(Some(path), Some(name)) => {
2262                assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
2263                assert_eq!(name, "MyTerm");
2264            }
2265            _ => panic!("Expected NewConsole variant with name"),
2266        }
2267
2268        // Test term config without name
2269        match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
2270            SerialConfigCli::NewConsole(Some(path), None) => {
2271                assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
2272            }
2273            _ => panic!("Expected NewConsole variant without name"),
2274        }
2275
2276        // Test TCP config
2277        match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
2278            SerialConfigCli::Tcp(addr) => {
2279                assert_eq!(addr.to_string(), "127.0.0.1:1234");
2280            }
2281            _ => panic!("Expected Tcp variant"),
2282        }
2283
2284        // Test pipe config
2285        match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
2286            SerialConfigCli::Pipe(path) => {
2287                assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
2288            }
2289            _ => panic!("Expected Pipe variant"),
2290        }
2291
2292        // Test error cases
2293        assert!(SerialConfigCli::from_str("").is_err());
2294        assert!(SerialConfigCli::from_str("unknown").is_err());
2295        assert!(SerialConfigCli::from_str("file").is_err());
2296        assert!(SerialConfigCli::from_str("listen").is_err());
2297    }
2298
2299    #[test]
2300    fn test_endpoint_config_from_str() {
2301        // Test none
2302        assert!(matches!(
2303            EndpointConfigCli::from_str("none").unwrap(),
2304            EndpointConfigCli::None
2305        ));
2306
2307        // Test consomme without cidr
2308        match EndpointConfigCli::from_str("consomme").unwrap() {
2309            EndpointConfigCli::Consomme { cidr: None } => (),
2310            _ => panic!("Expected Consomme variant without cidr"),
2311        }
2312
2313        // Test consomme with cidr
2314        match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
2315            EndpointConfigCli::Consomme { cidr: Some(cidr) } => {
2316                assert_eq!(cidr, "192.168.0.0/24");
2317            }
2318            _ => panic!("Expected Consomme variant with cidr"),
2319        }
2320
2321        // Test dio without id
2322        match EndpointConfigCli::from_str("dio").unwrap() {
2323            EndpointConfigCli::Dio { id: None } => (),
2324            _ => panic!("Expected Dio variant without id"),
2325        }
2326
2327        // Test dio with id
2328        match EndpointConfigCli::from_str("dio:test_id").unwrap() {
2329            EndpointConfigCli::Dio { id: Some(id) } => {
2330                assert_eq!(id, "test_id");
2331            }
2332            _ => panic!("Expected Dio variant with id"),
2333        }
2334
2335        // Test tap
2336        match EndpointConfigCli::from_str("tap:tap0").unwrap() {
2337            EndpointConfigCli::Tap { name } => {
2338                assert_eq!(name, "tap0");
2339            }
2340            _ => panic!("Expected Tap variant"),
2341        }
2342
2343        // Test error case
2344        assert!(EndpointConfigCli::from_str("invalid").is_err());
2345    }
2346
2347    #[test]
2348    fn test_nic_config_from_str() {
2349        use openvmm_defs::config::DeviceVtl;
2350
2351        // Test basic endpoint
2352        let config = NicConfigCli::from_str("none").unwrap();
2353        assert_eq!(config.vtl, DeviceVtl::Vtl0);
2354        assert!(config.max_queues.is_none());
2355        assert!(!config.underhill);
2356        assert!(config.pcie_port.is_none());
2357        assert!(matches!(config.endpoint, EndpointConfigCli::None));
2358
2359        // Test with vtl2
2360        let config = NicConfigCli::from_str("vtl2:none").unwrap();
2361        assert_eq!(config.vtl, DeviceVtl::Vtl2);
2362        assert!(config.pcie_port.is_none());
2363        assert!(matches!(config.endpoint, EndpointConfigCli::None));
2364
2365        // Test with queues
2366        let config = NicConfigCli::from_str("queues=4:none").unwrap();
2367        assert_eq!(config.max_queues, Some(4));
2368        assert!(config.pcie_port.is_none());
2369        assert!(matches!(config.endpoint, EndpointConfigCli::None));
2370
2371        // Test with underhill
2372        let config = NicConfigCli::from_str("uh:none").unwrap();
2373        assert!(config.underhill);
2374        assert!(config.pcie_port.is_none());
2375        assert!(matches!(config.endpoint, EndpointConfigCli::None));
2376
2377        // Test with pcie_port
2378        let config = NicConfigCli::from_str("pcie_port=rp0:none").unwrap();
2379        assert_eq!(config.pcie_port.unwrap(), "rp0".to_string());
2380        assert!(matches!(config.endpoint, EndpointConfigCli::None));
2381
2382        // Test error cases
2383        assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
2384        assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); // uh incompatible with vtl2
2385        assert!(NicConfigCli::from_str("pcie_port=rp0:vtl2:none").is_err());
2386        assert!(NicConfigCli::from_str("uh:pcie_port=rp0:none").is_err());
2387        assert!(NicConfigCli::from_str("pcie_port=:none").is_err());
2388        assert!(NicConfigCli::from_str("pcie_port:none").is_err());
2389    }
2390
2391    #[test]
2392    fn test_parse_pcie_port_prefix() {
2393        // Successful prefix parsing
2394        let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0:tag,path");
2395        assert_eq!(port.unwrap(), "rp0");
2396        assert_eq!(rest, "tag,path");
2397
2398        // No prefix
2399        let (port, rest) = parse_pcie_port_prefix("tag,path");
2400        assert!(port.is_none());
2401        assert_eq!(rest, "tag,path");
2402
2403        // Empty port name — not parsed as a prefix
2404        let (port, rest) = parse_pcie_port_prefix("pcie_port=:tag,path");
2405        assert!(port.is_none());
2406        assert_eq!(rest, "pcie_port=:tag,path");
2407
2408        // Missing colon — not parsed as a prefix
2409        let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0");
2410        assert!(port.is_none());
2411        assert_eq!(rest, "pcie_port=rp0");
2412    }
2413
2414    #[test]
2415    fn test_fs_args_pcie_port() {
2416        // Without pcie_port
2417        let args = FsArgs::from_str("myfs,/path").unwrap();
2418        assert_eq!(args.tag, "myfs");
2419        assert_eq!(args.path, "/path");
2420        assert!(args.pcie_port.is_none());
2421
2422        // With pcie_port
2423        let args = FsArgs::from_str("pcie_port=rp0:myfs,/path").unwrap();
2424        assert_eq!(args.pcie_port.unwrap(), "rp0");
2425        assert_eq!(args.tag, "myfs");
2426        assert_eq!(args.path, "/path");
2427
2428        // Error: wrong number of fields
2429        assert!(FsArgs::from_str("myfs").is_err());
2430        assert!(FsArgs::from_str("pcie_port=rp0:myfs").is_err());
2431    }
2432
2433    #[test]
2434    fn test_fs_args_with_options_pcie_port() {
2435        // Without pcie_port
2436        let args = FsArgsWithOptions::from_str("myfs,/path,uid=1000").unwrap();
2437        assert_eq!(args.tag, "myfs");
2438        assert_eq!(args.path, "/path");
2439        assert_eq!(args.options, "uid=1000");
2440        assert!(args.pcie_port.is_none());
2441
2442        // With pcie_port
2443        let args = FsArgsWithOptions::from_str("pcie_port=rp0:myfs,/path,uid=1000").unwrap();
2444        assert_eq!(args.pcie_port.unwrap(), "rp0");
2445        assert_eq!(args.tag, "myfs");
2446        assert_eq!(args.path, "/path");
2447        assert_eq!(args.options, "uid=1000");
2448
2449        // Error: missing path
2450        assert!(FsArgsWithOptions::from_str("myfs").is_err());
2451    }
2452
2453    #[test]
2454    fn test_virtio_pmem_args_pcie_port() {
2455        // Without pcie_port
2456        let args = VirtioPmemArgs::from_str("/path/to/file").unwrap();
2457        assert_eq!(args.path, "/path/to/file");
2458        assert!(args.pcie_port.is_none());
2459
2460        // With pcie_port
2461        let args = VirtioPmemArgs::from_str("pcie_port=rp0:/path/to/file").unwrap();
2462        assert_eq!(args.pcie_port.unwrap(), "rp0");
2463        assert_eq!(args.path, "/path/to/file");
2464
2465        // Error: empty path
2466        assert!(VirtioPmemArgs::from_str("").is_err());
2467        assert!(VirtioPmemArgs::from_str("pcie_port=rp0:").is_err());
2468    }
2469
2470    #[test]
2471    fn test_smt_config_from_str() {
2472        assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
2473        assert_eq!(
2474            SmtConfigCli::from_str("force").unwrap(),
2475            SmtConfigCli::Force
2476        );
2477        assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
2478
2479        // Test error cases
2480        assert!(SmtConfigCli::from_str("invalid").is_err());
2481        assert!(SmtConfigCli::from_str("").is_err());
2482    }
2483
2484    #[test]
2485    fn test_pcat_boot_order_from_str() {
2486        // Test single device
2487        let order = PcatBootOrderCli::from_str("optical").unwrap();
2488        assert_eq!(order.0[0], PcatBootDevice::Optical);
2489
2490        // Test multiple devices
2491        let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
2492        assert_eq!(order.0[0], PcatBootDevice::HardDrive);
2493        assert_eq!(order.0[1], PcatBootDevice::Network);
2494
2495        // Test error cases
2496        assert!(PcatBootOrderCli::from_str("invalid").is_err());
2497        assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); // duplicate device
2498    }
2499
2500    #[test]
2501    fn test_floppy_disk_from_str() {
2502        // Test basic disk
2503        let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
2504        assert!(!disk.read_only);
2505        match disk.kind {
2506            DiskCliKind::File {
2507                path,
2508                create_with_len,
2509            } => {
2510                assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
2511                assert_eq!(create_with_len, None);
2512            }
2513            _ => panic!("Expected File variant"),
2514        }
2515
2516        // Test with read-only flag
2517        let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
2518        assert!(disk.read_only);
2519
2520        // Test error cases
2521        assert!(FloppyDiskCli::from_str("").is_err());
2522        assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
2523    }
2524
2525    #[test]
2526    fn test_pcie_root_complex_from_str() {
2527        const ONE_MB: u64 = 1024 * 1024;
2528        const ONE_GB: u64 = 1024 * ONE_MB;
2529
2530        const DEFAULT_LOW_MMIO: u32 = (64 * ONE_MB) as u32;
2531        const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
2532
2533        assert_eq!(
2534            PcieRootComplexCli::from_str("rc0").unwrap(),
2535            PcieRootComplexCli {
2536                name: "rc0".to_string(),
2537                segment: 0,
2538                start_bus: 0,
2539                end_bus: 255,
2540                low_mmio: DEFAULT_LOW_MMIO,
2541                high_mmio: DEFAULT_HIGH_MMIO,
2542            }
2543        );
2544
2545        assert_eq!(
2546            PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
2547            PcieRootComplexCli {
2548                name: "rc1".to_string(),
2549                segment: 1,
2550                start_bus: 0,
2551                end_bus: 255,
2552                low_mmio: DEFAULT_LOW_MMIO,
2553                high_mmio: DEFAULT_HIGH_MMIO,
2554            }
2555        );
2556
2557        assert_eq!(
2558            PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
2559            PcieRootComplexCli {
2560                name: "rc2".to_string(),
2561                segment: 0,
2562                start_bus: 32,
2563                end_bus: 255,
2564                low_mmio: DEFAULT_LOW_MMIO,
2565                high_mmio: DEFAULT_HIGH_MMIO,
2566            }
2567        );
2568
2569        assert_eq!(
2570            PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
2571            PcieRootComplexCli {
2572                name: "rc3".to_string(),
2573                segment: 0,
2574                start_bus: 0,
2575                end_bus: 31,
2576                low_mmio: DEFAULT_LOW_MMIO,
2577                high_mmio: DEFAULT_HIGH_MMIO,
2578            }
2579        );
2580
2581        assert_eq!(
2582            PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
2583            PcieRootComplexCli {
2584                name: "rc4".to_string(),
2585                segment: 0,
2586                start_bus: 32,
2587                end_bus: 127,
2588                low_mmio: DEFAULT_LOW_MMIO,
2589                high_mmio: 2 * ONE_GB,
2590            }
2591        );
2592
2593        assert_eq!(
2594            PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
2595            PcieRootComplexCli {
2596                name: "rc5".to_string(),
2597                segment: 2,
2598                start_bus: 32,
2599                end_bus: 127,
2600                low_mmio: DEFAULT_LOW_MMIO,
2601                high_mmio: DEFAULT_HIGH_MMIO,
2602            }
2603        );
2604
2605        assert_eq!(
2606            PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
2607            PcieRootComplexCli {
2608                name: "rc6".to_string(),
2609                segment: 0,
2610                start_bus: 0,
2611                end_bus: 255,
2612                low_mmio: ONE_MB as u32,
2613                high_mmio: 64 * ONE_GB,
2614            }
2615        );
2616
2617        // Error cases
2618        assert!(PcieRootComplexCli::from_str("").is_err());
2619        assert!(PcieRootComplexCli::from_str("poorly,").is_err());
2620        assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
2621        assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
2622        assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
2623        assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
2624        assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
2625        assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
2626        assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
2627        assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
2628        assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
2629    }
2630
2631    #[test]
2632    fn test_pcie_root_port_from_str() {
2633        assert_eq!(
2634            PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
2635            PcieRootPortCli {
2636                root_complex_name: "rc0".to_string(),
2637                name: "rc0rp0".to_string(),
2638                hotplug: false,
2639            }
2640        );
2641
2642        assert_eq!(
2643            PcieRootPortCli::from_str("my_rc:port2").unwrap(),
2644            PcieRootPortCli {
2645                root_complex_name: "my_rc".to_string(),
2646                name: "port2".to_string(),
2647                hotplug: false,
2648            }
2649        );
2650
2651        // Test with hotplug flag
2652        assert_eq!(
2653            PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
2654            PcieRootPortCli {
2655                root_complex_name: "my_rc".to_string(),
2656                name: "port2".to_string(),
2657                hotplug: true,
2658            }
2659        );
2660
2661        // Error cases
2662        assert!(PcieRootPortCli::from_str("").is_err());
2663        assert!(PcieRootPortCli::from_str("rp0").is_err());
2664        assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
2665        assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
2666        assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
2667    }
2668
2669    #[test]
2670    fn test_pcie_switch_from_str() {
2671        assert_eq!(
2672            GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
2673            GenericPcieSwitchCli {
2674                port_name: "rp0".to_string(),
2675                name: "switch0".to_string(),
2676                num_downstream_ports: 4,
2677                hotplug: false,
2678            }
2679        );
2680
2681        assert_eq!(
2682            GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
2683            GenericPcieSwitchCli {
2684                port_name: "port1".to_string(),
2685                name: "my_switch".to_string(),
2686                num_downstream_ports: 4,
2687                hotplug: false,
2688            }
2689        );
2690
2691        assert_eq!(
2692            GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
2693            GenericPcieSwitchCli {
2694                port_name: "rp2".to_string(),
2695                name: "sw".to_string(),
2696                num_downstream_ports: 8,
2697                hotplug: false,
2698            }
2699        );
2700
2701        // Test hierarchical connections
2702        assert_eq!(
2703            GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
2704            GenericPcieSwitchCli {
2705                port_name: "switch0-downstream-1".to_string(),
2706                name: "child_switch".to_string(),
2707                num_downstream_ports: 4,
2708                hotplug: false,
2709            }
2710        );
2711
2712        // Test hotplug flag
2713        assert_eq!(
2714            GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
2715            GenericPcieSwitchCli {
2716                port_name: "rp0".to_string(),
2717                name: "switch0".to_string(),
2718                num_downstream_ports: 4,
2719                hotplug: true,
2720            }
2721        );
2722
2723        // Test hotplug with num_downstream_ports
2724        assert_eq!(
2725            GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
2726            GenericPcieSwitchCli {
2727                port_name: "rp0".to_string(),
2728                name: "switch0".to_string(),
2729                num_downstream_ports: 8,
2730                hotplug: true,
2731            }
2732        );
2733
2734        // Error cases
2735        assert!(GenericPcieSwitchCli::from_str("").is_err());
2736        assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
2737        assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
2738        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
2739        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
2740        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
2741        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
2742    }
2743
2744    #[test]
2745    fn test_pcie_remote_from_str() {
2746        // Basic port name only
2747        assert_eq!(
2748            PcieRemoteCli::from_str("rc0rp0").unwrap(),
2749            PcieRemoteCli {
2750                port_name: "rc0rp0".to_string(),
2751                socket_addr: None,
2752                hu: 0,
2753                controller: 0,
2754            }
2755        );
2756
2757        // With socket address
2758        assert_eq!(
2759            PcieRemoteCli::from_str("rc0rp0,socket=localhost:22567").unwrap(),
2760            PcieRemoteCli {
2761                port_name: "rc0rp0".to_string(),
2762                socket_addr: Some("localhost:22567".to_string()),
2763                hu: 0,
2764                controller: 0,
2765            }
2766        );
2767
2768        // With all options
2769        assert_eq!(
2770            PcieRemoteCli::from_str("myport,socket=localhost:22568,hu=1,controller=2").unwrap(),
2771            PcieRemoteCli {
2772                port_name: "myport".to_string(),
2773                socket_addr: Some("localhost:22568".to_string()),
2774                hu: 1,
2775                controller: 2,
2776            }
2777        );
2778
2779        // Only hu and controller
2780        assert_eq!(
2781            PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
2782            PcieRemoteCli {
2783                port_name: "port0".to_string(),
2784                socket_addr: None,
2785                hu: 5,
2786                controller: 3,
2787            }
2788        );
2789
2790        // Error cases
2791        assert!(PcieRemoteCli::from_str("").is_err());
2792        assert!(PcieRemoteCli::from_str("port,socket=").is_err());
2793        assert!(PcieRemoteCli::from_str("port,hu=").is_err());
2794        assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
2795        assert!(PcieRemoteCli::from_str("port,controller=").is_err());
2796        assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
2797        assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
2798    }
2799}